From bb80a567d9891f46ac0b276f7740de3389369eeb Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Thu, 4 Jun 2026 01:06:26 +0100 Subject: [PATCH 1/8] Send npm package dependency metadata with worker uploads --- .../package-dependencies-instrumentation.md | 18 + packages/deploy-helpers/src/deploy/deploy.ts | 1 + .../helpers/create-worker-upload-form.ts | 2 + .../src/deploy/versions-upload.ts | 1 + packages/deploy-helpers/src/shared/types.ts | 15 + packages/workers-utils/src/config/config.ts | 10 + .../workers-utils/src/config/validation.ts | 9 + packages/workers-utils/src/parse.ts | 1 + packages/workers-utils/src/types.ts | 5 + packages/workers-utils/src/worker.ts | 11 + .../normalize-and-validate-config.test.ts | 3 + .../deploy/deployment-metadata.test.ts | 355 ++++++++++++++++++ .../versions/versions.upload.test.ts | 87 +++++ .../src/deploy/deployment-metadata.ts | 165 ++++++++ .../deployment-bundle/merge-config-args.ts | 6 + 15 files changed, 689 insertions(+) create mode 100644 .changeset/package-dependencies-instrumentation.md create mode 100644 packages/wrangler/src/__tests__/deploy/deployment-metadata.test.ts create mode 100644 packages/wrangler/src/deploy/deployment-metadata.ts diff --git a/.changeset/package-dependencies-instrumentation.md b/.changeset/package-dependencies-instrumentation.md new file mode 100644 index 0000000000..3432db5cdd --- /dev/null +++ b/.changeset/package-dependencies-instrumentation.md @@ -0,0 +1,18 @@ +--- +"wrangler": minor +"@cloudflare/workers-utils": patch +--- + +Send npm package dependency metadata with worker uploads + +Wrangler now collects npm package dependency information from the project's `package.json` at deploy and version upload time, and includes it in the upload metadata sent to the Cloudflare API. This enables dependency analytics and future features like vulnerability alerting. + +The collected data includes the package name, the version constraint from `package.json`, and the exact installed version from `node_modules`. Both `dependencies` and `devDependencies` are included, while workspace packages, private packages, and unresolvable packages are excluded. The list is capped at 200 entries per upload. + +To opt out, set `dependencies_instrumentation` to `false` in your Wrangler configuration file: + +```json +{ + "dependencies_instrumentation": false +} +``` diff --git a/packages/deploy-helpers/src/deploy/deploy.ts b/packages/deploy-helpers/src/deploy/deploy.ts index 69099b98f7..1f84f90750 100644 --- a/packages/deploy-helpers/src/deploy/deploy.ts +++ b/packages/deploy-helpers/src/deploy/deploy.ts @@ -344,6 +344,7 @@ export default async function deploy( : undefined, observability: config.observability, cache: config.cache, + package_dependencies: props.packageDependencies, }; const sourceMapSize = worker.sourceMaps?.reduce( diff --git a/packages/deploy-helpers/src/deploy/helpers/create-worker-upload-form.ts b/packages/deploy-helpers/src/deploy/helpers/create-worker-upload-form.ts index 88648ecfed..911680c705 100644 --- a/packages/deploy-helpers/src/deploy/helpers/create-worker-upload-form.ts +++ b/packages/deploy-helpers/src/deploy/helpers/create-worker-upload-form.ts @@ -80,6 +80,7 @@ export function createWorkerUploadForm( assets, observability, cache, + package_dependencies, } = worker; const assetConfig: AssetConfigMetadata = { @@ -849,6 +850,7 @@ export function createWorkerUploadForm( }), ...(observability && { observability }), ...(cache && { cache_options: cache }), + ...(package_dependencies?.length && { package_dependencies }), }; if (options?.unsafe?.metadata !== undefined) { diff --git a/packages/deploy-helpers/src/deploy/versions-upload.ts b/packages/deploy-helpers/src/deploy/versions-upload.ts index dcc7a3cf26..6457fdc6e6 100644 --- a/packages/deploy-helpers/src/deploy/versions-upload.ts +++ b/packages/deploy-helpers/src/deploy/versions-upload.ts @@ -192,6 +192,7 @@ export default async function versionsUpload( logpush: undefined, // logpush and observability are non-versioned settings observability: undefined, cache: config.cache, // cache is a versioned setting + package_dependencies: props.packageDependencies, }; await printBundleSize( diff --git a/packages/deploy-helpers/src/shared/types.ts b/packages/deploy-helpers/src/shared/types.ts index c8f8f2b994..73455b588d 100644 --- a/packages/deploy-helpers/src/shared/types.ts +++ b/packages/deploy-helpers/src/shared/types.ts @@ -110,6 +110,21 @@ export type SharedDeployVersionsProps = { skipProvisioningConfigWriteback: boolean; /** From --strict arg. In strict mode, conflicting pre-upload checks abort instead of auto-continuing. */ strict: boolean; + /** temporary hack - cf is not yet a recognised deploy source, so any deploys from cf comes back normalised to 'api'*/ + skipLastDeployedFromApiCheck: boolean; + /** + * Pre-collected npm package dependency metadata from the project's package.json. + * Computed in wrangler before calling deploy-helpers and passed through to the upload form. + * `undefined` when the user has opted out via `dependencies_instrumentation: false` or + * when the project has no resolvable dependencies. + */ + packageDependencies?: + | Array<{ + name: string; + packageJsonVersion: string; + installedVersion: string; + }> + | undefined; }; export type DeployProps = SharedDeployVersionsProps & { diff --git a/packages/workers-utils/src/config/config.ts b/packages/workers-utils/src/config/config.ts index c7963ce45d..bd143e9946 100644 --- a/packages/workers-utils/src/config/config.ts +++ b/packages/workers-utils/src/config/config.ts @@ -72,6 +72,15 @@ export interface ConfigFields { */ send_metrics: boolean | undefined; + /** + * Whether Wrangler should collect and send npm package dependency metadata + * when deploying or uploading a Worker version. + * + * When set to `false`, Wrangler will not include `package_dependencies` in + * the upload payload. Defaults to `true` (enabled) when not specified. + */ + dependencies_instrumentation: boolean | undefined; + /** * Options to configure the development server that your worker will use. * @@ -302,6 +311,7 @@ export const defaultWranglerConfig: Config = { /* TOP-LEVEL ONLY FIELDS */ pages_build_output_dir: undefined, send_metrics: undefined, + dependencies_instrumentation: undefined, dev: { ip: process.platform === "win32" ? "127.0.0.1" : "localhost", port: undefined, // the default of 8787 is set at runtime diff --git a/packages/workers-utils/src/config/validation.ts b/packages/workers-utils/src/config/validation.ts index 8cdcd7bbc3..60474f0617 100644 --- a/packages/workers-utils/src/config/validation.ts +++ b/packages/workers-utils/src/config/validation.ts @@ -302,6 +302,14 @@ export function normalizeAndValidateConfig( "boolean" ); + validateOptionalProperty( + diagnostics, + "", + "dependencies_instrumentation", + rawConfig.dependencies_instrumentation, + "boolean" + ); + validateOptionalProperty( diagnostics, "", @@ -496,6 +504,7 @@ export function normalizeAndValidateConfig( /** Legacy_env is wrangler environments, as opposed to service environments. Wrangler environments is not legacy. */ legacy_env: !useServiceEnvironments, send_metrics: rawConfig.send_metrics, + dependencies_instrumentation: rawConfig.dependencies_instrumentation, keep_vars: rawConfig.keep_vars, ...activeEnv, dev: normalizeAndValidateDev(diagnostics, rawConfig.dev ?? {}, args), diff --git a/packages/workers-utils/src/parse.ts b/packages/workers-utils/src/parse.ts index 187c8219cd..d3971f2b3e 100644 --- a/packages/workers-utils/src/parse.ts +++ b/packages/workers-utils/src/parse.ts @@ -131,6 +131,7 @@ export function parseTOML(tomlContent: string, filePath?: string): unknown { export type PackageJSON = { name?: string; version?: string; + private?: boolean; devDependencies?: Record; dependencies?: Record; scripts?: Record; diff --git a/packages/workers-utils/src/types.ts b/packages/workers-utils/src/types.ts index 8efc562720..c45bb57fcc 100644 --- a/packages/workers-utils/src/types.ts +++ b/packages/workers-utils/src/types.ts @@ -293,6 +293,11 @@ type WorkerMetadataPut = { }; observability?: Observability | undefined; containers?: { class_name: string }[]; + package_dependencies?: Array<{ + name: string; + packageJsonVersion: string; + installedVersion: string; + }>; // Allow unsafe.metadata to add arbitrary properties at runtime [key: string]: unknown; }; diff --git a/packages/workers-utils/src/worker.ts b/packages/workers-utils/src/worker.ts index 76e9f6ceb7..8bdbeacb70 100644 --- a/packages/workers-utils/src/worker.ts +++ b/packages/workers-utils/src/worker.ts @@ -505,6 +505,17 @@ export interface CfWorkerInit { | undefined; observability: Observability | undefined; cache: CacheOptions | undefined; + /** + * The list of npm package dependencies collected from the project's package.json. + * Sent to the API for instrumentation and analytics purposes. + */ + package_dependencies?: + | Array<{ + name: string; + packageJsonVersion: string; + installedVersion: string; + }> + | undefined; } export interface CfWorkerContext { diff --git a/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts b/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts index c093e75fc3..4d238bec3f 100644 --- a/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts +++ b/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts @@ -67,6 +67,7 @@ describe("normalizeAndValidateConfig()", () => { bindings: [], }, send_metrics: undefined, + dependencies_instrumentation: undefined, main: undefined, migrations: [], exports: {}, @@ -176,6 +177,7 @@ describe("normalizeAndValidateConfig()", () => { const expectedConfig = { legacy_env: "FOO", send_metrics: "BAD", + dependencies_instrumentation: "NOPE", keep_vars: "NEVER", dev: { ip: 222, @@ -207,6 +209,7 @@ describe("normalizeAndValidateConfig()", () => { "Processing wrangler configuration: - Expected "legacy_env" to be of type boolean but got "FOO". - Expected "send_metrics" to be of type boolean but got "BAD". + - Expected "dependencies_instrumentation" to be of type boolean but got "NOPE". - Expected "keep_vars" to be of type boolean but got "NEVER". - Expected "dev.ip" to be of type string but got 222. - Expected "dev.port" to be of type number but got "FOO". diff --git a/packages/wrangler/src/__tests__/deploy/deployment-metadata.test.ts b/packages/wrangler/src/__tests__/deploy/deployment-metadata.test.ts new file mode 100644 index 0000000000..e9ba4713e3 --- /dev/null +++ b/packages/wrangler/src/__tests__/deploy/deployment-metadata.test.ts @@ -0,0 +1,355 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; +import { runInTempDir } from "@cloudflare/workers-utils/test-helpers"; +import { describe, it } from "vitest"; +import { collectPackageDependencies } from "../../deploy/deployment-metadata"; + +describe("collectPackageDependencies", () => { + runInTempDir(); + + it("should return undefined when no package.json exists", ({ expect }) => { + const result = collectPackageDependencies(process.cwd()); + + expect(result).toBeUndefined(); + }); + + it("should return undefined when package.json has no dependencies", ({ + expect, + }) => { + fs.writeFileSync( + "package.json", + JSON.stringify({ name: "test-project" }, null, 2) + ); + + const result = collectPackageDependencies(process.cwd()); + + expect(result).toBeUndefined(); + }); + + it("should skip workspace dependencies", ({ expect }) => { + fs.writeFileSync( + "package.json", + JSON.stringify( + { + name: "test-project", + dependencies: { + "local-package": "workspace:*", + "another-local": "workspace:^", + }, + }, + null, + 2 + ) + ); + + const result = collectPackageDependencies(process.cwd()); + + expect(result).toBeUndefined(); + }); + + it("should skip catalog dependencies", ({ expect }) => { + fs.writeFileSync( + "package.json", + JSON.stringify( + { + name: "test-project", + dependencies: { + "catalog-package": "catalog:", + "catalog-named": "catalog:default", + }, + }, + null, + 2 + ) + ); + + const result = collectPackageDependencies(process.cwd()); + + expect(result).toBeUndefined(); + }); + + it("should collect public package dependencies", ({ expect }) => { + const nodeModulesPath = path.join(process.cwd(), "node_modules"); + const packagePath = path.join(nodeModulesPath, "test-public-package"); + fs.mkdirSync(packagePath, { recursive: true }); + fs.writeFileSync(path.join(packagePath, "index.js"), "module.exports = {}"); + fs.writeFileSync( + path.join(packagePath, "package.json"), + JSON.stringify( + { + name: "test-public-package", + version: "1.2.3", + }, + null, + 2 + ) + ); + + fs.writeFileSync( + "package.json", + JSON.stringify( + { + name: "test-project", + dependencies: { + "test-public-package": "^1.0.0", + }, + }, + null, + 2 + ) + ); + + const result = collectPackageDependencies(process.cwd()); + + expect(result).toEqual([ + { + name: "test-public-package", + packageJsonVersion: "^1.0.0", + installedVersion: "1.2.3", + }, + ]); + }); + + it("should collect from both dependencies and devDependencies", ({ + expect, + }) => { + const nodeModulesPath = path.join(process.cwd(), "node_modules"); + + // Create a regular dependency + const depPath = path.join(nodeModulesPath, "prod-dep"); + fs.mkdirSync(depPath, { recursive: true }); + fs.writeFileSync(path.join(depPath, "index.js"), "module.exports = {}"); + fs.writeFileSync( + path.join(depPath, "package.json"), + JSON.stringify({ name: "prod-dep", version: "1.0.0" }, null, 2) + ); + + // Create a dev dependency + const devDepPath = path.join(nodeModulesPath, "dev-dep"); + fs.mkdirSync(devDepPath, { recursive: true }); + fs.writeFileSync(path.join(devDepPath, "index.js"), "module.exports = {}"); + fs.writeFileSync( + path.join(devDepPath, "package.json"), + JSON.stringify({ name: "dev-dep", version: "2.0.0" }, null, 2) + ); + + fs.writeFileSync( + "package.json", + JSON.stringify( + { + name: "test-project", + dependencies: { + "prod-dep": "^1.0.0", + }, + devDependencies: { + "dev-dep": "^2.0.0", + }, + }, + null, + 2 + ) + ); + + const result = collectPackageDependencies(process.cwd()); + + expect(result).toEqual([ + { + name: "prod-dep", + packageJsonVersion: "^1.0.0", + installedVersion: "1.0.0", + }, + { + name: "dev-dep", + packageJsonVersion: "^2.0.0", + installedVersion: "2.0.0", + }, + ]); + }); + + it("should skip private packages", ({ expect }) => { + const nodeModulesPath = path.join(process.cwd(), "node_modules"); + const packagePath = path.join(nodeModulesPath, "@company/private-package"); + fs.mkdirSync(packagePath, { recursive: true }); + fs.writeFileSync(path.join(packagePath, "index.js"), "module.exports = {}"); + fs.writeFileSync( + path.join(packagePath, "package.json"), + JSON.stringify( + { + name: "@company/private-package", + version: "2.0.0", + private: true, + }, + null, + 2 + ) + ); + + fs.writeFileSync( + "package.json", + JSON.stringify( + { + name: "test-project", + dependencies: { + "@company/private-package": "^2.0.0", + }, + }, + null, + 2 + ) + ); + + const result = collectPackageDependencies(process.cwd()); + + expect(result).toBeUndefined(); + }); + + it("should skip dependencies that cannot be resolved", ({ expect }) => { + fs.writeFileSync( + "package.json", + JSON.stringify( + { + name: "test-project", + dependencies: { + "nonexistent-package": "^1.0.0", + }, + }, + null, + 2 + ) + ); + + const result = collectPackageDependencies(process.cwd()); + + expect(result).toBeUndefined(); + }); + + it("should handle mixed dependencies correctly", ({ expect }) => { + const nodeModulesPath = path.join(process.cwd(), "node_modules"); + + // Create a public package + const publicPath = path.join(nodeModulesPath, "public-pkg"); + fs.mkdirSync(publicPath, { recursive: true }); + fs.writeFileSync(path.join(publicPath, "index.js"), "module.exports = {}"); + fs.writeFileSync( + path.join(publicPath, "package.json"), + JSON.stringify({ name: "public-pkg", version: "1.0.0" }, null, 2) + ); + + // Create a private package + const privatePath = path.join(nodeModulesPath, "private-pkg"); + fs.mkdirSync(privatePath, { recursive: true }); + fs.writeFileSync(path.join(privatePath, "index.js"), "module.exports = {}"); + fs.writeFileSync( + path.join(privatePath, "package.json"), + JSON.stringify( + { name: "private-pkg", version: "2.0.0", private: true }, + null, + 2 + ) + ); + + fs.writeFileSync( + "package.json", + JSON.stringify( + { + name: "test-project", + dependencies: { + "public-pkg": "^1.0.0", + "private-pkg": "^2.0.0", + "workspace-pkg": "workspace:^", + "nonexistent-pkg": "^3.0.0", + }, + }, + null, + 2 + ) + ); + + const result = collectPackageDependencies(process.cwd()); + + expect(result).toEqual([ + { + name: "public-pkg", + packageJsonVersion: "^1.0.0", + installedVersion: "1.0.0", + }, + ]); + }); + + it("should cap at 200 entries", ({ expect }) => { + const nodeModulesPath = path.join(process.cwd(), "node_modules"); + const dependencies: Record = {}; + + // Create 210 packages + for (let i = 0; i < 210; i++) { + const pkgName = `pkg-${String(i).padStart(3, "0")}`; + const pkgPath = path.join(nodeModulesPath, pkgName); + fs.mkdirSync(pkgPath, { recursive: true }); + fs.writeFileSync(path.join(pkgPath, "index.js"), "module.exports = {}"); + fs.writeFileSync( + path.join(pkgPath, "package.json"), + JSON.stringify({ name: pkgName, version: `1.0.${i}` }, null, 2) + ); + dependencies[pkgName] = `^1.0.${i}`; + } + + fs.writeFileSync( + "package.json", + JSON.stringify( + { + name: "test-project", + dependencies, + }, + null, + 2 + ) + ); + + const result = collectPackageDependencies(process.cwd()); + + expect(result).toBeDefined(); + expect(result).toHaveLength(200); + }); + + it("should use dependencies version when duplicate exists in devDependencies", ({ + expect, + }) => { + const nodeModulesPath = path.join(process.cwd(), "node_modules"); + + const pkgPath = path.join(nodeModulesPath, "shared-pkg"); + fs.mkdirSync(pkgPath, { recursive: true }); + fs.writeFileSync(path.join(pkgPath, "index.js"), "module.exports = {}"); + fs.writeFileSync( + path.join(pkgPath, "package.json"), + JSON.stringify({ name: "shared-pkg", version: "1.5.0" }, null, 2) + ); + + fs.writeFileSync( + "package.json", + JSON.stringify( + { + name: "test-project", + dependencies: { + "shared-pkg": "^1.0.0", + }, + devDependencies: { + "shared-pkg": "^1.5.0", + }, + }, + null, + 2 + ) + ); + + const result = collectPackageDependencies(process.cwd()); + + // devDependencies spread over dependencies, so devDependencies version wins + expect(result).toEqual([ + { + name: "shared-pkg", + packageJsonVersion: "^1.5.0", + installedVersion: "1.5.0", + }, + ]); + }); +}); diff --git a/packages/wrangler/src/__tests__/versions/versions.upload.test.ts b/packages/wrangler/src/__tests__/versions/versions.upload.test.ts index 1230b67a25..08d40ad58b 100644 --- a/packages/wrangler/src/__tests__/versions/versions.upload.test.ts +++ b/packages/wrangler/src/__tests__/versions/versions.upload.test.ts @@ -1,5 +1,6 @@ import assert from "node:assert"; import * as fs from "node:fs"; +import * as path from "node:path"; import { ACTOR_BINDING_DEPENDS_ON_EXPORT_CODE, generatePreviewAlias, @@ -2565,6 +2566,92 @@ describe("versions upload", () => { expect(std.out).toContain("Uploaded test-name"); }); }); + + describe("package_dependencies", () => { + beforeEach(() => { + setIsTTY(false); + }); + + test("should include package_dependencies in upload metadata", async ({ + expect, + }) => { + mockGetScript(); + const requests = mockUploadVersion(false, 0); + + // Create a resolvable public package in node_modules + const pkgPath = path.join(process.cwd(), "node_modules", "test-dep"); + fs.mkdirSync(pkgPath, { recursive: true }); + fs.writeFileSync(path.join(pkgPath, "index.js"), "module.exports = {}"); + fs.writeFileSync( + path.join(pkgPath, "package.json"), + JSON.stringify({ name: "test-dep", version: "1.2.3" }) + ); + + writeWranglerConfig({ + name: "test-name", + main: "./index.js", + }); + // Write package.json with the dependency + fs.writeFileSync( + "package.json", + JSON.stringify({ + name: "test-project", + dependencies: { + "test-dep": "^1.0.0", + }, + }) + ); + writeWorkerSource(); + + await runWrangler("versions upload"); + + const metadata = await getMetadata(requests[0]); + expect(metadata.package_dependencies).toEqual([ + { + name: "test-dep", + packageJsonVersion: "^1.0.0", + installedVersion: "1.2.3", + }, + ]); + }); + + test("should omit package_dependencies when dependencies_instrumentation is false", async ({ + expect, + }) => { + mockGetScript(); + const requests = mockUploadVersion(false, 0); + + // Create a resolvable public package in node_modules + const pkgPath = path.join(process.cwd(), "node_modules", "test-dep"); + fs.mkdirSync(pkgPath, { recursive: true }); + fs.writeFileSync(path.join(pkgPath, "index.js"), "module.exports = {}"); + fs.writeFileSync( + path.join(pkgPath, "package.json"), + JSON.stringify({ name: "test-dep", version: "1.2.3" }) + ); + + writeWranglerConfig({ + name: "test-name", + main: "./index.js", + dependencies_instrumentation: false, + }); + fs.writeFileSync( + "package.json", + JSON.stringify({ + name: "test-project", + dependencies: { + "test-dep": "^1.0.0", + }, + }) + ); + writeWorkerSource(); + + await runWrangler("versions upload"); + + const metadata = await getMetadata(requests[0]); + expect(metadata.package_dependencies).toBeUndefined(); + }); + }); }); const mockExecSync = vi.fn(); diff --git a/packages/wrangler/src/deploy/deployment-metadata.ts b/packages/wrangler/src/deploy/deployment-metadata.ts new file mode 100644 index 0000000000..c2dc43852f --- /dev/null +++ b/packages/wrangler/src/deploy/deployment-metadata.ts @@ -0,0 +1,165 @@ +import { existsSync } from "node:fs"; +import path from "node:path"; +import { getInstalledPackageVersion } from "@cloudflare/autoconfig"; +import { parsePackageJSON, readFileSync } from "@cloudflare/workers-utils"; +import { logger } from "../logger"; + +/** + * A single npm package dependency entry, matching the EWC API schema. + * Field names are camelCase to match the EWC Go struct JSON tags. + */ +export type PackageDependency = { + /** The npm package name, e.g. "lodash" or "@cloudflare/workers-types". */ + name: string; + /** The version constraint as written in package.json, e.g. "^4.17.21". */ + packageJsonVersion: string; + /** The exact version resolved and installed by the package manager, e.g. "4.17.22". */ + installedVersion: string; +}; + +/** + * Maximum number of dependency entries to include in a single upload. + * EWC will also truncate to this limit server-side, but we cap client-side + * to avoid sending unnecessarily large payloads. + */ +const MAX_PACKAGE_DEPENDENCIES = 200; + +/** + * Collects npm package dependency metadata from the project's package.json. + * + * Reads both `dependencies` and `devDependencies`, resolves each package's + * installed version from node_modules, and filters out: + * - Workspace packages (version prefixed with `workspace:`) + * - Pnpm catalog packages (version prefixed with `catalog:`) + * - Private packages (`"private": true` in the package's own package.json) + * - Packages whose installed version cannot be resolved + * + * The result is capped at {@link MAX_PACKAGE_DEPENDENCIES} entries. + * + * @param projectPath - Path to the project directory (where package.json is located) + * @returns An array of package dependency entries, or `undefined` if package.json + * cannot be read or no valid dependencies are found + */ +export function collectPackageDependencies( + projectPath: string +): PackageDependency[] | undefined { + const packageJsonPath = path.join(projectPath, "package.json"); + + if (!existsSync(packageJsonPath)) { + return undefined; + } + + try { + const content = readFileSync(packageJsonPath); + const packageJson = parsePackageJSON(content, packageJsonPath); + + const allDependencies: Record = { + ...toStringRecord(packageJson.dependencies), + ...toStringRecord(packageJson.devDependencies), + }; + + const result: PackageDependency[] = []; + + for (const [dependencyName, packageJsonVersion] of Object.entries( + allDependencies + )) { + if (result.length >= MAX_PACKAGE_DEPENDENCIES) { + break; + } + + if ( + packageJsonVersion.startsWith("workspace:") || + packageJsonVersion.startsWith("catalog:") + ) { + continue; + } + + const installedVersion = getInstalledPackageVersion( + dependencyName, + projectPath + ); + + if (!installedVersion) { + continue; + } + + if (isPackagePrivate(dependencyName, projectPath)) { + continue; + } + + result.push({ + name: dependencyName, + packageJsonVersion, + installedVersion, + }); + } + + return result.length > 0 ? result : undefined; + } catch (error) { + logger.debug( + `Failed to collect package dependencies: ${error instanceof Error ? error.message : error}` + ); + return undefined; + } +} + +/** + * Converts a `Record` (as typed in PackageJSON) to + * a `Record`, filtering out any non-string values. + * + * @param record - The record to convert + * @returns A new record containing only string-valued entries + */ +function toStringRecord( + record: Record | undefined +): Record { + if (!record) { + return {}; + } + const result: Record = {}; + for (const [key, value] of Object.entries(record)) { + if (typeof value === "string") { + result[key] = value; + } + } + return result; +} + +/** + * Checks whether a package is marked as private in its own package.json. + * + * Resolves the package from the project's node_modules and reads its + * package.json to check the `private` field. + * + * @param packageName - Name of the npm package to check + * @param projectPath - Path to the project directory + * @returns `true` if the package has `"private": true`, `false` otherwise + */ +function isPackagePrivate(packageName: string, projectPath: string): boolean { + try { + const packageEntryPath = require.resolve(packageName, { + paths: [projectPath], + }); + + // Walk up from the resolved entry point to find the package's package.json + let currentDir = path.dirname(packageEntryPath); + const root = path.parse(currentDir).root; + + while (currentDir !== root) { + const potentialPackageJson = path.join(currentDir, "package.json"); + if (existsSync(potentialPackageJson)) { + const content = readFileSync(potentialPackageJson); + const packageJson = parsePackageJSON(content, potentialPackageJson); + + if (packageJson.name === packageName) { + return packageJson.private === true; + } + } + currentDir = path.dirname(currentDir); + } + + return false; + } catch { + return false; + } +} diff --git a/packages/wrangler/src/deployment-bundle/merge-config-args.ts b/packages/wrangler/src/deployment-bundle/merge-config-args.ts index d4a955a9ff..4092f23468 100644 --- a/packages/wrangler/src/deployment-bundle/merge-config-args.ts +++ b/packages/wrangler/src/deployment-bundle/merge-config-args.ts @@ -11,6 +11,7 @@ import { validateAssetsArgsAndConfig, validateAssetsOptions, } from "../assets"; +import { collectPackageDependencies } from "../deploy/deployment-metadata"; import { getFlag } from "../experimental-flags"; import { logger } from "../logger"; import { getMetricsUsageHeaders } from "../metrics"; @@ -97,6 +98,11 @@ async function mergeSharedConfigArgs( resourcesProvision: getFlag("RESOURCES_PROVISION") ?? false, skipProvisioningConfigWriteback: false, strict: args.strict ?? false, + skipLastDeployedFromApiCheck: false, + packageDependencies: + config.dependencies_instrumentation !== false && entry.projectRoot + ? collectPackageDependencies(entry.projectRoot) + : undefined, }; const buildProps: BuildProps = { From c1c1703a951f2bbf0b0586c1b3fa730751fc7aba Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Tue, 30 Jun 2026 15:51:00 +0100 Subject: [PATCH 2/8] Move packages collection logic to deploy-helpers --- ...-package-dependencies-to-deploy-helpers.md | 7 + ...ove-package-resolution-to-workers-utils.md | 9 ++ .../package-dependencies-instrumentation.md | 1 - .../src/frameworks/utils/packages.ts | 105 +------------ packages/autoconfig/src/index.ts | 2 +- packages/deploy-helpers/src/deploy/deploy.ts | 6 +- .../deploy/helpers/package-dependencies.ts} | 21 ++- .../src/deploy/versions-upload.ts | 6 +- packages/deploy-helpers/src/index.ts | 1 + packages/deploy-helpers/src/shared/types.ts | 15 -- .../tests/package-dependencies.test.ts} | 4 +- packages/workers-utils/src/config/config.ts | 4 + packages/workers-utils/src/index.ts | 6 + .../workers-utils/src/package-resolution.ts | 146 ++++++++++++++++++ .../deployment-bundle/merge-config-args.ts | 6 - 15 files changed, 203 insertions(+), 136 deletions(-) create mode 100644 .changeset/move-package-dependencies-to-deploy-helpers.md create mode 100644 .changeset/move-package-resolution-to-workers-utils.md rename packages/{wrangler/src/deploy/deployment-metadata.ts => deploy-helpers/src/deploy/helpers/package-dependencies.ts} (91%) rename packages/{wrangler/src/__tests__/deploy/deployment-metadata.test.ts => deploy-helpers/tests/package-dependencies.test.ts} (97%) create mode 100644 packages/workers-utils/src/package-resolution.ts diff --git a/.changeset/move-package-dependencies-to-deploy-helpers.md b/.changeset/move-package-dependencies-to-deploy-helpers.md new file mode 100644 index 0000000000..09ef112087 --- /dev/null +++ b/.changeset/move-package-dependencies-to-deploy-helpers.md @@ -0,0 +1,7 @@ +--- +"@cloudflare/deploy-helpers": minor +--- + +Export `collectPackageDependencies` for npm dependency metadata collection + +The package dependency discovery logic (collecting installed npm package names and versions from a project's `package.json`) is now owned by `deploy-helpers` and called internally during deploy and version uploads, rather than being pre-computed in wrangler and passed through as a prop. diff --git a/.changeset/move-package-resolution-to-workers-utils.md b/.changeset/move-package-resolution-to-workers-utils.md new file mode 100644 index 0000000000..39f07be50f --- /dev/null +++ b/.changeset/move-package-resolution-to-workers-utils.md @@ -0,0 +1,9 @@ +--- +"@cloudflare/workers-utils": minor +--- + +Export `getInstalledPackageVersion`, `getPackagePath`, and `isPackageInstalled` utilities + +Package resolution helpers that were previously internal to `@cloudflare/autoconfig` are now exported from `@cloudflare/workers-utils` so they can be shared across packages without pulling in the full autoconfig dependency. + +`getPackagePath` now also consistently returns a directory path. Previously the fallback resolution strategy could return a file path (the package entry point) instead of its containing directory. diff --git a/.changeset/package-dependencies-instrumentation.md b/.changeset/package-dependencies-instrumentation.md index 3432db5cdd..fc9469a214 100644 --- a/.changeset/package-dependencies-instrumentation.md +++ b/.changeset/package-dependencies-instrumentation.md @@ -1,6 +1,5 @@ --- "wrangler": minor -"@cloudflare/workers-utils": patch --- Send npm package dependency metadata with worker uploads diff --git a/packages/autoconfig/src/frameworks/utils/packages.ts b/packages/autoconfig/src/frameworks/utils/packages.ts index e5853e4392..7d55577058 100644 --- a/packages/autoconfig/src/frameworks/utils/packages.ts +++ b/packages/autoconfig/src/frameworks/utils/packages.ts @@ -1,101 +1,4 @@ -import path from "node:path"; -import { parsePackageJSON, readFileSync } from "@cloudflare/workers-utils"; -import * as find from "empathic/find"; - -/** - * Checks wether a package is installed in a target project or not - * - * @param packageName the name of the target package - * @param projectPath the path of the project to check - * @returns true if the package is installed, false otherwise - */ -export function isPackageInstalled( - packageName: string, - projectPath: string -): boolean { - return !!getPackagePath(packageName, projectPath); -} - -/** - * Gets the exact version of a package installed by a project (or undefined if the package is not installed) - - * @param packageName the name of the target package - * @param projectPath the path of the project to check - * @param opts.stopAtProjectPath flag indicating whether the function should stop looking for the package at the project's path - * @returns the version of the package if the package is installed, undefined otherwise - */ -export function getInstalledPackageVersion( - packageName: string, - projectPath: string, - opts: { - stopAtProjectPath?: boolean; - } = {} -): string | undefined { - try { - const packagePath = getPackagePath(packageName, projectPath); - if (!packagePath) { - return undefined; - } - const packageJsonPath = find.file("package.json", { - cwd: packagePath, - last: opts.stopAtProjectPath === true ? projectPath : undefined, - }); - if (!packageJsonPath) { - return undefined; - } - const packageJson = parsePackageJSON( - readFileSync(packageJsonPath), - packageJsonPath - ); - // The requested package may be installed under an alias (e.g. vite+ - // installs `@voidzero-dev/vite-plus-core` under the `vite` alias). In that - // case the resolved package.json belongs to the aliased package, so its - // `version` is not the version of the requested package. - // - // `bundledVersions` is NOT a standard package.json field (it is not the - // standard `bundledDependencies`) — it is a vite+ convention that maps the - // names of the tools it bundles to the versions it provides. When the - // resolved package name doesn't match the requested one, prefer the version - // declared there for the requested package. - if (packageJson.name !== packageName) { - const bundledVersion = packageJson.bundledVersions?.[packageName]; - if (bundledVersion !== undefined) { - return bundledVersion; - } - } - return packageJson.version; - } catch {} -} - -/** - * Gets the path for a package installed by a project (or undefined if the package is not installed) - * - * @param packageName the name of the target package - * @param projectPath the path of the project - * @returns the path for the package if the package is installed, undefined otherwise - */ -function getPackagePath( - packageName: string, - projectPath: string -): string | undefined { - try { - // Note: we first try to `require.resolve` using the package.json this will succeed - // if the package.json is exported by the package - return path.dirname( - require.resolve(`${packageName}/package.json`, { - paths: [projectPath], - }) - ); - } catch {} - - try { - // Note: if `require.resolve` using the package.json failed (the package.json is not - // exported by the package) then let's try to `require.resolve` on the package - // name directly - return require.resolve(packageName, { - paths: [projectPath], - }); - } catch {} - - return undefined; -} +export { + isPackageInstalled, + getInstalledPackageVersion, +} from "@cloudflare/workers-utils"; diff --git a/packages/autoconfig/src/index.ts b/packages/autoconfig/src/index.ts index 2fee32a53d..a2f9df17f5 100644 --- a/packages/autoconfig/src/index.ts +++ b/packages/autoconfig/src/index.ts @@ -36,4 +36,4 @@ export { AutoConfigFrameworkConfigurationError, } from "./errors"; -export { getInstalledPackageVersion } from "./frameworks/utils/packages"; +export { getInstalledPackageVersion } from "@cloudflare/workers-utils"; diff --git a/packages/deploy-helpers/src/deploy/deploy.ts b/packages/deploy-helpers/src/deploy/deploy.ts index 1f84f90750..f6001cc408 100644 --- a/packages/deploy-helpers/src/deploy/deploy.ts +++ b/packages/deploy-helpers/src/deploy/deploy.ts @@ -39,6 +39,7 @@ import { renderExportsReconciliationSuccess, } from "./helpers/exports-reconciliation"; import { helpIfErrorIsSizeOrScriptStartup } from "./helpers/friendly-validator-errors"; +import { collectPackageDependencies } from "./helpers/package-dependencies"; import { parseBulkInputToObject } from "./helpers/parse-bulk-input"; import { parseConfigPlacement } from "./helpers/placement"; import { printBindings } from "./helpers/print-bindings"; @@ -344,7 +345,10 @@ export default async function deploy( : undefined, observability: config.observability, cache: config.cache, - package_dependencies: props.packageDependencies, + package_dependencies: + config.dependencies_instrumentation !== false && projectRoot + ? collectPackageDependencies(projectRoot) + : undefined, }; const sourceMapSize = worker.sourceMaps?.reduce( diff --git a/packages/wrangler/src/deploy/deployment-metadata.ts b/packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts similarity index 91% rename from packages/wrangler/src/deploy/deployment-metadata.ts rename to packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts index c2dc43852f..3ac166cceb 100644 --- a/packages/wrangler/src/deploy/deployment-metadata.ts +++ b/packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts @@ -1,8 +1,12 @@ import { existsSync } from "node:fs"; import path from "node:path"; -import { getInstalledPackageVersion } from "@cloudflare/autoconfig"; -import { parsePackageJSON, readFileSync } from "@cloudflare/workers-utils"; -import { logger } from "../logger"; +import { + getInstalledPackageVersion, + getPackagePath, + parsePackageJSON, + readFileSync, +} from "@cloudflare/workers-utils"; +import { logger } from "../../shared/context"; /** * A single npm package dependency entry, matching the EWC API schema. @@ -137,12 +141,13 @@ function toStringRecord( */ function isPackagePrivate(packageName: string, projectPath: string): boolean { try { - const packageEntryPath = require.resolve(packageName, { - paths: [projectPath], - }); + const packagePath = getPackagePath(packageName, projectPath); + if (!packagePath) { + return false; + } - // Walk up from the resolved entry point to find the package's package.json - let currentDir = path.dirname(packageEntryPath); + // Walk up from the resolved path to find the package's package.json + let currentDir = packagePath; const root = path.parse(currentDir).root; while (currentDir !== root) { diff --git a/packages/deploy-helpers/src/deploy/versions-upload.ts b/packages/deploy-helpers/src/deploy/versions-upload.ts index 6457fdc6e6..e64889e1a6 100644 --- a/packages/deploy-helpers/src/deploy/versions-upload.ts +++ b/packages/deploy-helpers/src/deploy/versions-upload.ts @@ -25,6 +25,7 @@ import { import { ACTOR_BINDING_DEPENDS_ON_EXPORT_CODE } from "./helpers/error-codes"; import { resolveExportsUploadPayload } from "./helpers/exports"; import { helpIfErrorIsSizeOrScriptStartup } from "./helpers/friendly-validator-errors"; +import { collectPackageDependencies } from "./helpers/package-dependencies"; import { parseBulkInputToObject } from "./helpers/parse-bulk-input"; import { parseConfigPlacement } from "./helpers/placement"; import { printBindings } from "./helpers/print-bindings"; @@ -192,7 +193,10 @@ export default async function versionsUpload( logpush: undefined, // logpush and observability are non-versioned settings observability: undefined, cache: config.cache, // cache is a versioned setting - package_dependencies: props.packageDependencies, + package_dependencies: + config.dependencies_instrumentation !== false && projectRoot + ? collectPackageDependencies(projectRoot) + : undefined, }; await printBundleSize( diff --git a/packages/deploy-helpers/src/index.ts b/packages/deploy-helpers/src/index.ts index 082b5daa52..78d74a4a2a 100644 --- a/packages/deploy-helpers/src/index.ts +++ b/packages/deploy-helpers/src/index.ts @@ -47,4 +47,5 @@ export * from "./deploy/helpers/print-bindings"; export * from "./deploy/helpers/assets"; export * from "./deploy/helpers/hash"; export * from "./deploy/helpers/jwt"; +export * from "./deploy/helpers/package-dependencies"; export * from "./deploy/helpers/provision-bindings"; diff --git a/packages/deploy-helpers/src/shared/types.ts b/packages/deploy-helpers/src/shared/types.ts index 73455b588d..c8f8f2b994 100644 --- a/packages/deploy-helpers/src/shared/types.ts +++ b/packages/deploy-helpers/src/shared/types.ts @@ -110,21 +110,6 @@ export type SharedDeployVersionsProps = { skipProvisioningConfigWriteback: boolean; /** From --strict arg. In strict mode, conflicting pre-upload checks abort instead of auto-continuing. */ strict: boolean; - /** temporary hack - cf is not yet a recognised deploy source, so any deploys from cf comes back normalised to 'api'*/ - skipLastDeployedFromApiCheck: boolean; - /** - * Pre-collected npm package dependency metadata from the project's package.json. - * Computed in wrangler before calling deploy-helpers and passed through to the upload form. - * `undefined` when the user has opted out via `dependencies_instrumentation: false` or - * when the project has no resolvable dependencies. - */ - packageDependencies?: - | Array<{ - name: string; - packageJsonVersion: string; - installedVersion: string; - }> - | undefined; }; export type DeployProps = SharedDeployVersionsProps & { diff --git a/packages/wrangler/src/__tests__/deploy/deployment-metadata.test.ts b/packages/deploy-helpers/tests/package-dependencies.test.ts similarity index 97% rename from packages/wrangler/src/__tests__/deploy/deployment-metadata.test.ts rename to packages/deploy-helpers/tests/package-dependencies.test.ts index e9ba4713e3..87007bcd6b 100644 --- a/packages/wrangler/src/__tests__/deploy/deployment-metadata.test.ts +++ b/packages/deploy-helpers/tests/package-dependencies.test.ts @@ -2,7 +2,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; import { runInTempDir } from "@cloudflare/workers-utils/test-helpers"; import { describe, it } from "vitest"; -import { collectPackageDependencies } from "../../deploy/deployment-metadata"; +import { collectPackageDependencies } from "../src/deploy/helpers/package-dependencies"; describe("collectPackageDependencies", () => { runInTempDir(); @@ -311,7 +311,7 @@ describe("collectPackageDependencies", () => { expect(result).toHaveLength(200); }); - it("should use dependencies version when duplicate exists in devDependencies", ({ + it("should use devDependencies version when duplicate exists in dependencies", ({ expect, }) => { const nodeModulesPath = path.join(process.cwd(), "node_modules"); diff --git a/packages/workers-utils/src/config/config.ts b/packages/workers-utils/src/config/config.ts index bd143e9946..d6bef5dace 100644 --- a/packages/workers-utils/src/config/config.ts +++ b/packages/workers-utils/src/config/config.ts @@ -78,6 +78,10 @@ export interface ConfigFields { * * When set to `false`, Wrangler will not include `package_dependencies` in * the upload payload. Defaults to `true` (enabled) when not specified. + * + * Note: This is considered build metadata, so managed separately from the + * telemetry one and not disabled when + * `send_metrics`/`WRANGLER_SEND_METRICS` is set to `false` */ dependencies_instrumentation: boolean | undefined; diff --git a/packages/workers-utils/src/index.ts b/packages/workers-utils/src/index.ts index dccd99c3f4..eb4dffe6c2 100644 --- a/packages/workers-utils/src/index.ts +++ b/packages/workers-utils/src/index.ts @@ -89,6 +89,12 @@ export type { ResolveConfigPathOptions } from "./config/config-helpers"; export * from "./errors"; export { assertNever } from "./assert-never"; +export { + getPackagePath, + isPackageInstalled, + getInstalledPackageVersion, +} from "./package-resolution"; + export * from "./constants"; export { mapWorkerMetadataBindings } from "./map-worker-metadata-bindings"; diff --git a/packages/workers-utils/src/package-resolution.ts b/packages/workers-utils/src/package-resolution.ts new file mode 100644 index 0000000000..7e1ad78752 --- /dev/null +++ b/packages/workers-utils/src/package-resolution.ts @@ -0,0 +1,146 @@ +import { statSync } from "node:fs"; +import path from "node:path"; +import { parsePackageJSON, readFileSync } from "./parse"; + +/** + * Resolves the filesystem path for an installed npm package. + * + * Tries two strategies: + * 1. `require.resolve("/package.json")` -- works when the package exports its package.json + * 2. `require.resolve("")` -- fallback for packages that don't export package.json + * + * @param packageName - The npm package name to resolve + * @param projectPath - The project directory to resolve from + * @returns The resolved directory path, or `undefined` if the package is not installed + */ +export function getPackagePath( + packageName: string, + projectPath: string +): string | undefined { + try { + // Try to resolve the package.json directly — works when the package exports it + return path.dirname( + require.resolve(`${packageName}/package.json`, { + paths: [projectPath], + }) + ); + } catch {} + + try { + // Fallback: resolve the package entry point and return its directory + return path.dirname( + require.resolve(packageName, { + paths: [projectPath], + }) + ); + } catch {} + + return undefined; +} + +/** + * Checks whether an npm package is installed in a target project. + * + * @param packageName - The name of the target package + * @param projectPath - The path of the project to check + * @returns `true` if the package is installed, `false` otherwise + */ +export function isPackageInstalled( + packageName: string, + projectPath: string +): boolean { + return !!getPackagePath(packageName, projectPath); +} + +/** + * Gets the exact version of an npm package installed in a project by resolving + * it from node_modules and reading its package.json. + * + * @param packageName - The name of the target package + * @param projectPath - The path of the project to check + * @param opts - Options + * @param opts.stopAtProjectPath - If `true`, stop walking up at the project's path + * @returns The installed version string, or `undefined` if the package is not installed + */ +export function getInstalledPackageVersion( + packageName: string, + projectPath: string, + opts: { + stopAtProjectPath?: boolean; + } = {} +): string | undefined { + try { + const packagePath = getPackagePath(packageName, projectPath); + if (!packagePath) { + return undefined; + } + + const lastDir = opts.stopAtProjectPath === true ? projectPath : undefined; + const packageJsonPath = findFileUp("package.json", packagePath, lastDir); + + if (!packageJsonPath) { + return undefined; + } + + const packageJson = parsePackageJSON( + readFileSync(packageJsonPath), + packageJsonPath + ); + // The requested package may be installed under an alias (e.g. vite+ + // installs `@voidzero-dev/vite-plus-core` under the `vite` alias). In that + // case the resolved package.json belongs to the aliased package, so its + // `version` is not the version of the requested package. + // + // `bundledVersions` is NOT a standard package.json field (it is not the + // standard `bundledDependencies`) — it is a vite+ convention that maps the + // names of the tools it bundles to the versions it provides. When the + // resolved package name doesn't match the requested one, prefer the version + // declared there for the requested package. + if (packageJson.name !== packageName) { + const bundledVersion = packageJson.bundledVersions?.[packageName]; + if (bundledVersion !== undefined) { + return bundledVersion; + } + } + return packageJson.version; + } catch {} +} + +/** + * Walks up from `startDir` looking for a file named `name`. + * Stops at `lastDir` (inclusive) if provided, otherwise walks to the filesystem root. + * + * @param name - The filename to search for + * @param startDir - The directory to start searching from + * @param lastDir - If provided, stop searching after reaching this directory + * @returns The full path to the found file, or `undefined` + */ +function findFileUp( + name: string, + startDir: string, + lastDir?: string +): string | undefined { + let dir = startDir; + const root = path.parse(dir).root; + + while (true) { + const candidate = path.join(dir, name); + try { + if (statSync(candidate).isFile()) { + return candidate; + } + } catch {} + + if (lastDir !== undefined && dir === lastDir) { + break; + } + + const parent = path.dirname(dir); + if (parent === dir || dir === root) { + break; + } + dir = parent; + } + + return undefined; +} diff --git a/packages/wrangler/src/deployment-bundle/merge-config-args.ts b/packages/wrangler/src/deployment-bundle/merge-config-args.ts index 4092f23468..d4a955a9ff 100644 --- a/packages/wrangler/src/deployment-bundle/merge-config-args.ts +++ b/packages/wrangler/src/deployment-bundle/merge-config-args.ts @@ -11,7 +11,6 @@ import { validateAssetsArgsAndConfig, validateAssetsOptions, } from "../assets"; -import { collectPackageDependencies } from "../deploy/deployment-metadata"; import { getFlag } from "../experimental-flags"; import { logger } from "../logger"; import { getMetricsUsageHeaders } from "../metrics"; @@ -98,11 +97,6 @@ async function mergeSharedConfigArgs( resourcesProvision: getFlag("RESOURCES_PROVISION") ?? false, skipProvisioningConfigWriteback: false, strict: args.strict ?? false, - skipLastDeployedFromApiCheck: false, - packageDependencies: - config.dependencies_instrumentation !== false && entry.projectRoot - ? collectPackageDependencies(entry.projectRoot) - : undefined, }; const buildProps: BuildProps = { From 6302b2801ecb44a3ff1d0dc66f84b442b3b8138d Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Wed, 8 Jul 2026 18:28:09 +0100 Subject: [PATCH 3/8] remove internal details in code comments --- .../src/deploy/helpers/package-dependencies.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts b/packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts index 3ac166cceb..ecf23b74d6 100644 --- a/packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts +++ b/packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts @@ -9,8 +9,8 @@ import { import { logger } from "../../shared/context"; /** - * A single npm package dependency entry, matching the EWC API schema. - * Field names are camelCase to match the EWC Go struct JSON tags. + * A single npm package dependency entry, matching the upload API schema + * see: https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/update. */ export type PackageDependency = { /** The npm package name, e.g. "lodash" or "@cloudflare/workers-types". */ @@ -23,7 +23,7 @@ export type PackageDependency = { /** * Maximum number of dependency entries to include in a single upload. - * EWC will also truncate to this limit server-side, but we cap client-side + * This also gets truncated to this limit server-side, but we cap client-side * to avoid sending unnecessarily large payloads. */ const MAX_PACKAGE_DEPENDENCIES = 200; From c7688c31f5fba67a2e015645797880f8c2db6b0d Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Wed, 8 Jul 2026 22:41:31 +0100 Subject: [PATCH 4/8] remove the `toStringRecord` utility --- .../deploy/helpers/package-dependencies.ts | 29 +++---------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts b/packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts index ecf23b74d6..e8496c48d5 100644 --- a/packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts +++ b/packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts @@ -57,10 +57,10 @@ export function collectPackageDependencies( const content = readFileSync(packageJsonPath); const packageJson = parsePackageJSON(content, packageJsonPath); - const allDependencies: Record = { - ...toStringRecord(packageJson.dependencies), - ...toStringRecord(packageJson.devDependencies), - }; + const allDependencies = { + ...packageJson.dependencies, + ...packageJson.devDependencies, + } as Record; const result: PackageDependency[] = []; @@ -107,27 +107,6 @@ export function collectPackageDependencies( } } -/** - * Converts a `Record` (as typed in PackageJSON) to - * a `Record`, filtering out any non-string values. - * - * @param record - The record to convert - * @returns A new record containing only string-valued entries - */ -function toStringRecord( - record: Record | undefined -): Record { - if (!record) { - return {}; - } - const result: Record = {}; - for (const [key, value] of Object.entries(record)) { - if (typeof value === "string") { - result[key] = value; - } - } - return result; -} /** * Checks whether a package is marked as private in its own package.json. From 16dd01e550266cedf56d3caacde1ef8dcc031860 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Thu, 9 Jul 2026 00:26:10 +0100 Subject: [PATCH 5/8] convert `collectPackageDependencies` to be `async` --- packages/deploy-helpers/src/deploy/deploy.ts | 2 +- .../deploy/helpers/package-dependencies.ts | 28 ++++++----- .../src/deploy/versions-upload.ts | 2 +- .../tests/package-dependencies.test.ts | 46 ++++++++++--------- 4 files changed, 43 insertions(+), 35 deletions(-) diff --git a/packages/deploy-helpers/src/deploy/deploy.ts b/packages/deploy-helpers/src/deploy/deploy.ts index f6001cc408..692fdc22cd 100644 --- a/packages/deploy-helpers/src/deploy/deploy.ts +++ b/packages/deploy-helpers/src/deploy/deploy.ts @@ -347,7 +347,7 @@ export default async function deploy( cache: config.cache, package_dependencies: config.dependencies_instrumentation !== false && projectRoot - ? collectPackageDependencies(projectRoot) + ? await collectPackageDependencies(projectRoot) : undefined, }; diff --git a/packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts b/packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts index e8496c48d5..19fbb08383 100644 --- a/packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts +++ b/packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts @@ -1,10 +1,9 @@ -import { existsSync } from "node:fs"; +import { access, readFile } from "node:fs/promises"; import path from "node:path"; import { getInstalledPackageVersion, getPackagePath, parsePackageJSON, - readFileSync, } from "@cloudflare/workers-utils"; import { logger } from "../../shared/context"; @@ -44,17 +43,19 @@ const MAX_PACKAGE_DEPENDENCIES = 200; * @returns An array of package dependency entries, or `undefined` if package.json * cannot be read or no valid dependencies are found */ -export function collectPackageDependencies( +export async function collectPackageDependencies( projectPath: string -): PackageDependency[] | undefined { +): Promise { const packageJsonPath = path.join(projectPath, "package.json"); - if (!existsSync(packageJsonPath)) { + try { + await access(packageJsonPath); + } catch { return undefined; } try { - const content = readFileSync(packageJsonPath); + const content = await readFile(packageJsonPath, "utf-8"); const packageJson = parsePackageJSON(content, packageJsonPath); const allDependencies = { @@ -87,7 +88,7 @@ export function collectPackageDependencies( continue; } - if (isPackagePrivate(dependencyName, projectPath)) { + if (await isPackagePrivate(dependencyName, projectPath)) { continue; } @@ -107,7 +108,6 @@ export function collectPackageDependencies( } } - /** * Checks whether a package is marked as private in its own package.json. * @@ -118,7 +118,10 @@ export function collectPackageDependencies( * @param projectPath - Path to the project directory * @returns `true` if the package has `"private": true`, `false` otherwise */ -function isPackagePrivate(packageName: string, projectPath: string): boolean { +async function isPackagePrivate( + packageName: string, + projectPath: string +): Promise { try { const packagePath = getPackagePath(packageName, projectPath); if (!packagePath) { @@ -131,13 +134,16 @@ function isPackagePrivate(packageName: string, projectPath: string): boolean { while (currentDir !== root) { const potentialPackageJson = path.join(currentDir, "package.json"); - if (existsSync(potentialPackageJson)) { - const content = readFileSync(potentialPackageJson); + try { + await access(potentialPackageJson); + const content = await readFile(potentialPackageJson, "utf-8"); const packageJson = parsePackageJSON(content, potentialPackageJson); if (packageJson.name === packageName) { return packageJson.private === true; } + } catch { + // package.json doesn't exist at this level, keep walking up } currentDir = path.dirname(currentDir); } diff --git a/packages/deploy-helpers/src/deploy/versions-upload.ts b/packages/deploy-helpers/src/deploy/versions-upload.ts index e64889e1a6..4d7b3be24d 100644 --- a/packages/deploy-helpers/src/deploy/versions-upload.ts +++ b/packages/deploy-helpers/src/deploy/versions-upload.ts @@ -195,7 +195,7 @@ export default async function versionsUpload( cache: config.cache, // cache is a versioned setting package_dependencies: config.dependencies_instrumentation !== false && projectRoot - ? collectPackageDependencies(projectRoot) + ? await collectPackageDependencies(projectRoot) : undefined, }; diff --git a/packages/deploy-helpers/tests/package-dependencies.test.ts b/packages/deploy-helpers/tests/package-dependencies.test.ts index 87007bcd6b..cdeac8d8ad 100644 --- a/packages/deploy-helpers/tests/package-dependencies.test.ts +++ b/packages/deploy-helpers/tests/package-dependencies.test.ts @@ -7,13 +7,15 @@ import { collectPackageDependencies } from "../src/deploy/helpers/package-depend describe("collectPackageDependencies", () => { runInTempDir(); - it("should return undefined when no package.json exists", ({ expect }) => { - const result = collectPackageDependencies(process.cwd()); + it("should return undefined when no package.json exists", async ({ + expect, + }) => { + const result = await collectPackageDependencies(process.cwd()); expect(result).toBeUndefined(); }); - it("should return undefined when package.json has no dependencies", ({ + it("should return undefined when package.json has no dependencies", async ({ expect, }) => { fs.writeFileSync( @@ -21,12 +23,12 @@ describe("collectPackageDependencies", () => { JSON.stringify({ name: "test-project" }, null, 2) ); - const result = collectPackageDependencies(process.cwd()); + const result = await collectPackageDependencies(process.cwd()); expect(result).toBeUndefined(); }); - it("should skip workspace dependencies", ({ expect }) => { + it("should skip workspace dependencies", async ({ expect }) => { fs.writeFileSync( "package.json", JSON.stringify( @@ -42,12 +44,12 @@ describe("collectPackageDependencies", () => { ) ); - const result = collectPackageDependencies(process.cwd()); + const result = await collectPackageDependencies(process.cwd()); expect(result).toBeUndefined(); }); - it("should skip catalog dependencies", ({ expect }) => { + it("should skip catalog dependencies", async ({ expect }) => { fs.writeFileSync( "package.json", JSON.stringify( @@ -63,12 +65,12 @@ describe("collectPackageDependencies", () => { ) ); - const result = collectPackageDependencies(process.cwd()); + const result = await collectPackageDependencies(process.cwd()); expect(result).toBeUndefined(); }); - it("should collect public package dependencies", ({ expect }) => { + it("should collect public package dependencies", async ({ expect }) => { const nodeModulesPath = path.join(process.cwd(), "node_modules"); const packagePath = path.join(nodeModulesPath, "test-public-package"); fs.mkdirSync(packagePath, { recursive: true }); @@ -99,7 +101,7 @@ describe("collectPackageDependencies", () => { ) ); - const result = collectPackageDependencies(process.cwd()); + const result = await collectPackageDependencies(process.cwd()); expect(result).toEqual([ { @@ -110,7 +112,7 @@ describe("collectPackageDependencies", () => { ]); }); - it("should collect from both dependencies and devDependencies", ({ + it("should collect from both dependencies and devDependencies", async ({ expect, }) => { const nodeModulesPath = path.join(process.cwd(), "node_modules"); @@ -150,7 +152,7 @@ describe("collectPackageDependencies", () => { ) ); - const result = collectPackageDependencies(process.cwd()); + const result = await collectPackageDependencies(process.cwd()); expect(result).toEqual([ { @@ -166,7 +168,7 @@ describe("collectPackageDependencies", () => { ]); }); - it("should skip private packages", ({ expect }) => { + it("should skip private packages", async ({ expect }) => { const nodeModulesPath = path.join(process.cwd(), "node_modules"); const packagePath = path.join(nodeModulesPath, "@company/private-package"); fs.mkdirSync(packagePath, { recursive: true }); @@ -198,12 +200,12 @@ describe("collectPackageDependencies", () => { ) ); - const result = collectPackageDependencies(process.cwd()); + const result = await collectPackageDependencies(process.cwd()); expect(result).toBeUndefined(); }); - it("should skip dependencies that cannot be resolved", ({ expect }) => { + it("should skip dependencies that cannot be resolved", async ({ expect }) => { fs.writeFileSync( "package.json", JSON.stringify( @@ -218,12 +220,12 @@ describe("collectPackageDependencies", () => { ) ); - const result = collectPackageDependencies(process.cwd()); + const result = await collectPackageDependencies(process.cwd()); expect(result).toBeUndefined(); }); - it("should handle mixed dependencies correctly", ({ expect }) => { + it("should handle mixed dependencies correctly", async ({ expect }) => { const nodeModulesPath = path.join(process.cwd(), "node_modules"); // Create a public package @@ -265,7 +267,7 @@ describe("collectPackageDependencies", () => { ) ); - const result = collectPackageDependencies(process.cwd()); + const result = await collectPackageDependencies(process.cwd()); expect(result).toEqual([ { @@ -276,7 +278,7 @@ describe("collectPackageDependencies", () => { ]); }); - it("should cap at 200 entries", ({ expect }) => { + it("should cap at 200 entries", async ({ expect }) => { const nodeModulesPath = path.join(process.cwd(), "node_modules"); const dependencies: Record = {}; @@ -305,13 +307,13 @@ describe("collectPackageDependencies", () => { ) ); - const result = collectPackageDependencies(process.cwd()); + const result = await collectPackageDependencies(process.cwd()); expect(result).toBeDefined(); expect(result).toHaveLength(200); }); - it("should use devDependencies version when duplicate exists in dependencies", ({ + it("should use devDependencies version when duplicate exists in dependencies", async ({ expect, }) => { const nodeModulesPath = path.join(process.cwd(), "node_modules"); @@ -341,7 +343,7 @@ describe("collectPackageDependencies", () => { ) ); - const result = collectPackageDependencies(process.cwd()); + const result = await collectPackageDependencies(process.cwd()); // devDependencies spread over dependencies, so devDependencies version wins expect(result).toEqual([ From 7ea3c180a39babc8d9b3c6d1a88cc4a42c5b6cfd Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Thu, 9 Jul 2026 00:42:56 +0100 Subject: [PATCH 6/8] exclude packages with `file:` or `link:` prefixes --- .../package-dependencies-instrumentation.md | 2 +- .../deploy/helpers/package-dependencies.ts | 5 ++++- .../tests/package-dependencies.test.ts | 22 +++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/.changeset/package-dependencies-instrumentation.md b/.changeset/package-dependencies-instrumentation.md index fc9469a214..20afd6abf6 100644 --- a/.changeset/package-dependencies-instrumentation.md +++ b/.changeset/package-dependencies-instrumentation.md @@ -6,7 +6,7 @@ Send npm package dependency metadata with worker uploads Wrangler now collects npm package dependency information from the project's `package.json` at deploy and version upload time, and includes it in the upload metadata sent to the Cloudflare API. This enables dependency analytics and future features like vulnerability alerting. -The collected data includes the package name, the version constraint from `package.json`, and the exact installed version from `node_modules`. Both `dependencies` and `devDependencies` are included, while workspace packages, private packages, and unresolvable packages are excluded. The list is capped at 200 entries per upload. +The collected data includes the package name, the version constraint from `package.json`, and the exact installed version from `node_modules`. Both `dependencies` and `devDependencies` are included, while workspace packages, local packages, private packages, and unresolvable packages are excluded. The list is capped at 200 entries per upload. To opt out, set `dependencies_instrumentation` to `false` in your Wrangler configuration file: diff --git a/packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts b/packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts index 19fbb08383..e631aff428 100644 --- a/packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts +++ b/packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts @@ -34,6 +34,7 @@ const MAX_PACKAGE_DEPENDENCIES = 200; * installed version from node_modules, and filters out: * - Workspace packages (version prefixed with `workspace:`) * - Pnpm catalog packages (version prefixed with `catalog:`) + * - Local packages (version prefixed with `file:` or `link:`) * - Private packages (`"private": true` in the package's own package.json) * - Packages whose installed version cannot be resolved * @@ -74,7 +75,9 @@ export async function collectPackageDependencies( if ( packageJsonVersion.startsWith("workspace:") || - packageJsonVersion.startsWith("catalog:") + packageJsonVersion.startsWith("catalog:") || + packageJsonVersion.startsWith("file:") || + packageJsonVersion.startsWith("link:") ) { continue; } diff --git a/packages/deploy-helpers/tests/package-dependencies.test.ts b/packages/deploy-helpers/tests/package-dependencies.test.ts index cdeac8d8ad..ab6d112953 100644 --- a/packages/deploy-helpers/tests/package-dependencies.test.ts +++ b/packages/deploy-helpers/tests/package-dependencies.test.ts @@ -168,6 +168,27 @@ describe("collectPackageDependencies", () => { ]); }); + it("should skip file: and link: dependencies", async ({ expect }) => { + fs.writeFileSync( + "package.json", + JSON.stringify( + { + name: "test-project", + dependencies: { + "local-file-pkg": "file:../local-file-pkg", + "local-link-pkg": "link:../local-link-pkg", + }, + }, + null, + 2 + ) + ); + + const result = await collectPackageDependencies(process.cwd()); + + expect(result).toBeUndefined(); + }); + it("should skip private packages", async ({ expect }) => { const nodeModulesPath = path.join(process.cwd(), "node_modules"); const packagePath = path.join(nodeModulesPath, "@company/private-package"); @@ -259,6 +280,7 @@ describe("collectPackageDependencies", () => { "public-pkg": "^1.0.0", "private-pkg": "^2.0.0", "workspace-pkg": "workspace:^", + "local-pkg": "file:../local-pkg", "nonexistent-pkg": "^3.0.0", }, }, From 572c43c28b638eb191efe5de7b51c4ab2f8a3089 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Thu, 9 Jul 2026 00:42:56 +0100 Subject: [PATCH 7/8] convert `dependencies_instrumentation` from boolean to object --- .../package-dependencies-instrumentation.md | 6 ++-- packages/deploy-helpers/src/deploy/deploy.ts | 2 +- .../src/deploy/versions-upload.ts | 2 +- packages/workers-utils/src/config/config.ts | 18 +++++++--- .../workers-utils/src/config/validation.ts | 35 +++++++++++++++---- .../normalize-and-validate-config.test.ts | 4 +-- .../versions/versions.upload.test.ts | 4 +-- 7 files changed, 51 insertions(+), 20 deletions(-) diff --git a/.changeset/package-dependencies-instrumentation.md b/.changeset/package-dependencies-instrumentation.md index 20afd6abf6..35c7339753 100644 --- a/.changeset/package-dependencies-instrumentation.md +++ b/.changeset/package-dependencies-instrumentation.md @@ -8,10 +8,12 @@ Wrangler now collects npm package dependency information from the project's `pac The collected data includes the package name, the version constraint from `package.json`, and the exact installed version from `node_modules`. Both `dependencies` and `devDependencies` are included, while workspace packages, local packages, private packages, and unresolvable packages are excluded. The list is capped at 200 entries per upload. -To opt out, set `dependencies_instrumentation` to `false` in your Wrangler configuration file: +To opt out, set `dependencies_instrumentation.enabled` to `false` in your Wrangler configuration file: ```json { - "dependencies_instrumentation": false + "dependencies_instrumentation": { + "enabled": false + } } ``` diff --git a/packages/deploy-helpers/src/deploy/deploy.ts b/packages/deploy-helpers/src/deploy/deploy.ts index 692fdc22cd..ac197d1831 100644 --- a/packages/deploy-helpers/src/deploy/deploy.ts +++ b/packages/deploy-helpers/src/deploy/deploy.ts @@ -346,7 +346,7 @@ export default async function deploy( observability: config.observability, cache: config.cache, package_dependencies: - config.dependencies_instrumentation !== false && projectRoot + config.dependencies_instrumentation?.enabled !== false && projectRoot ? await collectPackageDependencies(projectRoot) : undefined, }; diff --git a/packages/deploy-helpers/src/deploy/versions-upload.ts b/packages/deploy-helpers/src/deploy/versions-upload.ts index 4d7b3be24d..c0a973421a 100644 --- a/packages/deploy-helpers/src/deploy/versions-upload.ts +++ b/packages/deploy-helpers/src/deploy/versions-upload.ts @@ -194,7 +194,7 @@ export default async function versionsUpload( observability: undefined, cache: config.cache, // cache is a versioned setting package_dependencies: - config.dependencies_instrumentation !== false && projectRoot + config.dependencies_instrumentation?.enabled !== false && projectRoot ? await collectPackageDependencies(projectRoot) : undefined, }; diff --git a/packages/workers-utils/src/config/config.ts b/packages/workers-utils/src/config/config.ts index d6bef5dace..cb0197a70c 100644 --- a/packages/workers-utils/src/config/config.ts +++ b/packages/workers-utils/src/config/config.ts @@ -73,17 +73,25 @@ export interface ConfigFields { send_metrics: boolean | undefined; /** - * Whether Wrangler should collect and send npm package dependency metadata - * when deploying or uploading a Worker version. + * Configuration for npm package dependency instrumentation. * - * When set to `false`, Wrangler will not include `package_dependencies` in - * the upload payload. Defaults to `true` (enabled) when not specified. + * Controls whether Wrangler should collect and send npm package dependency + * metadata when deploying or uploading a Worker version. + * + * When `enabled` is set to `false`, Wrangler will not include + * `package_dependencies` in the upload payload. Defaults to enabled when + * not specified. * * Note: This is considered build metadata, so managed separately from the * telemetry one and not disabled when * `send_metrics`/`WRANGLER_SEND_METRICS` is set to `false` */ - dependencies_instrumentation: boolean | undefined; + dependencies_instrumentation: + | { + /** Whether dependency instrumentation is enabled. Defaults to `true`. */ + enabled: boolean; + } + | undefined; /** * Options to configure the development server that your worker will use. diff --git a/packages/workers-utils/src/config/validation.ts b/packages/workers-utils/src/config/validation.ts index 60474f0617..301ee8f3ea 100644 --- a/packages/workers-utils/src/config/validation.ts +++ b/packages/workers-utils/src/config/validation.ts @@ -302,13 +302,34 @@ export function normalizeAndValidateConfig( "boolean" ); - validateOptionalProperty( - diagnostics, - "", - "dependencies_instrumentation", - rawConfig.dependencies_instrumentation, - "boolean" - ); + if ( + validateOptionalProperty( + diagnostics, + "", + "dependencies_instrumentation", + rawConfig.dependencies_instrumentation, + "object" + ) + ) { + if (typeof rawConfig.dependencies_instrumentation === "object") { + validateOptionalProperty( + diagnostics, + "dependencies_instrumentation", + "enabled", + rawConfig.dependencies_instrumentation.enabled, + "boolean" + ); + + validateAdditionalProperties( + diagnostics, + "dependencies_instrumentation", + Object.keys( + rawConfig.dependencies_instrumentation as Record + ), + ["enabled"] + ); + } + } validateOptionalProperty( diagnostics, diff --git a/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts b/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts index 4d238bec3f..dd7ebc15c5 100644 --- a/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts +++ b/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts @@ -177,7 +177,7 @@ describe("normalizeAndValidateConfig()", () => { const expectedConfig = { legacy_env: "FOO", send_metrics: "BAD", - dependencies_instrumentation: "NOPE", + dependencies_instrumentation: "NOPE" as unknown, keep_vars: "NEVER", dev: { ip: 222, @@ -209,7 +209,7 @@ describe("normalizeAndValidateConfig()", () => { "Processing wrangler configuration: - Expected "legacy_env" to be of type boolean but got "FOO". - Expected "send_metrics" to be of type boolean but got "BAD". - - Expected "dependencies_instrumentation" to be of type boolean but got "NOPE". + - Expected "dependencies_instrumentation" to be of type object but got "NOPE". - Expected "keep_vars" to be of type boolean but got "NEVER". - Expected "dev.ip" to be of type string but got 222. - Expected "dev.port" to be of type number but got "FOO". diff --git a/packages/wrangler/src/__tests__/versions/versions.upload.test.ts b/packages/wrangler/src/__tests__/versions/versions.upload.test.ts index 08d40ad58b..05ead0bd38 100644 --- a/packages/wrangler/src/__tests__/versions/versions.upload.test.ts +++ b/packages/wrangler/src/__tests__/versions/versions.upload.test.ts @@ -2615,7 +2615,7 @@ describe("versions upload", () => { ]); }); - test("should omit package_dependencies when dependencies_instrumentation is false", async ({ + test("should omit package_dependencies when dependencies_instrumentation.enabled is false", async ({ expect, }) => { mockGetScript(); @@ -2633,7 +2633,7 @@ describe("versions upload", () => { writeWranglerConfig({ name: "test-name", main: "./index.js", - dependencies_instrumentation: false, + dependencies_instrumentation: { enabled: false }, }); fs.writeFileSync( "package.json", From 2051c2baf7294f3fc841256e5555778b27892117 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Thu, 9 Jul 2026 14:54:19 +0100 Subject: [PATCH 8/8] remove private packages filtering --- .../package-dependencies-instrumentation.md | 2 +- .../deploy/helpers/package-dependencies.ts | 52 ---------------- .../tests/package-dependencies.test.ts | 62 ++++--------------- 3 files changed, 14 insertions(+), 102 deletions(-) diff --git a/.changeset/package-dependencies-instrumentation.md b/.changeset/package-dependencies-instrumentation.md index 35c7339753..04e313c2cc 100644 --- a/.changeset/package-dependencies-instrumentation.md +++ b/.changeset/package-dependencies-instrumentation.md @@ -6,7 +6,7 @@ Send npm package dependency metadata with worker uploads Wrangler now collects npm package dependency information from the project's `package.json` at deploy and version upload time, and includes it in the upload metadata sent to the Cloudflare API. This enables dependency analytics and future features like vulnerability alerting. -The collected data includes the package name, the version constraint from `package.json`, and the exact installed version from `node_modules`. Both `dependencies` and `devDependencies` are included, while workspace packages, local packages, private packages, and unresolvable packages are excluded. The list is capped at 200 entries per upload. +The collected data includes the package name, the version constraint from `package.json`, and the exact installed version from `node_modules`. Both `dependencies` and `devDependencies` are included, while workspace packages, local packages, and unresolvable packages are excluded. The list is capped at 200 entries per upload. To opt out, set `dependencies_instrumentation.enabled` to `false` in your Wrangler configuration file: diff --git a/packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts b/packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts index e631aff428..c15cc73acd 100644 --- a/packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts +++ b/packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts @@ -2,7 +2,6 @@ import { access, readFile } from "node:fs/promises"; import path from "node:path"; import { getInstalledPackageVersion, - getPackagePath, parsePackageJSON, } from "@cloudflare/workers-utils"; import { logger } from "../../shared/context"; @@ -35,7 +34,6 @@ const MAX_PACKAGE_DEPENDENCIES = 200; * - Workspace packages (version prefixed with `workspace:`) * - Pnpm catalog packages (version prefixed with `catalog:`) * - Local packages (version prefixed with `file:` or `link:`) - * - Private packages (`"private": true` in the package's own package.json) * - Packages whose installed version cannot be resolved * * The result is capped at {@link MAX_PACKAGE_DEPENDENCIES} entries. @@ -91,10 +89,6 @@ export async function collectPackageDependencies( continue; } - if (await isPackagePrivate(dependencyName, projectPath)) { - continue; - } - result.push({ name: dependencyName, packageJsonVersion, @@ -110,49 +104,3 @@ export async function collectPackageDependencies( return undefined; } } - -/** - * Checks whether a package is marked as private in its own package.json. - * - * Resolves the package from the project's node_modules and reads its - * package.json to check the `private` field. - * - * @param packageName - Name of the npm package to check - * @param projectPath - Path to the project directory - * @returns `true` if the package has `"private": true`, `false` otherwise - */ -async function isPackagePrivate( - packageName: string, - projectPath: string -): Promise { - try { - const packagePath = getPackagePath(packageName, projectPath); - if (!packagePath) { - return false; - } - - // Walk up from the resolved path to find the package's package.json - let currentDir = packagePath; - const root = path.parse(currentDir).root; - - while (currentDir !== root) { - const potentialPackageJson = path.join(currentDir, "package.json"); - try { - await access(potentialPackageJson); - const content = await readFile(potentialPackageJson, "utf-8"); - const packageJson = parsePackageJSON(content, potentialPackageJson); - - if (packageJson.name === packageName) { - return packageJson.private === true; - } - } catch { - // package.json doesn't exist at this level, keep walking up - } - currentDir = path.dirname(currentDir); - } - - return false; - } catch { - return false; - } -} diff --git a/packages/deploy-helpers/tests/package-dependencies.test.ts b/packages/deploy-helpers/tests/package-dependencies.test.ts index ab6d112953..0d8e953a3a 100644 --- a/packages/deploy-helpers/tests/package-dependencies.test.ts +++ b/packages/deploy-helpers/tests/package-dependencies.test.ts @@ -189,43 +189,6 @@ describe("collectPackageDependencies", () => { expect(result).toBeUndefined(); }); - it("should skip private packages", async ({ expect }) => { - const nodeModulesPath = path.join(process.cwd(), "node_modules"); - const packagePath = path.join(nodeModulesPath, "@company/private-package"); - fs.mkdirSync(packagePath, { recursive: true }); - fs.writeFileSync(path.join(packagePath, "index.js"), "module.exports = {}"); - fs.writeFileSync( - path.join(packagePath, "package.json"), - JSON.stringify( - { - name: "@company/private-package", - version: "2.0.0", - private: true, - }, - null, - 2 - ) - ); - - fs.writeFileSync( - "package.json", - JSON.stringify( - { - name: "test-project", - dependencies: { - "@company/private-package": "^2.0.0", - }, - }, - null, - 2 - ) - ); - - const result = await collectPackageDependencies(process.cwd()); - - expect(result).toBeUndefined(); - }); - it("should skip dependencies that cannot be resolved", async ({ expect }) => { fs.writeFileSync( "package.json", @@ -249,7 +212,7 @@ describe("collectPackageDependencies", () => { it("should handle mixed dependencies correctly", async ({ expect }) => { const nodeModulesPath = path.join(process.cwd(), "node_modules"); - // Create a public package + // Create an installed package const publicPath = path.join(nodeModulesPath, "public-pkg"); fs.mkdirSync(publicPath, { recursive: true }); fs.writeFileSync(path.join(publicPath, "index.js"), "module.exports = {}"); @@ -258,17 +221,13 @@ describe("collectPackageDependencies", () => { JSON.stringify({ name: "public-pkg", version: "1.0.0" }, null, 2) ); - // Create a private package - const privatePath = path.join(nodeModulesPath, "private-pkg"); - fs.mkdirSync(privatePath, { recursive: true }); - fs.writeFileSync(path.join(privatePath, "index.js"), "module.exports = {}"); + // Create another installed package + const anotherPath = path.join(nodeModulesPath, "another-pkg"); + fs.mkdirSync(anotherPath, { recursive: true }); + fs.writeFileSync(path.join(anotherPath, "index.js"), "module.exports = {}"); fs.writeFileSync( - path.join(privatePath, "package.json"), - JSON.stringify( - { name: "private-pkg", version: "2.0.0", private: true }, - null, - 2 - ) + path.join(anotherPath, "package.json"), + JSON.stringify({ name: "another-pkg", version: "2.0.0" }, null, 2) ); fs.writeFileSync( @@ -278,7 +237,7 @@ describe("collectPackageDependencies", () => { name: "test-project", dependencies: { "public-pkg": "^1.0.0", - "private-pkg": "^2.0.0", + "another-pkg": "^2.0.0", "workspace-pkg": "workspace:^", "local-pkg": "file:../local-pkg", "nonexistent-pkg": "^3.0.0", @@ -297,6 +256,11 @@ describe("collectPackageDependencies", () => { packageJsonVersion: "^1.0.0", installedVersion: "1.0.0", }, + { + name: "another-pkg", + packageJsonVersion: "^2.0.0", + installedVersion: "2.0.0", + }, ]); });