@ykdz/template 0.0.33 → 0.0.35

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 (24) hide show
  1. package/node_modules/@ykdz/template-builtin-presets/dist/src/foundation.js +2 -1
  2. package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/development-container.d.ts +1 -0
  3. package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/development-container.js +17 -0
  4. package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/vue.js +1 -1
  5. package/node_modules/@ykdz/template-builtin-presets/dist/src/vike-app/definition.js +8 -10
  6. package/node_modules/@ykdz/template-builtin-presets/dist/src/vue-app/definition.js +0 -1
  7. package/node_modules/@ykdz/template-builtin-presets/dist/src/vue-hono-app/definition.js +0 -1
  8. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/gitignore +1 -0
  9. package/node_modules/@ykdz/template-builtin-presets/templates/shared/devcontainer/github-cli.Dockerfile +9 -0
  10. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db/src/readiness.ts +1 -1
  11. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db/test/global-setup.ts +21 -0
  12. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db/tsconfig.json +6 -1
  13. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db/vitest.config.ts +7 -0
  14. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db-migrations/drizzle.config.ts +5 -1
  15. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/playwright.config.ts +102 -22
  16. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/test/playwright-teardown.ts +12 -0
  17. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/vite.config.ts +39 -1
  18. package/node_modules/@ykdz/template-builtin-presets/templates/vue-app/playwright.config.ts +48 -7
  19. package/node_modules/@ykdz/template-builtin-presets/templates/vue-hono-app/web/playwright.config.ts +59 -8
  20. package/package.json +3 -3
  21. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db-migrations/scripts/prepare-database.ts +0 -45
  22. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/scripts/run-playwright.ts +0 -151
  23. package/node_modules/@ykdz/template-builtin-presets/templates/vue-app/scripts/run-playwright.ts +0 -62
  24. package/node_modules/@ykdz/template-builtin-presets/templates/vue-hono-app/web/scripts/run-playwright.ts +0 -103
@@ -12,6 +12,7 @@ import { projectCheckWorkflowTemplateSource, projectCheckWorkflowTemplateReplace
12
12
  import { planExplicitProjectLinks } from "#template-core/project-linking-v2";
13
13
  import { resolveTemplateSource, } from "#template-core/renderer";
14
14
  import { rustBinDefinition } from "./rust-bin/definition.js";
15
+ import { githubCliDevelopmentContainerToolLayer } from "./shared/development-container.js";
15
16
  import { typescriptConfigContribution, typescriptConfigPackageDefinition, typescriptConfigPackageName, } from "./shared/typescript.js";
16
17
  import { vuePnpmDependencyOverrides } from "./shared/vue.js";
17
18
  import { templateSources } from "./template-sources.js";
@@ -499,7 +500,7 @@ function contributedDevcontainerComposition(options) {
499
500
  },
500
501
  ],
501
502
  },
502
- layers: options.layers,
503
+ layers: [githubCliDevelopmentContainerToolLayer(), ...options.layers],
503
504
  });
