@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.
- package/README.md +106 -79
- package/dist/add.d.ts +59 -0
- package/dist/add.js +626 -0
- package/dist/build/artifacts.d.ts +115 -1
- package/dist/build/artifacts.js +4 -4
- package/dist/build/entries.js +1 -1
- package/dist/build/pipeline.d.ts +33 -1
- package/dist/build/pipeline.js +307 -65
- package/dist/cache/diff.js +9 -8
- package/dist/cache/reporters.js +1 -1
- package/dist/deploy-cli.d.ts +2 -0
- package/dist/deploy-cli.js +86 -0
- package/dist/diagnostics/summary.js +2 -2
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/manifest/pipeline.js +103 -32
- package/dist/provider.js +35 -17
- package/dist/runtime/bun.d.ts +51 -0
- package/dist/runtime/bun.js +499 -0
- package/dist/runtime/core.d.ts +141 -0
- package/dist/runtime/core.js +316 -0
- package/dist/runtime/deploy-backend.d.ts +20 -0
- package/dist/runtime/deploy-backend.js +175 -0
- package/dist/runtime/deploy-shared.d.ts +43 -0
- package/dist/runtime/deploy-shared.js +75 -0
- package/dist/runtime/deploy-static.d.ts +2 -0
- package/dist/runtime/deploy-static.js +161 -0
- package/dist/runtime/deploy.d.ts +3 -0
- package/dist/runtime/deploy.js +91 -0
- package/dist/runtime/forms.d.ts +73 -0
- package/dist/runtime/forms.js +236 -0
- package/dist/runtime/request-hooks.d.ts +47 -0
- package/dist/runtime/request-hooks.js +102 -0
- package/dist/runtime/session-metadata.d.ts +13 -0
- package/dist/runtime/session-metadata.js +98 -0
- package/dist/runtime/session-runtime.d.ts +28 -0
- package/dist/runtime/session-runtime.js +180 -0
- package/dist/runtime/session.d.ts +83 -0
- package/dist/runtime/session.js +396 -0
- package/dist/runtime/views.d.ts +74 -0
- package/dist/runtime/views.js +221 -0
- package/dist/scaffold/assets.js +25 -21
- package/dist/testing/context.js +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +100 -56
- package/dist/utils/bun.d.ts +2 -0
- package/dist/utils/bun.js +13 -0
- package/dist/watch.d.ts +13 -1
- package/dist/watch.js +345 -97
- package/dist/workspace.d.ts +8 -0
- package/dist/workspace.js +44 -3
- package/package.json +49 -14
- package/scripts/publish.sh +2 -92
- package/scripts/smoke.mjs +282 -107
- package/scripts/update-contract.sh +12 -10
- package/src/add.ts +964 -0
- package/src/build/artifacts.ts +49 -46
- package/src/build/entries.ts +12 -12
- package/src/build/pipeline.ts +779 -403
- package/src/cache/diff.ts +111 -105
- package/src/cache/reporters.ts +26 -26
- package/src/deploy-cli.ts +111 -0
- package/src/diagnostics/summary.ts +28 -22
- package/src/index.ts +11 -0
- package/src/manifest/pipeline.ts +328 -215
- package/src/provider.ts +115 -98
- package/src/runtime/bun.ts +793 -0
- package/src/runtime/core.ts +598 -0
- package/src/runtime/deploy-backend.ts +239 -0
- package/src/runtime/deploy-shared.ts +136 -0
- package/src/runtime/deploy-static.ts +191 -0
- package/src/runtime/deploy.ts +143 -0
- package/src/runtime/forms.ts +364 -0
- package/src/runtime/request-hooks.ts +165 -0
- package/src/runtime/session-metadata.ts +135 -0
- package/src/runtime/session-runtime.ts +267 -0
- package/src/runtime/session.ts +642 -0
- package/src/runtime/views.ts +385 -0
- package/src/scaffold/assets.ts +77 -73
- package/src/testing/context.js +8 -9
- package/src/testing/context.ts +9 -9
- package/src/testing/index.d.ts +14 -3
- package/src/testing/index.js +254 -175
- package/src/testing/index.ts +298 -195
- package/src/testing/types.d.ts +18 -19
- package/src/testing/types.ts +18 -18
- package/src/utils/bun.ts +26 -0
- package/src/watch.ts +503 -99
- package/src/workspace.ts +59 -3
- package/templates/backend/.env.example +15 -0
- package/templates/backend/auth/adapter.ts +335 -36
- package/templates/backend/db/connection.ts +190 -65
- package/templates/backend/db/migrate.ts +149 -43
- package/templates/backend/db/types.d.ts +1 -1
- package/templates/backend/env.ts +132 -20
- package/templates/backend/functions/hello/index.ts +1 -2
- package/templates/backend/index.ts +15 -508
- package/templates/backend/jobs/nightly/index.ts +1 -1
- package/templates/backend/jobs/runtime.ts +24 -11
- package/templates/backend/jobs/scheduler.ts +208 -46
- package/templates/backend/module.ts +227 -13
- package/templates/backend/observability/logger.ts +2 -12
- package/templates/backend/observability/metrics.ts +8 -5
- package/templates/backend/session/sqlite.ts +152 -0
- package/templates/backend/session/store.ts +45 -0
- package/templates/backend/tsconfig.json +1 -1
- package/tests/add.test.js +327 -0
- package/tests/authAdapter.test.js +315 -0
- package/tests/bundlerParity.test.js +217 -0
- package/tests/cacheReporter.test.js +10 -10
- package/tests/dbConnection.test.js +209 -0
- package/tests/deploy.test.js +357 -0
- package/tests/envLoader.test.js +271 -17
- package/tests/integration.test.js +2432 -3
- package/tests/jobsScheduler.test.js +253 -0
- package/tests/manifest.test.js +287 -12
- package/tests/migrationRunner.test.js +249 -0
- package/tests/sessionScaffoldStore.test.js +752 -0
- package/tests/sessionStore.test.js +490 -0
- package/tests/testing.test.js +252 -0
- package/tests/watch.test.js +192 -32
- package/tsconfig.json +3 -10
- package/templates/backend/server/fastify.ts +0 -288
package/src/manifest/pipeline.ts
CHANGED
|
@@ -1,270 +1,383 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
-
import { readFile } from 'node:fs/promises';
|
|
4
3
|
import { pathToFileURL } from 'node:url';
|
|
5
4
|
|
|
6
5
|
import { moduleManifestSchema, CONTRACT_VERSION } from '@webstir-io/module-contract';
|
|
7
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
ModuleDefinition,
|
|
8
|
+
ModuleDiagnostic,
|
|
9
|
+
ModuleManifest,
|
|
10
|
+
} from '@webstir-io/module-contract';
|
|
11
|
+
|
|
12
|
+
import { readTextFile } from '../utils/bun.js';
|
|
8
13
|
|
|
9
14
|
interface WorkspacePackageJson {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
readonly name?: string;
|
|
16
|
+
readonly version?: string;
|
|
17
|
+
readonly webstir?: {
|
|
18
|
+
readonly moduleManifest?: WorkspaceModuleConfig;
|
|
19
|
+
};
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
type WorkspaceModuleConfig = Partial<ModuleManifest> & {
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
readonly contractVersion?: string;
|
|
24
|
+
readonly capabilities?: ModuleManifest['capabilities'];
|
|
20
25
|
};
|
|
21
26
|
|
|
27
|
+
type ManifestRouteLike = { readonly method?: string; readonly path?: string };
|
|
28
|
+
type ManifestJobLike = { readonly name?: string; readonly schedule?: string | null };
|
|
29
|
+
|
|
22
30
|
export interface LoadManifestOptions {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
31
|
+
readonly workspaceRoot: string;
|
|
32
|
+
readonly buildRoot: string;
|
|
33
|
+
readonly entryPoints: readonly string[];
|
|
34
|
+
readonly diagnostics: ModuleDiagnostic[];
|
|
27
35
|
}
|
|
28
36
|
|
|
29
|
-
export async function loadBackendModuleManifest(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
export async function loadBackendModuleManifest(
|
|
38
|
+
options: LoadManifestOptions,
|
|
39
|
+
): Promise<ModuleManifest> {
|
|
40
|
+
const { workspaceRoot, buildRoot, entryPoints, diagnostics } = options;
|
|
41
|
+
const pkgPath = path.join(workspaceRoot, 'package.json');
|
|
42
|
+
let workspacePackage: WorkspacePackageJson | undefined;
|
|
33
43
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
44
|
+
try {
|
|
45
|
+
const raw = await readTextFile(pkgPath);
|
|
46
|
+
workspacePackage = JSON.parse(raw) as WorkspacePackageJson;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
const err = error as NodeJS.ErrnoException;
|
|
49
|
+
// Missing package.json is expected in some temporary workspaces; avoid noisy warnings.
|
|
50
|
+
if (err.code !== 'ENOENT') {
|
|
51
|
+
diagnostics.push({
|
|
52
|
+
severity: 'warn',
|
|
53
|
+
message: `[webstir-backend] unable to read ${pkgPath}: ${err.message}. Using defaults.`,
|
|
54
|
+
});
|
|
46
55
|
}
|
|
56
|
+
}
|
|
47
57
|
|
|
48
|
-
|
|
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
|
-
};
|
|
58
|
+
const moduleConfig = workspacePackage?.webstir?.moduleManifest ?? {};
|
|
64
59
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
60
|
+
let manifestCandidate: ModuleManifest = {
|
|
61
|
+
contractVersion:
|
|
62
|
+
typeof moduleConfig.contractVersion === 'string'
|
|
63
|
+
? moduleConfig.contractVersion
|
|
64
|
+
: CONTRACT_VERSION,
|
|
65
|
+
name:
|
|
66
|
+
typeof moduleConfig.name === 'string'
|
|
67
|
+
? moduleConfig.name
|
|
68
|
+
: deriveModuleName(workspacePackage, workspaceRoot),
|
|
69
|
+
version:
|
|
70
|
+
typeof moduleConfig.version === 'string'
|
|
71
|
+
? moduleConfig.version
|
|
72
|
+
: deriveModuleVersion(workspacePackage),
|
|
73
|
+
kind: 'backend',
|
|
74
|
+
capabilities: Array.isArray(moduleConfig.capabilities) ? moduleConfig.capabilities : [],
|
|
75
|
+
routes: moduleConfig.routes ?? [],
|
|
76
|
+
views: moduleConfig.views ?? [],
|
|
77
|
+
jobs: moduleConfig.jobs ?? [],
|
|
78
|
+
events: moduleConfig.events ?? [],
|
|
79
|
+
services: moduleConfig.services ?? [],
|
|
80
|
+
init: moduleConfig.init,
|
|
81
|
+
dispose: moduleConfig.dispose,
|
|
82
|
+
};
|
|
88
83
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
}
|
|
84
|
+
const definition = await loadModuleDefinition(buildRoot, diagnostics);
|
|
85
|
+
if (definition) {
|
|
86
|
+
const definitionManifest = definition.manifest ?? ({} as ModuleManifest);
|
|
87
|
+
const routesFromDefinition = definition.routes?.map((route) => route.definition);
|
|
88
|
+
const viewsFromDefinition = definition.views?.map((view) => view.definition);
|
|
111
89
|
|
|
112
|
-
const
|
|
90
|
+
const mergedCapabilities = Array.from(
|
|
91
|
+
new Set([
|
|
92
|
+
...(manifestCandidate.capabilities ?? []),
|
|
93
|
+
...(definitionManifest.capabilities ?? []),
|
|
94
|
+
]),
|
|
95
|
+
);
|
|
113
96
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
97
|
+
manifestCandidate = {
|
|
98
|
+
...manifestCandidate,
|
|
99
|
+
...definitionManifest,
|
|
100
|
+
capabilities: mergedCapabilities,
|
|
101
|
+
routes: mergeRouteDefinitions(
|
|
102
|
+
routesFromDefinition ?? definitionManifest.routes,
|
|
103
|
+
manifestCandidate.routes,
|
|
104
|
+
),
|
|
105
|
+
views: viewsFromDefinition ?? definitionManifest.views ?? manifestCandidate.views ?? [],
|
|
106
|
+
jobs: mergeJobDefinitions(definitionManifest.jobs, manifestCandidate.jobs),
|
|
107
|
+
events: definitionManifest.events ?? manifestCandidate.events ?? [],
|
|
108
|
+
services: definitionManifest.services ?? manifestCandidate.services ?? [],
|
|
109
|
+
init: definitionManifest.init ?? manifestCandidate.init,
|
|
110
|
+
dispose: definitionManifest.dispose ?? manifestCandidate.dispose,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const validation = moduleManifestSchema.safeParse(manifestCandidate);
|
|
115
|
+
if (!validation.success) {
|
|
116
|
+
const problems = validation.error.issues
|
|
117
|
+
.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
|
|
118
|
+
.join('; ');
|
|
119
|
+
diagnostics.push({
|
|
120
|
+
severity: 'error',
|
|
121
|
+
message: `[webstir-backend] module manifest validation failed (${problems}). Falling back to defaults.`,
|
|
122
|
+
});
|
|
123
|
+
return {
|
|
124
|
+
contractVersion: CONTRACT_VERSION,
|
|
125
|
+
name: deriveModuleName(workspacePackage, workspaceRoot),
|
|
126
|
+
version: deriveModuleVersion(workspacePackage),
|
|
127
|
+
kind: 'backend',
|
|
128
|
+
capabilities: [],
|
|
129
|
+
routes: [],
|
|
130
|
+
views: [],
|
|
131
|
+
jobs: [],
|
|
132
|
+
events: [],
|
|
133
|
+
services: [],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const manifest = validation.data;
|
|
137
138
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
try {
|
|
140
|
+
const normalizePath = (p: unknown) => {
|
|
141
|
+
let s = typeof p === 'string' ? p : '';
|
|
142
|
+
if (!s.startsWith('/')) s = `/${s}`;
|
|
143
|
+
s = s.replace(/\/+/, '/');
|
|
144
|
+
if (s.length > 1 && s.endsWith('/')) s = s.slice(0, -1);
|
|
145
|
+
return s;
|
|
146
|
+
};
|
|
147
|
+
const seen = new Map<string, number>();
|
|
148
|
+
for (const route of (manifest.routes ?? []) as readonly ManifestRouteLike[]) {
|
|
149
|
+
const method = typeof route.method === 'string' ? route.method.toUpperCase() : '';
|
|
150
|
+
const pathKey = normalizePath(route.path);
|
|
151
|
+
const key = `${method} ${pathKey}`;
|
|
152
|
+
seen.set(key, (seen.get(key) ?? 0) + 1);
|
|
153
|
+
}
|
|
154
|
+
const dups = Array.from(seen.entries()).filter(([, count]) => count > 1);
|
|
155
|
+
if (dups.length > 0) {
|
|
156
|
+
const list = dups.map(([k, c]) => `${k} (${c}x)`).join(', ');
|
|
157
|
+
diagnostics.push({
|
|
158
|
+
severity: 'warn',
|
|
159
|
+
message: `[webstir-backend] duplicate route definitions: ${list}`,
|
|
160
|
+
});
|
|
143
161
|
}
|
|
162
|
+
} catch {
|
|
163
|
+
// best-effort only
|
|
164
|
+
}
|
|
144
165
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
severity: 'warn',
|
|
163
|
-
message: `[webstir-backend] jobs without schedules: ${names}${omitted}`
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
} catch {
|
|
167
|
-
// best-effort only
|
|
166
|
+
if (manifest.routes?.length && entryPoints.length === 0) {
|
|
167
|
+
diagnostics.push({
|
|
168
|
+
severity: 'warn',
|
|
169
|
+
message:
|
|
170
|
+
'[webstir-backend] module manifest defines routes but no entry points were built. Ensure backend compilation produced handlers.',
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const jobs = Array.isArray(manifest.jobs) ? manifest.jobs : [];
|
|
176
|
+
const events = Array.isArray(manifest.events) ? manifest.events : [];
|
|
177
|
+
const services = Array.isArray(manifest.services) ? manifest.services : [];
|
|
178
|
+
if (jobs.length + events.length + services.length > 0) {
|
|
179
|
+
diagnostics.push({
|
|
180
|
+
severity: 'info',
|
|
181
|
+
message: `[webstir-backend] manifest jobs=${jobs.length} events=${events.length} services=${services.length}`,
|
|
182
|
+
});
|
|
168
183
|
}
|
|
169
184
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
185
|
+
const noSchedule = (jobs as readonly ManifestJobLike[]).filter(
|
|
186
|
+
(job) =>
|
|
187
|
+
typeof job.name === 'string' && (job.schedule === undefined || job.schedule === null),
|
|
188
|
+
);
|
|
189
|
+
if (noSchedule.length > 0) {
|
|
190
|
+
const MAX_LIST = 10;
|
|
191
|
+
const names = noSchedule
|
|
192
|
+
.map((job) => job.name)
|
|
193
|
+
.slice(0, MAX_LIST)
|
|
194
|
+
.join(', ');
|
|
195
|
+
const omitted =
|
|
196
|
+
noSchedule.length > MAX_LIST ? ` (+${noSchedule.length - MAX_LIST} more)` : '';
|
|
197
|
+
diagnostics.push({
|
|
198
|
+
severity: 'warn',
|
|
199
|
+
message: `[webstir-backend] jobs without schedules: ${names}${omitted}`,
|
|
200
|
+
});
|
|
177
201
|
}
|
|
202
|
+
} catch {
|
|
203
|
+
// best-effort only
|
|
204
|
+
}
|
|
178
205
|
|
|
179
|
-
|
|
206
|
+
try {
|
|
207
|
+
const routes = Array.isArray(manifest.routes) ? manifest.routes.length : 0;
|
|
208
|
+
const views = Array.isArray(manifest.views) ? manifest.views.length : 0;
|
|
209
|
+
const caps =
|
|
210
|
+
Array.isArray(manifest.capabilities) && manifest.capabilities.length > 0
|
|
211
|
+
? ` [${manifest.capabilities.join(', ')}]`
|
|
212
|
+
: '';
|
|
213
|
+
diagnostics.push({
|
|
214
|
+
severity: 'info',
|
|
215
|
+
message: `[webstir-backend] manifest routes=${routes} views=${views}${caps}`,
|
|
216
|
+
});
|
|
217
|
+
} catch {
|
|
218
|
+
// ignore
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return manifest;
|
|
180
222
|
}
|
|
181
223
|
|
|
182
224
|
async function loadModuleDefinition(
|
|
183
|
-
|
|
184
|
-
|
|
225
|
+
buildRoot: string,
|
|
226
|
+
diagnostics: ModuleDiagnostic[],
|
|
185
227
|
): Promise<ModuleDefinition | undefined> {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
}
|
|
228
|
+
const candidates = [
|
|
229
|
+
path.join(buildRoot, 'module.js'),
|
|
230
|
+
path.join(buildRoot, 'module.mjs'),
|
|
231
|
+
path.join(buildRoot, 'module/index.js'),
|
|
232
|
+
path.join(buildRoot, 'module/index.mjs'),
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
for (const fullPath of candidates) {
|
|
236
|
+
if (!existsSync(fullPath)) {
|
|
237
|
+
continue;
|
|
215
238
|
}
|
|
216
239
|
|
|
217
|
-
|
|
240
|
+
try {
|
|
241
|
+
const moduleUrl = `${pathToFileURL(fullPath).href}?t=${Date.now()}`;
|
|
242
|
+
const imported = (await import(moduleUrl)) as Record<string, unknown>;
|
|
243
|
+
const definitionCandidate = extractModuleDefinition(imported);
|
|
244
|
+
if (isModuleDefinition(definitionCandidate)) {
|
|
245
|
+
return definitionCandidate;
|
|
246
|
+
}
|
|
247
|
+
diagnostics.push({
|
|
248
|
+
severity: 'warn',
|
|
249
|
+
message: `[webstir-backend] module definition at ${fullPath} does not export a createModule() definition.`,
|
|
250
|
+
});
|
|
251
|
+
} catch (error) {
|
|
252
|
+
diagnostics.push({
|
|
253
|
+
severity: 'warn',
|
|
254
|
+
message: `[webstir-backend] failed to load module definition from ${fullPath}: ${(error as Error).message}`,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return undefined;
|
|
218
260
|
}
|
|
219
261
|
|
|
220
262
|
function extractModuleDefinition(exports: Record<string, unknown>): unknown {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
263
|
+
const keys = ['module', 'moduleDefinition', 'default', 'backendModule'];
|
|
264
|
+
for (const key of keys) {
|
|
265
|
+
if (key in exports) {
|
|
266
|
+
const value = exports[key as keyof typeof exports];
|
|
267
|
+
if (value !== null && value !== undefined) {
|
|
268
|
+
return value;
|
|
269
|
+
}
|
|
229
270
|
}
|
|
230
|
-
|
|
271
|
+
}
|
|
272
|
+
return undefined;
|
|
231
273
|
}
|
|
232
274
|
|
|
233
275
|
function isModuleDefinition(value: unknown): value is ModuleDefinition {
|
|
234
|
-
|
|
276
|
+
return (
|
|
277
|
+
typeof value === 'object' && value !== null && 'manifest' in (value as Record<string, unknown>)
|
|
278
|
+
);
|
|
235
279
|
}
|
|
236
280
|
|
|
237
281
|
function deriveModuleName(pkg: WorkspacePackageJson | undefined, workspaceRoot: string): string {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
282
|
+
if (
|
|
283
|
+
typeof pkg?.webstir?.moduleManifest?.name === 'string' &&
|
|
284
|
+
pkg.webstir.moduleManifest.name.length > 0
|
|
285
|
+
) {
|
|
286
|
+
return pkg.webstir.moduleManifest.name;
|
|
287
|
+
}
|
|
288
|
+
if (typeof pkg?.name === 'string' && pkg.name.length > 0) {
|
|
289
|
+
return pkg.name;
|
|
290
|
+
}
|
|
291
|
+
return `backend-module-${path.basename(workspaceRoot)}`;
|
|
245
292
|
}
|
|
246
293
|
|
|
247
294
|
function deriveModuleVersion(pkg: WorkspacePackageJson | undefined): string {
|
|
248
|
-
|
|
249
|
-
|
|
295
|
+
if (
|
|
296
|
+
typeof pkg?.webstir?.moduleManifest?.version === 'string' &&
|
|
297
|
+
pkg.webstir.moduleManifest.version.length > 0
|
|
298
|
+
) {
|
|
299
|
+
return pkg.webstir.moduleManifest.version;
|
|
300
|
+
}
|
|
301
|
+
if (typeof pkg?.version === 'string' && pkg.version.length > 0) {
|
|
302
|
+
return pkg.version;
|
|
303
|
+
}
|
|
304
|
+
return '0.0.0';
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function mergeRouteDefinitions(
|
|
308
|
+
definitionRoutes: ModuleManifest['routes'] | undefined,
|
|
309
|
+
packageRoutes: ModuleManifest['routes'] | undefined,
|
|
310
|
+
): ModuleManifest['routes'] {
|
|
311
|
+
const merged = Array.isArray(definitionRoutes) ? [...definitionRoutes] : [];
|
|
312
|
+
const seen = new Set(merged.map((route) => getRouteKey(route)).filter(Boolean));
|
|
313
|
+
|
|
314
|
+
for (const route of packageRoutes ?? []) {
|
|
315
|
+
const key = getRouteKey(route);
|
|
316
|
+
if (!key || seen.has(key)) {
|
|
317
|
+
continue;
|
|
250
318
|
}
|
|
251
|
-
|
|
252
|
-
|
|
319
|
+
merged.push(route);
|
|
320
|
+
seen.add(key);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return merged;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function mergeJobDefinitions(
|
|
327
|
+
definitionJobs: ModuleManifest['jobs'] | undefined,
|
|
328
|
+
packageJobs: ModuleManifest['jobs'] | undefined,
|
|
329
|
+
): ModuleManifest['jobs'] {
|
|
330
|
+
const merged = Array.isArray(definitionJobs) ? [...definitionJobs] : [];
|
|
331
|
+
const seen = new Set(merged.map((job) => getJobKey(job)).filter(Boolean));
|
|
332
|
+
|
|
333
|
+
for (const job of packageJobs ?? []) {
|
|
334
|
+
const key = getJobKey(job);
|
|
335
|
+
if (!key || seen.has(key)) {
|
|
336
|
+
continue;
|
|
253
337
|
}
|
|
254
|
-
|
|
338
|
+
merged.push(job);
|
|
339
|
+
seen.add(key);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return merged;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function getJobKey(job: ManifestJobLike | undefined): string | undefined {
|
|
346
|
+
return typeof job?.name === 'string' && job.name.length > 0 ? job.name : undefined;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function getRouteKey(route: ManifestRouteLike | undefined): string | undefined {
|
|
350
|
+
const method = typeof route?.method === 'string' ? route.method.toUpperCase() : '';
|
|
351
|
+
const routePath = normalizeRoutePath(route?.path);
|
|
352
|
+
if (!method || !routePath) {
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
return `${method} ${routePath}`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function normalizeRoutePath(routePath: unknown): string | undefined {
|
|
359
|
+
if (typeof routePath !== 'string' || routePath.length === 0) {
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
let normalized = routePath;
|
|
364
|
+
if (!normalized.startsWith('/')) normalized = `/${normalized}`;
|
|
365
|
+
normalized = normalized.replace(/\/+/g, '/');
|
|
366
|
+
if (normalized.length > 1 && normalized.endsWith('/')) normalized = normalized.slice(0, -1);
|
|
367
|
+
return normalized;
|
|
255
368
|
}
|
|
256
369
|
|
|
257
370
|
export async function summarizeBuiltManifest(
|
|
258
|
-
|
|
371
|
+
buildRoot: string,
|
|
259
372
|
): Promise<{ routes: number; views: number; capabilities?: readonly string[] } | undefined> {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
373
|
+
const definition = await loadModuleDefinition(buildRoot, []);
|
|
374
|
+
if (!definition || !definition.manifest) {
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
const manifest = definition.manifest;
|
|
378
|
+
return {
|
|
379
|
+
routes: Array.isArray(manifest.routes) ? manifest.routes.length : 0,
|
|
380
|
+
views: Array.isArray(manifest.views) ? manifest.views.length : 0,
|
|
381
|
+
capabilities: manifest.capabilities,
|
|
382
|
+
};
|
|
270
383
|
}
|