@ykdz/template 0.0.3 → 0.0.5

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 (95) hide show
  1. package/README.md +1 -1
  2. package/dist/src/cli.d.ts +0 -1
  3. package/dist/src/cli.js +26 -2
  4. package/dist/src/declarations.d.ts +38 -3
  5. package/dist/src/declarations.js +75 -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 +20 -2
  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-support.d.ts +5 -0
  19. package/dist/src/package-addition-support.js +4 -0
  20. package/dist/src/package-addition.d.ts +2 -1
  21. package/dist/src/package-addition.js +557 -55
  22. package/dist/src/package-linking.d.ts +55 -0
  23. package/dist/src/package-linking.js +116 -0
  24. package/dist/src/preset-projection.d.ts +4 -2
  25. package/dist/src/project-github.d.ts +3 -2
  26. package/dist/src/project-github.js +24 -6
  27. package/dist/src/renderer.d.ts +16 -2
  28. package/dist/src/renderer.js +96 -12
  29. package/dist/src/runtime-paths.d.ts +0 -1
  30. package/dist/src/template-boundary-check.d.ts +34 -0
  31. package/dist/src/template-boundary-check.js +448 -0
  32. package/dist/src/toolchain-resolution.d.ts +0 -1
  33. package/dist/templates/hono-api/projection.d.ts +2 -3
  34. package/dist/templates/hono-api/projection.js +258 -76
  35. package/dist/templates/projection-plans.d.ts +0 -1
  36. package/dist/templates/registry.d.ts +0 -1
  37. package/dist/templates/registry.js +5 -2
  38. package/dist/templates/rust-bin/projection.d.ts +2 -3
  39. package/dist/templates/rust-bin/projection.js +171 -53
  40. package/dist/templates/ts-lib/projection.d.ts +2 -3
  41. package/dist/templates/ts-lib/projection.js +251 -87
  42. package/dist/templates/vue-app/projection.d.ts +2 -3
  43. package/dist/templates/vue-app/projection.js +266 -94
  44. package/dist/templates/vue-hono-app/projection.d.ts +0 -1
  45. package/dist/templates/vue-hono-app/projection.js +232 -108
  46. package/package.json +10 -5
  47. package/pnpm-workspace.yaml +1 -0
  48. package/templates/hono-api/.github/dependabot.yml +13 -0
  49. package/templates/hono-api/src/server.ts +1 -1
  50. package/templates/hono-api/test/app.test.ts +1 -1
  51. package/templates/rust-bin/.github/dependabot.yml +18 -1
  52. package/templates/rust-bin/rust-toolchain.toml +3 -0
  53. package/templates/rust-bin/rustfmt.toml +2 -0
  54. package/templates/shared/devcontainer/browser-test.Dockerfile +2 -0
  55. package/templates/shared/devcontainer/node-pnpm.Dockerfile +6 -0
  56. package/templates/shared/devcontainer/rust.Dockerfile +16 -0
  57. package/templates/shared/editor-customization/capabilities.json +56 -0
  58. package/templates/ts-lib/.github/dependabot.yml +13 -0
  59. package/templates/ts-lib/src/index.ts +5 -1
  60. package/templates/ts-lib/src/name-schema.ts +3 -0
  61. package/templates/vue-app/.github/dependabot.yml +13 -0
  62. package/templates/vue-app/.github/workflows/check.yml +1 -1
  63. package/templates/vue-app/src/App.vue +1 -1
  64. package/templates/vue-app/test/app.test.ts +1 -1
  65. package/templates/vue-hono-app/.github/dependabot.yml +13 -0
  66. package/templates/vue-hono-app/web/src/App.vue +2 -2
  67. package/templates/vue-hono-app/web/test/app.test.ts +1 -1
  68. package/dist/src/cli.d.ts.map +0 -1
  69. package/dist/src/declarations.d.ts.map +0 -1
  70. package/dist/src/dependency-catalog.d.ts.map +0 -1
  71. package/dist/src/devcontainer.d.ts.map +0 -1
  72. package/dist/src/editor-customization.d.ts.map +0 -1
  73. package/dist/src/generation-context.d.ts.map +0 -1
  74. package/dist/src/module-graph.d.ts.map +0 -1
  75. package/dist/src/next-step-instructions.d.ts.map +0 -1
  76. package/dist/src/package-addition.d.ts.map +0 -1
  77. package/dist/src/preset-projection.d.ts.map +0 -1
  78. package/dist/src/project-github.d.ts.map +0 -1
  79. package/dist/src/renderer.d.ts.map +0 -1
  80. package/dist/src/runtime-paths.d.ts.map +0 -1
  81. package/dist/src/toolchain-resolution.d.ts.map +0 -1
  82. package/dist/templates/hono-api/projection.d.ts.map +0 -1
  83. package/dist/templates/projection-plans.d.ts.map +0 -1
  84. package/dist/templates/registry.d.ts.map +0 -1
  85. package/dist/templates/rust-bin/projection.d.ts.map +0 -1
  86. package/dist/templates/ts-lib/projection.d.ts.map +0 -1
  87. package/dist/templates/vue-app/projection.d.ts.map +0 -1
  88. package/dist/templates/vue-hono-app/projection.d.ts.map +0 -1
  89. package/templates/hono-api/projection.ts +0 -453
  90. package/templates/projection-plans.ts +0 -135
  91. package/templates/registry.ts +0 -59
  92. package/templates/rust-bin/projection.ts +0 -346
  93. package/templates/ts-lib/projection.ts +0 -454
  94. package/templates/vue-app/projection.ts +0 -687
  95. package/templates/vue-hono-app/projection.ts +0 -669
