@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.
package/dist/src/cli.js CHANGED
@@ -2,32 +2,41 @@
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
+ "Project Kit template CLI",
16
+ "",
13
17
  "Usage:",
18
+ " template <command> [options]",
19
+ "",
20
+ "Commands:",
14
21
  " template init <dir> --preset <name> --yes",
15
22
  " template add package --preset <name> --name <name> [--path <package-path>] [--link-from <package-path>]...",
16
23
  " template presets",
17
24
  " template schema preset",
25
+ " template schema preset-source",
18
26
  " template schema blueprint",
19
27
  " template preset validate <path>",
28
+ " template preset-source validate <path>",
20
29
  " template blueprint validate <path>",
21
30
  "",
22
31
  "Options:",
23
- " --preset <name> Project preset to generate",
24
- " --name <name> Package name to add",
25
- " --path <path> Two-segment Package Path to add",
32
+ " --preset <name> Project preset to generate",
33
+ " --name <name> Package name to add",
34
+ " --path <path> Two-segment Package Path to add",
26
35
  " --link-from <path> Existing consumer Package Path to link from; repeatable",
27
- " --scope <name> Package scope for workspace package names",
28
- " --yes Accept defaults for non-interactive generation",
29
- " --dry-run Print the planned generation without writing files",
30
- " --json Print machine-readable output",
36
+ " --scope <name> Package scope for workspace package names",
37
+ " --yes Accept defaults for non-interactive generation",
38
+ " --dry-run Print the planned generation without writing files",
39
+ " --json Print machine-readable output",
31
40
  ].join("\n");
32
41
  }
