@ykdz/template 0.0.41 → 0.0.42

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 (81) hide show
  1. package/node_modules/@ykdz/template-builtin-presets/dist/src/foundation.js +111 -99
  2. package/node_modules/@ykdz/template-builtin-presets/dist/src/rust-bin/definition.js +29 -4
  3. package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/vue.js +11 -2
  4. package/node_modules/@ykdz/template-builtin-presets/dist/src/ts-cli/definition.js +14 -4
  5. package/node_modules/@ykdz/template-builtin-presets/dist/src/ts-lib/definition.js +13 -0
  6. package/node_modules/@ykdz/template-builtin-presets/dist/src/vike-app/definition.js +44 -10
  7. package/node_modules/@ykdz/template-builtin-presets/dist/src/vue-app/definition.js +4 -1
  8. package/node_modules/@ykdz/template-builtin-presets/dist/src/vue-hono-app/definition.js +19 -8
  9. package/node_modules/@ykdz/template-builtin-presets/package.json +23 -10
  10. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/.github/workflows/check-deployment-diagnostics.yml +30 -29
  11. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/.github/workflows/check-deployment.yml +8 -21
  12. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/.github/workflows/check-diagnostics.yml +21 -5
  13. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/.github/workflows/check.yml +4 -2
  14. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/AGENTS.md.template +7 -1
  15. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/gitignore +1 -0
  16. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/pnpm-workspace.yaml +3 -0
  17. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/scripts/node-test-count-reporter.ts +31 -0
  18. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/scripts/run-rust-tests.ts +100 -0
  19. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/scripts/run-turbo-boundaries.ts +74 -0
  20. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/scripts/task-gates.ts +589 -0
  21. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/turbo-projections/database-preparation.json +17 -0
  22. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/turbo-projections/deployment.json +13 -0
  23. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/turbo-projections/rust-toolchain.json +5 -0
  24. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/turbo-projections/vue-hono-joint-e2e.json +13 -0
  25. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/turbo.json +14 -15
  26. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/typescript-config/turbo.json +30 -1
  27. package/node_modules/@ykdz/template-builtin-presets/templates/rust-bin/tsconfig.scripts.json +12 -0
  28. package/node_modules/@ykdz/template-builtin-presets/templates/rust-bin/turbo.json +19 -4
  29. package/node_modules/@ykdz/template-builtin-presets/templates/shared/vue/TESTING.md +7 -0
  30. package/node_modules/@ykdz/template-builtin-presets/templates/shared/vue/src/components/CounterButton.vue +18 -0
  31. package/node_modules/@ykdz/template-builtin-presets/templates/shared/vue/test/counter.browser.test.ts +16 -0
  32. package/node_modules/@ykdz/template-builtin-presets/templates/shared/vue/test/{app.test.ts → counter.node.test.ts} +1 -0
  33. package/node_modules/@ykdz/template-builtin-presets/templates/shared/vue/tsconfig.test.json +1 -1
  34. package/node_modules/@ykdz/template-builtin-presets/templates/shared/vue/vitest.config.ts +50 -0
  35. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/check-artifact.ts +16 -5
  36. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/release.yml +5 -2
  37. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/turbo.json +5 -2
  38. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication-setup/bridge.ts +1 -1
  39. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/src/cli-command-identity.ts +0 -45
  40. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/test/e2e/run-journeys.test.ts +11 -0
  41. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/test/integration/command.test.ts +43 -39
  42. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/test/unit/cli-command-identity.test.ts +52 -52
  43. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/tsconfig.json +1 -1
  44. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/turbo.json +58 -3
  45. package/node_modules/@ykdz/template-builtin-presets/templates/ts-lib/test/greet.test.ts +20 -0
  46. package/node_modules/@ykdz/template-builtin-presets/templates/ts-lib/tsconfig.json +1 -1
  47. package/node_modules/@ykdz/template-builtin-presets/templates/ts-lib/turbo.json +44 -1
  48. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db/test/todos.test.ts +8 -11
  49. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db/turbo.json +38 -1
  50. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db-migrations/turbo.json +44 -1
  51. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/playwright.config.ts +18 -2
  52. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/test/counter.browser.test.ts +14 -0
  53. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/tsconfig.test.json +8 -2
  54. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/turbo.json +61 -1
  55. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/vitest.config.ts +29 -2
  56. package/node_modules/@ykdz/template-builtin-presets/templates/vue-app/src/App.vue +2 -12
  57. package/node_modules/@ykdz/template-builtin-presets/templates/vue-app/turbo.json +60 -2
  58. package/node_modules/@ykdz/template-builtin-presets/templates/vue-hono-app/api/test/app.test.ts +7 -6
  59. package/node_modules/@ykdz/template-builtin-presets/templates/vue-hono-app/api/tsconfig.json +2 -7
  60. package/node_modules/@ykdz/template-builtin-presets/templates/vue-hono-app/api/turbo.json +46 -1
  61. package/node_modules/@ykdz/template-builtin-presets/templates/vue-hono-app/web/src/App.vue +2 -11
  62. package/node_modules/@ykdz/template-builtin-presets/templates/vue-hono-app/web/turbo.json +57 -1
  63. package/node_modules/@ykdz/template-core/dist/ci-diagnostic-artifact.d.ts +1 -1
  64. package/node_modules/@ykdz/template-core/dist/ci-diagnostic-artifact.js +6 -3
  65. package/node_modules/@ykdz/template-core/dist/module-graph.d.ts +0 -9
  66. package/node_modules/@ykdz/template-core/dist/module-graph.js +0 -40
  67. package/node_modules/@ykdz/template-core/dist/package-contribution.d.ts +2 -2
  68. package/node_modules/@ykdz/template-core/dist/package-contribution.js +2 -44
  69. package/node_modules/@ykdz/template-core/dist/pnpm-workspace.yaml +3 -0
  70. package/node_modules/@ykdz/template-core/dist/project-github.js +7 -3
  71. package/node_modules/@ykdz/template-core/dist/renderer.d.ts +1 -0
  72. package/node_modules/@ykdz/template-core/dist/renderer.js +39 -1
  73. package/node_modules/@ykdz/template-core/dist/template-boundary-check.js +34 -16
  74. package/node_modules/@ykdz/template-core/package.json +0 -5
  75. package/package.json +4 -10
  76. package/node_modules/@ykdz/template-builtin-presets/templates/rust-bin/.github/workflows/check.yml +0 -23
  77. package/node_modules/@ykdz/template-builtin-presets/templates/ts-lib/.github/workflows/check.yml +0 -19
  78. package/node_modules/@ykdz/template-builtin-presets/templates/vue-app/.github/workflows/check.yml +0 -20
  79. package/node_modules/@ykdz/template-builtin-presets/templates/vue-app/vitest.config.ts +0 -8
  80. package/node_modules/@ykdz/template-builtin-presets/templates/vue-hono-app/api/vitest.config.ts +0 -15
  81. package/node_modules/@ykdz/template-builtin-presets/templates/vue-hono-app/web/vitest.config.ts +0 -10
