@telorun/analyzer 0.44.0 → 0.45.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/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/module-version-order.d.ts +36 -0
- package/dist/module-version-order.d.ts.map +1 -0
- package/dist/module-version-order.js +91 -0
- package/dist/reconcile-module-versions.d.ts.map +1 -1
- package/dist/reconcile-module-versions.js +3 -65
- package/dist/sources/versioned-ref.d.ts +34 -0
- package/dist/sources/versioned-ref.d.ts.map +1 -0
- package/dist/sources/versioned-ref.js +60 -0
- package/package.json +1 -1
- package/src/index.ts +10 -0
- package/src/module-version-order.ts +91 -0
- package/src/reconcile-module-versions.ts +11 -61
- package/src/sources/versioned-ref.ts +77 -0
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export { parseLoadedFile } from "./parse-loaded-file.js";
|
|
|
20
20
|
export type { ParseOptions } from "./parse-loaded-file.js";
|
|
21
21
|
export { desugarLoadedFile, inlineImportManifests } from "./inline-imports.js";
|
|
22
22
|
export type { SyntheticImport } from "./inline-imports.js";
|
|
23
|
+
export { compareModuleVersions, compareParsedModuleVersions, isNewerModuleVersion, isSameModuleVersion, parseModuleVersion, } from "./module-version-order.js";
|
|
24
|
+
export type { ParsedModuleVersion } from "./module-version-order.js";
|
|
23
25
|
export { reconcileModuleVersions } from "./reconcile-module-versions.js";
|
|
24
26
|
export type { VersionReconciliation } from "./reconcile-module-versions.js";
|
|
25
27
|
export { residualEntrySchema, residualEntrySchemaMap } from "./residual-schema.js";
|
|
@@ -33,6 +35,8 @@ export { parseModuleRef, isRegistryRef } from "./sources/module-ref.js";
|
|
|
33
35
|
export type { ParsedModuleRef } from "./sources/module-ref.js";
|
|
34
36
|
export { OCI_SCHEME, isOciRef, parseOciRef } from "./sources/oci-ref.js";
|
|
35
37
|
export type { ParsedOciRef } from "./sources/oci-ref.js";
|
|
38
|
+
export { parseVersionedRef, withRefVersion } from "./sources/versioned-ref.js";
|
|
39
|
+
export type { ParsedVersionedRef } from "./sources/versioned-ref.js";
|
|
36
40
|
export { isLocalPathSource } from "./sources/local-path-ref.js";
|
|
37
41
|
export { MANIFEST_CACHE_BASE_URL, ManifestCacheSource, isHttpsModuleRef, manifestCacheKey, manifestCacheUrl, ociManifestCacheCoords, urlManifestCacheCoords, } from "./sources/manifest-cache.js";
|
|
38
42
|
export type { ManifestCacheCoords } from "./sources/manifest-cache.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ export { Loader } from "./manifest-loader.js";
|
|
|
11
11
|
export { isModuleKind, MODULE_KINDS } from "./module-kinds.js";
|
|
12
12
|
export { parseLoadedFile } from "./parse-loaded-file.js";
|
|
13
13
|
export { desugarLoadedFile, inlineImportManifests } from "./inline-imports.js";
|
|
14
|
+
export { compareModuleVersions, compareParsedModuleVersions, isNewerModuleVersion, isSameModuleVersion, parseModuleVersion, } from "./module-version-order.js";
|
|
14
15
|
export { reconcileModuleVersions } from "./reconcile-module-versions.js";
|
|
15
16
|
export { residualEntrySchema, residualEntrySchemaMap } from "./residual-schema.js";
|
|
16
17
|
export { buildDocumentPositions, buildLineOffsets, buildPositionIndex, documentLineOffsets, offsetToPosition, } from "./position-metadata.js";
|
|
@@ -20,6 +21,7 @@ export { defaultSources } from "./sources/default-sources.js";
|
|
|
20
21
|
export { splitIntegrity, foldIntegrity, verifyIntegrity, verifiedFetch, sha256Base64Url, IntegrityError, } from "./sources/integrity.js";
|
|
21
22
|
export { parseModuleRef, isRegistryRef } from "./sources/module-ref.js";
|
|
22
23
|
export { OCI_SCHEME, isOciRef, parseOciRef } from "./sources/oci-ref.js";
|
|
24
|
+
export { parseVersionedRef, withRefVersion } from "./sources/versioned-ref.js";
|
|
23
25
|
export { isLocalPathSource } from "./sources/local-path-ref.js";
|
|
24
26
|
export { MANIFEST_CACHE_BASE_URL, ManifestCacheSource, isHttpsModuleRef, manifestCacheKey, manifestCacheUrl, ociManifestCacheCoords, urlManifestCacheCoords, } from "./sources/manifest-cache.js";
|
|
25
27
|
export { withSyntheticPositions } from "./with-synthetic-positions.js";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** SemVer precedence for module versions — the single ordering rule shared by
|
|
2
|
+
* version reconciliation and by any host deciding whether an import is behind.
|
|
3
|
+
*
|
|
4
|
+
* Pure and dependency-free (no `semver` package), so the analyzer stays
|
|
5
|
+
* browser-safe and the editor can reach the same rule the kernel-side analysis
|
|
6
|
+
* uses. A caller that cannot parse a version must not guess: an OCI digest, a
|
|
7
|
+
* moving tag like `latest`, and a malformed pin all come back `null` rather
|
|
8
|
+
* than being ordered by some weaker fallback. */
|
|
9
|
+
export interface ParsedModuleVersion {
|
|
10
|
+
major: number;
|
|
11
|
+
minor: number;
|
|
12
|
+
patch: number;
|
|
13
|
+
/** Dot-separated prerelease identifiers, or `null` for a release version. */
|
|
14
|
+
pre: string[] | null;
|
|
15
|
+
}
|
|
16
|
+
/** Parse `X.Y.Z`, `vX.Y.Z`, or `X.Y.Z-pre.1`. Returns `null` for anything that
|
|
17
|
+
* isn't a plain three-part numeric core — an unparseable version is never
|
|
18
|
+
* silently ordered. */
|
|
19
|
+
export declare function parseModuleVersion(raw: string | undefined): ParsedModuleVersion | null;
|
|
20
|
+
/** SemVer precedence: numeric core, then a release outranks a prerelease, then
|
|
21
|
+
* prerelease identifiers compared field-by-field (numeric < non-numeric per
|
|
22
|
+
* spec, shorter set loses when all shared fields are equal). */
|
|
23
|
+
export declare function compareParsedModuleVersions(a: ParsedModuleVersion, b: ParsedModuleVersion): number;
|
|
24
|
+
/** Negative / zero / positive when both versions parse, `null` when either does
|
|
25
|
+
* not. The string-in convenience over {@link compareParsedModuleVersions}. */
|
|
26
|
+
export declare function compareModuleVersions(a: string, b: string): number | null;
|
|
27
|
+
/** True when `candidate` is strictly newer than `current` — the test for
|
|
28
|
+
* whether an import is behind. False when they are equal, when `current` is
|
|
29
|
+
* ahead (a version index can lag the module's own origin, and "upgrading" to
|
|
30
|
+
* what it knows would be a downgrade), or when either side is unparseable. */
|
|
31
|
+
export declare function isNewerModuleVersion(candidate: string, current: string): boolean;
|
|
32
|
+
/** True when two tags name the same version, tolerating a `v` prefix on either
|
|
33
|
+
* side. Falls back to exact equality for unparseable tags, so a digest still
|
|
34
|
+
* matches itself. */
|
|
35
|
+
export declare function isSameModuleVersion(a: string, b: string): boolean;
|
|
36
|
+
//# sourceMappingURL=module-version-order.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module-version-order.d.ts","sourceRoot":"","sources":["../src/module-version-order.ts"],"names":[],"mappings":"AAAA;;;;;;;kDAOkD;AAElD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CACtB;AAED;;wBAEwB;AACxB,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,mBAAmB,GAAG,IAAI,CAatF;AAED;;iEAEiE;AACjE,wBAAgB,2BAA2B,CACzC,CAAC,EAAE,mBAAmB,EACtB,CAAC,EAAE,mBAAmB,GACrB,MAAM,CAyBR;AAED;+EAC+E;AAC/E,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKzE;AAED;;;+EAG+E;AAC/E,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAEhF;AAED;;sBAEsB;AACtB,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAEjE"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** SemVer precedence for module versions — the single ordering rule shared by
|
|
2
|
+
* version reconciliation and by any host deciding whether an import is behind.
|
|
3
|
+
*
|
|
4
|
+
* Pure and dependency-free (no `semver` package), so the analyzer stays
|
|
5
|
+
* browser-safe and the editor can reach the same rule the kernel-side analysis
|
|
6
|
+
* uses. A caller that cannot parse a version must not guess: an OCI digest, a
|
|
7
|
+
* moving tag like `latest`, and a malformed pin all come back `null` rather
|
|
8
|
+
* than being ordered by some weaker fallback. */
|
|
9
|
+
/** Parse `X.Y.Z`, `vX.Y.Z`, or `X.Y.Z-pre.1`. Returns `null` for anything that
|
|
10
|
+
* isn't a plain three-part numeric core — an unparseable version is never
|
|
11
|
+
* silently ordered. */
|
|
12
|
+
export function parseModuleVersion(raw) {
|
|
13
|
+
if (typeof raw !== "string")
|
|
14
|
+
return null;
|
|
15
|
+
const v = raw.startsWith("v") ? raw.slice(1) : raw;
|
|
16
|
+
const [core, ...preParts] = v.split("-");
|
|
17
|
+
const pre = preParts.length > 0 ? preParts.join("-") : null;
|
|
18
|
+
const segments = core.split(".");
|
|
19
|
+
if (segments.length !== 3)
|
|
20
|
+
return null;
|
|
21
|
+
const [major, minor, patch] = segments.map((s) => {
|
|
22
|
+
if (!/^\d+$/.test(s))
|
|
23
|
+
return NaN;
|
|
24
|
+
return Number(s);
|
|
25
|
+
});
|
|
26
|
+
if ([major, minor, patch].some((n) => Number.isNaN(n)))
|
|
27
|
+
return null;
|
|
28
|
+
return { major, minor, patch, pre: pre === null ? null : pre.split(".") };
|
|
29
|
+
}
|
|
30
|
+
/** SemVer precedence: numeric core, then a release outranks a prerelease, then
|
|
31
|
+
* prerelease identifiers compared field-by-field (numeric < non-numeric per
|
|
32
|
+
* spec, shorter set loses when all shared fields are equal). */
|
|
33
|
+
export function compareParsedModuleVersions(a, b) {
|
|
34
|
+
if (a.major !== b.major)
|
|
35
|
+
return a.major - b.major;
|
|
36
|
+
if (a.minor !== b.minor)
|
|
37
|
+
return a.minor - b.minor;
|
|
38
|
+
if (a.patch !== b.patch)
|
|
39
|
+
return a.patch - b.patch;
|
|
40
|
+
if (a.pre === null && b.pre === null)
|
|
41
|
+
return 0;
|
|
42
|
+
if (a.pre === null)
|
|
43
|
+
return 1;
|
|
44
|
+
if (b.pre === null)
|
|
45
|
+
return -1;
|
|
46
|
+
const len = Math.max(a.pre.length, b.pre.length);
|
|
47
|
+
for (let i = 0; i < len; i++) {
|
|
48
|
+
const ai = a.pre[i];
|
|
49
|
+
const bi = b.pre[i];
|
|
50
|
+
if (ai === undefined)
|
|
51
|
+
return -1;
|
|
52
|
+
if (bi === undefined)
|
|
53
|
+
return 1;
|
|
54
|
+
const an = /^\d+$/.test(ai);
|
|
55
|
+
const bn = /^\d+$/.test(bi);
|
|
56
|
+
if (an && bn) {
|
|
57
|
+
const d = Number(ai) - Number(bi);
|
|
58
|
+
if (d !== 0)
|
|
59
|
+
return d;
|
|
60
|
+
}
|
|
61
|
+
else if (an !== bn) {
|
|
62
|
+
return an ? -1 : 1;
|
|
63
|
+
}
|
|
64
|
+
else if (ai !== bi) {
|
|
65
|
+
return ai < bi ? -1 : 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
/** Negative / zero / positive when both versions parse, `null` when either does
|
|
71
|
+
* not. The string-in convenience over {@link compareParsedModuleVersions}. */
|
|
72
|
+
export function compareModuleVersions(a, b) {
|
|
73
|
+
const left = parseModuleVersion(a);
|
|
74
|
+
const right = parseModuleVersion(b);
|
|
75
|
+
if (!left || !right)
|
|
76
|
+
return null;
|
|
77
|
+
return compareParsedModuleVersions(left, right);
|
|
78
|
+
}
|
|
79
|
+
/** True when `candidate` is strictly newer than `current` — the test for
|
|
80
|
+
* whether an import is behind. False when they are equal, when `current` is
|
|
81
|
+
* ahead (a version index can lag the module's own origin, and "upgrading" to
|
|
82
|
+
* what it knows would be a downgrade), or when either side is unparseable. */
|
|
83
|
+
export function isNewerModuleVersion(candidate, current) {
|
|
84
|
+
return (compareModuleVersions(candidate, current) ?? 0) > 0;
|
|
85
|
+
}
|
|
86
|
+
/** True when two tags name the same version, tolerating a `v` prefix on either
|
|
87
|
+
* side. Falls back to exact equality for unparseable tags, so a digest still
|
|
88
|
+
* matches itself. */
|
|
89
|
+
export function isSameModuleVersion(a, b) {
|
|
90
|
+
return a === b || compareModuleVersions(a, b) === 0;
|
|
91
|
+
}
|
|
@@ -1 +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;
|
|
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;AAOlE,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;AAgJD;;;;;;;;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,CAsEvB"}
|
|
@@ -1,69 +1,7 @@
|
|
|
1
1
|
import { isModuleKind } from "./module-kinds.js";
|
|
2
|
+
import { compareParsedModuleVersions, parseModuleVersion, } from "./module-version-order.js";
|
|
2
3
|
import { DiagnosticSeverity } from "./types.js";
|
|
3
4
|
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
5
|
/** The location identity of an import ref: the ref with its version stripped.
|
|
68
6
|
* Two refs share an identity when they address the same module at different
|
|
69
7
|
* versions, whatever transport owns them:
|
|
@@ -115,7 +53,7 @@ function moduleIdentityOf(mod, identity) {
|
|
|
115
53
|
source: mod.owner.source,
|
|
116
54
|
identity,
|
|
117
55
|
version,
|
|
118
|
-
parsed:
|
|
56
|
+
parsed: parseModuleVersion(version),
|
|
119
57
|
text: mod.owner.text,
|
|
120
58
|
};
|
|
121
59
|
}
|
|
@@ -135,7 +73,7 @@ function resolveGroup(members) {
|
|
|
135
73
|
return best;
|
|
136
74
|
if (!best.parsed)
|
|
137
75
|
return cur;
|
|
138
|
-
const cmp =
|
|
76
|
+
const cmp = compareParsedModuleVersions(cur.parsed, best.parsed);
|
|
139
77
|
if (cmp > 0)
|
|
140
78
|
return cur;
|
|
141
79
|
if (cmp === 0 && cur.source < best.source)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** A module ref split into the parts an upgrade needs: the version-independent
|
|
2
|
+
* ref, the version segment it currently names, and any inline pin. */
|
|
3
|
+
export interface ParsedVersionedRef {
|
|
4
|
+
/** The ref with its `@version` segment and integrity fragment removed —
|
|
5
|
+
* `std/run`, `oci://ghcr.io/telorun/timer`. This is the identity a version
|
|
6
|
+
* list is keyed by (the hub registers modules under exactly this form). */
|
|
7
|
+
baseRef: string;
|
|
8
|
+
/** The version segment, raw — a registry `@version`, an OCI tag, or an OCI
|
|
9
|
+
* digest reference (`sha256:…`). The caller applies its own SemVer check. */
|
|
10
|
+
version: string;
|
|
11
|
+
/** Telo's inline `sha256-<base64url>` pin, when the ref carried one. */
|
|
12
|
+
integrity?: string;
|
|
13
|
+
}
|
|
14
|
+
/** Split a versioned module ref. Returns `null` when the ref names no
|
|
15
|
+
* upgradeable version — a local path, a bare `https://` URL, or an OCI ref
|
|
16
|
+
* with no explicit reference (an implicit `latest` is not a pin).
|
|
17
|
+
*
|
|
18
|
+
* Browser-safe and transport-neutral: this is the *grammar* half of an
|
|
19
|
+
* upgrade, shared by the kernel transports (whose `refVersion` / `withVersion`
|
|
20
|
+
* delegate here) and the editor, which cannot use a transport at all — the
|
|
21
|
+
* *network* half (enumerating versions) is scheme-specific and stays behind
|
|
22
|
+
* `Transport.listVersions` on Node and the hub's `/module/versions` in the
|
|
23
|
+
* browser. */
|
|
24
|
+
export declare function parseVersionedRef(ref: string): ParsedVersionedRef | null;
|
|
25
|
+
/** `ref` rewritten to name `version`, dropping any integrity fragment (the
|
|
26
|
+
* caller re-pins the result). Appends the version when the ref carries none —
|
|
27
|
+
* an untagged `oci://host/repo` is still a versionable address.
|
|
28
|
+
*
|
|
29
|
+
* Throws when the ref's grammar has no version segment at all — a relative
|
|
30
|
+
* path, a bare `https://` URL. Producing `../lib@0.4.0` for those would write
|
|
31
|
+
* a ref nothing can resolve, so this fails where the transport-specific
|
|
32
|
+
* parsers it replaced (`parseOciRef` / `parseModuleRef`) also failed. */
|
|
33
|
+
export declare function withRefVersion(ref: string, version: string): string;
|
|
34
|
+
//# sourceMappingURL=versioned-ref.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"versioned-ref.d.ts","sourceRoot":"","sources":["../../src/sources/versioned-ref.ts"],"names":[],"mappings":"AAIA;uEACuE;AACvE,MAAM,WAAW,kBAAkB;IACjC;;gFAE4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB;kFAC8E;IAC9E,OAAO,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;eASe;AACf,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAOxE;AAED;;;;;;;0EAO0E;AAC1E,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAUnE"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { splitIntegrity } from "./integrity.js";
|
|
2
|
+
import { isRegistryRef } from "./module-ref.js";
|
|
3
|
+
import { OCI_SCHEME } from "./oci-ref.js";
|
|
4
|
+
/** Split a versioned module ref. Returns `null` when the ref names no
|
|
5
|
+
* upgradeable version — a local path, a bare `https://` URL, or an OCI ref
|
|
6
|
+
* with no explicit reference (an implicit `latest` is not a pin).
|
|
7
|
+
*
|
|
8
|
+
* Browser-safe and transport-neutral: this is the *grammar* half of an
|
|
9
|
+
* upgrade, shared by the kernel transports (whose `refVersion` / `withVersion`
|
|
10
|
+
* delegate here) and the editor, which cannot use a transport at all — the
|
|
11
|
+
* *network* half (enumerating versions) is scheme-specific and stays behind
|
|
12
|
+
* `Transport.listVersions` on Node and the hub's `/module/versions` in the
|
|
13
|
+
* browser. */
|
|
14
|
+
export function parseVersionedRef(ref) {
|
|
15
|
+
const { base, integrity } = splitIntegrity(ref);
|
|
16
|
+
const at = versionSeparator(base);
|
|
17
|
+
if (at === null)
|
|
18
|
+
return null;
|
|
19
|
+
const version = base.slice(at + 1);
|
|
20
|
+
if (!version)
|
|
21
|
+
return null;
|
|
22
|
+
return { baseRef: base.slice(0, at), version, integrity };
|
|
23
|
+
}
|
|
24
|
+
/** `ref` rewritten to name `version`, dropping any integrity fragment (the
|
|
25
|
+
* caller re-pins the result). Appends the version when the ref carries none —
|
|
26
|
+
* an untagged `oci://host/repo` is still a versionable address.
|
|
27
|
+
*
|
|
28
|
+
* Throws when the ref's grammar has no version segment at all — a relative
|
|
29
|
+
* path, a bare `https://` URL. Producing `../lib@0.4.0` for those would write
|
|
30
|
+
* a ref nothing can resolve, so this fails where the transport-specific
|
|
31
|
+
* parsers it replaced (`parseOciRef` / `parseModuleRef`) also failed. */
|
|
32
|
+
export function withRefVersion(ref, version) {
|
|
33
|
+
const { base } = splitIntegrity(ref);
|
|
34
|
+
if (refGrammar(base) === null) {
|
|
35
|
+
throw new Error(`Cannot set a version on '${ref}' — only registry (namespace/name@version) ` +
|
|
36
|
+
`and oci:// refs carry a version segment.`);
|
|
37
|
+
}
|
|
38
|
+
const at = versionSeparator(base);
|
|
39
|
+
return `${at === null ? base : base.slice(0, at)}@${version}`;
|
|
40
|
+
}
|
|
41
|
+
/** Which versionable ref grammar `base` is written in, or `null` when it is
|
|
42
|
+
* neither — a relative/absolute path, a `file:`/`https://` URL. */
|
|
43
|
+
function refGrammar(base) {
|
|
44
|
+
if (base.startsWith(OCI_SCHEME)) {
|
|
45
|
+
// A host alone is not addressable; the repo path is what carries a version.
|
|
46
|
+
return base.indexOf("/", OCI_SCHEME.length) > OCI_SCHEME.length ? "oci" : null;
|
|
47
|
+
}
|
|
48
|
+
// `isRegistryRef` requires the `@`, so a version-less `std/console` is not a
|
|
49
|
+
// registry ref by this test — matching `parseModuleRef`, which throws on it.
|
|
50
|
+
return isRegistryRef(base) ? "registry" : null;
|
|
51
|
+
}
|
|
52
|
+
/** Index of the `@` that separates the version, or `null` when the ref names
|
|
53
|
+
* none. Split on the LAST `@` so a digest reference (`repo@sha256:…`) keeps
|
|
54
|
+
* everything before it as the ref. */
|
|
55
|
+
function versionSeparator(base) {
|
|
56
|
+
if (refGrammar(base) === null)
|
|
57
|
+
return null;
|
|
58
|
+
const at = base.lastIndexOf("@");
|
|
59
|
+
return at > (base.startsWith(OCI_SCHEME) ? OCI_SCHEME.length : 0) ? at : null;
|
|
60
|
+
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -60,6 +60,14 @@ export { parseLoadedFile } from "./parse-loaded-file.js";
|
|
|
60
60
|
export type { ParseOptions } from "./parse-loaded-file.js";
|
|
61
61
|
export { desugarLoadedFile, inlineImportManifests } from "./inline-imports.js";
|
|
62
62
|
export type { SyntheticImport } from "./inline-imports.js";
|
|
63
|
+
export {
|
|
64
|
+
compareModuleVersions,
|
|
65
|
+
compareParsedModuleVersions,
|
|
66
|
+
isNewerModuleVersion,
|
|
67
|
+
isSameModuleVersion,
|
|
68
|
+
parseModuleVersion,
|
|
69
|
+
} from "./module-version-order.js";
|
|
70
|
+
export type { ParsedModuleVersion } from "./module-version-order.js";
|
|
63
71
|
export { reconcileModuleVersions } from "./reconcile-module-versions.js";
|
|
64
72
|
export type { VersionReconciliation } from "./reconcile-module-versions.js";
|
|
65
73
|
export { residualEntrySchema, residualEntrySchemaMap } from "./residual-schema.js";
|
|
@@ -86,6 +94,8 @@ export { parseModuleRef, isRegistryRef } from "./sources/module-ref.js";
|
|
|
86
94
|
export type { ParsedModuleRef } from "./sources/module-ref.js";
|
|
87
95
|
export { OCI_SCHEME, isOciRef, parseOciRef } from "./sources/oci-ref.js";
|
|
88
96
|
export type { ParsedOciRef } from "./sources/oci-ref.js";
|
|
97
|
+
export { parseVersionedRef, withRefVersion } from "./sources/versioned-ref.js";
|
|
98
|
+
export type { ParsedVersionedRef } from "./sources/versioned-ref.js";
|
|
89
99
|
export { isLocalPathSource } from "./sources/local-path-ref.js";
|
|
90
100
|
export {
|
|
91
101
|
MANIFEST_CACHE_BASE_URL,
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** SemVer precedence for module versions — the single ordering rule shared by
|
|
2
|
+
* version reconciliation and by any host deciding whether an import is behind.
|
|
3
|
+
*
|
|
4
|
+
* Pure and dependency-free (no `semver` package), so the analyzer stays
|
|
5
|
+
* browser-safe and the editor can reach the same rule the kernel-side analysis
|
|
6
|
+
* uses. A caller that cannot parse a version must not guess: an OCI digest, a
|
|
7
|
+
* moving tag like `latest`, and a malformed pin all come back `null` rather
|
|
8
|
+
* than being ordered by some weaker fallback. */
|
|
9
|
+
|
|
10
|
+
export interface ParsedModuleVersion {
|
|
11
|
+
major: number;
|
|
12
|
+
minor: number;
|
|
13
|
+
patch: number;
|
|
14
|
+
/** Dot-separated prerelease identifiers, or `null` for a release version. */
|
|
15
|
+
pre: string[] | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Parse `X.Y.Z`, `vX.Y.Z`, or `X.Y.Z-pre.1`. Returns `null` for anything that
|
|
19
|
+
* isn't a plain three-part numeric core — an unparseable version is never
|
|
20
|
+
* silently ordered. */
|
|
21
|
+
export function parseModuleVersion(raw: string | undefined): ParsedModuleVersion | null {
|
|
22
|
+
if (typeof raw !== "string") return null;
|
|
23
|
+
const v = raw.startsWith("v") ? raw.slice(1) : raw;
|
|
24
|
+
const [core, ...preParts] = v.split("-");
|
|
25
|
+
const pre = preParts.length > 0 ? preParts.join("-") : null;
|
|
26
|
+
const segments = core.split(".");
|
|
27
|
+
if (segments.length !== 3) return null;
|
|
28
|
+
const [major, minor, patch] = segments.map((s) => {
|
|
29
|
+
if (!/^\d+$/.test(s)) return NaN;
|
|
30
|
+
return Number(s);
|
|
31
|
+
});
|
|
32
|
+
if ([major, minor, patch].some((n) => Number.isNaN(n))) return null;
|
|
33
|
+
return { major, minor, patch, pre: pre === null ? null : pre.split(".") };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** SemVer precedence: numeric core, then a release outranks a prerelease, then
|
|
37
|
+
* prerelease identifiers compared field-by-field (numeric < non-numeric per
|
|
38
|
+
* spec, shorter set loses when all shared fields are equal). */
|
|
39
|
+
export function compareParsedModuleVersions(
|
|
40
|
+
a: ParsedModuleVersion,
|
|
41
|
+
b: ParsedModuleVersion,
|
|
42
|
+
): number {
|
|
43
|
+
if (a.major !== b.major) return a.major - b.major;
|
|
44
|
+
if (a.minor !== b.minor) return a.minor - b.minor;
|
|
45
|
+
if (a.patch !== b.patch) return a.patch - b.patch;
|
|
46
|
+
if (a.pre === null && b.pre === null) return 0;
|
|
47
|
+
if (a.pre === null) return 1;
|
|
48
|
+
if (b.pre === null) return -1;
|
|
49
|
+
const len = Math.max(a.pre.length, b.pre.length);
|
|
50
|
+
for (let i = 0; i < len; i++) {
|
|
51
|
+
const ai = a.pre[i];
|
|
52
|
+
const bi = b.pre[i];
|
|
53
|
+
if (ai === undefined) return -1;
|
|
54
|
+
if (bi === undefined) return 1;
|
|
55
|
+
const an = /^\d+$/.test(ai);
|
|
56
|
+
const bn = /^\d+$/.test(bi);
|
|
57
|
+
if (an && bn) {
|
|
58
|
+
const d = Number(ai) - Number(bi);
|
|
59
|
+
if (d !== 0) return d;
|
|
60
|
+
} else if (an !== bn) {
|
|
61
|
+
return an ? -1 : 1;
|
|
62
|
+
} else if (ai !== bi) {
|
|
63
|
+
return ai < bi ? -1 : 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Negative / zero / positive when both versions parse, `null` when either does
|
|
70
|
+
* not. The string-in convenience over {@link compareParsedModuleVersions}. */
|
|
71
|
+
export function compareModuleVersions(a: string, b: string): number | null {
|
|
72
|
+
const left = parseModuleVersion(a);
|
|
73
|
+
const right = parseModuleVersion(b);
|
|
74
|
+
if (!left || !right) return null;
|
|
75
|
+
return compareParsedModuleVersions(left, right);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** True when `candidate` is strictly newer than `current` — the test for
|
|
79
|
+
* whether an import is behind. False when they are equal, when `current` is
|
|
80
|
+
* ahead (a version index can lag the module's own origin, and "upgrading" to
|
|
81
|
+
* what it knows would be a downgrade), or when either side is unparseable. */
|
|
82
|
+
export function isNewerModuleVersion(candidate: string, current: string): boolean {
|
|
83
|
+
return (compareModuleVersions(candidate, current) ?? 0) > 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** True when two tags name the same version, tolerating a `v` prefix on either
|
|
87
|
+
* side. Falls back to exact equality for unparseable tags, so a digest still
|
|
88
|
+
* matches itself. */
|
|
89
|
+
export function isSameModuleVersion(a: string, b: string): boolean {
|
|
90
|
+
return a === b || compareModuleVersions(a, b) === 0;
|
|
91
|
+
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { ImportEdge, LoadedModule } from "./loaded-types.js";
|
|
2
2
|
import { isModuleKind } from "./module-kinds.js";
|
|
3
|
+
import {
|
|
4
|
+
compareParsedModuleVersions,
|
|
5
|
+
parseModuleVersion,
|
|
6
|
+
type ParsedModuleVersion,
|
|
7
|
+
} from "./module-version-order.js";
|
|
3
8
|
import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
|
|
4
9
|
|
|
5
10
|
const SOURCE = "telo-analyzer";
|
|
@@ -17,72 +22,17 @@ export interface VersionReconciliation {
|
|
|
17
22
|
diagnostics: AnalysisDiagnostic[];
|
|
18
23
|
}
|
|
19
24
|
|
|
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
25
|
interface ModuleIdentity {
|
|
29
26
|
source: string;
|
|
30
27
|
identity: string;
|
|
31
28
|
version: string;
|
|
32
|
-
|
|
29
|
+
/** `null` for an unparseable version, which forces the group onto the
|
|
30
|
+
* conflict path — we never silently hoist across a version we can't reason
|
|
31
|
+
* about. */
|
|
32
|
+
parsed: ParsedModuleVersion | null;
|
|
33
33
|
text: string;
|
|
34
34
|
}
|
|
35
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
36
|
/** The location identity of an import ref: the ref with its version stripped.
|
|
87
37
|
* Two refs share an identity when they address the same module at different
|
|
88
38
|
* versions, whatever transport owns them:
|
|
@@ -133,7 +83,7 @@ function moduleIdentityOf(mod: LoadedModule, identity: string): ModuleIdentity |
|
|
|
133
83
|
source: mod.owner.source,
|
|
134
84
|
identity,
|
|
135
85
|
version,
|
|
136
|
-
parsed:
|
|
86
|
+
parsed: parseModuleVersion(version),
|
|
137
87
|
text: mod.owner.text,
|
|
138
88
|
};
|
|
139
89
|
}
|
|
@@ -158,7 +108,7 @@ function resolveGroup(members: ModuleIdentity[]): GroupResolution {
|
|
|
158
108
|
const winner = members.reduce((best, cur) => {
|
|
159
109
|
if (!cur.parsed) return best;
|
|
160
110
|
if (!best.parsed) return cur;
|
|
161
|
-
const cmp =
|
|
111
|
+
const cmp = compareParsedModuleVersions(cur.parsed, best.parsed);
|
|
162
112
|
if (cmp > 0) return cur;
|
|
163
113
|
if (cmp === 0 && cur.source < best.source) return cur;
|
|
164
114
|
return best;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { splitIntegrity } from "./integrity.js";
|
|
2
|
+
import { isRegistryRef } from "./module-ref.js";
|
|
3
|
+
import { OCI_SCHEME } from "./oci-ref.js";
|
|
4
|
+
|
|
5
|
+
/** A module ref split into the parts an upgrade needs: the version-independent
|
|
6
|
+
* ref, the version segment it currently names, and any inline pin. */
|
|
7
|
+
export interface ParsedVersionedRef {
|
|
8
|
+
/** The ref with its `@version` segment and integrity fragment removed —
|
|
9
|
+
* `std/run`, `oci://ghcr.io/telorun/timer`. This is the identity a version
|
|
10
|
+
* list is keyed by (the hub registers modules under exactly this form). */
|
|
11
|
+
baseRef: string;
|
|
12
|
+
/** The version segment, raw — a registry `@version`, an OCI tag, or an OCI
|
|
13
|
+
* digest reference (`sha256:…`). The caller applies its own SemVer check. */
|
|
14
|
+
version: string;
|
|
15
|
+
/** Telo's inline `sha256-<base64url>` pin, when the ref carried one. */
|
|
16
|
+
integrity?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Split a versioned module ref. Returns `null` when the ref names no
|
|
20
|
+
* upgradeable version — a local path, a bare `https://` URL, or an OCI ref
|
|
21
|
+
* with no explicit reference (an implicit `latest` is not a pin).
|
|
22
|
+
*
|
|
23
|
+
* Browser-safe and transport-neutral: this is the *grammar* half of an
|
|
24
|
+
* upgrade, shared by the kernel transports (whose `refVersion` / `withVersion`
|
|
25
|
+
* delegate here) and the editor, which cannot use a transport at all — the
|
|
26
|
+
* *network* half (enumerating versions) is scheme-specific and stays behind
|
|
27
|
+
* `Transport.listVersions` on Node and the hub's `/module/versions` in the
|
|
28
|
+
* browser. */
|
|
29
|
+
export function parseVersionedRef(ref: string): ParsedVersionedRef | null {
|
|
30
|
+
const { base, integrity } = splitIntegrity(ref);
|
|
31
|
+
const at = versionSeparator(base);
|
|
32
|
+
if (at === null) return null;
|
|
33
|
+
const version = base.slice(at + 1);
|
|
34
|
+
if (!version) return null;
|
|
35
|
+
return { baseRef: base.slice(0, at), version, integrity };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** `ref` rewritten to name `version`, dropping any integrity fragment (the
|
|
39
|
+
* caller re-pins the result). Appends the version when the ref carries none —
|
|
40
|
+
* an untagged `oci://host/repo` is still a versionable address.
|
|
41
|
+
*
|
|
42
|
+
* Throws when the ref's grammar has no version segment at all — a relative
|
|
43
|
+
* path, a bare `https://` URL. Producing `../lib@0.4.0` for those would write
|
|
44
|
+
* a ref nothing can resolve, so this fails where the transport-specific
|
|
45
|
+
* parsers it replaced (`parseOciRef` / `parseModuleRef`) also failed. */
|
|
46
|
+
export function withRefVersion(ref: string, version: string): string {
|
|
47
|
+
const { base } = splitIntegrity(ref);
|
|
48
|
+
if (refGrammar(base) === null) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Cannot set a version on '${ref}' — only registry (namespace/name@version) ` +
|
|
51
|
+
`and oci:// refs carry a version segment.`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const at = versionSeparator(base);
|
|
55
|
+
return `${at === null ? base : base.slice(0, at)}@${version}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Which versionable ref grammar `base` is written in, or `null` when it is
|
|
59
|
+
* neither — a relative/absolute path, a `file:`/`https://` URL. */
|
|
60
|
+
function refGrammar(base: string): "oci" | "registry" | null {
|
|
61
|
+
if (base.startsWith(OCI_SCHEME)) {
|
|
62
|
+
// A host alone is not addressable; the repo path is what carries a version.
|
|
63
|
+
return base.indexOf("/", OCI_SCHEME.length) > OCI_SCHEME.length ? "oci" : null;
|
|
64
|
+
}
|
|
65
|
+
// `isRegistryRef` requires the `@`, so a version-less `std/console` is not a
|
|
66
|
+
// registry ref by this test — matching `parseModuleRef`, which throws on it.
|
|
67
|
+
return isRegistryRef(base) ? "registry" : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Index of the `@` that separates the version, or `null` when the ref names
|
|
71
|
+
* none. Split on the LAST `@` so a digest reference (`repo@sha256:…`) keeps
|
|
72
|
+
* everything before it as the ref. */
|
|
73
|
+
function versionSeparator(base: string): number | null {
|
|
74
|
+
if (refGrammar(base) === null) return null;
|
|
75
|
+
const at = base.lastIndexOf("@");
|
|
76
|
+
return at > (base.startsWith(OCI_SCHEME) ? OCI_SCHEME.length : 0) ? at : null;
|
|
77
|
+
}
|