@ykdz/template 0.0.3 → 0.0.4

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 (84) hide show
  1. package/README.md +1 -1
  2. package/dist/src/cli.d.ts +0 -1
  3. package/dist/src/cli.js +14 -2
  4. package/dist/src/declarations.d.ts +2 -3
  5. package/dist/src/declarations.js +46 -36
  6. package/dist/src/dependency-catalog.d.ts +9 -1
  7. package/dist/src/dependency-catalog.js +69 -0
  8. package/dist/src/devcontainer.d.ts +49 -9
  9. package/dist/src/devcontainer.js +192 -14
  10. package/dist/src/editor-customization.d.ts +0 -1
  11. package/dist/src/editor-customization.js +54 -47
  12. package/dist/src/generation-context.d.ts +0 -1
  13. package/dist/src/generation-context.js +4 -1
  14. package/dist/src/module-graph.d.ts +8 -5
  15. package/dist/src/module-graph.js +10 -0
  16. package/dist/src/next-step-instructions.d.ts +0 -1
  17. package/dist/src/next-step-instructions.js +35 -0
  18. package/dist/src/package-addition.d.ts +1 -1
  19. package/dist/src/package-addition.js +170 -15
  20. package/dist/src/preset-projection.d.ts +1 -1
  21. package/dist/src/project-github.d.ts +3 -2
  22. package/dist/src/project-github.js +24 -6
  23. package/dist/src/renderer.d.ts +16 -2
  24. package/dist/src/renderer.js +95 -12
  25. package/dist/src/runtime-paths.d.ts +0 -1
  26. package/dist/src/template-boundary-check.d.ts +34 -0
  27. package/dist/src/template-boundary-check.js +448 -0
  28. package/dist/src/toolchain-resolution.d.ts +0 -1
  29. package/dist/templates/hono-api/projection.d.ts +2 -3
  30. package/dist/templates/hono-api/projection.js +230 -69
  31. package/dist/templates/projection-plans.d.ts +0 -1
  32. package/dist/templates/registry.d.ts +0 -1
  33. package/dist/templates/registry.js +2 -2
  34. package/dist/templates/rust-bin/projection.d.ts +2 -3
  35. package/dist/templates/rust-bin/projection.js +161 -53
  36. package/dist/templates/ts-lib/projection.d.ts +2 -3
  37. package/dist/templates/ts-lib/projection.js +215 -64
  38. package/dist/templates/vue-app/projection.d.ts +2 -3
  39. package/dist/templates/vue-app/projection.js +246 -83
  40. package/dist/templates/vue-hono-app/projection.d.ts +0 -1
  41. package/dist/templates/vue-hono-app/projection.js +191 -87
  42. package/package.json +10 -5
  43. package/pnpm-workspace.yaml +1 -0
  44. package/templates/hono-api/.github/dependabot.yml +13 -0
  45. package/templates/rust-bin/.github/dependabot.yml +18 -1
  46. package/templates/rust-bin/rust-toolchain.toml +3 -0
  47. package/templates/rust-bin/rustfmt.toml +2 -0
  48. package/templates/shared/devcontainer/browser-test.Dockerfile +2 -0
  49. package/templates/shared/devcontainer/node-pnpm.Dockerfile +6 -0
  50. package/templates/shared/devcontainer/rust.Dockerfile +16 -0
  51. package/templates/shared/editor-customization/capabilities.json +56 -0
  52. package/templates/ts-lib/.github/dependabot.yml +13 -0
  53. package/templates/ts-lib/src/index.ts +5 -1
  54. package/templates/vue-app/.github/dependabot.yml +13 -0
  55. package/templates/vue-app/.github/workflows/check.yml +1 -1
  56. package/templates/vue-hono-app/.github/dependabot.yml +13 -0
  57. package/dist/src/cli.d.ts.map +0 -1
  58. package/dist/src/declarations.d.ts.map +0 -1
  59. package/dist/src/dependency-catalog.d.ts.map +0 -1
  60. package/dist/src/devcontainer.d.ts.map +0 -1
  61. package/dist/src/editor-customization.d.ts.map +0 -1
  62. package/dist/src/generation-context.d.ts.map +0 -1
  63. package/dist/src/module-graph.d.ts.map +0 -1
  64. package/dist/src/next-step-instructions.d.ts.map +0 -1
  65. package/dist/src/package-addition.d.ts.map +0 -1
  66. package/dist/src/preset-projection.d.ts.map +0 -1
  67. package/dist/src/project-github.d.ts.map +0 -1
  68. package/dist/src/renderer.d.ts.map +0 -1
  69. package/dist/src/runtime-paths.d.ts.map +0 -1
  70. package/dist/src/toolchain-resolution.d.ts.map +0 -1
  71. package/dist/templates/hono-api/projection.d.ts.map +0 -1
  72. package/dist/templates/projection-plans.d.ts.map +0 -1
  73. package/dist/templates/registry.d.ts.map +0 -1
  74. package/dist/templates/rust-bin/projection.d.ts.map +0 -1
  75. package/dist/templates/ts-lib/projection.d.ts.map +0 -1
  76. package/dist/templates/vue-app/projection.d.ts.map +0 -1
  77. package/dist/templates/vue-hono-app/projection.d.ts.map +0 -1
  78. package/templates/hono-api/projection.ts +0 -453
  79. package/templates/projection-plans.ts +0 -135
  80. package/templates/registry.ts +0 -59
  81. package/templates/rust-bin/projection.ts +0 -346
  82. package/templates/ts-lib/projection.ts +0 -454
  83. package/templates/vue-app/projection.ts +0 -687
  84. package/templates/vue-hono-app/projection.ts +0 -669
