@telorun/analyzer 0.22.0 → 0.23.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 +2 -2
- package/dist/alias-resolver.d.ts +7 -0
- package/dist/alias-resolver.d.ts.map +1 -1
- package/dist/alias-resolver.js +14 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +48 -13
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +4 -2
- package/dist/cel-environment.d.ts +8 -0
- package/dist/cel-environment.d.ts.map +1 -1
- package/dist/cel-environment.js +48 -0
- package/dist/flatten-for-analyzer.d.ts +52 -0
- package/dist/flatten-for-analyzer.d.ts.map +1 -1
- package/dist/flatten-for-analyzer.js +192 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/alias-resolver.ts +14 -0
- package/src/analyzer.ts +57 -11
- package/src/builtins.ts +4 -2
- package/src/cel-environment.ts +57 -0
- package/src/flatten-for-analyzer.ts +217 -4
- package/src/index.ts +7 -0
package/README.md
CHANGED
|
@@ -55,8 +55,8 @@ metadata:
|
|
|
55
55
|
A complete feedback collection REST API — no code, pure YAML.
|
|
56
56
|
Persists entries to SQLite and serves them over HTTP.
|
|
57
57
|
imports:
|
|
58
|
-
Http: std/http-server@0.
|
|
59
|
-
Sql: std/sql@0.
|
|
58
|
+
Http: std/http-server@0.11.0
|
|
59
|
+
Sql: std/sql@0.9.0
|
|
60
60
|
targets:
|
|
61
61
|
- !ref Migrations
|
|
62
62
|
- !ref Server
|
package/dist/alias-resolver.d.ts
CHANGED
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
export declare class AliasResolver {
|
|
4
4
|
private readonly importAliases;
|
|
5
5
|
private readonly importedKinds;
|
|
6
|
+
/** `${alias}.${suffix}` → canonical `<owningModule>.<Kind>` for kinds an import
|
|
7
|
+
* transitively RE-EXPORTS (`exports.kinds: [Alias.Kind]`), which don't live in the
|
|
8
|
+
* import's own module. Resolved before the normal `<module>.<suffix>` construction. */
|
|
9
|
+
private readonly reExportedKinds;
|
|
6
10
|
registerImport(alias: string, targetModule: string, exportedKinds: string[]): void;
|
|
11
|
+
/** Register that `<alias>.<suffix>` re-exports the kind canonically named `canonicalKind`
|
|
12
|
+
* (owned by a module the alias's target imports, possibly several hops away). */
|
|
13
|
+
registerKindReExport(alias: string, suffix: string, canonicalKind: string): void;
|
|
7
14
|
/** Real module name an alias points at (e.g. "Console" → "console"), or undefined.
|
|
8
15
|
* Used to resolve an alias-qualified instance reference "Console.writeLine" to the
|
|
9
16
|
* forwarded resource declared in that module. The `exports.resources` gate is enforced
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"alias-resolver.d.ts","sourceRoot":"","sources":["../src/alias-resolver.ts"],"names":[],"mappings":"AAAA;gFACgF;AAChF,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA6B;IAC3D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkC;
|
|
1
|
+
{"version":3,"file":"alias-resolver.d.ts","sourceRoot":"","sources":["../src/alias-resolver.ts"],"names":[],"mappings":"AAAA;gFACgF;AAChF,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA6B;IAC3D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkC;IAChE;;4FAEwF;IACxF,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA6B;IAE7D,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,IAAI;IAOlF;sFACkF;IAClF,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI;IAIhF;;;;qDAIiD;IACjD,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIjD,sFAAsF;IACtF,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAmB7C,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAIhC,YAAY,IAAI,MAAM,EAAE;IAIxB;;qEAEiE;IACjE,UAAU,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE;CAO3C"}
|
package/dist/alias-resolver.js
CHANGED
|
@@ -3,12 +3,21 @@
|
|
|
3
3
|
export class AliasResolver {
|
|
4
4
|
importAliases = new Map();
|
|
5
5
|
importedKinds = new Map();
|
|
6
|
+
/** `${alias}.${suffix}` → canonical `<owningModule>.<Kind>` for kinds an import
|
|
7
|
+
* transitively RE-EXPORTS (`exports.kinds: [Alias.Kind]`), which don't live in the
|
|
8
|
+
* import's own module. Resolved before the normal `<module>.<suffix>` construction. */
|
|
9
|
+
reExportedKinds = new Map();
|
|
6
10
|
registerImport(alias, targetModule, exportedKinds) {
|
|
7
11
|
this.importAliases.set(alias, targetModule);
|
|
8
12
|
if (exportedKinds.length > 0) {
|
|
9
13
|
this.importedKinds.set(alias, new Set(exportedKinds));
|
|
10
14
|
}
|
|
11
15
|
}
|
|
16
|
+
/** Register that `<alias>.<suffix>` re-exports the kind canonically named `canonicalKind`
|
|
17
|
+
* (owned by a module the alias's target imports, possibly several hops away). */
|
|
18
|
+
registerKindReExport(alias, suffix, canonicalKind) {
|
|
19
|
+
this.reExportedKinds.set(`${alias}.${suffix}`, canonicalKind);
|
|
20
|
+
}
|
|
12
21
|
/** Real module name an alias points at (e.g. "Console" → "console"), or undefined.
|
|
13
22
|
* Used to resolve an alias-qualified instance reference "Console.writeLine" to the
|
|
14
23
|
* forwarded resource declared in that module. The `exports.resources` gate is enforced
|
|
@@ -27,6 +36,11 @@ export class AliasResolver {
|
|
|
27
36
|
return undefined;
|
|
28
37
|
const prefix = kind.slice(0, dot);
|
|
29
38
|
const suffix = kind.slice(dot + 1);
|
|
39
|
+
// Re-export takes precedence: a re-exported kind resolves to its true owning module,
|
|
40
|
+
// not `${prefix-target}.${suffix}` (and bypasses the gate — it's explicitly re-exported).
|
|
41
|
+
const reExported = this.reExportedKinds.get(`${prefix}.${suffix}`);
|
|
42
|
+
if (reExported)
|
|
43
|
+
return reExported;
|
|
30
44
|
const realModule = this.importAliases.get(prefix);
|
|
31
45
|
if (!realModule)
|
|
32
46
|
return undefined;
|
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,
|
|
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,EAIL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAiB9B,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AA4hB/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;IA+sBvB,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;IAerB,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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defaultRegistry, isTaggedSentinel } from "@telorun/templating";
|
|
2
2
|
import { AliasResolver } from "./alias-resolver.js";
|
|
3
|
-
import { buildCelEnvironment, buildTypedCelEnvironment, } from "./cel-environment.js";
|
|
3
|
+
import { buildCelEnvironment, buildImportInputCelEnvironment, buildTypedCelEnvironment, } from "./cel-environment.js";
|
|
4
4
|
import { DefinitionRegistry } from "./definition-registry.js";
|
|
5
5
|
import { buildDependencyGraph, formatCycle } from "./dependency-graph.js";
|
|
6
6
|
import { buildKernelGlobalsSchema, mergeKernelGlobalsIntoContext } from "./kernel-globals.js";
|
|
@@ -579,17 +579,18 @@ export class StaticAnalyzer {
|
|
|
579
579
|
if (resolvedModuleName) {
|
|
580
580
|
defs.registerModuleIdentity(resolvedNamespace ?? null, resolvedModuleName);
|
|
581
581
|
}
|
|
582
|
+
// `metadata.reExportedKinds` (stamped by flattenForAnalyzer / the editor projection)
|
|
583
|
+
// maps an exported suffix to the true owning module's canonical kind for kinds this
|
|
584
|
+
// import transitively re-exports (`exports.kinds: [Alias.Kind]`).
|
|
585
|
+
const reExportedKinds = (m.metadata?.reExportedKinds ?? {});
|
|
582
586
|
// Alias registration is scoped: consumer imports vs. imported-library imports.
|
|
583
|
-
|
|
584
|
-
aliases
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
aliasesByModule.set(ownModule, libResolver);
|
|
591
|
-
}
|
|
592
|
-
libResolver.registerImport(alias, targetModule, exportedKinds);
|
|
587
|
+
const resolver = !ownModule || rootModules.has(ownModule)
|
|
588
|
+
? aliases
|
|
589
|
+
: (aliasesByModule.get(ownModule) ??
|
|
590
|
+
aliasesByModule.set(ownModule, new AliasResolver()).get(ownModule));
|
|
591
|
+
resolver.registerImport(alias, targetModule, exportedKinds);
|
|
592
|
+
for (const [suffix, canonical] of Object.entries(reExportedKinds)) {
|
|
593
|
+
resolver.registerKindReExport(alias, suffix, canonical);
|
|
593
594
|
}
|
|
594
595
|
}
|
|
595
596
|
}
|
|
@@ -735,6 +736,26 @@ export class StaticAnalyzer {
|
|
|
735
736
|
}
|
|
736
737
|
}
|
|
737
738
|
}
|
|
739
|
+
// `exports.resources` entries are plain names: `Db` (local) or `Alias.Name` (re-export),
|
|
740
|
+
// mirroring `exports.kinds`. The `!ref` tag is not accepted here — a `!ref` parses to a
|
|
741
|
+
// sentinel object that the schema's CEL/ref exemption would silently pass, so reject any
|
|
742
|
+
// non-string entry with an actionable message instead.
|
|
743
|
+
const exportsResources = m.exports?.resources;
|
|
744
|
+
if (Array.isArray(exportsResources)) {
|
|
745
|
+
for (let i = 0; i < exportsResources.length; i++) {
|
|
746
|
+
if (typeof exportsResources[i] === "string")
|
|
747
|
+
continue;
|
|
748
|
+
diagnostics.push({
|
|
749
|
+
severity: DiagnosticSeverity.Error,
|
|
750
|
+
code: "INVALID_EXPORT",
|
|
751
|
+
source: SOURCE,
|
|
752
|
+
message: `Telo.Library exports.resources[${i}]: write the exported name as a plain string — ` +
|
|
753
|
+
`'Name' to export a local instance, or 'Alias.Name' to re-export an imported one. ` +
|
|
754
|
+
`The '!ref' tag is not allowed in exports.resources.`,
|
|
755
|
+
data: { resource, filePath, path: `exports.resources.${i}` },
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
}
|
|
738
759
|
}
|
|
739
760
|
// Build typed kernel globals schema so x-telo-context chain validation
|
|
740
761
|
// recognises variables, secrets, resources, env automatically
|
|
@@ -804,8 +825,22 @@ export class StaticAnalyzer {
|
|
|
804
825
|
},
|
|
805
826
|
}
|
|
806
827
|
: definition.schema;
|
|
807
|
-
// Phase 1: CEL type checking — walk data+schema together, check env.check() return types
|
|
808
|
-
|
|
828
|
+
// Phase 1: CEL type checking — walk data+schema together, check env.check() return types.
|
|
829
|
+
// A Telo.Import's variables/secrets are a config-only contract evaluated against the
|
|
830
|
+
// IMPORTING module's scope, so type them from the owning module doc (matched by
|
|
831
|
+
// `metadata.module`) and drop `resources`/`env` so referencing them is an error. A
|
|
832
|
+
// library's own internal import is validated against that library in the library's
|
|
833
|
+
// standalone analysis; in this flattened app pass the library doc is absent, so the
|
|
834
|
+
// importer is undefined here and variables/secrets fall back to a permissive `map`
|
|
835
|
+
// (no false positives) while resources/env stay rejected.
|
|
836
|
+
const importerModule = m.kind === "Telo.Import"
|
|
837
|
+
? allManifests.find((mm) => (mm.kind === "Telo.Application" || mm.kind === "Telo.Library") &&
|
|
838
|
+
mm.metadata?.name ===
|
|
839
|
+
m.metadata?.module)
|
|
840
|
+
: undefined;
|
|
841
|
+
const baseTypedEnv = m.kind === "Telo.Import"
|
|
842
|
+
? buildImportInputCelEnvironment(this.celEnv, importerModule)
|
|
843
|
+
: buildTypedCelEnvironment(this.celEnv, m, undefined, moduleManifest);
|
|
809
844
|
const celIssues = collectCelTypeIssues(m, schema, "", definition, m, baseTypedEnv, this.celEnv, moduleManifest);
|
|
810
845
|
// Phase 2+3: AJV on substituted data — CEL fields replaced with typed placeholders
|
|
811
846
|
const ajvIssues = validateAgainstSchema(substituteCelFields(m, schema), schema);
|
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,EAud/C,CAAC"}
|
package/dist/builtins.js
CHANGED
|
@@ -453,8 +453,10 @@ export const KERNEL_BUILTINS = [
|
|
|
453
453
|
type: "object",
|
|
454
454
|
properties: {
|
|
455
455
|
kinds: { type: "array", items: { type: "string" } },
|
|
456
|
-
//
|
|
457
|
-
//
|
|
456
|
+
// An entry is a bare name (`Db`, a locally-owned export) or a dotted `Alias.Name`
|
|
457
|
+
// (re-export of the instance reached via this library's import aliased `Alias`,
|
|
458
|
+
// under the name `Name`) — mirroring `exports.kinds`. `variables` / `secrets` are
|
|
459
|
+
// reserved on the resources.<Alias> value-flow surface, so they may not be exported.
|
|
458
460
|
resources: {
|
|
459
461
|
type: "array",
|
|
460
462
|
items: { type: "string", not: { enum: ["variables", "secrets"] } },
|
|
@@ -13,4 +13,12 @@ export type { CelHandlers } from "@telorun/templating";
|
|
|
13
13
|
* NOTE: The set of kernel globals registered here must match `KERNEL_GLOBAL_NAMES`
|
|
14
14
|
* in kernel-globals.ts, which is used for chain-access validation. */
|
|
15
15
|
export declare function buildTypedCelEnvironment(baseEnv: Environment, manifest: ResourceManifest, extraContextSchema?: Record<string, any> | null, rootModuleManifest?: ResourceManifest): Environment;
|
|
16
|
+
/** CEL environment for the `variables:`/`secrets:` expressions on a `Telo.Import`.
|
|
17
|
+
*
|
|
18
|
+
* Import inputs are a config-only contract: their expressions are evaluated
|
|
19
|
+
* against the IMPORTING module's `variables`/`secrets`, never the import's own
|
|
20
|
+
* values map (the bug) nor the imported child's. `resources`, `env`, and `ports`
|
|
21
|
+
* are registered as empty typed objects, so referencing them is a "No such key"
|
|
22
|
+
* error that steers authors to a typed `variables` entry. */
|
|
23
|
+
export declare function buildImportInputCelEnvironment(baseEnv: Environment, moduleManifest: ResourceManifest | undefined): Environment;
|
|
16
24
|
//# sourceMappingURL=cel-environment.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cel-environment.d.ts","sourceRoot":"","sources":["../src/cel-environment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAUrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD;;;;;;;;;uEASuE;AACvE,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,gBAAgB,EAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,EAI/C,kBAAkB,CAAC,EAAE,gBAAgB,GACpC,WAAW,CAuEb"}
|
|
1
|
+
{"version":3,"file":"cel-environment.d.ts","sourceRoot":"","sources":["../src/cel-environment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAUrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD;;;;;;;;;uEASuE;AACvE,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,gBAAgB,EAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,EAI/C,kBAAkB,CAAC,EAAE,gBAAgB,GACpC,WAAW,CAuEb;AAuBD;;;;;;8DAM8D;AAC9D,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,WAAW,EACpB,cAAc,EAAE,gBAAgB,GAAG,SAAS,GAC3C,WAAW,CAwBb"}
|
package/dist/cel-environment.js
CHANGED
|
@@ -85,3 +85,51 @@ rootModuleManifest) {
|
|
|
85
85
|
return baseEnv.clone();
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
+
/** Register a `variables`/`secrets` namespace typed from a module doc's schema map
|
|
89
|
+
* (`{ name: <schema>, … }`), falling back to dyn `map` when absent or untyped. */
|
|
90
|
+
function registerConfigNamespace(env, block, name) {
|
|
91
|
+
if (block !== null && typeof block === "object" && !Array.isArray(block)) {
|
|
92
|
+
const entries = Object.entries(block).filter(([, v]) => v !== null && typeof v === "object" && !Array.isArray(v));
|
|
93
|
+
if (entries.length > 0) {
|
|
94
|
+
const schema = {};
|
|
95
|
+
for (const [k, v] of entries)
|
|
96
|
+
schema[k] = jsonSchemaToCelType(v);
|
|
97
|
+
env.registerVariable({ name, schema });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
env.registerVariable(name, "map");
|
|
102
|
+
}
|
|
103
|
+
/** CEL environment for the `variables:`/`secrets:` expressions on a `Telo.Import`.
|
|
104
|
+
*
|
|
105
|
+
* Import inputs are a config-only contract: their expressions are evaluated
|
|
106
|
+
* against the IMPORTING module's `variables`/`secrets`, never the import's own
|
|
107
|
+
* values map (the bug) nor the imported child's. `resources`, `env`, and `ports`
|
|
108
|
+
* are registered as empty typed objects, so referencing them is a "No such key"
|
|
109
|
+
* error that steers authors to a typed `variables` entry. */
|
|
110
|
+
export function buildImportInputCelEnvironment(baseEnv, moduleManifest) {
|
|
111
|
+
const env = baseEnv.clone();
|
|
112
|
+
for (const brand of Object.keys(VALUE_BRAND_BASE)) {
|
|
113
|
+
env.registerType(brand, { fields: {} });
|
|
114
|
+
}
|
|
115
|
+
const mod = moduleManifest;
|
|
116
|
+
// Typing variables/secrets from the importer's schema can fail on a malformed
|
|
117
|
+
// schema; degrade those to permissive `map` if so — but never lose the
|
|
118
|
+
// resources/env/ports rejection registered below (the catch is scoped so a
|
|
119
|
+
// typing failure can't silently re-open the config-only contract).
|
|
120
|
+
try {
|
|
121
|
+
registerConfigNamespace(env, mod?.variables, "variables");
|
|
122
|
+
registerConfigNamespace(env, mod?.secrets, "secrets");
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
env.registerVariable("variables", "map");
|
|
126
|
+
env.registerVariable("secrets", "map");
|
|
127
|
+
}
|
|
128
|
+
// Override the base env's dyn `resources`/`env`/`ports` with empty typed objects
|
|
129
|
+
// so any access (`resources.X`, `env.X`) is a "No such key" error — these
|
|
130
|
+
// surfaces are not part of the config-only import contract.
|
|
131
|
+
for (const name of ["resources", "env", "ports"]) {
|
|
132
|
+
env.registerVariable({ name, schema: {} });
|
|
133
|
+
}
|
|
134
|
+
return env;
|
|
135
|
+
}
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
2
|
import type { LoadedGraph, LoadedModule } from "./loaded-types.js";
|
|
3
|
+
/** One parsed `exports.resources` / `exports.kinds` entry. `name` is the exported
|
|
4
|
+
* instance name or kind suffix (the part after the dot, or the whole entry); `alias`
|
|
5
|
+
* (when set) is this library's own import the entry RE-EXPORTS from. */
|
|
6
|
+
export interface ParsedExportEntry {
|
|
7
|
+
name: string;
|
|
8
|
+
alias?: string;
|
|
9
|
+
}
|
|
10
|
+
/** Parse a single dotted export entry: `Alias.Name` → `{name: "Name", alias: "Alias"}`,
|
|
11
|
+
* bare `Name` → `{name: "Name"}`. The single grammar for `exports.resources` and
|
|
12
|
+
* `exports.kinds`, shared by the kernel's import controller and the analyzer/editor so
|
|
13
|
+
* the dotted-name split can't drift. A leading dot (`.Name`) has no alias by design —
|
|
14
|
+
* the empty prefix isn't a valid alias. */
|
|
15
|
+
export declare function parseExportEntry(entry: string): ParsedExportEntry;
|
|
3
16
|
/** The import-boundary forwarding rule, shared by `flattenForAnalyzer` (the
|
|
4
17
|
* CLI / kernel loader path) and the telo-editor's workspace projection so the
|
|
5
18
|
* two cannot drift. Given one module's stamped manifests and whether that
|
|
@@ -42,6 +55,45 @@ export declare function selectModuleManifestsForAnalysis(moduleManifests: Resour
|
|
|
42
55
|
* Position metadata (`positionIndex`) is NOT stamped on manifests —
|
|
43
56
|
* callers look it up via `findPositions(graph, ...)` on the LoadedGraph. */
|
|
44
57
|
export declare function flattenForAnalyzer(graph: LoadedGraph): ResourceManifest[];
|
|
58
|
+
/** A re-export declared in a library's `exports.resources` as a dotted `Alias.Name`:
|
|
59
|
+
* module `module` re-exports the instance `name` reached through its own import
|
|
60
|
+
* aliased `alias`. */
|
|
61
|
+
export interface ReExportSpec {
|
|
62
|
+
module: string;
|
|
63
|
+
alias: string;
|
|
64
|
+
name: string;
|
|
65
|
+
}
|
|
66
|
+
/** Extract re-export specs from a library doc's `exports.resources` — the dotted `Alias.Name`
|
|
67
|
+
* entries (bare-name locals are forwarded by the BFS instead). Shared by the CLI graph path
|
|
68
|
+
* and the editor's workspace projection so the two cannot drift. */
|
|
69
|
+
export declare function reExportSpecsFromExports(moduleName: string, exportsResources: readonly unknown[] | undefined): ReExportSpec[];
|
|
70
|
+
/** Forward re-exported instances (`exports.resources: [!ref Alias.name]`) transitively so a
|
|
71
|
+
* consumer's `!ref Consumer.name` resolves in `resolveRefSentinels` (keyed by the RE-EXPORTING
|
|
72
|
+
* module). The owning instance is already forwarded under its own module; here we emit an
|
|
73
|
+
* additional copy stamped under each re-exporting module, with an already-canonical kind. A
|
|
74
|
+
* fixpoint loop forwards chains of arbitrary depth (`app → api → domain → …`): each pass can
|
|
75
|
+
* resolve a re-export whose source was emitted in a prior pass. Graph-agnostic: `aliasToModule`
|
|
76
|
+
* maps `(module, alias)` to the imported module's name. Mutates `result` in place. */
|
|
77
|
+
export declare function forwardReExportManifests(result: ResourceManifest[], specs: readonly ReExportSpec[], aliasToModule: (module: string, alias: string) => string | undefined): void;
|
|
78
|
+
/** Resolve every library's `exports.kinds` to a per-module map `suffix → canonical
|
|
79
|
+
* <owningModule>.<Kind>`, following re-exports (`Alias.Kind`) transitively via a fixpoint.
|
|
80
|
+
* `modules` lists each library's name + its raw `exports.kinds`; `aliasToModule(module, alias)`
|
|
81
|
+
* maps one of that module's import aliases to the imported module's name. Graph-agnostic —
|
|
82
|
+
* shared by the CLI graph path and the editor's workspace projection. */
|
|
83
|
+
export declare function resolveExportedKinds(modules: ReadonlyArray<{
|
|
84
|
+
module: string;
|
|
85
|
+
exportsKinds: readonly string[];
|
|
86
|
+
}>, aliasToModule: (module: string, alias: string) => string | undefined): Map<string, Map<string, string>>;
|
|
87
|
+
/** Stamp `metadata.reExportedKinds` (suffix → canonical kind) onto every `Telo.Import` whose
|
|
88
|
+
* target re-exports kinds, so the analyzer can register the re-export mappings. Only entries
|
|
89
|
+
* that point at a module OTHER than the import's own target are stamped (genuine re-exports;
|
|
90
|
+
* a locally-defined kind resolves through the normal alias path). Stamped on `metadata` (which
|
|
91
|
+
* permits additional properties, like `resolvedModuleName`) since the `Telo.Import` schema
|
|
92
|
+
* forbids extra top-level fields. Shared by both paths. */
|
|
93
|
+
export declare function stampReExportedKinds(imports: ReadonlyArray<{
|
|
94
|
+
manifest: ResourceManifest;
|
|
95
|
+
targetModule: string;
|
|
96
|
+
}>, exportedKinds: Map<string, Map<string, string>>): void;
|
|
45
97
|
/** Project a LoadedModule (owner + partials) to a flat ResourceManifest[]
|
|
46
98
|
* with `metadata.module` stamped on non-module docs. The kernel's runtime
|
|
47
99
|
* entry load uses this to convert a `Loader.loadModule` result into the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flatten-for-analyzer.d.ts","sourceRoot":"","sources":["../src/flatten-for-analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"flatten-for-analyzer.d.ts","sourceRoot":"","sources":["../src/flatten-for-analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAInE;;yEAEyE;AACzE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;4CAI4C;AAC5C,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB,CAGjE;AAED;;;;;;;;;;;;;;;;;;;2CAmB2C;AAC3C,wBAAgB,gCAAgC,CAC9C,eAAe,EAAE,gBAAgB,EAAE,EACnC,MAAM,EAAE,OAAO,GACd,gBAAgB,EAAE,CA6BpB;AAED;;;;;;;;;;;;;;;;;;;6EAmB6E;AAC7E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,WAAW,GAAG,gBAAgB,EAAE,CAiDzE;AAED;;uBAEuB;AACvB,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;qEAEqE;AACrE,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,MAAM,EAClB,gBAAgB,EAAE,SAAS,OAAO,EAAE,GAAG,SAAS,GAC/C,YAAY,EAAE,CAShB;AAED;;;;;;uFAMuF;AACvF,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,gBAAgB,EAAE,EAC1B,KAAK,EAAE,SAAS,YAAY,EAAE,EAC9B,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GACnE,IAAI,CAgDN;AAED;;;;0EAI0E;AAC1E,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,aAAa,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC,EAC3E,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GACnE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CA8BlC;AAED;;;;;4DAK4D;AAC5D,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,aAAa,CAAC;IAAE,QAAQ,EAAE,gBAAgB,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC,EAC5E,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAC9C,IAAI,CAWN;AAwCD;;;;;6BAK6B;AAC7B,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,YAAY,GAAG,gBAAgB,EAAE,CAEzE"}
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { isModuleKind } from "./module-kinds.js";
|
|
2
|
+
/** Parse a single dotted export entry: `Alias.Name` → `{name: "Name", alias: "Alias"}`,
|
|
3
|
+
* bare `Name` → `{name: "Name"}`. The single grammar for `exports.resources` and
|
|
4
|
+
* `exports.kinds`, shared by the kernel's import controller and the analyzer/editor so
|
|
5
|
+
* the dotted-name split can't drift. A leading dot (`.Name`) has no alias by design —
|
|
6
|
+
* the empty prefix isn't a valid alias. */
|
|
7
|
+
export function parseExportEntry(entry) {
|
|
8
|
+
const dot = entry.indexOf(".");
|
|
9
|
+
return dot > 0 ? { name: entry.slice(dot + 1), alias: entry.slice(0, dot) } : { name: entry };
|
|
10
|
+
}
|
|
2
11
|
/** The import-boundary forwarding rule, shared by `flattenForAnalyzer` (the
|
|
3
12
|
* CLI / kernel loader path) and the telo-editor's workspace projection so the
|
|
4
13
|
* two cannot drift. Given one module's stamped manifests and whether that
|
|
@@ -23,7 +32,15 @@ export function selectModuleManifestsForAnalysis(moduleManifests, isRoot) {
|
|
|
23
32
|
if (isRoot)
|
|
24
33
|
return moduleManifests;
|
|
25
34
|
const libDoc = moduleManifests.find((m) => isModuleKind(m.kind));
|
|
26
|
-
|
|
35
|
+
// An `exports.resources` entry is a bare name or a dotted `Alias.Name` (re-export). Only the
|
|
36
|
+
// export NAME matches a local instance below; re-exports are forwarded by `forwardReExports`.
|
|
37
|
+
const exportedResources = new Set();
|
|
38
|
+
for (const entry of libDoc?.exports
|
|
39
|
+
?.resources ?? []) {
|
|
40
|
+
if (typeof entry !== "string")
|
|
41
|
+
continue;
|
|
42
|
+
exportedResources.add(parseExportEntry(entry).name);
|
|
43
|
+
}
|
|
27
44
|
const out = [];
|
|
28
45
|
for (const m of moduleManifests) {
|
|
29
46
|
if (m.kind === "Telo.Definition" || m.kind === "Telo.Abstract" || m.kind === "Telo.Import") {
|
|
@@ -81,6 +98,7 @@ export function flattenForAnalyzer(graph) {
|
|
|
81
98
|
result.push(...selectModuleManifestsForAnalysis(collectModuleManifests(targetModule), false));
|
|
82
99
|
}
|
|
83
100
|
}
|
|
101
|
+
forwardReExports(graph, result);
|
|
84
102
|
// Stamp resolved import identity on every Telo.Import in the result by
|
|
85
103
|
// reading the edge's pre-resolved name/namespace — no re-derivation from
|
|
86
104
|
// manifest metadata. The edge is keyed by (owner-file, alias) which is
|
|
@@ -105,6 +123,179 @@ export function flattenForAnalyzer(graph) {
|
|
|
105
123
|
}
|
|
106
124
|
return result;
|
|
107
125
|
}
|
|
126
|
+
/** Extract re-export specs from a library doc's `exports.resources` — the dotted `Alias.Name`
|
|
127
|
+
* entries (bare-name locals are forwarded by the BFS instead). Shared by the CLI graph path
|
|
128
|
+
* and the editor's workspace projection so the two cannot drift. */
|
|
129
|
+
export function reExportSpecsFromExports(moduleName, exportsResources) {
|
|
130
|
+
const specs = [];
|
|
131
|
+
for (const entry of exportsResources ?? []) {
|
|
132
|
+
if (typeof entry !== "string")
|
|
133
|
+
continue;
|
|
134
|
+
const { name, alias } = parseExportEntry(entry);
|
|
135
|
+
if (!alias || alias === "Self")
|
|
136
|
+
continue;
|
|
137
|
+
specs.push({ module: moduleName, alias, name });
|
|
138
|
+
}
|
|
139
|
+
return specs;
|
|
140
|
+
}
|
|
141
|
+
/** Forward re-exported instances (`exports.resources: [!ref Alias.name]`) transitively so a
|
|
142
|
+
* consumer's `!ref Consumer.name` resolves in `resolveRefSentinels` (keyed by the RE-EXPORTING
|
|
143
|
+
* module). The owning instance is already forwarded under its own module; here we emit an
|
|
144
|
+
* additional copy stamped under each re-exporting module, with an already-canonical kind. A
|
|
145
|
+
* fixpoint loop forwards chains of arbitrary depth (`app → api → domain → …`): each pass can
|
|
146
|
+
* resolve a re-export whose source was emitted in a prior pass. Graph-agnostic: `aliasToModule`
|
|
147
|
+
* maps `(module, alias)` to the imported module's name. Mutates `result` in place. */
|
|
148
|
+
export function forwardReExportManifests(result, specs, aliasToModule) {
|
|
149
|
+
// Index forwarded instances by `module\0name` (only re-export TARGETS are forwarded).
|
|
150
|
+
const forwarded = new Map();
|
|
151
|
+
for (const m of result) {
|
|
152
|
+
const meta = m.metadata;
|
|
153
|
+
if (meta?.forwardedExport && meta.module && meta.name) {
|
|
154
|
+
forwarded.set(`${meta.module}\0${meta.name}`, m);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Canonicalize an authored/forwarded kind to a scope-independent `<module>.<Kind>` using the
|
|
158
|
+
// owning module's own import aliases. Idempotent: an already-canonical kind whose prefix isn't
|
|
159
|
+
// an alias of `ownerModule` is returned unchanged, so re-exports of re-exports stay stable.
|
|
160
|
+
const canonicalKind = (kind, ownerModule) => {
|
|
161
|
+
if (kind.startsWith("Self."))
|
|
162
|
+
return `${ownerModule}.${kind.slice("Self.".length)}`;
|
|
163
|
+
const dot = kind.indexOf(".");
|
|
164
|
+
if (dot <= 0)
|
|
165
|
+
return kind;
|
|
166
|
+
const target = aliasToModule(ownerModule, kind.slice(0, dot));
|
|
167
|
+
return target ? `${target}.${kind.slice(dot + 1)}` : kind;
|
|
168
|
+
};
|
|
169
|
+
// Fixpoint — bounded by the number of specs (each can be satisfied at most once).
|
|
170
|
+
for (let pass = 0; pass <= specs.length; pass++) {
|
|
171
|
+
let added = false;
|
|
172
|
+
for (const spec of specs) {
|
|
173
|
+
const key = `${spec.module}\0${spec.name}`;
|
|
174
|
+
if (forwarded.has(key))
|
|
175
|
+
continue;
|
|
176
|
+
const sourceModule = aliasToModule(spec.module, spec.alias);
|
|
177
|
+
if (!sourceModule)
|
|
178
|
+
continue;
|
|
179
|
+
const src = forwarded.get(`${sourceModule}\0${spec.name}`);
|
|
180
|
+
if (!src)
|
|
181
|
+
continue; // source not forwarded yet — a later pass may satisfy it
|
|
182
|
+
const kind = canonicalKind(src.kind, sourceModule);
|
|
183
|
+
const manifest = {
|
|
184
|
+
...src,
|
|
185
|
+
kind,
|
|
186
|
+
metadata: {
|
|
187
|
+
...src.metadata,
|
|
188
|
+
name: spec.name,
|
|
189
|
+
module: spec.module,
|
|
190
|
+
forwardedExport: true,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
result.push(manifest);
|
|
194
|
+
forwarded.set(key, manifest);
|
|
195
|
+
added = true;
|
|
196
|
+
}
|
|
197
|
+
if (!added)
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** Resolve every library's `exports.kinds` to a per-module map `suffix → canonical
|
|
202
|
+
* <owningModule>.<Kind>`, following re-exports (`Alias.Kind`) transitively via a fixpoint.
|
|
203
|
+
* `modules` lists each library's name + its raw `exports.kinds`; `aliasToModule(module, alias)`
|
|
204
|
+
* maps one of that module's import aliases to the imported module's name. Graph-agnostic —
|
|
205
|
+
* shared by the CLI graph path and the editor's workspace projection. */
|
|
206
|
+
export function resolveExportedKinds(modules, aliasToModule) {
|
|
207
|
+
const out = new Map();
|
|
208
|
+
const tableFor = (m) => {
|
|
209
|
+
let t = out.get(m);
|
|
210
|
+
if (!t)
|
|
211
|
+
out.set(m, (t = new Map()));
|
|
212
|
+
return t;
|
|
213
|
+
};
|
|
214
|
+
for (let pass = 0; pass <= modules.length; pass++) {
|
|
215
|
+
let changed = false;
|
|
216
|
+
for (const { module, exportsKinds } of modules) {
|
|
217
|
+
const table = tableFor(module);
|
|
218
|
+
for (const entry of exportsKinds) {
|
|
219
|
+
const { name: suffix, alias } = parseExportEntry(entry);
|
|
220
|
+
if (table.has(suffix))
|
|
221
|
+
continue;
|
|
222
|
+
if (!alias) {
|
|
223
|
+
table.set(suffix, `${module}.${suffix}`);
|
|
224
|
+
changed = true;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const source = aliasToModule(module, alias);
|
|
228
|
+
const canonical = source ? out.get(source)?.get(suffix) : undefined;
|
|
229
|
+
if (canonical) {
|
|
230
|
+
table.set(suffix, canonical);
|
|
231
|
+
changed = true;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (!changed)
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
/** Stamp `metadata.reExportedKinds` (suffix → canonical kind) onto every `Telo.Import` whose
|
|
241
|
+
* target re-exports kinds, so the analyzer can register the re-export mappings. Only entries
|
|
242
|
+
* that point at a module OTHER than the import's own target are stamped (genuine re-exports;
|
|
243
|
+
* a locally-defined kind resolves through the normal alias path). Stamped on `metadata` (which
|
|
244
|
+
* permits additional properties, like `resolvedModuleName`) since the `Telo.Import` schema
|
|
245
|
+
* forbids extra top-level fields. Shared by both paths. */
|
|
246
|
+
export function stampReExportedKinds(imports, exportedKinds) {
|
|
247
|
+
for (const { manifest, targetModule } of imports) {
|
|
248
|
+
const table = exportedKinds.get(targetModule);
|
|
249
|
+
if (!table)
|
|
250
|
+
continue;
|
|
251
|
+
const reExported = {};
|
|
252
|
+
for (const [suffix, canonical] of table) {
|
|
253
|
+
if (canonical !== `${targetModule}.${suffix}`)
|
|
254
|
+
reExported[suffix] = canonical;
|
|
255
|
+
}
|
|
256
|
+
if (Object.keys(reExported).length === 0)
|
|
257
|
+
continue;
|
|
258
|
+
manifest.metadata.reExportedKinds = reExported;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/** CLI/kernel adapter: collect re-export specs + alias map from a LoadedGraph. */
|
|
262
|
+
function forwardReExports(graph, result) {
|
|
263
|
+
const ownerSourceOf = new Map();
|
|
264
|
+
const specs = [];
|
|
265
|
+
const kindModules = [];
|
|
266
|
+
for (const [source, mod] of graph.modules) {
|
|
267
|
+
if (source === graph.rootSource)
|
|
268
|
+
continue; // root is an Application — no exports
|
|
269
|
+
const libDoc = mod.owner.manifests.find((m) => m && isModuleKind(m.kind));
|
|
270
|
+
const moduleName = libDoc?.metadata?.name;
|
|
271
|
+
if (!libDoc || !moduleName)
|
|
272
|
+
continue;
|
|
273
|
+
ownerSourceOf.set(moduleName, mod.owner.source);
|
|
274
|
+
specs.push(...reExportSpecsFromExports(moduleName, libDoc.exports?.resources));
|
|
275
|
+
kindModules.push({ module: moduleName, exportsKinds: libDoc.exports?.kinds ?? [] });
|
|
276
|
+
}
|
|
277
|
+
const aliasToModule = (module, alias) => {
|
|
278
|
+
const ownerSource = ownerSourceOf.get(module);
|
|
279
|
+
return ownerSource
|
|
280
|
+
? (graph.importEdges.get(ownerSource)?.get(alias)?.targetModuleName ?? undefined)
|
|
281
|
+
: undefined;
|
|
282
|
+
};
|
|
283
|
+
forwardReExportManifests(result, specs, aliasToModule);
|
|
284
|
+
// Resolve every library's re-exported kinds and stamp them onto the consumer-facing
|
|
285
|
+
// Telo.Import manifests so the analyzer can register the re-export mappings.
|
|
286
|
+
const exportedKinds = resolveExportedKinds(kindModules, aliasToModule);
|
|
287
|
+
const imports = [];
|
|
288
|
+
for (const m of result) {
|
|
289
|
+
if (m.kind !== "Telo.Import")
|
|
290
|
+
continue;
|
|
291
|
+
const owner = m.metadata?.source;
|
|
292
|
+
const alias = m.metadata?.name;
|
|
293
|
+
const target = owner && alias ? graph.importEdges.get(owner)?.get(alias)?.targetModuleName : undefined;
|
|
294
|
+
if (target)
|
|
295
|
+
imports.push({ manifest: m, targetModule: target });
|
|
296
|
+
}
|
|
297
|
+
stampReExportedKinds(imports, exportedKinds);
|
|
298
|
+
}
|
|
108
299
|
/** Project a LoadedModule (owner + partials) to a flat ResourceManifest[]
|
|
109
300
|
* with `metadata.module` stamped on non-module docs. The kernel's runtime
|
|
110
301
|
* entry load uses this to convert a `Loader.loadModule` result into the
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { AnalysisRegistry } from "./analysis-registry.js";
|
|
|
2
2
|
export type { RefFieldInfo } from "./analysis-registry.js";
|
|
3
3
|
export { StaticAnalyzer } from "./analyzer.js";
|
|
4
4
|
export type { GraphLoadError, ImportEdge, LoadedFile, LoadedGraph, LoadedModule, ParseError, } from "./loaded-types.js";
|
|
5
|
-
export { flattenForAnalyzer, flattenLoadedModule, selectModuleManifestsForAnalysis, } from "./flatten-for-analyzer.js";
|
|
5
|
+
export { flattenForAnalyzer, flattenLoadedModule, forwardReExportManifests, parseExportEntry, reExportSpecsFromExports, resolveExportedKinds, selectModuleManifestsForAnalysis, stampReExportedKinds, type ParsedExportEntry, type ReExportSpec, } from "./flatten-for-analyzer.js";
|
|
6
6
|
export { visitManifest } from "./manifest-visitor.js";
|
|
7
7
|
export type { CelSiteEvent, ManifestVisitor, RefSiteEvent, ResourceEnterEvent, ResourceExitEvent, ScopeBoundaryEvent, SchemaFromSiteEvent, VisitOptions, } from "./manifest-visitor.js";
|
|
8
8
|
export { Loader } from "./manifest-loader.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,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,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,EACH,kBAAkB,EAClB,mBAAmB,EACnB,gCAAgC,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,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,EACH,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,gBAAgB,EAChB,wBAAwB,EACxB,oBAAoB,EACpB,gCAAgC,EAChC,oBAAoB,EACpB,KAAK,iBAAiB,EACtB,KAAK,YAAY,GACpB,MAAM,2BAA2B,CAAC;AACnC,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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { AnalysisRegistry } from "./analysis-registry.js";
|
|
2
2
|
export { StaticAnalyzer } from "./analyzer.js";
|
|
3
|
-
export { flattenForAnalyzer, flattenLoadedModule, selectModuleManifestsForAnalysis, } from "./flatten-for-analyzer.js";
|
|
3
|
+
export { flattenForAnalyzer, flattenLoadedModule, forwardReExportManifests, parseExportEntry, reExportSpecsFromExports, resolveExportedKinds, selectModuleManifestsForAnalysis, stampReExportedKinds, } from "./flatten-for-analyzer.js";
|
|
4
4
|
export { visitManifest } from "./manifest-visitor.js";
|
|
5
5
|
export { Loader } from "./manifest-loader.js";
|
|
6
6
|
export { isModuleKind, MODULE_KINDS } from "./module-kinds.js";
|
package/package.json
CHANGED
package/src/alias-resolver.ts
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
export class AliasResolver {
|
|
4
4
|
private readonly importAliases = new Map<string, string>();
|
|
5
5
|
private readonly importedKinds = new Map<string, Set<string>>();
|
|
6
|
+
/** `${alias}.${suffix}` → canonical `<owningModule>.<Kind>` for kinds an import
|
|
7
|
+
* transitively RE-EXPORTS (`exports.kinds: [Alias.Kind]`), which don't live in the
|
|
8
|
+
* import's own module. Resolved before the normal `<module>.<suffix>` construction. */
|
|
9
|
+
private readonly reExportedKinds = new Map<string, string>();
|
|
6
10
|
|
|
7
11
|
registerImport(alias: string, targetModule: string, exportedKinds: string[]): void {
|
|
8
12
|
this.importAliases.set(alias, targetModule);
|
|
@@ -11,6 +15,12 @@ export class AliasResolver {
|
|
|
11
15
|
}
|
|
12
16
|
}
|
|
13
17
|
|
|
18
|
+
/** Register that `<alias>.<suffix>` re-exports the kind canonically named `canonicalKind`
|
|
19
|
+
* (owned by a module the alias's target imports, possibly several hops away). */
|
|
20
|
+
registerKindReExport(alias: string, suffix: string, canonicalKind: string): void {
|
|
21
|
+
this.reExportedKinds.set(`${alias}.${suffix}`, canonicalKind);
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
/** Real module name an alias points at (e.g. "Console" → "console"), or undefined.
|
|
15
25
|
* Used to resolve an alias-qualified instance reference "Console.writeLine" to the
|
|
16
26
|
* forwarded resource declared in that module. The `exports.resources` gate is enforced
|
|
@@ -29,6 +39,10 @@ export class AliasResolver {
|
|
|
29
39
|
if (dot === -1) return undefined;
|
|
30
40
|
const prefix = kind.slice(0, dot);
|
|
31
41
|
const suffix = kind.slice(dot + 1);
|
|
42
|
+
// Re-export takes precedence: a re-exported kind resolves to its true owning module,
|
|
43
|
+
// not `${prefix-target}.${suffix}` (and bypasses the gate — it's explicitly re-exported).
|
|
44
|
+
const reExported = this.reExportedKinds.get(`${prefix}.${suffix}`);
|
|
45
|
+
if (reExported) return reExported;
|
|
32
46
|
const realModule = this.importAliases.get(prefix);
|
|
33
47
|
if (!realModule) return undefined;
|
|
34
48
|
const allowed = this.importedKinds.get(prefix);
|
package/src/analyzer.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { AliasResolver } from "./alias-resolver.js";
|
|
|
5
5
|
import { AnalysisRegistry } from "./analysis-registry.js";
|
|
6
6
|
import {
|
|
7
7
|
buildCelEnvironment,
|
|
8
|
+
buildImportInputCelEnvironment,
|
|
8
9
|
buildTypedCelEnvironment,
|
|
9
10
|
type CelHandlers,
|
|
10
11
|
} from "./cel-environment.js";
|
|
@@ -706,16 +707,22 @@ export class StaticAnalyzer {
|
|
|
706
707
|
if (resolvedModuleName) {
|
|
707
708
|
defs.registerModuleIdentity(resolvedNamespace ?? null, resolvedModuleName);
|
|
708
709
|
}
|
|
710
|
+
// `metadata.reExportedKinds` (stamped by flattenForAnalyzer / the editor projection)
|
|
711
|
+
// maps an exported suffix to the true owning module's canonical kind for kinds this
|
|
712
|
+
// import transitively re-exports (`exports.kinds: [Alias.Kind]`).
|
|
713
|
+
const reExportedKinds = ((m.metadata as any)?.reExportedKinds ?? {}) as Record<
|
|
714
|
+
string,
|
|
715
|
+
string
|
|
716
|
+
>;
|
|
709
717
|
// Alias registration is scoped: consumer imports vs. imported-library imports.
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
libResolver.registerImport(alias, targetModule, exportedKinds);
|
|
718
|
+
const resolver =
|
|
719
|
+
!ownModule || rootModules.has(ownModule)
|
|
720
|
+
? aliases
|
|
721
|
+
: (aliasesByModule.get(ownModule) ??
|
|
722
|
+
aliasesByModule.set(ownModule, new AliasResolver()).get(ownModule)!);
|
|
723
|
+
resolver.registerImport(alias, targetModule, exportedKinds);
|
|
724
|
+
for (const [suffix, canonical] of Object.entries(reExportedKinds)) {
|
|
725
|
+
resolver.registerKindReExport(alias, suffix, canonical);
|
|
719
726
|
}
|
|
720
727
|
}
|
|
721
728
|
}
|
|
@@ -864,6 +871,26 @@ export class StaticAnalyzer {
|
|
|
864
871
|
}
|
|
865
872
|
}
|
|
866
873
|
}
|
|
874
|
+
// `exports.resources` entries are plain names: `Db` (local) or `Alias.Name` (re-export),
|
|
875
|
+
// mirroring `exports.kinds`. The `!ref` tag is not accepted here — a `!ref` parses to a
|
|
876
|
+
// sentinel object that the schema's CEL/ref exemption would silently pass, so reject any
|
|
877
|
+
// non-string entry with an actionable message instead.
|
|
878
|
+
const exportsResources = (m as Record<string, any>).exports?.resources;
|
|
879
|
+
if (Array.isArray(exportsResources)) {
|
|
880
|
+
for (let i = 0; i < exportsResources.length; i++) {
|
|
881
|
+
if (typeof exportsResources[i] === "string") continue;
|
|
882
|
+
diagnostics.push({
|
|
883
|
+
severity: DiagnosticSeverity.Error,
|
|
884
|
+
code: "INVALID_EXPORT",
|
|
885
|
+
source: SOURCE,
|
|
886
|
+
message:
|
|
887
|
+
`Telo.Library exports.resources[${i}]: write the exported name as a plain string — ` +
|
|
888
|
+
`'Name' to export a local instance, or 'Alias.Name' to re-export an imported one. ` +
|
|
889
|
+
`The '!ref' tag is not allowed in exports.resources.`,
|
|
890
|
+
data: { resource, filePath, path: `exports.resources.${i}` },
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
}
|
|
867
894
|
}
|
|
868
895
|
|
|
869
896
|
// Build typed kernel globals schema so x-telo-context chain validation
|
|
@@ -943,8 +970,27 @@ export class StaticAnalyzer {
|
|
|
943
970
|
},
|
|
944
971
|
}
|
|
945
972
|
: definition.schema;
|
|
946
|
-
// Phase 1: CEL type checking — walk data+schema together, check env.check() return types
|
|
947
|
-
|
|
973
|
+
// Phase 1: CEL type checking — walk data+schema together, check env.check() return types.
|
|
974
|
+
// A Telo.Import's variables/secrets are a config-only contract evaluated against the
|
|
975
|
+
// IMPORTING module's scope, so type them from the owning module doc (matched by
|
|
976
|
+
// `metadata.module`) and drop `resources`/`env` so referencing them is an error. A
|
|
977
|
+
// library's own internal import is validated against that library in the library's
|
|
978
|
+
// standalone analysis; in this flattened app pass the library doc is absent, so the
|
|
979
|
+
// importer is undefined here and variables/secrets fall back to a permissive `map`
|
|
980
|
+
// (no false positives) while resources/env stay rejected.
|
|
981
|
+
const importerModule =
|
|
982
|
+
m.kind === "Telo.Import"
|
|
983
|
+
? allManifests.find(
|
|
984
|
+
(mm) =>
|
|
985
|
+
(mm.kind === "Telo.Application" || mm.kind === "Telo.Library") &&
|
|
986
|
+
(mm.metadata as { name?: string } | undefined)?.name ===
|
|
987
|
+
(m.metadata as { module?: string } | undefined)?.module,
|
|
988
|
+
)
|
|
989
|
+
: undefined;
|
|
990
|
+
const baseTypedEnv =
|
|
991
|
+
m.kind === "Telo.Import"
|
|
992
|
+
? buildImportInputCelEnvironment(this.celEnv, importerModule)
|
|
993
|
+
: buildTypedCelEnvironment(this.celEnv, m, undefined, moduleManifest);
|
|
948
994
|
const celIssues = collectCelTypeIssues(
|
|
949
995
|
m,
|
|
950
996
|
schema,
|
package/src/builtins.ts
CHANGED
|
@@ -455,8 +455,10 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
455
455
|
type: "object",
|
|
456
456
|
properties: {
|
|
457
457
|
kinds: { type: "array", items: { type: "string" } },
|
|
458
|
-
//
|
|
459
|
-
//
|
|
458
|
+
// An entry is a bare name (`Db`, a locally-owned export) or a dotted `Alias.Name`
|
|
459
|
+
// (re-export of the instance reached via this library's import aliased `Alias`,
|
|
460
|
+
// under the name `Name`) — mirroring `exports.kinds`. `variables` / `secrets` are
|
|
461
|
+
// reserved on the resources.<Alias> value-flow surface, so they may not be exported.
|
|
460
462
|
resources: {
|
|
461
463
|
type: "array",
|
|
462
464
|
items: { type: "string", not: { enum: ["variables", "secrets"] } },
|
package/src/cel-environment.ts
CHANGED
|
@@ -102,3 +102,60 @@ export function buildTypedCelEnvironment(
|
|
|
102
102
|
return baseEnv.clone();
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
+
|
|
106
|
+
/** Register a `variables`/`secrets` namespace typed from a module doc's schema map
|
|
107
|
+
* (`{ name: <schema>, … }`), falling back to dyn `map` when absent or untyped. */
|
|
108
|
+
function registerConfigNamespace(
|
|
109
|
+
env: Environment,
|
|
110
|
+
block: unknown,
|
|
111
|
+
name: "variables" | "secrets",
|
|
112
|
+
): void {
|
|
113
|
+
if (block !== null && typeof block === "object" && !Array.isArray(block)) {
|
|
114
|
+
const entries = Object.entries(block as Record<string, unknown>).filter(
|
|
115
|
+
([, v]) => v !== null && typeof v === "object" && !Array.isArray(v),
|
|
116
|
+
);
|
|
117
|
+
if (entries.length > 0) {
|
|
118
|
+
const schema: Record<string, string> = {};
|
|
119
|
+
for (const [k, v] of entries) schema[k] = jsonSchemaToCelType(v as Record<string, any>);
|
|
120
|
+
(env as any).registerVariable({ name, schema });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
env.registerVariable(name, "map");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** CEL environment for the `variables:`/`secrets:` expressions on a `Telo.Import`.
|
|
128
|
+
*
|
|
129
|
+
* Import inputs are a config-only contract: their expressions are evaluated
|
|
130
|
+
* against the IMPORTING module's `variables`/`secrets`, never the import's own
|
|
131
|
+
* values map (the bug) nor the imported child's. `resources`, `env`, and `ports`
|
|
132
|
+
* are registered as empty typed objects, so referencing them is a "No such key"
|
|
133
|
+
* error that steers authors to a typed `variables` entry. */
|
|
134
|
+
export function buildImportInputCelEnvironment(
|
|
135
|
+
baseEnv: Environment,
|
|
136
|
+
moduleManifest: ResourceManifest | undefined,
|
|
137
|
+
): Environment {
|
|
138
|
+
const env = baseEnv.clone();
|
|
139
|
+
for (const brand of Object.keys(VALUE_BRAND_BASE)) {
|
|
140
|
+
(env as any).registerType(brand, { fields: {} });
|
|
141
|
+
}
|
|
142
|
+
const mod = moduleManifest as Record<string, unknown> | undefined;
|
|
143
|
+
// Typing variables/secrets from the importer's schema can fail on a malformed
|
|
144
|
+
// schema; degrade those to permissive `map` if so — but never lose the
|
|
145
|
+
// resources/env/ports rejection registered below (the catch is scoped so a
|
|
146
|
+
// typing failure can't silently re-open the config-only contract).
|
|
147
|
+
try {
|
|
148
|
+
registerConfigNamespace(env, mod?.variables, "variables");
|
|
149
|
+
registerConfigNamespace(env, mod?.secrets, "secrets");
|
|
150
|
+
} catch {
|
|
151
|
+
env.registerVariable("variables", "map");
|
|
152
|
+
env.registerVariable("secrets", "map");
|
|
153
|
+
}
|
|
154
|
+
// Override the base env's dyn `resources`/`env`/`ports` with empty typed objects
|
|
155
|
+
// so any access (`resources.X`, `env.X`) is a "No such key" error — these
|
|
156
|
+
// surfaces are not part of the config-only import contract.
|
|
157
|
+
for (const name of ["resources", "env", "ports"]) {
|
|
158
|
+
(env as any).registerVariable({ name, schema: {} });
|
|
159
|
+
}
|
|
160
|
+
return env;
|
|
161
|
+
}
|
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
-
import type {
|
|
2
|
+
import type { LoadedGraph, LoadedModule } from "./loaded-types.js";
|
|
3
|
+
import type { LoadedFile } from "./loaded-types.js";
|
|
3
4
|
import { isModuleKind } from "./module-kinds.js";
|
|
4
5
|
|
|
6
|
+
/** One parsed `exports.resources` / `exports.kinds` entry. `name` is the exported
|
|
7
|
+
* instance name or kind suffix (the part after the dot, or the whole entry); `alias`
|
|
8
|
+
* (when set) is this library's own import the entry RE-EXPORTS from. */
|
|
9
|
+
export interface ParsedExportEntry {
|
|
10
|
+
name: string;
|
|
11
|
+
alias?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Parse a single dotted export entry: `Alias.Name` → `{name: "Name", alias: "Alias"}`,
|
|
15
|
+
* bare `Name` → `{name: "Name"}`. The single grammar for `exports.resources` and
|
|
16
|
+
* `exports.kinds`, shared by the kernel's import controller and the analyzer/editor so
|
|
17
|
+
* the dotted-name split can't drift. A leading dot (`.Name`) has no alias by design —
|
|
18
|
+
* the empty prefix isn't a valid alias. */
|
|
19
|
+
export function parseExportEntry(entry: string): ParsedExportEntry {
|
|
20
|
+
const dot = entry.indexOf(".");
|
|
21
|
+
return dot > 0 ? { name: entry.slice(dot + 1), alias: entry.slice(0, dot) } : { name: entry };
|
|
22
|
+
}
|
|
23
|
+
|
|
5
24
|
/** The import-boundary forwarding rule, shared by `flattenForAnalyzer` (the
|
|
6
25
|
* CLI / kernel loader path) and the telo-editor's workspace projection so the
|
|
7
26
|
* two cannot drift. Given one module's stamped manifests and whether that
|
|
@@ -29,9 +48,14 @@ export function selectModuleManifestsForAnalysis(
|
|
|
29
48
|
if (isRoot) return moduleManifests;
|
|
30
49
|
|
|
31
50
|
const libDoc = moduleManifests.find((m) => isModuleKind(m.kind));
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
);
|
|
51
|
+
// An `exports.resources` entry is a bare name or a dotted `Alias.Name` (re-export). Only the
|
|
52
|
+
// export NAME matches a local instance below; re-exports are forwarded by `forwardReExports`.
|
|
53
|
+
const exportedResources = new Set<string>();
|
|
54
|
+
for (const entry of (libDoc as { exports?: { resources?: unknown[] } } | undefined)?.exports
|
|
55
|
+
?.resources ?? []) {
|
|
56
|
+
if (typeof entry !== "string") continue;
|
|
57
|
+
exportedResources.add(parseExportEntry(entry).name);
|
|
58
|
+
}
|
|
35
59
|
|
|
36
60
|
const out: ResourceManifest[] = [];
|
|
37
61
|
for (const m of moduleManifests) {
|
|
@@ -96,6 +120,8 @@ export function flattenForAnalyzer(graph: LoadedGraph): ResourceManifest[] {
|
|
|
96
120
|
}
|
|
97
121
|
}
|
|
98
122
|
|
|
123
|
+
forwardReExports(graph, result);
|
|
124
|
+
|
|
99
125
|
// Stamp resolved import identity on every Telo.Import in the result by
|
|
100
126
|
// reading the edge's pre-resolved name/namespace — no re-derivation from
|
|
101
127
|
// manifest metadata. The edge is keyed by (owner-file, alias) which is
|
|
@@ -120,6 +146,193 @@ export function flattenForAnalyzer(graph: LoadedGraph): ResourceManifest[] {
|
|
|
120
146
|
return result;
|
|
121
147
|
}
|
|
122
148
|
|
|
149
|
+
/** A re-export declared in a library's `exports.resources` as a dotted `Alias.Name`:
|
|
150
|
+
* module `module` re-exports the instance `name` reached through its own import
|
|
151
|
+
* aliased `alias`. */
|
|
152
|
+
export interface ReExportSpec {
|
|
153
|
+
module: string;
|
|
154
|
+
alias: string;
|
|
155
|
+
name: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Extract re-export specs from a library doc's `exports.resources` — the dotted `Alias.Name`
|
|
159
|
+
* entries (bare-name locals are forwarded by the BFS instead). Shared by the CLI graph path
|
|
160
|
+
* and the editor's workspace projection so the two cannot drift. */
|
|
161
|
+
export function reExportSpecsFromExports(
|
|
162
|
+
moduleName: string,
|
|
163
|
+
exportsResources: readonly unknown[] | undefined,
|
|
164
|
+
): ReExportSpec[] {
|
|
165
|
+
const specs: ReExportSpec[] = [];
|
|
166
|
+
for (const entry of exportsResources ?? []) {
|
|
167
|
+
if (typeof entry !== "string") continue;
|
|
168
|
+
const { name, alias } = parseExportEntry(entry);
|
|
169
|
+
if (!alias || alias === "Self") continue;
|
|
170
|
+
specs.push({ module: moduleName, alias, name });
|
|
171
|
+
}
|
|
172
|
+
return specs;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Forward re-exported instances (`exports.resources: [!ref Alias.name]`) transitively so a
|
|
176
|
+
* consumer's `!ref Consumer.name` resolves in `resolveRefSentinels` (keyed by the RE-EXPORTING
|
|
177
|
+
* module). The owning instance is already forwarded under its own module; here we emit an
|
|
178
|
+
* additional copy stamped under each re-exporting module, with an already-canonical kind. A
|
|
179
|
+
* fixpoint loop forwards chains of arbitrary depth (`app → api → domain → …`): each pass can
|
|
180
|
+
* resolve a re-export whose source was emitted in a prior pass. Graph-agnostic: `aliasToModule`
|
|
181
|
+
* maps `(module, alias)` to the imported module's name. Mutates `result` in place. */
|
|
182
|
+
export function forwardReExportManifests(
|
|
183
|
+
result: ResourceManifest[],
|
|
184
|
+
specs: readonly ReExportSpec[],
|
|
185
|
+
aliasToModule: (module: string, alias: string) => string | undefined,
|
|
186
|
+
): void {
|
|
187
|
+
// Index forwarded instances by `module\0name` (only re-export TARGETS are forwarded).
|
|
188
|
+
const forwarded = new Map<string, ResourceManifest>();
|
|
189
|
+
for (const m of result) {
|
|
190
|
+
const meta = m.metadata as { module?: string; name?: string; forwardedExport?: boolean };
|
|
191
|
+
if (meta?.forwardedExport && meta.module && meta.name) {
|
|
192
|
+
forwarded.set(`${meta.module}\0${meta.name}`, m);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Canonicalize an authored/forwarded kind to a scope-independent `<module>.<Kind>` using the
|
|
197
|
+
// owning module's own import aliases. Idempotent: an already-canonical kind whose prefix isn't
|
|
198
|
+
// an alias of `ownerModule` is returned unchanged, so re-exports of re-exports stay stable.
|
|
199
|
+
const canonicalKind = (kind: string, ownerModule: string): string => {
|
|
200
|
+
if (kind.startsWith("Self.")) return `${ownerModule}.${kind.slice("Self.".length)}`;
|
|
201
|
+
const dot = kind.indexOf(".");
|
|
202
|
+
if (dot <= 0) return kind;
|
|
203
|
+
const target = aliasToModule(ownerModule, kind.slice(0, dot));
|
|
204
|
+
return target ? `${target}.${kind.slice(dot + 1)}` : kind;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Fixpoint — bounded by the number of specs (each can be satisfied at most once).
|
|
208
|
+
for (let pass = 0; pass <= specs.length; pass++) {
|
|
209
|
+
let added = false;
|
|
210
|
+
for (const spec of specs) {
|
|
211
|
+
const key = `${spec.module}\0${spec.name}`;
|
|
212
|
+
if (forwarded.has(key)) continue;
|
|
213
|
+
const sourceModule = aliasToModule(spec.module, spec.alias);
|
|
214
|
+
if (!sourceModule) continue;
|
|
215
|
+
const src = forwarded.get(`${sourceModule}\0${spec.name}`);
|
|
216
|
+
if (!src) continue; // source not forwarded yet — a later pass may satisfy it
|
|
217
|
+
const kind = canonicalKind(src.kind as string, sourceModule);
|
|
218
|
+
const manifest: ResourceManifest = {
|
|
219
|
+
...src,
|
|
220
|
+
kind,
|
|
221
|
+
metadata: {
|
|
222
|
+
...src.metadata,
|
|
223
|
+
name: spec.name,
|
|
224
|
+
module: spec.module,
|
|
225
|
+
forwardedExport: true,
|
|
226
|
+
} as ResourceManifest["metadata"],
|
|
227
|
+
};
|
|
228
|
+
result.push(manifest);
|
|
229
|
+
forwarded.set(key, manifest);
|
|
230
|
+
added = true;
|
|
231
|
+
}
|
|
232
|
+
if (!added) break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Resolve every library's `exports.kinds` to a per-module map `suffix → canonical
|
|
237
|
+
* <owningModule>.<Kind>`, following re-exports (`Alias.Kind`) transitively via a fixpoint.
|
|
238
|
+
* `modules` lists each library's name + its raw `exports.kinds`; `aliasToModule(module, alias)`
|
|
239
|
+
* maps one of that module's import aliases to the imported module's name. Graph-agnostic —
|
|
240
|
+
* shared by the CLI graph path and the editor's workspace projection. */
|
|
241
|
+
export function resolveExportedKinds(
|
|
242
|
+
modules: ReadonlyArray<{ module: string; exportsKinds: readonly string[] }>,
|
|
243
|
+
aliasToModule: (module: string, alias: string) => string | undefined,
|
|
244
|
+
): Map<string, Map<string, string>> {
|
|
245
|
+
const out = new Map<string, Map<string, string>>();
|
|
246
|
+
const tableFor = (m: string): Map<string, string> => {
|
|
247
|
+
let t = out.get(m);
|
|
248
|
+
if (!t) out.set(m, (t = new Map()));
|
|
249
|
+
return t;
|
|
250
|
+
};
|
|
251
|
+
for (let pass = 0; pass <= modules.length; pass++) {
|
|
252
|
+
let changed = false;
|
|
253
|
+
for (const { module, exportsKinds } of modules) {
|
|
254
|
+
const table = tableFor(module);
|
|
255
|
+
for (const entry of exportsKinds) {
|
|
256
|
+
const { name: suffix, alias } = parseExportEntry(entry);
|
|
257
|
+
if (table.has(suffix)) continue;
|
|
258
|
+
if (!alias) {
|
|
259
|
+
table.set(suffix, `${module}.${suffix}`);
|
|
260
|
+
changed = true;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const source = aliasToModule(module, alias);
|
|
264
|
+
const canonical = source ? out.get(source)?.get(suffix) : undefined;
|
|
265
|
+
if (canonical) {
|
|
266
|
+
table.set(suffix, canonical);
|
|
267
|
+
changed = true;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (!changed) break;
|
|
272
|
+
}
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Stamp `metadata.reExportedKinds` (suffix → canonical kind) onto every `Telo.Import` whose
|
|
277
|
+
* target re-exports kinds, so the analyzer can register the re-export mappings. Only entries
|
|
278
|
+
* that point at a module OTHER than the import's own target are stamped (genuine re-exports;
|
|
279
|
+
* a locally-defined kind resolves through the normal alias path). Stamped on `metadata` (which
|
|
280
|
+
* permits additional properties, like `resolvedModuleName`) since the `Telo.Import` schema
|
|
281
|
+
* forbids extra top-level fields. Shared by both paths. */
|
|
282
|
+
export function stampReExportedKinds(
|
|
283
|
+
imports: ReadonlyArray<{ manifest: ResourceManifest; targetModule: string }>,
|
|
284
|
+
exportedKinds: Map<string, Map<string, string>>,
|
|
285
|
+
): void {
|
|
286
|
+
for (const { manifest, targetModule } of imports) {
|
|
287
|
+
const table = exportedKinds.get(targetModule);
|
|
288
|
+
if (!table) continue;
|
|
289
|
+
const reExported: Record<string, string> = {};
|
|
290
|
+
for (const [suffix, canonical] of table) {
|
|
291
|
+
if (canonical !== `${targetModule}.${suffix}`) reExported[suffix] = canonical;
|
|
292
|
+
}
|
|
293
|
+
if (Object.keys(reExported).length === 0) continue;
|
|
294
|
+
(manifest.metadata as Record<string, unknown>).reExportedKinds = reExported;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** CLI/kernel adapter: collect re-export specs + alias map from a LoadedGraph. */
|
|
299
|
+
function forwardReExports(graph: LoadedGraph, result: ResourceManifest[]): void {
|
|
300
|
+
const ownerSourceOf = new Map<string, string>();
|
|
301
|
+
const specs: ReExportSpec[] = [];
|
|
302
|
+
const kindModules: Array<{ module: string; exportsKinds: string[] }> = [];
|
|
303
|
+
for (const [source, mod] of graph.modules) {
|
|
304
|
+
if (source === graph.rootSource) continue; // root is an Application — no exports
|
|
305
|
+
const libDoc = mod.owner.manifests.find((m) => m && isModuleKind(m.kind)) as
|
|
306
|
+
| (ResourceManifest & { exports?: { resources?: unknown[]; kinds?: string[] } })
|
|
307
|
+
| undefined;
|
|
308
|
+
const moduleName = libDoc?.metadata?.name as string | undefined;
|
|
309
|
+
if (!libDoc || !moduleName) continue;
|
|
310
|
+
ownerSourceOf.set(moduleName, mod.owner.source);
|
|
311
|
+
specs.push(...reExportSpecsFromExports(moduleName, libDoc.exports?.resources));
|
|
312
|
+
kindModules.push({ module: moduleName, exportsKinds: libDoc.exports?.kinds ?? [] });
|
|
313
|
+
}
|
|
314
|
+
const aliasToModule = (module: string, alias: string): string | undefined => {
|
|
315
|
+
const ownerSource = ownerSourceOf.get(module);
|
|
316
|
+
return ownerSource
|
|
317
|
+
? (graph.importEdges.get(ownerSource)?.get(alias)?.targetModuleName ?? undefined)
|
|
318
|
+
: undefined;
|
|
319
|
+
};
|
|
320
|
+
forwardReExportManifests(result, specs, aliasToModule);
|
|
321
|
+
|
|
322
|
+
// Resolve every library's re-exported kinds and stamp them onto the consumer-facing
|
|
323
|
+
// Telo.Import manifests so the analyzer can register the re-export mappings.
|
|
324
|
+
const exportedKinds = resolveExportedKinds(kindModules, aliasToModule);
|
|
325
|
+
const imports: Array<{ manifest: ResourceManifest; targetModule: string }> = [];
|
|
326
|
+
for (const m of result) {
|
|
327
|
+
if (m.kind !== "Telo.Import") continue;
|
|
328
|
+
const owner = (m.metadata as { source?: string } | undefined)?.source;
|
|
329
|
+
const alias = m.metadata?.name as string | undefined;
|
|
330
|
+
const target = owner && alias ? graph.importEdges.get(owner)?.get(alias)?.targetModuleName : undefined;
|
|
331
|
+
if (target) imports.push({ manifest: m, targetModule: target });
|
|
332
|
+
}
|
|
333
|
+
stampReExportedKinds(imports, exportedKinds);
|
|
334
|
+
}
|
|
335
|
+
|
|
123
336
|
/** Project a LoadedModule (owner + partials) to a flat ResourceManifest[]
|
|
124
337
|
* with `metadata.module` stamped on non-module docs. The kernel's runtime
|
|
125
338
|
* entry load uses this to convert a `Loader.loadModule` result into the
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,14 @@ export type {
|
|
|
12
12
|
export {
|
|
13
13
|
flattenForAnalyzer,
|
|
14
14
|
flattenLoadedModule,
|
|
15
|
+
forwardReExportManifests,
|
|
16
|
+
parseExportEntry,
|
|
17
|
+
reExportSpecsFromExports,
|
|
18
|
+
resolveExportedKinds,
|
|
15
19
|
selectModuleManifestsForAnalysis,
|
|
20
|
+
stampReExportedKinds,
|
|
21
|
+
type ParsedExportEntry,
|
|
22
|
+
type ReExportSpec,
|
|
16
23
|
} from "./flatten-for-analyzer.js";
|
|
17
24
|
export { visitManifest } from "./manifest-visitor.js";
|
|
18
25
|
export type {
|