@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
@@ -2,98 +2,8 @@
2
2
 
3
3
  set -euo pipefail
4
4
 
5
- usage() {
6
- cat <<'EOF'
7
- Usage: scripts/publish.sh <patch|minor|major|x.y.z> [--no-push]
8
-
9
- Examples:
10
- scripts/publish.sh patch
11
- scripts/publish.sh 0.1.0
12
-
13
- The script requires a clean git worktree.
14
-
15
- By default, the script pushes the version bump commit and tag. To skip pushing,
16
- pass --no-push or set PUBLISH_NO_PUSH=1.
17
- EOF
18
- exit 1
19
- }
20
-
21
5
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
22
6
  ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
7
+ MONOREPO_ROOT="$(git -C "$ROOT_DIR" rev-parse --show-toplevel)"
23
8
 
24
- main() {
25
- if [[ $# -lt 1 ]]; then
26
- echo "error: version bump argument missing" >&2
27
- usage
28
- fi
29
-
30
- local bump="$1"; shift || true
31
- local no_push="false"
32
-
33
- while [[ $# -gt 0 ]]; do
34
- case "$1" in
35
- --no-push)
36
- no_push="true"
37
- ;;
38
- *)
39
- echo "error: unknown option '$1'" >&2
40
- usage
41
- ;;
42
- esac
43
- shift || true
44
- done
45
-
46
- if [[ ! $bump =~ ^(patch|minor|major)$ && ! $bump =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
47
- echo "error: invalid bump '$bump'" >&2
48
- usage
49
- fi
50
-
51
- ensure_clean_git
52
-
53
- cd "$ROOT_DIR"
54
-
55
- echo "› npm version $bump"
56
- npm version "$bump" -m "v%s"
57
-
58
- echo "› npm install --package-lock-only"
59
- npm install --package-lock-only
60
-
61
- echo "› npm run clean"
62
- npm run clean
63
-
64
- echo "› npm run build"
65
- npm run build
66
-
67
- echo "› npm test"
68
- npm test
69
-
70
- echo "› npm run smoke"
71
- npm run smoke
72
-
73
- echo "› Skipping direct npm publish; pushing commit+tag will trigger the release workflow."
74
-
75
- if [[ "$no_push" == "true" || "${PUBLISH_NO_PUSH:-}" =~ ^([Yy][Ee][Ss]|[Yy]|1|true)$ ]]; then
76
- echo "› Skipping git push (no-push)."
77
- echo " To publish upstream later, run: git push && git push --tags"
78
- return 0
79
- fi
80
-
81
- echo "› git push"
82
- git push
83
- echo "› git push --tags"
84
- git push --tags
85
- }
86
-
87
- ensure_clean_git() {
88
- cd "$ROOT_DIR"
89
- if ! git diff --quiet --ignore-submodules HEAD; then
90
- echo "error: git worktree has uncommitted changes" >&2
91
- exit 1
92
- fi
93
- if ! git diff --quiet --cached --ignore-submodules; then
94
- echo "error: git index has staged changes" >&2
95
- exit 1
96
- fi
97
- }
98
-
99
- main "$@"
9
+ exec bun "$MONOREPO_ROOT/tools/release-package.mjs" --package-dir "$ROOT_DIR" "$@"
package/scripts/smoke.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import { fileURLToPath } from 'node:url';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
5
  import { spawn } from 'node:child_process';
6
6
  import { backendProvider } from '../dist/index.js';
7
7
  import { CONTRACT_VERSION } from '@webstir-io/module-contract';
@@ -12,6 +12,11 @@ function getLocalBinPath() {
12
12
  return path.join(pkgRoot, 'node_modules', '.bin');
13
13
  }
14
14
 
15
+ function getPackageRoot() {
16
+ const here = path.dirname(fileURLToPath(import.meta.url));
17
+ return path.resolve(here, '..');
18
+ }
19
+
15
20
  async function ensureDir(dir) {
16
21
  await fs.mkdir(dir, { recursive: true });
17
22
  }
@@ -23,17 +28,236 @@ async function copyFile(src, dest) {
23
28
 
24
29
  async function installPackages(workspace, packages, options = { dev: false }) {
25
30
  if (!packages || packages.length === 0) return;
26
- const args = ['install', '--silent', ...packages];
31
+ const args = ['add', '--silent', ...packages];
27
32
  if (options.dev) {
28
33
  args.push('-D');
29
34
  }
30
35
  await new Promise((resolve, reject) => {
31
- const child = spawn('npm', args, { cwd: workspace, stdio: 'ignore' });
36
+ const child = spawn('bun', args, { cwd: workspace, stdio: 'ignore' });
32
37
  child.on('error', reject);
33
- child.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`npm install failed (${code})`))));
38
+ child.on('close', (code) =>
39
+ code === 0 ? resolve() : reject(new Error(`bun add failed (${code})`)),
40
+ );
34
41
  });
35
42
  }
36
43
 
44
+ async function pathExists(target) {
45
+ try {
46
+ await fs.access(target);
47
+ return true;
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ async function runBunProbe(script, { cwd, env }) {
54
+ return await new Promise((resolve, reject) => {
55
+ const child = spawn('bun', ['--eval', script], {
56
+ cwd,
57
+ env: {
58
+ ...process.env,
59
+ ...env,
60
+ },
61
+ stdio: ['ignore', 'pipe', 'pipe'],
62
+ });
63
+
64
+ let stdout = '';
65
+ let stderr = '';
66
+ child.stdout.on('data', (chunk) => {
67
+ stdout += chunk.toString();
68
+ });
69
+ child.stderr.on('data', (chunk) => {
70
+ stderr += chunk.toString();
71
+ });
72
+ child.on('error', reject);
73
+ child.on('close', (code) => {
74
+ if (code !== 0) {
75
+ reject(new Error(`bun probe failed (${code}).\nstdout:\n${stdout}\nstderr:\n${stderr}`));
76
+ return;
77
+ }
78
+ const line = stdout
79
+ .split(/\r?\n/)
80
+ .map((value) => value.trim())
81
+ .find((value) => value.startsWith('{'));
82
+ if (!line) {
83
+ reject(new Error(`bun probe did not emit JSON.\nstdout:\n${stdout}\nstderr:\n${stderr}`));
84
+ return;
85
+ }
86
+ resolve(JSON.parse(line));
87
+ });
88
+ });
89
+ }
90
+
91
+ async function linkWorkspaceNodeModules(workspace) {
92
+ const packageRoot = getPackageRoot();
93
+ const source = path.join(packageRoot, 'node_modules');
94
+ const target = path.join(workspace, 'node_modules');
95
+ await fs.mkdir(target, { recursive: true });
96
+
97
+ const entries = await fs.readdir(source, { withFileTypes: true });
98
+ for (const entry of entries) {
99
+ if (entry.name === '@webstir-io') {
100
+ continue;
101
+ }
102
+ await createSymlinkIfMissing(
103
+ path.join(source, entry.name),
104
+ path.join(target, entry.name),
105
+ entry.isDirectory() ? 'dir' : 'file',
106
+ );
107
+ }
108
+
109
+ const scopeSource = path.join(source, '@webstir-io');
110
+ const scopeTarget = path.join(target, '@webstir-io');
111
+ await fs.mkdir(scopeTarget, { recursive: true });
112
+ const scopeEntries = await fs.readdir(scopeSource, { withFileTypes: true });
113
+ for (const entry of scopeEntries) {
114
+ await createSymlinkIfMissing(
115
+ path.join(scopeSource, entry.name),
116
+ path.join(scopeTarget, entry.name),
117
+ entry.isDirectory() ? 'dir' : 'file',
118
+ );
119
+ }
120
+
121
+ await createSymlinkIfMissing(packageRoot, path.join(scopeTarget, 'webstir-backend'), 'dir');
122
+ }
123
+
124
+ async function createSymlinkIfMissing(source, target, type) {
125
+ try {
126
+ await fs.symlink(source, target, type);
127
+ } catch (error) {
128
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'EEXIST') {
129
+ return;
130
+ }
131
+ throw error;
132
+ }
133
+ }
134
+
135
+ async function runTemplateSqliteProbes(workspace) {
136
+ const alternateCwd = await fs.mkdtemp(
137
+ path.join(os.tmpdir(), 'webstir-backend-smoke-sqlite-cwd-'),
138
+ );
139
+ const sessionStoreUrl = JSON.stringify(
140
+ pathToFileURL(path.join(workspace, 'src', 'backend', 'session', 'store.ts')).href,
141
+ );
142
+ const runtimeSessionUrl = JSON.stringify(
143
+ pathToFileURL(path.join(getPackageRoot(), 'dist', 'runtime', 'session.js')).href,
144
+ );
145
+ const dbConnectionUrl = JSON.stringify(
146
+ pathToFileURL(path.join(workspace, 'src', 'backend', 'db', 'connection.ts')).href,
147
+ );
148
+
149
+ const sessionProbe = `
150
+ const [{ sessionStore }, { prepareSessionState }] = await Promise.all([
151
+ import(${sessionStoreUrl}),
152
+ import(${runtimeSessionUrl})
153
+ ]);
154
+
155
+ const config = {
156
+ secret: 'smoke-session-secret',
157
+ cookieName: 'webstir_session',
158
+ secure: false,
159
+ maxAgeSeconds: 60
160
+ };
161
+ const loginRoute = {
162
+ form: {
163
+ session: { write: true },
164
+ flash: {
165
+ publish: [{ key: 'signed-in', level: 'success', when: 'success' }]
166
+ }
167
+ }
168
+ };
169
+ const accountRoute = {
170
+ session: { mode: 'optional' },
171
+ flash: { consume: ['signed-in'] }
172
+ };
173
+ const created = prepareSessionState({
174
+ cookies: '',
175
+ route: loginRoute,
176
+ config,
177
+ store: sessionStore
178
+ });
179
+ const createdCommit = created.commit({
180
+ session: {
181
+ userId: 'ada@example.com'
182
+ },
183
+ route: loginRoute,
184
+ result: {
185
+ status: 303,
186
+ redirect: { location: '/session/account' }
187
+ }
188
+ });
189
+ const cookie = String(createdCommit.setCookie).split(';')[0];
190
+ const read = prepareSessionState({
191
+ cookies: cookie,
192
+ route: accountRoute,
193
+ config,
194
+ store: sessionStore
195
+ });
196
+ console.log(JSON.stringify({
197
+ userId: read.session?.userId ?? null,
198
+ flash: read.flash.map((message) => ({ key: message.key, level: message.level }))
199
+ }));
200
+ `;
201
+
202
+ const dbProbe = `
203
+ const { createDatabaseClient } = await import(${dbConnectionUrl});
204
+ const db = await createDatabaseClient();
205
+ await db.execute('CREATE TABLE IF NOT EXISTS smoke_items (id TEXT PRIMARY KEY, value TEXT NOT NULL)');
206
+ await db.execute('DELETE FROM smoke_items');
207
+ await db.execute('INSERT INTO smoke_items (id, value) VALUES (?, ?)', ['row-1', 'Ada']);
208
+ const rows = await db.query('SELECT value FROM smoke_items WHERE id = ?', ['row-1']);
209
+ const databases = await db.query('PRAGMA database_list');
210
+ const main = databases.find((row) => row.name === 'main');
211
+ console.log(JSON.stringify({ target: main?.file ?? null, value: rows[0]?.value ?? null }));
212
+ await db.close();
213
+ `;
214
+
215
+ const sessionResult = await runBunProbe(sessionProbe, {
216
+ cwd: alternateCwd,
217
+ env: {
218
+ WORKSPACE_ROOT: ' ',
219
+ WEBSTIR_WORKSPACE_ROOT: workspace,
220
+ SESSION_STORE_DRIVER: 'sqlite',
221
+ SESSION_STORE_URL: 'file:./data/smoke-session.sqlite',
222
+ },
223
+ });
224
+ console.info('[smoke] sqlite session probe:', sessionResult);
225
+ if (sessionResult.userId !== 'ada@example.com') {
226
+ throw new Error(
227
+ `[smoke] sqlite session probe returned unexpected userId ${sessionResult.userId}`,
228
+ );
229
+ }
230
+ if (!(await pathExists(path.join(workspace, 'data', 'smoke-session.sqlite')))) {
231
+ throw new Error(
232
+ '[smoke] sqlite session probe did not create the workspace-root session database',
233
+ );
234
+ }
235
+ if (await pathExists(path.join(alternateCwd, 'data', 'smoke-session.sqlite'))) {
236
+ throw new Error(
237
+ '[smoke] sqlite session probe wrote to the probe cwd instead of the workspace root',
238
+ );
239
+ }
240
+
241
+ const dbResult = await runBunProbe(dbProbe, {
242
+ cwd: alternateCwd,
243
+ env: {
244
+ WORKSPACE_ROOT: ' ',
245
+ WEBSTIR_WORKSPACE_ROOT: workspace,
246
+ DATABASE_URL: 'file:./data/smoke-db.sqlite',
247
+ },
248
+ });
249
+ console.info('[smoke] sqlite db probe:', dbResult);
250
+ if (dbResult.value !== 'Ada') {
251
+ throw new Error(`[smoke] sqlite db probe returned unexpected value ${dbResult.value}`);
252
+ }
253
+ if (!(await pathExists(path.join(workspace, 'data', 'smoke-db.sqlite')))) {
254
+ throw new Error('[smoke] sqlite db probe did not create the workspace-root database');
255
+ }
256
+ if (await pathExists(path.join(alternateCwd, 'data', 'smoke-db.sqlite'))) {
257
+ throw new Error('[smoke] sqlite db probe wrote to the probe cwd instead of the workspace root');
258
+ }
259
+ }
260
+
37
261
  async function main() {
38
262
  const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'webstir-backend-smoke-'));
39
263
  const assets = await backendProvider.getScaffoldAssets();
@@ -41,7 +265,7 @@ async function main() {
41
265
  assets.map(async (asset) => {
42
266
  const target = path.join(workspace, asset.targetPath);
43
267
  await copyFile(asset.sourcePath, target);
44
- })
268
+ }),
45
269
  );