504
505
  return {
505
506
  operations: [
@@ -1,2 +1,3 @@
1
1
  import type { DevelopmentContainerToolLayer } from "#template-core/development-container-tool-layer";
2
+ export declare function githubCliDevelopmentContainerToolLayer(): DevelopmentContainerToolLayer;
2
3
  export declare function browserTestDevelopmentContainerToolLayer(): DevelopmentContainerToolLayer;
@@ -1,5 +1,22 @@
1
1
  import { loadTemplateDependencyCatalog } from "#template-core/dependency-catalog";
2
2
  import { templateSources } from "../template-sources.js";
3
+ export function githubCliDevelopmentContainerToolLayer() {
4
+ return {
5
+ identity: "github-cli",
6
+ dockerfile: {
7
+ source: templateSources.sharedDevcontainer,
8
+ from: "github-cli.Dockerfile",
9
+ },
10
+ requires: ["node-pnpm"],
11
+ probes: [
12
+ {
13
+ identity: "github-cli",
14
+ command: "gh",
15
+ args: ["--version"],
16
+ },
17
+ ],
18
+ };
19
+ }
3
20
  export function browserTestDevelopmentContainerToolLayer() {
4
21
  const playwrightTestVersion = loadTemplateDependencyCatalog()["@playwright/test"];
5
22
  if (playwrightTestVersion === undefined) {
@@ -32,7 +32,7 @@ export function vueApplicationScripts() {
32
32
  "lint:fix": "oxlint --format=unix --config ../../oxlint.config.ts . --fix",
33
33
  preview: "vite preview",
34
34
  test: "vitest run --reporter=agent --silent=passed-only",
35
- "test:e2e": "node --conditions=source scripts/run-playwright.ts",
35
+ "test:e2e": "playwright test",
36
36
  };
37
37
  }
38
38
  export function sharedVueSourceOperations(context, packagePath) {
@@ -82,19 +82,18 @@ function dockerClientDevelopmentContainerToolLayer() {
82
82
  };
83
83
  }
84
84
  function webScripts() {
85
- const prepareDatabase = "DATABASE_FILE=../../apps/web/data/app.sqlite pnpm --dir ../../packages/db-migrations run db:prepare:dev";
86
85
  return {
87
86
  build: "vike build",
88
87
  deployment: "node --conditions=source scripts/check-standalone-deployment.ts",
89
- dev: `${prepareDatabase} && vike dev`,
88
+ dev: "vike dev",
90
89
  "format:check": "oxfmt --list-different --config ../../oxfmt.config.ts .",
91
90
  "format:write": "oxfmt --write --config ../../oxfmt.config.ts .",
92
91
  lint: "shellcheck scripts/container-entrypoint.sh && oxlint --quiet --format=unix --type-aware --config ../../oxlint.config.ts .",
93
92
  "lint:fix": "oxlint --type-aware --format=unix --config ../../oxlint.config.ts . --fix",
94
- preview: `${prepareDatabase} && vike preview`,
93
+ preview: "vike preview",
95
94
  start: "node ./dist/server/index.mjs",
96
95
  test: "vitest run --reporter=agent --silent=passed-only --passWithNoTests",
97
- "test:e2e": "node --conditions=source scripts/run-playwright.ts",
96
+ "test:e2e": "playwright test",
98
97
  typecheck: "node --conditions=source scripts/run-vue-tsc.ts --build --noEmit --pretty false",
99
98
  };
100
99
  }
@@ -106,7 +105,7 @@ function databaseScripts() {
106
105
  "format:write": "oxfmt --write --config ../../oxfmt.config.ts .",
107
106
  lint: "oxlint --quiet --format=unix --config ../../oxlint.config.ts .",
108
107
  "lint:fix": "oxlint --format=unix --config ../../oxlint.config.ts . --fix",
109
- test: 'DATABASE_FILE="$(pwd)/node_modules/.tmp/test.sqlite" pnpm --dir ../db-migrations run db:prepare:test && DATABASE_FILE="$(pwd)/node_modules/.tmp/test.sqlite" vitest run --reporter=agent --silent=passed-only; status=$?; rm -f ./node_modules/.tmp/test.sqlite; exit $status',
108
+ test: "vitest run --reporter=agent --silent=passed-only",
110
109
  typecheck: "tsc -p tsconfig.json --noEmit --pretty false",
111
110
  };
112
111
  }
@@ -117,9 +116,7 @@ function migrationScripts(databasePackageName) {
117
116
  "db:generate": withDatabasePackage("drizzle-kit generate"),
118
117
  "db:migrate": withDatabasePackage("drizzle-kit migrate"),
119
118
  "db:prepare:deploy": "pnpm run db:migrate",
120
- "db:prepare:dev": `DATABASE_PACKAGE_NAME=${databasePackageName} node --conditions=source scripts/prepare-database.ts dev`,
121
- "db:prepare:test": `DATABASE_PACKAGE_NAME=${databasePackageName} node --conditions=source scripts/prepare-database.ts test`,
122
- "db:push": withDatabasePackage("mkdir -p data node_modules/.tmp && drizzle-kit push"),
119
+ "db:push": withDatabasePackage("drizzle-kit push"),
123
120
  "db:studio": withDatabasePackage("drizzle-kit studio"),
124
121
  "format:check": "oxfmt --list-different --config ../../oxfmt.config.ts .",
125
122
  "format:write": "oxfmt --write --config ../../oxfmt.config.ts .",
@@ -169,8 +166,8 @@ function webContribution(context) {
169
166
  "web/playwright.config.ts",
170
167
  "web/scripts/check-standalone-deployment.ts",
171
168
  "web/scripts/container-entrypoint.sh",
172
- "web/scripts/run-playwright.ts",
173
169
  "web/server/api.ts",
170
+ "web/test/playwright-teardown.ts",
174
171
  "web/test/e2e/app.spec.ts",
175
172
  "web/turbo.json",
176
173
  "web/types/env.d.ts",
@@ -334,6 +331,8 @@ function databaseContribution(context) {
334
331
  "db/tsconfig.json",
335
332
  "db/tsconfig.build.json",
336
333
  "db/scripts/seed-example.ts",
334
+ "db/vitest.config.ts",
335
+ "db/test/global-setup.ts",
337
336
  "db/src/db.ts",
338
337
  "db/src/index.ts",
339
338
  "db/src/queries/todos.ts",
@@ -396,7 +395,6 @@ function migrationsContribution(context) {
396
395
  "db-migrations/tsconfig.build.json",
397
396
  "db-migrations/drizzle/migrations/20260709120325_old_captain_flint/migration.sql",
398
397
  "db-migrations/drizzle/migrations/20260709120325_old_captain_flint/snapshot.json",
399
- "db-migrations/scripts/prepare-database.ts",
400
398
  "db-migrations/turbo.json",
401
399
  ];
402
400
  return {
@@ -20,7 +20,6 @@ function appContribution(options) {
20
20
  "vite.config.ts",
21
21
  "vitest.config.ts",
22
22
  "turbo.json",
23
- "scripts/run-playwright.ts",
24
23
  "src/App.vue",
25
24
  "test/e2e/app.spec.ts",
26
25
  ];
@@ -115,7 +115,6 @@ function webContribution(context) {
115
115
  "vite.config.ts",
116
116
  "vitest.config.ts",
117
117
  "turbo.json",
118
- "scripts/run-playwright.ts",
119
118
  "src/api.ts",
120
119
  "src/App.vue",
121
120
  "test/e2e/app.spec.ts",
@@ -6,5 +6,6 @@ target/
6
6
  coverage/
7
7
  .env
8
8
  .template/
9
+ data/
9
10
  playwright-report/
10
11
  test-results/
@@ -0,0 +1,9 @@
1
+ RUN apt-get update \
2
+ && apt-get install -y --no-install-recommends curl \
3
+ && install -m 0755 -d /etc/apt/keyrings \
4
+ && curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg -o /etc/apt/keyrings/githubcli-archive-keyring.gpg \
5
+ && chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
6
+ && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list \
7
+ && apt-get update \
8
+ && apt-get install -y --no-install-recommends gh \
9
+ && rm -rf /var/lib/apt/lists/*
@@ -13,7 +13,7 @@ export function assertDatabaseReady(db: Database) {
13
13
  throw new Error(
14
14
  [
15
15
  "Database is not ready.",
16
- "For local development, run `pnpm --dir packages/db-migrations run db:prepare:dev`.",
16
+ "For local development, run `pnpm --dir apps/web dev`.",
17
17
  "For deployment, run `pnpm --dir packages/db-migrations run db:prepare:deploy` before starting the app.",
18
18
  ].join(" "),
19
19
  { cause },
@@ -0,0 +1,21 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { rmSync } from "node:fs";
3
+ import path from "node:path";
4
+
5
+ const packageRoot = path.resolve(import.meta.dirname, "..");
6
+ const migrationsRoot = path.resolve(packageRoot, "../db-migrations");
7
+ const databaseFile = path.join(packageRoot, "node_modules/.tmp/test.sqlite");
8
+
9
+ export function setup(): void {
10
+ rmSync(databaseFile, { force: true });
11
+ process.env.DATABASE_FILE = databaseFile;
12
+ execFileSync("pnpm", ["run", "db:push"], {
13
+ cwd: migrationsRoot,
14
+ env: process.env,
15
+ stdio: "inherit",
16
+ });
17
+ }
18
+
19
+ export function teardown(): void {
20
+ rmSync(databaseFile, { force: true });
21
+ }
@@ -13,5 +13,10 @@
13
13
  "target": "es2023",
14
14
  "types": ["node"]
15
15
  },
16
- "include": ["src/**/*.ts", "scripts/**/*.ts", "test/**/*.ts"]
16
+ "include": [
17
+ "src/**/*.ts",
18
+ "scripts/**/*.ts",
19
+ "test/**/*.ts",
20
+ "vitest.config.ts"
21
+ ]
17
22
  }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globalSetup: ["./test/global-setup.ts"],
6
+ },
7
+ });
@@ -1,3 +1,5 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import path from "node:path";
1
3
  import { fileURLToPath } from "node:url";
2
4
 
3
5
  import { defineConfig } from "drizzle-kit";
@@ -6,12 +8,14 @@ const databasePackageName = process.env.DATABASE_PACKAGE_NAME ?? "@database";
6
8
  const schemaFile = fileURLToPath(
7
9
  import.meta.resolve(`${databasePackageName}/schema`),
8
10
  );
11
+ const databaseFile = process.env.DATABASE_FILE ?? "./data/app.sqlite";
12
+ mkdirSync(path.dirname(databaseFile), { recursive: true });
9
13
 
10
14
  export default defineConfig({
11
15
  dialect: "sqlite",
12
16
  schema: schemaFile,
13
17
  out: "./drizzle/migrations",
14
18
  dbCredentials: {
15
- url: process.env.DATABASE_FILE ?? "./data/app.sqlite",
19
+ url: databaseFile,
16
20
  },
17
21
  });
@@ -1,6 +1,18 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { rmSync } from "node:fs";
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { createServer } from "node:net";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
1
8
  import { defineConfig, devices } from "@playwright/test";
2
9
 
3
10
  const externalBaseUrlName = "PLAYWRIGHT_EXTERNAL_BASE_URL";
11
+ const externalReadinessTimeoutMs = 30_000;
12
+ const webRoot = path.dirname(fileURLToPath(import.meta.url));
13
+ const migrationsRoot = path.resolve(webRoot, "../../packages/db-migrations");
14
+ const databaseFile = path.join(webRoot, "node_modules/.tmp/e2e.sqlite");
15
+ const stateFile = path.join(webRoot, "node_modules/.tmp/playwright-port");
4
16
 
5
17
  function externalBaseUrl(): string | undefined {
6
18
  if (!Object.hasOwn(process.env, externalBaseUrlName)) {
@@ -26,31 +38,98 @@ function externalBaseUrl(): string | undefined {
26
38
  return url.toString();
27
39
  }
28
40
 
29
- function requiredPort(name: string): number {
30
- return Number(requiredEnv(name));
41
+ async function availablePort(): Promise<string> {
42
+ return await new Promise((resolve, reject) => {
43
+ const server = createServer();
44
+
45
+ server.once("error", reject);
46
+ server.listen(0, "127.0.0.1", () => {
47
+ const address = server.address();
48
+
49
+ if (typeof address !== "object" || address === null) {
50
+ server.close(() => {
51
+ reject(new Error("Could not allocate a Playwright web port"));
52
+ });
53
+ return;
54
+ }
55
+
56
+ const port = String(address.port);
57
+ server.close((error) => {
58
+ if (error) {
59
+ reject(error);
60
+ return;
61
+ }
62
+
63
+ resolve(port);
64
+ });
65
+ });
66
+ });
31
67
  }
32
68
 
33
- function requiredEnv(name: string): string {
34
- const value = process.env[name];
69
+ async function awaitExternalService(baseUrl: string): Promise<void> {
70
+ const deadline = Date.now() + externalReadinessTimeoutMs;
35
71
 
36
- if (!value) {
37
- throw new Error(`${name} must be set by scripts/run-playwright.ts`);
72
+ while (Date.now() < deadline) {
73
+ try {
74
+ const response = await fetch(baseUrl, {
75
+ signal: AbortSignal.timeout(2_000),
76
+ });
77
+ if (response.ok) {
78
+ return;
79
+ }
80
+ } catch {
81
+ // The externally managed service may still be starting.
82
+ }
83
+
84
+ await new Promise((resolve) => setTimeout(resolve, 250));
38
85
  }
39
86
 
40
- return value;
87
+ throw new Error(
88
+ `External Playwright service at ${baseUrl} was not ready within ${externalReadinessTimeoutMs}ms`,
89
+ );
90
+ }
91
+
92
+ async function sharedPort(envName: string): Promise<{
93
+ readonly port: string;
94
+ readonly prepared: boolean;
95
+ }> {
96
+ const envPort = process.env[envName];
97
+ if (envPort !== undefined) {
98
+ return { port: envPort, prepared: false };
99
+ }
100
+ try {
101
+ return { port: await readFile(stateFile, "utf8"), prepared: false };
102
+ } catch {
103
+ const port = await availablePort();
104
+ await mkdir(path.dirname(stateFile), { recursive: true });
105
+ await writeFile(stateFile, port, "utf8");
106
+ return { port, prepared: true };
107
+ }
108
+ }
109
+
110
+ function prepareLocalDatabase(): void {
111
+ rmSync(databaseFile, { force: true });
112
+ execFileSync("pnpm", ["run", "db:push"], {
113
+ cwd: migrationsRoot,
114
+ env: { ...process.env, DATABASE_FILE: databaseFile },
115
+ stdio: "inherit",
116
+ });
41
117
  }
42
118
 
43
119
  const externalServiceUrl = externalBaseUrl();
44
- const previewPort = externalServiceUrl
45
- ? undefined
46
- : requiredPort("PLAYWRIGHT_WEB_PORT");
47
- const previewUrl = externalServiceUrl ?? `http://127.0.0.1:${previewPort}`;
48
- const databaseFile = externalServiceUrl
49
- ? undefined
50
- : requiredEnv("DATABASE_FILE");
51
-
52
- function shellValue(value: string): string {
53
- return `'${value.replaceAll("'", "'\\''")}'`;
120
+ let previewUrl: string;
121
+
122
+ if (externalServiceUrl === undefined) {
123
+ const { port, prepared } = await sharedPort("PLAYWRIGHT_WEB_PORT");
124
+ previewUrl = `http://127.0.0.1:${port}`;
125
+ process.env.DATABASE_FILE = databaseFile;
126
+ process.env.PORT = port;
127
+ if (prepared) {
128
+ prepareLocalDatabase();
129
+ }
130
+ } else {
131
+ previewUrl = externalServiceUrl;
132
+ await awaitExternalService(externalServiceUrl);
54
133
  }
55
134
 
56
135
  export default defineConfig({
@@ -61,16 +140,17 @@ export default defineConfig({
61
140
  trace: "retain-on-failure",
62
141
  },
63
142
  reporter: [["list"], ["html"]],
64
- ...(externalServiceUrl
65
- ? {}
66
- : {
143
+ globalTeardown: "./test/playwright-teardown.ts",
144
+ ...(externalServiceUrl === undefined
145
+ ? {
67
146
  webServer: {
68
- command: `DATABASE_FILE=${shellValue(databaseFile!)} pnpm --dir ../../packages/db-migrations run db:prepare:test && PORT=${previewPort} node dist/server/index.mjs`,
147
+ command: "node dist/server/index.mjs",
69
148
  reuseExistingServer: !process.env.CI,
70
149
  timeout: 60_000,
71
150
  url: previewUrl,
72
151
  },
73
- }),
152
+ }
153
+ : {}),
74
154
  projects: [
75
155
  {
76
156
  name: "chromium",
@@ -0,0 +1,12 @@
1
+ import { rmSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const webRoot = path.dirname(fileURLToPath(import.meta.url));
6
+
7
+ export default function teardown(): void {
8
+ rmSync(path.join(webRoot, "node_modules/.tmp/e2e.sqlite"), { force: true });
9
+ rmSync(path.join(webRoot, "node_modules/.tmp/playwright-port"), {
10
+ force: true,
11
+ });
12
+ }
@@ -1,12 +1,50 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
1
5
  import tailwindcss from "@tailwindcss/vite";
2
6
  import vue from "@vitejs/plugin-vue";
3
7
  import telefunc from "telefunc/vite";
4
8
  import vike from "vike/plugin";
5
9
  import { defineConfig, type PluginOption } from "vite";
6
10
 
11
+ const webRoot = path.dirname(fileURLToPath(import.meta.url));
12
+ const migrationsRoot = path.resolve(webRoot, "../../packages/db-migrations");
13
+ const databaseRoot = path.resolve(webRoot, "../../packages/db");
14
+ const databaseFile = path.join(webRoot, "data/app.sqlite");
15
+
7
16
  // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- telefunc/vite currently types its plugin factory as any.
8
17
  const telefuncPlugin = telefunc() as PluginOption;
9
18
 
19
+ async function prepareDatabase(): Promise<void> {
20
+ const env = { ...process.env, DATABASE_FILE: databaseFile };
21
+ process.env.DATABASE_FILE = databaseFile;
22
+ execFileSync("pnpm", ["run", "db:push"], {
23
+ cwd: migrationsRoot,
24
+ env,
25
+ stdio: "inherit",
26
+ });
27
+ execFileSync("pnpm", ["run", "db:seed:example"], {
28
+ cwd: databaseRoot,
29
+ env,
30
+ stdio: "inherit",
31
+ });
32
+ }
33
+
34
+ function databasePreparationPlugin(): PluginOption {
35
+ return {
36
+ name: "database-preparation",
37
+ configureServer: () => prepareDatabase(),
38
+ configurePreviewServer: () => prepareDatabase(),
39
+ };
40
+ }
41
+
10
42
  export default defineConfig({
11
- plugins: [vike(), telefuncPlugin, vue(), tailwindcss()],
43
+ plugins: [
44
+ vike(),
45
+ telefuncPlugin,
46
+ vue(),
47
+ tailwindcss(),
48
+ databasePreparationPlugin(),
49
+ ],
12
50
  });
@@ -1,16 +1,57 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { createServer } from "node:net";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
1
6
  import { defineConfig, devices } from "@playwright/test";
2
7
 
3
- function requiredPort(name: string): number {
4
- const value = process.env[name];
8
+ const webRoot = path.dirname(fileURLToPath(import.meta.url));
9
+ const stateFile = path.join(webRoot, "node_modules/.tmp/playwright-port");
5
10
 
6
- if (!value) {
7
- throw new Error(`${name} must be set by scripts/run-playwright.ts`);
8
- }
11
+ async function availablePort(): Promise<string> {
12
+ return await new Promise((resolve, reject) => {
13
+ const server = createServer();
14
+
15
+ server.once("error", reject);
16
+ server.listen(0, "127.0.0.1", () => {
17
+ const address = server.address();
18
+
19
+ if (typeof address !== "object" || address === null) {
20
+ server.close(() => {
21
+ reject(new Error("Could not allocate a Playwright web port"));
22
+ });
23
+ return;
24
+ }
25
+
26
+ const port = String(address.port);
27
+ server.close((error) => {
28
+ if (error) {
29
+ reject(error);
30
+ return;
31
+ }
9
32
 
10
- return Number(value);
33
+ resolve(port);
34
+ });
35
+ });
36
+ });
37
+ }
38
+
39
+ async function sharedPort(envName: string): Promise<string> {
40
+ const envPort = process.env[envName];
41
+ if (envPort !== undefined) {
42
+ return envPort;
43
+ }
44
+ try {
45
+ return await readFile(stateFile, "utf8");
46
+ } catch {
47
+ const port = await availablePort();
48
+ await mkdir(path.dirname(stateFile), { recursive: true });
49
+ await writeFile(stateFile, port, "utf8");
50
+ return port;
51
+ }
11
52
  }
12
53
 
13
- const previewPort = requiredPort("PLAYWRIGHT_WEB_PORT");
54
+ const previewPort = await sharedPort("PLAYWRIGHT_WEB_PORT");
14
55
  const previewUrl = `http://127.0.0.1:${previewPort}`;
15
56
 
16
57
  export default defineConfig({
@@ -1,17 +1,68 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { createServer } from "node:net";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
1
6
  import { defineConfig, devices } from "@playwright/test";
2
7
 
3
- function requiredPort(name: string): number {
4
- const value = process.env[name];
8
+ const webRoot = path.dirname(fileURLToPath(import.meta.url));
9
+ const stateFile = path.join(webRoot, "node_modules/.tmp/playwright-ports");
5
10
 
6
- if (!value) {
7
- throw new Error(`${name} must be set by scripts/run-playwright.ts`);
8
- }
11
+ async function availablePort(): Promise<string> {
12
+ return await new Promise((resolve, reject) => {
13
+ const server = createServer();
14
+
15
+ server.once("error", reject);
16
+ server.listen(0, "127.0.0.1", () => {
17
+ const address = server.address();
18
+
19
+ if (typeof address !== "object" || address === null) {
20
+ server.close(() => {
21
+ reject(new Error("Could not allocate a Playwright web port"));
22
+ });
23
+ return;
24
+ }
25
+
26
+ const port = String(address.port);
27
+ server.close((error) => {
28
+ if (error) {
29
+ reject(error);
30
+ return;
31
+ }
9
32
 
10
- return Number(value);
33
+ resolve(port);
34
+ });
35
+ });
36
+ });
37
+ }
38
+
39
+ async function sharedPorts(): Promise<{
40
+ readonly api: string;
41
+ readonly web: string;
42
+ }> {
43
+ const envApi = process.env.PLAYWRIGHT_API_PORT;
44
+ const envWeb = process.env.PLAYWRIGHT_WEB_PORT;
45
+ if (envApi !== undefined && envWeb !== undefined) {
46
+ return { api: envApi, web: envWeb };
47
+ }
48
+ try {
49
+ const parsed = JSON.parse(await readFile(stateFile, "utf8")) as {
50
+ readonly api: string;
51
+ readonly web: string;
52
+ };
53
+ return parsed;
54
+ } catch {
55
+ const ports = {
56
+ api: envApi ?? (await availablePort()),
57
+ web: envWeb ?? (await availablePort()),
58
+ };
59
+ await mkdir(path.dirname(stateFile), { recursive: true });
60
+ await writeFile(stateFile, JSON.stringify(ports), "utf8");
61
+ return ports;
62
+ }
11
63
  }
12
64
 
13
- const apiPort = requiredPort("PLAYWRIGHT_API_PORT");
14
- const webPort = requiredPort("PLAYWRIGHT_WEB_PORT");
65
+ const { api: apiPort, web: webPort } = await sharedPorts();
15
66
  const apiBaseUrl = `http://127.0.0.1:${apiPort}`;
16
67
  const apiHealthUrl = `${apiBaseUrl}/api/health`;
17
68
  const webBaseUrl = `http://127.0.0.1:${webPort}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ykdz/template",
3
- "version": "0.0.33",
3
+ "version": "0.0.35",
4
4
  "private": false,
5
5
  "description": "Project template generator for YKDZ starter projects.",
6
6
  "keywords": [
@@ -60,8 +60,8 @@
60
60
  "semver": "^7.8.5",
61
61
  "typescript": "npm:typescript@^6.0.3",
62
62
  "valibot": "^1.4.2",
63
- "@ykdz/template-builtin-presets": "0.0.0",
64
- "@ykdz/template-core": "0.0.0"
63
+ "@ykdz/template-core": "0.0.0",
64
+ "@ykdz/template-builtin-presets": "0.0.0"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@types/node": "^24.13.3",
@@ -1,45 +0,0 @@
1
- import { execFileSync } from "node:child_process";
2
- import { mkdirSync, rmSync } from "node:fs";
3
- import path from "node:path";
4
-
5
- const mode = process.argv[2];
6
-
7
- if (mode !== "dev" && mode !== "test") {
8
- throw new Error("Usage: node scripts/prepare-database.ts <dev|test>");
9
- }
10
-
11
- const packageDirectory = process.cwd();
12
- const databaseFile = process.env.DATABASE_FILE
13
- ? path.resolve(
14
- process.env.INIT_CWD ?? packageDirectory,
15
- process.env.DATABASE_FILE,
16
- )
17
- : path.resolve(packageDirectory, "../../apps/web/data/app.sqlite");
18
- const environment = { ...process.env, DATABASE_FILE: databaseFile };
19
-
20
- mkdirSync(path.dirname(databaseFile), { recursive: true });
21
- if (mode === "test") {
22
- rmSync(databaseFile, { force: true });
23
- }
24
-
25
- execFileSync("pnpm", ["run", "db:push"], {
26
- cwd: packageDirectory,
27
- env: environment,
28
- stdio: "inherit",
29
- });
30
- const databasePackageName = process.env.DATABASE_PACKAGE_NAME;
31
- if (!databasePackageName) {
32
- throw new Error(
33
- "DATABASE_PACKAGE_NAME must name the database Package Boundary",
34
- );
35
- }
36
-
37
- execFileSync(
38
- "pnpm",
39
- ["--filter", databasePackageName, "run", "db:seed:example"],
40
- {
41
- cwd: packageDirectory,
42
- env: environment,
43
- stdio: "inherit",
44
- },
45
- );
@@ -1,151 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { mkdir, rm } from "node:fs/promises";
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
- }
36
-
37
- async function availablePort(): Promise<string> {
38
- return await new Promise((resolve, reject) => {
39
- const server = createServer();
40
-
41
- server.once("error", reject);
42
- server.listen(0, "127.0.0.1", () => {
43
- const address = server.address();
44
-
45
- if (typeof address !== "object" || address === null) {
46
- server.close(() => {
47
- reject(new Error("Could not allocate a Playwright web port"));
48
- });
49
- return;
50
- }
51
-
52
- const port = String(address.port);
53
- server.close((error) => {
54
- if (error) {
55
- reject(error);
56
- return;
57
- }
58
-
59
- resolve(port);
60
- });
61
- });
62
- });
63
- }
64
-
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> {
89
- return {
90
- ...process.env,
91
- DATABASE_FILE: process.env.DATABASE_FILE ?? databaseFile,
92
- PLAYWRIGHT_WEB_PORT:
93
- process.env.PLAYWRIGHT_WEB_PORT ?? (await availablePort()),
94
- };
95
- }
96
-
97
- const externalServiceUrl = externalBaseUrl();
98
- const env = externalServiceUrl
99
- ? { ...process.env }
100
- : await localPlaywrightEnv();
101
- const localDatabaseFile = externalServiceUrl ? undefined : env.DATABASE_FILE;
102
- delete env.NO_COLOR;
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
-
111
- const command = process.platform === "win32" ? "playwright.cmd" : "playwright";
112
- const child = spawn(command, ["test", ...process.argv.slice(2)], {
113
- env,
114
- stdio: "inherit",
115
- });
116
-
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
- }
124
-
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}`);
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
- }
151
- }
@@ -1,62 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { createServer } from "node:net";
3
-
4
- async function availablePort(): Promise<string> {
5
- return await new Promise((resolve, reject) => {
6
- const server = createServer();
7
-
8
- server.once("error", reject);
9
- server.listen(0, "127.0.0.1", () => {
10
- const address = server.address();
11
-
12
- if (typeof address !== "object" || address === null) {
13
- server.close(() => {
14
- reject(new Error("Could not allocate a Playwright web port"));
15
- });
16
- return;
17
- }
18
-
19
- const port = String(address.port);
20
- server.close((error) => {
21
- if (error) {
22
- reject(error);
23
- return;
24
- }
25
-
26
- resolve(port);
27
- });
28
- });
29
- });
30
- }
31
-
32
- async function playwrightEnv(): Promise<NodeJS.ProcessEnv> {
33
- return {
34
- ...process.env,
35
- PLAYWRIGHT_WEB_PORT:
36
- process.env.PLAYWRIGHT_WEB_PORT ?? (await availablePort()),
37
- };
38
- }
39
-
40
- const env = await playwrightEnv();
41
- delete env.NO_COLOR;
42
-
43
- const command = process.platform === "win32" ? "playwright.cmd" : "playwright";
44
- const child = spawn(command, ["test", ...process.argv.slice(2)], {
45
- env,
46
- stdio: "inherit",
47
- });
48
-
49
- child.once("error", (error: Error) => {
50
- console.error(error);
51
- process.exitCode = 1;
52
- });
53
-
54
- child.once("exit", (code: number | null, signal: NodeJS.Signals | null) => {
55
- if (signal) {
56
- console.error(`Playwright exited with signal ${signal}`);
57
- process.exitCode = 1;
58
- return;
59
- }
60
-
61
- process.exitCode = code ?? 1;
62
- });
@@ -1,103 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { createServer } from "node:net";
3
-
4
- async function listenOnAvailablePort(): Promise<{
5
- readonly port: string;
6
- readonly server: ReturnType<typeof createServer>;
7
- }> {
8
- return await new Promise((resolve, reject) => {
9
- const server = createServer();
10
-
11
- server.once("error", reject);
12
- server.listen(0, "127.0.0.1", () => {
13
- const address = server.address();
14
-
15
- if (typeof address !== "object" || address === null) {
16
- server.close(() => {
17
- reject(new Error("Could not allocate a Playwright server port"));
18
- });
19
- return;
20
- }
21
-
22
- resolve({ port: String(address.port), server });
23
- });
24
- });
25
- }
26
-
27
- async function availablePorts(count: number): Promise<string[]> {
28
- const leases: Array<{
29
- readonly port: string;
30
- readonly server: ReturnType<typeof createServer>;
31
- }> = [];
32
-
33
- try {
34
- for (let index = 0; index < count; index += 1) {
35
- leases.push(await listenOnAvailablePort());
36
- }
37
-
38
- return leases.map((lease) => lease.port);
39
- } finally {
40
- await Promise.all(
41
- leases.map(
42
- (lease) =>
43
- new Promise<void>((resolve, reject) => {
44
- lease.server.close((error) => {
45
- if (error) {
46
- reject(error);
47
- return;
48
- }
49
-
50
- resolve();
51
- });
52
- }),
53
- ),
54
- );
55
- }
56
- }
57
-
58
- async function playwrightEnv(): Promise<NodeJS.ProcessEnv> {
59
- const missingPortNames = [
60
- process.env.PLAYWRIGHT_API_PORT === undefined
61
- ? "PLAYWRIGHT_API_PORT"
62
- : undefined,
63
- process.env.PLAYWRIGHT_WEB_PORT === undefined
64
- ? "PLAYWRIGHT_WEB_PORT"
65
- : undefined,
66
- ].filter((name) => name !== undefined);
67
- const ports = await availablePorts(missingPortNames.length);
68
- const allocatedPorts = Object.fromEntries(
69
- missingPortNames.map((name, index) => [name, ports[index]]),
70
- );
71
-
72
- return {
73
- ...process.env,
74
- PLAYWRIGHT_API_PORT:
75
- process.env.PLAYWRIGHT_API_PORT ?? allocatedPorts.PLAYWRIGHT_API_PORT,
76
- PLAYWRIGHT_WEB_PORT:
77
- process.env.PLAYWRIGHT_WEB_PORT ?? allocatedPorts.PLAYWRIGHT_WEB_PORT,
78
- };
79
- }
80
-
81
- const env = await playwrightEnv();
82
- delete env.NO_COLOR;
83
-
84
- const command = process.platform === "win32" ? "playwright.cmd" : "playwright";
85
- const child = spawn(command, ["test", ...process.argv.slice(2)], {
86
- env,
87
- stdio: "inherit",
88
- });
89
-
90
- child.once("error", (error: Error) => {
91
- console.error(error);
92
- process.exitCode = 1;
93
- });
94
-
95
- child.once("exit", (code: number | null, signal: NodeJS.Signals | null) => {
96
- if (signal) {
97
- console.error(`Playwright exited with signal ${signal}`);
98
- process.exitCode = 1;
99
- return;
100
- }
101
-
102
- process.exitCode = code ?? 1;
103
- });