@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.
Files changed (79) hide show
  1. package/README.md +427 -0
  2. package/dist/build/artifacts.d.ts +113 -0
  3. package/dist/build/artifacts.js +53 -0
  4. package/dist/build/entries.d.ts +1 -0
  5. package/dist/build/entries.js +17 -0
  6. package/dist/build/pipeline.d.ts +31 -0
  7. package/dist/build/pipeline.js +424 -0
  8. package/dist/cache/diff.d.ts +4 -0
  9. package/dist/cache/diff.js +114 -0
  10. package/dist/cache/reporters.d.ts +12 -0
  11. package/dist/cache/reporters.js +23 -0
  12. package/dist/diagnostics/summary.d.ts +6 -0
  13. package/dist/diagnostics/summary.js +27 -0
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.js +2 -0
  16. package/dist/manifest/pipeline.d.ts +13 -0
  17. package/dist/manifest/pipeline.js +224 -0
  18. package/dist/provider.d.ts +2 -0
  19. package/dist/provider.js +101 -0
  20. package/dist/scaffold/assets.d.ts +2 -0
  21. package/dist/scaffold/assets.js +77 -0
  22. package/dist/testing/context.d.ts +3 -0
  23. package/dist/testing/context.js +14 -0
  24. package/dist/testing/index.d.ts +6 -0
  25. package/dist/testing/index.js +208 -0
  26. package/dist/testing/types.d.ts +28 -0
  27. package/dist/testing/types.js +1 -0
  28. package/dist/watch.d.ts +8 -0
  29. package/dist/watch.js +159 -0
  30. package/dist/workspace.d.ts +4 -0
  31. package/dist/workspace.js +15 -0
  32. package/package.json +74 -0
  33. package/scripts/publish.sh +99 -0
  34. package/scripts/smoke.mjs +241 -0
  35. package/scripts/update-contract.sh +122 -0
  36. package/src/build/artifacts.ts +67 -0
  37. package/src/build/entries.ts +19 -0
  38. package/src/build/pipeline.ts +507 -0
  39. package/src/cache/diff.ts +128 -0
  40. package/src/cache/reporters.ts +41 -0
  41. package/src/diagnostics/summary.ts +32 -0
  42. package/src/index.ts +2 -0
  43. package/src/manifest/pipeline.ts +270 -0
  44. package/src/provider.ts +124 -0
  45. package/src/scaffold/assets.ts +81 -0
  46. package/src/testing/context.d.ts +3 -0
  47. package/src/testing/context.js +14 -0
  48. package/src/testing/context.ts +17 -0
  49. package/src/testing/index.d.ts +6 -0
  50. package/src/testing/index.js +208 -0
  51. package/src/testing/index.ts +252 -0
  52. package/src/testing/types.d.ts +28 -0
  53. package/src/testing/types.js +1 -0
  54. package/src/testing/types.ts +32 -0
  55. package/src/watch.ts +177 -0
  56. package/src/workspace.ts +22 -0
  57. package/templates/backend/.env.example +13 -0
  58. package/templates/backend/auth/adapter.ts +160 -0
  59. package/templates/backend/db/connection.ts +99 -0
  60. package/templates/backend/db/migrate.ts +231 -0
  61. package/templates/backend/db/migrations/0001-example.ts +17 -0
  62. package/templates/backend/db/types.d.ts +2 -0
  63. package/templates/backend/env.ts +174 -0
  64. package/templates/backend/functions/hello/index.ts +29 -0
  65. package/templates/backend/index.ts +532 -0
  66. package/templates/backend/jobs/nightly/index.ts +28 -0
  67. package/templates/backend/jobs/runtime.ts +103 -0
  68. package/templates/backend/jobs/scheduler.ts +193 -0
  69. package/templates/backend/module.ts +87 -0
  70. package/templates/backend/observability/logger.ts +24 -0
  71. package/templates/backend/observability/metrics.ts +78 -0
  72. package/templates/backend/server/fastify.ts +288 -0
  73. package/templates/backend/tsconfig.json +19 -0
  74. package/tests/cacheReporter.test.js +89 -0
  75. package/tests/envLoader.test.js +64 -0
  76. package/tests/integration.test.js +108 -0
  77. package/tests/manifest.test.js +159 -0
  78. package/tests/watch.test.js +100 -0
  79. 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
+ }
@@ -0,0 +1,2 @@
1
+ export { backendProvider } from './provider.js';
2
+ export { startBackendWatch } from './watch.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { backendProvider } from './provider.js';
2
+ export { startBackendWatch } from './watch.js';
@@ -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>;