@webstir-io/webstir-backend 0.1.15
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 +427 -0
- package/dist/build/artifacts.d.ts +113 -0
- package/dist/build/artifacts.js +53 -0
- package/dist/build/entries.d.ts +1 -0
- package/dist/build/entries.js +17 -0
- package/dist/build/pipeline.d.ts +31 -0
- package/dist/build/pipeline.js +424 -0
- package/dist/cache/diff.d.ts +4 -0
- package/dist/cache/diff.js +114 -0
- package/dist/cache/reporters.d.ts +12 -0
- package/dist/cache/reporters.js +23 -0
- package/dist/diagnostics/summary.d.ts +6 -0
- package/dist/diagnostics/summary.js +27 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/manifest/pipeline.d.ts +13 -0
- package/dist/manifest/pipeline.js +224 -0
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +101 -0
- package/dist/scaffold/assets.d.ts +2 -0
- package/dist/scaffold/assets.js +77 -0
- package/dist/testing/context.d.ts +3 -0
- package/dist/testing/context.js +14 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.js +208 -0
- package/dist/testing/types.d.ts +28 -0
- package/dist/testing/types.js +1 -0
- package/dist/watch.d.ts +8 -0
- package/dist/watch.js +159 -0
- package/dist/workspace.d.ts +4 -0
- package/dist/workspace.js +15 -0
- package/package.json +74 -0
- package/scripts/publish.sh +99 -0
- package/scripts/smoke.mjs +241 -0
- package/scripts/update-contract.sh +122 -0
- package/src/build/artifacts.ts +67 -0
- package/src/build/entries.ts +19 -0
- package/src/build/pipeline.ts +507 -0
- package/src/cache/diff.ts +128 -0
- package/src/cache/reporters.ts +41 -0
- package/src/diagnostics/summary.ts +32 -0
- package/src/index.ts +2 -0
- package/src/manifest/pipeline.ts +270 -0
- package/src/provider.ts +124 -0
- package/src/scaffold/assets.ts +81 -0
- package/src/testing/context.d.ts +3 -0
- package/src/testing/context.js +14 -0
- package/src/testing/context.ts +17 -0
- package/src/testing/index.d.ts +6 -0
- package/src/testing/index.js +208 -0
- package/src/testing/index.ts +252 -0
- package/src/testing/types.d.ts +28 -0
- package/src/testing/types.js +1 -0
- package/src/testing/types.ts +32 -0
- package/src/watch.ts +177 -0
- package/src/workspace.ts +22 -0
- package/templates/backend/.env.example +13 -0
- package/templates/backend/auth/adapter.ts +160 -0
- package/templates/backend/db/connection.ts +99 -0
- package/templates/backend/db/migrate.ts +231 -0
- package/templates/backend/db/migrations/0001-example.ts +17 -0
- package/templates/backend/db/types.d.ts +2 -0
- package/templates/backend/env.ts +174 -0
- package/templates/backend/functions/hello/index.ts +29 -0
- package/templates/backend/index.ts +532 -0
- package/templates/backend/jobs/nightly/index.ts +28 -0
- package/templates/backend/jobs/runtime.ts +103 -0
- package/templates/backend/jobs/scheduler.ts +193 -0
- package/templates/backend/module.ts +87 -0
- package/templates/backend/observability/logger.ts +24 -0
- package/templates/backend/observability/metrics.ts +78 -0
- package/templates/backend/server/fastify.ts +288 -0
- package/templates/backend/tsconfig.json +19 -0
- package/tests/cacheReporter.test.js +89 -0
- package/tests/envLoader.test.js +64 -0
- package/tests/integration.test.js +108 -0
- package/tests/manifest.test.js +159 -0
- package/tests/watch.test.js +100 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { performance } from 'node:perf_hooks';
|
|
5
|
+
import { build as esbuild, context as esbuildContext } from 'esbuild';
|
|
6
|
+
import { glob } from 'glob';
|
|
7
|
+
import { discoverEntryPoints } from './entries.js';
|
|
8
|
+
const incrementalBuildCache = new Map();
|
|
9
|
+
if (typeof process !== 'undefined' && typeof process.once === 'function') {
|
|
10
|
+
process.once('exit', () => {
|
|
11
|
+
clearIncrementalCache();
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export async function runBackendBuildPipeline(options) {
|
|
15
|
+
const { sourceRoot, buildRoot, tsconfigPath, diagnostics, incremental, mode } = options;
|
|
16
|
+
const env = options.env ?? {};
|
|
17
|
+
console.info(`[webstir-backend] ${mode}:tsc start`);
|
|
18
|
+
if (shouldTypeCheck(mode, env)) {
|
|
19
|
+
await runTypeCheck(tsconfigPath, env, diagnostics);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
diagnostics.push({ severity: 'info', message: '[webstir-backend] type-check skipped by WEBSTIR_BACKEND_TYPECHECK' });
|
|
23
|
+
}
|
|
24
|
+
console.info(`[webstir-backend] ${mode}:tsc done`);
|
|
25
|
+
const entryPoints = await discoverEntryPoints(sourceRoot);
|
|
26
|
+
if (entryPoints.length === 0) {
|
|
27
|
+
diagnostics.push({
|
|
28
|
+
severity: 'warn',
|
|
29
|
+
message: `No backend entry points found under ${sourceRoot} (expected index.* or functions/*/index.* or jobs/*/index.*).`
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
console.info(`[webstir-backend] ${mode}:esbuild start`);
|
|
33
|
+
const outputs = await runEsbuild({
|
|
34
|
+
sourceRoot,
|
|
35
|
+
buildRoot,
|
|
36
|
+
tsconfigPath,
|
|
37
|
+
mode,
|
|
38
|
+
env,
|
|
39
|
+
incremental,
|
|
40
|
+
diagnostics,
|
|
41
|
+
entryPoints
|
|
42
|
+
});
|
|
43
|
+
console.info(`[webstir-backend] ${mode}:esbuild done`);
|
|
44
|
+
const moduleSource = await discoverModuleDefinitionSource(sourceRoot);
|
|
45
|
+
if (moduleSource) {
|
|
46
|
+
await buildModuleDefinition({
|
|
47
|
+
sourceFile: moduleSource,
|
|
48
|
+
sourceRoot,
|
|
49
|
+
buildRoot,
|
|
50
|
+
tsconfigPath,
|
|
51
|
+
mode,
|
|
52
|
+
env,
|
|
53
|
+
diagnostics
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const includePublishSourcemaps = mode === 'publish' && shouldEmitPublishSourcemaps(env);
|
|
57
|
+
return {
|
|
58
|
+
entryPoints,
|
|
59
|
+
outputs,
|
|
60
|
+
includePublishSourcemaps
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async function runTypeCheck(tsconfigPath, env, diagnostics) {
|
|
64
|
+
if (!existsSync(tsconfigPath)) {
|
|
65
|
+
diagnostics.push({
|
|
66
|
+
severity: 'warn',
|
|
67
|
+
message: `TypeScript config not found at ${tsconfigPath}; skipping type-check.`
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
await new Promise((resolve, reject) => {
|
|
72
|
+
const child = spawn('tsc', ['-p', tsconfigPath, '--noEmit'], {
|
|
73
|
+
stdio: 'pipe',
|
|
74
|
+
env: {
|
|
75
|
+
...process.env,
|
|
76
|
+
...env
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
let stdout = '';
|
|
80
|
+
let stderr = '';
|
|
81
|
+
child.stdout?.on('data', (chunk) => {
|
|
82
|
+
stdout += chunk.toString();
|
|
83
|
+
});
|
|
84
|
+
child.stderr?.on('data', (chunk) => {
|
|
85
|
+
stderr += chunk.toString();
|
|
86
|
+
});
|
|
87
|
+
child.on('error', (err) => {
|
|
88
|
+
const code = (err && typeof err === 'object') ? err.code : undefined;
|
|
89
|
+
if (code === 'ENOENT') {
|
|
90
|
+
diagnostics.push({ severity: 'warn', message: 'TypeScript compiler (tsc) not found in PATH; skipping type-check.' });
|
|
91
|
+
resolve();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
reject(err);
|
|
95
|
+
});
|
|
96
|
+
child.on('close', (code) => {
|
|
97
|
+
if (code === 0) {
|
|
98
|
+
resolve();
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
diagnostics.push({
|
|
102
|
+
severity: 'error',
|
|
103
|
+
message: `Type checking failed (exit code ${code}).`,
|
|
104
|
+
file: tsconfigPath
|
|
105
|
+
});
|
|
106
|
+
if (stderr.trim()) {
|
|
107
|
+
diagnostics.push({ severity: 'error', message: stderr.trim() });
|
|
108
|
+
}
|
|
109
|
+
if (stdout.trim()) {
|
|
110
|
+
diagnostics.push({ severity: 'info', message: stdout.trim() });
|
|
111
|
+
}
|
|
112
|
+
reject(new Error('Type checking failed.'));
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
export function shouldTypeCheck(mode, env) {
|
|
118
|
+
const flag = env?.WEBSTIR_BACKEND_TYPECHECK;
|
|
119
|
+
if (typeof flag === 'string' && flag.toLowerCase() === 'skip') {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
if (mode === 'publish') {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
function shouldEmitPublishSourcemaps(env) {
|
|
128
|
+
const flag = env?.WEBSTIR_BACKEND_SOURCEMAPS;
|
|
129
|
+
if (typeof flag !== 'string') {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const normalized = flag.trim().toLowerCase();
|
|
133
|
+
return normalized === 'on' || normalized === 'true' || normalized === '1' || normalized === 'yes';
|
|
134
|
+
}
|
|
135
|
+
async function discoverModuleDefinitionSource(sourceRoot) {
|
|
136
|
+
const patterns = ['module.{ts,tsx,js,mjs}', 'module/index.{ts,tsx,js,mjs}'];
|
|
137
|
+
for (const pattern of patterns) {
|
|
138
|
+
const matches = await glob(pattern, {
|
|
139
|
+
cwd: sourceRoot,
|
|
140
|
+
absolute: true,
|
|
141
|
+
nodir: true,
|
|
142
|
+
dot: false
|
|
143
|
+
});
|
|
144
|
+
if (matches.length > 0) {
|
|
145
|
+
return matches[0];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
async function buildModuleDefinition(options) {
|
|
151
|
+
const { sourceFile, sourceRoot, buildRoot, tsconfigPath, mode, env, diagnostics } = options;
|
|
152
|
+
const isProduction = mode === 'publish';
|
|
153
|
+
const nodeEnv = env?.NODE_ENV ?? (isProduction ? 'production' : 'development');
|
|
154
|
+
const emitPublishSourcemaps = isProduction && shouldEmitPublishSourcemaps(env);
|
|
155
|
+
const define = {
|
|
156
|
+
'process.env.NODE_ENV': JSON.stringify(nodeEnv)
|
|
157
|
+
};
|
|
158
|
+
try {
|
|
159
|
+
await esbuild({
|
|
160
|
+
entryPoints: [sourceFile],
|
|
161
|
+
bundle: false,
|
|
162
|
+
platform: 'node',
|
|
163
|
+
target: 'node20',
|
|
164
|
+
format: 'esm',
|
|
165
|
+
sourcemap: isProduction ? emitPublishSourcemaps : true,
|
|
166
|
+
outdir: buildRoot,
|
|
167
|
+
outbase: sourceRoot,
|
|
168
|
+
entryNames: '[dir]/[name]',
|
|
169
|
+
tsconfig: existsSync(tsconfigPath) ? tsconfigPath : undefined,
|
|
170
|
+
define,
|
|
171
|
+
logLevel: 'silent'
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (isEsbuildFailure(error)) {
|
|
176
|
+
for (const e of error.errors ?? []) {
|
|
177
|
+
diagnostics.push({ severity: 'error', message: formatEsbuildMessage(e) });
|
|
178
|
+
}
|
|
179
|
+
for (const w of error.warnings ?? []) {
|
|
180
|
+
diagnostics.push({ severity: 'warn', message: formatEsbuildMessage(w) });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else if (error instanceof Error) {
|
|
184
|
+
diagnostics.push({ severity: 'error', message: error.message });
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
diagnostics.push({ severity: 'error', message: String(error) });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
export async function buildSupportFile(options) {
|
|
192
|
+
const { sourceFile, sourceRoot, buildRoot, tsconfigPath, mode, env, diagnostics } = options;
|
|
193
|
+
const isProduction = mode === 'publish';
|
|
194
|
+
const nodeEnv = env?.NODE_ENV ?? (isProduction ? 'production' : 'development');
|
|
195
|
+
const emitPublishSourcemaps = isProduction && shouldEmitPublishSourcemaps(env);
|
|
196
|
+
const define = {
|
|
197
|
+
'process.env.NODE_ENV': JSON.stringify(nodeEnv)
|
|
198
|
+
};
|
|
199
|
+
try {
|
|
200
|
+
await esbuild({
|
|
201
|
+
entryPoints: [sourceFile],
|
|
202
|
+
bundle: false,
|
|
203
|
+
platform: 'node',
|
|
204
|
+
target: 'node20',
|
|
205
|
+
format: 'esm',
|
|
206
|
+
sourcemap: isProduction ? emitPublishSourcemaps : true,
|
|
207
|
+
outdir: buildRoot,
|
|
208
|
+
outbase: sourceRoot,
|
|
209
|
+
tsconfig: existsSync(tsconfigPath) ? tsconfigPath : undefined,
|
|
210
|
+
define,
|
|
211
|
+
logLevel: 'silent'
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
if (error instanceof Error) {
|
|
216
|
+
diagnostics.push({ severity: 'error', message: error.message });
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
diagnostics.push({ severity: 'error', message: String(error) });
|
|
220
|
+
}
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function runEsbuild(options) {
|
|
225
|
+
const { sourceRoot, buildRoot, tsconfigPath, mode, env, diagnostics, entryPoints } = options;
|
|
226
|
+
const isProduction = mode === 'publish';
|
|
227
|
+
const useIncremental = !isProduction && options.incremental === true;
|
|
228
|
+
const incrementalKey = useIncremental ? createIncrementalKey(mode, buildRoot) : undefined;
|
|
229
|
+
if (!entryPoints || entryPoints.length === 0) {
|
|
230
|
+
if (incrementalKey) {
|
|
231
|
+
await disposeIncrementalBuild(incrementalKey);
|
|
232
|
+
}
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
const entrySignature = useIncremental ? createEntrySignature(entryPoints) : undefined;
|
|
236
|
+
const nodeEnv = env?.NODE_ENV ?? (isProduction ? 'production' : 'development');
|
|
237
|
+
const diagMax = (() => {
|
|
238
|
+
const raw = env?.WEBSTIR_BACKEND_DIAG_MAX;
|
|
239
|
+
const n = typeof raw === 'string' ? parseInt(raw, 10) : NaN;
|
|
240
|
+
return Number.isFinite(n) && n > 0 ? n : 50;
|
|
241
|
+
})();
|
|
242
|
+
const define = {
|
|
243
|
+
'process.env.NODE_ENV': JSON.stringify(nodeEnv)
|
|
244
|
+
};
|
|
245
|
+
const emitPublishSourcemaps = isProduction && shouldEmitPublishSourcemaps(env);
|
|
246
|
+
const start = performance.now();
|
|
247
|
+
try {
|
|
248
|
+
let reusedIncremental = false;
|
|
249
|
+
let result;
|
|
250
|
+
if (isProduction) {
|
|
251
|
+
if (incrementalKey) {
|
|
252
|
+
await disposeIncrementalBuild(incrementalKey);
|
|
253
|
+
}
|
|
254
|
+
result = await esbuild({
|
|
255
|
+
entryPoints: entryPoints,
|
|
256
|
+
bundle: true,
|
|
257
|
+
packages: 'external',
|
|
258
|
+
platform: 'node',
|
|
259
|
+
target: 'node20',
|
|
260
|
+
format: 'esm',
|
|
261
|
+
minify: true,
|
|
262
|
+
sourcemap: emitPublishSourcemaps,
|
|
263
|
+
legalComments: 'none',
|
|
264
|
+
outdir: buildRoot,
|
|
265
|
+
outbase: sourceRoot,
|
|
266
|
+
entryNames: '[dir]/[name]',
|
|
267
|
+
tsconfig: existsSync(tsconfigPath) ? tsconfigPath : undefined,
|
|
268
|
+
define,
|
|
269
|
+
logLevel: 'silent',
|
|
270
|
+
metafile: true
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
else if (useIncremental && incrementalKey && entrySignature) {
|
|
274
|
+
const cached = incrementalBuildCache.get(incrementalKey);
|
|
275
|
+
if (cached && cached.entrySignature === entrySignature) {
|
|
276
|
+
reusedIncremental = true;
|
|
277
|
+
result = await cached.context.rebuild();
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
if (cached) {
|
|
281
|
+
await disposeIncrementalBuild(incrementalKey);
|
|
282
|
+
}
|
|
283
|
+
const ctx = await esbuildContext({
|
|
284
|
+
entryPoints: entryPoints,
|
|
285
|
+
bundle: false,
|
|
286
|
+
platform: 'node',
|
|
287
|
+
target: 'node20',
|
|
288
|
+
format: 'esm',
|
|
289
|
+
sourcemap: true,
|
|
290
|
+
outdir: buildRoot,
|
|
291
|
+
outbase: sourceRoot,
|
|
292
|
+
tsconfig: existsSync(tsconfigPath) ? tsconfigPath : undefined,
|
|
293
|
+
define,
|
|
294
|
+
logLevel: 'silent',
|
|
295
|
+
metafile: true
|
|
296
|
+
});
|
|
297
|
+
incrementalBuildCache.set(incrementalKey, {
|
|
298
|
+
entrySignature,
|
|
299
|
+
context: ctx
|
|
300
|
+
});
|
|
301
|
+
result = await ctx.rebuild();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
if (incrementalKey) {
|
|
306
|
+
await disposeIncrementalBuild(incrementalKey);
|
|
307
|
+
}
|
|
308
|
+
result = await esbuild({
|
|
309
|
+
entryPoints: entryPoints,
|
|
310
|
+
bundle: false,
|
|
311
|
+
platform: 'node',
|
|
312
|
+
target: 'node20',
|
|
313
|
+
format: 'esm',
|
|
314
|
+
sourcemap: true,
|
|
315
|
+
outdir: buildRoot,
|
|
316
|
+
outbase: sourceRoot,
|
|
317
|
+
tsconfig: existsSync(tsconfigPath) ? tsconfigPath : undefined,
|
|
318
|
+
define,
|
|
319
|
+
logLevel: 'silent',
|
|
320
|
+
metafile: true
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
const warnCount = result.warnings?.length ?? 0;
|
|
324
|
+
for (const w of (result.warnings ?? []).slice(0, diagMax)) {
|
|
325
|
+
diagnostics.push({ severity: 'warn', message: formatEsbuildMessage(w) });
|
|
326
|
+
}
|
|
327
|
+
if (warnCount > diagMax) {
|
|
328
|
+
diagnostics.push({
|
|
329
|
+
severity: 'info',
|
|
330
|
+
message: `[webstir-backend] ${isProduction ? 'publish:esbuild' : `${mode}:esbuild`} ... ${warnCount - diagMax} more warning(s) omitted`
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
const end = performance.now();
|
|
334
|
+
const reuseSuffix = reusedIncremental ? ' (incremental)' : '';
|
|
335
|
+
diagnostics.push({
|
|
336
|
+
severity: 'info',
|
|
337
|
+
message: `[webstir-backend] ${isProduction ? 'publish:esbuild' : `${mode}:esbuild`} 0 error(s), ${warnCount} warning(s) in ${(end - start).toFixed(1)}ms${reuseSuffix}`
|
|
338
|
+
});
|
|
339
|
+
return collectOutputSizes(result.metafile, buildRoot);
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
const end = performance.now();
|
|
343
|
+
if (incrementalKey) {
|
|
344
|
+
await disposeIncrementalBuild(incrementalKey);
|
|
345
|
+
}
|
|
346
|
+
if (isEsbuildFailure(error)) {
|
|
347
|
+
const errs = error.errors ?? [];
|
|
348
|
+
const warns = error.warnings ?? [];
|
|
349
|
+
for (const e of errs.slice(0, diagMax)) {
|
|
350
|
+
diagnostics.push({ severity: 'error', message: formatEsbuildMessage(e) });
|
|
351
|
+
}
|
|
352
|
+
for (const w of warns.slice(0, diagMax)) {
|
|
353
|
+
diagnostics.push({ severity: 'warn', message: formatEsbuildMessage(w) });
|
|
354
|
+
}
|
|
355
|
+
if (errs.length > diagMax) {
|
|
356
|
+
diagnostics.push({ severity: 'info', message: `[webstir-backend] ${mode}:esbuild ... ${errs.length - diagMax} more error(s) omitted` });
|
|
357
|
+
}
|
|
358
|
+
if (warns.length > diagMax) {
|
|
359
|
+
diagnostics.push({ severity: 'info', message: `[webstir-backend] ${mode}:esbuild ... ${warns.length - diagMax} more warning(s) omitted` });
|
|
360
|
+
}
|
|
361
|
+
diagnostics.push({ severity: 'info', message: `[webstir-backend] ${mode}:esbuild ${errs.length} error(s), ${warns.length} warning(s) in ${(end - start).toFixed(1)}ms` });
|
|
362
|
+
}
|
|
363
|
+
else if (error instanceof Error) {
|
|
364
|
+
diagnostics.push({ severity: 'error', message: error.message });
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
diagnostics.push({ severity: 'error', message: String(error) });
|
|
368
|
+
}
|
|
369
|
+
throw new Error('esbuild failed.');
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function createIncrementalKey(mode, buildRoot) {
|
|
373
|
+
return `${mode}:${path.resolve(buildRoot)}`;
|
|
374
|
+
}
|
|
375
|
+
async function disposeIncrementalBuild(key) {
|
|
376
|
+
const cached = incrementalBuildCache.get(key);
|
|
377
|
+
if (cached) {
|
|
378
|
+
try {
|
|
379
|
+
await cached.context.dispose();
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
// ignore
|
|
383
|
+
}
|
|
384
|
+
incrementalBuildCache.delete(key);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function clearIncrementalCache() {
|
|
388
|
+
for (const [key, entry] of incrementalBuildCache.entries()) {
|
|
389
|
+
try {
|
|
390
|
+
entry.context.dispose();
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
// ignore
|
|
394
|
+
}
|
|
395
|
+
incrementalBuildCache.delete(key);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function createEntrySignature(entryPoints) {
|
|
399
|
+
return Array.from(entryPoints).sort().join('|');
|
|
400
|
+
}
|
|
401
|
+
export function collectOutputSizes(metafile, buildRoot) {
|
|
402
|
+
const outputs = {};
|
|
403
|
+
if (!metafile || typeof metafile !== 'object') {
|
|
404
|
+
return outputs;
|
|
405
|
+
}
|
|
406
|
+
const mf = metafile;
|
|
407
|
+
for (const [outPath, info] of Object.entries(mf.outputs ?? {})) {
|
|
408
|
+
const rel = path.relative(buildRoot, outPath);
|
|
409
|
+
outputs[rel] = typeof info.bytes === 'number' ? info.bytes : 0;
|
|
410
|
+
}
|
|
411
|
+
return outputs;
|
|
412
|
+
}
|
|
413
|
+
function isEsbuildFailure(error) {
|
|
414
|
+
return typeof error === 'object' && error !== null && ('errors' in error || 'warnings' in error);
|
|
415
|
+
}
|
|
416
|
+
export function formatEsbuildMessage(msg) {
|
|
417
|
+
const text = typeof msg?.text === 'string' ? msg.text : String(msg);
|
|
418
|
+
const loc = msg?.location;
|
|
419
|
+
if (loc && typeof loc.file === 'string') {
|
|
420
|
+
const position = typeof loc.line === 'number' ? `${loc.line}:${loc.column ?? 1}` : '1:1';
|
|
421
|
+
return `${loc.file}:${position} ${text}`;
|
|
422
|
+
}
|
|
423
|
+
return text;
|
|
424
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ModuleManifest, ModuleDiagnostic } from '@webstir-io/module-contract';
|
|
2
|
+
import type { BackendBuildMode } from '../workspace.js';
|
|
3
|
+
export declare function persistAndDiffOutputs(workspaceRoot: string, _buildRoot: string, outputs: Record<string, number> | undefined, env: Record<string, string | undefined>, diagnostics: ModuleDiagnostic[], mode: BackendBuildMode): Promise<void>;
|
|
4
|
+
export declare function persistAndDiffManifest(workspaceRoot: string, manifest: ModuleManifest, env: Record<string, string | undefined>, diagnostics: ModuleDiagnostic[]): Promise<void>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
3
|
+
export async function persistAndDiffOutputs(workspaceRoot, _buildRoot, outputs, env, diagnostics, mode) {
|
|
4
|
+
if (!outputs)
|
|
5
|
+
return;
|
|
6
|
+
try {
|
|
7
|
+
const diagMax = resolveDiagMax(env);
|
|
8
|
+
const webstirDir = path.join(workspaceRoot, '.webstir');
|
|
9
|
+
const cachePath = path.join(webstirDir, 'backend-outputs.json');
|
|
10
|
+
await mkdir(webstirDir, { recursive: true });
|
|
11
|
+
let previous = {};
|
|
12
|
+
try {
|
|
13
|
+
const raw = await readFile(cachePath, 'utf8');
|
|
14
|
+
previous = JSON.parse(raw);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// first run or unreadable cache
|
|
18
|
+
}
|
|
19
|
+
const changed = [];
|
|
20
|
+
for (const [rel, bytes] of Object.entries(outputs)) {
|
|
21
|
+
if (previous[rel] !== bytes) {
|
|
22
|
+
changed.push(rel);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const removed = Object.keys(previous).filter((rel) => outputs[rel] === undefined);
|
|
26
|
+
if (changed.length + removed.length > 0) {
|
|
27
|
+
const list = changed.slice(0, diagMax).join(', ');
|
|
28
|
+
const omitted = changed.length > diagMax ? ` (+${changed.length - diagMax} more)` : '';
|
|
29
|
+
const removedInfo = removed.length > 0 ? `, removed=${removed.length}` : '';
|
|
30
|
+
diagnostics.push({
|
|
31
|
+
severity: 'info',
|
|
32
|
+
message: `[webstir-backend] ${mode}:changed ${changed.length} file(s): ${list}${omitted}${removedInfo}`
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
await writeFile(cachePath, JSON.stringify(outputs, null, 2), 'utf8');
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// ignore cache errors
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function persistAndDiffManifest(workspaceRoot, manifest, env, diagnostics) {
|
|
42
|
+
try {
|
|
43
|
+
const diagMax = resolveDiagMax(env);
|
|
44
|
+
const webstirDir = path.join(workspaceRoot, '.webstir');
|
|
45
|
+
const cachePath = path.join(webstirDir, 'backend-manifest-digest.json');
|
|
46
|
+
await mkdir(webstirDir, { recursive: true });
|
|
47
|
+
const routeKeys = Array.isArray(manifest.routes)
|
|
48
|
+
? manifest.routes.map((r) => `${(r.method ?? '').toUpperCase()} ${r.path ?? ''}`)
|
|
49
|
+
: [];
|
|
50
|
+
const viewPaths = Array.isArray(manifest.views)
|
|
51
|
+
? manifest.views.map((v) => `${v.path ?? ''}`)
|
|
52
|
+
: [];
|
|
53
|
+
const caps = Array.isArray(manifest.capabilities) ? manifest.capabilities : [];
|
|
54
|
+
let previous;
|
|
55
|
+
try {
|
|
56
|
+
const raw = await readFile(cachePath, 'utf8');
|
|
57
|
+
previous = JSON.parse(raw);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// first run; no diff
|
|
61
|
+
}
|
|
62
|
+
if (previous) {
|
|
63
|
+
const prevRoutes = new Set(previous.routes);
|
|
64
|
+
const prevViews = new Set(previous.views);
|
|
65
|
+
const nextRoutes = new Set(routeKeys);
|
|
66
|
+
const nextViews = new Set(viewPaths);
|
|
67
|
+
const addedRoutes = [];
|
|
68
|
+
const removedRoutes = [];
|
|
69
|
+
const addedViews = [];
|
|
70
|
+
const removedViews = [];
|
|
71
|
+
for (const r of nextRoutes)
|
|
72
|
+
if (!prevRoutes.has(r))
|
|
73
|
+
addedRoutes.push(r);
|
|
74
|
+
for (const r of prevRoutes)
|
|
75
|
+
if (!nextRoutes.has(r))
|
|
76
|
+
removedRoutes.push(r);
|
|
77
|
+
for (const v of nextViews)
|
|
78
|
+
if (!prevViews.has(v))
|
|
79
|
+
addedViews.push(v);
|
|
80
|
+
for (const v of prevViews)
|
|
81
|
+
if (!nextViews.has(v))
|
|
82
|
+
removedViews.push(v);
|
|
83
|
+
if (addedRoutes.length + removedRoutes.length + addedViews.length + removedViews.length > 0) {
|
|
84
|
+
const list = (items) => items.slice(0, diagMax).join(', ');
|
|
85
|
+
const routeDelta = `routes +${addedRoutes.length}/-${removedRoutes.length}`;
|
|
86
|
+
const viewDelta = `views +${addedViews.length}/-${removedViews.length}`;
|
|
87
|
+
let msg = `[webstir-backend] manifest changed: ${routeDelta}; ${viewDelta}`;
|
|
88
|
+
const details = [];
|
|
89
|
+
if (addedRoutes.length > 0)
|
|
90
|
+
details.push(`added routes: ${list(addedRoutes)}`);
|
|
91
|
+
if (removedRoutes.length > 0)
|
|
92
|
+
details.push(`removed routes: ${list(removedRoutes)}`);
|
|
93
|
+
if (addedViews.length > 0)
|
|
94
|
+
details.push(`added views: ${list(addedViews)}`);
|
|
95
|
+
if (removedViews.length > 0)
|
|
96
|
+
details.push(`removed views: ${list(removedViews)}`);
|
|
97
|
+
if (details.length > 0) {
|
|
98
|
+
msg += ` — ${details.join(' | ')}`;
|
|
99
|
+
}
|
|
100
|
+
diagnostics.push({ severity: 'info', message: msg });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const digest = { routes: routeKeys, views: viewPaths, capabilities: caps };
|
|
104
|
+
await writeFile(cachePath, JSON.stringify(digest, null, 2), 'utf8');
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// ignore cache errors
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function resolveDiagMax(env, fallback = 50) {
|
|
111
|
+
const raw = env?.WEBSTIR_BACKEND_DIAG_MAX;
|
|
112
|
+
const n = typeof raw === 'string' ? parseInt(raw, 10) : NaN;
|
|
113
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
114
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ModuleDiagnostic, ModuleManifest } from '@webstir-io/module-contract';
|
|
2
|
+
import type { BackendBuildMode } from '../workspace.js';
|
|
3
|
+
export interface CacheReporter {
|
|
4
|
+
readonly diffOutputs: (outputs: Record<string, number> | undefined, mode: BackendBuildMode) => Promise<void>;
|
|
5
|
+
readonly diffManifest: (manifest: ModuleManifest) => Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export declare function createCacheReporter(options: {
|
|
8
|
+
readonly workspaceRoot: string;
|
|
9
|
+
readonly buildRoot: string;
|
|
10
|
+
readonly env: Record<string, string | undefined>;
|
|
11
|
+
readonly diagnostics: ModuleDiagnostic[];
|
|
12
|
+
}): CacheReporter;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { persistAndDiffManifest, persistAndDiffOutputs } from './diff.js';
|
|
2
|
+
export function createCacheReporter(options) {
|
|
3
|
+
const { workspaceRoot, buildRoot, env, diagnostics } = options;
|
|
4
|
+
const diagnosticsTarget = shouldLogCacheDiffs(env) ? diagnostics : [];
|
|
5
|
+
return {
|
|
6
|
+
async diffOutputs(outputs, mode) {
|
|
7
|
+
await persistAndDiffOutputs(workspaceRoot, buildRoot, outputs, env, diagnosticsTarget, mode);
|
|
8
|
+
},
|
|
9
|
+
async diffManifest(manifest) {
|
|
10
|
+
await persistAndDiffManifest(workspaceRoot, manifest, env, diagnosticsTarget);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function shouldLogCacheDiffs(env) {
|
|
15
|
+
const raw = env?.WEBSTIR_BACKEND_CACHE_LOG;
|
|
16
|
+
if (!raw)
|
|
17
|
+
return true;
|
|
18
|
+
const normalized = raw.trim().toLowerCase();
|
|
19
|
+
if (['off', '0', 'false', 'quiet', 'silent', 'skip'].includes(normalized)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ModuleDiagnostic } from '@webstir-io/module-contract';
|
|
2
|
+
export declare function pushEntryBucketSummary(diagnostics: ModuleDiagnostic[], entryPoints: readonly string[]): void;
|
|
3
|
+
type Severity = 'info' | 'warn' | 'error';
|
|
4
|
+
export declare function normalizeLogLevel(value: unknown): Severity;
|
|
5
|
+
export declare function filterDiagnostics(list: readonly ModuleDiagnostic[], min: Severity): readonly ModuleDiagnostic[];
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function pushEntryBucketSummary(diagnostics, entryPoints) {
|
|
2
|
+
try {
|
|
3
|
+
const server = entryPoints.filter((p) => p === 'index.js' || /(^|\/)index\.js$/.test(p) && !/^(functions|jobs)\//.test(p)).length;
|
|
4
|
+
const functionsCount = entryPoints.filter((p) => p.startsWith('functions/')).length;
|
|
5
|
+
const jobsCount = entryPoints.filter((p) => p.startsWith('jobs/')).length;
|
|
6
|
+
diagnostics.push({
|
|
7
|
+
severity: 'info',
|
|
8
|
+
message: `[webstir-backend] entries by bucket: server=${server} functions=${functionsCount} jobs=${jobsCount}`
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// best-effort only
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function normalizeLogLevel(value) {
|
|
16
|
+
if (typeof value !== 'string')
|
|
17
|
+
return 'info';
|
|
18
|
+
const v = value.toLowerCase();
|
|
19
|
+
if (v === 'error' || v === 'warn' || v === 'info')
|
|
20
|
+
return v;
|
|
21
|
+
return 'info';
|
|
22
|
+
}
|
|
23
|
+
export function filterDiagnostics(list, min) {
|
|
24
|
+
const rank = (s) => (s === 'error' ? 3 : s === 'warn' ? 2 : 1);
|
|
25
|
+
const threshold = rank(min);
|
|
26
|
+
return list.filter((d) => rank(d.severity) >= threshold);
|
|
27
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ModuleDiagnostic, ModuleManifest } from '@webstir-io/module-contract';
|
|
2
|
+
export interface LoadManifestOptions {
|
|
3
|
+
readonly workspaceRoot: string;
|
|
4
|
+
readonly buildRoot: string;
|
|
5
|
+
readonly entryPoints: readonly string[];
|
|
6
|
+
readonly diagnostics: ModuleDiagnostic[];
|
|
7
|
+
}
|
|
8
|
+
export declare function loadBackendModuleManifest(options: LoadManifestOptions): Promise<ModuleManifest>;
|
|
9
|
+
export declare function summarizeBuiltManifest(buildRoot: string): Promise<{
|
|
10
|
+
routes: number;
|
|
11
|
+
views: number;
|
|
12
|
+
capabilities?: readonly string[];
|
|
13
|
+
} | undefined>;
|