@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.
Files changed (123) hide show
  1. package/README.md +106 -79
  2. package/dist/add.d.ts +59 -0
  3. package/dist/add.js +626 -0
  4. package/dist/build/artifacts.d.ts +115 -1
  5. package/dist/build/artifacts.js +4 -4
  6. package/dist/build/entries.js +1 -1
  7. package/dist/build/pipeline.d.ts +33 -1
  8. package/dist/build/pipeline.js +307 -65
  9. package/dist/cache/diff.js +9 -8
  10. package/dist/cache/reporters.js +1 -1
  11. package/dist/deploy-cli.d.ts +2 -0
  12. package/dist/deploy-cli.js +86 -0
  13. package/dist/diagnostics/summary.js +2 -2
  14. package/dist/index.d.ts +6 -0
  15. package/dist/index.js +4 -0
  16. package/dist/manifest/pipeline.js +103 -32
  17. package/dist/provider.js +35 -17
  18. package/dist/runtime/bun.d.ts +51 -0
  19. package/dist/runtime/bun.js +499 -0
  20. package/dist/runtime/core.d.ts +141 -0
  21. package/dist/runtime/core.js +316 -0
  22. package/dist/runtime/deploy-backend.d.ts +20 -0
  23. package/dist/runtime/deploy-backend.js +175 -0
  24. package/dist/runtime/deploy-shared.d.ts +43 -0
  25. package/dist/runtime/deploy-shared.js +75 -0
  26. package/dist/runtime/deploy-static.d.ts +2 -0
  27. package/dist/runtime/deploy-static.js +161 -0
  28. package/dist/runtime/deploy.d.ts +3 -0
  29. package/dist/runtime/deploy.js +91 -0
  30. package/dist/runtime/forms.d.ts +73 -0
  31. package/dist/runtime/forms.js +236 -0
  32. package/dist/runtime/request-hooks.d.ts +47 -0
  33. package/dist/runtime/request-hooks.js +102 -0
  34. package/dist/runtime/session-metadata.d.ts +13 -0
  35. package/dist/runtime/session-metadata.js +98 -0
  36. package/dist/runtime/session-runtime.d.ts +28 -0
  37. package/dist/runtime/session-runtime.js +180 -0
  38. package/dist/runtime/session.d.ts +83 -0
  39. package/dist/runtime/session.js +396 -0
  40. package/dist/runtime/views.d.ts +74 -0
  41. package/dist/runtime/views.js +221 -0
  42. package/dist/scaffold/assets.js +25 -21
  43. package/dist/testing/context.js +1 -1
  44. package/dist/testing/index.d.ts +1 -1
  45. package/dist/testing/index.js +100 -56
  46. package/dist/utils/bun.d.ts +2 -0
  47. package/dist/utils/bun.js +13 -0
  48. package/dist/watch.d.ts +13 -1
  49. package/dist/watch.js +345 -97
  50. package/dist/workspace.d.ts +8 -0
  51. package/dist/workspace.js +44 -3
  52. package/package.json +49 -14
  53. package/scripts/publish.sh +2 -92
  54. package/scripts/smoke.mjs +282 -107
  55. package/scripts/update-contract.sh +12 -10
  56. package/src/add.ts +964 -0
  57. package/src/build/artifacts.ts +49 -46
  58. package/src/build/entries.ts +12 -12
  59. package/src/build/pipeline.ts +779 -403
  60. package/src/cache/diff.ts +111 -105
  61. package/src/cache/reporters.ts +26 -26
  62. package/src/deploy-cli.ts +111 -0
  63. package/src/diagnostics/summary.ts +28 -22
  64. package/src/index.ts +11 -0
  65. package/src/manifest/pipeline.ts +328 -215
  66. package/src/provider.ts +115 -98
  67. package/src/runtime/bun.ts +793 -0
  68. package/src/runtime/core.ts +598 -0
  69. package/src/runtime/deploy-backend.ts +239 -0
  70. package/src/runtime/deploy-shared.ts +136 -0
  71. package/src/runtime/deploy-static.ts +191 -0
  72. package/src/runtime/deploy.ts +143 -0
  73. package/src/runtime/forms.ts +364 -0
  74. package/src/runtime/request-hooks.ts +165 -0
  75. package/src/runtime/session-metadata.ts +135 -0
  76. package/src/runtime/session-runtime.ts +267 -0
  77. package/src/runtime/session.ts +642 -0
  78. package/src/runtime/views.ts +385 -0
  79. package/src/scaffold/assets.ts +77 -73
  80. package/src/testing/context.js +8 -9
  81. package/src/testing/context.ts +9 -9
  82. package/src/testing/index.d.ts +14 -3
  83. package/src/testing/index.js +254 -175
  84. package/src/testing/index.ts +298 -195
  85. package/src/testing/types.d.ts +18 -19
  86. package/src/testing/types.ts +18 -18
  87. package/src/utils/bun.ts +26 -0
  88. package/src/watch.ts +503 -99
  89. package/src/workspace.ts +59 -3
  90. package/templates/backend/.env.example +15 -0
  91. package/templates/backend/auth/adapter.ts +335 -36
  92. package/templates/backend/db/connection.ts +190 -65
  93. package/templates/backend/db/migrate.ts +149 -43
  94. package/templates/backend/db/types.d.ts +1 -1
  95. package/templates/backend/env.ts +132 -20
  96. package/templates/backend/functions/hello/index.ts +1 -2
  97. package/templates/backend/index.ts +15 -508
  98. package/templates/backend/jobs/nightly/index.ts +1 -1
  99. package/templates/backend/jobs/runtime.ts +24 -11
  100. package/templates/backend/jobs/scheduler.ts +208 -46
  101. package/templates/backend/module.ts +227 -13
  102. package/templates/backend/observability/logger.ts +2 -12
  103. package/templates/backend/observability/metrics.ts +8 -5
  104. package/templates/backend/session/sqlite.ts +152 -0
  105. package/templates/backend/session/store.ts +45 -0
  106. package/templates/backend/tsconfig.json +1 -1
  107. package/tests/add.test.js +327 -0
  108. package/tests/authAdapter.test.js +315 -0
  109. package/tests/bundlerParity.test.js +217 -0
  110. package/tests/cacheReporter.test.js +10 -10
  111. package/tests/dbConnection.test.js +209 -0
  112. package/tests/deploy.test.js +357 -0
  113. package/tests/envLoader.test.js +271 -17
  114. package/tests/integration.test.js +2432 -3
  115. package/tests/jobsScheduler.test.js +253 -0
  116. package/tests/manifest.test.js +287 -12
  117. package/tests/migrationRunner.test.js +249 -0
  118. package/tests/sessionScaffoldStore.test.js +752 -0
  119. package/tests/sessionStore.test.js +490 -0
  120. package/tests/testing.test.js +252 -0
  121. package/tests/watch.test.js +192 -32
  122. package/tsconfig.json +3 -10
  123. package/templates/backend/server/fastify.ts +0 -288
