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
17 changes: 16 additions & 1 deletion actions/dependency-cooldown/dist/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11235,8 +11235,19 @@ var NON_REGISTRY_PROTOCOLS = [
"git+",
"github:",
"http:",
"https:"
"https:",
"root:"
];
var LOCAL_TARBALL_EXTENSIONS = [".tgz", ".tar.gz", ".tar"];
function isLocalTarballSpecifier(specifier) {
if (specifier.startsWith("./") || specifier.startsWith("../") || specifier.startsWith("/")) {
return true;
}
return LOCAL_TARBALL_EXTENSIONS.some((extension) => specifier.endsWith(extension));
}
function isNpmRegistryTuple(entry) {
return typeof entry[1] === "string";
}
function splitDescriptor(descriptor) {
const separator = descriptor.indexOf("@", 1);
if (separator <= 0) {
Expand Down Expand Up @@ -11283,6 +11294,10 @@ var bunLockfile = {
uncheckable.push({ name, version: null, reason: `resolved via ${protocol}` });
continue;
}
if (!isNpmRegistryTuple(entry) && isLocalTarballSpecifier(specifier)) {
uncheckable.push({ name, version: null, reason: "resolved via local tarball" });
continue;
}
refs.push({ registry: "npm", name, version: specifier });
}
return { refs, uncheckable };
Expand Down
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions packages/dependency-cooldown/src/lockfiles/bun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ import type { DependencyRef, LockfileFormat, ParsedLockfile } from "../types.js"
* `"<key>": ["<name>@<version>", registry, meta, integrity]`. Non-registry
* dependencies use a descriptor such as `name@workspace:path` or
* `name@git+https://...` instead of a semver version.
*
* Local tarballs are a separate resolution type: Bun writes `name@./file.tgz`
* with no `file:` prefix (folders use `file:`, remote tarball URLs use
* `http:`/`https:`). Distinguishing them from registry versions must use the
* tuple, not the specifier suffix: npm prereleases such as `1.2.3-release.tgz`
* are valid, and Bun still stores them as `[name@version, registry, meta,
* integrity]` with a string registry field. Local tarballs put the metadata
* object in that slot: `[name@tarball, meta]`.
*/
const NON_REGISTRY_PROTOCOLS = [
"workspace:",
Expand All @@ -16,8 +24,23 @@ const NON_REGISTRY_PROTOCOLS = [
"github:",
"http:",
"https:",
"root:",
];

const LOCAL_TARBALL_EXTENSIONS = [".tgz", ".tar.gz", ".tar"];

function isLocalTarballSpecifier(specifier: string): boolean {
if (specifier.startsWith("./") || specifier.startsWith("../") || specifier.startsWith("/")) {
return true;
}
return LOCAL_TARBALL_EXTENSIONS.some((extension) => specifier.endsWith(extension));
}

/** npm resolutions store a registry URL (or `""` for the default) at index 1. */
function isNpmRegistryTuple(entry: unknown[]): boolean {
return typeof entry[1] === "string";
}

function splitDescriptor(descriptor: string): { name: string; specifier: string } {
// The separator is the first @ after index 0: a scoped name starts with @,
// and a git specifier may itself contain one (git+ssh://git@host/...).
Expand Down Expand Up @@ -73,6 +96,10 @@ export const bunLockfile: LockfileFormat = {
uncheckable.push({ name, version: null, reason: `resolved via ${protocol}` });
continue;
}
if (!isNpmRegistryTuple(entry) && isLocalTarballSpecifier(specifier)) {
uncheckable.push({ name, version: null, reason: "resolved via local tarball" });
continue;
}
refs.push({ registry: "npm", name, version: specifier });
}

Expand Down
30 changes: 30 additions & 0 deletions packages/dependency-cooldown/test/end-to-end.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,4 +386,34 @@ describe("audit mode", () => {

expect(code).toBe(0);
});

it("audits a bun.lock that pins a local tarball without querying the registry for it", async () => {
write(
"bun.lock",
`{
"lockfileVersion": 1,
"packages": {
"left-pad": ["left-pad@1.3.0", "", {}, "sha512-bbbb"],
"human-readable-checksum": ["human-readable-checksum@./package/human-readable-checksum-0.3.0.tgz", {}]
}
}`,
);
commit("base");

const inner = httpStub({});
const http: HttpClient = {
async getJson(url) {
if (url === "https://registry.npmjs.org/human-readable-checksum") {
throw new Error(`GET ${url} failed with HTTP 404 Not Found`);
}
return inner.getJson(url);
},
};

const code = await run(["--mode=audit", `--repo-dir=${repoDir}`], { http, now: NOW });

expect(code).toBe(0);
expect(summary()).toContain("human-readable-checksum");
expect(summary()).toContain("could not be age-checked");
});
});
87 changes: 87 additions & 0 deletions packages/dependency-cooldown/test/lockfiles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,32 @@ describe("npm package-lock.json", () => {
),
).toThrow(/lockfileVersion 1/);
});

