@telorun/ide-support 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/import-upgrades/build-import-upgrades.d.ts +46 -9
- package/dist/import-upgrades/build-import-upgrades.d.ts.map +1 -1
- package/dist/import-upgrades/build-import-upgrades.js +92 -27
- package/dist/import-upgrades/find-import-entries.d.ts +17 -11
- package/dist/import-upgrades/find-import-entries.d.ts.map +1 -1
- package/dist/import-upgrades/find-import-entries.js +4 -5
- package/dist/import-upgrades/index.d.ts +3 -2
- package/dist/import-upgrades/index.d.ts.map +1 -1
- package/dist/import-upgrades/index.js +1 -0
- package/dist/import-upgrades/parse-module-versions.d.ts +19 -0
- package/dist/import-upgrades/parse-module-versions.d.ts.map +1 -0
- package/dist/import-upgrades/parse-module-versions.js +29 -0
- package/package.json +2 -2
- package/src/import-upgrades/build-import-upgrades.ts +150 -36
- package/src/import-upgrades/find-import-entries.ts +27 -19
- package/src/import-upgrades/index.ts +8 -1
- package/src/import-upgrades/parse-module-versions.ts +30 -0
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
import { type AstDocument, type Range } from "@telorun/analyzer";
|
|
2
|
+
/** One version of a module, as the hub reports it.
|
|
3
|
+
*
|
|
4
|
+
* `integrity` is the import pin for exactly this version (`sha256-<base64url>`
|
|
5
|
+
* over whatever the owning transport verifies its own reads against). It is
|
|
6
|
+
* absent for a version the hub tracked before it recorded pins, and for a ref
|
|
7
|
+
* no transport can hash — never an error, just no pin to write. */
|
|
8
|
+
export interface ModuleVersion {
|
|
9
|
+
version: string;
|
|
10
|
+
integrity?: string;
|
|
11
|
+
}
|
|
2
12
|
/** Version enumeration for one version-independent base ref, newest first.
|
|
3
13
|
*
|
|
4
14
|
* Narrower than the full `IdeEnvironmentAdapter` on purpose. It is the only
|
|
5
15
|
* environment capability an upgrade check needs, and a host that caches or
|
|
6
16
|
* throttles hub traffic wraps just this — a CodeLens re-resolves far more
|
|
7
17
|
* often than a completion popup opens, so the refresh cadence is the host's
|
|
8
|
-
* policy, not this module's.
|
|
9
|
-
*
|
|
10
|
-
|
|
18
|
+
* policy, not this module's.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately NOT `adapter.listVersionsForRef`, which answers `string[]`:
|
|
21
|
+
* completion offers names, an upgrade writes a pin, so the two want different
|
|
22
|
+
* things from one route. A host backed by the hub fetches
|
|
23
|
+
* `GET /module/versions` and passes the body through
|
|
24
|
+
* {@link parseModuleVersions}. */
|
|
25
|
+
export type ModuleVersionLookup = (baseRef: string) => Promise<ModuleVersion[]>;
|
|
11
26
|
/** A source edit a host applies verbatim to upgrade an import. Ranges never
|
|
12
27
|
* overlap, within an upgrade or across a batch, so a host may apply the whole
|
|
13
28
|
* set in one pass without ordering them. */
|
|
@@ -22,18 +37,38 @@ export interface ImportUpgrade {
|
|
|
22
37
|
source: string;
|
|
23
38
|
currentVersion: string;
|
|
24
39
|
latestVersion: string;
|
|
25
|
-
/**
|
|
26
|
-
*
|
|
40
|
+
/** What the import resolves to after the edits, in the same folded form as
|
|
41
|
+
* `source` — re-pointed at `latestVersion`, carrying the new pin as a
|
|
42
|
+
* `#sha256-…` fragment when one was available. Folded rather than literal
|
|
43
|
+
* because where the pin physically lands depends on the shape the author
|
|
44
|
+
* wrote (fragment vs `integrity:` sibling), and a host showing this as a
|
|
45
|
+
* preview wants the resolved import, not one of two spellings of it. */
|
|
27
46
|
newSource: string;
|
|
28
|
-
/** True when the replaced import carried a pin.
|
|
29
|
-
* the hash should say so — the pin covers the version being replaced, and
|
|
30
|
-
* `telo upgrade` re-pins either shape. */
|
|
47
|
+
/** True when the replaced import carried a pin. */
|
|
31
48
|
wasPinned: boolean;
|
|
49
|
+
/** True when the edits leave the import pinned to `latestVersion`. False
|
|
50
|
+
* means no pin was available for the target version — a host should say so
|
|
51
|
+
* when `wasPinned`, since the rewrite silently drops a hash the author had. */
|
|
52
|
+
repinned: boolean;
|
|
32
53
|
/** Span of the alias key — where a per-entry affordance anchors. */
|
|
33
54
|
keyRange: Range;
|
|
34
55
|
/** Apply all of these to upgrade this one import. */
|
|
35
56
|
edits: ImportUpgradeEdit[];
|
|
36
57
|
}
|
|
58
|
+
/** One import that is already at the newest version but carries no integrity
|
|
59
|
+
* pin, and for which a pin is available. Mirrors what `telo upgrade` does with
|
|
60
|
+
* `ensurePinned`: a rarely-released module whose version never moves would
|
|
61
|
+
* otherwise stay unpinned forever, because nothing ever offers to rewrite it. */
|
|
62
|
+
export interface ImportPin {
|
|
63
|
+
alias: string;
|
|
64
|
+
source: string;
|
|
65
|
+
version: string;
|
|
66
|
+
/** The source that replaces it: unchanged but for the appended pin. */
|
|
67
|
+
newSource: string;
|
|
68
|
+
/** Span of the alias key — where a per-entry affordance anchors. */
|
|
69
|
+
keyRange: Range;
|
|
70
|
+
edits: ImportUpgradeEdit[];
|
|
71
|
+
}
|
|
37
72
|
/** An import that IS behind but that this module declines to rewrite. Carries
|
|
38
73
|
* the same anchor and versions an {@link ImportUpgrade} does, so a host can
|
|
39
74
|
* render it in place of the upgrade affordance rather than leaving the author
|
|
@@ -51,6 +86,7 @@ export interface ImportUpgradeSet {
|
|
|
51
86
|
/** Span of the `imports:` key — where a summary affordance anchors. */
|
|
52
87
|
importsKeyRange: Range;
|
|
53
88
|
upgrades: ImportUpgrade[];
|
|
89
|
+
pins: ImportPin[];
|
|
54
90
|
skipped: ImportUpgradeSkip[];
|
|
55
91
|
/** Base refs whose version lookup failed. Never thrown: one unreachable ref
|
|
56
92
|
* must not blank the affordances for every other import in the file. The
|
|
@@ -63,7 +99,8 @@ export interface ImportUpgradeSet {
|
|
|
63
99
|
/**
|
|
64
100
|
* Find every `imports:` entry of a module document that names a version older
|
|
65
101
|
* than the newest one `listVersions` reports, and produce the source edits that
|
|
66
|
-
* re-point it
|
|
102
|
+
* re-point it — plus every entry already at the newest version that carries no
|
|
103
|
+
* integrity pin, and the edits that pin it.
|
|
67
104
|
*
|
|
68
105
|
* Skips what carries no upgradeable version: local path imports, bare URLs,
|
|
69
106
|
* untagged refs, and pins that are not SemVer (an OCI digest, a moving tag like
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-import-upgrades.d.ts","sourceRoot":"","sources":["../../src/import-upgrades/build-import-upgrades.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"build-import-upgrades.d.ts","sourceRoot":"","sources":["../../src/import-upgrades/build-import-upgrades.ts"],"names":[],"mappings":"AAAA,OAAO,EAYL,KAAK,WAAW,EAChB,KAAK,KAAK,EACX,MAAM,mBAAmB,CAAC;AAG3B;;;;;oEAKoE;AACpE,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;mCAYmC;AACnC,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;AAEhF;;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;;;;;6EAKyE;IACzE,SAAS,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,SAAS,EAAE,OAAO,CAAC;IACnB;;oFAEgF;IAChF,QAAQ,EAAE,OAAO,CAAC;IAClB,oEAAoE;IACpE,QAAQ,EAAE,KAAK,CAAC;IAChB,qDAAqD;IACrD,KAAK,EAAE,iBAAiB,EAAE,CAAC;CAC5B;AAED;;;kFAGkF;AAClF,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,QAAQ,EAAE,KAAK,CAAC;IAChB,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,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,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;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,mBAAmB,EACjC,IAAI,CAAC,EAAE,WAAW,EAAE,GACnB,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAmEvC"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { buildLineOffsets, isLocalPathSource, isNewerModuleVersion, newestModuleVersion, parseToAst, parseVersionedRef, withRefVersion, } from "@telorun/analyzer";
|
|
1
|
+
import { buildLineOffsets, foldIntegrity, isCanonicalIntegrity, isLocalPathSource, isNewerModuleVersion, isSameModuleVersion, newestModuleVersion, parseModuleVersion, parseToAst, parseVersionedRef, withRefVersion, } from "@telorun/analyzer";
|
|
2
2
|
import { findImportEntries } from "./find-import-entries.js";
|
|
3
3
|
/**
|
|
4
4
|
* Find every `imports:` entry of a module document that names a version older
|
|
5
5
|
* than the newest one `listVersions` reports, and produce the source edits that
|
|
6
|
-
* re-point it
|
|
6
|
+
* re-point it — plus every entry already at the newest version that carries no
|
|
7
|
+
* integrity pin, and the edits that pin it.
|
|
7
8
|
*
|
|
8
9
|
* Skips what carries no upgradeable version: local path imports, bare URLs,
|
|
9
10
|
* untagged refs, and pins that are not SemVer (an OCI digest, a moving tag like
|
|
@@ -26,70 +27,134 @@ export async function buildImportUpgrades(text, listVersions, docs) {
|
|
|
26
27
|
return ref ? [{ entry, ref }] : [];
|
|
27
28
|
});
|
|
28
29
|
const failures = [];
|
|
29
|
-
const
|
|
30
|
+
const known = await resolveVersions([...new Set(candidates.map((c) => c.ref.baseRef))], listVersions, failures);
|
|
30
31
|
const upgrades = [];
|
|
32
|
+
const pins = [];
|
|
31
33
|
const skipped = [];
|
|
32
34
|
for (const { entry, ref } of candidates) {
|
|
33
|
-
const
|
|
34
|
-
if (!
|
|
35
|
+
const versions = known.get(ref.baseRef);
|
|
36
|
+
if (!versions)
|
|
35
37
|
continue;
|
|
36
|
-
|
|
38
|
+
const newest = newestModuleVersion(versions.map((v) => v.version));
|
|
39
|
+
if (!newest)
|
|
40
|
+
continue;
|
|
41
|
+
if (!isNewerModuleVersion(newest, ref.version)) {
|
|
42
|
+
const pin = pinInPlace(entry, ref.version, ref.integrity, versions);
|
|
43
|
+
if (pin)
|
|
44
|
+
pins.push(pin);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const integrity = integrityFor(versions, newest);
|
|
48
|
+
// A stale pin that can be neither replaced nor removed is the one case left
|
|
49
|
+
// that has to be declined: re-pointing the source while leaving a hash for
|
|
50
|
+
// the version it replaced turns the next install into a tamper error.
|
|
51
|
+
if (ref.integrity && !integrity && entry.integrity && !entry.integrity.lineRange) {
|
|
37
52
|
skipped.push({
|
|
38
53
|
alias: entry.alias,
|
|
39
54
|
currentVersion: ref.version,
|
|
40
55
|
latestVersion: newest,
|
|
41
56
|
keyRange: entry.keyRange,
|
|
42
57
|
reason: `'${entry.alias}' carries an inline 'integrity:' that shares a line with other ` +
|
|
43
|
-
`fields,
|
|
58
|
+
`fields, and no pin is published for ${newest}, so the stale one cannot be ` +
|
|
59
|
+
`removed by a line edit. Run \`telo upgrade\`.`,
|
|
44
60
|
});
|
|
45
61
|
continue;
|
|
46
62
|
}
|
|
63
|
+
const newSource = withRefVersion(entry.source, newest);
|
|
47
64
|
upgrades.push({
|
|
48
65
|
alias: entry.alias,
|
|
49
66
|
source: entry.source,
|
|
50
67
|
currentVersion: ref.version,
|
|
51
68
|
latestVersion: newest,
|
|
52
|
-
newSource:
|
|
69
|
+
newSource: foldIntegrity(newSource, integrity),
|
|
53
70
|
wasPinned: ref.integrity != null,
|
|
71
|
+
repinned: integrity != null,
|
|
54
72
|
keyRange: entry.keyRange,
|
|
55
|
-
edits: buildEdits(entry,
|
|
73
|
+
edits: buildEdits(entry, newSource, integrity),
|
|
56
74
|
});
|
|
57
75
|
}
|
|
58
|
-
return { importsKeyRange: block.keyRange, upgrades, skipped, failures };
|
|
76
|
+
return { importsKeyRange: block.keyRange, upgrades, pins, skipped, failures };
|
|
59
77
|
}
|
|
60
|
-
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
|
|
78
|
+
/** The edits that re-point an entry at `newSource` and settle its pin.
|
|
79
|
+
*
|
|
80
|
+
* Where the pin is written follows the shape the author chose: an entry with an
|
|
81
|
+
* `integrity:` sibling keeps it (its value is replaced in place, which works in
|
|
82
|
+
* block and flow style alike), and everything else carries the pin inside the
|
|
83
|
+
* source as a `#sha256-…` fragment — the form `telo upgrade` writes.
|
|
84
|
+
*
|
|
85
|
+
* With no pin available the sibling is deleted instead. That is not optional:
|
|
86
|
+
* it hashes the `telo.yaml` of the version being replaced, so carrying it onto
|
|
87
|
+
* a different version turns the next install into a tamper error.
|
|
88
|
+
* `withRefVersion` has already stripped a fragment-form pin for the same
|
|
89
|
+
* reason, so the scalar shorthand needs no second edit. */
|
|
90
|
+
function buildEdits(entry, newSource, integrity) {
|
|
91
|
+
if (!entry.integrity) {
|
|
92
|
+
return [{ range: entry.sourceRange, newText: foldIntegrity(newSource, integrity) }];
|
|
93
|
+
}
|
|
66
94
|
const edits = [{ range: entry.sourceRange, newText: newSource }];
|
|
67
|
-
if (
|
|
68
|
-
edits.push({ range: entry.
|
|
95
|
+
if (integrity) {
|
|
96
|
+
edits.push({ range: entry.integrity.valueRange, newText: integrity });
|
|
97
|
+
}
|
|
98
|
+
else if (entry.integrity.lineRange) {
|
|
99
|
+
edits.push({ range: entry.integrity.lineRange, newText: "" });
|
|
69
100
|
}
|
|
70
101
|
return edits;
|
|
71
102
|
}
|
|
72
|
-
/**
|
|
73
|
-
* is
|
|
103
|
+
/** Pin an entry that is already at the newest version, or `undefined` when
|
|
104
|
+
* there is nothing to do — it is pinned already, the hub published no pin for
|
|
105
|
+
* the version it names, or that version is not one this module will order.
|
|
106
|
+
*
|
|
107
|
+
* The version gate matters: a moving tag (`latest`) or a digest names bytes
|
|
108
|
+
* that are expected to change, so pinning it would break the next release
|
|
109
|
+
* rather than protect it. `telo upgrade` draws the same line. */
|
|
110
|
+
function pinInPlace(entry, version, existingIntegrity, versions) {
|
|
111
|
+
if (existingIntegrity != null || entry.integrity)
|
|
112
|
+
return undefined;
|
|
113
|
+
if (parseModuleVersion(version) === null)
|
|
114
|
+
return undefined;
|
|
115
|
+
const integrity = integrityFor(versions, version);
|
|
116
|
+
if (!integrity)
|
|
117
|
+
return undefined;
|
|
118
|
+
const newSource = foldIntegrity(entry.source, integrity);
|
|
119
|
+
return {
|
|
120
|
+
alias: entry.alias,
|
|
121
|
+
source: entry.source,
|
|
122
|
+
version,
|
|
123
|
+
newSource,
|
|
124
|
+
keyRange: entry.keyRange,
|
|
125
|
+
edits: [{ range: entry.sourceRange, newText: newSource }],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/** The pin the hub reports for one version, matched by SemVer identity rather
|
|
129
|
+
* than string equality so a `v`-prefixed tag on either side still lines up.
|
|
130
|
+
*
|
|
131
|
+
* Re-checked here even though the host's parse already did: this value is
|
|
132
|
+
* spliced into the author's YAML, and a caller reaching the builder through
|
|
133
|
+
* its own `ModuleVersionLookup` never passed through that parse. */
|
|
134
|
+
function integrityFor(versions, version) {
|
|
135
|
+
const integrity = versions.find((v) => isSameModuleVersion(v.version, version))?.integrity;
|
|
136
|
+
return isCanonicalIntegrity(integrity) ? integrity : undefined;
|
|
137
|
+
}
|
|
138
|
+
/** Versions per base ref, fetched once each. A ref whose lookup rejects is
|
|
139
|
+
* recorded in `failures` and left out of the map, so it yields no upgrade
|
|
74
140
|
* rather than a wrong one. */
|
|
75
|
-
async function
|
|
141
|
+
async function resolveVersions(baseRefs, listVersions, failures) {
|
|
76
142
|
const results = await Promise.all(baseRefs.map(async (baseRef) => {
|
|
77
143
|
try {
|
|
78
|
-
|
|
79
|
-
return { baseRef, newest: newestModuleVersion(versions) };
|
|
144
|
+
return { baseRef, versions: await listVersions(baseRef) };
|
|
80
145
|
}
|
|
81
146
|
catch (err) {
|
|
82
147
|
failures.push({
|
|
83
148
|
baseRef,
|
|
84
149
|
message: err instanceof Error ? err.message : String(err),
|
|
85
150
|
});
|
|
86
|
-
return { baseRef,
|
|
151
|
+
return { baseRef, versions: undefined };
|
|
87
152
|
}
|
|
88
153
|
}));
|
|
89
154
|
const map = new Map();
|
|
90
|
-
for (const { baseRef,
|
|
91
|
-
if (
|
|
92
|
-
map.set(baseRef,
|
|
155
|
+
for (const { baseRef, versions } of results) {
|
|
156
|
+
if (versions)
|
|
157
|
+
map.set(baseRef, versions);
|
|
93
158
|
}
|
|
94
159
|
return map;
|
|
95
160
|
}
|
|
@@ -15,17 +15,23 @@ export interface ImportEntry {
|
|
|
15
15
|
* shorthand, the `source:` value for the object form. Replacing this span
|
|
16
16
|
* re-points the import. */
|
|
17
17
|
sourceRange: Range;
|
|
18
|
-
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
18
|
+
/** Where an object-form `integrity:` sibling sits, when the entry declares
|
|
19
|
+
* one. Absent for the scalar shorthand — there the pin rides inside
|
|
20
|
+
* `sourceRange` as a `#sha256-…` fragment — and for an unpinned entry. */
|
|
21
|
+
integrity?: ImportEntryIntegrity;
|
|
22
|
+
}
|
|
23
|
+
/** The two spans a caller needs to act on an object-form `integrity:`: one to
|
|
24
|
+
* re-pin it, one to remove it. They are separate because a flow-style map
|
|
25
|
+
* (`{source: …, integrity: …}`) supports the first and not the second. */
|
|
26
|
+
export interface ImportEntryIntegrity {
|
|
27
|
+
/** Span of the hash value itself. Replacing it re-pins the entry in the shape
|
|
28
|
+
* the author wrote, in any YAML style. */
|
|
29
|
+
valueRange: Range;
|
|
30
|
+
/** Whole-line span of the `integrity:` entry including its trailing newline,
|
|
31
|
+
* so a caller with no replacement hash can delete the line outright. Absent
|
|
32
|
+
* when the pair shares a line with other content, where removing it would
|
|
33
|
+
* need a structural rewrite rather than a line splice. */
|
|
34
|
+
lineRange?: Range;
|
|
29
35
|
}
|
|
30
36
|
/** Where the `imports:` map lives in a module document. */
|
|
31
37
|
export interface ImportsBlock {
|
|
@@ -1 +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
|
|
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;;+EAE2E;IAC3E,SAAS,CAAC,EAAE,oBAAoB,CAAC;CAClC;AAED;;2EAE2E;AAC3E,MAAM,WAAW,oBAAoB;IACnC;+CAC2C;IAC3C,UAAU,EAAE,KAAK,CAAC;IAClB;;;+DAG2D;IAC3D,SAAS,CAAC,EAAE,KAAK,CAAC;CACnB;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"}
|
|
@@ -56,11 +56,10 @@ function readEntry(alias, keyNode, valueNode, text, lineOffsets) {
|
|
|
56
56
|
sourceRange: toRange(sourceNode, lineOffsets),
|
|
57
57
|
};
|
|
58
58
|
if (integrityPair?.value && integrity !== undefined) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
entry.integrityInline = true;
|
|
59
|
+
entry.integrity = {
|
|
60
|
+
valueRange: toRange(integrityPair.value, lineOffsets),
|
|
61
|
+
lineRange: wholeLineSpan(integrityPair.key.range[0], integrityPair.value.range[1], text, lineOffsets),
|
|
62
|
+
};
|
|
64
63
|
}
|
|
65
64
|
return entry;
|
|
66
65
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { buildImportUpgrades } from "./build-import-upgrades.js";
|
|
2
|
-
export type { ImportUpgrade, ImportUpgradeEdit, ImportUpgradeSet, ImportUpgradeSkip, ModuleVersionLookup, } from "./build-import-upgrades.js";
|
|
2
|
+
export type { ImportPin, ImportUpgrade, ImportUpgradeEdit, ImportUpgradeSet, ImportUpgradeSkip, ModuleVersion, ModuleVersionLookup, } from "./build-import-upgrades.js";
|
|
3
|
+
export { parseModuleVersions } from "./parse-module-versions.js";
|
|
3
4
|
export { findImportEntries } from "./find-import-entries.js";
|
|
4
|
-
export type { ImportEntry, ImportsBlock } from "./find-import-entries.js";
|
|
5
|
+
export type { ImportEntry, ImportEntryIntegrity, ImportsBlock, } from "./find-import-entries.js";
|
|
5
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +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,
|
|
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,SAAS,EACT,aAAa,EACb,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,mBAAmB,GACpB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,YAAY,EACV,WAAW,EACX,oBAAoB,EACpB,YAAY,GACb,MAAM,0BAA0B,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ModuleVersion } from "./build-import-upgrades.js";
|
|
2
|
+
/**
|
|
3
|
+
* Read the hub's `GET /module/versions` payload into {@link ModuleVersion}s,
|
|
4
|
+
* newest first (the route's own ordering, preserved).
|
|
5
|
+
*
|
|
6
|
+
* Pure — the host owns the `fetch`, as everything else in this package does.
|
|
7
|
+
* It lives here rather than in each host because the shape is this package's:
|
|
8
|
+
* three hosts parsing the same route by hand is how one of them was left
|
|
9
|
+
* filtering for strings after the route started returning objects, which
|
|
10
|
+
* TypeScript could not catch behind the response cast and which surfaced only
|
|
11
|
+
* as a silently empty version list.
|
|
12
|
+
*
|
|
13
|
+
* Tolerant by design: an entry with no usable `version` is dropped rather than
|
|
14
|
+
* throwing, and an `integrity` that is not a canonical `sha256-<base64url>` is
|
|
15
|
+
* discarded rather than carried — a pin is spliced into the author's YAML, so a
|
|
16
|
+
* malformed one has to become "no pin", never corrupt text.
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseModuleVersions(payload: unknown): ModuleVersion[];
|
|
19
|
+
//# sourceMappingURL=parse-module-versions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-module-versions.d.ts","sourceRoot":"","sources":["../../src/import-upgrades/parse-module-versions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAEhE;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,aAAa,EAAE,CAUrE"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { isCanonicalIntegrity } from "@telorun/analyzer";
|
|
2
|
+
/**
|
|
3
|
+
* Read the hub's `GET /module/versions` payload into {@link ModuleVersion}s,
|
|
4
|
+
* newest first (the route's own ordering, preserved).
|
|
5
|
+
*
|
|
6
|
+
* Pure — the host owns the `fetch`, as everything else in this package does.
|
|
7
|
+
* It lives here rather than in each host because the shape is this package's:
|
|
8
|
+
* three hosts parsing the same route by hand is how one of them was left
|
|
9
|
+
* filtering for strings after the route started returning objects, which
|
|
10
|
+
* TypeScript could not catch behind the response cast and which surfaced only
|
|
11
|
+
* as a silently empty version list.
|
|
12
|
+
*
|
|
13
|
+
* Tolerant by design: an entry with no usable `version` is dropped rather than
|
|
14
|
+
* throwing, and an `integrity` that is not a canonical `sha256-<base64url>` is
|
|
15
|
+
* discarded rather than carried — a pin is spliced into the author's YAML, so a
|
|
16
|
+
* malformed one has to become "no pin", never corrupt text.
|
|
17
|
+
*/
|
|
18
|
+
export function parseModuleVersions(payload) {
|
|
19
|
+
const versions = payload?.versions;
|
|
20
|
+
if (!Array.isArray(versions))
|
|
21
|
+
return [];
|
|
22
|
+
return versions.flatMap((entry) => {
|
|
23
|
+
const version = entry?.version;
|
|
24
|
+
if (typeof version !== "string" || version === "")
|
|
25
|
+
return [];
|
|
26
|
+
const integrity = entry.integrity;
|
|
27
|
+
return [isCanonicalIntegrity(integrity) ? { version, integrity } : { version }];
|
|
28
|
+
});
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/ide-support",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
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.
|
|
39
|
+
"@telorun/analyzer": "0.53.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/node": "^20.0.0",
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildLineOffsets,
|
|
3
|
+
foldIntegrity,
|
|
4
|
+
isCanonicalIntegrity,
|
|
3
5
|
isLocalPathSource,
|
|
4
6
|
isNewerModuleVersion,
|
|
7
|
+
isSameModuleVersion,
|
|
5
8
|
newestModuleVersion,
|
|
9
|
+
parseModuleVersion,
|
|
6
10
|
parseToAst,
|
|
7
11
|
parseVersionedRef,
|
|
8
12
|
withRefVersion,
|
|
@@ -11,15 +15,31 @@ import {
|
|
|
11
15
|
} from "@telorun/analyzer";
|
|
12
16
|
import { findImportEntries, type ImportEntry } from "./find-import-entries.js";
|
|
13
17
|
|
|
18
|
+
/** One version of a module, as the hub reports it.
|
|
19
|
+
*
|
|
20
|
+
* `integrity` is the import pin for exactly this version (`sha256-<base64url>`
|
|
21
|
+
* over whatever the owning transport verifies its own reads against). It is
|
|
22
|
+
* absent for a version the hub tracked before it recorded pins, and for a ref
|
|
23
|
+
* no transport can hash — never an error, just no pin to write. */
|
|
24
|
+
export interface ModuleVersion {
|
|
25
|
+
version: string;
|
|
26
|
+
integrity?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
/** Version enumeration for one version-independent base ref, newest first.
|
|
15
30
|
*
|
|
16
31
|
* Narrower than the full `IdeEnvironmentAdapter` on purpose. It is the only
|
|
17
32
|
* environment capability an upgrade check needs, and a host that caches or
|
|
18
33
|
* throttles hub traffic wraps just this — a CodeLens re-resolves far more
|
|
19
34
|
* often than a completion popup opens, so the refresh cadence is the host's
|
|
20
|
-
* policy, not this module's.
|
|
21
|
-
*
|
|
22
|
-
|
|
35
|
+
* policy, not this module's.
|
|
36
|
+
*
|
|
37
|
+
* Deliberately NOT `adapter.listVersionsForRef`, which answers `string[]`:
|
|
38
|
+
* completion offers names, an upgrade writes a pin, so the two want different
|
|
39
|
+
* things from one route. A host backed by the hub fetches
|
|
40
|
+
* `GET /module/versions` and passes the body through
|
|
41
|
+
* {@link parseModuleVersions}. */
|
|
42
|
+
export type ModuleVersionLookup = (baseRef: string) => Promise<ModuleVersion[]>;
|
|
23
43
|
|
|
24
44
|
/** A source edit a host applies verbatim to upgrade an import. Ranges never
|
|
25
45
|
* overlap, within an upgrade or across a batch, so a host may apply the whole
|
|
@@ -36,19 +56,40 @@ export interface ImportUpgrade {
|
|
|
36
56
|
source: string;
|
|
37
57
|
currentVersion: string;
|
|
38
58
|
latestVersion: string;
|
|
39
|
-
/**
|
|
40
|
-
*
|
|
59
|
+
/** What the import resolves to after the edits, in the same folded form as
|
|
60
|
+
* `source` — re-pointed at `latestVersion`, carrying the new pin as a
|
|
61
|
+
* `#sha256-…` fragment when one was available. Folded rather than literal
|
|
62
|
+
* because where the pin physically lands depends on the shape the author
|
|
63
|
+
* wrote (fragment vs `integrity:` sibling), and a host showing this as a
|
|
64
|
+
* preview wants the resolved import, not one of two spellings of it. */
|
|
41
65
|
newSource: string;
|
|
42
|
-
/** True when the replaced import carried a pin.
|
|
43
|
-
* the hash should say so — the pin covers the version being replaced, and
|
|
44
|
-
* `telo upgrade` re-pins either shape. */
|
|
66
|
+
/** True when the replaced import carried a pin. */
|
|
45
67
|
wasPinned: boolean;
|
|
68
|
+
/** True when the edits leave the import pinned to `latestVersion`. False
|
|
69
|
+
* means no pin was available for the target version — a host should say so
|
|
70
|
+
* when `wasPinned`, since the rewrite silently drops a hash the author had. */
|
|
71
|
+
repinned: boolean;
|
|
46
72
|
/** Span of the alias key — where a per-entry affordance anchors. */
|
|
47
73
|
keyRange: Range;
|
|
48
74
|
/** Apply all of these to upgrade this one import. */
|
|
49
75
|
edits: ImportUpgradeEdit[];
|
|
50
76
|
}
|
|
51
77
|
|
|
78
|
+
/** One import that is already at the newest version but carries no integrity
|
|
79
|
+
* pin, and for which a pin is available. Mirrors what `telo upgrade` does with
|
|
80
|
+
* `ensurePinned`: a rarely-released module whose version never moves would
|
|
81
|
+
* otherwise stay unpinned forever, because nothing ever offers to rewrite it. */
|
|
82
|
+
export interface ImportPin {
|
|
83
|
+
alias: string;
|
|
84
|
+
source: string;
|
|
85
|
+
version: string;
|
|
86
|
+
/** The source that replaces it: unchanged but for the appended pin. */
|
|
87
|
+
newSource: string;
|
|
88
|
+
/** Span of the alias key — where a per-entry affordance anchors. */
|
|
89
|
+
keyRange: Range;
|
|
90
|
+
edits: ImportUpgradeEdit[];
|
|
91
|
+
}
|
|
92
|
+
|
|
52
93
|
/** An import that IS behind but that this module declines to rewrite. Carries
|
|
53
94
|
* the same anchor and versions an {@link ImportUpgrade} does, so a host can
|
|
54
95
|
* render it in place of the upgrade affordance rather than leaving the author
|
|
@@ -67,6 +108,7 @@ export interface ImportUpgradeSet {
|
|
|
67
108
|
/** Span of the `imports:` key — where a summary affordance anchors. */
|
|
68
109
|
importsKeyRange: Range;
|
|
69
110
|
upgrades: ImportUpgrade[];
|
|
111
|
+
pins: ImportPin[];
|
|
70
112
|
skipped: ImportUpgradeSkip[];
|
|
71
113
|
/** Base refs whose version lookup failed. Never thrown: one unreachable ref
|
|
72
114
|
* must not blank the affordances for every other import in the file. The
|
|
@@ -77,7 +119,8 @@ export interface ImportUpgradeSet {
|
|
|
77
119
|
/**
|
|
78
120
|
* Find every `imports:` entry of a module document that names a version older
|
|
79
121
|
* than the newest one `listVersions` reports, and produce the source edits that
|
|
80
|
-
* re-point it
|
|
122
|
+
* re-point it — plus every entry already at the newest version that carries no
|
|
123
|
+
* integrity pin, and the edits that pin it.
|
|
81
124
|
*
|
|
82
125
|
* Skips what carries no upgradeable version: local path imports, bare URLs,
|
|
83
126
|
* untagged refs, and pins that are not SemVer (an OCI digest, a moving tag like
|
|
@@ -104,20 +147,33 @@ export async function buildImportUpgrades(
|
|
|
104
147
|
});
|
|
105
148
|
|
|
106
149
|
const failures: Array<{ baseRef: string; message: string }> = [];
|
|
107
|
-
const
|
|
150
|
+
const known = await resolveVersions(
|
|
108
151
|
[...new Set(candidates.map((c) => c.ref.baseRef))],
|
|
109
152
|
listVersions,
|
|
110
153
|
failures,
|
|
111
154
|
);
|
|
112
155
|
|
|
113
156
|
const upgrades: ImportUpgrade[] = [];
|
|
157
|
+
const pins: ImportPin[] = [];
|
|
114
158
|
const skipped: ImportUpgradeSkip[] = [];
|
|
115
159
|
|
|
116
160
|
for (const { entry, ref } of candidates) {
|
|
117
|
-
const
|
|
118
|
-
if (!
|
|
161
|
+
const versions = known.get(ref.baseRef);
|
|
162
|
+
if (!versions) continue;
|
|
163
|
+
const newest = newestModuleVersion(versions.map((v) => v.version));
|
|
164
|
+
if (!newest) continue;
|
|
119
165
|
|
|
120
|
-
if (
|
|
166
|
+
if (!isNewerModuleVersion(newest, ref.version)) {
|
|
167
|
+
const pin = pinInPlace(entry, ref.version, ref.integrity, versions);
|
|
168
|
+
if (pin) pins.push(pin);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const integrity = integrityFor(versions, newest);
|
|
173
|
+
// A stale pin that can be neither replaced nor removed is the one case left
|
|
174
|
+
// that has to be declined: re-pointing the source while leaving a hash for
|
|
175
|
+
// the version it replaced turns the next install into a tamper error.
|
|
176
|
+
if (ref.integrity && !integrity && entry.integrity && !entry.integrity.lineRange) {
|
|
121
177
|
skipped.push({
|
|
122
178
|
alias: entry.alias,
|
|
123
179
|
currentVersion: ref.version,
|
|
@@ -125,66 +181,124 @@ export async function buildImportUpgrades(
|
|
|
125
181
|
keyRange: entry.keyRange,
|
|
126
182
|
reason:
|
|
127
183
|
`'${entry.alias}' carries an inline 'integrity:' that shares a line with other ` +
|
|
128
|
-
`fields,
|
|
184
|
+
`fields, and no pin is published for ${newest}, so the stale one cannot be ` +
|
|
185
|
+
`removed by a line edit. Run \`telo upgrade\`.`,
|
|
129
186
|
});
|
|
130
187
|
continue;
|
|
131
188
|
}
|
|
132
189
|
|
|
190
|
+
const newSource = withRefVersion(entry.source, newest);
|
|
133
191
|
upgrades.push({
|
|
134
192
|
alias: entry.alias,
|
|
135
193
|
source: entry.source,
|
|
136
194
|
currentVersion: ref.version,
|
|
137
195
|
latestVersion: newest,
|
|
138
|
-
newSource:
|
|
196
|
+
newSource: foldIntegrity(newSource, integrity),
|
|
139
197
|
wasPinned: ref.integrity != null,
|
|
198
|
+
repinned: integrity != null,
|
|
140
199
|
keyRange: entry.keyRange,
|
|
141
|
-
edits: buildEdits(entry,
|
|
200
|
+
edits: buildEdits(entry, newSource, integrity),
|
|
142
201
|
});
|
|
143
202
|
}
|
|
144
203
|
|
|
145
|
-
return { importsKeyRange: block.keyRange, upgrades, skipped, failures };
|
|
204
|
+
return { importsKeyRange: block.keyRange, upgrades, pins, skipped, failures };
|
|
146
205
|
}
|
|
147
206
|
|
|
148
|
-
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
|
|
207
|
+
/** The edits that re-point an entry at `newSource` and settle its pin.
|
|
208
|
+
*
|
|
209
|
+
* Where the pin is written follows the shape the author chose: an entry with an
|
|
210
|
+
* `integrity:` sibling keeps it (its value is replaced in place, which works in
|
|
211
|
+
* block and flow style alike), and everything else carries the pin inside the
|
|
212
|
+
* source as a `#sha256-…` fragment — the form `telo upgrade` writes.
|
|
213
|
+
*
|
|
214
|
+
* With no pin available the sibling is deleted instead. That is not optional:
|
|
215
|
+
* it hashes the `telo.yaml` of the version being replaced, so carrying it onto
|
|
216
|
+
* a different version turns the next install into a tamper error.
|
|
217
|
+
* `withRefVersion` has already stripped a fragment-form pin for the same
|
|
218
|
+
* reason, so the scalar shorthand needs no second edit. */
|
|
219
|
+
function buildEdits(
|
|
220
|
+
entry: ImportEntry,
|
|
221
|
+
newSource: string,
|
|
222
|
+
integrity: string | undefined,
|
|
223
|
+
): ImportUpgradeEdit[] {
|
|
224
|
+
if (!entry.integrity) {
|
|
225
|
+
return [{ range: entry.sourceRange, newText: foldIntegrity(newSource, integrity) }];
|
|
226
|
+
}
|
|
227
|
+
|
|
154
228
|
const edits: ImportUpgradeEdit[] = [{ range: entry.sourceRange, newText: newSource }];
|
|
155
|
-
if (
|
|
156
|
-
edits.push({ range: entry.
|
|
229
|
+
if (integrity) {
|
|
230
|
+
edits.push({ range: entry.integrity.valueRange, newText: integrity });
|
|
231
|
+
} else if (entry.integrity.lineRange) {
|
|
232
|
+
edits.push({ range: entry.integrity.lineRange, newText: "" });
|
|
157
233
|
}
|
|
158
234
|
return edits;
|
|
159
235
|
}
|
|
160
236
|
|
|
161
|
-
/**
|
|
162
|
-
* is
|
|
237
|
+
/** Pin an entry that is already at the newest version, or `undefined` when
|
|
238
|
+
* there is nothing to do — it is pinned already, the hub published no pin for
|
|
239
|
+
* the version it names, or that version is not one this module will order.
|
|
240
|
+
*
|
|
241
|
+
* The version gate matters: a moving tag (`latest`) or a digest names bytes
|
|
242
|
+
* that are expected to change, so pinning it would break the next release
|
|
243
|
+
* rather than protect it. `telo upgrade` draws the same line. */
|
|
244
|
+
function pinInPlace(
|
|
245
|
+
entry: ImportEntry,
|
|
246
|
+
version: string,
|
|
247
|
+
existingIntegrity: string | undefined,
|
|
248
|
+
versions: ModuleVersion[],
|
|
249
|
+
): ImportPin | undefined {
|
|
250
|
+
if (existingIntegrity != null || entry.integrity) return undefined;
|
|
251
|
+
if (parseModuleVersion(version) === null) return undefined;
|
|
252
|
+
const integrity = integrityFor(versions, version);
|
|
253
|
+
if (!integrity) return undefined;
|
|
254
|
+
|
|
255
|
+
const newSource = foldIntegrity(entry.source, integrity);
|
|
256
|
+
return {
|
|
257
|
+
alias: entry.alias,
|
|
258
|
+
source: entry.source,
|
|
259
|
+
version,
|
|
260
|
+
newSource,
|
|
261
|
+
keyRange: entry.keyRange,
|
|
262
|
+
edits: [{ range: entry.sourceRange, newText: newSource }],
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The pin the hub reports for one version, matched by SemVer identity rather
|
|
267
|
+
* than string equality so a `v`-prefixed tag on either side still lines up.
|
|
268
|
+
*
|
|
269
|
+
* Re-checked here even though the host's parse already did: this value is
|
|
270
|
+
* spliced into the author's YAML, and a caller reaching the builder through
|
|
271
|
+
* its own `ModuleVersionLookup` never passed through that parse. */
|
|
272
|
+
function integrityFor(versions: ModuleVersion[], version: string): string | undefined {
|
|
273
|
+
const integrity = versions.find((v) => isSameModuleVersion(v.version, version))?.integrity;
|
|
274
|
+
return isCanonicalIntegrity(integrity) ? integrity : undefined;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Versions per base ref, fetched once each. A ref whose lookup rejects is
|
|
278
|
+
* recorded in `failures` and left out of the map, so it yields no upgrade
|
|
163
279
|
* rather than a wrong one. */
|
|
164
|
-
async function
|
|
280
|
+
async function resolveVersions(
|
|
165
281
|
baseRefs: string[],
|
|
166
282
|
listVersions: ModuleVersionLookup,
|
|
167
283
|
failures: Array<{ baseRef: string; message: string }>,
|
|
168
|
-
): Promise<Map<string,
|
|
284
|
+
): Promise<Map<string, ModuleVersion[]>> {
|
|
169
285
|
const results = await Promise.all(
|
|
170
286
|
baseRefs.map(async (baseRef) => {
|
|
171
287
|
try {
|
|
172
|
-
|
|
173
|
-
return { baseRef, newest: newestModuleVersion(versions) };
|
|
288
|
+
return { baseRef, versions: await listVersions(baseRef) };
|
|
174
289
|
} catch (err) {
|
|
175
290
|
failures.push({
|
|
176
291
|
baseRef,
|
|
177
292
|
message: err instanceof Error ? err.message : String(err),
|
|
178
293
|
});
|
|
179
|
-
return { baseRef,
|
|
294
|
+
return { baseRef, versions: undefined };
|
|
180
295
|
}
|
|
181
296
|
}),
|
|
182
297
|
);
|
|
183
298
|
|
|
184
|
-
const map = new Map<string,
|
|
185
|
-
for (const { baseRef,
|
|
186
|
-
if (
|
|
299
|
+
const map = new Map<string, ModuleVersion[]>();
|
|
300
|
+
for (const { baseRef, versions } of results) {
|
|
301
|
+
if (versions) map.set(baseRef, versions);
|
|
187
302
|
}
|
|
188
303
|
return map;
|
|
189
304
|
}
|
|
190
|
-
|
|
@@ -24,17 +24,24 @@ export interface ImportEntry {
|
|
|
24
24
|
* shorthand, the `source:` value for the object form. Replacing this span
|
|
25
25
|
* re-points the import. */
|
|
26
26
|
sourceRange: Range;
|
|
27
|
-
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
27
|
+
/** Where an object-form `integrity:` sibling sits, when the entry declares
|
|
28
|
+
* one. Absent for the scalar shorthand — there the pin rides inside
|
|
29
|
+
* `sourceRange` as a `#sha256-…` fragment — and for an unpinned entry. */
|
|
30
|
+
integrity?: ImportEntryIntegrity;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The two spans a caller needs to act on an object-form `integrity:`: one to
|
|
34
|
+
* re-pin it, one to remove it. They are separate because a flow-style map
|
|
35
|
+
* (`{source: …, integrity: …}`) supports the first and not the second. */
|
|
36
|
+
export interface ImportEntryIntegrity {
|
|
37
|
+
/** Span of the hash value itself. Replacing it re-pins the entry in the shape
|
|
38
|
+
* the author wrote, in any YAML style. */
|
|
39
|
+
valueRange: Range;
|
|
40
|
+
/** Whole-line span of the `integrity:` entry including its trailing newline,
|
|
41
|
+
* so a caller with no replacement hash can delete the line outright. Absent
|
|
42
|
+
* when the pair shares a line with other content, where removing it would
|
|
43
|
+
* need a structural rewrite rather than a line splice. */
|
|
44
|
+
lineRange?: Range;
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
/** Where the `imports:` map lives in a module document. */
|
|
@@ -116,14 +123,15 @@ function readEntry(
|
|
|
116
123
|
};
|
|
117
124
|
|
|
118
125
|
if (integrityPair?.value && integrity !== undefined) {
|
|
119
|
-
|
|
120
|
-
integrityPair.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
entry.integrity = {
|
|
127
|
+
valueRange: toRange(integrityPair.value, lineOffsets),
|
|
128
|
+
lineRange: wholeLineSpan(
|
|
129
|
+
integrityPair.key.range[0],
|
|
130
|
+
integrityPair.value.range[1],
|
|
131
|
+
text,
|
|
132
|
+
lineOffsets,
|
|
133
|
+
),
|
|
134
|
+
};
|
|
127
135
|
}
|
|
128
136
|
|
|
129
137
|
return entry;
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
export { buildImportUpgrades } from "./build-import-upgrades.js";
|
|
2
2
|
export type {
|
|
3
|
+
ImportPin,
|
|
3
4
|
ImportUpgrade,
|
|
4
5
|
ImportUpgradeEdit,
|
|
5
6
|
ImportUpgradeSet,
|
|
6
7
|
ImportUpgradeSkip,
|
|
8
|
+
ModuleVersion,
|
|
7
9
|
ModuleVersionLookup,
|
|
8
10
|
} from "./build-import-upgrades.js";
|
|
11
|
+
export { parseModuleVersions } from "./parse-module-versions.js";
|
|
9
12
|
export { findImportEntries } from "./find-import-entries.js";
|
|
10
|
-
export type {
|
|
13
|
+
export type {
|
|
14
|
+
ImportEntry,
|
|
15
|
+
ImportEntryIntegrity,
|
|
16
|
+
ImportsBlock,
|
|
17
|
+
} from "./find-import-entries.js";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { isCanonicalIntegrity } from "@telorun/analyzer";
|
|
2
|
+
import type { ModuleVersion } from "./build-import-upgrades.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Read the hub's `GET /module/versions` payload into {@link ModuleVersion}s,
|
|
6
|
+
* newest first (the route's own ordering, preserved).
|
|
7
|
+
*
|
|
8
|
+
* Pure — the host owns the `fetch`, as everything else in this package does.
|
|
9
|
+
* It lives here rather than in each host because the shape is this package's:
|
|
10
|
+
* three hosts parsing the same route by hand is how one of them was left
|
|
11
|
+
* filtering for strings after the route started returning objects, which
|
|
12
|
+
* TypeScript could not catch behind the response cast and which surfaced only
|
|
13
|
+
* as a silently empty version list.
|
|
14
|
+
*
|
|
15
|
+
* Tolerant by design: an entry with no usable `version` is dropped rather than
|
|
16
|
+
* throwing, and an `integrity` that is not a canonical `sha256-<base64url>` is
|
|
17
|
+
* discarded rather than carried — a pin is spliced into the author's YAML, so a
|
|
18
|
+
* malformed one has to become "no pin", never corrupt text.
|
|
19
|
+
*/
|
|
20
|
+
export function parseModuleVersions(payload: unknown): ModuleVersion[] {
|
|
21
|
+
const versions = (payload as { versions?: unknown } | null | undefined)?.versions;
|
|
22
|
+
if (!Array.isArray(versions)) return [];
|
|
23
|
+
|
|
24
|
+
return versions.flatMap((entry) => {
|
|
25
|
+
const version = (entry as { version?: unknown } | null)?.version;
|
|
26
|
+
if (typeof version !== "string" || version === "") return [];
|
|
27
|
+
const integrity = (entry as { integrity?: unknown }).integrity;
|
|
28
|
+
return [isCanonicalIntegrity(integrity) ? { version, integrity } : { version }];
|
|
29
|
+
});
|
|
30
|
+
}
|