Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ dist/
*.log
.DS_Store
*.tsbuildinfo
templates/**/*-lock.yaml
.idea
42 changes: 35 additions & 7 deletions src/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ const COPY_IGNORE_FILE_PATTERNS: RegExp[] = [
/^yarn-error\.log.*$/
]

interface CopyDirOptions {
allowDist: boolean
}

interface CopyPluginFilesOptions {
allowDist?: boolean
}

/**
* 执行Git命令
*/
Expand Down Expand Up @@ -201,10 +209,21 @@ export function prepareBranch(pluginName: string): { existedRemotely: boolean; b
return { existedRemotely, branchName }
}

function shouldIgnoreEntry(name: string, isDir: boolean): boolean {
/**
* 判断复制时是否忽略目录或文件。
*
* dist 在源码项目中通常是临时产物,但在 src-ztools 最新结构中是生产入口目录,
* 因此复制插件根目录时需要按上下文允许 dist。
*/
function shouldIgnoreEntry(name: string, isDir: boolean, options: CopyDirOptions): boolean {
if (isDir && name === 'dist' && options.allowDist) {
return false
}

if (isDir) {
return COPY_IGNORE_DIRS.has(name)
}

return COPY_IGNORE_FILE_PATTERNS.some((re) => re.test(name))
}

Expand Down Expand Up @@ -238,14 +257,19 @@ export function mirrorForkPluginToCwd(pluginName: string, destDir: string): void
if (!fs.existsSync(sourceDir)) {
throw new Error(`fork 仓库里找不到 plugins/${pluginName}/,PR 分支可能已被删除或为空`)
}
copyDirRecursive(sourceDir, destDir)
fs.mkdirSync(destDir, { recursive: true })
copyDirRecursive(sourceDir, destDir, { allowDist: true })
}