@@ -2,6 +2,7 @@ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { findBuiltInPresetProjection } from "../templates/registry.js";
4
4
  import { validateProjectBlueprint, } from "./declarations.js";
5
+ import { collectGeneratedManifestCatalogReferences, pnpmWorkspaceYamlWithCatalogDependencies, } from "./dependency-catalog.js";
5
6
  import { renderProject } from "./renderer.js";
6
7
  function projectNameFromBlueprint(blueprint) {
7
8
  const firstPackage = blueprint.packages?.[0];
@@ -16,13 +17,80 @@ function assertSafePackageLeaf(name) {
16
17
  throw new Error("--name must be a lowercase package leaf name using letters, numbers, and hyphens");
17
18
  }
18
19
  }
20
+ const reservedPackagePathCollections = new Set([
21
+ ".devcontainer",
22
+ ".git",
23
+ ".github",
24
+ ".template",
25
+ "build",
26
+ "coverage",
27
+ "dist",
28
+ "node_modules",
29
+ "out",
30
+ "target",
31
+ ]);
32
+ function assertSafePackagePathSegment(packagePath, segment) {
33
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(segment)) {
34
+ throw new Error(`Package Path ${packagePath} must use safe path segments with lowercase letters, numbers, and hyphens`);
35
+ }
36
+ }
37
+ function validateExplicitPackagePath(packagePath) {
38
+ if (path.isAbsolute(packagePath)) {
39
+ throw new Error(`Package Path ${packagePath} must be relative`);
40
+ }
41
+ if (packagePath !== packagePath.trim()) {
42
+ throw new Error(`Package Path ${packagePath} must not contain whitespace`);
43
+ }
44
+ if (packagePath === ".." ||
45
+ packagePath.startsWith("../") ||
46
+ packagePath.endsWith("/..") ||
47
+ packagePath.includes("/../")) {
48
+ throw new Error(`Package Path ${packagePath} must not escape the workspace`);
49
+ }
50
+ const normalized = path.posix.normalize(packagePath);
51
+ if (normalized !== packagePath || packagePath.includes("\\")) {
52
+ throw new Error(`Package Path ${packagePath} must be exactly two safe path segments`);
53
+ }
54
+ const segments = packagePath.split("/");
55
+ if (segments.length !== 2) {
56
+ throw new Error(`Package Path ${packagePath} must be exactly two safe path segments`);
57
+ }
58
+ const [collection, packageDirectory] = segments;
59
+ if (!collection || !packageDirectory) {
60
+ throw new Error(`Package Path ${packagePath} must be exactly two safe path segments`);
61
+ }
62
+ if (reservedPackagePathCollections.has(collection)) {
63
+ throw new Error(`Package Path ${packagePath} uses reserved collection ${collection}`);
64
+ }
65
+ assertSafePackagePathSegment(packagePath, collection);
66
+ assertSafePackagePathSegment(packagePath, packageDirectory);
67
+ if (reservedPackagePathCollections.has(packageDirectory)) {
68
+ throw new Error(`Package Path ${packagePath} uses reserved package directory ${packageDirectory}`);
69
+ }
70
+ return packagePath;
71
+ }
72
+ function defaultPackagePathForPreset(preset, packageLeafName) {
73
+ return preset === "ts-lib"
74
+ ? `packages/${packageLeafName}`
75
+ : `apps/${packageLeafName}`;
76
+ }
77
+ function assertNoPackageConflict(blueprint, packageName, packagePath) {
78
+ const existingByName = blueprint.packages?.find((pkg) => pkg.name === packageName);
79
+ if (existingByName) {
80
+ throw new Error(`Package Path ${packagePath} conflicts with existing package ${existingByName.name} at ${existingByName.path}`);
81
+ }
82
+ const existingByPath = blueprint.packages?.find((pkg) => pkg.path === packagePath);
83
+ if (existingByPath) {
84
+ throw new Error(`Package Path ${packagePath} conflicts with existing package ${existingByPath.name} at ${existingByPath.path}`);
85
+ }
86
+ }
19
87
  async function readJson(filePath) {
20
88
  return JSON.parse(await readFile(filePath, "utf8"));
21
89
  }
