@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
package/dist/watch.js CHANGED
@@ -1,39 +1,43 @@
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 } from 'node:child_process';
4
5
  import { performance } from 'node:perf_hooks';
5
- import { context as createEsbuildContext } from 'esbuild';
6
- import { collectOutputSizes, formatEsbuildMessage, shouldTypeCheck } from './build/pipeline.js';
6
+ import { glob } from 'glob';
7
+ import { ensureModuleDefinitionBuild, formatEsbuildMessage, shouldTypeCheck, } from './build/pipeline.js';
7
8
  import { discoverEntryPoints } from './build/entries.js';
8
9
  import { loadBackendModuleManifest } from './manifest/pipeline.js';
9
10
  import { createCacheReporter } from './cache/reporters.js';
10
- import { normalizeMode, resolveWorkspacePaths } from './workspace.js';
11
+ import { normalizeMode, resolveWorkspacePaths, resolveWorkspaceRoot } from './workspace.js';
12
+ const WATCH_POLL_INTERVAL_MS = 250;
11
13
  export async function startBackendWatch(options) {
12
- const { workspaceRoot } = options;
13
- const env = options.env ?? {};
14
+ const env = { ...process.env, ...(options.env ?? {}) };
15
+ const workspaceRoot = resolveWorkspaceRoot({
16
+ workspaceRoot: options.workspaceRoot,
17
+ env,
18
+ });
14
19
  const paths = resolveWorkspacePaths(workspaceRoot);
15
20
  const tsconfigPath = path.join(paths.sourceRoot, 'tsconfig.json');
16
21
  const mode = normalizeMode(env.WEBSTIR_MODULE_MODE);
17
- const entryPoints = await discoverEntryPoints(paths.sourceRoot);
18
- if (entryPoints.length === 0) {
22
+ const initialEntryPoints = await discoverEntryPoints(paths.sourceRoot);
23
+ if (initialEntryPoints.length === 0) {
19
24
  console.warn(`[webstir-backend] watch: no entry found under ${paths.sourceRoot} (index.ts/js)`);
20
25
  throw new Error('No backend entry point found.');
21
26
  }
22
27
  const nodeEnv = env.NODE_ENV ?? (mode === 'publish' ? 'production' : 'development');
28
+ const shouldReportBunBenchmark = isEnabled(env.WEBSTIR_BACKEND_WATCH_BUN_BENCHMARK);
23
29
  const diagMax = (() => {
24
30
  const raw = env.WEBSTIR_BACKEND_DIAG_MAX;
25
31
  const n = typeof raw === 'string' ? parseInt(raw, 10) : NaN;
26
32
  return Number.isFinite(n) && n > 0 ? n : 20;
27
33
  })();
28
34
  console.info(`[webstir-backend] watch:start (${mode})`);
29
- // Start type-checker in watch mode (no emit) unless explicitly skipped for DX.
30
- const shouldRunTypecheck = shouldTypeCheck(mode, env);
31
35
  let tscProc;
32
- if (shouldRunTypecheck) {
36
+ if (shouldTypeCheck(mode, env)) {
33
37
  const tscArgs = ['-p', tsconfigPath, '--noEmit', '--watch'];
34
38
  tscProc = spawn('tsc', tscArgs, {
35
39
  stdio: ['ignore', 'pipe', 'pipe'],
36
- env: { ...process.env, ...env, NODE_ENV: nodeEnv },
40
+ env: { ...env, NODE_ENV: nodeEnv },
37
41
  cwd: workspaceRoot,
38
42
  });
39
43
  tscProc.stdout?.on('data', (chunk) => {
@@ -54,98 +58,86 @@ export async function startBackendWatch(options) {
54
58
  else {
55
59
  console.info('[webstir-backend] watch: type-check skipped by WEBSTIR_BACKEND_TYPECHECK');
56
60
  }
57
- const timingPlugin = {
58
- name: 'webstir-watch-logger',
59
- setup(build) {
60
- let start = 0;
61
- build.onStart(() => {
62
- start = performance.now();
63
- });
64
- build.onEnd(async (result) => {
65
- const end = performance.now();
66
- const warnCount = result.warnings?.length ?? 0;
67
- // errors is not in the typed result, but present at runtime
68
- const errorList = result.errors ?? [];
69
- const errorCount = Array.isArray(errorList) ? errorList.length : 0;
70
- // Print detailed diagnostics with file:line when available (capped for readability)
71
- if (errorCount > 0) {
72
- for (const msg of errorList.slice(0, diagMax)) {
73
- const text = formatEsbuildMessage(msg);
74
- console.error(`[webstir-backend][esbuild] ${text}`);
75
- }
76
- if (errorCount > diagMax) {
77
- console.error(`[webstir-backend][esbuild] ... ${errorCount - diagMax} more error(s) omitted`);
78
- }
79
- }
80
- if (warnCount > 0) {
81
- for (const msg of result.warnings.slice(0, diagMax)) {
82
- const text = formatEsbuildMessage(msg);
83
- console.warn(`[webstir-backend][esbuild] ${text}`);
84
- }
85
- if (warnCount > diagMax) {
86
- console.warn(`[webstir-backend][esbuild] ... ${warnCount - diagMax} more warning(s) omitted`);
87
- }
61
+ if (shouldReportBunBenchmark) {
62
+ console.info('[webstir-backend] watch: reporting primary Bun build timings via bunBenchmark* event fields.');
63
+ }
64
+ let stopping = false;
65
+ let watchTimer;
66
+ let currentSnapshot = await takeWatchSnapshot(workspaceRoot, paths.sourceRoot, tsconfigPath);
67
+ let buildInFlight = false;
68
+ let pendingBuild = false;
69
+ let buildFailure;
70
+ const runBuild = async () => {
71
+ if (stopping) {
72
+ return;
73
+ }
74
+ if (buildInFlight) {
75
+ pendingBuild = true;
76
+ return;
77
+ }
78
+ buildInFlight = true;
79
+ try {
80
+ do {
81
+ pendingBuild = false;
82
+ const nextSnapshot = await takeWatchSnapshot(workspaceRoot, paths.sourceRoot, tsconfigPath);
83
+ currentSnapshot = nextSnapshot;
84
+ const result = await performWatchBuild({
85
+ workspaceRoot,
86
+ sourceRoot: paths.sourceRoot,
87
+ buildRoot: paths.buildRoot,
88
+ tsconfigPath,
89
+ mode,
90
+ env,
91
+ nodeEnv,
92
+ diagMax,
93
+ shouldReportBunBenchmark,
94
+ onEvent: options.onEvent,
95
+ });
96
+ if (!result.succeeded) {
97
+ buildFailure = new Error('Backend watch build failed.');
88
98
  }
89
- console.info(`[webstir-backend] watch:esbuild ${errorCount} error(s), ${warnCount} warning(s) in ${(end - start).toFixed(1)}ms`);
90
- if (errorCount === 0) {
91
- const diagBuffer = [];
92
- const cacheReporter = createCacheReporter({
93
- workspaceRoot,
94
- buildRoot: paths.buildRoot,
95
- env,
96
- diagnostics: diagBuffer
97
- });
98
- try {
99
- const metafile = result.metafile;
100
- if (metafile && metafile.outputs) {
101
- const outputs = collectOutputSizes(metafile, paths.buildRoot);
102
- await cacheReporter.diffOutputs(outputs, mode);
103
- }
104
- const manifest = await loadBackendModuleManifest({
105
- workspaceRoot,
106
- buildRoot: paths.buildRoot,
107
- entryPoints,
108
- diagnostics: diagBuffer
109
- });
110
- await cacheReporter.diffManifest(manifest);
111
- }
112
- catch {
113
- // cache or manifest diff failure should not break watch
114
- }
115
- finally {
116
- for (const diag of diagBuffer) {
117
- const logger = diag.severity === 'error' ? console.error : diag.severity === 'warn' ? console.warn : console.info;
118
- logger(diag.message);
119
- }
120
- }
99
+ else {
100
+ buildFailure = undefined;
121
101
  }
122
- });
123
- },
102
+ } while (pendingBuild && !stopping);
103
+ }
104
+ finally {
105
+ buildInFlight = false;
106
+ }
124
107
  };
125
- const ctx = await createEsbuildContext({
126
- entryPoints,
127
- bundle: false,
128
- platform: 'node',
129
- target: 'node20',
130
- format: 'esm',
131
- sourcemap: true,
132
- outdir: paths.buildRoot,
133
- outbase: paths.sourceRoot,
134
- metafile: true,
135
- tsconfig: existsSync(tsconfigPath) ? tsconfigPath : undefined,
136
- define: { 'process.env.NODE_ENV': JSON.stringify(nodeEnv) },
137
- logLevel: 'silent',
138
- plugins: [timingPlugin],
139
- });
140
- await ctx.watch();
108
+ await runBuild();
141
109
  console.info('[webstir-backend] watch:ready');
110
+ const poll = async () => {
111
+ if (stopping) {
112
+ return;
113
+ }
114
+ try {
115
+ const nextSnapshot = await takeWatchSnapshot(workspaceRoot, paths.sourceRoot, tsconfigPath);
116
+ if (nextSnapshot !== currentSnapshot) {
117
+ currentSnapshot = nextSnapshot;
118
+ await runBuild();
119
+ }
120
+ }
121
+ catch (error) {
122
+ const message = error instanceof Error ? error.message : String(error);
123
+ console.warn(`[webstir-backend] watch:poll failed: ${message}`);
124
+ }
125
+ finally {
126
+ if (!stopping) {
127
+ watchTimer = setTimeout(() => {
128
+ void poll();
129
+ }, WATCH_POLL_INTERVAL_MS);
130
+ }
131
+ }
132
+ };
133
+ watchTimer = setTimeout(() => {
134
+ void poll();
135
+ }, WATCH_POLL_INTERVAL_MS);
142
136
  return {
143
137
  async stop() {
144
- try {
145
- await ctx.dispose();
146
- }
147
- catch {
148
- // ignore
138
+ stopping = true;
139
+ if (watchTimer) {
140
+ clearTimeout(watchTimer);
149
141
  }
150
142
  try {
151
143
  tscProc?.kill('SIGINT');
@@ -154,6 +146,262 @@ export async function startBackendWatch(options) {
154
146
  // ignore
155
147
  }
156
148
  console.info('[webstir-backend] watch:stopped');
149
+ if (buildFailure) {
150
+ buildFailure = undefined;
151
+ }
152
+ },
153
+ };
154
+ }
155
+ async function performWatchBuild(options) {
156
+ const start = performance.now();
157
+ await emitWatchEvent(options.onEvent, {
158
+ type: 'build-start',
159
+ });
160
+ const diagnostics = [];
161
+ const entryPoints = await discoverEntryPoints(options.sourceRoot);
162
+ if (entryPoints.length === 0) {
163
+ diagnostics.push({
164
+ severity: 'error',
165
+ message: `No backend entry points found under ${options.sourceRoot}.`,
166
+ });
167
+ flushDiagnostics(diagnostics);
168
+ const end = performance.now();
169
+ await emitWatchEvent(options.onEvent, {
170
+ type: 'build-complete',
171
+ succeeded: false,
172
+ errorCount: 1,
173
+ warningCount: 0,
174
+ durationMs: end - start,
175
+ });
176
+ return {
177
+ succeeded: false,
178
+ errorCount: 1,
179
+ warningCount: 0,
180
+ };
181
+ }
182
+ const buildResult = await runPrimaryBunWatchBuild({
183
+ entryPoints,
184
+ sourceRoot: options.sourceRoot,
185
+ buildRoot: options.buildRoot,
186
+ tsconfigPath: options.tsconfigPath,
187
+ nodeEnv: options.nodeEnv,
188
+ diagMax: options.diagMax,
189
+ });
190
+ console.info(`[webstir-backend] watch:bun ${buildResult.errorCount} error(s), ${buildResult.warningCount} warning(s) in ${buildResult.durationMs.toFixed(1)}ms`);
191
+ if (buildResult.succeeded) {
192
+ const cacheReporter = createCacheReporter({
193
+ workspaceRoot: options.workspaceRoot,
194
+ buildRoot: options.buildRoot,
195
+ env: options.env,
196
+ diagnostics,
197
+ });
198
+ try {
199
+ await ensureModuleDefinitionBuild({
200
+ sourceRoot: options.sourceRoot,
201
+ buildRoot: options.buildRoot,
202
+ tsconfigPath: options.tsconfigPath,
203
+ mode: options.mode,
204
+ env: options.env,
205
+ diagnostics,
206
+ });
207
+ await cacheReporter.diffOutputs(collectBunOutputSizes(buildResult.outputs, options.buildRoot), options.mode);
208
+ const manifest = await loadBackendModuleManifest({
209
+ workspaceRoot: options.workspaceRoot,
210
+ buildRoot: options.buildRoot,
211
+ entryPoints,
212
+ diagnostics,
213
+ });
214
+ await cacheReporter.diffManifest(manifest);
215
+ }
216
+ catch {
217
+ // cache or manifest diff failure should not break watch
218
+ }
219
+ }
220
+ flushDiagnostics(diagnostics);
221
+ const end = performance.now();
222
+ const bunBenchmark = options.shouldReportBunBenchmark
223
+ ? {
224
+ succeeded: buildResult.succeeded,
225
+ errorCount: buildResult.errorCount,
226
+ warningCount: buildResult.warningCount,
227
+ durationMs: buildResult.durationMs,
228
+ }
229
+ : undefined;
230
+ await emitWatchEvent(options.onEvent, {
231
+ type: 'build-complete',
232
+ succeeded: buildResult.succeeded,
233
+ errorCount: buildResult.errorCount,
234
+ warningCount: buildResult.warningCount,
235
+ durationMs: end - start,
236
+ bunBenchmarkSucceeded: bunBenchmark?.succeeded,
237
+ bunBenchmarkErrorCount: bunBenchmark?.errorCount,
238
+ bunBenchmarkWarningCount: bunBenchmark?.warningCount,
239
+ bunBenchmarkDurationMs: bunBenchmark?.durationMs,
240
+ });
241
+ return {
242
+ succeeded: buildResult.succeeded,
243
+ errorCount: buildResult.errorCount,
244
+ warningCount: buildResult.warningCount,
245
+ };
246
+ }
247
+ async function runPrimaryBunWatchBuild(options) {
248
+ const build = getBunBuild();
249
+ if (!build) {
250
+ throw new Error('Bun.build() is not available in the current runtime.');
251
+ }
252
+ await rm(options.buildRoot, { recursive: true, force: true });
253
+ await mkdir(options.buildRoot, { recursive: true });
254
+ const start = performance.now();
255
+ const result = await build({
256
+ entrypoints: [...options.entryPoints],
257
+ root: options.sourceRoot,
258
+ outdir: options.buildRoot,
259
+ target: 'node',
260
+ format: 'esm',
261
+ splitting: false,
262
+ packages: 'external',
263
+ sourcemap: 'linked',
264
+ tsconfig: existsSync(options.tsconfigPath) ? options.tsconfigPath : undefined,
265
+ define: {
266
+ 'process.env.NODE_ENV': JSON.stringify(options.nodeEnv),
157
267
  },
268
+ // Preserve the old esbuild watch behavior: transpile entries without requiring
269
+ // every relative import target to exist in minimal seeded workspaces.
270
+ plugins: [createRelativeImportPassthroughPlugin()],
271
+ throw: false,
272
+ });
273
+ const end = performance.now();
274
+ const { errorCount, warningCount } = logBunBuildResult(result, options.diagMax);
275
+ return {
276
+ succeeded: result.success && errorCount === 0,
277
+ errorCount,
278
+ warningCount,
279
+ durationMs: end - start,
280
+ outputs: result.outputs,
158
281
  };
159
282
  }
283
+ function logBunBuildResult(result, diagMax) {
284
+ const logs = Array.isArray(result.logs) ? result.logs : [];
285
+ const errorLogs = logs.filter((log) => log.level === 'error');
286
+ const warningLogs = logs.filter((log) => log.level === 'warning');
287
+ for (const log of errorLogs.slice(0, diagMax)) {
288
+ console.error(`[webstir-backend][bun] ${formatEsbuildMessage(log)}`);
289
+ }
290
+ if (errorLogs.length > diagMax) {
291
+ console.error(`[webstir-backend][bun] ... ${errorLogs.length - diagMax} more error(s) omitted`);
292
+ }
293
+ for (const log of warningLogs.slice(0, diagMax)) {
294
+ console.warn(`[webstir-backend][bun] ${formatEsbuildMessage(log)}`);
295
+ }
296
+ if (warningLogs.length > diagMax) {
297
+ console.warn(`[webstir-backend][bun] ... ${warningLogs.length - diagMax} more warning(s) omitted`);
298
+ }
299
+ return {
300
+ errorCount: errorLogs.length,
301
+ warningCount: warningLogs.length,
302
+ };
303
+ }
304
+ function collectBunOutputSizes(outputs, buildRoot) {
305
+ const collected = {};
306
+ for (const output of outputs ?? []) {
307
+ const rel = path.relative(buildRoot, output.path);
308
+ collected[rel] = typeof output.size === 'number' ? output.size : 0;
309
+ }
310
+ return collected;
311
+ }
312
+ async function takeWatchSnapshot(workspaceRoot, sourceRoot, tsconfigPath) {
313
+ const watchFiles = new Set();
314
+ const packageJsonPath = path.join(workspaceRoot, 'package.json');
315
+ if (existsSync(packageJsonPath)) {
316
+ watchFiles.add(packageJsonPath);
317
+ }
318
+ if (existsSync(tsconfigPath)) {
319
+ watchFiles.add(tsconfigPath);
320
+ }
321
+ const typesRoot = path.join(workspaceRoot, 'types');
322
+ const directoryRoots = [sourceRoot];
323
+ if (existsSync(typesRoot)) {
324
+ directoryRoots.push(typesRoot);
325
+ }
326
+ for (const directoryRoot of directoryRoots) {
327
+ for (const filePath of await listWatchFiles(directoryRoot)) {
328
+ watchFiles.add(filePath);
329
+ }
330
+ }
331
+ const entries = await Promise.all(Array.from(watchFiles)
332
+ .sort()
333
+ .map(async (filePath) => {
334
+ const fileStat = await stat(filePath);
335
+ return `${filePath}:${fileStat.size}:${fileStat.mtimeMs}`;
336
+ }));
337
+ return entries.join('|');
338
+ }
339
+ async function listWatchFiles(root) {
340
+ if (!existsSync(root)) {
341
+ return [];
342
+ }
343
+ const entries = await glob('**/*', {
344
+ cwd: root,
345
+ absolute: true,
346
+ dot: false,
347
+ nodir: false,
348
+ });
349
+ const files = [];
350
+ for (const entry of entries) {
351
+ try {
352
+ const entryStat = await stat(entry);
353
+ if (entryStat.isFile()) {
354
+ files.push(entry);
355
+ }
356
+ }
357
+ catch {
358
+ // Ignore files deleted between glob and stat.
359
+ }
360
+ }
361
+ return files;
362
+ }
363
+ function flushDiagnostics(diagnostics) {
364
+ for (const diag of diagnostics) {
365
+ const logger = diag.severity === 'error'
366
+ ? console.error
367
+ : diag.severity === 'warn'
368
+ ? console.warn
369
+ : console.info;
370
+ logger(diag.message);
371
+ }
372
+ }
373
+ async function emitWatchEvent(onEvent, event) {
374
+ if (!onEvent) {
375
+ return;
376
+ }
377
+ try {
378
+ await onEvent(event);
379
+ }
380
+ catch (error) {
381
+ const message = error instanceof Error ? error.message : String(error);
382
+ console.warn(`[webstir-backend] watch:event failed: ${message}`);
383
+ }
384
+ }
385
+ function getBunBuild() {
386
+ const runtime = globalThis;
387
+ const build = runtime.Bun?.build;
388
+ return typeof build === 'function' ? build.bind(runtime.Bun) : undefined;
389
+ }
390
+ function createRelativeImportPassthroughPlugin() {
391
+ return {
392
+ name: 'webstir-backend-watch-relative-imports',
393
+ setup(build) {
394
+ build.onResolve({ filter: /^\.\.?\// }, (args) => ({
395
+ path: args.path,
396
+ external: true,
397
+ }));
398
+ },
399
+ };
400
+ }
401
+ function isEnabled(value) {
402
+ if (typeof value !== 'string') {
403
+ return false;
404
+ }
405
+ const normalized = value.trim().toLowerCase();
406
+ return normalized === '1' || normalized === 'true' || normalized === 'on' || normalized === 'yes';
407
+ }
@@ -1,4 +1,12 @@
1
1
  import type { ResolvedModuleWorkspace } from '@webstir-io/module-contract';
2
2
  export type BackendBuildMode = 'build' | 'publish' | 'test';
3
+ interface ResolveWorkspaceRootOptions {
4
+ readonly workspaceRoot?: string;
5
+ readonly env?: Record<string, string | undefined>;
6
+ readonly cwd?: string;
7
+ readonly importMetaUrl?: string;
8
+ }
9
+ export declare function resolveWorkspaceRoot(options?: string | ResolveWorkspaceRootOptions): string;
3
10
  export declare function resolveWorkspacePaths(workspaceRoot: string): ResolvedModuleWorkspace;
4
11
  export declare function normalizeMode(rawMode: unknown): BackendBuildMode;
12
+ export {};
package/dist/workspace.js CHANGED
@@ -1,9 +1,33 @@
1
1
  import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ const WORKSPACE_ROOT_PATTERN = /^(.*)[/\\](?:src|build)[/\\]backend(?:[/\\].*)?$/;
4
+ export function resolveWorkspaceRoot(options) {
5
+ if (typeof options === 'string') {
6
+ return path.resolve(options);
7
+ }
8
+ const explicitRoot = options?.workspaceRoot?.trim();
9
+ if (explicitRoot) {
10
+ return path.resolve(explicitRoot);
11
+ }
12
+ const env = options?.env ?? process.env;
13
+ const envRoot = env.WORKSPACE_ROOT?.trim() || env.WEBSTIR_WORKSPACE_ROOT?.trim();
14
+ if (envRoot) {
15
+ return path.resolve(envRoot);
16
+ }
17
+ const inferredRoot = options?.importMetaUrl
18
+ ? inferWorkspaceRootFromImportMetaUrl(options.importMetaUrl)
19
+ : undefined;
20
+ if (inferredRoot) {
21
+ return inferredRoot;
22
+ }
23
+ return path.resolve(options?.cwd ?? process.cwd());
24
+ }
2
25
  export function resolveWorkspacePaths(workspaceRoot) {
26
+ const resolvedWorkspaceRoot = resolveWorkspaceRoot(workspaceRoot);
3
27
  return {
4
- sourceRoot: path.join(workspaceRoot, 'src', 'backend'),
5
- buildRoot: path.join(workspaceRoot, 'build', 'backend'),
6
- testsRoot: path.join(workspaceRoot, 'src', 'backend', 'tests')
28
+ sourceRoot: path.join(resolvedWorkspaceRoot, 'src', 'backend'),
29
+ buildRoot: path.join(resolvedWorkspaceRoot, 'build', 'backend'),
30
+ testsRoot: path.join(resolvedWorkspaceRoot, 'src', 'backend', 'tests'),
7
31
  };
8
32
  }
9
33
  export function normalizeMode(rawMode) {
@@ -13,3 +37,20 @@ export function normalizeMode(rawMode) {
13
37
  const normalized = rawMode.toLowerCase();
14
38
  return normalized === 'publish' || normalized === 'test' ? normalized : 'build';
15
39
  }
40
+ function inferWorkspaceRootFromImportMetaUrl(importMetaUrl) {
41
+ try {
42
+ return inferWorkspaceRootFromFilePath(fileURLToPath(importMetaUrl));
43
+ }
44
+ catch {
45
+ return undefined;
46
+ }
47
+ }
48
+ function inferWorkspaceRootFromFilePath(filePath) {
49
+ const normalizedFilePath = path.resolve(filePath);
50
+ const match = normalizedFilePath.match(WORKSPACE_ROOT_PATTERN);
51
+ if (!match) {
52
+ return undefined;
53
+ }
54
+ const inferredRoot = match[1];
55
+ return inferredRoot || path.parse(normalizedFilePath).root;
56
+ }
package/package.json CHANGED
@@ -1,16 +1,49 @@
1
1
  {
2
2
  "name": "@webstir-io/webstir-backend",
3
- "version": "0.1.15",
4
- "description": "Reserved manifest for the future Webstir backend tooling package.",
3
+ "version": "0.1.16",
4
+ "description": "Backend build and runtime tooling for Webstir workspaces.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "webstir-backend-deploy": "dist/deploy-cli.js"
10
+ },
8
11
  "exports": {
9
12
  ".": {
10
13
  "types": "./dist/index.d.ts",
11
14
  "import": "./dist/index.js",
12
15
  "default": "./dist/index.js"
13
16
  },
17
+ "./runtime/forms": {
18
+ "types": "./dist/runtime/forms.d.ts",
19
+ "import": "./dist/runtime/forms.js",
20
+ "default": "./dist/runtime/forms.js"
21
+ },
22
+ "./runtime/request-hooks": {
23
+ "types": "./dist/runtime/request-hooks.d.ts",
24
+ "import": "./dist/runtime/request-hooks.js",
25
+ "default": "./dist/runtime/request-hooks.js"
26
+ },
27
+ "./runtime/session": {
28
+ "types": "./dist/runtime/session.d.ts",
29
+ "import": "./dist/runtime/session.js",
30
+ "default": "./dist/runtime/session.js"
31
+ },
32
+ "./runtime/views": {
33
+ "types": "./dist/runtime/views.d.ts",
34
+ "import": "./dist/runtime/views.js",
35
+ "default": "./dist/runtime/views.js"
36
+ },
37
+ "./runtime/bun": {
38
+ "types": "./dist/runtime/bun.d.ts",
39
+ "import": "./dist/runtime/bun.js",
40
+ "default": "./dist/runtime/bun.js"
41
+ },
42
+ "./runtime/deploy": {
43
+ "types": "./dist/runtime/deploy.d.ts",
44
+ "import": "./dist/runtime/deploy.js",
45
+ "default": "./dist/runtime/deploy.js"
46
+ },
14
47
  "./provider": {
15
48
  "types": "./dist/provider.d.ts",
16
49
  "import": "./dist/provider.js",
@@ -30,13 +63,12 @@
30
63
  },
31
64
  "scripts": {
32
65
  "build": "tsc -p tsconfig.json",
33
- "test": "node -e \"const { spawnSync } = require('node:child_process'); const glob = require('glob'); const files = glob.sync('tests/**/*.test.js'); if (!files.length) { console.error('No backend test files found'); process.exit(1); } const result = spawnSync(process.execPath, ['--test', ...files], { stdio: 'inherit' }); process.exit(result.status ?? 1);\"",
66
+ "test": "tsc -p tsconfig.json && bun test tests",
34
67
  "clean": "rm -rf dist",
35
- "prepare": "npm run build",
36
- "watch": "npm run build && node -e \"import('./dist/watch.js').then(m=>m.startBackendWatch({workspaceRoot:process.cwd(),env:{WEBSTIR_MODULE_MODE:'build'}}));\"",
37
- "dev": "npm run watch",
38
- "dev:fast": "WEBSTIR_BACKEND_TYPECHECK=skip npm run watch",
39
- "smoke": "npm run build && node scripts/smoke.mjs",
68
+ "watch": "tsc -p tsconfig.json && bun -e \"import('./dist/watch.js').then(m=>m.startBackendWatch({env:{WEBSTIR_MODULE_MODE:'build'}}));\"",
69
+ "dev": "bun run watch",
70
+ "dev:fast": "WEBSTIR_BACKEND_TYPECHECK=skip bun run watch",
71
+ "smoke": "tsc -p tsconfig.json && bun scripts/smoke.mjs",
40
72
  "release": "bash scripts/publish.sh"
41
73
  },
42
74
  "files": [
@@ -45,25 +77,28 @@
45
77
  "src",
46
78
  "scripts",
47
79
  "tests",
48
- "tsconfig.json",
49
- "package-lock.json"
80
+ "tsconfig.json"
50
81
  ],
51
82
  "engines": {
52
- "node": ">=20.18.1"
83
+ "bun": ">=1.3.11"
53
84
  },
54
85
  "license": "MIT",
55
86
  "repository": {
56
87
  "type": "git",
57
- "url": "git+https://github.com/webstir-io/webstir-backend.git"
88
+ "url": "git+https://github.com/webstir-io/webstir.git",
89
+ "directory": "packages/tooling/webstir-backend"
90
+ },
91
+ "homepage": "https://github.com/webstir-io/webstir/tree/main/packages/tooling/webstir-backend#readme",
92
+ "bugs": {
93
+ "url": "https://github.com/webstir-io/webstir/issues"
58
94
  },
59
95
  "dependencies": {
60
- "@webstir-io/module-contract": "^0.1.13",
96
+ "@webstir-io/module-contract": "^0.1.16",
61
97
  "esbuild": "^0.25.10",
62
98
  "glob": "^10.4.1"
63
99
  },
64
100
  "devDependencies": {
65
101
  "@types/node": "^20.19.21",
66
- "fastify": "^5.6.2",
67
102
  "pino": "^10.1.0",
68
103
  "typescript": "^5.7.2"
69
104
  },