@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
@@ -248,6 +248,21 @@ function memberIdentity(member, policy) {
248
248
  return scalarIdentity(member);
249
249
  if (!isStructuredObject(member))
250
250
  return undefined;
251
+ if (policy.identity.kind === "projection") {
252
+ const matches = policy.identity.members.filter(({ match }) => Object.entries(match).every(([field, value]) => Object.hasOwn(member, field) &&
253
+ structuredValuesEqual(member[field], value)));
254
+ const matchingIdentities = new Set(matches.map(({ identity }) => identity));
255
+ if (matchingIdentities.size > 1)
256
+ return undefined;
257
+ const [matchingIdentity] = matchingIdentities;
258
+ if (matchingIdentity !== undefined) {
259
+ return `projection:${JSON.stringify(matchingIdentity)}`;
260
+ }
261
+ const fallbackFields = policy.identity.fallback.fields.map((field) => Object.hasOwn(member, field) ? scalarIdentity(member[field]) : undefined);
262
+ return fallbackFields.every((field) => field !== undefined)
263
+ ? `fallback:${JSON.stringify(fallbackFields)}`
264
+ : undefined;
265
+ }
251
266
  const fields = policy.identity.fields.map((field) => Object.hasOwn(member, field) ? scalarIdentity(member[field]) : undefined);
252
267
  return fields.every((field) => field !== undefined)
253
268
  ? JSON.stringify(fields)
@@ -472,8 +487,52 @@ function assertProjectionContract(projection) {
472
487
  }
473
488
  return policies;
474
489
  }
