@webstir-io/webstir-backend 0.1.15 → 0.1.16

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 (123) hide show
  1. package/README.md +106 -79
  2. package/dist/add.d.ts +59 -0
  3. package/dist/add.js +626 -0
  4. package/dist/build/artifacts.d.ts +115 -1
  5. package/dist/build/artifacts.js +4 -4
  6. package/dist/build/entries.js +1 -1
  7. package/dist/build/pipeline.d.ts +33 -1
  8. package/dist/build/pipeline.js +307 -65
  9. package/dist/cache/diff.js +9 -8
  10. package/dist/cache/reporters.js +1 -1
  11. package/dist/deploy-cli.d.ts +2 -0
  12. package/dist/deploy-cli.js +86 -0
  13. package/dist/diagnostics/summary.js +2 -2
  14. package/dist/index.d.ts +6 -0
  15. package/dist/index.js +4 -0
  16. package/dist/manifest/pipeline.js +103 -32
  17. package/dist/provider.js +35 -17
  18. package/dist/runtime/bun.d.ts +51 -0
  19. package/dist/runtime/bun.js +499 -0
  20. package/dist/runtime/core.d.ts +141 -0
  21. package/dist/runtime/core.js +316 -0
  22. package/dist/runtime/deploy-backend.d.ts +20 -0
  23. package/dist/runtime/deploy-backend.js +175 -0
  24. package/dist/runtime/deploy-shared.d.ts +43 -0
  25. package/dist/runtime/deploy-shared.js +75 -0
  26. package/dist/runtime/deploy-static.d.ts +2 -0
  27. package/dist/runtime/deploy-static.js +161 -0
  28. package/dist/runtime/deploy.d.ts +3 -0
  29. package/dist/runtime/deploy.js +91 -0
  30. package/dist/runtime/forms.d.ts +73 -0
  31. package/dist/runtime/forms.js +236 -0
  32. package/dist/runtime/request-hooks.d.ts +47 -0
  33. package/dist/runtime/request-hooks.js +102 -0
  34. package/dist/runtime/session-metadata.d.ts +13 -0
  35. package/dist/runtime/session-metadata.js +98 -0
  36. package/dist/runtime/session-runtime.d.ts +28 -0
  37. package/dist/runtime/session-runtime.js +180 -0
  38. package/dist/runtime/session.d.ts +83 -0
  39. package/dist/runtime/session.js +396 -0
  40. package/dist/runtime/views.d.ts +74 -0
  41. package/dist/runtime/views.js +221 -0
  42. package/dist/scaffold/assets.js +25 -21
  43. package/dist/testing/context.js +1 -1
  44. package/dist/testing/index.d.ts +1 -1
  45. package/dist/testing/index.js +100 -56
  46. package/dist/utils/bun.d.ts +2 -0
  47. package/dist/utils/bun.js +13 -0
  48. package/dist/watch.d.ts +13 -1
  49. package/dist/watch.js +345 -97
  50. package/dist/workspace.d.ts +8 -0
  51. package/dist/workspace.js +44 -3
  52. package/package.json +49 -14
  53. package/scripts/publish.sh +2 -92
  54. package/scripts/smoke.mjs +282 -107
  55. package/scripts/update-contract.sh +12 -10
  56. package/src/add.ts +964 -0
  57. package/src/build/artifacts.ts +49 -46
  58. package/src/build/entries.ts +12 -12
  59. package/src/build/pipeline.ts +779 -403
  60. package/src/cache/diff.ts +111 -105
  61. package/src/cache/reporters.ts +26 -26
  62. package/src/deploy-cli.ts +111 -0
  63. package/src/diagnostics/summary.ts +28 -22
  64. package/src/index.ts +11 -0
  65. package/src/manifest/pipeline.ts +328 -215
  66. package/src/provider.ts +115 -98
  67. package/src/runtime/bun.ts +793 -0
  68. package/src/runtime/core.ts +598 -0
  69. package/src/runtime/deploy-backend.ts +239 -0
  70. package/src/runtime/deploy-shared.ts +136 -0
  71. package/src/runtime/deploy-static.ts +191 -0
  72. package/src/runtime/deploy.ts +143 -0
  73. package/src/runtime/forms.ts +364 -0
  74. package/src/runtime/request-hooks.ts +165 -0
  75. package/src/runtime/session-metadata.ts +135 -0
  76. package/src/runtime/session-runtime.ts +267 -0
  77. package/src/runtime/session.ts +642 -0
  78. package/src/runtime/views.ts +385 -0
  79. package/src/scaffold/assets.ts +77 -73
  80. package/src/testing/context.js +8 -9
  81. package/src/testing/context.ts +9 -9
  82. package/src/testing/index.d.ts +14 -3
  83. package/src/testing/index.js +254 -175
  84. package/src/testing/index.ts +298 -195
  85. package/src/testing/types.d.ts +18 -19
  86. package/src/testing/types.ts +18 -18
  87. package/src/utils/bun.ts +26 -0
  88. package/src/watch.ts +503 -99
  89. package/src/workspace.ts +59 -3
  90. package/templates/backend/.env.example +15 -0
  91. package/templates/backend/auth/adapter.ts +335 -36
  92. package/templates/backend/db/connection.ts +190 -65
  93. package/templates/backend/db/migrate.ts +149 -43
  94. package/templates/backend/db/types.d.ts +1 -1
  95. package/templates/backend/env.ts +132 -20
  96. package/templates/backend/functions/hello/index.ts +1 -2
  97. package/templates/backend/index.ts +15 -508
  98. package/templates/backend/jobs/nightly/index.ts +1 -1
  99. package/templates/backend/jobs/runtime.ts +24 -11
  100. package/templates/backend/jobs/scheduler.ts +208 -46
  101. package/templates/backend/module.ts +227 -13
  102. package/templates/backend/observability/logger.ts +2 -12
  103. package/templates/backend/observability/metrics.ts +8 -5
  104. package/templates/backend/session/sqlite.ts +152 -0
  105. package/templates/backend/session/store.ts +45 -0
  106. package/templates/backend/tsconfig.json +1 -1
  107. package/tests/add.test.js +327 -0
  108. package/tests/authAdapter.test.js +315 -0
  109. package/tests/bundlerParity.test.js +217 -0
  110. package/tests/cacheReporter.test.js +10 -10
  111. package/tests/dbConnection.test.js +209 -0
  112. package/tests/deploy.test.js +357 -0
  113. package/tests/envLoader.test.js +271 -17
  114. package/tests/integration.test.js +2432 -3
  115. package/tests/jobsScheduler.test.js +253 -0
  116. package/tests/manifest.test.js +287 -12
  117. package/tests/migrationRunner.test.js +249 -0
  118. package/tests/sessionScaffoldStore.test.js +752 -0
  119. package/tests/sessionStore.test.js +490 -0
  120. package/tests/testing.test.js +252 -0
  121. package/tests/watch.test.js +192 -32
  122. package/tsconfig.json +3 -10
  123. package/templates/backend/server/fastify.ts +0 -288
