@farris/cli 2.1.2 → 2.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commands/build-components.js +39 -39
- package/lib/commands/build-components.js.map +1 -1
- package/lib/commands/build-css.js +38 -38
- package/lib/commands/build-css.js.map +1 -1
- package/lib/commands/build-lib.js +25 -25
- package/lib/commands/build.js +14 -14
- package/lib/commands/build.js.map +1 -1
- package/lib/commands/create-app.js +54 -54
- package/lib/commands/dev-serve.js +18 -18
- package/lib/commands/extract-i18n.js +74 -74
- package/lib/commands/extract-i18n.js.map +1 -1
- package/lib/commands/generate/api.js +186 -186
- package/lib/commands/generate/api.js.map +1 -1
- package/lib/commands/generate/common.js +100 -100
- package/lib/commands/generate/common.js.map +1 -1
- package/lib/commands/generate/component.js +163 -163
- package/lib/commands/generate/component.js.map +1 -1
- package/lib/commands/generate/view.js +193 -193
- package/lib/commands/generate/view.js.map +1 -1
- package/lib/commands/preview-serve.js +16 -16
- package/lib/commands/workspace/add-app.js +180 -180
- package/lib/commands/workspace/add-app.js.map +1 -1
- package/lib/commands/workspace/create.js +379 -379
- package/lib/commands/workspace/create.js.map +1 -1
- package/lib/commands/workspace/inspect.js +30 -30
- package/lib/commands/workspace/inspect.js.map +1 -1
- package/lib/commands/workspace/verify.js +38 -38
- package/lib/commands/workspace/verify.js.map +1 -1
- package/lib/common/constant.js +64 -63
- package/lib/common/constant.js.map +1 -1
- package/lib/common/generate-app.js +44 -44
- package/lib/common/get-dependencies.js +9 -9
- package/lib/common/get-farris-config.js +11 -11
- package/lib/common/get-farris-config.js.map +1 -1
- package/lib/common/get-version.js +6 -6
- package/lib/common/get-vite-config.js +105 -105
- package/lib/common/get-vite-config.js.map +1 -1
- package/lib/common/output.js +94 -94
- package/lib/common/output.js.map +1 -1
- package/lib/common/safety.js +171 -171
- package/lib/common/safety.js.map +1 -1
- package/lib/common/template-renderer.js +114 -114
- package/lib/common/template-renderer.js.map +1 -1
- package/lib/common/transaction.js +83 -83
- package/lib/common/transaction.js.map +1 -1
- package/lib/common/verifier.js +146 -146
- package/lib/common/verifier.js.map +1 -1
- package/lib/config/vite-app.js +13 -13
- package/lib/config/vite-common.js +20 -20
- package/lib/config/vite-common.js.map +1 -1
- package/lib/config/vite-component.js +30 -30
- package/lib/config/vite-component.js.map +1 -1
- package/lib/config/vite-lib.js +26 -26
- package/lib/index.js +175 -175
- package/lib/plugins/create-component-style.js +44 -44
- package/lib/plugins/create-package-json.js +33 -33
- package/lib/plugins/create-package-json.js.map +1 -1
- package/lib/plugins/dts.js +8 -8
- package/lib/plugins/html-system.js +9 -9
- package/lib/plugins/replace.js +17 -17
- package/lib/plugins/systemjs-bundle.js +15 -15
- package/lib/plugins/systemjs-bundle.js.map +1 -1
- package/package.json +1 -1
- package/templates/workspace/app/farris.config.mjs +13 -13
- package/templates/workspace/app/index.html +12 -12
- package/templates/workspace/app/package.json +22 -22
- package/templates/workspace/app/src/App.tsx +11 -11
- package/templates/workspace/app/src/main.ts +11 -11
- package/templates/workspace/app/src/router/index.ts +16 -16
- package/templates/workspace/app/src/views/home/composables/use-home.ts +7 -7
- package/templates/workspace/app/src/views/home/home.component.tsx +15 -15
- package/templates/workspace/app/src/views/home/index.ts +4 -4
- package/templates/workspace/app/tsconfig.json +21 -21
- package/templates/workspace/root/README.md +28 -28
- package/templates/workspace/root/package.json +13 -13
- package/templates/workspace/root/pnpm-workspace.yaml +12 -12
|
@@ -1,380 +1,380 @@
|
|
|
1
|
-
import { resolve, join } from 'node:path';
|
|
2
|
-
import { existsSync, mkdirSync } from 'node:fs';
|
|
3
|
-
import { spawn } from 'node:child_process';
|
|
4
|
-
import inquirer from 'inquirer';
|
|
5
|
-
import { DEFAULT_CATALOG_VERSIONS,
|
|
6
|
-
import { inspectTarget, checkPlannedConflicts } from '../../common/safety.js';
|
|
7
|
-
import { collectTemplateOutputPaths, renderTemplateDir } from '../../common/template-renderer.js';
|
|
8
|
-
import { Transaction } from '../../common/transaction.js';
|
|
9
|
-
import { verifyWorkspace } from '../../common/verifier.js';
|
|
10
|
-
import { sendJson, logInfo, logWarn, fail, setJsonMode, createSpinner } from '../../common/output.js';
|
|
11
|
-
import { getDirname } from '../../common/constant.js';
|
|
12
|
-
/**
|
|
13
|
-
* 从包名推导目录名(去掉 scope 并转为合法 kebab-case)
|
|
14
|
-
*/
|
|
15
|
-
export function deriveAppDir(name) {
|
|
16
|
-
const withoutScope = name.replace(/^@[^/]+\//, '');
|
|
17
|
-
return withoutScope
|
|
18
|
-
.toLowerCase()
|
|
19
|
-
.replace(/[^a-z0-9-_]/g, '-')
|
|
20
|
-
.replace(/^-+|-+$/g, '');
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* 校验 npm 包名有效性
|
|
24
|
-
*/
|
|
25
|
-
export function validatePackageName(name) {
|
|
26
|
-
if (!name || typeof name !== 'string') {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
return /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(name);
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* 执行 pnpm install 安装依赖
|
|
33
|
-
*/
|
|
34
|
-
export function runPnpmInstall(targetDir, isJson = false) {
|
|
35
|
-
return new Promise((resolve) => {
|
|
36
|
-
const child = spawn('pnpm', ['install'], {
|
|
37
|
-
cwd: targetDir,
|
|
38
|
-
shell: true,
|
|
39
|
-
stdio: isJson ? ['ignore', 'pipe', 'pipe'] : 'inherit'
|
|
40
|
-
});
|
|
41
|
-
let stderrData = '';
|
|
42
|
-
if (isJson && child.stderr) {
|
|
43
|
-
child.stderr.on('data', (chunk) => {
|
|
44
|
-
stderrData += chunk.toString();
|
|
45
|
-
process.stderr.write(chunk);
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
child.on('close', (code) => {
|
|
49
|
-
if (code === 0) {
|
|
50
|
-
resolve({ success: true });
|
|
51
|
-
}
|
|
52
|
-
else {
|
|
53
|
-
resolve({ success: false, error: `pnpm install 退出码: ${code}` });
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
child.on('error', (err) => {
|
|
57
|
-
resolve({ success: false, error: err.message });
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
/* eslint-disable complexity */
|
|
62
|
-
export async function createWorkspace(target = '.', options = {}) {
|
|
63
|
-
const isJson = !!options.json;
|
|
64
|
-
setJsonMode(isJson);
|
|
65
|
-
const inspect = inspectTarget(target);
|
|
66
|
-
if (!inspect.canCreate) {
|
|
67
|
-
fail({
|
|
68
|
-
command: 'workspace.create',
|
|
69
|
-
error: `目标目录无法创建 Workspace,状态: ${inspect.status}。${inspect.conflicts.length > 0 ? `冲突项: ${inspect.conflicts.join(', ')}` : ''}`,
|
|
70
|
-
exitCode: ExitCode.TARGET_CONFLICT,
|
|
71
|
-
extra: { status: inspect.status, conflicts: inspect.conflicts }
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
let { name, title } = options;
|
|
75
|
-
let router = options.router ?? false;
|
|
76
|
-
let routerMode = options.routerMode || 'hash';
|
|
77
|
-
let format = options.format || 'es';
|
|
78
|
-
let style = options.style || 'css';
|
|
79
|
-
let install = options.install ?? false;
|
|
80
|
-
// 非 JSON 模式下若缺失必填项,进行交互询问
|
|
81
|
-
if (!isJson && !options.yes && !name) {
|
|
82
|
-
const prompt = inquirer.createPromptModule();
|
|
83
|
-
const answers = await prompt([
|
|
84
|
-
{
|
|
85
|
-
type: 'input',
|
|
86
|
-
name: 'name',
|
|
87
|
-
message: '请输入应用名称 (package name):',
|
|
88
|
-
default: 'my-app',
|
|
89
|
-
validate: (input) => (validatePackageName(input) ? true : '请输入合法的 npm 包名 (如 my-app 或 @scope/my-app)')
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
type: 'input',
|
|
93
|
-
name: 'title',
|
|
94
|
-
message: '请输入展示标题 (title):',
|
|
95
|
-
default: (ans) => ans.name
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
type: 'confirm',
|
|
99
|
-
name: 'router',
|
|
100
|
-
message: '是否启用 Vue Router?',
|
|
101
|
-
default: false
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
type: 'list',
|
|
105
|
-
name: 'routerMode',
|
|
106
|
-
message: '选择路由模式:',
|
|
107
|
-
choices: ['hash', 'history'],
|
|
108
|
-
default: 'hash',
|
|
109
|
-
when: (ans) => ans.router
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
type: 'list',
|
|
113
|
-
name: 'style',
|
|
114
|
-
message: '选择应用样式方案:',
|
|
115
|
-
choices: ['css', 'scss', 'none'],
|
|
116
|
-
default: 'css'
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
type: 'list',
|
|
120
|
-
name: 'format',
|
|
121
|
-
message: '选择交付格式:',
|
|
122
|
-
choices: ['es', 'systemjs'],
|
|
123
|
-
default: 'es'
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
type: 'confirm',
|
|
127
|
-
name: 'install',
|
|
128
|
-
message: '是否在创建完成后自动安装依赖 (pnpm install)?',
|
|
129
|
-
default: false
|
|
130
|
-
}
|
|
131
|
-
]);
|
|
132
|
-
({ name } = answers);
|
|
133
|
-
title = answers.title || name;
|
|
134
|
-
router = !!answers.router;
|
|
135
|
-
if (answers.routerMode) {
|
|
136
|
-
({ routerMode } = answers);
|
|
137
|
-
}
|
|
138
|
-
if (answers.style) {
|
|
139
|
-
({ style } = answers);
|
|
140
|
-
}
|
|
141
|
-
if (answers.format) {
|
|
142
|
-
({ format } = answers);
|
|
143
|
-
}
|
|
144
|
-
if (answers.install !== undefined) {
|
|
145
|
-
install = !!answers.install;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
if (!name) {
|
|
149
|
-
fail({
|
|
150
|
-
command: 'workspace.create',
|
|
151
|
-
error: '必须指定应用名称 (--name <name>)',
|
|
152
|
-
exitCode: ExitCode.GENERAL_ERROR
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
if (!validatePackageName(name)) {
|
|
156
|
-
fail({
|
|
157
|
-
command: 'workspace.create',
|
|
158
|
-
error: `非法的应用包名: "${name}"`,
|
|
159
|
-
exitCode: ExitCode.GENERAL_ERROR
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
title = title || name;
|
|
163
|
-
const appDir = deriveAppDir(name);
|
|
164
|
-
if (!appDir || !/^[a-z0-9-_]+$/.test(appDir)) {
|
|
165
|
-
fail({
|
|
166
|
-
command: 'workspace.create',
|
|
167
|
-
error: `无法从应用名 "${name}" 推导出合法的子目录名`,
|
|
168
|
-
exitCode: ExitCode.GENERAL_ERROR
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
const targetDir = inspect.target;
|
|
172
|
-
const templatesBase = resolve(getDirname(import.meta.url), '../../../templates/workspace');
|
|
173
|
-
const rootTemplateDir = join(templatesBase, 'root');
|
|
174
|
-
const appTemplateDir = join(templatesBase, 'app');
|
|
175
|
-
const rootVariables = {
|
|
176
|
-
pnpmVersion: DEFAULT_CATALOG_VERSIONS['pnpm']
|
|
177
|
-
farrisUiVueVersion: DEFAULT_CATALOG_VERSIONS['@farris/ui-vue']
|
|
178
|
-
farrisCliVersion:
|
|
179
|
-
typescriptVersion: DEFAULT_CATALOG_VERSIONS['typescript']
|
|
180
|
-
viteVersion: DEFAULT_CATALOG_VERSIONS['vite']
|
|
181
|
-
vueVersion: DEFAULT_CATALOG_VERSIONS['vue']
|
|
182
|
-
vueRouterVersion: DEFAULT_CATALOG_VERSIONS['vue-router']
|
|
183
|
-
vueTscVersion: DEFAULT_CATALOG_VERSIONS['vue-tsc']
|
|
184
|
-
sassVersion: DEFAULT_CATALOG_VERSIONS['sass']
|
|
185
|
-
title,
|
|
186
|
-
appDir,
|
|
187
|
-
appName: name
|
|
188
|
-
};
|
|
189
|
-
const appTargetDir = join(targetDir, 'packages', appDir);
|
|
190
|
-
// 组装条件变量
|
|
191
|
-
const routerDependency = router ? ',\n "vue-router": "catalog:"' : '';
|
|
192
|
-
const sassDevDependency = style === 'scss' ? ',\n "sass": "catalog:"' : '';
|
|
193
|
-
let styleImport = '';
|
|
194
|
-
if (style === 'css') {
|
|
195
|
-
styleImport = "\nimport './styles/index.css';";
|
|
196
|
-
}
|
|
197
|
-
else if (style === 'scss') {
|
|
198
|
-
styleImport = "\nimport './styles/index.scss';";
|
|
199
|
-
}
|
|
200
|
-
const routerImport = router ? "\nimport router from './router';" : '';
|
|
201
|
-
const routerUse = router ? '\napp.use(router);' : '';
|
|
202
|
-
// 模板保留扩展点,但通用 CLI 不再内置 i18n 运行时契约。
|
|
203
|
-
const i18nImport = '';
|
|
204
|
-
const i18nUse = '';
|
|
205
|
-
const appRouterImport = router ? "\nimport { RouterView } from 'vue-router';" : '';
|
|
206
|
-
const appTemplateBody = router
|
|
207
|
-
? ' <RouterView />'
|
|
208
|
-
: ` <h1>{${JSON.stringify(title)}}</h1>`;
|
|
209
|
-
const appVariables = {
|
|
210
|
-
appName: name,
|
|
211
|
-
title,
|
|
212
|
-
appDir,
|
|
213
|
-
base: './',
|
|
214
|
-
routerMode: router ? routerMode : 'none',
|
|
215
|
-
format,
|
|
216
|
-
routerDependency,
|
|
217
|
-
sassDevDependency,
|
|
218
|
-
styleImport,
|
|
219
|
-
routerImport,
|
|
220
|
-
routerUse,
|
|
221
|
-
i18nImport,
|
|
222
|
-
i18nUse,
|
|
223
|
-
appRouterImport,
|
|
224
|
-
appTemplateBody,
|
|
225
|
-
routerHistoryFunction: routerMode === 'history' ? 'createWebHistory' : 'createWebHashHistory',
|
|
226
|
-
routerHistoryCall: routerMode === 'history'
|
|
227
|
-
? 'createWebHistory(import.meta.env.BASE_URL)'
|
|
228
|
-
: 'createWebHashHistory()'
|
|
229
|
-
};
|
|
230
|
-
// 模板文件过滤规则
|
|
231
|
-
const appFilter = (relPath) => {
|
|
232
|
-
const normalized = relPath.replace(/\\/g, '/');
|
|
233
|
-
// 样式过滤
|
|
234
|
-
if (style === 'none' && normalized.startsWith('src/styles')) {
|
|
235
|
-
return false;
|
|
236
|
-
}
|
|
237
|
-
if (style === 'css' && (normalized === 'src/styles/index.scss' || normalized === 'src/styles/index.scss.tpl')) {
|
|
238
|
-
return false;
|
|
239
|
-
}
|
|
240
|
-
if (style === 'scss' && (normalized === 'src/styles/index.css' || normalized === 'src/styles/index.css.tpl')) {
|
|
241
|
-
return false;
|
|
242
|
-
}
|
|
243
|
-
// Router 与 Views 过滤
|
|
244
|
-
if (!router) {
|
|
245
|
-
if (normalized.startsWith('src/router') || normalized.startsWith('src/views')) {
|
|
246
|
-
return false;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
return true;
|
|
250
|
-
};
|
|
251
|
-
const plannedRelativeFiles = [
|
|
252
|
-
...collectTemplateOutputPaths(rootTemplateDir),
|
|
253
|
-
...collectTemplateOutputPaths(appTemplateDir, appFilter).map((path) => join('packages', appDir, path))
|
|
254
|
-
];
|
|
255
|
-
const conflicts = checkPlannedConflicts(targetDir, plannedRelativeFiles);
|
|
256
|
-
if (conflicts.length > 0 && !options.force) {
|
|
257
|
-
const shouldOverwrite = !isJson &&
|
|
258
|
-
!options.yes &&
|
|
259
|
-
(await inquirer.createPromptModule()([
|
|
260
|
-
{
|
|
261
|
-
type: 'confirm',
|
|
262
|
-
name: 'overwrite',
|
|
263
|
-
message: `以下 ${conflicts.length} 个文件已存在,是否覆盖?\n${conflicts.join('\n')}`,
|
|
264
|
-
default: false
|
|
265
|
-
}
|
|
266
|
-
])).overwrite;
|
|
267
|
-
if (!shouldOverwrite) {
|
|
268
|
-
fail({
|
|
269
|
-
command: 'workspace.create',
|
|
270
|
-
error: `目标目录包含将被模板写入的文件: ${conflicts.join(', ')}。请交互确认,或显式传入 --force 覆盖。`,
|
|
271
|
-
exitCode: ExitCode.TARGET_CONFLICT,
|
|
272
|
-
extra: { conflicts }
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
const transaction = new Transaction();
|
|
277
|
-
plannedRelativeFiles.forEach((path) => transaction.prepareWrite(join(targetDir, path)));
|
|
278
|
-
const spinner = createSpinner('正在生成 Workspace 工程模板...');
|
|
279
|
-
spinner.start();
|
|
280
|
-
try {
|
|
281
|
-
if (!existsSync(targetDir)) {
|
|
282
|
-
mkdirSync(targetDir, { recursive: true });
|
|
283
|
-
transaction.recordCreatedDir(targetDir);
|
|
284
|
-
}
|
|
285
|
-
// 1. 渲染根目录模板
|
|
286
|
-
const rootRender = renderTemplateDir({
|
|
287
|
-
sourceDir: rootTemplateDir,
|
|
288
|
-
targetDir,
|
|
289
|
-
variables: rootVariables,
|
|
290
|
-
skipExisting: false
|
|
291
|
-
});
|
|
292
|
-
if (rootRender.unreplacedPlaceholders.length > 0) {
|
|
293
|
-
throw new Error(`根模板渲染出现未替换占位符: ${rootRender.unreplacedPlaceholders.map((p) => `${p.file} (${p.placeholder})`).join(', ')}`);
|
|
294
|
-
}
|
|
295
|
-
// 2. 渲染子应用目录 (packages/<appDir>/)
|
|
296
|
-
mkdirSync(appTargetDir, { recursive: true });
|
|
297
|
-
transaction.recordCreatedDir(appTargetDir);
|
|
298
|
-
const appRender = renderTemplateDir({
|
|
299
|
-
sourceDir: appTemplateDir,
|
|
300
|
-
targetDir: appTargetDir,
|
|
301
|
-
variables: appVariables,
|
|
302
|
-
skipExisting: false,
|
|
303
|
-
filter: appFilter
|
|
304
|
-
});
|
|
305
|
-
if (appRender.unreplacedPlaceholders.length > 0) {
|
|
306
|
-
throw new Error(`应用模板渲染出现未替换占位符: ${appRender.unreplacedPlaceholders.map((p) => `${p.file} (${p.placeholder})`).join(', ')}`);
|
|
307
|
-
}
|
|
308
|
-
// 3. 执行 Verify
|
|
309
|
-
const verifyResult = verifyWorkspace(targetDir, inspect.preservedInputEntries);
|
|
310
|
-
if (!verifyResult.ok) {
|
|
311
|
-
throw new Error(`Workspace 校验失败: ${verifyResult.errors.join('; ')}`);
|
|
312
|
-
}
|
|
313
|
-
// 4. 提交事务
|
|
314
|
-
transaction.commit();
|
|
315
|
-
spinner.succeed('Workspace 工程生成并校验通过!');
|
|
316
|
-
// 5. 若启用 --install,自动执行依赖安装
|
|
317
|
-
let installed = false;
|
|
318
|
-
let installError;
|
|
319
|
-
if (install) {
|
|
320
|
-
const installSpinner = createSpinner('正在安装依赖 (pnpm install)...');
|
|
321
|
-
installSpinner.start();
|
|
322
|
-
const installRes = await runPnpmInstall(targetDir, isJson);
|
|
323
|
-
if (installRes.success) {
|
|
324
|
-
installed = true;
|
|
325
|
-
installSpinner.succeed('依赖安装成功!');
|
|
326
|
-
}
|
|
327
|
-
else {
|
|
328
|
-
installed = false;
|
|
329
|
-
installError = installRes.error;
|
|
330
|
-
installSpinner.fail('依赖安装失败');
|
|
331
|
-
logWarn(`\n提示: 依赖安装出现异常 (${installError}),您可以稍后进入目录手动执行 "pnpm install"`);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
const allCreated = [...rootRender.filesCreated, ...appRender.filesCreated];
|
|
335
|
-
if (isJson) {
|
|
336
|
-
sendJson({
|
|
337
|
-
command: 'workspace.create',
|
|
338
|
-
ok: true,
|
|
339
|
-
target: targetDir,
|
|
340
|
-
appDir,
|
|
341
|
-
appName: name,
|
|
342
|
-
title,
|
|
343
|
-
router,
|
|
344
|
-
routerMode: router ? routerMode : 'none',
|
|
345
|
-
format,
|
|
346
|
-
style,
|
|
347
|
-
installed,
|
|
348
|
-
...(installError ? { installError } : {}),
|
|
349
|
-
filesCreated: allCreated,
|
|
350
|
-
preservedInputVerified: inspect.preservedInputEntries.length > 0
|
|
351
|
-
});
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
logInfo(`\n✨ Workspace 已就绪: ${targetDir}`);
|
|
355
|
-
logInfo(`- 应用: ${name} (目录: packages/${appDir})`);
|
|
356
|
-
logInfo(`- 路由: ${router ? routerMode : '未启用'}`);
|
|
357
|
-
logInfo(`- 样式: ${style}`);
|
|
358
|
-
logInfo(`- 交付格式: ${format}`);
|
|
359
|
-
logInfo(`- 依赖安装: ${installed ? '已完成' : '未执行 (可通过 pnpm install 安装)'}`);
|
|
360
|
-
logInfo('\n下一步 (Next Steps):');
|
|
361
|
-
logInfo(` 1. cd ${target}`);
|
|
362
|
-
if (!installed) {
|
|
363
|
-
logInfo(' 2. pnpm install');
|
|
364
|
-
logInfo(` 3. pnpm --filter "${name}" dev\n`);
|
|
365
|
-
}
|
|
366
|
-
else {
|
|
367
|
-
logInfo(` 2. pnpm --filter "${name}" dev\n`);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
catch (err) {
|
|
371
|
-
spinner.fail('Workspace 生成失败,正在回滚...');
|
|
372
|
-
transaction.rollback();
|
|
373
|
-
fail({
|
|
374
|
-
command: 'workspace.create',
|
|
375
|
-
error: `生成 Workspace 失败: ${err.message}`,
|
|
376
|
-
exitCode: ExitCode.GENERAL_ERROR
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
}
|
|
1
|
+
import { resolve, join } from 'node:path';
|
|
2
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import { DEFAULT_CATALOG_VERSIONS, ExitCode, } from '../../common/constant.js';
|
|
6
|
+
import { inspectTarget, checkPlannedConflicts } from '../../common/safety.js';
|
|
7
|
+
import { collectTemplateOutputPaths, renderTemplateDir } from '../../common/template-renderer.js';
|
|
8
|
+
import { Transaction } from '../../common/transaction.js';
|
|
9
|
+
import { verifyWorkspace } from '../../common/verifier.js';
|
|
10
|
+
import { sendJson, logInfo, logWarn, fail, setJsonMode, createSpinner } from '../../common/output.js';
|
|
11
|
+
import { getDirname } from '../../common/constant.js';
|
|
12
|
+
/**
|
|
13
|
+
* 从包名推导目录名(去掉 scope 并转为合法 kebab-case)
|
|
14
|
+
*/
|
|
15
|
+
export function deriveAppDir(name) {
|
|
16
|
+
const withoutScope = name.replace(/^@[^/]+\//, '');
|
|
17
|
+
return withoutScope
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.replace(/[^a-z0-9-_]/g, '-')
|
|
20
|
+
.replace(/^-+|-+$/g, '');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 校验 npm 包名有效性
|
|
24
|
+
*/
|
|
25
|
+
export function validatePackageName(name) {
|
|
26
|
+
if (!name || typeof name !== 'string') {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(name);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 执行 pnpm install 安装依赖
|
|
33
|
+
*/
|
|
34
|
+
export function runPnpmInstall(targetDir, isJson = false) {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
const child = spawn('pnpm', ['install'], {
|
|
37
|
+
cwd: targetDir,
|
|
38
|
+
shell: true,
|
|
39
|
+
stdio: isJson ? ['ignore', 'pipe', 'pipe'] : 'inherit'
|
|
40
|
+
});
|
|
41
|
+
let stderrData = '';
|
|
42
|
+
if (isJson && child.stderr) {
|
|
43
|
+
child.stderr.on('data', (chunk) => {
|
|
44
|
+
stderrData += chunk.toString();
|
|
45
|
+
process.stderr.write(chunk);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
child.on('close', (code) => {
|
|
49
|
+
if (code === 0) {
|
|
50
|
+
resolve({ success: true });
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
resolve({ success: false, error: `pnpm install 退出码: ${code}` });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
child.on('error', (err) => {
|
|
57
|
+
resolve({ success: false, error: err.message });
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/* eslint-disable complexity */
|
|
62
|
+
export async function createWorkspace(target = '.', options = {}) {
|
|
63
|
+
const isJson = !!options.json;
|
|
64
|
+
setJsonMode(isJson);
|
|
65
|
+
const inspect = inspectTarget(target);
|
|
66
|
+
if (!inspect.canCreate) {
|
|
67
|
+
fail({
|
|
68
|
+
command: 'workspace.create',
|
|
69
|
+
error: `目标目录无法创建 Workspace,状态: ${inspect.status}。${inspect.conflicts.length > 0 ? `冲突项: ${inspect.conflicts.join(', ')}` : ''}`,
|
|
70
|
+
exitCode: ExitCode.TARGET_CONFLICT,
|
|
71
|
+
extra: { status: inspect.status, conflicts: inspect.conflicts }
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
let { name, title } = options;
|
|
75
|
+
let router = options.router ?? false;
|
|
76
|
+
let routerMode = options.routerMode || 'hash';
|
|
77
|
+
let format = options.format || 'es';
|
|
78
|
+
let style = options.style || 'css';
|
|
79
|
+
let install = options.install ?? false;
|
|
80
|
+
// 非 JSON 模式下若缺失必填项,进行交互询问
|
|
81
|
+
if (!isJson && !options.yes && !name) {
|
|
82
|
+
const prompt = inquirer.createPromptModule();
|
|
83
|
+
const answers = await prompt([
|
|
84
|
+
{
|
|
85
|
+
type: 'input',
|
|
86
|
+
name: 'name',
|
|
87
|
+
message: '请输入应用名称 (package name):',
|
|
88
|
+
default: 'my-app',
|
|
89
|
+
validate: (input) => (validatePackageName(input) ? true : '请输入合法的 npm 包名 (如 my-app 或 @scope/my-app)')
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
type: 'input',
|
|
93
|
+
name: 'title',
|
|
94
|
+
message: '请输入展示标题 (title):',
|
|
95
|
+
default: (ans) => ans.name
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
type: 'confirm',
|
|
99
|
+
name: 'router',
|
|
100
|
+
message: '是否启用 Vue Router?',
|
|
101
|
+
default: false
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
type: 'list',
|
|
105
|
+
name: 'routerMode',
|
|
106
|
+
message: '选择路由模式:',
|
|
107
|
+
choices: ['hash', 'history'],
|
|
108
|
+
default: 'hash',
|
|
109
|
+
when: (ans) => ans.router
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
type: 'list',
|
|
113
|
+
name: 'style',
|
|
114
|
+
message: '选择应用样式方案:',
|
|
115
|
+
choices: ['css', 'scss', 'none'],
|
|
116
|
+
default: 'css'
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
type: 'list',
|
|
120
|
+
name: 'format',
|
|
121
|
+
message: '选择交付格式:',
|
|
122
|
+
choices: ['es', 'systemjs'],
|
|
123
|
+
default: 'es'
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
type: 'confirm',
|
|
127
|
+
name: 'install',
|
|
128
|
+
message: '是否在创建完成后自动安装依赖 (pnpm install)?',
|
|
129
|
+
default: false
|
|
130
|
+
}
|
|
131
|
+
]);
|
|
132
|
+
({ name } = answers);
|
|
133
|
+
title = answers.title || name;
|
|
134
|
+
router = !!answers.router;
|
|
135
|
+
if (answers.routerMode) {
|
|
136
|
+
({ routerMode } = answers);
|
|
137
|
+
}
|
|
138
|
+
if (answers.style) {
|
|
139
|
+
({ style } = answers);
|
|
140
|
+
}
|
|
141
|
+
if (answers.format) {
|
|
142
|
+
({ format } = answers);
|
|
143
|
+
}
|
|
144
|
+
if (answers.install !== undefined) {
|
|
145
|
+
install = !!answers.install;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (!name) {
|
|
149
|
+
fail({
|
|
150
|
+
command: 'workspace.create',
|
|
151
|
+
error: '必须指定应用名称 (--name <name>)',
|
|
152
|
+
exitCode: ExitCode.GENERAL_ERROR
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (!validatePackageName(name)) {
|
|
156
|
+
fail({
|
|
157
|
+
command: 'workspace.create',
|
|
158
|
+
error: `非法的应用包名: "${name}"`,
|
|
159
|
+
exitCode: ExitCode.GENERAL_ERROR
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
title = title || name;
|
|
163
|
+
const appDir = deriveAppDir(name);
|
|
164
|
+
if (!appDir || !/^[a-z0-9-_]+$/.test(appDir)) {
|
|
165
|
+
fail({
|
|
166
|
+
command: 'workspace.create',
|
|
167
|
+
error: `无法从应用名 "${name}" 推导出合法的子目录名`,
|
|
168
|
+
exitCode: ExitCode.GENERAL_ERROR
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const targetDir = inspect.target;
|
|
172
|
+
const templatesBase = resolve(getDirname(import.meta.url), '../../../templates/workspace');
|
|
173
|
+
const rootTemplateDir = join(templatesBase, 'root');
|
|
174
|
+
const appTemplateDir = join(templatesBase, 'app');
|
|
175
|
+
const rootVariables = {
|
|
176
|
+
pnpmVersion: DEFAULT_CATALOG_VERSIONS['pnpm'],
|
|
177
|
+
farrisUiVueVersion: DEFAULT_CATALOG_VERSIONS['@farris/ui-vue'],
|
|
178
|
+
farrisCliVersion: DEFAULT_CATALOG_VERSIONS['@farris/cli'],
|
|
179
|
+
typescriptVersion: DEFAULT_CATALOG_VERSIONS['typescript'],
|
|
180
|
+
viteVersion: DEFAULT_CATALOG_VERSIONS['vite'],
|
|
181
|
+
vueVersion: DEFAULT_CATALOG_VERSIONS['vue'],
|
|
182
|
+
vueRouterVersion: DEFAULT_CATALOG_VERSIONS['vue-router'],
|
|
183
|
+
vueTscVersion: DEFAULT_CATALOG_VERSIONS['vue-tsc'],
|
|
184
|
+
sassVersion: DEFAULT_CATALOG_VERSIONS['sass'],
|
|
185
|
+
title,
|
|
186
|
+
appDir,
|
|
187
|
+
appName: name
|
|
188
|
+
};
|
|
189
|
+
const appTargetDir = join(targetDir, 'packages', appDir);
|
|
190
|
+
// 组装条件变量
|
|
191
|
+
const routerDependency = router ? ',\n "vue-router": "catalog:"' : '';
|
|
192
|
+
const sassDevDependency = style === 'scss' ? ',\n "sass": "catalog:"' : '';
|
|
193
|
+
let styleImport = '';
|
|
194
|
+
if (style === 'css') {
|
|
195
|
+
styleImport = "\nimport './styles/index.css';";
|
|
196
|
+
}
|
|
197
|
+
else if (style === 'scss') {
|
|
198
|
+
styleImport = "\nimport './styles/index.scss';";
|
|
199
|
+
}
|
|
200
|
+
const routerImport = router ? "\nimport router from './router';" : '';
|
|
201
|
+
const routerUse = router ? '\napp.use(router);' : '';
|
|
202
|
+
// 模板保留扩展点,但通用 CLI 不再内置 i18n 运行时契约。
|
|
203
|
+
const i18nImport = '';
|
|
204
|
+
const i18nUse = '';
|
|
205
|
+
const appRouterImport = router ? "\nimport { RouterView } from 'vue-router';" : '';
|
|
206
|
+
const appTemplateBody = router
|
|
207
|
+
? ' <RouterView />'
|
|
208
|
+
: ` <h1>{${JSON.stringify(title)}}</h1>`;
|
|
209
|
+
const appVariables = {
|
|
210
|
+
appName: name,
|
|
211
|
+
title,
|
|
212
|
+
appDir,
|
|
213
|
+
base: './',
|
|
214
|
+
routerMode: router ? routerMode : 'none',
|
|
215
|
+
format,
|
|
216
|
+
routerDependency,
|
|
217
|
+
sassDevDependency,
|
|
218
|
+
styleImport,
|
|
219
|
+
routerImport,
|
|
220
|
+
routerUse,
|
|
221
|
+
i18nImport,
|
|
222
|
+
i18nUse,
|
|
223
|
+
appRouterImport,
|
|
224
|
+
appTemplateBody,
|
|
225
|
+
routerHistoryFunction: routerMode === 'history' ? 'createWebHistory' : 'createWebHashHistory',
|
|
226
|
+
routerHistoryCall: routerMode === 'history'
|
|
227
|
+
? 'createWebHistory(import.meta.env.BASE_URL)'
|
|
228
|
+
: 'createWebHashHistory()'
|
|
229
|
+
};
|
|
230
|
+
// 模板文件过滤规则
|
|
231
|
+
const appFilter = (relPath) => {
|
|
232
|
+
const normalized = relPath.replace(/\\/g, '/');
|
|
233
|
+
// 样式过滤
|
|
234
|
+
if (style === 'none' && normalized.startsWith('src/styles')) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
if (style === 'css' && (normalized === 'src/styles/index.scss' || normalized === 'src/styles/index.scss.tpl')) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
if (style === 'scss' && (normalized === 'src/styles/index.css' || normalized === 'src/styles/index.css.tpl')) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
// Router 与 Views 过滤
|
|
244
|
+
if (!router) {
|
|
245
|
+
if (normalized.startsWith('src/router') || normalized.startsWith('src/views')) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return true;
|
|
250
|
+
};
|
|
251
|
+
const plannedRelativeFiles = [
|
|
252
|
+
...collectTemplateOutputPaths(rootTemplateDir),
|
|
253
|
+
...collectTemplateOutputPaths(appTemplateDir, appFilter).map((path) => join('packages', appDir, path))
|
|
254
|
+
];
|
|
255
|
+
const conflicts = checkPlannedConflicts(targetDir, plannedRelativeFiles);
|
|
256
|
+
if (conflicts.length > 0 && !options.force) {
|
|
257
|
+
const shouldOverwrite = !isJson &&
|
|
258
|
+
!options.yes &&
|
|
259
|
+
(await inquirer.createPromptModule()([
|
|
260
|
+
{
|
|
261
|
+
type: 'confirm',
|
|
262
|
+
name: 'overwrite',
|
|
263
|
+
message: `以下 ${conflicts.length} 个文件已存在,是否覆盖?\n${conflicts.join('\n')}`,
|
|
264
|
+
default: false
|
|
265
|
+
}
|
|
266
|
+
])).overwrite;
|
|
267
|
+
if (!shouldOverwrite) {
|
|
268
|
+
fail({
|
|
269
|
+
command: 'workspace.create',
|
|
270
|
+
error: `目标目录包含将被模板写入的文件: ${conflicts.join(', ')}。请交互确认,或显式传入 --force 覆盖。`,
|
|
271
|
+
exitCode: ExitCode.TARGET_CONFLICT,
|
|
272
|
+
extra: { conflicts }
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const transaction = new Transaction();
|
|
277
|
+
plannedRelativeFiles.forEach((path) => transaction.prepareWrite(join(targetDir, path)));
|
|
278
|
+
const spinner = createSpinner('正在生成 Workspace 工程模板...');
|
|
279
|
+
spinner.start();
|
|
280
|
+
try {
|
|
281
|
+
if (!existsSync(targetDir)) {
|
|
282
|
+
mkdirSync(targetDir, { recursive: true });
|
|
283
|
+
transaction.recordCreatedDir(targetDir);
|
|
284
|
+
}
|
|
285
|
+
// 1. 渲染根目录模板
|
|
286
|
+
const rootRender = renderTemplateDir({
|
|
287
|
+
sourceDir: rootTemplateDir,
|
|
288
|
+
targetDir,
|
|
289
|
+
variables: rootVariables,
|
|
290
|
+
skipExisting: false
|
|
291
|
+
});
|
|
292
|
+
if (rootRender.unreplacedPlaceholders.length > 0) {
|
|
293
|
+
throw new Error(`根模板渲染出现未替换占位符: ${rootRender.unreplacedPlaceholders.map((p) => `${p.file} (${p.placeholder})`).join(', ')}`);
|
|
294
|
+
}
|
|
295
|
+
// 2. 渲染子应用目录 (packages/<appDir>/)
|
|
296
|
+
mkdirSync(appTargetDir, { recursive: true });
|
|
297
|
+
transaction.recordCreatedDir(appTargetDir);
|
|
298
|
+
const appRender = renderTemplateDir({
|
|
299
|
+
sourceDir: appTemplateDir,
|
|
300
|
+
targetDir: appTargetDir,
|
|
301
|
+
variables: appVariables,
|
|
302
|
+
skipExisting: false,
|
|
303
|
+
filter: appFilter
|
|
304
|
+
});
|
|
305
|
+
if (appRender.unreplacedPlaceholders.length > 0) {
|
|
306
|
+
throw new Error(`应用模板渲染出现未替换占位符: ${appRender.unreplacedPlaceholders.map((p) => `${p.file} (${p.placeholder})`).join(', ')}`);
|
|
307
|
+
}
|
|
308
|
+
// 3. 执行 Verify
|
|
309
|
+
const verifyResult = verifyWorkspace(targetDir, inspect.preservedInputEntries);
|
|
310
|
+
if (!verifyResult.ok) {
|
|
311
|
+
throw new Error(`Workspace 校验失败: ${verifyResult.errors.join('; ')}`);
|
|
312
|
+
}
|
|
313
|
+
// 4. 提交事务
|
|
314
|
+
transaction.commit();
|
|
315
|
+
spinner.succeed('Workspace 工程生成并校验通过!');
|
|
316
|
+
// 5. 若启用 --install,自动执行依赖安装
|
|
317
|
+
let installed = false;
|
|
318
|
+
let installError;
|
|
319
|
+
if (install) {
|
|
320
|
+
const installSpinner = createSpinner('正在安装依赖 (pnpm install)...');
|
|
321
|
+
installSpinner.start();
|
|
322
|
+
const installRes = await runPnpmInstall(targetDir, isJson);
|
|
323
|
+
if (installRes.success) {
|
|
324
|
+
installed = true;
|
|
325
|
+
installSpinner.succeed('依赖安装成功!');
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
installed = false;
|
|
329
|
+
installError = installRes.error;
|
|
330
|
+
installSpinner.fail('依赖安装失败');
|
|
331
|
+
logWarn(`\n提示: 依赖安装出现异常 (${installError}),您可以稍后进入目录手动执行 "pnpm install"`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const allCreated = [...rootRender.filesCreated, ...appRender.filesCreated];
|
|
335
|
+
if (isJson) {
|
|
336
|
+
sendJson({
|
|
337
|
+
command: 'workspace.create',
|
|
338
|
+
ok: true,
|
|
339
|
+
target: targetDir,
|
|
340
|
+
appDir,
|
|
341
|
+
appName: name,
|
|
342
|
+
title,
|
|
343
|
+
router,
|
|
344
|
+
routerMode: router ? routerMode : 'none',
|
|
345
|
+
format,
|
|
346
|
+
style,
|
|
347
|
+
installed,
|
|
348
|
+
...(installError ? { installError } : {}),
|
|
349
|
+
filesCreated: allCreated,
|
|
350
|
+
preservedInputVerified: inspect.preservedInputEntries.length > 0
|
|
351
|
+
});
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
logInfo(`\n✨ Workspace 已就绪: ${targetDir}`);
|
|
355
|
+
logInfo(`- 应用: ${name} (目录: packages/${appDir})`);
|
|
356
|
+
logInfo(`- 路由: ${router ? routerMode : '未启用'}`);
|
|
357
|
+
logInfo(`- 样式: ${style}`);
|
|
358
|
+
logInfo(`- 交付格式: ${format}`);
|
|
359
|
+
logInfo(`- 依赖安装: ${installed ? '已完成' : '未执行 (可通过 pnpm install 安装)'}`);
|
|
360
|
+
logInfo('\n下一步 (Next Steps):');
|
|
361
|
+
logInfo(` 1. cd ${target}`);
|
|
362
|
+
if (!installed) {
|
|
363
|
+
logInfo(' 2. pnpm install');
|
|
364
|
+
logInfo(` 3. pnpm --filter "${name}" dev\n`);
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
logInfo(` 2. pnpm --filter "${name}" dev\n`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
catch (err) {
|
|
371
|
+
spinner.fail('Workspace 生成失败,正在回滚...');
|
|
372
|
+
transaction.rollback();
|
|
373
|
+
fail({
|
|
374
|
+
command: 'workspace.create',
|
|
375
|
+
error: `生成 Workspace 失败: ${err.message}`,
|
|
376
|
+
exitCode: ExitCode.GENERAL_ERROR
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
380
|
//# sourceMappingURL=create.js.map
|