@ykdz/template 0.0.30 → 0.0.32

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 (57) hide show
  1. package/node_modules/@ykdz/template-builtin-presets/dist/src/foundation.d.ts +8 -1
  2. package/node_modules/@ykdz/template-builtin-presets/dist/src/foundation.js +283 -149
  3. package/node_modules/@ykdz/template-builtin-presets/dist/src/rust-bin/definition.js +66 -9
  4. package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/development-container.d.ts +2 -0
  5. package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/development-container.js +35 -0
  6. package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/typescript.d.ts +13 -0
  7. package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/typescript.js +70 -0
  8. package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/vue.d.ts +2 -1
  9. package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/vue.js +27 -8
  10. package/node_modules/@ykdz/template-builtin-presets/dist/src/ts-cli/definition.js +4 -3
  11. package/node_modules/@ykdz/template-builtin-presets/dist/src/ts-lib/definition.js +4 -3
  12. package/node_modules/@ykdz/template-builtin-presets/dist/src/vike-app/definition.js +81 -11
  13. package/node_modules/@ykdz/template-builtin-presets/dist/src/vue-app/definition.js +3 -2
  14. package/node_modules/@ykdz/template-builtin-presets/dist/src/vue-hono-app/definition.js +21 -10
  15. package/node_modules/@ykdz/template-builtin-presets/package.json +2 -1
  16. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/.github/workflows/check.dynamic.template +2 -2
  17. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/devcontainer.json +3 -3
  18. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/gitignore +1 -0
  19. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/turbo.json +1 -0
  20. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/typescript-config/base.json +15 -0
  21. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/typescript-config/turbo.json +4 -0
  22. package/node_modules/@ykdz/template-builtin-presets/templates/rust-bin/devcontainer/rust.Dockerfile +3 -3
  23. package/node_modules/@ykdz/template-builtin-presets/templates/shared/devcontainer/browser-test.Dockerfile +1 -1
  24. package/node_modules/@ykdz/template-builtin-presets/templates/shared/oxc/tsconfig.config.json +2 -12
  25. package/node_modules/@ykdz/template-builtin-presets/templates/shared/vue/tsconfig.app.json +6 -13
  26. package/node_modules/@ykdz/template-builtin-presets/templates/shared/vue/tsconfig.node.json +2 -12
  27. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/tsconfig.json +2 -6
  28. package/node_modules/@ykdz/template-builtin-presets/templates/ts-lib/tsconfig.json +3 -3
  29. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db/tsconfig.json +2 -11
  30. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db-migrations/tsconfig.json +2 -11
  31. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/devcontainer/docker-client.Dockerfile +3 -0
  32. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/scripts/check-standalone-deployment.ts +67 -27
  33. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/tsconfig.app.json +6 -12
  34. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/tsconfig.node.json +2 -11
  35. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/tsconfig.test.json +0 -1
  36. package/node_modules/@ykdz/template-builtin-presets/templates/vue-hono-app/api/tsconfig.json +2 -12
  37. package/node_modules/@ykdz/template-core/dist/development-container-tool-layer.d.ts +56 -0
  38. package/node_modules/@ykdz/template-core/dist/development-container-tool-layer.js +361 -0
  39. package/node_modules/@ykdz/template-core/dist/package-contribution.d.ts +12 -1
  40. package/node_modules/@ykdz/template-core/dist/pnpm-workspace.yaml +7 -5
  41. package/node_modules/@ykdz/template-core/dist/project-github.d.ts +1 -9
  42. package/node_modules/@ykdz/template-core/dist/project-github.js +0 -49
  43. package/node_modules/@ykdz/template-core/dist/project-projection.d.ts +9 -0
  44. package/node_modules/@ykdz/template-core/dist/project-projection.js +84 -5
  45. package/node_modules/@ykdz/template-core/dist/renderer.d.ts +1 -0
  46. package/node_modules/@ykdz/template-core/dist/renderer.js +47 -7
  47. package/node_modules/@ykdz/template-core/dist/template-boundary-check.js +21 -0
  48. package/package.json +4 -4
  49. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/rust/.vscode/extensions.json +0 -3
  50. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/rust/.vscode/settings.json +0 -5
  51. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/rust/devcontainer/devcontainer.json +0 -21
  52. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/rust/devcontainer/rust.Dockerfile +0 -18
  53. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/rust/rust-toolchain.toml +0 -3
  54. package/node_modules/@ykdz/template-builtin-presets/templates/rust-bin/devcontainer/devcontainer.json +0 -21
  55. package/node_modules/@ykdz/template-core/dist/devcontainer.d.ts +0 -83
  56. package/node_modules/@ykdz/template-core/dist/devcontainer.js +0 -217
  57. /package/node_modules/@ykdz/template-builtin-presets/templates/{shared → vike-app}/devcontainer/shellcheck.Dockerfile +0 -0
