@farris/cli 2.3.7 → 2.3.9

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