@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/utils/bun.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
type BunFileLike = {
|
|
2
|
+
text(): Promise<string>;
|
|
3
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
interface BunLike {
|
|
7
|
+
file(path: string): BunFileLike;
|
|
8
|
+
write(path: string, data: string | ArrayBufferView | Blob | BunFileLike): Promise<number>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getBunRuntime(): BunLike {
|
|
12
|
+
const runtime = globalThis as typeof globalThis & { Bun?: BunLike };
|
|
13
|
+
if (typeof runtime.Bun?.file === 'function' && typeof runtime.Bun?.write === 'function') {
|
|
14
|
+
return runtime.Bun;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
throw new Error('[webstir-backend] Bun runtime is required for package-level IO.');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function readTextFile(filePath: string): Promise<string> {
|
|
21
|
+
return await getBunRuntime().file(filePath).text();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function writeTextFile(filePath: string, contents: string): Promise<void> {
|
|
25
|
+
await getBunRuntime().write(filePath, contents);
|
|
26
|
+
}
|
package/src/watch.ts
CHANGED
|
@@ -1,40 +1,95 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
+
import { mkdir, rm, stat } from 'node:fs/promises';
|
|
3
4
|
import { spawn, type ChildProcess } from 'node:child_process';
|
|
4
5
|
import { performance } from 'node:perf_hooks';
|
|
5
|
-
|
|
6
|
+
|
|
7
|
+
import { glob } from 'glob';
|
|
6
8
|
|
|
7
9
|
import type { ModuleDiagnostic } from '@webstir-io/module-contract';
|
|
8
10
|
|
|
9
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
ensureModuleDefinitionBuild,
|
|
13
|
+
formatEsbuildMessage,
|
|
14
|
+
shouldTypeCheck,
|
|
15
|
+
} from './build/pipeline.js';
|
|
10
16
|
import { discoverEntryPoints } from './build/entries.js';
|
|
11
17
|
import { loadBackendModuleManifest } from './manifest/pipeline.js';
|
|
12
18
|
import { createCacheReporter } from './cache/reporters.js';
|
|
13
|
-
import { normalizeMode, resolveWorkspacePaths } from './workspace.js';
|
|
19
|
+
import { normalizeMode, resolveWorkspacePaths, resolveWorkspaceRoot } from './workspace.js';
|
|
14
20
|
|
|
15
21
|
export interface WatchHandle {
|
|
16
22
|
stop(): Promise<void>;
|
|
17
23
|
}
|
|
18
24
|
|
|
25
|
+
export interface BackendWatchEvent {
|
|
26
|
+
readonly type: 'build-start' | 'build-complete';
|
|
27
|
+
readonly succeeded?: boolean;
|
|
28
|
+
readonly errorCount?: number;
|
|
29
|
+
readonly warningCount?: number;
|
|
30
|
+
readonly durationMs?: number;
|
|
31
|
+
readonly bunBenchmarkSucceeded?: boolean;
|
|
32
|
+
readonly bunBenchmarkErrorCount?: number;
|
|
33
|
+
readonly bunBenchmarkWarningCount?: number;
|
|
34
|
+
readonly bunBenchmarkDurationMs?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
19
37
|
export interface StartWatchOptions {
|
|
20
|
-
readonly workspaceRoot
|
|
38
|
+
readonly workspaceRoot?: string;
|
|
21
39
|
readonly env?: Record<string, string | undefined>;
|
|
40
|
+
readonly onEvent?: (event: BackendWatchEvent) => void | Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface BunBuildOutputFile {
|
|
44
|
+
readonly path: string;
|
|
45
|
+
readonly size?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface BunBuildLog {
|
|
49
|
+
readonly level?: string;
|
|
50
|
+
readonly message?: string;
|
|
51
|
+
readonly text?: string;
|
|
52
|
+
readonly position?: {
|
|
53
|
+
readonly file?: string;
|
|
54
|
+
readonly line?: number;
|
|
55
|
+
readonly column?: number;
|
|
56
|
+
} | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface BunBuildOutput {
|
|
60
|
+
readonly success: boolean;
|
|
61
|
+
readonly outputs?: readonly BunBuildOutputFile[];
|
|
62
|
+
readonly logs?: readonly BunBuildLog[];
|
|
22
63
|
}
|
|
23
64
|
|
|
65
|
+
type BunBuildFunction = (config: Record<string, unknown>) => Promise<BunBuildOutput>;
|
|
66
|
+
|
|
67
|
+
interface WatchBuildResult {
|
|
68
|
+
readonly succeeded: boolean;
|
|
69
|
+
readonly errorCount: number;
|
|
70
|
+
readonly warningCount: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const WATCH_POLL_INTERVAL_MS = 250;
|
|
74
|
+
|
|
24
75
|
export async function startBackendWatch(options: StartWatchOptions): Promise<WatchHandle> {
|
|
25
|
-
const {
|
|
26
|
-
const
|
|
76
|
+
const env = { ...process.env, ...(options.env ?? {}) };
|
|
77
|
+
const workspaceRoot = resolveWorkspaceRoot({
|
|
78
|
+
workspaceRoot: options.workspaceRoot,
|
|
79
|
+
env,
|
|
80
|
+
});
|
|
27
81
|
const paths = resolveWorkspacePaths(workspaceRoot);
|
|
28
82
|
const tsconfigPath = path.join(paths.sourceRoot, 'tsconfig.json');
|
|
29
83
|
const mode = normalizeMode(env.WEBSTIR_MODULE_MODE);
|
|
30
84
|
|
|
31
|
-
const
|
|
32
|
-
if (
|
|
85
|
+
const initialEntryPoints = await discoverEntryPoints(paths.sourceRoot);
|
|
86
|
+
if (initialEntryPoints.length === 0) {
|
|
33
87
|
console.warn(`[webstir-backend] watch: no entry found under ${paths.sourceRoot} (index.ts/js)`);
|
|
34
88
|
throw new Error('No backend entry point found.');
|
|
35
89
|
}
|
|
36
90
|
|
|
37
91
|
const nodeEnv = env.NODE_ENV ?? (mode === 'publish' ? 'production' : 'development');
|
|
92
|
+
const shouldReportBunBenchmark = isEnabled(env.WEBSTIR_BACKEND_WATCH_BUN_BENCHMARK);
|
|
38
93
|
const diagMax = (() => {
|
|
39
94
|
const raw = env.WEBSTIR_BACKEND_DIAG_MAX;
|
|
40
95
|
const n = typeof raw === 'string' ? parseInt(raw, 10) : NaN;
|
|
@@ -43,14 +98,12 @@ export async function startBackendWatch(options: StartWatchOptions): Promise<Wat
|
|
|
43
98
|
|
|
44
99
|
console.info(`[webstir-backend] watch:start (${mode})`);
|
|
45
100
|
|
|
46
|
-
// Start type-checker in watch mode (no emit) unless explicitly skipped for DX.
|
|
47
|
-
const shouldRunTypecheck = shouldTypeCheck(mode, env);
|
|
48
101
|
let tscProc: ChildProcess | undefined;
|
|
49
|
-
if (
|
|
102
|
+
if (shouldTypeCheck(mode, env)) {
|
|
50
103
|
const tscArgs = ['-p', tsconfigPath, '--noEmit', '--watch'];
|
|
51
104
|
tscProc = spawn('tsc', tscArgs, {
|
|
52
105
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
53
|
-
env: { ...
|
|
106
|
+
env: { ...env, NODE_ENV: nodeEnv },
|
|
54
107
|
cwd: workspaceRoot,
|
|
55
108
|
});
|
|
56
109
|
|
|
@@ -70,101 +123,95 @@ export async function startBackendWatch(options: StartWatchOptions): Promise<Wat
|
|
|
70
123
|
console.info('[webstir-backend] watch: type-check skipped by WEBSTIR_BACKEND_TYPECHECK');
|
|
71
124
|
}
|
|
72
125
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
await cacheReporter.diffOutputs(outputs, mode);
|
|
120
|
-
}
|
|
121
|
-
const manifest = await loadBackendModuleManifest({
|
|
122
|
-
workspaceRoot,
|
|
123
|
-
buildRoot: paths.buildRoot,
|
|
124
|
-
entryPoints,
|
|
125
|
-
diagnostics: diagBuffer
|
|
126
|
-
});
|
|
127
|
-
await cacheReporter.diffManifest(manifest);
|
|
128
|
-
} catch {
|
|
129
|
-
// cache or manifest diff failure should not break watch
|
|
130
|
-
} finally {
|
|
131
|
-
for (const diag of diagBuffer) {
|
|
132
|
-
const logger =
|
|
133
|
-
diag.severity === 'error' ? console.error : diag.severity === 'warn' ? console.warn : console.info;
|
|
134
|
-
logger(diag.message);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
126
|
+
if (shouldReportBunBenchmark) {
|
|
127
|
+
console.info(
|
|
128
|
+
'[webstir-backend] watch: reporting primary Bun build timings via bunBenchmark* event fields.',
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let stopping = false;
|
|
133
|
+
let watchTimer: ReturnType<typeof setTimeout> | undefined;
|
|
134
|
+
let currentSnapshot = await takeWatchSnapshot(workspaceRoot, paths.sourceRoot, tsconfigPath);
|
|
135
|
+
let buildInFlight = false;
|
|
136
|
+
let pendingBuild = false;
|
|
137
|
+
let buildFailure: Error | undefined;
|
|
138
|
+
|
|
139
|
+
const runBuild = async (): Promise<void> => {
|
|
140
|
+
if (stopping) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (buildInFlight) {
|
|
145
|
+
pendingBuild = true;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
buildInFlight = true;
|
|
150
|
+
try {
|
|
151
|
+
do {
|
|
152
|
+
pendingBuild = false;
|
|
153
|
+
const nextSnapshot = await takeWatchSnapshot(workspaceRoot, paths.sourceRoot, tsconfigPath);
|
|
154
|
+
currentSnapshot = nextSnapshot;
|
|
155
|
+
const result = await performWatchBuild({
|
|
156
|
+
workspaceRoot,
|
|
157
|
+
sourceRoot: paths.sourceRoot,
|
|
158
|
+
buildRoot: paths.buildRoot,
|
|
159
|
+
tsconfigPath,
|
|
160
|
+
mode,
|
|
161
|
+
env,
|
|
162
|
+
nodeEnv,
|
|
163
|
+
diagMax,
|
|
164
|
+
shouldReportBunBenchmark,
|
|
165
|
+
onEvent: options.onEvent,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (!result.succeeded) {
|
|
169
|
+
buildFailure = new Error('Backend watch build failed.');
|
|
170
|
+
} else {
|
|
171
|
+
buildFailure = undefined;
|
|
137
172
|
}
|
|
138
|
-
});
|
|
139
|
-
}
|
|
173
|
+
} while (pendingBuild && !stopping);
|
|
174
|
+
} finally {
|
|
175
|
+
buildInFlight = false;
|
|
176
|
+
}
|
|
140
177
|
};
|
|
141
178
|
|
|
142
|
-
|
|
143
|
-
entryPoints,
|
|
144
|
-
bundle: false,
|
|
145
|
-
platform: 'node',
|
|
146
|
-
target: 'node20',
|
|
147
|
-
format: 'esm',
|
|
148
|
-
sourcemap: true,
|
|
149
|
-
outdir: paths.buildRoot,
|
|
150
|
-
outbase: paths.sourceRoot,
|
|
151
|
-
metafile: true,
|
|
152
|
-
tsconfig: existsSync(tsconfigPath) ? tsconfigPath : undefined,
|
|
153
|
-
define: { 'process.env.NODE_ENV': JSON.stringify(nodeEnv) },
|
|
154
|
-
logLevel: 'silent',
|
|
155
|
-
plugins: [timingPlugin],
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
await ctx.watch();
|
|
179
|
+
await runBuild();
|
|
159
180
|
|
|
160
181
|
console.info('[webstir-backend] watch:ready');
|
|
161
182
|
|
|
183
|
+
const poll = async (): Promise<void> => {
|
|
184
|
+
if (stopping) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const nextSnapshot = await takeWatchSnapshot(workspaceRoot, paths.sourceRoot, tsconfigPath);
|
|
190
|
+
if (nextSnapshot !== currentSnapshot) {
|
|
191
|
+
currentSnapshot = nextSnapshot;
|
|
192
|
+
await runBuild();
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
196
|
+
console.warn(`[webstir-backend] watch:poll failed: ${message}`);
|
|
197
|
+
} finally {
|
|
198
|
+
if (!stopping) {
|
|
199
|
+
watchTimer = setTimeout(() => {
|
|
200
|
+
void poll();
|
|
201
|
+
}, WATCH_POLL_INTERVAL_MS);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
watchTimer = setTimeout(() => {
|
|
207
|
+
void poll();
|
|
208
|
+
}, WATCH_POLL_INTERVAL_MS);
|
|
209
|
+
|
|
162
210
|
return {
|
|
163
211
|
async stop() {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
// ignore
|
|
212
|
+
stopping = true;
|
|
213
|
+
if (watchTimer) {
|
|
214
|
+
clearTimeout(watchTimer);
|
|
168
215
|
}
|
|
169
216
|
try {
|
|
170
217
|
tscProc?.kill('SIGINT');
|
|
@@ -172,6 +219,363 @@ export async function startBackendWatch(options: StartWatchOptions): Promise<Wat
|
|
|
172
219
|
// ignore
|
|
173
220
|
}
|
|
174
221
|
console.info('[webstir-backend] watch:stopped');
|
|
222
|
+
|
|
223
|
+
if (buildFailure) {
|
|
224
|
+
buildFailure = undefined;
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
interface PerformWatchBuildOptions {
|
|
231
|
+
readonly workspaceRoot: string;
|
|
232
|
+
readonly sourceRoot: string;
|
|
233
|
+
readonly buildRoot: string;
|
|
234
|
+
readonly tsconfigPath: string;
|
|
235
|
+
readonly mode: ReturnType<typeof normalizeMode>;
|
|
236
|
+
readonly env: Record<string, string | undefined>;
|
|
237
|
+
readonly nodeEnv: string;
|
|
238
|
+
readonly diagMax: number;
|
|
239
|
+
readonly shouldReportBunBenchmark: boolean;
|
|
240
|
+
readonly onEvent?: StartWatchOptions['onEvent'];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function performWatchBuild(options: PerformWatchBuildOptions): Promise<WatchBuildResult> {
|
|
244
|
+
const start = performance.now();
|
|
245
|
+
await emitWatchEvent(options.onEvent, {
|
|
246
|
+
type: 'build-start',
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const diagnostics: ModuleDiagnostic[] = [];
|
|
250
|
+
const entryPoints = await discoverEntryPoints(options.sourceRoot);
|
|
251
|
+
if (entryPoints.length === 0) {
|
|
252
|
+
diagnostics.push({
|
|
253
|
+
severity: 'error',
|
|
254
|
+
message: `No backend entry points found under ${options.sourceRoot}.`,
|
|
255
|
+
});
|
|
256
|
+
flushDiagnostics(diagnostics);
|
|
257
|
+
const end = performance.now();
|
|
258
|
+
await emitWatchEvent(options.onEvent, {
|
|
259
|
+
type: 'build-complete',
|
|
260
|
+
succeeded: false,
|
|
261
|
+
errorCount: 1,
|
|
262
|
+
warningCount: 0,
|
|
263
|
+
durationMs: end - start,
|
|
264
|
+
});
|
|
265
|
+
return {
|
|
266
|
+
succeeded: false,
|
|
267
|
+
errorCount: 1,
|
|
268
|
+
warningCount: 0,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const buildResult = await runPrimaryBunWatchBuild({
|
|
273
|
+
entryPoints,
|
|
274
|
+
sourceRoot: options.sourceRoot,
|
|
275
|
+
buildRoot: options.buildRoot,
|
|
276
|
+
tsconfigPath: options.tsconfigPath,
|
|
277
|
+
nodeEnv: options.nodeEnv,
|
|
278
|
+
diagMax: options.diagMax,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
console.info(
|
|
282
|
+
`[webstir-backend] watch:bun ${buildResult.errorCount} error(s), ${buildResult.warningCount} warning(s) in ${buildResult.durationMs.toFixed(1)}ms`,
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
if (buildResult.succeeded) {
|
|
286
|
+
const cacheReporter = createCacheReporter({
|
|
287
|
+
workspaceRoot: options.workspaceRoot,
|
|
288
|
+
buildRoot: options.buildRoot,
|
|
289
|
+
env: options.env,
|
|
290
|
+
diagnostics,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
await ensureModuleDefinitionBuild({
|
|
295
|
+
sourceRoot: options.sourceRoot,
|
|
296
|
+
buildRoot: options.buildRoot,
|
|
297
|
+
tsconfigPath: options.tsconfigPath,
|
|
298
|
+
mode: options.mode,
|
|
299
|
+
env: options.env,
|
|
300
|
+
diagnostics,
|
|
301
|
+
});
|
|
302
|
+
await cacheReporter.diffOutputs(
|
|
303
|
+
collectBunOutputSizes(buildResult.outputs, options.buildRoot),
|
|
304
|
+
options.mode,
|
|
305
|
+
);
|
|
306
|
+
const manifest = await loadBackendModuleManifest({
|
|
307
|
+
workspaceRoot: options.workspaceRoot,
|
|
308
|
+
buildRoot: options.buildRoot,
|
|
309
|
+
entryPoints,
|
|
310
|
+
diagnostics,
|
|
311
|
+
});
|
|
312
|
+
await cacheReporter.diffManifest(manifest);
|
|
313
|
+
} catch {
|
|
314
|
+
// cache or manifest diff failure should not break watch
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
flushDiagnostics(diagnostics);
|
|
319
|
+
const end = performance.now();
|
|
320
|
+
const bunBenchmark = options.shouldReportBunBenchmark
|
|
321
|
+
? {
|
|
322
|
+
succeeded: buildResult.succeeded,
|
|
323
|
+
errorCount: buildResult.errorCount,
|
|
324
|
+
warningCount: buildResult.warningCount,
|
|
325
|
+
durationMs: buildResult.durationMs,
|
|
326
|
+
}
|
|
327
|
+
: undefined;
|
|
328
|
+
|
|
329
|
+
await emitWatchEvent(options.onEvent, {
|
|
330
|
+
type: 'build-complete',
|
|
331
|
+
succeeded: buildResult.succeeded,
|
|
332
|
+
errorCount: buildResult.errorCount,
|
|
333
|
+
warningCount: buildResult.warningCount,
|
|
334
|
+
durationMs: end - start,
|
|
335
|
+
bunBenchmarkSucceeded: bunBenchmark?.succeeded,
|
|
336
|
+
bunBenchmarkErrorCount: bunBenchmark?.errorCount,
|
|
337
|
+
bunBenchmarkWarningCount: bunBenchmark?.warningCount,
|
|
338
|
+
bunBenchmarkDurationMs: bunBenchmark?.durationMs,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
succeeded: buildResult.succeeded,
|
|
343
|
+
errorCount: buildResult.errorCount,
|
|
344
|
+
warningCount: buildResult.warningCount,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
interface RunPrimaryBunWatchBuildOptions {
|
|
349
|
+
readonly entryPoints: readonly string[];
|
|
350
|
+
readonly sourceRoot: string;
|
|
351
|
+
readonly buildRoot: string;
|
|
352
|
+
readonly tsconfigPath: string;
|
|
353
|
+
readonly nodeEnv: string;
|
|
354
|
+
readonly diagMax: number;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
interface RunPrimaryBunWatchBuildResult {
|
|
358
|
+
readonly succeeded: boolean;
|
|
359
|
+
readonly errorCount: number;
|
|
360
|
+
readonly warningCount: number;
|
|
361
|
+
readonly durationMs: number;
|
|
362
|
+
readonly outputs?: readonly BunBuildOutputFile[];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function runPrimaryBunWatchBuild(
|
|
366
|
+
options: RunPrimaryBunWatchBuildOptions,
|
|
367
|
+
): Promise<RunPrimaryBunWatchBuildResult> {
|
|
368
|
+
const build = getBunBuild();
|
|
369
|
+
if (!build) {
|
|
370
|
+
throw new Error('Bun.build() is not available in the current runtime.');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
await rm(options.buildRoot, { recursive: true, force: true });
|
|
374
|
+
await mkdir(options.buildRoot, { recursive: true });
|
|
375
|
+
|
|
376
|
+
const start = performance.now();
|
|
377
|
+
const result = await build({
|
|
378
|
+
entrypoints: [...options.entryPoints],
|
|
379
|
+
root: options.sourceRoot,
|
|
380
|
+
outdir: options.buildRoot,
|
|
381
|
+
target: 'node',
|
|
382
|
+
format: 'esm',
|
|
383
|
+
splitting: false,
|
|
384
|
+
packages: 'external',
|
|
385
|
+
sourcemap: 'linked',
|
|
386
|
+
tsconfig: existsSync(options.tsconfigPath) ? options.tsconfigPath : undefined,
|
|
387
|
+
define: {
|
|
388
|
+
'process.env.NODE_ENV': JSON.stringify(options.nodeEnv),
|
|
389
|
+
},
|
|
390
|
+
// Preserve the old esbuild watch behavior: transpile entries without requiring
|
|
391
|
+
// every relative import target to exist in minimal seeded workspaces.
|
|
392
|
+
plugins: [createRelativeImportPassthroughPlugin()],
|
|
393
|
+
throw: false,
|
|
394
|
+
});
|
|
395
|
+
const end = performance.now();
|
|
396
|
+
|
|
397
|
+
const { errorCount, warningCount } = logBunBuildResult(result, options.diagMax);
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
succeeded: result.success && errorCount === 0,
|
|
401
|
+
errorCount,
|
|
402
|
+
warningCount,
|
|
403
|
+
durationMs: end - start,
|
|
404
|
+
outputs: result.outputs,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function logBunBuildResult(
|
|
409
|
+
result: BunBuildOutput,
|
|
410
|
+
diagMax: number,
|
|
411
|
+
): {
|
|
412
|
+
errorCount: number;
|
|
413
|
+
warningCount: number;
|
|
414
|
+
} {
|
|
415
|
+
const logs = Array.isArray(result.logs) ? result.logs : [];
|
|
416
|
+
const errorLogs = logs.filter((log) => log.level === 'error');
|
|
417
|
+
const warningLogs = logs.filter((log) => log.level === 'warning');
|
|
418
|
+
|
|
419
|
+
for (const log of errorLogs.slice(0, diagMax)) {
|
|
420
|
+
console.error(`[webstir-backend][bun] ${formatEsbuildMessage(log)}`);
|
|
421
|
+
}
|
|
422
|
+
if (errorLogs.length > diagMax) {
|
|
423
|
+
console.error(`[webstir-backend][bun] ... ${errorLogs.length - diagMax} more error(s) omitted`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
for (const log of warningLogs.slice(0, diagMax)) {
|
|
427
|
+
console.warn(`[webstir-backend][bun] ${formatEsbuildMessage(log)}`);
|
|
428
|
+
}
|
|
429
|
+
if (warningLogs.length > diagMax) {
|
|
430
|
+
console.warn(
|
|
431
|
+
`[webstir-backend][bun] ... ${warningLogs.length - diagMax} more warning(s) omitted`,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
errorCount: errorLogs.length,
|
|
437
|
+
warningCount: warningLogs.length,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function collectBunOutputSizes(
|
|
442
|
+
outputs: readonly BunBuildOutputFile[] | undefined,
|
|
443
|
+
buildRoot: string,
|
|
444
|
+
): Record<string, number> {
|
|
445
|
+
const collected: Record<string, number> = {};
|
|
446
|
+
for (const output of outputs ?? []) {
|
|
447
|
+
const rel = path.relative(buildRoot, output.path);
|
|
448
|
+
collected[rel] = typeof output.size === 'number' ? output.size : 0;
|
|
449
|
+
}
|
|
450
|
+
return collected;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function takeWatchSnapshot(
|
|
454
|
+
workspaceRoot: string,
|
|
455
|
+
sourceRoot: string,
|
|
456
|
+
tsconfigPath: string,
|
|
457
|
+
): Promise<string> {
|
|
458
|
+
const watchFiles = new Set<string>();
|
|
459
|
+
|
|
460
|
+
const packageJsonPath = path.join(workspaceRoot, 'package.json');
|
|
461
|
+
if (existsSync(packageJsonPath)) {
|
|
462
|
+
watchFiles.add(packageJsonPath);
|
|
463
|
+
}
|
|
464
|
+
if (existsSync(tsconfigPath)) {
|
|
465
|
+
watchFiles.add(tsconfigPath);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const typesRoot = path.join(workspaceRoot, 'types');
|
|
469
|
+
const directoryRoots = [sourceRoot];
|
|
470
|
+
if (existsSync(typesRoot)) {
|
|
471
|
+
directoryRoots.push(typesRoot);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
for (const directoryRoot of directoryRoots) {
|
|
475
|
+
for (const filePath of await listWatchFiles(directoryRoot)) {
|
|
476
|
+
watchFiles.add(filePath);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const entries = await Promise.all(
|
|
481
|
+
Array.from(watchFiles)
|
|
482
|
+
.sort()
|
|
483
|
+
.map(async (filePath) => {
|
|
484
|
+
const fileStat = await stat(filePath);
|
|
485
|
+
return `${filePath}:${fileStat.size}:${fileStat.mtimeMs}`;
|
|
486
|
+
}),
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
return entries.join('|');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function listWatchFiles(root: string): Promise<string[]> {
|
|
493
|
+
if (!existsSync(root)) {
|
|
494
|
+
return [];
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const entries = await glob('**/*', {
|
|
498
|
+
cwd: root,
|
|
499
|
+
absolute: true,
|
|
500
|
+
dot: false,
|
|
501
|
+
nodir: false,
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
const files: string[] = [];
|
|
505
|
+
for (const entry of entries) {
|
|
506
|
+
try {
|
|
507
|
+
const entryStat = await stat(entry);
|
|
508
|
+
if (entryStat.isFile()) {
|
|
509
|
+
files.push(entry);
|
|
510
|
+
}
|
|
511
|
+
} catch {
|
|
512
|
+
// Ignore files deleted between glob and stat.
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return files;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function flushDiagnostics(diagnostics: readonly ModuleDiagnostic[]): void {
|
|
520
|
+
for (const diag of diagnostics) {
|
|
521
|
+
const logger =
|
|
522
|
+
diag.severity === 'error'
|
|
523
|
+
? console.error
|
|
524
|
+
: diag.severity === 'warn'
|
|
525
|
+
? console.warn
|
|
526
|
+
: console.info;
|
|
527
|
+
logger(diag.message);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function emitWatchEvent(
|
|
532
|
+
onEvent: StartWatchOptions['onEvent'],
|
|
533
|
+
event: BackendWatchEvent,
|
|
534
|
+
): Promise<void> {
|
|
535
|
+
if (!onEvent) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
try {
|
|
540
|
+
await onEvent(event);
|
|
541
|
+
} catch (error) {
|
|
542
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
543
|
+
console.warn(`[webstir-backend] watch:event failed: ${message}`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function getBunBuild(): BunBuildFunction | undefined {
|
|
548
|
+
const runtime = globalThis as typeof globalThis & {
|
|
549
|
+
Bun?: {
|
|
550
|
+
build?: BunBuildFunction;
|
|
551
|
+
};
|
|
552
|
+
};
|
|
553
|
+
const build = runtime.Bun?.build;
|
|
554
|
+
return typeof build === 'function' ? build.bind(runtime.Bun) : undefined;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function createRelativeImportPassthroughPlugin(): Record<string, unknown> {
|
|
558
|
+
return {
|
|
559
|
+
name: 'webstir-backend-watch-relative-imports',
|
|
560
|
+
setup(build: {
|
|
561
|
+
onResolve(
|
|
562
|
+
options: { filter: RegExp },
|
|
563
|
+
callback: (args: { path: string }) => { path: string; external: boolean },
|
|
564
|
+
): void;
|
|
565
|
+
}) {
|
|
566
|
+
build.onResolve({ filter: /^\.\.?\// }, (args: { path: string }) => ({
|
|
567
|
+
path: args.path,
|
|
568
|
+
external: true,
|
|
569
|
+
}));
|
|
175
570
|
},
|
|
176
571
|
};
|
|
177
572
|
}
|
|
573
|
+
|
|
574
|
+
function isEnabled(value: string | undefined): boolean {
|
|
575
|
+
if (typeof value !== 'string') {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const normalized = value.trim().toLowerCase();
|
|
580
|
+
return normalized === '1' || normalized === 'true' || normalized === 'on' || normalized === 'yes';
|
|
581
|
+
}
|