@@ -0,0 +1,589 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { resolve } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+
5
+ export type TaskGate = "check" | "check:heavy" | "check:full" | "fix";
6
+
7
+ type ScriptRole =
8
+ | "quality:light"
9
+ | "quality:heavy"
10
+ | "quality:conditional"
11
+ | "quality:entry"
12
+ | "work:development"
13
+ | "work:repair"
14
+ | "work:release"
15
+ | "work:preparation"
16
+ | "work:lifecycle"
17
+ | "work:on-demand";
18
+
19
+ type TurboTask = {
20
+ readonly name: string;
21
+ readonly fullName: string;
22
+ readonly packageName: string;
23
+ readonly tags: readonly string[];
24
+ readonly executable: boolean;
25
+ readonly role?: ScriptRole;
26
+ };
27
+
28
+ type GateLayer = {
29
+ readonly role: "quality:light" | "quality:heavy" | "work:repair";
30
+ readonly label: string;
31
+ };
32
+
33
+ type CommandResult = {
34
+ readonly status: number;
35
+ readonly stdout: string;
36
+ readonly stderr: string;
37
+ readonly error: Error | undefined;
38
+ };
39
+
40
+ const turboTaskQuery =
41
+ "{ packages { items { name tasks { items { name fullName tags script command } } } } }";
42
+
43
+ const validRoles = new Set<ScriptRole>([
44
+ "quality:light",
45
+ "quality:heavy",
46
+ "quality:conditional",
47
+ "quality:entry",
48
+ "work:development",
49
+ "work:repair",
50
+ "work:release",
51
+ "work:preparation",
52
+ "work:lifecycle",
53
+ "work:on-demand",
54
+ ]);
55
+
56
+ const publicGateNames = new Set<TaskGate>([
57
+ "check",
58
+ "check:heavy",
59
+ "check:full",
60
+ "fix",
61
+ ]);
62
+
63
+ const maxTurboOutputBytes = 16 * 1024 * 1024;
64
+
65
+ function isRecord(value: unknown): value is Record<string, unknown> {
66
+ return typeof value === "object" && value !== null && !Array.isArray(value);
67
+ }
68
+
69
+ function isNonEmptyString(value: unknown): value is string {
70
+ return typeof value === "string" && value.trim().length > 0;
71
+ }
72
+
73
+ function report(message: string): void {
74
+ process.stderr.write(`${message}\n`);
75
+ }
76
+
77
+ function runCaptured(cwd: string, args: readonly string[]): CommandResult {
78
+ const result = spawnSync("pnpm", ["exec", "turbo", ...args], {
79
+ cwd,
80
+ encoding: "utf8",
81
+ maxBuffer: maxTurboOutputBytes,
82
+ });
83
+
84
+ return {
85
+ status: result.status ?? 1,
86
+ stdout: result.stdout ?? "",
87
+ stderr: result.stderr ?? "",
88
+ error: result.error,
89
+ };
90
+ }
91
+
92
+ function runVisible(cwd: string, args: readonly string[]): Promise<number> {
93
+ return new Promise((resolveStatus) => {
94
+ const command =
95
+ process.platform === "win32"
96
+ ? "pnpm"
97
+ : resolve(cwd, "node_modules/.bin/turbo");
98
+ const commandArgs =
99
+ process.platform === "win32" ? ["exec", "turbo", ...args] : args;
100
+ const child = spawn(command, commandArgs, {
101
+ cwd,
102
+ detached: process.platform !== "win32",
103
+ stdio: "inherit",
104
+ });
105
+ let interruptedBy: NodeJS.Signals | undefined;
106
+ const forward = (signal: NodeJS.Signals): void => {
107
+ if (child.exitCode === null && child.signalCode === null) {
108
+ interruptedBy = signal;
109
+ const turboSignal = signal === "SIGTERM" ? "SIGINT" : signal;
110
+ if (process.platform !== "win32" && child.pid !== undefined) {
111
+ try {
112
+ process.kill(-child.pid, turboSignal);
113
+ } catch {
114
+ child.kill(turboSignal);
115
+ }
116
+ } else {
117
+ child.kill(turboSignal);
118
+ }
119
+ }
120
+ };
121
+ const forwardInterrupt = (): void => forward("SIGINT");
122
+ const forwardTerminate = (): void => forward("SIGTERM");
123
+ const removeSignalHandlers = (): void => {
124
+ process.off("SIGINT", forwardInterrupt);
125
+ process.off("SIGTERM", forwardTerminate);
126
+ };
127
+
128
+ process.on("SIGINT", forwardInterrupt);
129
+ process.on("SIGTERM", forwardTerminate);
130
+ child.once("error", (error) => {
131
+ removeSignalHandlers();
132
+ report(`无法启动 Turbo 执行 ${args.join(" ")}:${error.message}`);
133
+ resolveStatus(1);
134
+ });
135
+ child.once("close", (code, signal) => {
136
+ removeSignalHandlers();
137
+ if (interruptedBy !== undefined) {
138
+ resolveStatus(interruptedBy === "SIGINT" ? 130 : 143);
139
+ } else if (code !== null) {
140
+ resolveStatus(code);
141
+ } else {
142
+ resolveStatus(
143
+ signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 1,
144
+ );
145
+ }
146
+ });
147
+ });
148
+ }
149
+
150
+ function commandFailure(label: string, result: CommandResult): string {
151
+ const details = result.stderr.trim();
152
+ if (result.error !== undefined) {
153
+ return `无法运行 Turbo ${label}:${result.error.message}`;
154
+ }
155
+ return `Turbo ${label} 失败(退出码 ${String(result.status)})${details === "" ? "" : `:\n${details}`}`;
156
+ }
157
+
158
+ function roleTags(tags: readonly string[]): readonly string[] {
159
+ return tags.filter(
160
+ (tag) => tag.startsWith("quality:") || tag.startsWith("work:"),
161
+ );
162
+ }
163
+
164
+ function publicGateTask(task: {
165
+ readonly name: string;
166
+ readonly packageName: string;
167
+ }): boolean {
168
+ return (
169
+ task.packageName === "//" && publicGateNames.has(task.name as TaskGate)
170
+ );
171
+ }
172
+
173
+ function protectedVerificationTask(task: TurboTask): boolean {
174
+ return (
175
+ task.name === "check" ||
176
+ task.name.startsWith("check:") ||
177
+ task.name === "test" ||
178
+ task.name.startsWith("test:") ||
179
+ task.name === "lint" ||
180
+ task.name === "typecheck" ||
181
+ task.name === "format:check"
182
+ );
183
+ }
184
+
185
+ function assignRole(
186
+ task: Omit<TurboTask, "role">,
187
+ diagnostics: string[],
188
+ ): ScriptRole | undefined {
189
+ const roles = roleTags(task.tags);
190
+ const isPublicGate = publicGateTask(task);
191
+ if (roles.length === 0) {
192
+ diagnostics.push(
193
+ `${task.fullName} 缺少唯一职责标签;请在 Turbo 任务标签中声明质量范围或工作用途`,
194
+ );
195
+ return undefined;
196
+ }
197
+ if (roles.length > 1) {
198
+ diagnostics.push(
199
+ `${task.fullName} 有多个职责标签:${roles.join("、")};每个可执行任务只能声明一个职责`,
200
+ );
201
+ return undefined;
202
+ }
203
+
204
+ const role = roles[0]!;
205
+ if (!validRoles.has(role as ScriptRole)) {
206
+ diagnostics.push(`${task.fullName} 使用未知职责标签 ${role}`);
207
+ return undefined;
208
+ }
209
+ const scriptRole = role as ScriptRole;
210
+ const publicRole = task.name === "fix" ? "work:repair" : "quality:entry";
211
+ if (isPublicGate && scriptRole !== publicRole) {
212
+ diagnostics.push(
213
+ `${task.fullName} 是公共任务入口,职责必须为 ${publicRole},当前为 ${scriptRole}`,
214
+ );
215
+ return undefined;
216
+ }
217
+ if (!isPublicGate && scriptRole === "quality:entry") {
218
+ diagnostics.push(
219
+ `${task.fullName} 不能使用 quality:entry;该职责只允许根 check、check:heavy、check:full`,
220
+ );
221
+ return undefined;
222
+ }
223
+ if (protectedVerificationTask(task) && !scriptRole.startsWith("quality:")) {
224
+ diagnostics.push(
225
+ `${task.fullName} 是保留验收任务,不能归入非验收职责 ${scriptRole}`,
226
+ );
227
+ return undefined;
228
+ }
229
+
230
+ return scriptRole;
231
+ }
232
+
233
+ function readTaskDirectory(
234
+ cwd: string,
235
+ entryCommand: string,
236
+ ): {
237
+ readonly tasks: readonly TurboTask[];
238
+ readonly diagnostics: readonly string[];
239
+ } {
240
+ const result = runCaptured(cwd, ["query", turboTaskQuery]);
241
+ if (result.error !== undefined || result.status !== 0) {
242
+ return {
243
+ diagnostics: [commandFailure("原生任务目录查询", result)],
244
+ tasks: [],
245
+ };
246
+ }
247
+
248
+ let response: unknown;
249
+ try {
250
+ response = JSON.parse(result.stdout) as unknown;
251
+ } catch (error) {
252
+ return {
253
+ diagnostics: [
254
+ `Turbo 原生任务目录不是有效 JSON:${error instanceof Error ? error.message : String(error)}`,
255
+ ],
256
+ tasks: [],
257
+ };
258
+ }
259
+
260
+ const packages =
261
+ isRecord(response) && isRecord(response.data)
262
+ ? response.data.packages
263
+ : undefined;
264
+ const packageItems = isRecord(packages) ? packages.items : undefined;
265
+ if (!Array.isArray(packageItems)) {
266
+ return {
267
+ diagnostics: ["Turbo 原生任务目录缺少 packages.items"],
268
+ tasks: [],
269
+ };
270
+ }
271
+
272
+ const diagnostics: string[] = [];
273
+ const tasks: TurboTask[] = [];
274
+ for (const packageItem of packageItems) {
275
+ if (!isRecord(packageItem) || !isNonEmptyString(packageItem.name)) {
276
+ diagnostics.push("Turbo 原生任务目录包含无法识别的工作区包");
277
+ continue;
278
+ }
279
+ const taskConnection = packageItem.tasks;
280
+ const taskItems = isRecord(taskConnection)
281
+ ? taskConnection.items
282
+ : undefined;
283
+ if (!Array.isArray(taskItems)) {
284
+ diagnostics.push(`${packageItem.name} 的 Turbo 任务目录缺少 tasks.items`);
285
+ continue;
286
+ }
287
+
288
+ for (const taskItem of taskItems) {
289
+ if (!isRecord(taskItem)) {
290
+ diagnostics.push(`${packageItem.name} 的 Turbo 任务目录包含无效任务`);
291
+ continue;
292
+ }
293
+ const { name, fullName, tags, script, command } = taskItem;
294
+ if (
295
+ !isNonEmptyString(name) ||
296
+ !isNonEmptyString(fullName) ||
297
+ !Array.isArray(tags) ||
298
+ !tags.every((tag) => typeof tag === "string")
299
+ ) {
300
+ diagnostics.push(`${packageItem.name} 的 Turbo 任务条目缺少名称或标签`);
301
+ continue;
302
+ }
303
+
304
+ const executable = isNonEmptyString(script) || isNonEmptyString(command);
305
+ const taskWithoutRole = {
306
+ name,
307
+ fullName,
308
+ packageName: packageItem.name,
309
+ tags,
310
+ executable,
311
+ };
312
+ if (publicGateTask(taskWithoutRole)) {
313
+ const expectedCommand = `${entryCommand} ${name}`;
314
+ if (script !== expectedCommand || command !== expectedCommand) {
315
+ diagnostics.push(
316
+ `${fullName} 公共入口接线错误;package.json script 和有效 command 必须为 ${expectedCommand}`,
317
+ );
318
+ }
319
+ }
320
+ if (!executable && roleTags(tags).length === 0) {
321
+ tasks.push(taskWithoutRole);
322
+ continue;
323
+ }
324
+
325
+ const role = assignRole(taskWithoutRole, diagnostics);
326
+ tasks.push(
327
+ role === undefined ? taskWithoutRole : { ...taskWithoutRole, role },
328
+ );
329
+ }
330
+ }
331
+
332
+ for (const name of publicGateNames) {
333
+ if (
334
+ !tasks.some((task) => task.packageName === "//" && task.name === name)
335
+ ) {
336
+ diagnostics.push(`缺少根公共任务入口 ${name}`);
337
+ }
338
+ }
339
+
340
+ return { diagnostics, tasks };
341
+ }
342
+
343
+ function parseDryRun(
344
+ cwd: string,
345
+ tasks: readonly TurboTask[],
346
+ ): {
347
+ readonly taskIds: readonly string[];
348
+ readonly diagnostics: readonly string[];
349
+ } {
350
+ if (tasks.length === 0) {
351
+ return { diagnostics: [], taskIds: [] };
352
+ }
353
+ const result = runCaptured(cwd, [
354
+ "run",
355
+ ...tasks.map((task) => task.fullName),
356
+ "--dry=json",
357
+ ]);
358
+ if (result.error !== undefined || result.status !== 0) {
359
+ return {
360
+ diagnostics: [commandFailure("执行图预览", result)],
361
+ taskIds: [],
362
+ };
363
+ }
364
+
365
+ let response: unknown;
366
+ try {
367
+ response = JSON.parse(result.stdout) as unknown;
368
+ } catch (error) {
369
+ return {
370
+ diagnostics: [
371
+ `Turbo 执行图预览不是有效 JSON:${error instanceof Error ? error.message : String(error)}`,
372
+ ],
373
+ taskIds: [],
374
+ };
375
+ }
376
+ if (!isRecord(response) || !Array.isArray(response.tasks)) {
377
+ return {
378
+ diagnostics: ["Turbo 执行图预览缺少 tasks"],
379
+ taskIds: [],
380
+ };
381
+ }
382
+
383
+ const taskIds: string[] = [];
384
+ const diagnostics: string[] = [];
385
+ for (const task of response.tasks) {
386
+ if (!isRecord(task) || !isNonEmptyString(task.taskId)) {
387
+ diagnostics.push("Turbo 执行图预览包含无法识别的任务标识");
388
+ continue;
389
+ }
390
+ taskIds.push(task.taskId);
391
+ }
392
+
393
+ return { diagnostics, taskIds };
394
+ }
395
+
396
+ function validateLayerClosure(
397
+ layer: GateLayer,
398
+ taskIds: readonly string[],
399
+ tasksByName: ReadonlyMap<string, TurboTask>,
400
+ ): readonly string[] {
401
+ const diagnostics: string[] = [];
402
+ for (const taskId of taskIds) {
403
+ const dependency = tasksByName.get(taskId);
404
+ if (dependency === undefined) {
405
+ diagnostics.push(
406
+ `${layer.label} 执行图包含任务目录中不存在的任务 ${taskId}`,
407
+ );
408
+ continue;
409
+ }
410
+ if (publicGateTask(dependency)) {
411
+ diagnostics.push(
412
+ `${layer.label} 执行图包含 ${dependency.fullName}:叶任务不能依赖公共聚合入口`,
413
+ );
414
+ } else if (
415
+ dependency.role !== undefined &&
416
+ dependency.role !== layer.role &&
417
+ dependency.role !== "work:preparation" &&
418
+ !(layer.role !== "quality:light" && dependency.role === "quality:light")
419
+ ) {
420
+ const scope =
421
+ layer.role === "quality:light" && dependency.role === "quality:heavy"
422
+ ? "轻检查不能间接启动重验收"
423
+ : dependency.role === "quality:conditional"
424
+ ? "常规验收不能间接启动条件验收"
425
+ : `不允许间接启动职责 ${dependency.role}`;
426
+ diagnostics.push(
427
+ `${layer.label} 执行图包含 ${dependency.fullName}:${scope}`,
428
+ );
429
+ }
430
+ }
431
+
432
+ return diagnostics;
433
+ }
434
+
435
+ function layerFor(role: GateLayer["role"]): GateLayer {
436
+ return {
437
+ role,
438
+ label:
439
+ role === "quality:light"
440
+ ? "轻检查"
441
+ : role === "quality:heavy"
442
+ ? "重验收"
443
+ : "修复",
444
+ };
445
+ }
446
+
447
+ function layersFor(gate: TaskGate): readonly GateLayer[] {
448
+ if (gate === "check") {
449
+ return [layerFor("quality:light")];
450
+ }
451
+ if (gate === "check:heavy") {
452
+ return [layerFor("quality:heavy")];
453
+ }
454
+ if (gate === "fix") {
455
+ return [layerFor("work:repair")];
456
+ }
457
+
458
+ return [layerFor("quality:light"), layerFor("quality:heavy")];
459
+ }
460
+
461
+ export async function runTaskGate(
462
+ gate: TaskGate,
463
+ cwd = process.cwd(),
464
+ entryCommand = "node --conditions=source scripts/task-gates.ts",
465
+ ): Promise<number> {
466
+ if (!publicGateNames.has(gate)) {
467
+ report(`未知公共任务入口:${String(gate)}`);
468
+ return 1;
469
+ }
470
+
471
+ const directory = readTaskDirectory(cwd, entryCommand);
472
+ if (directory.diagnostics.length > 0) {
473
+ report(directory.diagnostics.join("\n"));
474
+ return 1;
475
+ }
476
+ const tasksByName = new Map(
477
+ directory.tasks.map((task) => [task.fullName, task]),
478
+ );
479
+ const layers = layersFor(gate);
480
+ const plans: {
481
+ readonly layer: GateLayer;
482
+ readonly tasks: readonly TurboTask[];
483
+ }[] = [];
484
+ const diagnostics: string[] = [];
485
+
486
+ for (const task of directory.tasks) {
487
+ if (task.executable || task.role === undefined) {
488
+ continue;
489
+ }
490
+ const dryRun = parseDryRun(cwd, [task]);
491
+ diagnostics.push(...dryRun.diagnostics);
492
+ if (
493
+ dryRun.diagnostics.length === 0 &&
494
+ !dryRun.taskIds.some(
495
+ (taskId) => tasksByName.get(taskId)?.executable === true,
496
+ )
497
+ ) {
498
+ diagnostics.push(
499
+ `${task.fullName} 声明悬空职责标签 ${task.role}:无命令节点的原生执行图没有可执行工作`,
500
+ );
501
+ }
502
+ }
503
+
504
+ for (const layer of layers) {
505
+ const selected = directory.tasks.filter(
506
+ (task) => task.role === layer.role && !publicGateTask(task),
507
+ );
508
+ const dryRun = parseDryRun(cwd, selected);
509
+ diagnostics.push(...dryRun.diagnostics);
510
+ diagnostics.push(
511
+ ...validateLayerClosure(layer, dryRun.taskIds, tasksByName),
512
+ );
513
+ if (
514
+ layer.role === "quality:light" &&
515
+ !dryRun.taskIds.some(
516
+ (taskId) => tasksByName.get(taskId)?.executable === true,
517
+ )
518
+ ) {
519
+ diagnostics.push(
520
+ "轻检查没有选中有效任务;请为实际验收任务声明 quality:light",
521
+ );
522
+ }
523
+ plans.push({ layer, tasks: selected });
524
+ }
525
+
526
+ if (diagnostics.length > 0) {
527
+ report(diagnostics.join("\n"));
528
+ return 1;
529
+ }
530
+
531
+ const conditionalTasks = directory.tasks.filter(
532
+ (task) => task.role === "quality:conditional",
533
+ );
534
+ if (gate !== "fix" && conditionalTasks.length > 0) {
535
+ report(
536
+ `条件验收未执行:${conditionalTasks.map((task) => task.fullName).join("、")}`,
537
+ );
538
+ }
539
+
540
+ for (const plan of plans) {
541
+ if (plan.tasks.length === 0) {
542
+ report(`${plan.layer.label}没有选中任务,跳过执行`);
543
+ continue;
544
+ }
545
+ const status = await runVisible(cwd, [
546
+ "run",
547
+ ...plan.tasks.map((task) => task.fullName),
548
+ "--continue=dependencies-successful",
549
+ gate === "fix" ? "--output-logs=full" : "--output-logs=errors-only",
550
+ "--log-order=grouped",
551
+ "--log-prefix=task",
552
+ ]);
553
+ if (status !== 0) {
554
+ return status;
555
+ }
556
+ }
557
+
558
+ return 0;
559
+ }
560
+
561
+ const mainFile = process.argv[1];
562
+ if (
563
+ mainFile !== undefined &&
564
+ pathToFileURL(resolve(mainFile)).href === import.meta.url
565
+ ) {
566
+ const gate = process.argv[2];
567
+ if (
568
+ gate !== "check" &&
569
+ gate !== "check:heavy" &&
570
+ gate !== "check:full" &&
571
+ gate !== "fix"
572
+ ) {
573
+ report(
574
+ "用法:node scripts/task-gates.ts <check|check:heavy|check:full|fix>",
575
+ );
576
+ process.exitCode = 2;
577
+ } else {
578
+ void runTaskGate(gate)
579
+ .then((status) => {
580
+ process.exitCode = status;
581
+ })
582
+ .catch((error: unknown) => {
583
+ report(
584
+ `公共任务入口失败:${error instanceof Error ? error.message : String(error)}`,
585
+ );
586
+ process.exitCode = 1;
587
+ });
588
+ }
589
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "tasks": {
3
+ "//#dev": { "tags": ["work:development"] },
4
+ "//#database:prepare:dev": {
5
+ "cache": false,
6
+ "tags": ["work:preparation"]
7
+ },
8
+ "//#database:prepare:test": {
9
+ "cache": false,
10
+ "tags": ["work:preparation"]
11
+ },
12
+ "//#database:prepare:e2e": {
13
+ "cache": false,
14
+ "tags": ["work:preparation"]
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "tasks": {
3
+ "//#deployment": {
4
+ "dependsOn": [
5
+ "{{APPLICATION_PACKAGE}}#build",
6
+ "{{DATABASE_PACKAGE}}#build",
7
+ "{{MIGRATION_PACKAGE}}#build"
8
+ ],
9
+ "cache": false,
10
+ "tags": ["quality:heavy"]
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "tasks": {
3
+ "//#toolchain:prepare": { "cache": false, "tags": ["work:preparation"] }
4
+ }
5
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "tasks": {
3
+ "//#test:e2e": {
4
+ "dependsOn": [
5
+ "{{API_PACKAGE}}#build",
6
+ "{{WEB_PACKAGE}}#build",
7
+ "{{WEB_PACKAGE}}#browser:prepare"
8
+ ],
9
+ "cache": false,
10
+ "tags": ["quality:heavy"]
11
+ }
12
+ }
13
+ }
@@ -2,20 +2,19 @@
2
2
  "boundaries": { "tags": {} },
3
3
  "globalPassThroughEnv": ["CARGO_HOME", "RUSTUP_HOME", "RUSTUP_TOOLCHAIN"],
4
4
  "tasks": {
5
- "//#format:check": { "outputs": [] },
6
- "//#lint": { "outputs": [] },
7
- "//#format:write": { "dependsOn": ["//#lint:fix"], "cache": false },
8
- "//#lint:fix": { "cache": false },
9
- "//#toolchain:prepare": { "cache": false },
10
- "//#typecheck": { "outputs": [] },
11
- "format:check": { "outputs": [] },
12
- "lint": { "outputs": [] },
13
- "typecheck": { "dependsOn": ["^typecheck"], "outputs": [] },
14
- "build": { "dependsOn": ["^build"] },
15
- "test": { "outputs": [] },
16
- "test:e2e": { "dependsOn": ["build"], "cache": false },
17
- "deployment": { "cache": false },
18
- "format:write": { "dependsOn": ["lint:fix"], "cache": false },
19
- "lint:fix": { "cache": false }
5
+ "//#check": { "tags": ["quality:entry"] },
6
+ "//#check:heavy": { "tags": ["quality:entry"] },
7
+ "//#check:full": { "tags": ["quality:entry"] },
8
+ "//#boundaries": { "tags": ["quality:light"] },
9
+ "//#fix": { "tags": ["work:repair"] },
10
+ "//#format:check": { "outputs": [], "tags": ["quality:light"] },
11
+ "//#lint": { "outputs": [], "tags": ["quality:light"] },
12
+ "//#format:write": {
13
+ "dependsOn": ["//#lint:fix"],
14
+ "cache": false,
15
+ "tags": ["work:repair"]
16
+ },
17
+ "//#lint:fix": { "cache": false, "tags": ["work:repair"] },
18
+ "//#typecheck": { "outputs": [], "tags": ["quality:light"] }
20
19
  }
21
20
  }