@ykdz/template 0.0.10 → 0.0.11

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 (45) 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 +6 -5
  15. package/node_modules/@ykdz/template-builtin-source/package.json +3 -2
  16. package/node_modules/@ykdz/template-builtin-source/templates/preset-source.json +35 -9
  17. package/node_modules/@ykdz/template-core/dist/declarations.d.ts +2 -203
  18. package/node_modules/@ykdz/template-core/dist/declarations.js +1 -303
  19. package/node_modules/@ykdz/template-core/dist/devcontainer.d.ts +17 -0
  20. package/node_modules/@ykdz/template-core/dist/devcontainer.js +22 -21
  21. package/node_modules/@ykdz/template-core/dist/editor-customization.d.ts +10 -1
  22. package/node_modules/@ykdz/template-core/dist/editor-customization.js +36 -26
  23. package/node_modules/@ykdz/template-core/dist/generated-scenarios.d.ts +4 -0
  24. package/node_modules/@ykdz/template-core/dist/generated-scenarios.js +32 -22
  25. package/node_modules/@ykdz/template-core/dist/generation-context.d.ts +1 -1
  26. package/node_modules/@ykdz/template-core/dist/module-graph.d.ts +19 -1
  27. package/node_modules/@ykdz/template-core/dist/module-graph.js +42 -3
  28. package/node_modules/@ykdz/template-core/dist/next-step-instructions.d.ts +2 -0
  29. package/node_modules/@ykdz/template-core/dist/next-step-instructions.js +11 -22
  30. package/node_modules/@ykdz/template-core/dist/package-addition-support.d.ts +2 -1
  31. package/node_modules/@ykdz/template-core/dist/package-addition-support.js +2 -4
  32. package/node_modules/@ykdz/template-core/dist/package-addition.js +2 -2
  33. package/node_modules/@ykdz/template-core/dist/package-linking.d.ts +2 -6
  34. package/node_modules/@ykdz/template-core/dist/package-linking.js +1 -1
  35. package/node_modules/@ykdz/template-core/dist/preset-projection.d.ts +1 -2
  36. package/node_modules/@ykdz/template-core/dist/preset-source.d.ts +4 -418
  37. package/node_modules/@ykdz/template-core/dist/preset-source.js +81 -726
  38. package/node_modules/@ykdz/template-core/dist/project-github.js +2 -6
  39. package/node_modules/@ykdz/template-core/dist/projection-capabilities.d.ts +5 -53
  40. package/node_modules/@ykdz/template-core/dist/projection-capabilities.js +99 -497
  41. package/node_modules/@ykdz/template-core/package.json +3 -2
  42. package/node_modules/@ykdz/template-shared/dist/index.d.ts +794 -0
  43. package/node_modules/@ykdz/template-shared/dist/index.js +1637 -0
  44. package/node_modules/@ykdz/template-shared/package.json +32 -0
  45. 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),
@@ -193,8 +164,9 @@ const capabilityInterpreters = {
193
164
  },
