@webstir-io/webstir-backend 0.1.15 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +106 -79
  2. package/dist/add.d.ts +59 -0
  3. package/dist/add.js +626 -0
  4. package/dist/build/artifacts.d.ts +115 -1
  5. package/dist/build/artifacts.js +4 -4
  6. package/dist/build/entries.js +1 -1
  7. package/dist/build/pipeline.d.ts +33 -1
  8. package/dist/build/pipeline.js +307 -65
  9. package/dist/cache/diff.js +9 -8
  10. package/dist/cache/reporters.js +1 -1
  11. package/dist/deploy-cli.d.ts +2 -0
  12. package/dist/deploy-cli.js +86 -0
  13. package/dist/diagnostics/summary.js +2 -2
  14. package/dist/index.d.ts +6 -0
  15. package/dist/index.js +4 -0
  16. package/dist/manifest/pipeline.js +103 -32
  17. package/dist/provider.js +35 -17
  18. package/dist/runtime/bun.d.ts +51 -0
  19. package/dist/runtime/bun.js +499 -0
  20. package/dist/runtime/core.d.ts +141 -0
  21. package/dist/runtime/core.js +316 -0
  22. package/dist/runtime/deploy-backend.d.ts +20 -0
  23. package/dist/runtime/deploy-backend.js +175 -0
  24. package/dist/runtime/deploy-shared.d.ts +43 -0
  25. package/dist/runtime/deploy-shared.js +75 -0
  26. package/dist/runtime/deploy-static.d.ts +2 -0
  27. package/dist/runtime/deploy-static.js +161 -0
  28. package/dist/runtime/deploy.d.ts +3 -0
  29. package/dist/runtime/deploy.js +91 -0
  30. package/dist/runtime/forms.d.ts +73 -0
  31. package/dist/runtime/forms.js +236 -0
  32. package/dist/runtime/request-hooks.d.ts +47 -0
  33. package/dist/runtime/request-hooks.js +102 -0
  34. package/dist/runtime/session-metadata.d.ts +13 -0
  35. package/dist/runtime/session-metadata.js +98 -0
  36. package/dist/runtime/session-runtime.d.ts +28 -0
  37. package/dist/runtime/session-runtime.js +180 -0
  38. package/dist/runtime/session.d.ts +83 -0
  39. package/dist/runtime/session.js +396 -0
  40. package/dist/runtime/views.d.ts +74 -0
  41. package/dist/runtime/views.js +221 -0
  42. package/dist/scaffold/assets.js +25 -21
  43. package/dist/testing/context.js +1 -1
  44. package/dist/testing/index.d.ts +1 -1
  45. package/dist/testing/index.js +100 -56
  46. package/dist/utils/bun.d.ts +2 -0
  47. package/dist/utils/bun.js +13 -0
  48. package/dist/watch.d.ts +13 -1
  49. package/dist/watch.js +345 -97
  50. package/dist/workspace.d.ts +8 -0
  51. package/dist/workspace.js +44 -3
  52. package/package.json +49 -14
  53. package/scripts/publish.sh +2 -92
  54. package/scripts/smoke.mjs +282 -107
  55. package/scripts/update-contract.sh +12 -10
  56. package/src/add.ts +964 -0
  57. package/src/build/artifacts.ts +49 -46
  58. package/src/build/entries.ts +12 -12
  59. package/src/build/pipeline.ts +779 -403
  60. package/src/cache/diff.ts +111 -105
  61. package/src/cache/reporters.ts +26 -26
  62. package/src/deploy-cli.ts +111 -0
  63. package/src/diagnostics/summary.ts +28 -22
  64. package/src/index.ts +11 -0
  65. package/src/manifest/pipeline.ts +328 -215
  66. package/src/provider.ts +115 -98
  67. package/src/runtime/bun.ts +793 -0
  68. package/src/runtime/core.ts +598 -0
  69. package/src/runtime/deploy-backend.ts +239 -0
  70. package/src/runtime/deploy-shared.ts +136 -0
  71. package/src/runtime/deploy-static.ts +191 -0
  72. package/src/runtime/deploy.ts +143 -0
  73. package/src/runtime/forms.ts +364 -0
  74. package/src/runtime/request-hooks.ts +165 -0
  75. package/src/runtime/session-metadata.ts +135 -0
  76. package/src/runtime/session-runtime.ts +267 -0
  77. package/src/runtime/session.ts +642 -0
  78. package/src/runtime/views.ts +385 -0
  79. package/src/scaffold/assets.ts +77 -73
  80. package/src/testing/context.js +8 -9
  81. package/src/testing/context.ts +9 -9
  82. package/src/testing/index.d.ts +14 -3
  83. package/src/testing/index.js +254 -175
  84. package/src/testing/index.ts +298 -195
  85. package/src/testing/types.d.ts +18 -19
  86. package/src/testing/types.ts +18 -18
  87. package/src/utils/bun.ts +26 -0
  88. package/src/watch.ts +503 -99
  89. package/src/workspace.ts +59 -3
  90. package/templates/backend/.env.example +15 -0
  91. package/templates/backend/auth/adapter.ts +335 -36
  92. package/templates/backend/db/connection.ts +190 -65
  93. package/templates/backend/db/migrate.ts +149 -43
  94. package/templates/backend/db/types.d.ts +1 -1
  95. package/templates/backend/env.ts +132 -20
  96. package/templates/backend/functions/hello/index.ts +1 -2
  97. package/templates/backend/index.ts +15 -508
  98. package/templates/backend/jobs/nightly/index.ts +1 -1
  99. package/templates/backend/jobs/runtime.ts +24 -11
  100. package/templates/backend/jobs/scheduler.ts +208 -46
  101. package/templates/backend/module.ts +227 -13
  102. package/templates/backend/observability/logger.ts +2 -12
  103. package/templates/backend/observability/metrics.ts +8 -5
  104. package/templates/backend/session/sqlite.ts +152 -0
  105. package/templates/backend/session/store.ts +45 -0
  106. package/templates/backend/tsconfig.json +1 -1
  107. package/tests/add.test.js +327 -0
  108. package/tests/authAdapter.test.js +315 -0
  109. package/tests/bundlerParity.test.js +217 -0
  110. package/tests/cacheReporter.test.js +10 -10
  111. package/tests/dbConnection.test.js +209 -0
  112. package/tests/deploy.test.js +357 -0
  113. package/tests/envLoader.test.js +271 -17
  114. package/tests/integration.test.js +2432 -3
  115. package/tests/jobsScheduler.test.js +253 -0
  116. package/tests/manifest.test.js +287 -12
  117. package/tests/migrationRunner.test.js +249 -0
  118. package/tests/sessionScaffoldStore.test.js +752 -0
  119. package/tests/sessionStore.test.js +490 -0
  120. package/tests/testing.test.js +252 -0
  121. package/tests/watch.test.js +192 -32
  122. package/tsconfig.json +3 -10
  123. package/templates/backend/server/fastify.ts +0 -288
