@telorun/analyzer 0.1.3 → 0.2.0
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 +3 -3
- package/dist/analyzer.d.ts +6 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +45 -25
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +56 -24
- package/dist/cel-environment.d.ts +12 -5
- package/dist/cel-environment.d.ts.map +1 -1
- package/dist/cel-environment.js +31 -17
- package/dist/definition-registry.d.ts +5 -5
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +10 -10
- package/dist/dependency-graph.d.ts.map +1 -1
- package/dist/dependency-graph.js +9 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/kernel-globals.d.ts +6 -2
- package/dist/kernel-globals.d.ts.map +1 -1
- package/dist/kernel-globals.js +14 -8
- package/dist/manifest-loader.d.ts +9 -1
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +165 -11
- package/dist/module-kinds.d.ts +4 -0
- package/dist/module-kinds.d.ts.map +1 -0
- package/dist/module-kinds.js +4 -0
- package/dist/normalize-inline-resources.d.ts.map +1 -1
- package/dist/normalize-inline-resources.js +6 -1
- package/dist/precompile.d.ts +3 -2
- package/dist/precompile.d.ts.map +1 -1
- package/dist/precompile.js +13 -11
- package/dist/reference-field-map.d.ts +1 -1
- package/dist/resolve-throws-union.d.ts +30 -0
- package/dist/resolve-throws-union.d.ts.map +1 -0
- package/dist/resolve-throws-union.js +252 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-cel-context.js +1 -1
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +19 -12
- package/dist/validate-throws-coverage.d.ts +8 -0
- package/dist/validate-throws-coverage.d.ts.map +1 -0
- package/dist/validate-throws-coverage.js +461 -0
- package/package.json +3 -3
- package/src/analyzer.ts +60 -26
- package/src/builtins.ts +56 -24
- package/src/cel-environment.ts +40 -17
- package/src/definition-registry.ts +10 -10
- package/src/dependency-graph.ts +9 -2
- package/src/index.ts +2 -1
- package/src/kernel-globals.ts +19 -10
- package/src/manifest-loader.ts +202 -17
- package/src/module-kinds.ts +6 -0
- package/src/normalize-inline-resources.ts +6 -1
- package/src/precompile.ts +14 -11
- package/src/reference-field-map.ts +1 -1
- package/src/resolve-throws-union.ts +345 -0
- package/src/types.ts +13 -0
- package/src/validate-cel-context.ts +1 -1
- package/src/validate-references.ts +19 -12
- package/src/validate-throws-coverage.ts +565 -0
- package/dist/adapters/node-adapter.d.ts +0 -15
- package/dist/adapters/node-adapter.d.ts.map +0 -1
- package/dist/adapters/node-adapter.js +0 -33
- package/src/adapters/node-adapter.ts +0 -38
package/src/manifest-loader.ts
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
|
+
import type { Environment } from "@marcbachmann/cel-js";
|
|
1
2
|
import { isCompiledValue, type ResourceManifest } from "@telorun/sdk";
|
|
2
3
|
import { isMap, isPair, isScalar, isSeq, parseAllDocuments, type Document } from "yaml";
|
|
3
4
|
import { HttpAdapter } from "./adapters/http-adapter.js";
|
|
4
5
|
import { RegistryAdapter } from "./adapters/registry-adapter.js";
|
|
6
|
+
import { buildCelEnvironment } from "./cel-environment.js";
|
|
7
|
+
import { isModuleKind } from "./module-kinds.js";
|
|
5
8
|
import { precompileDoc } from "./precompile.js";
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_MANIFEST_FILENAME,
|
|
11
|
+
type LoadOptions,
|
|
12
|
+
type LoaderInitOptions,
|
|
13
|
+
type ManifestAdapter,
|
|
14
|
+
type Position,
|
|
15
|
+
type PositionIndex,
|
|
12
16
|
} from "./types.js";
|
|
13
17
|
|
|
18
|
+
const SYSTEM_KINDS = new Set([
|
|
19
|
+
"Telo.Application",
|
|
20
|
+
"Telo.Library",
|
|
21
|
+
"Telo.Import",
|
|
22
|
+
"Telo.Definition",
|
|
23
|
+
]);
|
|
24
|
+
|
|
14
25
|
export class Loader {
|
|
15
26
|
private static readonly moduleCache = new Map<
|
|
16
27
|
string,
|
|
@@ -18,6 +29,7 @@ export class Loader {
|
|
|
18
29
|
>();
|
|
19
30
|
|
|
20
31
|
protected adapters: ManifestAdapter[];
|
|
32
|
+
private readonly celEnv: Environment;
|
|
21
33
|
|
|
22
34
|
constructor(extraAdaptersOrOptions: ManifestAdapter[] | LoaderInitOptions = []) {
|
|
23
35
|
const options: LoaderInitOptions = Array.isArray(extraAdaptersOrOptions)
|
|
@@ -34,6 +46,8 @@ export class Loader {
|
|
|
34
46
|
if (options.extraAdapters?.length) {
|
|
35
47
|
this.adapters.unshift(...options.extraAdapters);
|
|
36
48
|
}
|
|
49
|
+
|
|
50
|
+
this.celEnv = buildCelEnvironment(options.celHandlers);
|
|
37
51
|
}
|
|
38
52
|
|
|
39
53
|
register(adapter: ManifestAdapter): this {
|
|
@@ -76,7 +90,7 @@ export class Loader {
|
|
|
76
90
|
let compiledDocs: unknown[];
|
|
77
91
|
if (options?.compile) {
|
|
78
92
|
try {
|
|
79
|
-
const result = precompileDoc(rawDoc);
|
|
93
|
+
const result = precompileDoc(rawDoc, this.celEnv);
|
|
80
94
|
compiledDocs = Array.isArray(result) ? result : [result];
|
|
81
95
|
} catch (error) {
|
|
82
96
|
throw new Error(
|
|
@@ -103,17 +117,19 @@ export class Loader {
|
|
|
103
117
|
}
|
|
104
118
|
}
|
|
105
119
|
|
|
106
|
-
const moduleManifests = resolved.filter((m) => m.kind
|
|
120
|
+
const moduleManifests = resolved.filter((m) => isModuleKind(m.kind));
|
|
107
121
|
if (moduleManifests.length > 1) {
|
|
122
|
+
const kinds = moduleManifests.map((m) => m.kind).join(", ");
|
|
108
123
|
throw new Error(
|
|
109
|
-
`File '${source}' contains ${moduleManifests.length}
|
|
124
|
+
`File '${source}' contains ${moduleManifests.length} module declarations (${kinds}). ` +
|
|
125
|
+
`A file may declare at most one Telo.Application or Telo.Library.`,
|
|
110
126
|
);
|
|
111
127
|
}
|
|
112
128
|
const moduleManifest = moduleManifests[0];
|
|
113
129
|
const moduleName = moduleManifest?.metadata?.name as string | undefined;
|
|
114
130
|
if (moduleName) {
|
|
115
131
|
for (const manifest of resolved) {
|
|
116
|
-
if (manifest.kind
|
|
132
|
+
if (!isModuleKind(manifest.kind) && !manifest.metadata?.module) {
|
|
117
133
|
const pi = (manifest.metadata as any)?.positionIndex;
|
|
118
134
|
manifest.metadata = { ...manifest.metadata, module: moduleName };
|
|
119
135
|
if (pi) {
|
|
@@ -128,10 +144,152 @@ export class Loader {
|
|
|
128
144
|
}
|
|
129
145
|
}
|
|
130
146
|
|
|
131
|
-
|
|
147
|
+
// Expand include directives — load partial files into the same module scope.
|
|
148
|
+
// Results with includes are NOT cached because partial file content is not
|
|
149
|
+
// tracked in the cache key — the cache would serve stale data if a partial changes.
|
|
150
|
+
let hasIncludes = false;
|
|
151
|
+
if (moduleManifest) {
|
|
152
|
+
const includePatterns = (moduleManifest as any).include as string[] | undefined;
|
|
153
|
+
if (includePatterns?.length) {
|
|
154
|
+
hasIncludes = true;
|
|
155
|
+
const adapter = this.pick(source);
|
|
156
|
+
const includedFiles = await this.resolveIncludes(source, includePatterns, adapter);
|
|
157
|
+
for (const includedUrl of includedFiles) {
|
|
158
|
+
const partialManifests = await this.loadPartialFile(includedUrl, moduleName, options);
|
|
159
|
+
resolved.push(...partialManifests);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!hasIncludes) {
|
|
165
|
+
Loader.moduleCache.set(cacheKey, { text, manifests: resolved });
|
|
166
|
+
}
|
|
132
167
|
return cloneManifestArray(resolved);
|
|
133
168
|
}
|
|
134
169
|
|
|
170
|
+
private async resolveIncludes(
|
|
171
|
+
ownerSource: string,
|
|
172
|
+
patterns: string[],
|
|
173
|
+
adapter: ManifestAdapter,
|
|
174
|
+
): Promise<string[]> {
|
|
175
|
+
const hasGlobs = patterns.some((p) => /[*?{}\[\]]/.test(p));
|
|
176
|
+
if (hasGlobs) {
|
|
177
|
+
if (!adapter.expandGlob) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`Include patterns in '${ownerSource}' contain globs but the adapter for this source ` +
|
|
180
|
+
`does not support glob expansion. Use explicit file paths instead of patterns like: ` +
|
|
181
|
+
patterns.filter((p) => /[*?{}\[\]]/.test(p)).join(", "),
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
return adapter.expandGlob(ownerSource, patterns);
|
|
185
|
+
}
|
|
186
|
+
// Literal relative paths — deduplicate in case the same file appears under multiple patterns.
|
|
187
|
+
return [...new Set(patterns.map((p) => adapter.resolveRelative(ownerSource, p)))];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private async loadPartialFile(
|
|
191
|
+
url: string,
|
|
192
|
+
ownerModuleName: string | undefined,
|
|
193
|
+
options?: LoadOptions,
|
|
194
|
+
): Promise<ResourceManifest[]> {
|
|
195
|
+
const { text, source } = await this.pick(url).read(url);
|
|
196
|
+
|
|
197
|
+
const parsedDocuments = parseAllDocuments(text);
|
|
198
|
+
const rawDocs = parsedDocuments.map((d) => d.toJSON());
|
|
199
|
+
const offsets = documentLineOffsets(text);
|
|
200
|
+
const lineOffsets = buildLineOffsets(text);
|
|
201
|
+
const resolved: ResourceManifest[] = [];
|
|
202
|
+
let docIdx = 0;
|
|
203
|
+
|
|
204
|
+
for (const rawDoc of rawDocs) {
|
|
205
|
+
const currentDocIdx = docIdx++;
|
|
206
|
+
const sourceLine = offsets[currentDocIdx] ?? 0;
|
|
207
|
+
const positionIndex = buildPositionIndex(parsedDocuments[currentDocIdx], lineOffsets);
|
|
208
|
+
if (rawDoc === null || rawDoc === undefined) continue;
|
|
209
|
+
|
|
210
|
+
const kind = rawDoc.kind as string | undefined;
|
|
211
|
+
if (kind && SYSTEM_KINDS.has(kind)) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Included file '${source}' contains '${kind}' which is not allowed in partial files. ` +
|
|
214
|
+
`Only the owner telo.yaml may declare ${kind} resources.`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let compiledDocs: unknown[];
|
|
219
|
+
if (options?.compile) {
|
|
220
|
+
try {
|
|
221
|
+
const result = precompileDoc(rawDoc, this.celEnv);
|
|
222
|
+
compiledDocs = Array.isArray(result) ? result : [result];
|
|
223
|
+
} catch (error) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`Failed to compile manifest in ${source}: ${error instanceof Error ? error.message : String(error)}`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
compiledDocs = [rawDoc];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for (const doc of compiledDocs) {
|
|
233
|
+
if (doc === null || doc === undefined) continue;
|
|
234
|
+
const manifest = doc as ResourceManifest;
|
|
235
|
+
const metadata = {
|
|
236
|
+
...manifest.metadata,
|
|
237
|
+
source,
|
|
238
|
+
sourceLine,
|
|
239
|
+
...(ownerModuleName && !manifest.metadata?.module ? { module: ownerModuleName } : {}),
|
|
240
|
+
};
|
|
241
|
+
Object.defineProperty(metadata, "positionIndex", {
|
|
242
|
+
value: positionIndex,
|
|
243
|
+
enumerable: false,
|
|
244
|
+
writable: true,
|
|
245
|
+
configurable: true,
|
|
246
|
+
});
|
|
247
|
+
resolved.push({ ...manifest, metadata });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return resolved;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async loadModuleForFile(
|
|
255
|
+
fileUrl: string,
|
|
256
|
+
): Promise<{
|
|
257
|
+
ownerUrl: string;
|
|
258
|
+
manifests: ResourceManifest[];
|
|
259
|
+
sourceManifests: Map<string, ResourceManifest[]>;
|
|
260
|
+
} | null> {
|
|
261
|
+
// Try loading as a regular module first (it might be a telo.yaml itself).
|
|
262
|
+
// Use loadManifests (not loadModule) so imported definitions are included —
|
|
263
|
+
// otherwise the analyzer won't know about kinds from Telo.Import sources.
|
|
264
|
+
try {
|
|
265
|
+
const docs = await this.loadModule(fileUrl);
|
|
266
|
+
const hasModule = docs.some((d) => isModuleKind(d.kind));
|
|
267
|
+
if (hasModule) {
|
|
268
|
+
const { source } = await this.pick(fileUrl).read(fileUrl);
|
|
269
|
+
const manifests = await this.loadManifests(fileUrl);
|
|
270
|
+
return { ownerUrl: source, manifests, sourceManifests: groupBySource(manifests) };
|
|
271
|
+
}
|
|
272
|
+
} catch (err) {
|
|
273
|
+
// If the file looks like an owner manifest (named telo.yaml), rethrow —
|
|
274
|
+
// a broken owner shouldn't silently fall through to parent lookup.
|
|
275
|
+
const normalized = fileUrl.replace(/\\/g, "/");
|
|
276
|
+
if (normalized.endsWith(`/${DEFAULT_MANIFEST_FILENAME}`) || normalized === DEFAULT_MANIFEST_FILENAME) {
|
|
277
|
+
throw err;
|
|
278
|
+
}
|
|
279
|
+
// Otherwise fall through to owner lookup — this is likely a partial file
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Find the owning telo.yaml via parent-directory traversal
|
|
283
|
+
const adapter = this.pick(fileUrl);
|
|
284
|
+
if (!adapter.resolveOwnerOf) return null;
|
|
285
|
+
const ownerUrl = await adapter.resolveOwnerOf(fileUrl);
|
|
286
|
+
if (!ownerUrl) return null;
|
|
287
|
+
|
|
288
|
+
// Load the owner module (which will load included files via include expansion)
|
|
289
|
+
const manifests = await this.loadManifests(ownerUrl);
|
|
290
|
+
return { ownerUrl, manifests, sourceManifests: groupBySource(manifests) };
|
|
291
|
+
}
|
|
292
|
+
|
|
135
293
|
async loadModuleGraph(
|
|
136
294
|
entryUrl: string,
|
|
137
295
|
onError?: (url: string, error: Error) => void,
|
|
@@ -146,7 +304,7 @@ export class Loader {
|
|
|
146
304
|
|
|
147
305
|
while (queue.length > 0) {
|
|
148
306
|
const m = queue.shift()!;
|
|
149
|
-
if (m.kind !== "
|
|
307
|
+
if (m.kind !== "Telo.Import") continue;
|
|
150
308
|
const importSource = (m as any).source as string | undefined;
|
|
151
309
|
if (!importSource) continue;
|
|
152
310
|
const base = (m.metadata as any)?.source ?? entryUrl;
|
|
@@ -166,7 +324,7 @@ export class Loader {
|
|
|
166
324
|
}
|
|
167
325
|
result.set(importUrl, imported);
|
|
168
326
|
for (const im of imported) {
|
|
169
|
-
if (im.kind === "
|
|
327
|
+
if (im.kind === "Telo.Import") queue.push(im);
|
|
170
328
|
}
|
|
171
329
|
}
|
|
172
330
|
|
|
@@ -182,7 +340,7 @@ export class Loader {
|
|
|
182
340
|
|
|
183
341
|
while (queue.length > 0) {
|
|
184
342
|
const m = queue.shift()!;
|
|
185
|
-
if (m.kind !== "
|
|
343
|
+
if (m.kind !== "Telo.Import") continue;
|
|
186
344
|
const importSource = (m as any).source as string | undefined;
|
|
187
345
|
if (!importSource) continue;
|
|
188
346
|
const base = (m.metadata as any)?.source ?? entryUrl;
|
|
@@ -200,7 +358,20 @@ export class Loader {
|
|
|
200
358
|
(e as any).sourceLine = (m.metadata as any)?.sourceLine ?? 0;
|
|
201
359
|
throw e;
|
|
202
360
|
}
|
|
203
|
-
|
|
361
|
+
// Import target must be a Telo.Library. Check the Library branch
|
|
362
|
+
// explicitly rather than "anything that's a module kind" so that a
|
|
363
|
+
// future third kind can't silently slip past as a valid import target.
|
|
364
|
+
const importedLibrary = imported.find((im) => im.kind === "Telo.Library");
|
|
365
|
+
const importedApplication = imported.find((im) => im.kind === "Telo.Application");
|
|
366
|
+
if (importedApplication) {
|
|
367
|
+
const e = new Error(
|
|
368
|
+
`Telo.Import target '${importSource}' is a Telo.Application. ` +
|
|
369
|
+
`Only Telo.Library modules may be imported. Applications are run directly, not imported.`,
|
|
370
|
+
);
|
|
371
|
+
(e as any).sourceLine = (m.metadata as any)?.sourceLine ?? 0;
|
|
372
|
+
throw e;
|
|
373
|
+
}
|
|
374
|
+
const importedModule = importedLibrary;
|
|
204
375
|
if (importedModule?.metadata?.name) {
|
|
205
376
|
const pi = (m.metadata as any)?.positionIndex;
|
|
206
377
|
m.metadata = {
|
|
@@ -218,8 +389,8 @@ export class Loader {
|
|
|
218
389
|
}
|
|
219
390
|
}
|
|
220
391
|
for (const im of imported) {
|
|
221
|
-
if (im.kind === "
|
|
222
|
-
if (im.kind === "
|
|
392
|
+
if (im.kind === "Telo.Definition") importedDefs.push(im);
|
|
393
|
+
if (im.kind === "Telo.Import") queue.push(im);
|
|
223
394
|
}
|
|
224
395
|
}
|
|
225
396
|
|
|
@@ -253,6 +424,20 @@ function cloneManifestValue<T>(value: T): T {
|
|
|
253
424
|
return value;
|
|
254
425
|
}
|
|
255
426
|
|
|
427
|
+
function groupBySource(manifests: ResourceManifest[]): Map<string, ResourceManifest[]> {
|
|
428
|
+
const map = new Map<string, ResourceManifest[]>();
|
|
429
|
+
for (const m of manifests) {
|
|
430
|
+
const src = (m.metadata?.source as string) ?? "unknown";
|
|
431
|
+
let list = map.get(src);
|
|
432
|
+
if (!list) {
|
|
433
|
+
list = [];
|
|
434
|
+
map.set(src, list);
|
|
435
|
+
}
|
|
436
|
+
list.push(m);
|
|
437
|
+
}
|
|
438
|
+
return map;
|
|
439
|
+
}
|
|
440
|
+
|
|
256
441
|
function documentLineOffsets(text: string): number[] {
|
|
257
442
|
const offsets = [0];
|
|
258
443
|
const lines = text.split("\n");
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const MODULE_KINDS = ["Telo.Application", "Telo.Library"] as const;
|
|
2
|
+
export type ModuleKind = (typeof MODULE_KINDS)[number];
|
|
3
|
+
|
|
4
|
+
export function isModuleKind(kind: string | undefined): kind is ModuleKind {
|
|
5
|
+
return kind === "Telo.Application" || kind === "Telo.Library";
|
|
6
|
+
}
|
|
@@ -3,7 +3,12 @@ import { isRefEntry, isScopeEntry, isInlineResource } from "./reference-field-ma
|
|
|
3
3
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
4
4
|
import type { AliasResolver } from "./alias-resolver.js";
|
|
5
5
|
|
|
6
|
-
const SYSTEM_KINDS = new Set([
|
|
6
|
+
const SYSTEM_KINDS = new Set([
|
|
7
|
+
"Telo.Definition",
|
|
8
|
+
"Telo.Application",
|
|
9
|
+
"Telo.Library",
|
|
10
|
+
"Telo.Import",
|
|
11
|
+
]);
|
|
7
12
|
|
|
8
13
|
/** Replaces characters outside [a-zA-Z0-9_] with underscores. */
|
|
9
14
|
function sanitizeName(raw: string): string {
|
package/src/precompile.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CompiledValue } from "@telorun/sdk";
|
|
2
|
-
import {
|
|
2
|
+
import type { Environment } from "@marcbachmann/cel-js";
|
|
3
3
|
|
|
4
4
|
const TEMPLATE_REGEX = /\$\{\{\s*([^}]+?)\s*\}\}/g;
|
|
5
5
|
const EXACT_TEMPLATE_REGEX = /^\s*\$\{\{\s*([^}]+?)\s*\}\}\s*$/;
|
|
@@ -8,31 +8,32 @@ const EXACT_TEMPLATE_REGEX = /^\s*\$\{\{\s*([^}]+?)\s*\}\}\s*$/;
|
|
|
8
8
|
* Walks a raw YAML document and replaces all "${{ expr }}" strings with
|
|
9
9
|
* CompiledValue wrappers. Throws on CEL syntax errors.
|
|
10
10
|
* Intended to be called once per document at load time.
|
|
11
|
-
*
|
|
11
|
+
* Telo.Definition documents are returned unchanged — their schema fields
|
|
12
12
|
* are static metadata and must not be treated as CEL templates.
|
|
13
13
|
*/
|
|
14
|
-
export function precompileDoc(doc: unknown): unknown {
|
|
15
|
-
if (typeof doc === "string") return compileString(doc);
|
|
16
|
-
if (Array.isArray(doc)) return doc.map(precompileDoc);
|
|
14
|
+
export function precompileDoc(doc: unknown, env: Environment): unknown {
|
|
15
|
+
if (typeof doc === "string") return compileString(doc, env);
|
|
16
|
+
if (Array.isArray(doc)) return doc.map((item) => precompileDoc(item, env));
|
|
17
17
|
// Only recurse into plain objects. Class instances (ResourceInstance, ScopeHandle, etc.)
|
|
18
18
|
// are returned as-is — their prototype methods must not be lost by object reconstruction.
|
|
19
19
|
if (doc !== null && typeof doc === "object" && Object.getPrototypeOf(doc) === Object.prototype) {
|
|
20
20
|
const result: Record<string, unknown> = {};
|
|
21
21
|
for (const [k, v] of Object.entries(doc as Record<string, unknown>)) {
|
|
22
|
-
result[k] = precompileDoc(v);
|
|
22
|
+
result[k] = precompileDoc(v, env);
|
|
23
23
|
}
|
|
24
24
|
return result;
|
|
25
25
|
}
|
|
26
26
|
return doc;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
function compileString(s: string): unknown {
|
|
29
|
+
function compileString(s: string, env: Environment): unknown {
|
|
30
30
|
if (!s.includes("${{")) return s;
|
|
31
31
|
|
|
32
32
|
const exact = s.match(EXACT_TEMPLATE_REGEX);
|
|
33
33
|
if (exact) {
|
|
34
|
-
const
|
|
35
|
-
|
|
34
|
+
const expr = exact[1].trim();
|
|
35
|
+
const fn = env.parse(expr);
|
|
36
|
+
return { __compiled: true, source: expr, call: (ctx: Record<string, unknown>) => fn(ctx) } satisfies CompiledValue;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
// Interpolated template — collect literal parts + compiled sub-expressions
|
|
@@ -40,14 +41,16 @@ function compileString(s: string): unknown {
|
|
|
40
41
|
let last = 0;
|
|
41
42
|
for (const m of s.matchAll(TEMPLATE_REGEX)) {
|
|
42
43
|
if (m.index! > last) parts.push(s.slice(last, m.index));
|
|
43
|
-
const
|
|
44
|
-
|
|
44
|
+
const expr = m[1].trim();
|
|
45
|
+
const fn = env.parse(expr);
|
|
46
|
+
parts.push({ __compiled: true, source: expr, call: (ctx: Record<string, unknown>) => fn(ctx) } satisfies CompiledValue);
|
|
45
47
|
last = m.index! + m[0].length;
|
|
46
48
|
}
|
|
47
49
|
if (last < s.length) parts.push(s.slice(last));
|
|
48
50
|
|
|
49
51
|
return {
|
|
50
52
|
__compiled: true,
|
|
53
|
+
source: s,
|
|
51
54
|
call: (ctx: Record<string, unknown>) =>
|
|
52
55
|
parts.map((p) => (typeof p === "string" ? p : String(p.call(ctx) ?? ""))).join(""),
|
|
53
56
|
} satisfies CompiledValue;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** An entry for a field that carries one or more x-telo-ref constraints. */
|
|
2
2
|
export interface RefFieldEntry {
|
|
3
|
-
/** One or more canonical ref strings ("namespace/module#TypeName" or "
|
|
3
|
+
/** One or more canonical ref strings ("namespace/module#TypeName" or "telo#TypeName").
|
|
4
4
|
* Multiple entries arise from anyOf branches. */
|
|
5
5
|
refs: string[];
|
|
6
6
|
/** True when the field path traversed through at least one array (path contains "[]"). */
|