Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions packages/extension/esbuild.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,27 @@ if (declaredBins.some((name) => existsSync(`${arRoot}/dist/${name}.js`))) {
// #2001: the What's-new page's release cards (docs/releases/<version>.md) ride
// the VSIX in dist/releases, so GET /amicode/releases works offline (and on a
// hub runner, from the same extension tree). dist/ ships (not in .vscodeignore).
// #2017: ALSO beside the standalone runner (bin/dist/releases) — the hub runs
// bin/dist/amicode-service-runner.mjs on its own, and resolveReleasesDir()
// checks <here>/releases first, so a runner deploy carries its cards with it.
const releasesSrc = "../../docs/releases";
if (existsSync(releasesSrc)) {
mkdirSync("dist/releases", { recursive: true });
for (const name of readdirSync(releasesSrc)) {
if (name.endsWith(".md")) cpSync(`${releasesSrc}/${name}`, `dist/releases/${name}`);
for (const out of ["dist/releases", "bin/dist/releases"]) {
mkdirSync(out, { recursive: true });
for (const name of readdirSync(releasesSrc)) {
if (name.endsWith(".md")) cpSync(`${releasesSrc}/${name}`, `${out}/${name}`);
}
}
} else {
console.warn("[esbuild] docs/releases missing — the What's-new page will have no release cards");
}

// #2017: bake this build's version into the node bundles — the hub runner has
// no package.json near it, and a 0.0.0 fallback filtered every release card.
// Read via a typeof guard (src/build_defines.d.ts) so vitest without it works.
const ownVersion = JSON.parse(readFileSync("package.json", "utf8")).version;
const versionDefine = { __AMICODE_VERSION__: JSON.stringify(ownVersion) };

const targets = [
// extension host entry point
{
Expand All @@ -83,6 +94,7 @@ const targets = [
format: "cjs",
outfile: "dist/extension.js",
external: ["vscode"],
define: versionDefine,
sourcemap: true,
minify: false,
logLevel: "info",
Expand Down Expand Up @@ -139,6 +151,7 @@ const targets = [
target: "node20",
format: "esm",
outfile: "bin/dist/amicode-service-runner.mjs",
define: versionDefine,
sourcemap: true,
minify: false,
logLevel: "info",
Expand Down
52 changes: 40 additions & 12 deletions packages/extension/src/amicode_service/releases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,31 +213,59 @@ function moduleDir(): string {
return typeof __dirname === "string" ? __dirname : process.cwd();
}

/** The bundled releases dir: $AMICODE_RELEASES_DIR, else the build's
* dist/releases (from dist/ or bin/dist/), else the repo's docs/releases
* (a dev checkout). undefined → no cards (the route still answers). */
export function resolveReleasesDir(env: NodeJS.ProcessEnv = process.env): string | undefined {
/** The bundled releases dir: $AMICODE_RELEASES_DIR, else <here>/releases
* (dist/releases beside the extension bundle, or bin/dist/releases beside the
* runner bundle — #2017: a hub deploy copies the runner + its releases/ only),
* else dist/releases from bin/dist, else the repo's docs/releases (a dev
* checkout). undefined → no cards (the route still answers). */
export function resolveReleasesDir(env: NodeJS.ProcessEnv = process.env, here: string = moduleDir()): string | undefined {
const override = env.AMICODE_RELEASES_DIR;
if (typeof override === "string" && override.trim() !== "") return override;
const here = moduleDir();
const candidates = [
path.resolve(here, "releases"), // dist/ → dist/releases
path.resolve(here, "releases"), // dist/ → dist/releases; bin/dist/ → bin/dist/releases
path.resolve(here, "..", "..", "dist", "releases"), // bin/dist/ → dist/releases
path.resolve(here, "..", "..", "..", "docs", "releases"), // dist/ in a checkout
path.resolve(here, "..", "..", "..", "..", "docs", "releases"), // src/amicode_service or bin/dist in a checkout
];
return candidates.find((c) => existsSync(c));
}

let cachedVersion: string | undefined;
/** The extension's own version (its package.json, name "amicode"). */
export function resolveOwnVersion(): string {
if (cachedVersion) return cachedVersion;
const here = moduleDir();
/** The build-time version esbuild bakes into BOTH the extension and the runner
* bundle (#2017). Guarded so vitest (no define) sees undefined, not a
* ReferenceError. */
function definedVersion(): string | undefined {
return typeof __AMICODE_VERSION__ === "string" ? __AMICODE_VERSION__ : undefined;
}

export interface ResolveOwnVersionOpts {
env?: NodeJS.ProcessEnv;
/** The bundle's dir (the package.json walk starts here). Default: moduleDir(). */
here?: string;
/** The build-time define. Default: __AMICODE_VERSION__ when defined. */
defined?: string | undefined;
}

const cachedByDir = new Map<string, string>();
/** The extension's own version: $AMICODE_VERSION, else the build-time define,
* else its package.json (name "amicode") near the bundle, else "0.0.0".
* (#2017: the hub's standalone runner has no package.json near it — the
* define is what keeps it from answering 0.0.0 and filtering every card.) */
export function resolveOwnVersion(opts: ResolveOwnVersionOpts = {}): string {
const env = opts.env ?? process.env;
const fromEnv = env.AMICODE_VERSION?.trim();
if (fromEnv && isVersion(fromEnv)) return fromEnv.replace(/^v/, "");
const defined = "defined" in opts ? opts.defined : definedVersion();
if (defined && isVersion(defined)) return defined.replace(/^v/, "");
const here = opts.here ?? moduleDir();
const cached = cachedByDir.get(here);
if (cached) return cached;
for (const rel of [[".."], ["..", ".."], ["..", "..", ".."]]) {
try {
const pkg = JSON.parse(readFileSync(path.resolve(here, ...rel, "package.json"), "utf8")) as { name?: unknown; version?: unknown };
if (pkg.name === "amicode" && isVersion(pkg.version)) return (cachedVersion = pkg.version);
if (pkg.name === "amicode" && isVersion(pkg.version)) {
cachedByDir.set(here, pkg.version);
return pkg.version;
}
} catch {
/* next candidate */
}
Expand Down
6 changes: 6 additions & 0 deletions packages/extension/src/build_defines.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Build-time constants injected by esbuild.config.mjs `define` (#2017).
// Absent under vitest/tsc-only runs — always read through a `typeof` guard.

/** packages/extension/package.json `version`, baked into the extension and
* the standalone service-runner bundles. */
declare const __AMICODE_VERSION__: string | undefined;
23 changes: 23 additions & 0 deletions packages/extension/test/amicode_service_releases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
parseReleaseFile,
releaseBaselineMs,
releasesResponse,
resolveOwnVersion,
resolveReleasesDir,
selectReleaseCards,
type ReleaseCard,
} from "../src/amicode_service/releases";
Expand Down Expand Up @@ -227,6 +229,27 @@ describe("GET /amicode/releases", () => {
}
});

it("#2017: the version resolves from AMICODE_VERSION, then the build-time define — never 0.0.0 when no package.json is near", () => {
const empty = mkdtempSync(join(tmpdir(), "amc-2017-empty-"));
// env override wins over everything
expect(resolveOwnVersion({ here: empty, env: { AMICODE_VERSION: "0.4.0-lane.7" }, defined: "0.4.0-lane.1" })).toBe("0.4.0-lane.7");
// a malformed env value is ignored → the define
expect(resolveOwnVersion({ here: empty, env: { AMICODE_VERSION: "banana" }, defined: "0.4.0-lane.1" })).toBe("0.4.0-lane.1");
// no env → the define (the hub runner bundle: no package.json near it)
expect(resolveOwnVersion({ here: empty, env: {}, defined: "0.4.0-lane.1" })).toBe("0.4.0-lane.1");
// no env, no define (plain vitest) → the package.json lookup still works
const pkg = JSON.parse(readFileSync(resolve(__dirname, "../package.json"), "utf8")) as { version: string };
expect(resolveOwnVersion({ here: resolve(__dirname, "../src/amicode_service"), env: {}, defined: undefined })).toBe(pkg.version);
});

it("#2017: the releases dir is found at <here>/releases (the runner bundle's bin/dist/releases)", () => {
const here = mkdtempSync(join(tmpdir(), "amc-2017-bin-dist-"));
mkdirSync(join(here, "releases"));
writeFileSync(join(here, "releases", "0.4.0.md"), md("0.4.0", "2026-10-29"));
expect(resolveReleasesDir({}, here)).toBe(resolve(here, "releases"));
expect(resolveReleasesDir({ AMICODE_RELEASES_DIR: "/x/y" }, here)).toBe("/x/y");
});

it("the default version is the extension's own package.json version", async () => {
const pkg = JSON.parse(readFileSync(resolve(__dirname, "../package.json"), "utf8")) as { version: string };
const body = JSON.parse(await releasesResponse({ since: pkg.version }, { dir: releasesDir(files) }));
Expand Down
Loading