@@ -1,5 +1,6 @@
1
1
  import { test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
+ import crypto from 'node:crypto';
3
4
  import fs from 'node:fs/promises';
4
5
  import os from 'node:os';
5
6
  import path from 'node:path';
@@ -17,31 +18,50 @@ async function copyFile(src, dest) {
17
18
  await fs.copyFile(src, dest);
18
19
  }
19
20
 
20
- function getLocalBinPath() {
21
- const here = path.dirname(fileURLToPath(import.meta.url));
22
- const pkgRoot = path.resolve(here, '..');
23
- return path.join(pkgRoot, 'node_modules', '.bin');
24
- }
25
-
26
- test('env loader reads .env files and surfaces typed config', async () => {
27
- const workspace = await createTempWorkspace();
21
+ async function seedBackendWorkspace(workspace, name) {
28
22
  const assets = await backendProvider.getScaffoldAssets();
29
23
  for (const asset of assets) {
30
24
  await copyFile(asset.sourcePath, path.join(workspace, asset.targetPath));
31
25
  }
26
+
32
27
  await fs.writeFile(
33
28
  path.join(workspace, 'package.json'),
34
29
  JSON.stringify(
35
30
  {
36
- name: '@demo/env-loader',
31
+ name,
37
32
  version: '0.0.0',
38
- type: 'module'
33
+ type: 'module',
39
34
  },
40
35
  null,
41
- 2
36
+ 2,
42
37
  ),
43
- 'utf8'
38
+ 'utf8',
44
39
  );
40
+ }
41
+
42
+ function getLocalBinPath() {
43
+ const here = path.dirname(fileURLToPath(import.meta.url));
44
+ const pkgRoot = path.resolve(here, '..');
45
+ return path.join(pkgRoot, 'node_modules', '.bin');
46
+ }
47
+
48
+ function snapshotEnv(keys) {
49
+ return Object.fromEntries(keys.map((key) => [key, process.env[key]]));
50
+ }
51
+
52
+ function restoreEnv(snapshot) {
53
+ for (const [key, value] of Object.entries(snapshot)) {
54
+ if (value === undefined) {
55
+ delete process.env[key];
56
+ } else {
57
+ process.env[key] = value;
58
+ }
59
+ }
60
+ }
61
+
62
+ test('env loader reads .env files and surfaces typed config', async () => {
63
+ const workspace = await createTempWorkspace();
64
+ await seedBackendWorkspace(workspace, '@demo/env-loader');
45
65
 
46
66
  const envContents = `NODE_ENV=development\nPORT=5055\nAPI_BASE_URL=https://api.example.com\n`;
47
67
  await fs.writeFile(path.join(workspace, '.env'), envContents, 'utf8');
@@ -50,15 +70,249 @@ test('env loader reads .env files and surfaces typed config', async () => {
50
70
  WEBSTIR_MODULE_MODE: 'build',
51
71
  WEBSTIR_BACKEND_TYPECHECK: 'skip',
52
72
  NODE_OPTIONS: '--experimental-transform-types',
53
- PATH: `${getLocalBinPath()}${path.delimiter}${process.env.PATH ?? ''}`
73
+ PATH: `${getLocalBinPath()}${path.delimiter}${process.env.PATH ?? ''}`,
74
+ };
75
+
76
+ await backendProvider.build({ workspaceRoot: workspace, env, incremental: false });
77
+
78
+ const builtEnvModule = path.join(workspace, 'build', 'backend', 'env.js');
79
+ const previousEnv = snapshotEnv(['NODE_ENV', 'PORT', 'API_BASE_URL']);
80
+
81
+ try {
82
+ delete process.env.NODE_ENV;
83
+ delete process.env.PORT;
84
+ delete process.env.API_BASE_URL;
85
+
86
+ const mod = await import(pathToFileURL(builtEnvModule).href);
87
+ const loaded = mod.loadEnv();
88
+
89
+ assert.equal(loaded.PORT, 5055);
90
+ assert.equal(loaded.API_BASE_URL, 'https://api.example.com');
91
+ } finally {
92
+ restoreEnv(previousEnv);
93
+ }
94
+ });
95
+
96
+ test('env loader generates a non-literal session secret fallback when unset', async () => {
97
+ const workspace = await createTempWorkspace('webstir-backend-env-session-');
98
+ await seedBackendWorkspace(workspace, '@demo/env-loader-session');
99
+
100
+ const env = {
101
+ WEBSTIR_MODULE_MODE: 'build',
102
+ WEBSTIR_BACKEND_TYPECHECK: 'skip',
103
+ NODE_OPTIONS: '--experimental-transform-types',
104
+ PATH: `${getLocalBinPath()}${path.delimiter}${process.env.PATH ?? ''}`,
105
+ };
106
+
107
+ const previousSessionSecret = process.env.SESSION_SECRET;
108
+ const previousJwtSecret = process.env.AUTH_JWT_SECRET;
109
+ const previousNodeEnv = process.env.NODE_ENV;
110
+ const previousPort = process.env.PORT;
111
+ const previousApiBaseUrl = process.env.API_BASE_URL;
112
+ delete process.env.SESSION_SECRET;
113
+ delete process.env.AUTH_JWT_SECRET;
114
+
115
+ try {
116
+ await backendProvider.build({ workspaceRoot: workspace, env, incremental: false });
117
+
118
+ const builtEnvModule = path.join(workspace, 'build', 'backend', 'env.js');
119
+ const mod = await import(pathToFileURL(builtEnvModule).href);
120
+ const first = mod.loadEnv();
121
+ const second = mod.loadEnv();
122
+
123
+ assert.notEqual(first.sessions.secret, 'webstir-dev-session-secret');
124
+ assert.equal(first.sessions.secret, second.sessions.secret);
125
+ assert.ok(first.sessions.secret.length >= 32);
126
+ } finally {
127
+ if (previousSessionSecret === undefined) {
128
+ delete process.env.SESSION_SECRET;
129
+ } else {
130
+ process.env.SESSION_SECRET = previousSessionSecret;
131
+ }
132
+ if (previousJwtSecret === undefined) {
133
+ delete process.env.AUTH_JWT_SECRET;
134
+ } else {
135
+ process.env.AUTH_JWT_SECRET = previousJwtSecret;
136
+ }
137
+ if (previousNodeEnv === undefined) {
138
+ delete process.env.NODE_ENV;
139
+ } else {
140
+ process.env.NODE_ENV = previousNodeEnv;
141
+ }
142
+ if (previousPort === undefined) {
143
+ delete process.env.PORT;
144
+ } else {
145
+ process.env.PORT = previousPort;
146
+ }
147
+ if (previousApiBaseUrl === undefined) {
148
+ delete process.env.API_BASE_URL;
149
+ } else {
150
+ process.env.API_BASE_URL = previousApiBaseUrl;
151
+ }
152
+ }
153
+ });
154
+
155
+ test('env loader requires SESSION_SECRET in production', async () => {
156
+ const workspace = await createTempWorkspace('webstir-backend-env-session-prod-');
157
+ await seedBackendWorkspace(workspace, '@demo/env-loader-session-prod');
158
+
159
+ const env = {
160
+ WEBSTIR_MODULE_MODE: 'build',
161
+ WEBSTIR_BACKEND_TYPECHECK: 'skip',
162
+ NODE_OPTIONS: '--experimental-transform-types',
163
+ PATH: `${getLocalBinPath()}${path.delimiter}${process.env.PATH ?? ''}`,
164
+ };
165
+
166
+ const previousEnv = snapshotEnv(['NODE_ENV', 'SESSION_SECRET', 'AUTH_JWT_SECRET']);
167
+
168
+ try {
169
+ await backendProvider.build({ workspaceRoot: workspace, env, incremental: false });
170
+
171
+ const builtEnvModule = path.join(workspace, 'build', 'backend', 'env.js');
172
+ process.env.NODE_ENV = 'production';
173
+ delete process.env.SESSION_SECRET;
174
+ delete process.env.AUTH_JWT_SECRET;
175
+
176
+ const mod = await import(pathToFileURL(builtEnvModule).href);
177
+ assert.throws(() => mod.loadEnv(), /SESSION_SECRET is required when NODE_ENV=production/);
178
+ } finally {
179
+ restoreEnv(previousEnv);
180
+ }
181
+ });
182
+
183
+ test('env loader falls back from blank WORKSPACE_ROOT to WEBSTIR_WORKSPACE_ROOT outside the workspace cwd', async () => {
184
+ const workspace = await createTempWorkspace('webstir-backend-env-root-');
185
+ const alternateCwd = await createTempWorkspace('webstir-backend-env-root-cwd-');
186
+ await seedBackendWorkspace(workspace, '@demo/env-loader-root');
187
+ await fs.writeFile(
188
+ path.join(workspace, '.env'),
189
+ 'PORT=6060\nAPI_BASE_URL=https://root.example.com\n',
190
+ 'utf8',
191
+ );
192
+
193
+ const env = {
194
+ WEBSTIR_MODULE_MODE: 'build',
195
+ WEBSTIR_BACKEND_TYPECHECK: 'skip',
196
+ NODE_OPTIONS: '--experimental-transform-types',
197
+ PATH: `${getLocalBinPath()}${path.delimiter}${process.env.PATH ?? ''}`,
54
198
  };
55
199
 
56
200
  await backendProvider.build({ workspaceRoot: workspace, env, incremental: false });
57
201
 
58
202
  const builtEnvModule = path.join(workspace, 'build', 'backend', 'env.js');
59
- const mod = await import(pathToFileURL(builtEnvModule).href);
60
- const loaded = mod.loadEnv();
203
+ const previousEnv = snapshotEnv([
204
+ 'WORKSPACE_ROOT',
205
+ 'WEBSTIR_WORKSPACE_ROOT',
206
+ 'PORT',
207
+ 'API_BASE_URL',
208
+ ]);
209
+ const previousCwd = process.cwd();
210
+
211
+ try {
212
+ process.env.WORKSPACE_ROOT = ' ';
213
+ process.env.WEBSTIR_WORKSPACE_ROOT = workspace;
214
+ delete process.env.PORT;
215
+ delete process.env.API_BASE_URL;
216
+ process.chdir(alternateCwd);
61
217
 
62
- assert.equal(loaded.PORT, 5055);
63
- assert.equal(loaded.API_BASE_URL, 'https://api.example.com');
218
+ const mod = await import(pathToFileURL(builtEnvModule).href);
219
+ const loaded = mod.loadEnv();
220
+
221
+ assert.equal(loaded.PORT, 6060);
222
+ assert.equal(loaded.API_BASE_URL, 'https://root.example.com');
223
+ } finally {
224
+ process.chdir(previousCwd);
225
+ restoreEnv(previousEnv);
226
+ }
227
+ });
228
+
229
+ test('env loader infers workspace root from the built module path outside the workspace cwd', async () => {
230
+ const workspace = await createTempWorkspace('webstir-backend-env-infer-');
231
+ const alternateCwd = await createTempWorkspace('webstir-backend-env-infer-cwd-');
232
+ await seedBackendWorkspace(workspace, '@demo/env-loader-infer');
233
+ await fs.writeFile(
234
+ path.join(workspace, '.env'),
235
+ 'PORT=7070\nAPI_BASE_URL=https://infer.example.com\n',
236
+ 'utf8',
237
+ );
238
+
239
+ const env = {
240
+ WEBSTIR_MODULE_MODE: 'build',
241
+ WEBSTIR_BACKEND_TYPECHECK: 'skip',
242
+ NODE_OPTIONS: '--experimental-transform-types',
243
+ PATH: `${getLocalBinPath()}${path.delimiter}${process.env.PATH ?? ''}`,
244
+ };
245
+
246
+ await backendProvider.build({ workspaceRoot: workspace, env, incremental: false });
247
+
248
+ const builtEnvModule = path.join(workspace, 'build', 'backend', 'env.js');
249
+ const previousEnv = snapshotEnv([
250
+ 'WORKSPACE_ROOT',
251
+ 'WEBSTIR_WORKSPACE_ROOT',
252
+ 'PORT',
253
+ 'API_BASE_URL',
254
+ ]);
255
+ const previousCwd = process.cwd();
256
+
257
+ try {
258
+ delete process.env.WORKSPACE_ROOT;
259
+ delete process.env.WEBSTIR_WORKSPACE_ROOT;
260
+ delete process.env.PORT;
261
+ delete process.env.API_BASE_URL;
262
+ process.chdir(alternateCwd);
263
+
264
+ const mod = await import(pathToFileURL(builtEnvModule).href);
265
+ const loaded = mod.loadEnv();
266
+
267
+ assert.equal(loaded.PORT, 7070);
268
+ assert.equal(loaded.API_BASE_URL, 'https://infer.example.com');
269
+ } finally {
270
+ process.chdir(previousCwd);
271
+ restoreEnv(previousEnv);
272
+ }
273
+ });
274
+
275
+ test('env loader resolves AUTH_JWT_PUBLIC_KEY_FILE from the workspace root', async () => {
276
+ const workspace = await createTempWorkspace('webstir-backend-env-auth-key-');
277
+ const alternateCwd = await createTempWorkspace('webstir-backend-env-auth-key-cwd-');
278
+ await seedBackendWorkspace(workspace, '@demo/env-loader-auth-key');
279
+
280
+ const { publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });
281
+ const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }).toString();
282
+ await fs.mkdir(path.join(workspace, 'config'), { recursive: true });
283
+ await fs.writeFile(path.join(workspace, 'config', 'jwt-public.pem'), publicKeyPem, 'utf8');
284
+
285
+ const env = {
286
+ WEBSTIR_MODULE_MODE: 'build',
287
+ WEBSTIR_BACKEND_TYPECHECK: 'skip',
288
+ NODE_OPTIONS: '--experimental-transform-types',
289
+ PATH: `${getLocalBinPath()}${path.delimiter}${process.env.PATH ?? ''}`,
290
+ };
291
+
292
+ await backendProvider.build({ workspaceRoot: workspace, env, incremental: false });
293
+
294
+ const builtEnvModule = path.join(workspace, 'build', 'backend', 'env.js');
295
+ const previousEnv = snapshotEnv([
296
+ 'WORKSPACE_ROOT',
297
+ 'WEBSTIR_WORKSPACE_ROOT',
298
+ 'AUTH_JWT_PUBLIC_KEY',
299
+ 'AUTH_JWT_PUBLIC_KEY_FILE',
300
+ ]);
301
+ const previousCwd = process.cwd();
302
+
303
+ try {
304
+ process.env.WORKSPACE_ROOT = ' ';
305
+ process.env.WEBSTIR_WORKSPACE_ROOT = workspace;
306
+ delete process.env.AUTH_JWT_PUBLIC_KEY;
307
+ process.env.AUTH_JWT_PUBLIC_KEY_FILE = 'config/jwt-public.pem';
308
+ process.chdir(alternateCwd);
309
+
310
+ const mod = await import(pathToFileURL(builtEnvModule).href);
311
+ const loaded = mod.loadEnv();
312
+
313
+ assert.match(loaded.auth.jwtPublicKey ?? '', /BEGIN PUBLIC KEY/);
314
+ } finally {
315
+ process.chdir(previousCwd);
316
+ restoreEnv(previousEnv);
317
+ }
64
318
  });