194
165
  "oxc-format-lint": {
195
166
  kind: "oxc-format-lint",
196
- contribute({ state }) {
167
+ contribute({ capability, state }) {
197
168
  state.sourceRoots.sharedOxc = state.projectionSourceRoots.sharedOxc();
169
+ setEditorCustomizationResource(state, capability.editorCustomizationResourceId, "oxc-format-lint");
198
170
  state.rootCheckComponents.unshift({
199
171
  kind: "oxc-format-check",
200
172
  owner: strictTypescriptRootBoundary,
@@ -243,9 +215,8 @@ const capabilityInterpreters = {
243
215
  },
244
216
  "node-pnpm-devcontainer": {
245
217
  kind: "node-pnpm-devcontainer",
246
- contribute({ state }) {
247
- state.sourceRoots.sharedDevcontainer =
248
- state.projectionSourceRoots.sharedDevcontainer();
218
+ contribute({ capability, state }) {
219
+ setDevelopmentContainerResource(state, capability.devcontainerResourceId, "node-pnpm-devcontainer");
249
220
  state.flags.devcontainer = true;
250
221
  state.operationFactories.push(nodePnpmDevcontainerOperations);
251
222
  },
@@ -306,111 +277,6 @@ function contributeProjectionCapability(capability, state) {
306
277
  return;
307
278
  }
308
279
  }
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
280
  export function interpretPresetProjectionDeclaration(options) {
415
281
  const validation = validateProjectionCapabilities(options.declaration);
416
282
  if (!validation.ok) {
@@ -559,354 +425,37 @@ function createProjectionCompositionState(sourceRoots) {
559
425
  flags: {},
560
426
  };
561
427
  }
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;
428
+ function uniqueValues(values) {
429
+ return [...new Set(values)];
576
430
  }
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);
431
+ function setDevelopmentContainerResource(state, resourceId, capabilityKind) {
432
+ const root = state.projectionSourceRoots.sharedResource(resourceId);
433
+ if (root === undefined) {
434
+ throw new Error(`Projection Capability ${capabilityKind} references unresolved Development Container Shared Resource: ${resourceId}`);
584
435
  }
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
- }));
436
+ const sourceRootKey = developmentContainerResourceSourceRootKey(resourceId);
437
+ state.devcontainerResource = { id: resourceId, root, sourceRootKey };
438
+ state.sourceRoots[sourceRootKey] = root;
595
439
  }
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
- });
616
- }
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;
440
+ function setEditorCustomizationResource(state, resourceId, capabilityKind) {
441
+ const root = state.projectionSourceRoots.sharedResource(resourceId);
442
+ if (root === undefined) {
443
+ throw new Error(`Projection Capability ${capabilityKind} references unresolved Editor Customization Shared Resource: ${resourceId}`);
637
444
  }
638
- return {
639
- kind: "workspace-library-package",
640
- workspacePackageGlob: "packages/*",
641
- packageRole: "shared-library",
642
- packageSourcePreset: "ts-lib",
643
- sourceFiles,
445
+ state.editorCustomizationResource = {
446
+ id: resourceId,
447
+ root,
448
+ declarations: loadEditorCustomizationDeclarations(root),
644
449
  };
645
450
  }
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
- };
451
+ function developmentContainerResourceSourceRootKey(resourceId) {
452
+ return `devcontainer:${resourceId}`;
683
453
  }
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
- });
454
+ function editorCustomizationDeclarationsForState(state) {
455
+ if (state.editorCustomizationResource === undefined) {
456
+ throw new Error("Projection Capability composition did not provide Editor Customization Shared Resource");
704
457
  }
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;
839
- }
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)];
458
+ return state.editorCustomizationResource.declarations;
910
459
  }
911
460
  function projectNameFromDir(targetDir) {
912
461
  return path.basename(path.resolve(targetDir));
@@ -1242,7 +791,10 @@ function workspaceGlobBoundary(workspacePackageGlob) {
1242
791
  };
1243
792
  }
1244
793
  function hasVuePackage(capability) {
1245
- return capability.packages.some((nodePackage) => nodePackage.kind === "vue-app");
794
+ return findVuePackage(capability) !== undefined;
795
+ }
796
+ function findVuePackage(capability) {
797
+ return capability.packages.find((nodePackage) => nodePackage.kind === "vue-app");
1246
798
  }
