@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.
Files changed (40) 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 +11 -0
  3. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/.github/dependabot.yml +4 -0
  4. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/.github/workflows/check.yml +2 -0
  5. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/db/drizzle/migrations/20260709120325_old_captain_flint/migration.sql +6 -0
  6. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/db/drizzle/migrations/20260709120325_old_captain_flint/snapshot.json +67 -0
  7. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/db/scripts/seed-example.ts +7 -0
  8. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/db/src/db.ts +2 -1
  9. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/db/src/index.ts +1 -0
  10. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/db/src/queries/todos.ts +17 -1
  11. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/db/src/readiness.ts +22 -0
  12. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/db/src/schema.ts +1 -1
  13. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/db/src/seed/example.ts +23 -0
  14. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/db/test/todos.test.ts +2 -0
  15. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/+server.ts +2 -1
  16. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/Dockerfile +83 -0
  17. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/Dockerfile.dockerignore +14 -0
  18. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/pages/index/+Page.telefunc.ts +1 -2
  19. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/pages/index/+Page.vue +6 -2
  20. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/playwright.config.ts +50 -14
  21. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/scripts/check-standalone-deployment.ts +449 -0
  22. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/scripts/container-entrypoint.sh +52 -0
  23. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/scripts/prepare-database.sh +6 -0
  24. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/scripts/run-playwright.ts +101 -12
  25. package/node_modules/@ykdz/template-builtin-source/templates/vike-app/web/server/app.ts +23 -12
  26. package/node_modules/@ykdz/template-core/dist/dependency-catalog.js +13 -0
  27. package/node_modules/@ykdz/template-core/dist/generated-scenarios.d.ts +3 -2
  28. package/node_modules/@ykdz/template-core/dist/generated-scenarios.js +99 -13
  29. package/node_modules/@ykdz/template-core/dist/module-graph.d.ts +12 -0
  30. package/node_modules/@ykdz/template-core/dist/module-graph.js +18 -0
  31. package/node_modules/@ykdz/template-core/dist/pnpm-workspace.yaml +7 -4
  32. package/node_modules/@ykdz/template-core/dist/project-github.d.ts +1 -0
  33. package/node_modules/@ykdz/template-core/dist/project-github.js +18 -5
  34. package/node_modules/@ykdz/template-core/dist/projection-capabilities.js +57 -7
  35. package/node_modules/@ykdz/template-core/dist/renderer.js +4 -1
  36. package/node_modules/@ykdz/template-core/dist/template-boundary-check.js +1 -1
  37. package/node_modules/@ykdz/template-core/package.json +2 -1
  38. package/node_modules/@ykdz/template-shared/dist/index.d.ts +6 -6
  39. package/node_modules/@ykdz/template-shared/package.json +1 -0
  40. 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
@@ -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,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 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> {
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 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;
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
- child.once("error", (error: Error) => {
50
- console.error(error);
51
- process.exitCode = 1;
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
- child.once("exit", (code: number | null, signal: NodeJS.Signals | null) => {
55
- if (signal) {
56
- console.error(`Playwright exited with signal ${signal}`);
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
- return;
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
- process.exitCode = code ?? 1;
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 const app = new Hono();
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.route("/api", api);
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
- app.all("/_telefunc", async (c) => {
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
- return response ?? c.notFound();
23
- });
33
+ vike(app);
24
34
 
25
- vike(app);
35
+ return app;
36
+ }