@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
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env bun
2
+ import path from 'node:path';
3
+ import { startPublishedWorkspaceServer } from './runtime/deploy.js';
4
+ async function main(argv) {
5
+ const args = parseArgs(argv);
6
+ if (args.help) {
7
+ printHelp();
8
+ return;
9
+ }
10
+ const workspaceRoot = path.resolve(args.workspace ?? process.cwd());
11
+ const server = await startPublishedWorkspaceServer({
12
+ workspaceRoot,
13
+ host: args.host,
14
+ port: args.port,
15
+ });
16
+ const shutdown = async (signal) => {
17
+ process.stderr.write(`[webstir-backend-deploy] received ${signal}, stopping.\n`);
18
+ await server.stop();
19
+ process.exit(0);
20
+ };
21
+ process.on('SIGINT', () => void shutdown('SIGINT'));
22
+ process.on('SIGTERM', () => void shutdown('SIGTERM'));
23
+ process.stdout.write(`[webstir-backend-deploy] serving ${server.mode} workspace at ${server.origin}\n`);
24
+ await new Promise(() => {
25
+ // Keep the process alive until it receives a signal.
26
+ });
27
+ }
28
+ function parseArgs(argv) {
29
+ let workspace;
30
+ let host;
31
+ let port;
32
+ let help = false;
33
+ for (let index = 0; index < argv.length; index += 1) {
34
+ const arg = argv[index];
35
+ if (!arg) {
36
+ continue;
37
+ }
38
+ if (arg === '--help' || arg === '-h') {
39
+ help = true;
40
+ continue;
41
+ }
42
+ if (arg === '--workspace') {
43
+ const next = argv[index + 1];
44
+ if (!next) {
45
+ throw new Error('Missing value for --workspace.');
46
+ }
47
+ workspace = next;
48
+ index += 1;
49
+ continue;
50
+ }
51
+ if (arg === '--host') {
52
+ const next = argv[index + 1];
53
+ if (!next) {
54
+ throw new Error('Missing value for --host.');
55
+ }
56
+ host = next;
57
+ index += 1;
58
+ continue;
59
+ }
60
+ if (arg === '--port') {
61
+ const next = argv[index + 1];
62
+ if (!next) {
63
+ throw new Error('Missing value for --port.');
64
+ }
65
+ const parsed = Number(next);
66
+ if (!Number.isInteger(parsed) || parsed <= 0) {
67
+ throw new Error(`Invalid value for --port: ${next}`);
68
+ }
69
+ port = parsed;
70
+ index += 1;
71
+ continue;
72
+ }
73
+ throw new Error(`Unknown argument: ${arg}`);
74
+ }
75
+ return { workspace, host, port, help };
76
+ }
77
+ function printHelp() {
78
+ process.stdout.write(`Usage: webstir-backend-deploy [--workspace <path>] [--host <host>] [--port <port>]
79
+
80
+ Starts a published api or full Webstir workspace with a single Bun entrypoint.
81
+ `);
82
+ }
83
+ main(process.argv.slice(2)).catch((error) => {
84
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
85
+ process.exit(1);
86
+ });
@@ -1,11 +1,11 @@
1
1
  export function pushEntryBucketSummary(diagnostics, entryPoints) {
2
2
  try {
3
- const server = entryPoints.filter((p) => p === 'index.js' || /(^|\/)index\.js$/.test(p) && !/^(functions|jobs)\//.test(p)).length;
3
+ const server = entryPoints.filter((p) => p === 'index.js' || (/(^|\/)index\.js$/.test(p) && !/^(functions|jobs)\//.test(p))).length;
4
4
  const functionsCount = entryPoints.filter((p) => p.startsWith('functions/')).length;
5
5
  const jobsCount = entryPoints.filter((p) => p.startsWith('jobs/')).length;
6
6
  diagnostics.push({
7
7
  severity: 'info',
8
- message: `[webstir-backend] entries by bucket: server=${server} functions=${functionsCount} jobs=${jobsCount}`
8
+ message: `[webstir-backend] entries by bucket: server=${server} functions=${functionsCount} jobs=${jobsCount}`,
9
9
  });
10
10
  }
11
11
  catch {
package/dist/index.d.ts CHANGED
@@ -1,2 +1,8 @@
1
+ export { runAddJob, runAddRoute, runUpdateRouteContract } from './add.js';
2
+ export type { AddJobOptions, AddRouteOptions, UpdateRouteContractOptions } from './add.js';
1
3
  export { backendProvider } from './provider.js';
2
4
  export { startBackendWatch } from './watch.js';
5
+ export { getBackendScaffoldAssets } from './scaffold/assets.js';
6
+ export { createDefaultBunBackendBootstrap, startBunBackend } from './runtime/bun.js';
7
+ export type { BunRuntimeEnvLike, DefaultBunBackendBootstrapOptions, MetricsTracker, RuntimeLogger, } from './runtime/bun.js';
8
+ export { startPublishedWorkspaceServer } from './runtime/deploy.js';
package/dist/index.js CHANGED
@@ -1,2 +1,6 @@
1
+ export { runAddJob, runAddRoute, runUpdateRouteContract } from './add.js';
1
2
  export { backendProvider } from './provider.js';
2
3
  export { startBackendWatch } from './watch.js';
4
+ export { getBackendScaffoldAssets } from './scaffold/assets.js';
5
+ export { createDefaultBunBackendBootstrap, startBunBackend } from './runtime/bun.js';
6
+ export { startPublishedWorkspaceServer } from './runtime/deploy.js';
@@ -1,14 +1,14 @@
1
1
  import path from 'node:path';
2
2
  import { existsSync } from 'node:fs';
3
- import { readFile } from 'node:fs/promises';
4
3
  import { pathToFileURL } from 'node:url';
5
4
  import { moduleManifestSchema, CONTRACT_VERSION } from '@webstir-io/module-contract';
5
+ import { readTextFile } from '../utils/bun.js';
6
6
  export async function loadBackendModuleManifest(options) {
7
7
  const { workspaceRoot, buildRoot, entryPoints, diagnostics } = options;
8
8
  const pkgPath = path.join(workspaceRoot, 'package.json');
9
9
  let workspacePackage;
10
10
  try {
11
- const raw = await readFile(pkgPath, 'utf8');
11
+ const raw = await readTextFile(pkgPath);
12
12
  workspacePackage = JSON.parse(raw);
13
13
  }
14
14
  catch (error) {
@@ -17,15 +17,21 @@ export async function loadBackendModuleManifest(options) {
17
17
  if (err.code !== 'ENOENT') {
18
18
  diagnostics.push({
19
19
  severity: 'warn',
20
- message: `[webstir-backend] unable to read ${pkgPath}: ${err.message}. Using defaults.`
20
+ message: `[webstir-backend] unable to read ${pkgPath}: ${err.message}. Using defaults.`,
21
21
  });
22
22
  }
23
23
  }
24
24
  const moduleConfig = workspacePackage?.webstir?.moduleManifest ?? {};
25
25
  let manifestCandidate = {
26
- contractVersion: typeof moduleConfig.contractVersion === 'string' ? moduleConfig.contractVersion : CONTRACT_VERSION,
27
- name: typeof moduleConfig.name === 'string' ? moduleConfig.name : deriveModuleName(workspacePackage, workspaceRoot),
28
- version: typeof moduleConfig.version === 'string' ? moduleConfig.version : deriveModuleVersion(workspacePackage),
26
+ contractVersion: typeof moduleConfig.contractVersion === 'string'
27
+ ? moduleConfig.contractVersion
28
+ : CONTRACT_VERSION,
29
+ name: typeof moduleConfig.name === 'string'
30
+ ? moduleConfig.name
31
+ : deriveModuleName(workspacePackage, workspaceRoot),
32
+ version: typeof moduleConfig.version === 'string'
33
+ ? moduleConfig.version
34
+ : deriveModuleVersion(workspacePackage),
29
35
  kind: 'backend',
30
36
  capabilities: Array.isArray(moduleConfig.capabilities) ? moduleConfig.capabilities : [],
31
37
  routes: moduleConfig.routes ?? [],
@@ -34,25 +40,28 @@ export async function loadBackendModuleManifest(options) {
34
40
  events: moduleConfig.events ?? [],
35
41
  services: moduleConfig.services ?? [],
36
42
  init: moduleConfig.init,
37
- dispose: moduleConfig.dispose
43
+ dispose: moduleConfig.dispose,
38
44
  };
39
45
  const definition = await loadModuleDefinition(buildRoot, diagnostics);
40
46
  if (definition) {
41
47
  const definitionManifest = definition.manifest ?? {};
42
48
  const routesFromDefinition = definition.routes?.map((route) => route.definition);
43
49
  const viewsFromDefinition = definition.views?.map((view) => view.definition);
44
- const mergedCapabilities = Array.from(new Set([...(manifestCandidate.capabilities ?? []), ...(definitionManifest.capabilities ?? [])]));
50
+ const mergedCapabilities = Array.from(new Set([
51
+ ...(manifestCandidate.capabilities ?? []),
52
+ ...(definitionManifest.capabilities ?? []),
53
+ ]));
45
54
  manifestCandidate = {
46
55
  ...manifestCandidate,
47
56
  ...definitionManifest,
48
57
  capabilities: mergedCapabilities,
49
- routes: routesFromDefinition ?? definitionManifest.routes ?? manifestCandidate.routes ?? [],
58
+ routes: mergeRouteDefinitions(routesFromDefinition ?? definitionManifest.routes, manifestCandidate.routes),
50
59
  views: viewsFromDefinition ?? definitionManifest.views ?? manifestCandidate.views ?? [],
51
- jobs: definitionManifest.jobs ?? manifestCandidate.jobs ?? [],
60
+ jobs: mergeJobDefinitions(definitionManifest.jobs, manifestCandidate.jobs),
52
61
  events: definitionManifest.events ?? manifestCandidate.events ?? [],
53
62
  services: definitionManifest.services ?? manifestCandidate.services ?? [],
54
63
  init: definitionManifest.init ?? manifestCandidate.init,
55
- dispose: definitionManifest.dispose ?? manifestCandidate.dispose
64
+ dispose: definitionManifest.dispose ?? manifestCandidate.dispose,
56
65
  };
57
66
  }
58
67
  const validation = moduleManifestSchema.safeParse(manifestCandidate);
@@ -62,7 +71,7 @@ export async function loadBackendModuleManifest(options) {
62
71
  .join('; ');
63
72
  diagnostics.push({
64
73
  severity: 'error',
65
- message: `[webstir-backend] module manifest validation failed (${problems}). Falling back to defaults.`
74
+ message: `[webstir-backend] module manifest validation failed (${problems}). Falling back to defaults.`,
66
75
  });
67
76
  return {
68
77
  contractVersion: CONTRACT_VERSION,
@@ -74,7 +83,7 @@ export async function loadBackendModuleManifest(options) {
74
83
  views: [],
75
84
  jobs: [],
76
85
  events: [],
77
- services: []
86
+ services: [],
78
87
  };
79
88
  }
80
89
  const manifest = validation.data;
@@ -82,23 +91,26 @@ export async function loadBackendModuleManifest(options) {
82
91
  const normalizePath = (p) => {
83
92
  let s = typeof p === 'string' ? p : '';
84
93
  if (!s.startsWith('/'))
85
- s = '/' + s;
94
+ s = `/${s}`;
86
95
  s = s.replace(/\/+/, '/');
87
96
  if (s.length > 1 && s.endsWith('/'))
88
97
  s = s.slice(0, -1);
89
98
  return s;
90
99
  };
91
100
  const seen = new Map();
92
- for (const r of manifest.routes ?? []) {
93
- const method = typeof r.method === 'string' ? r.method.toUpperCase() : '';
94
- const pathKey = normalizePath(r.path);
101
+ for (const route of (manifest.routes ?? [])) {
102
+ const method = typeof route.method === 'string' ? route.method.toUpperCase() : '';
103
+ const pathKey = normalizePath(route.path);
95
104
  const key = `${method} ${pathKey}`;
96
105
  seen.set(key, (seen.get(key) ?? 0) + 1);
97
106
  }
98
107
  const dups = Array.from(seen.entries()).filter(([, count]) => count > 1);
99
108
  if (dups.length > 0) {
100
109
  const list = dups.map(([k, c]) => `${k} (${c}x)`).join(', ');
101
- diagnostics.push({ severity: 'warn', message: `[webstir-backend] duplicate route definitions: ${list}` });
110
+ diagnostics.push({
111
+ severity: 'warn',
112
+ message: `[webstir-backend] duplicate route definitions: ${list}`,
113
+ });
102
114
  }
103
115
  }
104
116
  catch {
@@ -107,7 +119,7 @@ export async function loadBackendModuleManifest(options) {
107
119
  if (manifest.routes?.length && entryPoints.length === 0) {
108
120
  diagnostics.push({
109
121
  severity: 'warn',
110
- message: '[webstir-backend] module manifest defines routes but no entry points were built. Ensure backend compilation produced handlers.'
122
+ message: '[webstir-backend] module manifest defines routes but no entry points were built. Ensure backend compilation produced handlers.',
111
123
  });
112
124
  }
113
125
  try {
@@ -117,17 +129,20 @@ export async function loadBackendModuleManifest(options) {
117
129
  if (jobs.length + events.length + services.length > 0) {
118
130
  diagnostics.push({
119
131
  severity: 'info',
120
- message: `[webstir-backend] manifest jobs=${jobs.length} events=${events.length} services=${services.length}`
132
+ message: `[webstir-backend] manifest jobs=${jobs.length} events=${events.length} services=${services.length}`,
121
133
  });
122
134
  }
123
- const noSchedule = jobs.filter((j) => j && typeof j.name === 'string' && (j.schedule === undefined || j.schedule === null));
135
+ const noSchedule = jobs.filter((job) => typeof job.name === 'string' && (job.schedule === undefined || job.schedule === null));
124
136
  if (noSchedule.length > 0) {
125
137
  const MAX_LIST = 10;
126
- const names = noSchedule.map((j) => j.name).slice(0, MAX_LIST).join(', ');
138
+ const names = noSchedule
139
+ .map((job) => job.name)
140
+ .slice(0, MAX_LIST)
141
+ .join(', ');
127
142
  const omitted = noSchedule.length > MAX_LIST ? ` (+${noSchedule.length - MAX_LIST} more)` : '';
128
143
  diagnostics.push({
129
144
  severity: 'warn',
130
- message: `[webstir-backend] jobs without schedules: ${names}${omitted}`
145
+ message: `[webstir-backend] jobs without schedules: ${names}${omitted}`,
131
146
  });
132
147
  }
133
148
  }
@@ -137,8 +152,13 @@ export async function loadBackendModuleManifest(options) {
137
152
  try {
138
153
  const routes = Array.isArray(manifest.routes) ? manifest.routes.length : 0;
139
154
  const views = Array.isArray(manifest.views) ? manifest.views.length : 0;
140
- const caps = Array.isArray(manifest.capabilities) && manifest.capabilities.length > 0 ? ` [${manifest.capabilities.join(', ')}]` : '';
141
- diagnostics.push({ severity: 'info', message: `[webstir-backend] manifest routes=${routes} views=${views}${caps}` });
155
+ const caps = Array.isArray(manifest.capabilities) && manifest.capabilities.length > 0
156
+ ? ` [${manifest.capabilities.join(', ')}]`
157
+ : '';
158
+ diagnostics.push({
159
+ severity: 'info',
160
+ message: `[webstir-backend] manifest routes=${routes} views=${views}${caps}`,
161
+ });
142
162
  }
143
163
  catch {
144
164
  // ignore
@@ -150,7 +170,7 @@ async function loadModuleDefinition(buildRoot, diagnostics) {
150
170
  path.join(buildRoot, 'module.js'),
151
171
  path.join(buildRoot, 'module.mjs'),
152
172
  path.join(buildRoot, 'module/index.js'),
153
- path.join(buildRoot, 'module/index.mjs')
173
+ path.join(buildRoot, 'module/index.mjs'),
154
174
  ];
155
175
  for (const fullPath of candidates) {
156
176
  if (!existsSync(fullPath)) {
@@ -165,13 +185,13 @@ async function loadModuleDefinition(buildRoot, diagnostics) {
165
185
  }
166
186
  diagnostics.push({
167
187
  severity: 'warn',
168
- message: `[webstir-backend] module definition at ${fullPath} does not export a createModule() definition.`
188
+ message: `[webstir-backend] module definition at ${fullPath} does not export a createModule() definition.`,
169
189
  });
170
190
  }
171
191
  catch (error) {
172
192
  diagnostics.push({
173
193
  severity: 'warn',
174
- message: `[webstir-backend] failed to load module definition from ${fullPath}: ${error.message}`
194
+ message: `[webstir-backend] failed to load module definition from ${fullPath}: ${error.message}`,
175
195
  });
176
196
  }
177
197
  }
@@ -190,10 +210,11 @@ function extractModuleDefinition(exports) {
190
210
  return undefined;
191
211
  }
192
212
  function isModuleDefinition(value) {
193
- return typeof value === 'object' && value !== null && 'manifest' in value;
213
+ return (typeof value === 'object' && value !== null && 'manifest' in value);
194
214
  }
195
215
  function deriveModuleName(pkg, workspaceRoot) {
196
- if (typeof pkg?.webstir?.moduleManifest?.name === 'string' && pkg.webstir.moduleManifest.name.length > 0) {
216
+ if (typeof pkg?.webstir?.moduleManifest?.name === 'string' &&
217
+ pkg.webstir.moduleManifest.name.length > 0) {
197
218
  return pkg.webstir.moduleManifest.name;
198
219
  }
199
220
  if (typeof pkg?.name === 'string' && pkg.name.length > 0) {
@@ -202,7 +223,8 @@ function deriveModuleName(pkg, workspaceRoot) {
202
223
  return `backend-module-${path.basename(workspaceRoot)}`;
203
224
  }
204
225
  function deriveModuleVersion(pkg) {
205
- if (typeof pkg?.webstir?.moduleManifest?.version === 'string' && pkg.webstir.moduleManifest.version.length > 0) {
226
+ if (typeof pkg?.webstir?.moduleManifest?.version === 'string' &&
227
+ pkg.webstir.moduleManifest.version.length > 0) {
206
228
  return pkg.webstir.moduleManifest.version;
207
229
  }
208
230
  if (typeof pkg?.version === 'string' && pkg.version.length > 0) {
@@ -210,6 +232,55 @@ function deriveModuleVersion(pkg) {
210
232
  }
211
233
  return '0.0.0';
212
234
  }
235
+ function mergeRouteDefinitions(definitionRoutes, packageRoutes) {
236
+ const merged = Array.isArray(definitionRoutes) ? [...definitionRoutes] : [];
237
+ const seen = new Set(merged.map((route) => getRouteKey(route)).filter(Boolean));
238
+ for (const route of packageRoutes ?? []) {
239
+ const key = getRouteKey(route);
240
+ if (!key || seen.has(key)) {
241
+ continue;
242
+ }
243
+ merged.push(route);
244
+ seen.add(key);
245
+ }
246
+ return merged;
247
+ }
248
+ function mergeJobDefinitions(definitionJobs, packageJobs) {
249
+ const merged = Array.isArray(definitionJobs) ? [...definitionJobs] : [];
250
+ const seen = new Set(merged.map((job) => getJobKey(job)).filter(Boolean));
251
+ for (const job of packageJobs ?? []) {
252
+ const key = getJobKey(job);
253
+ if (!key || seen.has(key)) {
254
+ continue;
255
+ }
256
+ merged.push(job);
257
+ seen.add(key);
258
+ }
259
+ return merged;
260
+ }
261
+ function getJobKey(job) {
262
+ return typeof job?.name === 'string' && job.name.length > 0 ? job.name : undefined;
263
+ }
264
+ function getRouteKey(route) {
265
+ const method = typeof route?.method === 'string' ? route.method.toUpperCase() : '';
266
+ const routePath = normalizeRoutePath(route?.path);
267
+ if (!method || !routePath) {
268
+ return undefined;
269
+ }
270
+ return `${method} ${routePath}`;
271
+ }
272
+ function normalizeRoutePath(routePath) {
273
+ if (typeof routePath !== 'string' || routePath.length === 0) {
274
+ return undefined;
275
+ }
276
+ let normalized = routePath;
277
+ if (!normalized.startsWith('/'))
278
+ normalized = `/${normalized}`;
279
+ normalized = normalized.replace(/\/+/g, '/');
280
+ if (normalized.length > 1 && normalized.endsWith('/'))
281
+ normalized = normalized.slice(0, -1);
282
+ return normalized;
283
+ }
213
284
  export async function summarizeBuiltManifest(buildRoot) {
214
285
  const definition = await loadModuleDefinition(buildRoot, []);
215
286
  if (!definition || !definition.manifest) {
@@ -219,6 +290,6 @@ export async function summarizeBuiltManifest(buildRoot) {
219
290
  return {
220
291
  routes: Array.isArray(manifest.routes) ? manifest.routes.length : 0,
221
292
  views: Array.isArray(manifest.views) ? manifest.views.length : 0,
222
- capabilities: manifest.capabilities
293
+ capabilities: manifest.capabilities,
223
294
  };
224
295
  }
package/dist/provider.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import path from 'node:path';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { collectArtifacts, createBuildManifest } from './build/artifacts.js';
4
- import { buildSupportFile, runBackendBuildPipeline } from './build/pipeline.js';
4
+ import { buildSupportFile, resolveBackendBundler, runBackendBuildPipeline, } from './build/pipeline.js';
5
5
  import { loadBackendModuleManifest } from './manifest/pipeline.js';
6
6
  import { createCacheReporter } from './cache/reporters.js';
7
7
  import { normalizeLogLevel, filterDiagnostics } from './diagnostics/summary.js';
8
8
  import { getBackendScaffoldAssets } from './scaffold/assets.js';
9
- import { normalizeMode, resolveWorkspacePaths } from './workspace.js';
9
+ import { normalizeMode, resolveWorkspacePaths, resolveWorkspaceRoot } from './workspace.js';
10
10
  import packageJson from '../package.json' with { type: 'json' };
11
11
  const pkg = packageJson;
12
12
  export const backendProvider = {
@@ -16,19 +16,32 @@ export const backendProvider = {
16
16
  version: pkg.version ?? '0.0.0',
17
17
  compatibility: {
18
18
  minCliVersion: '0.1.0',
19
- nodeRange: pkg.engines?.node ?? '>=20.18.1'
20
- }
19
+ nodeRange: pkg.engines?.node ?? '>=20.18.1',
20
+ ...(pkg.engines?.bun ? { notes: `Requires Bun ${pkg.engines.bun} at runtime.` } : {}),
21
+ },
21
22
  },
22
23
  resolveWorkspace(options) {
23
- return resolveWorkspacePaths(options.workspaceRoot);
24
+ const workspaceRoot = resolveWorkspaceRoot({
25
+ workspaceRoot: options.workspaceRoot,
26
+ });
27
+ return resolveWorkspacePaths(workspaceRoot);
24
28
  },
25
29
  async build(options) {
26
- const paths = resolveWorkspacePaths(options.workspaceRoot);
30
+ const env = options.env ?? {};
31
+ const workspaceRoot = resolveWorkspaceRoot({
32
+ workspaceRoot: options.workspaceRoot,
33
+ env,
34
+ });
35
+ const paths = resolveWorkspacePaths(workspaceRoot);
27
36
  const tsconfigPath = path.join(paths.sourceRoot, 'tsconfig.json');
28
37
  const diagnostics = [];
29
38
  const incremental = options.incremental === true;
30
- const env = options.env ?? {};
31
39
  const mode = normalizeMode(env.WEBSTIR_MODULE_MODE);
40
+ const bundler = resolveBackendBundler({
41
+ env,
42
+ incremental,
43
+ diagnostics,
44
+ });
32
45
  console.info(`[webstir-backend] ${mode}:start`);
33
46
  const { entryPoints, outputs, includePublishSourcemaps } = await runBackendBuildPipeline({
34
47
  sourceRoot: paths.sourceRoot,
@@ -37,7 +50,8 @@ export const backendProvider = {
37
50
  mode,
38
51
  env,
39
52
  incremental,
40
- diagnostics
53
+ diagnostics,
54
+ bundler,
41
55
  });
42
56
  const artifacts = await collectArtifacts(paths.buildRoot, includePublishSourcemaps);
43
57
  const envSource = path.join(paths.sourceRoot, 'env.ts');
@@ -50,7 +64,8 @@ export const backendProvider = {
50
64
  tsconfigPath,
51
65
  mode,
52
66
  env,
53
- diagnostics
67
+ diagnostics,
68
+ bundler,
54
69
  });
55
70
  }
56
71
  catch {
@@ -58,19 +73,22 @@ export const backendProvider = {
58
73
  }
59
74
  }
60
75
  const moduleManifest = await loadBackendModuleManifest({
61
- workspaceRoot: options.workspaceRoot,
76
+ workspaceRoot,
62
77
  buildRoot: paths.buildRoot,
63
78
  entryPoints,
64
- diagnostics
79
+ diagnostics,
65
80
  });
66
81
  const manifest = createBuildManifest(paths.buildRoot, artifacts, diagnostics, moduleManifest);
67
82
  console.info(`[webstir-backend] ${mode}:complete (entries=${manifest.entryPoints.length})`);
68
- diagnostics.push({ severity: 'info', message: `[webstir-backend] ${mode}:built entries=${manifest.entryPoints.length}` });
83
+ diagnostics.push({
84
+ severity: 'info',
85
+ message: `[webstir-backend] ${mode}:built entries=${manifest.entryPoints.length}`,
86
+ });
69
87
  const cacheReporter = createCacheReporter({
70
- workspaceRoot: options.workspaceRoot,
88
+ workspaceRoot,
71
89
  buildRoot: paths.buildRoot,
72
90
  env,
73
- diagnostics
91
+ diagnostics,
74
92
  });
75
93
  try {
76
94
  await cacheReporter.diffOutputs(outputs, mode);
@@ -91,11 +109,11 @@ export const backendProvider = {
91
109
  artifacts,
92
110
  manifest: {
93
111
  ...manifest,
94
- diagnostics: filteredDiagnostics
95
- }
112
+ diagnostics: filteredDiagnostics,
113
+ },
96
114
  };
97
115
  },
98
116
  async getScaffoldAssets() {
99
117
  return await getBackendScaffoldAssets();
100
- }
118
+ },
101
119
  };
@@ -0,0 +1,51 @@
1
+ import { type SessionCookieConfig, type SessionStore } from './session.js';
2
+ export type { RouteHandlerResult } from './core.js';
3
+ export interface RuntimeLogger {
4
+ child(bindings: Record<string, unknown>): RuntimeLogger;
5
+ info(value: unknown, message?: string): void;
6
+ warn(value: unknown, message?: string): void;
7
+ error(value: unknown, message?: string): void;
8
+ }
9
+ export interface MetricsTracker {
10
+ record(metric: {
11
+ method: string;
12
+ route: string;
13
+ status: number;
14
+ durationMs: number;
15
+ }): void;
16
+ snapshot(): unknown;
17
+ }
18
+ export interface BunRuntimeEnvLike<TAuthConfig = unknown, TMetricsConfig = unknown> {
19
+ NODE_ENV: string;
20
+ PORT: number;
21
+ auth: TAuthConfig;
22
+ metrics: TMetricsConfig;
23
+ http: {
24
+ bodyLimitBytes: number;
25
+ };
26
+ sessions: SessionCookieConfig;
27
+ }
28
+ export interface BunRuntimeBootstrapOptions<TEnv extends BunRuntimeEnvLike = BunRuntimeEnvLike, TLogger extends RuntimeLogger = RuntimeLogger, TSession extends Record<string, unknown> = Record<string, unknown>, TAuth = unknown, TMetricsTracker extends MetricsTracker = MetricsTracker> {
29
+ importMetaUrl: string;
30
+ moduleCandidates?: readonly string[];
31
+ loadEnv(): TEnv;
32
+ resolveWorkspaceRoot(): string;
33
+ resolveRequestAuth(request: Request, auth: TEnv['auth'], logger?: {
34
+ warn?(message: string, metadata?: Record<string, unknown>): void;
35
+ }): Promise<TAuth | undefined>;
36
+ createBaseLogger(env: TEnv): TLogger;
37
+ createMetricsTracker(config: TEnv['metrics']): TMetricsTracker;
38
+ sessionStore: SessionStore<TSession>;
39
+ }
40
+ export interface DefaultBunBackendBootstrapOptions<TEnv extends BunRuntimeEnvLike = BunRuntimeEnvLike, TLogger extends RuntimeLogger = RuntimeLogger, TSession extends Record<string, unknown> = Record<string, unknown>, TAuth = unknown, TMetricsTracker extends MetricsTracker = MetricsTracker> {
41
+ importMetaUrl: string;
42
+ loadEnv(): TEnv;
43
+ moduleCandidates?: readonly string[];
44
+ resolveWorkspaceRoot?: () => string;
45
+ resolveRequestAuth?: BunRuntimeBootstrapOptions<TEnv, TLogger, TSession, TAuth, TMetricsTracker>['resolveRequestAuth'];
46
+ createBaseLogger?: (env: TEnv) => TLogger;
47
+ createMetricsTracker?: (config: TEnv['metrics']) => TMetricsTracker;
48
+ sessionStore?: SessionStore<TSession>;
49
+ }
50
+ export declare function startBunBackend<TEnv extends BunRuntimeEnvLike, TLogger extends RuntimeLogger, TSession extends Record<string, unknown>, TAuth, TMetricsTracker extends MetricsTracker>(options: BunRuntimeBootstrapOptions<TEnv, TLogger, TSession, TAuth, TMetricsTracker>): Promise<void>;
51
+ export declare function createDefaultBunBackendBootstrap<TEnv extends BunRuntimeEnvLike, TLogger extends RuntimeLogger = RuntimeLogger, TSession extends Record<string, unknown> = Record<string, unknown>, TAuth = unknown, TMetricsTracker extends MetricsTracker = MetricsTracker>(options: DefaultBunBackendBootstrapOptions<TEnv, TLogger, TSession, TAuth, TMetricsTracker>): BunRuntimeBootstrapOptions<TEnv, TLogger, TSession, TAuth, TMetricsTracker>;