33
42
  function printJson(value) {
@@ -52,10 +61,18 @@ function formatValidationIssues(issues) {
52
61
  .map((issue) => ` - ${issue.path}: ${issue.message}`)
53
62
  .join("\n");
54
63
  }
64
+ function formatFieldRows(rows) {
65
+ const width = Math.max(...rows.map(([label]) => `${label}:`.length));
66
+ return rows.map(([label, value]) => ` ${`${label}:`.padEnd(width)} ${value}`);
67
+ }
55
68
  function formatPresetCatalog() {
56
69
  return [
57
70
  "Built-in presets",
58
- ...builtInPresets.map((preset) => ` ${preset.name.padEnd(8)} ${preset.title} (${preset.generation}) - ${preset.description}`),
71
+ "",
72
+ ...formatFieldRows(builtInPresets.map((preset) => [
73
+ preset.name,
74
+ `${preset.title} (${preset.generation}) - ${preset.description}`,
75
+ ])),
59
76
  ].join("\n");
60
77
  }
61
78
  function parseInitOptions(args) {
@@ -135,42 +152,35 @@ function formatList(values) {
135
152
  }
136
153
  function blueprintForInit(options) {
137
154
  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;
155
+ return blueprintForPresetSourcePreset(preset, {
156
+ targetDir: options.dir,
157
+ scope: options.scope,
158
+ });
155
159
  }
156
160
  function formatBlueprintSummary(targetDir, blueprint) {
157
- const lines = [
158
- "Project Blueprint",
159
- ` Target: ${targetDir}`,
160
- ` Preset: ${blueprint.preset}`,
161
- ` Project kind: ${blueprint.projectKind}`,
161
+ const rows = [
162
+ ["Target", targetDir],
163
+ ["Preset", blueprint.preset],
164
+ ["Project kind", blueprint.projectKind],
162
165
  ];
163
166
  if (blueprint.packageManager) {
164
- lines.push(` Package manager: ${blueprint.packageManager}`);
167
+ rows.push(["Package manager", blueprint.packageManager]);
165
168
  }
166
- if (blueprint.packages) {
167
- lines.push(" Packages:");
168
- for (const pkg of blueprint.packages) {
169
- lines.push(` - ${pkg.name} (${pkg.path})`);
170
- }
171
- }
172
- lines.push(` Features: ${blueprint.features.join(", ")}`);
173
- return lines.join("\n");
169
+ return [
170
+ "Project Blueprint",
171
+ "",
172
+ ...formatFieldRows(rows),
173
+ ...(blueprint.packages
174
+ ? [
175
+ "",
176
+ " Packages:",
177
+ ...blueprint.packages.map((pkg) => ` - ${pkg.name} (${pkg.path})`),
178
+ ]
179
+ : []),
180
+ "",
181
+ " Features:",
182
+ ...blueprint.features.map((feature) => ` - ${feature}`),
183
+ ].join("\n");
174
184
  }
175
185
  function formatProjectionNextSteps(targetDir, projectionPlan) {
176
186
  const steps = planNextStepInstructionsForProjection({
@@ -179,7 +189,11 @@ function formatProjectionNextSteps(targetDir, projectionPlan) {
179
189
  });
180
190
  return [
181
191
  "Next Step Instructions:",
182
- ...steps.map((step) => ` ${step.label}: ${step.display}`),
192
+ "",
193
+ ...steps.flatMap((step, index) => [
194
+ ` ${index + 1}. ${step.label}`,
195
+ ` ${step.display}`,
196
+ ]),
183
197
  ].join("\n");
184
198
  }
185
199
  function toolchainReport(toolchain) {
@@ -193,9 +207,12 @@ function toolchainReport(toolchain) {
193
207
  function formatToolchainReport(toolchain) {
194
208
  return [
195
209
  "Toolchain Resolution:",
196
- ` Source: ${toolchain.source}`,
197
- ` Node LTS major: ${toolchain.nodeLtsMajor.value}`,
198
- ` Package Manager Pin: ${toolchain.packageManagerPin.value}`,
210
+ "",
211
+ ...formatFieldRows([
212
+ ["Source", toolchain.source],
213
+ ["Node LTS major", toolchain.nodeLtsMajor.value],
214
+ ["Package Manager Pin", toolchain.packageManagerPin.value],
215
+ ]),
199
216
  ...toolchain.diagnostics.map((diagnostic) => ` ${diagnostic}`),
200
217
  ].join("\n");
201
218
  }
@@ -207,7 +224,8 @@ function toolchainResolutionSourceFromEnv() {
207
224
  return undefined;
208
225
  }
209
226
  async function generationContextForInit(options, blueprint) {
210
- if (!findBuiltInPresetProjection(options.preset)) {
227
+ if (findBuiltInPresetSourceManifestPreset(options.preset)?.projection ===
228
+ undefined) {
211
229
  return undefined;
212
230
  }
213
231
  return assembleGenerationContext({
@@ -221,20 +239,28 @@ async function generationContextForInit(options, blueprint) {
221
239
  });
222
240
  }
223
241
  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;
242
+ const preset = findBuiltInPresetSourceManifestPreset(options.preset);
243
+ if (!preset?.projection) {
244
+ throw new Error(formatSupportedPresetError());
232
245
  }
233
- throw new Error(formatSupportedPresetError());
246
+ if (!generationContext) {
247
+ throw new Error(`Missing Generation Context for Preset Projection: ${options.preset}`);
248
+ }
249
+ const plan = projectPresetSourcePreset({
250
+ preset,
251
+ context: generationContext,
252
+ });
253
+ await renderNewProject({
254
+ sourceRoot: plan.sourceRoot,
255
+ sourceRoots: plan.sourceRoots,
256
+ targetRoot: options.dir,
257
+ operations: [...plan.operations],
258
+ });
234
259
  }
235
260
  function printInitComplete(options, blueprint, generationContext) {
236
- const projectionPlan = generationContext
237
- ? projectPresetThroughRegistry(generationContext)
261
+ const preset = findBuiltInPresetSourceManifestPreset(blueprint.preset);
262
+ const projectionPlan = generationContext && preset?.projection
263
+ ? projectPresetSourcePreset({ preset, context: generationContext })
238
264
  : undefined;
239
265
  if (!projectionPlan) {
240
266
  throw new Error(`Missing Preset Projection plan: ${blueprint.preset}`);
@@ -256,10 +282,19 @@ function printInitComplete(options, blueprint, generationContext) {
256
282
  });
257
283
  return;
258
284
  }
259
- console.log(`Initialized ${options.preset} project in ${options.dir}`);
285
+ console.log([
286
+ "Initialized project",
287
+ "",
288
+ ...formatFieldRows([
289
+ ["Preset", options.preset],
290
+ ["Target", options.dir],
291
+ ]),
292
+ ].join("\n"));
260
293
  if (generationContext) {
294
+ console.log("");
261
295
  console.log(formatToolchainReport(generationContext.toolchain));
262
296
  }
297
+ console.log("");
263
298
  console.log(formatProjectionNextSteps(options.dir, projectionPlan));
264
299
  }
265
300
  function isInteractiveTerminal() {
@@ -349,7 +384,11 @@ async function main(args) {
349
384
  printJson(blueprintJsonSchema);
350
385
  return;
351
386
  }
352
- throw new Error("schema requires preset or blueprint");
387
+ if (schemaName === "preset-source") {
388
+ printJson(presetSourceManifestJsonSchema);
389
+ return;
390
+ }
391
+ throw new Error("schema requires preset, preset-source, or blueprint");
353
392
  }
354
393
  if (command === "preset" && args[1] === "validate") {
355
394
  const filePath = args[2];
@@ -363,6 +402,32 @@ async function main(args) {
363
402
  console.log(`Preset file is valid: ${result.value.name}`);
364
403
  return;
365
404
  }
405
+ if (command === "preset-source" && args[1] === "validate") {
406
+ const filePath = args[2];
407
+ if (!filePath) {
408
+ throw new Error("preset-source validate requires a path");
409
+ }
410
+ const declaration = await readJsonDeclaration(filePath);
411
+ const sourceName = typeof declaration === "object" &&
412
+ declaration !== null &&
413
+ !Array.isArray(declaration) &&
414
+ "name" in declaration &&
415
+ typeof declaration.name === "string"
416
+ ? declaration.name
417
+ : undefined;
418
+ const result = sourceName === "built-in"
419
+ ? validateBuiltInPresetSourceManifest(declaration, {
420
+ sourceRoot: path.dirname(filePath),
421
+ })
422
+ : validatePresetSourceManifest(declaration, {
423
+ sourceRoot: path.dirname(filePath),
424
+ });
425
+ if (!result.ok) {
426
+ throw new Error(`Preset Source Manifest is invalid:\n${formatValidationIssues(result.issues)}`);
427
+ }
428
+ console.log(`Preset Source Manifest is valid: ${result.value.name} (${result.value.presets.map((preset) => preset.name).join(", ")})`);
429
+ return;
430
+ }
366
431
  if (command === "blueprint" && args[1] === "validate") {
367
432
  const filePath = args[2];
368
433
  if (!filePath) {
@@ -380,8 +445,11 @@ async function main(args) {
380
445
  const blueprint = blueprintForInit(options);
381
446
  if (options.dryRun) {
382
447
  const generationContext = await generationContextForInit(options, blueprint);
448
+ const preset = findBuiltInPresetSourceManifestPreset(options.preset);
383
449
  const projectionPlan = generationContext
384
- ? projectPresetThroughRegistry(generationContext)
450
+ ? preset?.projection
451
+ ? projectPresetSourcePreset({ preset, context: generationContext })
452
+ : undefined
385
453
  : undefined;
386
454
  if (!projectionPlan) {
387
455
  throw new Error(`Missing Preset Projection plan: ${blueprint.preset}`);
@@ -405,8 +473,10 @@ async function main(args) {
405
473
  }
406
474
  console.log(formatBlueprintSummary(options.dir, blueprint));
407
475
  if (generationContext) {
476
+ console.log("");
408
477
  console.log(formatToolchainReport(generationContext.toolchain));
409
478
  }
479
+ console.log("");
410
480
  console.log(formatProjectionNextSteps(options.dir, projectionPlan));
411
481
  return;
412
482
  }
@@ -430,7 +500,15 @@ async function main(args) {
430
500
  path: options.path,
431
501
  linkFrom: options.linkFrom,
432
502
  });
433
- console.log(`Added ${options.preset} package ${options.name}`);
503
+ console.log([
504
+ "Added package",
505
+ "",
506
+ ...formatFieldRows([
507
+ ["Preset", options.preset],
508
+ ["Name", options.name],
509
+ ...(options.path ? [["Path", options.path]] : []),
510
+ ]),
511
+ ].join("\n"));
434
512
  return;
435
513
  }
436
514
  if (command === "--help" || command === "-h") {
@@ -441,8 +519,8 @@ async function main(args) {
441
519
  }
442
520
  main(process.argv.slice(2)).catch((error) => {
443
521
  const message = error instanceof Error ? error.message : String(error);
444
- console.error(message);
522
+ console.error(`Error: ${message}`);
445
523
  console.error("");
446
- console.error(usage());
524
+ console.error("Run `template --help` for usage.");
447
525
  process.exitCode = 1;
448
526
  });
@@ -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>;