@ykdz/template 0.0.5 → 0.0.6

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/dist/src/cli.js CHANGED
@@ -2,11 +2,13 @@
2
2
  import { readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { createInterface } from "node:readline/promises";
5
- import { findBuiltInPresetProjection, projectPresetThroughRegistry, } from "../templates/registry.js";
6
5
  import { blueprintJsonSchema, builtInPresets, presetFileJsonSchema, validatePresetFile, validateProjectBlueprint, } from "./declarations.js";
7
6
  import { assembleGenerationContext, } from "./generation-context.js";
8
7
  import { addPackage } from "./package-addition.js";
9
8
  import { planNextStepInstructionsForProjection, } from "./preset-projection.js";
9
+ import { findBuiltInPresetSourceManifestPreset, presetSourceManifestJsonSchema, validateBuiltInPresetSourceManifest, validatePresetSourceManifest, } from "./preset-source.js";
10
+ import { blueprintForPresetSourcePreset, projectPresetSourcePreset, } from "./projection-capabilities.js";
11
+ import { renderNewProject } from "./renderer.js";
10
12
  import { resolveToolchainVersions, } from "./toolchain-resolution.js";
11
13
  function usage() {
12
14
  return [
@@ -15,8 +17,10 @@ function usage() {
15
17
  " template add package --preset <name> --name <name> [--path <package-path>] [--link-from <package-path>]...",
16
18
  " template presets",
17
19
  " template schema preset",
20
+ " template schema preset-source",
18
21
  " template schema blueprint",
19
22
  " template preset validate <path>",
23
+ " template preset-source validate <path>",
20
24
  " template blueprint validate <path>",
21
25
  "",
22
26
  "Options:",
@@ -135,23 +139,10 @@ function formatList(values) {
135
139
  }
136
140
  function blueprintForInit(options) {
137
141
  const preset = supportedPreset(options.preset);
138
- const projection = findBuiltInPresetProjection(preset.name);
139
- if (projection) {
140
- return projection.blueprint({
141
- targetDir: options.dir,
142
- scope: options.scope,
143
- });
144
- }
145
- const blueprint = {
146
- schemaVersion: 1,
147
- preset: preset.name,
148
- projectKind: preset.supportedProjectKinds[0],
149
- features: [...preset.features],
150
- };
151
- if (preset.supportedPackageManagers[0]) {
152
- blueprint.packageManager = preset.supportedPackageManagers[0];
153
- }
154
- return blueprint;
142
+ return blueprintForPresetSourcePreset(preset, {
143
+ targetDir: options.dir,
144
+ scope: options.scope,
145
+ });
155
146
  }
156
147
  function formatBlueprintSummary(targetDir, blueprint) {
157
148
  const lines = [
@@ -207,7 +198,8 @@ function toolchainResolutionSourceFromEnv() {
207
198
  return undefined;
208
199
  }
209
200
  async function generationContextForInit(options, blueprint) {
210
- if (!findBuiltInPresetProjection(options.preset)) {
201
+ if (findBuiltInPresetSourceManifestPreset(options.preset)?.projection ===
202
+ undefined) {
211
203
  return undefined;
212
204
  }
213
205
  return assembleGenerationContext({
@@ -221,20 +213,28 @@ async function generationContextForInit(options, blueprint) {
221
213
  });
222
214
  }
223
215
  async function generateInitProject(options, generationContext) {
224
- const projection = findBuiltInPresetProjection(options.preset);
225
- if (projection) {
226
- if (!generationContext) {
227
- throw new Error(`Missing Generation Context for Preset Projection: ${options.preset}`);
228
- }
229
- const plan = projection.project(generationContext);
230
- await projection.render({ targetDir: options.dir, plan });
231
- return;
216
+ const preset = findBuiltInPresetSourceManifestPreset(options.preset);
217
+ if (!preset?.projection) {
218
+ throw new Error(formatSupportedPresetError());
219
+ }
220
+ if (!generationContext) {
221
+ throw new Error(`Missing Generation Context for Preset Projection: ${options.preset}`);
232
222
  }
233
- throw new Error(formatSupportedPresetError());
223
+ const plan = projectPresetSourcePreset({
224
+ preset,
225
+ context: generationContext,
226
+ });
227
+ await renderNewProject({
228
+ sourceRoot: plan.sourceRoot,
229
+ sourceRoots: plan.sourceRoots,
230
+ targetRoot: options.dir,
231
+ operations: [...plan.operations],
232
+ });
234
233
  }
235
234
  function printInitComplete(options, blueprint, generationContext) {
236
- const projectionPlan = generationContext
237
- ? projectPresetThroughRegistry(generationContext)
235
+ const preset = findBuiltInPresetSourceManifestPreset(blueprint.preset);
236
+ const projectionPlan = generationContext && preset?.projection
237
+ ? projectPresetSourcePreset({ preset, context: generationContext })
238
238
  : undefined;
239
239
  if (!projectionPlan) {
240
240
  throw new Error(`Missing Preset Projection plan: ${blueprint.preset}`);
@@ -349,7 +349,11 @@ async function main(args) {
349
349
  printJson(blueprintJsonSchema);
350
350
  return;
351
351
  }
352
- throw new Error("schema requires preset or blueprint");
352
+ if (schemaName === "preset-source") {
353
+ printJson(presetSourceManifestJsonSchema);
354
+ return;
355
+ }
356
+ throw new Error("schema requires preset, preset-source, or blueprint");
353
357
  }
354
358
  if (command === "preset" && args[1] === "validate") {
355
359
  const filePath = args[2];
@@ -363,6 +367,32 @@ async function main(args) {
363
367
  console.log(`Preset file is valid: ${result.value.name}`);
364
368
  return;
365
369
  }
370
+ if (command === "preset-source" && args[1] === "validate") {
371
+ const filePath = args[2];
372
+ if (!filePath) {
373
+ throw new Error("preset-source validate requires a path");
374
+ }
375
+ const declaration = await readJsonDeclaration(filePath);
376
+ const sourceName = typeof declaration === "object" &&
377
+ declaration !== null &&
378
+ !Array.isArray(declaration) &&
379
+ "name" in declaration &&
380
+ typeof declaration.name === "string"
381
+ ? declaration.name
382
+ : undefined;
383
+ const result = sourceName === "built-in"
384
+ ? validateBuiltInPresetSourceManifest(declaration, {
385
+ sourceRoot: path.dirname(filePath),
386
+ })
387
+ : validatePresetSourceManifest(declaration, {
388
+ sourceRoot: path.dirname(filePath),
389
+ });
390
+ if (!result.ok) {
391
+ throw new Error(`Preset Source Manifest is invalid:\n${formatValidationIssues(result.issues)}`);
392
+ }
393
+ console.log(`Preset Source Manifest is valid: ${result.value.name} (${result.value.presets.map((preset) => preset.name).join(", ")})`);
394
+ return;
395
+ }
366
396
  if (command === "blueprint" && args[1] === "validate") {
367
397
  const filePath = args[2];
368
398
  if (!filePath) {
@@ -380,8 +410,11 @@ async function main(args) {
380
410
  const blueprint = blueprintForInit(options);
381
411
  if (options.dryRun) {
382
412
  const generationContext = await generationContextForInit(options, blueprint);
413
+ const preset = findBuiltInPresetSourceManifestPreset(options.preset);
383
414
  const projectionPlan = generationContext
384
- ? projectPresetThroughRegistry(generationContext)
415
+ ? preset?.projection
416
+ ? projectPresetSourcePreset({ preset, context: generationContext })
417
+ : undefined
385
418
  : undefined;
386
419
  if (!projectionPlan) {
387
420
  throw new Error(`Missing Preset Projection plan: ${blueprint.preset}`);
@@ -1,6 +1,6 @@
1
1
  import * as v from "valibot";
2
- import { builtInPresetMetadata } from "../templates/registry.js";
3
- export const builtInPresets = builtInPresetMetadata;
2
+ import { loadBuiltInPresetSourceManifest } from "./preset-source.js";
3
+ export const builtInPresets = loadBuiltInPresetSourceManifest().presets;
4
4
  const featureNames = [
5
5
  "pnpm-catalog",
6
6
  "oxc-format-lint",
@@ -0,0 +1,62 @@
1
+ import type { PresetSourceManifest } from "./preset-source.js";
2
+ export type GeneratedScenarioSet = "init" | "package-addition-matrix";
3
+ export type GeneratedScenario = {
4
+ readonly set: GeneratedScenarioSet;
5
+ readonly basePreset: string;
6
+ readonly addedPreset?: string;
7
+ readonly linkFrom?: readonly string[];
8
+ readonly id: string;
9
+ readonly label: string;
10
+ };
11
+ export type SkippedGeneratedScenario = {
12
+ readonly set: GeneratedScenarioSet;
13
+ readonly basePreset: string;
14
+ readonly addedPreset: string;
15
+ readonly id: string;
16
+ readonly label: string;
17
+ readonly reason: string;
18
+ };
19
+ export type GeneratedScenarioSelection = {
20
+ readonly runnable: readonly GeneratedScenario[];
21
+ readonly skipped: readonly SkippedGeneratedScenario[];
22
+ };
23
+ export type GeneratedScenarioCommandStep = {
24
+ readonly id: string;
25
+ readonly command: string;
26
+ readonly args: readonly string[];
27
+ readonly cwd: string;
28
+ readonly display: string;
29
+ };
30
+ export type GeneratedScenarioCommandRunner = (command: string, args: readonly string[], cwd: string, options?: {
31
+ readonly env?: Record<string, string>;
32
+ readonly logPrefix?: string;
33
+ }) => Promise<void>;
34
+ export type GeneratedScenarioReporter = {
35
+ readonly info?: (message: string) => void;
36
+ readonly error?: (message: string) => void;
37
+ };
38
+ export type GeneratedScenarioRunnerOptions = {
39
+ readonly repoRoot: string;
40
+ readonly cliPath: string;
41
+ readonly runCommand?: GeneratedScenarioCommandRunner;
42
+ readonly reporter?: GeneratedScenarioReporter;
43
+ readonly deterministicToolchainEnv?: Record<string, string>;
44
+ readonly rootCheckLock?: ExclusiveLock;
45
+ };
46
+ export type ExclusiveLock = {
47
+ readonly run: <T>(callback: () => Promise<T>) => Promise<T>;
48
+ };
49
+ export declare function createExclusiveLock(): ExclusiveLock;
50
+ export declare function generatedScenarioId(scenario: Pick<GeneratedScenario, "basePreset" | "addedPreset" | "linkFrom">): string;
51
+ export declare function generatedScenarioLabel(scenario: Pick<GeneratedScenario, "basePreset" | "addedPreset" | "linkFrom">): string;
52
+ export declare function selectGeneratedScenarios(manifest: PresetSourceManifest, set: GeneratedScenarioSet): GeneratedScenarioSelection;
53
+ export declare function packageLeafNameForAddedPreset(manifest: PresetSourceManifest, presetName: string): string;
54
+ export declare function defaultGeneratedScenarioCommandRunner(command: string, args: readonly string[], cwd: string, options?: {
55
+ readonly env?: Record<string, string>;
56
+ readonly logPrefix?: string;
57
+ }): Promise<void>;
58
+ export declare function generatedScenarioQualityGateSteps(manifest: PresetSourceManifest, scenario: GeneratedScenario, projectDir: string, addedPackagePath: string | undefined, options: GeneratedScenarioRunnerOptions): GeneratedScenarioCommandStep[];
59
+ export declare function fixtureConcurrency(scenarioCount: number): number;
60
+ export declare function errorForFailedGeneratedScenario(scenario: GeneratedScenario, error: unknown): Error;
61
+ export declare function runGeneratedScenariosConcurrently(manifest: PresetSourceManifest, scenarios: readonly GeneratedScenario[], workspace: string, concurrency: number, options: GeneratedScenarioRunnerOptions): Promise<void>;
62
+ export declare function runGeneratedScenarioSet(manifest: PresetSourceManifest, set: GeneratedScenarioSet, workspace: string, options: GeneratedScenarioRunnerOptions): Promise<GeneratedScenarioSelection>;
@@ -0,0 +1,361 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { execa } from "execa";
4
+ import { assembleGenerationContext } from "./generation-context.js";
5
+ import { renderPlaywrightBrowserInstallCommand, } from "./module-graph.js";
6
+ import { planNextStepInstructions } from "./next-step-instructions.js";
7
+ import { findPresetSourceManifestPreset } from "./preset-source.js";
8
+ import { blueprintForPresetSourcePreset, projectPresetSourcePreset, } from "./projection-capabilities.js";
9
+ const defaultDeterministicToolchainEnv = {
10
+ TEMPLATE_TOOLCHAIN_RESOLUTION: "bundled-fallback",
11
+ };
12
+ const defaultFixtureConcurrency = 2;
13
+ export function createExclusiveLock() {
14
+ let tail = Promise.resolve();
15
+ return {
16
+ async run(callback) {
17
+ const previous = tail;
18
+ let release;
19
+ tail = new Promise((resolve) => {
20
+ release = resolve;
21
+ });
22
+ await previous;
23
+ try {
24
+ return await callback();
25
+ }
26
+ finally {
27
+ release();
28
+ }
29
+ },
30
+ };
31
+ }
32
+ export function generatedScenarioId(scenario) {
33
+ if (!scenario.addedPreset) {
34
+ return scenario.basePreset;
35
+ }
36
+ if (scenario.linkFrom && scenario.linkFrom.length > 0) {
37
+ const linkFromId = scenario.linkFrom
38
+ .map((packagePath) => packagePath.replaceAll("/", "-"))
39
+ .join("-");
40
+ return `${scenario.basePreset}-add-${scenario.addedPreset}-link-from-${linkFromId}`;
41
+ }
42
+ return `${scenario.basePreset}-add-${scenario.addedPreset}`;
43
+ }
44
+ export function generatedScenarioLabel(scenario) {
45
+ if (!scenario.addedPreset) {
46
+ return scenario.basePreset;
47
+ }
48
+ if (scenario.linkFrom && scenario.linkFrom.length > 0) {
49
+ return `${scenario.basePreset} + ${scenario.addedPreset} linked from ${scenario.linkFrom.join(", ")}`;
50
+ }
51
+ return `${scenario.basePreset} + ${scenario.addedPreset}`;
52
+ }
53
+ function scenario(set, input) {
54
+ return {
55
+ set,
56
+ basePreset: input.basePreset,
57
+ addedPreset: input.addedPreset,
58
+ linkFrom: input.linkFrom ? [...input.linkFrom] : undefined,
59
+ id: generatedScenarioId(input),
60
+ label: generatedScenarioLabel(input),
61
+ };
62
+ }
63
+ export function selectGeneratedScenarios(manifest, set) {
64
+ const contract = manifest.fixtureMatrix;
65
+ if (!contract) {
66
+ throw new Error(`Preset Source ${manifest.name} does not declare a Fixture Matrix Contract`);
67
+ }
68
+ const initScenarios = contract.initSupport.map((support) => scenario(set, { basePreset: support.preset }));
69
+ if (set === "init") {
70
+ return { runnable: initScenarios, skipped: [] };
71
+ }
72
+ const matrixScenarios = contract.supportedCombinations.map((combination) => scenario(set, {
73
+ basePreset: combination.basePreset,
74
+ addedPreset: combination.addedPreset,
75
+ linkFrom: combination.linkFrom,
76
+ }));
77
+ const skipped = contract.semanticSkips.map((skip) => ({
78
+ set,
79
+ basePreset: skip.basePreset,
80
+ addedPreset: skip.addedPreset,
81
+ id: generatedScenarioId(skip),
82
+ label: generatedScenarioLabel(skip),
83
+ reason: skip.reason,
84
+ }));
85
+ return {
86
+ runnable: matrixScenarios,
87
+ skipped,
88
+ };
89
+ }
90
+ export function packageLeafNameForAddedPreset(manifest, presetName) {
91
+ const support = manifest.fixtureMatrix?.packageAdditionSupport.find((entry) => entry.preset === presetName);
92
+ if (!support) {
93
+ throw new Error(`Missing fixture Package Addition leaf name for preset: ${presetName}`);
94
+ }
95
+ return support.packageLeafName;
96
+ }
97
+ export async function defaultGeneratedScenarioCommandRunner(command, args, cwd, options = {}) {
98
+ const prefix = options.logPrefix ? `[${options.logPrefix}] ` : "";
99
+ console.log(`${prefix}$ ${[command, ...args].join(" ")}`);
100
+ await execa(command, [...args], {
101
+ cwd,
102
+ env: options.env,
103
+ stdio: "inherit",
104
+ });
105
+ }
106
+ async function generateScenario(scenario, workspace, options) {
107
+ const projectDir = path.join(workspace, `fixture-${scenario.id}`);
108
+ await options.runCommand("pnpm", [
109
+ "exec",
110
+ "tsx",
111
+ options.cliPath,
112
+ "init",
113
+ projectDir,
114
+ "--preset",
115
+ scenario.basePreset,
116
+ "--yes",
117
+ ], options.repoRoot, {
118
+ env: options.deterministicToolchainEnv,
119
+ logPrefix: scenario.label,
120
+ });
121
+ return projectDir;
122
+ }
123
+ async function applyPackageAddition(manifest, scenario, projectDir, options) {
124
+ if (!scenario.addedPreset) {
125
+ return undefined;
126
+ }
127
+ const packageLeafName = packageLeafNameForAddedPreset(manifest, scenario.addedPreset);
128
+ const beforeConsumerSources = await linkedConsumerSourceSnapshots(projectDir, scenario.linkFrom ?? []);
129
+ await options.runCommand("pnpm", [
130
+ "exec",
131
+ "tsx",
132
+ options.cliPath,
133
+ "add",
134
+ "package",
135
+ "--preset",
136
+ scenario.addedPreset,
137
+ "--name",
138
+ packageLeafName,
139
+ ...linkFromArgs(scenario.linkFrom ?? []),
140
+ ], projectDir, { logPrefix: scenario.label });
141
+ await assertLinkedConsumerSourcesUnchanged(projectDir, beforeConsumerSources);
142
+ return readAddedPackagePath(projectDir, packageLeafName);
143
+ }
144
+ function linkFromArgs(packagePaths) {
145
+ return packagePaths.flatMap((packagePath) => ["--link-from", packagePath]);
146
+ }
147
+ async function linkedConsumerSourceSnapshots(projectDir, packagePaths) {
148
+ const snapshots = new Map();
149
+ for (const packagePath of packagePaths) {
150
+ snapshots.set(packagePath, await sourceFileSnapshot(path.join(projectDir, packagePath, "src")));
151
+ }
152
+ return snapshots;
153
+ }
154
+ async function sourceFileSnapshot(sourceDir, currentDir = sourceDir) {
155
+ const snapshot = {};
156
+ const entries = await readdir(currentDir, { withFileTypes: true });
157
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
158
+ const entryPath = path.join(currentDir, entry.name);
159
+ if (entry.isDirectory()) {
160
+ Object.assign(snapshot, await sourceFileSnapshot(sourceDir, entryPath));
161
+ continue;
162
+ }
163
+ if (entry.isFile()) {
164
+ snapshot[path.relative(sourceDir, entryPath)] = await readFile(entryPath, "utf8");
165
+ }
166
+ }
167
+ return snapshot;
168
+ }
169
+ async function assertLinkedConsumerSourcesUnchanged(projectDir, beforeSnapshots) {
170
+ for (const [packagePath, beforeSnapshot] of beforeSnapshots) {
171
+ const afterSnapshot = await sourceFileSnapshot(path.join(projectDir, packagePath, "src"));
172
+ if (JSON.stringify(afterSnapshot) !== JSON.stringify(beforeSnapshot)) {
173
+ throw new Error(`Package Link Intent fixture modified consumer source files for ${packagePath}`);
174
+ }
175
+ }
176
+ }
177
+ async function readAddedPackagePath(projectDir, packageLeafName) {
178
+ const blueprint = JSON.parse(await readFile(path.join(projectDir, ".template", "blueprint.json"), "utf8"));
179
+ const packageDefinition = blueprint.packages?.find((pkg) => path.basename(pkg.path) === packageLeafName);
180
+ if (!packageDefinition) {
181
+ throw new Error(`Package Addition fixture did not record package leaf ${packageLeafName}`);
182
+ }
183
+ return packageDefinition.path;
184
+ }
185
+ function projectionPlanForPreset(presetName, projectDir, manifest, options, packageScope) {
186
+ const preset = findPresetSourceManifestPreset(manifest, presetName);
187
+ if (!preset?.projection) {
188
+ throw new Error(`Missing Preset Projection for fixture preset ${presetName}`);
189
+ }
190
+ const blueprint = blueprintForPresetSourcePreset(preset, {
191
+ targetDir: projectDir,
192
+ scope: packageScope,
193
+ });
194
+ return projectPresetSourcePreset({
195
+ preset,
196
+ context: assembleGenerationContext({
197
+ targetDir: projectDir,
198
+ blueprint,
199
+ toolchain: {
200
+ nodeLtsMajor: { kind: "NodeLtsMajor", value: "24" },
201
+ packageManagerPin: { kind: "PackageManagerPin", value: "pnpm@10.0.0" },
202
+ source: "bundled-fallback",
203
+ diagnostics: [],
204
+ },
205
+ }),
206
+ });
207
+ }
208
+ function machineVerifiableNextStepsForPreset(presetName, projectDir, manifest, options) {
209
+ const projectionPlan = projectionPlanForPreset(presetName, projectDir, manifest, options);
210
+ const plan = planNextStepInstructions({
211
+ targetDir: projectDir,
212
+ projectionPlan,
213
+ });
214
+ return plan.steps
215
+ .filter((step) => step.machineVerifiable && step.kind === "command")
216
+ .map((step) => ({
217
+ id: step.id,
218
+ command: step.command,
219
+ args: step.args,
220
+ cwd: step.cwd,
221
+ display: step.display,
222
+ }));
223
+ }
224
+ function addedPresetEnvironmentNeeds(manifest, scenario, projectDir, addedPackagePath, options) {
225
+ if (!scenario.addedPreset || !addedPackagePath) {
226
+ return [];
227
+ }
228
+ return projectionPlanForPreset(scenario.addedPreset, projectDir, manifest, options, path.basename(projectDir)).checkPlan.environmentNeeds.map((need) => {
229
+ if (need.owner.kind !== "package-boundary") {
230
+ return need;
231
+ }
232
+ return {
233
+ ...need,
234
+ owner: { kind: "package-boundary", path: addedPackagePath },
235
+ };
236
+ });
237
+ }
238
+ function environmentNeedStep(need, projectDir) {
239
+ const display = renderPlaywrightBrowserInstallCommand(need);
240
+ const [command, ...args] = display.split(" ");
241
+ return {
242
+ id: `install-${need.owner.path}-playwright-browsers`,
243
+ command,
244
+ args,
245
+ cwd: projectDir,
246
+ display,
247
+ };
248
+ }
249
+ export function generatedScenarioQualityGateSteps(manifest, scenario, projectDir, addedPackagePath, options) {
250
+ const normalizedOptions = normalizeRunnerOptions(options);
251
+ const steps = machineVerifiableNextStepsForPreset(scenario.basePreset, projectDir, manifest, normalizedOptions);
252
+ const addedEnvironmentSteps = addedPresetEnvironmentNeeds(manifest, scenario, projectDir, addedPackagePath, normalizedOptions).map((need) => environmentNeedStep(need, projectDir));
253
+ const existingDisplays = new Set(steps.map((step) => step.display));
254
+ const extraEnvironmentSteps = addedEnvironmentSteps.filter((step) => !existingDisplays.has(step.display));
255
+ if (extraEnvironmentSteps.length === 0) {
256
+ return steps;
257
+ }
258
+ const rootCheckIndex = steps.findIndex((step) => step.id === "run-root-check");
259
+ if (rootCheckIndex === -1) {
260
+ return [...steps, ...extraEnvironmentSteps];
261
+ }
262
+ return [
263
+ ...steps.slice(0, rootCheckIndex),
264
+ ...extraEnvironmentSteps,
265
+ ...steps.slice(rootCheckIndex),
266
+ ];
267
+ }
268
+ async function runGeneratedScenarioQualityGate(manifest, scenario, projectDir, addedPackagePath, options) {
269
+ const steps = generatedScenarioQualityGateSteps(manifest, scenario, projectDir, addedPackagePath, options);
270
+ const requiresSerializedPlaywrightRootCheck = steps.some((step) => step.id.endsWith("-playwright-browsers"));
271
+ for (const step of steps) {
272
+ const env = step.id === "run-root-check" ? { CI: "1" } : undefined;
273
+ const runStep = async () => {
274
+ await options.runCommand(step.command, [...step.args], step.cwd, {
275
+ env,
276
+ logPrefix: scenario.label,
277
+ });
278
+ };
279
+ if (step.id === "run-root-check" && requiresSerializedPlaywrightRootCheck) {
280
+ await options.rootCheckLock.run(runStep);
281
+ continue;
282
+ }
283
+ await runStep();
284
+ }
285
+ }
286
+ async function checkGeneratedScenario(manifest, scenario, workspace, options) {
287
+ options.reporter.info?.(`\n== ${scenario.label} ==`);
288
+ const projectDir = await generateScenario(scenario, workspace, options);
289
+ const addedPackagePath = await applyPackageAddition(manifest, scenario, projectDir, options);
290
+ await runGeneratedScenarioQualityGate(manifest, scenario, projectDir, addedPackagePath, options);
291
+ }
292
+ export function fixtureConcurrency(scenarioCount) {
293
+ const rawValue = process.env.TEMPLATE_FIXTURE_CONCURRENCY;
294
+ if (rawValue === undefined || rawValue === "") {
295
+ return Math.min(defaultFixtureConcurrency, scenarioCount);
296
+ }
297
+ const value = Number(rawValue);
298
+ if (!Number.isInteger(value) || value < 1) {
299
+ throw new Error("TEMPLATE_FIXTURE_CONCURRENCY must be a positive integer");
300
+ }
301
+ return Math.min(value, scenarioCount);
302
+ }
303
+ export function errorForFailedGeneratedScenario(scenario, error) {
304
+ if (error instanceof Error) {
305
+ return new Error(`Fixture scenario failed: ${scenario.label}`, {
306
+ cause: error,
307
+ });
308
+ }
309
+ return new Error(`Fixture scenario failed: ${scenario.label}: ${String(error)}`);
310
+ }
311
+ function normalizeRunnerOptions(options) {
312
+ return {
313
+ repoRoot: options.repoRoot,
314
+ cliPath: options.cliPath,
315
+ runCommand: options.runCommand ?? defaultGeneratedScenarioCommandRunner,
316
+ reporter: options.reporter ?? {
317
+ info: console.log,
318
+ error: console.error,
319
+ },
320
+ deterministicToolchainEnv: options.deterministicToolchainEnv ?? defaultDeterministicToolchainEnv,
321
+ rootCheckLock: options.rootCheckLock ?? createExclusiveLock(),
322
+ };
323
+ }
324
+ export async function runGeneratedScenariosConcurrently(manifest, scenarios, workspace, concurrency, options) {
325
+ const normalizedOptions = normalizeRunnerOptions(options);
326
+ let nextScenarioIndex = 0;
327
+ let firstFailure;
328
+ async function worker() {
329
+ while (firstFailure === undefined) {
330
+ const scenario = scenarios[nextScenarioIndex];
331
+ nextScenarioIndex += 1;
332
+ if (!scenario) {
333
+ return;
334
+ }
335
+ try {
336
+ await checkGeneratedScenario(manifest, scenario, workspace, normalizedOptions);
337
+ }
338
+ catch (error) {
339
+ firstFailure ??= errorForFailedGeneratedScenario(scenario, error);
340
+ return;
341
+ }
342
+ }
343
+ }
344
+ await Promise.all(Array.from({ length: concurrency }, async () => {
345
+ await worker();
346
+ }));
347
+ if (firstFailure) {
348
+ throw firstFailure;
349
+ }
350
+ }
351
+ export async function runGeneratedScenarioSet(manifest, set, workspace, options) {
352
+ const normalizedOptions = normalizeRunnerOptions(options);
353
+ const selection = selectGeneratedScenarios(manifest, set);
354
+ const concurrency = fixtureConcurrency(selection.runnable.length);
355
+ for (const skippedScenario of selection.skipped) {
356
+ normalizedOptions.reporter.info?.(`-- Skipping ${skippedScenario.label}: ${skippedScenario.reason}`);
357
+ }
358
+ normalizedOptions.reporter.info?.(`Checking ${selection.runnable.length} fixture scenarios with concurrency ${concurrency}.`);
359
+ await runGeneratedScenariosConcurrently(manifest, selection.runnable, workspace, concurrency, normalizedOptions);
360
+ return selection;
361
+ }
@@ -1,10 +1,11 @@
1
1
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { builtInPresetProjections, findBuiltInPresetProjection, } from "../templates/registry.js";
4
3
  import { validateProjectBlueprint, } from "./declarations.js";
5
4
  import { collectGeneratedManifestCatalogReferences, pnpmWorkspaceYamlWithCatalogDependencies, } from "./dependency-catalog.js";
6
5
  import { PackageAdditionSupport } from "./package-addition-support.js";
7
6
  import { assertTypeScriptPackageBoundaryForLinkIntent, packageTurboTasks, planPackageLinks, } from "./package-linking.js";
7
+ import { findBuiltInPresetSourceManifestPreset, loadBuiltInPresetSourceManifest, } from "./preset-source.js";
8
+ import { defaultPackagePathForPresetSourcePackageAddition, planPresetSourcePackageAddition, } from "./projection-capabilities.js";
8
9
  import { renderProject } from "./renderer.js";
9
10
  function projectNameFromBlueprint(blueprint, fallbackProjectName) {
10
11
  const firstPackage = blueprint.packages?.[0];
@@ -74,16 +75,10 @@ function validateExplicitPackagePath(packagePath) {
74
75
  }
75
76
  return packagePath;
76
77
  }
77
- function defaultPackagePathForPreset(preset, packageLeafName) {
78
- return preset === "ts-lib"
79
- ? `packages/${packageLeafName}`
80
- : `apps/${packageLeafName}`;
81
- }
82
78
  function supportedPackageAdditionPresetNames() {
83
- return builtInPresetProjections
84
- .filter((projection) => projection.metadata.packageAdditionSupport ===
85
- PackageAdditionSupport.Supported)
86
- .map((projection) => projection.metadata.name);
79
+ return loadBuiltInPresetSourceManifest()
80
+ .presets.filter((preset) => preset.packageAdditionSupport === PackageAdditionSupport.Supported)
81
+ .map((preset) => preset.name);
87
82
  }
88
83
  function formatUnsupportedPackageAdditionPresetError(preset) {
89
84
  return [
@@ -627,28 +622,26 @@ export async function addPackage(options) {
627
622
  const repositoryMetadata = await readGeneratedRepositoryPackageMetadata(root);
628
623
  const projectName = projectNameFromBlueprint(blueprint, repositoryMetadata.projectName);
629
624
  const packageName = `@${projectName}/${options.name}`;
630
- const packagePath = options.path
631
- ? validateExplicitPackagePath(options.path)
632
- : defaultPackagePathForPreset(options.preset, options.name);
633
- const projection = findBuiltInPresetProjection(options.preset);
634
- if (!projection) {
625
+ const preset = findBuiltInPresetSourceManifestPreset(options.preset);
626
+ if (!preset) {
635
627
  throw new Error(`Unknown preset for Package Addition: ${options.preset}`);
636
628
  }
637
- if (projection.metadata.packageAdditionSupport !==
638
- PackageAdditionSupport.Supported) {
629
+ if (preset.packageAdditionSupport !== PackageAdditionSupport.Supported) {
639
630
  throw new Error(formatUnsupportedPackageAdditionPresetError(options.preset));
640
631
  }
641
- const packageAddition = projection.capabilities?.packageAddition;
642
- if (!packageAddition) {
643
- throw new Error(`Preset ${options.preset} declares Package Addition support but has no Package Addition implementation`);
644
- }
645
- const additionPlan = await packageAddition.planPackageAddition({
646
- root,
647
- blueprint,
648
- packageLeafName: options.name,
649
- packageName,
650
- packagePath,
651
- nodeVersion: repositoryMetadata.nodeVersion,
632
+ const packagePath = options.path
633
+ ? validateExplicitPackagePath(options.path)
634
+ : defaultPackagePathForPresetSourcePackageAddition(preset, options.name);
635
+ const additionPlan = await planPresetSourcePackageAddition({
636
+ preset,
637
+ addition: {
638
+ root,
639
+ blueprint,
640
+ packageLeafName: options.name,
641
+ packageName,
642
+ packagePath,
643
+ nodeVersion: repositoryMetadata.nodeVersion,
644
+ },
652
645
  });
653
646
  const requiresSharedOxcConfiguration = additionPlan.sourceRoots?.sharedOxc !== undefined;
654
647
  assertNoPackageConflict(blueprint, packageName, additionPlan.packagePath);
@@ -663,7 +656,7 @@ export async function addPackage(options) {
663
656
  packageLinkIntents,
664
657
  });
665
658
  await assertMissingPackagePath(additionPlan.packagePath, path.join(root, additionPlan.packagePath));
666
- const rootUpdatePlan = await planRootUpdates(root, blueprint, packageName, additionPlan.packagePath, additionPlan.packageRole, additionPlan.packageSourcePreset, packageLinkIntents, additionPlan.workspacePackageGlob, packageAdditionCatalogDependencies(additionPlan.operations), requiresSharedOxcConfiguration, additionPlan.operations);
659
+ const rootUpdatePlan = await planRootUpdates(root, blueprint, packageName, additionPlan.packagePath, additionPlan.packageRole, additionPlan.packageSourcePreset, packageLinkIntents, additionPlan.workspaceMembershipGlob ?? additionPlan.workspacePackageGlob, packageAdditionCatalogDependencies(additionPlan.operations), requiresSharedOxcConfiguration, additionPlan.operations);
667
660
  await mkdir(path.join(root, additionPlan.packagePath), { recursive: true });
668
661
  await ensureRootOxcConfiguration({
669
662
  root,