diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index d8c9402e2871..6bbcde2e85e9 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -9808,12 +9808,26 @@ declare module "bun" { * The structure of Bun's lockfile, `bun.lock` */ type BunLockFile = { - lockfileVersion: 0 | 1 | 2; + /** + * Version of the file format. A release of Bun that does not know a lockfile's version cannot read it. + * + * - `0`: workspace entries in {@link BunLockFile.packages} carry a trailing object with the workspace's dependencies. + * - `1`: workspace entries are `["name@workspace:path"]` only. + * - `2`: same content as `1`. Bun reads it more strictly, for example an npm package resolved to a tarball + * outside the configured registry must carry an integrity hash. + * - `3`: {@link BunLockFile.overrides} holds nested or version-scoped rules, see {@link BunLockFileScopedOverrides}. + */ + lockfileVersion: 0 | 1 | 2 | 3; workspaces: { [workspace: string]: BunLockFileWorkspacePackage; }; - /** @see https://bun.com/docs/install/overrides */ - overrides?: Record; + /** + * A string value applies to every dependency on the package named by the key. + * An object value holds the rules scoped to the package selected by the key, see {@link BunLockFileScopedOverrides}. + * + * @see https://bun.com/docs/install/overrides + */ + overrides?: Record; /** @see https://bun.com/docs/install/patch */ patchedDependencies?: Record; /** @see https://bun.com/docs/install/lifecycle#trusteddependencies */ @@ -9853,6 +9867,28 @@ declare module "bun" { }; }; + /** + * The object form of a {@link BunLockFile.overrides} value. Bun writes it for the nested + * (`"parent": { "child": "1.0.0" }`, `"parent>child"`) and version-scoped (`"pkg@1"`) rules of + * `package.json`. + * + * The key in `overrides` selects the package these rules are scoped to, as `name` or `name@range`. + * Inside the object, `"."` is the rule for the selected package itself and every other key, again + * `name` or `name@range`, is the rule for that dependency of the selected package. Every value is a + * dependency specifier: rules nest one level deep only. + * + * @example + * ```jsonc + * "overrides": { + * "no-deps": "1.0.0", + * "no-deps@1": { ".": "1.1.0" }, + * "one-dep": { "no-deps": "2.0.0" }, + * "one-range-dep": { "no-deps": "1.0.1", "no-deps@1": "2.0.0" }, + * } + * ``` + */ + type BunLockFileScopedOverrides = Record; + type BunLockFileBasePackageInfo = { dependencies?: Record; devDependencies?: Record; diff --git a/test/integration/bun-types/bun-types.test.ts b/test/integration/bun-types/bun-types.test.ts index 05fa801f45e5..d3b26032d4b8 100644 --- a/test/integration/bun-types/bun-types.test.ts +++ b/test/integration/bun-types/bun-types.test.ts @@ -400,6 +400,79 @@ describe("@types/bun integration test", () => { }); }); + describe("BunLockFile", () => { + test("accepts lockfileVersion 3 and the object form of overrides", async () => { + const checkDir = join(TEMP_DIR, "bun-lock-check"); + const tsconfig = structuredClone(sourceTsconfig); + tsconfig.include = ["bun-lock.ts"]; + tsconfig.compilerOptions.typeRoots = [join(BASE_FIXTURE_DIR, "node_modules", "@types")]; + await mkdir(checkDir, { recursive: true }); + await makeTree(checkDir, { + "tsconfig.json": JSON.stringify(tsconfig, null, 2), + // The overrides are what bun install writes for nested and version-scoped rules + // (test/cli/install/nested-overrides.test.ts). No bun.lock exists in checkDir: the + // import is typed by the "*/bun.lock" module declaration in extensions.d.ts. + "bun-lock.ts": `import type { BunLockFile, BunLockFileScopedOverrides } from "bun"; + import imported from "./bun.lock"; + + ({ + lockfileVersion: 3, + workspaces: { "": { name: "app", dependencies: { "one-dep": "1.0.0" } } }, + overrides: { + "no-deps": "1.0.0", + "no-deps@1": { ".": "1.1.0" }, + "one-dep": { "no-deps": "2.0.0" }, + "one-range-dep": { "no-deps": "1.0.1", "no-deps@1": "2.0.0" }, + }, + packages: {}, + }) satisfies BunLockFile; + + ({ ".": "1.1.0", "no-deps@1": "2.0.0" }) satisfies BunLockFileScopedOverrides; + + declare const parsed: BunLockFile; + if (parsed.lockfileVersion === 3) parsed.lockfileVersion satisfies 3; + if (imported.lockfileVersion === 3) imported.lockfileVersion satisfies 3; + + for (const rule of Object.values(parsed.overrides ?? {})) { + if (typeof rule === "string") continue; + rule satisfies BunLockFileScopedOverrides; + for (const specifier of Object.values(rule)) specifier satisfies string; + } + + ({ + lockfileVersion: 3, + workspaces: {}, + overrides: { + // @ts-expect-error rules nest one level deep only + "one-dep": { "no-deps": { ".": "2.0.0" } }, + }, + packages: {}, + }) satisfies BunLockFile; + + ({ + // @ts-expect-error lockfileVersion is a closed set of known versions, not number + lockfileVersion: 4, + workspaces: {}, + packages: {}, + }) satisfies BunLockFile;`, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), join(BASE_FIXTURE_DIR, "node_modules", "typescript", "bin", "tsc"), "-p", "."], + env: bunEnv, + cwd: checkDir, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stderr.trim()).toBe(""); + expect(stdout.trim()).toBe(""); + expect(exitCode).toBe(0); + }); + }); + describe("Test Globals", () => { const code = ` const test_shouldBeAFunction: Function = test;