@ykdz/template 0.0.21 → 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 +11 -0
- package/node_modules/@ykdz/template-builtin-source/templates/vike-app/.github/dependabot.yml +4 -0
- package/node_modules/@ykdz/template-builtin-source/templates/vike-app/.github/workflows/check.yml +2 -0
- package/node_modules/@ykdz/template-builtin-source/templates/vike-app/db/drizzle/migrations/20260709120325_old_captain_flint/migration.sql +6 -0
- package/node_modules/@ykdz/template-builtin-source/templates/vike-app/db/drizzle/migrations/20260709120325_old_captain_flint/snapshot.json +67 -0
- package/node_modules/@ykdz/template-builtin-source/templates/vike-app/db/scripts/seed-example.ts +7 -0
- package/node_modules/@ykdz/template-builtin-source/templates/vike-app/db/src/db.ts +2 -1
- package/node_modules/@ykdz/template-builtin-source/templates/vike-app/db/src/index.ts +1 -0
- package/node_modules/@ykdz/template-builtin-source/templates/vike-app/db/src/queries/todos.ts +17 -1
- package/node_modules/@ykdz/template-builtin-source/templates/vike-app/db/src/readiness.ts +22 -0
- package/node_modules/@ykdz/template-builtin-source/templates/vike-app/db/src/schema.ts +1 -1
- package/node_modules/@ykdz/template-builtin-source/templates/vike-app/db/src/seed/example.ts +23 -0
- package/node_modules/@ykdz/template-builtin-source/templates/vike-app/db/test/todos.test.ts +2 -0
- package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/+server.ts +2 -1
- package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/Dockerfile +83 -0
- package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/Dockerfile.dockerignore +14 -0
- 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/pages/index/+Page.vue +6 -2
- package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/playwright.config.ts +50 -14
- 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 +101 -12
- package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/server/app.ts +23 -12
- 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/project-github.d.ts +1 -0
- package/node_modules/@ykdz/template-core/dist/project-github.js +18 -5
- package/node_modules/@ykdz/template-core/dist/projection-capabilities.js +57 -7
- package/node_modules/@ykdz/template-core/dist/renderer.js +4 -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,449 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const webRoot = path.resolve(import.meta.dirname, "..");
|
|
5
|
+
const repositoryRoot = path.resolve(webRoot, "../..");
|
|
6
|
+
const uniqueId = `${process.pid}-${Date.now()}`;
|
|
7
|
+
const standaloneImage = `vike-deployment-check-standalone:${uniqueId}`;
|
|
8
|
+
const runtimeImage = `vike-deployment-check-runtime:${uniqueId}`;
|
|
9
|
+
const containers = {
|
|
10
|
+
standalone: `vike-deployment-check-standalone-${uniqueId}`,
|
|
11
|
+
preparation: `vike-deployment-check-preparation-${uniqueId}`,
|
|
12
|
+
runtime: `vike-deployment-check-runtime-${uniqueId}`,
|
|
13
|
+
unprepared: `vike-deployment-check-unprepared-${uniqueId}`,
|
|
14
|
+
} as const;
|
|
15
|
+
const volumes = {
|
|
16
|
+
standalone: `vike-deployment-check-standalone-${uniqueId}`,
|
|
17
|
+
prepared: `vike-deployment-check-prepared-${uniqueId}`,
|
|
18
|
+
unprepared: `vike-deployment-check-unprepared-${uniqueId}`,
|
|
19
|
+
} as const;
|
|
20
|
+
const abortController = new AbortController();
|
|
21
|
+
const forwardedSignals = ["SIGINT", "SIGTERM"] as const;
|
|
22
|
+
const signalHandlers = new Map<NodeJS.Signals, () => void>();
|
|
23
|
+
const defaultReadinessTimeoutMs = 60_000;
|
|
24
|
+
|
|
25
|
+
interface CommandResult {
|
|
26
|
+
readonly stderr: string;
|
|
27
|
+
readonly stdout: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface CommandOptions {
|
|
31
|
+
readonly cwd?: string;
|
|
32
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
33
|
+
readonly signal?: AbortSignal;
|
|
34
|
+
readonly timeout: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type DeploymentMode =
|
|
38
|
+
| "standalone"
|
|
39
|
+
| "external-preparation"
|
|
40
|
+
| "runtime"
|
|
41
|
+
| "fresh-runtime";
|
|
42
|
+
|
|
43
|
+
async function deploymentPhase<T>(
|
|
44
|
+
mode: DeploymentMode,
|
|
45
|
+
phase: string,
|
|
46
|
+
operation: () => Promise<T>,
|
|
47
|
+
): Promise<T> {
|
|
48
|
+
try {
|
|
49
|
+
return await operation();
|
|
50
|
+
} catch (error: unknown) {
|
|
51
|
+
throw new Error(`Deployment mode ${mode} failed during ${phase}.`, {
|
|
52
|
+
cause: error,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function commandDisplay(command: string, args: readonly string[]): string {
|
|
58
|
+
return [command, ...args].map((part) => JSON.stringify(part)).join(" ");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function runCommand(
|
|
62
|
+
command: string,
|
|
63
|
+
args: readonly string[],
|
|
64
|
+
options: CommandOptions,
|
|
65
|
+
): Promise<CommandResult> {
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const child = spawn(command, args, {
|
|
68
|
+
cwd: options.cwd,
|
|
69
|
+
env: options.env,
|
|
70
|
+
signal: options.signal,
|
|
71
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
72
|
+
});
|
|
73
|
+
let stderr = "";
|
|
74
|
+
let stdout = "";
|
|
75
|
+
let timedOut = false;
|
|
76
|
+
let settled = false;
|
|
77
|
+
child.stdout.setEncoding("utf8");
|
|
78
|
+
child.stderr.setEncoding("utf8");
|
|
79
|
+
child.stdout.on("data", (chunk: string) => {
|
|
80
|
+
stdout += chunk;
|
|
81
|
+
});
|
|
82
|
+
child.stderr.on("data", (chunk: string) => {
|
|
83
|
+
stderr += chunk;
|
|
84
|
+
});
|
|
85
|
+
const timeout = setTimeout(() => {
|
|
86
|
+
timedOut = true;
|
|
87
|
+
child.kill("SIGKILL");
|
|
88
|
+
}, options.timeout);
|
|
89
|
+
const finish = (error?: Error): void => {
|
|
90
|
+
if (settled) return;
|
|
91
|
+
settled = true;
|
|
92
|
+
clearTimeout(timeout);
|
|
93
|
+
if (error === undefined) {
|
|
94
|
+
resolve({ stderr, stdout });
|
|
95
|
+
} else {
|
|
96
|
+
reject(Object.assign(error, { stderr, stdout }));
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
child.once("error", (error) => finish(error));
|
|
100
|
+
child.once("close", (code, signal) => {
|
|
101
|
+
if (code === 0 && !timedOut) {
|
|
102
|
+
finish();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
finish(
|
|
106
|
+
Object.assign(
|
|
107
|
+
new Error(
|
|
108
|
+
`${commandDisplay(command, args)} ${
|
|
109
|
+
timedOut
|
|
110
|
+
? `timed out after ${options.timeout}ms`
|
|
111
|
+
: `exited with code ${String(code)}${signal === null ? "" : ` from ${signal}`}`
|
|
112
|
+
}\nstdout:\n${stdout}\nstderr:\n${stderr}`,
|
|
113
|
+
),
|
|
114
|
+
{ code, signal },
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function readinessTimeoutMs(): number {
|
|
122
|
+
const configured = process.env.STANDALONE_DEPLOYMENT_READINESS_TIMEOUT_MS;
|
|
123
|
+
if (configured === undefined) {
|
|
124
|
+
return defaultReadinessTimeoutMs;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const timeout = Number(configured);
|
|
128
|
+
if (!Number.isSafeInteger(timeout) || timeout <= 0) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
"STANDALONE_DEPLOYMENT_READINESS_TIMEOUT_MS must be a positive integer",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return timeout;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function dockerWithTimeout(
|
|
137
|
+
timeout: number,
|
|
138
|
+
...args: string[]
|
|
139
|
+
): Promise<string> {
|
|
140
|
+
const { stdout } = await runCommand("docker", args, {
|
|
141
|
+
cwd: repositoryRoot,
|
|
142
|
+
signal: abortController.signal,
|
|
143
|
+
timeout,
|
|
144
|
+
});
|
|
145
|
+
return stdout.trim();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function docker(...args: string[]): Promise<string> {
|
|
149
|
+
return dockerWithTimeout(600_000, ...args);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function cleanup(): Promise<void> {
|
|
153
|
+
const cleanupCommands = [
|
|
154
|
+
...Object.values(containers).map((name) => ["rm", "--force", name]),
|
|
155
|
+
...Object.values(volumes).map((name) => ["volume", "rm", "--force", name]),
|
|
156
|
+
["image", "rm", "--force", runtimeImage],
|
|
157
|
+
["image", "rm", "--force", standaloneImage],
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
for (const args of cleanupCommands) {
|
|
161
|
+
try {
|
|
162
|
+
await runCommand("docker", args, {
|
|
163
|
+
cwd: repositoryRoot,
|
|
164
|
+
timeout: 30_000,
|
|
165
|
+
});
|
|
166
|
+
} catch {
|
|
167
|
+
// A resource may not have been created yet.
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function containerLogs(container: string): Promise<string> {
|
|
173
|
+
try {
|
|
174
|
+
const { stderr, stdout } = await runCommand("docker", ["logs", container], {
|
|
175
|
+
cwd: repositoryRoot,
|
|
176
|
+
signal: AbortSignal.timeout(10_000),
|
|
177
|
+
timeout: 10_000,
|
|
178
|
+
});
|
|
179
|
+
return [stdout.trim(), stderr.trim()].filter(Boolean).join("\n");
|
|
180
|
+
} catch (error) {
|
|
181
|
+
return `Container logs unavailable: ${String(error)}`;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function deploymentLogs(): Promise<string> {
|
|
186
|
+
const logs = await Promise.all(
|
|
187
|
+
Object.entries(containers).map(
|
|
188
|
+
async ([phase, container]) =>
|
|
189
|
+
`${phase} container logs:\n${await containerLogs(container)}`,
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
return logs.join("\n");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function publishedBaseUrl(container: string): Promise<string> {
|
|
196
|
+
const portOutput = await dockerWithTimeout(
|
|
197
|
+
10_000,
|
|
198
|
+
"port",
|
|
199
|
+
container,
|
|
200
|
+
"3000/tcp",
|
|
201
|
+
);
|
|
202
|
+
const match = /:(\d+)$/u.exec(portOutput.split("\n")[0] ?? "");
|
|
203
|
+
if (!match?.[1]) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
`Could not determine the published web port: ${portOutput}`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return `http://127.0.0.1:${match[1]}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function waitForReady(
|
|
212
|
+
phase: "Standalone" | "Runtime",
|
|
213
|
+
container: string,
|
|
214
|
+
baseUrl: string,
|
|
215
|
+
): Promise<void> {
|
|
216
|
+
const timeout = readinessTimeoutMs();
|
|
217
|
+
const deadline = Date.now() + timeout;
|
|
218
|
+
while (Date.now() < deadline) {
|
|
219
|
+
try {
|
|
220
|
+
const response = await fetch(baseUrl, {
|
|
221
|
+
signal: AbortSignal.timeout(2_000),
|
|
222
|
+
});
|
|
223
|
+
if (response.ok) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
// The container may still be starting the server.
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const running = await dockerWithTimeout(
|
|
231
|
+
5_000,
|
|
232
|
+
"inspect",
|
|
233
|
+
"--format={{.State.Running}}",
|
|
234
|
+
container,
|
|
235
|
+
);
|
|
236
|
+
if (running !== "true") {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`${phase} container exited before readiness.\n${await containerLogs(container)}`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
throw new Error(
|
|
245
|
+
`${phase} container was not ready within ${timeout}ms.\n${await containerLogs(container)}`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function assertProcessIdentity(
|
|
250
|
+
phase: "Standalone" | "Runtime",
|
|
251
|
+
container: string,
|
|
252
|
+
): Promise<void> {
|
|
253
|
+
await docker(
|
|
254
|
+
"exec",
|
|
255
|
+
container,
|
|
256
|
+
"node",
|
|
257
|
+
"--input-type=module",
|
|
258
|
+
"--eval",
|
|
259
|
+
`import { statSync } from "node:fs";
|
|
260
|
+
import { DatabaseSync } from "node:sqlite";
|
|
261
|
+
const expectedIdentity = 1001;
|
|
262
|
+
if (process.getuid() !== expectedIdentity || process.getgid() !== expectedIdentity) {
|
|
263
|
+
throw new Error(\`${phase} process identity must be 1001:1001, received \${process.getuid()}:\${process.getgid()}\`);
|
|
264
|
+
}
|
|
265
|
+
const databaseStat = statSync(process.env.DATABASE_FILE);
|
|
266
|
+
if (databaseStat.uid !== expectedIdentity || databaseStat.gid !== expectedIdentity) {
|
|
267
|
+
throw new Error(\`Database ownership must be 1001:1001, received \${databaseStat.uid}:\${databaseStat.gid}\`);
|
|
268
|
+
}`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function assertEmptyDatabase(container: string): Promise<void> {
|
|
273
|
+
await docker(
|
|
274
|
+
"exec",
|
|
275
|
+
container,
|
|
276
|
+
"node",
|
|
277
|
+
"--input-type=module",
|
|
278
|
+
"--eval",
|
|
279
|
+
`import { DatabaseSync } from "node:sqlite";
|
|
280
|
+
const db = new DatabaseSync(process.env.DATABASE_FILE);
|
|
281
|
+
const row = db.prepare("select count(*) as count from todos").get();
|
|
282
|
+
if (row.count !== 0) throw new Error(\`Fresh deployment must contain zero TODOs, received \${row.count}\`);`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function runPlaywright(baseUrl: string): Promise<void> {
|
|
287
|
+
await runCommand("pnpm", ["--dir", "apps/web", "run", "test:e2e:run"], {
|
|
288
|
+
cwd: repositoryRoot,
|
|
289
|
+
env: {
|
|
290
|
+
...process.env,
|
|
291
|
+
PLAYWRIGHT_EXTERNAL_BASE_URL: baseUrl,
|
|
292
|
+
},
|
|
293
|
+
signal: abortController.signal,
|
|
294
|
+
timeout: 120_000,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function runStartedDeployment(
|
|
299
|
+
mode: "standalone" | "runtime",
|
|
300
|
+
readinessLabel: "Standalone" | "Runtime",
|
|
301
|
+
container: string,
|
|
302
|
+
volume: string,
|
|
303
|
+
image: string,
|
|
304
|
+
): Promise<void> {
|
|
305
|
+
await deploymentPhase(mode, "start", async () =>
|
|
306
|
+
docker(
|
|
307
|
+
"run",
|
|
308
|
+
"--detach",
|
|
309
|
+
"--name",
|
|
310
|
+
container,
|
|
311
|
+
"--publish",
|
|
312
|
+
"127.0.0.1::3000",
|
|
313
|
+
"--volume",
|
|
314
|
+
`${volume}:/data`,
|
|
315
|
+
image,
|
|
316
|
+
),
|
|
317
|
+
);
|
|
318
|
+
const baseUrl = await deploymentPhase(mode, "start", async () =>
|
|
319
|
+
publishedBaseUrl(container),
|
|
320
|
+
);
|
|
321
|
+
await deploymentPhase(mode, "readiness", async () =>
|
|
322
|
+
waitForReady(readinessLabel, container, baseUrl),
|
|
323
|
+
);
|
|
324
|
+
await deploymentPhase(mode, "identity", async () =>
|
|
325
|
+
assertProcessIdentity(readinessLabel, container),
|
|
326
|
+
);
|
|
327
|
+
await deploymentPhase(mode, "empty-database", async () =>
|
|
328
|
+
assertEmptyDatabase(container),
|
|
329
|
+
);
|
|
330
|
+
await deploymentPhase(mode, "playwright", async () => runPlaywright(baseUrl));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function assertUnpreparedRuntimeFails(): Promise<void> {
|
|
334
|
+
try {
|
|
335
|
+
await dockerWithTimeout(
|
|
336
|
+
30_000,
|
|
337
|
+
"run",
|
|
338
|
+
"--name",
|
|
339
|
+
containers.unprepared,
|
|
340
|
+
"--volume",
|
|
341
|
+
`${volumes.unprepared}:/data`,
|
|
342
|
+
runtimeImage,
|
|
343
|
+
);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
const failure = error as {
|
|
346
|
+
code?: unknown;
|
|
347
|
+
stderr?: string;
|
|
348
|
+
stdout?: string;
|
|
349
|
+
};
|
|
350
|
+
const output = `${failure.stdout ?? ""}\n${failure.stderr ?? ""}`;
|
|
351
|
+
if (typeof failure.code === "number" && failure.code !== 0) {
|
|
352
|
+
if (!output.includes("Database is not ready")) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
`Unprepared runtime did not report database readiness failure.\n${output}\n${await containerLogs(containers.unprepared)}`,
|
|
355
|
+
{ cause: error },
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
throw new Error("Unprepared runtime started successfully");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
for (const signal of forwardedSignals) {
|
|
367
|
+
const handler = (): void => {
|
|
368
|
+
abortController.abort(new Error(`Received ${signal}`));
|
|
369
|
+
};
|
|
370
|
+
signalHandlers.set(signal, handler);
|
|
371
|
+
process.once(signal, handler);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
await deploymentPhase("standalone", "build", async () =>
|
|
376
|
+
docker(
|
|
377
|
+
"build",
|
|
378
|
+
"--file",
|
|
379
|
+
"apps/web/Dockerfile",
|
|
380
|
+
"--target",
|
|
381
|
+
"standalone",
|
|
382
|
+
"--build-arg",
|
|
383
|
+
`DEPLOYMENT_BUILD_ID=${uniqueId}`,
|
|
384
|
+
"--tag",
|
|
385
|
+
standaloneImage,
|
|
386
|
+
".",
|
|
387
|
+
),
|
|
388
|
+
);
|
|
389
|
+
await deploymentPhase("runtime", "build", async () =>
|
|
390
|
+
docker(
|
|
391
|
+
"build",
|
|
392
|
+
"--file",
|
|
393
|
+
"apps/web/Dockerfile",
|
|
394
|
+
"--target",
|
|
395
|
+
"runtime",
|
|
396
|
+
"--build-arg",
|
|
397
|
+
`DEPLOYMENT_BUILD_ID=${uniqueId}`,
|
|
398
|
+
"--tag",
|
|
399
|
+
runtimeImage,
|
|
400
|
+
".",
|
|
401
|
+
),
|
|
402
|
+
);
|
|
403
|
+
for (const volume of Object.values(volumes)) {
|
|
404
|
+
await docker("volume", "create", volume);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
await runStartedDeployment(
|
|
408
|
+
"standalone",
|
|
409
|
+
"Standalone",
|
|
410
|
+
containers.standalone,
|
|
411
|
+
volumes.standalone,
|
|
412
|
+
standaloneImage,
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
await deploymentPhase("external-preparation", "prepare", async () =>
|
|
416
|
+
docker(
|
|
417
|
+
"run",
|
|
418
|
+
"--name",
|
|
419
|
+
containers.preparation,
|
|
420
|
+
"--volume",
|
|
421
|
+
`${volumes.prepared}:/data`,
|
|
422
|
+
standaloneImage,
|
|
423
|
+
"prepare-only",
|
|
424
|
+
),
|
|
425
|
+
);
|
|
426
|
+
await runStartedDeployment(
|
|
427
|
+
"runtime",
|
|
428
|
+
"Runtime",
|
|
429
|
+
containers.runtime,
|
|
430
|
+
volumes.prepared,
|
|
431
|
+
runtimeImage,
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
await deploymentPhase("fresh-runtime", "rejection", async () =>
|
|
435
|
+
assertUnpreparedRuntimeFails(),
|
|
436
|
+
);
|
|
437
|
+
} catch (error) {
|
|
438
|
+
console.error(error);
|
|
439
|
+
console.error(await deploymentLogs());
|
|
440
|
+
process.exitCode = 1;
|
|
441
|
+
} finally {
|
|
442
|
+
for (const signal of forwardedSignals) {
|
|
443
|
+
const handler = signalHandlers.get(signal);
|
|
444
|
+
if (handler) {
|
|
445
|
+
process.removeListener(signal, handler);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
await cleanup();
|
|
449
|
+
}
|
|
@@ -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,5 +1,38 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
2
3
|
import { createServer } from "node:net";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const databaseFile = fileURLToPath(
|
|
8
|
+
new URL("../node_modules/.tmp/e2e.sqlite", import.meta.url),
|
|
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
|
+
}
|
|
3
36
|
|
|
4
37
|
async function availablePort(): Promise<string> {
|
|
5
38
|
return await new Promise((resolve, reject) => {
|
|
@@ -29,34 +62,90 @@ async function availablePort(): Promise<string> {
|
|
|
29
62
|
});
|
|
30
63
|
}
|
|
31
64
|
|
|
32
|
-
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> {
|
|
33
89
|
return {
|
|
34
90
|
...process.env,
|
|
91
|
+
DATABASE_FILE: process.env.DATABASE_FILE ?? databaseFile,
|
|
35
92
|
PLAYWRIGHT_WEB_PORT:
|
|
36
93
|
process.env.PLAYWRIGHT_WEB_PORT ?? (await availablePort()),
|
|
37
94
|
};
|
|
38
95
|
}
|
|
39
96
|
|
|
40
|
-
const
|
|
97
|
+
const externalServiceUrl = externalBaseUrl();
|
|
98
|
+
const env = externalServiceUrl
|
|
99
|
+
? { ...process.env }
|
|
100
|
+
: await localPlaywrightEnv();
|
|
101
|
+
const localDatabaseFile = externalServiceUrl ? undefined : env.DATABASE_FILE;
|
|
41
102
|
delete env.NO_COLOR;
|
|
42
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
|
+
}
|
|
110
|
+
|
|
43
111
|
const command = process.platform === "win32" ? "playwright.cmd" : "playwright";
|
|
44
112
|
const child = spawn(command, ["test", ...process.argv.slice(2)], {
|
|
45
113
|
env,
|
|
46
114
|
stdio: "inherit",
|
|
47
115
|
});
|
|
48
116
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
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
|
+
}
|
|
53
124
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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 }));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (result.signal) {
|
|
135
|
+
console.error(`Playwright exited with signal ${result.signal}`);
|
|
57
136
|
process.exitCode = 1;
|
|
58
|
-
|
|
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);
|
|
59
146
|
}
|
|
60
147
|
|
|
61
|
-
|
|
62
|
-
});
|
|
148
|
+
if (localDatabaseFile) {
|
|
149
|
+
await rm(localDatabaseFile, { force: true });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -1,25 +1,36 @@
|
|
|
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();
|
|
9
9
|
|
|
10
|
-
export
|
|
10
|
+
export function createApp() {
|
|
11
|
+
try {
|
|
12
|
+
assertDatabaseReady(createDatabase());
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.error(error);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const app = new Hono();
|
|
19
|
+
|
|
20
|
+
app.route("/api", api);
|
|
11
21
|
|
|
12
|
-
app.
|
|
22
|
+
app.all("/_telefunc", async (c) => {
|
|
23
|
+
const response = await telefunc.serve({
|
|
24
|
+
request: c.req.raw,
|
|
25
|
+
context: {
|
|
26
|
+
db: createDatabase(),
|
|
27
|
+
},
|
|
28
|
+
});
|
|
13
29
|
|
|
14
|
-
|
|
15
|
-
const response = await telefunc.serve({
|
|
16
|
-
request: c.req.raw,
|
|
17
|
-
context: {
|
|
18
|
-
db: createDatabase(),
|
|
19
|
-
},
|
|
30
|
+
return response ?? c.notFound();
|
|
20
31
|
});
|
|
21
32
|
|
|
22
|
-
|
|
23
|
-
});
|
|
33
|
+
vike(app);
|
|
24
34
|
|
|
25
|
-
|
|
35
|
+
return app;
|
|
36
|
+
}
|