@telorun/ide-support 0.7.10 → 0.9.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.
Files changed (52) hide show
  1. package/README.md +3 -1
  2. package/dist/completions/import-source.js +2 -2
  3. package/dist/completions/resolve-node.d.ts +5 -0
  4. package/dist/completions/resolve-node.d.ts.map +1 -1
  5. package/dist/completions/resolve-node.js +4 -0
  6. package/dist/definition/alias-qualified-value.d.ts +23 -0
  7. package/dist/definition/alias-qualified-value.d.ts.map +1 -0
  8. package/dist/definition/alias-qualified-value.js +24 -0
  9. package/dist/definition/build-definition.d.ts +15 -8
  10. package/dist/definition/build-definition.d.ts.map +1 -1
  11. package/dist/definition/build-definition.js +35 -89
  12. package/dist/definition/manifest-navigation.d.ts +36 -0
  13. package/dist/definition/manifest-navigation.d.ts.map +1 -0
  14. package/dist/definition/manifest-navigation.js +92 -0
  15. package/dist/definition/resolve-cel-target.d.ts +17 -0
  16. package/dist/definition/resolve-cel-target.d.ts.map +1 -0
  17. package/dist/definition/resolve-cel-target.js +125 -0
  18. package/dist/definition/resolve-export-chain.d.ts +23 -0
  19. package/dist/definition/resolve-export-chain.d.ts.map +1 -0
  20. package/dist/definition/resolve-export-chain.js +89 -0
  21. package/dist/definition/resolve-kind-target.d.ts +26 -0
  22. package/dist/definition/resolve-kind-target.d.ts.map +1 -0
  23. package/dist/definition/resolve-kind-target.js +51 -0
  24. package/dist/definition/resolve-ref-target.d.ts +14 -0
  25. package/dist/definition/resolve-ref-target.d.ts.map +1 -0
  26. package/dist/definition/resolve-ref-target.js +25 -0
  27. package/dist/import-upgrades/build-import-upgrades.d.ts +78 -0
  28. package/dist/import-upgrades/build-import-upgrades.d.ts.map +1 -0
  29. package/dist/import-upgrades/build-import-upgrades.js +95 -0
  30. package/dist/import-upgrades/find-import-entries.d.ts +45 -0
  31. package/dist/import-upgrades/find-import-entries.d.ts.map +1 -0
  32. package/dist/import-upgrades/find-import-entries.js +102 -0
  33. package/dist/import-upgrades/index.d.ts +5 -0
  34. package/dist/import-upgrades/index.d.ts.map +1 -0
  35. package/dist/import-upgrades/index.js +2 -0
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1 -0
  39. package/package.json +2 -2
  40. package/src/completions/import-source.ts +2 -2
  41. package/src/completions/resolve-node.ts +9 -0
  42. package/src/definition/alias-qualified-value.ts +42 -0
  43. package/src/definition/build-definition.ts +34 -104
  44. package/src/definition/manifest-navigation.ts +124 -0
  45. package/src/definition/resolve-cel-target.ts +151 -0
  46. package/src/definition/resolve-export-chain.ts +119 -0
  47. package/src/definition/resolve-kind-target.ts +61 -0
  48. package/src/definition/resolve-ref-target.ts +34 -0
  49. package/src/import-upgrades/build-import-upgrades.ts +190 -0
  50. package/src/import-upgrades/find-import-entries.ts +175 -0
  51. package/src/import-upgrades/index.ts +10 -0
  52. package/src/index.ts +1 -0