46
270
 
47
271
  const backendTsconfigPath = path.join(workspace, 'src', 'backend', 'tsconfig.json');
@@ -51,7 +275,11 @@ async function main() {
51
275
  if (backendTsconfig?.compilerOptions) {
52
276
  delete backendTsconfig.compilerOptions.types;
53
277
  }
54
- await fs.writeFile(backendTsconfigPath, `${JSON.stringify(backendTsconfig, null, 2)}\n`, 'utf8');
278
+ await fs.writeFile(
279
+ backendTsconfigPath,
280
+ `${JSON.stringify(backendTsconfig, null, 2)}\n`,
281
+ 'utf8',
282
+ );
55
283
  } catch (error) {
56
284
  console.warn('[smoke] failed to adjust backend tsconfig:', error);
57
285
  }
@@ -70,24 +298,15 @@ async function main() {
70
298
  kind: 'backend',
71
299
  capabilities: [],
72
300
  routes: [],
73
- views: []
74
- }
75
- }
301
+ views: [],
302
+ },
303
+ },
76
304
  };
77
305
  await fs.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8');
78
306
 
79
- await installPackages(workspace, ['pino', 'better-sqlite3']);
307
+ await installPackages(workspace, ['pino']);
80
308
 
81
- if (process.env.WEBSTIR_BACKEND_SMOKE_FASTIFY !== 'skip') {
82
- // Add optional Fastify dependency so the scaffold type-checks if present
83
- try {
84
- await installPackages(workspace, ['fastify', '@types/node@^20'], { dev: true });
85
- } catch (err) {
86
- console.warn('[smoke] skipping Fastify install:', err);
87
- }
88
- } else {
89
- console.info('[smoke] fastify install skipped by WEBSTIR_BACKEND_SMOKE_FASTIFY=skip');
90
- }
309
+ await linkWorkspaceNodeModules(workspace);
91
310
 
