@ykdz/template 0.0.31 → 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 +22 -7
  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 +1 -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 +1 -0
  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 +3 -3
  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
@@ -0,0 +1,361 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { resolveTemplateSource, } from "./renderer.js";
5
+ function hasContinuation(line, escapeCharacter) {
6
+ const trimmed = line.trimEnd();
7
+ let escapeCount = 0;
8
+ for (let index = trimmed.length - 1; index >= 0 && trimmed[index] === escapeCharacter; index -= 1) {
9
+ escapeCount += 1;
10
+ }
11
+ return escapeCount % 2 === 1;
12
+ }
13
+ function parseLogicalDockerfileInstructions(text, sourceFile) {
14
+ const instructions = [];
15
+ let escapeCharacter = "\\";
16
+ let logicalLine = "";
17
+ for (const physicalLine of text.split(/\r?\n/u)) {
18
+ const trimmed = physicalLine.trim();
19
+ if (logicalLine.length === 0 && trimmed.startsWith("#")) {
20
+ const escapeDirective = trimmed.match(/^#\s*escape\s*=\s*([\\`])\s*$/iu);
21
+ if (escapeDirective?.[1] !== undefined) {
22
+ escapeCharacter = escapeDirective[1];
23
+ }
24
+ continue;
25
+ }
26
+ if (trimmed.length === 0 || trimmed.startsWith("#")) {
27
+ continue;
28
+ }
29
+ const continuation = hasContinuation(physicalLine, escapeCharacter);
30
+ const physicalContent = continuation
31
+ ? physicalLine.trimEnd().slice(0, -1)
32
+ : physicalLine;
33
+ logicalLine += physicalContent.trimStart();
34
+ if (continuation) {
35
+ continue;
36
+ }
37
+ const match = logicalLine.match(/^([A-Za-z]+)(?:[ \t]+(.*))?$/u);
38
+ if (match?.[1] === undefined) {
39
+ throw new Error(`Development Container Dockerfile fragment ${sourceFile} contains an invalid logical instruction: ${logicalLine}`);
40
+ }
41
+ instructions.push({
42
+ name: match[1].toUpperCase(),
43
+ arguments: match[2] ?? "",
44
+ });
45
+ logicalLine = "";
46
+ }
47
+ if (logicalLine.length > 0) {
48
+ throw new Error(`Development Container Dockerfile fragment ${sourceFile} ends with an unterminated line continuation.`);
49
+ }
50
+ return instructions;
51
+ }
52
+ function instructionNames(instructions, instruction) {
53
+ return new Set(instructions
54
+ .filter(({ name }) => name === instruction)
55
+ .map(({ arguments: value }) => value.trim().split(/[\s=]/u)[0])
56
+ .filter((name) => name.length > 0));
57
+ }
58
+ function dockerfileWords(value) {
59
+ const words = [];
60
+ let word = "";
61
+ let quote;
62
+ let escaped = false;
63
+ for (const character of value) {
64
+ if (escaped) {
65
+ word += character;
66
+ escaped = false;
67
+ continue;
68
+ }
69
+ if (character === "\\" && quote !== "'") {
70
+ escaped = true;
71
+ continue;
72
+ }
73
+ if (quote !== undefined) {
74
+ if (character === quote) {
75
+ quote = undefined;
76
+ }
77
+ else {
78
+ word += character;
79
+ }
80
+ continue;
81
+ }
82
+ if (character === '"' || character === "'") {
83
+ quote = character;
84
+ continue;
85
+ }
86
+ if (/\s/u.test(character)) {
87
+ if (word.length > 0) {
88
+ words.push(word);
89
+ word = "";
90
+ }
91
+ continue;
92
+ }
93
+ word += character;
94
+ }
95
+ if (escaped) {
96
+ word += "\\";
97
+ }
98
+ if (word.length > 0) {
99
+ words.push(word);
100
+ }
101
+ return words;
102
+ }
103
+ function environmentNames(instructions) {
104
+ const names = new Set();
105
+ for (const instruction of instructions) {
106
+ if (instruction.name !== "ENV") {
107
+ continue;
108
+ }
109
+ const words = dockerfileWords(instruction.arguments);
110
+ const first = words[0];
111
+ if (first === undefined) {
112
+ continue;
113
+ }
114
+ if (!first.includes("=")) {
115
+ names.add(first);
116
+ continue;
117
+ }
118
+ for (const assignment of words) {
119
+ const separator = assignment.indexOf("=");
120
+ if (separator > 0) {
121
+ names.add(assignment.slice(0, separator));
122
+ }
123
+ }
124
+ }
125
+ return names;
126
+ }
127
+ function validateDockerfileFragment(fragment) {
128
+ if (!fragment.from.endsWith(".Dockerfile")) {
129
+ throw new Error(`Development Container Tool Layer ${fragment.identity} must reference checked .Dockerfile template source; received ${fragment.from}.`);
130
+ }
131
+ const instructions = parseLogicalDockerfileInstructions(fragment.text, fragment.from);
132
+ const allowedInstructions = fragment.kind === "base"
133
+ ? new Set(["ARG", "ENV", "FROM", "RUN", "SHELL"])
134
+ : new Set(["ARG", "ENV", "RUN"]);
135
+ for (const instruction of instructions) {
136
+ if (!allowedInstructions.has(instruction.name)) {
137
+ const role = fragment.kind === "base" ? "base" : "optional";
138
+ const allowed = fragment.kind === "base"
139
+ ? "ARG, ENV, FROM, RUN, and SHELL"
140
+ : "ARG, ENV, and RUN";
141
+ throw new Error(`Development Container Tool Layer ${fragment.identity} ${role} fragment ${fragment.from} must not use Dockerfile instruction ${instruction.name}; allowed instructions are ${allowed}.`);
142
+ }
143
+ }
144
+ if (fragment.kind === "base") {
145
+ const fromCount = instructions.filter((instruction) => instruction.name === "FROM").length;
146
+ if (fromCount !== 1) {
147
+ throw new Error(`Development Container Tool Layer ${fragment.identity} base fragment ${fragment.from} must contain exactly one FROM instruction; found ${fromCount}.`);
148
+ }
149
+ }
150
+ return instructions;
151
+ }
152
+ export function validateDevelopmentContainerDockerfileFragments(options) {
153
+ validateDockerfileFragment({ ...options.baseFragment, kind: "base" });
154
+ for (const fragment of options.fragments ?? []) {
155
+ validateDockerfileFragment({ ...fragment, kind: "optional" });
156
+ }
157
+ }
158
+ function compareText(left, right) {
159
+ return left < right ? -1 : left > right ? 1 : 0;
160
+ }
161
+ function assertDockerfileTemplateSource(layer) {
162
+ if (!layer.dockerfile.from.endsWith(".Dockerfile")) {
163
+ throw new Error(`Development Container Tool Layer ${layer.identity} must reference checked .Dockerfile template source; received ${layer.dockerfile.from}.`);
164
+ }
165
+ }
166
+ function normalizeLayer(layer) {
167
+ assertDockerfileTemplateSource(layer);
168
+ const buildArgumentNames = new Set();
169
+ for (const argument of layer.buildArguments ?? []) {
170
+ if (buildArgumentNames.has(argument.name)) {
171
+ throw new Error(`Development Container Tool Layer ${layer.identity} declares build argument ${argument.name} more than once.`);
172
+ }
173
+ buildArgumentNames.add(argument.name);
174
+ }
175
+ return {
176
+ ...layer,
177
+ requires: [...new Set(layer.requires ?? [])].toSorted(compareText),
178
+ buildArguments: [...(layer.buildArguments ?? [])].toSorted((left, right) => compareText(left.name, right.name) ||
179
+ compareText(left.value, right.value)),
180
+ mounts: [...(layer.mounts ?? [])]
181
+ .map((mount) => ({
182
+ ...mount,
183
+ target: path.posix.normalize(mount.target),
184
+ }))
185
+ .toSorted((left, right) => compareText(left.identity, right.identity) ||
186
+ compareText(left.target, right.target)),
187
+ probes: [...(layer.probes ?? [])]
188
+ .map((probe) => ({
189
+ ...probe,
190
+ args: [...(probe.args ?? [])],
191
+ }))
192
+ .toSorted((left, right) => compareText(left.identity, right.identity) ||
193
+ compareText(left.command, right.command) ||
194
+ compareText(JSON.stringify(left.args), JSON.stringify(right.args))),
195
+ };
196
+ }
197
+ function layerFingerprint(layer, kind) {
198
+ return JSON.stringify({
199
+ kind,
200
+ identity: layer.identity,
201
+ dockerfile: resolveTemplateSource(layer.dockerfile.source, layer.dockerfile.from),
202
+ requires: layer.requires,
203
+ buildArguments: layer.buildArguments,
204
+ mounts: layer.mounts,
205
+ probes: layer.probes,
206
+ });
207
+ }
208
+ function findDependencyCycle(layers) {
209
+ const byIdentity = new Map(layers.map((layer) => [layer.identity, layer]));
210
+ const complete = new Set();
211
+ const active = [];
212
+ const activeSet = new Set();
213
+ const visit = (identity) => {
214
+ if (activeSet.has(identity)) {
215
+ return active.slice(active.indexOf(identity)).toSorted(compareText);
216
+ }
217
+ if (complete.has(identity)) {
218
+ return undefined;
219
+ }
220
+ active.push(identity);
221
+ activeSet.add(identity);
222
+ const layer = byIdentity.get(identity);
223
+ for (const dependency of layer?.requires ?? []) {
224
+ if (!byIdentity.has(dependency)) {
225
+ continue;
226
+ }
227
+ const cycle = visit(dependency);
228
+ if (cycle !== undefined) {
229
+ return cycle;
230
+ }
231
+ }
232
+ active.pop();
233
+ activeSet.delete(identity);
234
+ complete.add(identity);
235
+ return undefined;
236
+ };
237
+ for (const identity of [...byIdentity.keys()].toSorted(compareText)) {
238
+ const cycle = visit(identity);
239
+ if (cycle !== undefined) {
240
+ return cycle;
241
+ }
242
+ }
243
+ return undefined;
244
+ }
245
+ function orderLayers(baseLayer, optionalLayers) {
246
+ const normalizedBase = normalizeLayer(baseLayer);
247
+ if ((normalizedBase.requires?.length ?? 0) > 0) {
248
+ throw new Error(`Development Container Tool Layer ${normalizedBase.identity} is the base foundation and must not require other layers.`);
249
+ }
250
+ const base = {
251
+ ...normalizedBase,
252
+ kind: "base",
253
+ };
254
+ const identities = new Map([[base.identity, layerFingerprint(base, "base")]]);
255
+ const uniqueOptionalLayers = [];
256
+ for (const layer of optionalLayers) {
257
+ const normalized = normalizeLayer(layer);
258
+ const fingerprint = layerFingerprint(normalized, "optional");
259
+ const previous = identities.get(normalized.identity);
260
+ if (previous === fingerprint) {
261
+ continue;
262
+ }
263
+ if (previous !== undefined) {
264
+ throw new Error(`Development Container Tool Layer identity ${normalized.identity} has conflicting descriptors.`);
265
+ }
266
+ identities.set(normalized.identity, fingerprint);
267
+ uniqueOptionalLayers.push(normalized);
268
+ }
269
+ for (const layer of uniqueOptionalLayers) {
270
+ for (const requiredIdentity of layer.requires ?? []) {
271
+ if (!identities.has(requiredIdentity)) {
272
+ throw new Error(`Development Container Tool Layer ${layer.identity} requires missing layer ${requiredIdentity}.`);
273
+ }
274
+ }
275
+ }
276
+ const cycle = findDependencyCycle(uniqueOptionalLayers);
277
+ if (cycle !== undefined) {
278
+ throw new Error(`Development Container Tool Layer dependency cycle involves: ${cycle.join(", ")}.`);
279
+ }
280
+ const remaining = uniqueOptionalLayers
281
+ .map((layer) => ({
282
+ ...layer,
283
+ kind: "optional",
284
+ }))
285
+ .toSorted((left, right) => compareText(left.identity, right.identity));
286
+ const ordered = [base];
287
+ const resolved = new Set([base.identity]);
288
+ while (remaining.length > 0) {
289
+ const readyIndex = remaining.findIndex((layer) => (layer.requires ?? []).every((identity) => resolved.has(identity)));
290
+ if (readyIndex === -1) {
291
+ throw new Error("Development Container Tool Layer dependencies cannot be resolved.");
292
+ }
293
+ const [next] = remaining.splice(readyIndex, 1);
294
+ if (next === undefined) {
295
+ throw new Error("Development Container Tool Layer dependency resolution failed.");
296
+ }
297
+ ordered.push(next);
298
+ resolved.add(next.identity);
299
+ }
300
+ return ordered;
301
+ }
302
+ function completeDevelopmentContainerToolLayerPlan(layers, fragmentTexts) {
303
+ const buildArgumentOwners = new Map();
304
+ const environmentOwners = new Map();
305
+ const mountIdentityOwners = new Map();
306
+ const mountTargetOwners = new Map();
307
+ for (const [index, layer] of layers.entries()) {
308
+ for (const mount of layer.mounts ?? []) {
309
+ const previousIdentityOwner = mountIdentityOwners.get(mount.identity);
310
+ if (previousIdentityOwner !== undefined) {
311
+ throw new Error(`Development Container Tool Layer mount identity ${mount.identity} is declared by both ${previousIdentityOwner} and ${layer.identity}.`);
312
+ }
313
+ const previousTargetOwner = mountTargetOwners.get(mount.target);
314
+ if (previousTargetOwner !== undefined) {
315
+ throw new Error(`Development Container Tool Layer mount target ${mount.target} is declared by both ${previousTargetOwner} and ${layer.identity}.`);
316
+ }
317
+ mountIdentityOwners.set(mount.identity, layer.identity);
318
+ mountTargetOwners.set(mount.target, layer.identity);
319
+ }
320
+ const instructions = validateDockerfileFragment({
321
+ identity: layer.identity,
322
+ kind: layer.kind,
323
+ from: layer.dockerfile.from,
324
+ text: fragmentTexts[index],
325
+ });
326
+ const declaredArguments = instructionNames(instructions, "ARG");
327
+ for (const name of declaredArguments) {
328
+ const previousOwner = buildArgumentOwners.get(name);
329
+ if (previousOwner !== undefined) {
330
+ throw new Error(`Development Container Tool Layer build argument ${name} is declared by both ${previousOwner} and ${layer.identity}.`);
331
+ }
332
+ buildArgumentOwners.set(name, layer.identity);
333
+ }
334
+ for (const name of environmentNames(instructions)) {
335
+ const previousOwner = environmentOwners.get(name);
336
+ if (previousOwner !== undefined) {
337
+ throw new Error(`Development Container Tool Layer environment ${name} is declared by both ${previousOwner} and ${layer.identity}.`);
338
+ }
339
+ environmentOwners.set(name, layer.identity);
340
+ }
341
+ for (const argument of layer.buildArguments ?? []) {
342
+ if (!declaredArguments.has(argument.name)) {
343
+ throw new Error(`Development Container Tool Layer ${layer.identity} supplies build argument ${argument.name}, but ${layer.dockerfile.from} does not declare it with ARG.`);
344
+ }
345
+ }
346
+ }
347
+ return {
348
+ layers,
349
+ buildArguments: layers.flatMap((layer) => layer.buildArguments ?? []),
350
+ mounts: layers.flatMap((layer) => layer.mounts ?? []),
351
+ probes: layers.flatMap((layer) => layer.probes ?? []),
352
+ };
353
+ }
354
+ export function planDevelopmentContainerToolLayersSync(options) {
355
+ const layers = orderLayers(options.baseLayer, options.layers ?? []);
356
+ return completeDevelopmentContainerToolLayerPlan(layers, layers.map((layer) => readFileSync(resolveTemplateSource(layer.dockerfile.source, layer.dockerfile.from), "utf8")));
357
+ }
358
+ export async function planDevelopmentContainerToolLayers(options) {
359
+ const layers = orderLayers(options.baseLayer, options.layers ?? []);
360
+ return completeDevelopmentContainerToolLayerPlan(layers, await Promise.all(layers.map((layer) => readFile(resolveTemplateSource(layer.dockerfile.source, layer.dockerfile.from), "utf8"))));
361
+ }
@@ -1,8 +1,9 @@
1
+ import type { DevelopmentContainerToolLayer } from "./development-container-tool-layer.ts";
1
2
  import type { EditorCustomizationCapability } from "./editor-customization.ts";
2
3
  import type { CheckEnvironmentNeed, DeploymentEnvironmentNeed } from "./module-graph.ts";
3
4
  import type { PackageDefinition } from "./project-blueprint-v2.ts";
4
5
  import type { DependencyMaintenancePolicy } from "./project-github.ts";
5
- import type { RenderOperation } from "./renderer.ts";
6
+ import type { RenderOperation, TemplateSourceHandle } from "./renderer.ts";
6
7
  export type FoundationContribution = {
7
8
  /** Toolchains the Foundation must install and project into coordinated root files. */
8
9
  readonly toolchains: {
@@ -19,6 +20,16 @@ export type FoundationContribution = {
19
20
  readonly workspacePackageGlobs?: readonly string[];
20
21
  /** Dependency Catalog entries required by package-owned manifests. */
21
22
  readonly dependencyCatalog?: Readonly<Record<string, string>>;
23
+ /** Source-backed Development Container capabilities coordinated by Foundation. */
24
+ readonly developmentContainerToolLayers?: readonly DevelopmentContainerToolLayer[] | undefined;
25
+ /** Source-backed coordinated files projected by Foundation. */
26
+ readonly templateFiles?: readonly {
27
+ readonly identity: string;
28
+ readonly source: TemplateSourceHandle;
29
+ readonly from: string;
30
+ readonly to: string;
31
+ readonly replacements?: Readonly<Record<string, string>> | undefined;
32
+ }[] | undefined;
22
33
  };
23
34
  /** A preset-agnostic package-sized part of a Generated Repository Plan. */
24
35
  export type PackageContribution = {
@@ -21,6 +21,7 @@ overrides:
21
21
  "vue>typescript": "-"
22
22
 
23
23
  catalog:
24
+ "@devcontainers/cli": 0.88.0
24
25
  "@hono/node-server": ^2.0.12
25
26
  "@playwright/test": ^1.61.1
26
27
  "@tailwindcss/vite": ^4.3.3
@@ -1,4 +1,4 @@
1
- import { type CheckEnvironmentNeed, type DeploymentEnvironmentNeed } from "./module-graph.ts";
1
+ import type { DeploymentEnvironmentNeed } from "./module-graph.ts";
2
2
  export type CiCapability = {
3
3
  readonly workflowName: "Check";
4
4
  readonly jobName: "check";
@@ -6,7 +6,6 @@ export type CiCapability = {
6
6
  };
7
7
  export type CiEnvironmentPreparation = {
8
8
  readonly nodeFromPackageMetadata: boolean;
9
- readonly rustToolchain: boolean;
10
9
  };
11
10
  export type PnpmTaskLayer = {
12
11
  readonly installCommand: "pnpm install";
@@ -21,9 +20,6 @@ export type DependencyMaintenancePolicy = {
21
20
  readonly interval: "weekly";
22
21
  };
23
22
  type ProjectCheckWorkflowOptions = {
24
- readonly environment: {
25
- readonly needs: readonly CheckEnvironmentNeed[];
26
- };
27
23
  readonly deploymentEnvironmentNeeds?: readonly DeploymentEnvironmentNeed[];
28
24
  readonly hasDeploymentTask?: boolean | undefined;
29
25
  readonly capability?: CiCapability | undefined;
@@ -33,12 +29,8 @@ type ProjectCheckWorkflowOptions = {
33
29
  export declare function projectCheckWorkflow(options: ProjectCheckWorkflowOptions): string;
34
30
  /** Limited substitutions for the Foundation-owned workflow Template Source. */
35
31
  export declare function projectCheckWorkflowTemplateReplacements(options: {
36
- readonly environment: {
37
- readonly needs: readonly CheckEnvironmentNeed[];
38
- };
39
32
  readonly deploymentEnvironmentNeeds?: readonly DeploymentEnvironmentNeed[];
40
33
  readonly hasDeploymentTask?: boolean | undefined;
41
- readonly environmentPreparation?: Partial<CiEnvironmentPreparation>;
42
34
  }): Record<string, string>;
43
35
  export declare function projectDependabotConfig(policy: DependencyMaintenancePolicy): string;
44
36
  /** Limited substitution for the Foundation-owned Dependabot Template Source. */
@@ -1,4 +1,3 @@
1
- import { renderPlaywrightBrowserInstallCommand, } from "./module-graph.js";
2
1
  const defaultCiCapability = {
3
2
  workflowName: "Check",
4
3
  jobName: "check",
@@ -12,11 +11,8 @@ export function projectCheckWorkflow(options) {
12
11
  const capability = options.capability ?? defaultCiCapability;
13
12
  const environmentPreparation = {
14
13
  nodeFromPackageMetadata: true,
15
- rustToolchain: false,
16
14
  ...options.environmentPreparation,
17
15
  };
18
- const requiresRustToolchain = environmentPreparation.rustToolchain ||
19
- options.environment.needs.some((need) => need.kind === "rust-toolchain");
20
16
  const taskLayer = options.taskLayer ?? pnpmTaskLayer;
21
17
  const hasDeploymentTask = options.hasDeploymentTask === true;
22
18
  const needsDocker = (options.deploymentEnvironmentNeeds ?? []).some((need) => need.kind === "docker-engine");
@@ -44,20 +40,7 @@ export function projectCheckWorkflow(options) {
44
40
  if (hasDeploymentTask && needsDocker) {
45
41
  lines.push(" - uses: docker/setup-buildx-action@v3", " if: matrix.check == 'deployment'");
46
42
  }
47
- if (requiresRustToolchain)
48
- lines.push(...rustCiPreparationLines());
49
43
  lines.push(` - run: ${taskLayer.installCommand}`);
50
- const checkEnvironmentLines = [];
51
- for (const need of options.environment.needs) {
52
- if (need.kind === "rust-toolchain") {
53
- continue;
54
- }
55
- checkEnvironmentLines.push(` - run: ${renderCiEnvironmentNeedCommand(need)}`);
56
- if (hasDeploymentTask && need.kind === "shellcheck-command") {
57
- checkEnvironmentLines.push(" if: matrix.check == 'root'");
58
- }
59
- }
60
- lines.push(...checkEnvironmentLines);
61
44
  lines.push(` - run: ${taskLayer.checkCommand}`);
62
45
  if (hasDeploymentTask) {
63
46
  lines.push(" if: matrix.check == 'root'");
@@ -68,33 +51,11 @@ export function projectCheckWorkflow(options) {
68
51
  lines.push("");
69
52
  return lines.join("\n");
70
53
  }
71
- function rustCiPreparationLines() {
72
- return [
73
- " - uses: dtolnay/rust-toolchain@stable",
74
- " with:",
75
- " components: rustfmt, clippy",
76
- " - uses: Swatinem/rust-cache@v2",
77
- ];
78
- }
79
54
  /** Limited substitutions for the Foundation-owned workflow Template Source. */
80
55
  export function projectCheckWorkflowTemplateReplacements(options) {
81
- const requiresRustToolchain = options.environmentPreparation?.rustToolchain === true ||
82
- options.environment.needs.some((need) => need.kind === "rust-toolchain");
83
56
  const hasDeploymentTask = options.hasDeploymentTask === true;
84
57
  const needsDocker = (options.deploymentEnvironmentNeeds ?? []).some((need) => need.kind === "docker-engine");
85
- const environmentSteps = options.environment.needs
86
- .filter((need) => need.kind !== "rust-toolchain")
87
- .map((need) => [
88
- ` - run: ${renderCiEnvironmentNeedCommand(need)}`,
89
- ...(hasDeploymentTask && need.kind === "shellcheck-command"
90
- ? [" if: matrix.check == 'root'"]
91
- : []),
92
- ].join("\n"));
93
58
  return {
94
- RUST_CI_PREPARATION: requiresRustToolchain
95
- ? `\n${rustCiPreparationLines().join("\n")}`
96
- : "",
97
- CHECK_ENVIRONMENT_PREPARATION: environmentSteps.length === 0 ? "" : `\n${environmentSteps.join("\n")}`,
98
59
  DEPLOYMENT_MATRIX: hasDeploymentTask
99
60
  ? "\n strategy:\n matrix:\n check: [root, deployment]"
100
61
  : "",
@@ -152,13 +113,3 @@ function renderDependabotDirectory(ecosystem, directory) {
152
113
  function defaultDependabotDirectory(ecosystem) {
153
114
  return ecosystem === "docker" ? "/.devcontainer" : "/";
154
115
  }
155
- function renderCiEnvironmentNeedCommand(need) {
156
- switch (need.kind) {
157
- case "playwright-browser-assets":
158
- return renderPlaywrightBrowserInstallCommand(need, { withDeps: true });
159
- case "shellcheck-command":
160
- return need.nextStep.display;
161
- case "rust-toolchain":
162
- return need.nextStep.display;
163
- }
164
- }
@@ -17,6 +17,15 @@ export type StructuredIdentitySetPolicy = {
17
17
  } | {
18
18
  readonly kind: "fields";
19
19
  readonly fields: readonly string[];
20
+ } | {
21
+ readonly kind: "projection";
22
+ readonly members: readonly {
23
+ readonly identity: string;
24
+ readonly match: Readonly<Record<string, string | number | boolean | null>>;
25
+ }[];
26
+ readonly fallback: {
27
+ readonly fields: readonly string[];
28
+ };
20
29
  };
21
30
  };
22
31
  export type ProjectProjectionReconciliation = {
@@ -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
  };