@ykdz/template 0.0.5 → 0.0.7

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.
@@ -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,
@@ -16,6 +16,7 @@ export type PresetPackageAdditionOptions = {
16
16
  export type PresetPackageAdditionPlan = {
17
17
  readonly packagePath: string;
18
18
  readonly workspacePackageGlob: string;
19
+ readonly workspaceMembershipGlob?: string;
19
20
  readonly packageRole: PackageRole;
20
21
  readonly packageSourcePreset: PackageSourcePreset;
21
22
  readonly sourceRoot: string;