@@ -2,251 +2,354 @@ import { spawn, type ChildProcess } from 'node:child_process';
2
2
  import { once } from 'node:events';
3
3
  import { existsSync } from 'node:fs';
4
4
  import { readFile } from 'node:fs/promises';
5
- import net from 'node:net';
6
5
  import path from 'node:path';
7
6
 
8
7
  import type { ModuleManifest } from '@webstir-io/module-contract';
9
8
 
10
9
  import { getBackendTestContext, setBackendTestContext } from './context.js';
10
+ import { resolveWorkspaceRoot } from '../workspace.js';
11
11
  import type {
12
- BackendTestCallback,
13
- BackendTestContext,
14
- BackendTestHarness,
15
- BackendTestHarnessOptions
12
+ BackendTestCallback,
13
+ BackendTestContext,
14
+ BackendTestHarness,
15
+ BackendTestHarnessOptions,
16
16
  } from './types.js';
17
17
 
18
18
  const DEFAULT_PORT = 4100;
19
19
  const DEFAULT_READY_TEXT = 'API server running';
20
20
  const DEFAULT_READY_TIMEOUT_MS = 15_000;
21
21
 
22
- export type { BackendTestCallback, BackendTestContext, BackendTestHarness, BackendTestHarnessOptions } from './types.js';
22
+ export type {
23
+ BackendTestCallback,
24
+ BackendTestContext,
25
+ BackendTestHarness,
26
+ BackendTestHarnessOptions,
27
+ } from './types.js';
23
28
  export { getBackendTestContext, setBackendTestContext };
