@ykdz/template 0.0.10 → 0.0.12

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 (48) hide show
  1. package/dist/cli.js +2 -2
  2. package/node_modules/@ykdz/template-builtin-source/dist/src/index.d.ts +4 -4
  3. package/node_modules/@ykdz/template-builtin-source/dist/src/index.js +12 -5
  4. package/node_modules/@ykdz/template-builtin-source/dist/templates/hono-api/projection.d.ts +1 -1
  5. package/node_modules/@ykdz/template-builtin-source/dist/templates/hono-api/projection.js +1 -1
  6. package/node_modules/@ykdz/template-builtin-source/dist/templates/projection-plans.js +9 -6
  7. package/node_modules/@ykdz/template-builtin-source/dist/templates/rust-bin/projection.d.ts +1 -1
  8. package/node_modules/@ykdz/template-builtin-source/dist/templates/rust-bin/projection.js +1 -1
  9. package/node_modules/@ykdz/template-builtin-source/dist/templates/ts-lib/projection.d.ts +1 -1
  10. package/node_modules/@ykdz/template-builtin-source/dist/templates/ts-lib/projection.js +1 -1
  11. package/node_modules/@ykdz/template-builtin-source/dist/templates/vue-app/projection.d.ts +1 -1
  12. package/node_modules/@ykdz/template-builtin-source/dist/templates/vue-app/projection.js +1 -1
  13. package/node_modules/@ykdz/template-builtin-source/dist/templates/vue-hono-app/projection.d.ts +1 -1
  14. package/node_modules/@ykdz/template-builtin-source/dist/templates/vue-hono-app/projection.js +10 -12
  15. package/node_modules/@ykdz/template-builtin-source/package.json +3 -2
  16. package/node_modules/@ykdz/template-builtin-source/templates/hono-api/vitest.config.ts +1 -0
  17. package/node_modules/@ykdz/template-builtin-source/templates/preset-source.json +35 -9
  18. package/node_modules/@ykdz/template-builtin-source/templates/vue-hono-app/api/vitest.config.ts +7 -0
  19. package/node_modules/@ykdz/template-builtin-source/templates/vue-hono-app/web/playwright.config.ts +23 -7
  20. package/node_modules/@ykdz/template-core/dist/declarations.d.ts +2 -203
  21. package/node_modules/@ykdz/template-core/dist/declarations.js +1 -303
  22. package/node_modules/@ykdz/template-core/dist/devcontainer.d.ts +17 -0
  23. package/node_modules/@ykdz/template-core/dist/devcontainer.js +22 -21
  24. package/node_modules/@ykdz/template-core/dist/editor-customization.d.ts +10 -1
  25. package/node_modules/@ykdz/template-core/dist/editor-customization.js +36 -26
  26. package/node_modules/@ykdz/template-core/dist/generated-scenarios.d.ts +4 -0
  27. package/node_modules/@ykdz/template-core/dist/generated-scenarios.js +32 -22
  28. package/node_modules/@ykdz/template-core/dist/generation-context.d.ts +1 -1
  29. package/node_modules/@ykdz/template-core/dist/module-graph.d.ts +19 -1
  30. package/node_modules/@ykdz/template-core/dist/module-graph.js +42 -3
  31. package/node_modules/@ykdz/template-core/dist/next-step-instructions.d.ts +2 -0
  32. package/node_modules/@ykdz/template-core/dist/next-step-instructions.js +11 -22
  33. package/node_modules/@ykdz/template-core/dist/package-addition-support.d.ts +2 -1
  34. package/node_modules/@ykdz/template-core/dist/package-addition-support.js +2 -4
  35. package/node_modules/@ykdz/template-core/dist/package-addition.js +5 -22
  36. package/node_modules/@ykdz/template-core/dist/package-linking.d.ts +2 -6
  37. package/node_modules/@ykdz/template-core/dist/package-linking.js +1 -1
  38. package/node_modules/@ykdz/template-core/dist/preset-projection.d.ts +1 -2
  39. package/node_modules/@ykdz/template-core/dist/preset-source.d.ts +4 -418
  40. package/node_modules/@ykdz/template-core/dist/preset-source.js +81 -726
  41. package/node_modules/@ykdz/template-core/dist/project-github.js +2 -6
  42. package/node_modules/@ykdz/template-core/dist/projection-capabilities.d.ts +5 -53
  43. package/node_modules/@ykdz/template-core/dist/projection-capabilities.js +103 -515
  44. package/node_modules/@ykdz/template-core/package.json +3 -2
  45. package/node_modules/@ykdz/template-shared/dist/index.d.ts +794 -0
  46. package/node_modules/@ykdz/template-shared/dist/index.js +1637 -0
  47. package/node_modules/@ykdz/template-shared/package.json +32 -0
  48. package/package.json +9 -6
@@ -1,22 +1,13 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import { readFile } from "node:fs/promises";
2
3
  import path from "node:path";
