@webstir-io/webstir 0.1.1 → 0.1.3
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 +13 -0
- package/assets/deployment/docker/.dockerignore +7 -0
- package/assets/deployment/docker/Dockerfile +17 -0
- package/assets/deployment/docker/README.md +44 -0
- package/assets/deployment/docker/example.env +3 -0
- package/assets/features/client_nav/client_nav.ts +369 -264
- package/assets/features/client_nav/document_navigation.ts +344 -0
- package/assets/features/client_nav/form_enhancement.ts +275 -0
- package/assets/templates/api/src/backend/index.ts +71 -10
- package/assets/templates/api/src/backend/tsconfig.json +6 -1
- package/assets/templates/full/src/backend/index.ts +71 -10
- package/assets/templates/full/src/backend/module.ts +515 -0
- package/assets/templates/full/src/backend/tests/progressive-enhancement.test.ts +180 -0
- package/assets/templates/full/src/backend/tsconfig.json +6 -1
- package/assets/templates/full/src/frontend/app/scripts/features/client-nav.ts +574 -0
- package/assets/templates/full/src/frontend/app/scripts/features/document-navigation.ts +344 -0
- package/assets/templates/full/src/frontend/app/scripts/features/form-enhancement.ts +275 -0
- package/assets/templates/full/src/frontend/pages/home/index.css +8 -0
- package/assets/templates/full/src/frontend/pages/home/index.html +6 -1
- package/assets/templates/full/src/frontend/pages/home/tests/home.test.ts +12 -2
- package/assets/templates/spa/src/frontend/pages/home/tests/home.test.ts +10 -2
- package/package.json +31 -13
- package/scripts/check-feature-projections.mjs +87 -0
- package/scripts/check-full-demo-sync.mjs +89 -0
- package/scripts/check-package-install.mjs +537 -0
- package/scripts/check-standalone-install.mjs +221 -0
- package/scripts/pack-standalone.mjs +52 -28
- package/scripts/publish.sh +9 -0
- package/scripts/run-tests.mjs +103 -0
- package/scripts/sync-assets.mjs +175 -17
- package/src/add-backend-compat.ts +628 -0
- package/src/add-backend.ts +155 -27
- package/src/add.ts +111 -4
- package/src/agent.ts +393 -0
- package/src/api-watch.ts +7 -4
- package/src/backend-inspect.ts +70 -2
- package/src/backend-runtime.ts +22 -14
- package/src/build.ts +1 -3
- package/src/bun-generated-frontend-watch.ts +209 -0
- package/src/bun-globals.d.ts +23 -0
- package/src/bun-spa-document.ts +310 -0
- package/src/bun-spa-routes.ts +159 -0
- package/src/bun-spa-watch.ts +29 -0
- package/src/bun-ssg-watch.ts +304 -0
- package/src/cli.ts +381 -50
- package/src/compile-tests.ts +37 -29
- package/src/dev-server.ts +215 -144
- package/src/doctor.ts +164 -0
- package/src/enable-assets.ts +18 -1
- package/src/enable.ts +133 -41
- package/src/execute.ts +30 -4
- package/src/external-workspace.ts +178 -0
- package/src/format.ts +296 -17
- package/src/frontend-inspect.ts +32 -0
- package/src/frontend-watch.ts +27 -102
- package/src/full-watch.ts +13 -18
- package/src/index.ts +7 -0
- package/src/init-assets.ts +41 -11
- package/src/init.ts +85 -71
- package/src/inspect.ts +112 -0
- package/src/mcp/run-cli-json.ts +46 -0
- package/src/mcp/server.ts +307 -0
- package/src/operations.ts +176 -0
- package/src/providers.ts +20 -18
- package/src/refresh.ts +29 -3
- package/src/repair.ts +110 -43
- package/src/runtime-filter.ts +41 -0
- package/src/runtime.ts +1 -1
- package/src/smoke.ts +48 -16
- package/src/test.ts +54 -16
- package/src/testing-runtime.ts +273 -0
- package/src/types.ts +1 -4
- package/src/watch-events.ts +46 -17
- package/src/watch.ts +25 -14
- package/src/workspace-lock.ts +207 -0
- package/src/workspace-watcher.ts +10 -6
- package/src/workspace.ts +4 -2
- package/src/watch-daemon-client.ts +0 -171
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { access, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { constants as fsConstants } from 'node:fs';
|
|
7
|
+
|
|
8
|
+
const scriptRoot = path.dirname(new URL(import.meta.url).pathname);
|
|
9
|
+
const packageRoot = path.resolve(scriptRoot, '..');
|
|
10
|
+
const repoRoot = path.resolve(packageRoot, '..', '..');
|
|
11
|
+
const moduleContractPackageRoot = path.join(repoRoot, 'packages', 'contracts', 'module-contract');
|
|
12
|
+
const testingContractPackageRoot = path.join(repoRoot, 'packages', 'contracts', 'testing-contract');
|
|
13
|
+
const backendPackageRoot = path.join(repoRoot, 'packages', 'tooling', 'webstir-backend');
|
|
14
|
+
const frontendPackageRoot = path.join(repoRoot, 'packages', 'tooling', 'webstir-frontend');
|
|
15
|
+
const testingPackageRoot = path.join(repoRoot, 'packages', 'tooling', 'webstir-testing');
|
|
16
|
+
|
|
17
|
+
async function main() {
|
|
18
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'webstir-package-install-'));
|
|
19
|
+
const installRoot = path.join(tempRoot, 'install-root');
|
|
20
|
+
const workspaceRoot = path.join(tempRoot, 'site');
|
|
21
|
+
const apiWorkspaceRoot = path.join(tempRoot, 'api-site');
|
|
22
|
+
const addTestTarget = 'frontend/pages/home/package-install-smoke';
|
|
23
|
+
const addRouteTarget = 'package-install-accounts';
|
|
24
|
+
const addJobTarget = 'package-install-nightly';
|
|
25
|
+
const addRoutePath = '/api/package-install-accounts';
|
|
26
|
+
const addJobSchedule = '0 0 * * *';
|
|
27
|
+
const addJobDescription = 'Package install nightly run';
|
|
28
|
+
const addJobPriority = 5;
|
|
29
|
+
const addedTestPath = path.join(
|
|
30
|
+
workspaceRoot,
|
|
31
|
+
'src',
|
|
32
|
+
'frontend',
|
|
33
|
+
'pages',
|
|
34
|
+
'home',
|
|
35
|
+
'tests',
|
|
36
|
+
'package-install-smoke.test.ts',
|
|
37
|
+
);
|
|
38
|
+
const addedJobPath = path.join(workspaceRoot, 'src', 'backend', 'jobs', addJobTarget, 'index.ts');
|
|
39
|
+
const existingTarballs = await listTarballs(packageRoot);
|
|
40
|
+
const localPackageSpecs = {
|
|
41
|
+
'@webstir-io/module-contract': `file:${moduleContractPackageRoot}`,
|
|
42
|
+
'@webstir-io/testing-contract': `file:${testingContractPackageRoot}`,
|
|
43
|
+
'@webstir-io/webstir-backend': `file:${backendPackageRoot}`,
|
|
44
|
+
'@webstir-io/webstir-frontend': `file:${frontendPackageRoot}`,
|
|
45
|
+
'@webstir-io/webstir-testing': `file:${testingPackageRoot}`,
|
|
46
|
+
};
|
|
47
|
+
let tarballPath = null;
|
|
48
|
+
let keepWorkspace = false;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await mkdir(installRoot, { recursive: true });
|
|
52
|
+
await writeInstallRootManifest(installRoot);
|
|
53
|
+
|
|
54
|
+
tarballPath = await packPackageTarball();
|
|
55
|
+
console.log(`[webstir][install-smoke] tarball ${tarballPath}`);
|
|
56
|
+
|
|
57
|
+
await writeInstallRootManifest(installRoot, {
|
|
58
|
+
webstirTarballPath: tarballPath,
|
|
59
|
+
localPackageSpecs,
|
|
60
|
+
});
|
|
61
|
+
await run(['bun', 'install'], installRoot);
|
|
62
|
+
|
|
63
|
+
const cliPath = path.join(installRoot, 'node_modules', '.bin', 'webstir');
|
|
64
|
+
await assertExists(cliPath, 'installed CLI binary');
|
|
65
|
+
|
|
66
|
+
await run([cliPath, 'init', 'full', workspaceRoot], installRoot);
|
|
67
|
+
await assertPackageManagedBackendScaffold(workspaceRoot, { expectModule: true });
|
|
68
|
+
|
|
69
|
+
await assertExists(
|
|
70
|
+
path.join(workspaceRoot, 'package.json'),
|
|
71
|
+
'scaffolded workspace package.json',
|
|
72
|
+
);
|
|
73
|
+
await assertPublishedDependencySpecs(installRoot, workspaceRoot, [
|
|
74
|
+
'@webstir-io/webstir-frontend',
|
|
75
|
+
'@webstir-io/webstir-backend',
|
|
76
|
+
'@webstir-io/webstir-testing',
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
await writeWorkspaceLocalPackageOverrides(workspaceRoot, localPackageSpecs);
|
|
80
|
+
await run(['bun', 'install'], workspaceRoot);
|
|
81
|
+
await installLocalBackendPackage(
|
|
82
|
+
workspaceRoot,
|
|
83
|
+
localPackageSpecs['@webstir-io/webstir-backend'],
|
|
84
|
+
);
|
|
85
|
+
await assertExists(
|
|
86
|
+
path.join(workspaceRoot, 'node_modules', '.bin', 'webstir-backend-deploy'),
|
|
87
|
+
'workspace deploy runner binary',
|
|
88
|
+
);
|
|
89
|
+
const addTestOutput = await run(
|
|
90
|
+
[cliPath, 'add-test', addTestTarget, '--workspace', workspaceRoot],
|
|
91
|
+
workspaceRoot,
|
|
92
|
+
);
|
|
93
|
+
if (!addTestOutput.includes('[webstir] add-test complete')) {
|
|
94
|
+
throw new Error(`Expected add-test summary in output, received:\n${addTestOutput}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const addRouteOutput = await run(
|
|
98
|
+
[
|
|
99
|
+
cliPath,
|
|
100
|
+
'add-route',
|
|
101
|
+
addRouteTarget,
|
|
102
|
+
'--method',
|
|
103
|
+
'GET',
|
|
104
|
+
'--path',
|
|
105
|
+
addRoutePath,
|
|
106
|
+
'--workspace',
|
|
107
|
+
workspaceRoot,
|
|
108
|
+
],
|
|
109
|
+
workspaceRoot,
|
|
110
|
+
);
|
|
111
|
+
if (!addRouteOutput.includes('[webstir] add-route complete')) {
|
|
112
|
+
throw new Error(`Expected add-route summary in output, received:\n${addRouteOutput}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const addJobOutput = await run(
|
|
116
|
+
[
|
|
117
|
+
cliPath,
|
|
118
|
+
'add-job',
|
|
119
|
+
addJobTarget,
|
|
120
|
+
'--schedule',
|
|
121
|
+
addJobSchedule,
|
|
122
|
+
'--description',
|
|
123
|
+
addJobDescription,
|
|
124
|
+
'--priority',
|
|
125
|
+
String(addJobPriority),
|
|
126
|
+
'--workspace',
|
|
127
|
+
workspaceRoot,
|
|
128
|
+
],
|
|
129
|
+
workspaceRoot,
|
|
130
|
+
);
|
|
131
|
+
if (!addJobOutput.includes('[webstir] add-job complete')) {
|
|
132
|
+
throw new Error(`Expected add-job summary in output, received:\n${addJobOutput}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await assertExists(addedTestPath, 'scaffolded add-test file');
|
|
136
|
+
await assertFileContains(addedTestPath, "test('sample passes'", 'sample test body');
|
|
137
|
+
await assertExists(addedJobPath, 'scaffolded add-job file');
|
|
138
|
+
await assertFileContains(
|
|
139
|
+
addedJobPath,
|
|
140
|
+
'// Generated by webstir add-job',
|
|
141
|
+
'add-job scaffold body',
|
|
142
|
+
);
|
|
143
|
+
await assertManifestEntries(workspaceRoot, {
|
|
144
|
+
routeName: addRouteTarget,
|
|
145
|
+
routePath: addRoutePath,
|
|
146
|
+
jobName: addJobTarget,
|
|
147
|
+
jobSchedule: addJobSchedule,
|
|
148
|
+
jobDescription: addJobDescription,
|
|
149
|
+
jobPriority: addJobPriority,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await run([cliPath, 'build', '--workspace', workspaceRoot], workspaceRoot);
|
|
153
|
+
|
|
154
|
+
await assertExists(
|
|
155
|
+
path.join(workspaceRoot, 'build', 'backend', 'index.js'),
|
|
156
|
+
'backend build output',
|
|
157
|
+
);
|
|
158
|
+
await assertExists(
|
|
159
|
+
path.join(workspaceRoot, 'build', 'backend', 'jobs', addJobTarget, 'index.js'),
|
|
160
|
+
'add-job build output',
|
|
161
|
+
);
|
|
162
|
+
await assertExists(path.join(workspaceRoot, 'build', 'frontend'), 'frontend build output');
|
|
163
|
+
await assertExists(
|
|
164
|
+
path.join(workspaceRoot, '.webstir', 'backend-outputs.json'),
|
|
165
|
+
'backend outputs cache',
|
|
166
|
+
);
|
|
167
|
+
await assertExists(
|
|
168
|
+
path.join(workspaceRoot, '.webstir', 'backend-manifest-digest.json'),
|
|
169
|
+
'backend manifest digest',
|
|
170
|
+
);
|
|
171
|
+
await assertExists(
|
|
172
|
+
path.join(workspaceRoot, '.webstir', 'frontend-manifest.json'),
|
|
173
|
+
'frontend manifest',
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const inspectOutput = await run(
|
|
177
|
+
[cliPath, 'backend-inspect', '--workspace', workspaceRoot],
|
|
178
|
+
workspaceRoot,
|
|
179
|
+
);
|
|
180
|
+
if (!inspectOutput.includes('[webstir] backend-inspect complete')) {
|
|
181
|
+
throw new Error(`Expected backend-inspect summary in output, received:\n${inspectOutput}`);
|
|
182
|
+
}
|
|
183
|
+
if (!inspectOutput.includes(`GET ${addRoutePath} (${addRouteTarget})`)) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`Expected backend-inspect route summary in output, received:\n${inspectOutput}`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
if (
|
|
189
|
+
!inspectOutput.includes(
|
|
190
|
+
`${addJobTarget} (schedule: ${addJobSchedule}, description: ${addJobDescription}, priority: ${addJobPriority})`,
|
|
191
|
+
)
|
|
192
|
+
) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
`Expected backend-inspect job summary in output, received:\n${inspectOutput}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const doctorOutput = await run(
|
|
199
|
+
[cliPath, 'doctor', '--workspace', workspaceRoot],
|
|
200
|
+
workspaceRoot,
|
|
201
|
+
);
|
|
202
|
+
if (!doctorOutput.includes('[webstir] doctor complete')) {
|
|
203
|
+
throw new Error(`Expected doctor summary in output, received:\n${doctorOutput}`);
|
|
204
|
+
}
|
|
205
|
+
if (!doctorOutput.includes('healthy: true')) {
|
|
206
|
+
throw new Error(`Expected healthy doctor status in output, received:\n${doctorOutput}`);
|
|
207
|
+
}
|
|
208
|
+
if (!doctorOutput.includes('issues: none')) {
|
|
209
|
+
throw new Error(`Expected zero doctor issues in output, received:\n${doctorOutput}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const operationsOutput = await run([cliPath, 'operations', '--json'], workspaceRoot);
|
|
213
|
+
if (!operationsOutput.includes('"command": "operations"')) {
|
|
214
|
+
throw new Error(`Expected operations JSON output, received:\n${operationsOutput}`);
|
|
215
|
+
}
|
|
216
|
+
if (!operationsOutput.includes('"id": "doctor"')) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Expected doctor operation in catalog output, received:\n${operationsOutput}`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const frontendInspectOutput = await run(
|
|
223
|
+
[cliPath, 'frontend-inspect', '--json', '--workspace', workspaceRoot],
|
|
224
|
+
workspaceRoot,
|
|
225
|
+
);
|
|
226
|
+
if (!frontendInspectOutput.includes('"command": "frontend-inspect"')) {
|
|
227
|
+
throw new Error(`Expected frontend-inspect JSON output, received:\n${frontendInspectOutput}`);
|
|
228
|
+
}
|
|
229
|
+
if (!frontendInspectOutput.includes('"pages"')) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
`Expected frontend-inspect page data in output, received:\n${frontendInspectOutput}`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const inspectOutputJson = await run(
|
|
236
|
+
[cliPath, 'inspect', '--json', '--workspace', workspaceRoot],
|
|
237
|
+
workspaceRoot,
|
|
238
|
+
);
|
|
239
|
+
if (!inspectOutputJson.includes('"command": "inspect"')) {
|
|
240
|
+
throw new Error(`Expected inspect JSON output, received:\n${inspectOutputJson}`);
|
|
241
|
+
}
|
|
242
|
+
if (!inspectOutputJson.includes('"frontend"')) {
|
|
243
|
+
throw new Error(`Expected inspect frontend data in output, received:\n${inspectOutputJson}`);
|
|
244
|
+
}
|
|
245
|
+
if (!inspectOutputJson.includes('"backend"')) {
|
|
246
|
+
throw new Error(`Expected inspect backend data in output, received:\n${inspectOutputJson}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const agentInspectOutput = await run(
|
|
250
|
+
[cliPath, 'agent', 'inspect', '--json', '--workspace', workspaceRoot],
|
|
251
|
+
workspaceRoot,
|
|
252
|
+
);
|
|
253
|
+
if (!agentInspectOutput.includes('"command": "agent"')) {
|
|
254
|
+
throw new Error(`Expected agent JSON output, received:\n${agentInspectOutput}`);
|
|
255
|
+
}
|
|
256
|
+
if (!agentInspectOutput.includes('"goal": "inspect"')) {
|
|
257
|
+
throw new Error(`Expected inspect goal in agent output, received:\n${agentInspectOutput}`);
|
|
258
|
+
}
|
|
259
|
+
if (!agentInspectOutput.includes('"success": true')) {
|
|
260
|
+
throw new Error(`Expected successful agent inspect output, received:\n${agentInspectOutput}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const testOutput = await run(
|
|
264
|
+
[cliPath, 'test', '--workspace', workspaceRoot, '--runtime', 'frontend'],
|
|
265
|
+
workspaceRoot,
|
|
266
|
+
);
|
|
267
|
+
if (!testOutput.includes('[webstir] test complete')) {
|
|
268
|
+
throw new Error(`Expected test summary in output, received:\n${testOutput}`);
|
|
269
|
+
}
|
|
270
|
+
if (!testOutput.includes('runtime: frontend')) {
|
|
271
|
+
throw new Error(`Expected frontend runtime summary in output, received:\n${testOutput}`);
|
|
272
|
+
}
|
|
273
|
+
if (!testOutput.includes('failed: 0')) {
|
|
274
|
+
throw new Error(`Expected zero frontend test failures in output, received:\n${testOutput}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
await run([cliPath, 'init', 'api', apiWorkspaceRoot], installRoot);
|
|
278
|
+
await assertPackageManagedBackendScaffold(apiWorkspaceRoot);
|
|
279
|
+
await assertPublishedDependencySpecs(installRoot, apiWorkspaceRoot, [
|
|
280
|
+
'@webstir-io/webstir-backend',
|
|
281
|
+
'@webstir-io/webstir-testing',
|
|
282
|
+
]);
|
|
283
|
+
await writeWorkspaceLocalPackageOverrides(apiWorkspaceRoot, localPackageSpecs);
|
|
284
|
+
await run(['bun', 'install'], apiWorkspaceRoot);
|
|
285
|
+
await installLocalBackendPackage(
|
|
286
|
+
apiWorkspaceRoot,
|
|
287
|
+
localPackageSpecs['@webstir-io/webstir-backend'],
|
|
288
|
+
);
|
|
289
|
+
await assertExists(
|
|
290
|
+
path.join(apiWorkspaceRoot, 'node_modules', '.bin', 'webstir-backend-deploy'),
|
|
291
|
+
'api workspace deploy runner binary',
|
|
292
|
+
);
|
|
293
|
+
await run([cliPath, 'build', '--workspace', apiWorkspaceRoot], apiWorkspaceRoot);
|
|
294
|
+
await assertExists(
|
|
295
|
+
path.join(apiWorkspaceRoot, 'build', 'backend', 'index.js'),
|
|
296
|
+
'api backend build output',
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
console.log('[webstir][install-smoke] package install smoke passed');
|
|
300
|
+
} catch (error) {
|
|
301
|
+
keepWorkspace = (process.env.WEBSTIR_INSTALL_SMOKE_KEEP ?? '').toLowerCase() === '1';
|
|
302
|
+
if (keepWorkspace) {
|
|
303
|
+
console.error(`[webstir][install-smoke] preserving failed workspace at ${tempRoot}`);
|
|
304
|
+
}
|
|
305
|
+
throw error;
|
|
306
|
+
} finally {
|
|
307
|
+
if (tarballPath) {
|
|
308
|
+
await cleanupGeneratedTarball(tarballPath, existingTarballs);
|
|
309
|
+
}
|
|
310
|
+
if (!keepWorkspace) {
|
|
311
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function assertPublishedDependencySpecs(installRoot, workspaceRoot, packageNames) {
|
|
317
|
+
const workspacePackageJson = JSON.parse(
|
|
318
|
+
await readFile(path.join(workspaceRoot, 'package.json'), 'utf8'),
|
|
319
|
+
);
|
|
320
|
+
const dependencies = workspacePackageJson.dependencies ?? {};
|
|
321
|
+
|
|
322
|
+
for (const packageName of packageNames) {
|
|
323
|
+
const dependencySpec = dependencies[packageName];
|
|
324
|
+
if (!dependencySpec) {
|
|
325
|
+
throw new Error(
|
|
326
|
+
`Expected scaffolded dependency ${packageName} in ${path.join(workspaceRoot, 'package.json')}`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
if (dependencySpec === 'workspace:*') {
|
|
330
|
+
throw new Error(`Expected published dependency for ${packageName}, received workspace:*`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const installedVersion = await readInstalledPackageVersion(installRoot, packageName);
|
|
334
|
+
if (dependencySpec !== `^${installedVersion}`) {
|
|
335
|
+
throw new Error(
|
|
336
|
+
`Expected ${packageName} dependency spec ^${installedVersion}, received ${dependencySpec}`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function assertManifestEntries(workspaceRoot, expected) {
|
|
343
|
+
const workspacePackageJson = JSON.parse(
|
|
344
|
+
await readFile(path.join(workspaceRoot, 'package.json'), 'utf8'),
|
|
345
|
+
);
|
|
346
|
+
const moduleManifest = workspacePackageJson.webstir?.moduleManifest ?? {};
|
|
347
|
+
const routes = Array.isArray(moduleManifest.routes) ? moduleManifest.routes : [];
|
|
348
|
+
const jobs = Array.isArray(moduleManifest.jobs) ? moduleManifest.jobs : [];
|
|
349
|
+
|
|
350
|
+
const route = routes.find((entry) => entry?.name === expected.routeName);
|
|
351
|
+
if (!route || route.method !== 'GET' || route.path !== expected.routePath) {
|
|
352
|
+
throw new Error(
|
|
353
|
+
`Expected backend route manifest entry for ${expected.routeName}, received:\n${JSON.stringify(route, null, 2)}`,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const job = jobs.find((entry) => entry?.name === expected.jobName);
|
|
358
|
+
if (
|
|
359
|
+
!job ||
|
|
360
|
+
job.schedule !== expected.jobSchedule ||
|
|
361
|
+
job.description !== expected.jobDescription ||
|
|
362
|
+
job.priority !== expected.jobPriority
|
|
363
|
+
) {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`Expected backend job manifest entry for ${expected.jobName}, received:\n${JSON.stringify(job, null, 2)}`,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function assertPackageManagedBackendScaffold(workspaceRoot, options = {}) {
|
|
371
|
+
const backendRoot = path.join(workspaceRoot, 'src', 'backend');
|
|
372
|
+
const backendIndexPath = path.join(backendRoot, 'index.ts');
|
|
373
|
+
const backendIndex = await readFile(backendIndexPath, 'utf8');
|
|
374
|
+
|
|
375
|
+
if (!backendIndex.includes('createDefaultBunBackendBootstrap')) {
|
|
376
|
+
throw new Error(`Expected package-managed Bun bootstrap helper in ${backendIndexPath}`);
|
|
377
|
+
}
|
|
378
|
+
if (backendIndex.includes('http.createServer')) {
|
|
379
|
+
throw new Error(`Expected thinner backend entrypoint in ${backendIndexPath}`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
await assertMissing(path.join(backendRoot, 'server', 'bun.ts'), 'legacy Bun backend wrapper');
|
|
383
|
+
await assertMissing(
|
|
384
|
+
path.join(backendRoot, 'runtime'),
|
|
385
|
+
'legacy backend runtime wrapper directory',
|
|
386
|
+
);
|
|
387
|
+
if (options.expectModule) {
|
|
388
|
+
await assertExists(path.join(backendRoot, 'module.ts'), 'backend module definition');
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function installLocalBackendPackage(workspaceRoot, backendPackageSpec) {
|
|
393
|
+
await run(['bun', 'remove', '@webstir-io/webstir-backend'], workspaceRoot);
|
|
394
|
+
await run(['bun', 'add', backendPackageSpec], workspaceRoot);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function writeWorkspaceLocalPackageOverrides(workspaceRoot, localPackageSpecs) {
|
|
398
|
+
const packageJsonPath = path.join(workspaceRoot, 'package.json');
|
|
399
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
|
|
400
|
+
packageJson.overrides = {
|
|
401
|
+
...(packageJson.overrides ?? {}),
|
|
402
|
+
...localPackageSpecs,
|
|
403
|
+
};
|
|
404
|
+
await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function writeInstallRootManifest(installRoot, options = {}) {
|
|
408
|
+
const dependencies = { ...(options.localPackageSpecs ?? {}) };
|
|
409
|
+
if (options.webstirTarballPath) {
|
|
410
|
+
dependencies['@webstir-io/webstir'] = options.webstirTarballPath;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const packageJson = {
|
|
414
|
+
name: 'webstir-package-smoke',
|
|
415
|
+
private: true,
|
|
416
|
+
...(Object.keys(dependencies).length > 0 ? { dependencies } : {}),
|
|
417
|
+
...(options.localPackageSpecs
|
|
418
|
+
? {
|
|
419
|
+
overrides: options.localPackageSpecs,
|
|
420
|
+
}
|
|
421
|
+
: {}),
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
await writeFile(
|
|
425
|
+
path.join(installRoot, 'package.json'),
|
|
426
|
+
`${JSON.stringify(packageJson, null, 2)}\n`,
|
|
427
|
+
'utf8',
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function packPackageTarball(root = packageRoot) {
|
|
432
|
+
const output =
|
|
433
|
+
root === packageRoot
|
|
434
|
+
? await run(['bun', 'run', 'pack:local'], root)
|
|
435
|
+
: await run(['bun', 'pm', 'pack'], root);
|
|
436
|
+
return resolvePackedTarballPath(output, root);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function resolvePackedTarballPath(output, root) {
|
|
440
|
+
const tarballPath = output
|
|
441
|
+
.split(/\r?\n/)
|
|
442
|
+
.map((line) => line.trim())
|
|
443
|
+
.filter((line) => line.endsWith('.tgz'))
|
|
444
|
+
.at(-1);
|
|
445
|
+
|
|
446
|
+
if (!tarballPath) {
|
|
447
|
+
throw new Error(`Unable to determine package tarball path from pack output:\n${output}`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return path.resolve(root, tarballPath);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function readInstalledPackageVersion(installRoot, packageName) {
|
|
454
|
+
const packageJsonPath = path.join(
|
|
455
|
+
installRoot,
|
|
456
|
+
'node_modules',
|
|
457
|
+
...packageName.split('/'),
|
|
458
|
+
'package.json',
|
|
459
|
+
);
|
|
460
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
|
|
461
|
+
if (!packageJson.version) {
|
|
462
|
+
throw new Error(`Missing version in ${packageJsonPath}`);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return packageJson.version;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function assertExists(targetPath, label) {
|
|
469
|
+
try {
|
|
470
|
+
await access(targetPath, fsConstants.F_OK);
|
|
471
|
+
} catch {
|
|
472
|
+
throw new Error(`Expected ${label} at ${targetPath}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function assertFileContains(targetPath, expectedSnippet, label) {
|
|
477
|
+
const content = await readFile(targetPath, 'utf8');
|
|
478
|
+
if (!content.includes(expectedSnippet)) {
|
|
479
|
+
throw new Error(`Expected ${label} in ${targetPath}`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function assertMissing(targetPath, label) {
|
|
484
|
+
try {
|
|
485
|
+
await access(targetPath, fsConstants.F_OK);
|
|
486
|
+
} catch {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
throw new Error(`Expected ${label} to be absent at ${targetPath}`);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async function listTarballs(root) {
|
|
494
|
+
try {
|
|
495
|
+
return new Set((await readdir(root)).filter((entry) => entry.endsWith('.tgz')));
|
|
496
|
+
} catch {
|
|
497
|
+
return new Set();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async function cleanupGeneratedTarball(tarballPath, existingTarballs) {
|
|
502
|
+
const tarballName = path.basename(tarballPath);
|
|
503
|
+
if (existingTarballs.has(tarballName)) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
await rm(tarballPath, { force: true });
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function run(command, cwd) {
|
|
511
|
+
const proc = Bun.spawn({
|
|
512
|
+
cmd: command,
|
|
513
|
+
cwd,
|
|
514
|
+
stdout: 'pipe',
|
|
515
|
+
stderr: 'pipe',
|
|
516
|
+
env: process.env,
|
|
517
|
+
});
|
|
518
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
519
|
+
new Response(proc.stdout).text(),
|
|
520
|
+
new Response(proc.stderr).text(),
|
|
521
|
+
proc.exited,
|
|
522
|
+
]);
|
|
523
|
+
|
|
524
|
+
if (exitCode !== 0) {
|
|
525
|
+
const detail = [stdout.trim(), stderr.trim()].filter(Boolean).join('\n');
|
|
526
|
+
throw new Error(
|
|
527
|
+
`Command failed (${exitCode}): ${command.join(' ')}${detail ? `\n${detail}` : ''}`,
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return stdout;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
main().catch((error) => {
|
|
535
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
536
|
+
process.exit(1);
|
|
537
|
+
});
|