@@ -1,5 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { existsSync } from 'node:fs';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { mkdir, rm } from 'node:fs/promises';
3
5
  import { spawn } from 'node:child_process';
4
6
  import { performance } from 'node:perf_hooks';
5
7
  import { build as esbuild, context as esbuildContext } from 'esbuild';
@@ -14,57 +16,76 @@ if (typeof process !== 'undefined' && typeof process.once === 'function') {
14
16
  export async function runBackendBuildPipeline(options) {
15
17
  const { sourceRoot, buildRoot, tsconfigPath, diagnostics, incremental, mode } = options;
16
18
  const env = options.env ?? {};
19
+ const bundler = options.bundler ??
20
+ resolveBackendBundler({
21
+ env,
22
+ incremental,
23
+ diagnostics,
24
+ });
17
25
  console.info(`[webstir-backend] ${mode}:tsc start`);
18
26
  if (shouldTypeCheck(mode, env)) {
19
27
  await runTypeCheck(tsconfigPath, env, diagnostics);
20
28
  }
21
29
  else {
22
- diagnostics.push({ severity: 'info', message: '[webstir-backend] type-check skipped by WEBSTIR_BACKEND_TYPECHECK' });
30
+ diagnostics.push({
31
+ severity: 'info',
32
+ message: '[webstir-backend] type-check skipped by WEBSTIR_BACKEND_TYPECHECK',
33
+ });
23
34
  }
24
35
  console.info(`[webstir-backend] ${mode}:tsc done`);
25
36
  const entryPoints = await discoverEntryPoints(sourceRoot);
26
37
  if (entryPoints.length === 0) {
27
38
  diagnostics.push({
28
39
  severity: 'warn',
29
- message: `No backend entry points found under ${sourceRoot} (expected index.* or functions/*/index.* or jobs/*/index.*).`
40
+ message: `No backend entry points found under ${sourceRoot} (expected index.* or functions/*/index.* or jobs/*/index.*).`,
30
41
  });
31
42
  }
32
- console.info(`[webstir-backend] ${mode}:esbuild start`);
33
- const outputs = await runEsbuild({
43
+ if (bundler === 'bun') {
44
+ await resetBuildRoot(buildRoot);
45
+ }
46
+ console.info(`[webstir-backend] ${mode}:${bundler} start`);
47
+ const outputs = bundler === 'bun'
48
+ ? await runBunBuild({
49
+ sourceRoot,
50
+ buildRoot,
51
+ tsconfigPath,
52
+ mode,
53
+ env,
54
+ incremental,
55
+ diagnostics,
56
+ entryPoints,
57
+ })
58
+ : await runEsbuild({
59
+ sourceRoot,
60
+ buildRoot,
61
+ tsconfigPath,
62
+ mode,
63
+ env,
64
+ incremental,
65
+ diagnostics,
66
+ entryPoints,
67
+ });
68
+ console.info(`[webstir-backend] ${mode}:${bundler} done`);
69
+ await ensureModuleDefinitionBuild({
34
70
  sourceRoot,
35
71
  buildRoot,
36
72
  tsconfigPath,
37
73
  mode,
38
74
  env,
39
- incremental,
40
75
  diagnostics,
41
- entryPoints
42
76
  });
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
77
  const includePublishSourcemaps = mode === 'publish' && shouldEmitPublishSourcemaps(env);
57
78
  return {
58
79
  entryPoints,
59
80
  outputs,
60
- includePublishSourcemaps
81
+ includePublishSourcemaps,
61
82
  };
62
83
  }
63
84
  async function runTypeCheck(tsconfigPath, env, diagnostics) {
64
85
  if (!existsSync(tsconfigPath)) {
65
86
  diagnostics.push({
66
87
  severity: 'warn',
67
- message: `TypeScript config not found at ${tsconfigPath}; skipping type-check.`
88
+ message: `TypeScript config not found at ${tsconfigPath}; skipping type-check.`,
68
89
  });
69
90
  return;
70
91
  }
@@ -73,8 +94,8 @@ async function runTypeCheck(tsconfigPath, env, diagnostics) {
73
94
  stdio: 'pipe',
74
95
  env: {
75
96
  ...process.env,
76
- ...env
77
- }
97
+ ...env,
98
+ },
78
99
  });
79
100
  let stdout = '';
80
101
  let stderr = '';
@@ -85,9 +106,12 @@ async function runTypeCheck(tsconfigPath, env, diagnostics) {
85
106
  stderr += chunk.toString();
86
107
  });
87
108
  child.on('error', (err) => {
88
- const code = (err && typeof err === 'object') ? err.code : undefined;
109
+ const code = err.code;
89
110
  if (code === 'ENOENT') {
90
- diagnostics.push({ severity: 'warn', message: 'TypeScript compiler (tsc) not found in PATH; skipping type-check.' });
111
+ diagnostics.push({
112
+ severity: 'warn',
113
+ message: 'TypeScript compiler (tsc) not found in PATH; skipping type-check.',
114
+ });
91
115
  resolve();
92
116
  return;
93
117
  }
@@ -101,7 +125,7 @@ async function runTypeCheck(tsconfigPath, env, diagnostics) {
101
125
  diagnostics.push({
102
126
  severity: 'error',
103
127
  message: `Type checking failed (exit code ${code}).`,
104
- file: tsconfigPath
128
+ file: tsconfigPath,
105
129
  });
106
130
  if (stderr.trim()) {
107
131
  diagnostics.push({ severity: 'error', message: stderr.trim() });
@@ -124,6 +148,32 @@ export function shouldTypeCheck(mode, env) {
124
148
  }
125
149
  return true;
126
150
  }
151
+ export function resolveBackendBundler(options) {
152
+ const requestedBundler = normalizeBackendBundler(options.env?.WEBSTIR_BACKEND_BUNDLER);
153
+ if (requestedBundler !== 'bun') {
154
+ return 'esbuild';
155
+ }
156
+ if (options.incremental) {
157
+ options.diagnostics?.push({
158
+ severity: 'info',
159
+ message: '[webstir-backend] WEBSTIR_BACKEND_BUNDLER=bun requested for an incremental build; falling back to esbuild.',
160
+ });
161
+ return 'esbuild';
162
+ }
163
+ if (!getBunBuild()) {
164
+ options.diagnostics?.push({
165
+ severity: 'warn',
166
+ message: '[webstir-backend] WEBSTIR_BACKEND_BUNDLER=bun requested outside a Bun runtime; falling back to esbuild.',
167
+ });
168
+ return 'esbuild';
169
+ }
170
+ return 'bun';
171
+ }
172
+ function normalizeBackendBundler(rawBundler) {
173
+ return typeof rawBundler === 'string' && rawBundler.trim().toLowerCase() === 'bun'
174
+ ? 'bun'
175
+ : 'esbuild';
176
+ }
127
177
  function shouldEmitPublishSourcemaps(env) {
128
178
  const flag = env?.WEBSTIR_BACKEND_SOURCEMAPS;
129
179
  if (typeof flag !== 'string') {
@@ -139,36 +189,74 @@ async function discoverModuleDefinitionSource(sourceRoot) {
139
189
  cwd: sourceRoot,
140
190
  absolute: true,
141
191
  nodir: true,
142
- dot: false
192
+ dot: false,
143
193
  });
144
194
  if (matches.length > 0) {
145
195
  return matches[0];
146
196
  }
147
197
  }
198
+ const indexPatterns = ['index.{ts,tsx,js,mjs}'];
199
+ for (const pattern of indexPatterns) {
200
+ const matches = await glob(pattern, {
201
+ cwd: sourceRoot,
202
+ absolute: true,
203
+ nodir: true,
204
+ dot: false,
205
+ });
206
+ for (const candidate of matches) {
207
+ if (await sourceExportsNamedModuleDefinition(candidate)) {
208
+ return candidate;
209
+ }
210
+ }
211
+ }
148
212
  return undefined;
149
213
  }
214
+ async function sourceExportsNamedModuleDefinition(sourceFile) {
215
+ try {
216
+ const source = await readFile(sourceFile, 'utf8');
217
+ return (/\bexport\s+(const|let|var)\s+module\b/.test(source) ||
218
+ /\bexport\s*\{\s*module\b/.test(source));
219
+ }
220
+ catch {
221
+ return false;
222
+ }
223
+ }
224
+ export async function ensureModuleDefinitionBuild(options) {
225
+ const moduleSource = await discoverModuleDefinitionSource(options.sourceRoot);
226
+ if (!moduleSource) {
227
+ return;
228
+ }
229
+ await buildModuleDefinition({
230
+ sourceFile: moduleSource,
231
+ sourceRoot: options.sourceRoot,
232
+ buildRoot: options.buildRoot,
233
+ tsconfigPath: options.tsconfigPath,
234
+ mode: options.mode,
235
+ env: options.env,
236
+ diagnostics: options.diagnostics,
237
+ });
238
+ }
150
239
  async function buildModuleDefinition(options) {
151
- const { sourceFile, sourceRoot, buildRoot, tsconfigPath, mode, env, diagnostics } = options;
240
+ const { sourceFile, buildRoot, tsconfigPath, mode, env, diagnostics } = options;
152
241
  const isProduction = mode === 'publish';
153
242
  const nodeEnv = env?.NODE_ENV ?? (isProduction ? 'production' : 'development');
154
243
  const emitPublishSourcemaps = isProduction && shouldEmitPublishSourcemaps(env);
155
244
  const define = {
156
- 'process.env.NODE_ENV': JSON.stringify(nodeEnv)
245
+ 'process.env.NODE_ENV': JSON.stringify(nodeEnv),
157
246
  };
158
247
  try {
159
248
  await esbuild({
160
249
  entryPoints: [sourceFile],
161
- bundle: false,
250
+ bundle: true,
251
+ packages: 'external',
162
252
  platform: 'node',
163
253
  target: 'node20',
164
254
  format: 'esm',
165
255
  sourcemap: isProduction ? emitPublishSourcemaps : true,
166
- outdir: buildRoot,
167
- outbase: sourceRoot,
168
- entryNames: '[dir]/[name]',
256
+ outfile: path.join(buildRoot, 'module.js'),
169
257
  tsconfig: existsSync(tsconfigPath) ? tsconfigPath : undefined,
170
258
  define,
171
- logLevel: 'silent'
259
+ logLevel: 'silent',
172
260
  });
173
261
  }
174
262
  catch (error) {
@@ -194,22 +282,43 @@ export async function buildSupportFile(options) {
194
282
  const nodeEnv = env?.NODE_ENV ?? (isProduction ? 'production' : 'development');
195
283
  const emitPublishSourcemaps = isProduction && shouldEmitPublishSourcemaps(env);
196
284
  const define = {
197
- 'process.env.NODE_ENV': JSON.stringify(nodeEnv)
285
+ 'process.env.NODE_ENV': JSON.stringify(nodeEnv),
198
286
  };
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'
287
+ const bundler = options.bundler ??
288
+ resolveBackendBundler({
289
+ env,
290
+ incremental: false,
291
+ diagnostics,
212
292
  });
293
+ const diagMax = readDiagMax(env, 50);
294
+ try {
295
+ if (bundler === 'bun') {
296
+ const result = await runBunCompile({
297
+ entryPoints: [sourceFile],
298
+ sourceRoot,
299
+ buildRoot,
300
+ tsconfigPath,
301
+ define,
302
+ minify: false,
303
+ includeSourceMaps: isProduction ? emitPublishSourcemaps : true,
304
+ });
305
+ ensureBunCompileSucceeded(result, diagnostics, `${mode}:bun:support`, diagMax);
306
+ }
307
+ else {
308
+ await esbuild({
309
+ entryPoints: [sourceFile],
310
+ bundle: false,
311
+ platform: 'node',
312
+ target: 'node20',
313
+ format: 'esm',
314
+ sourcemap: isProduction ? emitPublishSourcemaps : true,
315
+ outdir: buildRoot,
316
+ outbase: sourceRoot,
317
+ tsconfig: existsSync(tsconfigPath) ? tsconfigPath : undefined,
318
+ define,
319
+ logLevel: 'silent',
320
+ });
321
+ }
213
322
  }
214
323
  catch (error) {
215
324
  if (error instanceof Error) {
@@ -234,13 +343,9 @@ async function runEsbuild(options) {
234
343
  }
235
344
  const entrySignature = useIncremental ? createEntrySignature(entryPoints) : undefined;
236
345
  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
- })();
346
+ const diagMax = readDiagMax(env, 50);
242
347
  const define = {
243
- 'process.env.NODE_ENV': JSON.stringify(nodeEnv)
348
+ 'process.env.NODE_ENV': JSON.stringify(nodeEnv),
244
349
  };
245
350
  const emitPublishSourcemaps = isProduction && shouldEmitPublishSourcemaps(env);
246
351
  const start = performance.now();
@@ -267,7 +372,7 @@ async function runEsbuild(options) {
267
372
  tsconfig: existsSync(tsconfigPath) ? tsconfigPath : undefined,
268
373
  define,
269
374
  logLevel: 'silent',
270
- metafile: true
375
+ metafile: true,
271
376
  });
272
377
  }
273
378
  else if (useIncremental && incrementalKey && entrySignature) {
@@ -292,11 +397,11 @@ async function runEsbuild(options) {
292
397
  tsconfig: existsSync(tsconfigPath) ? tsconfigPath : undefined,
293
398
  define,
294
399
  logLevel: 'silent',
295
- metafile: true
400
+ metafile: true,
296
401
  });
297
402
  incrementalBuildCache.set(incrementalKey, {
298
403
  entrySignature,
299
- context: ctx
404
+ context: ctx,
300
405
  });
301
406
  result = await ctx.rebuild();
302
407
  }
@@ -317,7 +422,7 @@ async function runEsbuild(options) {
317
422
  tsconfig: existsSync(tsconfigPath) ? tsconfigPath : undefined,
318
423
  define,
319
424
  logLevel: 'silent',
320
- metafile: true
425
+ metafile: true,
321
426
  });
322
427
  }
323
428
  const warnCount = result.warnings?.length ?? 0;
@@ -327,14 +432,14 @@ async function runEsbuild(options) {
327
432
  if (warnCount > diagMax) {
328
433
  diagnostics.push({
329
434
  severity: 'info',
330
- message: `[webstir-backend] ${isProduction ? 'publish:esbuild' : `${mode}:esbuild`} ... ${warnCount - diagMax} more warning(s) omitted`
435
+ message: `[webstir-backend] ${isProduction ? 'publish:esbuild' : `${mode}:esbuild`} ... ${warnCount - diagMax} more warning(s) omitted`,
331
436
  });
332
437
  }
333
438
  const end = performance.now();
334
439
  const reuseSuffix = reusedIncremental ? ' (incremental)' : '';
335
440
  diagnostics.push({
336
441
  severity: 'info',
337
- message: `[webstir-backend] ${isProduction ? 'publish:esbuild' : `${mode}:esbuild`} 0 error(s), ${warnCount} warning(s) in ${(end - start).toFixed(1)}ms${reuseSuffix}`
442
+ message: `[webstir-backend] ${isProduction ? 'publish:esbuild' : `${mode}:esbuild`} 0 error(s), ${warnCount} warning(s) in ${(end - start).toFixed(1)}ms${reuseSuffix}`,
338
443
  });
339
444
  return collectOutputSizes(result.metafile, buildRoot);
340
445
  }
@@ -353,12 +458,21 @@ async function runEsbuild(options) {
353
458
  diagnostics.push({ severity: 'warn', message: formatEsbuildMessage(w) });
354
459
  }
355
460
  if (errs.length > diagMax) {
356
- diagnostics.push({ severity: 'info', message: `[webstir-backend] ${mode}:esbuild ... ${errs.length - diagMax} more error(s) omitted` });
461
+ diagnostics.push({
462
+ severity: 'info',
463
+ message: `[webstir-backend] ${mode}:esbuild ... ${errs.length - diagMax} more error(s) omitted`,
464
+ });
357
465
  }
358
466
  if (warns.length > diagMax) {
359
- diagnostics.push({ severity: 'info', message: `[webstir-backend] ${mode}:esbuild ... ${warns.length - diagMax} more warning(s) omitted` });
467
+ diagnostics.push({
468
+ severity: 'info',
469
+ message: `[webstir-backend] ${mode}:esbuild ... ${warns.length - diagMax} more warning(s) omitted`,
470
+ });
360
471
  }
361
- diagnostics.push({ severity: 'info', message: `[webstir-backend] ${mode}:esbuild ${errs.length} error(s), ${warns.length} warning(s) in ${(end - start).toFixed(1)}ms` });
472
+ diagnostics.push({
473
+ severity: 'info',
474
+ message: `[webstir-backend] ${mode}:esbuild ${errs.length} error(s), ${warns.length} warning(s) in ${(end - start).toFixed(1)}ms`,
475
+ });
362
476
  }
363
477
  else if (error instanceof Error) {
364
478
  diagnostics.push({ severity: 'error', message: error.message });
@@ -369,6 +483,130 @@ async function runEsbuild(options) {
369
483
  throw new Error('esbuild failed.');
370
484
  }
371
485
  }
486
+ async function runBunBuild(options) {
487
+ const { sourceRoot, buildRoot, tsconfigPath, mode, env, diagnostics, entryPoints } = options;
488
+ const isProduction = mode === 'publish';
489
+ if (!entryPoints || entryPoints.length === 0) {
490
+ return undefined;
491
+ }
492
+ const nodeEnv = env?.NODE_ENV ?? (isProduction ? 'production' : 'development');
493
+ const diagMax = readDiagMax(env, 50);
494
+ const define = {
495
+ 'process.env.NODE_ENV': JSON.stringify(nodeEnv),
496
+ };
497
+ const emitPublishSourcemaps = isProduction && shouldEmitPublishSourcemaps(env);
498
+ const start = performance.now();
499
+ try {
500
+ const result = await runBunCompile({
501
+ entryPoints,
502
+ sourceRoot,
503
+ buildRoot,
504
+ tsconfigPath,
505
+ define,
506
+ minify: isProduction,
507
+ includeSourceMaps: isProduction ? emitPublishSourcemaps : true,
508
+ });
509
+ const { errorCount, warningCount } = pushBunLogs(diagnostics, result.logs, `${mode}:bun`, diagMax);
510
+ const end = performance.now();
511
+ diagnostics.push({
512
+ severity: 'info',
513
+ message: `[webstir-backend] ${mode}:bun ${errorCount} error(s), ${warningCount} warning(s) in ${(end - start).toFixed(1)}ms`,
514
+ });
515
+ if (!result.success || errorCount > 0) {
516
+ throw new Error('bun build failed.');
517
+ }
518
+ return collectBunOutputSizes(result.outputs, buildRoot);
519
+ }
520
+ catch (error) {
521
+ const end = performance.now();
522
+ if (error instanceof Error) {
523
+ diagnostics.push({ severity: 'error', message: error.message });
524
+ }
525
+ else {
526
+ diagnostics.push({ severity: 'error', message: String(error) });
527
+ }
528
+ diagnostics.push({
529
+ severity: 'info',
530
+ message: `[webstir-backend] ${mode}:bun failed in ${(end - start).toFixed(1)}ms`,
531
+ });
532
+ throw new Error('bun build failed.');
533
+ }
534
+ }
535
+ async function runBunCompile(options) {
536
+ const build = getBunBuild();
537
+ if (!build) {
538
+ throw new Error('Bun.build() is not available in the current runtime.');
539
+ }
540
+ return await build({
541
+ entrypoints: [...options.entryPoints],
542
+ root: options.sourceRoot,
543
+ outdir: options.buildRoot,
544
+ target: 'node',
545
+ format: 'esm',
546
+ splitting: false,
547
+ packages: 'external',
548
+ minify: options.minify,
549
+ sourcemap: options.includeSourceMaps ? 'linked' : 'none',
550
+ tsconfig: existsSync(options.tsconfigPath) ? options.tsconfigPath : undefined,
551
+ define: options.define,
552
+ throw: false,
553
+ });
554
+ }
555
+ function ensureBunCompileSucceeded(result, diagnostics, label, diagMax) {
556
+ const { errorCount } = pushBunLogs(diagnostics, result.logs, label, diagMax);
557
+ if (!result.success || errorCount > 0) {
558
+ throw new Error('bun build failed.');
559
+ }
560
+ }
561
+ function pushBunLogs(diagnostics, logs, label, diagMax) {
562
+ const entries = Array.isArray(logs) ? logs : [];
563
+ const errorLogs = entries.filter((log) => log.level === 'error');
564
+ const warningLogs = entries.filter((log) => log.level === 'warning');
565
+ for (const entry of errorLogs.slice(0, diagMax)) {
566
+ diagnostics.push({ severity: 'error', message: formatEsbuildMessage(entry) });
567
+ }
568
+ for (const entry of warningLogs.slice(0, diagMax)) {
569
+ diagnostics.push({ severity: 'warn', message: formatEsbuildMessage(entry) });
570
+ }
571
+ if (errorLogs.length > diagMax) {
572
+ diagnostics.push({
573
+ severity: 'info',
574
+ message: `[webstir-backend] ${label} ... ${errorLogs.length - diagMax} more error(s) omitted`,
575
+ });
576
+ }
577
+ if (warningLogs.length > diagMax) {
578
+ diagnostics.push({
579
+ severity: 'info',
580
+ message: `[webstir-backend] ${label} ... ${warningLogs.length - diagMax} more warning(s) omitted`,
581
+ });
582
+ }
583
+ return {
584
+ errorCount: errorLogs.length,
585
+ warningCount: warningLogs.length,
586
+ };
587
+ }
588
+ function getBunBuild() {
589
+ const runtime = globalThis;
590
+ const build = runtime.Bun?.build;
591
+ return typeof build === 'function' ? build.bind(runtime.Bun) : undefined;
592
+ }
593
+ function collectBunOutputSizes(outputs, buildRoot) {
594
+ const collected = {};
595
+ for (const output of outputs ?? []) {
596
+ const rel = path.relative(buildRoot, output.path);
597
+ collected[rel] = typeof output.size === 'number' ? output.size : 0;
598
+ }
599
+ return collected;
600
+ }
601
+ async function resetBuildRoot(buildRoot) {
602
+ await rm(buildRoot, { recursive: true, force: true });
603
+ await mkdir(buildRoot, { recursive: true });
604
+ }
605
+ function readDiagMax(env, fallback) {
606
+ const raw = env?.WEBSTIR_BACKEND_DIAG_MAX;
607
+ const n = typeof raw === 'string' ? parseInt(raw, 10) : NaN;
608
+ return Number.isFinite(n) && n > 0 ? n : fallback;
609
+ }
372
610
  function createIncrementalKey(mode, buildRoot) {
373
611
  return `${mode}:${path.resolve(buildRoot)}`;
374
612
  }
@@ -414,8 +652,12 @@ function isEsbuildFailure(error) {
414
652
  return typeof error === 'object' && error !== null && ('errors' in error || 'warnings' in error);
415
653
  }
416
654
  export function formatEsbuildMessage(msg) {
417
- const text = typeof msg?.text === 'string' ? msg.text : String(msg);
418
- const loc = msg?.location;
655
+ const text = typeof msg.message === 'string'
656
+ ? msg.message
657
+ : typeof msg.text === 'string'
658
+ ? msg.text
659
+ : String(msg);
660
+ const loc = msg.location ?? msg.position;
419
661
  if (loc && typeof loc.file === 'string') {
420
662
  const position = typeof loc.line === 'number' ? `${loc.line}:${loc.column ?? 1}` : '1:1';
421
663
  return `${loc.file}:${position} ${text}`;
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path';
2
- import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
+ import { mkdir } from 'node:fs/promises';
3
+ import { readTextFile, writeTextFile } from '../utils/bun.js';
3
4
  export async function persistAndDiffOutputs(workspaceRoot, _buildRoot, outputs, env, diagnostics, mode) {
4
5
  if (!outputs)
5
6
  return;
@@ -10,7 +11,7 @@ export async function persistAndDiffOutputs(workspaceRoot, _buildRoot, outputs,
10
11
  await mkdir(webstirDir, { recursive: true });
11
12
  let previous = {};
12
13
  try {
13
- const raw = await readFile(cachePath, 'utf8');
14
+ const raw = await readTextFile(cachePath);
14
15
  previous = JSON.parse(raw);
15
16
  }
16
17
  catch {
@@ -29,10 +30,10 @@ export async function persistAndDiffOutputs(workspaceRoot, _buildRoot, outputs,
29
30
  const removedInfo = removed.length > 0 ? `, removed=${removed.length}` : '';
30
31
  diagnostics.push({
31
32
  severity: 'info',
32
- message: `[webstir-backend] ${mode}:changed ${changed.length} file(s): ${list}${omitted}${removedInfo}`
33
+ message: `[webstir-backend] ${mode}:changed ${changed.length} file(s): ${list}${omitted}${removedInfo}`,
33
34
  });
34
35
  }
35
- await writeFile(cachePath, JSON.stringify(outputs, null, 2), 'utf8');
36
+ await writeTextFile(cachePath, JSON.stringify(outputs, null, 2));
36
37
  }
37
38
  catch {
38
39
  // ignore cache errors
@@ -45,15 +46,15 @@ export async function persistAndDiffManifest(workspaceRoot, manifest, env, diagn
45
46
  const cachePath = path.join(webstirDir, 'backend-manifest-digest.json');
46
47
  await mkdir(webstirDir, { recursive: true });
47
48
  const routeKeys = Array.isArray(manifest.routes)
48
- ? manifest.routes.map((r) => `${(r.method ?? '').toUpperCase()} ${r.path ?? ''}`)
49
+ ? manifest.routes.map((route) => `${(route.method ?? '').toUpperCase()} ${route.path ?? ''}`)
49
50
  : [];
50
51
  const viewPaths = Array.isArray(manifest.views)
51
- ? manifest.views.map((v) => `${v.path ?? ''}`)
52
+ ? manifest.views.map((view) => `${view.path ?? ''}`)
52
53
  : [];
53
54
  const caps = Array.isArray(manifest.capabilities) ? manifest.capabilities : [];
54
55
  let previous;
55
56
  try {
56
- const raw = await readFile(cachePath, 'utf8');
57
+ const raw = await readTextFile(cachePath);
57
58
  previous = JSON.parse(raw);
58
59
  }
59
60
  catch {
@@ -101,7 +102,7 @@ export async function persistAndDiffManifest(workspaceRoot, manifest, env, diagn
101
102
  }
102
103
  }
103
104
  const digest = { routes: routeKeys, views: viewPaths, capabilities: caps };
104
- await writeFile(cachePath, JSON.stringify(digest, null, 2), 'utf8');
105
+ await writeTextFile(cachePath, JSON.stringify(digest, null, 2));
105
106
  }
106
107
  catch {
107
108
  // ignore cache errors
@@ -8,7 +8,7 @@ export function createCacheReporter(options) {
8
8
  },
9
9
  async diffManifest(manifest) {
10
10
  await persistAndDiffManifest(workspaceRoot, manifest, env, diagnosticsTarget);
11
- }
11
+ },
12
12
  };
13
13
  }
14
14
  function shouldLogCacheDiffs(env) {
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ export {};