@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,385 @@
1
+ import path from 'node:path';
2
+ import { access, stat } from 'node:fs/promises';
3
+
4
+ import { resolveWorkspaceRoot } from '../workspace.js';
5
+ import { readTextFile } from '../utils/bun.js';
6
+
7
+ export interface EnvAccessorLike {
8
+ get(name: string): string | undefined;
9
+ require(name: string): string;
10
+ entries(): Record<string, string | undefined>;
11
+ }
12
+
13
+ export interface LoggerLike {
14
+ readonly level?: string;
15
+ log?(level: string, message: string, metadata?: Record<string, unknown>): void;
16
+ debug?(message: string, metadata?: Record<string, unknown>): void;
17
+ info?(message: string, metadata?: Record<string, unknown>): void;
18
+ warn?(message: string, metadata?: Record<string, unknown>): void;
19
+ error?(message: string, metadata?: Record<string, unknown>): void;
20
+ with?(bindings: Record<string, unknown>): LoggerLike;
21
+ }
22
+
23
+ export interface ViewDefinitionLike {
24
+ name?: string;
25
+ path?: string;
26
+ renderMode?: 'ssg' | 'ssr' | 'spa';
27
+ }
28
+
29
+ export type RequestTimeDocumentCacheStatus = 'miss' | 'hit' | 'stale';
30
+
31
+ export interface RequestTimeDocumentCacheMetadata {
32
+ readonly status: RequestTimeDocumentCacheStatus;
33
+ readonly documentPath: string;
34
+ }
35
+
36
+ export interface RenderedRequestTimeView {
37
+ readonly html: string;
38
+ readonly documentCache: RequestTimeDocumentCacheMetadata;
39
+ }
40
+
41
+ export interface SSRContextLike {
42
+ readonly url: URL;
43
+ readonly params: Record<string, string>;
44
+ readonly cookies: Record<string, string>;
45
+ readonly headers: Record<string, string>;
46
+ readonly auth: unknown;
47
+ readonly session: Record<string, unknown> | null;
48
+ readonly env: EnvAccessorLike;
49
+ readonly logger: LoggerLike;
50
+ readonly requestId?: string;
51
+ readonly now: () => Date;
52
+ }
53
+
54
+ export interface ModuleViewLike {
55
+ readonly definition?: ViewDefinitionLike;
56
+ readonly load?: (context: SSRContextLike) => Promise<unknown> | unknown;
57
+ }
58
+
59
+ export interface CompiledView {
60
+ readonly name: string;
61
+ readonly pathPattern: string;
62
+ readonly definition?: ViewDefinitionLike;
63
+ readonly load?: ModuleViewLike['load'];
64
+ readonly match: (pathname: string) => {
65
+ matched: boolean;
66
+ params: Record<string, string>;
67
+ };
68
+ }
69
+
70
+ export function compileViews(views: readonly ModuleViewLike[]): CompiledView[] {
71
+ const compiled: CompiledView[] = [];
72
+ for (const view of views) {
73
+ const pathPattern = normalizePath(view.definition?.path ?? '/');
74
+ compiled.push({
75
+ name: view.definition?.name ?? pathPattern,
76
+ pathPattern,
77
+ definition: view.definition,
78
+ load: view.load,
79
+ match: createPathMatcher(pathPattern),
80
+ });
81
+ }
82
+ return compiled;
83
+ }
84
+
85
+ export function matchView(
86
+ views: readonly CompiledView[],
87
+ pathname: string,
88
+ ): { view: CompiledView; params: Record<string, string> } | undefined {
89
+ for (const view of views) {
90
+ const matched = view.match(pathname);
91
+ if (matched.matched) {
92
+ return {
93
+ view,
94
+ params: matched.params,
95
+ };
96
+ }
97
+ }
98
+ return undefined;
99
+ }
100
+
101
+ export async function renderRequestTimeView(options: {
102
+ workspaceRoot?: string;
103
+ url: URL;
104
+ view: CompiledView;
105
+ params: Record<string, string>;
106
+ cookies: Record<string, string>;
107
+ headers: Record<string, string>;
108
+ auth: unknown;
109
+ session: Record<string, unknown> | null;
110
+ env: EnvAccessorLike;
111
+ logger: LoggerLike;
112
+ requestId?: string;
113
+ now?: () => Date;
114
+ }): Promise<RenderedRequestTimeView> {
115
+ const {
116
+ workspaceRoot,
117
+ url,
118
+ view,
119
+ params,
120
+ cookies,
121
+ headers,
122
+ auth,
123
+ session,
124
+ env,
125
+ logger,
126
+ requestId,
127
+ } = options;
128
+ const now = options.now ?? (() => new Date());
129
+ const document = await loadFrontendDocument(resolveWorkspaceRoot(workspaceRoot), url.pathname);
130
+
131
+ const viewData = view.load
132
+ ? await view.load({
133
+ url,
134
+ params,
135
+ cookies,
136
+ headers,
137
+ auth,
138
+ session,
139
+ env,
140
+ logger,
141
+ requestId,
142
+ now,
143
+ })
144
+ : null;
145
+
146
+ return {
147
+ html: injectViewState(document.html, {
148
+ name: view.name,
149
+ templatePath: view.pathPattern,
150
+ pathname: normalizePath(url.pathname),
151
+ params,
152
+ data: viewData ?? null,
153
+ requestId,
154
+ }),
155
+ documentCache: {
156
+ status: document.cacheStatus,
157
+ documentPath: document.path,
158
+ },
159
+ };
160
+ }
161
+
162
+ export function toHeaderRecord(
163
+ headers: Record<string, string | string[] | undefined>,
164
+ ): Record<string, string> {
165
+ const normalized: Record<string, string> = {};
166
+ for (const [key, value] of Object.entries(headers)) {
167
+ if (typeof value === 'string') {
168
+ normalized[key] = value;
169
+ continue;
170
+ }
171
+ if (Array.isArray(value)) {
172
+ normalized[key] = value.join(', ');
173
+ }
174
+ }
175
+ return normalized;
176
+ }
177
+
178
+ function createPathMatcher(pattern: string) {
179
+ const normalized = normalizePath(pattern);
180
+ const paramRegex = /:([A-Za-z0-9_]+)/g;
181
+ const regex = new RegExp(
182
+ '^' +
183
+ normalized
184
+ .replace(/\//g, '\\/')
185
+ .replace(paramRegex, (_segment, name) => `(?<${name}>[^/]+)`) +
186
+ '$',
187
+ );
188
+
189
+ return (pathname: string) => {
190
+ const pathToTest = normalizePath(pathname);
191
+ const match = regex.exec(pathToTest);
192
+ if (!match) {
193
+ return { matched: false, params: {} };
194
+ }
195
+ return {
196
+ matched: true,
197
+ params: (match.groups ?? {}) as Record<string, string>,
198
+ };
199
+ };
200
+ }
201
+
202
+ function normalizePath(value: string | undefined): string {
203
+ if (!value || value === '/') {
204
+ return '/';
205
+ }
206
+ const trimmed = value.endsWith('/') ? value.slice(0, -1) : value;
207
+ return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
208
+ }
209
+
210
+ function firstPathSegment(pathname: string): string | undefined {
211
+ const normalized = normalizePath(pathname);
212
+ if (normalized === '/') {
213
+ return undefined;
214
+ }
215
+ const parts = normalized.split('/').filter(Boolean);
216
+ return parts[0];
217
+ }
218
+
219
+ const documentTemplateCache = new Map<
220
+ string,
221
+ {
222
+ html: string;
223
+ ctimeMs: number;
224
+ mtimeMs: number;
225
+ size: number;
226
+ }
227
+ >();
228
+
229
+ async function loadFrontendDocument(
230
+ workspaceRoot: string,
231
+ pathname: string,
232
+ ): Promise<{
233
+ path: string;
234
+ html: string;
235
+ cacheStatus: RequestTimeDocumentCacheStatus;
236
+ }> {
237
+ const documentPath = await resolveFrontendDocumentPath(workspaceRoot, pathname);
238
+ const documentStats = await stat(documentPath);
239
+ const cached = documentTemplateCache.get(documentPath);
240
+
241
+ if (
242
+ cached &&
243
+ cached.ctimeMs === documentStats.ctimeMs &&
244
+ cached.mtimeMs === documentStats.mtimeMs &&
245
+ cached.size === documentStats.size
246
+ ) {
247
+ return {
248
+ path: documentPath,
249
+ html: cached.html,
250
+ cacheStatus: 'hit',
251
+ };
252
+ }
253
+
254
+ const html = await readTextFile(documentPath);
255
+ documentTemplateCache.set(documentPath, {
256
+ html,
257
+ ctimeMs: documentStats.ctimeMs,
258
+ mtimeMs: documentStats.mtimeMs,
259
+ size: documentStats.size,
260
+ });
261
+
262
+ return {
263
+ path: documentPath,
264
+ html,
265
+ cacheStatus: cached ? 'stale' : 'miss',
266
+ };
267
+ }
268
+
269
+ async function resolveFrontendDocumentPath(
270
+ workspaceRoot: string,
271
+ pathname: string,
272
+ ): Promise<string> {
273
+ const candidates = getFrontendDocumentCandidates(workspaceRoot, pathname);
274
+
275
+ for (const candidate of candidates) {
276
+ if (await fileExists(candidate)) {
277
+ return candidate;
278
+ }
279
+ }
280
+
281
+ throw new Error(
282
+ `Frontend document for ${normalizePath(pathname)} was not found. Checked ${candidates.join(', ')}.`,
283
+ );
284
+ }
285
+
286
+ function getFrontendDocumentCandidates(workspaceRoot: string, pathname: string): string[] {
287
+ const pageName = firstPathSegment(pathname) ?? 'home';
288
+ const relativeCandidates =
289
+ pageName === 'home'
290
+ ? [
291
+ path.join('pages', 'home', 'index.html'),
292
+ path.join('home', 'index.html'),
293
+ 'home.html',
294
+ 'index.html',
295
+ ]
296
+ : [
297
+ path.join('pages', pageName, 'index.html'),
298
+ path.join(pageName, 'index.html'),
299
+ `${pageName}.html`,
300
+ ];
301
+
302
+ const candidates = [
303
+ ...relativeCandidates.map((relativePath) =>
304
+ path.join(workspaceRoot, 'build', 'frontend', relativePath),
305
+ ),
306
+ ...relativeCandidates.map((relativePath) =>
307
+ path.join(workspaceRoot, 'dist', 'frontend', relativePath),
308
+ ),
309
+ ];
310
+
311
+ return Array.from(new Set(candidates));
312
+ }
313
+
314
+ async function fileExists(targetPath: string): Promise<boolean> {
315
+ try {
316
+ await access(targetPath);
317
+ return true;
318
+ } catch {
319
+ return false;
320
+ }
321
+ }
322
+
323
+ function injectViewState(
324
+ documentHtml: string,
325
+ state: {
326
+ name: string;
327
+ templatePath: string;
328
+ pathname: string;
329
+ params: Record<string, string>;
330
+ data: unknown;
331
+ requestId?: string;
332
+ },
333
+ ): string {
334
+ const payload = serializeJsonForHtml({
335
+ view: {
336
+ name: state.name,
337
+ path: state.templatePath,
338
+ pathname: state.pathname,
339
+ params: state.params,
340
+ },
341
+ data: state.data,
342
+ requestId: state.requestId ?? null,
343
+ });
344
+
345
+ const scriptTag = `<script type="application/json" id="webstir-view-state">${payload}</script>`;
346
+ const htmlWithBodyAttributes = documentHtml.replace(
347
+ /<body\b([^>]*)>/i,
348
+ (_match, existingAttributes) => {
349
+ const attrs = [
350
+ `data-webstir-view-name="${escapeHtmlAttribute(state.name)}"`,
351
+ `data-webstir-view-pathname="${escapeHtmlAttribute(state.pathname)}"`,
352
+ `data-webstir-view-template="${escapeHtmlAttribute(state.templatePath)}"`,
353
+ ];
354
+ return `<body${existingAttributes} ${attrs.join(' ')}>`;
355
+ },
356
+ );
357
+
358
+ if (/<\/body>/i.test(htmlWithBodyAttributes)) {
359
+ return htmlWithBodyAttributes.replace(/<\/body>/i, `${scriptTag}\n</body>`);
360
+ }
361
+
362
+ if (/<\/html>/i.test(htmlWithBodyAttributes)) {
363
+ return htmlWithBodyAttributes.replace(/<\/html>/i, `${scriptTag}\n</html>`);
364
+ }
365
+
366
+ return `${htmlWithBodyAttributes}\n${scriptTag}`;
367
+ }
368
+
369
+ function serializeJsonForHtml(value: unknown): string {
370
+ return JSON.stringify(value)
371
+ .replace(/</g, '\\u003c')
372
+ .replace(/>/g, '\\u003e')
373
+ .replace(/&/g, '\\u0026')
374
+ .replace(/\u2028/g, '\\u2028')
375
+ .replace(/\u2029/g, '\\u2029');
376
+ }
377
+
378
+ function escapeHtmlAttribute(value: string): string {
379
+ return value
380
+ .replaceAll('&', '&amp;')
381
+ .replaceAll('"', '&quot;')
382
+ .replaceAll("'", '&#39;')
383
+ .replaceAll('<', '&lt;')
384
+ .replaceAll('>', '&gt;');
385
+ }
@@ -4,78 +4,82 @@ import { fileURLToPath } from 'node:url';
4
4
  import type { ModuleAsset } from '@webstir-io/module-contract';
5
5
 
6
6
  export async function getBackendScaffoldAssets(): Promise<readonly ModuleAsset[]> {
7
- const here = path.dirname(fileURLToPath(import.meta.url));
8
- const packageRoot = path.resolve(here, '..', '..');
9
- const templatesRoot = path.join(packageRoot, 'templates', 'backend');
7
+ const here = path.dirname(fileURLToPath(import.meta.url));
8
+ const packageRoot = path.resolve(here, '..', '..');
9
+ const templatesRoot = path.join(packageRoot, 'templates', 'backend');
10
10
 
11
- return [
12
- {
13
- sourcePath: path.join(templatesRoot, 'tsconfig.json'),
14
- targetPath: path.join('src', 'backend', 'tsconfig.json')
15
- },
16
- {
17
- sourcePath: path.join(templatesRoot, 'index.ts'),
18
- targetPath: path.join('src', 'backend', 'index.ts')
19
- },
20
- {
21
- sourcePath: path.join(templatesRoot, 'server', 'fastify.ts'),
22
- targetPath: path.join('src', 'backend', 'server', 'fastify.ts')
23
- },
24
- {
25
- sourcePath: path.join(templatesRoot, 'module.ts'),
26
- targetPath: path.join('src', 'backend', 'module.ts')
27
- },
28
- {
29
- sourcePath: path.join(templatesRoot, 'auth', 'adapter.ts'),
30
- targetPath: path.join('src', 'backend', 'auth', 'adapter.ts')
31
- },
32
- {
33
- sourcePath: path.join(templatesRoot, 'observability', 'logger.ts'),
34
- targetPath: path.join('src', 'backend', 'observability', 'logger.ts')
35
- },
36
- {
37
- sourcePath: path.join(templatesRoot, 'observability', 'metrics.ts'),
38
- targetPath: path.join('src', 'backend', 'observability', 'metrics.ts')
39
- },
40
- {
41
- sourcePath: path.join(templatesRoot, 'env.ts'),
42
- targetPath: path.join('src', 'backend', 'env.ts')
43
- },
44
- {
45
- sourcePath: path.join(templatesRoot, 'functions', 'hello', 'index.ts'),
46
- targetPath: path.join('src', 'backend', 'functions', 'hello', 'index.ts')
47
- },
48
- {
49
- sourcePath: path.join(templatesRoot, 'jobs', 'nightly', 'index.ts'),
50
- targetPath: path.join('src', 'backend', 'jobs', 'nightly', 'index.ts')
51
- },
52
- {
53
- sourcePath: path.join(templatesRoot, 'jobs', 'runtime.ts'),
54
- targetPath: path.join('src', 'backend', 'jobs', 'runtime.ts')
55
- },
56
- {
57
- sourcePath: path.join(templatesRoot, 'jobs', 'scheduler.ts'),
58
- targetPath: path.join('src', 'backend', 'jobs', 'scheduler.ts')
59
- },
60
- {
61
- sourcePath: path.join(templatesRoot, 'db', 'connection.ts'),
62
- targetPath: path.join('src', 'backend', 'db', 'connection.ts')
63
- },
64
- {
65
- sourcePath: path.join(templatesRoot, 'db', 'migrate.ts'),
66
- targetPath: path.join('src', 'backend', 'db', 'migrate.ts')
67
- },
68
- {
69
- sourcePath: path.join(templatesRoot, 'db', 'migrations', '0001-example.ts'),
70
- targetPath: path.join('src', 'backend', 'db', 'migrations', '0001-example.ts')
71
- },
72
- {
73
- sourcePath: path.join(templatesRoot, 'db', 'types.d.ts'),
74
- targetPath: path.join('src', 'backend', 'db', 'types.d.ts')
75
- },
76
- {
77
- sourcePath: path.join(templatesRoot, '.env.example'),
78
- targetPath: path.join('.env.example')
79
- }
80
- ];
11
+ return [
12
+ {
13
+ sourcePath: path.join(templatesRoot, 'tsconfig.json'),
14
+ targetPath: path.join('src', 'backend', 'tsconfig.json'),
15
+ },
16
+ {
17
+ sourcePath: path.join(templatesRoot, 'index.ts'),
18
+ targetPath: path.join('src', 'backend', 'index.ts'),
19
+ },
20
+ {
21
+ sourcePath: path.join(templatesRoot, 'module.ts'),
22
+ targetPath: path.join('src', 'backend', 'module.ts'),
23
+ },
24
+ {
25
+ sourcePath: path.join(templatesRoot, 'auth', 'adapter.ts'),
26
+ targetPath: path.join('src', 'backend', 'auth', 'adapter.ts'),
27
+ },
28
+ {
29
+ sourcePath: path.join(templatesRoot, 'observability', 'logger.ts'),
30
+ targetPath: path.join('src', 'backend', 'observability', 'logger.ts'),
31
+ },
32
+ {
33
+ sourcePath: path.join(templatesRoot, 'observability', 'metrics.ts'),
34
+ targetPath: path.join('src', 'backend', 'observability', 'metrics.ts'),
35
+ },
36
+ {
37
+ sourcePath: path.join(templatesRoot, 'env.ts'),
38
+ targetPath: path.join('src', 'backend', 'env.ts'),
39
+ },
40
+ {
41
+ sourcePath: path.join(templatesRoot, 'session', 'store.ts'),
42
+ targetPath: path.join('src', 'backend', 'session', 'store.ts'),
43
+ },
44
+ {
45
+ sourcePath: path.join(templatesRoot, 'session', 'sqlite.ts'),
46
+ targetPath: path.join('src', 'backend', 'session', 'sqlite.ts'),
47
+ },
48
+ {
49
+ sourcePath: path.join(templatesRoot, 'functions', 'hello', 'index.ts'),
50
+ targetPath: path.join('src', 'backend', 'functions', 'hello', 'index.ts'),
51
+ },
52
+ {
53
+ sourcePath: path.join(templatesRoot, 'jobs', 'nightly', 'index.ts'),
54
+ targetPath: path.join('src', 'backend', 'jobs', 'nightly', 'index.ts'),
55
+ },
56
+ {
57
+ sourcePath: path.join(templatesRoot, 'jobs', 'runtime.ts'),
58
+ targetPath: path.join('src', 'backend', 'jobs', 'runtime.ts'),
59
+ },
60
+ {
61
+ sourcePath: path.join(templatesRoot, 'jobs', 'scheduler.ts'),
62
+ targetPath: path.join('src', 'backend', 'jobs', 'scheduler.ts'),
63
+ },
64
+ {
65
+ sourcePath: path.join(templatesRoot, 'db', 'connection.ts'),
66
+ targetPath: path.join('src', 'backend', 'db', 'connection.ts'),
67
+ },
68
+ {
69
+ sourcePath: path.join(templatesRoot, 'db', 'migrate.ts'),
70
+ targetPath: path.join('src', 'backend', 'db', 'migrate.ts'),
71
+ },
72
+ {
73
+ sourcePath: path.join(templatesRoot, 'db', 'migrations', '0001-example.ts'),
74
+ targetPath: path.join('src', 'backend', 'db', 'migrations', '0001-example.ts'),
75
+ },
76
+ {
77
+ sourcePath: path.join(templatesRoot, 'db', 'types.d.ts'),
78
+ targetPath: path.join('src', 'backend', 'db', 'types.d.ts'),
79
+ },
80
+ {
81
+ sourcePath: path.join(templatesRoot, '.env.example'),
82
+ targetPath: path.join('.env.example'),
83
+ },
84
+ ];
81
85
  }
@@ -1,14 +1,13 @@
1
1
  const GLOBAL_KEY = '__webstirBackendTestContext__';
2
2
  export function setBackendTestContext(context) {
3
- const target = globalThis;
4
- if (context) {
5
- target[GLOBAL_KEY] = context;
6
- }
7
- else {
8
- delete target[GLOBAL_KEY];
9
- }
3
+ const target = globalThis;
4
+ if (context) {
5
+ target[GLOBAL_KEY] = context;
6
+ } else {
7
+ delete target[GLOBAL_KEY];
8
+ }
10
9
  }
11
10
  export function getBackendTestContext() {
12
- const target = globalThis;
13
- return (target[GLOBAL_KEY] ?? null);
11
+ const target = globalThis;
12
+ return target[GLOBAL_KEY] ?? null;
14
13
  }
@@ -1,17 +1,17 @@
1
1
  import type { BackendTestContext } from './types.js';
2
2
 
3
- const GLOBAL_KEY = '__webstirBackendTestContext__';
3
+ const GLOBAL_KEY = Symbol.for('webstir.backendTestContext');
4
4
 
5
5
  export function setBackendTestContext(context: BackendTestContext | null): void {
6
- const target = globalThis as Record<string, unknown>;
7
- if (context) {
8
- target[GLOBAL_KEY] = context;
9
- } else {
10
- delete target[GLOBAL_KEY];
11
- }
6
+ const target = globalThis as Record<string | symbol, unknown>;
7
+ if (context) {
8
+ target[GLOBAL_KEY] = context;
9
+ } else {
10
+ delete target[GLOBAL_KEY];
11
+ }
12
12
  }
13
13
 
14
14
  export function getBackendTestContext(): BackendTestContext | null {
15
- const target = globalThis as Record<string, unknown>;
16
- return (target[GLOBAL_KEY] ?? null) as BackendTestContext | null;
15
+ const target = globalThis as Record<string | symbol, unknown>;
16
+ return (target[GLOBAL_KEY] ?? null) as BackendTestContext | null;
17
17
  }
@@ -1,6 +1,17 @@
1
1
  import { getBackendTestContext, setBackendTestContext } from './context.js';
2
- import type { BackendTestCallback, BackendTestHarness, BackendTestHarnessOptions } from './types.js';
3
- export type { BackendTestCallback, BackendTestContext, BackendTestHarness, BackendTestHarnessOptions } from './types.js';
2
+ import type {
3
+ BackendTestCallback,
4
+ BackendTestHarness,
5
+ BackendTestHarnessOptions,
6
+ } from './types.js';
7
+ export type {
8
+ BackendTestCallback,
9
+ BackendTestContext,
10
+ BackendTestHarness,
11
+ BackendTestHarnessOptions,
12
+ } from './types.js';
4
13
  export { getBackendTestContext, setBackendTestContext };
5
- export declare function createBackendTestHarness(options?: BackendTestHarnessOptions): Promise<BackendTestHarness>;
14
+ export declare function createBackendTestHarness(
15
+ options?: BackendTestHarnessOptions,
16
+ ): Promise<BackendTestHarness>;
6
17
  export declare function backendTest(name: string, callback: BackendTestCallback): void;