@farris/cli 2.3.8 → 2.3.11
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/add-app.js +5 -5
- package/lib/commands/add-lib.js +5 -5
- package/lib/commands/add-micro.js +11 -11
- package/lib/commands/add-project.js +196 -196
- package/lib/commands/add-project.js.map +1 -1
- package/lib/commands/build-components.js +60 -60
- 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 +30 -30
- package/lib/commands/build.js +19 -19
- package/lib/commands/build.js.map +1 -1
- package/lib/commands/create-app-options.js +85 -85
- package/lib/commands/create-app-options.js.map +1 -1
- package/lib/commands/create-app.js +131 -131
- package/lib/commands/create-app.js.map +1 -1
- package/lib/commands/create-options.js +69 -58
- package/lib/commands/create-options.js.map +1 -1
- package/lib/commands/create.js +345 -341
- package/lib/commands/create.js.map +1 -1
- 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 +124 -124
- package/lib/commands/generate/api.js.map +1 -1
- package/lib/commands/generate/common.js +101 -101
- package/lib/commands/generate/common.js.map +1 -1
- package/lib/commands/generate/component.js +156 -156
- package/lib/commands/generate/component.js.map +1 -1
- package/lib/commands/generate/view.js +168 -168
- package/lib/commands/generate/view.js.map +1 -1
- package/lib/commands/preview-serve.js +16 -16
- package/lib/commands/workspace/add-app.js +15 -15
- package/lib/commands/workspace/add-app.js.map +1 -1
- package/lib/commands/workspace/create.js +62 -62
- 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/agents-doc.js +33 -33
- package/lib/common/agents-doc.js.map +1 -1
- package/lib/common/apply-catalog-deps.js +15 -15
- package/lib/common/apply-catalog-deps.js.map +1 -1
- package/lib/common/component-file-name.js +3 -3
- package/lib/common/constant.js +85 -84
- package/lib/common/constant.js.map +1 -1
- package/lib/common/create-app-target.js +46 -46
- package/lib/common/create-app-target.js.map +1 -1
- package/lib/common/generate-app.js +106 -106
- package/lib/common/generate-app.js.map +1 -1
- package/lib/common/get-dependencies.js +9 -9
- package/lib/common/get-farris-config.js +25 -25
- 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 +192 -192
- package/lib/common/get-vite-config.js.map +1 -1
- package/lib/common/lib-package-exports.js +24 -24
- package/lib/common/lib-package-exports.js.map +1 -1
- package/lib/common/list-lib-components.js +23 -23
- package/lib/common/list-lib-components.js.map +1 -1
- package/lib/common/output.js +164 -164
- package/lib/common/output.js.map +1 -1
- package/lib/common/project-layout.js +155 -155
- package/lib/common/project-layout.js.map +1 -1
- package/lib/common/safety.js +172 -172
- package/lib/common/safety.js.map +1 -1
- package/lib/common/template-manifest.js +18 -18
- package/lib/common/template-manifest.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 +187 -187
- package/lib/common/verifier.js.map +1 -1
- package/lib/common/workspace-contract.js +195 -191
- package/lib/common/workspace-contract.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 +32 -32
- package/lib/config/vite-component.js.map +1 -1
- package/lib/config/vite-lib.js +26 -26
- package/lib/index.js +280 -261
- package/lib/index.js.map +1 -1
- package/lib/plugins/create-app-package-json.js +23 -23
- package/lib/plugins/create-component-style.js +44 -44
- package/lib/plugins/create-package-json.js +48 -48
- 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/mock.js +240 -292
- package/lib/plugins/mock.js.map +1 -1
- package/lib/plugins/msw-adapter.js +141 -0
- package/lib/plugins/msw-adapter.js.map +1 -0
- 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 +93 -87
- package/templates/create/AGENTS.md +4 -3
- package/templates/lib/.gitignore.tpl +1 -0
- package/templates/micro/.gitignore.tpl +1 -0
- package/templates/micro/mock/README.md +39 -26
- package/templates/micro/mock/tsconfig.json +10 -0
- package/templates/micro/package.json +1 -0
- package/templates/mobile/.gitignore.tpl +1 -0
- package/templates/mobile/mock/README.md +39 -26
- package/templates/mobile/mock/tsconfig.json +10 -0
- package/templates/mobile/package.json +1 -0
- package/templates/web/.gitignore.tpl +1 -0
- package/templates/web/mock/README.md +39 -26
- package/templates/web/mock/tsconfig.json +10 -0
- package/templates/web/package.json +1 -0
package/lib/commands/create.js
CHANGED
|
@@ -1,342 +1,346 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { basename, isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
|
-
import inquirer from 'inquirer';
|
|
4
|
-
import { DEFAULT_CATALOG_VERSIONS, ExitCode, getDirname, renderCatalogEntries } from '../common/constant.js';
|
|
5
|
-
import { applyCatalogDeps } from '../common/apply-catalog-deps.js';
|
|
6
|
-
import { checkPlannedConflicts, inspectTarget } from '../common/safety.js';
|
|
7
|
-
import { collectTemplateOutputPaths, renderTemplateDir } from '../common/template-renderer.js';
|
|
8
|
-
import { CreateAppError } from '../common/create-app-target.js';
|
|
9
|
-
import { generateApp } from '../common/generate-app.js';
|
|
10
|
-
import { fail, printNextSteps, printSuccess, sendJson, setJsonMode } from '../common/output.js';
|
|
11
|
-
import { PROJECT_CONTAINERS, containerFor, deriveAppDir } from '../common/project-layout.js';
|
|
12
|
-
import { renderCommandTable, renderProjectList, renderProjectOverview, renderTechStack } from '../common/workspace-contract.js';
|
|
13
|
-
import { ALLOWED_TYPES, DEFAULT_APP_FORMAT, DEFAULT_LIB_FORMATS, DEFAULT_MICRO_FORMAT, parseFormatsFlag, resolvePackageName, validateCliInput } from './create-app-options.js';
|
|
14
|
-
export { parseCreateArgv } from './create-options.js';
|
|
15
|
-
const DEFAULT_WORKSPACE = 'my-workspace';
|
|
16
|
-
const DEFAULT_APP = 'app';
|
|
17
|
-
function createTemplateDir() {
|
|
18
|
-
return resolve(getDirname(import.meta.url), '../../templates/create');
|
|
19
|
-
}
|
|
20
|
-
function catalogVariables() {
|
|
21
|
-
return {
|
|
22
|
-
pnpmVersion: DEFAULT_CATALOG_VERSIONS.pnpm,
|
|
23
|
-
catalogEntries: renderCatalogEntries()
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
function assertAppName(appName) {
|
|
27
|
-
const trimmed = appName.trim();
|
|
28
|
-
if (!trimmed || trimmed === '.' || trimmed === '..') {
|
|
29
|
-
throw new CreateAppError('应用名称不能为空或为 . / ..');
|
|
30
|
-
}
|
|
31
|
-
if (trimmed.includes('\0') || trimmed.includes('\\')) {
|
|
32
|
-
throw new CreateAppError(`应用名称必须是单段目录名或带 scope 的包名: ${appName}`);
|
|
33
|
-
}
|
|
34
|
-
// 允许 @scope/name(包名保留 scope,目录名由 deriveAppDir 推导),其余多段路径一律拒绝
|
|
35
|
-
if (trimmed.includes('/') && !/^@[^/]+\/[^/]+$/.test(trimmed)) {
|
|
36
|
-
throw new CreateAppError(`应用名称必须是单段目录名或带 scope 的包名: ${appName}`);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
function assertProjectType(type) {
|
|
40
|
-
if (!ALLOWED_TYPES.includes(type)) {
|
|
41
|
-
throw new CreateAppError(`非法的工程类型: ${type},允许值为 ${ALLOWED_TYPES.join(', ')}`);
|
|
42
|
-
}
|
|
43
|
-
return type;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* create 的目标路径:允许 `.`(就地生成,见设计文档 §9.2),仍禁止逃出 cwd。
|
|
47
|
-
*/
|
|
48
|
-
function resolveCreateTarget(target, cwd = process.cwd()) {
|
|
49
|
-
const trimmed = (target ?? '').trim();
|
|
50
|
-
if (trimmed.includes('\0')) {
|
|
51
|
-
throw new CreateAppError('目标路径包含非法字符');
|
|
52
|
-
}
|
|
53
|
-
const resolved = resolve(cwd, trimmed);
|
|
54
|
-
const rel = relative(cwd, resolved);
|
|
55
|
-
if (rel.startsWith('..') || isAbsolute(rel)) {
|
|
56
|
-
throw new CreateAppError(`目标目录必须位于当前工作目录内: ${target}`);
|
|
57
|
-
}
|
|
58
|
-
return resolved;
|
|
59
|
-
}
|
|
60
|
-
function rootTemplateVariables(createApp, appRel, pkgType = 'app') {
|
|
61
|
-
if (createApp && appRel) {
|
|
62
|
-
// 事项 F 收尾:app 与 lib 的交付物目录都是 dist/
|
|
63
|
-
const artifactDir = `${appRel}/dist`;
|
|
64
|
-
const kindLabel = pkgType === 'lib' ? '组件库' : pkgType === 'micro' ? '微应用' : '应用';
|
|
65
|
-
return {
|
|
66
|
-
ciBuildSteps: [
|
|
67
|
-
` - pnpm --filter ./${appRel} run build`,
|
|
68
|
-
' - mkdir -p dist',
|
|
69
|
-
` - cp -r ${artifactDir}/. dist/`
|
|
70
|
-
].join('\n'),
|
|
71
|
-
ciPublishSteps: [' - cd dist', ' - npm publish'].join('\n'),
|
|
72
|
-
readmeBody: `pnpm workspace,包含一个 Farris ${kindLabel}(\`${appRel}\`)。`
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
return {
|
|
76
|
-
ciBuildSteps: '',
|
|
77
|
-
ciPublishSteps: ' - echo "no app to publish"',
|
|
78
|
-
readmeBody: `pnpm workspace,可在 \`${PROJECT_CONTAINERS.app}\` / \`${PROJECT_CONTAINERS.lib}\` 下添加工程。`
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
/** 交互模式收集 workspace 名、是否建工程、工程类型与工程名;非交互直接用默认值。 */
|
|
82
|
-
async function resolveCreateInputs(nameArg, options) {
|
|
83
|
-
let name = (nameArg ?? options.name ?? '').trim();
|
|
84
|
-
let createApp = options.noProject ? false : !!(options.yes || options.json || options.project != null);
|
|
85
|
-
let appName = (options.project ?? '').trim() || DEFAULT_APP;
|
|
86
|
-
let pkgType = assertProjectType(options.type ?? 'app');
|
|
87
|
-
if (options.yes || options.json) {
|
|
88
|
-
// 非交互路径:非法值直接报错,不等生成到一半才失败
|
|
89
|
-
validateCliInput({
|
|
90
|
-
type: options.type,
|
|
91
|
-
platform: options.platform,
|
|
92
|
-
format: options.format,
|
|
93
|
-
formats: options.formats
|
|
94
|
-
});
|
|
95
|
-
return {
|
|
96
|
-
name: name || DEFAULT_WORKSPACE,
|
|
97
|
-
createApp: options.noProject ? false : createApp,
|
|
98
|
-
appName,
|
|
99
|
-
pkgType
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
const prompt = inquirer.createPromptModule();
|
|
103
|
-
if (!name) {
|
|
104
|
-
const answers = await prompt([
|
|
105
|
-
{
|
|
106
|
-
type: 'input',
|
|
107
|
-
name: 'name',
|
|
108
|
-
message: '请输入 workspace 名称',
|
|
109
|
-
default: DEFAULT_WORKSPACE
|
|
110
|
-
}
|
|
111
|
-
]);
|
|
112
|
-
name = String(answers.name).trim() || DEFAULT_WORKSPACE;
|
|
113
|
-
}
|
|
114
|
-
if (!options.noProject && options.project == null) {
|
|
115
|
-
const answers = await prompt([
|
|
116
|
-
{
|
|
117
|
-
type: 'confirm',
|
|
118
|
-
name: 'createApp',
|
|
119
|
-
message: '是否现在创建一个工程?',
|
|
120
|
-
default: true
|
|
121
|
-
}
|
|
122
|
-
]);
|
|
123
|
-
createApp = !!answers.createApp;
|
|
124
|
-
if (createApp) {
|
|
125
|
-
const typeAnswers = await prompt([
|
|
126
|
-
{
|
|
127
|
-
type: 'list',
|
|
128
|
-
name: 'type',
|
|
129
|
-
message: '请选择工程类型',
|
|
130
|
-
choices: [...ALLOWED_TYPES],
|
|
131
|
-
default: 'app'
|
|
132
|
-
}
|
|
133
|
-
]);
|
|
134
|
-
pkgType = assertProjectType(String(typeAnswers.type));
|
|
135
|
-
const appAnswers = await prompt([
|
|
136
|
-
{
|
|
137
|
-
type: 'input',
|
|
138
|
-
name: 'appName',
|
|
139
|
-
message: '请输入工程名称',
|
|
140
|
-
default: DEFAULT_APP
|
|
141
|
-
}
|
|
142
|
-
]);
|
|
143
|
-
appName = String(appAnswers.appName).trim() || DEFAULT_APP;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return { name, createApp, appName, pkgType };
|
|
147
|
-
}
|
|
148
|
-
/** 校验工程名并推导包名与容器内相对目录;不建工程时返回空对象。 */
|
|
149
|
-
function resolveAppTargets(createApp, appName, pkgType) {
|
|
150
|
-
if (!createApp) {
|
|
151
|
-
return {};
|
|
152
|
-
}
|
|
153
|
-
assertAppName(appName);
|
|
154
|
-
const appPackageName = resolvePackageName(appName);
|
|
155
|
-
assertAppName(appPackageName);
|
|
156
|
-
const appDir = deriveAppDir(appPackageName);
|
|
157
|
-
if (!appDir || !/^[a-z0-9-_]+$/.test(appDir)) {
|
|
158
|
-
throw new CreateAppError(`无法从工程名 "${appPackageName}" 推导出合法的子目录名`);
|
|
159
|
-
}
|
|
160
|
-
return { appPackageName, appDir, appRel: `${containerFor(pkgType)}/${appDir}` };
|
|
161
|
-
}
|
|
162
|
-
/** 写入前冲突预检(设计文档 §9.1):非交互必须显式 --force,交互可确认覆盖。 */
|
|
163
|
-
async function confirmRootOverwrite(conflicts, options, isJson) {
|
|
164
|
-
if (conflicts.length === 0 || options.force) {
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
const interactive = !isJson && !(options.yes || options.json);
|
|
168
|
-
const shouldOverwrite = interactive &&
|
|
169
|
-
(await inquirer.createPromptModule()([
|
|
170
|
-
{
|
|
171
|
-
type: 'confirm',
|
|
172
|
-
name: 'overwrite',
|
|
173
|
-
message: `以下 ${conflicts.length} 个文件已存在,是否覆盖?\n${conflicts.join('\n')}`,
|
|
174
|
-
default: false
|
|
175
|
-
}
|
|
176
|
-
])).overwrite;
|
|
177
|
-
if (!shouldOverwrite) {
|
|
178
|
-
throw new CreateAppError(`目标目录包含将被模板写入的文件: ${conflicts.join(', ')}。请交互确认,或显式传入 --force 覆盖。`, ExitCode.TARGET_CONFLICT);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
/** 渲染根模板并补齐两个容器目录。 */
|
|
182
|
-
function scaffoldWorkspaceRoot(targetPath, context) {
|
|
183
|
-
const rootRender = renderTemplateDir({
|
|
184
|
-
sourceDir: createTemplateDir(),
|
|
185
|
-
targetDir: targetPath,
|
|
186
|
-
variables: {
|
|
187
|
-
...catalogVariables(),
|
|
188
|
-
...rootTemplateVariables(context.createApp, context.appRel, context.pkgType),
|
|
189
|
-
appContainer: PROJECT_CONTAINERS.app,
|
|
190
|
-
libContainer: PROJECT_CONTAINERS.lib,
|
|
191
|
-
commandTable: renderCommandTable(),
|
|
192
|
-
projectOverview: renderProjectOverview(context.workspaceTitle),
|
|
193
|
-
projectList: renderProjectList(context.plannedProjects),
|
|
194
|
-
techStack: renderTechStack(),
|
|
195
|
-
title: context.workspaceTitle
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
if (rootRender.unreplacedPlaceholders.length > 0) {
|
|
199
|
-
throw new Error(`根模板未替换: ${rootRender.unreplacedPlaceholders.map((p) => p.placeholder).join(', ')}`);
|
|
200
|
-
}
|
|
201
|
-
// 容器按需创建:只建「真的有工程」的那一类,空容器目录不出现。
|
|
202
|
-
// 另一类在首次 add-app / add-lib 时自动生成;pnpm 的 `container/*` glob 在目录不存在时
|
|
203
|
-
// 只是匹配为空,不影响安装,因此无需 .gitkeep 占位。
|
|
204
|
-
const containers = new Set(context.plannedProjects.map((project) => containerFor(project.type)));
|
|
205
|
-
for (const container of containers) {
|
|
206
|
-
mkdirSync(join(targetPath, container), { recursive: true });
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
/** 渲染子工程模板、去掉子工程内的 CI 文件并把可入 catalog 的依赖改写为 catalog:。 */
|
|
210
|
-
function scaffoldAppProject(targetPath, context) {
|
|
211
|
-
const { appRel, appPackageName, pkgType, workspaceTitle, options } = context;
|
|
212
|
-
generateApp({
|
|
213
|
-
name: join(targetPath, appRel).replace(/\\/g, '/'),
|
|
214
|
-
packageName: appPackageName,
|
|
215
|
-
title: workspaceTitle,
|
|
216
|
-
type: pkgType,
|
|
217
|
-
...(pkgType === 'lib'
|
|
218
|
-
? { formats: parseFormatsFlag(options.formats) ?? [...DEFAULT_LIB_FORMATS] }
|
|
219
|
-
: {
|
|
220
|
-
platform: options.platform ?? 'web',
|
|
221
|
-
// 微应用默认 iife 单文件(设计文档 §3.3)
|
|
222
|
-
format: options.format ?? (pkgType === 'micro' ? DEFAULT_MICRO_FORMAT : DEFAULT_APP_FORMAT),
|
|
223
|
-
style: options.style ?? 'scss'
|
|
224
|
-
}),
|
|
225
|
-
hints: false
|
|
226
|
-
});
|
|
227
|
-
// 根目录只保留一份 .gitlab-ci.yml
|
|
228
|
-
const appGitlab = join(targetPath, appRel, '.gitlab-ci.yml');
|
|
229
|
-
if (existsSync(appGitlab)) {
|
|
230
|
-
unlinkSync(appGitlab);
|
|
231
|
-
}
|
|
232
|
-
const appPkgPath = join(targetPath, appRel, 'package.json');
|
|
233
|
-
const appPkg = JSON.parse(readFileSync(appPkgPath, 'utf8'));
|
|
234
|
-
applyCatalogDeps(appPkg);
|
|
235
|
-
writeFileSync(appPkgPath, JSON.stringify(appPkg, null, 2) + '\n');
|
|
236
|
-
}
|
|
237
|
-
async function installDependenciesIfRequested(options, targetPath, isJson) {
|
|
238
|
-
if (!options.install) {
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
const { runPnpmInstall } = await import('./workspace/create.js');
|
|
242
|
-
const result = await runPnpmInstall(targetPath, isJson);
|
|
243
|
-
if (!result.success) {
|
|
244
|
-
throw new CreateAppError(result.error || 'pnpm install 失败');
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
function reportCreate(result) {
|
|
248
|
-
if (result.isJson) {
|
|
249
|
-
sendJson({
|
|
250
|
-
command: 'create',
|
|
251
|
-
ok: true,
|
|
252
|
-
name: result.workspaceTitle,
|
|
253
|
-
createdApp: result.createApp,
|
|
254
|
-
...(result.createApp
|
|
255
|
-
? {
|
|
256
|
-
packageName: result.targets.appPackageName,
|
|
257
|
-
appDir: result.targets.appRel,
|
|
258
|
-
type: result.targets.pkgType
|
|
259
|
-
}
|
|
260
|
-
: {}),
|
|
261
|
-
target: result.targetPath
|
|
262
|
-
});
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
printSuccess(`工作空间已创建 ${result.targetPath}`);
|
|
266
|
-
printNextSteps({
|
|
267
|
-
targetArg: result.targetArg,
|
|
268
|
-
targetDir: result.targetPath,
|
|
269
|
-
cwd: result.cwd,
|
|
270
|
-
installed: result.installed,
|
|
271
|
-
appPackageName: result.targets.appPackageName
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
export async function create(nameArg, options = {}) {
|
|
275
|
-
const isJson = !!options.json;
|
|
276
|
-
setJsonMode(isJson);
|
|
277
|
-
try {
|
|
278
|
-
const inputs = await resolveCreateInputs(nameArg, options);
|
|
279
|
-
const cwd = process.cwd();
|
|
280
|
-
const targetArg = inputs.name;
|
|
281
|
-
const targetPath = resolveCreateTarget(inputs.name, cwd);
|
|
282
|
-
const workspaceTitle = targetArg === '.' ? basename(targetPath) : targetArg;
|
|
283
|
-
const inspect = inspectTarget(targetPath);
|
|
284
|
-
if (!inspect.canCreate) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
:
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import { DEFAULT_CATALOG_VERSIONS, ExitCode, WorkspaceStatus, getDirname, renderCatalogEntries } from '../common/constant.js';
|
|
5
|
+
import { applyCatalogDeps } from '../common/apply-catalog-deps.js';
|
|
6
|
+
import { checkPlannedConflicts, inspectTarget } from '../common/safety.js';
|
|
7
|
+
import { collectTemplateOutputPaths, renderTemplateDir } from '../common/template-renderer.js';
|
|
8
|
+
import { CreateAppError } from '../common/create-app-target.js';
|
|
9
|
+
import { generateApp } from '../common/generate-app.js';
|
|
10
|
+
import { fail, printNextSteps, printSuccess, sendJson, setJsonMode } from '../common/output.js';
|
|
11
|
+
import { PROJECT_CONTAINERS, containerFor, deriveAppDir } from '../common/project-layout.js';
|
|
12
|
+
import { renderCommandTable, renderProjectList, renderProjectOverview, renderTechStack } from '../common/workspace-contract.js';
|
|
13
|
+
import { ALLOWED_TYPES, DEFAULT_APP_FORMAT, DEFAULT_LIB_FORMATS, DEFAULT_MICRO_FORMAT, parseFormatsFlag, resolvePackageName, validateCliInput } from './create-app-options.js';
|
|
14
|
+
export { excessArgumentMessage, parseCreateArgv } from './create-options.js';
|
|
15
|
+
const DEFAULT_WORKSPACE = 'my-workspace';
|
|
16
|
+
const DEFAULT_APP = 'app';
|
|
17
|
+
function createTemplateDir() {
|
|
18
|
+
return resolve(getDirname(import.meta.url), '../../templates/create');
|
|
19
|
+
}
|
|
20
|
+
function catalogVariables() {
|
|
21
|
+
return {
|
|
22
|
+
pnpmVersion: DEFAULT_CATALOG_VERSIONS.pnpm,
|
|
23
|
+
catalogEntries: renderCatalogEntries()
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function assertAppName(appName) {
|
|
27
|
+
const trimmed = appName.trim();
|
|
28
|
+
if (!trimmed || trimmed === '.' || trimmed === '..') {
|
|
29
|
+
throw new CreateAppError('应用名称不能为空或为 . / ..');
|
|
30
|
+
}
|
|
31
|
+
if (trimmed.includes('\0') || trimmed.includes('\\')) {
|
|
32
|
+
throw new CreateAppError(`应用名称必须是单段目录名或带 scope 的包名: ${appName}`);
|
|
33
|
+
}
|
|
34
|
+
// 允许 @scope/name(包名保留 scope,目录名由 deriveAppDir 推导),其余多段路径一律拒绝
|
|
35
|
+
if (trimmed.includes('/') && !/^@[^/]+\/[^/]+$/.test(trimmed)) {
|
|
36
|
+
throw new CreateAppError(`应用名称必须是单段目录名或带 scope 的包名: ${appName}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function assertProjectType(type) {
|
|
40
|
+
if (!ALLOWED_TYPES.includes(type)) {
|
|
41
|
+
throw new CreateAppError(`非法的工程类型: ${type},允许值为 ${ALLOWED_TYPES.join(', ')}`);
|
|
42
|
+
}
|
|
43
|
+
return type;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* create 的目标路径:允许 `.`(就地生成,见设计文档 §9.2),仍禁止逃出 cwd。
|
|
47
|
+
*/
|
|
48
|
+
function resolveCreateTarget(target, cwd = process.cwd()) {
|
|
49
|
+
const trimmed = (target ?? '').trim();
|
|
50
|
+
if (trimmed.includes('\0')) {
|
|
51
|
+
throw new CreateAppError('目标路径包含非法字符');
|
|
52
|
+
}
|
|
53
|
+
const resolved = resolve(cwd, trimmed);
|
|
54
|
+
const rel = relative(cwd, resolved);
|
|
55
|
+
if (rel.startsWith('..') || isAbsolute(rel)) {
|
|
56
|
+
throw new CreateAppError(`目标目录必须位于当前工作目录内: ${target}`);
|
|
57
|
+
}
|
|
58
|
+
return resolved;
|
|
59
|
+
}
|
|
60
|
+
function rootTemplateVariables(createApp, appRel, pkgType = 'app') {
|
|
61
|
+
if (createApp && appRel) {
|
|
62
|
+
// 事项 F 收尾:app 与 lib 的交付物目录都是 dist/
|
|
63
|
+
const artifactDir = `${appRel}/dist`;
|
|
64
|
+
const kindLabel = pkgType === 'lib' ? '组件库' : pkgType === 'micro' ? '微应用' : '应用';
|
|
65
|
+
return {
|
|
66
|
+
ciBuildSteps: [
|
|
67
|
+
` - pnpm --filter ./${appRel} run build`,
|
|
68
|
+
' - mkdir -p dist',
|
|
69
|
+
` - cp -r ${artifactDir}/. dist/`
|
|
70
|
+
].join('\n'),
|
|
71
|
+
ciPublishSteps: [' - cd dist', ' - npm publish'].join('\n'),
|
|
72
|
+
readmeBody: `pnpm workspace,包含一个 Farris ${kindLabel}(\`${appRel}\`)。`
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
ciBuildSteps: '',
|
|
77
|
+
ciPublishSteps: ' - echo "no app to publish"',
|
|
78
|
+
readmeBody: `pnpm workspace,可在 \`${PROJECT_CONTAINERS.app}\` / \`${PROJECT_CONTAINERS.lib}\` 下添加工程。`
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** 交互模式收集 workspace 名、是否建工程、工程类型与工程名;非交互直接用默认值。 */
|
|
82
|
+
async function resolveCreateInputs(nameArg, options) {
|
|
83
|
+
let name = (nameArg ?? options.name ?? '').trim();
|
|
84
|
+
let createApp = options.noProject ? false : !!(options.yes || options.json || options.project != null);
|
|
85
|
+
let appName = (options.project ?? '').trim() || DEFAULT_APP;
|
|
86
|
+
let pkgType = assertProjectType(options.type ?? 'app');
|
|
87
|
+
if (options.yes || options.json) {
|
|
88
|
+
// 非交互路径:非法值直接报错,不等生成到一半才失败
|
|
89
|
+
validateCliInput({
|
|
90
|
+
type: options.type,
|
|
91
|
+
platform: options.platform,
|
|
92
|
+
format: options.format,
|
|
93
|
+
formats: options.formats
|
|
94
|
+
});
|
|
95
|
+
return {
|
|
96
|
+
name: name || DEFAULT_WORKSPACE,
|
|
97
|
+
createApp: options.noProject ? false : createApp,
|
|
98
|
+
appName,
|
|
99
|
+
pkgType
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const prompt = inquirer.createPromptModule();
|
|
103
|
+
if (!name) {
|
|
104
|
+
const answers = await prompt([
|
|
105
|
+
{
|
|
106
|
+
type: 'input',
|
|
107
|
+
name: 'name',
|
|
108
|
+
message: '请输入 workspace 名称',
|
|
109
|
+
default: DEFAULT_WORKSPACE
|
|
110
|
+
}
|
|
111
|
+
]);
|
|
112
|
+
name = String(answers.name).trim() || DEFAULT_WORKSPACE;
|
|
113
|
+
}
|
|
114
|
+
if (!options.noProject && options.project == null) {
|
|
115
|
+
const answers = await prompt([
|
|
116
|
+
{
|
|
117
|
+
type: 'confirm',
|
|
118
|
+
name: 'createApp',
|
|
119
|
+
message: '是否现在创建一个工程?',
|
|
120
|
+
default: true
|
|
121
|
+
}
|
|
122
|
+
]);
|
|
123
|
+
createApp = !!answers.createApp;
|
|
124
|
+
if (createApp) {
|
|
125
|
+
const typeAnswers = await prompt([
|
|
126
|
+
{
|
|
127
|
+
type: 'list',
|
|
128
|
+
name: 'type',
|
|
129
|
+
message: '请选择工程类型',
|
|
130
|
+
choices: [...ALLOWED_TYPES],
|
|
131
|
+
default: 'app'
|
|
132
|
+
}
|
|
133
|
+
]);
|
|
134
|
+
pkgType = assertProjectType(String(typeAnswers.type));
|
|
135
|
+
const appAnswers = await prompt([
|
|
136
|
+
{
|
|
137
|
+
type: 'input',
|
|
138
|
+
name: 'appName',
|
|
139
|
+
message: '请输入工程名称',
|
|
140
|
+
default: DEFAULT_APP
|
|
141
|
+
}
|
|
142
|
+
]);
|
|
143
|
+
appName = String(appAnswers.appName).trim() || DEFAULT_APP;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { name, createApp, appName, pkgType };
|
|
147
|
+
}
|
|
148
|
+
/** 校验工程名并推导包名与容器内相对目录;不建工程时返回空对象。 */
|
|
149
|
+
function resolveAppTargets(createApp, appName, pkgType) {
|
|
150
|
+
if (!createApp) {
|
|
151
|
+
return {};
|
|
152
|
+
}
|
|
153
|
+
assertAppName(appName);
|
|
154
|
+
const appPackageName = resolvePackageName(appName);
|
|
155
|
+
assertAppName(appPackageName);
|
|
156
|
+
const appDir = deriveAppDir(appPackageName);
|
|
157
|
+
if (!appDir || !/^[a-z0-9-_]+$/.test(appDir)) {
|
|
158
|
+
throw new CreateAppError(`无法从工程名 "${appPackageName}" 推导出合法的子目录名`);
|
|
159
|
+
}
|
|
160
|
+
return { appPackageName, appDir, appRel: `${containerFor(pkgType)}/${appDir}` };
|
|
161
|
+
}
|
|
162
|
+
/** 写入前冲突预检(设计文档 §9.1):非交互必须显式 --force,交互可确认覆盖。 */
|
|
163
|
+
async function confirmRootOverwrite(conflicts, options, isJson) {
|
|
164
|
+
if (conflicts.length === 0 || options.force) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const interactive = !isJson && !(options.yes || options.json);
|
|
168
|
+
const shouldOverwrite = interactive &&
|
|
169
|
+
(await inquirer.createPromptModule()([
|
|
170
|
+
{
|
|
171
|
+
type: 'confirm',
|
|
172
|
+
name: 'overwrite',
|
|
173
|
+
message: `以下 ${conflicts.length} 个文件已存在,是否覆盖?\n${conflicts.join('\n')}`,
|
|
174
|
+
default: false
|
|
175
|
+
}
|
|
176
|
+
])).overwrite;
|
|
177
|
+
if (!shouldOverwrite) {
|
|
178
|
+
throw new CreateAppError(`目标目录包含将被模板写入的文件: ${conflicts.join(', ')}。请交互确认,或显式传入 --force 覆盖。`, ExitCode.TARGET_CONFLICT);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** 渲染根模板并补齐两个容器目录。 */
|
|
182
|
+
function scaffoldWorkspaceRoot(targetPath, context) {
|
|
183
|
+
const rootRender = renderTemplateDir({
|
|
184
|
+
sourceDir: createTemplateDir(),
|
|
185
|
+
targetDir: targetPath,
|
|
186
|
+
variables: {
|
|
187
|
+
...catalogVariables(),
|
|
188
|
+
...rootTemplateVariables(context.createApp, context.appRel, context.pkgType),
|
|
189
|
+
appContainer: PROJECT_CONTAINERS.app,
|
|
190
|
+
libContainer: PROJECT_CONTAINERS.lib,
|
|
191
|
+
commandTable: renderCommandTable(),
|
|
192
|
+
projectOverview: renderProjectOverview(context.workspaceTitle),
|
|
193
|
+
projectList: renderProjectList(context.plannedProjects),
|
|
194
|
+
techStack: renderTechStack(),
|
|
195
|
+
title: context.workspaceTitle
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
if (rootRender.unreplacedPlaceholders.length > 0) {
|
|
199
|
+
throw new Error(`根模板未替换: ${rootRender.unreplacedPlaceholders.map((p) => p.placeholder).join(', ')}`);
|
|
200
|
+
}
|
|
201
|
+
// 容器按需创建:只建「真的有工程」的那一类,空容器目录不出现。
|
|
202
|
+
// 另一类在首次 add-app / add-lib 时自动生成;pnpm 的 `container/*` glob 在目录不存在时
|
|
203
|
+
// 只是匹配为空,不影响安装,因此无需 .gitkeep 占位。
|
|
204
|
+
const containers = new Set(context.plannedProjects.map((project) => containerFor(project.type)));
|
|
205
|
+
for (const container of containers) {
|
|
206
|
+
mkdirSync(join(targetPath, container), { recursive: true });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** 渲染子工程模板、去掉子工程内的 CI 文件并把可入 catalog 的依赖改写为 catalog:。 */
|
|
210
|
+
function scaffoldAppProject(targetPath, context) {
|
|
211
|
+
const { appRel, appPackageName, pkgType, workspaceTitle, options } = context;
|
|
212
|
+
generateApp({
|
|
213
|
+
name: join(targetPath, appRel).replace(/\\/g, '/'),
|
|
214
|
+
packageName: appPackageName,
|
|
215
|
+
title: workspaceTitle,
|
|
216
|
+
type: pkgType,
|
|
217
|
+
...(pkgType === 'lib'
|
|
218
|
+
? { formats: parseFormatsFlag(options.formats) ?? [...DEFAULT_LIB_FORMATS] }
|
|
219
|
+
: {
|
|
220
|
+
platform: options.platform ?? 'web',
|
|
221
|
+
// 微应用默认 iife 单文件(设计文档 §3.3)
|
|
222
|
+
format: options.format ?? (pkgType === 'micro' ? DEFAULT_MICRO_FORMAT : DEFAULT_APP_FORMAT),
|
|
223
|
+
style: options.style ?? 'scss'
|
|
224
|
+
}),
|
|
225
|
+
hints: false
|
|
226
|
+
});
|
|
227
|
+
// 根目录只保留一份 .gitlab-ci.yml
|
|
228
|
+
const appGitlab = join(targetPath, appRel, '.gitlab-ci.yml');
|
|
229
|
+
if (existsSync(appGitlab)) {
|
|
230
|
+
unlinkSync(appGitlab);
|
|
231
|
+
}
|
|
232
|
+
const appPkgPath = join(targetPath, appRel, 'package.json');
|
|
233
|
+
const appPkg = JSON.parse(readFileSync(appPkgPath, 'utf8'));
|
|
234
|
+
applyCatalogDeps(appPkg);
|
|
235
|
+
writeFileSync(appPkgPath, JSON.stringify(appPkg, null, 2) + '\n');
|
|
236
|
+
}
|
|
237
|
+
async function installDependenciesIfRequested(options, targetPath, isJson) {
|
|
238
|
+
if (!options.install) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const { runPnpmInstall } = await import('./workspace/create.js');
|
|
242
|
+
const result = await runPnpmInstall(targetPath, isJson);
|
|
243
|
+
if (!result.success) {
|
|
244
|
+
throw new CreateAppError(result.error || 'pnpm install 失败');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function reportCreate(result) {
|
|
248
|
+
if (result.isJson) {
|
|
249
|
+
sendJson({
|
|
250
|
+
command: 'create',
|
|
251
|
+
ok: true,
|
|
252
|
+
name: result.workspaceTitle,
|
|
253
|
+
createdApp: result.createApp,
|
|
254
|
+
...(result.createApp
|
|
255
|
+
? {
|
|
256
|
+
packageName: result.targets.appPackageName,
|
|
257
|
+
appDir: result.targets.appRel,
|
|
258
|
+
type: result.targets.pkgType
|
|
259
|
+
}
|
|
260
|
+
: {}),
|
|
261
|
+
target: result.targetPath
|
|
262
|
+
});
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
printSuccess(`工作空间已创建 ${result.targetPath}`);
|
|
266
|
+
printNextSteps({
|
|
267
|
+
targetArg: result.targetArg,
|
|
268
|
+
targetDir: result.targetPath,
|
|
269
|
+
cwd: result.cwd,
|
|
270
|
+
installed: result.installed,
|
|
271
|
+
appPackageName: result.targets.appPackageName
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
export async function create(nameArg, options = {}) {
|
|
275
|
+
const isJson = !!options.json;
|
|
276
|
+
setJsonMode(isJson);
|
|
277
|
+
try {
|
|
278
|
+
const inputs = await resolveCreateInputs(nameArg, options);
|
|
279
|
+
const cwd = process.cwd();
|
|
280
|
+
const targetArg = inputs.name;
|
|
281
|
+
const targetPath = resolveCreateTarget(inputs.name, cwd);
|
|
282
|
+
const workspaceTitle = targetArg === '.' ? basename(targetPath) : targetArg;
|
|
283
|
+
const inspect = inspectTarget(targetPath);
|
|
284
|
+
if (!inspect.canCreate) {
|
|
285
|
+
// 必须带上实际解析到的目标路径:参数被 npm 吞掉时,用户看不出 CLI 究竟去了哪个目录
|
|
286
|
+
const remediation = inspect.status === WorkspaceStatus.EXISTING_WORKSPACE
|
|
287
|
+
? ';该目录已是 workspace,请换一个目录名'
|
|
288
|
+
: '';
|
|
289
|
+
throw new CreateAppError(`目标目录不能用于创建 workspace: ${targetPath}(状态: ${inspect.status}${inspect.conflicts.length > 0 ? `,冲突项: ${inspect.conflicts.join(', ')}` : ''})${remediation}`, ExitCode.TARGET_CONFLICT);
|
|
290
|
+
}
|
|
291
|
+
const created = !existsSync(targetPath);
|
|
292
|
+
const targets = {
|
|
293
|
+
...resolveAppTargets(inputs.createApp, inputs.appName, inputs.pkgType),
|
|
294
|
+
pkgType: inputs.pkgType
|
|
295
|
+
};
|
|
296
|
+
const plannedProjects = targets.appRel && targets.appDir
|
|
297
|
+
? [{ type: inputs.pkgType, dir: targets.appRel, name: targets.appDir }]
|
|
298
|
+
: [];
|
|
299
|
+
// create 的子工程落在全新容器内,因此只需预检根模板。
|
|
300
|
+
await confirmRootOverwrite(checkPlannedConflicts(targetPath, collectTemplateOutputPaths(createTemplateDir())), options, isJson);
|
|
301
|
+
try {
|
|
302
|
+
scaffoldWorkspaceRoot(targetPath, {
|
|
303
|
+
createApp: inputs.createApp,
|
|
304
|
+
appRel: targets.appRel,
|
|
305
|
+
pkgType: inputs.pkgType,
|
|
306
|
+
workspaceTitle,
|
|
307
|
+
plannedProjects
|
|
308
|
+
});
|
|
309
|
+
if (inputs.createApp && targets.appRel && targets.appPackageName) {
|
|
310
|
+
scaffoldAppProject(targetPath, {
|
|
311
|
+
appRel: targets.appRel,
|
|
312
|
+
appPackageName: targets.appPackageName,
|
|
313
|
+
pkgType: inputs.pkgType,
|
|
314
|
+
workspaceTitle,
|
|
315
|
+
options
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
await installDependenciesIfRequested(options, targetPath, isJson);
|
|
319
|
+
reportCreate({
|
|
320
|
+
isJson,
|
|
321
|
+
workspaceTitle,
|
|
322
|
+
createApp: inputs.createApp,
|
|
323
|
+
targets,
|
|
324
|
+
targetPath,
|
|
325
|
+
targetArg,
|
|
326
|
+
cwd,
|
|
327
|
+
installed: !!options.install
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
catch (inner) {
|
|
331
|
+
if (created && existsSync(targetPath)) {
|
|
332
|
+
rmSync(targetPath, { recursive: true, force: true });
|
|
333
|
+
}
|
|
334
|
+
throw inner;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
if (isJson) {
|
|
339
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
340
|
+
const exitCode = error instanceof CreateAppError ? error.exitCode : ExitCode.GENERAL_ERROR;
|
|
341
|
+
fail({ command: 'create', error: message, exitCode });
|
|
342
|
+
}
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
342
346
|
//# sourceMappingURL=create.js.map
|