@ykdz/template 0.0.4 → 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.
Files changed (46) hide show
  1. package/dist/src/cli.js +79 -34
  2. package/dist/src/declarations.d.ts +36 -0
  3. package/dist/src/declarations.js +31 -2
  4. package/dist/src/generated-scenarios.d.ts +62 -0
  5. package/dist/src/generated-scenarios.js +361 -0
  6. package/dist/src/module-graph.d.ts +1 -1
  7. package/dist/src/module-graph.js +10 -2
  8. package/dist/src/package-addition-support.d.ts +5 -0
  9. package/dist/src/package-addition-support.js +4 -0
  10. package/dist/src/package-addition.d.ts +1 -0
  11. package/dist/src/package-addition.js +409 -69
  12. package/dist/src/package-linking.d.ts +55 -0
  13. package/dist/src/package-linking.js +116 -0
  14. package/dist/src/preset-projection.d.ts +4 -1
  15. package/dist/src/preset-source.d.ts +430 -0
  16. package/dist/src/preset-source.js +940 -0
  17. package/dist/src/projection-capabilities.d.ts +67 -0
  18. package/dist/src/projection-capabilities.js +2207 -0
  19. package/dist/src/renderer.js +20 -1
  20. package/dist/src/template-boundary-check.d.ts +7 -3
  21. package/dist/src/template-boundary-check.js +104 -10
  22. package/package.json +3 -2
  23. package/templates/hono-api/src/server.ts +1 -1
  24. package/templates/hono-api/test/app.test.ts +1 -1
  25. package/templates/preset-source.json +499 -0
  26. package/templates/ts-lib/src/index.ts +2 -2
  27. package/templates/ts-lib/src/name-schema.ts +3 -0
  28. package/templates/vue-app/playwright.package-addition.config.ts +22 -0
  29. package/templates/vue-app/src/App.vue +1 -1
  30. package/templates/vue-app/test/app.test.ts +1 -1
  31. package/templates/vue-hono-app/web/src/App.vue +2 -2
  32. package/templates/vue-hono-app/web/test/app.test.ts +1 -1
  33. package/dist/templates/hono-api/projection.d.ts +0 -6
  34. package/dist/templates/hono-api/projection.js +0 -538
  35. package/dist/templates/projection-plans.d.ts +0 -7
  36. package/dist/templates/projection-plans.js +0 -94
  37. package/dist/templates/registry.d.ts +0 -7
  38. package/dist/templates/registry.js +0 -42
  39. package/dist/templates/rust-bin/projection.d.ts +0 -9
  40. package/dist/templates/rust-bin/projection.js +0 -390
  41. package/dist/templates/ts-lib/projection.d.ts +0 -6
  42. package/dist/templates/ts-lib/projection.js +0 -523
  43. package/dist/templates/vue-app/projection.d.ts +0 -6
  44. package/dist/templates/vue-app/projection.js +0 -757
  45. package/dist/templates/vue-hono-app/projection.d.ts +0 -8
  46. package/dist/templates/vue-hono-app/projection.js +0 -693
@@ -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
+ }
@@ -7,7 +7,7 @@ export type WorkspaceOrchestrationOwner = {
7
7
  readonly path: ".";
8
8
  };
9
9
  export type ComponentOwner = PackageBoundaryOwner | WorkspaceOrchestrationOwner;
10
- export type CheckComponentKind = "typescript-typecheck" | "oxc-lint" | "oxc-format-check" | "build" | "unit-test" | "e2e-test" | "turbo-check" | "turbo-package-check" | "rustfmt-check" | "cargo-clippy" | "cargo-test";
10
+ export type CheckComponentKind = "typescript-typecheck" | "oxc-lint" | "oxc-format-check" | "build" | "unit-test" | "e2e-test" | "turbo-check" | "turbo-package-typecheck" | "turbo-package-build" | "turbo-package-test" | "turbo-package-e2e-test" | "turbo-package-check" | "rustfmt-check" | "cargo-clippy" | "cargo-test";
11
11
  export type FixComponentKind = "oxc-format-write" | "oxc-lint-fix" | "turbo-fix" | "turbo-package-fix" | "rustfmt-write";
12
12
  export type CheckEnvironmentNeed = {
13
13
  readonly kind: "playwright-browser-assets";
@@ -20,6 +20,14 @@ function renderCheckComponentCommand(component) {
20
20
  return "pnpm run test:e2e";
21
21
  case "turbo-check":
22
22
  return "turbo run check";
23
+ case "turbo-package-typecheck":
24
+ return `turbo run typecheck ${renderTurboPackageFilter(component.owner)}`;
25
+ case "turbo-package-build":
26
+ return `turbo run build ${renderTurboPackageFilter(component.owner)}`;
27
+ case "turbo-package-test":
28
+ return `turbo run test ${renderTurboPackageFilter(component.owner)}`;
29
+ case "turbo-package-e2e-test":
30
+ return `turbo run test:e2e ${renderTurboPackageFilter(component.owner)}`;
23
31
  case "turbo-package-check":
24
32
  return `turbo run check ${renderTurboPackageFilter(component.owner)}`;
25
33
  case "rustfmt-check":
@@ -51,8 +59,8 @@ export function renderFixCommand(plan) {
51
59
  return plan.components.map(renderFixComponentCommand).join(" && ");
52
60
  }
53
61
  export function renderPlaywrightBrowserInstallCommand(need) {
54
- if (need.owner.path === "apps/web") {
55
- return `pnpm --filter ./apps/web exec playwright install ${need.browser}`;
62
+ if (need.owner.kind === "package-boundary" && need.owner.path !== ".") {
63
+ return `pnpm --filter ./${need.owner.path} exec playwright install ${need.browser}`;
56
64
  }
57
65
  return `pnpm exec playwright install ${need.browser}`;
58
66
  }
@@ -0,0 +1,5 @@
1
+ export declare const PackageAdditionSupport: {
2
+ readonly Supported: "supported";
3
+ readonly Unsupported: "unsupported";
4
+ };
5
+ export type PackageAdditionSupport = (typeof PackageAdditionSupport)[keyof typeof PackageAdditionSupport];
@@ -0,0 +1,4 @@
1
+ export const PackageAdditionSupport = {
2
+ Supported: "supported",
3
+ Unsupported: "unsupported",
4
+ };
@@ -3,5 +3,6 @@ export type AddPackageOptions = {
3
3
  preset: string;
4
4
  name: string;
5
5
  path?: string;
6
+ linkFrom?: readonly string[];
6
7
  };
7
8
  export declare function addPackage(options: AddPackageOptions): Promise<void>;