@@ -1,28 +1,186 @@
1
1
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { findBuiltInPresetProjection } from "../templates/registry.js";
3
+ import { builtInPresetProjections, findBuiltInPresetProjection, } from "../templates/registry.js";
4
4
  import { validateProjectBlueprint, } from "./declarations.js";
5
+ import { collectGeneratedManifestCatalogReferences, pnpmWorkspaceYamlWithCatalogDependencies, } from "./dependency-catalog.js";
6
+ import { PackageAdditionSupport } from "./package-addition-support.js";
7
+ import { assertTypeScriptPackageBoundaryForLinkIntent, packageTurboTasks, planPackageLinks, } from "./package-linking.js";
5
8
  import { renderProject } from "./renderer.js";
6
- function projectNameFromBlueprint(blueprint) {
9
+ function projectNameFromBlueprint(blueprint, fallbackProjectName) {
7
10
  const firstPackage = blueprint.packages?.[0];
8
11
  const match = firstPackage?.name.match(/^@([^/]+)\//);
9
- if (!match) {
10
- throw new Error("Cannot infer workspace package scope from the stored Project Blueprint");
12
+ if (match) {
13
+ return match[1];
11
14
  }
12
- return match[1];
15
+ if (/^[a-z0-9][a-z0-9._-]*$/.test(fallbackProjectName)) {
16
+ return fallbackProjectName;
17
+ }
18
+ throw new Error("Cannot infer workspace package scope from the stored Project Blueprint or root package.json");
13
19
  }
14
20
  function assertSafePackageLeaf(name) {
15
21
  if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) {
16
22
  throw new Error("--name must be a lowercase package leaf name using letters, numbers, and hyphens");
17
23
  }
18
24
  }
25
+ const reservedPackagePathCollections = new Set([
26
+ ".devcontainer",
27
+ ".git",
28
+ ".github",
29
+ ".template",
30
+ "build",
31
+ "coverage",
32
+ "dist",
33
+ "node_modules",
34
+ "out",
35
+ "target",
36
+ ]);
37
+ function assertSafePackagePathSegment(packagePath, segment) {
38
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(segment)) {
39
+ throw new Error(`Package Path ${packagePath} must use safe path segments with lowercase letters, numbers, and hyphens`);
40
+ }
41
+ }
42
+ function validateExplicitPackagePath(packagePath) {
43
+ if (path.isAbsolute(packagePath)) {
44
+ throw new Error(`Package Path ${packagePath} must be relative`);
45
+ }
46
+ if (packagePath !== packagePath.trim()) {
47
+ throw new Error(`Package Path ${packagePath} must not contain whitespace`);
48
+ }
49
+ if (packagePath === ".." ||
50
+ packagePath.startsWith("../") ||
51
+ packagePath.endsWith("/..") ||
52
+ packagePath.includes("/../")) {
53
+ throw new Error(`Package Path ${packagePath} must not escape the workspace`);
54
+ }
55
+ const normalized = path.posix.normalize(packagePath);
56
+ if (normalized !== packagePath || packagePath.includes("\\")) {
57
+ throw new Error(`Package Path ${packagePath} must be exactly two safe path segments`);
58
+ }
59
+ const segments = packagePath.split("/");
60
+ if (segments.length !== 2) {
61
+ throw new Error(`Package Path ${packagePath} must be exactly two safe path segments`);
62
+ }
63
+ const [collection, packageDirectory] = segments;
64
+ if (!collection || !packageDirectory) {
65
+ throw new Error(`Package Path ${packagePath} must be exactly two safe path segments`);
66
+ }
67
+ if (reservedPackagePathCollections.has(collection)) {
68
+ throw new Error(`Package Path ${packagePath} uses reserved collection ${collection}`);
69
+ }
70
+ assertSafePackagePathSegment(packagePath, collection);
71
+ assertSafePackagePathSegment(packagePath, packageDirectory);
72
+ if (reservedPackagePathCollections.has(packageDirectory)) {
73
+ throw new Error(`Package Path ${packagePath} uses reserved package directory ${packageDirectory}`);
74
+ }
75
+ return packagePath;
76
+ }
77
+ function defaultPackagePathForPreset(preset, packageLeafName) {
78
+ return preset === "ts-lib"
79
+ ? `packages/${packageLeafName}`
80
+ : `apps/${packageLeafName}`;
81
+ }
82
+ function supportedPackageAdditionPresetNames() {
83
+ return builtInPresetProjections
84
+ .filter((projection) => projection.metadata.packageAdditionSupport ===
85
+ PackageAdditionSupport.Supported)
86
+ .map((projection) => projection.metadata.name);
87
+ }
88
+ function formatUnsupportedPackageAdditionPresetError(preset) {
89
+ return [
90
+ `Preset ${preset} cannot be used for Package Addition.`,
91
+ "It can still initialize a Generated Repository, but it cannot be added to an existing one.",
92
+ `Supported Package Addition presets: ${supportedPackageAdditionPresetNames().join(", ")}`,
93
+ ].join("\n");
94
+ }
95
+ function assertNoPackageConflict(blueprint, packageName, packagePath) {
96
+ const existingByName = blueprint.packages?.find((pkg) => pkg.name === packageName);
97
+ if (existingByName) {
98
+ throw new Error(`Package Path ${packagePath} conflicts with existing package ${existingByName.name} at ${existingByName.path}`);
99
+ }
100
+ const existingByPath = blueprint.packages?.find((pkg) => pkg.path === packagePath);
101
+ if (existingByPath) {
102
+ throw new Error(`Package Path ${packagePath} conflicts with existing package ${existingByPath.name} at ${existingByPath.path}`);
103
+ }
104
+ }
105
+ function formatAvailablePackagePaths(blueprint) {
106
+ return (blueprint.packages ?? [])
107
+ .map((projectPackage) => projectPackage.path)
108
+ .join(", ");
109
+ }
110
+ function packageLinkIntentForSingleConsumer(options) {
111
+ if (options.consumerPackagePath === options.providerPackagePath) {
112
+ throw new Error(`Package Link Intent cannot link ${options.providerPackagePath} from itself`);
113
+ }
114
+ const consumer = options.blueprint.packages?.find((projectPackage) => projectPackage.path === options.consumerPackagePath);
115
+ if (!consumer) {
116
+ const availablePackagePaths = formatAvailablePackagePaths(options.blueprint);
117
+ throw new Error([
118
+ `Unknown Package Path for --link-from: ${options.consumerPackagePath}`,
119
+ availablePackagePaths
120
+ ? `Available Package Paths: ${availablePackagePaths}`
121
+ : undefined,
122
+ ]
123
+ .filter((line) => line !== undefined)
124
+ .join("\n"));
125
+ }
126
+ return {
127
+ consumerPackagePath: options.consumerPackagePath,
128
+ providerPackagePath: options.providerPackagePath,
129
+ };
130
+ }
131
+ function packageLinkIntentsForConsumers(options) {
132
+ const packageLinkIntents = [];
133
+ const seenConsumerPackagePaths = new Set();
134
+ for (const consumerPackagePath of options.consumerPackagePaths) {
135
+ if (seenConsumerPackagePaths.has(consumerPackagePath)) {
136
+ continue;
137
+ }
138
+ seenConsumerPackagePaths.add(consumerPackagePath);
139
+ packageLinkIntents.push(packageLinkIntentForSingleConsumer({
140
+ blueprint: options.blueprint,
141
+ consumerPackagePath,
142
+ providerPackagePath: options.providerPackagePath,
143
+ }));
144
+ }
145
+ return packageLinkIntents;
146
+ }
147
+ async function assertPackageLinkIntentConsumersAreTypeScriptBoundaries(options) {
148
+ for (const intent of options.packageLinkIntents) {
149
+ const consumer = options.blueprint.packages?.find((projectPackage) => projectPackage.path === intent.consumerPackagePath);
150
+ if (!consumer) {
151
+ continue;
152
+ }
153
+ if (consumer.role !== undefined && consumer.sourcePreset !== undefined) {
154
+ assertTypeScriptPackageBoundaryForLinkIntent(consumer, "consumer");
155
+ continue;
156
+ }
157
+ try {
158
+ const manifest = await readJson(path.join(options.root, consumer.path, "package.json"));
159
+ const scripts = isRecord(manifest) ? manifest.scripts : undefined;
160
+ const hasTypeScriptPackageShape = isRecord(manifest) &&
161
+ manifest.type === "module" &&
162
+ isRecord(scripts) &&
163
+ typeof scripts.typecheck === "string";
164
+ if (hasTypeScriptPackageShape) {
165
+ continue;
166
+ }
167
+ assertTypeScriptPackageBoundaryForLinkIntent(consumer, "consumer");
168
+ }
169
+ catch (error) {
170
+ if (isNodeError(error) && error.code === "ENOENT") {
171
+ assertTypeScriptPackageBoundaryForLinkIntent(consumer, "consumer");
172
+ }
173
+ throw error;
174
+ }
175
+ }
176
+ }
19
177
  async function readJson(filePath) {
20
178
  return JSON.parse(await readFile(filePath, "utf8"));
21
179
  }
22
180
  async function writeJson(filePath, value) {
23
181
  await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
24
182
  }
25
- async function assertMissing(targetPath) {
183
+ async function assertMissingPackagePath(packagePath, targetPath) {
26
184
  try {
27
185
  await stat(targetPath);
28
186
  }
@@ -32,7 +190,7 @@ async function assertMissing(targetPath) {
32
190
  }
33
191
  throw error;
34
192
  }
35
- throw new Error(`Package Addition would overwrite an existing path: ${targetPath}`);
193
+ throw new Error(`Package Path ${packagePath} conflicts with existing filesystem path ${targetPath}`);
36
194
  }
37
195
  function resolveGeneratedPath(root, relativePath) {
38
196
  if (path.isAbsolute(relativePath)) {
@@ -53,11 +211,39 @@ function isNodeError(error) {
53
211
  function isRecord(value) {
54
212
  return typeof value === "object" && value !== null && !Array.isArray(value);
55
213
  }
214
+ function assertRootPackageJson(input) {
215
+ if (!isRecord(input)) {
216
+ throw new Error("Cannot update root Package Addition scripts: package.json must be an object");
217
+ }
218
+ if (!isRecord(input.scripts)) {
219
+ throw new Error("Cannot update root Package Addition scripts: scripts must be an object");
220
+ }
221
+ for (const scriptName of ["check", "fix"]) {
222
+ if (typeof input.scripts[scriptName] !== "string") {
223
+ throw new Error(`Cannot update root Package Addition scripts: scripts.${scriptName} must be a string`);
224
+ }
225
+ }
226
+ }
56
227
  async function readGeneratedWorkspaceBlueprint(root) {
57
- const blueprintPath = path.join(root, ".project-kit/blueprint.json");
58
- const result = validateProjectBlueprint(await readJson(blueprintPath));
228
+ const blueprintPath = path.join(root, ".template/blueprint.json");
229
+ let blueprintJson;
230
+ try {
231
+ blueprintJson = await readJson(blueprintPath);
232
+ }
233
+ catch (error) {
234
+ if (error instanceof SyntaxError ||
235
+ (isNodeError(error) && error.code === "ENOENT")) {
236
+ throw new Error("Package Addition requires a valid .template/blueprint.json");
237
+ }
238
+ throw error;
239
+ }
240
+ const result = validateProjectBlueprint(blueprintJson);
59
241
  if (!result.ok) {
60
- throw new Error("Package Addition requires a valid .project-kit/blueprint.json");
242
+ if (isRecord(blueprintJson) &&
243
+ blueprintJson.projectKind === "single-package") {
244
+ throw new Error("Package Addition only supports existing workspace Generated Repositories");
245
+ }
246
+ throw new Error("Package Addition requires a valid .template/blueprint.json");
61
247
  }
62
248
  const blueprint = result.value;
63
249
  if (blueprint.projectKind !== "multi-package") {
@@ -73,16 +259,23 @@ async function readGeneratedWorkspaceBlueprint(root) {
73
259
  await stat(path.join(root, "pnpm-workspace.yaml"));
74
260
  return blueprint;
75
261
  }
76
- async function readGeneratedRepositoryNodeVersion(root) {
262
+ async function readGeneratedRepositoryPackageMetadata(root) {
77
263
  const packageJson = await readJson(path.join(root, "package.json"));
78
- if (!isRecord(packageJson) || !isRecord(packageJson.engines)) {
264
+ if (!isRecord(packageJson)) {
265
+ throw new Error("Package Addition requires root package.json to be an object");
266
+ }
267
+ const projectName = packageJson.name;
268
+ if (typeof projectName !== "string" || projectName.length === 0) {
269
+ throw new Error("Package Addition requires root package.json to declare name");
270
+ }
271
+ if (!isRecord(packageJson.engines)) {
79
272
  throw new Error("Package Addition requires root package.json to declare engines.node");
80
273
  }
81
274
  const nodeVersion = packageJson.engines.node;
82
275
  if (typeof nodeVersion !== "string" || nodeVersion.length === 0) {
83
276
  throw new Error("Package Addition requires root package.json to declare engines.node");
84
277
  }
85
- return nodeVersion;
278
+ return { projectName, nodeVersion };
86
279
  }
87
280
  function workspaceTextWithPackageGlob(text, glob) {
88
281
  if (text.includes(` - ${glob}`)) {
@@ -94,85 +287,390 @@ function workspaceTextWithPackageGlob(text, glob) {
94
287
  }
95
288
  return nextText;
96
289
  }
97
- function assertRootTsconfig(input) {
98
- if (!isRecord(input)) {
99
- throw new Error("Cannot update root TypeScript project references: tsconfig.json must be an object");
100
- }
101
- const references = input.references;
102
- if (references === undefined) {
103
- return;
104
- }
105
- if (!Array.isArray(references)) {
106
- throw new Error("Cannot update root TypeScript project references: references must be an array");
107
- }
108
- for (const reference of references) {
109
- if (!isRecord(reference) || typeof reference.path !== "string") {
110
- throw new Error("Cannot update root TypeScript project references: each reference must have a string path");
290
+ function packageAdditionCatalogDependencies(operations) {
291
+ const manifests = [];
292
+ for (const operation of operations) {
293
+ if (operation.kind !== "writeJson" ||
294
+ path.basename(operation.to) !== "package.json") {
295
+ continue;
296
+ }
297
+ if (!isRecord(operation.value)) {
298
+ throw new Error(`Package Addition package manifest must be an object: ${operation.to}`);
111
299
  }
300
+ manifests.push(operation.value);
112
301
  }
302
+ return collectGeneratedManifestCatalogReferences(manifests);
113
303
  }
114
- function rootTsconfigWithReferences(input, referencePaths) {
115
- assertRootTsconfig(input);
116
- const tsconfig = input;
117
- const references = tsconfig.references ?? [];
118
- for (const referencePath of referencePaths) {
119
- if (!references.some((reference) => reference.path === referencePath)) {
120
- references.push({ path: referencePath });
304
+ function generatedPackageManifestFromOperations(operations, packagePath) {
305
+ for (const operation of operations) {
306
+ if (operation.kind !== "writeJson" ||
307
+ operation.to !== `${packagePath}/package.json`) {
308
+ continue;
121
309
  }
310
+ if (!isRecord(operation.value)) {
311
+ throw new Error(`Package Addition package manifest must be an object: ${operation.to}`);
312
+ }
313
+ return operation.value;
314
+ }
315
+ return undefined;
316
+ }
317
+ async function packageManifestsForTaskGraph(options) {
318
+ const manifests = [];
319
+ for (const packageDefinition of options.blueprint.packages ?? []) {
320
+ const manifestOverride = options.manifestOverrides?.get(packageDefinition.path);
321
+ const manifestFromOperations = generatedPackageManifestFromOperations(options.operations, packageDefinition.path);
322
+ manifests.push(manifestOverride ??
323
+ manifestFromOperations ??
324
+ (await readJson(path.join(options.root, packageDefinition.path, "package.json"))));
122
325
  }
123
- return { ...tsconfig, references };
326
+ return manifests;
124
327
  }
125
- function blueprintWithPackage(blueprint, packageName, packagePath) {
328
+ function blueprintWithPackage(blueprint, packageName, packagePath, packageRole, packageSourcePreset, packageLinkIntents) {
126
329
  const nextBlueprint = {
127
330
  ...blueprint,
128
331
  packages: [
129
332
  ...(blueprint.packages ?? []),
130
- { name: packageName, path: packagePath },
333
+ {
334
+ name: packageName,
335
+ path: packagePath,
336
+ role: packageRole,
337
+ sourcePreset: packageSourcePreset,
338
+ },
131
339
  ],
340
+ packageLinkIntents: packageLinkIntents.length > 0
341
+ ? [...(blueprint.packageLinkIntents ?? []), ...packageLinkIntents]
342
+ : blueprint.packageLinkIntents,
132
343
  };
133
344
  const result = validateProjectBlueprint(nextBlueprint);
134
345
  if (!result.ok) {
135
- throw new Error("Package Addition would write an invalid .project-kit/blueprint.json");
346
+ throw new Error("Package Addition would write an invalid .template/blueprint.json");
136
347
  }
137
348
  return result.value;
138
349
  }
139
- async function planRootUpdates(root, blueprint, packageName, packagePath, workspacePackageGlob, rootTsconfigReferences) {
140
- const workspaceText = workspaceTextWithPackageGlob(await readFile(path.join(root, "pnpm-workspace.yaml"), "utf8"), workspacePackageGlob);
141
- const rootTsconfig = rootTsconfigWithReferences(await readJson(path.join(root, "tsconfig.json")), rootTsconfigReferences);
350
+ function assertObjectManifest(input, packagePath) {
351
+ if (!isRecord(input)) {
352
+ throw new Error(`Cannot update Package Link Intent manifest for ${packagePath}: package.json must be an object`);
353
+ }
354
+ }
355
+ function recordSortedByKey(record) {
356
+ return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
357
+ }
358
+ async function consumerManifestUpdatesForPackageLinkIntents(options) {
359
+ const updates = [];
360
+ for (const [packagePath, packageLinkDependencies,] of options.manifestDependenciesByPackagePath) {
361
+ const manifest = await readJson(path.join(options.root, packagePath, "package.json"));
362
+ assertObjectManifest(manifest, packagePath);
363
+ if (manifest.dependencies !== undefined &&
364
+ !isRecord(manifest.dependencies)) {
365
+ throw new Error(`Cannot update Package Link Intent manifest for ${packagePath}: dependencies must be an object`);
366
+ }
367
+ updates.push({
368
+ packagePath,
369
+ manifest: {
370
+ ...manifest,
371
+ dependencies: recordSortedByKey({
372
+ ...manifest.dependencies,
373
+ ...packageLinkDependencies,
374
+ }),
375
+ },
376
+ });
377
+ }
378
+ return updates;
379
+ }
380
+ function workspacePackageGlobsFromBlueprint(blueprint) {
381
+ const globs = [];
382
+ for (const packageDefinition of blueprint.packages ?? []) {
383
+ const [workspaceDir] = packageDefinition.path.split("/");
384
+ if (!workspaceDir) {
385
+ throw new Error(`Cannot update root Package Addition scripts: invalid package path ${packageDefinition.path}`);
386
+ }
387
+ const glob = `${workspaceDir}/*`;
388
+ if (!globs.includes(glob)) {
389
+ globs.push(glob);
390
+ }
391
+ }
392
+ return globs;
393
+ }
394
+ function turboPackageTaskCommand(task, workspacePackageGlobs) {
395
+ const filters = workspacePackageGlobs.map((glob) => `--filter './${glob}'`);
396
+ return [`turbo run ${task}`, ...filters].join(" ");
397
+ }
398
+ function rootScriptWithTurboPackageTask(script, task, workspacePackageGlobs) {
399
+ const commands = script.split(" && ");
400
+ const turboCommandIndex = commands.findIndex((command) => command.startsWith(`turbo run ${task}`));
401
+ if (turboCommandIndex === -1) {
402
+ throw new Error(`Cannot update root Package Addition scripts: scripts.${task} must run Turbo package tasks`);
403
+ }
404
+ commands[turboCommandIndex] = turboPackageTaskCommand(task, workspacePackageGlobs);
405
+ return commands.join(" && ");
406
+ }
407
+ function turboTaskNamesForPackageManifests(manifests) {
408
+ const taskNames = [];
409
+ for (const taskName of [
410
+ "typecheck",
411
+ "build",
412
+ "test",
413
+ "test:e2e",
414
+ "check",
415
+ ]) {
416
+ if (manifests.some((manifest) => typeof manifest.scripts?.[taskName] === "string")) {
417
+ taskNames.push(taskName);
418
+ }
419
+ }
420
+ return taskNames;
421
+ }
422
+ function rootScriptWithTurboPackageTasks(options) {
423
+ const rootCommands = options.script
424
+ .split(" && ")
425
+ .filter((command) => !command.startsWith("turbo run "));
426
+ const turboCommands = options.taskNames.map((taskName) => turboPackageTaskCommand(taskName, options.workspacePackageGlobs));
427
+ return [...rootCommands, ...turboCommands].join(" && ");
428
+ }
429
+ function rootPackageJsonWithPackageTaskFilters(input, blueprint, packageManifests) {
430
+ assertRootPackageJson(input);
431
+ const workspacePackageGlobs = workspacePackageGlobsFromBlueprint(blueprint);
432
+ const taskNames = turboTaskNamesForPackageManifests(packageManifests);
433
+ return {
434
+ ...input,
435
+ scripts: {
436
+ ...input.scripts,
437
+ check: rootScriptWithTurboPackageTasks({
438
+ script: input.scripts.check,
439
+ taskNames,
440
+ workspacePackageGlobs,
441
+ }),
442
+ fix: rootScriptWithTurboPackageTask(input.scripts.fix, "fix", workspacePackageGlobs),
443
+ },
444
+ };
445
+ }
446
+ function manifestExportUsesCompiledRuntime(value) {
447
+ if (!isRecord(value)) {
448
+ return false;
449
+ }
450
+ const rootExport = value["."];
451
+ if (!isRecord(rootExport)) {
452
+ return false;
453
+ }
454
+ return (typeof rootExport.default === "string" &&
455
+ rootExport.default.startsWith("./dist/"));
456
+ }
457
+ function packageManifestsNeedDependencyBuilds(manifests) {
458
+ const compiledPackageNames = new Set(manifests
459
+ .filter((manifest) => manifestExportUsesCompiledRuntime(manifest.exports))
460
+ .map((manifest) => manifest.name)
461
+ .filter((name) => typeof name === "string"));
462
+ if (compiledPackageNames.size === 0) {
463
+ return false;
464
+ }
465
+ return manifests.some((manifest) => Object.entries(manifest.dependencies ?? {}).some(([dependencyName, specifier]) => specifier === "workspace:*" && compiledPackageNames.has(dependencyName)));
466
+ }
467
+ function turboConfigForPackageManifests(manifests) {
468
+ return {
469
+ tasks: packageTurboTasks({
470
+ dependencyBuildsRequired: packageManifestsNeedDependencyBuilds(manifests),
471
+ }),
472
+ };
473
+ }
474
+ function rootPackageJsonWithSharedOxcRuntimeDependencies(input, requiresSharedOxcConfiguration) {
475
+ if (!requiresSharedOxcConfiguration) {
476
+ return input;
477
+ }
478
+ if (input.type !== undefined && input.type !== "module") {
479
+ throw new Error('Cannot update root OXC configuration: package.json type must be "module" or omitted');
480
+ }
481
+ if (input.devDependencies !== undefined && !isRecord(input.devDependencies)) {
482
+ throw new Error("Cannot update root OXC configuration dependencies: devDependencies must be an object");
483
+ }
142
484
  return {
143
- blueprint: blueprintWithPackage(blueprint, packageName, packagePath),
144
- rootTsconfig,
485
+ ...input,
486
+ type: "module",
487
+ devDependencies: {
488
+ ...input.devDependencies,
489
+ oxfmt: input.devDependencies?.oxfmt ?? "catalog:",
490
+ oxlint: input.devDependencies?.oxlint ?? "catalog:",
491
+ },
492
+ };
493
+ }
494
+ function catalogDependenciesWithSharedOxcRuntimeDependencies(catalogDependencies, requiresSharedOxcConfiguration) {
495
+ const dependencies = [...catalogDependencies];
496
+ if (!requiresSharedOxcConfiguration) {
497
+ return dependencies;
498
+ }
499
+ for (const dependency of ["oxfmt", "oxlint"]) {
500
+ if (!dependencies.includes(dependency)) {
501
+ dependencies.push(dependency);
502
+ }
503
+ }
504
+ return dependencies;
505
+ }
506
+ async function planRootUpdates(root, blueprint, packageName, packagePath, packageRole, packageSourcePreset, packageLinkIntents, workspacePackageGlob, catalogDependencies, requiresSharedOxcConfiguration, operations) {
507
+ const nextBlueprint = blueprintWithPackage(blueprint, packageName, packagePath, packageRole, packageSourcePreset, packageLinkIntents);
508
+ const consumerManifestUpdates = packageLinkIntents.length > 0
509
+ ? await consumerManifestUpdatesForPackageLinkIntents({
510
+ root,
511
+ manifestDependenciesByPackagePath: planPackageLinks([
512
+ {
513
+ name: packageName,
514
+ path: packagePath,
515
+ role: packageRole,
516
+ sourcePreset: packageSourcePreset,
517
+ },
518
+ ], packageLinkIntents).manifestDependenciesByPackagePath,
519
+ })
520
+ : [];
521
+ const manifestOverrides = new Map(consumerManifestUpdates.map((update) => [
522
+ update.packagePath,
523
+ update.manifest,
524
+ ]));
525
+ const packageManifests = await packageManifestsForTaskGraph({
526
+ root,
527
+ blueprint: nextBlueprint,
528
+ operations,
529
+ manifestOverrides,
530
+ });
531
+ const workspaceText = pnpmWorkspaceYamlWithCatalogDependencies(workspaceTextWithPackageGlob(await readFile(path.join(root, "pnpm-workspace.yaml"), "utf8"), workspacePackageGlob), catalogDependenciesWithSharedOxcRuntimeDependencies(catalogDependencies, requiresSharedOxcConfiguration));
532
+ const rootPackageJson = rootPackageJsonWithPackageTaskFilters(await readJson(path.join(root, "package.json")), nextBlueprint, packageManifests);
533
+ return {
534
+ blueprint: nextBlueprint,
535
+ rootPackageJson: rootPackageJsonWithSharedOxcRuntimeDependencies(rootPackageJson, requiresSharedOxcConfiguration),
536
+ turboConfig: turboConfigForPackageManifests(packageManifests),
145
537
  workspaceText,
538
+ consumerManifestUpdates,
146
539
  };
147
540
  }
541
+ async function readTextIfExists(filePath) {
542
+ try {
543
+ return await readFile(filePath, "utf8");
544
+ }
545
+ catch (error) {
546
+ if (isNodeError(error) && error.code === "ENOENT") {
547
+ return undefined;
548
+ }
549
+ throw error;
550
+ }
551
+ }
552
+ async function writeIfMissing(filePath, text) {
553
+ if ((await readTextIfExists(filePath)) !== undefined) {
554
+ return;
555
+ }
556
+ await writeFile(filePath, text, "utf8");
557
+ }
558
+ async function ensureRootOxfmtConfig(root, sharedOxcRoot) {
559
+ await writeIfMissing(path.join(root, "oxfmt.config.ts"), await readFile(path.join(sharedOxcRoot, "oxfmt.config.ts"), "utf8"));
560
+ }
561
+ async function ensureRootOxlintConfig(options) {
562
+ const nodeConfig = await readFile(path.join(options.sharedOxcRoot, "node", "oxlint.config.ts"), "utf8");
563
+ const vueConfig = await readFile(path.join(options.sharedOxcRoot, "vue", "oxlint.config.ts"), "utf8");
564
+ const targetPath = path.join(options.root, "oxlint.config.ts");
565
+ const currentConfig = await readTextIfExists(targetPath);
566
+ if (currentConfig === undefined) {
567
+ await writeFile(targetPath, options.addedPreset === "vue-app" ? vueConfig : nodeConfig, "utf8");
568
+ return;
569
+ }
570
+ if (options.addedPreset !== "vue-app" || currentConfig === vueConfig) {
571
+ return;
572
+ }
573
+ if (currentConfig === nodeConfig) {
574
+ await writeFile(targetPath, vueConfig, "utf8");
575
+ return;
576
+ }
577
+ throw new Error("Cannot update root OXC lint configuration for Vue Package Addition: oxlint.config.ts has local changes");
578
+ }
579
+ async function ensureRootOxcConfiguration(options) {
580
+ if (!options.sharedOxcRoot) {
581
+ return;
582
+ }
583
+ await ensureRootOxfmtConfig(options.root, options.sharedOxcRoot);
584
+ await ensureRootOxlintConfig({
585
+ root: options.root,
586
+ sharedOxcRoot: options.sharedOxcRoot,
587
+ addedPreset: options.addedPreset,
588
+ });
589
+ }
590
+ function gitignoreContainsEntry(text, entry) {
591
+ return text
592
+ .split(/\r?\n/)
593
+ .some((line) => line.trim() === entry || line.trim() === `${entry}/`);
594
+ }
595
+ function gitignoreTextWithEntries(text, entries) {
596
+ const missingEntries = entries.filter((entry) => !gitignoreContainsEntry(text, entry));
597
+ if (missingEntries.length === 0) {
598
+ return text;
599
+ }
600
+ const prefix = text.trimEnd();
601
+ return `${prefix}${prefix.length > 0 ? "\n" : ""}${missingEntries.join("\n")}\n`;
602
+ }
603
+ function rootGitignoreEntriesForPackageAddition(preset, requiresSharedOxcConfiguration) {
604
+ const entries = requiresSharedOxcConfiguration
605
+ ? ["node_modules", "dist"]
606
+ : [];
607
+ if (preset === "vue-app") {
608
+ entries.push("playwright-report", "test-results");
609
+ }
610
+ return entries;
611
+ }
612
+ async function ensureRootGitignoreEntries(root, entries) {
613
+ if (entries.length === 0) {
614
+ return;
615
+ }
616
+ const gitignorePath = path.join(root, ".gitignore");
617
+ const currentText = (await readTextIfExists(gitignorePath)) ?? "";
618
+ const nextText = gitignoreTextWithEntries(currentText, entries);
619
+ if (nextText !== currentText) {
620
+ await writeFile(gitignorePath, nextText, "utf8");
621
+ }
622
+ }
148
623
  export async function addPackage(options) {
149
624
  assertSafePackageLeaf(options.name);
150
625
  const root = path.resolve(options.cwd);
151
626
  const blueprint = await readGeneratedWorkspaceBlueprint(root);
152
- const nodeVersion = await readGeneratedRepositoryNodeVersion(root);
153
- const projectName = projectNameFromBlueprint(blueprint);
627
+ const repositoryMetadata = await readGeneratedRepositoryPackageMetadata(root);
628
+ const projectName = projectNameFromBlueprint(blueprint, repositoryMetadata.projectName);
154
629
  const packageName = `@${projectName}/${options.name}`;
630
+ const packagePath = options.path
631
+ ? validateExplicitPackagePath(options.path)
632
+ : defaultPackagePathForPreset(options.preset, options.name);
155
633
  const projection = findBuiltInPresetProjection(options.preset);
156
- const packageAddition = projection?.capabilities?.packageAddition;
157
634
  if (!projection) {
158
635
  throw new Error(`Unknown preset for Package Addition: ${options.preset}`);
159
636
  }
637
+ if (projection.metadata.packageAdditionSupport !==
638
+ PackageAdditionSupport.Supported) {
639
+ throw new Error(formatUnsupportedPackageAdditionPresetError(options.preset));
640
+ }
641
+ const packageAddition = projection.capabilities?.packageAddition;
160
642
  if (!packageAddition) {
161
- throw new Error(`Package Addition is not supported by preset: ${options.preset}`);
643
+ throw new Error(`Preset ${options.preset} declares Package Addition support but has no Package Addition implementation`);
162
644
  }
163
645
  const additionPlan = await packageAddition.planPackageAddition({
164
646
  root,
165
647
  blueprint,
166
648
  packageLeafName: options.name,
167
649
  packageName,
168
- nodeVersion,
650
+ packagePath,
651
+ nodeVersion: repositoryMetadata.nodeVersion,
169
652
  });
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);
653
+ const requiresSharedOxcConfiguration = additionPlan.sourceRoots?.sharedOxc !== undefined;
654
+ assertNoPackageConflict(blueprint, packageName, additionPlan.packagePath);
655
+ const packageLinkIntents = packageLinkIntentsForConsumers({
656
+ blueprint,
657
+ consumerPackagePaths: options.linkFrom ?? [],
658
+ providerPackagePath: additionPlan.packagePath,
659
+ });
660
+ await assertPackageLinkIntentConsumersAreTypeScriptBoundaries({
661
+ root,
662
+ blueprint,
663
+ packageLinkIntents,
664
+ });
665
+ 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);
175
667
  await mkdir(path.join(root, additionPlan.packagePath), { recursive: true });
668
+ await ensureRootOxcConfiguration({
669
+ root,
670
+ sharedOxcRoot: additionPlan.sourceRoots?.sharedOxc,
671
+ addedPreset: options.preset,
672
+ });
673
+ await ensureRootGitignoreEntries(root, rootGitignoreEntriesForPackageAddition(options.preset, requiresSharedOxcConfiguration));
176
674
  await renderProject({
177
675
  sourceRoot: additionPlan.sourceRoot,
178
676
  sourceRoots: additionPlan.sourceRoots,
@@ -183,6 +681,10 @@ export async function addPackage(options) {
183
681
  await writeFile(resolveGeneratedPath(root, textFile.path), textFile.text, "utf8");
184
682
  }
185
683
  await writeFile(path.join(root, "pnpm-workspace.yaml"), rootUpdatePlan.workspaceText, "utf8");
186
- await writeJson(path.join(root, "tsconfig.json"), rootUpdatePlan.rootTsconfig);
187
- await writeJson(path.join(root, ".project-kit/blueprint.json"), rootUpdatePlan.blueprint);
684
+ await writeJson(path.join(root, "package.json"), rootUpdatePlan.rootPackageJson);
685
+ await writeJson(path.join(root, "turbo.json"), rootUpdatePlan.turboConfig);
686
+ for (const update of rootUpdatePlan.consumerManifestUpdates) {
687
+ await writeJson(path.join(root, update.packagePath, "package.json"), update.manifest);
688
+ }
689
+ await writeJson(path.join(root, ".template/blueprint.json"), rootUpdatePlan.blueprint);
188
690
  }