@telorun/analyzer 0.15.0 → 0.16.1
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/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +56 -0
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +58 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/inline-imports.d.ts +34 -0
- package/dist/inline-imports.d.ts.map +1 -0
- package/dist/inline-imports.js +106 -0
- package/dist/manifest-loader.d.ts +10 -1
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +45 -22
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/analyzer.ts +56 -0
- package/src/builtins.ts +58 -0
- package/src/index.ts +2 -0
- package/src/inline-imports.ts +130 -0
- package/src/manifest-loader.ts +52 -21
- package/src/types.ts +9 -0
package/dist/analyzer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAiB9B,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAuhB/F,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,OAAO,GAAE,qBAA0B;IAI/C;;;;;;;;;;;;;;OAcG;IACH,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAiB9B,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAuhB/F,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,OAAO,GAAE,qBAA0B;IAI/C;;;;;;;;;;;;;;OAcG;IACH,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAypBvB,aAAa,CACX,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAMvB,SAAS,CACP,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,gBAAgB,EAI1B,kBAAkB,CAAC,EAAE,gBAAgB,EAAE,GACtC,gBAAgB,EAAE;IAqBrB,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,gBAAgB,GACzB;QAAE,WAAW,EAAE,kBAAkB,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;CAsB5F"}
|
package/dist/analyzer.js
CHANGED
|
@@ -498,6 +498,11 @@ export class StaticAnalyzer {
|
|
|
498
498
|
}
|
|
499
499
|
}
|
|
500
500
|
const aliasesByModule = ctx?.aliasesByModule ?? new Map();
|
|
501
|
+
// Per-module-scope seen aliases for DUPLICATE_IMPORT_ALIAS. Authored
|
|
502
|
+
// Telo.Import docs and synthetic-from-inline-`imports:` share one alias
|
|
503
|
+
// namespace per module, so a repeat — across either form — is an error
|
|
504
|
+
// rather than the silent last-writer-wins the resolver would otherwise do.
|
|
505
|
+
const seenAliasByScope = new Map();
|
|
501
506
|
for (const m of manifests) {
|
|
502
507
|
if (isModuleKind(m.kind)) {
|
|
503
508
|
const namespace = m.metadata.namespace ?? null;
|
|
@@ -535,6 +540,34 @@ export class StaticAnalyzer {
|
|
|
535
540
|
const resolvedModuleName = m.metadata.resolvedModuleName;
|
|
536
541
|
const resolvedNamespace = m.metadata.resolvedNamespace;
|
|
537
542
|
const ownModule = m.metadata?.module;
|
|
543
|
+
if (alias) {
|
|
544
|
+
const scopeKey = ownModule ?? "";
|
|
545
|
+
let seen = seenAliasByScope.get(scopeKey);
|
|
546
|
+
if (!seen) {
|
|
547
|
+
seen = new Set();
|
|
548
|
+
seenAliasByScope.set(scopeKey, seen);
|
|
549
|
+
}
|
|
550
|
+
if (seen.has(alias)) {
|
|
551
|
+
diagnostics.push({
|
|
552
|
+
severity: DiagnosticSeverity.Error,
|
|
553
|
+
code: "DUPLICATE_IMPORT_ALIAS",
|
|
554
|
+
source: SOURCE,
|
|
555
|
+
message: `Duplicate import alias '${alias}'. An alias may be declared once per module — ` +
|
|
556
|
+
`across both inline 'imports:' entries and 'Telo.Import' documents. ` +
|
|
557
|
+
`Rename or remove the duplicate.`,
|
|
558
|
+
data: {
|
|
559
|
+
resource: { kind: "Telo.Import", name: alias },
|
|
560
|
+
filePath: m.metadata?.source,
|
|
561
|
+
path: "metadata.name",
|
|
562
|
+
},
|
|
563
|
+
});
|
|
564
|
+
// Keep the first alias→target mapping intact; don't re-register the
|
|
565
|
+
// duplicate (last-writer-wins would shadow the original and cascade
|
|
566
|
+
// misleading follow-on diagnostics).
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
seen.add(alias);
|
|
570
|
+
}
|
|
538
571
|
if (alias && source) {
|
|
539
572
|
const targetModule = resolvedModuleName ?? source.split("/").filter(Boolean).pop() ?? source;
|
|
540
573
|
// Module identity is registered globally so x-telo-ref resolution sees
|
|
@@ -557,6 +590,29 @@ export class StaticAnalyzer {
|
|
|
557
590
|
}
|
|
558
591
|
}
|
|
559
592
|
}
|
|
593
|
+
// Seed `Self` for every module that contributes definitions but whose own
|
|
594
|
+
// Telo.Library doc isn't in this manifest set. `flattenForAnalyzer` forwards an
|
|
595
|
+
// imported library's definitions/abstracts/imports but NOT its module doc, so the
|
|
596
|
+
// module-doc loop above can't register `Self` for imported modules. Without this, a
|
|
597
|
+
// definition's `extends: Self.X` (a kind defined in the same library as the abstract)
|
|
598
|
+
// can't resolve and its `extendedBy` edge mis-keys under the literal "Self.X" — which
|
|
599
|
+
// stays invisible until another module also implements that abstract and flips the
|
|
600
|
+
// reference check from lenient to strict. `Self` always maps a module to its own name.
|
|
601
|
+
for (const m of manifests) {
|
|
602
|
+
if (m.kind !== "Telo.Definition" && m.kind !== "Telo.Abstract")
|
|
603
|
+
continue;
|
|
604
|
+
const ownModule = m.metadata?.module;
|
|
605
|
+
if (!ownModule || rootModules.has(ownModule))
|
|
606
|
+
continue;
|
|
607
|
+
let libResolver = aliasesByModule.get(ownModule);
|
|
608
|
+
if (!libResolver) {
|
|
609
|
+
libResolver = new AliasResolver();
|
|
610
|
+
aliasesByModule.set(ownModule, libResolver);
|
|
611
|
+
}
|
|
612
|
+
if (!libResolver.hasAlias("Self")) {
|
|
613
|
+
libResolver.registerImport("Self", ownModule, []);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
560
616
|
// Register definitions from Telo.Definition AND Telo.Abstract resources.
|
|
561
617
|
// Abstracts declare contracts that implementations target via `extends` (canonical)
|
|
562
618
|
// or `capability: <AbstractKind>` (legacy). Until they're registered, validateReferences
|
package/dist/builtins.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,eAAO,MAAM,eAAe,EAAE,kBAAkB,
|
|
1
|
+
{"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,eAAO,MAAM,eAAe,EAAE,kBAAkB,EAsd/C,CAAC"}
|
package/dist/builtins.js
CHANGED
|
@@ -299,6 +299,37 @@ export const KERNEL_BUILTINS = [
|
|
|
299
299
|
type: "array",
|
|
300
300
|
items: { type: "string" },
|
|
301
301
|
},
|
|
302
|
+
// Inline imports — name-keyed map sugar for separate `Telo.Import`
|
|
303
|
+
// documents. The key is the PascalCase alias (the import's
|
|
304
|
+
// `metadata.name`). Each value is either a bare source string
|
|
305
|
+
// (shorthand for `{ source }`) or the full object form. The loader
|
|
306
|
+
// desugars each entry into a synthetic `Telo.Import` before discovery;
|
|
307
|
+
// authored `Telo.Import` docs keep working alongside this. See
|
|
308
|
+
// analyzer/nodejs/src/inline-imports.ts.
|
|
309
|
+
imports: {
|
|
310
|
+
type: "object",
|
|
311
|
+
additionalProperties: {
|
|
312
|
+
oneOf: [
|
|
313
|
+
{ type: "string" },
|
|
314
|
+
{
|
|
315
|
+
type: "object",
|
|
316
|
+
required: ["source"],
|
|
317
|
+
properties: {
|
|
318
|
+
source: { type: "string" },
|
|
319
|
+
variables: { type: "object" },
|
|
320
|
+
secrets: { type: "object" },
|
|
321
|
+
runtime: {
|
|
322
|
+
oneOf: [
|
|
323
|
+
{ type: "string" },
|
|
324
|
+
{ type: "array", items: { type: "string" } },
|
|
325
|
+
],
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
additionalProperties: false,
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
},
|
|
332
|
+
},
|
|
302
333
|
// Application-level environment contract. Each entry layers `env:`
|
|
303
334
|
// (required, names the source env var) and `default:` (optional, used
|
|
304
335
|
// when the env var is unset) on top of an open JSON Schema property
|
|
@@ -392,6 +423,33 @@ export const KERNEL_BUILTINS = [
|
|
|
392
423
|
type: "array",
|
|
393
424
|
items: { type: "string" },
|
|
394
425
|
},
|
|
426
|
+
// Inline imports — same name-keyed map sugar as Telo.Application; the
|
|
427
|
+
// loader desugars each entry into a synthetic Telo.Import. See the
|
|
428
|
+
// Application schema above and analyzer/nodejs/src/inline-imports.ts.
|
|
429
|
+
imports: {
|
|
430
|
+
type: "object",
|
|
431
|
+
additionalProperties: {
|
|
432
|
+
oneOf: [
|
|
433
|
+
{ type: "string" },
|
|
434
|
+
{
|
|
435
|
+
type: "object",
|
|
436
|
+
required: ["source"],
|
|
437
|
+
properties: {
|
|
438
|
+
source: { type: "string" },
|
|
439
|
+
variables: { type: "object" },
|
|
440
|
+
secrets: { type: "object" },
|
|
441
|
+
runtime: {
|
|
442
|
+
oneOf: [
|
|
443
|
+
{ type: "string" },
|
|
444
|
+
{ type: "array", items: { type: "string" } },
|
|
445
|
+
],
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
additionalProperties: false,
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
},
|
|
452
|
+
},
|
|
395
453
|
exports: {
|
|
396
454
|
type: "object",
|
|
397
455
|
properties: {
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export { isModuleKind, MODULE_KINDS } from "./module-kinds.js";
|
|
|
9
9
|
export type { ModuleKind } from "./module-kinds.js";
|
|
10
10
|
export { parseLoadedFile } from "./parse-loaded-file.js";
|
|
11
11
|
export type { ParseOptions } from "./parse-loaded-file.js";
|
|
12
|
+
export { desugarLoadedFile, inlineImportManifests } from "./inline-imports.js";
|
|
13
|
+
export type { SyntheticImport } from "./inline-imports.js";
|
|
12
14
|
export { residualEntrySchema, residualEntrySchemaMap } from "./residual-schema.js";
|
|
13
15
|
export { buildDocumentPositions, buildLineOffsets, buildPositionIndex, documentLineOffsets, } from "./position-metadata.js";
|
|
14
16
|
export type { DocumentPosition } from "./position-metadata.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EACR,cAAc,EACd,UAAU,EACV,UAAU,EACV,WAAW,EACX,YAAY,EACZ,UAAU,GACb,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACpF,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,YAAY,EACR,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,mBAAmB,EACnB,YAAY,GACf,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC/D,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AACnF,OAAO,EACH,sBAAsB,EACtB,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,GACtB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC3E,YAAY,EACR,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,WAAW,EACX,cAAc,EACd,QAAQ,EACR,aAAa,EACb,KAAK,EACR,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EACR,cAAc,EACd,UAAU,EACV,UAAU,EACV,WAAW,EACX,YAAY,EACZ,UAAU,GACb,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACpF,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,YAAY,EACR,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,mBAAmB,EACnB,YAAY,GACf,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC/D,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC/E,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AACnF,OAAO,EACH,sBAAsB,EACtB,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,GACtB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC3E,YAAY,EACR,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,WAAW,EACX,cAAc,EACd,QAAQ,EACR,aAAa,EACb,KAAK,EACR,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { visitManifest } from "./manifest-visitor.js";
|
|
|
5
5
|
export { Loader } from "./manifest-loader.js";
|
|
6
6
|
export { isModuleKind, MODULE_KINDS } from "./module-kinds.js";
|
|
7
7
|
export { parseLoadedFile } from "./parse-loaded-file.js";
|
|
8
|
+
export { desugarLoadedFile, inlineImportManifests } from "./inline-imports.js";
|
|
8
9
|
export { residualEntrySchema, residualEntrySchemaMap } from "./residual-schema.js";
|
|
9
10
|
export { buildDocumentPositions, buildLineOffsets, buildPositionIndex, documentLineOffsets, } from "./position-metadata.js";
|
|
10
11
|
export { HttpSource } from "./sources/http-source.js";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import type { LoadedFile } from "./loaded-types.js";
|
|
3
|
+
import type { DocumentPosition } from "./position-metadata.js";
|
|
4
|
+
/** A synthetic `Telo.Import` produced by desugaring an `imports:` map entry,
|
|
5
|
+
* paired with the position metadata that pins its diagnostics back to the
|
|
6
|
+
* authoring line in the module document. */
|
|
7
|
+
export interface SyntheticImport {
|
|
8
|
+
manifest: ResourceManifest;
|
|
9
|
+
position: DocumentPosition;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Desugar a module document's inline `imports:` map into synthetic
|
|
13
|
+
* `Telo.Import` manifests. Each entry value is either a bare source string
|
|
14
|
+
* (shorthand for `{ source }`) or the full object form carrying
|
|
15
|
+
* `variables` / `secrets` / `runtime`. Malformed entries (object without a
|
|
16
|
+
* string `source`) are skipped here — the module document's own schema
|
|
17
|
+
* validation reports them against the precise `imports.<Alias>.source` path.
|
|
18
|
+
*
|
|
19
|
+
* The synthetic manifests are indistinguishable from authored `Telo.Import`
|
|
20
|
+
* documents downstream (alias registration, discovery, the kernel's
|
|
21
|
+
* import-controller), so the feature is purely additive at the declaration
|
|
22
|
+
* site. Pure and browser-safe — no I/O, no Node built-ins.
|
|
23
|
+
*/
|
|
24
|
+
export declare function inlineImportManifests(moduleManifest: ResourceManifest, modulePosition: DocumentPosition | undefined): SyntheticImport[];
|
|
25
|
+
/** Returns a copy of `file` with synthetic `Telo.Import` manifests (from the
|
|
26
|
+
* module document's inline `imports:` map) appended to `manifests` and
|
|
27
|
+
* `positions`. `documents` is intentionally left untouched: it is the raw
|
|
28
|
+
* YAML-AST array round-trip consumers pair by index, and a synthetic import
|
|
29
|
+
* has no backing node. Every flatten/discovery loop iterates `manifests` and
|
|
30
|
+
* indexes `positions[i]` — never `documents[i]` in lockstep — so the trailing
|
|
31
|
+
* synthetics are visible to resolution while the AST round-trip stays intact.
|
|
32
|
+
* Returns `file` unchanged when there is no module doc or no inline imports. */
|
|
33
|
+
export declare function desugarLoadedFile(file: LoadedFile): LoadedFile;
|
|
34
|
+
//# sourceMappingURL=inline-imports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inline-imports.d.ts","sourceRoot":"","sources":["../src/inline-imports.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG/D;;6CAE6C;AAC7C,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,QAAQ,EAAE,gBAAgB,CAAC;CAC5B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CACnC,cAAc,EAAE,gBAAgB,EAChC,cAAc,EAAE,gBAAgB,GAAG,SAAS,GAC3C,eAAe,EAAE,CA0BnB;AAED;;;;;;;iFAOiF;AACjF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,CAmB9D"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { isModuleKind } from "./module-kinds.js";
|
|
2
|
+
/**
|
|
3
|
+
* Desugar a module document's inline `imports:` map into synthetic
|
|
4
|
+
* `Telo.Import` manifests. Each entry value is either a bare source string
|
|
5
|
+
* (shorthand for `{ source }`) or the full object form carrying
|
|
6
|
+
* `variables` / `secrets` / `runtime`. Malformed entries (object without a
|
|
7
|
+
* string `source`) are skipped here — the module document's own schema
|
|
8
|
+
* validation reports them against the precise `imports.<Alias>.source` path.
|
|
9
|
+
*
|
|
10
|
+
* The synthetic manifests are indistinguishable from authored `Telo.Import`
|
|
11
|
+
* documents downstream (alias registration, discovery, the kernel's
|
|
12
|
+
* import-controller), so the feature is purely additive at the declaration
|
|
13
|
+
* site. Pure and browser-safe — no I/O, no Node built-ins.
|
|
14
|
+
*/
|
|
15
|
+
export function inlineImportManifests(moduleManifest, modulePosition) {
|
|
16
|
+
const raw = moduleManifest.imports;
|
|
17
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
18
|
+
return [];
|
|
19
|
+
const out = [];
|
|
20
|
+
for (const [alias, value] of Object.entries(raw)) {
|
|
21
|
+
const scalar = typeof value === "string";
|
|
22
|
+
const entry = scalar
|
|
23
|
+
? { source: value }
|
|
24
|
+
: value && typeof value === "object" && !Array.isArray(value)
|
|
25
|
+
? value
|
|
26
|
+
: undefined;
|
|
27
|
+
if (!entry || typeof entry.source !== "string")
|
|
28
|
+
continue;
|
|
29
|
+
const manifest = {
|
|
30
|
+
kind: "Telo.Import",
|
|
31
|
+
metadata: { name: alias },
|
|
32
|
+
source: entry.source,
|
|
33
|
+
...(entry.variables !== undefined ? { variables: entry.variables } : {}),
|
|
34
|
+
...(entry.secrets !== undefined ? { secrets: entry.secrets } : {}),
|
|
35
|
+
...(entry.runtime !== undefined ? { runtime: entry.runtime } : {}),
|
|
36
|
+
};
|
|
37
|
+
out.push({ manifest, position: synthPosition(modulePosition, alias, scalar) });
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
/** Returns a copy of `file` with synthetic `Telo.Import` manifests (from the
|
|
42
|
+
* module document's inline `imports:` map) appended to `manifests` and
|
|
43
|
+
* `positions`. `documents` is intentionally left untouched: it is the raw
|
|
44
|
+
* YAML-AST array round-trip consumers pair by index, and a synthetic import
|
|
45
|
+
* has no backing node. Every flatten/discovery loop iterates `manifests` and
|
|
46
|
+
* indexes `positions[i]` — never `documents[i]` in lockstep — so the trailing
|
|
47
|
+
* synthetics are visible to resolution while the AST round-trip stays intact.
|
|
48
|
+
* Returns `file` unchanged when there is no module doc or no inline imports. */
|
|
49
|
+
export function desugarLoadedFile(file) {
|
|
50
|
+
let moduleIndex = -1;
|
|
51
|
+
for (let i = 0; i < file.manifests.length; i++) {
|
|
52
|
+
const m = file.manifests[i];
|
|
53
|
+
if (m && isModuleKind(m.kind)) {
|
|
54
|
+
moduleIndex = i;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (moduleIndex < 0)
|
|
59
|
+
return file;
|
|
60
|
+
const synthetic = inlineImportManifests(file.manifests[moduleIndex], file.positions[moduleIndex]);
|
|
61
|
+
if (synthetic.length === 0)
|
|
62
|
+
return file;
|
|
63
|
+
return {
|
|
64
|
+
...file,
|
|
65
|
+
manifests: [...file.manifests, ...synthetic.map((s) => s.manifest)],
|
|
66
|
+
positions: [...file.positions, ...synthetic.map((s) => s.position)],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Build a `DocumentPosition` for a synthetic import by re-rooting the module
|
|
70
|
+
* document's `imports.<Alias>` position subtree at the import manifest's own
|
|
71
|
+
* paths (`source`, `variables.*`, `metadata.name`, …). This makes a
|
|
72
|
+
* diagnostic on the synthetic's `source` land on the `imports:` entry's
|
|
73
|
+
* authoring line rather than a phantom document. */
|
|
74
|
+
function synthPosition(modulePosition, alias, scalar) {
|
|
75
|
+
if (!modulePosition)
|
|
76
|
+
return { sourceLine: 0, positionIndex: new Map() };
|
|
77
|
+
const base = modulePosition.positionIndex;
|
|
78
|
+
const index = new Map();
|
|
79
|
+
const keyRange = base.get(`@key:imports.${alias}`);
|
|
80
|
+
const valueRange = base.get(`imports.${alias}`);
|
|
81
|
+
if (keyRange) {
|
|
82
|
+
index.set("metadata.name", keyRange);
|
|
83
|
+
index.set("@key:metadata.name", keyRange);
|
|
84
|
+
}
|
|
85
|
+
if (scalar) {
|
|
86
|
+
// `Console: std/console@1.2.3` — the entry value IS the source scalar.
|
|
87
|
+
if (valueRange)
|
|
88
|
+
index.set("source", valueRange);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const valuePrefix = `imports.${alias}.`;
|
|
92
|
+
const keyPrefix = `@key:imports.${alias}.`;
|
|
93
|
+
for (const [path, range] of base) {
|
|
94
|
+
if (path.startsWith(valuePrefix)) {
|
|
95
|
+
index.set(path.slice(valuePrefix.length), range);
|
|
96
|
+
}
|
|
97
|
+
else if (path.startsWith(keyPrefix)) {
|
|
98
|
+
index.set(`@key:${path.slice(keyPrefix.length)}`, range);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (valueRange)
|
|
103
|
+
index.set("", valueRange);
|
|
104
|
+
const sourceLine = (keyRange ?? valueRange)?.start.line ?? modulePosition.sourceLine;
|
|
105
|
+
return { sourceLine, positionIndex: index };
|
|
106
|
+
}
|
|
@@ -30,6 +30,15 @@ export declare class Loader {
|
|
|
30
30
|
* private mutable copy must call `parseLoadedFile` directly with the
|
|
31
31
|
* LoadedFile's `text`. */
|
|
32
32
|
loadFile(url: string, options?: LoadOptions): Promise<LoadedFile>;
|
|
33
|
+
/** Parse `text` into a LoadedFile, then desugar inline `imports:` when the
|
|
34
|
+
* caller opted in. Desugaring lives here, not in the pure `parseLoadedFile`,
|
|
35
|
+
* so round-trip consumers (the editor) keep a raw manifest/AST/position
|
|
36
|
+
* triple they can pair by index; only resolved consumers that pass
|
|
37
|
+
* `desugarImports` see synthetic Telo.Import manifests. */
|
|
38
|
+
private parseAndMaybeDesugar;
|
|
39
|
+
/** Raw text of any already-cached variant for `source`, so a cache miss on
|
|
40
|
+
* one (compile, desugar) variant reparses without a second source read. */
|
|
41
|
+
private findCachedText;
|
|
33
42
|
/** Load an owner file plus every partial reachable through its `include:`
|
|
34
43
|
* list. Globs are expanded via the owning source's `expandGlob`. The
|
|
35
44
|
* partials list is empty when the owner declares no `include:`. */
|
|
@@ -57,7 +66,7 @@ export declare class Loader {
|
|
|
57
66
|
* owner) and return the `LoadedGraph` rooted at that owner. Returns
|
|
58
67
|
* `null` only when `fileUrl` is neither an owner nor reachable from one
|
|
59
68
|
* via parent-directory traversal. */
|
|
60
|
-
loadGraphForFile(fileUrl: string): Promise<{
|
|
69
|
+
loadGraphForFile(fileUrl: string, options?: LoadOptions): Promise<{
|
|
61
70
|
graph: LoadedGraph;
|
|
62
71
|
ownerUrl: string;
|
|
63
72
|
} | null>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest-loader.d.ts","sourceRoot":"","sources":["../src/manifest-loader.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAGV,UAAU,EACV,WAAW,EACX,YAAY,EACb,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"manifest-loader.d.ts","sourceRoot":"","sources":["../src/manifest-loader.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAGV,UAAU,EACV,WAAW,EACX,YAAY,EACb,MAAM,mBAAmB,CAAC;AAI3B,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACpB,MAAM,YAAY,CAAC;AAiBpB,qBAAa,MAAM;IACjB;;;yEAGqE;IACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiC;IAE3D;;;;;8BAK0B;IAC1B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IAEzD,SAAS,CAAC,OAAO,EAAE,cAAc,EAAE,CAAC;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,qBAAqB,GAAE,cAAc,EAAE,GAAG,iBAAsB;IAmB5E,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI;IAKtC,OAAO,CAAC,IAAI;IAMN,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAUrD;;;;2CAIuC;IACvC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAM7C;;;+BAG2B;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IAyCvE;;;;gEAI4D;IAC5D,OAAO,CAAC,oBAAoB;IAa5B;gFAC4E;IAC5E,OAAO,CAAC,cAAc;IAQtB;;wEAEoE;IAC9D,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAsB3E;;;qCAGiC;IAC3B,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IAwG9E;;;;;;;;sBAQkB;IAClB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM;IAOlE,OAAO,CAAC,6BAA6B;IAarC,OAAO,CAAC,mCAAmC;IAc3C,OAAO,CAAC,2BAA2B;YAkCrB,eAAe;IAmB7B;;;0CAGsC;IAChC,gBAAgB,CACpB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC;QAAE,KAAK,EAAE,WAAW,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CA0B5D"}
|
package/dist/manifest-loader.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { HttpSource } from "./sources/http-source.js";
|
|
2
2
|
import { RegistrySource } from "./sources/registry-source.js";
|
|
3
3
|
import { buildCelEnvironment } from "./cel-environment.js";
|
|
4
|
+
import { desugarLoadedFile } from "./inline-imports.js";
|
|
4
5
|
import { isModuleKind } from "./module-kinds.js";
|
|
5
6
|
import { parseLoadedFile } from "./parse-loaded-file.js";
|
|
6
7
|
import { DEFAULT_MANIFEST_FILENAME, } from "./types.js";
|
|
@@ -10,6 +11,13 @@ const SYSTEM_KINDS = new Set([
|
|
|
10
11
|
"Telo.Import",
|
|
11
12
|
"Telo.Definition",
|
|
12
13
|
]);
|
|
14
|
+
/** File cache variant tags: compile (c/r) × desugarImports (d/n). A desugared
|
|
15
|
+
* and a raw load of the same file are distinct entries so neither sees the
|
|
16
|
+
* wrong manifest tree. */
|
|
17
|
+
const CACHE_VARIANTS = ["rn", "rd", "cn", "cd"];
|
|
18
|
+
function variantKey(options) {
|
|
19
|
+
return `${options?.compile ? "c" : "r"}${options?.desugarImports ? "d" : "n"}`;
|
|
20
|
+
}
|
|
13
21
|
export class Loader {
|
|
14
22
|
/** LoadedFile cache keyed by `${compile ? "compiled" : "raw"}:${source}`.
|
|
15
23
|
* Same dual-keying as the legacy ResourceManifest[] cache: a compile-mode
|
|
@@ -74,30 +82,26 @@ export class Loader {
|
|
|
74
82
|
* private mutable copy must call `parseLoadedFile` directly with the
|
|
75
83
|
* LoadedFile's `text`. */
|
|
76
84
|
async loadFile(url, options) {
|
|
77
|
-
const
|
|
85
|
+
const variant = variantKey(options);
|
|
78
86
|
const knownSource = this.urlToSource.get(url);
|
|
79
87
|
if (knownSource) {
|
|
80
|
-
const cached = this.fileCache.get(`${
|
|
88
|
+
const cached = this.fileCache.get(`${variant}:${knownSource}`);
|
|
81
89
|
if (cached)
|
|
82
90
|
return cached;
|
|
83
|
-
//
|
|
91
|
+
// Another variant of this source is cached — reparse from its text
|
|
84
92
|
// instead of re-reading the source.
|
|
85
93
|
//
|
|
86
94
|
// NOTE for watch-mode reactivation (cli/nodejs/src/commands/run.ts
|
|
87
95
|
// currently has `setupWatchMode` commented out): this branch
|
|
88
96
|
// assumes file contents don't change underneath a single Loader.
|
|
89
97
|
// Reviving watch mode will need a public `invalidate(url)` (or
|
|
90
|
-
// similar) that drops both `urlToSource[url]` and
|
|
91
|
-
//
|
|
92
|
-
// file again.
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
compile: options?.compile,
|
|
98
|
-
celEnv: this.celEnv,
|
|
99
|
-
});
|
|
100
|
-
this.fileCache.set(`${compileKey}:${knownSource}`, reparsed);
|
|
98
|
+
// similar) that drops both `urlToSource[url]` and every cached
|
|
99
|
+
// variant entry for its canonical source before the loader serves
|
|
100
|
+
// the file again.
|
|
101
|
+
const altText = this.findCachedText(knownSource);
|
|
102
|
+
if (altText !== undefined) {
|
|
103
|
+
const reparsed = this.parseAndMaybeDesugar(knownSource, url, altText, options);
|
|
104
|
+
this.fileCache.set(`${variant}:${knownSource}`, reparsed);
|
|
101
105
|
return reparsed;
|
|
102
106
|
}
|
|
103
107
|
}
|
|
@@ -109,16 +113,35 @@ export class Loader {
|
|
|
109
113
|
// for that exact URL — hit the urlToSource fast path instead of
|
|
110
114
|
// falling through to a redundant `pick(url).read(url)`.
|
|
111
115
|
this.urlToSource.set(source, source);
|
|
112
|
-
const cacheKey = `${
|
|
116
|
+
const cacheKey = `${variant}:${source}`;
|
|
113
117
|
const cached = this.fileCache.get(cacheKey);
|
|
114
118
|
if (cached && cached.text === text)
|
|
115
119
|
return cached;
|
|
116
|
-
const loaded =
|
|
120
|
+
const loaded = this.parseAndMaybeDesugar(source, url, text, options);
|
|
121
|
+
this.fileCache.set(cacheKey, loaded);
|
|
122
|
+
return loaded;
|
|
123
|
+
}
|
|
124
|
+
/** Parse `text` into a LoadedFile, then desugar inline `imports:` when the
|
|
125
|
+
* caller opted in. Desugaring lives here, not in the pure `parseLoadedFile`,
|
|
126
|
+
* so round-trip consumers (the editor) keep a raw manifest/AST/position
|
|
127
|
+
* triple they can pair by index; only resolved consumers that pass
|
|
128
|
+
* `desugarImports` see synthetic Telo.Import manifests. */
|
|
129
|
+
parseAndMaybeDesugar(source, requestedUrl, text, options) {
|
|
130
|
+
const loaded = parseLoadedFile(source, requestedUrl, text, {
|
|
117
131
|
compile: options?.compile,
|
|
118
132
|
celEnv: this.celEnv,
|
|
119
133
|
});
|
|
120
|
-
|
|
121
|
-
|
|
134
|
+
return options?.desugarImports ? desugarLoadedFile(loaded) : loaded;
|
|
135
|
+
}
|
|
136
|
+
/** Raw text of any already-cached variant for `source`, so a cache miss on
|
|
137
|
+
* one (compile, desugar) variant reparses without a second source read. */
|
|
138
|
+
findCachedText(source) {
|
|
139
|
+
for (const v of CACHE_VARIANTS) {
|
|
140
|
+
const cached = this.fileCache.get(`${v}:${source}`);
|
|
141
|
+
if (cached)
|
|
142
|
+
return cached.text;
|
|
143
|
+
}
|
|
144
|
+
return undefined;
|
|
122
145
|
}
|
|
123
146
|
/** Load an owner file plus every partial reachable through its `include:`
|
|
124
147
|
* list. Globs are expanded via the owning source's `expandGlob`. The
|
|
@@ -319,12 +342,12 @@ export class Loader {
|
|
|
319
342
|
* owner) and return the `LoadedGraph` rooted at that owner. Returns
|
|
320
343
|
* `null` only when `fileUrl` is neither an owner nor reachable from one
|
|
321
344
|
* via parent-directory traversal. */
|
|
322
|
-
async loadGraphForFile(fileUrl) {
|
|
345
|
+
async loadGraphForFile(fileUrl, options) {
|
|
323
346
|
try {
|
|
324
|
-
const owner = await this.loadFile(fileUrl);
|
|
347
|
+
const owner = await this.loadFile(fileUrl, options);
|
|
325
348
|
const isOwner = owner.manifests.some((m) => m && isModuleKind(m.kind));
|
|
326
349
|
if (isOwner) {
|
|
327
|
-
const graph = await this.loadGraph(fileUrl);
|
|
350
|
+
const graph = await this.loadGraph(fileUrl, options);
|
|
328
351
|
return { graph, ownerUrl: graph.rootSource };
|
|
329
352
|
}
|
|
330
353
|
}
|
|
@@ -341,7 +364,7 @@ export class Loader {
|
|
|
341
364
|
const ownerUrl = await source.resolveOwnerOf(fileUrl);
|
|
342
365
|
if (!ownerUrl)
|
|
343
366
|
return null;
|
|
344
|
-
const graph = await this.loadGraph(ownerUrl);
|
|
367
|
+
const graph = await this.loadGraph(ownerUrl, options);
|
|
345
368
|
return { graph, ownerUrl: graph.rootSource };
|
|
346
369
|
}
|
|
347
370
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -57,6 +57,15 @@ export interface LoadOptions {
|
|
|
57
57
|
* so the kernel can evaluate them at runtime. Leave unset (false) for static analysis —
|
|
58
58
|
* the analyzer works on raw strings and does not need compiled values. */
|
|
59
59
|
compile?: boolean;
|
|
60
|
+
/** When true, each module document's inline `imports:` map is desugared into
|
|
61
|
+
* synthetic `Telo.Import` manifests appended to the file's `manifests` /
|
|
62
|
+
* `positions` (the AST `documents` array is left raw). On for every resolved
|
|
63
|
+
* consumer — the kernel's analysis and runtime loads, and the analyzer — so
|
|
64
|
+
* inline imports participate in discovery, alias resolution, and execution.
|
|
65
|
+
* Off for the editor's round-trip view, which reads the raw `imports:` map and
|
|
66
|
+
* pairs manifests to YAML nodes by index. Folded into the file cache key so a
|
|
67
|
+
* desugared and a raw load of the same file never collide. */
|
|
68
|
+
desugarImports?: boolean;
|
|
60
69
|
}
|
|
61
70
|
export interface LoaderInitOptions {
|
|
62
71
|
/** Sources inserted with highest priority before built-ins. */
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;qHACqH;AACrH,eAAO,MAAM,kBAAkB;;;;;CAKrB,CAAC;AACX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,OAAO,kBAAkB,CAAC,CAAC;AAE9F,gFAAgF;AAChF,eAAO,MAAM,yBAAyB,cAAc,CAAC;AAErD,MAAM,WAAW,QAAQ;IACvB,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,QAAQ,CAAC;IAChB,GAAG,EAAE,QAAQ,CAAC;CACf;AAED;;oDAEoD;AACpD,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAE/C;6EAC6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,2BAA2B;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7D,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAExD;;qEAEiE;IACjE,UAAU,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAEjE;;qEAEiE;IACjE,cAAc,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC1D;AAED,MAAM,WAAW,WAAW;IAC1B;;;+EAG2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;qHACqH;AACrH,eAAO,MAAM,kBAAkB;;;;;CAKrB,CAAC;AACX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,OAAO,kBAAkB,CAAC,CAAC;AAE9F,gFAAgF;AAChF,eAAO,MAAM,yBAAyB,cAAc,CAAC;AAErD,MAAM,WAAW,QAAQ;IACvB,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,QAAQ,CAAC;IAChB,GAAG,EAAE,QAAQ,CAAC;CACf;AAED;;oDAEoD;AACpD,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAE/C;6EAC6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,2BAA2B;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7D,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAExD;;qEAEiE;IACjE,UAAU,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAEjE;;qEAEiE;IACjE,cAAc,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC1D;AAED,MAAM,WAAW,WAAW;IAC1B;;;+EAG2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;mEAO+D;IAC/D,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,+DAA+D;IAC/D,YAAY,CAAC,EAAE,cAAc,EAAE,CAAC;IAChC,qDAAqD;IACrD,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,yDAAyD;IACzD,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;6FACyF;IACzF,WAAW,CAAC,EAAE,OAAO,sBAAsB,EAAE,WAAW,CAAC;CAC1D;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;;;;;sDAUkD;IAClD,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;gEAKgE;AAChE,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,OAAO,qBAAqB,EAAE,aAAa,CAAC;IACtD,WAAW,CAAC,EAAE,OAAO,0BAA0B,EAAE,kBAAkB,CAAC;IACpE;;;;+EAI2E;IAC3E,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,qBAAqB,EAAE,aAAa,CAAC,CAAC;CAC5E"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/analyzer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@types/node": "^20.0.0",
|
|
49
49
|
"typescript": "^5.0.0",
|
|
50
50
|
"vitest": "^2.1.8",
|
|
51
|
-
"@telorun/sdk": "0.
|
|
51
|
+
"@telorun/sdk": "0.17.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"@telorun/sdk": "*"
|
package/src/analyzer.ts
CHANGED
|
@@ -621,6 +621,11 @@ export class StaticAnalyzer {
|
|
|
621
621
|
}
|
|
622
622
|
}
|
|
623
623
|
const aliasesByModule = ctx?.aliasesByModule ?? new Map<string, AliasResolver>();
|
|
624
|
+
// Per-module-scope seen aliases for DUPLICATE_IMPORT_ALIAS. Authored
|
|
625
|
+
// Telo.Import docs and synthetic-from-inline-`imports:` share one alias
|
|
626
|
+
// namespace per module, so a repeat — across either form — is an error
|
|
627
|
+
// rather than the silent last-writer-wins the resolver would otherwise do.
|
|
628
|
+
const seenAliasByScope = new Map<string, Set<string>>();
|
|
624
629
|
for (const m of manifests) {
|
|
625
630
|
if (isModuleKind(m.kind)) {
|
|
626
631
|
const namespace = ((m.metadata as any).namespace as string | undefined) ?? null;
|
|
@@ -659,6 +664,35 @@ export class StaticAnalyzer {
|
|
|
659
664
|
| null
|
|
660
665
|
| undefined;
|
|
661
666
|
const ownModule = (m.metadata as { module?: string } | undefined)?.module;
|
|
667
|
+
if (alias) {
|
|
668
|
+
const scopeKey = ownModule ?? "";
|
|
669
|
+
let seen = seenAliasByScope.get(scopeKey);
|
|
670
|
+
if (!seen) {
|
|
671
|
+
seen = new Set<string>();
|
|
672
|
+
seenAliasByScope.set(scopeKey, seen);
|
|
673
|
+
}
|
|
674
|
+
if (seen.has(alias)) {
|
|
675
|
+
diagnostics.push({
|
|
676
|
+
severity: DiagnosticSeverity.Error,
|
|
677
|
+
code: "DUPLICATE_IMPORT_ALIAS",
|
|
678
|
+
source: SOURCE,
|
|
679
|
+
message:
|
|
680
|
+
`Duplicate import alias '${alias}'. An alias may be declared once per module — ` +
|
|
681
|
+
`across both inline 'imports:' entries and 'Telo.Import' documents. ` +
|
|
682
|
+
`Rename or remove the duplicate.`,
|
|
683
|
+
data: {
|
|
684
|
+
resource: { kind: "Telo.Import", name: alias },
|
|
685
|
+
filePath: (m.metadata as { source?: string } | undefined)?.source,
|
|
686
|
+
path: "metadata.name",
|
|
687
|
+
},
|
|
688
|
+
});
|
|
689
|
+
// Keep the first alias→target mapping intact; don't re-register the
|
|
690
|
+
// duplicate (last-writer-wins would shadow the original and cascade
|
|
691
|
+
// misleading follow-on diagnostics).
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
seen.add(alias);
|
|
695
|
+
}
|
|
662
696
|
if (alias && source) {
|
|
663
697
|
const targetModule =
|
|
664
698
|
resolvedModuleName ?? source.split("/").filter(Boolean).pop() ?? source;
|
|
@@ -682,6 +716,28 @@ export class StaticAnalyzer {
|
|
|
682
716
|
}
|
|
683
717
|
}
|
|
684
718
|
|
|
719
|
+
// Seed `Self` for every module that contributes definitions but whose own
|
|
720
|
+
// Telo.Library doc isn't in this manifest set. `flattenForAnalyzer` forwards an
|
|
721
|
+
// imported library's definitions/abstracts/imports but NOT its module doc, so the
|
|
722
|
+
// module-doc loop above can't register `Self` for imported modules. Without this, a
|
|
723
|
+
// definition's `extends: Self.X` (a kind defined in the same library as the abstract)
|
|
724
|
+
// can't resolve and its `extendedBy` edge mis-keys under the literal "Self.X" — which
|
|
725
|
+
// stays invisible until another module also implements that abstract and flips the
|
|
726
|
+
// reference check from lenient to strict. `Self` always maps a module to its own name.
|
|
727
|
+
for (const m of manifests) {
|
|
728
|
+
if (m.kind !== "Telo.Definition" && m.kind !== "Telo.Abstract") continue;
|
|
729
|
+
const ownModule = (m.metadata as { module?: string } | undefined)?.module;
|
|
730
|
+
if (!ownModule || rootModules.has(ownModule)) continue;
|
|
731
|
+
let libResolver = aliasesByModule.get(ownModule);
|
|
732
|
+
if (!libResolver) {
|
|
733
|
+
libResolver = new AliasResolver();
|
|
734
|
+
aliasesByModule.set(ownModule, libResolver);
|
|
735
|
+
}
|
|
736
|
+
if (!libResolver.hasAlias("Self")) {
|
|
737
|
+
libResolver.registerImport("Self", ownModule, []);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
685
741
|
// Register definitions from Telo.Definition AND Telo.Abstract resources.
|
|
686
742
|
// Abstracts declare contracts that implementations target via `extends` (canonical)
|
|
687
743
|
// or `capability: <AbstractKind>` (legacy). Until they're registered, validateReferences
|
package/src/builtins.ts
CHANGED
|
@@ -301,6 +301,37 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
301
301
|
type: "array",
|
|
302
302
|
items: { type: "string" },
|
|
303
303
|
},
|
|
304
|
+
// Inline imports — name-keyed map sugar for separate `Telo.Import`
|
|
305
|
+
// documents. The key is the PascalCase alias (the import's
|
|
306
|
+
// `metadata.name`). Each value is either a bare source string
|
|
307
|
+
// (shorthand for `{ source }`) or the full object form. The loader
|
|
308
|
+
// desugars each entry into a synthetic `Telo.Import` before discovery;
|
|
309
|
+
// authored `Telo.Import` docs keep working alongside this. See
|
|
310
|
+
// analyzer/nodejs/src/inline-imports.ts.
|
|
311
|
+
imports: {
|
|
312
|
+
type: "object",
|
|
313
|
+
additionalProperties: {
|
|
314
|
+
oneOf: [
|
|
315
|
+
{ type: "string" },
|
|
316
|
+
{
|
|
317
|
+
type: "object",
|
|
318
|
+
required: ["source"],
|
|
319
|
+
properties: {
|
|
320
|
+
source: { type: "string" },
|
|
321
|
+
variables: { type: "object" },
|
|
322
|
+
secrets: { type: "object" },
|
|
323
|
+
runtime: {
|
|
324
|
+
oneOf: [
|
|
325
|
+
{ type: "string" },
|
|
326
|
+
{ type: "array", items: { type: "string" } },
|
|
327
|
+
],
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
additionalProperties: false,
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
},
|
|
334
|
+
},
|
|
304
335
|
// Application-level environment contract. Each entry layers `env:`
|
|
305
336
|
// (required, names the source env var) and `default:` (optional, used
|
|
306
337
|
// when the env var is unset) on top of an open JSON Schema property
|
|
@@ -394,6 +425,33 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
394
425
|
type: "array",
|
|
395
426
|
items: { type: "string" },
|
|
396
427
|
},
|
|
428
|
+
// Inline imports — same name-keyed map sugar as Telo.Application; the
|
|
429
|
+
// loader desugars each entry into a synthetic Telo.Import. See the
|
|
430
|
+
// Application schema above and analyzer/nodejs/src/inline-imports.ts.
|
|
431
|
+
imports: {
|
|
432
|
+
type: "object",
|
|
433
|
+
additionalProperties: {
|
|
434
|
+
oneOf: [
|
|
435
|
+
{ type: "string" },
|
|
436
|
+
{
|
|
437
|
+
type: "object",
|
|
438
|
+
required: ["source"],
|
|
439
|
+
properties: {
|
|
440
|
+
source: { type: "string" },
|
|
441
|
+
variables: { type: "object" },
|
|
442
|
+
secrets: { type: "object" },
|
|
443
|
+
runtime: {
|
|
444
|
+
oneOf: [
|
|
445
|
+
{ type: "string" },
|
|
446
|
+
{ type: "array", items: { type: "string" } },
|
|
447
|
+
],
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
additionalProperties: false,
|
|
451
|
+
},
|
|
452
|
+
],
|
|
453
|
+
},
|
|
454
|
+
},
|
|
397
455
|
exports: {
|
|
398
456
|
type: "object",
|
|
399
457
|
properties: {
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,8 @@ export { isModuleKind, MODULE_KINDS } from "./module-kinds.js";
|
|
|
25
25
|
export type { ModuleKind } from "./module-kinds.js";
|
|
26
26
|
export { parseLoadedFile } from "./parse-loaded-file.js";
|
|
27
27
|
export type { ParseOptions } from "./parse-loaded-file.js";
|
|
28
|
+
export { desugarLoadedFile, inlineImportManifests } from "./inline-imports.js";
|
|
29
|
+
export type { SyntheticImport } from "./inline-imports.js";
|
|
28
30
|
export { residualEntrySchema, residualEntrySchemaMap } from "./residual-schema.js";
|
|
29
31
|
export {
|
|
30
32
|
buildDocumentPositions,
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import type { LoadedFile } from "./loaded-types.js";
|
|
3
|
+
import { isModuleKind } from "./module-kinds.js";
|
|
4
|
+
import type { DocumentPosition } from "./position-metadata.js";
|
|
5
|
+
import type { PositionIndex } from "./types.js";
|
|
6
|
+
|
|
7
|
+
/** A synthetic `Telo.Import` produced by desugaring an `imports:` map entry,
|
|
8
|
+
* paired with the position metadata that pins its diagnostics back to the
|
|
9
|
+
* authoring line in the module document. */
|
|
10
|
+
export interface SyntheticImport {
|
|
11
|
+
manifest: ResourceManifest;
|
|
12
|
+
position: DocumentPosition;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Desugar a module document's inline `imports:` map into synthetic
|
|
17
|
+
* `Telo.Import` manifests. Each entry value is either a bare source string
|
|
18
|
+
* (shorthand for `{ source }`) or the full object form carrying
|
|
19
|
+
* `variables` / `secrets` / `runtime`. Malformed entries (object without a
|
|
20
|
+
* string `source`) are skipped here — the module document's own schema
|
|
21
|
+
* validation reports them against the precise `imports.<Alias>.source` path.
|
|
22
|
+
*
|
|
23
|
+
* The synthetic manifests are indistinguishable from authored `Telo.Import`
|
|
24
|
+
* documents downstream (alias registration, discovery, the kernel's
|
|
25
|
+
* import-controller), so the feature is purely additive at the declaration
|
|
26
|
+
* site. Pure and browser-safe — no I/O, no Node built-ins.
|
|
27
|
+
*/
|
|
28
|
+
export function inlineImportManifests(
|
|
29
|
+
moduleManifest: ResourceManifest,
|
|
30
|
+
modulePosition: DocumentPosition | undefined,
|
|
31
|
+
): SyntheticImport[] {
|
|
32
|
+
const raw = (moduleManifest as { imports?: unknown }).imports;
|
|
33
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return [];
|
|
34
|
+
|
|
35
|
+
const out: SyntheticImport[] = [];
|
|
36
|
+
for (const [alias, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
37
|
+
const scalar = typeof value === "string";
|
|
38
|
+
const entry = scalar
|
|
39
|
+
? { source: value as string }
|
|
40
|
+
: value && typeof value === "object" && !Array.isArray(value)
|
|
41
|
+
? (value as Record<string, unknown>)
|
|
42
|
+
: undefined;
|
|
43
|
+
if (!entry || typeof entry.source !== "string") continue;
|
|
44
|
+
|
|
45
|
+
const manifest = {
|
|
46
|
+
kind: "Telo.Import",
|
|
47
|
+
metadata: { name: alias },
|
|
48
|
+
source: entry.source,
|
|
49
|
+
...(entry.variables !== undefined ? { variables: entry.variables } : {}),
|
|
50
|
+
...(entry.secrets !== undefined ? { secrets: entry.secrets } : {}),
|
|
51
|
+
...(entry.runtime !== undefined ? { runtime: entry.runtime } : {}),
|
|
52
|
+
} as unknown as ResourceManifest;
|
|
53
|
+
|
|
54
|
+
out.push({ manifest, position: synthPosition(modulePosition, alias, scalar) });
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Returns a copy of `file` with synthetic `Telo.Import` manifests (from the
|
|
60
|
+
* module document's inline `imports:` map) appended to `manifests` and
|
|
61
|
+
* `positions`. `documents` is intentionally left untouched: it is the raw
|
|
62
|
+
* YAML-AST array round-trip consumers pair by index, and a synthetic import
|
|
63
|
+
* has no backing node. Every flatten/discovery loop iterates `manifests` and
|
|
64
|
+
* indexes `positions[i]` — never `documents[i]` in lockstep — so the trailing
|
|
65
|
+
* synthetics are visible to resolution while the AST round-trip stays intact.
|
|
66
|
+
* Returns `file` unchanged when there is no module doc or no inline imports. */
|
|
67
|
+
export function desugarLoadedFile(file: LoadedFile): LoadedFile {
|
|
68
|
+
let moduleIndex = -1;
|
|
69
|
+
for (let i = 0; i < file.manifests.length; i++) {
|
|
70
|
+
const m = file.manifests[i];
|
|
71
|
+
if (m && isModuleKind(m.kind)) {
|
|
72
|
+
moduleIndex = i;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (moduleIndex < 0) return file;
|
|
77
|
+
|
|
78
|
+
const synthetic = inlineImportManifests(file.manifests[moduleIndex]!, file.positions[moduleIndex]);
|
|
79
|
+
if (synthetic.length === 0) return file;
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
...file,
|
|
83
|
+
manifests: [...file.manifests, ...synthetic.map((s) => s.manifest)],
|
|
84
|
+
positions: [...file.positions, ...synthetic.map((s) => s.position)],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Build a `DocumentPosition` for a synthetic import by re-rooting the module
|
|
89
|
+
* document's `imports.<Alias>` position subtree at the import manifest's own
|
|
90
|
+
* paths (`source`, `variables.*`, `metadata.name`, …). This makes a
|
|
91
|
+
* diagnostic on the synthetic's `source` land on the `imports:` entry's
|
|
92
|
+
* authoring line rather than a phantom document. */
|
|
93
|
+
function synthPosition(
|
|
94
|
+
modulePosition: DocumentPosition | undefined,
|
|
95
|
+
alias: string,
|
|
96
|
+
scalar: boolean,
|
|
97
|
+
): DocumentPosition {
|
|
98
|
+
if (!modulePosition) return { sourceLine: 0, positionIndex: new Map() };
|
|
99
|
+
|
|
100
|
+
const base = modulePosition.positionIndex;
|
|
101
|
+
const index: PositionIndex = new Map();
|
|
102
|
+
|
|
103
|
+
const keyRange = base.get(`@key:imports.${alias}`);
|
|
104
|
+
const valueRange = base.get(`imports.${alias}`);
|
|
105
|
+
|
|
106
|
+
if (keyRange) {
|
|
107
|
+
index.set("metadata.name", keyRange);
|
|
108
|
+
index.set("@key:metadata.name", keyRange);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (scalar) {
|
|
112
|
+
// `Console: std/console@1.2.3` — the entry value IS the source scalar.
|
|
113
|
+
if (valueRange) index.set("source", valueRange);
|
|
114
|
+
} else {
|
|
115
|
+
const valuePrefix = `imports.${alias}.`;
|
|
116
|
+
const keyPrefix = `@key:imports.${alias}.`;
|
|
117
|
+
for (const [path, range] of base) {
|
|
118
|
+
if (path.startsWith(valuePrefix)) {
|
|
119
|
+
index.set(path.slice(valuePrefix.length), range);
|
|
120
|
+
} else if (path.startsWith(keyPrefix)) {
|
|
121
|
+
index.set(`@key:${path.slice(keyPrefix.length)}`, range);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (valueRange) index.set("", valueRange);
|
|
127
|
+
|
|
128
|
+
const sourceLine = (keyRange ?? valueRange)?.start.line ?? modulePosition.sourceLine;
|
|
129
|
+
return { sourceLine, positionIndex: index };
|
|
130
|
+
}
|
package/src/manifest-loader.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
LoadedGraph,
|
|
11
11
|
LoadedModule,
|
|
12
12
|
} from "./loaded-types.js";
|
|
13
|
+
import { desugarLoadedFile } from "./inline-imports.js";
|
|
13
14
|
import { isModuleKind } from "./module-kinds.js";
|
|
14
15
|
import { parseLoadedFile } from "./parse-loaded-file.js";
|
|
15
16
|
import {
|
|
@@ -26,6 +27,14 @@ const SYSTEM_KINDS = new Set([
|
|
|
26
27
|
"Telo.Definition",
|
|
27
28
|
]);
|
|
28
29
|
|
|
30
|
+
/** File cache variant tags: compile (c/r) × desugarImports (d/n). A desugared
|
|
31
|
+
* and a raw load of the same file are distinct entries so neither sees the
|
|
32
|
+
* wrong manifest tree. */
|
|
33
|
+
const CACHE_VARIANTS = ["rn", "rd", "cn", "cd"] as const;
|
|
34
|
+
function variantKey(options?: LoadOptions): string {
|
|
35
|
+
return `${options?.compile ? "c" : "r"}${options?.desugarImports ? "d" : "n"}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
export class Loader {
|
|
30
39
|
/** LoadedFile cache keyed by `${compile ? "compiled" : "raw"}:${source}`.
|
|
31
40
|
* Same dual-keying as the legacy ResourceManifest[] cache: a compile-mode
|
|
@@ -100,29 +109,25 @@ export class Loader {
|
|
|
100
109
|
* private mutable copy must call `parseLoadedFile` directly with the
|
|
101
110
|
* LoadedFile's `text`. */
|
|
102
111
|
async loadFile(url: string, options?: LoadOptions): Promise<LoadedFile> {
|
|
103
|
-
const
|
|
112
|
+
const variant = variantKey(options);
|
|
104
113
|
const knownSource = this.urlToSource.get(url);
|
|
105
114
|
if (knownSource) {
|
|
106
|
-
const cached = this.fileCache.get(`${
|
|
115
|
+
const cached = this.fileCache.get(`${variant}:${knownSource}`);
|
|
107
116
|
if (cached) return cached;
|
|
108
|
-
//
|
|
117
|
+
// Another variant of this source is cached — reparse from its text
|
|
109
118
|
// instead of re-reading the source.
|
|
110
119
|
//
|
|
111
120
|
// NOTE for watch-mode reactivation (cli/nodejs/src/commands/run.ts
|
|
112
121
|
// currently has `setupWatchMode` commented out): this branch
|
|
113
122
|
// assumes file contents don't change underneath a single Loader.
|
|
114
123
|
// Reviving watch mode will need a public `invalidate(url)` (or
|
|
115
|
-
// similar) that drops both `urlToSource[url]` and
|
|
116
|
-
//
|
|
117
|
-
// file again.
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
compile: options?.compile,
|
|
123
|
-
celEnv: this.celEnv,
|
|
124
|
-
});
|
|
125
|
-
this.fileCache.set(`${compileKey}:${knownSource}`, reparsed);
|
|
124
|
+
// similar) that drops both `urlToSource[url]` and every cached
|
|
125
|
+
// variant entry for its canonical source before the loader serves
|
|
126
|
+
// the file again.
|
|
127
|
+
const altText = this.findCachedText(knownSource);
|
|
128
|
+
if (altText !== undefined) {
|
|
129
|
+
const reparsed = this.parseAndMaybeDesugar(knownSource, url, altText, options);
|
|
130
|
+
this.fileCache.set(`${variant}:${knownSource}`, reparsed);
|
|
126
131
|
return reparsed;
|
|
127
132
|
}
|
|
128
133
|
}
|
|
@@ -135,16 +140,41 @@ export class Loader {
|
|
|
135
140
|
// for that exact URL — hit the urlToSource fast path instead of
|
|
136
141
|
// falling through to a redundant `pick(url).read(url)`.
|
|
137
142
|
this.urlToSource.set(source, source);
|
|
138
|
-
const cacheKey = `${
|
|
143
|
+
const cacheKey = `${variant}:${source}`;
|
|
139
144
|
const cached = this.fileCache.get(cacheKey);
|
|
140
145
|
if (cached && cached.text === text) return cached;
|
|
141
146
|
|
|
142
|
-
const loaded =
|
|
147
|
+
const loaded = this.parseAndMaybeDesugar(source, url, text, options);
|
|
148
|
+
this.fileCache.set(cacheKey, loaded);
|
|
149
|
+
return loaded;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Parse `text` into a LoadedFile, then desugar inline `imports:` when the
|
|
153
|
+
* caller opted in. Desugaring lives here, not in the pure `parseLoadedFile`,
|
|
154
|
+
* so round-trip consumers (the editor) keep a raw manifest/AST/position
|
|
155
|
+
* triple they can pair by index; only resolved consumers that pass
|
|
156
|
+
* `desugarImports` see synthetic Telo.Import manifests. */
|
|
157
|
+
private parseAndMaybeDesugar(
|
|
158
|
+
source: string,
|
|
159
|
+
requestedUrl: string,
|
|
160
|
+
text: string,
|
|
161
|
+
options?: LoadOptions,
|
|
162
|
+
): LoadedFile {
|
|
163
|
+
const loaded = parseLoadedFile(source, requestedUrl, text, {
|
|
143
164
|
compile: options?.compile,
|
|
144
165
|
celEnv: this.celEnv,
|
|
145
166
|
});
|
|
146
|
-
|
|
147
|
-
|
|
167
|
+
return options?.desugarImports ? desugarLoadedFile(loaded) : loaded;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Raw text of any already-cached variant for `source`, so a cache miss on
|
|
171
|
+
* one (compile, desugar) variant reparses without a second source read. */
|
|
172
|
+
private findCachedText(source: string): string | undefined {
|
|
173
|
+
for (const v of CACHE_VARIANTS) {
|
|
174
|
+
const cached = this.fileCache.get(`${v}:${source}`);
|
|
175
|
+
if (cached) return cached.text;
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
148
178
|
}
|
|
149
179
|
|
|
150
180
|
/** Load an owner file plus every partial reachable through its `include:`
|
|
@@ -382,12 +412,13 @@ export class Loader {
|
|
|
382
412
|
* via parent-directory traversal. */
|
|
383
413
|
async loadGraphForFile(
|
|
384
414
|
fileUrl: string,
|
|
415
|
+
options?: LoadOptions,
|
|
385
416
|
): Promise<{ graph: LoadedGraph; ownerUrl: string } | null> {
|
|
386
417
|
try {
|
|
387
|
-
const owner = await this.loadFile(fileUrl);
|
|
418
|
+
const owner = await this.loadFile(fileUrl, options);
|
|
388
419
|
const isOwner = owner.manifests.some((m) => m && isModuleKind(m.kind));
|
|
389
420
|
if (isOwner) {
|
|
390
|
-
const graph = await this.loadGraph(fileUrl);
|
|
421
|
+
const graph = await this.loadGraph(fileUrl, options);
|
|
391
422
|
return { graph, ownerUrl: graph.rootSource };
|
|
392
423
|
}
|
|
393
424
|
} catch (err) {
|
|
@@ -404,7 +435,7 @@ export class Loader {
|
|
|
404
435
|
if (!source.resolveOwnerOf) return null;
|
|
405
436
|
const ownerUrl = await source.resolveOwnerOf(fileUrl);
|
|
406
437
|
if (!ownerUrl) return null;
|
|
407
|
-
const graph = await this.loadGraph(ownerUrl);
|
|
438
|
+
const graph = await this.loadGraph(ownerUrl, options);
|
|
408
439
|
return { graph, ownerUrl: graph.rootSource };
|
|
409
440
|
}
|
|
410
441
|
|
package/src/types.ts
CHANGED
|
@@ -63,6 +63,15 @@ export interface LoadOptions {
|
|
|
63
63
|
* so the kernel can evaluate them at runtime. Leave unset (false) for static analysis —
|
|
64
64
|
* the analyzer works on raw strings and does not need compiled values. */
|
|
65
65
|
compile?: boolean;
|
|
66
|
+
/** When true, each module document's inline `imports:` map is desugared into
|
|
67
|
+
* synthetic `Telo.Import` manifests appended to the file's `manifests` /
|
|
68
|
+
* `positions` (the AST `documents` array is left raw). On for every resolved
|
|
69
|
+
* consumer — the kernel's analysis and runtime loads, and the analyzer — so
|
|
70
|
+
* inline imports participate in discovery, alias resolution, and execution.
|
|
71
|
+
* Off for the editor's round-trip view, which reads the raw `imports:` map and
|
|
72
|
+
* pairs manifests to YAML nodes by index. Folded into the file cache key so a
|
|
73
|
+
* desugared and a raw load of the same file never collide. */
|
|
74
|
+
desugarImports?: boolean;
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
export interface LoaderInitOptions {
|