@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 +65 -32
- package/dist/src/declarations.js +2 -2
- package/dist/src/generated-scenarios.d.ts +62 -0
- package/dist/src/generated-scenarios.js +361 -0
- package/dist/src/package-addition.js +22 -29
- package/dist/src/preset-projection.d.ts +1 -0
- package/dist/src/preset-source.d.ts +430 -0
- package/dist/src/preset-source.js +940 -0
- package/dist/src/projection-capabilities.d.ts +67 -0
- package/dist/src/projection-capabilities.js +2207 -0
- package/dist/src/renderer.js +19 -1
- package/dist/src/template-boundary-check.d.ts +7 -3
- package/dist/src/template-boundary-check.js +104 -10
- package/package.json +3 -2
- package/templates/preset-source.json +499 -0
- package/templates/vue-app/playwright.package-addition.config.ts +22 -0
- package/dist/templates/hono-api/projection.d.ts +0 -6
- package/dist/templates/hono-api/projection.js +0 -559
- package/dist/templates/projection-plans.d.ts +0 -7
- package/dist/templates/projection-plans.js +0 -94
- package/dist/templates/registry.d.ts +0 -7
- package/dist/templates/registry.js +0 -45
- package/dist/templates/rust-bin/projection.d.ts +0 -9
- package/dist/templates/rust-bin/projection.js +0 -400
- package/dist/templates/ts-lib/projection.d.ts +0 -6
- package/dist/templates/ts-lib/projection.js +0 -536
- package/dist/templates/vue-app/projection.d.ts +0 -6
- package/dist/templates/vue-app/projection.js +0 -766
- package/dist/templates/vue-hono-app/projection.d.ts +0 -8
- package/dist/templates/vue-hono-app/projection.js +0 -713
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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 (
|
|
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
|
|
225
|
-
if (projection) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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
|
|
237
|
-
|
|
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
|
-
|
|
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
|
-
?
|
|
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}`);
|
package/dist/src/declarations.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as v from "valibot";
|
|
2
|
-
import {
|
|
3
|
-
export const builtInPresets =
|
|
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
|
|
84
|
-
.filter((
|
|
85
|
-
|
|
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
|
|
631
|
-
|
|
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 (
|
|
638
|
-
PackageAdditionSupport.Supported) {
|
|
629
|
+
if (preset.packageAdditionSupport !== PackageAdditionSupport.Supported) {
|
|
639
630
|
throw new Error(formatUnsupportedPackageAdditionPresetError(options.preset));
|
|
640
631
|
}
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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,
|