Skip to content
Open
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
5 changes: 1 addition & 4 deletions lib/getVersion.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { createRequire } from "node:module"
import pkg from "../package.json"
import semver from "semver"
import { getCliVersion } from "./shared/get-cli-version"

const require = createRequire(import.meta.url)

Expand All @@ -17,8 +16,6 @@ const resolvePackageVersionFromNodeModules: VersionResolver = (packageName) => {
}
}

const getCliVersion = () => semver.inc(pkg.version, "patch") ?? pkg.version

type GlobalWithTscircuitVersion = typeof globalThis & {
TSCIRCUIT_VERSION?: string
}
Expand Down
6 changes: 2 additions & 4 deletions lib/shared/check-for-cli-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@ import ky from "ky"
import { getPackageManager } from "./get-package-manager"
import { getGlobalDepsInstallCommand } from "lib/shared/get-dep-install-command"
import { execSync } from "node:child_process"
import { program } from "cli/main"
import semver from "semver"
import { version as pkgVersion } from "../../package.json"
import { getCliVersion } from "./get-cli-version"
import kleur from "kleur"
import { prompts } from "lib/utils/prompts"
import { shouldBeInteractive } from "lib/utils/should-be-interactive"

export const currentCliVersion = () =>
program?.version() ?? semver.inc(pkgVersion, "patch") ?? pkgVersion
export const currentCliVersion = getCliVersion

