@ykdz/template 0.0.22 → 0.0.23

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 (27) hide show
  1. package/node_modules/@ykdz/template-builtin-source/package.json +1 -1
  2. package/node_modules/@ykdz/template-builtin-source/templates/preset-source.json +7 -0
  3. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/.github/workflows/check.yml +2 -8
  4. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/Dockerfile +45 -9
  5. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/pages/index/+Page.telefunc.ts +1 -2
  6. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/playwright.config.ts +45 -9
  7. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/scripts/check-standalone-deployment.ts +449 -0
  8. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/scripts/container-entrypoint.sh +52 -0
  9. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/scripts/prepare-database.sh +6 -0
  10. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/scripts/run-playwright.ts +95 -25
  11. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/server/app.ts +1 -1
  12. package/node_modules/@ykdz/template-core/dist/dependency-catalog.js +13 -0
  13. package/node_modules/@ykdz/template-core/dist/generated-scenarios.d.ts +3 -2
  14. package/node_modules/@ykdz/template-core/dist/generated-scenarios.js +99 -13
  15. package/node_modules/@ykdz/template-core/dist/module-graph.d.ts +12 -0
  16. package/node_modules/@ykdz/template-core/dist/module-graph.js +18 -0
  17. package/node_modules/@ykdz/template-core/dist/pnpm-workspace.yaml +7 -4
  18. package/node_modules/@ykdz/template-core/dist/preset-projection.d.ts +1 -2
  19. package/node_modules/@ykdz/template-core/dist/project-github.d.ts +0 -8
  20. package/node_modules/@ykdz/template-core/dist/project-github.js +11 -4
  21. package/node_modules/@ykdz/template-core/dist/projection-capabilities.js +15 -13
  22. package/node_modules/@ykdz/template-core/dist/renderer.js +1 -1
  23. package/node_modules/@ykdz/template-core/dist/template-boundary-check.js +1 -1
  24. package/node_modules/@ykdz/template-core/package.json +2 -1
  25. package/node_modules/@ykdz/template-shared/dist/index.d.ts +6 -6
  26. package/node_modules/@ykdz/template-shared/package.json +1 -0
  27. package/package.json +5 -4
