@webstir-io/webstir-backend 0.1.15

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 (79) hide show
  1. package/README.md +427 -0
  2. package/dist/build/artifacts.d.ts +113 -0
  3. package/dist/build/artifacts.js +53 -0
  4. package/dist/build/entries.d.ts +1 -0
  5. package/dist/build/entries.js +17 -0
  6. package/dist/build/pipeline.d.ts +31 -0
  7. package/dist/build/pipeline.js +424 -0
  8. package/dist/cache/diff.d.ts +4 -0
  9. package/dist/cache/diff.js +114 -0
  10. package/dist/cache/reporters.d.ts +12 -0
  11. package/dist/cache/reporters.js +23 -0
  12. package/dist/diagnostics/summary.d.ts +6 -0
  13. package/dist/diagnostics/summary.js +27 -0
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.js +2 -0
  16. package/dist/manifest/pipeline.d.ts +13 -0
  17. package/dist/manifest/pipeline.js +224 -0
  18. package/dist/provider.d.ts +2 -0
  19. package/dist/provider.js +101 -0
  20. package/dist/scaffold/assets.d.ts +2 -0
  21. package/dist/scaffold/assets.js +77 -0
  22. package/dist/testing/context.d.ts +3 -0
  23. package/dist/testing/context.js +14 -0
  24. package/dist/testing/index.d.ts +6 -0
  25. package/dist/testing/index.js +208 -0
  26. package/dist/testing/types.d.ts +28 -0
  27. package/dist/testing/types.js +1 -0
  28. package/dist/watch.d.ts +8 -0
  29. package/dist/watch.js +159 -0
  30. package/dist/workspace.d.ts +4 -0
  31. package/dist/workspace.js +15 -0
  32. package/package.json +74 -0
  33. package/scripts/publish.sh +99 -0
  34. package/scripts/smoke.mjs +241 -0
  35. package/scripts/update-contract.sh +122 -0
  36. package/src/build/artifacts.ts +67 -0
  37. package/src/build/entries.ts +19 -0
  38. package/src/build/pipeline.ts +507 -0
  39. package/src/cache/diff.ts +128 -0
  40. package/src/cache/reporters.ts +41 -0
  41. package/src/diagnostics/summary.ts +32 -0
  42. package/src/index.ts +2 -0
  43. package/src/manifest/pipeline.ts +270 -0
  44. package/src/provider.ts +124 -0
  45. package/src/scaffold/assets.ts +81 -0
  46. package/src/testing/context.d.ts +3 -0
  47. package/src/testing/context.js +14 -0
  48. package/src/testing/context.ts +17 -0
  49. package/src/testing/index.d.ts +6 -0
  50. package/src/testing/index.js +208 -0
  51. package/src/testing/index.ts +252 -0
  52. package/src/testing/types.d.ts +28 -0
  53. package/src/testing/types.js +1 -0
  54. package/src/testing/types.ts +32 -0
  55. package/src/watch.ts +177 -0
  56. package/src/workspace.ts +22 -0
  57. package/templates/backend/.env.example +13 -0
  58. package/templates/backend/auth/adapter.ts +160 -0
  59. package/templates/backend/db/connection.ts +99 -0
  60. package/templates/backend/db/migrate.ts +231 -0
  61. package/templates/backend/db/migrations/0001-example.ts +17 -0
  62. package/templates/backend/db/types.d.ts +2 -0
  63. package/templates/backend/env.ts +174 -0
  64. package/templates/backend/functions/hello/index.ts +29 -0
  65. package/templates/backend/index.ts +532 -0
  66. package/templates/backend/jobs/nightly/index.ts +28 -0
  67. package/templates/backend/jobs/runtime.ts +103 -0
  68. package/templates/backend/jobs/scheduler.ts +193 -0
  69. package/templates/backend/module.ts +87 -0
  70. package/templates/backend/observability/logger.ts +24 -0
  71. package/templates/backend/observability/metrics.ts +78 -0
  72. package/templates/backend/server/fastify.ts +288 -0
  73. package/templates/backend/tsconfig.json +19 -0
  74. package/tests/cacheReporter.test.js +89 -0
  75. package/tests/envLoader.test.js +64 -0
  76. package/tests/integration.test.js +108 -0
  77. package/tests/manifest.test.js +159 -0
  78. package/tests/watch.test.js +100 -0
  79. package/tsconfig.json +27 -0