export const getLatestVersion = async () => {
const { version: latestCliVersion } = await ky
Expand Down
34 changes: 34 additions & 0 deletions lib/shared/get-cli-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { readFileSync } from "node:fs"
import semver from "semver"
import { version as bundledVersion } from "../../package.json"

export const getCliVersion = (): string => {
try {
// Both this source file and dist/{cli,lib} are two levels below the manifest.
const manifest: unknown = JSON.parse(
readFileSync(new URL("../../package.json", import.meta.url), "utf8"),
)
if (
typeof manifest === "object" &&
manifest !== null &&
"name" in manifest &&
manifest.name === "@tscircuit/cli" &&
"version" in manifest &&
typeof manifest.version === "string" &&
semver.valid(manifest.version) !== null
) {
return manifest.version
}
return bundledVersion
} catch (error) {
if (
error instanceof SyntaxError ||
(error instanceof Error &&
"code" in error &&
["ENOENT", "ENOTDIR", "EACCES", "EPERM"].includes(String(error.code)))
) {
return bundledVersion
}
throw error
}
}
133 changes: 133 additions & 0 deletions tests/lib/get-cli-version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { afterEach, expect, test } from "bun:test"
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os"
import { dirname, join } from "node:path"
import { fileURLToPath } from "node:url"
import { version as sourceVersion } from "../../package.json"

const temporaryDirectories: string[] = []
let cachedBundle: string | undefined

afterEach(async () => {
for (const directory of temporaryDirectories.splice(0)) {
await rm(directory, { recursive: true, force: true })
}
})

const createBundle = async (outputPath: string) => {
const root = await mkdtemp(join(tmpdir(), "cli-version-"))
temporaryDirectories.push(root)
const entrypoint = join(root, "entry.ts")
const getVersionPath = fileURLToPath(
new URL("../../lib/getVersion.ts", import.meta.url),
)
const helperPath = fileURLToPath(
new URL("../../lib/shared/get-cli-version.ts", import.meta.url),
)
await writeFile(
entrypoint,
`
import { getVersion, getVersionInfo } from ${JSON.stringify(getVersionPath)}
import { getCliVersion } from ${JSON.stringify(helperPath)}
Object.assign(globalThis, { TSCIRCUIT_VERSION: "9.9.9" })
console.log(JSON.stringify({
cli: getVersionInfo(() => undefined).cliVersion,
currentCli: getCliVersion(),
wrapper: getVersion(),
}))
`,
)
const output = join(root, outputPath)
if (cachedBundle === undefined) {
const build = await Bun.build({
entrypoints: [entrypoint],
target: "node",
format: "esm",
})
expect(build.success).toBe(true)
const artifact = build.outputs[0]
if (!artifact) throw new TypeError("Missing version test bundle")
const text: string = await artifact.text()
cachedBundle = text
}
await mkdir(dirname(output), { recursive: true })
await writeFile(output, cachedBundle)
await writeFile(join(root, "dist", "package.json"), '{"type":"module"}')
const cwd = join(root, "unrelated-project")
await mkdir(cwd)
await writeFile(
join(cwd, "package.json"),
'{"name":"@tscircuit/cli","version":"8.8.8"}',
)
return { root, output, cwd }
}

const runBundle = async (fixture: Awaited<ReturnType<typeof createBundle>>) => {
const results: unknown[] = []
// Exercise the node-target bundle in both supported runtimes.
for (const executable of [process.execPath, "node"]) {
const result = Bun.spawnSync([executable, fixture.output], {
cwd: fixture.cwd,
})
expect(result.exitCode).toBe(0)
results.push(JSON.parse(result.stdout.toString()))
}
return results
}

for (const layout of ["dist/cli/main.js", "dist/lib/index.js"]) {
test(`${layout} reports the installed version when inline metadata is stale`, async () => {
// Given a bundle built before the installed manifest receives its new version.
const fixture = await createBundle(layout)
await writeFile(
join(fixture.root, "package.json"),
JSON.stringify({
name: "@tscircuit/cli",
version: "0.1.2021",
type: "module",
exports: { ".": "./dist/cli/main.js", "./lib": "./dist/lib/index.js" },
}),
)
// When the bundle runs from an unrelated package directory.
const results = await runBundle(fixture)
// Then CLI and wrapper versions remain distinct and no patch is guessed.
for (const result of results) {
expect(result).toEqual({
cli: "0.1.2021",
currentCli: "0.1.2021",
wrapper: "9.9.9",
})
}
})

for (const [description, manifest] of [
["missing", undefined],
["malformed JSON", "{"],
[
"wrong package name",
JSON.stringify({ name: "tscircuit", version: "8.8.8" }),
],
[
"invalid version",
JSON.stringify({ name: "@tscircuit/cli", version: "not-semver" }),
],
["missing version", JSON.stringify({ name: "@tscircuit/cli" })],
]) {
test(`${layout} uses the unchanged source fallback for ${description} metadata`, async () => {
// Given unavailable or invalid installed metadata and a different CWD manifest.
const fixture = await createBundle(layout)
if (manifest !== undefined)
await writeFile(join(fixture.root, "package.json"), manifest)
// When the actual bundle reads its own installation metadata.
const results = await runBundle(fixture)
// Then fallback uses the exact source version, not a guessed next release.
for (const result of results) {
expect(result).toEqual({
cli: sourceVersion,
currentCli: sourceVersion,
wrapper: "9.9.9",
})
}
})
}
}
9 changes: 9 additions & 0 deletions tests/lib/getVersion.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { afterEach, expect, test } from "bun:test"
import { getVersion, getVersionInfo } from "lib/getVersion"
import { version as sourceVersion } from "../../package.json"

type GlobalWithTscircuitVersion = typeof globalThis & {
TSCIRCUIT_VERSION?: string
Expand Down Expand Up @@ -41,3 +42,11 @@ test("getVersion verbose output prints all requested packages", () => {
expect(output).toContain("@tscircuit/runframe:")
expect(output).toContain("@tscircuit/core:")
})

test("getVersionInfo reports the source package version without guessing a patch", () => {
// Given the source checkout has its own current package version.
// When reading the CLI version independently of the wrapper package.
const { cliVersion } = getVersionInfo(() => undefined)
// Then the reported version matches the installed manifest exactly.
expect(cliVersion).toBe(sourceVersion)
})
Loading