@@ -1,3 +1,4 @@
1
+ import { type DevelopmentContainerToolLayerBuildArgument, type DevelopmentContainerToolLayerMount, type DevelopmentContainerToolLayerProbe, type PlannedDevelopmentContainerToolLayer } from "#template-core/development-container-tool-layer";
1
2
  import type { CheckEnvironmentNeed, DeploymentEnvironmentNeed } from "#template-core/module-graph";
2
3
  import { type PackageContribution } from "#template-core/package-contribution";
3
4
  import type { BuiltInPresetDefinition, GenerationContext } from "#template-core/preset-definition";
@@ -16,7 +17,7 @@ export type NextStepInstruction = {
16
17
  type GeneratedPackagePlanningRecord = {
17
18
  readonly path: string;
18
19
  readonly definitionName: string;
19
- readonly planningContribution: "planInitialization" | "planPackageAddition";
20
+ readonly planningContribution: "foundationPlan" | "planInitialization" | "planPackageAddition";
20
21
  };
21
22
  type GenerationRecord = {
22
23
  readonly schemaVersion: 1;
@@ -33,6 +34,12 @@ export type GeneratedRepositoryPlan = {
33
34
  readonly generationRecord: GenerationRecord;
34
35
  readonly operations: readonly RenderOperation[];
35
36
  readonly reconciliation: readonly ProjectProjectionReconciliation[];
37
+ readonly developmentContainer: {
38
+ readonly toolLayers: readonly PlannedDevelopmentContainerToolLayer[];
39
+ readonly buildArguments: readonly DevelopmentContainerToolLayerBuildArgument[];
40
+ readonly mounts: readonly DevelopmentContainerToolLayerMount[];
41
+ readonly probes: readonly DevelopmentContainerToolLayerProbe[];
42
+ };
36
43
  readonly environmentNeeds: readonly CheckEnvironmentNeed[];
37
44
  readonly deploymentEnvironmentNeeds: readonly DeploymentEnvironmentNeed[];
38
45
  /** Structured manifests used to derive the generated Dependency Catalog. */
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { collectGeneratedManifestCatalogReferences, selectTemplateDependencyCatalogEntries, } from "#template-core/dependency-catalog";
5
- import { browserTestToolLayer } from "#template-core/devcontainer";
5
+ import { planDevelopmentContainerToolLayersSync, } from "#template-core/development-container-tool-layer";
6
6
  import { editorCustomizationForCapabilities, loadEditorCustomizationDeclarations, } from "#template-core/editor-customization";
7
7
  import { checkEnvironmentNeedFact, checkEnvironmentNeedFromFact, deploymentEnvironmentNeedFact, deploymentEnvironmentNeedFromFact, renderDeploymentCheckCommand, renderFixCommand, renderRootCheckCommand, } from "#template-core/module-graph";
8
8
  import { assertPackageContribution, } from "#template-core/package-contribution";
@@ -11,6 +11,7 @@ import { projectCheckWorkflowTemplateReplacements, projectDependabotTemplateRepl
11
11
  import { planExplicitProjectLinks } from "#template-core/project-linking-v2";
12
12
  import { resolveTemplateSource, } from "#template-core/renderer";
13
13
  import { rustBinDefinition } from "./rust-bin/definition.js";
14
+ import { typescriptConfigContribution, typescriptConfigPackageDefinition, typescriptConfigPackageName, } from "./shared/typescript.js";
14
15
  import { vuePnpmDependencyOverrides } from "./shared/vue.js";
15
16
  import { templateSources } from "./template-sources.js";
16
17
  import { tsCliDefinition } from "./ts-cli/definition.js";
@@ -188,7 +189,7 @@ function readGenerationRecord(options) {
188
189
  if (!isRecord(item) ||
189
190
  typeof item.path !== "string" ||
190
191
  typeof item.definitionName !== "string" ||
191
- !["planInitialization", "planPackageAddition"].includes(String(item.planningContribution))) {
192
+ !["foundationPlan", "planInitialization", "planPackageAddition"].includes(String(item.planningContribution))) {
192
193
  throw new Error(`Package Addition requires valid package planning facts at .template/generation.json packages[${index}]`);
193
194
  }
194
195
  packages.push(item);
@@ -219,6 +220,13 @@ function readGenerationRecord(options) {
219
220
  return generationRecord;
220
221
  }
221
222
  function assertGenerationRecordFoundationConsistency(options) {
223
+ const configDefinition = typescriptConfigPackageDefinition(options.context);
224
+ const foundationRecords = options.generationRecord.packages.filter((record) => record.planningContribution === "foundationPlan");
225
+ if (foundationRecords.length !== 1 ||
226
+ foundationRecords[0].definitionName !== "foundation" ||
227
+ foundationRecords[0].path !== configDefinition.path) {
228
+ throw new Error("Package Addition Generation Record must contain exactly one Foundation TypeScript configuration provenance record");
229
+ }
222
230
  const initialRecords = options.generationRecord.packages.filter((record) => record.planningContribution === "planInitialization");
223
231
  if (initialRecords.length === 0) {
224
232
  throw new Error(`Package Addition Generation Record preset ${options.generationRecord.preset} has no initial Package provenance`);
@@ -370,7 +378,10 @@ function readExistingPackageAdditionState(options) {
370
378
  const generationRecord = readGenerationRecord(options);
371
379
  const persistedEnvironmentNeeds = readPersistedEnvironmentNeeds(options.context.targetDir);
372
380
  const manifestTruthByPackagePath = new Map();
373
- const contributions = generationRecord.packages.map((record) => {
381
+ const contributions = generationRecord.packages.flatMap((record) => {
382
+ if (record.planningContribution === "foundationPlan") {
383
+ return [];
384
+ }
374
385
  const expectedDefinition = options.blueprint.packages.find((definition) => definition.path === record.path);
375
386
  const definition = builtInPresetRegistry.require(record.definitionName);
376
387
  let candidates;
@@ -397,7 +408,7 @@ function readExistingPackageAdditionState(options) {
397
408
  if (contribution === undefined) {
398
409
  throw new Error(`Generation Record cannot reproduce Package Definition ${expectedDefinition.name} at ${expectedDefinition.path}`);
399
410
  }
400
- return contribution;
411
+ return [contribution];
401
412
  });
402
413
  for (const packagePath of new Set(options.manifestTruthPackagePaths ?? [])) {
403
414
  const expectedDefinition = options.blueprint.packages.find((definition) => definition.path === packagePath);
@@ -460,95 +471,267 @@ function turboBoundaryTagsForContributions(contributions) {
460
471
  }
461
472
  return [...selectedTags];
462
473
  }
463
- function devcontainerDockerfileOperations(options) {
464
- const hasBrowserTests = options.environmentNeeds.some((need) => need.kind === "playwright-browser-assets");
465
- const hasShellChecks = options.environmentNeeds.some((need) => need.kind === "shellcheck-command");
466
- const browserLayer = hasBrowserTests ? browserTestToolLayer() : undefined;
467
- return [
468
- {
469
- kind: "writeTextTemplate",
470
- source: templateSources.foundation,
471
- from: "devcontainer.json",
472
- to: ".devcontainer/devcontainer.json",
473
- replacements: {
474
- PROJECT_NAME: options.context.projectName,
475
- NODE_LTS_MAJOR: options.context.toolchain.nodeLtsMajor,
476
- PACKAGE_MANAGER_PIN: options.context.toolchain.packageManagerPin,
477
- PLAYWRIGHT_CLI_PACKAGE: browserLayer?.playwrightCliPackage ?? "unused",
474
+ function contributedDevcontainerComposition(options) {
475
+ const layerPlan = planDevelopmentContainerToolLayersSync({
476
+ baseLayer: {
477
+ identity: "node-pnpm",
478
+ dockerfile: {
479
+ source: templateSources.sharedDevcontainer,
480
+ from: "node-pnpm.Dockerfile",
478
481
  },
479
- },
480
- {
481
- kind: "writeTextFromFragments",
482
- to: ".devcontainer/Dockerfile",
483
- fragments: [
482
+ buildArguments: [
483
+ {
484
+ name: "NODE_VERSION",
485
+ value: options.context.toolchain.nodeLtsMajor,
486
+ },
487
+ {
488
+ name: "PACKAGE_MANAGER_PIN",
489
+ value: options.context.toolchain.packageManagerPin,
490
+ },
491
+ ],
492
+ mounts: [
484
493
  {
485
- source: templateSources.sharedDevcontainer,
486
- from: "node-pnpm.Dockerfile",
494
+ identity: "pnpm-store",
495
+ type: "volume",
496
+ source: "${devcontainerId}-pnpm-store",
497
+ target: "/pnpm/store",
487
498
  },
488
- ...(browserLayer === undefined
489
- ? []
490
- : [
491
- {
492
- source: templateSources.sharedDevcontainer,
493
- from: "browser-test.Dockerfile",
494
- },
495
- ]),
496
- ...(hasShellChecks
497
- ? [
498
- {
499
- source: templateSources.sharedDevcontainer,
500
- from: "shellcheck.Dockerfile",
501
- },
502
- ]
503
- : []),
504
499
  ],
505
500
  },
501
+ layers: options.layers,
502
+ });
503
+ return {
504
+ operations: [
505
+ {
506
+ kind: "writeTextTemplate",
507
+ source: templateSources.foundation,
508
+ from: "devcontainer.json",
509
+ to: ".devcontainer/devcontainer.json",
510
+ replacements: {
511
+ PROJECT_NAME: options.context.projectName,
512
+ NODE_LTS_MAJOR: options.context.toolchain.nodeLtsMajor,
513
+ PACKAGE_MANAGER_PIN: options.context.toolchain.packageManagerPin,
514
+ },
515
+ },
516
+ {
517
+ kind: "mergeJson",
518
+ to: ".devcontainer/devcontainer.json",
519
+ value: {
520
+ build: {
521
+ args: Object.fromEntries(layerPlan.buildArguments.map((argument) => [
522
+ argument.name,
523
+ argument.value,
524
+ ])),
525
+ },
526
+ ...(layerPlan.mounts.length === 0
527
+ ? {}
528
+ : {
529
+ mounts: layerPlan.mounts.map(({ identity: _identity, ...mount }) => mount),
530
+ }),
531
+ },
532
+ },
533
+ {
534
+ kind: "writeTextFromFragments",
535
+ to: ".devcontainer/Dockerfile",
536
+ validation: "development-container-dockerfile",
537
+ fragments: layerPlan.layers.map((layer) => layer.dockerfile),
538
+ },
539
+ ],
540
+ mountIdentitySet: {
541
+ location: "/mounts",
542
+ identity: {
543
+ kind: "projection",
544
+ members: layerPlan.mounts.map(({ identity, target, }) => ({
545
+ identity,
546
+ match: { target },
547
+ })),
548
+ fallback: { fields: ["target"] },
549
+ },
550
+ },
551
+ toolLayers: layerPlan.layers,
552
+ buildArguments: layerPlan.buildArguments,
553
+ mounts: layerPlan.mounts,
554
+ probes: layerPlan.probes,
555
+ };
556
+ }
557
+ function contributedFoundationTemplateFileOperations(contributions) {
558
+ const byIdentity = new Map();
559
+ const outputOwners = new Map();
560
+ for (const file of contributions.flatMap((contribution) => contribution.foundation.templateFiles ?? [])) {
561
+ const previous = byIdentity.get(file.identity);
562
+ if (previous !== undefined) {
563
+ const previousFingerprint = JSON.stringify({
564
+ source: resolveTemplateSource(previous.source, previous.from),
565
+ from: previous.from,
566
+ to: previous.to,
567
+ replacements: previous.replacements ?? {},
568
+ });
569
+ const fingerprint = JSON.stringify({
570
+ source: resolveTemplateSource(file.source, file.from),
571
+ from: file.from,
572
+ to: file.to,
573
+ replacements: file.replacements ?? {},
574
+ });
575
+ if (fingerprint !== previousFingerprint) {
576
+ throw new Error(`Foundation Template File identity ${file.identity} has conflicting descriptors`);
577
+ }
578
+ continue;
579
+ }
580
+ const outputOwner = outputOwners.get(file.to);
581
+ if (outputOwner !== undefined) {
582
+ throw new Error(`Foundation Template File output ${file.to} is declared by both ${outputOwner} and ${file.identity}`);
583
+ }
584
+ byIdentity.set(file.identity, file);
585
+ outputOwners.set(file.to, file.identity);
586
+ }
587
+ return [...byIdentity.values()]
588
+ .toSorted((left, right) => left.identity.localeCompare(right.identity))
589
+ .map((file) => ({
590
+ kind: "writeTextTemplate",
591
+ source: file.source,
592
+ from: file.from,
593
+ to: file.to,
594
+ replacements: { ...file.replacements },
595
+ }));
596
+ }
597
+ function assertCompatibleRustToolchainFacts(contributions) {
598
+ const facts = contributions.flatMap((contribution) => {
599
+ const rust = contribution.foundation.toolchains.rust;
600
+ return rust === undefined
601
+ ? []
602
+ : [
603
+ JSON.stringify({
604
+ toolchain: rust.toolchain,
605
+ components: [...new Set(rust.components)].toSorted(),
606
+ }),
607
+ ];
608
+ });
609
+ if (new Set(facts).size > 1) {
610
+ throw new Error("Foundation requires compatible Rust toolchain facts");
611
+ }
612
+ }
613
+ function composeDependencyMaintenancePolicy(contributions) {
614
+ const ecosystems = [
615
+ ...new Set(contributions.flatMap((contribution) => contribution.foundation.dependencyMaintenance.ecosystems)),
506
616
  ];
617
+ const directories = {};
618
+ const extraDirectories = {};
619
+ for (const ecosystem of ecosystems) {
620
+ const candidates = [
621
+ ...new Set(contributions.flatMap((contribution) => {
622
+ const policy = contribution.foundation.dependencyMaintenance;
623
+ const primary = policy.directories?.[ecosystem];
624
+ return [
625
+ ...(primary === undefined ? [] : [primary]),
626
+ ...(policy.extraDirectories?.[ecosystem] ?? []),
627
+ ];
628
+ })),
629
+ ];
630
+ const [primary, ...extra] = candidates;
631
+ if (primary !== undefined)
632
+ directories[ecosystem] = primary;
633
+ if (extra.length > 0)
634
+ extraDirectories[ecosystem] = extra;
635
+ }
636
+ return {
637
+ ecosystems,
638
+ ...(Object.keys(directories).length === 0 ? {} : { directories }),
639
+ ...(Object.keys(extraDirectories).length === 0 ? {} : { extraDirectories }),
640
+ interval: "weekly",
641
+ };
507
642
  }
508
643
  function foundationPlan(options) {
509
644
  assertProjectBlueprintV2(options.blueprint);
645
+ const configDefinition = typescriptConfigPackageDefinition(options.context);
646
+ const persistedConfigDefinition = options.blueprint.packages.find((definition) => definition.name === configDefinition.name ||
647
+ definition.path === configDefinition.path);
648
+ if (persistedConfigDefinition === undefined ||
649
+ !packageDefinitionsEqual(persistedConfigDefinition, configDefinition)) {
650
+ throw new Error("Project Blueprint must contain the Foundation TypeScript configuration Package Definition");
651
+ }
652
+ const initialProjectLinkPlan = planExplicitProjectLinks({
653
+ blueprint: options.blueprint,
654
+ contributions: options.contributions,
655
+ ...(options.manifestTruthByPackagePath === undefined
656
+ ? {}
657
+ : {
658
+ manifestTruthByPackagePath: options.manifestTruthByPackagePath,
659
+ }),
660
+ });
661
+ const injectedProviderNames = new Set([...initialProjectLinkPlan.manifestPatchesByPackagePath.values()].flatMap((patch) => Object.entries(patch.dependenciesMeta)
662
+ .filter(([, metadata]) => metadata.injected)
663
+ .map(([name]) => name)));
664
+ const configPackageName = typescriptConfigPackageName(options.context);
665
+ const packageContributions = options.contributions.map((contribution) => {
666
+ const ownsTypeScriptConfig = contribution.operations.some((operation) => "to" in operation &&
667
+ operation.to === `${contribution.definition.path}/tsconfig.json`);
668
+ if (!ownsTypeScriptConfig)
669
+ return contribution;
670
+ const dependencyField = injectedProviderNames.has(contribution.definition.name)
671
+ ? "dependencies"
672
+ : "devDependencies";
673
+ const existingDependencies = contribution.manifest[dependencyField];
674
+ return {
675
+ ...contribution,
676
+ manifest: {
677
+ ...contribution.manifest,
678
+ [dependencyField]: {
679
+ ...(isRecord(existingDependencies) ? existingDependencies : {}),
680
+ [configPackageName]: "workspace:*",
681
+ },
682
+ },
683
+ };
684
+ });
685
+ const configContribution = typescriptConfigContribution(options.context);
686
+ const contributions = [configContribution, ...packageContributions];
510
687
  const generationRecord = options.generationRecord ?? {
511
688
  schemaVersion: 1,
512
689
  preset: options.definition.metadata.name,
513
690
  templateVersion: "0.0.0",
514
691
  toolchain: options.context.toolchain,
515
- packages: options.contributions.map((contribution) => ({
516
- path: contribution.definition.path,
517
- definitionName: options.definition.metadata.name,
518
- planningContribution: options.mode === "initialization"
519
- ? "planInitialization"
520
- : "planPackageAddition",
521
- })),
692
+ packages: [
693
+ ...options.contributions.map((contribution) => ({
694
+ path: contribution.definition.path,
695
+ definitionName: options.definition.metadata.name,
696
+ planningContribution: options.mode === "initialization"
697
+ ? "planInitialization"
698
+ : "planPackageAddition",
699
+ })),
700
+ {
701
+ path: configDefinition.path,
702
+ definitionName: "foundation",
703
+ planningContribution: "foundationPlan",
704
+ },
705
+ ],
522
706
  };
523
- const environmentNeeds = uniqueEnvironmentNeeds(options.contributions.flatMap((item) => item.environmentNeeds));
707
+ const environmentNeeds = uniqueEnvironmentNeeds(contributions.flatMap((item) => item.environmentNeeds));
524
708
  const deploymentEnvironmentNeeds = uniqueEnvironmentNeeds([
525
709
  ...(options.existingDeploymentEnvironmentNeeds ?? []),
526
- ...options.contributions.flatMap((item) => item.deploymentEnvironmentNeeds ?? []),
710
+ ...contributions.flatMap((item) => item.deploymentEnvironmentNeeds ?? []),
527
711
  ]);
528
712
  const persistedEnvironmentNeeds = {
529
713
  schemaVersion: 1,
530
714
  check: environmentNeeds.map(checkEnvironmentNeedFact),
531
715
  deployment: deploymentEnvironmentNeeds.map(deploymentEnvironmentNeedFact),
532
716
  };
533
- const hasDeploymentTask = options.contributions.some((contribution) => {
717
+ const hasDeploymentTask = contributions.some((contribution) => {
534
718
  const scripts = contribution.manifest.scripts;
535
719
  return (typeof scripts === "object" &&
536
720
  scripts !== null &&
537
721
  typeof scripts.deployment === "string");
538
722
  });
539
- const packagePaths = options.contributions.map((contribution) => contribution.definition.path);
723
+ const packagePaths = contributions.map((contribution) => contribution.definition.path);
540
724
  if (new Set(packagePaths).size !== packagePaths.length)
541
725
  throw new Error("Package Contributions must have unique Package Paths");
542
- const packageNames = options.contributions.map((contribution) => contribution.definition.name);
726
+ const packageNames = contributions.map((contribution) => contribution.definition.name);
543
727
  if (new Set(packageNames).size !== packageNames.length)
544
728
  throw new Error("Package Contributions must have unique package names");
545
- const rustToolchain = options.contributions
546
- .map((contribution) => contribution.foundation.toolchains.rust)
547
- .find((toolchain) => toolchain !== undefined);
548
- if (options.contributions.some((contribution) => contribution.foundation.toolchains.rust !== undefined &&
549
- contribution.foundation.toolchains.rust !== rustToolchain)) {
550
- throw new Error("Foundation requires one coordinated Rust toolchain");
551
- }
729
+ assertCompatibleRustToolchainFacts(contributions);
730
+ const contributedToolLayers = contributions.flatMap((contribution) => contribution.foundation.developmentContainerToolLayers ?? []);
731
+ const developmentContainer = contributedDevcontainerComposition({
732
+ context: options.context,
733
+ layers: contributedToolLayers,
734
+ });
552
735
  const workspacePackageGlobs = [
553
736
  "apps/*",
554
737
  "packages/*",
@@ -556,20 +739,13 @@ function foundationPlan(options) {
556
739
  ...options.blueprint.packages
557
740
  .map((definition) => `${definition.path.split("/")[0]}/*`)
558
741
  .filter((glob) => glob !== "apps/*" && glob !== "packages/*"),
559
- ...options.contributions
742
+ ...contributions
560
743
  .flatMap((contribution) => contribution.foundation.workspacePackageGlobs ?? [])
561
744
  .filter((glob) => glob !== "apps/*" && glob !== "packages/*"),
562
745
  ]),
563
746
  ];
564
- const editorCustomization = editorCustomizationForCapabilities(options.contributions.flatMap((contribution) => contribution.foundation.editorCapabilities), loadEditorCustomizationDeclarations(resolveTemplateSource(templateSources.editorCustomization, "capabilities.json")));
565
- const dependencyMaintenancePolicy = {
566
- ecosystems: [
567
- ...new Set(options.contributions.flatMap((contribution) => contribution.foundation.dependencyMaintenance.ecosystems)),
568
- ],
569
- directories: Object.assign({}, ...options.contributions.map((contribution) => contribution.foundation.dependencyMaintenance.directories ?? {})),
570
- extraDirectories: Object.assign({}, ...options.contributions.map((contribution) => contribution.foundation.dependencyMaintenance.extraDirectories ?? {})),
571
- interval: "weekly",
572
- };
747
+ const editorCustomization = editorCustomizationForCapabilities(contributions.flatMap((contribution) => contribution.foundation.editorCapabilities), loadEditorCustomizationDeclarations(resolveTemplateSource(templateSources.editorCustomization, "capabilities.json")));
748
+ const dependencyMaintenancePolicy = composeDependencyMaintenancePolicy(contributions);
573
749
  const rootManifest = {
574
750
  name: options.context.projectName,
575
751
  version: "0.0.0",
@@ -600,7 +776,7 @@ function foundationPlan(options) {
600
776
  packageManager: options.context.toolchain.packageManagerPin,
601
777
  };
602
778
  const dependencyCatalog = selectTemplateDependencyCatalogEntries(collectGeneratedManifestCatalogReferences([
603
- ...options.contributions.map((contribution) => contribution.manifest),
779
+ ...contributions.map((contribution) => contribution.manifest),
604
780
  rootManifest,
605
781
  ]));
606
782
  const dependencyOverrides = {
@@ -640,7 +816,6 @@ function foundationPlan(options) {
640
816
  from: ".github/workflows/check.dynamic.template",
641
817
  to: ".github/workflows/check.yml",
642
818
  replacements: projectCheckWorkflowTemplateReplacements({
643
- environment: { needs: environmentNeeds },
644
819
  deploymentEnvironmentNeeds,
645
820
  hasDeploymentTask,
646
821
  }),
@@ -655,16 +830,8 @@ function foundationPlan(options) {
655
830
  replacements: projectDependabotTemplateReplacements(dependencyMaintenancePolicy),
656
831
  },
657
832
  ];
658
- const projectLinkPlan = planExplicitProjectLinks({
659
- blueprint: options.blueprint,
660
- contributions: options.contributions,
661
- ...(options.manifestTruthByPackagePath === undefined
662
- ? {}
663
- : {
664
- manifestTruthByPackagePath: options.manifestTruthByPackagePath,
665
- }),
666
- });
667
- const turboBoundaryTags = turboBoundaryTagsForContributions(options.contributions);
833
+ const projectLinkPlan = initialProjectLinkPlan;
834
+ const turboBoundaryTags = turboBoundaryTagsForContributions(contributions);
668
835
  const initializationFoundationOperations = [
669
836
  {
670
837
  kind: "writeJson",
@@ -741,64 +908,8 @@ function foundationPlan(options) {
741
908
  to: ".vscode/settings.json",
742
909
  value: editorCustomization.settings,
743
910
  },
744
- ...(rustToolchain === undefined
745
- ? [
746
- ...devcontainerDockerfileOperations({
747
- context: options.context,
748
- environmentNeeds,
749
- }),
750
- ]
751
- : [
752
- {
753
- kind: "writeTextTemplate",
754
- source: templateSources.foundation,
755
- from: "rust/rust-toolchain.toml",
756
- to: "rust-toolchain.toml",
757
- replacements: { RUST_TOOLCHAIN: rustToolchain.toolchain },
758
- },
759
- {
760
- kind: "writeTextTemplate",
761
- source: templateSources.foundation,
762
- from: "rust/devcontainer/devcontainer.json",
763
- to: ".devcontainer/devcontainer.json",
764
- replacements: {
765
- PROJECT_NAME: options.context.projectName,
766
- NODE_LTS_MAJOR: options.context.toolchain.nodeLtsMajor,
767
- PACKAGE_MANAGER_PIN: options.context.toolchain.packageManagerPin,
768
- RUST_TOOLCHAIN: rustToolchain.toolchain,
769
- },
770
- },
771
- {
772
- kind: "writeTextFromFragments",
773
- to: ".devcontainer/Dockerfile",
774
- fragments: [
775
- {
776
- source: templateSources.sharedDevcontainer,
777
- from: "node-pnpm.Dockerfile",
778
- },
779
- {
780
- source: templateSources.foundation,
781
- from: "rust/devcontainer/rust.Dockerfile",
782
- },
783
- ...(environmentNeeds.some((need) => need.kind === "playwright-browser-assets")
784
- ? [
785
- {
786
- source: templateSources.sharedDevcontainer,
787
- from: "browser-test.Dockerfile",
788
- },
789
- ]
790
- : []),
791
- ...(environmentNeeds.some((need) => need.kind === "shellcheck-command")
792
- ? [
793
- {
794
- source: templateSources.sharedDevcontainer,
795
- from: "shellcheck.Dockerfile",
796
- },
797
- ]
798
- : []),
799
- ],
800
- },
801
- ]),
911
+ ...developmentContainer.operations,
912
+ ...contributedFoundationTemplateFileOperations(contributions),
802
913
  ...workflowOperations,
803
914
  {
804
915
  kind: "writeJson",
@@ -843,7 +954,15 @@ function foundationPlan(options) {
843
954
  };
844
955
  const withProvenance = (operation, provenance) => ({ ...operation, provenance });
845
956
  const operations = [
846
- ...(options.renderContributions ?? options.contributions)
957
+ ...(options.renderContributions === undefined
958
+ ? contributions
959
+ : options.renderContributions.map((renderContribution) => {
960
+ const enriched = packageContributions.find((candidate) => candidate.definition.path === renderContribution.definition.path);
961
+ if (enriched === undefined) {
962
+ throw new Error(`Foundation cannot render unknown Package Contribution ${renderContribution.definition.path}`);
963
+ }
964
+ return enriched;
965
+ }))
847
966
  .map((item) => assertPackageContribution(item, {
848
967
  definitionName: options.definition.metadata.name,
849
968
  planner: options.mode === "addition"
@@ -865,7 +984,11 @@ function foundationPlan(options) {
865
984
  ];
866
985
  const reconciliation = [
867
986
  { path: "turbo.json", driver: "structured" },
868
- { path: ".devcontainer/devcontainer.json", driver: "structured" },
987
+ {
988
+ path: ".devcontainer/devcontainer.json",
989
+ driver: "structured",
990
+ identitySets: [developmentContainer.mountIdentitySet],
991
+ },
869
992
  {
870
993
  path: ".vscode/extensions.json",
871
994
  driver: "structured",
@@ -890,24 +1013,35 @@ function foundationPlan(options) {
890
1013
  generationRecord,
891
1014
  operations,
892
1015
  reconciliation,
1016
+ developmentContainer: {
1017
+ toolLayers: developmentContainer.toolLayers,
1018
+ buildArguments: developmentContainer.buildArguments,
1019
+ mounts: developmentContainer.mounts,
1020
+ probes: developmentContainer.probes,
1021
+ },
893
1022
  environmentNeeds,
894
1023
  deploymentEnvironmentNeeds,
895
- manifests: [
896
- ...options.contributions.map((item) => item.manifest),
897
- rootManifest,
898
- ],
1024
+ manifests: [...contributions.map((item) => item.manifest), rootManifest],
899
1025
  dependencyCatalog,
900
1026
  dependencyMaintenancePolicy,
901
1027
  nextStepInstructions: [
902
1028
  { display: "pnpm install" },
903
- ...environmentNeeds.map((need) => ({ display: need.nextStep.display })),
904
1029
  { display: "pnpm run fix" },
905
1030
  { display: "pnpm run check" },
906
1031
  ],
907
1032
  };
908
1033
  }
909
1034
  export function planGeneratedRepositoryInitialization(options) {
910
- const blueprint = options.definition.blueprint(options.context);
1035
+ const presetBlueprint = options.definition.blueprint(options.context);
1036
+ const configDefinition = typescriptConfigPackageDefinition(options.context);
1037
+ if (presetBlueprint.packages.some((definition) => definition.name === configDefinition.name ||
1038
+ definition.path === configDefinition.path)) {
1039
+ throw new Error("Preset Blueprint must not redefine the Foundation TypeScript configuration Package Definition");
1040
+ }
1041
+ const blueprint = {
1042
+ ...presetBlueprint,
1043
+ packages: [...presetBlueprint.packages, configDefinition],
1044
+ };
911
1045
  const contributions = options.definition.planInitializationContributions?.(options.context) ?? [options.definition.planInitialization(options.context)];
912
1046
  return foundationPlan({
913
1047
  definition: options.definition,