4
+ import { normalizePresetProjectionDeclaration, projectionCapabilityIssues, validatePresetProjectionDeclaration as validateProjectionCapabilities, } from "@ykdz/template-shared";
3
5
  import { collectGeneratedManifestCatalogDependencies, collectGeneratedManifestCatalogReferences, renderGeneratedPnpmWorkspaceYaml, } from "./dependency-catalog.js";
4
6
  import { browserTestToolLayer, checkedDockerfileFirstNodePnpmDevcontainer, dockerfileFirstRustPnpmDevcontainer, nodePnpmToolLayer, rustToolLayer, } from "./devcontainer.js";
5
- import { editorCustomizationForCapabilities, } from "./editor-customization.js";
6
- import { renderFixCommand, renderRootCheckCommand, } from "./module-graph.js";
7
+ import { editorCustomizationForCapabilities, loadEditorCustomizationDeclarations, } from "./editor-customization.js";
8
+ import { playwrightBrowserAssetsEnvironmentNeed, renderFixCommand, renderRootCheckCommand, } from "./module-graph.js";
7
9
  import { packageManifestExposureFields, planPackageLinks, } from "./package-linking.js";
8
- const projectionCapabilityKinds = [
9
- "workspace-library-package",
10
- "workspace-node-packages",
11
- "rust-binary-workspace",
12
- "strict-typescript-root",
13
- "oxc-format-lint",
14
- "node-pnpm-devcontainer",
15
- "github-maintenance",
16
- ];
17
- const projectionCapabilityKindSet = new Set(projectionCapabilityKinds);
18
- const workspaceNodePackageKindSet = new Set(["hono-api", "vue-app"]);
19
- const workspaceNodePackagePathSet = new Set(["apps/api", "apps/web"]);
10
+ export { normalizePresetProjectionDeclaration, projectionCapabilityIssues, validateProjectionCapabilities, };
20
11
  const strictTypescriptRootBoundary = {
21
12
  kind: "workspace-orchestration",
22
13
  path: ".",
@@ -52,26 +43,6 @@ const requiredPlanCapabilityProviders = [
52
43
  label: "development container support",
53
44
  },
54
45
  ];
55
- const exactCapabilityKeys = {
56
- "workspace-library-package": [
57
- "kind",
58
- "workspacePackageGlob",
59
- "packageRole",
60
- "packageSourcePreset",
61
- "sourceFiles",
62
- ],
63
- "workspace-node-packages": [
64
- "kind",
65
- "workspacePackageGlob",
66
- "packages",
67
- "packageLinks",
68
- ],
69
- "rust-binary-workspace": ["kind", "workspacePackageGlob", "sourceFiles"],
70
- "strict-typescript-root": ["kind"],
71
- "oxc-format-lint": ["kind"],
72
- "node-pnpm-devcontainer": ["kind"],
73
- "github-maintenance": ["kind"],
74
- };
75
46
  const dependencyMaintenanceEcosystems = ["npm", "github-actions", "docker"];