475
- function reconciliationPoliciesEqual(left, right) {
476
- return JSON.stringify(left) === JSON.stringify(right);
490
+ function reconciliationPoliciesCompatible(left, right) {
491
+ if (left.path !== right.path ||
492
+ left.driver !== right.driver ||
493
+ left.driver !== "structured" ||
494
+ right.driver !== "structured") {
495
+ return JSON.stringify(left) === JSON.stringify(right);
496
+ }
497
+ const leftIdentitySets = left.identitySets ?? [];
498
+ const rightIdentitySets = right.identitySets ?? [];
499
+ if (leftIdentitySets.length !== rightIdentitySets.length)
500
+ return false;
501
+ return leftIdentitySets.every((leftPolicy, index) => {
502
+ const rightPolicy = rightIdentitySets[index];
503
+ if (rightPolicy === undefined ||
504
+ leftPolicy.location !== rightPolicy.location ||
505
+ leftPolicy.identity.kind !== rightPolicy.identity.kind) {
506
+ return false;
507
+ }
508
+ if (leftPolicy.identity.kind !== "projection" ||
509
+ rightPolicy.identity.kind !== "projection") {
510
+ return JSON.stringify(leftPolicy) === JSON.stringify(rightPolicy);
511
+ }
512
+ return (JSON.stringify(leftPolicy.identity.fallback) ===
513
+ JSON.stringify(rightPolicy.identity.fallback));
514
+ });
515
+ }
516
+ function combinedIdentitySets(before, after) {
517
+ const beforeIdentitySets = before?.driver === "structured" ? (before.identitySets ?? []) : [];
518
+ const afterIdentitySets = after?.driver === "structured" ? (after.identitySets ?? []) : [];
519
+ return afterIdentitySets.map((afterPolicy, index) => {
520
+ const beforePolicy = beforeIdentitySets[index];
521
+ if (beforePolicy?.identity.kind !== "projection" ||
522
+ afterPolicy.identity.kind !== "projection") {
523
+ return afterPolicy;
524
+ }
525
+ return {
526
+ ...afterPolicy,
527
+ identity: {
528
+ ...afterPolicy.identity,
529
+ members: [
530
+ ...beforePolicy.identity.members,
531
+ ...afterPolicy.identity.members,
532
+ ],
533
+ },
534
+ };
535
+ });
477
536
  }
478
537
  function structuredValueAtLocation(root, pathSegments) {
479
538
  let value = root;
@@ -521,6 +580,24 @@ function assertIdentitySetPolicies(entry, reconciliation) {
521
580
  policy.identity.fields.some((field) => field.length === 0))) {
522
581
  throw new Error(`Project Projection identity-set fields must be unique non-empty names: ${entry.path} ${location}`);
523
582
  }
583
+ if (policy.identity.kind === "projection") {
584
+ const fallbackFields = policy.identity.fallback.fields;
585
+ if (fallbackFields.length === 0 ||
586
+ new Set(fallbackFields).size !== fallbackFields.length ||
587
+ fallbackFields.some((field) => field.length === 0)) {
588
+ throw new Error(`Project Projection identity-set projection fallback fields must be unique non-empty names: ${entry.path} ${location}`);
589
+ }
590
+ const memberIdentities = policy.identity.members.map((member) => member.identity);
591
+ if (memberIdentities.some((identity) => identity.length === 0) ||
592
+ new Set(memberIdentities).size !== memberIdentities.length ||
593
+ policy.identity.members.some((member) => Object.keys(member.match).length === 0 ||
594
+ Object.values(member.match).some((value) => value !== null &&
595
+ typeof value !== "string" &&
596
+ typeof value !== "number" &&
597
+ typeof value !== "boolean"))) {
598
+ throw new Error(`Project Projection identity-set projection members require unique non-empty identities and non-empty scalar matches: ${entry.path} ${location}`);
599
+ }
600
+ }
524
601
  const value = structuredValueAtLocation(parsed.value, pathSegments);
525
602
  if (!Array.isArray(value)) {
526
603
  throw new Error(`Project Projection identity-set location must reference an array: ${entry.path} ${location}`);
@@ -547,7 +624,7 @@ export async function reconcileProjectProjections(options) {
547
624
  }
548
625
  if (afterPolicy !== undefined &&
549
626
  beforePolicy !== undefined &&
550
- !reconciliationPoliciesEqual(beforePolicy, afterPolicy)) {
627
+ !reconciliationPoliciesCompatible(beforePolicy, afterPolicy)) {
551
628
  throw new Error(`Project Projection reconciliation policy changed for ${projectionPath}`);
552
629
  }
553
630
  }
@@ -561,7 +638,9 @@ export async function reconcileProjectProjections(options) {
561
638
  for (const projectionPath of deltaPaths) {
562
639
  const before = beforeByPath.get(projectionPath);
563
640
  const after = afterByPath.get(projectionPath);
564
- const policy = afterPolicies.get(projectionPath) ?? beforePolicies.get(projectionPath);
641
+ const beforePolicy = beforePolicies.get(projectionPath);
642
+ const afterPolicy = afterPolicies.get(projectionPath);
643
+ const policy = afterPolicy ?? beforePolicy;
565
644
  const driver = policy.driver;
566
645
  const current = await options.readCurrent(projectionPath);
567
646
  if (entriesEqual(current, after))
@@ -622,7 +701,7 @@ export async function reconcileProjectProjections(options) {
622
701
  before,
623
702
  current,
624
703
  after,
625
- identitySets: policy.identitySets ?? [],
704
+ identitySets: combinedIdentitySets(beforePolicy, afterPolicy),
626
705
  })
627
706
  : undefined;
628
707
  const text = driver === "text"
@@ -66,6 +66,7 @@ export type WriteTextFromFragmentsOperation = {
66
66
  from: string;
67
67
  source: TemplateSourceHandle;
68
68
  }[];
69
+ validation?: "development-container-dockerfile" | undefined;
69
70
  overwrite?: boolean;
70
71
  provenance?: RenderOperationProvenance;
71
72
  };
@@ -176,16 +176,55 @@ async function renderWriteText(operation, options) {
176
176
  await mkdir(path.dirname(to), { recursive: true });
177
177
  await writeGeneratedFile(to, operation.text);
178
178
  }
179
- async function renderWriteTextFromFragments(operation, options) {
180
- const toPath = expandOperationPath(operation.to, options);
181
- const to = resolveContainedPath(options.targetRoot, toPath);
182
- const texts = await Promise.all(operation.fragments.map(async (fragment) => {
179
+ async function loadTextFragments(operation, options) {
180
+ return Promise.all(operation.fragments.map(async (fragment) => {
183
181
  const fromPath = expandTemplatePath(fragment.from, options.variables ?? {});
184
182
  const from = resolveTemplateSource(fragment.source, fromPath);
185
- return readFile(from, "utf8");
183
+ return { from: fromPath, text: await readFile(from, "utf8") };
186
184
  }));
185
+ }
186
+ async function validateDevelopmentContainerDockerfileOperation(operation, fragments) {
187
+ if (operation.validation !== "development-container-dockerfile") {
188
+ return;
189
+ }
190
+ const [baseFragment, ...optionalFragments] = fragments;
191
+ if (baseFragment === undefined) {
192
+ throw new Error("Development Container Dockerfile requires a base fragment.");
193
+ }
194
+ const { validateDevelopmentContainerDockerfileFragments } = await import("./development-container-tool-layer.js");
195
+ validateDevelopmentContainerDockerfileFragments({
196
+ baseFragment: {
197
+ identity: baseFragment.from,
198
+ from: baseFragment.from,
199
+ text: baseFragment.text,
200
+ },
201
+ fragments: optionalFragments.map((fragment) => ({
202
+ identity: fragment.from,
203
+ from: fragment.from,
204
+ text: fragment.text,
205
+ })),
206
+ });
207
+ }
208
+ async function preflightDevelopmentContainerDockerfiles(options) {
209
+ const loadedFragments = new Map();
210
+ for (const operation of options.operations) {
211
+ if (operation.kind !== "writeTextFromFragments" ||
212
+ operation.validation !== "development-container-dockerfile") {
213
+ continue;
214
+ }
215
+ const fragments = await loadTextFragments(operation, options);
216
+ await validateDevelopmentContainerDockerfileOperation(operation, fragments);
217
+ loadedFragments.set(operation, fragments);
218
+ }
219
+ return loadedFragments;
220
+ }
221
+ async function renderWriteTextFromFragments(operation, options, preloadedFragments) {
222
+ const toPath = expandOperationPath(operation.to, options);
223
+ const to = resolveContainedPath(options.targetRoot, toPath);
224
+ const fragments = preloadedFragments ?? (await loadTextFragments(operation, options));
225
+ await validateDevelopmentContainerDockerfileOperation(operation, fragments);
187
226
  await mkdir(path.dirname(to), { recursive: true });
188
- await writeGeneratedFile(to, texts.map((text) => text.trimEnd()).join("\n\n") + "\n", operation.overwrite ?? false);
227
+ await writeGeneratedFile(to, fragments.map(({ text }) => text.trimEnd()).join("\n\n") + "\n", operation.overwrite ?? false);
189
228
  }
190
229
  function replaceTextTemplateVariables(sourceText, replacements) {
191
230
  const used = new Set();
@@ -341,6 +380,7 @@ async function writeGeneratedFile(to, text, overwrite = false) {
341
380
  }
342
381
  }
343
382
  export async function renderProject(options) {
383
+ const preloadedDevelopmentContainerDockerfiles = await preflightDevelopmentContainerDockerfiles(options);
344
384
  for (const operation of options.operations) {
345
385
  if (operation.kind === "copyFile") {
346
386
  await renderCopyFile(operation, options);
@@ -363,7 +403,7 @@ export async function renderProject(options) {
363
403
  continue;
364
404
  }
365
405
  if (operation.kind === "writeTextFromFragments") {
366
- await renderWriteTextFromFragments(operation, options);
406
+ await renderWriteTextFromFragments(operation, options, preloadedDevelopmentContainerDockerfiles.get(operation));
367
407
  continue;
368
408
  }
369
409
  if (operation.kind === "writeTextTemplate") {
@@ -479,7 +479,28 @@ function isStructuralTsconfigConfigValue(value) {
479
479
  function isDevelopmentContainerPlanValue(expression) {
480
480
  return isPropertyAccess(expression, "developmentContainer", "devcontainer");
481
481
  }
482
+ function isDevelopmentContainerToolLayerMergeValue(value) {
483
+ if (!isRecord(value) ||
484
+ !(hasExactKeys(value, ["build"]) || hasExactKeys(value, ["build", "mounts"])) ||
485
+ !isRecord(value.build) ||
486
+ !hasExactKeys(value.build, ["args"]) ||
487
+ !isRecord(value.build.args) ||
488
+ !Object.values(value.build.args).every((argument) => typeof argument === "string")) {
489
+ return false;
490
+ }
491
+ return (value.mounts === undefined ||
492
+ (Array.isArray(value.mounts) &&
493
+ value.mounts.every((mount) => isRecord(mount) &&
494
+ hasExactKeys(mount, ["type", "source", "target"]) &&
495
+ (mount.type === "bind" || mount.type === "volume") &&
496
+ typeof mount.source === "string" &&
497
+ typeof mount.target === "string")));
498
+ }
482
499
  function isAllowedStructuralMachineDeclaration(sourceFile, operation) {
500
+ if (operation.kind === "mergeJson" &&
501
+ operation.to === ".devcontainer/devcontainer.json") {
502
+ return isDevelopmentContainerToolLayerMergeValue(operation.value);
503
+ }
483
504
  if (operation.kind !== "writeJson") {
484
505
  return false;
485
506
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ykdz/template",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
4
4
  "private": false,
5
5
  "description": "Project template generator for YKDZ starter projects.",
6
6
  "keywords": [
@@ -60,8 +60,8 @@
60
60
  "semver": "^7.8.5",
61
61
  "typescript": "npm:typescript@^6.0.3",
62
62
  "valibot": "^1.4.2",
63
- "@ykdz/template-builtin-presets": "0.0.0",
64
- "@ykdz/template-core": "0.0.0"
63
+ "@ykdz/template-core": "0.0.0",
64
+ "@ykdz/template-builtin-presets": "0.0.0"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@types/node": "^24.13.3",
@@ -70,7 +70,7 @@
70
70
  "execa": "^10.0.0",
71
71
  "oxfmt": "^0.58.0",
72
72
  "oxlint": "^1.73.0",
73
- "oxlint-tsgolint": "^0.24.0",
73
+ "oxlint-tsgolint": "^7.0.2001",
74
74
  "typescript-7": "npm:typescript@^7.0.2",
75
75
  "vitest": "^4.1.10"
76
76
  },
@@ -1,3 +0,0 @@
1
- {
2
- "recommendations": ["rust-lang.rust-analyzer", "tamasfe.even-better-toml"]
3
- }
@@ -1,5 +0,0 @@
1
- {
2
- "[rust]": {
3
- "editor.defaultFormatter": "rust-lang.rust-analyzer"
4
- }
5
- }
@@ -1,21 +0,0 @@
1
- {
2
- "name": "{{PROJECT_NAME}}",
3
- "build": {
4
- "dockerfile": "Dockerfile",
5
- "args": {
6
- "NODE_VERSION": "{{NODE_LTS_MAJOR}}",
7
- "PACKAGE_MANAGER_PIN": "{{PACKAGE_MANAGER_PIN}}",
8
- "RUST_TOOLCHAIN": "{{RUST_TOOLCHAIN}}"
9
- }
10
- },
11
- "customizations": {
12
- "vscode": {
13
- "extensions": ["rust-lang.rust-analyzer", "tamasfe.even-better-toml"]
14
- }
15
- },
16
- "mounts": [
17
- "source=${localWorkspaceFolderBasename}-cargo-registry,target=/usr/local/cargo/registry,type=volume",
18
- "source=${localWorkspaceFolderBasename}-cargo-git,target=/usr/local/cargo/git,type=volume",
19
- "source=${localWorkspaceFolderBasename}-target,target=${containerWorkspaceFolder}/target,type=volume"
20
- ]
21
- }
@@ -1,18 +0,0 @@
1
- ARG RUST_TOOLCHAIN
2
- ENV RUSTUP_HOME=/usr/local/rustup
3
- ENV CARGO_HOME=/usr/local/cargo
4
- ENV PATH="/usr/local/cargo/bin:${PATH}"
5
-
6
- RUN apt-get update && apt-get install -y --no-install-recommends \
7
- ca-certificates \
8
- curl \
9
- gcc \
10
- git \
11
- libc6-dev \
12
- && rm -rf /var/lib/apt/lists/* \
13
- && git config --system init.defaultBranch main
14
- RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
15
- sh -s -- -y --profile minimal --default-toolchain none \
16
- && rustup toolchain install ${RUST_TOOLCHAIN} --profile minimal --component rustfmt --component clippy \
17
- && rustup default ${RUST_TOOLCHAIN} \
18
- && chmod -R a+w ${RUSTUP_HOME} ${CARGO_HOME}
@@ -1,3 +0,0 @@
1
- [toolchain]
2
- channel = "{{RUST_TOOLCHAIN}}"
3
- components = ["rustfmt", "clippy"]
@@ -1,21 +0,0 @@
1
- {
2
- "name": "{{PROJECT_NAME}}",
3
- "build": {
4
- "dockerfile": "Dockerfile",
5
- "args": {
6
- "NODE_VERSION": "{{NODE_LTS_MAJOR}}",
7
- "PACKAGE_MANAGER_PIN": "{{PACKAGE_MANAGER_PIN}}",
8
- "RUST_TOOLCHAIN": "{{RUST_TOOLCHAIN}}"
9
- }
10
- },
11
- "customizations": {
12
- "vscode": {
13
- "extensions": ["rust-lang.rust-analyzer", "tamasfe.even-better-toml"]
14
- }
15
- },
16
- "mounts": [
17
- "source=${localWorkspaceFolderBasename}-cargo-registry,target=/usr/local/cargo/registry,type=volume",
18
- "source=${localWorkspaceFolderBasename}-cargo-git,target=/usr/local/cargo/git,type=volume",
19
- "source=${localWorkspaceFolderBasename}-target,target=${containerWorkspaceFolder}/target,type=volume"
20
- ]
21
- }
@@ -1,83 +0,0 @@
1
- import type { TemplateSourceHandle, WriteTextFromFragmentsOperation } from "./renderer.ts";
2
- export type DevelopmentContainerNodePnpmLayer = {
3
- readonly kind: "node-pnpm";
4
- readonly nodeVersion: string;
5
- readonly packageManagerPin: string;
6
- };
7
- export type DevelopmentContainerBrowserTestLayer = {
8
- readonly kind: "browser-test";
9
- readonly playwrightCliPackage: string;
10
- };
11
- export type DevelopmentContainerRustLayer = {
12
- readonly kind: "rust";
13
- readonly toolchain: string;
14
- };
15
- export type DevelopmentContainerShellCheckLayer = {
16
- readonly kind: "shellcheck";
17
- };
18
- type DevelopmentContainerCapabilityLayer = DevelopmentContainerBrowserTestLayer | DevelopmentContainerShellCheckLayer | DevelopmentContainerRustLayer;
19
- export type DevelopmentContainerDockerfileFragments = {
20
- readonly source: TemplateSourceHandle;
21
- readonly nodePnpm: {
22
- readonly from: string;
23
- readonly text: string;
24
- };
25
- readonly browserTest?: {
26
- readonly from: string;
27
- readonly text: string;
28
- } | undefined;
29
- readonly shellCheck?: {
30
- readonly from: string;
31
- readonly text: string;
32
- } | undefined;
33
- readonly rust?: {
34
- readonly from: string;
35
- readonly text: string;
36
- } | undefined;
37
- };
38
- export type DevelopmentContainerPlan = {
39
- readonly devcontainer: Record<string, unknown>;
40
- readonly dockerfile: string;
41
- readonly dockerfileOperation?: WriteTextFromFragmentsOperation;
42
- };
43
- export type DevelopmentContainerDockerfileLayer = {
44
- readonly kind: "base" | "capability";
45
- readonly name: string;
46
- readonly text: string;
47
- };
48
- export declare function composeDevelopmentContainerDockerfile(options: {
49
- readonly layers: readonly DevelopmentContainerDockerfileLayer[];
50
- }): string;
51
- export declare function nodePnpmToolLayer(options: {
52
- readonly nodeVersion: string;
53
- readonly packageManagerPin: string;
54
- }): DevelopmentContainerNodePnpmLayer;
55
- export declare function browserTestToolLayer(): DevelopmentContainerBrowserTestLayer;
56
- export declare function shellCheckToolLayer(): DevelopmentContainerShellCheckLayer;
57
- export declare function rustToolLayer(options?: {
58
- readonly toolchain?: string;
59
- }): DevelopmentContainerRustLayer;
60
- export declare function dockerfileFirstNodePnpmDevcontainer(options: {
61
- readonly name: string;
62
- readonly layer: DevelopmentContainerNodePnpmLayer;
63
- readonly extensions: readonly string[];
64
- readonly settings?: Record<string, unknown> | undefined;
65
- }): DevelopmentContainerPlan;
66
- export declare function checkedDockerfileFirstNodePnpmDevcontainer(options: {
67
- readonly name: string;
68
- readonly layer: DevelopmentContainerNodePnpmLayer;
69
- readonly dockerfileFragments: DevelopmentContainerDockerfileFragments;
70
- readonly additionalLayers?: readonly DevelopmentContainerCapabilityLayer[] | undefined;
71
- readonly extensions: readonly string[];
72
- readonly settings?: Record<string, unknown> | undefined;
73
- readonly mounts?: readonly string[] | undefined;
74
- }): DevelopmentContainerPlan;
75
- export declare function dockerfileFirstRustPnpmDevcontainer(options: {
76
- readonly name: string;
77
- readonly nodeLayer: DevelopmentContainerNodePnpmLayer;
78
- readonly rustLayer: DevelopmentContainerRustLayer;
79
- readonly dockerfileFragments: DevelopmentContainerDockerfileFragments;
80
- readonly extensions: readonly string[];
81
- readonly settings?: Record<string, unknown> | undefined;
82
- }): DevelopmentContainerPlan;
83
- export {};
@@ -1,217 +0,0 @@
1
- import { loadTemplateDependencyCatalog } from "./dependency-catalog.js";
2
- function requireDockerfileFragment(fragments, name) {
3
- const fragment = fragments[name];
4
- if (fragment === undefined) {
5
- throw new Error(`Development Container Dockerfile fragment is missing: ${name}`);
6
- }
7
- return fragment;
8
- }
9
- const runtimeDockerfileInstructions = new Set([
10
- "CMD",
11
- "ENTRYPOINT",
12
- "EXPOSE",
13
- "HEALTHCHECK",
14
- "STOPSIGNAL",
15
- "VOLUME",
16
- ]);
17
- function dockerfileInstructions(text) {
18
- return text
19
- .split(/\r?\n/)
20
- .map((line) => line.trim())
21
- .filter((line) => line.length > 0 && !line.startsWith("#"))
22
- .map((line) => line.match(/^([A-Za-z]+)/)?.[1]?.toUpperCase())
23
- .filter((instruction) => instruction !== undefined);
24
- }
25
- export function composeDevelopmentContainerDockerfile(options) {
26
- const baseImageInstructionCount = options.layers.reduce((count, layer) => count +
27
- dockerfileInstructions(layer.text).filter((instruction) => instruction === "FROM").length, 0);
28
- if (baseImageInstructionCount !== 1) {
29
- throw new Error(`Development Container Dockerfile must have exactly one base layer with a FROM instruction; found ${baseImageInstructionCount}.`);
30
- }
31
- for (const layer of options.layers) {
32
- const instructions = dockerfileInstructions(layer.text);
33
- if (layer.kind === "capability" && instructions.includes("FROM")) {
34
- throw new Error(`${layer.name} capability layer must not supply a Dockerfile FROM instruction.`);
35
- }
36
- if (layer.kind !== "capability") {
37
- continue;
38
- }
39
- for (const instruction of instructions) {
40
- if (runtimeDockerfileInstructions.has(instruction)) {
41
- throw new Error(`${layer.name} must not use runtime Dockerfile instruction ${instruction}.`);
42
- }
43
- }
44
- }
45
- return options.layers.map((layer) => layer.text.trimEnd()).join("\n\n");
46
- }
47
- export function nodePnpmToolLayer(options) {
48
- return {
49
- kind: "node-pnpm",
50
- nodeVersion: options.nodeVersion,
51
- packageManagerPin: options.packageManagerPin,
52
- };
53
- }
54
- export function browserTestToolLayer() {
55
- const playwrightTestVersion = loadTemplateDependencyCatalog()["@playwright/test"];
56
- if (playwrightTestVersion === undefined) {
57
- throw new Error("Template Dependency Catalog is missing dependency: @playwright/test");
58
- }
59
- return {
60
- kind: "browser-test",
61
- playwrightCliPackage: `@playwright/test@${playwrightTestVersion}`,
62
- };
63
- }
64
- export function shellCheckToolLayer() {
65
- return { kind: "shellcheck" };
66
- }
67
- export function rustToolLayer(options = {}) {
68
- return { kind: "rust", toolchain: options.toolchain ?? "stable" };
69
- }
70
- function checkedNodePnpmDockerfile(options) {
71
- return composeDevelopmentContainerDockerfile({
72
- layers: [
73
- {
74
- kind: "base",
75
- name: "node-pnpm",
76
- text: options.dockerfileFragments.nodePnpm.text,
77
- },
78
- ...(options.additionalLayers ?? []).map((layer) => {
79
- switch (layer.kind) {
80
- case "browser-test":
81
- return {
82
- kind: "capability",
83
- name: "browser-test",
84
- text: requireDockerfileFragment(options.dockerfileFragments, "browserTest").text,
85
- };
86
- case "shellcheck":
87
- return {
88
- kind: "capability",
89
- name: "shellcheck",
90
- text: requireDockerfileFragment(options.dockerfileFragments, "shellCheck").text,
91
- };
92
- case "rust":
93
- return {
94
- kind: "capability",
95
- name: "rust",
96
- text: requireDockerfileFragment(options.dockerfileFragments, "rust").text,
97
- };
98
- }
99
- }),
100
- ],
101
- });
102
- }
103
- function checkedNodePnpmDockerfileFragments(options) {
104
- return [
105
- {
106
- source: options.dockerfileFragments.source,
107
- from: options.dockerfileFragments.nodePnpm.from,
108
- },
109
- ...(options.additionalLayers ?? []).map((layer) => {
110
- switch (layer.kind) {
111
- case "browser-test":
112
- return {
113
- source: options.dockerfileFragments.source,
114
- from: requireDockerfileFragment(options.dockerfileFragments, "browserTest").from,
115
- };
116
- case "shellcheck":
117
- return {
118
- source: options.dockerfileFragments.source,
119
- from: requireDockerfileFragment(options.dockerfileFragments, "shellCheck").from,
120
- };
121
- case "rust":
122
- return {
123
- source: options.dockerfileFragments.source,
124
- from: requireDockerfileFragment(options.dockerfileFragments, "rust")
125
- .from,
126
- };
127
- }
128
- }),
129
- ];
130
- }
131
- function browserTestBuildArgs(layers = []) {
132
- const browserTestLayer = layers.find((layer) => layer.kind === "browser-test");
133
- return browserTestLayer === undefined
134
- ? {}
135
- : { PLAYWRIGHT_CLI_PACKAGE: browserTestLayer.playwrightCliPackage };
136
- }
137
- function rustBuildArgs(layers = []) {
138
- const rustLayer = layers.find((layer) => layer.kind === "rust");
139
- return rustLayer === undefined ? {} : { RUST_TOOLCHAIN: rustLayer.toolchain };
140
- }
141
- export function dockerfileFirstNodePnpmDevcontainer(options) {
142
- return {
143
- devcontainer: {
144
- name: options.name,
145
- build: {
146
- dockerfile: "Dockerfile",
147
- args: {
148
- NODE_VERSION: options.layer.nodeVersion,
149
- PACKAGE_MANAGER_PIN: options.layer.packageManagerPin,
150
- },
151
- },
152
- customizations: {
153
- vscode: {
154
- extensions: options.extensions,
155
- ...(options.settings ? { settings: options.settings } : {}),
156
- },
157
- },
158
- },
159
- dockerfile: [
160
- `FROM mcr.microsoft.com/devcontainers/typescript-node:${options.layer.nodeVersion}`,
161
- "",
162
- 'SHELL ["/bin/bash", "-o", "pipefail", "-c"]',
163
- `RUN corepack enable && corepack prepare ${options.layer.packageManagerPin} --activate`,
164
- "",
165
- ].join("\n"),
166
- };
167
- }
168
- export function checkedDockerfileFirstNodePnpmDevcontainer(options) {
169
- return {
170
- devcontainer: {
171
- name: options.name,
172
- build: {
173
- dockerfile: "Dockerfile",
174
- args: {
175
- NODE_VERSION: options.layer.nodeVersion,
176
- PACKAGE_MANAGER_PIN: options.layer.packageManagerPin,
177
- ...browserTestBuildArgs(options.additionalLayers),
178
- ...rustBuildArgs(options.additionalLayers),
179
- },
180
- },
181
- customizations: {
182
- vscode: {
183
- extensions: options.extensions,
184
- ...(options.settings ? { settings: options.settings } : {}),
185
- },
186
- },
187
- ...(options.mounts ? { mounts: options.mounts } : {}),
188
- },
189
- dockerfile: checkedNodePnpmDockerfile({
190
- dockerfileFragments: options.dockerfileFragments,
191
- additionalLayers: options.additionalLayers,
192
- }),
193
- dockerfileOperation: {
194
- kind: "writeTextFromFragments",
195
- to: ".devcontainer/Dockerfile",
196
- fragments: checkedNodePnpmDockerfileFragments({
197
- dockerfileFragments: options.dockerfileFragments,
198
- additionalLayers: options.additionalLayers,
199
- }),
200
- },
201
- };
202
- }
203
- export function dockerfileFirstRustPnpmDevcontainer(options) {
204
- return checkedDockerfileFirstNodePnpmDevcontainer({
205
- name: options.name,
206
- layer: options.nodeLayer,
207
- dockerfileFragments: options.dockerfileFragments,
208
- additionalLayers: [options.rustLayer],
209
- extensions: options.extensions,
210
- settings: options.settings,
211
- mounts: [
212
- "source=${localWorkspaceFolderBasename}-cargo-registry,target=/usr/local/cargo/registry,type=volume",
213
- "source=${localWorkspaceFolderBasename}-cargo-git,target=/usr/local/cargo/git,type=volume",
214
- "source=${localWorkspaceFolderBasename}-target,target=${containerWorkspaceFolder}/target,type=volume",
215
- ],
216
- });
217
- }