@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
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import net from 'node:net';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
import { createBackendTestHarness } from '../src/testing/index.js';
|
|
9
|
+
|
|
10
|
+
async function createTempDir(prefix) {
|
|
11
|
+
return await fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function writeHarnessFixture(
|
|
15
|
+
workspace,
|
|
16
|
+
{
|
|
17
|
+
entryPath = path.join(workspace, 'build', 'backend', 'index.js'),
|
|
18
|
+
manifestPath = path.join(workspace, '.webstir', 'backend-manifest.json'),
|
|
19
|
+
manifest = { name: '@demo/backend', version: '0.0.1', routes: [] },
|
|
20
|
+
entrySource = "console.log('API server running');\nsetInterval(() => {}, 1000);\n",
|
|
21
|
+
} = {},
|
|
22
|
+
) {
|
|
23
|
+
await fs.mkdir(path.dirname(entryPath), { recursive: true });
|
|
24
|
+
await fs.writeFile(entryPath, entrySource, 'utf8');
|
|
25
|
+
await fs.mkdir(path.dirname(manifestPath), { recursive: true });
|
|
26
|
+
await fs.writeFile(manifestPath, JSON.stringify(manifest), 'utf8');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function canListenOnTcp() {
|
|
30
|
+
return await new Promise((resolve) => {
|
|
31
|
+
const server = net.createServer();
|
|
32
|
+
const settle = (value) => {
|
|
33
|
+
server.removeAllListeners();
|
|
34
|
+
server.close(() => resolve(value));
|
|
35
|
+
};
|
|
36
|
+
server.once('error', (error) => {
|
|
37
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'EPERM') {
|
|
38
|
+
resolve(false);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
resolve(false);
|
|
42
|
+
});
|
|
43
|
+
server.listen(0, '127.0.0.1', () => settle(true));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function getOpenPort() {
|
|
48
|
+
return await new Promise((resolve, reject) => {
|
|
49
|
+
const server = net.createServer();
|
|
50
|
+
server.once('error', reject);
|
|
51
|
+
server.listen(0, '127.0.0.1', () => {
|
|
52
|
+
const address = server.address();
|
|
53
|
+
if (!address || typeof address === 'string') {
|
|
54
|
+
reject(new Error('Failed to allocate an open port.'));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
server.close((error) => {
|
|
58
|
+
if (error) {
|
|
59
|
+
reject(error);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
resolve(address.port);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
test('createBackendTestHarness resolves WORKSPACE_ROOT when WEBSTIR_WORKSPACE_ROOT is unset', async (t) => {
|
|
69
|
+
if (!(await canListenOnTcp())) {
|
|
70
|
+
t.skip('TCP listen is not permitted in this environment.');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const workspace = await createTempDir('webstir-backend-harness-workspace-');
|
|
75
|
+
const alternateCwd = await createTempDir('webstir-backend-harness-cwd-');
|
|
76
|
+
const port = await getOpenPort();
|
|
77
|
+
await writeHarnessFixture(workspace);
|
|
78
|
+
|
|
79
|
+
const previousWorkspaceRoot = process.env.WORKSPACE_ROOT;
|
|
80
|
+
const previousWebstirWorkspaceRoot = process.env.WEBSTIR_WORKSPACE_ROOT;
|
|
81
|
+
const previousCwd = process.cwd();
|
|
82
|
+
let harness;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
process.env.WORKSPACE_ROOT = workspace;
|
|
86
|
+
delete process.env.WEBSTIR_WORKSPACE_ROOT;
|
|
87
|
+
process.chdir(alternateCwd);
|
|
88
|
+
|
|
89
|
+
harness = await createBackendTestHarness({ port });
|
|
90
|
+
|
|
91
|
+
assert.equal(harness.context.env.WORKSPACE_ROOT, workspace);
|
|
92
|
+
assert.match(harness.context.baseUrl, /^http:\/\/127\.0\.0\.1:\d+\/?$/);
|
|
93
|
+
} finally {
|
|
94
|
+
await harness?.stop();
|
|
95
|
+
process.chdir(previousCwd);
|
|
96
|
+
if (previousWorkspaceRoot === undefined) {
|
|
97
|
+
delete process.env.WORKSPACE_ROOT;
|
|
98
|
+
} else {
|
|
99
|
+
process.env.WORKSPACE_ROOT = previousWorkspaceRoot;
|
|
100
|
+
}
|
|
101
|
+
if (previousWebstirWorkspaceRoot === undefined) {
|
|
102
|
+
delete process.env.WEBSTIR_WORKSPACE_ROOT;
|
|
103
|
+
} else {
|
|
104
|
+
process.env.WEBSTIR_WORKSPACE_ROOT = previousWebstirWorkspaceRoot;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('createBackendTestHarness resolves WEBSTIR_WORKSPACE_ROOT from env overrides outside the workspace cwd', async (t) => {
|
|
110
|
+
if (!(await canListenOnTcp())) {
|
|
111
|
+
t.skip('TCP listen is not permitted in this environment.');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const workspace = await createTempDir('webstir-backend-harness-env-workspace-');
|
|
116
|
+
const alternateCwd = await createTempDir('webstir-backend-harness-env-cwd-');
|
|
117
|
+
const port = await getOpenPort();
|
|
118
|
+
await writeHarnessFixture(workspace, {
|
|
119
|
+
entryPath: path.join(workspace, 'custom', 'backend', 'index.js'),
|
|
120
|
+
manifestPath: path.join(workspace, '.webstir', 'custom-backend-manifest.json'),
|
|
121
|
+
manifest: {
|
|
122
|
+
name: '@demo/env-harness',
|
|
123
|
+
version: '1.2.3',
|
|
124
|
+
routes: [{ path: '/env', method: 'GET' }],
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const previousCwd = process.cwd();
|
|
129
|
+
let harness;
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
process.chdir(alternateCwd);
|
|
133
|
+
|
|
134
|
+
harness = await createBackendTestHarness({
|
|
135
|
+
port,
|
|
136
|
+
env: {
|
|
137
|
+
WEBSTIR_WORKSPACE_ROOT: workspace,
|
|
138
|
+
WEBSTIR_BACKEND_BUILD_ROOT: 'custom/backend',
|
|
139
|
+
WEBSTIR_BACKEND_TEST_MANIFEST: '.webstir/custom-backend-manifest.json',
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
assert.equal(harness.context.env.WORKSPACE_ROOT, workspace);
|
|
144
|
+
assert.equal(harness.context.manifest?.name, '@demo/env-harness');
|
|
145
|
+
assert.equal(harness.context.routes[0]?.path, '/env');
|
|
146
|
+
assert.match(harness.context.baseUrl, /^http:\/\/127\.0\.0\.1:\d+\/?$/);
|
|
147
|
+
} finally {
|
|
148
|
+
await harness?.stop();
|
|
149
|
+
process.chdir(previousCwd);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('createBackendTestHarness retries on EADDRINUSE and reports the bound port', async (t) => {
|
|
154
|
+
if (!(await canListenOnTcp())) {
|
|
155
|
+
t.skip('TCP listen is not permitted in this environment.');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const workspace = await createTempDir('webstir-backend-harness-port-retry-');
|
|
160
|
+
const requestedPort = await getOpenPort();
|
|
161
|
+
const blocker = net.createServer();
|
|
162
|
+
await new Promise((resolve, reject) => {
|
|
163
|
+
blocker.once('error', reject);
|
|
164
|
+
blocker.listen(requestedPort, '127.0.0.1', () => resolve());
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await writeHarnessFixture(workspace, {
|
|
168
|
+
entrySource: `
|
|
169
|
+
import net from 'node:net';
|
|
170
|
+
|
|
171
|
+
const server = net.createServer((socket) => socket.end());
|
|
172
|
+
const port = Number(process.env.PORT ?? '0');
|
|
173
|
+
|
|
174
|
+
server.listen(port, '127.0.0.1', () => {
|
|
175
|
+
console.log('API server running');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const shutdown = () => {
|
|
179
|
+
server.close(() => process.exit(0));
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
process.on('SIGTERM', shutdown);
|
|
183
|
+
process.on('SIGINT', shutdown);
|
|
184
|
+
`,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
let harness;
|
|
188
|
+
try {
|
|
189
|
+
harness = await createBackendTestHarness({
|
|
190
|
+
workspaceRoot: workspace,
|
|
191
|
+
port: requestedPort,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
assert.notEqual(harness.context.port, requestedPort);
|
|
195
|
+
assert.equal(harness.context.env.PORT, String(harness.context.port));
|
|
196
|
+
assert.match(harness.context.baseUrl, new RegExp(`:${harness.context.port}/?$`));
|
|
197
|
+
} finally {
|
|
198
|
+
await harness?.stop();
|
|
199
|
+
await new Promise((resolve, reject) => {
|
|
200
|
+
blocker.close((error) => {
|
|
201
|
+
if (error) {
|
|
202
|
+
reject(error);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
resolve();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('createBackendTestHarness resolves option-based relative entry and manifest paths outside the workspace cwd', async (t) => {
|
|
212
|
+
if (!(await canListenOnTcp())) {
|
|
213
|
+
t.skip('TCP listen is not permitted in this environment.');
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const workspace = await createTempDir('webstir-backend-harness-options-workspace-');
|
|
218
|
+
const alternateCwd = await createTempDir('webstir-backend-harness-options-cwd-');
|
|
219
|
+
const port = await getOpenPort();
|
|
220
|
+
await writeHarnessFixture(workspace, {
|
|
221
|
+
entryPath: path.join(workspace, 'artifacts', 'backend', 'server.js'),
|
|
222
|
+
manifestPath: path.join(workspace, 'artifacts', 'backend', 'manifest.json'),
|
|
223
|
+
manifest: {
|
|
224
|
+
name: '@demo/options-harness',
|
|
225
|
+
version: '4.5.6',
|
|
226
|
+
routes: [{ path: '/options', method: 'POST' }],
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const previousCwd = process.cwd();
|
|
231
|
+
let harness;
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
process.chdir(alternateCwd);
|
|
235
|
+
|
|
236
|
+
harness = await createBackendTestHarness({
|
|
237
|
+
workspaceRoot: workspace,
|
|
238
|
+
buildRoot: 'artifacts/backend',
|
|
239
|
+
entry: 'artifacts/backend/server.js',
|
|
240
|
+
manifestPath: 'artifacts/backend/manifest.json',
|
|
241
|
+
port,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
assert.equal(harness.context.env.WORKSPACE_ROOT, workspace);
|
|
245
|
+
assert.equal(harness.context.manifest?.name, '@demo/options-harness');
|
|
246
|
+
assert.equal(harness.context.routes[0]?.path, '/options');
|
|
247
|
+
assert.match(harness.context.baseUrl, /^http:\/\/127\.0\.0\.1:\d+\/?$/);
|
|
248
|
+
} finally {
|
|
249
|
+
await harness?.stop();
|
|
250
|
+
process.chdir(previousCwd);
|
|
251
|
+
}
|
|
252
|
+
});
|
package/tests/watch.test.js
CHANGED
|
@@ -31,6 +31,22 @@ async function seedBackendEntry(workspace) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
async function seedBackendScaffold(workspace) {
|
|
35
|
+
const assets = await backendProvider.getScaffoldAssets();
|
|
36
|
+
for (const asset of assets) {
|
|
37
|
+
if (!asset.targetPath.startsWith(path.join('src', 'backend'))) continue;
|
|
38
|
+
const target = path.join(workspace, asset.targetPath);
|
|
39
|
+
await copyFile(asset.sourcePath, target);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function linkWorkspacePackage(workspace) {
|
|
44
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
45
|
+
const scopeRoot = path.join(workspace, 'node_modules', '@webstir-io');
|
|
46
|
+
await fs.mkdir(scopeRoot, { recursive: true });
|
|
47
|
+
await fs.symlink(packageRoot, path.join(scopeRoot, 'webstir-backend'), 'dir');
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
function getLocalBinPath() {
|
|
35
51
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
36
52
|
const pkgRoot = path.resolve(here, '..');
|
|
@@ -46,55 +62,199 @@ async function waitFor(checkFn, timeoutMs = 5000, pollMs = 100) {
|
|
|
46
62
|
throw new Error('waitFor timed out');
|
|
47
63
|
}
|
|
48
64
|
|
|
49
|
-
test('startBackendWatch updates cache files after rebuild', async () => {
|
|
65
|
+
test('startBackendWatch updates cache files after rebuild', { timeout: 15_000 }, async () => {
|
|
50
66
|
const workspace = await createTempWorkspace();
|
|
51
67
|
await seedBackendEntry(workspace);
|
|
52
68
|
|
|
53
69
|
const env = {
|
|
54
70
|
WEBSTIR_MODULE_MODE: 'build',
|
|
55
71
|
WEBSTIR_BACKEND_TYPECHECK: 'skip',
|
|
56
|
-
PATH: `${getLocalBinPath()}${path.delimiter}${process.env.PATH ?? ''}
|
|
72
|
+
PATH: `${getLocalBinPath()}${path.delimiter}${process.env.PATH ?? ''}`,
|
|
57
73
|
};
|
|
58
74
|
|
|
59
|
-
const handle = await startBackendWatch({ workspaceRoot: workspace, env });
|
|
60
75
|
try {
|
|
76
|
+
const handle = await startBackendWatch({ workspaceRoot: workspace, env });
|
|
61
77
|
const outputsPath = path.join(workspace, '.webstir', 'backend-outputs.json');
|
|
62
78
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
79
|
+
try {
|
|
80
|
+
await waitFor(async () => {
|
|
81
|
+
try {
|
|
82
|
+
await fs.access(outputsPath);
|
|
83
|
+
return true;
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const before = JSON.parse(await fs.readFile(outputsPath, 'utf8'));
|
|
90
|
+
const indexPath = path.join(workspace, 'src', 'backend', 'index.ts');
|
|
91
|
+
await fs.appendFile(indexPath, '\nconsole.log("watch-test");\n', 'utf8');
|
|
92
|
+
|
|
93
|
+
await waitFor(async () => {
|
|
94
|
+
try {
|
|
95
|
+
const after = JSON.parse(await fs.readFile(outputsPath, 'utf8'));
|
|
96
|
+
const key = Object.keys(after)[0];
|
|
97
|
+
return before[key] !== after[key];
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const manifestDigestPath = path.join(workspace, '.webstir', 'backend-manifest-digest.json');
|
|
104
|
+
await waitFor(async () => {
|
|
105
|
+
try {
|
|
106
|
+
await fs.access(manifestDigestPath);
|
|
107
|
+
return true;
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
assert.ok(true, 'watch updated cache files after rebuild');
|
|
114
|
+
} finally {
|
|
115
|
+
await handle.stop();
|
|
116
|
+
}
|
|
117
|
+
} finally {
|
|
118
|
+
await fs.rm(workspace, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('startBackendWatch emits build outcome events for successful and failed rebuilds', {
|
|
123
|
+
timeout: 15_000,
|
|
124
|
+
}, async () => {
|
|
125
|
+
const workspace = await createTempWorkspace('webstir-backend-watch-events-');
|
|
126
|
+
await seedBackendEntry(workspace);
|
|
127
|
+
|
|
128
|
+
const env = {
|
|
129
|
+
WEBSTIR_MODULE_MODE: 'build',
|
|
130
|
+
WEBSTIR_BACKEND_TYPECHECK: 'skip',
|
|
131
|
+
PATH: `${getLocalBinPath()}${path.delimiter}${process.env.PATH ?? ''}`,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const events = [];
|
|
135
|
+
try {
|
|
136
|
+
const handle = await startBackendWatch({
|
|
137
|
+
workspaceRoot: workspace,
|
|
138
|
+
env,
|
|
139
|
+
onEvent(event) {
|
|
140
|
+
events.push(event);
|
|
141
|
+
},
|
|
70
142
|
});
|
|
71
143
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
144
|
+
try {
|
|
145
|
+
await waitFor(async () =>
|
|
146
|
+
events.some((event) => event.type === 'build-complete' && event.succeeded === true),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const indexPath = path.join(workspace, 'src', 'backend', 'index.ts');
|
|
150
|
+
await fs.writeFile(indexPath, 'export default () => {\n', 'utf8');
|
|
151
|
+
|
|
152
|
+
await waitFor(async () =>
|
|
153
|
+
events.some((event) => event.type === 'build-complete' && event.succeeded === false),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
assert.ok(events.some((event) => event.type === 'build-start'));
|
|
157
|
+
assert.ok(
|
|
158
|
+
events.some((event) => event.type === 'build-complete' && event.succeeded === true),
|
|
159
|
+
);
|
|
160
|
+
assert.ok(
|
|
161
|
+
events.some((event) => event.type === 'build-complete' && event.succeeded === false),
|
|
162
|
+
);
|
|
163
|
+
} finally {
|
|
164
|
+
await handle.stop();
|
|
165
|
+
}
|
|
166
|
+
} finally {
|
|
167
|
+
await fs.rm(workspace, { recursive: true, force: true });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('startBackendWatch reports Bun benchmark timings when enabled', {
|
|
172
|
+
timeout: 15_000,
|
|
173
|
+
}, async () => {
|
|
174
|
+
const workspace = await createTempWorkspace('webstir-backend-watch-benchmark-');
|
|
175
|
+
await seedBackendScaffold(workspace);
|
|
176
|
+
await linkWorkspacePackage(workspace);
|
|
177
|
+
|
|
178
|
+
const env = {
|
|
179
|
+
WEBSTIR_MODULE_MODE: 'build',
|
|
180
|
+
WEBSTIR_BACKEND_TYPECHECK: 'skip',
|
|
181
|
+
WEBSTIR_BACKEND_WATCH_BUN_BENCHMARK: '1',
|
|
182
|
+
PATH: `${getLocalBinPath()}${path.delimiter}${process.env.PATH ?? ''}`,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const events = [];
|
|
186
|
+
try {
|
|
187
|
+
const handle = await startBackendWatch({
|
|
188
|
+
workspaceRoot: workspace,
|
|
189
|
+
env,
|
|
190
|
+
onEvent(event) {
|
|
191
|
+
events.push(event);
|
|
192
|
+
},
|
|
84
193
|
});
|
|
85
194
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
195
|
+
try {
|
|
196
|
+
await waitFor(async () =>
|
|
197
|
+
events.some(
|
|
198
|
+
(event) =>
|
|
199
|
+
event.type === 'build-complete' &&
|
|
200
|
+
event.succeeded === true &&
|
|
201
|
+
typeof event.bunBenchmarkDurationMs === 'number' &&
|
|
202
|
+
event.bunBenchmarkDurationMs > 0,
|
|
203
|
+
),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const completedEvent = events.find(
|
|
207
|
+
(event) =>
|
|
208
|
+
event.type === 'build-complete' &&
|
|
209
|
+
event.succeeded === true &&
|
|
210
|
+
typeof event.bunBenchmarkDurationMs === 'number',
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
assert.equal(completedEvent?.bunBenchmarkSucceeded, true);
|
|
214
|
+
assert.equal(completedEvent?.bunBenchmarkErrorCount, 0);
|
|
215
|
+
assert.ok((completedEvent?.bunBenchmarkDurationMs ?? 0) > 0);
|
|
216
|
+
} finally {
|
|
217
|
+
await handle.stop();
|
|
218
|
+
}
|
|
219
|
+
} finally {
|
|
220
|
+
await fs.rm(workspace, { recursive: true, force: true });
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('startBackendWatch resolves WEBSTIR_WORKSPACE_ROOT outside the workspace cwd when workspaceRoot is omitted', {
|
|
225
|
+
timeout: 15_000,
|
|
226
|
+
}, async () => {
|
|
227
|
+
const workspace = await createTempWorkspace('webstir-backend-watch-root-');
|
|
228
|
+
const alternateCwd = await createTempWorkspace('webstir-backend-watch-cwd-');
|
|
229
|
+
const previousCwd = process.cwd();
|
|
230
|
+
await seedBackendEntry(workspace);
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
process.chdir(alternateCwd);
|
|
234
|
+
|
|
235
|
+
const handle = await startBackendWatch({
|
|
236
|
+
env: {
|
|
237
|
+
WEBSTIR_MODULE_MODE: 'build',
|
|
238
|
+
WEBSTIR_BACKEND_TYPECHECK: 'skip',
|
|
239
|
+
WEBSTIR_WORKSPACE_ROOT: workspace,
|
|
240
|
+
},
|
|
94
241
|
});
|
|
95
242
|
|
|
96
|
-
|
|
243
|
+
try {
|
|
244
|
+
await waitFor(async () => {
|
|
245
|
+
try {
|
|
246
|
+
await fs.access(path.join(workspace, '.webstir', 'backend-outputs.json'));
|
|
247
|
+
return true;
|
|
248
|
+
} catch {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
} finally {
|
|
253
|
+
await handle.stop();
|
|
254
|
+
}
|
|
97
255
|
} finally {
|
|
98
|
-
|
|
256
|
+
process.chdir(previousCwd);
|
|
257
|
+
await fs.rm(alternateCwd, { recursive: true, force: true });
|
|
258
|
+
await fs.rm(workspace, { recursive: true, force: true });
|
|
99
259
|
}
|
|
100
260
|
});
|
package/tsconfig.json
CHANGED
|
@@ -12,16 +12,9 @@
|
|
|
12
12
|
"declaration": true,
|
|
13
13
|
"declarationMap": false,
|
|
14
14
|
"sourceMap": false,
|
|
15
|
-
"types": [
|
|
16
|
-
"node"
|
|
17
|
-
],
|
|
15
|
+
"types": ["node"],
|
|
18
16
|
"resolveJsonModule": true
|
|
19
17
|
},
|
|
20
|
-
"include": [
|
|
21
|
-
|
|
22
|
-
],
|
|
23
|
-
"exclude": [
|
|
24
|
-
"dist",
|
|
25
|
-
"node_modules"
|
|
26
|
-
]
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["dist", "node_modules"]
|
|
27
20
|
}
|