chore: import upstream snapshot with attribution
Deploy (to testing) and Test Playground Preview Worker / Deploy Playground Preview Worker (testing) (push) Has been skipped
Deploy Workers Shared Staging / Deploy Workers Shared Staging (push) Failing after 0s
Prerelease / build (push) Has been skipped
Handle Changesets / Handle Changesets (push) Has been cancelled
Semgrep OSS scan / semgrep-oss (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:30:11 +08:00
commit 70bf21e064
5213 changed files with 731895 additions and 0 deletions
@@ -0,0 +1,33 @@
import { it } from "vitest";
import { getNextMiniflareVersion } from "../../../.github/changeset-version";
// prettier-ignore
const miniflareVersionTestCases = [
// workerd, mf previous, bump mf after, mf corrected
["1.20231001.0", "3.20231001.0", /* patch */ "3.20231001.1", "3.20231001.1"],
["1.20231001.0", "3.20231001.0", /* minor */ "3.20231002.0", "3.20231001.1"],
["1.20231002.0", "3.20231001.0", /* minor */ "3.20231002.0", "3.20231002.0"],
["1.20231001.0", "3.20231001.2", /* minor */ "3.20231002.0", "3.20231001.3"],
["1.20231001.0", "3.20231001.0", /* major */ "4.0.0", "4.20231001.0"],
["1.20231008.0", "3.20231001.0", /* patch */ "3.20231001.1", "3.20231008.0"],
["1.20231008.0", "3.20231001.0", /* minor */ "3.20231002.0", "3.20231008.0"],
["1.20231008.0", "3.20231001.0", /* major */ "4.0.0", "4.20231008.0"],
];
for (const [
workerdVersion,
previousMiniflareVersion,
miniflareVersion,
correctMiniflareVersion,
] of miniflareVersionTestCases) {
it(`changeset version ${workerdVersion} ${previousMiniflareVersion} -> ${miniflareVersion} = ${correctMiniflareVersion}`, ({
expect,
}) => {
const actual = getNextMiniflareVersion(
workerdVersion,
previousMiniflareVersion,
miniflareVersion
);
expect(actual).toEqual(correctMiniflareVersion);
});
}
@@ -0,0 +1,176 @@
import { spawnSync } from "node:child_process";
import { afterEach, describe, it, vitest } from "vitest";
import {
deployNonNpmPackages,
deployPackage,
findDeployablePackageNames,
getUpdatedPackages,
} from "../deploy-non-npm-packages";
import type { UpdatedPackage } from "../deploy-non-npm-packages";
import type { Mock } from "vitest";
vitest.mock("node:child_process", async () => {
return {
spawnSync: vitest.fn(),
};
});
describe("getUpdatedPackages()", () => {
const originalEnv = process.env;
afterEach(() => {
process.env = originalEnv;
});
it("should default to an empty array", ({ expect }) => {
expect(getUpdatedPackages()).toEqual([]);
});
it("should parse JSON from the PUBLISHED_PACKAGES env var", ({ expect }) => {
const expectedPackages = [
{ name: "a", version: "1.0.0" },
{ name: "b", version: "2.3.4" },
];
process.env = {
PUBLISHED_PACKAGES: JSON.stringify(expectedPackages),
};
expect(getUpdatedPackages()).toEqual(expectedPackages);
});
it("should validate the shape of the PUBLISHED_PACKAGES JSON", ({
expect,
}) => {
process.env = {
PUBLISHED_PACKAGES: `"bad"`,
};
expect(() => getUpdatedPackages()).toThrowErrorMatchingInlineSnapshot(
`[AssertionError: Expected PUBLISHED_PACKAGES to be an array but got string.]`
);
process.env = {
PUBLISHED_PACKAGES: `["bad"]`,
};
expect(() => getUpdatedPackages()).toThrowErrorMatchingInlineSnapshot(
`[AssertionError: Expected item 0 in array to be an array but got string.]`
);
process.env = {
PUBLISHED_PACKAGES: `[{}]`,
};
expect(() => getUpdatedPackages()).toThrowErrorMatchingInlineSnapshot(
`[AssertionError: Expected item 0 to have a "name" property of type string but got undefined.]`
);
process.env = {
PUBLISHED_PACKAGES: `[{ "name": 123 }]`,
};
expect(() => getUpdatedPackages()).toThrowErrorMatchingInlineSnapshot(
`[AssertionError: Expected item 0 to have a "name" property of type string but got 123.]`
);
process.env = {
PUBLISHED_PACKAGES: `[{ "name": "package" }]`,
};
expect(() => getUpdatedPackages()).toThrowErrorMatchingInlineSnapshot(
`[AssertionError: Expected item 0 to have a "version" property of type string but got undefined.]`
);
process.env = {
PUBLISHED_PACKAGES: `[{ "name": "package", "version": ["bad"] }]`,
};
expect(() => getUpdatedPackages()).toThrowErrorMatchingInlineSnapshot(
`[AssertionError: Expected item 0 to have a "version" property of type string but got bad.]`
);
process.env = {
PUBLISHED_PACKAGES: `[{ "name": "package", "version": "1.2.3" }, {}]`,
};
expect(() => getUpdatedPackages()).toThrowErrorMatchingInlineSnapshot(
`[AssertionError: Expected item 1 to have a "name" property of type string but got undefined.]`
);
});
});
describe("findDeployablePackageNames()", () => {
it("should return all the private packages which contain deploy scripts", ({
expect,
}) => {
expect(findDeployablePackageNames()).toMatchInlineSnapshot(`
Set {
"@cloudflare/chrome-devtools-patches",
"@cloudflare/devprod-status-bot",
"@cloudflare/edge-preview-authenticated-proxy",
"@cloudflare/format-errors",
"@cloudflare/playground-preview-worker",
"@cloudflare/quick-edit",
"@cloudflare/turbo-r2-archive",
"@cloudflare/workers-playground",
"@cloudflare/workers-shared",
}
`);
});
});
describe("deployPackage", () => {
it("should run `pnpm deploy` for the given package via `spawnSync`", ({
expect,
}) => {
deployPackage("foo", new Map());
expect(spawnSync).toHaveBeenCalledWith(
"pnpm",
["-F", "foo", "run", "deploy"],
expect.any(Object)
);
});
it("should ignore failures in `spawnSync`", ({ expect }) => {
(spawnSync as Mock).mockImplementationOnce(() => {
throw new Error("Bad deployment");
});
const logs: string[] = [];
vitest.spyOn(console, "error").mockImplementation((v) => logs.push(v));
deployPackage("foo", new Map());
expect(logs[0]).toMatchInlineSnapshot(`"::error::Failed to deploy "foo"."`);
});
});
describe("deployNonNpmPackages()", () => {
it("should run `pnpm deploy` on each deployable updated package", ({
expect,
}) => {
const updatedPackages: UpdatedPackage[] = [
{ name: "a", version: "1.0.0" },
{ name: "b", version: "2.0.4" },
{ name: "c", version: "3.0.0" },
{ name: "d", version: "4.0.0" },
];
const deployablePackageNames = new Set(["a", "c", "e"]);
vitest.spyOn(console, "log").mockImplementation(() => {});
deployNonNpmPackages(updatedPackages, deployablePackageNames);
expect((console.log as Mock).mock.calls.flat()).toMatchInlineSnapshot(`
[
"Checking for non-npm packages to deploy...",
"Package "a@1.0.0": deploying...",
"Package "b@2.0.4": already deployed via npm.",
"Package "c@3.0.0": deploying...",
"Package "d@4.0.0": already deployed via npm.",
"Deployed 2 non-npm packages.",
]
`);
});
it("should run display an informative message if no packages to deploy", ({
expect,
}) => {
const updatedPackages: UpdatedPackage[] = [];
const deployablePackageNames = new Set(["a", "c", "e"]);
vitest.spyOn(console, "log").mockImplementation(() => {});
deployNonNpmPackages(updatedPackages, deployablePackageNames);
expect((console.log as Mock).mock.calls.flat()).toMatchInlineSnapshot(`
[
"Checking for non-npm packages to deploy...",
"No non-npm packages to deploy.",
]
`);
});
});
@@ -0,0 +1,237 @@
import {
existsSync,
mkdtempSync,
realpathSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import dedent from "ts-dedent";
import { afterEach, beforeEach, describe, it } from "vitest";
import {
findPackages,
readChangesets,
validateChangesets,
} from "../validate-changesets";
import type { PackageJSON } from "../validate-changesets";
describe("findPackageNames()", () => {
it("should return all the private packages which contain deploy scripts", ({
expect,
}) => {
expect(new Set(findPackages().keys())).toEqual(
new Set([
"@cloudflare/autoconfig",
"@cloudflare/chrome-devtools-patches",
"@cloudflare/cli-shared-helpers",
"@cloudflare/codemod",
"@cloudflare/containers-shared",
"@cloudflare/deploy-helpers",
"@cloudflare/devprod-status-bot",
"@cloudflare/edge-preview-authenticated-proxy",
"@cloudflare/lint-config-shared",
"@cloudflare/format-errors",
"@cloudflare/kv-asset-handler",
"@cloudflare/local-explorer-ui",
"@cloudflare/pages-shared",
"@cloudflare/playground-preview-worker",
"@cloudflare/quick-edit",
"@cloudflare/turbo-r2-archive",
"@cloudflare/unenv-preset",
"@cloudflare/vite-plugin",
"@cloudflare/vitest-pool-workers",
"@cloudflare/workers-auth",
"@cloudflare/workers-editor-shared",
"@cloudflare/workers-playground",
"@cloudflare/workers-shared",
"@cloudflare/workers-utils",
"@cloudflare/workflows-shared",
"create-cloudflare",
"miniflare",
"solarflare-theme",
"wrangler",
])
);
});
});
describe("readChangesets()", () => {
let tmpDir: string;
beforeEach(() => {
// Use realpath because the temporary path can point to a symlink rather than the actual path.
tmpDir = realpathSync(mkdtempSync(join(tmpdir(), "tools-tests")));
});
afterEach(() => {
if (existsSync(tmpDir)) {
// eslint-disable-next-line workers-sdk/no-direct-recursive-rm -- test cleanup
rmSync(tmpDir, { recursive: true });
}
});
it("should load files from the changeset directory that look like changesets", ({
expect,
}) => {
writeFileSync(resolve(tmpDir, "README.md"), "Some text");
writeFileSync(resolve(tmpDir, ".hidden.md"), "Some text");
writeFileSync(resolve(tmpDir, "change-set-one.md"), "Some text");
writeFileSync(resolve(tmpDir, "change-set-two.md"), "Some text");
writeFileSync(resolve(tmpDir, "change-set-two.md"), "Some text");
writeFileSync(resolve(tmpDir, "config.json"), "Some text");
const changesets = readChangesets(tmpDir);
expect(changesets).toMatchInlineSnapshot(`
[
{
"contents": "Some text",
"file": "change-set-one.md",
},
{
"contents": "Some text",
"file": "change-set-two.md",
},
]
`);
});
});
describe("validateChangesets()", () => {
it("should report errors for any invalid changesets", ({ expect }) => {
const errors = validateChangesets(
new Map<string, PackageJSON>([
["package-a", { name: "package-a" }],
["package-b", { name: "package-b" }],
["package-c", { name: "package-c" }],
]),
[
{
file: "valid-one.md",
contents: dedent`
---
"package-a": patch
---
refactor: test`,
},
{
file: "valid-two.md",
contents: dedent`
---
"package-b": minor
---
feature: test`,
},
{
file: "valid-three.md",
contents: dedent`
---
"package-c": minor
---
chore: test`,
},
{
file: "valid-three.md",
contents: dedent`
---
"package-c": minor
---
fix: test`,
},
{ file: "invalid-frontmatter.md", contents: "" },
{
file: "invalid-package.md",
contents: dedent`
---
"package-invalid": minor
---
feat: test`,
},
{
file: "invalid-type.md",
contents: dedent`
---
"package-a": foo
---
docs: test`,
},
]
);
expect(errors).toMatchInlineSnapshot(`
[
"Error: could not parse changeset - invalid frontmatter: at file "invalid-frontmatter.md"",
"Unknown package name "package-invalid" in changeset at "invalid-package.md".",
"Invalid type "foo" for package "package-a" in changeset at "invalid-type.md".",
]
`);
});
it("should allow major bumps for private packages", ({ expect }) => {
const errors = validateChangesets(
new Map<string, PackageJSON>([
["package-b", { name: "package-b", private: true }],
]),
[
{
file: "major-private.md",
contents: dedent`
---
"package-b": major
---
breaking change for private pkg!`,
},
]
);
expect(errors).toMatchInlineSnapshot(`[]`);
});
it("should report errors for major bump changesets", ({ expect }) => {
const errors = validateChangesets(
new Map<string, PackageJSON>([
["package-a", { name: "package-a" }],
["package-b", { name: "package-b" }],
["package-c", { name: "package-c" }],
]),
[
{
file: "patch-one.md",
contents: dedent`
---
"package-a": patch
---
refactor: test`,
},
{
file: "minor-two.md",
contents: dedent`
---
"package-b": minor
---
feature: test`,
},
{
file: "major-three.md",
contents: dedent`
---
"package-c": major
---
breaking change!`,
},
]
);
expect(errors).toMatchInlineSnapshot(`
[
"Major version bumps are not allowed for package "package-c" in changeset at "major-three.md".",
]
`);
});
});
@@ -0,0 +1,737 @@
import { describe, it } from "vitest";
import {
extractBareImports,
getAllDependencies,
getEntryPointPaths,
getNonWorkspaceDependencies,
getPackageNameFromSpecifier,
getPublicPackages,
isBareSpecifier,
validateDistImports,
validatePackageDependencies,
} from "../validate-package-dependencies";
describe("getAllDependencies()", () => {
it("should return empty array for undefined", ({ expect }) => {
expect(getAllDependencies(undefined)).toEqual([]);
});
it("should return empty array for empty object", ({ expect }) => {
expect(getAllDependencies({})).toEqual([]);
});
it("should return all dependency names", ({ expect }) => {
expect(
getAllDependencies({
foo: "1.0.0",
bar: "workspace:*",
baz: "^2.0.0",
})
).toEqual(["foo", "bar", "baz"]);
});
});
describe("getNonWorkspaceDependencies()", () => {
it("should return empty array for undefined", ({ expect }) => {
expect(getNonWorkspaceDependencies(undefined)).toEqual([]);
});
it("should return empty array for empty object", ({ expect }) => {
expect(getNonWorkspaceDependencies({})).toEqual([]);
});
it("should filter out workspace dependencies", ({ expect }) => {
expect(
getNonWorkspaceDependencies({
foo: "1.0.0",
bar: "workspace:*",
baz: "workspace:^",
qux: "^2.0.0",
})
).toEqual(["foo", "qux"]);
});
it("should return all deps if none are workspace deps", ({ expect }) => {
expect(
getNonWorkspaceDependencies({
foo: "1.0.0",
bar: "^2.0.0",
})
).toEqual(["foo", "bar"]);
});
});
describe("validatePackageDependencies()", () => {
it("should return no errors for package with no dependencies", ({
expect,
}) => {
const errors = validatePackageDependencies(
"test-package",
"test-package",
{ name: "test-package" },
null
);
expect(errors).toEqual([]);
});
it("should return no errors for package with only workspace dependencies", ({
expect,
}) => {
const errors = validatePackageDependencies(
"test-package",
"test-package",
{
name: "test-package",
dependencies: {
"@cloudflare/foo": "workspace:*",
"@cloudflare/bar": "workspace:^",
},
},
null
);
expect(errors).toEqual([]);
});
it("should return error when package has non-workspace deps but no allowlist", ({
expect,
}) => {
const errors = validatePackageDependencies(
"test-package",
"test-package",
{
name: "test-package",
dependencies: {
lodash: "^4.0.0",
zod: "^3.0.0",
},
},
null
);
expect(errors).toHaveLength(1);
expect(errors[0]).toContain('Package "test-package"');
expect(errors[0]).toContain("2 non-workspace dependencies");
expect(errors[0]).toContain("no scripts/deps.ts file");
expect(errors[0]).toContain("lodash, zod");
});
it("should return no errors when all deps are in allowlist", ({ expect }) => {
const errors = validatePackageDependencies(
"test-package",
"test-package",
{
name: "test-package",
dependencies: {
lodash: "^4.0.0",
zod: "^3.0.0",
},
},
["lodash", "zod"]
);
expect(errors).toEqual([]);
});
it("should return error for dependency not in allowlist", ({ expect }) => {
const errors = validatePackageDependencies(
"test-package",
"test-package",
{
name: "test-package",
dependencies: {
lodash: "^4.0.0",
zod: "^3.0.0",
"new-dep": "^1.0.0",
},
},
["lodash", "zod"]
);
expect(errors).toHaveLength(1);
expect(errors[0]).toContain('Package "test-package"');
expect(errors[0]).toContain('"new-dep"');
expect(errors[0]).toContain("not listed in EXTERNAL_DEPENDENCIES");
});
it("should return error for stale allowlist entry", ({ expect }) => {
const errors = validatePackageDependencies(
"test-package",
"test-package",
{
name: "test-package",
dependencies: {
lodash: "^4.0.0",
},
},
["lodash", "removed-dep"]
);
expect(errors).toHaveLength(1);
expect(errors[0]).toContain('Package "test-package"');
expect(errors[0]).toContain('"removed-dep"');
expect(errors[0]).toContain("not in dependencies or peerDependencies");
});
it("should allow workspace deps in EXTERNAL_DEPENDENCIES without error", ({
expect,
}) => {
const errors = validatePackageDependencies(
"test-package",
"test-package",
{
name: "test-package",
dependencies: {
lodash: "^4.0.0",
miniflare: "workspace:*",
},
},
["lodash", "miniflare"]
);
expect(errors).toEqual([]);
});
it("should check peerDependencies for stale allowlist entries", ({
expect,
}) => {
const errors = validatePackageDependencies(
"test-package",
"test-package",
{
name: "test-package",
dependencies: {
lodash: "^4.0.0",
},
peerDependencies: {
vite: "^5.0.0",
},
},
["lodash", "vite"]
);
expect(errors).toEqual([]);
});
it("should return multiple errors for multiple issues", ({ expect }) => {
const errors = validatePackageDependencies(
"test-package",
"test-package",
{
name: "test-package",
dependencies: {
lodash: "^4.0.0",
"undeclared-dep": "^1.0.0",
},
},
["lodash", "stale-dep"]
);
expect(errors).toHaveLength(2);
expect(errors[0]).toContain('"undeclared-dep"');
expect(errors[1]).toContain('"stale-dep"');
});
it("should ignore devDependencies completely", ({ expect }) => {
const errors = validatePackageDependencies(
"test-package",
"test-package",
{
name: "test-package",
devDependencies: {
vitest: "^1.0.0",
typescript: "^5.0.0",
esbuild: "^0.20.0",
},
},
null // No allowlist needed since devDependencies are ignored
);
expect(errors).toEqual([]);
});
it("should ignore devDependencies when validating against allowlist", ({
expect,
}) => {
const errors = validatePackageDependencies(
"test-package",
"test-package",
{
name: "test-package",
dependencies: {
lodash: "^4.0.0",
},
devDependencies: {
// These should NOT trigger "not in allowlist" errors
vitest: "^1.0.0",
typescript: "^5.0.0",
"some-dev-tool": "^1.0.0",
},
},
["lodash"] // Only lodash in allowlist, devDependencies should be ignored
);
expect(errors).toEqual([]);
});
it("should not require devDependencies to be in allowlist", ({ expect }) => {
const errors = validatePackageDependencies(
"test-package",
"test-package",
{
name: "test-package",
dependencies: {
lodash: "^4.0.0",
zod: "^3.0.0",
},
devDependencies: {
// Many devDependencies that are NOT in the allowlist
vitest: "^1.0.0",
typescript: "^5.0.0",
esbuild: "^0.20.0",
prettier: "^3.0.0",
eslint: "^8.0.0",
},
},
["lodash", "zod"] // Only runtime deps in allowlist
);
// Should pass - devDependencies don't need to be in allowlist
expect(errors).toEqual([]);
});
it("should not count devDependencies as stale allowlist entries", ({
expect,
}) => {
// If a package has something in devDependencies AND in the allowlist,
// it should be flagged as stale (since devDeps are bundled, not external)
const errors = validatePackageDependencies(
"test-package",
"test-package",
{
name: "test-package",
dependencies: {
lodash: "^4.0.0",
},
devDependencies: {
// esbuild is in devDependencies (will be bundled)
esbuild: "^0.20.0",
},
},
["lodash", "esbuild"] // esbuild in allowlist but only in devDeps = stale
);
expect(errors).toHaveLength(1);
expect(errors[0]).toContain('"esbuild"');
expect(errors[0]).toContain("not in dependencies or peerDependencies");
});
});
describe("getPackageNameFromSpecifier()", () => {
it("should return the package name from a bare specifier", ({ expect }) => {
expect(getPackageNameFromSpecifier("lodash")).toBe("lodash");
});
it("should strip subpath imports", ({ expect }) => {
expect(getPackageNameFromSpecifier("lodash/fp")).toBe("lodash");
expect(getPackageNameFromSpecifier("semver/functions/satisfies.js")).toBe(
"semver"
);
});
it("should preserve scoped package names", ({ expect }) => {
expect(getPackageNameFromSpecifier("@cloudflare/workers-utils")).toBe(
"@cloudflare/workers-utils"
);
expect(
getPackageNameFromSpecifier("@cloudflare/workers-utils/test-helpers")
).toBe("@cloudflare/workers-utils");
});
});
describe("isBareSpecifier()", () => {
it("should accept bare package names", ({ expect }) => {
expect(isBareSpecifier("lodash")).toBe(true);
expect(isBareSpecifier("@cloudflare/workers-utils")).toBe(true);
expect(isBareSpecifier("vitest/runtime")).toBe(true);
});
it("should reject relative paths", ({ expect }) => {
expect(isBareSpecifier("./foo")).toBe(false);
expect(isBareSpecifier("../bar")).toBe(false);
expect(isBareSpecifier("/abs/path")).toBe(false);
});
it("should reject empty specifiers", ({ expect }) => {
expect(isBareSpecifier("")).toBe(false);
});
it("should reject Node.js built-ins (with and without prefix)", ({
expect,
}) => {
expect(isBareSpecifier("node:fs")).toBe(false);
expect(isBareSpecifier("fs")).toBe(false);
expect(isBareSpecifier("node:child_process")).toBe(false);
expect(isBareSpecifier("child_process")).toBe(false);
expect(isBareSpecifier("assert")).toBe(false);
});
it("should reject Cloudflare/workerd built-ins", ({ expect }) => {
expect(isBareSpecifier("cloudflare:workers")).toBe(false);
expect(isBareSpecifier("workerd:unsafe")).toBe(false);
});
it("should reject bundler virtual modules", ({ expect }) => {
expect(isBareSpecifier("virtual:react-router")).toBe(false);
expect(isBareSpecifier("wrangler:modules-watch")).toBe(false);
});
it("should reject ALL_CAPS virtual modules", ({ expect }) => {
expect(isBareSpecifier("__VITEST_POOL_WORKERS_DEFINES")).toBe(false);
expect(isBareSpecifier("__BUILD_CONFIG")).toBe(false);
});
it("should reject specifiers that are not package-name shaped", ({
expect,
}) => {
expect(isBareSpecifier(",")).toBe(false);
expect(isBareSpecifier("some random text")).toBe(false);
expect(isBareSpecifier("ASSERT.IN.MIXED-CASE")).toBe(false);
});
});
describe("extractBareImports()", () => {
it("should extract named imports", async ({ expect }) => {
const imports = await extractBareImports(`import { x } from "lodash";`);
expect([...imports]).toEqual(["lodash"]);
});
it("should extract default imports", async ({ expect }) => {
const imports = await extractBareImports(`import x from "lodash";`);
expect([...imports]).toEqual(["lodash"]);
});
it("should extract namespace imports", async ({ expect }) => {
const imports = await extractBareImports(`import * as x from "lodash";`);
expect([...imports]).toEqual(["lodash"]);
});
it("should extract side-effect imports", async ({ expect }) => {
const imports = await extractBareImports(`import "lodash";`);
expect([...imports]).toEqual(["lodash"]);
});
it("should extract re-exports", async ({ expect }) => {
const imports = await extractBareImports(`export { x } from "lodash";`);
expect([...imports]).toEqual(["lodash"]);
});
it("should extract require() calls", async ({ expect }) => {
const imports = await extractBareImports(
`const x = require("lodash"); require("foo");`
);
expect([...imports].sort()).toEqual(["foo", "lodash"]);
});
it("should extract dynamic import() calls with string literals", async ({
expect,
}) => {
const imports = await extractBareImports(
`const x = await import("lodash");`
);
expect([...imports]).toEqual(["lodash"]);
});
it("should strip subpath imports down to package name", async ({
expect,
}) => {
const imports = await extractBareImports(
`import x from "semver/functions/satisfies.js";`
);
expect([...imports]).toEqual(["semver"]);
});
it("should skip relative imports", async ({ expect }) => {
const imports = await extractBareImports(
`import x from "./foo"; import y from "../bar";`
);
expect([...imports]).toEqual([]);
});
it("should skip Node built-in imports", async ({ expect }) => {
const imports = await extractBareImports(
`import fs from "node:fs"; const path = require("node:path");`
);
expect([...imports]).toEqual([]);
});
it("should skip Cloudflare/workerd built-in imports", async ({ expect }) => {
const imports = await extractBareImports(
`import { env } from "cloudflare:workers"; import x from "workerd:unsafe";`
);
expect([...imports]).toEqual([]);
});
it("should skip imports inside line comments", async ({ expect }) => {
const imports = await extractBareImports(
`// import x from "lodash";\nimport y from "react";`
);
expect([...imports]).toEqual(["react"]);
});
it("should skip imports inside block comments", async ({ expect }) => {
const imports = await extractBareImports(
`/* import x from "lodash"; */\nimport y from "react";`
);
expect([...imports]).toEqual(["react"]);
});
it("should skip imports inside template literals (regression: createViteConfig)", async ({
expect,
}) => {
// Regression test: wrangler's createViteConfig bundles the literal text
// of a vite.config.ts into a template literal. The regex-based scanner
// used to match `import { defineConfig } from "vite"` as a real import.
const imports = await extractBareImports(
[
"function createViteConfig() {",
' const content = `import { cloudflare } from "@cloudflare/vite-plugin";',
'import { defineConfig } from "vite";',
"",
"export default defineConfig({",
"\tplugins: [cloudflare()],",
"});",
"`;",
" return content;",
"}",
'import real from "react";',
].join("\n")
);
expect([...imports]).toEqual(["react"]);
});
it("should skip imports inside single- and double-quoted strings", async ({
expect,
}) => {
const imports = await extractBareImports(
[
'const a = "import { x } from \\"lodash\\";";',
"const b = 'import { y } from \"react\";';",
'import real from "commander";',
].join("\n")
);
expect([...imports]).toEqual(["commander"]);
});
it("should not match `from` keywords that aren't part of import/export", async ({
expect,
}) => {
const imports = await extractBareImports(
`function foo() { return "hello"; } const z = "from";`
);
expect([...imports]).toEqual([]);
});
it("should deduplicate imports", async ({ expect }) => {
const imports = await extractBareImports(
`import { x } from "lodash";\nimport { y } from "lodash";`
);
expect([...imports]).toEqual(["lodash"]);
});
it("should handle multiple imports in one file", async ({ expect }) => {
const imports = await extractBareImports(`
import { x } from "lodash";
import y from "react";
import "polyfill";
const z = require("commander");
`);
expect([...imports].sort()).toEqual([
"commander",
"lodash",
"polyfill",
"react",
]);
});
});
describe("validateDistImports()", () => {
it("should pass when all imports are declared dependencies", ({ expect }) => {
const errors = validateDistImports(
"test-package",
{
name: "test-package",
dependencies: { lodash: "^4.0.0", react: "^18.0.0" },
},
new Set(["lodash", "react"])
);
expect(errors).toEqual([]);
});
it("should pass when imports are peerDependencies", ({ expect }) => {
const errors = validateDistImports(
"test-package",
{
name: "test-package",
peerDependencies: { vitest: "^4.0.0" },
},
new Set(["vitest"])
);
expect(errors).toEqual([]);
});
it("should allow self-imports without error", ({ expect }) => {
const errors = validateDistImports(
"my-package",
{ name: "my-package" },
new Set(["my-package"])
);
expect(errors).toEqual([]);
});
it("should flag devDependency-only imports with a tailored message", ({
expect,
}) => {
const errors = validateDistImports(
"test-package",
{
name: "test-package",
devDependencies: { undici: "^7.0.0" },
},
new Set(["undici"])
);
expect(errors).toHaveLength(1);
expect(errors[0]).toContain('"undici"');
expect(errors[0]).toContain("only a devDependency");
expect(errors[0]).toContain("IGNORED_DIST_IMPORTS");
});
it("should flag undeclared imports", ({ expect }) => {
const errors = validateDistImports(
"test-package",
{ name: "test-package" },
new Set(["mystery-package"])
);
expect(errors).toHaveLength(1);
expect(errors[0]).toContain('"mystery-package"');
expect(errors[0]).toContain(
"not declared in dependencies or peerDependencies"
);
});
it("should skip imports in the ignored list", ({ expect }) => {
const errors = validateDistImports(
"test-package",
{
name: "test-package",
devDependencies: { "@netlify/build-info": "^1.0.0" },
},
new Set(["react-router", "@angular/ssr"]),
["react-router", "@angular/ssr"]
);
expect(errors).toEqual([]);
});
it("should still flag non-ignored imports when ignored list is non-empty", ({
expect,
}) => {
const errors = validateDistImports(
"test-package",
{
name: "test-package",
devDependencies: { undici: "^7.0.0" },
},
new Set(["undici", "react-router"]),
["react-router"]
);
expect(errors).toHaveLength(1);
expect(errors[0]).toContain('"undici"');
});
});
describe("getEntryPointPaths()", () => {
it("should collect main field", ({ expect }) => {
const paths = getEntryPointPaths({
name: "p",
main: "dist/index.js",
});
expect(paths).toEqual(["dist/index.js"]);
});
it("should collect module field", ({ expect }) => {
const paths = getEntryPointPaths({
name: "p",
module: "dist/index.mjs",
});
expect(paths).toEqual(["dist/index.mjs"]);
});
it("should walk nested exports object", ({ expect }) => {
const paths = getEntryPointPaths({
name: "p",
exports: {
".": {
import: "./dist/index.mjs",
require: "./dist/index.cjs",
types: "./dist/index.d.ts",
},
"./test-helpers": {
import: "./dist/test-helpers/index.mjs",
},
},
});
expect(paths.sort()).toEqual([
"./dist/index.cjs",
"./dist/index.d.ts",
"./dist/index.mjs",
"./dist/test-helpers/index.mjs",
]);
});
it("should collect string bin entries", ({ expect }) => {
const paths = getEntryPointPaths({
name: "p",
bin: "./bin/cli.js",
});
expect(paths).toEqual(["./bin/cli.js"]);
});
it("should collect object bin entries", ({ expect }) => {
const paths = getEntryPointPaths({
name: "p",
bin: { foo: "./bin/foo.js", bar: "./bin/bar.js" },
});
expect(paths.sort()).toEqual(["./bin/bar.js", "./bin/foo.js"]);
});
it("should deduplicate paths across fields", ({ expect }) => {
const paths = getEntryPointPaths({
name: "p",
main: "./dist/index.js",
module: "./dist/index.js",
exports: { ".": "./dist/index.js" },
});
expect(paths).toEqual(["./dist/index.js"]);
});
it("should return empty array when no entry points are declared", ({
expect,
}) => {
const paths = getEntryPointPaths({ name: "p" });
expect(paths).toEqual([]);
});
});
describe("getPublicPackages()", () => {
it("should return only non-private packages", async ({ expect }) => {
const packages = await getPublicPackages();
// All returned packages should be non-private
for (const pkg of packages) {
expect(pkg.packageJson.private).not.toBe(true);
}
// Should include known public packages
const packageNames = packages.map((p) => p.packageJson.name);
expect(packageNames).toContain("wrangler");
expect(packageNames).toContain("miniflare");
expect(packageNames).toContain("create-cloudflare");
expect(packageNames).toContain("@cloudflare/autoconfig");
});
it("should not include private packages", async ({ expect }) => {
const packages = await getPublicPackages();
const packageNames = packages.map((p) => p.packageJson.name);
// These are known private packages
expect(packageNames).not.toContain("@cloudflare/workers-shared");
});
});
@@ -0,0 +1,198 @@
import { describe, it } from "vitest";
import { parseCatalog } from "../validate-catalog-usage";
import {
isPinnedVersion,
validateCatalogPins,
validatePackagePins,
} from "../validate-pinned-dependencies";
describe("isPinnedVersion()", () => {
it("should accept exact versions", ({ expect }) => {
expect(isPinnedVersion("1.2.3")).toBe(true);
expect(isPinnedVersion("0.0.14")).toBe(true);
expect(isPinnedVersion("1.20260529.1")).toBe(true);
});
it("should accept exact prerelease and build versions", ({ expect }) => {
expect(isPinnedVersion("4.1.0-beta.10")).toBe(true);
expect(isPinnedVersion("2.0.0-rc.24")).toBe(true);
expect(isPinnedVersion("1.2.3+build.5")).toBe(true);
});
it("should reject caret and tilde ranges", ({ expect }) => {
expect(isPinnedVersion("^1.2.3")).toBe(false);
expect(isPinnedVersion("~5.8.3")).toBe(false);
});
it("should reject comparator ranges", ({ expect }) => {
expect(isPinnedVersion(">1.20260305.0 <2.0.0-0")).toBe(false);
expect(isPinnedVersion(">=1.0.0")).toBe(false);
expect(isPinnedVersion("^6.1.0 || ^7.0.0 || ^8.0.0")).toBe(false);
});
it("should reject wildcards and partial versions", ({ expect }) => {
expect(isPinnedVersion("*")).toBe(false);
expect(isPinnedVersion("1.x")).toBe(false);
expect(isPinnedVersion("1.2")).toBe(false);
expect(isPinnedVersion("1")).toBe(false);
});
it("should reject non-version specifiers", ({ expect }) => {
expect(isPinnedVersion("")).toBe(false);
expect(isPinnedVersion("latest")).toBe(false);
expect(isPinnedVersion("workspace:*")).toBe(false);
expect(isPinnedVersion("catalog:default")).toBe(false);
});
});
describe("validateCatalogPins()", () => {
it("should pass when all entries are pinned", ({ expect }) => {
const errors = validateCatalogPins(
new Map([
["undici", "7.24.8"],
["esbuild", "0.28.1"],
["youch", "4.1.0-beta.10"],
])
);
expect(errors).toEqual([]);
});
it("should flag ranged entries", ({ expect }) => {
const errors = validateCatalogPins(
new Map([
["undici", "7.24.8"],
["ci-info", "^4.4.0"],
["typescript", "~5.8.3"],
])
);
expect(errors).toHaveLength(2);
expect(errors[0]).toContain('"ci-info"');
expect(errors[0]).toContain('"^4.4.0"');
expect(errors[1]).toContain('"typescript"');
});
it("should skip entries in the exceptions allowlist", ({ expect }) => {
const errors = validateCatalogPins(
new Map([["@cloudflare/workers-types", "^4.20260529.1"]]),
new Set(["@cloudflare/workers-types"])
);
expect(errors).toEqual([]);
});
it("should still flag non-excepted ranged entries", ({ expect }) => {
const errors = validateCatalogPins(
new Map([
["@cloudflare/workers-types", "^4.20260529.1"],
["vite", "^8.0.12"],
]),
new Set(["@cloudflare/workers-types"])
);
expect(errors).toHaveLength(1);
expect(errors[0]).toContain('"vite"');
});
it("should use the default exceptions allowlist", ({ expect }) => {
const errors = validateCatalogPins(
new Map([["@cloudflare/workers-types", "^4.20260529.1"]])
);
expect(errors).toEqual([]);
});
});
describe("validatePackagePins()", () => {
it("should pass when all dependencies are pinned", ({ expect }) => {
const errors = validatePackagePins("test-package", "test-package", {
name: "test-package",
dependencies: { "blake3-wasm": "2.1.5", "path-to-regexp": "6.3.0" },
});
expect(errors).toEqual([]);
});
it("should flag ranged dependencies", ({ expect }) => {
const errors = validatePackagePins("test-package", "test-package", {
name: "test-package",
dependencies: { sharp: "^0.34.5" },
});
expect(errors).toHaveLength(1);
expect(errors[0]).toContain('"sharp"');
expect(errors[0]).toContain('"^0.34.5"');
expect(errors[0]).toContain("dependencies");
});
it("should flag ranged optionalDependencies", ({ expect }) => {
const errors = validatePackagePins("test-package", "test-package", {
name: "test-package",
optionalDependencies: { fsevents: "~2.3.2" },
});
expect(errors).toHaveLength(1);
expect(errors[0]).toContain('"fsevents"');
expect(errors[0]).toContain("optionalDependencies");
});
it("should skip workspace, catalog, npm, link and file specifiers", ({
expect,
}) => {
const errors = validatePackagePins("test-package", "test-package", {
name: "test-package",
dependencies: {
miniflare: "workspace:*",
"@cloudflare/kv-asset-handler": "workspace:^",
esbuild: "catalog:default",
aliased: "npm:other@1.2.3",
linked: "link:../other",
filed: "file:../other",
},
});
expect(errors).toEqual([]);
});
it("should ignore peerDependencies and devDependencies", ({ expect }) => {
const errors = validatePackagePins("test-package", "test-package", {
name: "test-package",
peerDependencies: { vitest: "^4.1.0", react: "^17.0.2 || ^18.2.21" },
devDependencies: { typescript: "^5.0.0", esbuild: "^0.20.0" },
});
expect(errors).toEqual([]);
});
it("should report multiple violations across sections", ({ expect }) => {
const errors = validatePackagePins("test-package", "test-package", {
name: "test-package",
dependencies: { zod: "^3.25.76", "blake3-wasm": "2.1.5" },
optionalDependencies: { fsevents: "~2.3.2" },
});
expect(errors).toHaveLength(2);
expect(errors[0]).toContain('"zod"');
expect(errors[1]).toContain('"fsevents"');
});
});
describe("parseCatalog()", () => {
it("should parse quoted and unquoted entries", ({ expect }) => {
const catalog = parseCatalog(
[
"packages:",
" - packages/*",
"",
"minimumReleaseAge: 1440",
"",
"catalog:",
" # a comment",
' "@cloudflare/workers-types": "^4.20260529.1"',
' undici: "7.24.8"',
" '@vitest/runner': 4.1.0",
' typescript: "~5.8.3"',
"",
"overrides:",
' "@types/node": "$@types/node"',
].join("\n")
);
expect(catalog.get("@cloudflare/workers-types")).toBe("^4.20260529.1");
expect(catalog.get("undici")).toBe("7.24.8");
expect(catalog.get("@vitest/runner")).toBe("4.1.0");
expect(catalog.get("typescript")).toBe("~5.8.3");
// Should stop at the next top-level key.
expect(catalog.has("@types/node")).toBe(false);
});
});
@@ -0,0 +1,151 @@
import { describe, it } from "vitest";
import { validateDescription } from "../validate-pr-description";
describe("validateDescription()", () => {
it("should skip validation with the `ci:skip-pr-description-validation` label", ({
expect,
}) => {
expect(
validateDescription("", "", '["ci:skip-pr-description-validation"]', "[]")
).toHaveLength(0);
});
it("should show errors with default template + TODOs checked", ({
expect,
}) => {
expect(
validateDescription(
"",
`Fixes #[insert GH or internal issue link(s)].
_Describe your change..._
---
<!--
Please don't delete the checkboxes <3
The following selections do not need to be completed if this PR only contains changes to .md files
-->
- Tests
- [ ] Tests included/updated
- [ ] Automated tests not possible - manual testing has been completed as follows:
- [ ] Additional testing not necessary because:
- Public documentation
- [ ] Cloudflare docs PR(s): <!--e.g. <https://github.com/cloudflare/cloudflare-docs/pull/>...-->
- [ ] Documentation not necessary because:
<!--
Have you read our [Contributing guide](https://github.com/cloudflare/workers-sdk/blob/main/CONTRIBUTING.md)?
In particular, for non-trivial changes, please always engage on the issue or create a discussion or feature request issue first before writing your code.
-->
`,
"[]",
"[]"
)
).toMatchInlineSnapshot(`
[
"Your PR must include tests, or provide justification for why no tests are required in the PR description and apply the \`ci:no-tests\` label",
"Your PR doesn't include a changeset. Either include one (following the instructions in CONTRIBUTING.md) or add the 'ci:no-changeset-required' label to bypass this check. Most PRs should have a changeset, so only bypass this check if you're sure that your change doesn't need one: see https://github.com/cloudflare/workers-sdk/blob/main/CONTRIBUTING.md#changesets for more details.",
"Your PR must include documentation (in the form of a link to a Cloudflare Docs issue or PR), or provide justification for why no documentation is required",
]
`);
});
it("should bypass changesets check with label", ({ expect }) => {
expect(
validateDescription(
"",
`Fixes #[insert GH or internal issue link(s)].
_Describe your change..._
---
<!--
Please don't delete the checkboxes <3
The following selections do not need to be completed if this PR only contains changes to .md files
-->
- Tests
- [ ] Tests included/updated
- [ ] Automated tests not possible - manual testing has been completed as follows:
- [x] Additional testing not necessary because: test
- Public documentation
- [ ] Cloudflare docs PR(s): <!--e.g. <https://github.com/cloudflare/cloudflare-docs/pull/>...-->
- [x] Documentation not necessary because: test
<!--
Have you read our [Contributing guide](https://github.com/cloudflare/workers-sdk/blob/main/CONTRIBUTING.md)?
In particular, for non-trivial changes, please always engage on the issue or create a discussion or feature request issue first before writing your code.
-->`,
'["ci:no-changeset-required"]',
"[]"
)
).toMatchInlineSnapshot(`[]`);
});
it("should accept everything included", ({ expect }) => {
expect(
validateDescription(
"",
`Fixes #[insert GH or internal issue link(s)].
_Describe your change..._
---
<!--
Please don't delete the checkboxes <3
The following selections do not need to be completed if this PR only contains changes to .md files
-->
- Tests
- [x] Tests included/updated
- [ ] Automated tests not possible - manual testing has been completed as follows:
- [ ] Additional testing not necessary because:
- Public documentation
- [x] Cloudflare docs PR(s): https://github.com/cloudflare/cloudflare-docs/pull/100
- [ ] Documentation not necessary because:
<!--
Have you read our [Contributing guide](https://github.com/cloudflare/workers-sdk/blob/main/CONTRIBUTING.md)?
In particular, for non-trivial changes, please always engage on the issue or create a discussion or feature request issue first before writing your code.
-->
`,
"[]",
'[".changeset/hello-world.md"]'
)
).toHaveLength(0);
});
it("should accept a docs link", ({ expect }) => {
expect(
validateDescription(
"",
`## What this PR solves / how to test
Fixes [AA-000](https://jira.cfdata.org/browse/AA-000).
## Author has addressed the following
- Tests
- [ ] TODO (before merge)
- [x] Tests included/updated
- [ ] Automated tests not possible - manual testing has been completed as follows:
- [ ] Additional testing not necessary because:
- E2E Tests CI Job required? (Use "e2e" label or ask maintainer to run separately)
- [ ] I don't know
- [ ] Required
- [x] Not required because: test
- Public documentation
- [ ] TODO (before merge)
- [x] Cloudflare docs PR(s): https://developers.cloudflare.com/workers/something-here/
- [ ] Documentation not necessary because:
`,
"[]",
'[".changeset/hello-world.md"]'
)
).toHaveLength(0);
});
});
+52
View File
@@ -0,0 +1,52 @@
/* eslint-disable turbo/no-undeclared-env-vars -- this script reads CI environment variables */
if (require.main === module) {
const status = [];
if (process.env.PUBLISH_STATUS === "failure") {
if (process.env.HAS_CHANGESETS === "true") {
console.log(
"Version PR creation failed. Please retry the job if needed."
);
process.exit(1);
}
status.push({
label: "NPM publish",
details: "Packages failed to publish",
});
}
if (process.env.DEPLOYMENT_STATUS) {
try {
const deploymentStatus = JSON.parse(
process.env.DEPLOYMENT_STATUS as string
);
for (const [pkg, err] of Object.entries(deploymentStatus)) {
status.push({
label: pkg,
details: err,
});
}
} catch (e) {
status.push({
label: "Deployment status",
details: `Failed to parse deployment status: ${e}. Received: "${process.env.DEPLOYMENT_STATUS}"`,
});
}
}
if (status.length > 0) {
void fetch(
"https://devprod-status-bot.devprod.workers.dev/release-failure",
{
body: JSON.stringify({ status, url: process.env.RUN_URL }),
headers: {
"X-Auth-Header": process.env.TOKEN as string,
},
method: "POST",
}
);
process.exitCode = 1;
}
}
+20
View File
@@ -0,0 +1,20 @@
import { execSync } from "node:child_process";
import { compare } from "semver";
/**
* For Trusted Publishing to work, we need npm version 11.5.1 or higher.
*/
function checkNpmVersion() {
const npmVersionBuffer = execSync("npm --version");
const npmVersion = npmVersionBuffer.toString().trim();
if (compare(npmVersion, "11.5.1") === -1) {
console.error(
`Error: npm version 11.5.1 or higher is required for Trusted Publishing to work, found version ${npmVersion}`
);
process.exit(1);
}
}
if (require.main === module) {
checkNpmVersion();
}
@@ -0,0 +1,146 @@
import assert from "node:assert";
import { spawnSync } from "node:child_process";
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
if (require.main === module) {
if (process.argv[2] === "check") {
findDeployablePackageNames();
} else {
deployNonNpmPackages(getUpdatedPackages(), findDeployablePackageNames());
}
}
/**
* Deploy all packages that had updates but are not deployed to npm automatically by the changesets tooling.
*
* @param updatedPackages An array of all the packages that were updated by changesets.
* @param deployablePackageNames A set of the names of packages that can be deployed by this script.
*/
export function deployNonNpmPackages(
updatedPackages: UpdatedPackage[],
deployablePackageNames: Set<string>
) {
let deployedPackageCount = 0;
console.log("Checking for non-npm packages to deploy...");
const deploymentErrors = new Map<string, string>();
for (const pkg of updatedPackages) {
if (deployablePackageNames.has(pkg.name)) {
console.log(`Package "${pkg.name}@${pkg.version}": deploying...`);
deployPackage(pkg.name, deploymentErrors);
deployedPackageCount++;
} else {
console.log(
`Package "${pkg.name}@${pkg.version}": already deployed via npm.`
);
}
}
if (deployedPackageCount === 0) {
console.log("No non-npm packages to deploy.");
} else {
console.log(`Deployed ${deployedPackageCount} non-npm packages.`);
}
writeFileSync(
"deployment-status.json",
JSON.stringify(Object.fromEntries(deploymentErrors.entries()))
);
}
/**
* Get a list of all the packages that got bumped by changesets from the PUBLISHED_PACKAGES environment variable.
*/
export function getUpdatedPackages(): UpdatedPackage[] {
const packages = JSON.parse(process.env.PUBLISHED_PACKAGES ?? "[]");
assert(
Array.isArray(packages),
`Expected PUBLISHED_PACKAGES to be an array but got ${typeof packages}.`
);
packages.forEach((p, i) => {
assert(
typeof p === "object" && p !== null,
`Expected item ${i} in array to be an array but got ${typeof p}.`
);
assert(
typeof p.name === "string",
`Expected item ${i} to have a "name" property of type string but got ${p.name}.`
);
assert(
typeof p.version === "string",
`Expected item ${i} to have a "version" property of type string but got ${p.version}.`
);
});
return packages;
}
/**
* Look for all the packages (under the top-level "packages" directory of the monorepo)
* that can be deployed using this script.
*
* This is determined by the package containing a package.json "workers-sdk": { "deploy": true }`
*/
export function findDeployablePackageNames(): Set<string> {
const packagesDirectory = resolve(__dirname, "../../packages");
const allPackageDirectories = readdirSync(packagesDirectory).map((p) =>
resolve(packagesDirectory, p)
);
const allPackages: PackageJSON[] = [];
for (const dir of allPackageDirectories) {
try {
allPackages.push(
JSON.parse(readFileSync(resolve(dir, "package.json"), "utf-8"))
);
} catch {
// Do nothing
}
}
const deployablePackages = new Set<string>();
for (const pkg of allPackages) {
if (pkg["workers-sdk"]?.deploy) {
assert(
pkg.scripts?.deploy !== undefined,
`Expected package "${pkg.name}" to have a deploy script`
);
deployablePackages.add(pkg.name);
}
}
return deployablePackages;
}
/**
* Try to run `pnpm deploy` on the given package in the monorepo.
*
* If this deployment fails, log an error and continue.
*
* @param pkgName the package to deploy
*/
export function deployPackage(
pkgName: string,
deploymentErrors: Map<string, string>
) {
try {
spawnSync(`pnpm`, [`-F`, pkgName, `run`, `deploy`], {
env: process.env,
stdio: "inherit",
});
} catch (e) {
console.error(`::error::Failed to deploy "${pkgName}".`);
console.error(
"Work out why this happened and then potentially run a manual deployment."
);
console.error(e);
deploymentErrors.set(pkgName, String(e));
}
}
export type UpdatedPackage = { name: string; version: string };
export type PackageJSON = {
name: string;
private?: boolean;
scripts?: Record<string, unknown>;
"workers-sdk"?: {
deploy?: boolean;
};
};
+131
View File
@@ -0,0 +1,131 @@
/**
* Validates that workspace packages use `catalog:` references for any dependency
* that exists in the pnpm catalog. Exceptions: `workspace:` refs, `npm:` aliases,
* and packages under `templates/` (scaffolded for end users).
*/
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { glob } from "tinyglobby";
const ROOT = resolve(__dirname, "../..");
// Deps that are deliberately pinned outside the catalog (e.g. workerd is
// bumped in coordinated PRs with its own automation).
const IGNORED_DEPS = new Set(["workerd"]);
/**
* Parses the `catalog:` block of a pnpm-workspace.yaml into a map of
* dependency name -> version specifier.
*/
export function parseCatalog(workspaceYaml: string): Map<string, string> {
const catalog = new Map<string, string>();
let inCatalog = false;
for (const line of workspaceYaml.split("\n")) {
if (line.startsWith("catalog:")) {
inCatalog = true;
continue;
}
if (inCatalog && /^\S/.test(line)) {
break;
}
if (!inCatalog) {
continue;
}
const trimmed = line.trim();
if (trimmed === "" || trimmed.startsWith("#")) {
continue;
}
const match = trimmed.match(
/^["']?(@?[^"':]+)["']?\s*:\s*["']?([^"'#\s]+)["']?/
);
if (match) {
catalog.set(match[1].trim(), match[2]);
}
}
return catalog;
}
/**
* Loads the pnpm catalog as a map of dependency name -> version specifier.
*/
export function loadCatalog(): Map<string, string> {
const content = readFileSync(resolve(ROOT, "pnpm-workspace.yaml"), "utf-8");
return parseCatalog(content);
}
function loadCatalogDeps(): Set<string> {
return new Set(loadCatalog().keys());
}
async function main(): Promise<void> {
console.log("::group::Checking catalog usage");
const catalogDeps = loadCatalogDeps();
const errors: string[] = [];
const packageJsonPaths = await glob(
[
"package.json",
"packages/*/package.json",
"packages/vite-plugin-cloudflare/playground/*/package.json",
"packages/vite-plugin-cloudflare/playground/package.json",
"fixtures/*/package.json",
"tools/package.json",
],
{ cwd: ROOT, absolute: true }
);
for (const pkgPath of packageJsonPaths) {
if (pkgPath.includes("/templates/")) {
continue;
}
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
const rel = pkgPath.replace(ROOT + "/", "");
for (const section of ["dependencies", "devDependencies"] as const) {
for (const [name, version] of Object.entries(
(pkg[section] ?? {}) as Record<string, string>
)) {
if (
!catalogDeps.has(name) ||
IGNORED_DEPS.has(name) ||
version.startsWith("catalog:") ||
version.startsWith("workspace:") ||
version.startsWith("npm:")
) {
continue;
}
errors.push(
`${rel}: "${name}" uses "${version}" in ${section} but should use "catalog:default"`
);
}
}
}
if (errors.length > 0) {
console.error(
"::error::Catalog usage violations:" +
errors.map((e) => `\n- ${e}`).join("")
);
} else {
console.log("All catalog dependencies are correctly referenced.");
}
console.log("::endgroup::");
process.exit(errors.length > 0 ? 1 : 0);
}
if (require.main === module) {
main().catch((error) => {
console.log("::endgroup::");
console.error("An unexpected error occurred", error);
process.exit(1);
});
}
+110
View File
@@ -0,0 +1,110 @@
import { readdirSync, readFileSync } from "node:fs";
import { resolve } from "node:path";
import parseChangeset from "@changesets/parse";
if (require.main === module) {
const packages = findPackages();
const changesets = readChangesets(resolve(__dirname, "../../.changeset"));
const errors = validateChangesets(packages, changesets);
if (errors.length > 0) {
console.error("Validation errors in changesets:");
for (const error of errors) {
console.error("- ", error);
}
process.exit(1);
}
}
export function validateChangesets(
packages: Map<string, PackageJSON>,
changesets: ChangesetFile[]
) {
const errors: string[] = [];
changesets.map(({ file, contents }) => {
try {
const changeset = parseChangeset(contents);
for (const release of changeset.releases) {
const targetPackage = packages.get(release.name);
if (!targetPackage) {
errors.push(
`Unknown package name "${release.name}" in changeset at "${file}".`
);
}
if (release.type === "major" && targetPackage?.private !== true) {
errors.push(
`Major version bumps are not allowed for package "${release.name}" in changeset at "${file}".`
);
}
if (!["major", "minor", "patch", "none"].includes(release.type)) {
errors.push(
`Invalid type "${release.type}" for package "${release.name}" in changeset at "${file}".`
);
}
}
} catch (e) {
if (e instanceof Error) {
errors.push(e.toString() + `at file "${file}"`);
} else {
throw e;
}
}
});
return errors;
}
export function readChangesets(changesetDir: string): ChangesetFile[] {
return readdirSync(changesetDir)
.filter(
(file) =>
!file.startsWith(".") && file.endsWith(".md") && file !== "README.md"
)
.map((file) => ({
file,
contents: readFileSync(resolve(changesetDir, file), "utf-8"),
}));
}
/**
* Look for all the packages (under the top-level "packages" directory of the monorepo) that can have changesets.
*
* This is determined by the package containing a package.json that is public or
* is marked as `"private": true` and has a `deploy` script.
*/
export function findPackages(): Map<string, PackageJSON> {
const packagesDirectory = resolve(__dirname, "../../packages");
const allPackageDirectories = readdirSync(packagesDirectory).map((p) =>
resolve(packagesDirectory, p)
);
const allPackages: PackageJSON[] = [];
for (const dir of allPackageDirectories) {
try {
allPackages.push(
JSON.parse(readFileSync(resolve(dir, "package.json"), "utf-8"))
);
} catch {
// Do nothing
}
}
const deployablePackages = new Map<string, PackageJSON>();
for (const pkg of allPackages) {
if (!pkg.private || pkg.scripts?.deploy !== undefined) {
deployablePackages.set(pkg.name, pkg);
}
}
return deployablePackages;
}
export type ChangesetFile = {
file: string;
contents: string;
};
export type PackageJSON = {
name: string;
private?: boolean;
scripts?: Record<string, unknown>;
"workers-sdk"?: {
deploy?: boolean;
};
};
+78
View File
@@ -0,0 +1,78 @@
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { resolve } from "node:path";
if (require.main === module) {
console.log("::group::Checking fixtures");
const errors = checkFixtures();
if (errors.length > 0) {
console.error("::error::Fixture checks:" + errors.map((e) => `\n- ${e}`));
}
console.log("::endgroup::");
process.exit(errors.length > 0 ? 1 : 0);
}
function getFixtures() {
const fixturesDirectory = resolve(__dirname, "../../fixtures");
const allFixtureDirectories = readdirSync(fixturesDirectory);
return allFixtureDirectories
.map((dir) => {
return {
dir,
packageJson: resolve(fixturesDirectory, dir, "package.json"),
};
})
.filter(({ packageJson }) => {
return existsSync(packageJson);
});
}
/**
* Ensures that we don't accidentally create git tags and releases for fixtures
* by someone inadvertently adding a version to their package.json or making them non-private.
*
* Check the package.json for each fixture and ensure that they are all private and have no `version` property.
*/
export function checkFixtures(): string[] {
const allFixtures = getFixtures();
const errors: string[] = [];
for (const { dir, packageJson } of allFixtures) {
try {
console.log(`- ${dir}`);
const fixturePackage = JSON.parse(
readFileSync(packageJson, "utf-8")
) as PackageJSON;
// Check the package is not deployable.
if (fixturePackage.private !== true) {
errors.push(
`Fixture "${dir}" is not private. Add \`"private": true\` to its package.json`
);
}
// Check the package does not have a version.
if (fixturePackage.version) {
errors.push(
`Fixture "${dir}" has the disallowed "version" property. Please remove this.`
);
}
// Check the package has a name starting with "@fixture/".
if (!fixturePackage.name.startsWith("@fixture/")) {
errors.push(
`Fixture in directory "fixtures/${dir}" has a name that does not start with "@fixture/". Please rename it to start with "@fixture/".`
);
}
} catch {
errors.push(
`Unable to load or parse fixture "${dir}" package. Please check it has a valid package.json.`
);
}
}
return errors;
}
export type PackageJSON = {
name: string;
private?: boolean;
version?: string;
};
@@ -0,0 +1,555 @@
/**
* Validates that packages explicitly declare their external (non-bundled) dependencies.
*
* This prevents accidental dependency chain poisoning where a dependency of ours
* has not pinned its versions and allows users to install unexpected upstream
* transitive dependencies.
*
* Packages should bundle their dependencies into the distributable code via
* devDependencies. Any dependency that MUST remain external (native binaries,
* WASM, runtime-resolved code) should be:
* 1. Listed in `dependencies` (or `peerDependencies`) in package.json
* 2. Listed in `scripts/deps.ts` with EXTERNAL_DEPENDENCIES export
* 3. Documented with a comment explaining WHY it can't be bundled
*
* In addition to validating the manifest, this script also scans the actual
* built/published files (from the `files` field in package.json) for bare
* import specifiers and checks they all resolve to a declared runtime
* dependency or peer dependency. This catches cases where a bundler config's
* `external` list drifts from `package.json` — for example, a devDependency
* being incorrectly externalized would leave the published bundle with an
* unresolved import.
*/
import { existsSync, readFileSync } from "node:fs";
import { isBuiltin } from "node:module";
import { dirname, resolve } from "node:path";
import * as esbuild from "esbuild";
import { glob } from "tinyglobby";
export interface PackageJSON {
name: string;
private?: boolean;
main?: string;
module?: string;
exports?: unknown;
bin?: string | Record<string, string>;
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
}
export interface PackageInfo {
dir: string;
packageJson: PackageJSON;
}
if (require.main === module) {
console.log("::group::Checking package dependencies");
checkPackageDependencies()
.then((errors) => {
if (errors.length > 0) {
console.error(
"::error::Package dependency checks:" + errors.map((e) => `\n- ${e}`)
);
}
console.log("::endgroup::");
process.exit(errors.length > 0 ? 1 : 0);
})
.catch((error) => {
console.log("::endgroup::");
console.error("An unexpected error occurred", error);
process.exit(1);
});
}
/**
* Gets all non-private package.json files under packages/
*/
export async function getPublicPackages(): Promise<PackageInfo[]> {
const packagesDir = resolve(__dirname, "../../packages");
const packageJsonPaths = await glob("*/package.json", {
cwd: packagesDir,
absolute: true,
});
return packageJsonPaths
.map((packageJsonPath) => {
const packageJson = JSON.parse(
readFileSync(packageJsonPath, "utf-8")
) as PackageJSON;
return {
dir: dirname(packageJsonPath),
packageJson,
};
})
.filter(({ packageJson }) => !packageJson.private);
}
/**
* Gets all dependency names from a package.json dependencies object
*/
export function getAllDependencies(
dependencies: Record<string, string> | undefined
): string[] {
if (!dependencies) {
return [];
}
return Object.keys(dependencies);
}
/**
* Gets the non-workspace dependencies from a package.json
*/
export function getNonWorkspaceDependencies(
dependencies: Record<string, string> | undefined
): string[] {
if (!dependencies) {
return [];
}
return Object.entries(dependencies)
.filter(([, version]) => !version.startsWith("workspace:"))
.map(([name]) => name);
}
/**
* Attempts to load EXTERNAL_DEPENDENCIES from a package's scripts/deps.ts
*/
export function loadExternalDependencies(packageDir: string): string[] | null {
const depsModule = loadDepsModule(packageDir);
if (!depsModule || !Array.isArray(depsModule.EXTERNAL_DEPENDENCIES)) {
return null;
}
return depsModule.EXTERNAL_DEPENDENCIES;
}
/**
* Attempts to load IGNORED_DIST_IMPORTS from a package's scripts/deps.ts.
*
* `IGNORED_DIST_IMPORTS` is an allowlist of package names that the dist-scan
* validator should not flag as missing dependencies. Use this for legitimate
* but unfixable patterns such as:
* - Optional imports inside try/catch blocks in bundled libraries
* (e.g. `@netlify/build-info` probing for installed frameworks)
* - Optional native binaries (e.g. `@aws-sdk/signature-v4-crt`)
* - Code paths that are only reachable when consumers also install the
* listed package themselves
*/
export function loadIgnoredDistImports(packageDir: string): string[] {
const depsModule = loadDepsModule(packageDir);
if (!depsModule || !Array.isArray(depsModule.IGNORED_DIST_IMPORTS)) {
return [];
}
return depsModule.IGNORED_DIST_IMPORTS;
}
function loadDepsModule(packageDir: string): {
EXTERNAL_DEPENDENCIES?: string[];
IGNORED_DIST_IMPORTS?: string[];
} | null {
const depsFilePath = resolve(packageDir, "scripts/deps.ts");
if (!existsSync(depsFilePath)) {
return null;
}
// Use require with esbuild-register (which is already loaded)
// eslint-disable-next-line @typescript-eslint/no-require-imports -- dynamic require needed for esbuild-register compatibility
return require(depsFilePath) as {
EXTERNAL_DEPENDENCIES?: string[];
IGNORED_DIST_IMPORTS?: string[];
};
}
/**
* Validates a single package's dependencies against its allowlist.
* Returns an array of error messages (empty if valid).
*/
export function validatePackageDependencies(
packageName: string,
relativePath: string,
packageJson: PackageJSON,
externalDeps: string[] | null
): string[] {
const errors: string[] = [];
// Get non-workspace dependencies
const nonWorkspaceDeps = getNonWorkspaceDependencies(
packageJson.dependencies
);
// Skip packages with no non-workspace dependencies
if (nonWorkspaceDeps.length === 0) {
return errors;
}
// Check if allowlist exists
if (externalDeps === null) {
errors.push(
`Package "${packageName}" has ${nonWorkspaceDeps.length} non-workspace dependencies ` +
`but no scripts/deps.ts file with EXTERNAL_DEPENDENCIES export.\n` +
` Create packages/${relativePath}/scripts/deps.ts with an EXTERNAL_DEPENDENCIES export ` +
`listing all dependencies that cannot be bundled, with comments explaining why.\n` +
` Dependencies: ${nonWorkspaceDeps.join(", ")}`
);
return errors;
}
// Check for dependencies not in the allowlist
const undeclaredDeps = nonWorkspaceDeps.filter(
(dep) => !externalDeps.includes(dep)
);
for (const dep of undeclaredDeps) {
errors.push(
`Package "${packageName}" has dependency "${dep}" that is not listed in ` +
`EXTERNAL_DEPENDENCIES (scripts/deps.ts). Either:\n` +
` 1. Bundle this dependency by moving it to devDependencies, or\n` +
` 2. Add it to EXTERNAL_DEPENDENCIES with a comment explaining why it can't be bundled`
);
}
// Check for stale entries in the allowlist (not in dependencies or peerDependencies)
// Note: we check against ALL dependencies here (including workspace ones) because
// EXTERNAL_DEPENDENCIES is also used by the bundler to mark dependencies as external
const allDeclaredDeps = [
...getAllDependencies(packageJson.dependencies),
...getAllDependencies(packageJson.peerDependencies),
];
const staleDeps = externalDeps.filter(
(dep) => !allDeclaredDeps.includes(dep)
);
for (const dep of staleDeps) {
errors.push(
`Package "${packageName}" has "${dep}" in EXTERNAL_DEPENDENCIES but it's not in ` +
`dependencies or peerDependencies. Remove it from scripts/deps.ts.`
);
}
return errors;
}
/**
* Given an import specifier, returns the package name part.
*
* Examples:
* "lodash" -> "lodash"
* "lodash/fp" -> "lodash"
* "@cloudflare/workers-utils" -> "@cloudflare/workers-utils"
* "@cloudflare/workers-utils/test-helpers" -> "@cloudflare/workers-utils"
* "vitest/runtime" -> "vitest"
*/
export function getPackageNameFromSpecifier(spec: string): string {
if (spec.startsWith("@")) {
const parts = spec.split("/");
return parts.slice(0, 2).join("/");
}
return spec.split("/")[0];
}
/**
* Returns true if the specifier refers to an external bare package import
* (i.e. not a built-in, virtual module, or relative path).
*/
export function isBareSpecifier(spec: string): boolean {
if (!spec || spec.startsWith(".") || spec.startsWith("/")) {
return false;
}
// Node built-ins, both prefixed (node:fs) and unprefixed (fs).
if (isBuiltin(spec)) {
return false;
}
// Known non-package protocols used inside bundled user code / templates.
// These appear inside string literals in wrangler's shipped code templates,
// not as real imports of an npm package.
if (/^[a-z][a-z0-9+\-.]*:/.test(spec) && !spec.startsWith("@")) {
return false;
}
// Virtual / runtime-injected modules — anything in ALL_CAPS_WITH_UNDERSCORES
// is conventionally a build-time virtual module (e.g. __VITEST_POOL_WORKERS_DEFINES).
if (/^__[A-Z][A-Z0-9_]*$/.test(spec)) {
return false;
}
// Specifier must look like a valid npm package name: lowercase letters/digits,
// hyphens, dots, underscores; optionally scoped (@scope/name). npm package
// names cannot contain uppercase letters per the npm naming rules.
const packageNameRe =
/^(?:@[a-z0-9._~-]+\/)?[a-z0-9][a-z0-9._~-]*(?:\/[a-z0-9._~-]+)*$/;
if (!packageNameRe.test(spec)) {
return false;
}
return true;
}
/**
* Extracts all bare-specifier imports from a JS/CJS source file.
*
* Handles:
* import x from "pkg"
* import { x } from "pkg"
* import * as x from "pkg"
* import "pkg"
* export ... from "pkg"
* require("pkg")
* await import("pkg") (only when specifier is a string literal)
*
* Returns top-level package names (e.g. "vitest" for "vitest/runtime").
*
* Uses esbuild's parser (via a `bundle: true` build with everything marked
* external) so that specifiers found inside string literals, template
* literals, comments, etc. are correctly ignored. `bundle: true` is required
* to surface `require()` calls in the metafile — `bundle: false` only
* reports ESM `import` statements. The `externalize-all` plugin short-
* circuits resolution so esbuild never has to find the imports on disk.
*/
export async function extractBareImports(
content: string
): Promise<Set<string>> {
const imports = new Set<string>();
const result = await esbuild.build({
stdin: { contents: content, loader: "js" },
metafile: true,
bundle: true,
write: false,
logLevel: "silent",
platform: "neutral",
plugins: [
{
name: "externalize-all",
setup(build) {
build.onResolve({ filter: /.*/ }, (args) => {
if (args.kind === "entry-point") {
return undefined;
}
return { path: args.path, external: true };
});
},
},
],
});
for (const input of Object.values(result.metafile.inputs)) {
for (const imp of input.imports) {
if (isBareSpecifier(imp.path)) {
imports.add(getPackageNameFromSpecifier(imp.path));
}
}
}
return imports;
}
/**
* Collects every path string referenced by the package's main/module/exports/bin
* fields. These are the entry points that Node.js will actually load when the
* package is imported or executed — and thus the surface that needs to have
* all of its imports resolvable from `dependencies`/`peerDependencies`.
*
* Other entries in `files` (e.g. user-facing scaffolding templates) are
* intentionally excluded because they aren't loaded by the package itself.
*/
export function getEntryPointPaths(packageJson: PackageJSON): string[] {
const paths = new Set<string>();
if (packageJson.main) {
paths.add(packageJson.main);
}
if (packageJson.module) {
paths.add(packageJson.module);
}
walkExportValue(packageJson.exports, paths);
if (typeof packageJson.bin === "string") {
paths.add(packageJson.bin);
} else if (packageJson.bin && typeof packageJson.bin === "object") {
for (const v of Object.values(packageJson.bin)) {
if (typeof v === "string") {
paths.add(v);
}
}
}
return [...paths];
}
function walkExportValue(value: unknown, paths: Set<string>): void {
if (!value) {
return;
}
if (typeof value === "string") {
paths.add(value);
return;
}
if (Array.isArray(value)) {
for (const v of value) {
walkExportValue(v, paths);
}
return;
}
if (typeof value === "object") {
for (const v of Object.values(value as Record<string, unknown>)) {
walkExportValue(v, paths);
}
}
}
/**
* Walks the package's runtime entry points (and any sibling files in the same
* output directories — to cover code-split chunks) and returns the union of
* all bare-specifier imports found across them.
*/
export async function scanDistForExternalImports(
packageDir: string,
packageJson: PackageJSON
): Promise<Set<string>> {
const imports = new Set<string>();
const entryPaths = getEntryPointPaths(packageJson);
// Build the set of patterns to scan. For each entry-point path that points
// at a JS file, also scan its containing directory recursively to cover
// code-split chunks. (E.g. workers-utils' dist/index.mjs re-exports from
// dist/chunk-*.mjs, which we want to validate too.)
const patterns = new Set<string>();
for (const rawPath of entryPaths) {
const cleaned = rawPath.replace(/^\.\//, "");
if (!/\.(mjs|cjs|js)$/.test(cleaned)) {
continue;
}
const absPath = resolve(packageDir, cleaned);
if (!existsSync(absPath)) {
continue;
}
const containingDir = dirname(cleaned);
if (containingDir && containingDir !== ".") {
patterns.add(`${containingDir}/**/*.{mjs,cjs,js}`);
} else {
patterns.add(cleaned);
}
}
if (patterns.size === 0) {
return imports;
}
const matched = await glob([...patterns], {
cwd: packageDir,
absolute: true,
});
for (const file of matched) {
if (file.endsWith(".map") || file.endsWith(".d.ts")) {
continue;
}
const content = readFileSync(file, "utf-8");
for (const imp of await extractBareImports(content)) {
imports.add(imp);
}
}
return imports;
}
/**
* Validates that every bare-specifier import found in a package's published
* files is declared as either a `dependency` or `peerDependency`.
*
* Catches drift between bundler config `external` lists and `package.json` —
* for example, a devDependency that's incorrectly marked external in the
* bundler config would leave the published bundle importing an undeclared
* runtime dependency.
*/
export function validateDistImports(
packageName: string,
packageJson: PackageJSON,
importedPackages: Set<string>,
ignoredImports: string[] = []
): string[] {
const errors: string[] = [];
const declaredRuntimeDeps = new Set([
...getAllDependencies(packageJson.dependencies),
...getAllDependencies(packageJson.peerDependencies),
]);
const devDeps = new Set(getAllDependencies(packageJson.devDependencies));
const ignored = new Set(ignoredImports);
for (const imp of importedPackages) {
// A package importing itself by name is always fine — it's a
// self-reference inside the bundle (e.g. wrangler's bundled cli.js
// contains code that references "wrangler" as a string literal).
if (imp === packageName) {
continue;
}
if (declaredRuntimeDeps.has(imp)) {
continue;
}
if (ignored.has(imp)) {
continue;
}
if (devDeps.has(imp)) {
errors.push(
`Package "${packageName}" imports "${imp}" in its published files, but ` +
`"${imp}" is only a devDependency. Either:\n` +
` 1. Move "${imp}" to dependencies (and add to scripts/deps.ts if appropriate), or\n` +
` 2. Bundle "${imp}" by removing it from the bundler config's "external" list, or\n` +
` 3. Add "${imp}" to IGNORED_DIST_IMPORTS in scripts/deps.ts if it's a legitimate optional/try-catch import.`
);
} else {
errors.push(
`Package "${packageName}" imports "${imp}" in its published files, but ` +
`"${imp}" is not declared in dependencies or peerDependencies. ` +
`Add it to package.json or to IGNORED_DIST_IMPORTS in scripts/deps.ts.`
);
}
}
return errors;
}
/**
* Validates that all packages properly declare their external dependencies
*/
export async function checkPackageDependencies(): Promise<string[]> {
const packages = await getPublicPackages();
const errors: string[] = [];
for (const { dir, packageJson } of packages) {
const packageName = packageJson.name;
const relativePath = dir.split("/packages/")[1];
console.log(`- ${packageName}`);
const externalDeps = loadExternalDependencies(dir);
const packageErrors = validatePackageDependencies(
packageName,
relativePath,
packageJson,
externalDeps
);
errors.push(...packageErrors);
// Scan the published runtime files (main/module/exports/bin) to catch
// drift between bundler `external` lists and `package.json`. This
// catches devDeps incorrectly marked as external — which would leave
// the published bundle with unresolvable imports for end users.
try {
const importedPackages = await scanDistForExternalImports(
dir,
packageJson
);
const ignoredImports = loadIgnoredDistImports(dir);
const distErrors = validateDistImports(
packageName,
packageJson,
importedPackages,
ignoredImports
);
errors.push(...distErrors);
} catch (e) {
errors.push(
`Package "${packageName}" dist scan failed: ${e instanceof Error ? e.message : String(e)}.\n` +
` Make sure the package is built before running this check.`
);
}
}
return errors;
}
@@ -0,0 +1,174 @@
/**
* Validates that all non-bundled dependencies of published packages are pinned
* to exact versions.
*
* Anything a published package does not bundle is installed into the consumer's
* dependency tree at install time. If those specifiers are ranges, a malicious
* or broken upstream release can be pulled into users' installs without us
* having a chance to vet it. Pinning to exact versions locks down exactly what
* ships. (devDependencies are intentionally NOT checked: npm never installs a
* published package's devDependencies into a consumer's tree, so their
* specifiers cannot affect users.)
*
* The check has two halves:
*
* 1. Catalog pinning — every entry in the `catalog:` block of
* pnpm-workspace.yaml must be an exact version. Because the catalog is
* guaranteed pinned, any `catalog:` reference elsewhere is trusted and
* doesn't need to be resolved per-package.
*
* 2. Package pinning — in every published (non-private) package, every
* `dependencies` and `optionalDependencies` entry must be an exact
* version. Specifiers using `workspace:`, `catalog:`, `npm:`, `link:` or
* `file:` are skipped (the catalog ones are covered by half 1; workspace
* ones are released atomically with the monorepo). `peerDependencies` are
* excluded because ranges there are intentional and generally necessary.
*/
import { loadCatalog } from "./validate-catalog-usage";
import {
getPublicPackages,
type PackageJSON,
} from "./validate-package-dependencies";
/**
* Matches an exact semantic version: MAJOR.MINOR.PATCH with optional
* `-prerelease` and `+build` metadata (e.g. "1.2.3", "4.1.0-beta.10",
* "2.0.0-rc.24"). Rejects ranges (^, ~, >, <, ||, hyphen ranges), wildcards
* (*, x), and partial versions.
*/
const PINNED_VERSION_RE =
/^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/;
/**
* Specifier prefixes that are not literal version pins. These are validated
* elsewhere (catalog entries by `validateCatalogPins`) or are released
* atomically with the monorepo (workspace), so they are skipped here.
*/
const NON_LITERAL_PREFIXES = [
"workspace:",
"catalog:",
"npm:",
"link:",
"file:",
];
/**
* Catalog entries that are deliberately left as ranges.
*
* `@cloudflare/workers-types` is consumed as an (optional) peerDependency via
* `catalog:default`. A range is intentional there so consumers aren't forced
* onto a single exact version of the types package.
*/
export const CATALOG_PIN_EXCEPTIONS = new Set(["@cloudflare/workers-types"]);
export function isPinnedVersion(version: string): boolean {
return PINNED_VERSION_RE.test(version);
}
function isNonLiteralSpecifier(version: string): boolean {
return NON_LITERAL_PREFIXES.some((prefix) => version.startsWith(prefix));
}
/**
* Validates that every catalog entry is pinned to an exact version (except
* deliberately-ranged entries in the exceptions allowlist).
*/
export function validateCatalogPins(
catalog: Map<string, string>,
exceptions: Set<string> = CATALOG_PIN_EXCEPTIONS
): string[] {
const errors: string[] = [];
for (const [name, version] of catalog) {
if (exceptions.has(name)) {
continue;
}
if (!isPinnedVersion(version)) {
errors.push(
`Catalog entry "${name}" uses "${version}" but must be pinned to an exact ` +
`version in pnpm-workspace.yaml (e.g. "1.2.3", not a range). Catalog entries ` +
`can be consumed as non-bundled dependencies, so their versions must be locked down.`
);
}
}
return errors;
}
/**
* Validates that a single package's non-bundled dependencies are pinned.
* Returns an array of error messages (empty if valid).
*/
export function validatePackagePins(
packageName: string,
relativePath: string,
packageJson: PackageJSON
): string[] {
const errors: string[] = [];
const sections = ["dependencies", "optionalDependencies"] as const;
for (const section of sections) {
const deps = packageJson[section];
if (!deps) {
continue;
}
for (const [name, version] of Object.entries(deps)) {
if (isNonLiteralSpecifier(version)) {
continue;
}
if (!isPinnedVersion(version)) {
errors.push(
`Package "${packageName}" has ${section} "${name}" set to "${version}" ` +
`(packages/${relativePath}/package.json), but non-bundled dependencies must be ` +
`pinned to an exact version (e.g. "1.2.3", not a range). If it should be ` +
`sourced from the pnpm catalog, use "catalog:default" instead.`
);
}
}
}
return errors;
}
/**
* Validates that all catalog entries and all published packages' non-bundled
* dependencies are pinned to exact versions.
*/
export async function checkPinnedDependencies(): Promise<string[]> {
const errors: string[] = [];
console.log("- catalog (pnpm-workspace.yaml)");
errors.push(...validateCatalogPins(loadCatalog()));
const packages = await getPublicPackages();
for (const { dir, packageJson } of packages) {
const relativePath = dir.split("/packages/")[1];
console.log(`- ${packageJson.name}`);
errors.push(
...validatePackagePins(packageJson.name, relativePath, packageJson)
);
}
return errors;
}
if (require.main === module) {
console.log("::group::Checking dependency version pinning");
checkPinnedDependencies()
.then((errors) => {
if (errors.length > 0) {
console.error(
"::error::Dependency pinning checks:" +
errors.map((e) => `\n- ${e}`).join("")
);
}
console.log("::endgroup::");
process.exit(errors.length > 0 ? 1 : 0);
})
.catch((error) => {
console.log("::endgroup::");
console.error("An unexpected error occurred", error);
process.exit(1);
});
}
@@ -0,0 +1,85 @@
/* eslint-disable turbo/no-undeclared-env-vars -- this script reads CI environment variables */
if (require.main === module) {
const errors = validateDescription(
process.env.TITLE as string,
process.env.BODY as string,
process.env.LABELS as string,
process.env.FILES as string
);
if (errors.length > 0) {
console.error("Validation errors in PR description:");
for (const error of errors) {
console.error("- ", error);
}
if (process.env.DRAFT !== "true") {
process.exit(1);
} else {
console.error("These errors must be fixed before you can merge this PR.");
console.error(
"When you mark this PR as ready for review the CI check will start failing."
);
}
}
}
export function validateDescription(
title: string,
body: string,
labels: string,
changedFilesJson: string
) {
const errors: string[] = [];
console.log("PR:", title);
const parsedLabels = JSON.parse(labels) as string[];
if (parsedLabels.includes("ci:skip-pr-description-validation")) {
console.log(
"Skipping validation because the `ci:skip-pr-description-validation` label has been applied"
);
return [];
}
if (
!(
/- \[x\] Tests included\/updated/i.test(body) ||
/- \[x\] Automated tests not possible - manual testing has been completed as follows:\s*.+/i.test(
body
) ||
/- \[x\] Additional testing not necessary because:\s*.+/i.test(body)
)
) {
errors.push(
"Your PR must include tests, or provide justification for why no tests are required in the PR description and apply the `ci:no-tests` label"
);
}
const changedFiles = JSON.parse(changedFilesJson) as string[];
const changesetIncluded = changedFiles.some((f) =>
f.startsWith(".changeset/")
);
if (
!changesetIncluded &&
!parsedLabels.includes("ci:no-changeset-required")
) {
errors.push(
"Your PR doesn't include a changeset. Either include one (following the instructions in CONTRIBUTING.md) or add the 'ci:no-changeset-required' label to bypass this check. Most PRs should have a changeset, so only bypass this check if you're sure that your change doesn't need one: see https://github.com/cloudflare/workers-sdk/blob/main/CONTRIBUTING.md#changesets for more details."
);
}
if (
!(
/- \[x\] Cloudflare docs PR\(s\): https:\/\/(github\.com\/cloudflare\/cloudflare-docs\/(pull|issues)\/\d+|developers\.cloudflare\.com\/.*)/i.test(
body
) || /- \[x\] Documentation not necessary because: .+/i.test(body)
)
) {
errors.push(
"Your PR must include documentation (in the form of a link to a Cloudflare Docs issue or PR), or provide justification for why no documentation is required"
);
}
return errors;
}
@@ -0,0 +1,75 @@
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { glob } from "tinyglobby";
import type { PackageJSON } from "./validate-fixtures";
if (require.main === module) {
console.log("::group::Checking package names");
checkPrivatePackageScopes()
.then((errors) => {
if (errors.length > 0) {
console.error(
"::error::Package names checks:" + errors.map((e) => `\n- ${e}`)
);
}
console.log("::endgroup::");
process.exit(errors.length > 0 ? 1 : 0);
})
.catch((error) => {
console.log("::endgroup::");
console.error("An unexpected error occurred", error);
process.exit(1);
});
}
async function getPrivatePackageJsons() {
const packageJsonPaths = await glob("**/package.json", {
cwd: resolve(__dirname, "../../"),
ignore: [
// We are not interested in dependencies
"**/node_modules/**",
// The package.jsons in the fixtures directory use the `@fixture/` scope
// TODO: consider if we should use the `@cloudflare/` scope instead
"fixtures/**",
// C3 template package.jsons have placeholder names that are populated by C3 during the app creation
"packages/create-cloudflare/templates/**",
// The package.jsons in the vite-plugin playground use the `@playground/` scope
// TODO: consider if we should use the `@cloudflare/` scope instead
"packages/vite-plugin-cloudflare/playground/**",
// We are not interested in vendor sub-packages
"vendor/**",
],
});
return (
packageJsonPaths
.map((packageJson) => {
const resolved = resolve(packageJson);
return {
dir: dirname(resolved),
packageJson: JSON.parse(
readFileSync(packageJson, "utf-8")
) as PackageJSON,
};
})
// We are only interested in private packages
.filter(({ packageJson }) => packageJson.private)
);
}
/**
* Ensures that private packages are all scoped to `@cloudflare/`.
*/
async function checkPrivatePackageScopes(): Promise<string[]> {
const relevantPackageJsons = await getPrivatePackageJsons();
const errors: string[] = [];
for (const { dir, packageJson } of relevantPackageJsons) {
console.log(`- ${dir}`);
if (!packageJson.name.startsWith("@cloudflare/")) {
errors.push(
`Private package in directory "${dir}" has a name that does not start with "@cloudflare/". Please rename it to start with "@cloudflare/".`
);
}
}
return errors;
}