@telorun/analyzer 0.1.3 → 0.1.4
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/adapters/node-adapter.d.ts +2 -0
- package/dist/adapters/node-adapter.d.ts.map +1 -1
- package/dist/adapters/node-adapter.js +41 -3
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +4 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/manifest-loader.d.ts +8 -1
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +133 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/builtins.ts +4 -0
- package/src/index.ts +0 -1
- package/src/manifest-loader.ts +166 -7
- package/src/types.ts +10 -0
- package/src/adapters/node-adapter.ts +0 -38
|
@@ -9,6 +9,8 @@ export declare class NodeAdapter implements ManifestAdapter {
|
|
|
9
9
|
source: string;
|
|
10
10
|
}>;
|
|
11
11
|
resolveRelative(base: string, relative: string): string;
|
|
12
|
+
expandGlob(base: string, patterns: string[]): Promise<string[]>;
|
|
13
|
+
resolveOwnerOf(fileUrl: string): Promise<string | null>;
|
|
12
14
|
}
|
|
13
15
|
/** @deprecated Use `new NodeAdapter(cwd)` instead */
|
|
14
16
|
export declare function createNodeAdapter(cwd?: string): ManifestAdapter;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/node-adapter.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"node-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/node-adapter.ts"],"names":[],"mappings":"AAIA,OAAO,EAA6B,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAM9E,gFAAgF;AAChF,qBAAa,WAAY,YAAW,eAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,GAAE,MAAsB;IAExD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAUxB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IASlE,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAKjD,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAc/D,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CAoB9D;AAED,qDAAqD;AACrD,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAsB,GAAG,eAAe,CAE9E"}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import * as fs from "fs/promises";
|
|
2
2
|
import * as path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { minimatch } from "minimatch";
|
|
3
5
|
import { DEFAULT_MANIFEST_FILENAME } from "../types.js";
|
|
6
|
+
function toFilePath(url) {
|
|
7
|
+
return url.startsWith("file://") ? fileURLToPath(url) : url;
|
|
8
|
+
}
|
|
4
9
|
/** Node.js fs-based ManifestAdapter for local files. Not browser-compatible. */
|
|
5
10
|
export class NodeAdapter {
|
|
6
11
|
cwd;
|
|
@@ -15,17 +20,50 @@ export class NodeAdapter {
|
|
|
15
20
|
(!url.includes("://") && !url.includes("@")));
|
|
16
21
|
}
|
|
17
22
|
async read(url) {
|
|
18
|
-
const filePath =
|
|
23
|
+
const filePath = toFilePath(url);
|
|
19
24
|
const stat = await fs.stat(filePath).catch(() => null);
|
|
20
25
|
const resolvedPath = stat?.isDirectory() ? path.join(filePath, DEFAULT_MANIFEST_FILENAME) : filePath;
|
|
21
26
|
const text = await fs.readFile(resolvedPath, "utf8");
|
|
22
27
|
return { text, source: resolvedPath };
|
|
23
28
|
}
|
|
24
29
|
resolveRelative(base, relative) {
|
|
25
|
-
const
|
|
26
|
-
const baseDir = path.dirname(path.resolve(this.cwd, basePath));
|
|
30
|
+
const baseDir = path.dirname(path.resolve(this.cwd, toFilePath(base)));
|
|
27
31
|
return path.resolve(baseDir, relative);
|
|
28
32
|
}
|
|
33
|
+
async expandGlob(base, patterns) {
|
|
34
|
+
const baseDir = path.dirname(path.resolve(this.cwd, toFilePath(base)));
|
|
35
|
+
const entries = await fs.readdir(baseDir, { recursive: true, encoding: "utf8" });
|
|
36
|
+
const normalizedPatterns = patterns.map((p) => p.replace(/\\/g, "/").replace(/^\.\//, ""));
|
|
37
|
+
const matched = [];
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
const normalized = entry.replace(/\\/g, "/");
|
|
40
|
+
if (normalizedPatterns.some((p) => minimatch(normalized, p))) {
|
|
41
|
+
matched.push(path.resolve(baseDir, entry));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return matched.sort();
|
|
45
|
+
}
|
|
46
|
+
async resolveOwnerOf(fileUrl) {
|
|
47
|
+
const resolved = path.resolve(this.cwd, toFilePath(fileUrl));
|
|
48
|
+
let dir = path.dirname(resolved);
|
|
49
|
+
while (true) {
|
|
50
|
+
const candidate = path.join(dir, DEFAULT_MANIFEST_FILENAME);
|
|
51
|
+
if (candidate !== resolved) {
|
|
52
|
+
try {
|
|
53
|
+
await fs.access(candidate);
|
|
54
|
+
return candidate;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// telo.yaml not found at this level
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const parent = path.dirname(dir);
|
|
61
|
+
if (parent === dir)
|
|
62
|
+
break;
|
|
63
|
+
dir = parent;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
29
67
|
}
|
|
30
68
|
/** @deprecated Use `new NodeAdapter(cwd)` instead */
|
|
31
69
|
export function createNodeAdapter(cwd = process.cwd()) {
|
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,EAgH/C,CAAC"}
|
package/dist/builtins.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export { HttpAdapter } from "./adapters/http-adapter.js";
|
|
2
|
-
export { createNodeAdapter, NodeAdapter } from "./adapters/node-adapter.js";
|
|
3
2
|
export { RegistryAdapter } from "./adapters/registry-adapter.js";
|
|
4
3
|
export { AnalysisRegistry } from "./analysis-registry.js";
|
|
5
4
|
export { StaticAnalyzer } from "./analyzer.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,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC3E,YAAY,EACR,kBAAkB,EAClB,eAAe,EAAE,iBAAiB,EAAE,WAAW,EAAE,eAAe,EAChE,QAAQ,EACR,aAAa,EACb,KAAK,EACR,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export { HttpAdapter } from "./adapters/http-adapter.js";
|
|
2
|
-
export { createNodeAdapter, NodeAdapter } from "./adapters/node-adapter.js";
|
|
3
2
|
export { RegistryAdapter } from "./adapters/registry-adapter.js";
|
|
4
3
|
export { AnalysisRegistry } from "./analysis-registry.js";
|
|
5
4
|
export { StaticAnalyzer } from "./analyzer.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ResourceManifest } from "@telorun/sdk";
|
|
2
|
-
import type
|
|
2
|
+
import { type LoadOptions, type LoaderInitOptions, type ManifestAdapter } from "./types.js";
|
|
3
3
|
export declare class Loader {
|
|
4
4
|
private static readonly moduleCache;
|
|
5
5
|
protected adapters: ManifestAdapter[];
|
|
@@ -8,6 +8,13 @@ export declare class Loader {
|
|
|
8
8
|
private pick;
|
|
9
9
|
resolveEntryPoint(url: string): Promise<string>;
|
|
10
10
|
loadModule(url: string, options?: LoadOptions): Promise<ResourceManifest[]>;
|
|
11
|
+
private resolveIncludes;
|
|
12
|
+
private loadPartialFile;
|
|
13
|
+
loadModuleForFile(fileUrl: string): Promise<{
|
|
14
|
+
ownerUrl: string;
|
|
15
|
+
manifests: ResourceManifest[];
|
|
16
|
+
sourceManifests: Map<string, ResourceManifest[]>;
|
|
17
|
+
} | null>;
|
|
11
18
|
loadModuleGraph(entryUrl: string, onError?: (url: string, error: Error) => void): Promise<Map<string, ResourceManifest[]>>;
|
|
12
19
|
loadManifests(entryUrl: string): Promise<ResourceManifest[]>;
|
|
13
20
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest-loader.d.ts","sourceRoot":"","sources":["../src/manifest-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAKtE,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"manifest-loader.d.ts","sourceRoot":"","sources":["../src/manifest-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAKtE,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EAGrB,MAAM,YAAY,CAAC;AAIpB,qBAAa,MAAM;IACjB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAG/B;IAEJ,SAAS,CAAC,QAAQ,EAAE,eAAe,EAAE,CAAC;gBAE1B,sBAAsB,GAAE,eAAe,EAAE,GAAG,iBAAsB;IAiB9E,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI;IAKxC,OAAO,CAAC,IAAI;IAMN,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAK/C,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;YAmGnE,eAAe;YAoBf,eAAe;IAgEvB,iBAAiB,CACrB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;QACT,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,gBAAgB,EAAE,CAAC;QAC9B,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;KAClD,GAAG,IAAI,CAAC;IAiCH,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,GAC5C,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;IAsCrC,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;CAoDnE"}
|
package/dist/manifest-loader.js
CHANGED
|
@@ -3,6 +3,8 @@ import { isMap, isPair, isScalar, isSeq, parseAllDocuments } from "yaml";
|
|
|
3
3
|
import { HttpAdapter } from "./adapters/http-adapter.js";
|
|
4
4
|
import { RegistryAdapter } from "./adapters/registry-adapter.js";
|
|
5
5
|
import { precompileDoc } from "./precompile.js";
|
|
6
|
+
import { DEFAULT_MANIFEST_FILENAME, } from "./types.js";
|
|
7
|
+
const SYSTEM_KINDS = new Set(["Kernel.Module", "Kernel.Import", "Kernel.Definition"]);
|
|
6
8
|
export class Loader {
|
|
7
9
|
static moduleCache = new Map();
|
|
8
10
|
adapters;
|
|
@@ -105,9 +107,126 @@ export class Loader {
|
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
109
|
}
|
|
108
|
-
|
|
110
|
+
// Expand include directives — load partial files into the same module scope.
|
|
111
|
+
// Results with includes are NOT cached because partial file content is not
|
|
112
|
+
// tracked in the cache key — the cache would serve stale data if a partial changes.
|
|
113
|
+
let hasIncludes = false;
|
|
114
|
+
if (moduleManifest) {
|
|
115
|
+
const includePatterns = moduleManifest.include;
|
|
116
|
+
if (includePatterns?.length) {
|
|
117
|
+
hasIncludes = true;
|
|
118
|
+
const adapter = this.pick(source);
|
|
119
|
+
const includedFiles = await this.resolveIncludes(source, includePatterns, adapter);
|
|
120
|
+
for (const includedUrl of includedFiles) {
|
|
121
|
+
const partialManifests = await this.loadPartialFile(includedUrl, moduleName, options);
|
|
122
|
+
resolved.push(...partialManifests);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (!hasIncludes) {
|
|
127
|
+
Loader.moduleCache.set(cacheKey, { text, manifests: resolved });
|
|
128
|
+
}
|
|
109
129
|
return cloneManifestArray(resolved);
|
|
110
130
|
}
|
|
131
|
+
async resolveIncludes(ownerSource, patterns, adapter) {
|
|
132
|
+
const hasGlobs = patterns.some((p) => /[*?{}\[\]]/.test(p));
|
|
133
|
+
if (hasGlobs) {
|
|
134
|
+
if (!adapter.expandGlob) {
|
|
135
|
+
throw new Error(`Include patterns in '${ownerSource}' contain globs but the adapter for this source ` +
|
|
136
|
+
`does not support glob expansion. Use explicit file paths instead of patterns like: ` +
|
|
137
|
+
patterns.filter((p) => /[*?{}\[\]]/.test(p)).join(", "));
|
|
138
|
+
}
|
|
139
|
+
return adapter.expandGlob(ownerSource, patterns);
|
|
140
|
+
}
|
|
141
|
+
// Literal relative paths — deduplicate in case the same file appears under multiple patterns.
|
|
142
|
+
return [...new Set(patterns.map((p) => adapter.resolveRelative(ownerSource, p)))];
|
|
143
|
+
}
|
|
144
|
+
async loadPartialFile(url, ownerModuleName, options) {
|
|
145
|
+
const { text, source } = await this.pick(url).read(url);
|
|
146
|
+
const parsedDocuments = parseAllDocuments(text);
|
|
147
|
+
const rawDocs = parsedDocuments.map((d) => d.toJSON());
|
|
148
|
+
const offsets = documentLineOffsets(text);
|
|
149
|
+
const lineOffsets = buildLineOffsets(text);
|
|
150
|
+
const resolved = [];
|
|
151
|
+
let docIdx = 0;
|
|
152
|
+
for (const rawDoc of rawDocs) {
|
|
153
|
+
const currentDocIdx = docIdx++;
|
|
154
|
+
const sourceLine = offsets[currentDocIdx] ?? 0;
|
|
155
|
+
const positionIndex = buildPositionIndex(parsedDocuments[currentDocIdx], lineOffsets);
|
|
156
|
+
if (rawDoc === null || rawDoc === undefined)
|
|
157
|
+
continue;
|
|
158
|
+
const kind = rawDoc.kind;
|
|
159
|
+
if (kind && SYSTEM_KINDS.has(kind)) {
|
|
160
|
+
throw new Error(`Included file '${source}' contains '${kind}' which is not allowed in partial files. ` +
|
|
161
|
+
`Only the owner telo.yaml may declare ${kind} resources.`);
|
|
162
|
+
}
|
|
163
|
+
let compiledDocs;
|
|
164
|
+
if (options?.compile) {
|
|
165
|
+
try {
|
|
166
|
+
const result = precompileDoc(rawDoc);
|
|
167
|
+
compiledDocs = Array.isArray(result) ? result : [result];
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
throw new Error(`Failed to compile manifest in ${source}: ${error instanceof Error ? error.message : String(error)}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
compiledDocs = [rawDoc];
|
|
175
|
+
}
|
|
176
|
+
for (const doc of compiledDocs) {
|
|
177
|
+
if (doc === null || doc === undefined)
|
|
178
|
+
continue;
|
|
179
|
+
const manifest = doc;
|
|
180
|
+
const metadata = {
|
|
181
|
+
...manifest.metadata,
|
|
182
|
+
source,
|
|
183
|
+
sourceLine,
|
|
184
|
+
...(ownerModuleName && !manifest.metadata?.module ? { module: ownerModuleName } : {}),
|
|
185
|
+
};
|
|
186
|
+
Object.defineProperty(metadata, "positionIndex", {
|
|
187
|
+
value: positionIndex,
|
|
188
|
+
enumerable: false,
|
|
189
|
+
writable: true,
|
|
190
|
+
configurable: true,
|
|
191
|
+
});
|
|
192
|
+
resolved.push({ ...manifest, metadata });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return resolved;
|
|
196
|
+
}
|
|
197
|
+
async loadModuleForFile(fileUrl) {
|
|
198
|
+
// Try loading as a regular module first (it might be a telo.yaml itself).
|
|
199
|
+
// Use loadManifests (not loadModule) so imported definitions are included —
|
|
200
|
+
// otherwise the analyzer won't know about kinds from Kernel.Import sources.
|
|
201
|
+
try {
|
|
202
|
+
const docs = await this.loadModule(fileUrl);
|
|
203
|
+
const hasModule = docs.some((d) => d.kind === "Kernel.Module");
|
|
204
|
+
if (hasModule) {
|
|
205
|
+
const { source } = await this.pick(fileUrl).read(fileUrl);
|
|
206
|
+
const manifests = await this.loadManifests(fileUrl);
|
|
207
|
+
return { ownerUrl: source, manifests, sourceManifests: groupBySource(manifests) };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
// If the file looks like an owner manifest (named telo.yaml), rethrow —
|
|
212
|
+
// a broken owner shouldn't silently fall through to parent lookup.
|
|
213
|
+
const normalized = fileUrl.replace(/\\/g, "/");
|
|
214
|
+
if (normalized.endsWith(`/${DEFAULT_MANIFEST_FILENAME}`) || normalized === DEFAULT_MANIFEST_FILENAME) {
|
|
215
|
+
throw err;
|
|
216
|
+
}
|
|
217
|
+
// Otherwise fall through to owner lookup — this is likely a partial file
|
|
218
|
+
}
|
|
219
|
+
// Find the owning telo.yaml via parent-directory traversal
|
|
220
|
+
const adapter = this.pick(fileUrl);
|
|
221
|
+
if (!adapter.resolveOwnerOf)
|
|
222
|
+
return null;
|
|
223
|
+
const ownerUrl = await adapter.resolveOwnerOf(fileUrl);
|
|
224
|
+
if (!ownerUrl)
|
|
225
|
+
return null;
|
|
226
|
+
// Load the owner module (which will load included files via include expansion)
|
|
227
|
+
const manifests = await this.loadManifests(ownerUrl);
|
|
228
|
+
return { ownerUrl, manifests, sourceManifests: groupBySource(manifests) };
|
|
229
|
+
}
|
|
111
230
|
async loadModuleGraph(entryUrl, onError) {
|
|
112
231
|
const visited = new Set([entryUrl]);
|
|
113
232
|
const result = new Map();
|
|
@@ -224,6 +343,19 @@ function cloneManifestValue(value) {
|
|
|
224
343
|
}
|
|
225
344
|
return value;
|
|
226
345
|
}
|
|
346
|
+
function groupBySource(manifests) {
|
|
347
|
+
const map = new Map();
|
|
348
|
+
for (const m of manifests) {
|
|
349
|
+
const src = m.metadata?.source ?? "unknown";
|
|
350
|
+
let list = map.get(src);
|
|
351
|
+
if (!list) {
|
|
352
|
+
list = [];
|
|
353
|
+
map.set(src, list);
|
|
354
|
+
}
|
|
355
|
+
list.push(m);
|
|
356
|
+
}
|
|
357
|
+
return map;
|
|
358
|
+
}
|
|
227
359
|
function documentLineOffsets(text) {
|
|
228
360
|
const offsets = [0];
|
|
229
361
|
const lines = text.split("\n");
|
package/dist/types.d.ts
CHANGED
|
@@ -42,6 +42,14 @@ export interface ManifestAdapter {
|
|
|
42
42
|
source: string;
|
|
43
43
|
}>;
|
|
44
44
|
resolveRelative(base: string, relative: string): string;
|
|
45
|
+
/** Expand glob patterns relative to a base source. Returns sources in the same
|
|
46
|
+
* format as read().source — suitable to pass back into read() / resolveRelative().
|
|
47
|
+
* Optional — only filesystem-capable adapters implement this. */
|
|
48
|
+
expandGlob?(base: string, patterns: string[]): Promise<string[]>;
|
|
49
|
+
/** Walk parent directories from fileUrl looking for the nearest telo.yaml.
|
|
50
|
+
* Returns the source in the same format as read().source, or null if none found.
|
|
51
|
+
* Optional — only filesystem-capable adapters implement this. */
|
|
52
|
+
resolveOwnerOf?(fileUrl: string): Promise<string | null>;
|
|
45
53
|
}
|
|
46
54
|
export interface LoadOptions {
|
|
47
55
|
/** When true, each YAML document is passed through the CEL precompiler before being
|
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,eAAe;IAC9B,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;
|
|
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,eAAe;IAC9B,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;;sEAEkE;IAClE,UAAU,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAEjE;;sEAEkE;IAClE,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;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,gEAAgE;IAChE,aAAa,CAAC,EAAE,eAAe,EAAE,CAAC;IAClC,sDAAsD;IACtD,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,0DAA0D;IAC1D,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,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;CACrE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/analyzer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"@marcbachmann/cel-js": "^7.5.3",
|
|
40
40
|
"ajv": "^8.17.1",
|
|
41
41
|
"ajv-formats": "^3.0.1",
|
|
42
|
-
"yaml": "^2.8.3",
|
|
43
42
|
"jsonpath-plus": "^10.3.0",
|
|
43
|
+
"yaml": "^2.8.3",
|
|
44
44
|
"@telorun/sdk": "0.2.8"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
package/src/builtins.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export { HttpAdapter } from "./adapters/http-adapter.js";
|
|
2
|
-
export { createNodeAdapter, NodeAdapter } from "./adapters/node-adapter.js";
|
|
3
2
|
export { RegistryAdapter } from "./adapters/registry-adapter.js";
|
|
4
3
|
export { AnalysisRegistry } from "./analysis-registry.js";
|
|
5
4
|
export { StaticAnalyzer } from "./analyzer.js";
|
package/src/manifest-loader.ts
CHANGED
|
@@ -3,14 +3,17 @@ import { isMap, isPair, isScalar, isSeq, parseAllDocuments, type Document } from
|
|
|
3
3
|
import { HttpAdapter } from "./adapters/http-adapter.js";
|
|
4
4
|
import { RegistryAdapter } from "./adapters/registry-adapter.js";
|
|
5
5
|
import { precompileDoc } from "./precompile.js";
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_MANIFEST_FILENAME,
|
|
8
|
+
type LoadOptions,
|
|
9
|
+
type LoaderInitOptions,
|
|
10
|
+
type ManifestAdapter,
|
|
11
|
+
type Position,
|
|
12
|
+
type PositionIndex,
|
|
12
13
|
} from "./types.js";
|
|
13
14
|
|
|
15
|
+
const SYSTEM_KINDS = new Set(["Kernel.Module", "Kernel.Import", "Kernel.Definition"]);
|
|
16
|
+
|
|
14
17
|
export class Loader {
|
|
15
18
|
private static readonly moduleCache = new Map<
|
|
16
19
|
string,
|
|
@@ -128,10 +131,152 @@ export class Loader {
|
|
|
128
131
|
}
|
|
129
132
|
}
|
|
130
133
|
|
|
131
|
-
|
|
134
|
+
// Expand include directives — load partial files into the same module scope.
|
|
135
|
+
// Results with includes are NOT cached because partial file content is not
|
|
136
|
+
// tracked in the cache key — the cache would serve stale data if a partial changes.
|
|
137
|
+
let hasIncludes = false;
|
|
138
|
+
if (moduleManifest) {
|
|
139
|
+
const includePatterns = (moduleManifest as any).include as string[] | undefined;
|
|
140
|
+
if (includePatterns?.length) {
|
|
141
|
+
hasIncludes = true;
|
|
142
|
+
const adapter = this.pick(source);
|
|
143
|
+
const includedFiles = await this.resolveIncludes(source, includePatterns, adapter);
|
|
144
|
+
for (const includedUrl of includedFiles) {
|
|
145
|
+
const partialManifests = await this.loadPartialFile(includedUrl, moduleName, options);
|
|
146
|
+
resolved.push(...partialManifests);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!hasIncludes) {
|
|
152
|
+
Loader.moduleCache.set(cacheKey, { text, manifests: resolved });
|
|
153
|
+
}
|
|
132
154
|
return cloneManifestArray(resolved);
|
|
133
155
|
}
|
|
134
156
|
|
|
157
|
+
private async resolveIncludes(
|
|
158
|
+
ownerSource: string,
|
|
159
|
+
patterns: string[],
|
|
160
|
+
adapter: ManifestAdapter,
|
|
161
|
+
): Promise<string[]> {
|
|
162
|
+
const hasGlobs = patterns.some((p) => /[*?{}\[\]]/.test(p));
|
|
163
|
+
if (hasGlobs) {
|
|
164
|
+
if (!adapter.expandGlob) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Include patterns in '${ownerSource}' contain globs but the adapter for this source ` +
|
|
167
|
+
`does not support glob expansion. Use explicit file paths instead of patterns like: ` +
|
|
168
|
+
patterns.filter((p) => /[*?{}\[\]]/.test(p)).join(", "),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return adapter.expandGlob(ownerSource, patterns);
|
|
172
|
+
}
|
|
173
|
+
// Literal relative paths — deduplicate in case the same file appears under multiple patterns.
|
|
174
|
+
return [...new Set(patterns.map((p) => adapter.resolveRelative(ownerSource, p)))];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private async loadPartialFile(
|
|
178
|
+
url: string,
|
|
179
|
+
ownerModuleName: string | undefined,
|
|
180
|
+
options?: LoadOptions,
|
|
181
|
+
): Promise<ResourceManifest[]> {
|
|
182
|
+
const { text, source } = await this.pick(url).read(url);
|
|
183
|
+
|
|
184
|
+
const parsedDocuments = parseAllDocuments(text);
|
|
185
|
+
const rawDocs = parsedDocuments.map((d) => d.toJSON());
|
|
186
|
+
const offsets = documentLineOffsets(text);
|
|
187
|
+
const lineOffsets = buildLineOffsets(text);
|
|
188
|
+
const resolved: ResourceManifest[] = [];
|
|
189
|
+
let docIdx = 0;
|
|
190
|
+
|
|
191
|
+
for (const rawDoc of rawDocs) {
|
|
192
|
+
const currentDocIdx = docIdx++;
|
|
193
|
+
const sourceLine = offsets[currentDocIdx] ?? 0;
|
|
194
|
+
const positionIndex = buildPositionIndex(parsedDocuments[currentDocIdx], lineOffsets);
|
|
195
|
+
if (rawDoc === null || rawDoc === undefined) continue;
|
|
196
|
+
|
|
197
|
+
const kind = rawDoc.kind as string | undefined;
|
|
198
|
+
if (kind && SYSTEM_KINDS.has(kind)) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`Included file '${source}' contains '${kind}' which is not allowed in partial files. ` +
|
|
201
|
+
`Only the owner telo.yaml may declare ${kind} resources.`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let compiledDocs: unknown[];
|
|
206
|
+
if (options?.compile) {
|
|
207
|
+
try {
|
|
208
|
+
const result = precompileDoc(rawDoc);
|
|
209
|
+
compiledDocs = Array.isArray(result) ? result : [result];
|
|
210
|
+
} catch (error) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`Failed to compile manifest in ${source}: ${error instanceof Error ? error.message : String(error)}`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
compiledDocs = [rawDoc];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const doc of compiledDocs) {
|
|
220
|
+
if (doc === null || doc === undefined) continue;
|
|
221
|
+
const manifest = doc as ResourceManifest;
|
|
222
|
+
const metadata = {
|
|
223
|
+
...manifest.metadata,
|
|
224
|
+
source,
|
|
225
|
+
sourceLine,
|
|
226
|
+
...(ownerModuleName && !manifest.metadata?.module ? { module: ownerModuleName } : {}),
|
|
227
|
+
};
|
|
228
|
+
Object.defineProperty(metadata, "positionIndex", {
|
|
229
|
+
value: positionIndex,
|
|
230
|
+
enumerable: false,
|
|
231
|
+
writable: true,
|
|
232
|
+
configurable: true,
|
|
233
|
+
});
|
|
234
|
+
resolved.push({ ...manifest, metadata });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return resolved;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async loadModuleForFile(
|
|
242
|
+
fileUrl: string,
|
|
243
|
+
): Promise<{
|
|
244
|
+
ownerUrl: string;
|
|
245
|
+
manifests: ResourceManifest[];
|
|
246
|
+
sourceManifests: Map<string, ResourceManifest[]>;
|
|
247
|
+
} | null> {
|
|
248
|
+
// Try loading as a regular module first (it might be a telo.yaml itself).
|
|
249
|
+
// Use loadManifests (not loadModule) so imported definitions are included —
|
|
250
|
+
// otherwise the analyzer won't know about kinds from Kernel.Import sources.
|
|
251
|
+
try {
|
|
252
|
+
const docs = await this.loadModule(fileUrl);
|
|
253
|
+
const hasModule = docs.some((d) => d.kind === "Kernel.Module");
|
|
254
|
+
if (hasModule) {
|
|
255
|
+
const { source } = await this.pick(fileUrl).read(fileUrl);
|
|
256
|
+
const manifests = await this.loadManifests(fileUrl);
|
|
257
|
+
return { ownerUrl: source, manifests, sourceManifests: groupBySource(manifests) };
|
|
258
|
+
}
|
|
259
|
+
} catch (err) {
|
|
260
|
+
// If the file looks like an owner manifest (named telo.yaml), rethrow —
|
|
261
|
+
// a broken owner shouldn't silently fall through to parent lookup.
|
|
262
|
+
const normalized = fileUrl.replace(/\\/g, "/");
|
|
263
|
+
if (normalized.endsWith(`/${DEFAULT_MANIFEST_FILENAME}`) || normalized === DEFAULT_MANIFEST_FILENAME) {
|
|
264
|
+
throw err;
|
|
265
|
+
}
|
|
266
|
+
// Otherwise fall through to owner lookup — this is likely a partial file
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Find the owning telo.yaml via parent-directory traversal
|
|
270
|
+
const adapter = this.pick(fileUrl);
|
|
271
|
+
if (!adapter.resolveOwnerOf) return null;
|
|
272
|
+
const ownerUrl = await adapter.resolveOwnerOf(fileUrl);
|
|
273
|
+
if (!ownerUrl) return null;
|
|
274
|
+
|
|
275
|
+
// Load the owner module (which will load included files via include expansion)
|
|
276
|
+
const manifests = await this.loadManifests(ownerUrl);
|
|
277
|
+
return { ownerUrl, manifests, sourceManifests: groupBySource(manifests) };
|
|
278
|
+
}
|
|
279
|
+
|
|
135
280
|
async loadModuleGraph(
|
|
136
281
|
entryUrl: string,
|
|
137
282
|
onError?: (url: string, error: Error) => void,
|
|
@@ -253,6 +398,20 @@ function cloneManifestValue<T>(value: T): T {
|
|
|
253
398
|
return value;
|
|
254
399
|
}
|
|
255
400
|
|
|
401
|
+
function groupBySource(manifests: ResourceManifest[]): Map<string, ResourceManifest[]> {
|
|
402
|
+
const map = new Map<string, ResourceManifest[]>();
|
|
403
|
+
for (const m of manifests) {
|
|
404
|
+
const src = (m.metadata?.source as string) ?? "unknown";
|
|
405
|
+
let list = map.get(src);
|
|
406
|
+
if (!list) {
|
|
407
|
+
list = [];
|
|
408
|
+
map.set(src, list);
|
|
409
|
+
}
|
|
410
|
+
list.push(m);
|
|
411
|
+
}
|
|
412
|
+
return map;
|
|
413
|
+
}
|
|
414
|
+
|
|
256
415
|
function documentLineOffsets(text: string): number[] {
|
|
257
416
|
const offsets = [0];
|
|
258
417
|
const lines = text.split("\n");
|
package/src/types.ts
CHANGED
|
@@ -45,6 +45,16 @@ export interface ManifestAdapter {
|
|
|
45
45
|
supports(url: string): boolean;
|
|
46
46
|
read(url: string): Promise<{ text: string; source: string }>;
|
|
47
47
|
resolveRelative(base: string, relative: string): string;
|
|
48
|
+
|
|
49
|
+
/** Expand glob patterns relative to a base source. Returns sources in the same
|
|
50
|
+
* format as read().source — suitable to pass back into read() / resolveRelative().
|
|
51
|
+
* Optional — only filesystem-capable adapters implement this. */
|
|
52
|
+
expandGlob?(base: string, patterns: string[]): Promise<string[]>;
|
|
53
|
+
|
|
54
|
+
/** Walk parent directories from fileUrl looking for the nearest telo.yaml.
|
|
55
|
+
* Returns the source in the same format as read().source, or null if none found.
|
|
56
|
+
* Optional — only filesystem-capable adapters implement this. */
|
|
57
|
+
resolveOwnerOf?(fileUrl: string): Promise<string | null>;
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
export interface LoadOptions {
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import * as fs from "fs/promises";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
import { DEFAULT_MANIFEST_FILENAME, type ManifestAdapter } from "../types.js";
|
|
4
|
-
|
|
5
|
-
/** Node.js fs-based ManifestAdapter for local files. Not browser-compatible. */
|
|
6
|
-
export class NodeAdapter implements ManifestAdapter {
|
|
7
|
-
constructor(private readonly cwd: string = process.cwd()) {}
|
|
8
|
-
|
|
9
|
-
supports(url: string): boolean {
|
|
10
|
-
return (
|
|
11
|
-
url.startsWith("file://") ||
|
|
12
|
-
url.startsWith("/") ||
|
|
13
|
-
url.startsWith("./") ||
|
|
14
|
-
url.startsWith("../") ||
|
|
15
|
-
(!url.includes("://") && !url.includes("@"))
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async read(url: string): Promise<{ text: string; source: string }> {
|
|
20
|
-
const filePath = url.startsWith("file://") ? new URL(url).pathname : url;
|
|
21
|
-
const stat = await fs.stat(filePath).catch(() => null);
|
|
22
|
-
const resolvedPath =
|
|
23
|
-
stat?.isDirectory() ? path.join(filePath, DEFAULT_MANIFEST_FILENAME) : filePath;
|
|
24
|
-
const text = await fs.readFile(resolvedPath, "utf8");
|
|
25
|
-
return { text, source: resolvedPath };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
resolveRelative(base: string, relative: string): string {
|
|
29
|
-
const basePath = base.startsWith("file://") ? new URL(base).pathname : base;
|
|
30
|
-
const baseDir = path.dirname(path.resolve(this.cwd, basePath));
|
|
31
|
-
return path.resolve(baseDir, relative);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** @deprecated Use `new NodeAdapter(cwd)` instead */
|
|
36
|
-
export function createNodeAdapter(cwd: string = process.cwd()): ManifestAdapter {
|
|
37
|
-
return new NodeAdapter(cwd);
|
|
38
|
-
}
|