@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,253 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
|
+
const templatesRoot = path.join(packageRoot, 'templates', 'backend', 'jobs');
|
|
11
|
+
|
|
12
|
+
async function createWorkspace(prefix = 'webstir-backend-jobs-') {
|
|
13
|
+
const workspace = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
|
14
|
+
await fs.mkdir(path.join(workspace, 'src', 'backend', 'jobs'), { recursive: true });
|
|
15
|
+
await fs.copyFile(
|
|
16
|
+
path.join(packageRoot, 'templates', 'backend', 'env.ts'),
|
|
17
|
+
path.join(workspace, 'src', 'backend', 'env.ts'),
|
|
18
|
+
);
|
|
19
|
+
await fs.copyFile(
|
|
20
|
+
path.join(templatesRoot, 'runtime.ts'),
|
|
21
|
+
path.join(workspace, 'src', 'backend', 'jobs', 'runtime.ts'),
|
|
22
|
+
);
|
|
23
|
+
await fs.copyFile(
|
|
24
|
+
path.join(templatesRoot, 'scheduler.ts'),
|
|
25
|
+
path.join(workspace, 'src', 'backend', 'jobs', 'scheduler.ts'),
|
|
26
|
+
);
|
|
27
|
+
return workspace;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function writePackage(workspace, jobs) {
|
|
31
|
+
await fs.writeFile(
|
|
32
|
+
path.join(workspace, 'package.json'),
|
|
33
|
+
JSON.stringify({ type: 'module', webstir: { moduleManifest: { jobs } } }, null, 2),
|
|
34
|
+
'utf8',
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function writeJob(workspace, name, source) {
|
|
39
|
+
const dir = path.join(workspace, 'src', 'backend', 'jobs', name);
|
|
40
|
+
await fs.mkdir(dir, { recursive: true });
|
|
41
|
+
await fs.writeFile(path.join(dir, 'index.ts'), source, 'utf8');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function runScheduler(workspace, args, options = {}) {
|
|
45
|
+
return collectProcess(startScheduler(workspace, args, options), options);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function startScheduler(workspace, args, options = {}) {
|
|
49
|
+
const child = spawn('bun', [path.join('src', 'backend', 'jobs', 'scheduler.ts'), ...args], {
|
|
50
|
+
cwd: workspace,
|
|
51
|
+
env: { ...process.env, ...options.env },
|
|
52
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
53
|
+
});
|
|
54
|
+
return child;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function collectProcess(child, options = {}) {
|
|
58
|
+
let stdout = '';
|
|
59
|
+
let stderr = '';
|
|
60
|
+
child.stdout.setEncoding('utf8');
|
|
61
|
+
child.stderr.setEncoding('utf8');
|
|
62
|
+
child.stdout.on('data', (chunk) => {
|
|
63
|
+
stdout += chunk;
|
|
64
|
+
});
|
|
65
|
+
child.stderr.on('data', (chunk) => {
|
|
66
|
+
stderr += chunk;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
let timeout;
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
if (options.timeoutMs) {
|
|
72
|
+
timeout = setTimeout(() => {
|
|
73
|
+
child.kill('SIGTERM');
|
|
74
|
+
reject(new Error(`scheduler timed out\nstdout:\n${stdout}\nstderr:\n${stderr}`));
|
|
75
|
+
}, options.timeoutMs);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
child.once('error', reject);
|
|
79
|
+
child.once('exit', (code, signal) => {
|
|
80
|
+
clearTimeout(timeout);
|
|
81
|
+
resolve({ code, signal, stdout, stderr });
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
test('jobs scheduler lists stable text and JSON metadata', async () => {
|
|
87
|
+
const workspace = await createWorkspace();
|
|
88
|
+
try {
|
|
89
|
+
await writePackage(workspace, [
|
|
90
|
+
{
|
|
91
|
+
name: 'nightly',
|
|
92
|
+
schedule: 'rate(5 minutes)',
|
|
93
|
+
description: 'Refresh snapshots',
|
|
94
|
+
priority: 10,
|
|
95
|
+
},
|
|
96
|
+
]);
|
|
97
|
+
await writeJob(workspace, 'nightly', 'export async function run() {}\n');
|
|
98
|
+
|
|
99
|
+
const listed = await runScheduler(workspace, ['--list']);
|
|
100
|
+
assert.equal(listed.code, 0);
|
|
101
|
+
assert.match(listed.stdout, /- nightly \(rate\(5 minutes\)\).*Refresh snapshots/);
|
|
102
|
+
|
|
103
|
+
const json = await runScheduler(workspace, ['--json']);
|
|
104
|
+
assert.equal(json.code, 0);
|
|
105
|
+
assert.deepEqual(JSON.parse(json.stdout), [
|
|
106
|
+
{
|
|
107
|
+
name: 'nightly',
|
|
108
|
+
schedule: 'rate(5 minutes)',
|
|
109
|
+
description: 'Refresh snapshots',
|
|
110
|
+
priority: 10,
|
|
111
|
+
},
|
|
112
|
+
]);
|
|
113
|
+
} finally {
|
|
114
|
+
await fs.rm(workspace, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('jobs scheduler runs named jobs and reports missing jobs clearly', async () => {
|
|
119
|
+
const workspace = await createWorkspace();
|
|
120
|
+
try {
|
|
121
|
+
await writePackage(workspace, [{ name: 'existing' }]);
|
|
122
|
+
await writeJob(
|
|
123
|
+
workspace,
|
|
124
|
+
'existing',
|
|
125
|
+
'export async function run() { console.log("ran job"); }\n',
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const existing = await runScheduler(workspace, ['--job', 'existing']);
|
|
129
|
+
assert.equal(existing.code, 0);
|
|
130
|
+
assert.match(existing.stdout, /ran job/);
|
|
131
|
+
assert.match(existing.stdout, /\[jobs\] existing completed/);
|
|
132
|
+
|
|
133
|
+
const missing = await runScheduler(workspace, ['--job', 'missing']);
|
|
134
|
+
assert.equal(missing.code, 1);
|
|
135
|
+
assert.match(missing.stderr, /\[jobs\] job 'missing' not found/);
|
|
136
|
+
} finally {
|
|
137
|
+
await fs.rm(workspace, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('jobs scheduler reports missing modules, missing run exports, and thrown errors', async () => {
|
|
142
|
+
const workspace = await createWorkspace();
|
|
143
|
+
try {
|
|
144
|
+
await writePackage(workspace, [
|
|
145
|
+
{ name: 'missing-module' },
|
|
146
|
+
{ name: 'missing-run' },
|
|
147
|
+
{ name: 'throws' },
|
|
148
|
+
]);
|
|
149
|
+
await writeJob(workspace, 'missing-run', 'export const value = 1;\n');
|
|
150
|
+
await writeJob(
|
|
151
|
+
workspace,
|
|
152
|
+
'throws',
|
|
153
|
+
'export async function run() { throw new Error("database unavailable"); }\n',
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const missingModule = await runScheduler(workspace, ['--job', 'missing-module']);
|
|
157
|
+
assert.equal(missingModule.code, 1);
|
|
158
|
+
assert.match(missingModule.stderr, /missing-module failed/);
|
|
159
|
+
assert.match(missingModule.stderr, /unable to load job 'missing-module'/);
|
|
160
|
+
|
|
161
|
+
const missingRun = await runScheduler(workspace, ['--job', 'missing-run']);
|
|
162
|
+
assert.equal(missingRun.code, 1);
|
|
163
|
+
assert.match(missingRun.stderr, /job module must export a run\(\) or default function/);
|
|
164
|
+
|
|
165
|
+
const throws = await runScheduler(workspace, ['--job', 'throws']);
|
|
166
|
+
assert.equal(throws.code, 1);
|
|
167
|
+
assert.match(throws.stderr, /throws failed: database unavailable/);
|
|
168
|
+
} finally {
|
|
169
|
+
await fs.rm(workspace, { recursive: true, force: true });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('jobs scheduler supports @reboot, rate schedules, cron schedules, and unsupported diagnostics', {
|
|
174
|
+
timeout: 10_000,
|
|
175
|
+
}, async () => {
|
|
176
|
+
const workspace = await createWorkspace();
|
|
177
|
+
try {
|
|
178
|
+
await writePackage(workspace, [
|
|
179
|
+
{ name: 'boot', schedule: '@reboot' },
|
|
180
|
+
{ name: 'fast', schedule: 'rate(1 second)' },
|
|
181
|
+
{ name: 'cron', schedule: '* * * * *' },
|
|
182
|
+
{ name: 'bad', schedule: 'every Tuesdayish' },
|
|
183
|
+
]);
|
|
184
|
+
await writeJob(workspace, 'boot', 'export async function run() { console.log("booted"); }\n');
|
|
185
|
+
await writeJob(workspace, 'fast', 'export async function run() { console.log("fast"); }\n');
|
|
186
|
+
await writeJob(workspace, 'cron', 'export async function run() { console.log("cron"); }\n');
|
|
187
|
+
await writeJob(workspace, 'bad', 'export async function run() { console.log("bad"); }\n');
|
|
188
|
+
|
|
189
|
+
const reboot = await runScheduler(workspace, ['--watch', '--job', 'boot']);
|
|
190
|
+
assert.equal(reboot.code, 0);
|
|
191
|
+
assert.match(reboot.stdout, /booted/);
|
|
192
|
+
assert.match(reboot.stdout, /completed @reboot jobs and exiting/);
|
|
193
|
+
|
|
194
|
+
const unsupported = await runScheduler(workspace, ['--watch', '--job', 'bad']);
|
|
195
|
+
assert.equal(unsupported.code, 0);
|
|
196
|
+
assert.match(unsupported.stdout, /unsupported schedule 'every Tuesdayish'/);
|
|
197
|
+
|
|
198
|
+
const rate = startScheduler(workspace, ['--watch', '--job', 'fast']);
|
|
199
|
+
const rateResultPromise = collectProcess(rate, { timeoutMs: 4000 });
|
|
200
|
+
await wait(1500);
|
|
201
|
+
rate.kill('SIGTERM');
|
|
202
|
+
const rateResult = await rateResultPromise;
|
|
203
|
+
assert.equal(rateResult.code, 0);
|
|
204
|
+
assert.match(rateResult.stdout, /watching jobs: fast/);
|
|
205
|
+
assert.match(rateResult.stdout, /fast/);
|
|
206
|
+
assert.match(rateResult.stdout, /received SIGTERM; stopped 1 scheduled job timer/);
|
|
207
|
+
|
|
208
|
+
const cron = startScheduler(workspace, ['--watch', '--job', 'cron']);
|
|
209
|
+
const cronResultPromise = collectProcess(cron, { timeoutMs: 4000 });
|
|
210
|
+
await wait(300);
|
|
211
|
+
cron.kill('SIGTERM');
|
|
212
|
+
const cronResult = await cronResultPromise;
|
|
213
|
+
assert.equal(cronResult.code, 0);
|
|
214
|
+
assert.match(cronResult.stdout, /watching jobs: cron/);
|
|
215
|
+
assert.match(cronResult.stdout, /received SIGTERM; stopped 1 scheduled job timer/);
|
|
216
|
+
} finally {
|
|
217
|
+
await fs.rm(workspace, { recursive: true, force: true });
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('jobs scheduler skips overlapping rate runs', { timeout: 10_000 }, async () => {
|
|
222
|
+
const workspace = await createWorkspace();
|
|
223
|
+
try {
|
|
224
|
+
await writePackage(workspace, [{ name: 'slow', schedule: 'rate(1 second)' }]);
|
|
225
|
+
await writeJob(
|
|
226
|
+
workspace,
|
|
227
|
+
'slow',
|
|
228
|
+
[
|
|
229
|
+
'const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));',
|
|
230
|
+
'export async function run() {',
|
|
231
|
+
' console.log("slow-start");',
|
|
232
|
+
' await sleep(1400);',
|
|
233
|
+
' console.log("slow-end");',
|
|
234
|
+
'}',
|
|
235
|
+
].join('\n'),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const child = startScheduler(workspace, ['--watch', '--job', 'slow']);
|
|
239
|
+
const resultPromise = collectProcess(child, { timeoutMs: 6000 });
|
|
240
|
+
await wait(2300);
|
|
241
|
+
child.kill('SIGTERM');
|
|
242
|
+
const result = await resultPromise;
|
|
243
|
+
assert.equal(result.code, 0);
|
|
244
|
+
assert.match(result.stdout, /slow-start/);
|
|
245
|
+
assert.match(result.stderr, /skipping slow; previous run is still active/);
|
|
246
|
+
} finally {
|
|
247
|
+
await fs.rm(workspace, { recursive: true, force: true });
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
async function wait(delayMs) {
|
|
252
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
253
|
+
}
|
package/tests/manifest.test.js
CHANGED
|
@@ -50,15 +50,19 @@ test('manifest loader honors package overrides', async () => {
|
|
|
50
50
|
name: '@demo/custom',
|
|
51
51
|
version: '2.0.0',
|
|
52
52
|
kind: 'backend',
|
|
53
|
-
capabilities: ['db']
|
|
54
|
-
}
|
|
55
|
-
}
|
|
53
|
+
capabilities: ['db'],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
56
|
};
|
|
57
|
-
await fs.writeFile(
|
|
57
|
+
await fs.writeFile(
|
|
58
|
+
path.join(workspace, 'package.json'),
|
|
59
|
+
JSON.stringify(pkgJson, null, 2),
|
|
60
|
+
'utf8',
|
|
61
|
+
);
|
|
58
62
|
|
|
59
63
|
const env = {
|
|
60
64
|
WEBSTIR_MODULE_MODE: 'build',
|
|
61
|
-
PATH: `${getLocalBinPath()}${path.delimiter}${process.env.PATH ?? ''}
|
|
65
|
+
PATH: `${getLocalBinPath()}${path.delimiter}${process.env.PATH ?? ''}`,
|
|
62
66
|
};
|
|
63
67
|
|
|
64
68
|
const result = await backendProvider.build({ workspaceRoot: workspace, env, incremental: false });
|
|
@@ -76,13 +80,17 @@ test('manifest loader falls back to package name/version when no overrides prese
|
|
|
76
80
|
const pkgJson = {
|
|
77
81
|
name: '@demo/fallback',
|
|
78
82
|
version: '4.5.6',
|
|
79
|
-
type: 'module'
|
|
83
|
+
type: 'module',
|
|
80
84
|
};
|
|
81
|
-
await fs.writeFile(
|
|
85
|
+
await fs.writeFile(
|
|
86
|
+
path.join(workspace, 'package.json'),
|
|
87
|
+
JSON.stringify(pkgJson, null, 2),
|
|
88
|
+
'utf8',
|
|
89
|
+
);
|
|
82
90
|
|
|
83
91
|
const env = {
|
|
84
92
|
WEBSTIR_MODULE_MODE: 'build',
|
|
85
|
-
PATH: `${getLocalBinPath()}${path.delimiter}${process.env.PATH ?? ''}
|
|
93
|
+
PATH: `${getLocalBinPath()}${path.delimiter}${process.env.PATH ?? ''}`,
|
|
86
94
|
};
|
|
87
95
|
|
|
88
96
|
const result = await backendProvider.build({ workspaceRoot: workspace, env, incremental: false });
|
|
@@ -113,12 +121,12 @@ test('manifest loader merges compiled module definition metadata', async () => {
|
|
|
113
121
|
await fs.writeFile(
|
|
114
122
|
path.join(workspace, 'package.json'),
|
|
115
123
|
JSON.stringify({ name: '@demo/fallback-package', version: '0.0.1', type: 'module' }, null, 2),
|
|
116
|
-
'utf8'
|
|
124
|
+
'utf8',
|
|
117
125
|
);
|
|
118
126
|
|
|
119
127
|
const env = {
|
|
120
128
|
WEBSTIR_MODULE_MODE: 'build',
|
|
121
|
-
PATH: `${getLocalBinPath()}${path.delimiter}${process.env.PATH ?? ''}
|
|
129
|
+
PATH: `${getLocalBinPath()}${path.delimiter}${process.env.PATH ?? ''}`,
|
|
122
130
|
};
|
|
123
131
|
|
|
124
132
|
const result = await backendProvider.build({ workspaceRoot: workspace, env, incremental: false });
|
|
@@ -130,6 +138,214 @@ test('manifest loader merges compiled module definition metadata', async () => {
|
|
|
130
138
|
assert.deepEqual(moduleManifest?.capabilities, ['search']);
|
|
131
139
|
});
|
|
132
140
|
|
|
141
|
+
test('manifest loader merges package routes with compiled module routes without duplicating overlaps', async () => {
|
|
142
|
+
const workspace = await createTempWorkspace();
|
|
143
|
+
await seedBackendEntry(workspace);
|
|
144
|
+
|
|
145
|
+
const moduleSource = `export const module = {
|
|
146
|
+
manifest: {
|
|
147
|
+
contractVersion: '1.0.0',
|
|
148
|
+
name: '@demo/with-routes',
|
|
149
|
+
version: '1.2.3',
|
|
150
|
+
kind: 'backend',
|
|
151
|
+
capabilities: ['http'],
|
|
152
|
+
routes: [
|
|
153
|
+
{
|
|
154
|
+
name: 'builtInRoute',
|
|
155
|
+
method: 'GET',
|
|
156
|
+
path: '/demo/built-in',
|
|
157
|
+
summary: 'Built-in route'
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'overlapRoute',
|
|
161
|
+
method: 'GET',
|
|
162
|
+
path: '/demo/overlap',
|
|
163
|
+
summary: 'Built-in overlap route'
|
|
164
|
+
}
|
|
165
|
+
],
|
|
166
|
+
views: []
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
`;
|
|
170
|
+
|
|
171
|
+
await fs.writeFile(path.join(workspace, 'src', 'backend', 'module.ts'), moduleSource, 'utf8');
|
|
172
|
+
await fs.writeFile(
|
|
173
|
+
path.join(workspace, 'package.json'),
|
|
174
|
+
JSON.stringify(
|
|
175
|
+
{
|
|
176
|
+
name: '@demo/routes-package',
|
|
177
|
+
version: '0.0.1',
|
|
178
|
+
type: 'module',
|
|
179
|
+
webstir: {
|
|
180
|
+
moduleManifest: {
|
|
181
|
+
routes: [
|
|
182
|
+
{
|
|
183
|
+
name: 'overlapRouteFromPackage',
|
|
184
|
+
method: 'GET',
|
|
185
|
+
path: '/demo/overlap',
|
|
186
|
+
summary: 'Package overlap route',
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: 'packageOnlyRoute',
|
|
190
|
+
method: 'POST',
|
|
191
|
+
path: '/demo/package-only',
|
|
192
|
+
summary: 'Package-only route',
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
null,
|
|
199
|
+
2,
|
|
200
|
+
),
|
|
201
|
+
'utf8',
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const env = {
|
|
205
|
+
WEBSTIR_MODULE_MODE: 'build',
|
|
206
|
+
PATH: `${getLocalBinPath()}${path.delimiter}${process.env.PATH ?? ''}`,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const result = await backendProvider.build({ workspaceRoot: workspace, env, incremental: false });
|
|
210
|
+
const moduleManifest = result.manifest.module;
|
|
211
|
+
|
|
212
|
+
assert.deepEqual(
|
|
213
|
+
moduleManifest?.routes?.map((route) => ({
|
|
214
|
+
name: route.name,
|
|
215
|
+
method: route.method,
|
|
216
|
+
path: route.path,
|
|
217
|
+
})),
|
|
218
|
+
[
|
|
219
|
+
{ name: 'builtInRoute', method: 'GET', path: '/demo/built-in' },
|
|
220
|
+
{ name: 'overlapRoute', method: 'GET', path: '/demo/overlap' },
|
|
221
|
+
{ name: 'packageOnlyRoute', method: 'POST', path: '/demo/package-only' },
|
|
222
|
+
],
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('manifest loader merges package jobs with compiled module jobs without duplicating names', async () => {
|
|
227
|
+
const workspace = await createTempWorkspace();
|
|
228
|
+
await seedBackendEntry(workspace);
|
|
229
|
+
|
|
230
|
+
const moduleSource = `export const module = {
|
|
231
|
+
manifest: {
|
|
232
|
+
contractVersion: '1.0.0',
|
|
233
|
+
name: '@demo/with-jobs',
|
|
234
|
+
version: '1.2.3',
|
|
235
|
+
kind: 'backend',
|
|
236
|
+
jobs: [
|
|
237
|
+
{
|
|
238
|
+
name: 'nightly',
|
|
239
|
+
schedule: '0 0 * * *'
|
|
240
|
+
}
|
|
241
|
+
],
|
|
242
|
+
routes: [],
|
|
243
|
+
views: []
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
`;
|
|
247
|
+
|
|
248
|
+
await fs.writeFile(path.join(workspace, 'src', 'backend', 'module.ts'), moduleSource, 'utf8');
|
|
249
|
+
await fs.writeFile(
|
|
250
|
+
path.join(workspace, 'package.json'),
|
|
251
|
+
JSON.stringify(
|
|
252
|
+
{
|
|
253
|
+
name: '@demo/jobs-package',
|
|
254
|
+
version: '0.0.1',
|
|
255
|
+
type: 'module',
|
|
256
|
+
webstir: {
|
|
257
|
+
moduleManifest: {
|
|
258
|
+
jobs: [
|
|
259
|
+
{
|
|
260
|
+
name: 'nightly',
|
|
261
|
+
schedule: 'rate(1 hour)',
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: 'session-cleanup',
|
|
265
|
+
schedule: 'rate(5 minutes)',
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
null,
|
|
272
|
+
2,
|
|
273
|
+
),
|
|
274
|
+
'utf8',
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const env = {
|
|
278
|
+
WEBSTIR_MODULE_MODE: 'build',
|
|
279
|
+
PATH: `${getLocalBinPath()}${path.delimiter}${process.env.PATH ?? ''}`,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const result = await backendProvider.build({ workspaceRoot: workspace, env, incremental: false });
|
|
283
|
+
const moduleManifest = result.manifest.module;
|
|
284
|
+
|
|
285
|
+
assert.deepEqual(
|
|
286
|
+
moduleManifest?.jobs?.map((job) => ({
|
|
287
|
+
name: job.name,
|
|
288
|
+
schedule: job.schedule,
|
|
289
|
+
})),
|
|
290
|
+
[
|
|
291
|
+
{ name: 'nightly', schedule: '0 0 * * *' },
|
|
292
|
+
{ name: 'session-cleanup', schedule: 'rate(5 minutes)' },
|
|
293
|
+
],
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('manifest loader falls back to module exports from the compiled index entry', async () => {
|
|
298
|
+
const workspace = await createTempWorkspace();
|
|
299
|
+
await ensureDir(path.join(workspace, 'src', 'backend'));
|
|
300
|
+
|
|
301
|
+
const indexSource = `export const module = {
|
|
302
|
+
manifest: {
|
|
303
|
+
contractVersion: '1.0.0',
|
|
304
|
+
name: '@demo/index-module',
|
|
305
|
+
version: '3.2.1',
|
|
306
|
+
kind: 'backend',
|
|
307
|
+
capabilities: ['http'],
|
|
308
|
+
routes: [
|
|
309
|
+
{
|
|
310
|
+
name: 'indexEntryRoute',
|
|
311
|
+
method: 'GET',
|
|
312
|
+
path: '/demo/index-entry',
|
|
313
|
+
summary: 'Route surfaced from index.ts'
|
|
314
|
+
}
|
|
315
|
+
],
|
|
316
|
+
views: []
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
`;
|
|
320
|
+
|
|
321
|
+
await fs.writeFile(path.join(workspace, 'src', 'backend', 'index.ts'), indexSource, 'utf8');
|
|
322
|
+
await fs.writeFile(
|
|
323
|
+
path.join(workspace, 'package.json'),
|
|
324
|
+
JSON.stringify(
|
|
325
|
+
{ name: '@demo/index-fallback-package', version: '0.0.1', type: 'module' },
|
|
326
|
+
null,
|
|
327
|
+
2,
|
|
328
|
+
),
|
|
329
|
+
'utf8',
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const env = {
|
|
333
|
+
WEBSTIR_MODULE_MODE: 'build',
|
|
334
|
+
PATH: `${getLocalBinPath()}${path.delimiter}${process.env.PATH ?? ''}`,
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const result = await backendProvider.build({ workspaceRoot: workspace, env, incremental: false });
|
|
338
|
+
const moduleManifest = result.manifest.module;
|
|
339
|
+
|
|
340
|
+
assert.equal(moduleManifest?.name, '@demo/index-module');
|
|
341
|
+
assert.equal(moduleManifest?.version, '3.2.1');
|
|
342
|
+
assert.deepEqual(moduleManifest?.capabilities, ['http']);
|
|
343
|
+
assert.deepEqual(
|
|
344
|
+
moduleManifest?.routes?.map((route) => route.path),
|
|
345
|
+
['/demo/index-entry'],
|
|
346
|
+
);
|
|
347
|
+
});
|
|
348
|
+
|
|
133
349
|
test('scaffold assets expose core backend templates', async () => {
|
|
134
350
|
const assets = await backendProvider.getScaffoldAssets();
|
|
135
351
|
const targetSet = new Set(assets.map((asset) => asset.targetPath));
|
|
@@ -138,10 +354,11 @@ test('scaffold assets expose core backend templates', async () => {
|
|
|
138
354
|
path.join('src', 'backend', 'tsconfig.json'),
|
|
139
355
|
path.join('src', 'backend', 'index.ts'),
|
|
140
356
|
path.join('src', 'backend', 'module.ts'),
|
|
141
|
-
path.join('src', 'backend', 'server', 'fastify.ts'),
|
|
142
357
|
path.join('src', 'backend', 'auth', 'adapter.ts'),
|
|
143
358
|
path.join('src', 'backend', 'observability', 'logger.ts'),
|
|
144
359
|
path.join('src', 'backend', 'observability', 'metrics.ts'),
|
|
360
|
+
path.join('src', 'backend', 'session', 'store.ts'),
|
|
361
|
+
path.join('src', 'backend', 'session', 'sqlite.ts'),
|
|
145
362
|
path.join('src', 'backend', 'functions', 'hello', 'index.ts'),
|
|
146
363
|
path.join('src', 'backend', 'jobs', 'nightly', 'index.ts'),
|
|
147
364
|
path.join('src', 'backend', 'jobs', 'runtime.ts'),
|
|
@@ -150,10 +367,68 @@ test('scaffold assets expose core backend templates', async () => {
|
|
|
150
367
|
path.join('src', 'backend', 'db', 'migrate.ts'),
|
|
151
368
|
path.join('src', 'backend', 'db', 'migrations', '0001-example.ts'),
|
|
152
369
|
path.join('src', 'backend', 'db', 'types.d.ts'),
|
|
153
|
-
path.join('.env.example')
|
|
370
|
+
path.join('.env.example'),
|
|
154
371
|
];
|
|
155
372
|
|
|
156
373
|
for (const target of requiredTargets) {
|
|
157
374
|
assert.ok(targetSet.has(target), `expected scaffold assets to include ${target}`);
|
|
158
375
|
}
|
|
376
|
+
|
|
377
|
+
const removedTargets = [
|
|
378
|
+
path.join('src', 'backend', 'server', 'bun.ts'),
|
|
379
|
+
path.join('src', 'backend', 'runtime', 'request-hooks.ts'),
|
|
380
|
+
path.join('src', 'backend', 'runtime', 'session.ts'),
|
|
381
|
+
path.join('src', 'backend', 'runtime', 'forms.ts'),
|
|
382
|
+
path.join('src', 'backend', 'runtime', 'views.ts'),
|
|
383
|
+
path.join('src', 'backend', 'runtime', 'core.ts'),
|
|
384
|
+
path.join('src', 'backend', 'runtime', 'fastify.ts'),
|
|
385
|
+
path.join('src', 'backend', 'server', 'fastify.ts'),
|
|
386
|
+
];
|
|
387
|
+
|
|
388
|
+
for (const target of removedTargets) {
|
|
389
|
+
assert.ok(!targetSet.has(target), `expected scaffold assets to omit ${target}`);
|
|
390
|
+
}
|
|
391
|
+
const sessionStoreAsset = assets.find(
|
|
392
|
+
(asset) => asset.targetPath === path.join('src', 'backend', 'session', 'store.ts'),
|
|
393
|
+
);
|
|
394
|
+
assert.ok(sessionStoreAsset, 'expected scaffold assets to include the session store helper');
|
|
395
|
+
|
|
396
|
+
const sessionStoreSource = await fs.readFile(sessionStoreAsset.sourcePath, 'utf8');
|
|
397
|
+
assert.match(sessionStoreSource, /createSessionStoreFromEnv/);
|
|
398
|
+
assert.match(sessionStoreSource, /@webstir-io\/webstir-backend\/runtime\/session/);
|
|
399
|
+
assert.match(sessionStoreSource, /SESSION_STORE_DRIVER/);
|
|
400
|
+
|
|
401
|
+
const sqliteSessionStoreAsset = assets.find(
|
|
402
|
+
(asset) => asset.targetPath === path.join('src', 'backend', 'session', 'sqlite.ts'),
|
|
403
|
+
);
|
|
404
|
+
assert.ok(
|
|
405
|
+
sqliteSessionStoreAsset,
|
|
406
|
+
'expected scaffold assets to include the durable sqlite session store helper',
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
const sqliteSessionStoreSource = await fs.readFile(sqliteSessionStoreAsset.sourcePath, 'utf8');
|
|
410
|
+
assert.match(sqliteSessionStoreSource, /createSqliteSessionStore/);
|
|
411
|
+
|
|
412
|
+
const schedulerAsset = assets.find(
|
|
413
|
+
(asset) => asset.targetPath === path.join('src', 'backend', 'jobs', 'scheduler.ts'),
|
|
414
|
+
);
|
|
415
|
+
assert.ok(schedulerAsset, 'expected scaffold assets to include the job scheduler');
|
|
416
|
+
|
|
417
|
+
const schedulerSource = await fs.readFile(schedulerAsset.sourcePath, 'utf8');
|
|
418
|
+
assert.match(schedulerSource, /^#!\/usr\/bin\/env bun/m);
|
|
419
|
+
assert.match(schedulerSource, /bun build\/backend\/jobs\/scheduler\.js --job <name>/);
|
|
420
|
+
assert.match(schedulerSource, /Bun\.cron\.parse/);
|
|
421
|
+
assert.match(
|
|
422
|
+
schedulerSource,
|
|
423
|
+
/--json\s+Print registered job metadata as JSON for external schedulers/,
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
const migrateAsset = assets.find(
|
|
427
|
+
(asset) => asset.targetPath === path.join('src', 'backend', 'db', 'migrate.ts'),
|
|
428
|
+
);
|
|
429
|
+
assert.ok(migrateAsset, 'expected scaffold assets to include the database migration runner');
|
|
430
|
+
|
|
431
|
+
const migrateSource = await fs.readFile(migrateAsset.sourcePath, 'utf8');
|
|
432
|
+
assert.match(migrateSource, /^#!\/usr\/bin\/env bun/m);
|
|
433
|
+
assert.match(migrateSource, /bun src\/backend\/db\/migrate\.ts \[--list\]/);
|
|
159
434
|
});
|