24
29
 
25
- export async function createBackendTestHarness(options: BackendTestHarnessOptions = {}): Promise<BackendTestHarness> {
26
- const workspaceRoot = options.workspaceRoot ?? process.env.WEBSTIR_WORKSPACE_ROOT ?? process.cwd();
27
- const buildRoot = options.buildRoot ?? process.env.WEBSTIR_BACKEND_BUILD_ROOT ?? path.join(workspaceRoot, 'build', 'backend');
28
- const entry = options.entry ?? process.env.WEBSTIR_BACKEND_TEST_ENTRY ?? path.join(buildRoot, 'index.js');
29
- const manifestPath =
30
- options.manifestPath ??
31
- process.env.WEBSTIR_BACKEND_TEST_MANIFEST ??
32
- path.join(workspaceRoot, '.webstir', 'backend-manifest.json');
33
- const readyText = options.readyText ?? process.env.WEBSTIR_BACKEND_TEST_READY ?? DEFAULT_READY_TEXT;
34
- const readyTimeoutMs =
35
- options.readyTimeoutMs ?? readInt(process.env.WEBSTIR_BACKEND_TEST_READY_TIMEOUT, DEFAULT_READY_TIMEOUT_MS);
36
-
37
- if (!existsSync(entry)) {
38
- throw new Error(
39
- `Backend test entry not found at ${entry}. Run the backend build before executing backend tests.`
40
- );
41
- }
30
+ export async function createBackendTestHarness(
31
+ options: BackendTestHarnessOptions = {},
32
+ ): Promise<BackendTestHarness> {
33
+ const resolvedEnv = { ...process.env, ...(options.env ?? {}) };
34
+ const workspaceRoot = resolveWorkspaceRoot({
35
+ workspaceRoot: options.workspaceRoot,
36
+ env: resolvedEnv,
37
+ });
38
+ const buildRoot = resolveWorkspacePath(
39
+ workspaceRoot,
40
+ options.buildRoot ??
41
+ resolvedEnv.WEBSTIR_BACKEND_BUILD_ROOT ??
42
+ path.join(workspaceRoot, 'build', 'backend'),
43
+ );
44
+ const entry = resolveWorkspacePath(
45
+ workspaceRoot,
46
+ options.entry ?? resolvedEnv.WEBSTIR_BACKEND_TEST_ENTRY ?? path.join(buildRoot, 'index.js'),
47
+ );
48
+ const manifestPath = resolveWorkspacePath(
49
+ workspaceRoot,
50
+ options.manifestPath ??
51
+ resolvedEnv.WEBSTIR_BACKEND_TEST_MANIFEST ??
52
+ path.join(workspaceRoot, '.webstir', 'backend-manifest.json'),
53
+ );
54
+ const readyText =
55
+ options.readyText ?? resolvedEnv.WEBSTIR_BACKEND_TEST_READY ?? DEFAULT_READY_TEXT;
56
+ const readyTimeoutMs =
57
+ options.readyTimeoutMs ??
58
+ readInt(resolvedEnv.WEBSTIR_BACKEND_TEST_READY_TIMEOUT, DEFAULT_READY_TIMEOUT_MS);
42
59
 
43
- const requestedPort = options.port ?? readInt(process.env.WEBSTIR_BACKEND_TEST_PORT, DEFAULT_PORT);
44
- const port = await findOpenPort(requestedPort);
45
- const env = createRuntimeEnv({
46
- workspaceRoot,
47
- port,
48
- overrides: options.env
49
- });
50
- const manifest = await loadManifest(manifestPath);
51
- const child = spawn(process.execPath, [entry], {
52
- cwd: workspaceRoot,
53
- env,
54
- stdio: ['ignore', 'pipe', 'pipe']
55
- });
60
+ if (!existsSync(entry)) {
61
+ throw new Error(
62
+ `Backend test entry not found at ${entry}. Run the backend build before executing backend tests.`,
63
+ );
64
+ }
56
65
 
57
- try {
58
- await waitForReady(child, readyText, readyTimeoutMs);
59
- } catch (error) {
60
- await stopProcess(child);
61
- throw error;
62
- }
66
+ const manifest = await loadManifest(manifestPath);
67
+ const requestedPort =
68
+ options.port ?? readInt(resolvedEnv.WEBSTIR_BACKEND_TEST_PORT, DEFAULT_PORT);
69
+ const { child, env, port } = await startBackendTestProcess({
70
+ workspaceRoot,
71
+ entry,
72
+ requestedPort,
73
+ baseEnv: resolvedEnv,
74
+ overrides: options.env,
75
+ readyText,
76
+ readyTimeoutMs,
77
+ });
63
78
 
64
- const baseUrl = new URL(env.API_BASE_URL ?? `http://127.0.0.1:${port}`);
65
- const context: BackendTestContext = {
66
- baseUrl: baseUrl.toString(),
67
- url: baseUrl,
68
- port,
69
- manifest,
70
- routes: Array.isArray(manifest?.routes) ? (manifest.routes as NonNullable<ModuleManifest['routes']>) : [],
71
- env,
72
- request: async (pathOrUrl = '/', init) => {
73
- const target = toUrl(baseUrl, pathOrUrl);
74
- return await fetch(target, init);
75
- }
76
- };
79
+ const baseUrl = new URL(env.API_BASE_URL ?? `http://127.0.0.1:${port}`);
80
+ const context: BackendTestContext = {
81
+ baseUrl: baseUrl.toString(),
82
+ url: baseUrl,
83
+ port,
84
+ manifest,
85
+ routes: Array.isArray(manifest?.routes)
86
+ ? (manifest.routes as NonNullable<ModuleManifest['routes']>)
87
+ : [],
88
+ env,
89
+ request: async (pathOrUrl = '/', init) => {
90
+ const target = toUrl(baseUrl, pathOrUrl);
91
+ return await fetch(target, init);
92
+ },
93
+ };
77
94
 
78
- return {
79
- context,
80
- async stop() {
81
- await stopProcess(child);
82
- }
83
- };
95
+ return {
96
+ context,
97
+ async stop() {
98
+ await stopProcess(child);
99
+ },
100
+ };
84
101
  }
85
102
 
86
103
  export function backendTest(name: string, callback: BackendTestCallback): void {
87
- const globalTest = (globalThis as { test?: (id: string, fn: () => Promise<void> | void) => void }).test;
88
- if (typeof globalTest !== 'function') {
89
- throw new Error('backendTest() requires the @webstir-io/webstir-testing runtime.');
90
- }
104
+ const globalTest = (globalThis as { test?: (id: string, fn: () => Promise<void> | void) => void })
105
+ .test;
106
+ if (typeof globalTest !== 'function') {
107
+ throw new Error('backendTest() requires the @webstir-io/webstir-testing runtime.');
108
+ }
91
109
 
92
- globalTest(name, async () => {
93
- const context = getBackendTestContext();
94
- if (!context) {
95
- throw new Error(
96
- 'Backend test context not available. Ensure backend tests run via the Webstir CLI (`webstir test`).'
97
- );
98
- }
99
- await callback(context);
100
- });
110
+ globalTest(name, async () => {
111
+ const context = getBackendTestContext();
112
+ if (!context) {
113
+ throw new Error(
114
+ 'Backend test context not available. Ensure backend tests run via the Webstir CLI (`webstir test`).',
115
+ );
116
+ }
117
+ await callback(context);
118
+ });
101
119
  }
102
120
 
103
121
  function toUrl(base: URL, pathOrUrl: string | URL): string {
104
- if (pathOrUrl instanceof URL) {
105
- return pathOrUrl.toString();
106
- }
122
+ if (pathOrUrl instanceof URL) {
123
+ return pathOrUrl.toString();
124
+ }
107
125
 
108
- if (/^https?:/i.test(pathOrUrl)) {
109
- return pathOrUrl;
110
- }
126
+ if (/^https?:/i.test(pathOrUrl)) {
127
+ return pathOrUrl;
128
+ }
111
129
 
112
- return new URL(pathOrUrl, base).toString();
130
+ return new URL(pathOrUrl, base).toString();
113
131
  }
114
132
 
115
133
  function readInt(value: string | undefined, fallback: number): number {
116
- if (!value) return fallback;
117
- const parsed = Number.parseInt(value, 10);
118
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
134
+ if (!value) return fallback;
135
+ const parsed = Number.parseInt(value, 10);
136
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
119
137
  }
120
138
 
121
- async function findOpenPort(start: number, attempts = 10): Promise<number> {
122
- let candidate = start;
123
- for (let index = 0; index < attempts; index += 1) {
124
- // eslint-disable-next-line no-await-in-loop
125
- if (await isPortAvailable(candidate)) {
126
- return candidate;
127
- }
128
- candidate += 1;
129
- }
130
- throw new Error(`Unable to find an open port for backend tests (tried starting at ${start}).`);
139
+ interface RuntimeEnvOptions {
140
+ workspaceRoot: string;
141
+ port: number;
142
+ baseEnv: Record<string, string | undefined>;
143
+ overrides?: Record<string, string | undefined>;
131
144
  }
132
145
 
133
- function isPortAvailable(port: number): Promise<boolean> {
134
- return new Promise((resolve) => {
135
- const server = net.createServer();
136
- server.once('error', () => {
137
- server.close(() => resolve(false));
138
- });
139
- server.once('listening', () => {
140
- server.close(() => resolve(true));
141
- });
142
- server.listen(port, '127.0.0.1');
143
- });
146
+ interface StartBackendTestProcessOptions {
147
+ workspaceRoot: string;
148
+ entry: string;
149
+ requestedPort: number;
150
+ baseEnv: Record<string, string | undefined>;
151
+ overrides?: Record<string, string | undefined>;
152
+ readyText: string;
153
+ readyTimeoutMs: number;
144
154
  }
145
155
 
146
- interface RuntimeEnvOptions {
147
- workspaceRoot: string;
148
- port: number;
149
- overrides?: Record<string, string | undefined>;
156
+ async function startBackendTestProcess(
157
+ options: StartBackendTestProcessOptions,
158
+ ): Promise<{ child: ChildProcess; env: Record<string, string>; port: number }> {
159
+ let candidate = options.requestedPort;
160
+ let lastError: Error | null = null;
161
+
162
+ for (let attempt = 0; attempt < 10; attempt += 1) {
163
+ const env = createRuntimeEnv({
164
+ workspaceRoot: options.workspaceRoot,
165
+ port: candidate,
166
+ baseEnv: options.baseEnv,
167
+ overrides: options.overrides,
168
+ });
169
+ const child = spawn(process.execPath, [options.entry], {
170
+ cwd: options.workspaceRoot,
171
+ env,
172
+ stdio: ['ignore', 'pipe', 'pipe'],
173
+ });
174
+ const output = captureChildOutput(child);
175
+
176
+ try {
177
+ await waitForReady(child, options.readyText, options.readyTimeoutMs);
178
+ output.stop();
179
+ return { child, env, port: candidate };
180
+ } catch (error) {
181
+ const captured = output.read();
182
+ output.stop();
183
+ await stopProcess(child);
184
+ const message = error instanceof Error ? error.message : String(error);
185
+ const failure = new Error(
186
+ `Backend test server did not become ready on port ${candidate}.\nstdout:\n${captured.stdout}\nstderr:\n${captured.stderr}\nerror:\n${message}`,
187
+ );
188
+
189
+ if (attempt < 9 && indicatesPortInUse(captured.stdout, captured.stderr, message)) {
190
+ lastError = failure;
191
+ candidate += 1;
192
+ continue;
193
+ }
194
+
195
+ throw failure;
196
+ }
197
+ }
198
+
199
+ throw lastError ?? new Error('Backend test server did not become ready.');
150
200
  }
151
201
 
152
202
  function createRuntimeEnv(options: RuntimeEnvOptions): Record<string, string> {
153
- const overrides: Record<string, string> = {};
154
- for (const [key, value] of Object.entries(options.overrides ?? {})) {
155
- if (value !== undefined) {
156
- overrides[key] = value;
157
- }
203
+ const overrides: Record<string, string> = {};
204
+ for (const [key, value] of Object.entries(options.overrides ?? {})) {
205
+ if (value !== undefined) {
206
+ overrides[key] = value;
158
207
  }
208
+ }
159
209
 
160
- const baseUrl = overrides.API_BASE_URL ?? process.env.API_BASE_URL ?? `http://127.0.0.1:${options.port}`;
161
- return {
162
- ...process.env,
163
- ...overrides,
164
- PORT: String(options.port),
165
- API_BASE_URL: baseUrl,
166
- NODE_ENV: overrides.NODE_ENV ?? process.env.NODE_ENV ?? 'test',
167
- WORKSPACE_ROOT: options.workspaceRoot,
168
- WEBSTIR_BACKEND_TEST_RUN: '1'
169
- };
210
+ const baseUrl =
211
+ overrides.API_BASE_URL ?? options.baseEnv.API_BASE_URL ?? `http://127.0.0.1:${options.port}`;
212
+ return {
213
+ ...options.baseEnv,
214
+ ...overrides,
215
+ PORT: String(options.port),
216
+ API_BASE_URL: baseUrl,
217
+ NODE_ENV: overrides.NODE_ENV ?? options.baseEnv.NODE_ENV ?? 'test',
218
+ WORKSPACE_ROOT: options.workspaceRoot,
219
+ WEBSTIR_BACKEND_TEST_RUN: '1',
220
+ };
221
+ }
222
+
223
+ function captureChildOutput(child: ChildProcess): {
224
+ stop(): void;
225
+ read(): { stdout: string; stderr: string };
226
+ } {
227
+ let stdout = '';
228
+ let stderr = '';
229
+
230
+ const onStdout = (chunk: Buffer | string) => {
231
+ stdout += chunk.toString();
232
+ };
233
+ const onStderr = (chunk: Buffer | string) => {
234
+ stderr += chunk.toString();
235
+ };
236
+
237
+ child.stdout?.on('data', onStdout);
238
+ child.stderr?.on('data', onStderr);
239
+
240
+ return {
241
+ stop() {
242
+ child.stdout?.off('data', onStdout);
243
+ child.stderr?.off('data', onStderr);
244
+ },
245
+ read() {
246
+ return { stdout, stderr };
247
+ },
248
+ };
249
+ }
250
+
251
+ function indicatesPortInUse(stdout: string, stderr: string, message: string): boolean {
252
+ return [stdout, stderr, message].some(
253
+ (value) =>
254
+ value.includes('EADDRINUSE') ||
255
+ value.includes('address already in use') ||
256
+ value.includes('Failed to listen at 127.0.0.1'),
257
+ );
258
+ }
259
+
260
+ function resolveWorkspacePath(workspaceRoot: string, value: string): string {
261
+ return path.isAbsolute(value) ? path.resolve(value) : path.resolve(workspaceRoot, value);
170
262
  }
171
263
 
172
264
  async function loadManifest(manifestPath: string): Promise<ModuleManifest | null> {
173
- try {
174
- const raw = await readFile(manifestPath, 'utf8');
175
- return JSON.parse(raw) as ModuleManifest;
176
- } catch {
177
- return null;
178
- }
265
+ try {
266
+ const raw = await readFile(manifestPath, 'utf8');
267
+ return JSON.parse(raw) as ModuleManifest;
268
+ } catch {
269
+ return null;
270
+ }
179
271
  }
180
272
 
181
273
  function emitModuleEvent(level: 'info' | 'warn' | 'error', message: string): void {
182
- const payload = JSON.stringify({ type: level, message });
183
- process.stdout.write(`WEBSTIR_MODULE_EVENT ${payload}\n`);
274
+ const payload = JSON.stringify({ type: level, message });
275
+ process.stdout.write(`WEBSTIR_MODULE_EVENT ${payload}\n`);
184
276
  }
185
277
 
186
- async function waitForReady(child: ChildProcess, readyText: string, timeoutMs: number): Promise<void> {
187
- const normalized = readyText
188
- .split('|')
189
- .map((token) => token.trim())
190
- .filter(Boolean);
191
- const readinessMatches = (line: string) => (normalized.length === 0 ? line.length > 0 : normalized.some((token) => line.includes(token)));
192
-
193
- await new Promise<void>((resolve, reject) => {
194
- const cleanup = () => {
195
- child.stdout?.off('data', onStdout);
196
- child.stderr?.off('data', onStderr);
197
- child.off('exit', onExit);
198
- clearTimeout(timer);
199
- };
200
-
201
- const onStdout = (chunk: Buffer | string) => {
202
- const text = chunk.toString();
203
- for (const line of text.split(/\r?\n/)) {
204
- if (!line) continue;
205
- emitModuleEvent('info', line);
206
- if (readinessMatches(line)) {
207
- cleanup();
208
- resolve();
209
- }
210
- }
211
- };
212
-
213
- const onStderr = (chunk: Buffer | string) => {
214
- const text = chunk.toString();
215
- for (const line of text.split(/\r?\n/)) {
216
- if (!line) continue;
217
- emitModuleEvent('error', line);
218
- if (readinessMatches(line)) {
219
- cleanup();
220
- resolve();
221
- }
222
- }
223
- };
224
-
225
- const onExit = (code: number | null) => {
226
- cleanup();
227
- reject(new Error(`Backend test server exited before it became ready (code ${code ?? 'null'}).`));
228
- };
229
-
230
- const timer = setTimeout(() => {
231
- cleanup();
232
- emitModuleEvent('error', 'Backend test server readiness timed out.');
233
- reject(new Error(`Backend test server did not become ready within ${timeoutMs}ms. Check server logs for details.`));
234
- }, timeoutMs);
235
-
236
- child.stdout?.on('data', onStdout);
237
- child.stderr?.on('data', onStderr);
238
- child.once('exit', onExit);
239
- });
278
+ async function waitForReady(
279
+ child: ChildProcess,
280
+ readyText: string,
281
+ timeoutMs: number,
282
+ ): Promise<void> {
283
+ const normalized = readyText
284
+ .split('|')
285
+ .map((token) => token.trim())
286
+ .filter(Boolean);
287
+ const readinessMatches = (line: string) =>
288
+ normalized.length === 0 ? line.length > 0 : normalized.some((token) => line.includes(token));
289
+
290
+ await new Promise<void>((resolve, reject) => {
291
+ const cleanup = () => {
292
+ child.stdout?.off('data', onStdout);
293
+ child.stderr?.off('data', onStderr);
294
+ child.off('exit', onExit);
295
+ clearTimeout(timer);
296
+ };
297
+
298
+ const onStdout = (chunk: Buffer | string) => {
299
+ const text = chunk.toString();
300
+ for (const line of text.split(/\r?\n/)) {
301
+ if (!line) continue;
302
+ emitModuleEvent('info', line);
303
+ if (readinessMatches(line)) {
304
+ cleanup();
305
+ resolve();
306
+ }
307
+ }
308
+ };
309
+
310
+ const onStderr = (chunk: Buffer | string) => {
311
+ const text = chunk.toString();
312
+ for (const line of text.split(/\r?\n/)) {
313
+ if (!line) continue;
314
+ emitModuleEvent('error', line);
315
+ if (readinessMatches(line)) {
316
+ cleanup();
317
+ resolve();
318
+ }
319
+ }
320
+ };
321
+
322
+ const onExit = (code: number | null) => {
323
+ cleanup();
324
+ reject(
325
+ new Error(`Backend test server exited before it became ready (code ${code ?? 'null'}).`),
326
+ );
327
+ };
328
+
329
+ const timer = setTimeout(() => {
330
+ cleanup();
331
+ emitModuleEvent('error', 'Backend test server readiness timed out.');
332
+ reject(
333
+ new Error(
334
+ `Backend test server did not become ready within ${timeoutMs}ms. Check server logs for details.`,
335
+ ),
336
+ );
337
+ }, timeoutMs);
338
+
339
+ child.stdout?.on('data', onStdout);
340
+ child.stderr?.on('data', onStderr);
341
+ child.once('exit', onExit);
342
+ });
240
343
  }
241
344
 
242
345
  async function stopProcess(child: ChildProcess): Promise<void> {
243
- if (!child || child.killed || child.exitCode !== null) {
244
- return;
245
- }
246
- child.kill('SIGTERM');
247
- try {
248
- await once(child, 'exit');
249
- } catch {
250
- // ignore
251
- }
346
+ if (!child || child.killed || child.exitCode !== null) {
347
+ return;
348
+ }
349
+ child.kill('SIGTERM');
350
+ try {
351
+ await once(child, 'exit');
352
+ } catch {
353
+ // ignore
354
+ }
252
355
  }
@@ -1,28 +1,27 @@
1
1
  import type { ModuleManifest } from '@webstir-io/module-contract';
2
2
  type ModuleRoute = NonNullable<ModuleManifest['routes']>[number];
3
3
  export interface BackendTestContext {
4
- readonly baseUrl: string;
5
- readonly url: URL;
6
- readonly port: number;
7
- readonly manifest: ModuleManifest | null;
8
- readonly routes: readonly ModuleRoute[];
9
- readonly env: Readonly<Record<string, string>>;
10
- request(pathOrUrl?: string | URL, init?: RequestInit): Promise<Response>;
4
+ readonly baseUrl: string;
5
+ readonly url: URL;
6
+ readonly port: number;
7
+ readonly manifest: ModuleManifest | null;
8
+ readonly routes: readonly ModuleRoute[];
9
+ readonly env: Readonly<Record<string, string>>;
10
+ request(pathOrUrl?: string | URL, init?: RequestInit): Promise<Response>;
11
11
  }
12
12
  export interface BackendTestHarness {
13
- readonly context: BackendTestContext;
14
- stop(): Promise<void>;
13
+ readonly context: BackendTestContext;
14
+ stop(): Promise<void>;
15
15
  }
16
16
  export interface BackendTestHarnessOptions {
17
- workspaceRoot?: string;
18
- buildRoot?: string;
19
- entry?: string;
20
- manifestPath?: string;
21
- env?: Record<string, string | undefined>;
22
- port?: number;
23
- readyText?: string;
24
- readyTimeoutMs?: number;
25
- reuseExistingServer?: boolean;
17
+ workspaceRoot?: string;
18
+ buildRoot?: string;
19
+ entry?: string;
20
+ manifestPath?: string;
21
+ env?: Record<string, string | undefined>;
22
+ port?: number;
23
+ readyText?: string;
24
+ readyTimeoutMs?: number;
25
+ reuseExistingServer?: boolean;
26
26
  }
27
27
  export type BackendTestCallback = (context: BackendTestContext) => Promise<void> | void;
28
- export {};
@@ -3,30 +3,30 @@ import type { ModuleManifest } from '@webstir-io/module-contract';
3
3
  type ModuleRoute = NonNullable<ModuleManifest['routes']>[number];
4
4
 
5
5
  export interface BackendTestContext {
6
- readonly baseUrl: string;
7
- readonly url: URL;
8
- readonly port: number;
9
- readonly manifest: ModuleManifest | null;
10
- readonly routes: readonly ModuleRoute[];
11
- readonly env: Readonly<Record<string, string>>;
12
- request(pathOrUrl?: string | URL, init?: RequestInit): Promise<Response>;
6
+ readonly baseUrl: string;
7
+ readonly url: URL;
8
+ readonly port: number;
9
+ readonly manifest: ModuleManifest | null;
10
+ readonly routes: readonly ModuleRoute[];
11
+ readonly env: Readonly<Record<string, string>>;
12
+ request(pathOrUrl?: string | URL, init?: RequestInit): Promise<Response>;
13
13
  }
14
14
 
15
15
  export interface BackendTestHarness {
16
- readonly context: BackendTestContext;
17
- stop(): Promise<void>;
16
+ readonly context: BackendTestContext;
17
+ stop(): Promise<void>;
18
18
  }
19
19
 
20
20
  export interface BackendTestHarnessOptions {
21
- workspaceRoot?: string;
22
- buildRoot?: string;
23
- entry?: string;
24
- manifestPath?: string;
25
- env?: Record<string, string | undefined>;
26
- port?: number;
27
- readyText?: string;
28
- readyTimeoutMs?: number;
29
- reuseExistingServer?: boolean;
21
+ workspaceRoot?: string;
22
+ buildRoot?: string;
23
+ entry?: string;
24
+ manifestPath?: string;
25
+ env?: Record<string, string | undefined>;
26
+ port?: number;
27
+ readyText?: string;
28
+ readyTimeoutMs?: number;
29
+ reuseExistingServer?: boolean;
30
30
  }
31
31
 
32
32
  export type BackendTestCallback = (context: BackendTestContext) => Promise<void> | void;