@@ -0,0 +1,89 @@
1
+ import { parseExportEntry, resolveExportedKinds, } from "@telorun/analyzer";
2
+ import { locateKindDefinition, locateResource, moduleDoc, moduleFiles, moduleName, } from "./manifest-navigation.js";
3
+ /** One `exports.*` list as authored. `undefined` means the block is ABSENT,
4
+ * which is not the same as an empty list — the distinction is the gate itself,
5
+ * and it is load-bearing in both halves (`AliasResolver.registerImport`,
6
+ * `ModuleContext.buildExportTable`), so it must survive here too. */
7
+ function exportList(mod, block) {
8
+ const exports = moduleDoc(mod)?.exports;
9
+ const list = exports?.[block];
10
+ if (!Array.isArray(list))
11
+ return undefined;
12
+ return list.filter((e) => typeof e === "string");
13
+ }
14
+ /** Every loaded module keyed by its `metadata.name` — what `resolveExportedKinds`
15
+ * speaks, since a canonical kind names its owning module rather than a URL. */
16
+ function modulesByName(graph) {
17
+ const byName = new Map();
18
+ for (const mod of graph.modules.values()) {
19
+ const name = moduleName(mod);
20
+ if (name && !byName.has(name))
21
+ byName.set(name, mod);
22
+ }
23
+ return byName;
24
+ }
25
+ /** Resolve the kind suffix `name` exported by the module at `targetSource` to
26
+ * the `Telo.Definition` / `Telo.Abstract` doc that owns it.
27
+ *
28
+ * The gate and the transitive re-export chain come from the analyzer's own
29
+ * `resolveExportedKinds` fixpoint rather than a local walk, so navigation
30
+ * cannot disagree with `telo check` about what an import exposes — including
31
+ * the two rules a hand-rolled walk gets wrong: `exports.kinds: []` gates
32
+ * everything while an absent block gates nothing, and a re-export FROM an
33
+ * ungated module resolves straight to it. */
34
+ export function resolveExportedKind(graph, targetSource, name) {
35
+ const target = graph.modules.get(targetSource);
36
+ if (!target)
37
+ return undefined;
38
+ // No `exports.kinds` block → every kind the module defines is importable (the
39
+ // legacy permissive default the kernel still honors for already-published
40
+ // versions). The fixpoint builds tables only from declared entries, so an
41
+ // ungated module has none to look this up in — and this is also the cheap
42
+ // path, so it settles before anything is indexed.
43
+ if (exportList(target, "kinds") === undefined) {
44
+ return locateKindDefinition(moduleFiles(target), name);
45
+ }
46
+ const targetName = moduleName(target);
47
+ if (!targetName)
48
+ return undefined;
49
+ const byName = modulesByName(graph);
50
+ const tables = resolveExportedKinds([...byName].map(([module, mod]) => ({ module, exportsKinds: exportList(mod, "kinds") })), (module, alias) => {
51
+ const owner = byName.get(module)?.owner.source;
52
+ return owner ? graph.importEdges.get(owner)?.get(alias)?.targetModuleName ?? undefined : undefined;
53
+ });
54
+ const canonical = tables.get(targetName)?.get(name);
55
+ if (!canonical)
56
+ return undefined;
57
+ // `<owningModule>.<Kind>` — a re-export resolves to its true owner, so the
58
+ // jump lands where the kind is actually declared, however many hops away.
59
+ const dot = canonical.lastIndexOf(".");
60
+ const owner = byName.get(canonical.slice(0, dot));
61
+ return owner ? locateKindDefinition(moduleFiles(owner), canonical.slice(dot + 1)) : undefined;
62
+ }
63
+ /** Resolve the instance `name` exported by the module at `targetSource`,
64
+ * following `<Alias>.<name>` re-exports transitively. `seen` bounds cyclic
65
+ * import graphs.
66
+ *
67
+ * Unlike kinds there is no permissive default: the kernel reads
68
+ * `exports.resources ?? []` and builds its export table strictly from that, so
69
+ * an absent block exports nothing and an unlisted name is an
70
+ * `UNRESOLVED_REFERENCE` — navigating to it would tell the author a wiring is
71
+ * real that the analyzer rejects. */
72
+ export function resolveExportedResource(graph, targetSource, name, seen = new Set()) {
73
+ if (seen.has(targetSource))
74
+ return undefined;
75
+ seen.add(targetSource);
76
+ const target = graph.modules.get(targetSource);
77
+ if (!target)
78
+ return undefined;
79
+ const entries = (exportList(target, "resources") ?? []).map(parseExportEntry);
80
+ const entry = entries.find((e) => e.name === name);
81
+ if (!entry)
82
+ return undefined;
83
+ // `Self.<name>` names the declaring module's own instance, exactly as a bare
84
+ // name does — `ModuleContext.buildExportTable` treats the two alike.
85
+ if (!entry.alias || entry.alias === "Self")
86
+ return locateResource(moduleFiles(target), name);
87
+ const edge = graph.importEdges.get(target.owner.source)?.get(entry.alias);
88
+ return edge ? resolveExportedResource(graph, edge.targetSource, name, seen) : undefined;
89
+ }
@@ -0,0 +1,26 @@
1
+ import type { LoadedGraph, LoadedModule } from "@telorun/analyzer";
2
+ import type { DefinitionResult } from "../types.js";
3
+ import type { ResolvedCursor } from "../completions/resolve-node.js";
4
+ import type { AliasQualifiedValue } from "./alias-qualified-value.js";
5
+ /** Whether the cursor sits in a slot whose value is an alias-qualified kind.
6
+ *
7
+ * `x-telo-ref` is an `x-telo-*` annotation, unambiguous wherever it appears.
8
+ * `extends:` is only a kind at the top level of a definition doc, so it is
9
+ * gated on both. `kind:` is positional by nature: a map carrying a
10
+ * `<Alias>.<Kind>` value under `kind` IS an inline resource declaration as far
11
+ * as the analyzer is concerned too (`resourceKindOf`), so there is no further
12
+ * structure to check — an unresolvable value simply navigates nowhere.
13
+ *
14
+ * `capability:` is deliberately absent — its values are kernel built-ins with
15
+ * no manifest to jump to (hover documents them instead). */
16
+ export declare function isKindSlot(resolved: ResolvedCursor): boolean;
17
+ /** Resolve an alias-qualified kind (`Http.Server`) to the `Telo.Definition` /
18
+ * `Telo.Abstract` doc that registers it, or — when the cursor sits on the
19
+ * alias — to the import that declares the alias.
20
+ *
21
+ * `Self.<Kind>` is the declaring module's own kind; `Telo.<Kind>` is a kernel
22
+ * built-in with no manifest, so it resolves to nothing. Across an import the
23
+ * walk honors `exports.kinds` and follows `<Inner>.<Kind>` re-exports to the
24
+ * owning module, matching what the kernel resolves the kind to at runtime. */
25
+ export declare function resolveKindTarget(graph: LoadedGraph, currentModule: LoadedModule, kind: AliasQualifiedValue): DefinitionResult | undefined;
26
+ //# sourceMappingURL=resolve-kind-target.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-kind-target.d.ts","sourceRoot":"","sources":["../../src/definition/resolve-kind-target.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAMtE;;;;;;;;;;6DAU6D;AAC7D,wBAAgB,UAAU,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAQ5D;AAED;;;;;;;+EAO+E;AAC/E,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,WAAW,EAClB,aAAa,EAAE,YAAY,EAC3B,IAAI,EAAE,mBAAmB,GACxB,gBAAgB,GAAG,SAAS,CAkB9B"}
@@ -0,0 +1,51 @@
1
+ import { locateImport, locateKindDefinition, moduleFiles } from "./manifest-navigation.js";
2
+ import { resolveExportedKind } from "./resolve-export-chain.js";
3
+ const DEFINITION_DOC_KINDS = new Set(["Telo.Definition", "Telo.Abstract"]);
4
+ /** Whether the cursor sits in a slot whose value is an alias-qualified kind.
5
+ *
6
+ * `x-telo-ref` is an `x-telo-*` annotation, unambiguous wherever it appears.
7
+ * `extends:` is only a kind at the top level of a definition doc, so it is
8
+ * gated on both. `kind:` is positional by nature: a map carrying a
9
+ * `<Alias>.<Kind>` value under `kind` IS an inline resource declaration as far
10
+ * as the analyzer is concerned too (`resourceKindOf`), so there is no further
11
+ * structure to check — an unresolvable value simply navigates nowhere.
12
+ *
13
+ * `capability:` is deliberately absent — its values are kernel built-ins with
14
+ * no manifest to jump to (hover documents them instead). */
15
+ export function isKindSlot(resolved) {
16
+ const key = resolved.path[resolved.path.length - 1];
17
+ if (key === "x-telo-ref" || key === "kind")
18
+ return true;
19
+ return (key === "extends" &&
20
+ resolved.path.length === 1 &&
21
+ DEFINITION_DOC_KINDS.has(resolved.docKind ?? ""));
22
+ }
23
+ /** Resolve an alias-qualified kind (`Http.Server`) to the `Telo.Definition` /
24
+ * `Telo.Abstract` doc that registers it, or — when the cursor sits on the
25
+ * alias — to the import that declares the alias.
26
+ *
27
+ * `Self.<Kind>` is the declaring module's own kind; `Telo.<Kind>` is a kernel
28
+ * built-in with no manifest, so it resolves to nothing. Across an import the
29
+ * walk honors `exports.kinds` and follows `<Inner>.<Kind>` re-exports to the
30
+ * owning module, matching what the kernel resolves the kind to at runtime. */
31
+ export function resolveKindTarget(graph, currentModule, kind) {
32
+ const { alias, name, onAlias } = kind;
33
+ if (alias === undefined) {
34
+ // The legacy `<namespace>/<module>#<Kind>` identity form of `x-telo-ref`
35
+ // still resolves for already-published module versions, but it names a
36
+ // module this manifest need not import, so there is no alias to follow.
37
+ if (name.includes("#"))
38
+ return undefined;
39
+ return locateKindDefinition(moduleFiles(currentModule), name);
40
+ }
41
+ if (alias === "Self")
42
+ return locateKindDefinition(moduleFiles(currentModule), name);
43
+ if (alias === "Telo")
44
+ return undefined;
45
+ if (onAlias)
46
+ return locateImport(currentModule, alias);
47
+ const edge = graph.importEdges.get(currentModule.owner.source)?.get(alias);
48
+ if (!edge)
49
+ return undefined;
50
+ return resolveExportedKind(graph, edge.targetSource, name);
51
+ }
@@ -0,0 +1,14 @@
1
+ import type { LoadedGraph, LoadedModule } from "@telorun/analyzer";
2
+ import type { DefinitionResult } from "../types.js";
3
+ import type { AliasQualifiedValue } from "./alias-qualified-value.js";
4
+ /** Resolve a `!ref` target to the resource instance it names.
5
+ *
6
+ * The grammar mirrors `resolveRefSentinels`: a bare name (or `Self.name`) is a
7
+ * local resource in the current module; `Alias.name` is an exported instance of
8
+ * the module the import `Alias` points at, followed transitively through
9
+ * re-exports and gated on each module's `exports.resources`. The alias half
10
+ * navigates to the import that declares it. Returns `undefined` when the target
11
+ * can't be found (a scope-local name, an instance the target does not export,
12
+ * or an import that failed to load). */
13
+ export declare function resolveRefTarget(graph: LoadedGraph, currentModule: LoadedModule, ref: AliasQualifiedValue): DefinitionResult | undefined;
14
+ //# sourceMappingURL=resolve-ref-target.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-ref-target.d.ts","sourceRoot":"","sources":["../../src/definition/resolve-ref-target.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAItE;;;;;;;;yCAQyC;AACzC,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,WAAW,EAClB,aAAa,EAAE,YAAY,EAC3B,GAAG,EAAE,mBAAmB,GACvB,gBAAgB,GAAG,SAAS,CAc9B"}
@@ -0,0 +1,25 @@
1
+ import { locateImport, locateResource, moduleFiles } from "./manifest-navigation.js";
2
+ import { resolveExportedResource } from "./resolve-export-chain.js";
3
+ /** Resolve a `!ref` target to the resource instance it names.
4
+ *
5
+ * The grammar mirrors `resolveRefSentinels`: a bare name (or `Self.name`) is a
6
+ * local resource in the current module; `Alias.name` is an exported instance of
7
+ * the module the import `Alias` points at, followed transitively through
8
+ * re-exports and gated on each module's `exports.resources`. The alias half
9
+ * navigates to the import that declares it. Returns `undefined` when the target
10
+ * can't be found (a scope-local name, an instance the target does not export,
11
+ * or an import that failed to load). */
12
+ export function resolveRefTarget(graph, currentModule, ref) {
13
+ const { alias, name, onAlias } = ref;
14
+ if (alias === undefined || alias === "Self") {
15
+ // `Self` names the declaring module itself — there is no import to jump to,
16
+ // so the alias half resolves like the name half.
17
+ return locateResource(moduleFiles(currentModule), name);
18
+ }
19
+ if (onAlias)
20
+ return locateImport(currentModule, alias);
21
+ const edge = graph.importEdges.get(currentModule.owner.source)?.get(alias);
22
+ if (!edge)
23
+ return undefined;
24
+ return resolveExportedResource(graph, edge.targetSource, name);
25
+ }
@@ -0,0 +1,78 @@
1
+ import { type AstDocument, type Range } from "@telorun/analyzer";
2
+ /** Version enumeration for one version-independent base ref, newest first.
3
+ *
4
+ * Narrower than the full `IdeEnvironmentAdapter` on purpose. It is the only
5
+ * environment capability an upgrade check needs, and a host that caches or
6
+ * throttles hub traffic wraps just this — a CodeLens re-resolves far more
7
+ * often than a completion popup opens, so the refresh cadence is the host's
8
+ * policy, not this module's. A host backed by the hub passes
9
+ * `adapter.listVersionsForRef`. */
10
+ export type ModuleVersionLookup = (baseRef: string) => Promise<string[]>;
11
+ /** A source edit a host applies verbatim to upgrade an import. Ranges never
12
+ * overlap, within an upgrade or across a batch, so a host may apply the whole
13
+ * set in one pass without ordering them. */
14
+ export interface ImportUpgradeEdit {
15
+ range: Range;
16
+ newText: string;
17
+ }
18
+ /** One import that has a newer version available. */
19
+ export interface ImportUpgrade {
20
+ alias: string;
21
+ /** The source as written, with any object-form `integrity:` folded in. */
22
+ source: string;
23
+ currentVersion: string;
24
+ latestVersion: string;
25
+ /** The source that replaces it: re-pointed at `latestVersion` with the
26
+ * integrity pin dropped. */
27
+ newSource: string;
28
+ /** True when the replaced import carried a pin. A host that cannot recompute
29
+ * the hash should say so — the pin covers the version being replaced, and
30
+ * `telo upgrade` re-pins either shape. */
31
+ wasPinned: boolean;
32
+ /** Span of the alias key — where a per-entry affordance anchors. */
33
+ keyRange: Range;
34
+ /** Apply all of these to upgrade this one import. */
35
+ edits: ImportUpgradeEdit[];
36
+ }
37
+ /** An import that IS behind but that this module declines to rewrite. Carries
38
+ * the same anchor and versions an {@link ImportUpgrade} does, so a host can
39
+ * render it in place of the upgrade affordance rather than leaving the author
40
+ * wondering why a stale import shows nothing at all. */
41
+ export interface ImportUpgradeSkip {
42
+ alias: string;
43
+ currentVersion: string;
44
+ latestVersion: string;
45
+ /** Span of the alias key — where a per-entry affordance anchors. */
46
+ keyRange: Range;
47
+ /** Author-facing sentence: what was not done, and what to run instead. */
48
+ reason: string;
49
+ }
50
+ export interface ImportUpgradeSet {
51
+ /** Span of the `imports:` key — where a summary affordance anchors. */
52
+ importsKeyRange: Range;
53
+ upgrades: ImportUpgrade[];
54
+ skipped: ImportUpgradeSkip[];
55
+ /** Base refs whose version lookup failed. Never thrown: one unreachable ref
56
+ * must not blank the affordances for every other import in the file. The
57
+ * host decides whether to log or surface these. */
58
+ failures: Array<{
59
+ baseRef: string;
60
+ message: string;
61
+ }>;
62
+ }
63
+ /**
64
+ * Find every `imports:` entry of a module document that names a version older
65
+ * than the newest one `listVersions` reports, and produce the source edits that
66
+ * re-point it.
67
+ *
68
+ * Skips what carries no upgradeable version: local path imports, bare URLs,
69
+ * untagged refs, and pins that are not SemVer (an OCI digest, a moving tag like
70
+ * `latest`) — `parseVersionedRef` and `isNewerModuleVersion` both decline to
71
+ * guess, so those simply produce no upgrade.
72
+ *
73
+ * Pure apart from `listVersions`: no filesystem, no direct network, no host
74
+ * API. Returns `undefined` when the file declares no module document or the
75
+ * module declares no `imports:`.
76
+ */
77
+ export declare function buildImportUpgrades(text: string, listVersions: ModuleVersionLookup, docs?: AstDocument[]): Promise<ImportUpgradeSet | undefined>;
78
+ //# sourceMappingURL=build-import-upgrades.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-import-upgrades.d.ts","sourceRoot":"","sources":["../../src/import-upgrades/build-import-upgrades.ts"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,WAAW,EAChB,KAAK,KAAK,EACX,MAAM,mBAAmB,CAAC;AAG3B;;;;;;;oCAOoC;AACpC,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;AAEzE;;6CAE6C;AAC7C,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,KAAK,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qDAAqD;AACrD,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB;iCAC6B;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB;;+CAE2C;IAC3C,SAAS,EAAE,OAAO,CAAC;IACnB,oEAAoE;IACpE,QAAQ,EAAE,KAAK,CAAC;IAChB,qDAAqD;IACrD,KAAK,EAAE,iBAAiB,EAAE,CAAC;CAC5B;AAED;;;yDAGyD;AACzD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,QAAQ,EAAE,KAAK,CAAC;IAChB,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,uEAAuE;IACvE,eAAe,EAAE,KAAK,CAAC;IACvB,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B;;wDAEoD;IACpD,QAAQ,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACvD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,mBAAmB,EACjC,IAAI,CAAC,EAAE,WAAW,EAAE,GACnB,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAmDvC"}
@@ -0,0 +1,95 @@
1
+ import { buildLineOffsets, isLocalPathSource, isNewerModuleVersion, newestModuleVersion, parseToAst, parseVersionedRef, withRefVersion, } from "@telorun/analyzer";
2
+ import { findImportEntries } from "./find-import-entries.js";
3
+ /**
4
+ * Find every `imports:` entry of a module document that names a version older
5
+ * than the newest one `listVersions` reports, and produce the source edits that
6
+ * re-point it.
7
+ *
8
+ * Skips what carries no upgradeable version: local path imports, bare URLs,
9
+ * untagged refs, and pins that are not SemVer (an OCI digest, a moving tag like
10
+ * `latest`) — `parseVersionedRef` and `isNewerModuleVersion` both decline to
11
+ * guess, so those simply produce no upgrade.
12
+ *
13
+ * Pure apart from `listVersions`: no filesystem, no direct network, no host
14
+ * API. Returns `undefined` when the file declares no module document or the
15
+ * module declares no `imports:`.
16
+ */
17
+ export async function buildImportUpgrades(text, listVersions, docs) {
18
+ const lineOffsets = buildLineOffsets(text);
19
+ const block = findImportEntries(text, docs ?? parseToAst(text), lineOffsets);
20
+ if (!block)
21
+ return undefined;
22
+ const candidates = block.entries.flatMap((entry) => {
23
+ if (isLocalPathSource(entry.source))
24
+ return [];
25
+ const ref = parseVersionedRef(entry.source);
26
+ return ref ? [{ entry, ref }] : [];
27
+ });
28
+ const failures = [];
29
+ const latest = await resolveLatest([...new Set(candidates.map((c) => c.ref.baseRef))], listVersions, failures);
30
+ const upgrades = [];
31
+ const skipped = [];
32
+ for (const { entry, ref } of candidates) {
33
+ const newest = latest.get(ref.baseRef);
34
+ if (!newest || !isNewerModuleVersion(newest, ref.version))
35
+ continue;
36
+ if (entry.integrityInline) {
37
+ skipped.push({
38
+ alias: entry.alias,
39
+ currentVersion: ref.version,
40
+ latestVersion: newest,
41
+ keyRange: entry.keyRange,
42
+ reason: `'${entry.alias}' carries an inline 'integrity:' that shares a line with other ` +
43
+ `fields, so the stale pin cannot be removed by a line edit. Run \`telo upgrade\`.`,
44
+ });
45
+ continue;
46
+ }
47
+ upgrades.push({
48
+ alias: entry.alias,
49
+ source: entry.source,
50
+ currentVersion: ref.version,
51
+ latestVersion: newest,
52
+ newSource: withRefVersion(entry.source, newest),
53
+ wasPinned: ref.integrity != null,
54
+ keyRange: entry.keyRange,
55
+ edits: buildEdits(entry, withRefVersion(entry.source, newest)),
56
+ });
57
+ }
58
+ return { importsKeyRange: block.keyRange, upgrades, skipped, failures };
59
+ }
60
+ /** Re-point the source scalar, and delete a now-stale object-form `integrity:`
61
+ * line. `withRefVersion` already strips an inline `#sha256-…` fragment, so the
62
+ * scalar shorthand needs no second edit. Dropping the pin is not optional: it
63
+ * hashes the `telo.yaml` of the version being replaced, so carrying it onto a
64
+ * different version turns the next install into a tamper error. */
65
+ function buildEdits(entry, newSource) {
66
+ const edits = [{ range: entry.sourceRange, newText: newSource }];
67
+ if (entry.integrityLineRange) {
68
+ edits.push({ range: entry.integrityLineRange, newText: "" });
69
+ }
70
+ return edits;
71
+ }
72
+ /** Newest version per base ref, fetched once each. A ref whose lookup rejects
73
+ * is recorded in `failures` and left out of the map, so it yields no upgrade
74
+ * rather than a wrong one. */
75
+ async function resolveLatest(baseRefs, listVersions, failures) {
76
+ const results = await Promise.all(baseRefs.map(async (baseRef) => {
77
+ try {
78
+ const versions = await listVersions(baseRef);
79
+ return { baseRef, newest: newestModuleVersion(versions) };
80
+ }
81
+ catch (err) {
82
+ failures.push({
83
+ baseRef,
84
+ message: err instanceof Error ? err.message : String(err),
85
+ });
86
+ return { baseRef, newest: undefined };
87
+ }
88
+ }));
89
+ const map = new Map();
90
+ for (const { baseRef, newest } of results) {
91
+ if (newest)
92
+ map.set(baseRef, newest);
93
+ }
94
+ return map;
95
+ }
@@ -0,0 +1,45 @@
1
+ import { type AstDocument, type Range } from "@telorun/analyzer";
2
+ /** One `imports:` map entry, located in the source text.
3
+ *
4
+ * `source` is the *folded* form — an object-form `integrity:` sibling is
5
+ * folded into the source string as a `#sha256-…` fragment, exactly as
6
+ * `inlineImportManifests` does, so callers reason about a single
7
+ * representation regardless of which shape the author wrote. */
8
+ export interface ImportEntry {
9
+ alias: string;
10
+ /** The source with any `integrity:` sibling folded in as a fragment. */
11
+ source: string;
12
+ /** Span of the alias key — where a per-entry affordance anchors. */
13
+ keyRange: Range;
14
+ /** Span of the source scalar's value: the entry value itself for the scalar
15
+ * shorthand, the `source:` value for the object form. Replacing this span
16
+ * re-points the import. */
17
+ sourceRange: Range;
18
+ /** Whole-line span of an object-form `integrity:` entry, including its
19
+ * trailing newline, so a caller can delete the line. Absent for the scalar
20
+ * shorthand (where the pin rides inside `sourceRange`) and for an entry that
21
+ * declares no `integrity:`. */
22
+ integrityLineRange?: Range;
23
+ /** Set when the entry carries an `integrity:` sibling that does NOT occupy
24
+ * whole lines of its own (a flow-style `{source: …, integrity: …}` map).
25
+ * Deleting it would need a structural rewrite rather than a line splice, so
26
+ * a caller that cannot leave the pin behind must skip this entry rather
27
+ * than re-point it and strand a hash for the version it replaced. */
28
+ integrityInline?: boolean;
29
+ }
30
+ /** Where the `imports:` map lives in a module document. */
31
+ export interface ImportsBlock {
32
+ /** Span of the `imports:` key — where a summary affordance anchors. */
33
+ keyRange: Range;
34
+ entries: ImportEntry[];
35
+ }
36
+ /** Locate the `imports:` map of the file's module document (`Telo.Application`
37
+ * / `Telo.Library`). Returns `undefined` when the file declares no module doc
38
+ * or the doc has no `imports:` map — a partial file, or a module with no
39
+ * dependencies.
40
+ *
41
+ * Reads the AST rather than the analyzer's flattened manifests because the
42
+ * affordances built on top of this write back to the source: the exact span of
43
+ * each source scalar is the deliverable, not the resolved value. */
44
+ export declare function findImportEntries(text: string, docs: AstDocument[], lineOffsets: number[]): ImportsBlock | undefined;
45
+ //# sourceMappingURL=find-import-entries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"find-import-entries.d.ts","sourceRoot":"","sources":["../../src/import-upgrades/find-import-entries.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,WAAW,EAGhB,KAAK,KAAK,EACX,MAAM,mBAAmB,CAAC;AAE3B;;;;;iEAKiE;AACjE,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,QAAQ,EAAE,KAAK,CAAC;IAChB;;gCAE4B;IAC5B,WAAW,EAAE,KAAK,CAAC;IACnB;;;oCAGgC;IAChC,kBAAkB,CAAC,EAAE,KAAK,CAAC;IAC3B;;;;0EAIsE;IACtE,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,2DAA2D;AAC3D,MAAM,WAAW,YAAY;IAC3B,uEAAuE;IACvE,QAAQ,EAAE,KAAK,CAAC;IAChB,OAAO,EAAE,WAAW,EAAE,CAAC;CACxB;AAED;;;;;;;qEAOqE;AACrE,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,WAAW,EAAE,EACnB,WAAW,EAAE,MAAM,EAAE,GACpB,YAAY,GAAG,SAAS,CAsB1B"}
@@ -0,0 +1,102 @@
1
+ import { foldIntegrity, isModuleKind, offsetToPosition, } from "@telorun/analyzer";
2
+ /** Locate the `imports:` map of the file's module document (`Telo.Application`
3
+ * / `Telo.Library`). Returns `undefined` when the file declares no module doc
4
+ * or the doc has no `imports:` map — a partial file, or a module with no
5
+ * dependencies.
6
+ *
7
+ * Reads the AST rather than the analyzer's flattened manifests because the
8
+ * affordances built on top of this write back to the source: the exact span of
9
+ * each source scalar is the deliverable, not the resolved value. */
10
+ export function findImportEntries(text, docs, lineOffsets) {
11
+ for (const doc of docs) {
12
+ const root = doc.root;
13
+ if (!root || root.kind !== "map")
14
+ continue;
15
+ if (!isModuleKind(scalarString(mapGet(root, "kind"))))
16
+ continue;
17
+ const importsPair = root.entries.find((p) => p.key.kind === "scalar" && p.key.value === "imports");
18
+ if (!importsPair?.value || importsPair.value.kind !== "map")
19
+ return undefined;
20
+ return {
21
+ keyRange: toRange(importsPair.key, lineOffsets),
22
+ entries: importsPair.value.entries.flatMap((pair) => {
23
+ const alias = scalarString(pair.key);
24
+ if (alias === undefined || !pair.value)
25
+ return [];
26
+ const entry = readEntry(alias, pair.key, pair.value, text, lineOffsets);
27
+ return entry ? [entry] : [];
28
+ }),
29
+ };
30
+ }
31
+ return undefined;
32
+ }
33
+ /** Read one entry in either authored shape. Returns `undefined` for a malformed
34
+ * entry (an object with no string `source:`) — the module document's own
35
+ * schema validation already reports those against `imports.<Alias>.source`. */
36
+ function readEntry(alias, keyNode, valueNode, text, lineOffsets) {
37
+ const keyRange = toRange(keyNode, lineOffsets);
38
+ if (valueNode.kind === "scalar") {
39
+ const source = scalarString(valueNode);
40
+ if (source === undefined)
41
+ return undefined;
42
+ return { alias, source, keyRange, sourceRange: toRange(valueNode, lineOffsets) };
43
+ }
44
+ if (valueNode.kind !== "map")
45
+ return undefined;
46
+ const sourceNode = mapGet(valueNode, "source");
47
+ const source = scalarString(sourceNode);
48
+ if (sourceNode === undefined || source === undefined)
49
+ return undefined;
50
+ const integrityPair = valueNode.entries.find((p) => p.key.kind === "scalar" && p.key.value === "integrity");
51
+ const integrity = scalarString(integrityPair?.value);
52
+ const entry = {
53
+ alias,
54
+ source: foldIntegrity(source, integrity),
55
+ keyRange,
56
+ sourceRange: toRange(sourceNode, lineOffsets),
57
+ };
58
+ if (integrityPair?.value && integrity !== undefined) {
59
+ const lineSpan = wholeLineSpan(integrityPair.key.range[0], integrityPair.value.range[1], text, lineOffsets);
60
+ if (lineSpan)
61
+ entry.integrityLineRange = lineSpan;
62
+ else
63
+ entry.integrityInline = true;
64
+ }
65
+ return entry;
66
+ }
67
+ /** The whole-lines span covering `[start, end)` plus its trailing newline, or
68
+ * `undefined` when the span shares a line with other content — only leading
69
+ * indentation may precede it and nothing but spacing may follow. A flow-style
70
+ * map (`{source: …, integrity: …}`) fails this test, which is what keeps a
71
+ * line splice from eating a sibling field. */
72
+ function wholeLineSpan(start, end, text, lineOffsets) {
73
+ const lineStart = lineOffsets[offsetToPosition(start, lineOffsets).line];
74
+ if (text.slice(lineStart, start).trim() !== "")
75
+ return undefined;
76
+ const nextNewline = text.indexOf("\n", end);
77
+ const lineEnd = nextNewline === -1 ? text.length : nextNewline + 1;
78
+ if (text.slice(end, nextNewline === -1 ? text.length : nextNewline).trim() !== "") {
79
+ return undefined;
80
+ }
81
+ return {
82
+ start: offsetToPosition(lineStart, lineOffsets),
83
+ end: offsetToPosition(lineEnd, lineOffsets),
84
+ };
85
+ }
86
+ function toRange(node, lineOffsets) {
87
+ return {
88
+ start: offsetToPosition(node.range[0], lineOffsets),
89
+ end: offsetToPosition(node.range[1], lineOffsets),
90
+ };
91
+ }
92
+ function mapGet(node, key) {
93
+ return node.entries.find((p) => p.key.kind === "scalar" && p.key.value === key)?.value;
94
+ }
95
+ /** The node's value when it is a plain (untagged) string scalar. A `!cel` /
96
+ * `!ref` scalar resolves to a sentinel object, not a string, so it falls out
97
+ * here — an import source is never an expression. */
98
+ function scalarString(node) {
99
+ if (!node || node.kind !== "scalar")
100
+ return undefined;
101
+ return typeof node.value === "string" ? node.value : undefined;
102
+ }
@@ -0,0 +1,5 @@
1
+ export { buildImportUpgrades } from "./build-import-upgrades.js";
2
+ export type { ImportUpgrade, ImportUpgradeEdit, ImportUpgradeSet, ImportUpgradeSkip, ModuleVersionLookup, } from "./build-import-upgrades.js";
3
+ export { findImportEntries } from "./find-import-entries.js";
4
+ export type { ImportEntry, ImportsBlock } from "./find-import-entries.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/import-upgrades/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,YAAY,EACV,aAAa,EACb,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { buildImportUpgrades } from "./build-import-upgrades.js";
2
+ export { findImportEntries } from "./find-import-entries.js";
package/dist/index.d.ts CHANGED
@@ -4,4 +4,5 @@ export * from "./diagnostics/index.js";
4
4
  export * from "./hover/index.js";