92
311
  const rootTsconfigPath = path.join(workspace, 'tsconfig.json');
93
312
  const rootTsconfig = {
@@ -99,8 +318,8 @@ async function main() {
99
318
  strict: true,
100
319
  isolatedModules: true,
101
320
  esModuleInterop: true,
102
- skipLibCheck: true
103
- }
321
+ skipLibCheck: true,
322
+ },
104
323
  };
105
324
  await fs.writeFile(rootTsconfigPath, `${JSON.stringify(rootTsconfig, null, 2)}\n`, 'utf8');
106
325
 
@@ -108,131 +327,87 @@ async function main() {
108
327
  PATH: `${getLocalBinPath()}${path.delimiter}${process.env.PATH ?? ''}`,
109
328
  WEBSTIR_BACKEND_TYPECHECK: 'skip',
110
329
  // Exercise provider diagnostic filtering: suppress info by default
111
- WEBSTIR_BACKEND_LOG_LEVEL: 'warn'
330
+ WEBSTIR_BACKEND_LOG_LEVEL: 'warn',
112
331
  };
113
332
 
114
333
  console.info('[smoke] build mode');
115
334
  const buildResult = await backendProvider.build({
116
335
  workspaceRoot: workspace,
117
336
  env: { ...envBase, WEBSTIR_MODULE_MODE: 'build' },
118
- incremental: false
337
+ incremental: false,
119
338
  });
120
339
  const buildEntries = buildResult.manifest.entryPoints;
121
340
  const buildFunctions = buildEntries.filter((p) => p.startsWith('functions/')).length;
122
341
  const buildJobs = buildEntries.filter((p) => p.startsWith('jobs/')).length;
123
- const buildServer = buildEntries.filter((p) => p === 'index.js' || /(^|\/)index\.js$/.test(p) && !/^(functions|jobs)\//.test(p)).length;
342
+ const buildServer = buildEntries.filter(
343
+ (p) => p === 'index.js' || (/(^|\/)index\.js$/.test(p) && !/^(functions|jobs)\//.test(p)),
344
+ ).length;
124
345
  console.info('[smoke] build entryPoints:', buildEntries);
125
- console.info('[smoke] build entry counts:', { server: buildServer, functions: buildFunctions, jobs: buildJobs });
346
+ console.info('[smoke] build entry counts:', {
347
+ server: buildServer,
348
+ functions: buildFunctions,
349
+ jobs: buildJobs,
350
+ });
126
351
  if (buildFunctions < 1 || buildJobs < 1) {
127
- throw new Error(`[smoke] expected scaffold to include functions and jobs (got functions=${buildFunctions}, jobs=${buildJobs})`);
352
+ throw new Error(
353
+ `[smoke] expected scaffold to include functions and jobs (got functions=${buildFunctions}, jobs=${buildJobs})`,
354
+ );
128
355
  }
129
356
  const buildModule = buildResult.manifest.module ?? {};
130
357
  console.info('[smoke] build routes/views summary:', {
131
358
  routes: Array.isArray(buildModule.routes) ? buildModule.routes.length : 0,
132
- views: Array.isArray(buildModule.views) ? buildModule.views.length : 0
359
+ views: Array.isArray(buildModule.views) ? buildModule.views.length : 0,
133
360
  });
134
- console.info('[smoke] build diagnostics (>=warn):', buildResult.manifest.diagnostics.map((d) => d.message));
361
+ console.info(
362
+ '[smoke] build diagnostics (>=warn):',
363
+ buildResult.manifest.diagnostics.map((d) => d.message),
364
+ );
365
+ console.info('[smoke] sqlite template probes');
366
+ await runTemplateSqliteProbes(workspace);
135
367
 
136
368
  console.info('[smoke] publish mode');
137
369
  const publishResult = await backendProvider.build({
138
370
  workspaceRoot: workspace,
139
371
  // Intentionally clear PATH so `tsc` is not found; provider will warn and continue
140
372
  env: { ...envBase, WEBSTIR_MODULE_MODE: 'publish', PATH: '' },
141
- incremental: false
373
+ incremental: false,
142
374
  });
143
375
  const publishEntries = publishResult.manifest.entryPoints;
144
376
  const publishFunctions = publishEntries.filter((p) => p.startsWith('functions/')).length;
145
377
  const publishJobs = publishEntries.filter((p) => p.startsWith('jobs/')).length;
146
- const publishServer = publishEntries.filter((p) => p === 'index.js' || /(^|\/)index\.js$/.test(p) && !/^(functions|jobs)\//.test(p)).length;
378
+ const publishServer = publishEntries.filter(
379
+ (p) => p === 'index.js' || (/(^|\/)index\.js$/.test(p) && !/^(functions|jobs)\//.test(p)),
380
+ ).length;
147
381
  console.info('[smoke] publish entryPoints:', publishEntries);
148
- console.info('[smoke] publish entry counts:', { server: publishServer, functions: publishFunctions, jobs: publishJobs });
382
+ console.info('[smoke] publish entry counts:', {
383
+ server: publishServer,
384
+ functions: publishFunctions,
385
+ jobs: publishJobs,
386
+ });
149
387
  if (publishFunctions < 1 || publishJobs < 1) {
150
- throw new Error(`[smoke] expected scaffold to include functions and jobs after publish (got functions=${publishFunctions}, jobs=${publishJobs})`);
388
+ throw new Error(
389
+ `[smoke] expected scaffold to include functions and jobs after publish (got functions=${publishFunctions}, jobs=${publishJobs})`,
390
+ );
151
391
  }
152
392
  const publishModule = publishResult.manifest.module ?? {};
153
393
  console.info('[smoke] publish routes/views summary:', {
154
394
  routes: Array.isArray(publishModule.routes) ? publishModule.routes.length : 0,
155
- views: Array.isArray(publishModule.views) ? publishModule.views.length : 0
395
+ views: Array.isArray(publishModule.views) ? publishModule.views.length : 0,
156
396
  });
157
397
  const publishDiagnostics = publishResult.manifest.diagnostics
158
398
  .map((d) => ({ ...d, message: d.message.trim() }))
159
399
  .filter((d) => d.severity !== 'info');
160
- const unexpectedPublishDiagnostics = publishDiagnostics.filter((d) => !/TypeScript compiler \(tsc\) not found|Type checking failed/.test(d.message));
400
+ const unexpectedPublishDiagnostics = publishDiagnostics.filter(
401
+ (d) => !/TypeScript compiler \(tsc\) not found|Type checking failed/.test(d.message),
402
+ );
161
403
  if (unexpectedPublishDiagnostics.length > 0) {
162
- console.info('[smoke] publish diagnostics (non-info):', unexpectedPublishDiagnostics.map((d) => d.message));
163
- }
164
-
165
- if (process.env.WEBSTIR_BACKEND_SMOKE_FASTIFY !== 'skip') {
166
- // Fastify scaffold type-check (no run): ensure tsc sees server/fastify.ts
167
- console.info('[smoke] fastify type-check');
168
- const typecheckResult = await backendProvider.build({
169
- workspaceRoot: workspace,
170
- env: { PATH: envBase.PATH, WEBSTIR_BACKEND_LOG_LEVEL: 'warn', WEBSTIR_MODULE_MODE: 'build', WEBSTIR_BACKEND_TYPECHECK: 'skip' },
171
- incremental: false
172
- });
173
- const typecheckErrors = typecheckResult.manifest.diagnostics.filter((d) => d.severity === 'error');
174
- if (typecheckErrors.length > 0) {
175
- throw new Error(`[smoke] fastify type-check reported errors: ${typecheckErrors.map((d) => d.message).join('; ')}`);
176
- }
177
-
178
- // Optionally run server and hit /api/health
179
- if (process.env.WEBSTIR_BACKEND_SMOKE_FASTIFY_RUN !== 'skip') {
180
- console.info('[smoke] fastify run + health check');
181
- const port = 47891;
182
- const child = spawn(process.execPath, ['build/backend/server/fastify.js'], {
183
- cwd: workspace,
184
- stdio: ['ignore', 'pipe', 'pipe'],
185
- env: { ...process.env, PORT: String(port) }
186
- });
187
-
188
- let ready = false;
189
- const outChunks = [];
190
- child.stdout.on('data', (c) => {
191
- const s = c.toString();
192
- outChunks.push(s);
193
- if (!ready && s.includes('API server running')) {
194
- ready = true;
195
- (async () => {
196
- try {
197
- const res = await fetch(`http://127.0.0.1:${port}/api/health`);
198
- if (!res.ok) throw new Error(`health returned ${res.status}`);
199
- const json = await res.json();
200
- if (!json || json.ok !== true) throw new Error('health payload invalid');
201
- } catch (err) {
202
- console.error('[smoke] fastify health check failed:', err);
203
- child.kill();
204
- throw err;
205
- } finally {
206
- child.kill();
207
- }
208
- })().catch((err) => {
209
- console.error(err);
210
- process.exitCode = 1;
211
- });
212
- }
213
- });
214
-
215
- await new Promise((resolve) => {
216
- const timer = setTimeout(() => {
217
- if (!ready) {
218
- console.error('[smoke] fastify did not reach readiness');
219
- child.kill();
220
- }
221
- resolve(null);
222
- }, 8000);
223
- child.on('close', () => {
224
- clearTimeout(timer);
225
- resolve(null);
226
- });
227
- });
228
- } else {
229
- console.info('[smoke] fastify run skipped by WEBSTIR_BACKEND_SMOKE_FASTIFY_RUN=skip');
230
- }
231
- } else {
232
- console.info('[smoke] fastify type-check skipped by WEBSTIR_BACKEND_SMOKE_FASTIFY=skip');
404
+ console.info(
405
+ '[smoke] publish diagnostics (non-info):',
406
+ unexpectedPublishDiagnostics.map((d) => d.message),
407
+ );
233
408
  }
234
409
 
235
- console.info('[smoke] completed: build ✔ publish ✔ fastify ✔');
410
+ console.info('[smoke] completed: build ✔ publish ✔');
236
411
  }
237
412
 
238
413
  main().catch((err) => {
@@ -22,6 +22,8 @@ EOF
22
22
 
23
23
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
24
24
  ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
25
+ MONOREPO_ROOT="$(git -C "$ROOT_DIR" rev-parse --show-toplevel)"
26
+ PACKAGE_NAME="$(node -p "require('./package.json').name" 2>/dev/null)"
25
27
 
26
28
  has_script() {
27
29
  local script_name="$1"
@@ -85,33 +87,33 @@ main() {
85
87
  echo "› Setting @webstir-io/module-contract to $spec"
86
88
  npm pkg set "dependencies.@webstir-io/module-contract=$spec"
87
89
 
88
- echo "› npm install (refresh lockfile)"
90
+ echo "› bun install (refresh workspace lockfile)"
89
91
  if [[ "$fast" == "true" ]]; then
90
- npm install --package-lock-only --no-audit --no-fund --ignore-scripts
92
+ bun install --cwd "$MONOREPO_ROOT" --filter "$PACKAGE_NAME" --lockfile-only
91
93
  else
92
- npm install --no-audit --no-fund
94
+ bun install --cwd "$MONOREPO_ROOT" --filter "$PACKAGE_NAME"
93
95
  fi
94
96
 
95
97
  # Clarify versions in logs to avoid confusion with backend package version
96
98
  local backend_ver
97
99
  backend_ver="$(node -p "require('./package.json').version" 2>/dev/null || echo 'unknown')"
98
100
  local installed_contract
99
- installed_contract="$(npm ls @webstir-io/module-contract --json 2>/dev/null | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{try{const j=JSON.parse(d);const v=(j.dependencies&&j.dependencies['@webstir-io/module-contract']&&j.dependencies['@webstir-io/module-contract'].version)||'';console.log(v||'unknown')}catch{console.log('unknown')}})")"
101
+ installed_contract="$(node -p "try { require('@webstir-io/module-contract/package.json').version } catch { 'unknown' }" 2>/dev/null || echo 'unknown')"
100
102
  echo "› Backend package: @webstir-io/webstir-backend@${backend_ver}"
101
103
  echo "› Contract installed: @webstir-io/module-contract@${installed_contract}"
102
104
 
103
105
  if [[ "$fast" != "true" ]]; then
104
- echo "› npm run build"
105
- npm run build
106
+ echo "› bun run build"
107
+ bun run build
106
108
 
107
109
  if has_script test; then
108
- echo "› npm test"
109
- npm test
110
+ echo "› bun run test"
111
+ bun run test
110
112
  fi
111
113
 
112
114
  if has_script smoke; then
113
- echo "› npm run smoke"
114
- npm run smoke
115
+ echo "› bun run smoke"
116
+ bun run smoke
115
117
  fi
116
118
  fi
117
119