@@ -0,0 +1,52 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ database_file=${DATABASE_FILE:-/data/app.sqlite}
5
+ container_capability=${CONTAINER_CAPABILITY:-prepare-and-start}
6
+ prepare_database_command=${PREPARE_DATABASE_COMMAND:-/usr/local/bin/prepare-database}
7
+ case "$database_file" in
8
+ /*) ;;
9
+ *)
10
+ echo "DATABASE_FILE must be an absolute path: $database_file" >&2
11
+ exit 64
12
+ ;;
13
+ esac
14
+
15
+ prepare_database() {
16
+ "$prepare_database_command" "$database_file"
17
+ }
18
+
19
+ if [ "$container_capability" = "start-only" ]; then
20
+ case "${1:-}" in
21
+ start-only)
22
+ exec node /app/dist/server/index.mjs
23
+ ;;
24
+ prepare-only | prepare-and-start)
25
+ echo "Container capability 'start-only' does not support '${1}'." >&2
26
+ exit 64
27
+ ;;
28
+ *)
29
+ echo "Unsupported container command '${1:-}'. Expected start-only." >&2
30
+ exit 64
31
+ ;;
32
+ esac
33
+ fi
34
+
35
+ if [ "$container_capability" != "prepare-and-start" ]; then
36
+ echo "Unsupported CONTAINER_CAPABILITY '$container_capability'." >&2
37
+ exit 64
38
+ fi
39
+
40
+ case "${1:-}" in
41
+ prepare-only)
42
+ prepare_database
43
+ ;;
44
+ prepare-and-start)
45
+ prepare_database
46
+ exec node /app/dist/server/index.mjs
47
+ ;;
48
+ *)
49
+ echo "Unsupported container command '${1:-}'. Expected prepare-only or prepare-and-start." >&2
50
+ exit 64
51
+ ;;
52
+ esac
@@ -0,0 +1,6 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ database_file=$1
5
+ mkdir -p "$(dirname "$database_file")"
6
+ DATABASE_FILE="$database_file" pnpm --dir /repo/packages/db run db:prepare:deploy
@@ -1,11 +1,38 @@
1
1
  import { spawn } from "node:child_process";
2
- import { rm } from "node:fs/promises";
2
+ import { mkdir, rm } from "node:fs/promises";
3
3
  import { createServer } from "node:net";
4
+ import path from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
 
6
7
  const databaseFile = fileURLToPath(
7
8
  new URL("../node_modules/.tmp/e2e.sqlite", import.meta.url),
8
9
  );
10
+ const externalBaseUrlName = "PLAYWRIGHT_EXTERNAL_BASE_URL";
11
+ const externalReadinessTimeoutMs = 30_000;
12
+
13
+ function externalBaseUrl(): string | undefined {
14
+ if (!Object.hasOwn(process.env, externalBaseUrlName)) {
15
+ return undefined;
16
+ }
17
+
18
+ const value = process.env[externalBaseUrlName]?.trim();
19
+ if (!value) {
20
+ throw new Error(`${externalBaseUrlName} must be a non-empty HTTP(S) URL`);
21
+ }
22
+
23
+ let url: URL;
24
+ try {
25
+ url = new URL(value);
26
+ } catch {
27
+ throw new Error(`${externalBaseUrlName} must be a valid HTTP(S) URL`);
28
+ }
29
+
30
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
31
+ throw new Error(`${externalBaseUrlName} must use HTTP or HTTPS`);
32
+ }
33
+
34
+ return url.toString();
35
+ }
9
36
 
10
37
  async function availablePort(): Promise<string> {
11
38
  return await new Promise((resolve, reject) => {
@@ -35,7 +62,30 @@ async function availablePort(): Promise<string> {
35
62
  });
36
63
  }
37
64
 
38
- async function playwrightEnv(): Promise<NodeJS.ProcessEnv> {
65
+ async function awaitExternalService(baseUrl: string): Promise<void> {
66
+ const deadline = Date.now() + externalReadinessTimeoutMs;
67
+
68
+ while (Date.now() < deadline) {
69
+ try {
70
+ const response = await fetch(baseUrl, {
71
+ signal: AbortSignal.timeout(2_000),
72
+ });
73
+ if (response.ok) {
74
+ return;
75
+ }
76
+ } catch {
77
+ // The externally managed service may still be starting.
78
+ }
79
+
80
+ await new Promise((resolve) => setTimeout(resolve, 250));
81
+ }
82
+
83
+ throw new Error(
84
+ `External Playwright service at ${baseUrl} was not ready within ${externalReadinessTimeoutMs}ms`,
85
+ );
86
+ }
87
+
88
+ async function localPlaywrightEnv(): Promise<NodeJS.ProcessEnv> {
39
89
  return {
40
90
  ...process.env,
41
91
  DATABASE_FILE: process.env.DATABASE_FILE ?? databaseFile,
@@ -44,9 +94,19 @@ async function playwrightEnv(): Promise<NodeJS.ProcessEnv> {
44
94
  };
45
95
  }
46
96
 
47
- const env = await playwrightEnv();
97
+ const externalServiceUrl = externalBaseUrl();
98
+ const env = externalServiceUrl
99
+ ? { ...process.env }
100
+ : await localPlaywrightEnv();
101
+ const localDatabaseFile = externalServiceUrl ? undefined : env.DATABASE_FILE;
48
102
  delete env.NO_COLOR;
49
- await rm(env.DATABASE_FILE!, { force: true });
103
+
104
+ if (externalServiceUrl) {
105
+ await awaitExternalService(externalServiceUrl);
106
+ } else {
107
+ await mkdir(path.dirname(localDatabaseFile!), { recursive: true });
108
+ await rm(localDatabaseFile!, { force: true });
109
+ }
50
110
 
51
111
  const command = process.platform === "win32" ? "playwright.cmd" : "playwright";
52
112
  const child = spawn(command, ["test", ...process.argv.slice(2)], {
@@ -54,28 +114,38 @@ const child = spawn(command, ["test", ...process.argv.slice(2)], {
54
114
  stdio: "inherit",
55
115
  });
56
116
 
57
- child.once("error", (error: Error) => {
58
- console.error(error);
59
- process.exitCode = 1;
60
- });
61
-
62
- child.once("exit", (code: number | null, signal: NodeJS.Signals | null) => {
63
- void rm(env.DATABASE_FILE!, { force: true }).finally(() => {
64
- if (signal) {
65
- console.error(`Playwright exited with signal ${signal}`);
66
- process.exitCode = 1;
67
- return;
68
- }
117
+ const forwardedSignals = ["SIGINT", "SIGTERM"] as const;
118
+ const forwardSignal = (signal: NodeJS.Signals): void => {
119
+ child.kill(signal);
120
+ };
121
+ for (const signal of forwardedSignals) {
122
+ process.once(signal, forwardSignal);
123
+ }
69
124
 
70
- process.exitCode = code ?? 1;
125
+ try {
126
+ const result = await new Promise<{
127
+ code: number | null;
128
+ signal: NodeJS.Signals | null;
129
+ }>((resolve, reject) => {
130
+ child.once("error", reject);
131
+ child.once("exit", (code, signal) => resolve({ code, signal }));
71
132
  });
72
- });
73
133
 
74
- for (const signal of ["SIGINT", "SIGTERM"] as const) {
75
- process.once(signal, () => {
76
- child.kill(signal);
77
- void rm(env.DATABASE_FILE!, { force: true }).finally(() => {
78
- process.exitCode = 1;
79
- });
80
- });
134
+ if (result.signal) {
135
+ console.error(`Playwright exited with signal ${result.signal}`);
136
+ process.exitCode = 1;
137
+ } else {
138
+ process.exitCode = result.code ?? 1;
139
+ }
140
+ } catch (error) {
141
+ console.error(error);
142
+ process.exitCode = 1;
143
+ } finally {
144
+ for (const signal of forwardedSignals) {
145
+ process.removeListener(signal, forwardSignal);
146
+ }
147
+
148
+ if (localDatabaseFile) {
149
+ await rm(localDatabaseFile, { force: true });
150
+ }
81
151
  }
@@ -1,8 +1,8 @@
1
+ // @template-anchor db-package-import
1
2
  import vike from "@vikejs/hono";
2
3
  import { Hono } from "hono";
3
4
  import { Telefunc } from "telefunc/node";
4
5
 
5
- // @template-anchor db-package-import
6
6
  import { api } from "#/server/api";
7
7
 
8
8
  const telefunc = new Telefunc();
@@ -1,6 +1,17 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ const templateInternalDependencyIdentities = new Set([
5
+ "@typescript/native",
6
+ "@typescript/typescript6",
7
+ ]);
8
+ function assertGeneratedDependencyCatalogBoundary(dependencies) {
9
+ for (const dependency of dependencies) {
10
+ if (templateInternalDependencyIdentities.has(dependency)) {
11
+ throw new Error(`Generated Repository Dependency Catalog cannot include template-internal dependency: ${dependency}`);
12
+ }
13
+ }
14
+ }
4
15
  function templateRepositoryRoot() {
5
16
  const moduleDir = path.dirname(fileURLToPath(import.meta.url));
6
17
  const candidates = [
@@ -151,6 +162,7 @@ function renderCatalogKey(key) {
151
162
  return key.startsWith("@") ? JSON.stringify(key) : key;
152
163
  }
153
164
  export function pnpmWorkspaceYamlWithCatalogDependencies(workspaceYaml, dependencies, templateCatalog = loadTemplateDependencyCatalog()) {
165
+ assertGeneratedDependencyCatalogBoundary(dependencies);
154
166
  const catalogStart = workspaceYaml
155
167
  .split(/\r?\n/)
156
168
  .findIndex((line) => line === "catalog:");
@@ -183,6 +195,7 @@ export function pnpmWorkspaceYamlWithCatalogDependencies(workspaceYaml, dependen
183
195
  ].join("\n");
184
196
  }
185
197
  export function renderGeneratedPnpmWorkspaceYaml(options) {
198
+ assertGeneratedDependencyCatalogBoundary(options.dependencies);
186
199
  const catalog = selectTemplateDependencyCatalogEntries(options.dependencies);
187
200
  const packages = options.packages ?? ["."];
188
201
  const lines = [
@@ -1,4 +1,4 @@
1
- import { type CheckEnvironmentNeed } from "./module-graph.js";
1
+ import { type CheckEnvironmentNeed, type DeploymentCheckEnvironmentNeed } from "./module-graph.js";
2
2
  import type { PresetSourceManifest } from "./preset-source.js";
3
3
  import { type PresetProjectionSourceRoots } from "./projection-capabilities.js";
4
4
  export type GeneratedScenarioSet = "init" | "package-addition-matrix" | "focused";
@@ -28,7 +28,8 @@ export type GeneratedScenarioCommandStep = {
28
28
  readonly args: readonly string[];
29
29
  readonly cwd: string;
30
30
  readonly display: string;
31
- readonly environmentNeedKind?: CheckEnvironmentNeed["kind"];
31
+ readonly environmentNeedKind?: CheckEnvironmentNeed["kind"] | DeploymentCheckEnvironmentNeed["kind"];
32
+ readonly phase?: "deployment-preparation" | "deployment";
32
33
  };
33
34
  export type GeneratedScenarioCommandRunner = (command: string, args: readonly string[], cwd: string, options?: {
34
35
  readonly env?: Record<string, string> | undefined;
@@ -5,7 +5,7 @@ import { PackageAdditionSupport } from "@ykdz/template-shared";
5
5
  import { execa } from "execa";
6
6
  import * as v from "valibot";
7
7
  import { assembleGenerationContext } from "./generation-context.js";
8
- import { playwrightBrowserAssetsEnvironmentNeed, } from "./module-graph.js";
8
+ import { deploymentCheckEnvironmentNeeds, playwrightBrowserAssetsEnvironmentNeed, } from "./module-graph.js";
9
9
  import { planNextStepInstructions } from "./next-step-instructions.js";
10
10
  import { canPlanPackageLinkIntent, } from "./package-linking.js";
11
11
  import { findPresetSourceManifestPreset } from "./preset-source.js";
@@ -14,7 +14,7 @@ const defaultDeterministicToolchainEnv = {
14
14
  TEMPLATE_TOOLCHAIN_RESOLUTION: "bundled-fallback",
15
15
  };
16
16
  const defaultFixtureConcurrency = 2;
17
- const scenarioFingerprintVersion = 1;
17
+ const scenarioFingerprintVersion = 2;
18
18
  const storedBlueprintSchema = v.object({
19
19
  packages: v.optional(v.array(v.object({
20
20
  name: v.string(),
@@ -235,6 +235,11 @@ async function writeScenarioReplayMarker(cache, fingerprint, scenario) {
235
235
  await mkdir(cache.directory, { recursive: true });
236
236
  await writeFile(path.join(cache.directory, `${fingerprint}.passed`), `${JSON.stringify({ scenario, fingerprint })}\n`, "utf8");
237
237
  }
238
+ function deploymentReplayFingerprint(fingerprint) {
239
+ return createHash("sha256")
240
+ .update(`${fingerprint}\0docker-engine-available`)
241
+ .digest("hex");
242
+ }
238
243
  export function packageLeafNameForAddedPreset(manifest, presetName) {
239
244
  const preset = manifest.presets.find((entry) => entry.name === presetName);
240
245
  if (!preset) {
@@ -392,6 +397,36 @@ function machineVerifiableNextStepsForPreset(presetName, projectDir, manifest, o
392
397
  };
393
398
  });
394
399
  }
400
+ function deploymentStepsForPreset(presetName, scenario, projectDir, manifest, options) {
401
+ if (scenario.set === "init") {
402
+ return [];
403
+ }
404
+ const deploymentChecks = projectionPlanForPreset(presetName, projectDir, manifest, options).checkPlan
405
+ .deploymentChecks ?? [];
406
+ const needsDocker = deploymentChecks.some((check) => deploymentCheckEnvironmentNeeds(check).some((need) => need.kind === "docker-engine"));
407
+ if (!needsDocker) {
408
+ return [];
409
+ }
410
+ return [
411
+ {
412
+ id: "check-docker-engine",
413
+ command: "docker",
414
+ args: ["info", "--format", "{{.ServerVersion}}"],
415
+ cwd: projectDir,
416
+ display: "docker info --format {{.ServerVersion}}",
417
+ environmentNeedKind: "docker-engine",
418
+ phase: "deployment-preparation",
419
+ },
420
+ {
421
+ id: "run-deployment-check",
422
+ command: "pnpm",
423
+ args: ["run", "check:deployment"],
424
+ cwd: projectDir,
425
+ display: "pnpm run check:deployment",
426
+ phase: "deployment",
427
+ },
428
+ ];
429
+ }
395
430
  function addedPresetEnvironmentNeeds(manifest, scenario, projectDir, addedPackagePath, options) {
396
431
  if (!scenario.addedPreset || !addedPackagePath) {
397
432
  return [];
@@ -431,30 +466,37 @@ export function generatedScenarioQualityGateSteps(manifest, scenario, projectDir
431
466
  const addedEnvironmentSteps = generatedScenarioEnvironmentNeedSteps(addedPresetEnvironmentNeeds(manifest, scenario, projectDir, addedPackagePath, normalizedOptions), projectDir);
432
467
  const existingDisplays = new Set(steps.map((step) => step.display));
433
468
  const extraEnvironmentSteps = addedEnvironmentSteps.filter((step) => !existingDisplays.has(step.display));
469
+ const deploymentSteps = deploymentStepsForPreset(scenario.basePreset, scenario, projectDir, manifest, normalizedOptions);
434
470
  if (extraEnvironmentSteps.length === 0) {
435
- return steps;
471
+ return [...steps, ...deploymentSteps];
436
472
  }
437
473
  const rootCheckIndex = steps.findIndex((step) => step.id === "run-root-check");
438
474
  if (rootCheckIndex === -1) {
439
- return [...steps, ...extraEnvironmentSteps];
475
+ return [...steps, ...extraEnvironmentSteps, ...deploymentSteps];
440
476
  }
441
477
  return [
442
478
  ...steps.slice(0, rootCheckIndex),
443
479
  ...extraEnvironmentSteps,
444
480
  ...steps.slice(rootCheckIndex),
481
+ ...deploymentSteps,
445
482
  ];
446
483
  }
447
484
  async function runGeneratedScenarioQualityGate(manifest, scenario, projectDir, addedPackagePath, options) {
448
485
  const steps = generatedScenarioQualityGateSteps(manifest, scenario, projectDir, addedPackagePath, options);
449
486
  const requiresSerializedPlaywrightRootCheck = generatedScenarioRequiresSerializedRootCheck(steps);
450
487
  let replayFingerprint;
488
+ let baseQualityGateReplayed = false;
489
+ let baseQualityGateMarkerWritten = false;
451
490
  for (const step of steps) {
452
491
  if (step.id === "install-dependencies") {
453
492
  const installResult = await runGeneratedScenarioInstallStep(step, scenario, options);
454
493
  replayFingerprint = installResult.fingerprint;
455
- if (installResult.replayed) {
456
- return;
457
- }
494
+ baseQualityGateReplayed = installResult.baseQualityGateReplayed;
495
+ continue;
496
+ }
497
+ if (baseQualityGateReplayed &&
498
+ step.phase !== "deployment-preparation" &&
499
+ step.phase !== "deployment") {
458
500
  continue;
459
501
  }
460
502
  const env = step.id === "run-root-check" ? { CI: "1" } : undefined;
@@ -464,13 +506,57 @@ async function runGeneratedScenarioQualityGate(manifest, scenario, projectDir, a
464
506
  logPrefix: scenario.label,
465
507
  });
466
508
  };
509
+ if (step.environmentNeedKind === "docker-engine") {
510
+ try {
511
+ await runStep();
512
+ }
513
+ catch (error) {
514
+ const reason = error instanceof Error ? error.message : String(error);
515
+ options.reporter.info?.(`-- Skipping deployment check for ${scenario.label}: Docker engine is unavailable (${reason})`);
516
+ break;
517
+ }
518
+ continue;
519
+ }
520
+ if (step.phase === "deployment") {
521
+ const deploymentFingerprint = replayFingerprint === undefined
522
+ ? undefined
523
+ : deploymentReplayFingerprint(replayFingerprint);
524
+ if (deploymentFingerprint !== undefined &&
525
+ options.replayCache?.read &&
526
+ (await scenarioReplayMarkerExists(options.replayCache, deploymentFingerprint))) {
527
+ options.reporter.info?.(`-- Replayed passed deployment fixture ${scenario.label}: ${deploymentFingerprint}`);
528
+ continue;
529
+ }
530
+ try {
531
+ await runStep();
532
+ }
533
+ catch (error) {
534
+ throw new Error(`Generated deployment command failed for ${scenario.label}; command: ${step.display}. See the cause for the deployment mode, phase, command output, and retained container logs.`, { cause: error });
535
+ }
536
+ if (deploymentFingerprint !== undefined && options.replayCache?.write) {
537
+ await writeScenarioReplayMarker(options.replayCache, deploymentFingerprint, scenario);
538
+ options.reporter.info?.(`-- Stored passed deployment fixture replay marker for ${scenario.label}: ${deploymentFingerprint}`);
539
+ }
540
+ continue;
541
+ }
467
542
  if (step.id === "run-root-check" && requiresSerializedPlaywrightRootCheck) {
468
543
  await options.rootCheckLock.run(runStep);
469
- continue;
470
544
  }
471
- await runStep();
545
+ else {
546
+ await runStep();
547
+ }
548
+ if (step.id === "run-root-check" &&
549
+ options.replayCache?.write &&
550
+ replayFingerprint !== undefined) {
551
+ await writeScenarioReplayMarker(options.replayCache, replayFingerprint, scenario);
552
+ baseQualityGateMarkerWritten = true;
553
+ options.reporter.info?.(`-- Stored passed fixture replay marker for ${scenario.label}: ${replayFingerprint}`);
554
+ }
472
555
  }
473
- if (options.replayCache?.write && replayFingerprint !== undefined) {
556
+ if (options.replayCache?.write &&
557
+ replayFingerprint !== undefined &&
558
+ !baseQualityGateMarkerWritten &&
559
+ !baseQualityGateReplayed) {
474
560
  await writeScenarioReplayMarker(options.replayCache, replayFingerprint, scenario);
475
561
  options.reporter.info?.(`-- Stored passed fixture replay marker for ${scenario.label}: ${replayFingerprint}`);
476
562
  }
@@ -482,15 +568,15 @@ async function runGeneratedScenarioInstallStep(step, scenario, options) {
482
568
  replayFingerprint = await hashGeneratedScenarioDirectory(scenario, step.cwd);
483
569
  if (options.replayCache.read &&
484
570
  (await scenarioReplayMarkerExists(options.replayCache, replayFingerprint))) {
485
- options.reporter.info?.(`-- Replayed passed fixture ${scenario.label}: ${replayFingerprint}`);
486
- return { fingerprint: replayFingerprint, replayed: true };
571
+ options.reporter.info?.(`-- Replayed passed base quality gate for ${scenario.label}: ${replayFingerprint}`);
572
+ return { fingerprint: replayFingerprint, baseQualityGateReplayed: true };
487
573
  }
488
574
  }
489
575
  await options.runCommand(step.command, ["fetch"], step.cwd, {
490
576
  logPrefix: scenario.label,
491
577
  });
492
578
  await options.runCommand(step.command, ["install", "--offline", "--frozen-lockfile"], step.cwd, { logPrefix: scenario.label });
493
- return { fingerprint: replayFingerprint, replayed: false };
579
+ return { fingerprint: replayFingerprint, baseQualityGateReplayed: false };
494
580
  }
495
581
  async function checkGeneratedScenario(manifest, scenario, workspace, options) {
496
582
  options.reporter.info?.(`\n== ${scenario.label} ==`);
@@ -26,6 +26,13 @@ export type CheckComponent = {
26
26
  readonly kind: CheckComponentKind;
27
27
  readonly owner: ComponentOwner;
28
28
  };
29
+ export type DeploymentCheckComponent = {
30
+ readonly kind: "deployment-image";
31
+ readonly owner: PackageBoundaryOwner;
32
+ };
33
+ export type DeploymentCheckEnvironmentNeed = {
34
+ readonly kind: "docker-engine";
35
+ };
29
36
  export type FixComponent = {
30
37
  readonly kind: FixComponentKind;
31
38
  readonly owner: ComponentOwner;
@@ -33,6 +40,7 @@ export type FixComponent = {
33
40
  export type CheckPlan = {
34
41
  readonly components: CheckComponent[];
35
42
  readonly environmentNeeds: CheckEnvironmentNeed[];
43
+ readonly deploymentChecks?: DeploymentCheckComponent[] | undefined;
36
44
  };
37
45
  export type FixPlan = {
38
46
  readonly components: FixComponent[];
@@ -43,6 +51,10 @@ export declare function fixComponentTaskName(component: FixComponent): string;
43
51
  export declare function renderCheckLeafCommand(component: CheckComponent): string;
44
52
  export declare function renderFixLeafCommand(component: FixComponent): string;
45
53
  export declare function renderRootCheckCommand(plan: CheckPlan): string;
54
+ export declare function deploymentCheckEnvironmentNeeds(check: DeploymentCheckComponent): DeploymentCheckEnvironmentNeed[];
55
+ export declare function deploymentCheckTaskName(check: DeploymentCheckComponent): "check:deployment";
56
+ export declare function renderDeploymentCheckLeafCommand(check: DeploymentCheckComponent): string;
57
+ export declare function renderDeploymentCheckCommand(plan: Pick<CheckPlan, "deploymentChecks">): string;
46
58
  export declare function renderFixCommand(plan: FixPlan): string;
47
59
  export declare function renderPlaywrightBrowserInstallCommand(need: CheckEnvironmentNeed, options?: {
48
60
  readonly withDeps?: boolean;
@@ -119,6 +119,24 @@ export function renderRootCheckCommand(plan) {
119
119
  "check:run",
120
120
  ]));
121
121
  }
122
+ export function deploymentCheckEnvironmentNeeds(check) {
123
+ switch (check.kind) {
124
+ case "deployment-image":
125
+ return [{ kind: "docker-engine" }];
126
+ }
127
+ }
128
+ export function deploymentCheckTaskName(check) {
129
+ switch (check.kind) {
130
+ case "deployment-image":
131
+ return "check:deployment";
132
+ }
133
+ }
134
+ export function renderDeploymentCheckLeafCommand(check) {
135
+ return `pnpm --filter './${check.owner.path}' run ${deploymentCheckTaskName(check)}`;
136
+ }
137
+ export function renderDeploymentCheckCommand(plan) {
138
+ return uniqueTaskNames((plan.deploymentChecks ?? []).map(renderDeploymentCheckLeafCommand)).join(" && ");
139
+ }
122
140
  export function renderFixCommand(plan) {
123
141
  return renderTurboRunCommand(uniqueTaskNames([
124
142
  ...plan.components.flatMap(fixComponentTaskNames),
@@ -4,10 +4,12 @@ packages:
4
4
  nodeLinker: hoisted
5
5
 
6
6
  catalog:
7
+ "@typescript/native": npm:typescript@^7.0.2
8
+ "@typescript/typescript6": ^6.0.2
7
9
  "@hono/node-server": ^2.0.6
8
10
  "@playwright/test": ^1.61.1
9
11
  "@tailwindcss/vite": ^4.3.2
10
- "@types/node": ^24.0.0
12
+ "@types/node": ^24.13.3
11
13
  "@types/web-bluetooth": ^0.0.21
12
14
  "@vitejs/plugin-vue": ^6.0.7
13
15
  "@vikejs/hono": ^0.2.1
@@ -17,17 +19,18 @@ catalog:
17
19
  execa: ^9.6.1
18
20
  hono: ^4.12.27
19
21
  oxfmt: ^0.58.0
20
- oxlint: ^1.72.0
22
+ oxlint: ^1.73.0
21
23
  oxlint-tsgolint: ^0.24.0
22
24
  pinia: ^3.0.4
25
+ srvx: ^0.11.21
23
26
  tailwindcss: ^4.3.2
24
27
  telefunc: ^0.2.22
25
28
  tsc-alias: ^1.8.17
26
- tsx: ^4.22.4
29
+ tsx: ^4.23.0
27
30
  turbo: 2.10.2
28
31
  typescript: ^6.0.3
29
32
  valibot: ^1.4.2
30
- vite: ^8.1.2
33
+ vite: ^8.1.4
31
34
  vike: ^0.4.260
32
35
  vike-vue: ^0.9.12
33
36
  vitest: ^4.1.10
@@ -2,7 +2,7 @@ import type { BuiltInPreset, PackageRole, PackageSourcePreset, ProjectBlueprint
2
2
  import type { GenerationContext } from "./generation-context.js";
3
3
  import { type CheckPlan, type FixPlan } from "./module-graph.js";
4
4
  import { type NextStepInstruction } from "./next-step-instructions.js";
5
- import type { CiDockerImageBuild, DependencyMaintenancePolicy } from "./project-github.js";
5
+ import type { DependencyMaintenancePolicy } from "./project-github.js";
6
6
  import type { RenderOperation } from "./renderer.js";
7
7
  export type PresetPackageAdditionOptions = {
8
8
  readonly root: string;
@@ -35,7 +35,6 @@ export type PresetProjectionPlan = {
35
35
  readonly operations: readonly RenderOperation[];
36
36
  readonly checkPlan: CheckPlan;
37
37
  readonly fixPlan: FixPlan;
38
- readonly ciDockerImageBuild?: CiDockerImageBuild | undefined;
39
38
  readonly dependencyMaintenancePolicy: DependencyMaintenancePolicy;
40
39
  readonly packageScripts: Record<string, string>;
41
40
  readonly capabilities: {
@@ -12,13 +12,6 @@ export type PnpmTaskLayer = {
12
12
  readonly installCommand: "pnpm install";
13
13
  readonly checkCommand: "pnpm run check";
14
14
  };
15
- export type CiDockerImageBuild = {
16
- readonly dockerfile: string;
17
- readonly targets: readonly {
18
- readonly name: string;
19
- readonly tag: string;
20
- }[];
21
- };
22
15
  export type DependencyEcosystem = "npm" | "cargo" | "github-actions" | "docker" | "rust-toolchain";
23
16
  export type DependabotDirectory = `/${string}`;
24
17
  export type DependencyMaintenancePolicy = {
@@ -32,7 +25,6 @@ type ProjectCheckWorkflowOptions = {
32
25
  readonly capability?: CiCapability | undefined;
33
26
  readonly environmentPreparation?: Partial<CiEnvironmentPreparation> | undefined;
34
27
  readonly taskLayer?: PnpmTaskLayer | undefined;
35
- readonly dockerImageBuild?: CiDockerImageBuild | undefined;
36
28
  };
37
29
  export declare function projectCheckWorkflow(options: ProjectCheckWorkflowOptions): string;
38
30
  export declare function projectDependabotConfig(policy: DependencyMaintenancePolicy): string;
@@ -1,4 +1,4 @@
1
- import { renderPlaywrightBrowserInstallCommand, } from "./module-graph.js";
1
+ import { deploymentCheckEnvironmentNeeds, deploymentCheckTaskName, renderPlaywrightBrowserInstallCommand, } from "./module-graph.js";
2
2
  const defaultCiCapability = {
3
3
  workflowName: "Check",
4
4
  jobName: "check",
@@ -16,6 +16,8 @@ export function projectCheckWorkflow(options) {
16
16
  ...options.environmentPreparation,
17
17
  };
18
18
  const taskLayer = options.taskLayer ?? pnpmTaskLayer;
19
+ const deploymentChecks = options.checkPlan.deploymentChecks ?? [];
20
+ const needsDocker = deploymentChecks.some((check) => deploymentCheckEnvironmentNeeds(check).some((need) => need.kind === "docker-engine"));
19
21
  const lines = [
20
22
  `name: ${capability.workflowName}`,
21
23
  "",
@@ -34,15 +36,20 @@ export function projectCheckWorkflow(options) {
34
36
  if (environmentPreparation.nodeFromPackageMetadata) {
35
37
  lines.push(" - uses: actions/setup-node@v6", " with:", " node-version-file: package.json", " - run: corepack enable");
36
38
  }
39
+ if (needsDocker) {
40
+ lines.push(" - uses: docker/setup-buildx-action@v3");
41
+ }
37
42
  if (environmentPreparation.rustToolchain) {
38
43
  lines.push(" - uses: dtolnay/rust-toolchain@stable", " with:", " components: rustfmt, clippy", " - uses: Swatinem/rust-cache@v2");
39
44
  }
40
45
  lines.push(` - run: ${taskLayer.installCommand}`);
41
46
  lines.push(...options.checkPlan.environmentNeeds.map((need) => ` - run: ${renderCiEnvironmentNeedCommand(need)}`));
42
- lines.push(` - run: ${taskLayer.checkCommand}`, "");
43
- if (options.dockerImageBuild !== undefined) {
44
- lines.push(" docker-image:", ` runs-on: ${capability.runner}`, " steps:", " - uses: actions/checkout@v7", " - uses: docker/setup-buildx-action@v3", ...options.dockerImageBuild.targets.map((target) => ` - run: docker buildx build --load --file ${options.dockerImageBuild.dockerfile} --target ${target.name} --tag ${target.tag} .`), "");
47
+ lines.push(` - run: ${taskLayer.checkCommand}`);
48
+ const deploymentCheck = deploymentChecks[0];
49
+ if (deploymentCheck !== undefined) {
50
+ lines.push(` - run: pnpm run ${deploymentCheckTaskName(deploymentCheck)}`);
45
51
  }
52
+ lines.push("");
46
53
  return lines.join("\n");
47
54
  }
48
55
  export function projectDependabotConfig(policy) {