@@ -0,0 +1,32 @@
1
+ import type { ModuleDiagnostic } from '@webstir-io/module-contract';
2
+
3
+ export function pushEntryBucketSummary(diagnostics: ModuleDiagnostic[], entryPoints: readonly string[]): void {
4
+ try {
5
+ const server = entryPoints.filter(
6
+ (p) => p === 'index.js' || /(^|\/)index\.js$/.test(p) && !/^(functions|jobs)\//.test(p)
7
+ ).length;
8
+ const functionsCount = entryPoints.filter((p) => p.startsWith('functions/')).length;
9
+ const jobsCount = entryPoints.filter((p) => p.startsWith('jobs/')).length;
10
+ diagnostics.push({
11
+ severity: 'info',
12
+ message: `[webstir-backend] entries by bucket: server=${server} functions=${functionsCount} jobs=${jobsCount}`
13
+ });
14
+ } catch {
15
+ // best-effort only
16
+ }
17
+ }
18
+
19
+ type Severity = 'info' | 'warn' | 'error';
20
+
21
+ export function normalizeLogLevel(value: unknown): Severity {
22
+ if (typeof value !== 'string') return 'info';
23
+ const v = value.toLowerCase();
24
+ if (v === 'error' || v === 'warn' || v === 'info') return v;
25
+ return 'info';
26
+ }
27
+
28
+ export function filterDiagnostics(list: readonly ModuleDiagnostic[], min: Severity): readonly ModuleDiagnostic[] {
29
+ const rank = (s: Severity) => (s === 'error' ? 3 : s === 'warn' ? 2 : 1);
30
+ const threshold = rank(min);
31
+ return list.filter((d) => rank(d.severity as Severity) >= threshold);
32
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { backendProvider } from './provider.js';
2
+ export { startBackendWatch } from './watch.js';
@@ -0,0 +1,270 @@
1
+ import path from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { pathToFileURL } from 'node:url';
5
+
6
+ import { moduleManifestSchema, CONTRACT_VERSION } from '@webstir-io/module-contract';
7
+ import type { ModuleDefinition, ModuleDiagnostic, ModuleManifest } from '@webstir-io/module-contract';
8
+
9
+ interface WorkspacePackageJson {
10
+ readonly name?: string;
11
+ readonly version?: string;
12
+ readonly webstir?: {
13
+ readonly moduleManifest?: WorkspaceModuleConfig;
14
+ };
15
+ }
16
+
17
+ type WorkspaceModuleConfig = Partial<ModuleManifest> & {
18
+ readonly contractVersion?: string;
19
+ readonly capabilities?: ModuleManifest['capabilities'];
20
+ };
21
+
22
+ export interface LoadManifestOptions {
23
+ readonly workspaceRoot: string;
24
+ readonly buildRoot: string;
25
+ readonly entryPoints: readonly string[];
26
+ readonly diagnostics: ModuleDiagnostic[];
27
+ }
28
+
29
+ export async function loadBackendModuleManifest(options: LoadManifestOptions): Promise<ModuleManifest> {
30
+ const { workspaceRoot, buildRoot, entryPoints, diagnostics } = options;
31
+ const pkgPath = path.join(workspaceRoot, 'package.json');
32
+ let workspacePackage: WorkspacePackageJson | undefined;
33
+
34
+ try {
35
+ const raw = await readFile(pkgPath, 'utf8');
36
+ workspacePackage = JSON.parse(raw) as WorkspacePackageJson;
37
+ } catch (error) {
38
+ const err = error as NodeJS.ErrnoException;
39
+ // Missing package.json is expected in some temporary workspaces; avoid noisy warnings.
40
+ if (err.code !== 'ENOENT') {
41
+ diagnostics.push({
42
+ severity: 'warn',
43
+ message: `[webstir-backend] unable to read ${pkgPath}: ${err.message}. Using defaults.`
44
+ });
45
+ }
46
+ }
47
+
48
+ const moduleConfig = workspacePackage?.webstir?.moduleManifest ?? {};
49
+
50
+ let manifestCandidate: ModuleManifest = {
51
+ contractVersion: typeof moduleConfig.contractVersion === 'string' ? moduleConfig.contractVersion : CONTRACT_VERSION,
52
+ name: typeof moduleConfig.name === 'string' ? moduleConfig.name : deriveModuleName(workspacePackage, workspaceRoot),
53
+ version: typeof moduleConfig.version === 'string' ? moduleConfig.version : deriveModuleVersion(workspacePackage),
54
+ kind: 'backend',
55
+ capabilities: Array.isArray(moduleConfig.capabilities) ? moduleConfig.capabilities : [],
56
+ routes: moduleConfig.routes ?? [],
57
+ views: moduleConfig.views ?? [],
58
+ jobs: moduleConfig.jobs ?? [],
59
+ events: moduleConfig.events ?? [],
60
+ services: moduleConfig.services ?? [],
61
+ init: moduleConfig.init,
62
+ dispose: moduleConfig.dispose
63
+ };
64
+
65
+ const definition = await loadModuleDefinition(buildRoot, diagnostics);
66
+ if (definition) {
67
+ const definitionManifest = definition.manifest ?? ({} as ModuleManifest);
68
+ const routesFromDefinition = definition.routes?.map((route) => route.definition);
69
+ const viewsFromDefinition = definition.views?.map((view) => view.definition);
70
+
71
+ const mergedCapabilities = Array.from(
72
+ new Set([...(manifestCandidate.capabilities ?? []), ...(definitionManifest.capabilities ?? [])])
73
+ );
74
+
75
+ manifestCandidate = {
76
+ ...manifestCandidate,
77
+ ...definitionManifest,
78
+ capabilities: mergedCapabilities,
79
+ routes: routesFromDefinition ?? definitionManifest.routes ?? manifestCandidate.routes ?? [],
80
+ views: viewsFromDefinition ?? definitionManifest.views ?? manifestCandidate.views ?? [],
81
+ jobs: definitionManifest.jobs ?? manifestCandidate.jobs ?? [],
82
+ events: definitionManifest.events ?? manifestCandidate.events ?? [],
83
+ services: definitionManifest.services ?? manifestCandidate.services ?? [],
84
+ init: definitionManifest.init ?? manifestCandidate.init,
85
+ dispose: definitionManifest.dispose ?? manifestCandidate.dispose
86
+ };
87
+ }
88
+
89
+ const validation = moduleManifestSchema.safeParse(manifestCandidate);
90
+ if (!validation.success) {
91
+ const problems = validation.error.issues
92
+ .map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
93
+ .join('; ');
94
+ diagnostics.push({
95
+ severity: 'error',
96
+ message: `[webstir-backend] module manifest validation failed (${problems}). Falling back to defaults.`
97
+ });
98
+ return {
99
+ contractVersion: CONTRACT_VERSION,
100
+ name: deriveModuleName(workspacePackage, workspaceRoot),
101
+ version: deriveModuleVersion(workspacePackage),
102
+ kind: 'backend',
103
+ capabilities: [],
104
+ routes: [],
105
+ views: [],
106
+ jobs: [],
107
+ events: [],
108
+ services: []
109
+ };
110
+ }
111
+
112
+ const manifest = validation.data;
113
+
114
+ try {
115
+ const normalizePath = (p: unknown) => {
116
+ let s = typeof p === 'string' ? p : '';
117
+ if (!s.startsWith('/')) s = '/' + s;
118
+ s = s.replace(/\/+/, '/');
119
+ if (s.length > 1 && s.endsWith('/')) s = s.slice(0, -1);
120
+ return s;
121
+ };
122
+ const seen = new Map<string, number>();
123
+ for (const r of manifest.routes ?? []) {
124
+ const method = typeof (r as any).method === 'string' ? (r as any).method.toUpperCase() : '';
125
+ const pathKey = normalizePath((r as any).path);
126
+ const key = `${method} ${pathKey}`;
127
+ seen.set(key, (seen.get(key) ?? 0) + 1);
128
+ }
129
+ const dups = Array.from(seen.entries()).filter(([, count]) => count > 1);
130
+ if (dups.length > 0) {
131
+ const list = dups.map(([k, c]) => `${k} (${c}x)`).join(', ');
132
+ diagnostics.push({ severity: 'warn', message: `[webstir-backend] duplicate route definitions: ${list}` });
133
+ }
134
+ } catch {
135
+ // best-effort only
136
+ }
137
+
138
+ if (manifest.routes?.length && entryPoints.length === 0) {
139
+ diagnostics.push({
140
+ severity: 'warn',
141
+ message: '[webstir-backend] module manifest defines routes but no entry points were built. Ensure backend compilation produced handlers.'
142
+ });
143
+ }
144
+
145
+ try {
146
+ const jobs = Array.isArray(manifest.jobs) ? manifest.jobs : [];
147
+ const events = Array.isArray(manifest.events) ? manifest.events : [];
148
+ const services = Array.isArray(manifest.services) ? manifest.services : [];
149
+ if (jobs.length + events.length + services.length > 0) {
150
+ diagnostics.push({
151
+ severity: 'info',
152
+ message: `[webstir-backend] manifest jobs=${jobs.length} events=${events.length} services=${services.length}`
153
+ });
154
+ }
155
+
156
+ const noSchedule = jobs.filter((j: any) => j && typeof j.name === 'string' && (j.schedule === undefined || j.schedule === null));
157
+ if (noSchedule.length > 0) {
158
+ const MAX_LIST = 10;
159
+ const names = noSchedule.map((j: any) => j.name).slice(0, MAX_LIST).join(', ');
160
+ const omitted = noSchedule.length > MAX_LIST ? ` (+${noSchedule.length - MAX_LIST} more)` : '';
161
+ diagnostics.push({
162
+ severity: 'warn',
163
+ message: `[webstir-backend] jobs without schedules: ${names}${omitted}`
164
+ });
165
+ }
166
+ } catch {
167
+ // best-effort only
168
+ }
169
+
170
+ try {
171
+ const routes = Array.isArray(manifest.routes) ? manifest.routes.length : 0;
172
+ const views = Array.isArray(manifest.views) ? manifest.views.length : 0;
173
+ const caps = Array.isArray(manifest.capabilities) && manifest.capabilities.length > 0 ? ` [${manifest.capabilities.join(', ')}]` : '';
174
+ diagnostics.push({ severity: 'info', message: `[webstir-backend] manifest routes=${routes} views=${views}${caps}` });
175
+ } catch {
176
+ // ignore
177
+ }
178
+
179
+ return manifest;
180
+ }
181
+
182
+ async function loadModuleDefinition(
183
+ buildRoot: string,
184
+ diagnostics: ModuleDiagnostic[]
185
+ ): Promise<ModuleDefinition | undefined> {
186
+ const candidates = [
187
+ path.join(buildRoot, 'module.js'),
188
+ path.join(buildRoot, 'module.mjs'),
189
+ path.join(buildRoot, 'module/index.js'),
190
+ path.join(buildRoot, 'module/index.mjs')
191
+ ];
192
+
193
+ for (const fullPath of candidates) {
194
+ if (!existsSync(fullPath)) {
195
+ continue;
196
+ }
197
+
198
+ try {
199
+ const moduleUrl = `${pathToFileURL(fullPath).href}?t=${Date.now()}`;
200
+ const imported = (await import(moduleUrl)) as Record<string, unknown>;
201
+ const definitionCandidate = extractModuleDefinition(imported);
202
+ if (isModuleDefinition(definitionCandidate)) {
203
+ return definitionCandidate;
204
+ }
205
+ diagnostics.push({
206
+ severity: 'warn',
207
+ message: `[webstir-backend] module definition at ${fullPath} does not export a createModule() definition.`
208
+ });
209
+ } catch (error) {
210
+ diagnostics.push({
211
+ severity: 'warn',
212
+ message: `[webstir-backend] failed to load module definition from ${fullPath}: ${(error as Error).message}`
213
+ });
214
+ }
215
+ }
216
+
217
+ return undefined;
218
+ }
219
+
220
+ function extractModuleDefinition(exports: Record<string, unknown>): unknown {
221
+ const keys = ['module', 'moduleDefinition', 'default', 'backendModule'];
222
+ for (const key of keys) {
223
+ if (key in exports) {
224
+ const value = exports[key as keyof typeof exports];
225
+ if (value !== null && value !== undefined) {
226
+ return value;
227
+ }
228
+ }
229
+ }
230
+ return undefined;
231
+ }
232
+
233
+ function isModuleDefinition(value: unknown): value is ModuleDefinition {
234
+ return typeof value === 'object' && value !== null && 'manifest' in (value as Record<string, unknown>);
235
+ }
236
+
237
+ function deriveModuleName(pkg: WorkspacePackageJson | undefined, workspaceRoot: string): string {
238
+ if (typeof pkg?.webstir?.moduleManifest?.name === 'string' && pkg.webstir.moduleManifest.name.length > 0) {
239
+ return pkg.webstir.moduleManifest.name;
240
+ }
241
+ if (typeof pkg?.name === 'string' && pkg.name.length > 0) {
242
+ return pkg.name;
243
+ }
244
+ return `backend-module-${path.basename(workspaceRoot)}`;
245
+ }
246
+
247
+ function deriveModuleVersion(pkg: WorkspacePackageJson | undefined): string {
248
+ if (typeof pkg?.webstir?.moduleManifest?.version === 'string' && pkg.webstir.moduleManifest.version.length > 0) {
249
+ return pkg.webstir.moduleManifest.version;
250
+ }
251
+ if (typeof pkg?.version === 'string' && pkg.version.length > 0) {
252
+ return pkg.version;
253
+ }
254
+ return '0.0.0';
255
+ }
256
+
257
+ export async function summarizeBuiltManifest(
258
+ buildRoot: string
259
+ ): Promise<{ routes: number; views: number; capabilities?: readonly string[] } | undefined> {
260
+ const definition = await loadModuleDefinition(buildRoot, []);
261
+ if (!definition || !definition.manifest) {
262
+ return undefined;
263
+ }
264
+ const manifest = definition.manifest;
265
+ return {
266
+ routes: Array.isArray(manifest.routes) ? manifest.routes.length : 0,
267
+ views: Array.isArray(manifest.views) ? manifest.views.length : 0,
268
+ capabilities: manifest.capabilities
269
+ };
270
+ }
@@ -0,0 +1,124 @@
1
+ import path from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+
4
+ import type {
5
+ ModuleBuildOptions,
6
+ ModuleBuildResult,
7
+ ModuleDiagnostic,
8
+ ModuleManifest,
9
+ ModuleProvider
10
+ } from '@webstir-io/module-contract';
11
+
12
+ import { collectArtifacts, createBuildManifest } from './build/artifacts.js';
13
+ import { buildSupportFile, runBackendBuildPipeline } from './build/pipeline.js';
14
+ import { loadBackendModuleManifest } from './manifest/pipeline.js';
15
+ import { createCacheReporter } from './cache/reporters.js';
16
+ import { pushEntryBucketSummary, normalizeLogLevel, filterDiagnostics } from './diagnostics/summary.js';
17
+ import { getBackendScaffoldAssets } from './scaffold/assets.js';
18
+ import { normalizeMode, resolveWorkspacePaths } from './workspace.js';
19
+
20
+ import packageJson from '../package.json' with { type: 'json' };
21
+
22
+ interface PackageJson {
23
+ readonly name: string;
24
+ readonly version: string;
25
+ readonly engines?: {
26
+ readonly node?: string;
27
+ };
28
+ }
29
+
30
+ const pkg = packageJson as PackageJson;
31
+
32
+ export const backendProvider: ModuleProvider = {
33
+ metadata: {
34
+ id: pkg.name ?? '@webstir-io/webstir-backend',
35
+ kind: 'backend',
36
+ version: pkg.version ?? '0.0.0',
37
+ compatibility: {
38
+ minCliVersion: '0.1.0',
39
+ nodeRange: pkg.engines?.node ?? '>=20.18.1'
40
+ }
41
+ },
42
+ resolveWorkspace(options) {
43
+ return resolveWorkspacePaths(options.workspaceRoot);
44
+ },
45
+ async build(options) {
46
+ const paths = resolveWorkspacePaths(options.workspaceRoot);
47
+ const tsconfigPath = path.join(paths.sourceRoot, 'tsconfig.json');
48
+
49
+ const diagnostics: ModuleDiagnostic[] = [];
50
+
51
+ const incremental = options.incremental === true;
52
+ const env = options.env ?? {};
53
+ const mode = normalizeMode(env.WEBSTIR_MODULE_MODE);
54
+ console.info(`[webstir-backend] ${mode}:start`);
55
+
56
+ const { entryPoints, outputs, includePublishSourcemaps } = await runBackendBuildPipeline({
57
+ sourceRoot: paths.sourceRoot,
58
+ buildRoot: paths.buildRoot,
59
+ tsconfigPath,
60
+ mode,
61
+ env,
62
+ incremental,
63
+ diagnostics
64
+ });
65
+
66
+ const artifacts = await collectArtifacts(paths.buildRoot, includePublishSourcemaps);
67
+ const envSource = path.join(paths.sourceRoot, 'env.ts');
68
+ if (existsSync(envSource)) {
69
+ try {
70
+ await buildSupportFile({
71
+ sourceFile: envSource,
72
+ sourceRoot: paths.sourceRoot,
73
+ buildRoot: paths.buildRoot,
74
+ tsconfigPath,
75
+ mode,
76
+ env,
77
+ diagnostics
78
+ });
79
+ } catch {
80
+ // env compilation errors are already captured in diagnostics
81
+ }
82
+ }
83
+ const moduleManifest = await loadBackendModuleManifest({
84
+ workspaceRoot: options.workspaceRoot,
85
+ buildRoot: paths.buildRoot,
86
+ entryPoints,
87
+ diagnostics
88
+ });
89
+ const manifest = createBuildManifest(paths.buildRoot, artifacts, diagnostics, moduleManifest);
90
+
91
+ console.info(`[webstir-backend] ${mode}:complete (entries=${manifest.entryPoints.length})`);
92
+ diagnostics.push({ severity: 'info', message: `[webstir-backend] ${mode}:built entries=${manifest.entryPoints.length}` });
93
+ const cacheReporter = createCacheReporter({
94
+ workspaceRoot: options.workspaceRoot,
95
+ buildRoot: paths.buildRoot,
96
+ env,
97
+ diagnostics
98
+ });
99
+ try {
100
+ await cacheReporter.diffOutputs(outputs, mode);
101
+ } catch {
102
+ // ignore cache errors
103
+ }
104
+ try {
105
+ await cacheReporter.diffManifest(moduleManifest);
106
+ } catch {
107
+ // ignore cache errors
108
+ }
109
+ // Optionally filter diagnostics by severity for orchestrator consumption
110
+ const minLevel = normalizeLogLevel(env.WEBSTIR_BACKEND_LOG_LEVEL);
111
+ const filteredDiagnostics = filterDiagnostics(manifest.diagnostics, minLevel);
112
+
113
+ return {
114
+ artifacts,
115
+ manifest: {
116
+ ...manifest,
117
+ diagnostics: filteredDiagnostics
118
+ }
119
+ };
120
+ },
121
+ async getScaffoldAssets() {
122
+ return await getBackendScaffoldAssets();
123
+ }
124
+ };
@@ -0,0 +1,81 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+
4
+ import type { ModuleAsset } from '@webstir-io/module-contract';
5
+
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');
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
+ ];
81
+ }
@@ -0,0 +1,3 @@
1
+ import type { BackendTestContext } from './types.js';
2
+ export declare function setBackendTestContext(context: BackendTestContext | null): void;
3
+ export declare function getBackendTestContext(): BackendTestContext | null;
@@ -0,0 +1,14 @@
1
+ const GLOBAL_KEY = '__webstirBackendTestContext__';
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
+ }
10
+ }
11
+ export function getBackendTestContext() {
12
+ const target = globalThis;
13
+ return (target[GLOBAL_KEY] ?? null);
14
+ }
@@ -0,0 +1,17 @@
1
+ import type { BackendTestContext } from './types.js';
2
+
3
+ const GLOBAL_KEY = '__webstirBackendTestContext__';
4
+
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
+ }
12
+ }
13
+
14
+ export function getBackendTestContext(): BackendTestContext | null {
15
+ const target = globalThis as Record<string, unknown>;
16
+ return (target[GLOBAL_KEY] ?? null) as BackendTestContext | null;
17
+ }
@@ -0,0 +1,6 @@
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';
4
+ export { getBackendTestContext, setBackendTestContext };
5
+ export declare function createBackendTestHarness(options?: BackendTestHarnessOptions): Promise<BackendTestHarness>;
6
+ export declare function backendTest(name: string, callback: BackendTestCallback): void;