1247
799
  function sourceRootPreset(capability) {
1248
800
  if (capability.packages.length === 1) {
@@ -1629,7 +1181,8 @@ function rustBinaryWorkspaceOperations({ context, state, packageScripts, package
1629
1181
  const rootManifest = rustRootPackageJson(context, packageScripts, state);
1630
1182
  const packageManifest = rustWorkspacePackageJson(context, workspacePackageScripts);
1631
1183
  const rustLayer = rustToolLayer();
1632
- const editorCustomization = editorCustomizationForCapabilities(state.editorCustomizationCapabilities);
1184
+ const dockerfileFragments = readDevelopmentContainerDockerfileFragments(state, ["rust"]);
1185
+ const editorCustomization = editorCustomizationForCapabilities(state.editorCustomizationCapabilities, editorCustomizationDeclarationsForState(state));
1633
1186
  const developmentContainer = dockerfileFirstRustPnpmDevcontainer({
1634
1187
  name: context.projectName.value,
1635
1188
  nodeLayer: nodePnpmToolLayer({
@@ -1637,6 +1190,7 @@ function rustBinaryWorkspaceOperations({ context, state, packageScripts, package
1637
1190
  packageManagerPin: context.toolchain.packageManagerPin.value,
1638
1191
  }),
1639
1192
  rustLayer,
1193
+ dockerfileFragments,
1640
1194
  extensions: editorCustomization.extensions,
1641
1195
  settings: editorCustomization.settings,
1642
1196
  });
@@ -2168,7 +1722,7 @@ function rootTsconfigReferences(workspace) {
2168
1722
  });
2169
1723
  }
2170
1724
  function oxcFormatLintOperations({ state, }) {
2171
- const editorCustomization = editorCustomizationForCapabilities(state.editorCustomizationCapabilities);
1725
+ const editorCustomization = editorCustomizationForCapabilities(state.editorCustomizationCapabilities, editorCustomizationDeclarationsForState(state));
2172
1726
  return [
2173
1727
  {
2174
1728
  kind: "copyFile",
@@ -2200,16 +1754,19 @@ function oxcFormatLintOperations({ state, }) {
2200
1754
  ];
2201
1755
  }
2202
1756
  function nodePnpmDevcontainerOperations({ context, state, }) {
2203
- const editorCustomization = editorCustomizationForCapabilities(state.editorCustomizationCapabilities);
1757
+ const additionalLayers = state.nodeWorkspace && hasVuePackage(state.nodeWorkspace)
1758
+ ? [browserTestToolLayer()]
1759
+ : [];
1760
+ const dockerfileFragments = readDevelopmentContainerDockerfileFragments(state, additionalLayers.length === 0 ? [] : ["browserTest"]);
1761
+ const editorCustomization = editorCustomizationForCapabilities(state.editorCustomizationCapabilities, editorCustomizationDeclarationsForState(state));
2204
1762
  const developmentContainer = checkedDockerfileFirstNodePnpmDevcontainer({
2205
1763
  name: context.projectName.value,
2206
1764
  layer: nodePnpmToolLayer({
2207
1765
  nodeVersion: context.toolchain.nodeLtsMajor.value,
2208
1766
  packageManagerPin: context.toolchain.packageManagerPin.value,
2209
1767
  }),
2210
- additionalLayers: state.nodeWorkspace && hasVuePackage(state.nodeWorkspace)
2211
- ? [browserTestToolLayer()]
2212
- : [],
1768
+ dockerfileFragments,
1769
+ additionalLayers,
2213
1770
  extensions: editorCustomization.extensions,
2214
1771
  settings: editorCustomization.settings,
2215
1772
  });
@@ -2224,6 +1781,51 @@ function nodePnpmDevcontainerOperations({ context, state, }) {
2224
1781
  },
2225
1782
  ];
2226
1783
  }
1784
+ function readDevelopmentContainerDockerfileFragments(state, optionalFragments) {
1785
+ if (state.devcontainerResource === undefined) {
1786
+ throw new Error("Projection Capability composition did not provide a Development Container Shared Resource");
1787
+ }
1788
+ const fragmentNames = {
1789
+ nodePnpm: "node-pnpm.Dockerfile",
1790
+ browserTest: "browser-test.Dockerfile",
1791
+ rust: "rust.Dockerfile",
1792
+ };
1793
+ const selected = new Set(optionalFragments);
1794
+ return {
1795
+ sourceRoot: state.devcontainerResource.sourceRootKey,
1796
+ nodePnpm: readDevelopmentContainerDockerfileFragment(state.devcontainerResource, fragmentNames.nodePnpm),
1797
+ ...(selected.has("browserTest")
1798
+ ? {
1799
+ browserTest: readDevelopmentContainerDockerfileFragment(state.devcontainerResource, fragmentNames.browserTest),
1800
+ }
1801
+ : {}),
1802
+ ...(selected.has("rust")
1803
+ ? {
1804
+ rust: readDevelopmentContainerDockerfileFragment(state.devcontainerResource, fragmentNames.rust),
1805
+ }
1806
+ : {}),
1807
+ };
1808
+ }
1809
+ function readDevelopmentContainerDockerfileFragment(resource, fragmentName) {
1810
+ const filePath = path.join(resource.root, fragmentName);
1811
+ try {
1812
+ return {
1813
+ from: fragmentName,
1814
+ text: readFileSync(filePath, "utf8"),
1815
+ };
1816
+ }
1817
+ catch (error) {
1818
+ if (isMissingDevelopmentContainerFragmentError(error)) {
1819
+ throw new Error(`Development Container Shared Resource ${resource.id} is missing Dockerfile fragment: ${fragmentName}`, { cause: error });
1820
+ }
1821
+ throw error;
1822
+ }
1823
+ }
1824
+ function isMissingDevelopmentContainerFragmentError(error) {
1825
+ return (error instanceof Error &&
1826
+ "code" in error &&
1827
+ (error.code === "ENOENT" || error.code === "ENOTDIR"));
1828
+ }
2227
1829
  function githubMaintenanceOperations() {
2228
1830
  return [
2229
1831
  {