@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.
- package/node_modules/@ykdz/template-builtin-source/package.json +1 -1
- package/node_modules/@ykdz/template-builtin-source/templates/preset-source.json +7 -0
- package/node_modules/@ykdz/template-builtin-source/templates/vike-app/.github/workflows/check.yml +2 -8
- package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/Dockerfile +45 -9
- package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/pages/index/+Page.telefunc.ts +1 -2
- package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/playwright.config.ts +45 -9
- package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/scripts/check-standalone-deployment.ts +449 -0
- package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/scripts/container-entrypoint.sh +52 -0
- package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/scripts/prepare-database.sh +6 -0
- package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/scripts/run-playwright.ts +95 -25
- package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/server/app.ts +1 -1
- package/node_modules/@ykdz/template-core/dist/dependency-catalog.js +13 -0
- package/node_modules/@ykdz/template-core/dist/generated-scenarios.d.ts +3 -2
- package/node_modules/@ykdz/template-core/dist/generated-scenarios.js +99 -13
- package/node_modules/@ykdz/template-core/dist/module-graph.d.ts +12 -0
- package/node_modules/@ykdz/template-core/dist/module-graph.js +18 -0
- package/node_modules/@ykdz/template-core/dist/pnpm-workspace.yaml +7 -4
- package/node_modules/@ykdz/template-core/dist/preset-projection.d.ts +1 -2
- package/node_modules/@ykdz/template-core/dist/project-github.d.ts +0 -8
- package/node_modules/@ykdz/template-core/dist/project-github.js +11 -4
- package/node_modules/@ykdz/template-core/dist/projection-capabilities.js +15 -13
- package/node_modules/@ykdz/template-core/dist/renderer.js +1 -1
- package/node_modules/@ykdz/template-core/dist/template-boundary-check.js +1 -1
- package/node_modules/@ykdz/template-core/package.json +2 -1
- package/node_modules/@ykdz/template-shared/dist/index.d.ts +6 -6
- package/node_modules/@ykdz/template-shared/package.json +1 -0
- 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
|
package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/scripts/run-playwright.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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 =
|
|
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
|
-
|
|
456
|
-
|
|
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
|
-
|
|
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 &&
|
|
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
|
|
486
|
-
return { fingerprint: replayFingerprint,
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
44
|
-
|
|
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) {
|