@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.
- package/README.md +427 -0
- package/dist/build/artifacts.d.ts +113 -0
- package/dist/build/artifacts.js +53 -0
- package/dist/build/entries.d.ts +1 -0
- package/dist/build/entries.js +17 -0
- package/dist/build/pipeline.d.ts +31 -0
- package/dist/build/pipeline.js +424 -0
- package/dist/cache/diff.d.ts +4 -0
- package/dist/cache/diff.js +114 -0
- package/dist/cache/reporters.d.ts +12 -0
- package/dist/cache/reporters.js +23 -0
- package/dist/diagnostics/summary.d.ts +6 -0
- package/dist/diagnostics/summary.js +27 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/manifest/pipeline.d.ts +13 -0
- package/dist/manifest/pipeline.js +224 -0
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +101 -0
- package/dist/scaffold/assets.d.ts +2 -0
- package/dist/scaffold/assets.js +77 -0
- package/dist/testing/context.d.ts +3 -0
- package/dist/testing/context.js +14 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.js +208 -0
- package/dist/testing/types.d.ts +28 -0
- package/dist/testing/types.js +1 -0
- package/dist/watch.d.ts +8 -0
- package/dist/watch.js +159 -0
- package/dist/workspace.d.ts +4 -0
- package/dist/workspace.js +15 -0
- package/package.json +74 -0
- package/scripts/publish.sh +99 -0
- package/scripts/smoke.mjs +241 -0
- package/scripts/update-contract.sh +122 -0
- package/src/build/artifacts.ts +67 -0
- package/src/build/entries.ts +19 -0
- package/src/build/pipeline.ts +507 -0
- package/src/cache/diff.ts +128 -0
- package/src/cache/reporters.ts +41 -0
- package/src/diagnostics/summary.ts +32 -0
- package/src/index.ts +2 -0
- package/src/manifest/pipeline.ts +270 -0
- package/src/provider.ts +124 -0
- package/src/scaffold/assets.ts +81 -0
- package/src/testing/context.d.ts +3 -0
- package/src/testing/context.js +14 -0
- package/src/testing/context.ts +17 -0
- package/src/testing/index.d.ts +6 -0
- package/src/testing/index.js +208 -0
- package/src/testing/index.ts +252 -0
- package/src/testing/types.d.ts +28 -0
- package/src/testing/types.js +1 -0
- package/src/testing/types.ts +32 -0
- package/src/watch.ts +177 -0
- package/src/workspace.ts +22 -0
- package/templates/backend/.env.example +13 -0
- package/templates/backend/auth/adapter.ts +160 -0
- package/templates/backend/db/connection.ts +99 -0
- package/templates/backend/db/migrate.ts +231 -0
- package/templates/backend/db/migrations/0001-example.ts +17 -0
- package/templates/backend/db/types.d.ts +2 -0
- package/templates/backend/env.ts +174 -0
- package/templates/backend/functions/hello/index.ts +29 -0
- package/templates/backend/index.ts +532 -0
- package/templates/backend/jobs/nightly/index.ts +28 -0
- package/templates/backend/jobs/runtime.ts +103 -0
- package/templates/backend/jobs/scheduler.ts +193 -0
- package/templates/backend/module.ts +87 -0
- package/templates/backend/observability/logger.ts +24 -0
- package/templates/backend/observability/metrics.ts +78 -0
- package/templates/backend/server/fastify.ts +288 -0
- package/templates/backend/tsconfig.json +19 -0
- package/tests/cacheReporter.test.js +89 -0
- package/tests/envLoader.test.js +64 -0
- package/tests/integration.test.js +108 -0
- package/tests/manifest.test.js +159 -0
- package/tests/watch.test.js +100 -0
- 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,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
|
+
}
|
package/src/provider.ts
ADDED
|
@@ -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,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;
|