/**
* 把用户工作目录的插件文件复制到 fork 仓库的 plugins/<name>/ 下。
* 复制前先清空目标目录,确保用户本地删除的文件也会反映到 fork。
*/
export function copyPluginFiles(pluginName: string, sourceDir: string): void {
export function copyPluginFiles(
pluginName: string,
sourceDir: string,
options: CopyPluginFilesOptions = {}
): void {
const destDir = path.join(FORK_REPO_DIR, 'plugins', pluginName)
console.log(cyan(`\n同步插件文件到 plugins/${pluginName}/ ...`))

Expand All @@ -254,19 +278,23 @@ export function copyPluginFiles(pluginName: string, sourceDir: string): void {
}
fs.mkdirSync(destDir, { recursive: true })

copyDirRecursive(sourceDir, destDir)
copyDirRecursive(sourceDir, destDir, { allowDist: options.allowDist ?? false })
console.log(green('✓ 文件同步完成'))
}

function copyDirRecursive(src: string, dest: string): void {
function copyDirRecursive(
src: string,
dest: string,
options: CopyDirOptions = { allowDist: false }
): void {
const entries = fs.readdirSync(src, { withFileTypes: true })
for (const entry of entries) {
if (shouldIgnoreEntry(entry.name, entry.isDirectory())) continue
if (shouldIgnoreEntry(entry.name, entry.isDirectory(), options)) continue
const srcPath = path.join(src, entry.name)
const destPath = path.join(dest, entry.name)
if (entry.isDirectory()) {
fs.mkdirSync(destPath, { recursive: true })
copyDirRecursive(srcPath, destPath)
copyDirRecursive(srcPath, destPath, options)
} else if (entry.isSymbolicLink()) {
const link = fs.readlinkSync(srcPath)
fs.symlinkSync(link, destPath)
Expand Down
106 changes: 106 additions & 0 deletions src/plugin-project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import fs from 'node:fs'
import path from 'node:path'
import type { DiscoveredPluginProject, PluginConfig, PluginLayout } from './types.js'

interface PluginPathCandidate {
layout: PluginLayout
pluginRoot: string
pluginJsonPath: string
}

/**
* 插件项目发现模块。
*
* 该模块集中维护 CLI 对插件根目录的判断规则,避免 publish、pull 等入口各自
* 手写路径候选列表后产生结构兼容差异。最新的 src-ztools 结构优先,旧结构继续
* 兼容以避免破坏已有插件项目。
*/
export function discoverPluginProject(cwd: string = process.cwd()): DiscoveredPluginProject {
const projectRoot = path.resolve(cwd)
const candidates = buildPluginPathCandidates(projectRoot)
const existing = candidates.filter((candidate) => fs.existsSync(candidate.pluginJsonPath))

if (existing.length === 0) {
throw new Error(
'未找到 plugin.json,请确保在插件项目根目录下执行此命令\n' +
'支持的路径:./src-ztools/plugin.json, ./plugin.json, ./public/plugin.json'
)
}

const selected = existing[0]
const config = readPluginConfig(selected.pluginJsonPath)
const warnings = buildMultipleConfigWarnings(existing)

return {
projectRoot,
pluginRoot: selected.pluginRoot,
pluginJsonPath: selected.pluginJsonPath,
layout: selected.layout,
config,
warnings
}
}

/**
* 按优先级构造候选路径。
*
* src-ztools 是新模板结构,必须优先于根目录和旧 public 结构;否则同一项目中残留
* 旧配置时,CLI 会错误读取过期配置。
*/
function buildPluginPathCandidates(projectRoot: string): PluginPathCandidate[] {
return [
{
layout: 'src-ztools',
pluginRoot: path.join(projectRoot, 'src-ztools'),
pluginJsonPath: path.join(projectRoot, 'src-ztools', 'plugin.json')
},
{
layout: 'root',
pluginRoot: projectRoot,
pluginJsonPath: path.join(projectRoot, 'plugin.json')
},
{
layout: 'public',
pluginRoot: path.join(projectRoot, 'public'),
pluginJsonPath: path.join(projectRoot, 'public', 'plugin.json')
}
]
}

/**
* 读取插件配置。
*
* 只负责 JSON 解析,不做业务字段校验;字段校验仍由 publish 等调用方根据命令场景
* 执行,避免解析器承担过多职责。
*/
function readPluginConfig(pluginJsonPath: string): PluginConfig {
try {
const content = fs.readFileSync(pluginJsonPath, 'utf-8')
return JSON.parse(content) as PluginConfig
} catch (error) {
throw new Error(`读取 plugin.json 失败: ${(error as Error).message}`)
}
}
Comment on lines +76 to +83

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在解析 plugin.json 时,JSON.parse 可能会返回 null、数组或非对象类型(例如,如果文件内容是 null 或非对象字符串)。建议在返回前添加防御性检查,确保解析后的内容是一个非空的键值对对象,以避免后续属性访问时抛出运行时错误。

function readPluginConfig(pluginJsonPath: string): PluginConfig {
  try {
    const content = fs.readFileSync(pluginJsonPath, 'utf-8')
    const config = JSON.parse(content)
    if (!config || typeof config !== 'object' || Array.isArray(config)) {
      throw new Error('内容不是一个有效的 JSON 对象')
    }
    return config as PluginConfig
  } catch (error) {
    throw new Error(`读取 plugin.json 失败: ${(error as Error).message}`)
  }
}


/**
* 构造多配置文件提示。
*
* 多个 plugin.json 同时存在通常意味着迁移残留。这里不直接失败,是为了保留旧项目
* 的渐进迁移能力;调用方负责把 warning 展示给用户。
*/
function buildMultipleConfigWarnings(existing: PluginPathCandidate[]): string[] {
if (existing.length <= 1) {
return []
}

const selected = existing[0]
const ignored = existing.slice(1).map((candidate) => candidate.pluginJsonPath)

return [
[
`检测到多个 plugin.json,已使用 ${selected.pluginJsonPath}`,
'被忽略的配置:',
...ignored.map((item) => ` - ${item}`)
].join('\n')
]
}
45 changes: 15 additions & 30 deletions src/publish.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { execSync } from 'node:child_process'
import { blue, cyan, green, red, yellow } from 'kolorist'
import fs from 'node:fs'
import path from 'node:path'
import prompts from 'prompts'
import { ensureAuth } from './auth.js'
Expand Down Expand Up @@ -28,7 +27,8 @@ import {
pluginExistsUpstream,
syncForkMain
} from './github.js'
import type { PluginConfig } from './types.js'
import { discoverPluginProject } from './plugin-project.js'
import type { DiscoveredPluginProject, PluginConfig } from './types.js'

/**
* 验证插件名称格式
Expand All @@ -47,32 +47,16 @@ function validateVersion(version: string): boolean {
}

/**
* 验证插件项目
* 验证插件项目。
*
* 返回完整发现结果而不只返回配置,是为了让发布流程后续复制正确的插件根目录。
*/
function validatePluginProject(): PluginConfig {
const possiblePaths = [
path.join(process.cwd(), 'plugin.json'),
path.join(process.cwd(), 'public', 'plugin.json')
]

let pluginJsonPath: string | null = null
for (const p of possiblePaths) {
if (fs.existsSync(p)) {
pluginJsonPath = p
break
}
}
function validatePluginProject(): DiscoveredPluginProject {
const pluginProject = discoverPluginProject()
const pluginConfig = pluginProject.config

if (!pluginJsonPath) {
throw new Error('未找到plugin.json,请确保在插件项目根目录下执行此命令\n支持的路径:./plugin.json, ./public/plugin.json')
}

let pluginConfig: PluginConfig
try {
const content = fs.readFileSync(pluginJsonPath, 'utf-8')
pluginConfig = JSON.parse(content)
} catch (error) {
throw new Error(`读取plugin.json失败: ${(error as Error).message}`)
for (const warning of pluginProject.warnings) {
console.log(yellow(`⚠ ${warning}`))
}

if (!pluginConfig.name) {
Expand Down Expand Up @@ -121,7 +105,7 @@ function validatePluginProject(): PluginConfig {
)
}

return pluginConfig
return pluginProject
}

/**
Expand Down Expand Up @@ -313,7 +297,8 @@ export async function publish(): Promise<void> {
try {
// 1. 验证插件项目
console.log(cyan('📋 验证插件项目...'))
const pluginConfig = validatePluginProject()
const pluginProject = validatePluginProject()
const pluginConfig = pluginProject.config
const displayName = pluginConfig.title || pluginConfig.name
console.log(green(`✓ 插件: ${displayName} (${pluginConfig.name})`))
console.log(green(`✓ 描述: ${pluginConfig.description || 'N/A'}`))
Expand Down Expand Up @@ -352,8 +337,8 @@ export async function publish(): Promise<void> {
// 7. 切换到 plugin/<name> 分支(仅用于决定从哪里出发追加 commit)
prepareBranch(pluginConfig.name)

// 8. 把工作目录文件复制进 plugins/<name>/
copyPluginFiles(pluginConfig.name, process.cwd())
// 8. 把插件根目录文件复制进 plugins/<name>/
copyPluginFiles(pluginConfig.name, pluginProject.pluginRoot, { allowDist: true })

// 9. 组装 commit 标题 / 正文 / PR 标题
const commitSubjects = getLocalCommitSubjectsSinceLastPublish()
Expand Down
37 changes: 10 additions & 27 deletions src/pull.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { execSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import { blue, cyan, green, red, yellow } from 'kolorist'
import { ensureAuth } from './auth.js'
import {
Expand All @@ -15,29 +13,7 @@ import {
remotePluginBranchExists
} from './git.js'
import { ensureFork, getCurrentUser } from './github.js'
import type { PluginConfig } from './types.js'

function readPluginConfig(): PluginConfig {
const candidates = [
path.join(process.cwd(), 'plugin.json'),
path.join(process.cwd(), 'public', 'plugin.json')
]
let pluginJsonPath: string | null = null
for (const p of candidates) {
if (fs.existsSync(p)) {
pluginJsonPath = p
break
}
}
if (!pluginJsonPath) {
throw new Error(
'未找到 plugin.json,请确保在插件项目根目录下执行此命令\n支持的路径:./plugin.json, ./public/plugin.json'
)
}
const cfg = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf-8')) as PluginConfig
if (!cfg.name) throw new Error('plugin.json 中缺少 name 字段')
return cfg
}
import { discoverPluginProject } from './plugin-project.js'

function runInCwd(cmd: string): void {
execSync(cmd, { cwd: process.cwd(), stdio: ['pipe', 'inherit', 'inherit'] })
Expand Down Expand Up @@ -96,7 +72,14 @@ export async function pullContributions(): Promise<void> {
'find no ztools-last-publish 标签——请先 ztools publish 成功一次后再使用 pull-contributions。'
)
}
const pluginConfig = readPluginConfig()
const pluginProject = discoverPluginProject()
const pluginConfig = pluginProject.config

for (const warning of pluginProject.warnings) {
console.log(yellow(`⚠ ${warning}`))
}

if (!pluginConfig.name) throw new Error('plugin.json 中缺少 name 字段')
const displayName = pluginConfig.title || pluginConfig.name
console.log(green(`✓ 插件: ${displayName} (${pluginConfig.name})`))
console.log(green(`✓ 当前分支: ${originalBranch}`))
Expand Down Expand Up @@ -126,7 +109,7 @@ export async function pullContributions(): Promise<void> {
runInCwd(`git checkout -b "${tempBranch}" "${baseSha}"`)

// 7. 把 fork 当前 plugin 内容镜像到工作树并提交(这就是 theirs 的内容快照)
mirrorForkPluginToCwd(pluginConfig.name, cwd)
mirrorForkPluginToCwd(pluginConfig.name, pluginProject.pluginRoot)
runInCwd('git add -A')
const hasForkDiff = !tryRunInCwd('git diff --cached --quiet')

Expand Down
11 changes: 11 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,14 @@ export interface CommitInfo {
date: string
message: string
}

export type PluginLayout = 'src-ztools' | 'root' | 'public'

export interface DiscoveredPluginProject {
projectRoot: string
pluginRoot: string
pluginJsonPath: string
layout: PluginLayout
config: PluginConfig
warnings: string[]
}
2 changes: 2 additions & 0 deletions templates/vue-vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
"vue": "^3.5.13"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.5",
"@vitejs/plugin-vue": "^5.2.1",
"@vue/tsconfig": "^0.9.1",
"@ztools-center/ztools-api-types": "^1.0.1",
"typescript": "^5.3.0",
"vite": "^6.0.11",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "node_modules/@ztools-center/ztools-api-types/resource/ztools.schema.json",
"$schema": "../node_modules/@ztools-center/ztools-api-types/resource/ztools.schema.json",
"name": "{{PLUGIN_NAME}}",
"title": "{{PLUGIN_TITLE}}",
"description": "{{DESCRIPTION}}",
Expand Down
13 changes: 13 additions & 0 deletions templates/vue-vite/tsconfig.app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*", "src-ztools/**"],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",

"paths": {
"@/*": ["./src/*"]
},
"types": ["@ztools-center/ztools-api-types"]
}
}
Loading