@ontrails/adapter-kit 1.0.0-beta.30 → 1.0.0-beta.39
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/package.json +7 -1
- package/src/catalog.ts +25 -88
- package/src/check.ts +11 -94
- package/src/index.ts +2 -0
- package/src/overlay.ts +158 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ontrails/adapter-kit",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.39",
|
|
4
4
|
"description": "Internal adapter authoring kit for Trails metadata, scaffolding, and checks.",
|
|
5
5
|
"files": [
|
|
6
6
|
"src/**/*.ts",
|
|
@@ -19,5 +19,11 @@
|
|
|
19
19
|
"typecheck": "tsc --noEmit",
|
|
20
20
|
"lint": "oxlint ./src",
|
|
21
21
|
"clean": "rm -rf dist *.tsbuildinfo"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@ontrails/core": "^1.0.0-beta.39"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"zod": "^4.3.5"
|
|
22
28
|
}
|
|
23
29
|
}
|
package/src/catalog.ts
CHANGED
|
@@ -6,15 +6,11 @@
|
|
|
6
6
|
* internal tooling package.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
existsSync,
|
|
11
|
-
readdirSync,
|
|
12
|
-
readFileSync,
|
|
13
|
-
realpathSync,
|
|
14
|
-
statSync,
|
|
15
|
-
} from 'node:fs';
|
|
9
|
+
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
16
10
|
import { dirname, join, resolve } from 'node:path';
|
|
17
11
|
|
|
12
|
+
import { listWorkspacePackages } from '@ontrails/core';
|
|
13
|
+
|
|
18
14
|
export const adapterTargetPlacements = ['extracted', 'subpath'] as const;
|
|
19
15
|
|
|
20
16
|
export type AdapterTargetPlacementValue =
|
|
@@ -66,16 +62,16 @@ export interface AdapterTargetCatalog {
|
|
|
66
62
|
readonly targets: readonly AdapterTargetCatalogEntry[];
|
|
67
63
|
}
|
|
68
64
|
|
|
69
|
-
interface RootManifest {
|
|
70
|
-
readonly workspaces?: unknown;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
65
|
export interface AdapterTargetPackageManifest {
|
|
74
66
|
readonly exports?: unknown;
|
|
75
67
|
readonly name?: unknown;
|
|
76
68
|
readonly trails?: unknown;
|
|
77
69
|
}
|
|
78
70
|
|
|
71
|
+
type NamedAdapterTargetPackageManifest = AdapterTargetPackageManifest & {
|
|
72
|
+
readonly name: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
79
75
|
export interface AdapterTargetParseContext {
|
|
80
76
|
readonly blockedExportSpecifiers: readonly string[];
|
|
81
77
|
readonly exportTargets: Readonly<Record<string, string>>;
|
|
@@ -181,14 +177,6 @@ const localDeclarationKind = (
|
|
|
181
177
|
return typeDeclarationPattern.test(code) ? 'type' : undefined;
|
|
182
178
|
};
|
|
183
179
|
|
|
184
|
-
const readJson = <T>(path: string): T | undefined => {
|
|
185
|
-
try {
|
|
186
|
-
return JSON.parse(readFileSync(path, 'utf8')) as T;
|
|
187
|
-
} catch {
|
|
188
|
-
return undefined;
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
|
|
192
180
|
const maskDeadSourceText = (
|
|
193
181
|
source: string,
|
|
194
182
|
options: { strings: boolean }
|
|
@@ -414,44 +402,6 @@ const exportedLocalBindingKind = (
|
|
|
414
402
|
return importSpecifier ? resolveImportKind(importSpecifier) : undefined;
|
|
415
403
|
};
|
|
416
404
|
|
|
417
|
-
const workspacePatternsFromManifest = (
|
|
418
|
-
manifest: RootManifest | undefined
|
|
419
|
-
): readonly string[] => {
|
|
420
|
-
const { workspaces } = manifest ?? {};
|
|
421
|
-
if (Array.isArray(workspaces)) {
|
|
422
|
-
return workspaces.filter(
|
|
423
|
-
(pattern): pattern is string => typeof pattern === 'string'
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const packages = isRecord(workspaces) ? workspaces['packages'] : undefined;
|
|
428
|
-
return Array.isArray(packages)
|
|
429
|
-
? packages.filter(
|
|
430
|
-
(pattern): pattern is string => typeof pattern === 'string'
|
|
431
|
-
)
|
|
432
|
-
: [];
|
|
433
|
-
};
|
|
434
|
-
|
|
435
|
-
const workspaceDirsForPattern = (
|
|
436
|
-
rootDir: string,
|
|
437
|
-
pattern: string
|
|
438
|
-
): readonly string[] => {
|
|
439
|
-
if (!pattern.endsWith('/*')) {
|
|
440
|
-
const workspaceDir = join(rootDir, pattern);
|
|
441
|
-
return existsSync(workspaceDir) ? [workspaceDir] : [];
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
const groupDir = join(rootDir, pattern.slice(0, -2));
|
|
445
|
-
if (!existsSync(groupDir)) {
|
|
446
|
-
return [];
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
return readdirSync(groupDir, { withFileTypes: true })
|
|
450
|
-
.filter((entry) => entry.isDirectory())
|
|
451
|
-
.map((entry) => join(groupDir, entry.name))
|
|
452
|
-
.toSorted();
|
|
453
|
-
};
|
|
454
|
-
|
|
455
405
|
const exportConditions = new Set([
|
|
456
406
|
'bun',
|
|
457
407
|
'node',
|
|
@@ -1401,40 +1351,27 @@ export const parseAdapterTargetsFromManifest = (
|
|
|
1401
1351
|
export const deriveAdapterTargetCatalog = (
|
|
1402
1352
|
rootDir: string
|
|
1403
1353
|
): AdapterTargetCatalog => {
|
|
1404
|
-
const normalizedRoot = normalizeRealPath(rootDir);
|
|
1405
|
-
const rootManifest = readJson<RootManifest>(
|
|
1406
|
-
join(normalizedRoot, 'package.json')
|
|
1407
|
-
);
|
|
1408
1354
|
const diagnostics: AdapterTargetCatalogDiagnostic[] = [];
|
|
1409
1355
|
const targets: AdapterTargetCatalogEntry[] = [];
|
|
1410
1356
|
|
|
1411
|
-
for (const
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
blockedExportSpecifiers: normalizedExports.blocked,
|
|
1430
|
-
exportTargets: normalizedExports.targets,
|
|
1431
|
-
packageJsonPath: normalizeRealPath(packageJsonPath),
|
|
1432
|
-
packageName: manifest.name,
|
|
1433
|
-
packageRoot,
|
|
1434
|
-
});
|
|
1435
|
-
diagnostics.push(...parsed.diagnostics);
|
|
1436
|
-
targets.push(...parsed.targets);
|
|
1437
|
-
}
|
|
1357
|
+
for (const workspacePackage of listWorkspacePackages<NamedAdapterTargetPackageManifest>(
|
|
1358
|
+
rootDir
|
|
1359
|
+
)) {
|
|
1360
|
+
const { manifest, packageJsonPath, packageRoot } = workspacePackage;
|
|
1361
|
+
const normalizedExports = normalizeExportTargets(
|
|
1362
|
+
packageRoot,
|
|
1363
|
+
manifest.name,
|
|
1364
|
+
manifest.exports
|
|
1365
|
+
);
|
|
1366
|
+
const parsed = parseAdapterTargetsFromManifest(manifest, {
|
|
1367
|
+
blockedExportSpecifiers: normalizedExports.blocked,
|
|
1368
|
+
exportTargets: normalizedExports.targets,
|
|
1369
|
+
packageJsonPath,
|
|
1370
|
+
packageName: manifest.name,
|
|
1371
|
+
packageRoot,
|
|
1372
|
+
});
|
|
1373
|
+
diagnostics.push(...parsed.diagnostics);
|
|
1374
|
+
targets.push(...parsed.targets);
|
|
1438
1375
|
}
|
|
1439
1376
|
|
|
1440
1377
|
const sortedTargets = targets.toSorted((left, right) =>
|
package/src/check.ts
CHANGED
|
@@ -14,6 +14,12 @@ import {
|
|
|
14
14
|
} from 'node:fs';
|
|
15
15
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
16
16
|
|
|
17
|
+
import { escapeRegExp, listWorkspacePackages } from '@ontrails/core';
|
|
18
|
+
import type {
|
|
19
|
+
DiagnosticBase,
|
|
20
|
+
WorkspacePackage as CoreWorkspacePackage,
|
|
21
|
+
} from '@ontrails/core';
|
|
22
|
+
|
|
17
23
|
import { deriveAdapterTargetCatalog } from './catalog.js';
|
|
18
24
|
import type {
|
|
19
25
|
AdapterTargetCatalog,
|
|
@@ -33,15 +39,14 @@ export type AdapterCheckDiagnosticCode =
|
|
|
33
39
|
| 'unknown-adapter-target'
|
|
34
40
|
| 'unsupported-placement';
|
|
35
41
|
|
|
36
|
-
export type AdapterCheckDiagnosticSeverity = '
|
|
42
|
+
export type AdapterCheckDiagnosticSeverity = DiagnosticBase['severity'];
|
|
37
43
|
|
|
38
|
-
export interface AdapterCheckDiagnostic {
|
|
44
|
+
export interface AdapterCheckDiagnostic extends DiagnosticBase<AdapterCheckDiagnosticCode> {
|
|
39
45
|
readonly code: AdapterCheckDiagnosticCode;
|
|
40
46
|
readonly message: string;
|
|
41
47
|
readonly packageJsonPath: string;
|
|
42
48
|
readonly packageName?: string | undefined;
|
|
43
49
|
readonly placement?: AdapterTargetPlacement | undefined;
|
|
44
|
-
readonly severity: AdapterCheckDiagnosticSeverity;
|
|
45
50
|
readonly target?: string | undefined;
|
|
46
51
|
}
|
|
47
52
|
|
|
@@ -93,10 +98,6 @@ export interface AdapterCheckReport {
|
|
|
93
98
|
readonly targets: readonly AdapterTargetCatalogEntry[];
|
|
94
99
|
}
|
|
95
100
|
|
|
96
|
-
interface RootManifest {
|
|
97
|
-
readonly workspaces?: unknown;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
101
|
interface AdapterCheckPackageManifest {
|
|
101
102
|
readonly dependencies?: unknown;
|
|
102
103
|
readonly devDependencies?: unknown;
|
|
@@ -107,12 +108,7 @@ interface AdapterCheckPackageManifest {
|
|
|
107
108
|
readonly trails?: unknown;
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
|
|
111
|
-
readonly manifest: AdapterCheckPackageManifest;
|
|
112
|
-
readonly packageJsonPath: string;
|
|
113
|
-
readonly packageRoot: string;
|
|
114
|
-
readonly workspacePath: string;
|
|
115
|
-
}
|
|
111
|
+
type WorkspacePackage = CoreWorkspacePackage<AdapterCheckPackageManifest>;
|
|
116
112
|
|
|
117
113
|
interface AdapterMetadata {
|
|
118
114
|
readonly target: string;
|
|
@@ -135,84 +131,8 @@ const normalizeRealPath = (path: string): string => {
|
|
|
135
131
|
}
|
|
136
132
|
};
|
|
137
133
|
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
return JSON.parse(readFileSync(path, 'utf8')) as T;
|
|
141
|
-
} catch {
|
|
142
|
-
return undefined;
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
const workspacePatternsFromManifest = (
|
|
147
|
-
manifest: RootManifest | undefined
|
|
148
|
-
): readonly string[] => {
|
|
149
|
-
const { workspaces } = manifest ?? {};
|
|
150
|
-
if (Array.isArray(workspaces)) {
|
|
151
|
-
return workspaces.filter(
|
|
152
|
-
(pattern): pattern is string => typeof pattern === 'string'
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const packages = isRecord(workspaces) ? workspaces['packages'] : undefined;
|
|
157
|
-
return Array.isArray(packages)
|
|
158
|
-
? packages.filter(
|
|
159
|
-
(pattern): pattern is string => typeof pattern === 'string'
|
|
160
|
-
)
|
|
161
|
-
: [];
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
const workspaceDirsForPattern = (
|
|
165
|
-
rootDir: string,
|
|
166
|
-
pattern: string
|
|
167
|
-
): readonly string[] => {
|
|
168
|
-
if (!pattern.endsWith('/*')) {
|
|
169
|
-
const workspaceDir = join(rootDir, pattern);
|
|
170
|
-
return existsSync(workspaceDir) ? [workspaceDir] : [];
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const groupDir = join(rootDir, pattern.slice(0, -2));
|
|
174
|
-
if (!existsSync(groupDir)) {
|
|
175
|
-
return [];
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return readdirSync(groupDir, { withFileTypes: true })
|
|
179
|
-
.filter((entry) => entry.isDirectory())
|
|
180
|
-
.map((entry) => join(groupDir, entry.name))
|
|
181
|
-
.toSorted();
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
const workspacePackages = (rootDir: string): readonly WorkspacePackage[] => {
|
|
185
|
-
const normalizedRoot = normalizeRealPath(rootDir);
|
|
186
|
-
const rootManifest = readJson<RootManifest>(
|
|
187
|
-
join(normalizedRoot, 'package.json')
|
|
188
|
-
);
|
|
189
|
-
const packages: WorkspacePackage[] = [];
|
|
190
|
-
|
|
191
|
-
for (const pattern of workspacePatternsFromManifest(rootManifest)) {
|
|
192
|
-
for (const workspaceDir of workspaceDirsForPattern(
|
|
193
|
-
normalizedRoot,
|
|
194
|
-
pattern
|
|
195
|
-
)) {
|
|
196
|
-
const packageJsonPath = join(workspaceDir, 'package.json');
|
|
197
|
-
const manifest = readJson<AdapterCheckPackageManifest>(packageJsonPath);
|
|
198
|
-
if (!manifest || typeof manifest.name !== 'string') {
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const packageRoot = normalizeRealPath(dirname(packageJsonPath));
|
|
203
|
-
packages.push({
|
|
204
|
-
manifest,
|
|
205
|
-
packageJsonPath: normalizeRealPath(packageJsonPath),
|
|
206
|
-
packageRoot,
|
|
207
|
-
workspacePath: normalizePath(relative(normalizedRoot, packageRoot)),
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return packages.toSorted((left, right) =>
|
|
213
|
-
left.workspacePath.localeCompare(right.workspacePath)
|
|
214
|
-
);
|
|
215
|
-
};
|
|
134
|
+
const workspacePackages = (rootDir: string): readonly WorkspacePackage[] =>
|
|
135
|
+
listWorkspacePackages<AdapterCheckPackageManifest>(rootDir);
|
|
216
136
|
|
|
217
137
|
const resolveExportTarget = (
|
|
218
138
|
target: unknown,
|
|
@@ -433,9 +353,6 @@ const collectSourceFiles = (dir: string): readonly string[] => {
|
|
|
433
353
|
return files.toSorted();
|
|
434
354
|
};
|
|
435
355
|
|
|
436
|
-
const escapeRegExp = (value: string): string =>
|
|
437
|
-
value.replaceAll(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
438
|
-
|
|
439
356
|
const isIdentifierChar = (char: string | undefined): boolean =>
|
|
440
357
|
char !== undefined && /[$\w]/u.test(char);
|
|
441
358
|
|
package/src/index.ts
CHANGED
package/src/overlay.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The adapter overlay contract.
|
|
3
|
+
*
|
|
4
|
+
* Adapters export an overlay object describing one namespaced fact
|
|
5
|
+
* overlay; the app module re-exports it (conventionally as
|
|
6
|
+
* `trailsOverlays`), and the compile path validates `derive(topo)`
|
|
7
|
+
* output against the overlay's schema before embedding the facts as
|
|
8
|
+
* `overlays.<namespace>` in `trails.lock`. The lock schema and graph type
|
|
9
|
+
* never change — unknown namespaces are preserved byte-for-byte by older
|
|
10
|
+
* toolchains (tolerant reader).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { ValidationError } from '@ontrails/core';
|
|
14
|
+
import type { OverlayProvenance, Topo } from '@ontrails/core';
|
|
15
|
+
import type { z } from 'zod';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* One adapter-owned namespaced fact overlay for `trails.lock`.
|
|
19
|
+
*
|
|
20
|
+
* An overlay is authored by an adapter package and opted into by the
|
|
21
|
+
* app: the app module exports `trailsOverlays` next to its topo export,
|
|
22
|
+
* and `trails compile` runs each overlay's {@link derive} over the
|
|
23
|
+
* compiled topo, validates the result against {@link schema}, and embeds it
|
|
24
|
+
* as `overlays.<namespace>` in the committed lock.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* import type { Overlay } from '@ontrails/adapter-kit';
|
|
29
|
+
* import { z } from 'zod';
|
|
30
|
+
*
|
|
31
|
+
* const factsSchema = z.object({ regions: z.array(z.string()) }).strict();
|
|
32
|
+
*
|
|
33
|
+
* export const overlay = {
|
|
34
|
+
* namespace: 'acme',
|
|
35
|
+
* schema: factsSchema,
|
|
36
|
+
* derive: (topo) => ({
|
|
37
|
+
* regions: topo
|
|
38
|
+
* .listResources()
|
|
39
|
+
* .map((definition) => definition.id)
|
|
40
|
+
* .toSorted(),
|
|
41
|
+
* }),
|
|
42
|
+
* } satisfies Overlay;
|
|
43
|
+
*
|
|
44
|
+
* // In the app module, next to the topo export:
|
|
45
|
+
* // export const trailsOverlays = [overlay];
|
|
46
|
+
* // `trails compile` then embeds the facts as `overlays.acme`.
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export interface Overlay {
|
|
50
|
+
/**
|
|
51
|
+
* The lock overlay this overlay owns.
|
|
52
|
+
*
|
|
53
|
+
* Dotted kebab-case: `/^[a-z][a-z0-9-]*(\.[a-z0-9-]+)*$/`. Each
|
|
54
|
+
* overlay owns exactly one namespace, and the facts land at
|
|
55
|
+
* `overlays.<namespace>` in `trails.lock`. Unknown namespaces are
|
|
56
|
+
* preserved byte-for-byte by older toolchains, so new overlays never
|
|
57
|
+
* break existing lock readers.
|
|
58
|
+
*/
|
|
59
|
+
readonly namespace: string;
|
|
60
|
+
/**
|
|
61
|
+
* Who authored this overlay. Absent means adapter-derived.
|
|
62
|
+
*
|
|
63
|
+
* Surfaces obey app-authored overlays only: the well-known `surfaces`
|
|
64
|
+
* namespace requires `provenance: 'app-authored'` (authored via
|
|
65
|
+
* `surfaceOverlay()` in the app module), and the compile path rejects
|
|
66
|
+
* adapter-derived envelopes that claim it. Adapter overlays contribute
|
|
67
|
+
* facts, never bindings, and can leave this field absent.
|
|
68
|
+
*/
|
|
69
|
+
readonly provenance?: OverlayProvenance | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* The elevated fact schema.
|
|
72
|
+
*
|
|
73
|
+
* The compile path enforces this schema against every {@link derive}
|
|
74
|
+
* output before embedding the facts, so a drifting derive function fails
|
|
75
|
+
* `trails compile` instead of committing invalid facts.
|
|
76
|
+
*/
|
|
77
|
+
readonly schema: z.ZodType;
|
|
78
|
+
/**
|
|
79
|
+
* Project the topo into this overlay's facts.
|
|
80
|
+
*
|
|
81
|
+
* Must be deterministic — the same topo always yields the same facts
|
|
82
|
+
* (sort any collections) — and must return JSON-plain data, because the
|
|
83
|
+
* result is embedded verbatim in the committed lock.
|
|
84
|
+
*/
|
|
85
|
+
readonly derive: (topo: Topo) => unknown;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const isFunction = (value: unknown): value is (...args: unknown[]) => unknown =>
|
|
89
|
+
typeof value === 'function';
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Structurally recognize an {@link Overlay}.
|
|
93
|
+
*
|
|
94
|
+
* Used by the compile-side collector when scanning an app module's
|
|
95
|
+
* `trailsOverlays` export — adapters never import this at runtime. The
|
|
96
|
+
* check is hand-rolled (string `namespace`, schema object exposing a
|
|
97
|
+
* `safeParse` function, function `derive`) so recognizing overlays
|
|
98
|
+
* needs no zod runtime dependency.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* import { isOverlay } from '@ontrails/adapter-kit';
|
|
103
|
+
*
|
|
104
|
+
* const recognized = candidates.filter(isOverlay);
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export const isOverlay = (value: unknown): value is Overlay => {
|
|
108
|
+
if (typeof value !== 'object' || value === null) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
const candidate = value as Partial<Record<keyof Overlay, unknown>>;
|
|
112
|
+
return (
|
|
113
|
+
typeof candidate.namespace === 'string' &&
|
|
114
|
+
typeof candidate.schema === 'object' &&
|
|
115
|
+
candidate.schema !== null &&
|
|
116
|
+
isFunction((candidate.schema as { safeParse?: unknown }).safeParse) &&
|
|
117
|
+
isFunction(candidate.derive)
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Read an app module's `trailsOverlays` export as validated overlays.
|
|
123
|
+
*
|
|
124
|
+
* This is the one shared collection channel for app-module overlays: the
|
|
125
|
+
* compile path's fresh app lease and Warden's fresh topo loading both read
|
|
126
|
+
* the export through this function, so every fresh derivation carries the
|
|
127
|
+
* same overlays the committed lock embeds. An absent export returns
|
|
128
|
+
* `undefined`; a present export that is not an array of {@link Overlay}
|
|
129
|
+
* values throws a fix-forward `ValidationError` naming `sourceLabel`.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```ts
|
|
133
|
+
* import { resolveTrailsOverlays } from '@ontrails/adapter-kit';
|
|
134
|
+
*
|
|
135
|
+
* const mod = await import(appModulePath);
|
|
136
|
+
* const overlays = resolveTrailsOverlays(
|
|
137
|
+
* mod as Record<string, unknown>,
|
|
138
|
+
* appModulePath
|
|
139
|
+
* );
|
|
140
|
+
* // => readonly Overlay[] | undefined
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
export const resolveTrailsOverlays = (
|
|
144
|
+
moduleExports: Record<string, unknown>,
|
|
145
|
+
sourceLabel: string
|
|
146
|
+
): readonly Overlay[] | undefined => {
|
|
147
|
+
const value = moduleExports['trailsOverlays'];
|
|
148
|
+
if (value === undefined) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
if (!Array.isArray(value) || !value.every(isOverlay)) {
|
|
152
|
+
throw new ValidationError(
|
|
153
|
+
`trailsOverlays export in "${sourceLabel}" must be an array of overlays ({ namespace, schema, derive }). Fix the app module export and rerun \`trails compile\`.`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return value;
|
|
158
|
+
};
|