76
47
  function strictTypeScriptCompilerOptions(options) {
77
48
  return {
@@ -110,12 +81,12 @@ const capabilityInterpreters = {
110
81
  if (capability.packages.length > 1) {
111
82
  state.rootScriptFragments.dev = "turbo run dev --parallel";
112
83
  }
113
- if (hasVuePackage(capability)) {
114
- state.rootCheckEnvironmentNeeds.push({
115
- kind: "playwright-browser-assets",
84
+ const vuePackage = findVuePackage(capability);
85
+ if (vuePackage !== undefined) {
86
+ state.rootCheckEnvironmentNeeds.push(playwrightBrowserAssetsEnvironmentNeed({
116
87
  browser: "chromium",
117
- owner: { kind: "package-boundary", path: "apps/web" },
118
- });
88
+ owner: { kind: "package-boundary", path: vuePackage.path },
89
+ }));
119
90
  state.editorCustomizationCapabilities.push("vue", "tailwind");
120
91
  }
121
92
  state.editorCustomizationCapabilities.push("vitest");
@@ -126,8 +97,8 @@ const capabilityInterpreters = {
126
97
  contribute({ capability, state }) {
127
98
  state.rustWorkspace = capability;
128
99
  state.sourceRoot = templateSourceRootForPreset(state, "rust-bin");
129
- state.sourceRoots.sharedDevcontainer =
130
- state.projectionSourceRoots.sharedDevcontainer();
100
+ setDevelopmentContainerResource(state, capability.devcontainerResourceId, "rust-binary-workspace");
101
+ setEditorCustomizationResource(state, capability.editorCustomizationResourceId, "rust-binary-workspace");
131
102
  state.rootCheckComponents.push({
132
103
  kind: "turbo-package-check",
133
104
  owner: workspaceGlobBoundary(capability.workspacePackageGlob),
@@ -159,20 +130,6 @@ const capabilityInterpreters = {
159
130
  owner: strictTypescriptRootBoundary,
160
131
  });
161
132
  const workspaceBoundary = workspaceBoundaryForState(state);
162
- state.rootCheckComponents.push({
163
- kind: "turbo-package-typecheck",
164
- owner: workspaceBoundary,
165
- });
166
- if (state.nodeWorkspace !== undefined) {
167
- state.rootCheckComponents.push({
168
- kind: "turbo-package-build",
169
- owner: workspaceBoundary,
170
- });
171
- state.rootCheckComponents.push({
172
- kind: "turbo-package-test",
173
- owner: workspaceBoundary,
174
- });
175
- }
176
133
  state.rootCheckComponents.push({
177
134
  kind: "turbo-package-check",
178
135
  owner: workspaceBoundary,
@@ -193,8 +150,9 @@ const capabilityInterpreters = {
193
150
  },
194
151
  "oxc-format-lint": {
195
152
  kind: "oxc-format-lint",
196
- contribute({ state }) {
153
+ contribute({ capability, state }) {
197
154
  state.sourceRoots.sharedOxc = state.projectionSourceRoots.sharedOxc();
155
+ setEditorCustomizationResource(state, capability.editorCustomizationResourceId, "oxc-format-lint");
198
156
  state.rootCheckComponents.unshift({
199
157
  kind: "oxc-format-check",
200
158
  owner: strictTypescriptRootBoundary,
@@ -215,13 +173,13 @@ const capabilityInterpreters = {
215
173
  });
216
174
  state.packageFixComponents.push({ kind: "oxc-format-write", owner: workspacePackageBoundary }, { kind: "oxc-lint-fix", owner: workspacePackageBoundary });
217
175
  state.rootScriptFragments["format:check"] =
218
- "oxfmt --check --config oxfmt.config.ts oxlint.config.ts oxfmt.config.ts";
176
+ "oxfmt --check oxlint.config.ts oxfmt.config.ts";
219
177
  state.rootScriptFragments["format:write"] =
220
- "oxfmt --write --config oxfmt.config.ts oxlint.config.ts oxfmt.config.ts";
178
+ "oxfmt --write oxlint.config.ts oxfmt.config.ts";
221
179
  state.rootScriptFragments.lint =
222
- "oxlint --config oxlint.config.ts oxlint.config.ts oxfmt.config.ts";
180
+ "oxlint oxlint.config.ts oxfmt.config.ts";
223
181
  state.rootScriptFragments["lint:fix"] =
224
- "oxlint --config oxlint.config.ts oxlint.config.ts oxfmt.config.ts --fix";
182
+ "oxlint oxlint.config.ts oxfmt.config.ts --fix";
225
183
  state.packageScriptFragments["format:check"] =
226
184
  "oxfmt --check --config ../../oxfmt.config.ts .";
227
185
  state.packageScriptFragments["format:write"] =
@@ -243,9 +201,8 @@ const capabilityInterpreters = {
243
201
  },
244
202
  "node-pnpm-devcontainer": {
245
203
  kind: "node-pnpm-devcontainer",
246
- contribute({ state }) {
247
- state.sourceRoots.sharedDevcontainer =
248
- state.projectionSourceRoots.sharedDevcontainer();
204
+ contribute({ capability, state }) {
205
+ setDevelopmentContainerResource(state, capability.devcontainerResourceId, "node-pnpm-devcontainer");
249
206
  state.flags.devcontainer = true;
250
207
  state.operationFactories.push(nodePnpmDevcontainerOperations);
251
208
  },
@@ -306,111 +263,6 @@ function contributeProjectionCapability(capability, state) {
306
263
  return;
307
264
  }
308
265
  }
309
- export function validateProjectionCapabilities(input) {
310
- if (!isRecord(input) || !Array.isArray(input.capabilities)) {
311
- return {
312
- ok: false,
313
- issues: [
314
- {
315
- path: "$.capabilities",
316
- message: "Projection Declaration must select Projection Capabilities",
317
- },
318
- ],
319
- };
320
- }
321
- const issues = [];
322
- const capabilities = [];
323
- input.capabilities.forEach((capability, index) => {
324
- const pathPrefix = `$.capabilities[${index}]`;
325
- if (!isRecord(capability)) {
326
- issues.push({
327
- path: pathPrefix,
328
- message: "Projection Capability must be an object",
329
- });
330
- return;
331
- }
332
- if (typeof capability.kind !== "string") {
333
- issues.push({
334
- path: `${pathPrefix}.kind`,
335
- message: "Projection Capability kind is required",
336
- });
337
- return;
338
- }
339
- if (!isProjectionCapabilityKind(capability.kind)) {
340
- issues.push({
341
- path: `${pathPrefix}.kind`,
342
- message: `Unknown Projection Capability kind: ${capability.kind}`,
343
- });
344
- return;
345
- }
346
- const kind = capability.kind;
347
- issues.push(...unknownCapabilityPropertyIssues(kind, capability, pathPrefix));
348
- if (kind === "workspace-library-package") {
349
- const workspaceCapability = parseWorkspaceLibraryPackageCapability(capability, pathPrefix);
350
- if (Array.isArray(workspaceCapability)) {
351
- issues.push(...workspaceCapability);
352
- return;
353
- }
354
- capabilities.push(workspaceCapability);
355
- return;
356
- }
357
- if (kind === "workspace-node-packages") {
358
- const workspaceCapability = parseWorkspaceNodePackagesCapability(capability, pathPrefix);
359
- if (Array.isArray(workspaceCapability)) {
360
- issues.push(...workspaceCapability);
361
- return;
362
- }
363
- capabilities.push(workspaceCapability);
364
- return;
365
- }
366
- if (kind === "rust-binary-workspace") {
367
- const workspaceCapability = parseRustBinaryWorkspaceCapability(capability, pathPrefix);
368
- if (Array.isArray(workspaceCapability)) {
369
- issues.push(...workspaceCapability);
370
- return;
371
- }
372
- capabilities.push(workspaceCapability);
373
- return;
374
- }
375
- capabilities.push({ kind });
376
- });
377
- if (issues.length === 0) {
378
- issues.push(...duplicateCapabilityIssues(capabilities));
379
- issues.push(...capabilityCompositionIssues(capabilities));
380
- }
381
- if (issues.length > 0) {
382
- return { ok: false, issues };
383
- }
384
- return {
385
- ok: true,
386
- value: {
387
- capabilities,
388
- },
389
- };
390
- }
391
- export function projectionCapabilityIssues(presets) {
392
- return presets.flatMap((preset, presetIndex) => {
393
- if (preset.projection === undefined) {
394
- return [];
395
- }
396
- const result = validateProjectionCapabilities(preset.projection);
397
- return result.ok
398
- ? []
399
- : result.issues.map((issue) => ({
400
- path: `$.presets[${presetIndex}].projection${issue.path.slice(1)}`,
401
- message: issue.message,
402
- }));
403
- });
404
- }
405
- export function normalizePresetProjectionDeclaration(declaration) {
406
- const result = validateProjectionCapabilities(declaration);
407
- if (!result.ok) {
408
- throw new Error(`Projection Declaration is invalid:\n${result.issues
409
- .map((issue) => ` - ${issue.path}: ${issue.message}`)
410
- .join("\n")}`);
411
- }
412
- return result.value;
413
- }
414
266
  export function interpretPresetProjectionDeclaration(options) {
415
267
  const validation = validateProjectionCapabilities(options.declaration);
416
268
  if (!validation.ok) {
@@ -559,354 +411,37 @@ function createProjectionCompositionState(sourceRoots) {
559
411
  flags: {},
560
412
  };
561
413
  }
562
- function isRecord(value) {
563
- return typeof value === "object" && value !== null && !Array.isArray(value);
564
- }
565
- function isProjectionCapabilityKind(value) {
566
- return projectionCapabilityKindSet.has(value);
567
- }
568
- function isWorkspaceNodePackageKind(value) {
569
- return typeof value === "string" && workspaceNodePackageKindSet.has(value);
570
- }
571
- function isWorkspaceNodePackagePath(value) {
572
- return typeof value === "string" && workspaceNodePackagePathSet.has(value);
573
- }
574
- function isNonEmptyString(value) {
575
- return typeof value === "string" && value.length > 0;
414
+ function uniqueValues(values) {
415
+ return [...new Set(values)];
576
416
  }
577
- function nonEmptyStringArray(value) {
578
- const strings = [];
579
- for (const entry of value) {
580
- if (!isNonEmptyString(entry)) {
581
- return undefined;
582
- }
583
- strings.push(entry);
417
+ function setDevelopmentContainerResource(state, resourceId, capabilityKind) {
418
+ const root = state.projectionSourceRoots.sharedResource(resourceId);
419
+ if (root === undefined) {
420
+ throw new Error(`Projection Capability ${capabilityKind} references unresolved Development Container Shared Resource: ${resourceId}`);
584
421
  }
585
- return strings.length === 0 ? undefined : strings;
586
- }
587
- function unknownCapabilityPropertyIssues(kind, capability, pathPrefix) {
588
- const allowedKeys = new Set(exactCapabilityKeys[kind]);
589
- return Object.keys(capability)
590
- .filter((key) => !allowedKeys.has(key))
591
- .map((key) => ({
592
- path: `${pathPrefix}.${key}`,
593
- message: `Projection Capability ${kind} does not support property: ${key}`,
594
- }));
422
+ const sourceRootKey = developmentContainerResourceSourceRootKey(resourceId);
423
+ state.devcontainerResource = { id: resourceId, root, sourceRootKey };
424
+ state.sourceRoots[sourceRootKey] = root;
595
425
  }
596
- function parseWorkspaceLibraryPackageCapability(capability, pathPrefix) {
597
- const issues = [];
598
- let sourceFiles = [];
599
- if (capability.workspacePackageGlob !== "packages/*") {
600
- issues.push({
601
- path: `${pathPrefix}.workspacePackageGlob`,
602
- message: "workspace-library-package currently supports workspacePackageGlob: packages/*",
603
- });
604
- }
605
- if (capability.packageRole !== "shared-library") {
606
- issues.push({
607
- path: `${pathPrefix}.packageRole`,
608
- message: "workspace-library-package currently supports packageRole: shared-library",
609
- });
610
- }
611
- if (capability.packageSourcePreset !== "ts-lib") {
612
- issues.push({
613
- path: `${pathPrefix}.packageSourcePreset`,
614
- message: "workspace-library-package currently supports packageSourcePreset: ts-lib",
615
- });
426
+ function setEditorCustomizationResource(state, resourceId, capabilityKind) {
427
+ const root = state.projectionSourceRoots.sharedResource(resourceId);
428
+ if (root === undefined) {
429
+ throw new Error(`Projection Capability ${capabilityKind} references unresolved Editor Customization Shared Resource: ${resourceId}`);
616
430
  }
617
- if (!Array.isArray(capability.sourceFiles)) {
618
- issues.push({
619
- path: `${pathPrefix}.sourceFiles`,
620
- message: "workspace-library-package sourceFiles must be a non-empty array",
621
- });
622
- }
623
- else {
624
- const parsedSourceFiles = nonEmptyStringArray(capability.sourceFiles);
625
- if (parsedSourceFiles === undefined) {
626
- issues.push({
627
- path: `${pathPrefix}.sourceFiles`,
628
- message: "workspace-library-package sourceFiles must be a non-empty array of paths",
629
- });
630
- }
631
- else {
632
- sourceFiles = parsedSourceFiles;
633
- }
634
- }
635
- if (issues.length > 0) {
636
- return issues;
637
- }
638
- return {
639
- kind: "workspace-library-package",
640
- workspacePackageGlob: "packages/*",
641
- packageRole: "shared-library",
642
- packageSourcePreset: "ts-lib",
643
- sourceFiles,
431
+ state.editorCustomizationResource = {
432
+ id: resourceId,
433
+ root,
434
+ declarations: loadEditorCustomizationDeclarations(root),
644
435
  };
645
436
  }
646
- function parseWorkspaceNodePackagesCapability(capability, pathPrefix) {
647
- const issues = [];
648
- if (capability.workspacePackageGlob !== "apps/*") {
649
- issues.push({
650
- path: `${pathPrefix}.workspacePackageGlob`,
651
- message: "workspace-node-packages currently supports workspacePackageGlob: apps/*",
652
- });
653
- }
654
- if (!Array.isArray(capability.packages) || capability.packages.length === 0) {
655
- issues.push({
656
- path: `${pathPrefix}.packages`,
657
- message: "workspace-node-packages packages must be a non-empty array",
658
- });
659
- }
660
- const packages = Array.isArray(capability.packages)
661
- ? capability.packages.flatMap((nodePackage, packageIndex) => {
662
- const packagePath = `${pathPrefix}.packages[${packageIndex}]`;
663
- const parsed = parseWorkspaceNodePackage(nodePackage, packagePath);
664
- if (Array.isArray(parsed)) {
665
- issues.push(...parsed);
666
- return [];
667
- }
668
- return [parsed];
669
- })
670
- : [];
671
- const packageLinks = capability.packageLinks === undefined
672
- ? undefined
673
- : parseWorkspaceNodePackageLinks(capability.packageLinks, `${pathPrefix}.packageLinks`, new Set(packages.map((nodePackage) => nodePackage.path)), issues);
674
- if (issues.length > 0) {
675
- return issues;
676
- }
677
- return {
678
- kind: "workspace-node-packages",
679
- workspacePackageGlob: "apps/*",
680
- packages,
681
- ...(packageLinks === undefined ? {} : { packageLinks }),
682
- };
437
+ function developmentContainerResourceSourceRootKey(resourceId) {
438
+ return `devcontainer:${resourceId}`;
683
439
  }
684
- function parseWorkspaceNodePackage(value, pathPrefix) {
685
- const issues = [];
686
- if (!isRecord(value)) {
687
- return [
688
- {
689
- path: pathPrefix,
690
- message: "workspace-node-packages package must be an object",
691
- },
692
- ];
693
- }
694
- const kind = isWorkspaceNodePackageKind(value.kind) ? value.kind : undefined;
695
- const packagePath = isWorkspaceNodePackagePath(value.path)
696
- ? value.path
697
- : undefined;
698
- let sourceFiles = [];
699
- if (kind === undefined) {
700
- issues.push({
701
- path: `${pathPrefix}.kind`,
702
- message: "workspace-node-packages package kind must be hono-api or vue-app",
703
- });
704
- }
705
- if (packagePath === undefined) {
706
- issues.push({
707
- path: `${pathPrefix}.path`,
708
- message: "workspace-node-packages package path must be apps/api or apps/web",
709
- });
710
- }
711
- if (!Array.isArray(value.sourceFiles)) {
712
- issues.push({
713
- path: `${pathPrefix}.sourceFiles`,
714
- message: "workspace-node-packages package sourceFiles must be a non-empty array",
715
- });
716
- }
717
- else {
718
- const parsedSourceFiles = nonEmptyStringArray(value.sourceFiles);
719
- if (parsedSourceFiles === undefined) {
720
- issues.push({
721
- path: `${pathPrefix}.sourceFiles`,
722
- message: "workspace-node-packages package sourceFiles must be a non-empty array of paths",
723
- });
724
- }
725
- else {
726
- sourceFiles = parsedSourceFiles;
727
- }
728
- }
729
- const allowedKeys = new Set(["kind", "path", "sourceFiles"]);
730
- for (const key of Object.keys(value)) {
731
- if (!allowedKeys.has(key)) {
732
- issues.push({
733
- path: `${pathPrefix}.${key}`,
734
- message: `workspace-node-packages package does not support property: ${key}`,
735
- });
736
- }
737
- }
738
- if (issues.length > 0) {
739
- return issues;
740
- }
741
- if (kind === undefined || packagePath === undefined) {
742
- throw new Error(`workspace-node-packages package validation failed without diagnostics at ${pathPrefix}`);
743
- }
744
- return {
745
- kind,
746
- path: packagePath,
747
- sourceFiles,
748
- };
749
- }
750
- function parseWorkspaceNodePackageLinks(value, pathPrefix, declaredPackagePaths, issues) {
751
- if (!Array.isArray(value)) {
752
- issues.push({
753
- path: pathPrefix,
754
- message: "workspace-node-packages packageLinks must be an array",
755
- });
756
- return undefined;
757
- }
758
- return value.flatMap((link, linkIndex) => {
759
- const linkPath = `${pathPrefix}[${linkIndex}]`;
760
- const linkIssues = [];
761
- if (!isRecord(link)) {
762
- issues.push({
763
- path: linkPath,
764
- message: "workspace-node-packages packageLink must be an object",
765
- });
766
- return [];
767
- }
768
- const allowedKeys = new Set(["consumerPackagePath", "providerPackagePath"]);
769
- for (const key of Object.keys(link)) {
770
- if (!allowedKeys.has(key)) {
771
- linkIssues.push({
772
- path: `${linkPath}.${key}`,
773
- message: `workspace-node-packages packageLink does not support property: ${key}`,
774
- });
775
- }
776
- }
777
- if (link.consumerPackagePath !== "apps/web" ||
778
- link.providerPackagePath !== "apps/api") {
779
- linkIssues.push({
780
- path: linkPath,
781
- message: "workspace-node-packages currently supports links from apps/web to apps/api",
782
- });
783
- }
784
- else {
785
- if (!declaredPackagePaths.has(link.consumerPackagePath)) {
786
- linkIssues.push({
787
- path: `${linkPath}.consumerPackagePath`,
788
- message: "workspace-node-packages packageLink consumerPackagePath must reference a package declared in the same packages array: apps/web",
789
- });
790
- }
791
- if (!declaredPackagePaths.has(link.providerPackagePath)) {
792
- linkIssues.push({
793
- path: `${linkPath}.providerPackagePath`,
794
- message: "workspace-node-packages packageLink providerPackagePath must reference a package declared in the same packages array: apps/api",
795
- });
796
- }
797
- }
798
- if (linkIssues.length > 0) {
799
- issues.push(...linkIssues);
800
- return [];
801
- }
802
- return [
803
- {
804
- consumerPackagePath: "apps/web",
805
- providerPackagePath: "apps/api",
806
- },
807
- ];
808
- });
809
- }
810
- function parseRustBinaryWorkspaceCapability(capability, pathPrefix) {
811
- const issues = [];
812
- let sourceFiles = [];
813
- if (capability.workspacePackageGlob !== "packages/*") {
814
- issues.push({
815
- path: `${pathPrefix}.workspacePackageGlob`,
816
- message: "rust-binary-workspace currently supports workspacePackageGlob: packages/*",
817
- });
818
- }
819
- if (!Array.isArray(capability.sourceFiles)) {
820
- issues.push({
821
- path: `${pathPrefix}.sourceFiles`,
822
- message: "rust-binary-workspace sourceFiles must be a non-empty array",
823
- });
824
- }
825
- else {
826
- const parsedSourceFiles = nonEmptyStringArray(capability.sourceFiles);
827
- if (parsedSourceFiles === undefined) {
828
- issues.push({
829
- path: `${pathPrefix}.sourceFiles`,
830
- message: "rust-binary-workspace sourceFiles must be a non-empty array of paths",
831
- });
832
- }
833
- else {
834
- sourceFiles = parsedSourceFiles;
835
- }
836
- }
837
- if (issues.length > 0) {
838
- return issues;
440
+ function editorCustomizationDeclarationsForState(state) {
441
+ if (state.editorCustomizationResource === undefined) {
442
+ throw new Error("Projection Capability composition did not provide Editor Customization Shared Resource");
839
443
  }
840
- return {
841
- kind: "rust-binary-workspace",
842
- workspacePackageGlob: "packages/*",
843
- sourceFiles,
844
- };
845
- }
846
- function duplicateCapabilityIssues(capabilities) {
847
- const firstSeen = new Map();
848
- const issues = [];
849
- capabilities.forEach((capability, index) => {
850
- const firstIndex = firstSeen.get(capability.kind);
851
- if (firstIndex === undefined) {
852
- firstSeen.set(capability.kind, index);
853
- return;
854
- }
855
- issues.push({
856
- path: `$.capabilities[${index}].kind`,
857
- message: `Duplicate Projection Capability kind: ${capability.kind}`,
858
- });
859
- });
860
- return issues;
861
- }
862
- function capabilityCompositionIssues(capabilities) {
863
- const kinds = new Set(capabilities.map((capability) => capability.kind));
864
- const issues = [];
865
- const companionKinds = uniqueValues(capabilities
866
- .map((capability) => capability.kind)
867
- .filter((kind) => kind !== "rust-binary-workspace"));
868
- if (kinds.has("rust-binary-workspace") && companionKinds.length > 0) {
869
- issues.push({
870
- path: "$.capabilities",
871
- message: "rust-binary-workspace is a complete domain capability and must be selected by itself; remove companion Projection Capabilities: " +
872
- companionKinds.join(", "),
873
- });
874
- return issues;
875
- }
876
- if (!kinds.has("workspace-library-package") &&
877
- !kinds.has("workspace-node-packages") &&
878
- !kinds.has("rust-binary-workspace")) {
879
- issues.push({
880
- path: "$.capabilities",
881
- message: "Projection Capability composition must include a workspace package layout capability",
882
- });
883
- }
884
- if (kinds.has("strict-typescript-root") &&
885
- !kinds.has("workspace-library-package") &&
886
- !kinds.has("workspace-node-packages")) {
887
- issues.push({
888
- path: "$.capabilities",
889
- message: "strict-typescript-root requires a workspace package layout so package typecheck tasks have a workspace target",
890
- });
891
- }
892
- if (kinds.has("node-pnpm-devcontainer") && !kinds.has("oxc-format-lint")) {
893
- issues.push({
894
- path: "$.capabilities",
895
- message: "node-pnpm-devcontainer requires oxc-format-lint so editor customization is derived from declared tooling",
896
- });
897
- }
898
- for (const requirement of requiredPlanCapabilityProviders) {
899
- if (!kinds.has(requirement.kind) && !kinds.has("rust-binary-workspace")) {
900
- issues.push({
901
- path: "$.capabilities",
902
- message: `Projection Capability composition must include ${requirement.kind} to provide ${requirement.label}`,
903
- });
904
- }
905
- }
906
- return issues;
907
- }
908
- function uniqueValues(values) {
909
- return [...new Set(values)];
444
+ return state.editorCustomizationResource.declarations;
910
445
  }
911
446
  function projectNameFromDir(targetDir) {
912
447
  return path.basename(path.resolve(targetDir));
@@ -1242,7 +777,10 @@ function workspaceGlobBoundary(workspacePackageGlob) {
1242
777
  };
1243
778
  }
1244
779
  function hasVuePackage(capability) {
1245
- return capability.packages.some((nodePackage) => nodePackage.kind === "vue-app");
780
+ return findVuePackage(capability) !== undefined;
781
+ }
782
+ function findVuePackage(capability) {
783
+ return capability.packages.find((nodePackage) => nodePackage.kind === "vue-app");
1246
784
  }
1247
785
  function sourceRootPreset(capability) {
1248
786
  if (capability.packages.length === 1) {
@@ -1629,7 +1167,8 @@ function rustBinaryWorkspaceOperations({ context, state, packageScripts, package
1629
1167
  const rootManifest = rustRootPackageJson(context, packageScripts, state);
1630
1168
  const packageManifest = rustWorkspacePackageJson(context, workspacePackageScripts);
1631
1169
  const rustLayer = rustToolLayer();
1632
- const editorCustomization = editorCustomizationForCapabilities(state.editorCustomizationCapabilities);
1170
+ const dockerfileFragments = readDevelopmentContainerDockerfileFragments(state, ["rust"]);
1171
+ const editorCustomization = editorCustomizationForCapabilities(state.editorCustomizationCapabilities, editorCustomizationDeclarationsForState(state));
1633
1172
  const developmentContainer = dockerfileFirstRustPnpmDevcontainer({
1634
1173
  name: context.projectName.value,
1635
1174
  nodeLayer: nodePnpmToolLayer({
@@ -1637,6 +1176,7 @@ function rustBinaryWorkspaceOperations({ context, state, packageScripts, package
1637
1176
  packageManagerPin: context.toolchain.packageManagerPin.value,
1638
1177
  }),
1639
1178
  rustLayer,
1179
+ dockerfileFragments,
1640
1180
  extensions: editorCustomization.extensions,
1641
1181
  settings: editorCustomization.settings,
1642
1182
  });
@@ -2168,7 +1708,7 @@ function rootTsconfigReferences(workspace) {
2168
1708
  });
2169
1709
  }
2170
1710
  function oxcFormatLintOperations({ state, }) {
2171
- const editorCustomization = editorCustomizationForCapabilities(state.editorCustomizationCapabilities);
1711
+ const editorCustomization = editorCustomizationForCapabilities(state.editorCustomizationCapabilities, editorCustomizationDeclarationsForState(state));
2172
1712
  return [
2173
1713
  {
2174
1714
  kind: "copyFile",
@@ -2200,16 +1740,19 @@ function oxcFormatLintOperations({ state, }) {
2200
1740
  ];
2201
1741
  }
2202
1742
  function nodePnpmDevcontainerOperations({ context, state, }) {
2203
- const editorCustomization = editorCustomizationForCapabilities(state.editorCustomizationCapabilities);
1743
+ const additionalLayers = state.nodeWorkspace && hasVuePackage(state.nodeWorkspace)
1744
+ ? [browserTestToolLayer()]
1745
+ : [];
1746
+ const dockerfileFragments = readDevelopmentContainerDockerfileFragments(state, additionalLayers.length === 0 ? [] : ["browserTest"]);
1747
+ const editorCustomization = editorCustomizationForCapabilities(state.editorCustomizationCapabilities, editorCustomizationDeclarationsForState(state));
2204
1748
  const developmentContainer = checkedDockerfileFirstNodePnpmDevcontainer({
2205
1749
  name: context.projectName.value,
2206
1750
  layer: nodePnpmToolLayer({
2207
1751
  nodeVersion: context.toolchain.nodeLtsMajor.value,
2208
1752
  packageManagerPin: context.toolchain.packageManagerPin.value,
2209
1753
  }),
2210
- additionalLayers: state.nodeWorkspace && hasVuePackage(state.nodeWorkspace)
2211
- ? [browserTestToolLayer()]
2212
- : [],
1754
+ dockerfileFragments,
1755
+ additionalLayers,
2213
1756
  extensions: editorCustomization.extensions,
2214
1757
  settings: editorCustomization.settings,
2215
1758
  });
@@ -2224,6 +1767,51 @@ function nodePnpmDevcontainerOperations({ context, state, }) {
2224
1767
  },
2225
1768
  ];
2226
1769
  }
1770
+ function readDevelopmentContainerDockerfileFragments(state, optionalFragments) {
1771
+ if (state.devcontainerResource === undefined) {
1772
+ throw new Error("Projection Capability composition did not provide a Development Container Shared Resource");
1773
+ }
1774
+ const fragmentNames = {
1775
+ nodePnpm: "node-pnpm.Dockerfile",
1776
+ browserTest: "browser-test.Dockerfile",
1777
+ rust: "rust.Dockerfile",
1778
+ };
1779
+ const selected = new Set(optionalFragments);
1780
+ return {
1781
+ sourceRoot: state.devcontainerResource.sourceRootKey,
1782
+ nodePnpm: readDevelopmentContainerDockerfileFragment(state.devcontainerResource, fragmentNames.nodePnpm),
1783
+ ...(selected.has("browserTest")
1784
+ ? {
1785
+ browserTest: readDevelopmentContainerDockerfileFragment(state.devcontainerResource, fragmentNames.browserTest),
1786
+ }
1787
+ : {}),
1788
+ ...(selected.has("rust")
1789
+ ? {
1790
+ rust: readDevelopmentContainerDockerfileFragment(state.devcontainerResource, fragmentNames.rust),
1791
+ }
1792
+ : {}),
1793
+ };
1794
+ }
1795
+ function readDevelopmentContainerDockerfileFragment(resource, fragmentName) {
1796
+ const filePath = path.join(resource.root, fragmentName);
1797
+ try {
1798
+ return {
1799
+ from: fragmentName,
1800
+ text: readFileSync(filePath, "utf8"),
1801
+ };
1802
+ }
1803
+ catch (error) {
1804
+ if (isMissingDevelopmentContainerFragmentError(error)) {
1805
+ throw new Error(`Development Container Shared Resource ${resource.id} is missing Dockerfile fragment: ${fragmentName}`, { cause: error });
1806
+ }
1807
+ throw error;
1808
+ }
1809
+ }
1810
+ function isMissingDevelopmentContainerFragmentError(error) {
1811
+ return (error instanceof Error &&
1812
+ "code" in error &&
1813
+ (error.code === "ENOENT" || error.code === "ENOTDIR"));
1814
+ }
2227
1815
  function githubMaintenanceOperations() {
2228
1816
  return [
2229
1817
  {