22
90
  async function writeJson(filePath, value) {
23
91
  await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
24
92
  }
25
- async function assertMissing(targetPath) {
93
+ async function assertMissingPackagePath(packagePath, targetPath) {
26
94
  try {
27
95
  await stat(targetPath);
28
96
  }
@@ -32,7 +100,7 @@ async function assertMissing(targetPath) {
32
100
  }
33
101
  throw error;
34
102
  }
35
- throw new Error(`Package Addition would overwrite an existing path: ${targetPath}`);
103
+ throw new Error(`Package Path ${packagePath} conflicts with existing filesystem path ${targetPath}`);
36
104
  }
37
105
  function resolveGeneratedPath(root, relativePath) {
38
106
  if (path.isAbsolute(relativePath)) {
@@ -53,11 +121,39 @@ function isNodeError(error) {
53
121
  function isRecord(value) {
54
122
  return typeof value === "object" && value !== null && !Array.isArray(value);
55
123
  }
124
+ function assertRootPackageJson(input) {
125
+ if (!isRecord(input)) {
126
+ throw new Error("Cannot update root Package Addition scripts: package.json must be an object");
127
+ }
128
+ if (!isRecord(input.scripts)) {
129
+ throw new Error("Cannot update root Package Addition scripts: scripts must be an object");
130
+ }
131
+ for (const scriptName of ["check", "fix"]) {
132
+ if (typeof input.scripts[scriptName] !== "string") {
133
+ throw new Error(`Cannot update root Package Addition scripts: scripts.${scriptName} must be a string`);
134
+ }
135
+ }
136
+ }
56
137
  async function readGeneratedWorkspaceBlueprint(root) {
57
- const blueprintPath = path.join(root, ".project-kit/blueprint.json");
58
- const result = validateProjectBlueprint(await readJson(blueprintPath));
138
+ const blueprintPath = path.join(root, ".template/blueprint.json");
139
+ let blueprintJson;
140
+ try {
141
+ blueprintJson = await readJson(blueprintPath);
142
+ }
143
+ catch (error) {
144
+ if (error instanceof SyntaxError ||
145
+ (isNodeError(error) && error.code === "ENOENT")) {
146
+ throw new Error("Package Addition requires a valid .template/blueprint.json");
147
+ }
148
+ throw error;
149
+ }
150
+ const result = validateProjectBlueprint(blueprintJson);
59
151
  if (!result.ok) {
60
- throw new Error("Package Addition requires a valid .project-kit/blueprint.json");
152
+ if (isRecord(blueprintJson) &&
153
+ blueprintJson.projectKind === "single-package") {
154
+ throw new Error("Package Addition only supports existing workspace Generated Repositories");
155
+ }
156
+ throw new Error("Package Addition requires a valid .template/blueprint.json");
61
157
  }
62
158
  const blueprint = result.value;
63
159
  if (blueprint.projectKind !== "multi-package") {
@@ -122,6 +218,20 @@ function rootTsconfigWithReferences(input, referencePaths) {
122
218
  }
123
219
  return { ...tsconfig, references };
124
220
  }
221
+ function packageAdditionCatalogDependencies(operations) {
222
+ const manifests = [];
223
+ for (const operation of operations) {
224
+ if (operation.kind !== "writeJson" ||
225
+ path.basename(operation.to) !== "package.json") {
226
+ continue;
227
+ }
228
+ if (!isRecord(operation.value)) {
229
+ throw new Error(`Package Addition package manifest must be an object: ${operation.to}`);
230
+ }
231
+ manifests.push(operation.value);
232
+ }
233
+ return collectGeneratedManifestCatalogReferences(manifests);
234
+ }
125
235
  function blueprintWithPackage(blueprint, packageName, packagePath) {
126
236
  const nextBlueprint = {
127
237
  ...blueprint,
@@ -132,15 +242,57 @@ function blueprintWithPackage(blueprint, packageName, packagePath) {
132
242
  };
133
243
  const result = validateProjectBlueprint(nextBlueprint);
134
244
  if (!result.ok) {
135
- throw new Error("Package Addition would write an invalid .project-kit/blueprint.json");
245
+ throw new Error("Package Addition would write an invalid .template/blueprint.json");
136
246
  }
137
247
  return result.value;
138
248
  }
139
- async function planRootUpdates(root, blueprint, packageName, packagePath, workspacePackageGlob, rootTsconfigReferences) {
140
- const workspaceText = workspaceTextWithPackageGlob(await readFile(path.join(root, "pnpm-workspace.yaml"), "utf8"), workspacePackageGlob);
249
+ function workspacePackageGlobsFromBlueprint(blueprint) {
250
+ const globs = [];
251
+ for (const packageDefinition of blueprint.packages ?? []) {
252
+ const [workspaceDir] = packageDefinition.path.split("/");
253
+ if (!workspaceDir) {
254
+ throw new Error(`Cannot update root Package Addition scripts: invalid package path ${packageDefinition.path}`);
255
+ }
256
+ const glob = `${workspaceDir}/*`;
257
+ if (!globs.includes(glob)) {
258
+ globs.push(glob);
259
+ }
260
+ }
261
+ return globs;
262
+ }
263
+ function turboPackageTaskCommand(task, workspacePackageGlobs) {
264
+ const filters = workspacePackageGlobs.map((glob) => `--filter './${glob}'`);
265
+ return [`turbo run ${task}`, ...filters].join(" ");
266
+ }
267
+ function rootScriptWithTurboPackageTask(script, task, workspacePackageGlobs) {
268
+ const commands = script.split(" && ");
269
+ const turboCommandIndex = commands.findIndex((command) => command.startsWith(`turbo run ${task}`));
270
+ if (turboCommandIndex === -1) {
271
+ throw new Error(`Cannot update root Package Addition scripts: scripts.${task} must run Turbo package tasks`);
272
+ }
273
+ commands[turboCommandIndex] = turboPackageTaskCommand(task, workspacePackageGlobs);
274
+ return commands.join(" && ");
275
+ }
276
+ function rootPackageJsonWithPackageTaskFilters(input, blueprint) {
277
+ assertRootPackageJson(input);
278
+ const workspacePackageGlobs = workspacePackageGlobsFromBlueprint(blueprint);
279
+ return {
280
+ ...input,
281
+ scripts: {
282
+ ...input.scripts,
283
+ check: rootScriptWithTurboPackageTask(input.scripts.check, "check", workspacePackageGlobs),
284
+ fix: rootScriptWithTurboPackageTask(input.scripts.fix, "fix", workspacePackageGlobs),
285
+ },
286
+ };
287
+ }
288
+ async function planRootUpdates(root, blueprint, packageName, packagePath, workspacePackageGlob, rootTsconfigReferences, catalogDependencies) {
289
+ const nextBlueprint = blueprintWithPackage(blueprint, packageName, packagePath);
290
+ const workspaceText = pnpmWorkspaceYamlWithCatalogDependencies(workspaceTextWithPackageGlob(await readFile(path.join(root, "pnpm-workspace.yaml"), "utf8"), workspacePackageGlob), catalogDependencies);
141
291
  const rootTsconfig = rootTsconfigWithReferences(await readJson(path.join(root, "tsconfig.json")), rootTsconfigReferences);
292
+ const rootPackageJson = rootPackageJsonWithPackageTaskFilters(await readJson(path.join(root, "package.json")), nextBlueprint);
142
293
  return {
143
- blueprint: blueprintWithPackage(blueprint, packageName, packagePath),
294
+ blueprint: nextBlueprint,
295
+ rootPackageJson,
144
296
  rootTsconfig,
145
297
  workspaceText,
146
298
  };
@@ -152,6 +304,9 @@ export async function addPackage(options) {
152
304
  const nodeVersion = await readGeneratedRepositoryNodeVersion(root);
153
305
  const projectName = projectNameFromBlueprint(blueprint);
154
306
  const packageName = `@${projectName}/${options.name}`;
307
+ const packagePath = options.path
308
+ ? validateExplicitPackagePath(options.path)
309
+ : defaultPackagePathForPreset(options.preset, options.name);
155
310
  const projection = findBuiltInPresetProjection(options.preset);
156
311
  const packageAddition = projection?.capabilities?.packageAddition;
157
312
  if (!projection) {
@@ -165,13 +320,12 @@ export async function addPackage(options) {
165
320
  blueprint,
166
321
  packageLeafName: options.name,
167
322
  packageName,
323
+ packagePath,
168
324
  nodeVersion,
169
325
  });
170
- if (blueprint.packages?.some((pkg) => pkg.name === packageName || pkg.path === additionPlan.packagePath)) {
171
- throw new Error(`Package Addition conflicts with an existing package definition: ${packageName}`);
172
- }
173
- await assertMissing(path.join(root, additionPlan.packagePath));
174
- const rootUpdatePlan = await planRootUpdates(root, blueprint, packageName, additionPlan.packagePath, additionPlan.workspacePackageGlob, additionPlan.rootTsconfigReferences);
326
+ assertNoPackageConflict(blueprint, packageName, additionPlan.packagePath);
327
+ await assertMissingPackagePath(additionPlan.packagePath, path.join(root, additionPlan.packagePath));
328
+ const rootUpdatePlan = await planRootUpdates(root, blueprint, packageName, additionPlan.packagePath, additionPlan.workspacePackageGlob, additionPlan.rootTsconfigReferences, packageAdditionCatalogDependencies(additionPlan.operations));
175
329
  await mkdir(path.join(root, additionPlan.packagePath), { recursive: true });
176
330
  await renderProject({
177
331
  sourceRoot: additionPlan.sourceRoot,
@@ -183,6 +337,7 @@ export async function addPackage(options) {
183
337
  await writeFile(resolveGeneratedPath(root, textFile.path), textFile.text, "utf8");
184
338
  }
185
339
  await writeFile(path.join(root, "pnpm-workspace.yaml"), rootUpdatePlan.workspaceText, "utf8");
340
+ await writeJson(path.join(root, "package.json"), rootUpdatePlan.rootPackageJson);
186
341
  await writeJson(path.join(root, "tsconfig.json"), rootUpdatePlan.rootTsconfig);
187
- await writeJson(path.join(root, ".project-kit/blueprint.json"), rootUpdatePlan.blueprint);
342
+ await writeJson(path.join(root, ".template/blueprint.json"), rootUpdatePlan.blueprint);
188
343
  }
@@ -9,6 +9,7 @@ export type PresetPackageAdditionOptions = {
9
9
  readonly blueprint: ProjectBlueprint;
10
10
  readonly packageLeafName: string;
11
11
  readonly packageName: string;
12
+ readonly packagePath: string;
12
13
  readonly nodeVersion: string;
13
14
  };
14
15
  export type PresetPackageAdditionPlan = {
@@ -63,4 +64,3 @@ export declare function planNextStepInstructionsForProjection(options: {
63
64
  readonly targetDir: string;
64
65
  readonly plan: PresetProjectionPlan;
65
66
  }): readonly NextStepInstruction[];
66
- //# sourceMappingURL=preset-projection.d.ts.map
@@ -12,9 +12,11 @@ export type PnpmTaskLayer = {
12
12
  readonly installCommand: "pnpm install";
13
13
  readonly checkCommand: "pnpm run check";
14
14
  };
15
- export type DependencyEcosystem = "npm" | "cargo" | "github-actions";
15
+ export type DependencyEcosystem = "npm" | "cargo" | "github-actions" | "docker" | "rust-toolchain";
16
+ export type DependabotDirectory = `/${string}`;
16
17
  export type DependencyMaintenancePolicy = {
17
18
  readonly ecosystems: DependencyEcosystem[];
19
+ readonly directories?: Partial<Record<DependencyEcosystem, DependabotDirectory>>;
18
20
  readonly interval: "weekly";
19
21
  };
20
22
  type ProjectCheckWorkflowOptions = {
@@ -26,4 +28,3 @@ type ProjectCheckWorkflowOptions = {
26
28
  export declare function projectCheckWorkflow(options: ProjectCheckWorkflowOptions): string;
27
29
  export declare function projectDependabotConfig(policy: DependencyMaintenancePolicy): string;
28
30
  export {};
29
- //# sourceMappingURL=project-github.d.ts.map
@@ -44,16 +44,34 @@ export function projectCheckWorkflow(options) {
44
44
  export function projectDependabotConfig(policy) {
45
45
  return [
46
46
  "version: 2",
47
+ "",
47
48
  "updates:",
48
- ...policy.ecosystems.flatMap((ecosystem) => [
49
- ` - package-ecosystem: ${ecosystem}`,
50
- " directory: /",
51
- " schedule:",
52
- ` interval: ${policy.interval}`,
53
- ]),
49
+ ...policy.ecosystems.flatMap((ecosystem) => renderDependabotUpdate(ecosystem, policy.directories?.[ecosystem] ??
50
+ defaultDependabotDirectory(ecosystem), policy.interval)),
54
51
  "",
55
52
  ].join("\n");
56
53
  }
54
+ function renderDependabotUpdate(ecosystem, directory, interval) {
55
+ const lines = [
56
+ ` - package-ecosystem: ${ecosystem}`,
57
+ ` directory: ${renderDependabotDirectory(ecosystem, directory)}`,
58
+ " schedule:",
59
+ ` interval: ${interval}`,
60
+ ];
61
+ if (ecosystem === "npm") {
62
+ lines.push(" ignore:", ' - dependency-name: "@types/node"', " update-types:", " - version-update:semver-major");
63
+ }
64
+ if (ecosystem === "docker") {
65
+ lines.push(" ignore:", " - dependency-name: mcr.microsoft.com/devcontainers/typescript-node", " update-types:", " - version-update:semver-major");
66
+ }
67
+ return lines;
68
+ }
69
+ function renderDependabotDirectory(ecosystem, directory) {
70
+ return ecosystem === "cargo" ? JSON.stringify(directory) : directory;
71
+ }
72
+ function defaultDependabotDirectory(ecosystem) {
73
+ return ecosystem === "docker" ? "/.devcontainer" : "/";
74
+ }
57
75
  function renderCiEnvironmentNeedCommand(need) {
58
76
  if (need.kind === "playwright-browser-assets" &&
59
77
  need.owner.path === "apps/web") {
@@ -21,6 +21,21 @@ export type WriteTextOperation = {
21
21
  to: string;
22
22
  text: string;
23
23
  };
24
+ export type WriteTextFromFragmentsOperation = {
25
+ kind: "writeTextFromFragments";
26
+ to: string;
27
+ fragments: readonly {
28
+ from: string;
29
+ sourceRoot?: string;
30
+ }[];
31
+ };
32
+ export type WriteTextTemplateOperation = {
33
+ kind: "writeTextTemplate";
34
+ from: string;
35
+ to: string;
36
+ sourceRoot?: string;
37
+ replacements: Record<string, string>;
38
+ };
24
39
  export type SetExecutableOperation = {
25
40
  kind: "setExecutable";
26
41
  path: string;
@@ -32,7 +47,7 @@ export type ReplaceAnchorsOperation = {
32
47
  language: "typescript";
33
48
  replacements: Record<string, string>;
34
49
  };
35
- export type RenderOperation = CopyFileOperation | WriteJsonOperation | MergeJsonOperation | WriteTextOperation | SetExecutableOperation | ReplaceAnchorsOperation;
50
+ export type RenderOperation = CopyFileOperation | WriteJsonOperation | MergeJsonOperation | WriteTextOperation | WriteTextFromFragmentsOperation | WriteTextTemplateOperation | SetExecutableOperation | ReplaceAnchorsOperation;
36
51
  export type RenderProjectOptions = {
37
52
  sourceRoot: string;
38
53
  sourceRoots?: Record<string, string>;
@@ -43,4 +58,3 @@ export type RenderProjectOptions = {
43
58
  export declare function renderProject(options: RenderProjectOptions): Promise<void>;
44
59
  export declare function renderNewProject(options: RenderProjectOptions): Promise<void>;
45
60
  export {};
46
- //# sourceMappingURL=renderer.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { constants } from "node:fs";
2
- import { chmod, copyFile, mkdir, mkdtemp, readFile, readdir, rename, rm, rmdir, stat, writeFile } from "node:fs/promises";
2
+ import { chmod, copyFile, mkdir, mkdtemp, readFile, readdir, rename, rm, rmdir, stat, writeFile, } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  function expandTemplatePath(templatePath, variables) {
5
5
  return templatePath.replaceAll(/\{\{([A-Za-z][A-Za-z0-9_]*)\}\}/g, (_, name) => {
@@ -19,7 +19,8 @@ function resolveContainedPath(root, relativePath) {
19
19
  }
20
20
  const resolvedRoot = path.resolve(root);
21
21
  const resolvedPath = path.resolve(resolvedRoot, relativePath);
22
- const insideRoot = resolvedPath === resolvedRoot || resolvedPath.startsWith(`${resolvedRoot}${path.sep}`);
22
+ const insideRoot = resolvedPath === resolvedRoot ||
23
+ resolvedPath.startsWith(`${resolvedRoot}${path.sep}`);
23
24
  if (!insideRoot) {
24
25
  throw new Error(`Renderer path escapes its root: ${relativePath}`);
25
26
  }
@@ -47,7 +48,7 @@ function isRecord(value) {
47
48
  return typeof value === "object" && value !== null && !Array.isArray(value);
48
49
  }
49
50
  function isScalarJsonValue(value) {
50
- return value === null || ["boolean", "number", "string"].includes(typeof value);
51
+ return (value === null || ["boolean", "number", "string"].includes(typeof value));
51
52
  }
52
53
  function serializeJsonValue(value, indentation, pathSegments, multilineArrays, rootKeyOrder) {
53
54
  if (Array.isArray(value)) {
@@ -88,7 +89,13 @@ const packageJsonRootKeyOrder = [
88
89
  "peerDependencies",
89
90
  "optionalDependencies",
90
91
  "engines",
91
- "packageManager"
92
+ "packageManager",
93
+ ];
94
+ const devcontainerJsonRootKeyOrder = [
95
+ "name",
96
+ "build",
97
+ "image",
98
+ "customizations",
92
99
  ];
93
100
  function compareJsonKeys(left, right, pathSegments, rootKeyOrder) {
94
101
  if (pathSegments.length === 0 && rootKeyOrder) {
@@ -101,10 +108,13 @@ function compareJsonKeys(left, right, pathSegments, rootKeyOrder) {
101
108
  return left.localeCompare(right);
102
109
  }
103
110
  function rootKeyOrderForPath(toPath) {
104
- if (path.basename(toPath) !== "package.json") {
105
- return undefined;
111
+ if (path.basename(toPath) === "package.json") {
112
+ return new Map(packageJsonRootKeyOrder.map((key, index) => [key, index]));
113
+ }
114
+ if (toPath.split(path.sep).join("/") === ".devcontainer/devcontainer.json") {
115
+ return new Map(devcontainerJsonRootKeyOrder.map((key, index) => [key, index]));
106
116
  }
107
- return new Map(packageJsonRootKeyOrder.map((key, index) => [key, index]));
117
+ return undefined;
108
118
  }
109
119
  function serializeJson(value, multilineArrays = [], rootKeyOrder) {
110
120
  return `${serializeJsonValue(value, 0, [], new Set(multilineArrays), rootKeyOrder)}\n`;
@@ -151,7 +161,7 @@ const foundationTextFiles = new Set([
151
161
  "README",
152
162
  "README.md",
153
163
  "LICENSE",
154
- "CHANGELOG.md"
164
+ "CHANGELOG.md",
155
165
  ]);
156
166
  function assertFoundationTextPath(relativePath) {
157
167
  const normalizedPath = relativePath.split(path.sep).join("/");
@@ -162,11 +172,20 @@ function assertFoundationTextPath(relativePath) {
162
172
  if (isRootLevel && relativePath.endsWith(".md")) {
163
173
  return;
164
174
  }
165
- if (isRootLevel && (relativePath.endsWith(".yaml") || relativePath.endsWith(".yml"))) {
175
+ if (isRootLevel &&
176
+ (relativePath.endsWith(".yaml") || relativePath.endsWith(".yml"))) {
166
177
  return;
167
178
  }
168
179
  if (isRootLevel &&
169
- ["Cargo.toml", "Cargo.lock", "rustfmt.toml"].includes(normalizedPath)) {
180
+ [
181
+ "Cargo.toml",
182
+ "Cargo.lock",
183
+ "rustfmt.toml",
184
+ "rust-toolchain.toml",
185
+ ].includes(normalizedPath)) {
186
+ return;
187
+ }
188
+ if (/^packages\/[A-Za-z0-9._-]+\/(?:Cargo\.toml|Cargo\.lock|rustfmt\.toml)$/.test(normalizedPath)) {
170
189
  return;
171
190
  }
172
191
  if (/^\.github\/workflows\/[A-Za-z0-9._-]+\.ya?ml$/.test(normalizedPath)) {
@@ -175,6 +194,9 @@ function assertFoundationTextPath(relativePath) {
175
194
  if (/^\.github\/dependabot\.ya?ml$/.test(normalizedPath)) {
176
195
  return;
177
196
  }
197
+ if (normalizedPath === ".devcontainer/Dockerfile") {
198
+ return;
199
+ }
178
200
  throw new Error(`Text output is limited to foundation files: ${relativePath}`);
179
201
  }
180
202
  async function renderWriteText(operation, options) {
@@ -184,6 +206,56 @@ async function renderWriteText(operation, options) {
184
206
  await mkdir(path.dirname(to), { recursive: true });
185
207
  await writeGeneratedFile(to, operation.text);
186
208
  }
209
+ async function renderWriteTextFromFragments(operation, options) {
210
+ const toPath = expandOperationPath(operation.to, options);
211
+ assertFoundationTextPath(toPath);
212
+ const to = resolveContainedPath(options.targetRoot, toPath);
213
+ const texts = await Promise.all(operation.fragments.map(async (fragment) => {
214
+ const sourceRoot = fragment.sourceRoot === undefined
215
+ ? options.sourceRoot
216
+ : options.sourceRoots?.[fragment.sourceRoot];
217
+ if (sourceRoot === undefined) {
218
+ throw new Error(`Unknown renderer source root: ${fragment.sourceRoot}`);
219
+ }
220
+ const fromPath = expandTemplatePath(fragment.from, options.variables ?? {});
221
+ const from = resolveContainedPath(sourceRoot, fromPath);
222
+ return readFile(from, "utf8");
223
+ }));
224
+ await mkdir(path.dirname(to), { recursive: true });
225
+ await writeGeneratedFile(to, texts.map((text) => text.trimEnd()).join("\n\n") + "\n");
226
+ }
227
+ function replaceTextTemplateVariables(sourceText, replacements) {
228
+ const used = new Set();
229
+ const rendered = sourceText.replaceAll(/\{\{([A-Za-z][A-Za-z0-9_]*)\}\}/g, (_placeholder, name) => {
230
+ const replacement = replacements[name];
231
+ if (replacement === undefined) {
232
+ throw new Error(`Missing text template variable: ${name}`);
233
+ }
234
+ used.add(name);
235
+ return replacement;
236
+ });
237
+ for (const name of Object.keys(replacements)) {
238
+ if (!used.has(name)) {
239
+ throw new Error(`Unused text template variable: ${name}`);
240
+ }
241
+ }
242
+ return rendered;
243
+ }
244
+ async function renderWriteTextTemplate(operation, options) {
245
+ const sourceRoot = operation.sourceRoot === undefined
246
+ ? options.sourceRoot
247
+ : options.sourceRoots?.[operation.sourceRoot];
248
+ if (sourceRoot === undefined) {
249
+ throw new Error(`Unknown renderer source root: ${operation.sourceRoot}`);
250
+ }
251
+ const toPath = expandOperationPath(operation.to, options);
252
+ assertFoundationTextPath(toPath);
253
+ const from = resolveContainedPath(sourceRoot, expandTemplatePath(operation.from, options.variables ?? {}));
254
+ const to = resolveContainedPath(options.targetRoot, toPath);
255
+ const sourceText = await readFile(from, "utf8");
256
+ await mkdir(path.dirname(to), { recursive: true });
257
+ await writeGeneratedFile(to, replaceTextTemplateVariables(sourceText, operation.replacements));
258
+ }
187
259
  async function renderSetExecutable(operation, options) {
188
260
  const filePath = resolveContainedPath(options.targetRoot, expandOperationPath(operation.path, options));
189
261
  const currentMode = (await stat(filePath)).mode;
@@ -224,7 +296,7 @@ async function findTypeScriptAnchorRanges(sourceText) {
224
296
  anchors.push({
225
297
  name,
226
298
  start: comment.pos,
227
- end: comment.end
299
+ end: comment.end,
228
300
  });
229
301
  }
230
302
  ts.forEachChild(node, collectNodeAnchors);
@@ -296,7 +368,10 @@ async function copyGeneratedFile(from, to) {
296
368
  }
297
369
  async function writeGeneratedFile(to, text, overwrite = false) {
298
370
  try {
299
- await writeFile(to, text, { encoding: "utf8", flag: overwrite ? "w" : "wx" });
371
+ await writeFile(to, text, {
372
+ encoding: "utf8",
373
+ flag: overwrite ? "w" : "wx",
374
+ });
300
375
  }
301
376
  catch (error) {
302
377
  if (isNodeError(error) && error.code === "EEXIST") {
@@ -323,6 +398,14 @@ export async function renderProject(options) {
323
398
  await renderWriteText(operation, options);
324
399
  continue;
325
400
  }
401
+ if (operation.kind === "writeTextFromFragments") {
402
+ await renderWriteTextFromFragments(operation, options);
403
+ continue;
404
+ }
405
+ if (operation.kind === "writeTextTemplate") {
406
+ await renderWriteTextTemplate(operation, options);
407
+ continue;
408
+ }
326
409
  if (operation.kind === "setExecutable") {
327
410
  await renderSetExecutable(operation, options);
328
411
  continue;
@@ -1,2 +1 @@
1
1
  export declare function packageTemplateRoot(moduleDir: string, ...segments: string[]): string;
2
- //# sourceMappingURL=runtime-paths.d.ts.map
@@ -0,0 +1,34 @@
1
+ import type { PresetProjectionPlan } from "./preset-projection.js";
2
+ import type { RenderOperation } from "./renderer.js";
3
+ export type TemplateBoundaryDebt = {
4
+ readonly preset: string;
5
+ readonly generatedPath: string;
6
+ readonly owningFunction: string;
7
+ readonly reason: string;
8
+ };
9
+ export type TemplateBoundaryViolation = {
10
+ readonly preset: string;
11
+ readonly generatedPath: string;
12
+ readonly owningFunction: string;
13
+ readonly operationKind: RenderOperation["kind"];
14
+ readonly sourceFilePath: string;
15
+ readonly allowlistReason?: string;
16
+ };
17
+ export type TemplateBoundaryCheckProjection = {
18
+ readonly name: string;
19
+ readonly sourceFilePath: string;
20
+ readonly plan: PresetProjectionPlan;
21
+ };
22
+ export type TemplateBoundaryCheckResult = {
23
+ readonly ok: boolean;
24
+ readonly violations: readonly TemplateBoundaryViolation[];
25
+ readonly allowlistedDebt: readonly TemplateBoundaryViolation[];
26
+ readonly unusedAllowlistEntries: readonly TemplateBoundaryDebt[];
27
+ };
28
+ export type CheckTemplateSourceBoundaryOptions = {
29
+ readonly projections: readonly TemplateBoundaryCheckProjection[];
30
+ readonly allowlist?: readonly TemplateBoundaryDebt[];
31
+ };
32
+ export declare const templateBoundaryDebtAllowlist: readonly TemplateBoundaryDebt[];
33
+ export declare function isProtectedGeneratedPath(generatedPath: string): boolean;
34
+ export declare function checkTemplateSourceBoundary({ projections, allowlist, }: CheckTemplateSourceBoundaryOptions): Promise<TemplateBoundaryCheckResult>;