5
5
  export * from "./semantic-tokens/index.js";
6
6
  export * from "./definition/index.js";
7
+ export * from "./import-upgrades/index.js";
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AACjC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AACjC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,4BAA4B,CAAC"}
package/dist/index.js CHANGED
@@ -4,3 +4,4 @@ export * from "./diagnostics/index.js";
4
4
  export * from "./hover/index.js";
5
5
  export * from "./semantic-tokens/index.js";
6
6
  export * from "./definition/index.js";
7
+ export * from "./import-upgrades/index.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/ide-support",
3
- "version": "0.7.10",
3
+ "version": "0.9.0",
4
4
  "description": "Editor-host-agnostic IDE support (completions, diagnostic normalization) for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -36,7 +36,7 @@
36
36
  "src/**"
37
37
  ],
38
38
  "dependencies": {
39
- "@telorun/analyzer": "0.49.1"
39
+ "@telorun/analyzer": "0.51.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "^20.0.0",
@@ -161,8 +161,8 @@ async function refSearchCompletions(
161
161
 
162
162
  /** The `org/name` tail of a location ref: its last two path segments, with the
163
163
  * transport scheme (`oci://`, `https://`, …) and registry host dropped.
164
- * `oci://ghcr.io/telorun/telo-console` → `telorun/telo-console`; `std/console`
165
- * → `std/console`. Falls back to fewer segments (or the whole ref) when there
164
+ * `oci://ghcr.io/telorun/telo-console` → `telorun/telo-console`; `acme/console`
165
+ * → `acme/console`. Falls back to fewer segments (or the whole ref) when there
166
166
  * aren't two. */
167
167
  function refDisplayName(ref: string): string {
168
168
  const withoutScheme = ref.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "");