Skip to content

Commit ab8e203

Browse files
ufukaltinokalfonso-magic-context
authored andcommitted
fix(auto-update): break cache.ts<->checker.ts circular import (cold-graph crash)
CI failed (Check plugin, 10/10 auto-update-checker tests) with: SyntaxError: Export named 'isValidSemver' not found in module '.../cache.ts' Root cause: a circular import introduced in b137074 when semver validation was added — checker.ts imported isValidSemver from cache.ts, while cache.ts already imported getCurrentRuntimePackageJsonPath from checker.ts. On a cold module graph (fresh CI install) the cycle breaks static export resolution, so the whole checker.test.ts file fails to load — including pure tests like extractChannel(). A warm local bun module cache tolerated the cycle, which is why it passed locally and only surfaced in CI (and would equally bite a fresh user install). Fix: move the pure leaf isValidSemver into its own dependency-free module (auto-update-checker/semver.ts). checker.ts now imports it from there and no longer imports cache.ts at all, breaking the cycle. cache.ts keeps using it via the new leaf import. Reproduced CI's condition locally (cold BUN_INSTALL_CACHE_DIR) — fails before, 10/10 pass after. plugin 1888/0, tsc + biome clean. Co-authored-by: Alfonso [Magic Context] <288211368+alfonso-magic-context@users.noreply.github.com>
1 parent a16f6a6 commit ab8e203

3 files changed

Lines changed: 20 additions & 14 deletions

File tree

packages/plugin/src/hooks/auto-update-checker/cache.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { parse as parseJsonc } from "comment-json";
66
import { log } from "../../shared/logger";
77
import { getCurrentRuntimePackageJsonPath } from "./checker";
88
import { CACHE_DIR, PACKAGE_NAME } from "./constants";
9+
import { isValidSemver } from "./semver";
910
import { PackageJsonSchema } from "./types";
1011

1112
/**
@@ -183,19 +184,6 @@ export function resolveInstallContext(
183184
return null;
184185
}
185186

186-
/**
187-
* Strict semver core with optional prerelease + build metadata (1.2.3,
188-
* 1.2.3-beta.1, 1.2.3+build.5). The version reaching the update path comes from
189-
* the npm registry `dist-tags` envelope — Zod-parsed only as a STRING shape, not
190-
* as semver — so a malformed/crafted tag value (`npm:@evil/pkg@1.0.0`,
191-
* `file:/tmp/x`, `git+ssh://...`) must never be written into package.json's
192-
* dependency spec before `npm install`. Shared by the auto-update prepare path
193-
* and the pinned-config rewrite.
194-
*/
195-
export function isValidSemver(version: string): boolean {
196-
return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version);
197-
}
198-
199187
export function preparePackageUpdate(
200188
version: string,
201189
packageName: string = PACKAGE_NAME,

packages/plugin/src/hooks/auto-update-checker/checker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { fileURLToPath } from "node:url";
55
import { parse as parseJsonc } from "comment-json";
66

77
import { log } from "../../shared/logger";
8-
import { isValidSemver } from "./cache";
98
import {
109
CACHE_DIR,
1110
NPM_FETCH_TIMEOUT,
@@ -14,6 +13,7 @@ import {
1413
USER_OPENCODE_CONFIG,
1514
USER_OPENCODE_CONFIG_JSONC,
1615
} from "./constants";
16+
import { isValidSemver } from "./semver";
1717
import {
1818
NpmPackageEnvelopeSchema,
1919
OpencodeConfigSchema,
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Strict semver validator for the auto-update path.
3+
*
4+
* The plugin version string flows from the npm registry / dist-tags into
5+
* package.json's dependency spec before `npm install`. A malformed or crafted
6+
* value (`npm:@evil/pkg@1.0.0`, `file:/tmp/x`, `git+ssh://...`) must never be
7+
* written there — this is the arbitrary-package-install / SSRF guard.
8+
*
9+
* Lives in its own leaf module (no imports beyond this) so both `cache.ts` and
10+
* `checker.ts` can use it without creating a circular import between them
11+
* (`cache.ts` imports `checker.ts` for the runtime package.json path; if
12+
* `checker.ts` also imported this from `cache.ts`, the cycle breaks static
13+
* export resolution on a cold module graph — CI / fresh installs — even though
14+
* a warm bun cache tolerates it).
15+
*/
16+
export function isValidSemver(version: string): boolean {
17+
return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version);
18+
}

0 commit comments

Comments
 (0)