mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
refactor(cli): move version-compare helper into dev.ts
isVersionNewer/parseVersionCore no longer gate the npx re-exec (which always runs @latest); their only remaining consumer is the dashboard restart check in dev.ts. Move them (and their tests) there so self-update.ts is purely about the re-exec.
This commit is contained in:
parent
e8f25edc14
commit
6df30665a4
@ -3,7 +3,54 @@ import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { recordLocalDashboardProcess } from "../lib/dev-env-state.js";
|
||||
import { killLocalDashboard, processExists, shouldRestartDashboard } from "./dev.js";
|
||||
import { isVersionNewer, killLocalDashboard, processExists, shouldRestartDashboard } from "./dev.js";
|
||||
|
||||
describe("isVersionNewer", () => {
|
||||
it("compares core versions numerically", () => {
|
||||
expect(isVersionNewer("2.8.110", "2.8.109")).toBe(true);
|
||||
expect(isVersionNewer("2.9.0", "2.8.999")).toBe(true);
|
||||
expect(isVersionNewer("3.0.0", "2.999.999")).toBe(true);
|
||||
expect(isVersionNewer("2.8.109", "2.8.109")).toBe(false);
|
||||
expect(isVersionNewer("2.8.108", "2.8.109")).toBe(false);
|
||||
});
|
||||
|
||||
it("does not treat double-digit segments as strings", () => {
|
||||
expect(isVersionNewer("2.8.10", "2.8.9")).toBe(true);
|
||||
});
|
||||
|
||||
it("ranks a final release above a prerelease of the same core", () => {
|
||||
expect(isVersionNewer("2.8.109", "2.8.109-beta.1")).toBe(true);
|
||||
expect(isVersionNewer("2.8.109-beta.1", "2.8.109")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for unparseable versions (never downgrade or guess)", () => {
|
||||
expect(isVersionNewer("garbage", "2.8.109")).toBe(false);
|
||||
expect(isVersionNewer("2.8.110", "garbage")).toBe(false);
|
||||
});
|
||||
|
||||
it("tolerates a leading v and surrounding whitespace on either side", () => {
|
||||
expect(isVersionNewer("v2.8.110", "2.8.109")).toBe(true);
|
||||
expect(isVersionNewer("2.8.110", "v2.8.109")).toBe(true);
|
||||
expect(isVersionNewer(" 2.8.110 ", "2.8.109")).toBe(true);
|
||||
expect(isVersionNewer("v2.8.110", "v2.8.110")).toBe(false);
|
||||
});
|
||||
|
||||
it("treats a two-segment version (x.y) as unparseable", () => {
|
||||
expect(isVersionNewer("2.8", "2.8.109")).toBe(false);
|
||||
expect(isVersionNewer("2.8.109", "2.8")).toBe(false);
|
||||
});
|
||||
|
||||
it("ignores prerelease identifiers when both cores are equal prereleases", () => {
|
||||
// Only "release beats prerelease" is modeled; beta.2 is NOT newer than beta.1.
|
||||
expect(isVersionNewer("2.8.109-beta.2", "2.8.109-beta.1")).toBe(false);
|
||||
expect(isVersionNewer("2.8.109-beta.1", "2.8.109-beta.2")).toBe(false);
|
||||
});
|
||||
|
||||
it("compares very large numeric segments correctly", () => {
|
||||
expect(isVersionNewer("2.8.1000000000", "2.8.999999999")).toBe(true);
|
||||
expect(isVersionNewer("10000000000.0.0", "9999999999.0.0")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldRestartDashboard", () => {
|
||||
it("restarts only when ours is strictly newer than the running dashboard", () => {
|
||||
|
||||
@ -9,7 +9,7 @@ import { resolveConfigFilePathOption } from "../lib/config-file-path.js";
|
||||
import { devEnvStatePath, ensureLocalDashboardSecret, readDevEnvState, recordLocalDashboardProcess } from "../lib/dev-env-state.js";
|
||||
import { CliError } from "../lib/errors.js";
|
||||
import { cliVersion } from "../lib/own-package.js";
|
||||
import { isVersionNewer, maybeReexecToLatest } from "../lib/self-update.js";
|
||||
import { maybeReexecToLatest } from "../lib/self-update.js";
|
||||
|
||||
type ChildCommand = {
|
||||
command: string,
|
||||
@ -273,6 +273,43 @@ async function isDashboardReachable(url: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
type ParsedVersion = {
|
||||
core: [number, number, number],
|
||||
hasPrerelease: boolean,
|
||||
};
|
||||
|
||||
function parseVersionCore(version: string): ParsedVersion | null {
|
||||
const trimmed = version.trim();
|
||||
const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(trimmed);
|
||||
if (!match) return null;
|
||||
return {
|
||||
core: [Number(match[1]), Number(match[2]), Number(match[3])],
|
||||
// A `-` immediately after the core marks a semver prerelease (e.g.
|
||||
// 2.8.109-beta.1). `.test()` returns a plain boolean, sidestepping the
|
||||
// optional-capture-group typing.
|
||||
hasPrerelease: /^v?\d+\.\d+\.\d+-/.test(trimmed),
|
||||
};
|
||||
}
|
||||
|
||||
// Returns true only when `candidate` is strictly newer than `current`. Unknown
|
||||
// or unparseable versions return false so we never act on a version we can't
|
||||
// reason about (and never downgrade). Prerelease identifiers beyond the
|
||||
// "release beats same-core prerelease" rule are intentionally not ordered. Only
|
||||
// the dashboard restart check below needs this; the CLI re-exec just always runs
|
||||
// `@latest`. Exported for unit testing.
|
||||
export function isVersionNewer(candidate: string, current: string): boolean {
|
||||
const a = parseVersionCore(candidate);
|
||||
const b = parseVersionCore(current);
|
||||
if (a == null || b == null) return false;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (a.core[i] !== b.core[i]) {
|
||||
return a.core[i] > b.core[i];
|
||||
}
|
||||
}
|
||||
// Same x.y.z: a final release outranks a prerelease of the same core.
|
||||
return !a.hasPrerelease && b.hasPrerelease;
|
||||
}
|
||||
|
||||
// Restart the running dashboard only when ours is strictly newer; this is how a
|
||||
// re-exec'd `npx @latest` rolls out a fresh dashboard without a reinstall.
|
||||
// Equal/older/unknown versions (e.g. a dashboard recorded by a pre-feature CLI
|
||||
|
||||
@ -4,7 +4,6 @@ import {
|
||||
decideReexec,
|
||||
DISABLE_AUTO_UPDATE_ENV,
|
||||
isEnvFlagEnabled,
|
||||
isVersionNewer,
|
||||
maybeReexecToLatest,
|
||||
shouldAutoUpdate,
|
||||
SKIP_AUTO_UPDATE_ENV,
|
||||
@ -52,53 +51,6 @@ describe("shouldAutoUpdate", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("isVersionNewer", () => {
|
||||
it("compares core versions numerically", () => {
|
||||
expect(isVersionNewer("2.8.110", "2.8.109")).toBe(true);
|
||||
expect(isVersionNewer("2.9.0", "2.8.999")).toBe(true);
|
||||
expect(isVersionNewer("3.0.0", "2.999.999")).toBe(true);
|
||||
expect(isVersionNewer("2.8.109", "2.8.109")).toBe(false);
|
||||
expect(isVersionNewer("2.8.108", "2.8.109")).toBe(false);
|
||||
});
|
||||
|
||||
it("does not treat double-digit segments as strings", () => {
|
||||
expect(isVersionNewer("2.8.10", "2.8.9")).toBe(true);
|
||||
});
|
||||
|
||||
it("ranks a final release above a prerelease of the same core", () => {
|
||||
expect(isVersionNewer("2.8.109", "2.8.109-beta.1")).toBe(true);
|
||||
expect(isVersionNewer("2.8.109-beta.1", "2.8.109")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for unparseable versions (never downgrade or guess)", () => {
|
||||
expect(isVersionNewer("garbage", "2.8.109")).toBe(false);
|
||||
expect(isVersionNewer("2.8.110", "garbage")).toBe(false);
|
||||
});
|
||||
|
||||
it("tolerates a leading v and surrounding whitespace on either side", () => {
|
||||
expect(isVersionNewer("v2.8.110", "2.8.109")).toBe(true);
|
||||
expect(isVersionNewer("2.8.110", "v2.8.109")).toBe(true);
|
||||
expect(isVersionNewer(" 2.8.110 ", "2.8.109")).toBe(true);
|
||||
expect(isVersionNewer("v2.8.110", "v2.8.110")).toBe(false);
|
||||
});
|
||||
|
||||
it("treats a two-segment version (x.y) as unparseable", () => {
|
||||
expect(isVersionNewer("2.8", "2.8.109")).toBe(false);
|
||||
expect(isVersionNewer("2.8.109", "2.8")).toBe(false);
|
||||
});
|
||||
|
||||
it("ignores prerelease identifiers when both cores are equal prereleases", () => {
|
||||
// Only "release beats prerelease" is modeled; beta.2 is NOT newer than beta.1.
|
||||
expect(isVersionNewer("2.8.109-beta.2", "2.8.109-beta.1")).toBe(false);
|
||||
expect(isVersionNewer("2.8.109-beta.1", "2.8.109-beta.2")).toBe(false);
|
||||
});
|
||||
|
||||
it("compares very large numeric segments correctly", () => {
|
||||
expect(isVersionNewer("2.8.1000000000", "2.8.999999999")).toBe(true);
|
||||
expect(isVersionNewer("10000000000.0.0", "9999999999.0.0")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildNpxInvocation", () => {
|
||||
it("pins @latest and forwards the subcommand through the bin", () => {
|
||||
const { command, args } = buildNpxInvocation({
|
||||
|
||||
@ -31,43 +31,6 @@ export function shouldAutoUpdate(env: NodeJS.ProcessEnv): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
type ParsedVersion = {
|
||||
core: [number, number, number],
|
||||
hasPrerelease: boolean,
|
||||
};
|
||||
|
||||
function parseVersionCore(version: string): ParsedVersion | null {
|
||||
const trimmed = version.trim();
|
||||
const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(trimmed);
|
||||
if (!match) return null;
|
||||
return {
|
||||
core: [Number(match[1]), Number(match[2]), Number(match[3])],
|
||||
// A `-` immediately after the core marks a semver prerelease (e.g.
|
||||
// 2.8.109-beta.1). `.test()` returns a plain boolean, sidestepping the
|
||||
// optional-capture-group typing.
|
||||
hasPrerelease: /^v?\d+\.\d+\.\d+-/.test(trimmed),
|
||||
};
|
||||
}
|
||||
|
||||
// Returns true only when `candidate` is strictly newer than `current`. Unknown
|
||||
// or unparseable versions return false so we never act on a version we can't
|
||||
// reason about (and never downgrade). Prerelease identifiers beyond the
|
||||
// "release beats same-core prerelease" rule are intentionally not ordered.
|
||||
// Used by the dashboard restart check in dev.ts (the re-exec itself just always
|
||||
// runs `@latest`).
|
||||
export function isVersionNewer(candidate: string, current: string): boolean {
|
||||
const a = parseVersionCore(candidate);
|
||||
const b = parseVersionCore(current);
|
||||
if (a == null || b == null) return false;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (a.core[i] !== b.core[i]) {
|
||||
return a.core[i] > b.core[i];
|
||||
}
|
||||
}
|
||||
// Same x.y.z: a final release outranks a prerelease of the same core.
|
||||
return !a.hasPrerelease && b.hasPrerelease;
|
||||
}
|
||||
|
||||
export type NpxInvocation = {
|
||||
command: string,
|
||||
args: string[],
|
||||
|
||||
Loading…
Reference in New Issue
Block a user