@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.
- package/README.md +106 -79
- package/dist/add.d.ts +59 -0
- package/dist/add.js +626 -0
- package/dist/build/artifacts.d.ts +115 -1
- package/dist/build/artifacts.js +4 -4
- package/dist/build/entries.js +1 -1
- package/dist/build/pipeline.d.ts +33 -1
- package/dist/build/pipeline.js +307 -65
- package/dist/cache/diff.js +9 -8
- package/dist/cache/reporters.js +1 -1
- package/dist/deploy-cli.d.ts +2 -0
- package/dist/deploy-cli.js +86 -0
- package/dist/diagnostics/summary.js +2 -2
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/manifest/pipeline.js +103 -32
- package/dist/provider.js +35 -17
- package/dist/runtime/bun.d.ts +51 -0
- package/dist/runtime/bun.js +499 -0
- package/dist/runtime/core.d.ts +141 -0
- package/dist/runtime/core.js +316 -0
- package/dist/runtime/deploy-backend.d.ts +20 -0
- package/dist/runtime/deploy-backend.js +175 -0
- package/dist/runtime/deploy-shared.d.ts +43 -0
- package/dist/runtime/deploy-shared.js +75 -0
- package/dist/runtime/deploy-static.d.ts +2 -0
- package/dist/runtime/deploy-static.js +161 -0
- package/dist/runtime/deploy.d.ts +3 -0
- package/dist/runtime/deploy.js +91 -0
- package/dist/runtime/forms.d.ts +73 -0
- package/dist/runtime/forms.js +236 -0
- package/dist/runtime/request-hooks.d.ts +47 -0
- package/dist/runtime/request-hooks.js +102 -0
- package/dist/runtime/session-metadata.d.ts +13 -0
- package/dist/runtime/session-metadata.js +98 -0
- package/dist/runtime/session-runtime.d.ts +28 -0
- package/dist/runtime/session-runtime.js +180 -0
- package/dist/runtime/session.d.ts +83 -0
- package/dist/runtime/session.js +396 -0
- package/dist/runtime/views.d.ts +74 -0
- package/dist/runtime/views.js +221 -0
- package/dist/scaffold/assets.js +25 -21
- package/dist/testing/context.js +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +100 -56
- package/dist/utils/bun.d.ts +2 -0
- package/dist/utils/bun.js +13 -0
- package/dist/watch.d.ts +13 -1
- package/dist/watch.js +345 -97
- package/dist/workspace.d.ts +8 -0
- package/dist/workspace.js +44 -3
- package/package.json +49 -14
- package/scripts/publish.sh +2 -92
- package/scripts/smoke.mjs +282 -107
- package/scripts/update-contract.sh +12 -10
- package/src/add.ts +964 -0
- package/src/build/artifacts.ts +49 -46
- package/src/build/entries.ts +12 -12
- package/src/build/pipeline.ts +779 -403
- package/src/cache/diff.ts +111 -105
- package/src/cache/reporters.ts +26 -26
- package/src/deploy-cli.ts +111 -0
- package/src/diagnostics/summary.ts +28 -22
- package/src/index.ts +11 -0
- package/src/manifest/pipeline.ts +328 -215
- package/src/provider.ts +115 -98
- package/src/runtime/bun.ts +793 -0
- package/src/runtime/core.ts +598 -0
- package/src/runtime/deploy-backend.ts +239 -0
- package/src/runtime/deploy-shared.ts +136 -0
- package/src/runtime/deploy-static.ts +191 -0
- package/src/runtime/deploy.ts +143 -0
- package/src/runtime/forms.ts +364 -0
- package/src/runtime/request-hooks.ts +165 -0
- package/src/runtime/session-metadata.ts +135 -0
- package/src/runtime/session-runtime.ts +267 -0
- package/src/runtime/session.ts +642 -0
- package/src/runtime/views.ts +385 -0
- package/src/scaffold/assets.ts +77 -73
- package/src/testing/context.js +8 -9
- package/src/testing/context.ts +9 -9
- package/src/testing/index.d.ts +14 -3
- package/src/testing/index.js +254 -175
- package/src/testing/index.ts +298 -195
- package/src/testing/types.d.ts +18 -19
- package/src/testing/types.ts +18 -18
- package/src/utils/bun.ts +26 -0
- package/src/watch.ts +503 -99
- package/src/workspace.ts +59 -3
- package/templates/backend/.env.example +15 -0
- package/templates/backend/auth/adapter.ts +335 -36
- package/templates/backend/db/connection.ts +190 -65
- package/templates/backend/db/migrate.ts +149 -43
- package/templates/backend/db/types.d.ts +1 -1
- package/templates/backend/env.ts +132 -20
- package/templates/backend/functions/hello/index.ts +1 -2
- package/templates/backend/index.ts +15 -508
- package/templates/backend/jobs/nightly/index.ts +1 -1
- package/templates/backend/jobs/runtime.ts +24 -11
- package/templates/backend/jobs/scheduler.ts +208 -46
- package/templates/backend/module.ts +227 -13
- package/templates/backend/observability/logger.ts +2 -12
- package/templates/backend/observability/metrics.ts +8 -5
- package/templates/backend/session/sqlite.ts +152 -0
- package/templates/backend/session/store.ts +45 -0
- package/templates/backend/tsconfig.json +1 -1
- package/tests/add.test.js +327 -0
- package/tests/authAdapter.test.js +315 -0
- package/tests/bundlerParity.test.js +217 -0
- package/tests/cacheReporter.test.js +10 -10
- package/tests/dbConnection.test.js +209 -0
- package/tests/deploy.test.js +357 -0
- package/tests/envLoader.test.js +271 -17
- package/tests/integration.test.js +2432 -3
- package/tests/jobsScheduler.test.js +253 -0
- package/tests/manifest.test.js +287 -12
- package/tests/migrationRunner.test.js +249 -0
- package/tests/sessionScaffoldStore.test.js +752 -0
- package/tests/sessionStore.test.js +490 -0
- package/tests/testing.test.js +252 -0
- package/tests/watch.test.js +192 -32
- package/tsconfig.json +3 -10
- package/templates/backend/server/fastify.ts +0 -288
package/src/testing/index.ts
CHANGED
|
@@ -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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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 {
|
|
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(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
122
|
+
if (pathOrUrl instanceof URL) {
|
|
123
|
+
return pathOrUrl.toString();
|
|
124
|
+
}
|
|
107
125
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
126
|
+
if (/^https?:/i.test(pathOrUrl)) {
|
|
127
|
+
return pathOrUrl;
|
|
128
|
+
}
|
|
111
129
|
|
|
112
|
-
|
|
130
|
+
return new URL(pathOrUrl, base).toString();
|
|
113
131
|
}
|
|
114
132
|
|
|
115
133
|
function readInt(value: string | undefined, fallback: number): number {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
183
|
-
|
|
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(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
}
|
package/src/testing/types.d.ts
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
14
|
-
|
|
13
|
+
readonly context: BackendTestContext;
|
|
14
|
+
stop(): Promise<void>;
|
|
15
15
|
}
|
|
16
16
|
export interface BackendTestHarnessOptions {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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 {};
|
package/src/testing/types.ts
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
17
|
-
|
|
16
|
+
readonly context: BackendTestContext;
|
|
17
|
+
stop(): Promise<void>;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export interface BackendTestHarnessOptions {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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;
|