it("reports a local tarball as uncheckable rather than treating it as a registry version", () => {
const parsed = npmLockfile.parse(
JSON.stringify({
lockfileVersion: 3,
packages: {
"": { name: "demo-app", version: "1.0.0" },
"node_modules/human-readable-checksum": {
version: "0.3.0",
resolved: "file:package/human-readable-checksum-0.3.0.tgz",
},
},
}),
"package-lock.json",
);

expect(parsed.refs).toEqual([]);
expect(parsed.uncheckable).toEqual([
{
name: "human-readable-checksum",
version: "0.3.0",
reason:
"not resolved from registry.npmjs.org (file:package/human-readable-checksum-0.3.0.tgz)",
},
]);
});
});

describe("bun.lock", () => {
Expand Down Expand Up @@ -90,6 +116,67 @@ describe("bun.lock", () => {
"workspace-lib",
]);
});

it.each(["1.2.3-release.tgz", "1.0.0-beta.tar.gz", "2.0.0-rc.tar"])(
"collects registry semver %s even though it ends with a tarball extension",
(version) => {
const parsed = bunLockfile.parse(
`{ "packages": { "pkg": ["pkg@${version}", "", {}, "sha512-abcd"] } }`,
"bun.lock",
);

expect(parsed.uncheckable).toEqual([]);
expect(parsed.refs).toEqual([{ registry: "npm", name: "pkg", version }]);
},
);

it("reports a relative local tarball as uncheckable rather than treating it as a registry version", () => {
const parsed = bunLockfile.parse(
'{ "packages": { "human-readable-checksum": ["human-readable-checksum@./package/human-readable-checksum-0.3.0.tgz", {}] } }',
"bun.lock",
);

expect(parsed.refs).toEqual([]);
expect(parsed.uncheckable).toEqual([
{
name: "human-readable-checksum",
version: null,
reason: "resolved via local tarball",
},
]);
});

it.each([
"../vendor/pkg-1.0.0.tgz",
"/opt/pkg-1.0.0.tgz",
"package/pkg-1.0.0.tgz",
"./1.2.3-release.tgz",
])(
"reports local tarball specifier %s as uncheckable",
(specifier) => {
const parsed = bunLockfile.parse(
`{ "packages": { "pkg": ["pkg@${specifier}", {}] } }`,
"bun.lock",
);

expect(parsed.refs).toEqual([]);
expect(parsed.uncheckable).toEqual([
{ name: "pkg", version: null, reason: "resolved via local tarball" },
]);
},
);

it("reports the lockfile root entry as uncheckable", () => {
const parsed = bunLockfile.parse(
'{ "packages": { "demo-app": ["demo-app@root:", { "bin": { "demo": "bin.js" } }] } }',
"bun.lock",
);

expect(parsed.refs).toEqual([]);
expect(parsed.uncheckable).toEqual([
{ name: "demo-app", version: null, reason: "resolved via root:" },
]);
});
});

describe("pubspec.lock", () => {
Expand Down
Loading