@appstrate/core 2.0.0 → 2.1.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/package.json +4 -2
- package/src/dist-tags.ts +12 -0
- package/src/semver.ts +53 -0
- package/src/version-policy.ts +66 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appstrate/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": ["src"],
|
|
6
6
|
"exports": {
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
"./semver": "./src/semver.ts",
|
|
18
18
|
"./registry-deps": "./src/registry-deps.ts",
|
|
19
19
|
"./update-check": "./src/update-check.ts",
|
|
20
|
-
"./publish-manifest": "./src/publish-manifest.ts"
|
|
20
|
+
"./publish-manifest": "./src/publish-manifest.ts",
|
|
21
|
+
"./dist-tags": "./src/dist-tags.ts",
|
|
22
|
+
"./version-policy": "./src/version-policy.ts"
|
|
21
23
|
},
|
|
22
24
|
"dependencies": {
|
|
23
25
|
"fflate": "^0.8.0",
|
package/src/dist-tags.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Regex for valid dist-tag names: lowercase alphanumeric, dots, hyphens, underscores. */
|
|
2
|
+
export const DIST_TAG_REGEX = /^[a-z][a-z0-9._-]*$/;
|
|
3
|
+
|
|
4
|
+
/** Check whether `tag` is a valid dist-tag name. */
|
|
5
|
+
export function isValidDistTag(tag: string): boolean {
|
|
6
|
+
return DIST_TAG_REGEX.test(tag);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Check whether `tag` is a protected tag that cannot be manually set or removed. */
|
|
10
|
+
export function isProtectedTag(tag: string): boolean {
|
|
11
|
+
return tag === "latest";
|
|
12
|
+
}
|
package/src/semver.ts
CHANGED
|
@@ -34,6 +34,11 @@ export function isPrerelease(v: string): boolean {
|
|
|
34
34
|
return semver.prerelease(v) !== null;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/** Auto-bump the patch segment of `currentVersion`. Returns null if invalid semver. */
|
|
38
|
+
export function bumpPatch(currentVersion: string): string | null {
|
|
39
|
+
return semver.inc(currentVersion, "patch");
|
|
40
|
+
}
|
|
41
|
+
|
|
37
42
|
/** Returns true if `remote` is a strictly newer version than `installed`.
|
|
38
43
|
* Returns false if either is null/undefined or invalid. */
|
|
39
44
|
export function hasNewerVersion(
|
|
@@ -71,3 +76,51 @@ export function resolveLatestVersion(
|
|
|
71
76
|
|
|
72
77
|
return versions.at(-1)?.version ?? null;
|
|
73
78
|
}
|
|
79
|
+
|
|
80
|
+
export interface CatalogVersion {
|
|
81
|
+
id: number;
|
|
82
|
+
version: string;
|
|
83
|
+
yanked: boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Resolve a version query against a catalog of versions and dist-tags.
|
|
88
|
+
* 3-step resolution: exact match → dist-tag → semver range.
|
|
89
|
+
*
|
|
90
|
+
* - Exact match includes yanked versions (like npm/crates.io: exact pins always resolve).
|
|
91
|
+
* - Dist-tag lookup excludes yanked versions.
|
|
92
|
+
* - Semver range excludes yanked versions.
|
|
93
|
+
*
|
|
94
|
+
* Returns the version id, or null if no match.
|
|
95
|
+
*/
|
|
96
|
+
export function resolveVersionFromCatalog(
|
|
97
|
+
query: string,
|
|
98
|
+
versions: CatalogVersion[],
|
|
99
|
+
distTags: DistTagEntry[],
|
|
100
|
+
): number | null {
|
|
101
|
+
// 1. Exact match — includes yanked
|
|
102
|
+
if (isValidVersion(query)) {
|
|
103
|
+
const exact = versions.find((v) => v.version === query);
|
|
104
|
+
if (exact) return exact.id;
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 2. Dist-tag — excludes yanked
|
|
109
|
+
const tagEntry = distTags.find((t) => t.tag === query);
|
|
110
|
+
if (tagEntry) {
|
|
111
|
+
const tagged = versions.find((v) => v.id === tagEntry.versionId && !v.yanked);
|
|
112
|
+
if (tagged) return tagged.id;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 3. Semver range — excludes yanked
|
|
116
|
+
if (isValidRange(query)) {
|
|
117
|
+
const nonYanked = versions.filter((v) => !v.yanked);
|
|
118
|
+
const versionStrings = nonYanked.map((v) => v.version).filter(isValidVersion);
|
|
119
|
+
const best = matchVersion(versionStrings, query);
|
|
120
|
+
if (!best) return null;
|
|
121
|
+
const match = nonYanked.find((v) => v.version === best);
|
|
122
|
+
return match?.id ?? null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { isValidVersion, versionGt, compareVersionsDesc, isPrerelease } from "./semver.ts";
|
|
2
|
+
|
|
3
|
+
export type ForwardVersionError = "VERSION_EXISTS" | "VERSION_NOT_HIGHER";
|
|
4
|
+
|
|
5
|
+
export interface ForwardVersionResult {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
error?: ForwardVersionError;
|
|
8
|
+
highest?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validate that `newVersion` is strictly higher than all existing versions.
|
|
13
|
+
* Callers must pass ALL versions (including yanked) to prevent re-publishing.
|
|
14
|
+
* Rejects duplicates and versions not higher than the current maximum.
|
|
15
|
+
*/
|
|
16
|
+
export function validateForwardVersion(
|
|
17
|
+
newVersion: string,
|
|
18
|
+
existingVersions: string[],
|
|
19
|
+
): ForwardVersionResult {
|
|
20
|
+
if (existingVersions.includes(newVersion)) {
|
|
21
|
+
return { ok: false, error: "VERSION_EXISTS" };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const validVersions = existingVersions.filter(isValidVersion);
|
|
25
|
+
if (validVersions.length === 0) {
|
|
26
|
+
return { ok: true };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const highest = validVersions.sort(compareVersionsDesc)[0]!;
|
|
30
|
+
if (!versionGt(newVersion, highest)) {
|
|
31
|
+
return { ok: false, error: "VERSION_NOT_HIGHER", highest };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { ok: true };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Find the best candidate for dist-tag reassignment after a yank:
|
|
39
|
+
* highest non-yanked, non-prerelease version.
|
|
40
|
+
*/
|
|
41
|
+
export function findBestStableVersion(
|
|
42
|
+
candidates: Array<{ id: number; version: string }>,
|
|
43
|
+
): { id: number; version: string } | null {
|
|
44
|
+
const stable = candidates.filter((v) => !isPrerelease(v.version));
|
|
45
|
+
if (stable.length === 0) return null;
|
|
46
|
+
|
|
47
|
+
const sorted = stable.sort((a, b) => compareVersionsDesc(a.version, b.version));
|
|
48
|
+
return sorted[0]!;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Determine whether a newly published version should replace the "latest" dist-tag.
|
|
53
|
+
* Prereleases never update "latest". A stable version updates "latest" only if
|
|
54
|
+
* there is no current latest or the new version is >= the current one.
|
|
55
|
+
*/
|
|
56
|
+
export function shouldUpdateLatestTag(
|
|
57
|
+
newVersion: string,
|
|
58
|
+
currentLatestVersion: string | null,
|
|
59
|
+
): boolean {
|
|
60
|
+
if (isPrerelease(newVersion)) return false;
|
|
61
|
+
if (!currentLatestVersion) return true;
|
|
62
|
+
return !versionGt(currentLatestVersion, newVersion);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Re-export bumpPatch from semver.ts for backward compatibility
|
|
66
|
+
export { bumpPatch } from "./semver.ts";
|