@telorun/analyzer 0.27.0 → 0.28.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 +1 -140
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/loaded-types.d.ts +15 -2
- package/dist/loaded-types.d.ts.map +1 -1
- package/dist/manifest-loader.d.ts +6 -1
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +21 -17
- package/dist/reconcile-module-versions.d.ts +25 -0
- package/dist/reconcile-module-versions.d.ts.map +1 -0
- package/dist/reconcile-module-versions.js +215 -0
- package/dist/sources/default-sources.d.ts +8 -0
- package/dist/sources/default-sources.d.ts.map +1 -0
- package/dist/sources/default-sources.js +10 -0
- package/dist/types.d.ts +0 -8
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +3 -0
- package/src/loaded-types.ts +15 -2
- package/src/manifest-loader.ts +22 -19
- package/src/reconcile-module-versions.ts +253 -0
- package/src/sources/default-sources.ts +12 -0
- package/src/types.ts +0 -8
package/README.md
CHANGED
|
@@ -44,146 +44,7 @@ $ telo ./examples/hello-api
|
|
|
44
44
|
|
|
45
45
|
## Example manifest
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
```yaml
|
|
50
|
-
kind: Telo.Application
|
|
51
|
-
metadata:
|
|
52
|
-
name: feedback
|
|
53
|
-
version: 1.0.0
|
|
54
|
-
description: |
|
|
55
|
-
A complete feedback collection REST API — no code, pure YAML.
|
|
56
|
-
Persists entries to SQLite and serves them over HTTP.
|
|
57
|
-
imports:
|
|
58
|
-
Http: std/http-server@0.12.0
|
|
59
|
-
Sql: std/sql@0.9.2
|
|
60
|
-
targets:
|
|
61
|
-
- !ref Migrations
|
|
62
|
-
- !ref Server
|
|
63
|
-
---
|
|
64
|
-
# SQLite database — swap driver/host/database for PostgreSQL with zero YAML changes
|
|
65
|
-
kind: Sql.Connection
|
|
66
|
-
metadata:
|
|
67
|
-
name: Db
|
|
68
|
-
driver: sqlite
|
|
69
|
-
file: ./tmp/feedback.db
|
|
70
|
-
---
|
|
71
|
-
# Migrations: applied automatically before the server starts
|
|
72
|
-
kind: Sql.Migrations
|
|
73
|
-
metadata:
|
|
74
|
-
name: Migrations
|
|
75
|
-
connection: !ref Db
|
|
76
|
-
---
|
|
77
|
-
kind: Sql.Migration
|
|
78
|
-
metadata:
|
|
79
|
-
name: Migration_20260413_182154_CreateFeedback
|
|
80
|
-
version: 20260413_182154_CreateFeedback
|
|
81
|
-
sql: |
|
|
82
|
-
CREATE TABLE IF NOT EXISTS feedback (
|
|
83
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
84
|
-
text TEXT NOT NULL,
|
|
85
|
-
source TEXT,
|
|
86
|
-
score INTEGER NOT NULL DEFAULT 0,
|
|
87
|
-
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
88
|
-
)
|
|
89
|
-
---
|
|
90
|
-
kind: Http.Server
|
|
91
|
-
metadata:
|
|
92
|
-
name: Server
|
|
93
|
-
baseUrl: http://localhost:8844
|
|
94
|
-
port: 8844
|
|
95
|
-
logger: true
|
|
96
|
-
openapi:
|
|
97
|
-
info:
|
|
98
|
-
title: Feedback API
|
|
99
|
-
version: 1.0.0
|
|
100
|
-
mounts:
|
|
101
|
-
- path: /v1
|
|
102
|
-
mount: !ref FeedbackRoutes
|
|
103
|
-
---
|
|
104
|
-
kind: Http.Api
|
|
105
|
-
metadata:
|
|
106
|
-
name: FeedbackRoutes
|
|
107
|
-
routes:
|
|
108
|
-
# POST /v1/feedback — insert a new entry, score derived from body length heuristic
|
|
109
|
-
- request:
|
|
110
|
-
path: /feedback
|
|
111
|
-
method: POST
|
|
112
|
-
schema:
|
|
113
|
-
body:
|
|
114
|
-
type: object
|
|
115
|
-
properties:
|
|
116
|
-
text:
|
|
117
|
-
type: string
|
|
118
|
-
minLength: 1
|
|
119
|
-
source:
|
|
120
|
-
type: string
|
|
121
|
-
required: [ text ]
|
|
122
|
-
handler:
|
|
123
|
-
kind: Sql.Exec
|
|
124
|
-
connection: !ref Db
|
|
125
|
-
inputs:
|
|
126
|
-
sql: "INSERT INTO feedback (text, source, score) VALUES (?, ?, ?)"
|
|
127
|
-
bindings:
|
|
128
|
-
- "${{ request.body.text }}"
|
|
129
|
-
- "${{ request.body.source }}"
|
|
130
|
-
- "${{ size(request.body.text) }}"
|
|
131
|
-
response:
|
|
132
|
-
- status: 201
|
|
133
|
-
headers:
|
|
134
|
-
Content-Type: application/json
|
|
135
|
-
body:
|
|
136
|
-
ok: true
|
|
137
|
-
message: Feedback received
|
|
138
|
-
|
|
139
|
-
# GET /v1/feedback — list all entries, newest first
|
|
140
|
-
- request:
|
|
141
|
-
path: /feedback
|
|
142
|
-
method: GET
|
|
143
|
-
handler:
|
|
144
|
-
kind: Sql.Select
|
|
145
|
-
connection: !ref Db
|
|
146
|
-
from: feedback
|
|
147
|
-
columns: [ id, text, source, score, created_at ]
|
|
148
|
-
orderBy:
|
|
149
|
-
- { column: created_at, direction: desc }
|
|
150
|
-
response:
|
|
151
|
-
- status: 200
|
|
152
|
-
headers:
|
|
153
|
-
Content-Type: application/json
|
|
154
|
-
body: "${{ result.rows }}"
|
|
155
|
-
|
|
156
|
-
# GET /v1/feedback/{id} — fetch a single entry
|
|
157
|
-
- request:
|
|
158
|
-
path: /feedback/{id}
|
|
159
|
-
method: GET
|
|
160
|
-
schema:
|
|
161
|
-
params:
|
|
162
|
-
type: object
|
|
163
|
-
properties:
|
|
164
|
-
id:
|
|
165
|
-
type: integer
|
|
166
|
-
required: [ id ]
|
|
167
|
-
handler:
|
|
168
|
-
kind: Sql.Select
|
|
169
|
-
connection: !ref Db
|
|
170
|
-
from: feedback
|
|
171
|
-
columns: [ id, text, source, score, created_at ]
|
|
172
|
-
where:
|
|
173
|
-
- { column: id, op: "=", value: "${{ request.params.id }}" }
|
|
174
|
-
response:
|
|
175
|
-
- status: 200
|
|
176
|
-
when: "size(result.rows) > 0"
|
|
177
|
-
headers:
|
|
178
|
-
Content-Type: application/json
|
|
179
|
-
body: "${{ result.rows[0] }}"
|
|
180
|
-
- status: 404
|
|
181
|
-
headers:
|
|
182
|
-
Content-Type: application/json
|
|
183
|
-
body:
|
|
184
|
-
ok: false
|
|
185
|
-
message: Not found
|
|
186
|
-
```
|
|
47
|
+
See [examples/](./examples/) for a list of working applications.
|
|
187
48
|
|
|
188
49
|
## Status
|
|
189
50
|
|
package/dist/index.d.ts
CHANGED
|
@@ -12,11 +12,14 @@ export { parseLoadedFile } from "./parse-loaded-file.js";
|
|
|
12
12
|
export type { ParseOptions } from "./parse-loaded-file.js";
|
|
13
13
|
export { desugarLoadedFile, inlineImportManifests } from "./inline-imports.js";
|
|
14
14
|
export type { SyntheticImport } from "./inline-imports.js";
|
|
15
|
+
export { reconcileModuleVersions } from "./reconcile-module-versions.js";
|
|
16
|
+
export type { VersionReconciliation } from "./reconcile-module-versions.js";
|
|
15
17
|
export { residualEntrySchema, residualEntrySchemaMap } from "./residual-schema.js";
|
|
16
18
|
export { buildDocumentPositions, buildLineOffsets, buildPositionIndex, documentLineOffsets, } from "./position-metadata.js";
|
|
17
19
|
export type { DocumentPosition } from "./position-metadata.js";
|
|
18
20
|
export { HttpSource } from "./sources/http-source.js";
|
|
19
21
|
export { RegistrySource } from "./sources/registry-source.js";
|
|
22
|
+
export { defaultSources } from "./sources/default-sources.js";
|
|
20
23
|
export { withSyntheticPositions } from "./with-synthetic-positions.js";
|
|
21
24
|
export { DEFAULT_MANIFEST_FILENAME, DiagnosticSeverity } from "./types.js";
|
|
22
25
|
export type { AnalysisDiagnostic, AnalysisOptions, LoaderInitOptions, LoadOptions, ManifestSource, Position, PositionIndex, Range } from "./types.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,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"}
|
|
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,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,YAAY,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAC5E,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,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
|
@@ -6,9 +6,11 @@ export { Loader } from "./manifest-loader.js";
|
|
|
6
6
|
export { isModuleKind, MODULE_KINDS } from "./module-kinds.js";
|
|
7
7
|
export { parseLoadedFile } from "./parse-loaded-file.js";
|
|
8
8
|
export { desugarLoadedFile, inlineImportManifests } from "./inline-imports.js";
|
|
9
|
+
export { reconcileModuleVersions } from "./reconcile-module-versions.js";
|
|
9
10
|
export { residualEntrySchema, residualEntrySchemaMap } from "./residual-schema.js";
|
|
10
11
|
export { buildDocumentPositions, buildLineOffsets, buildPositionIndex, documentLineOffsets, } from "./position-metadata.js";
|
|
11
12
|
export { HttpSource } from "./sources/http-source.js";
|
|
12
13
|
export { RegistrySource } from "./sources/registry-source.js";
|
|
14
|
+
export { defaultSources } from "./sources/default-sources.js";
|
|
13
15
|
export { withSyntheticPositions } from "./with-synthetic-positions.js";
|
|
14
16
|
export { DEFAULT_MANIFEST_FILENAME, DiagnosticSeverity } from "./types.js";
|
package/dist/loaded-types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
2
|
import type { Document } from "yaml";
|
|
3
3
|
import type { DocumentPosition } from "./position-metadata.js";
|
|
4
|
-
import type { Range } from "./types.js";
|
|
4
|
+
import type { AnalysisDiagnostic, Range } from "./types.js";
|
|
5
5
|
/** One physical file's parsed result. Returned for the owner manifest, for
|
|
6
6
|
* each `include:` partial, and for each external import target.
|
|
7
7
|
*
|
|
@@ -65,8 +65,21 @@ export interface LoadedGraph {
|
|
|
65
65
|
* its partials. */
|
|
66
66
|
modules: Map<string, LoadedModule>;
|
|
67
67
|
/** Per-Telo.Import resolution. Keyed by the resolved URL of the file the
|
|
68
|
-
* Telo.Import was declared in, then by the import's PascalCase alias.
|
|
68
|
+
* Telo.Import was declared in, then by the import's PascalCase alias.
|
|
69
|
+
* Version reconciliation repoints losing edges at their winner here, so a
|
|
70
|
+
* consumer walking these edges (`flattenForAnalyzer`) sees one version per
|
|
71
|
+
* module identity. */
|
|
69
72
|
importEdges: Map<string, Map<string, ImportEdge>>;
|
|
73
|
+
/** Version-reconciliation redirects: a losing module's canonical source URL →
|
|
74
|
+
* the winning version's canonical source URL. The runtime consults this when
|
|
75
|
+
* it independently re-resolves an import (the analyzer already sees repointed
|
|
76
|
+
* `importEdges`). Empty when no module identity appeared at two sources. */
|
|
77
|
+
overrides: Map<string, string>;
|
|
78
|
+
/** Diagnostics produced while reconciling module versions — one per import
|
|
79
|
+
* edge redirected to a different version (warning for a same-major hoist,
|
|
80
|
+
* error for a major mismatch). Surfaced alongside `analyze()` diagnostics by
|
|
81
|
+
* every consumer (CLI, editor, VS Code). */
|
|
82
|
+
versionDiagnostics: AnalysisDiagnostic[];
|
|
70
83
|
/** Surface-level errors that did not abort the graph load (e.g. an import
|
|
71
84
|
* whose target failed to fetch). */
|
|
72
85
|
errors: GraphLoadError[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loaded-types.d.ts","sourceRoot":"","sources":["../src/loaded-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"loaded-types.d.ts","sourceRoot":"","sources":["../src/loaded-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAE5D;;;;4EAI4E;AAC5E,MAAM,WAAW,UAAU;IACzB;+DAC2D;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf;gEAC4D;IAC5D,YAAY,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,6EAA6E;IAC7E,SAAS,EAAE,KAAK,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAC1C,0EAA0E;IAC1E,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,0EAA0E;IAC1E,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED;mCACmC;AACnC,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,UAAU,CAAC;IAClB;oEACgE;IAChE,QAAQ,EAAE,UAAU,EAAE,CAAC;CACxB;AAED;;;;;2EAK2E;AAC3E,MAAM,WAAW,UAAU;IACzB,mEAAmE;IACnE,YAAY,EAAE,MAAM,CAAC;IACrB;6EACyE;IACzE,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,oEAAoE;IACpE,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED;0BAC0B;AAC1B,MAAM,WAAW,WAAW;IAC1B,4DAA4D;IAC5D,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,YAAY,CAAC;IACpB;;wBAEoB;IACpB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACnC;;;;2BAIuB;IACvB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;IAClD;;;iFAG6E;IAC7E,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B;;;iDAG6C;IAC7C,kBAAkB,EAAE,kBAAkB,EAAE,CAAC;IACzC;yCACqC;IACrC,MAAM,EAAE,cAAc,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,2EAA2E;IAC3E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,KAAK,CAAC;CACd"}
|
|
@@ -15,7 +15,12 @@ export declare class Loader {
|
|
|
15
15
|
private readonly urlToSource;
|
|
16
16
|
protected sources: ManifestSource[];
|
|
17
17
|
private readonly celEnv;
|
|
18
|
-
|
|
18
|
+
/** Sources are resolved in order — the first whose `supports(url)` matches
|
|
19
|
+
* wins. The caller (composition root) decides which concrete sources exist
|
|
20
|
+
* and supplies them; `defaultSources()` bundles the browser-safe built-ins
|
|
21
|
+
* (HTTP + registry) for the common case. `register()` prepends a source at
|
|
22
|
+
* runtime. */
|
|
23
|
+
constructor(sources?: ManifestSource[], options?: LoaderInitOptions);
|
|
19
24
|
register(source: ManifestSource): this;
|
|
20
25
|
private pick;
|
|
21
26
|
resolveEntryPoint(url: string): Promise<string>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest-loader.d.ts","sourceRoot":"","sources":["../src/manifest-loader.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"manifest-loader.d.ts","sourceRoot":"","sources":["../src/manifest-loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAGV,UAAU,EACV,WAAW,EACX,YAAY,EACb,MAAM,mBAAmB,CAAC;AAK3B,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACpB,MAAM,YAAY,CAAC;AAiBpB,qBAAa,MAAM;IACjB;;;yEAGqE;IACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiC;IAE3D;;;;;8BAK0B;IAC1B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IAEzD,SAAS,CAAC,OAAO,EAAE,cAAc,EAAE,CAAC;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IAErC;;;;mBAIe;gBACH,OAAO,GAAE,cAAc,EAAO,EAAE,OAAO,GAAE,iBAAsB;IAK3E,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI;IAKtC,OAAO,CAAC,IAAI;IAMN,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAUrD;;;;2CAIuC;IACvC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAM7C;;;+BAG2B;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IAyCvE;;;;gEAI4D;IAC5D,OAAO,CAAC,oBAAoB;IAa5B;gFAC4E;IAC5E,OAAO,CAAC,cAAc;IAQtB;;wEAEoE;IAC9D,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAsB3E;;;qCAGiC;IAC3B,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IAqH9E;;;;;;;;sBAQkB;IAClB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM;IAOlE,OAAO,CAAC,6BAA6B;IAarC,OAAO,CAAC,mCAAmC;IAc3C,OAAO,CAAC,2BAA2B;YAkCrB,eAAe;IAmB7B;;;0CAGsC;IAChC,gBAAgB,CACpB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC;QAAE,KAAK,EAAE,WAAW,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CA0B5D"}
|
package/dist/manifest-loader.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { HttpSource } from "./sources/http-source.js";
|
|
2
|
-
import { RegistrySource } from "./sources/registry-source.js";
|
|
3
1
|
import { buildCelEnvironment } from "./cel-environment.js";
|
|
4
2
|
import { desugarLoadedFile } from "./inline-imports.js";
|
|
5
3
|
import { isModuleKind } from "./module-kinds.js";
|
|
6
4
|
import { parseLoadedFile } from "./parse-loaded-file.js";
|
|
5
|
+
import { reconcileModuleVersions } from "./reconcile-module-versions.js";
|
|
7
6
|
import { DEFAULT_MANIFEST_FILENAME, } from "./types.js";
|
|
8
7
|
const SYSTEM_KINDS = new Set([
|
|
9
8
|
"Telo.Application",
|
|
@@ -33,20 +32,13 @@ export class Loader {
|
|
|
33
32
|
urlToSource = new Map();
|
|
34
33
|
sources;
|
|
35
34
|
celEnv;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
this.sources = [];
|
|
43
|
-
if (includeHttpSource)
|
|
44
|
-
this.sources.push(new HttpSource());
|
|
45
|
-
if (includeRegistrySource)
|
|
46
|
-
this.sources.push(new RegistrySource(options.registryUrl));
|
|
47
|
-
if (options.extraSources?.length) {
|
|
48
|
-
this.sources.unshift(...options.extraSources);
|
|
49
|
-
}
|
|
35
|
+
/** Sources are resolved in order — the first whose `supports(url)` matches
|
|
36
|
+
* wins. The caller (composition root) decides which concrete sources exist
|
|
37
|
+
* and supplies them; `defaultSources()` bundles the browser-safe built-ins
|
|
38
|
+
* (HTTP + registry) for the common case. `register()` prepends a source at
|
|
39
|
+
* runtime. */
|
|
40
|
+
constructor(sources = [], options = {}) {
|
|
41
|
+
this.sources = [...sources];
|
|
50
42
|
this.celEnv = buildCelEnvironment(options.celHandlers);
|
|
51
43
|
}
|
|
52
44
|
register(source) {
|
|
@@ -265,7 +257,19 @@ export class Loader {
|
|
|
265
257
|
importEdges.set(file.source, aliases);
|
|
266
258
|
}
|
|
267
259
|
}
|
|
268
|
-
|
|
260
|
+
// Collapse multiple versions of the same module identity onto one version
|
|
261
|
+
// before any consumer walks the edges: repoints losing `importEdges` in
|
|
262
|
+
// place and yields the runtime override map + hoist/conflict diagnostics.
|
|
263
|
+
const { overrides, diagnostics } = reconcileModuleVersions(modules, importEdges);
|
|
264
|
+
return {
|
|
265
|
+
rootSource,
|
|
266
|
+
entry,
|
|
267
|
+
modules,
|
|
268
|
+
importEdges,
|
|
269
|
+
overrides,
|
|
270
|
+
versionDiagnostics: diagnostics,
|
|
271
|
+
errors,
|
|
272
|
+
};
|
|
269
273
|
}
|
|
270
274
|
/** Resolve an `import` URL against the file it appears in. Relative /
|
|
271
275
|
* absolute-path forms run through the owning `ManifestSource`'s
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ImportEdge, LoadedModule } from "./loaded-types.js";
|
|
2
|
+
import { type AnalysisDiagnostic } from "./types.js";
|
|
3
|
+
/** Outcome of reconciling a module name that appears at more than one resolved
|
|
4
|
+
* source in a single import graph. The `overrides` map redirects each losing
|
|
5
|
+
* canonical URL to the winner's canonical URL — consulted by the runtime when
|
|
6
|
+
* it independently re-resolves an import (the analyzer side is handled by
|
|
7
|
+
* repointing `importEdges` in place). */
|
|
8
|
+
export interface VersionReconciliation {
|
|
9
|
+
/** Loser canonical source URL → winner canonical source URL. */
|
|
10
|
+
overrides: Map<string, string>;
|
|
11
|
+
/** One diagnostic per import edge that pointed at a non-winner: a warning for
|
|
12
|
+
* a same-major hoist, an error for an incompatible major mismatch. */
|
|
13
|
+
diagnostics: AnalysisDiagnostic[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Reconcile a loaded import graph so each module identity (`namespace/name`)
|
|
17
|
+
* resolves to a single version. Within a shared major the highest version wins
|
|
18
|
+
* (a non-lossy hoist, given Telo's additive-only pre-1.0 policy); a major
|
|
19
|
+
* mismatch is a hard conflict. Mutates `importEdges` in place — every edge that
|
|
20
|
+
* pointed at a losing source is repointed at the winner — so `flattenForAnalyzer`
|
|
21
|
+
* walks a deduplicated graph and the runtime collision (two definitions of the
|
|
22
|
+
* same kind) cannot occur. Pure and browser-safe: no I/O, no Node built-ins.
|
|
23
|
+
*/
|
|
24
|
+
export declare function reconcileModuleVersions(modules: Map<string, LoadedModule>, importEdges: Map<string, Map<string, ImportEdge>>): VersionReconciliation;
|
|
25
|
+
//# sourceMappingURL=reconcile-module-versions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconcile-module-versions.d.ts","sourceRoot":"","sources":["../src/reconcile-module-versions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAElE,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAIzE;;;;0CAI0C;AAC1C,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B;2EACuE;IACvE,WAAW,EAAE,kBAAkB,EAAE,CAAC;CACnC;AA8KD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAClC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,GAChD,qBAAqB,CAiDvB"}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { isModuleKind } from "./module-kinds.js";
|
|
2
|
+
import { DiagnosticSeverity } from "./types.js";
|
|
3
|
+
const SOURCE = "telo-analyzer";
|
|
4
|
+
/** Parse `X.Y.Z`, `vX.Y.Z`, or `X.Y.Z-pre.1`. Returns `null` for anything that
|
|
5
|
+
* isn't a plain three-part numeric core — an unparseable version forces the
|
|
6
|
+
* group onto the conflict path (we never silently hoist across a version we
|
|
7
|
+
* can't reason about). Pure: no dependency on the `semver` package, so the
|
|
8
|
+
* analyzer stays browser-safe and dependency-free. */
|
|
9
|
+
function parseVersion(raw) {
|
|
10
|
+
if (typeof raw !== "string")
|
|
11
|
+
return null;
|
|
12
|
+
const v = raw.startsWith("v") ? raw.slice(1) : raw;
|
|
13
|
+
const [core, ...preParts] = v.split("-");
|
|
14
|
+
const pre = preParts.length > 0 ? preParts.join("-") : null;
|
|
15
|
+
const segments = core.split(".");
|
|
16
|
+
if (segments.length !== 3)
|
|
17
|
+
return null;
|
|
18
|
+
const [major, minor, patch] = segments.map((s) => {
|
|
19
|
+
if (!/^\d+$/.test(s))
|
|
20
|
+
return NaN;
|
|
21
|
+
return Number(s);
|
|
22
|
+
});
|
|
23
|
+
if ([major, minor, patch].some((n) => Number.isNaN(n)))
|
|
24
|
+
return null;
|
|
25
|
+
return { major, minor, patch, pre: pre === null ? null : pre.split(".") };
|
|
26
|
+
}
|
|
27
|
+
/** SemVer precedence: numeric core, then a release outranks a prerelease, then
|
|
28
|
+
* prerelease identifiers compared field-by-field (numeric < non-numeric per
|
|
29
|
+
* spec, shorter set loses when all shared fields are equal). */
|
|
30
|
+
function compareVersions(a, b) {
|
|
31
|
+
if (a.major !== b.major)
|
|
32
|
+
return a.major - b.major;
|
|
33
|
+
if (a.minor !== b.minor)
|
|
34
|
+
return a.minor - b.minor;
|
|
35
|
+
if (a.patch !== b.patch)
|
|
36
|
+
return a.patch - b.patch;
|
|
37
|
+
if (a.pre === null && b.pre === null)
|
|
38
|
+
return 0;
|
|
39
|
+
if (a.pre === null)
|
|
40
|
+
return 1;
|
|
41
|
+
if (b.pre === null)
|
|
42
|
+
return -1;
|
|
43
|
+
const len = Math.max(a.pre.length, b.pre.length);
|
|
44
|
+
for (let i = 0; i < len; i++) {
|
|
45
|
+
const ai = a.pre[i];
|
|
46
|
+
const bi = b.pre[i];
|
|
47
|
+
if (ai === undefined)
|
|
48
|
+
return -1;
|
|
49
|
+
if (bi === undefined)
|
|
50
|
+
return 1;
|
|
51
|
+
const an = /^\d+$/.test(ai);
|
|
52
|
+
const bn = /^\d+$/.test(bi);
|
|
53
|
+
if (an && bn) {
|
|
54
|
+
const d = Number(ai) - Number(bi);
|
|
55
|
+
if (d !== 0)
|
|
56
|
+
return d;
|
|
57
|
+
}
|
|
58
|
+
else if (an !== bn) {
|
|
59
|
+
return an ? -1 : 1;
|
|
60
|
+
}
|
|
61
|
+
else if (ai !== bi) {
|
|
62
|
+
return ai < bi ? -1 : 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
/** Read a loaded module's `namespace/name` identity, version, and raw owner
|
|
68
|
+
* text. Returns `null` for modules without a namespace: only a registry
|
|
69
|
+
* identity (`<namespace>/<name>`) is a stable cross-import key. Two namespace-
|
|
70
|
+
* less local libraries that merely share a `metadata.name` are distinct modules
|
|
71
|
+
* reached via distinct source URLs — reconciling them would drop one and break
|
|
72
|
+
* its kinds; the same local file reached via two paths is already collapsed by
|
|
73
|
+
* canonical-source dedup, so there is nothing left to reconcile here. */
|
|
74
|
+
function moduleIdentityOf(mod) {
|
|
75
|
+
const doc = mod.owner.manifests.find((m) => m && isModuleKind(m.kind));
|
|
76
|
+
if (!doc)
|
|
77
|
+
return null;
|
|
78
|
+
const meta = doc.metadata;
|
|
79
|
+
const name = meta?.name;
|
|
80
|
+
if (typeof name !== "string" || name.length === 0)
|
|
81
|
+
return null;
|
|
82
|
+
if (typeof meta.namespace !== "string" || meta.namespace.length === 0)
|
|
83
|
+
return null;
|
|
84
|
+
const version = typeof meta.version === "string" ? meta.version : "";
|
|
85
|
+
return {
|
|
86
|
+
source: mod.owner.source,
|
|
87
|
+
identity: `${meta.namespace}/${name}`,
|
|
88
|
+
version,
|
|
89
|
+
parsed: parseVersion(version),
|
|
90
|
+
text: mod.owner.text,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** Pick the winning member of a same-identity group and classify it. The winner
|
|
94
|
+
* is the highest version (deterministic tiebreak on source URL for equal
|
|
95
|
+
* versions / same-version-different-source). A major disagreement — or any
|
|
96
|
+
* unparseable version — marks the group a conflict; we still pick a winner so
|
|
97
|
+
* the rest of analysis proceeds against a single version instead of cascading
|
|
98
|
+
* duplicate-kind errors. */
|
|
99
|
+
function resolveGroup(members) {
|
|
100
|
+
const majors = new Set();
|
|
101
|
+
for (const m of members)
|
|
102
|
+
majors.add(m.parsed ? m.parsed.major : null);
|
|
103
|
+
const conflict = majors.has(null) || majors.size > 1;
|
|
104
|
+
const winner = members.reduce((best, cur) => {
|
|
105
|
+
if (!cur.parsed)
|
|
106
|
+
return best;
|
|
107
|
+
if (!best.parsed)
|
|
108
|
+
return cur;
|
|
109
|
+
const cmp = compareVersions(cur.parsed, best.parsed);
|
|
110
|
+
if (cmp > 0)
|
|
111
|
+
return cur;
|
|
112
|
+
if (cmp === 0 && cur.source < best.source)
|
|
113
|
+
return cur;
|
|
114
|
+
return best;
|
|
115
|
+
}, members[0]);
|
|
116
|
+
return { winner, conflict };
|
|
117
|
+
}
|
|
118
|
+
/** The diagnostic for a redirected edge, or `null` when the redirect is a
|
|
119
|
+
* silent dedupe (the same version resolved from two sources with identical
|
|
120
|
+
* content — no decision was made, so nothing to report). */
|
|
121
|
+
function hoistDiagnostic(identity, importerSource, alias, loser, winner, conflict) {
|
|
122
|
+
const data = { filePath: importerSource, path: `imports.${alias}` };
|
|
123
|
+
if (conflict) {
|
|
124
|
+
return {
|
|
125
|
+
severity: DiagnosticSeverity.Error,
|
|
126
|
+
code: "MODULE_VERSION_CONFLICT",
|
|
127
|
+
source: SOURCE,
|
|
128
|
+
message: `Module '${identity}' is imported at incompatible major versions: ` +
|
|
129
|
+
`${loser.version || "<unknown>"} here and ${winner.version} elsewhere in the same graph. ` +
|
|
130
|
+
`Major versions can carry breaking changes and cannot be reconciled automatically — ` +
|
|
131
|
+
`align every importer on one major.`,
|
|
132
|
+
data,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (loser.version === winner.version) {
|
|
136
|
+
// Same version, two sources. Identical content is a no-op dedupe; differing
|
|
137
|
+
// content means one is masquerading as the other (e.g. a local checkout vs
|
|
138
|
+
// the published version) — worth surfacing.
|
|
139
|
+
if (loser.text === winner.text)
|
|
140
|
+
return null;
|
|
141
|
+
return {
|
|
142
|
+
severity: DiagnosticSeverity.Warning,
|
|
143
|
+
code: "MODULE_VERSION_HOISTED",
|
|
144
|
+
source: SOURCE,
|
|
145
|
+
message: `Module '${identity}@${winner.version}' is imported from two sources whose contents ` +
|
|
146
|
+
`differ ('${loser.source}' and '${winner.source}'). Using '${winner.source}' for every ` +
|
|
147
|
+
`importer — pin a single source to remove the ambiguity.`,
|
|
148
|
+
data,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
severity: DiagnosticSeverity.Warning,
|
|
153
|
+
code: "MODULE_VERSION_HOISTED",
|
|
154
|
+
source: SOURCE,
|
|
155
|
+
message: `Module '${identity}@${loser.version || "<unknown>"}' was hoisted to '${winner.version}' ` +
|
|
156
|
+
`because the same module is imported at the higher version elsewhere in the graph. ` +
|
|
157
|
+
`Pre-1.0 versions are additive, so the higher version is used for every importer.`,
|
|
158
|
+
data,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Reconcile a loaded import graph so each module identity (`namespace/name`)
|
|
163
|
+
* resolves to a single version. Within a shared major the highest version wins
|
|
164
|
+
* (a non-lossy hoist, given Telo's additive-only pre-1.0 policy); a major
|
|
165
|
+
* mismatch is a hard conflict. Mutates `importEdges` in place — every edge that
|
|
166
|
+
* pointed at a losing source is repointed at the winner — so `flattenForAnalyzer`
|
|
167
|
+
* walks a deduplicated graph and the runtime collision (two definitions of the
|
|
168
|
+
* same kind) cannot occur. Pure and browser-safe: no I/O, no Node built-ins.
|
|
169
|
+
*/
|
|
170
|
+
export function reconcileModuleVersions(modules, importEdges) {
|
|
171
|
+
const overrides = new Map();
|
|
172
|
+
const diagnostics = [];
|
|
173
|
+
const groups = new Map();
|
|
174
|
+
const infoBySource = new Map();
|
|
175
|
+
for (const mod of modules.values()) {
|
|
176
|
+
const info = moduleIdentityOf(mod);
|
|
177
|
+
if (!info)
|
|
178
|
+
continue;
|
|
179
|
+
infoBySource.set(info.source, info);
|
|
180
|
+
const list = groups.get(info.identity);
|
|
181
|
+
if (list)
|
|
182
|
+
list.push(info);
|
|
183
|
+
else
|
|
184
|
+
groups.set(info.identity, [info]);
|
|
185
|
+
}
|
|
186
|
+
const conflictByIdentity = new Map();
|
|
187
|
+
for (const [identity, members] of groups) {
|
|
188
|
+
if (members.length < 2)
|
|
189
|
+
continue;
|
|
190
|
+
const { winner, conflict } = resolveGroup(members);
|
|
191
|
+
conflictByIdentity.set(identity, conflict);
|
|
192
|
+
for (const member of members) {
|
|
193
|
+
if (member.source !== winner.source)
|
|
194
|
+
overrides.set(member.source, winner.source);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (overrides.size === 0)
|
|
198
|
+
return { overrides, diagnostics };
|
|
199
|
+
for (const [importerSource, aliasMap] of importEdges) {
|
|
200
|
+
for (const [alias, edge] of aliasMap) {
|
|
201
|
+
const winnerSource = overrides.get(edge.targetSource);
|
|
202
|
+
if (!winnerSource)
|
|
203
|
+
continue;
|
|
204
|
+
const loser = infoBySource.get(edge.targetSource);
|
|
205
|
+
const winner = infoBySource.get(winnerSource);
|
|
206
|
+
if (loser && winner) {
|
|
207
|
+
const diag = hoistDiagnostic(loser.identity, importerSource, alias, loser, winner, conflictByIdentity.get(loser.identity) ?? false);
|
|
208
|
+
if (diag)
|
|
209
|
+
diagnostics.push(diag);
|
|
210
|
+
}
|
|
211
|
+
edge.targetSource = winnerSource;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return { overrides, diagnostics };
|
|
215
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ManifestSource } from "../types.js";
|
|
2
|
+
/** The browser-safe built-in sources, in resolution order: HTTP fetch then
|
|
3
|
+
* registry. Node-specific sources (local filesystem) are supplied by the
|
|
4
|
+
* consuming package and passed alongside these into the `Loader` constructor.
|
|
5
|
+
* Callers that only want a subset (e.g. the editor, which brings its own
|
|
6
|
+
* registry adapters) construct the individual sources directly. */
|
|
7
|
+
export declare function defaultSources(registryUrl?: string): ManifestSource[];
|
|
8
|
+
//# sourceMappingURL=default-sources.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"default-sources.d.ts","sourceRoot":"","sources":["../../src/sources/default-sources.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAIlD;;;;oEAIoE;AACpE,wBAAgB,cAAc,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,cAAc,EAAE,CAErE"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { HttpSource } from "./http-source.js";
|
|
2
|
+
import { RegistrySource } from "./registry-source.js";
|
|
3
|
+
/** The browser-safe built-in sources, in resolution order: HTTP fetch then
|
|
4
|
+
* registry. Node-specific sources (local filesystem) are supplied by the
|
|
5
|
+
* consuming package and passed alongside these into the `Loader` constructor.
|
|
6
|
+
* Callers that only want a subset (e.g. the editor, which brings its own
|
|
7
|
+
* registry adapters) construct the individual sources directly. */
|
|
8
|
+
export function defaultSources(registryUrl) {
|
|
9
|
+
return [new HttpSource(), new RegistrySource(registryUrl)];
|
|
10
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -68,14 +68,6 @@ export interface LoadOptions {
|
|
|
68
68
|
desugarImports?: boolean;
|
|
69
69
|
}
|
|
70
70
|
export interface LoaderInitOptions {
|
|
71
|
-
/** Sources inserted with highest priority before built-ins. */
|
|
72
|
-
extraSources?: ManifestSource[];
|
|
73
|
-
/** Include built-in HttpSource. Defaults to true. */
|
|
74
|
-
includeHttpSource?: boolean;
|
|
75
|
-
/** Include built-in RegistrySource. Defaults to true. */
|
|
76
|
-
includeRegistrySource?: boolean;
|
|
77
|
-
/** Base URL used by built-in RegistrySource when enabled. */
|
|
78
|
-
registryUrl?: string;
|
|
79
71
|
/** Handlers for CEL stdlib functions (e.g. `sha256`). Analyzer-only callers may
|
|
80
72
|
* omit this and get throwing stubs; runtime callers (kernel) must supply real impls. */
|
|
81
73
|
celHandlers?: import("./cel-environment.js").CelHandlers;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;qHACqH;AACrH,eAAO,MAAM,kBAAkB;;;;;CAKrB,CAAC;AACX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,OAAO,kBAAkB,CAAC,CAAC;AAE9F,gFAAgF;AAChF,eAAO,MAAM,yBAAyB,cAAc,CAAC;AAErD,MAAM,WAAW,QAAQ;IACvB,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,QAAQ,CAAC;IAChB,GAAG,EAAE,QAAQ,CAAC;CACf;AAED;;oDAEoD;AACpD,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAE/C;6EAC6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,2BAA2B;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7D,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAExD;;qEAEiE;IACjE,UAAU,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAEjE;;qEAEiE;IACjE,cAAc,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC1D;AAED,MAAM,WAAW,WAAW;IAC1B;;;+EAG2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;mEAO+D;IAC/D,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;qHACqH;AACrH,eAAO,MAAM,kBAAkB;;;;;CAKrB,CAAC;AACX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,OAAO,kBAAkB,CAAC,CAAC;AAE9F,gFAAgF;AAChF,eAAO,MAAM,yBAAyB,cAAc,CAAC;AAErD,MAAM,WAAW,QAAQ;IACvB,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,QAAQ,CAAC;IAChB,GAAG,EAAE,QAAQ,CAAC;CACf;AAED;;oDAEoD;AACpD,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAE/C;6EAC6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,2BAA2B;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7D,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAExD;;qEAEiE;IACjE,UAAU,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAEjE;;qEAEiE;IACjE,cAAc,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC1D;AAED,MAAM,WAAW,WAAW;IAC1B;;;+EAG2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;mEAO+D;IAC/D,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC;6FACyF;IACzF,WAAW,CAAC,EAAE,OAAO,sBAAsB,EAAE,WAAW,CAAC;CAC1D;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;;;;;sDAUkD;IAClD,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;gEAKgE;AAChE,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,OAAO,qBAAqB,EAAE,aAAa,CAAC;IACtD,WAAW,CAAC,EAAE,OAAO,0BAA0B,EAAE,kBAAkB,CAAC;IACpE;;;;+EAI2E;IAC3E,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,qBAAqB,EAAE,aAAa,CAAC,CAAC;CAC5E"}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -39,6 +39,8 @@ export { parseLoadedFile } from "./parse-loaded-file.js";
|
|
|
39
39
|
export type { ParseOptions } from "./parse-loaded-file.js";
|
|
40
40
|
export { desugarLoadedFile, inlineImportManifests } from "./inline-imports.js";
|
|
41
41
|
export type { SyntheticImport } from "./inline-imports.js";
|
|
42
|
+
export { reconcileModuleVersions } from "./reconcile-module-versions.js";
|
|
43
|
+
export type { VersionReconciliation } from "./reconcile-module-versions.js";
|
|
42
44
|
export { residualEntrySchema, residualEntrySchemaMap } from "./residual-schema.js";
|
|
43
45
|
export {
|
|
44
46
|
buildDocumentPositions,
|
|
@@ -49,6 +51,7 @@ export {
|
|
|
49
51
|
export type { DocumentPosition } from "./position-metadata.js";
|
|
50
52
|
export { HttpSource } from "./sources/http-source.js";
|
|
51
53
|
export { RegistrySource } from "./sources/registry-source.js";
|
|
54
|
+
export { defaultSources } from "./sources/default-sources.js";
|
|
52
55
|
export { withSyntheticPositions } from "./with-synthetic-positions.js";
|
|
53
56
|
export { DEFAULT_MANIFEST_FILENAME, DiagnosticSeverity } from "./types.js";
|
|
54
57
|
export type {
|
package/src/loaded-types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
2
|
import type { Document } from "yaml";
|
|
3
3
|
import type { DocumentPosition } from "./position-metadata.js";
|
|
4
|
-
import type { Range } from "./types.js";
|
|
4
|
+
import type { AnalysisDiagnostic, Range } from "./types.js";
|
|
5
5
|
|
|
6
6
|
/** One physical file's parsed result. Returned for the owner manifest, for
|
|
7
7
|
* each `include:` partial, and for each external import target.
|
|
@@ -70,8 +70,21 @@ export interface LoadedGraph {
|
|
|
70
70
|
* its partials. */
|
|
71
71
|
modules: Map<string, LoadedModule>;
|
|
72
72
|
/** Per-Telo.Import resolution. Keyed by the resolved URL of the file the
|
|
73
|
-
* Telo.Import was declared in, then by the import's PascalCase alias.
|
|
73
|
+
* Telo.Import was declared in, then by the import's PascalCase alias.
|
|
74
|
+
* Version reconciliation repoints losing edges at their winner here, so a
|
|
75
|
+
* consumer walking these edges (`flattenForAnalyzer`) sees one version per
|
|
76
|
+
* module identity. */
|
|
74
77
|
importEdges: Map<string, Map<string, ImportEdge>>;
|
|
78
|
+
/** Version-reconciliation redirects: a losing module's canonical source URL →
|
|
79
|
+
* the winning version's canonical source URL. The runtime consults this when
|
|
80
|
+
* it independently re-resolves an import (the analyzer already sees repointed
|
|
81
|
+
* `importEdges`). Empty when no module identity appeared at two sources. */
|
|
82
|
+
overrides: Map<string, string>;
|
|
83
|
+
/** Diagnostics produced while reconciling module versions — one per import
|
|
84
|
+
* edge redirected to a different version (warning for a same-major hoist,
|
|
85
|
+
* error for a major mismatch). Surfaced alongside `analyze()` diagnostics by
|
|
86
|
+
* every consumer (CLI, editor, VS Code). */
|
|
87
|
+
versionDiagnostics: AnalysisDiagnostic[];
|
|
75
88
|
/** Surface-level errors that did not abort the graph load (e.g. an import
|
|
76
89
|
* whose target failed to fetch). */
|
|
77
90
|
errors: GraphLoadError[];
|
package/src/manifest-loader.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { Environment } from "@marcbachmann/cel-js";
|
|
2
2
|
import type { ResourceManifest } from "@telorun/sdk";
|
|
3
|
-
import { HttpSource } from "./sources/http-source.js";
|
|
4
|
-
import { RegistrySource } from "./sources/registry-source.js";
|
|
5
3
|
import { buildCelEnvironment } from "./cel-environment.js";
|
|
6
4
|
import type {
|
|
7
5
|
GraphLoadError,
|
|
@@ -13,6 +11,7 @@ import type {
|
|
|
13
11
|
import { desugarLoadedFile } from "./inline-imports.js";
|
|
14
12
|
import { isModuleKind } from "./module-kinds.js";
|
|
15
13
|
import { parseLoadedFile } from "./parse-loaded-file.js";
|
|
14
|
+
import { reconcileModuleVersions } from "./reconcile-module-versions.js";
|
|
16
15
|
import {
|
|
17
16
|
DEFAULT_MANIFEST_FILENAME,
|
|
18
17
|
type LoadOptions,
|
|
@@ -53,22 +52,13 @@ export class Loader {
|
|
|
53
52
|
protected sources: ManifestSource[];
|
|
54
53
|
private readonly celEnv: Environment;
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
this.sources = [];
|
|
65
|
-
if (includeHttpSource) this.sources.push(new HttpSource());
|
|
66
|
-
if (includeRegistrySource) this.sources.push(new RegistrySource(options.registryUrl));
|
|
67
|
-
|
|
68
|
-
if (options.extraSources?.length) {
|
|
69
|
-
this.sources.unshift(...options.extraSources);
|
|
70
|
-
}
|
|
71
|
-
|
|
55
|
+
/** Sources are resolved in order — the first whose `supports(url)` matches
|
|
56
|
+
* wins. The caller (composition root) decides which concrete sources exist
|
|
57
|
+
* and supplies them; `defaultSources()` bundles the browser-safe built-ins
|
|
58
|
+
* (HTTP + registry) for the common case. `register()` prepends a source at
|
|
59
|
+
* runtime. */
|
|
60
|
+
constructor(sources: ManifestSource[] = [], options: LoaderInitOptions = {}) {
|
|
61
|
+
this.sources = [...sources];
|
|
72
62
|
this.celEnv = buildCelEnvironment(options.celHandlers);
|
|
73
63
|
}
|
|
74
64
|
|
|
@@ -307,7 +297,20 @@ export class Loader {
|
|
|
307
297
|
}
|
|
308
298
|
}
|
|
309
299
|
|
|
310
|
-
|
|
300
|
+
// Collapse multiple versions of the same module identity onto one version
|
|
301
|
+
// before any consumer walks the edges: repoints losing `importEdges` in
|
|
302
|
+
// place and yields the runtime override map + hoist/conflict diagnostics.
|
|
303
|
+
const { overrides, diagnostics } = reconcileModuleVersions(modules, importEdges);
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
rootSource,
|
|
307
|
+
entry,
|
|
308
|
+
modules,
|
|
309
|
+
importEdges,
|
|
310
|
+
overrides,
|
|
311
|
+
versionDiagnostics: diagnostics,
|
|
312
|
+
errors,
|
|
313
|
+
};
|
|
311
314
|
}
|
|
312
315
|
|
|
313
316
|
/** Resolve an `import` URL against the file it appears in. Relative /
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import type { ImportEdge, LoadedModule } from "./loaded-types.js";
|
|
2
|
+
import { isModuleKind } from "./module-kinds.js";
|
|
3
|
+
import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
|
|
4
|
+
|
|
5
|
+
const SOURCE = "telo-analyzer";
|
|
6
|
+
|
|
7
|
+
/** Outcome of reconciling a module name that appears at more than one resolved
|
|
8
|
+
* source in a single import graph. The `overrides` map redirects each losing
|
|
9
|
+
* canonical URL to the winner's canonical URL — consulted by the runtime when
|
|
10
|
+
* it independently re-resolves an import (the analyzer side is handled by
|
|
11
|
+
* repointing `importEdges` in place). */
|
|
12
|
+
export interface VersionReconciliation {
|
|
13
|
+
/** Loser canonical source URL → winner canonical source URL. */
|
|
14
|
+
overrides: Map<string, string>;
|
|
15
|
+
/** One diagnostic per import edge that pointed at a non-winner: a warning for
|
|
16
|
+
* a same-major hoist, an error for an incompatible major mismatch. */
|
|
17
|
+
diagnostics: AnalysisDiagnostic[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ParsedVersion {
|
|
21
|
+
major: number;
|
|
22
|
+
minor: number;
|
|
23
|
+
patch: number;
|
|
24
|
+
/** Dot-separated prerelease identifiers, or `null` for a release version. */
|
|
25
|
+
pre: string[] | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ModuleIdentity {
|
|
29
|
+
source: string;
|
|
30
|
+
identity: string;
|
|
31
|
+
version: string;
|
|
32
|
+
parsed: ParsedVersion | null;
|
|
33
|
+
text: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Parse `X.Y.Z`, `vX.Y.Z`, or `X.Y.Z-pre.1`. Returns `null` for anything that
|
|
37
|
+
* isn't a plain three-part numeric core — an unparseable version forces the
|
|
38
|
+
* group onto the conflict path (we never silently hoist across a version we
|
|
39
|
+
* can't reason about). Pure: no dependency on the `semver` package, so the
|
|
40
|
+
* analyzer stays browser-safe and dependency-free. */
|
|
41
|
+
function parseVersion(raw: string | undefined): ParsedVersion | null {
|
|
42
|
+
if (typeof raw !== "string") return null;
|
|
43
|
+
const v = raw.startsWith("v") ? raw.slice(1) : raw;
|
|
44
|
+
const [core, ...preParts] = v.split("-");
|
|
45
|
+
const pre = preParts.length > 0 ? preParts.join("-") : null;
|
|
46
|
+
const segments = core.split(".");
|
|
47
|
+
if (segments.length !== 3) return null;
|
|
48
|
+
const [major, minor, patch] = segments.map((s) => {
|
|
49
|
+
if (!/^\d+$/.test(s)) return NaN;
|
|
50
|
+
return Number(s);
|
|
51
|
+
});
|
|
52
|
+
if ([major, minor, patch].some((n) => Number.isNaN(n))) return null;
|
|
53
|
+
return { major, minor, patch, pre: pre === null ? null : pre.split(".") };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** SemVer precedence: numeric core, then a release outranks a prerelease, then
|
|
57
|
+
* prerelease identifiers compared field-by-field (numeric < non-numeric per
|
|
58
|
+
* spec, shorter set loses when all shared fields are equal). */
|
|
59
|
+
function compareVersions(a: ParsedVersion, b: ParsedVersion): number {
|
|
60
|
+
if (a.major !== b.major) return a.major - b.major;
|
|
61
|
+
if (a.minor !== b.minor) return a.minor - b.minor;
|
|
62
|
+
if (a.patch !== b.patch) return a.patch - b.patch;
|
|
63
|
+
if (a.pre === null && b.pre === null) return 0;
|
|
64
|
+
if (a.pre === null) return 1;
|
|
65
|
+
if (b.pre === null) return -1;
|
|
66
|
+
const len = Math.max(a.pre.length, b.pre.length);
|
|
67
|
+
for (let i = 0; i < len; i++) {
|
|
68
|
+
const ai = a.pre[i];
|
|
69
|
+
const bi = b.pre[i];
|
|
70
|
+
if (ai === undefined) return -1;
|
|
71
|
+
if (bi === undefined) return 1;
|
|
72
|
+
const an = /^\d+$/.test(ai);
|
|
73
|
+
const bn = /^\d+$/.test(bi);
|
|
74
|
+
if (an && bn) {
|
|
75
|
+
const d = Number(ai) - Number(bi);
|
|
76
|
+
if (d !== 0) return d;
|
|
77
|
+
} else if (an !== bn) {
|
|
78
|
+
return an ? -1 : 1;
|
|
79
|
+
} else if (ai !== bi) {
|
|
80
|
+
return ai < bi ? -1 : 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Read a loaded module's `namespace/name` identity, version, and raw owner
|
|
87
|
+
* text. Returns `null` for modules without a namespace: only a registry
|
|
88
|
+
* identity (`<namespace>/<name>`) is a stable cross-import key. Two namespace-
|
|
89
|
+
* less local libraries that merely share a `metadata.name` are distinct modules
|
|
90
|
+
* reached via distinct source URLs — reconciling them would drop one and break
|
|
91
|
+
* its kinds; the same local file reached via two paths is already collapsed by
|
|
92
|
+
* canonical-source dedup, so there is nothing left to reconcile here. */
|
|
93
|
+
function moduleIdentityOf(mod: LoadedModule): ModuleIdentity | null {
|
|
94
|
+
const doc = mod.owner.manifests.find((m) => m && isModuleKind(m.kind));
|
|
95
|
+
if (!doc) return null;
|
|
96
|
+
const meta = doc.metadata as { name?: string; namespace?: string | null; version?: string };
|
|
97
|
+
const name = meta?.name;
|
|
98
|
+
if (typeof name !== "string" || name.length === 0) return null;
|
|
99
|
+
if (typeof meta.namespace !== "string" || meta.namespace.length === 0) return null;
|
|
100
|
+
const version = typeof meta.version === "string" ? meta.version : "";
|
|
101
|
+
return {
|
|
102
|
+
source: mod.owner.source,
|
|
103
|
+
identity: `${meta.namespace}/${name}`,
|
|
104
|
+
version,
|
|
105
|
+
parsed: parseVersion(version),
|
|
106
|
+
text: mod.owner.text,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface GroupResolution {
|
|
111
|
+
winner: ModuleIdentity;
|
|
112
|
+
/** True when members disagree on major version (or a version is unparseable). */
|
|
113
|
+
conflict: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Pick the winning member of a same-identity group and classify it. The winner
|
|
117
|
+
* is the highest version (deterministic tiebreak on source URL for equal
|
|
118
|
+
* versions / same-version-different-source). A major disagreement — or any
|
|
119
|
+
* unparseable version — marks the group a conflict; we still pick a winner so
|
|
120
|
+
* the rest of analysis proceeds against a single version instead of cascading
|
|
121
|
+
* duplicate-kind errors. */
|
|
122
|
+
function resolveGroup(members: ModuleIdentity[]): GroupResolution {
|
|
123
|
+
const majors = new Set<number | null>();
|
|
124
|
+
for (const m of members) majors.add(m.parsed ? m.parsed.major : null);
|
|
125
|
+
const conflict = majors.has(null) || majors.size > 1;
|
|
126
|
+
|
|
127
|
+
const winner = members.reduce((best, cur) => {
|
|
128
|
+
if (!cur.parsed) return best;
|
|
129
|
+
if (!best.parsed) return cur;
|
|
130
|
+
const cmp = compareVersions(cur.parsed, best.parsed);
|
|
131
|
+
if (cmp > 0) return cur;
|
|
132
|
+
if (cmp === 0 && cur.source < best.source) return cur;
|
|
133
|
+
return best;
|
|
134
|
+
}, members[0]);
|
|
135
|
+
|
|
136
|
+
return { winner, conflict };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The diagnostic for a redirected edge, or `null` when the redirect is a
|
|
140
|
+
* silent dedupe (the same version resolved from two sources with identical
|
|
141
|
+
* content — no decision was made, so nothing to report). */
|
|
142
|
+
function hoistDiagnostic(
|
|
143
|
+
identity: string,
|
|
144
|
+
importerSource: string,
|
|
145
|
+
alias: string,
|
|
146
|
+
loser: ModuleIdentity,
|
|
147
|
+
winner: ModuleIdentity,
|
|
148
|
+
conflict: boolean,
|
|
149
|
+
): AnalysisDiagnostic | null {
|
|
150
|
+
const data = { filePath: importerSource, path: `imports.${alias}` };
|
|
151
|
+
if (conflict) {
|
|
152
|
+
return {
|
|
153
|
+
severity: DiagnosticSeverity.Error,
|
|
154
|
+
code: "MODULE_VERSION_CONFLICT",
|
|
155
|
+
source: SOURCE,
|
|
156
|
+
message:
|
|
157
|
+
`Module '${identity}' is imported at incompatible major versions: ` +
|
|
158
|
+
`${loser.version || "<unknown>"} here and ${winner.version} elsewhere in the same graph. ` +
|
|
159
|
+
`Major versions can carry breaking changes and cannot be reconciled automatically — ` +
|
|
160
|
+
`align every importer on one major.`,
|
|
161
|
+
data,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (loser.version === winner.version) {
|
|
165
|
+
// Same version, two sources. Identical content is a no-op dedupe; differing
|
|
166
|
+
// content means one is masquerading as the other (e.g. a local checkout vs
|
|
167
|
+
// the published version) — worth surfacing.
|
|
168
|
+
if (loser.text === winner.text) return null;
|
|
169
|
+
return {
|
|
170
|
+
severity: DiagnosticSeverity.Warning,
|
|
171
|
+
code: "MODULE_VERSION_HOISTED",
|
|
172
|
+
source: SOURCE,
|
|
173
|
+
message:
|
|
174
|
+
`Module '${identity}@${winner.version}' is imported from two sources whose contents ` +
|
|
175
|
+
`differ ('${loser.source}' and '${winner.source}'). Using '${winner.source}' for every ` +
|
|
176
|
+
`importer — pin a single source to remove the ambiguity.`,
|
|
177
|
+
data,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
severity: DiagnosticSeverity.Warning,
|
|
182
|
+
code: "MODULE_VERSION_HOISTED",
|
|
183
|
+
source: SOURCE,
|
|
184
|
+
message:
|
|
185
|
+
`Module '${identity}@${loser.version || "<unknown>"}' was hoisted to '${winner.version}' ` +
|
|
186
|
+
`because the same module is imported at the higher version elsewhere in the graph. ` +
|
|
187
|
+
`Pre-1.0 versions are additive, so the higher version is used for every importer.`,
|
|
188
|
+
data,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Reconcile a loaded import graph so each module identity (`namespace/name`)
|
|
194
|
+
* resolves to a single version. Within a shared major the highest version wins
|
|
195
|
+
* (a non-lossy hoist, given Telo's additive-only pre-1.0 policy); a major
|
|
196
|
+
* mismatch is a hard conflict. Mutates `importEdges` in place — every edge that
|
|
197
|
+
* pointed at a losing source is repointed at the winner — so `flattenForAnalyzer`
|
|
198
|
+
* walks a deduplicated graph and the runtime collision (two definitions of the
|
|
199
|
+
* same kind) cannot occur. Pure and browser-safe: no I/O, no Node built-ins.
|
|
200
|
+
*/
|
|
201
|
+
export function reconcileModuleVersions(
|
|
202
|
+
modules: Map<string, LoadedModule>,
|
|
203
|
+
importEdges: Map<string, Map<string, ImportEdge>>,
|
|
204
|
+
): VersionReconciliation {
|
|
205
|
+
const overrides = new Map<string, string>();
|
|
206
|
+
const diagnostics: AnalysisDiagnostic[] = [];
|
|
207
|
+
|
|
208
|
+
const groups = new Map<string, ModuleIdentity[]>();
|
|
209
|
+
const infoBySource = new Map<string, ModuleIdentity>();
|
|
210
|
+
for (const mod of modules.values()) {
|
|
211
|
+
const info = moduleIdentityOf(mod);
|
|
212
|
+
if (!info) continue;
|
|
213
|
+
infoBySource.set(info.source, info);
|
|
214
|
+
const list = groups.get(info.identity);
|
|
215
|
+
if (list) list.push(info);
|
|
216
|
+
else groups.set(info.identity, [info]);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const conflictByIdentity = new Map<string, boolean>();
|
|
220
|
+
for (const [identity, members] of groups) {
|
|
221
|
+
if (members.length < 2) continue;
|
|
222
|
+
const { winner, conflict } = resolveGroup(members);
|
|
223
|
+
conflictByIdentity.set(identity, conflict);
|
|
224
|
+
for (const member of members) {
|
|
225
|
+
if (member.source !== winner.source) overrides.set(member.source, winner.source);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (overrides.size === 0) return { overrides, diagnostics };
|
|
230
|
+
|
|
231
|
+
for (const [importerSource, aliasMap] of importEdges) {
|
|
232
|
+
for (const [alias, edge] of aliasMap) {
|
|
233
|
+
const winnerSource = overrides.get(edge.targetSource);
|
|
234
|
+
if (!winnerSource) continue;
|
|
235
|
+
const loser = infoBySource.get(edge.targetSource);
|
|
236
|
+
const winner = infoBySource.get(winnerSource);
|
|
237
|
+
if (loser && winner) {
|
|
238
|
+
const diag = hoistDiagnostic(
|
|
239
|
+
loser.identity,
|
|
240
|
+
importerSource,
|
|
241
|
+
alias,
|
|
242
|
+
loser,
|
|
243
|
+
winner,
|
|
244
|
+
conflictByIdentity.get(loser.identity) ?? false,
|
|
245
|
+
);
|
|
246
|
+
if (diag) diagnostics.push(diag);
|
|
247
|
+
}
|
|
248
|
+
edge.targetSource = winnerSource;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return { overrides, diagnostics };
|
|
253
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ManifestSource } from "../types.js";
|
|
2
|
+
import { HttpSource } from "./http-source.js";
|
|
3
|
+
import { RegistrySource } from "./registry-source.js";
|
|
4
|
+
|
|
5
|
+
/** The browser-safe built-in sources, in resolution order: HTTP fetch then
|
|
6
|
+
* registry. Node-specific sources (local filesystem) are supplied by the
|
|
7
|
+
* consuming package and passed alongside these into the `Loader` constructor.
|
|
8
|
+
* Callers that only want a subset (e.g. the editor, which brings its own
|
|
9
|
+
* registry adapters) construct the individual sources directly. */
|
|
10
|
+
export function defaultSources(registryUrl?: string): ManifestSource[] {
|
|
11
|
+
return [new HttpSource(), new RegistrySource(registryUrl)];
|
|
12
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -75,14 +75,6 @@ export interface LoadOptions {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
export interface LoaderInitOptions {
|
|
78
|
-
/** Sources inserted with highest priority before built-ins. */
|
|
79
|
-
extraSources?: ManifestSource[];
|
|
80
|
-
/** Include built-in HttpSource. Defaults to true. */
|
|
81
|
-
includeHttpSource?: boolean;
|
|
82
|
-
/** Include built-in RegistrySource. Defaults to true. */
|
|
83
|
-
includeRegistrySource?: boolean;
|
|
84
|
-
/** Base URL used by built-in RegistrySource when enabled. */
|
|
85
|
-
registryUrl?: string;
|
|
86
78
|
/** Handlers for CEL stdlib functions (e.g. `sha256`). Analyzer-only callers may
|
|
87
79
|
* omit this and get throwing stubs; runtime callers (kernel) must supply real impls. */
|
|
88
80
|
celHandlers?: import("./cel-environment.js").CelHandlers;
|