From dbba126554c342e5fa49f1c05c405208c7576bcf Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 16 Sep 2026 13:16:30 -0700 Subject: [PATCH 1/2] Enforce exact npm dependency policy Pin Node.js production dependencies at their existing resolved versions and require external npm releases to age seven full days before release packaging. Preserve exact requirements in packed packages and default future npm saves to exact versions. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 823a3ba3-43a5-4b6a-be71-625e7660994a --- nodejs/.npmrc | 1 + nodejs/package-lock.json | 13 +- nodejs/package.json | 10 +- nodejs/samples/package-lock.json | 7 +- nodejs/scripts/dependency-policy.ts | 147 +++++++++++++++++++++ nodejs/scripts/verify-dependency-policy.ts | 28 ++++ nodejs/scripts/verify-release-packages.ts | 11 +- nodejs/test/dependency-policy.test.ts | 108 +++++++++++++++ nodejs/tsconfig.test.json | 1 + 9 files changed, 315 insertions(+), 11 deletions(-) create mode 100644 nodejs/scripts/dependency-policy.ts create mode 100644 nodejs/scripts/verify-dependency-policy.ts create mode 100644 nodejs/test/dependency-policy.test.ts diff --git a/nodejs/.npmrc b/nodejs/.npmrc index 100721e01a..9d4297362f 100644 --- a/nodejs/.npmrc +++ b/nodejs/.npmrc @@ -1 +1,2 @@ omit-lockfile-registry-resolved=true +save-exact=true diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 0de83bc883..4214cc8cd7 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -9,13 +9,14 @@ "version": "0.0.0-dev", "license": "MIT", "dependencies": { - "koffi": "^3.2.1", - "vscode-jsonrpc": "^8.2.1", - "zod": "^4.3.6" + "koffi": "3.2.1", + "vscode-jsonrpc": "8.2.1", + "zod": "4.3.6" }, "devDependencies": { "@platformatic/vfs": "^0.3.0", "@types/node": "^25.2.0", + "@types/semver": "7.8.0", "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^8.54.0", "@typescript-eslint/parser": "^8.54.0", @@ -1351,6 +1352,12 @@ "undici-types": "~7.18.0" } }, + "node_modules/@types/semver": { + "version": "7.8.0", + "integrity": "sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", diff --git a/nodejs/package.json b/nodejs/package.json index 1c255f7e09..440c533d7d 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -37,7 +37,8 @@ "clean": "rimraf --glob dist *.tgz", "build": "tsx esbuild-copilotsdk-nodejs.ts", "acquire:runtime-packages": "tsx scripts/runtime-package-acquisition.ts", - "pack:release": "tsx scripts/package-sdk.ts", + "pack:release": "npm run verify:dependency-policy && tsx scripts/package-sdk.ts", + "verify:dependency-policy": "tsx scripts/verify-dependency-policy.ts", "verify:release-packages": "tsx scripts/verify-release-packages.ts", "release:manifest": "tsx scripts/release-manifest.ts", "prepare:runtime": "tsx scripts/prepare-runtime.ts", @@ -64,13 +65,14 @@ "author": "GitHub", "license": "MIT", "dependencies": { - "koffi": "^3.2.1", - "vscode-jsonrpc": "^8.2.1", - "zod": "^4.3.6" + "koffi": "3.2.1", + "vscode-jsonrpc": "8.2.1", + "zod": "4.3.6" }, "devDependencies": { "@platformatic/vfs": "^0.3.0", "@types/node": "^25.2.0", + "@types/semver": "7.8.0", "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^8.54.0", "@typescript-eslint/parser": "^8.54.0", diff --git a/nodejs/samples/package-lock.json b/nodejs/samples/package-lock.json index 6c29f98383..03cdb82911 100644 --- a/nodejs/samples/package-lock.json +++ b/nodejs/samples/package-lock.json @@ -18,13 +18,14 @@ "version": "0.0.0-dev", "license": "MIT", "dependencies": { - "koffi": "^3.2.1", - "vscode-jsonrpc": "^8.2.1", - "zod": "^4.3.6" + "koffi": "3.2.1", + "vscode-jsonrpc": "8.2.1", + "zod": "4.3.6" }, "devDependencies": { "@platformatic/vfs": "^0.3.0", "@types/node": "^25.2.0", + "@types/semver": "7.8.0", "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^8.54.0", "@typescript-eslint/parser": "^8.54.0", diff --git a/nodejs/scripts/dependency-policy.ts b/nodejs/scripts/dependency-policy.ts new file mode 100644 index 0000000000..fcbc33dea9 --- /dev/null +++ b/nodejs/scripts/dependency-policy.ts @@ -0,0 +1,147 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import * as semver from "semver"; + +const NPM_REGISTRY_URL = "https://registry.npmjs.org"; + +export const MINIMUM_DEPENDENCY_AGE_MS = 7 * 24 * 60 * 60 * 1000; + +export interface ProductionDependencyManifest { + dependencies?: Record; + optionalDependencies?: Record; +} + +export interface ProductionDependency { + name: string; + section: "dependencies" | "optionalDependencies"; + version: string; +} + +export interface RegistryResponse { + json(): Promise; + ok: boolean; + status: number; + statusText: string; +} + +export type RegistryRequest = (url: string) => Promise; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function dependencyKey(dependency: ProductionDependency): string { + return `${dependency.name}@${dependency.version}`; +} + +function readDependencySection( + manifest: Record, + section: "dependencies" | "optionalDependencies" +): Record | undefined { + const value = manifest[section]; + if (value === undefined) { + return undefined; + } + assert(isRecord(value), `${section} must be a JSON object`); + const dependencies: Record = {}; + for (const [name, version] of Object.entries(value)) { + assert(typeof version === "string", `${section}.${name} must be a string`); + dependencies[name] = version; + } + return dependencies; +} + +export function readProductionDependencyManifest(path: string): ProductionDependencyManifest { + const parsed: unknown = JSON.parse(readFileSync(path, "utf8")); + assert(isRecord(parsed), "package.json must contain a JSON object"); + return { + dependencies: readDependencySection(parsed, "dependencies"), + optionalDependencies: readDependencySection(parsed, "optionalDependencies"), + }; +} + +export function assertExactProductionDependencies( + manifest: ProductionDependencyManifest +): ProductionDependency[] { + const dependencies: ProductionDependency[] = []; + for (const section of ["dependencies", "optionalDependencies"] as const) { + for (const [name, version] of Object.entries(manifest[section] ?? {})) { + assert.equal( + semver.valid(version), + version, + `${section}.${name} must use an exact SemVer version; found '${version}'` + ); + dependencies.push({ name, section, version }); + } + } + return dependencies; +} + +export function requiresPublicationCooldown(dependencyName: string): boolean { + return !dependencyName.startsWith("@github/"); +} + +export async function loadNpmPublicationTimes( + dependencies: readonly ProductionDependency[], + request: RegistryRequest = (url) => fetch(url) +): Promise> { + const externalDependencies = dependencies.filter((dependency) => + requiresPublicationCooldown(dependency.name) + ); + const packageNames = [...new Set(externalDependencies.map((dependency) => dependency.name))]; + const metadata = new Map>(); + + await Promise.all( + packageNames.map(async (packageName) => { + const response = await request( + `${NPM_REGISTRY_URL}/${encodeURIComponent(packageName)}` + ); + assert( + response.ok, + `npm metadata request for ${packageName} failed: ${response.status} ${response.statusText}` + ); + const body = await response.json(); + assert(isRecord(body), `npm metadata for ${packageName} must be a JSON object`); + const times = body.time; + assert(isRecord(times), `npm metadata for ${packageName} is missing publication times`); + metadata.set(packageName, times); + }) + ); + + return new Map( + externalDependencies.map((dependency) => { + const publishedAt = metadata.get(dependency.name)?.[dependency.version]; + assert( + typeof publishedAt === "string", + `npm metadata is missing a publication time for ${dependencyKey(dependency)}` + ); + return [dependencyKey(dependency), publishedAt]; + }) + ); +} + +export function assertMinimumPublicationAge( + dependencies: readonly ProductionDependency[], + publicationTimes: ReadonlyMap, + now = Date.now() +): void { + assert(Number.isFinite(now), "Dependency policy verification time must be finite"); + + for (const dependency of dependencies) { + if (!requiresPublicationCooldown(dependency.name)) { + continue; + } + const key = dependencyKey(dependency); + const publishedAt = publicationTimes.get(key); + assert(publishedAt, `Missing npm publication time for ${key}`); + const publishedAtMs = Date.parse(publishedAt); + assert(Number.isFinite(publishedAtMs), `Invalid npm publication time for ${key}`); + const eligibleAtMs = publishedAtMs + MINIMUM_DEPENDENCY_AGE_MS; + assert( + now >= eligibleAtMs, + `${key} was published at ${publishedAt} and is not eligible until ${new Date( + eligibleAtMs + ).toISOString()}` + ); + } +} diff --git a/nodejs/scripts/verify-dependency-policy.ts b/nodejs/scripts/verify-dependency-policy.ts new file mode 100644 index 0000000000..e102a91c36 --- /dev/null +++ b/nodejs/scripts/verify-dependency-policy.ts @@ -0,0 +1,28 @@ +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + assertExactProductionDependencies, + assertMinimumPublicationAge, + loadNpmPublicationTimes, + readProductionDependencyManifest, + requiresPublicationCooldown, +} from "./dependency-policy.js"; + +async function main(): Promise { + const nodeRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); + const manifest = readProductionDependencyManifest(join(nodeRoot, "package.json")); + const dependencies = assertExactProductionDependencies(manifest); + const publicationTimes = await loadNpmPublicationTimes(dependencies); + assertMinimumPublicationAge(dependencies, publicationTimes); + const externalCount = dependencies.filter((dependency) => + requiresPublicationCooldown(dependency.name) + ).length; + console.log( + `Verified ${dependencies.length} exact production dependencies; ${externalCount} meet the seven-day npm publication age requirement.` + ); +} + +main().catch((error) => { + console.error(`::error::${error instanceof Error ? error.message : String(error)}`); + process.exitCode = 1; +}); diff --git a/nodejs/scripts/verify-release-packages.ts b/nodejs/scripts/verify-release-packages.ts index 0d2f4589d9..93306d3d98 100644 --- a/nodejs/scripts/verify-release-packages.ts +++ b/nodejs/scripts/verify-release-packages.ts @@ -6,13 +6,15 @@ import { fileURLToPath } from "node:url"; import { globSync } from "glob"; import { t as listTar, x as extractTar } from "tar"; import { getRuntimePackageName, RUNTIME_PLATFORMS } from "../src/runtimeArtifacts.js"; +import { assertExactProductionDependencies } from "./dependency-policy.js"; interface PackedPackage { manifest: { + dependencies?: Record; name: string; + optionalDependencies?: Record; version: string; repository?: string | { type?: string; url?: string }; - optionalDependencies?: Record; }; entries: Set; } @@ -27,6 +29,7 @@ assert( : sourceManifest.repository?.url?.trim(), "Main package is missing repository metadata" ); +assertExactProductionDependencies(sourceManifest); const expectedRuntimePackages = Object.fromEntries( RUNTIME_PLATFORMS.map((platform) => [getRuntimePackageName(platform), sourceManifest.version]) ); @@ -77,11 +80,17 @@ assert.deepEqual( const mainPackage = packages.get(sourceManifest.name); assert(mainPackage, `Missing ${sourceManifest.name} tarball`); +assert.deepEqual( + mainPackage.manifest.dependencies, + sourceManifest.dependencies, + "Main package dependencies do not match the source manifest" +); assert.deepEqual( mainPackage.manifest.optionalDependencies, expectedRuntimePackages, "Main package optional dependencies do not match the platform packages" ); +assertExactProductionDependencies(mainPackage.manifest); assert(mainPackage.entries.has("package/dist/index.js"), "Main package is missing dist/index.js"); assert( mainPackage.entries.has("package/dist/cjs/index.js"), diff --git a/nodejs/test/dependency-policy.test.ts b/nodejs/test/dependency-policy.test.ts new file mode 100644 index 0000000000..49a1b4f802 --- /dev/null +++ b/nodejs/test/dependency-policy.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, it } from "vitest"; +import { + assertExactProductionDependencies, + assertMinimumPublicationAge, + loadNpmPublicationTimes, + MINIMUM_DEPENDENCY_AGE_MS, + type ProductionDependency, + type RegistryRequest, +} from "../scripts/dependency-policy.js"; + +const dependency: ProductionDependency = { + name: "koffi", + section: "dependencies", + version: "3.2.1", +}; +const publishedAt = "2026-09-04T07:39:01.277Z"; + +describe("production dependency policy", () => { + it("accepts exact dependencies and optional dependencies", () => { + expect( + assertExactProductionDependencies({ + dependencies: { koffi: "3.2.1" }, + optionalDependencies: { "optional-package": "1.0.0-beta.1" }, + }) + ).toEqual([ + dependency, + { + name: "optional-package", + section: "optionalDependencies", + version: "1.0.0-beta.1", + }, + ]); + }); + + it.each(["^3.2.1", "~3.2.1", ">=3.2.1", "latest", "file:../package"])( + "rejects non-exact requirement %s", + (version) => { + expect(() => + assertExactProductionDependencies({ dependencies: { koffi: version } }) + ).toThrow(`dependencies.koffi must use an exact SemVer version; found '${version}'`); + } + ); + + it("accepts a version at the exact seven-day boundary", () => { + const now = Date.parse(publishedAt) + MINIMUM_DEPENDENCY_AGE_MS; + expect(() => + assertMinimumPublicationAge([dependency], new Map([["koffi@3.2.1", publishedAt]]), now) + ).not.toThrow(); + }); + + it("rejects a version newer than seven full days", () => { + const now = Date.parse(publishedAt) + MINIMUM_DEPENDENCY_AGE_MS - 1; + expect(() => + assertMinimumPublicationAge([dependency], new Map([["koffi@3.2.1", publishedAt]]), now) + ).toThrow("is not eligible until 2026-09-11T07:39:01.277Z"); + }); + + it("exempts GitHub packages from the publication cooldown", () => { + expect(() => + assertMinimumPublicationAge( + [ + { + name: "@github/copilot", + section: "dependencies", + version: "1.0.0", + }, + ], + new Map(), + Date.parse("2026-09-16T00:00:00Z") + ) + ).not.toThrow(); + }); + + it("loads authoritative publication times from the npm registry", async () => { + const requestedUrls: string[] = []; + const request: RegistryRequest = async (url) => { + requestedUrls.push(url); + return { + ok: true, + status: 200, + statusText: "OK", + json: async () => ({ + time: { + "3.2.1": publishedAt, + }, + }), + }; + }; + + await expect(loadNpmPublicationTimes([dependency], request)).resolves.toEqual( + new Map([["koffi@3.2.1", publishedAt]]) + ); + expect(requestedUrls).toEqual(["https://registry.npmjs.org/koffi"]); + }); + + it("surfaces npm metadata request failures", async () => { + const request: RegistryRequest = async () => ({ + ok: false, + status: 503, + statusText: "Service Unavailable", + json: async () => ({}), + }); + + await expect(loadNpmPublicationTimes([dependency], request)).rejects.toThrow( + "npm metadata request for koffi failed: 503 Service Unavailable" + ); + }); +}); diff --git a/nodejs/tsconfig.test.json b/nodejs/tsconfig.test.json index cc659e9a3a..8d24d6dfd8 100644 --- a/nodejs/tsconfig.test.json +++ b/nodejs/tsconfig.test.json @@ -7,6 +7,7 @@ }, "include": [ "src/**/*", + "test/dependency-policy.test.ts", "test/ffiRuntimeHost.test.ts", "test/session-event-types.test.ts", "test/message-source.test.ts" From 4e0ab1994e90ba2c54c41a775bcee20a6e701bd4 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 16 Sep 2026 14:35:38 -0700 Subject: [PATCH 2/2] Validate resolved production dependency ages Walk the package-lock v3 production closure so optional and transitive package versions are subject to the seven-day npm publication policy while dev-only packages remain excluded. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 823a3ba3-43a5-4b6a-be71-625e7660994a --- nodejs/scripts/dependency-policy.ts | 39 +++++++++- nodejs/scripts/verify-dependency-policy.ts | 14 ++-- nodejs/test/dependency-policy.test.ts | 85 +++++++++++++++++++++- 3 files changed, 128 insertions(+), 10 deletions(-) diff --git a/nodejs/scripts/dependency-policy.ts b/nodejs/scripts/dependency-policy.ts index fcbc33dea9..735a2acdf6 100644 --- a/nodejs/scripts/dependency-policy.ts +++ b/nodejs/scripts/dependency-policy.ts @@ -13,7 +13,6 @@ export interface ProductionDependencyManifest { export interface ProductionDependency { name: string; - section: "dependencies" | "optionalDependencies"; version: string; } @@ -71,12 +70,48 @@ export function assertExactProductionDependencies( version, `${section}.${name} must use an exact SemVer version; found '${version}'` ); - dependencies.push({ name, section, version }); + dependencies.push({ name, version }); } } return dependencies; } +function packageNameFromLockfilePath(path: string): string | undefined { + return /(?:^|\/)node_modules\/((?:@[^/]+\/)?[^/]+)$/.exec(path)?.[1]; +} + +export function parseResolvedProductionDependencies(lockfile: unknown): ProductionDependency[] { + assert(isRecord(lockfile), "package-lock.json must contain a JSON object"); + assert.equal(lockfile.lockfileVersion, 3, "package-lock.json must use lockfileVersion 3"); + assert(isRecord(lockfile.packages), "package-lock.json must contain a packages object"); + + const dependencies = new Map(); + for (const [path, value] of Object.entries(lockfile.packages)) { + if (path === "") { + continue; + } + assert(isRecord(value), `Package-lock entry '${path}' must be a JSON object`); + // npm marks dev=true only when a package is strictly dev-only. Optional and + // devOptional packages reachable from production dependencies remain included. + if (value.dev === true || value.link === true || value.inBundle === true) { + continue; + } + const name = packageNameFromLockfilePath(path); + assert(name, `Unsupported production package-lock path '${path}'`); + const version = value.version; + assert( + typeof version === "string" && semver.valid(version) === version, + `Resolved production dependency ${name} must use an exact SemVer version` + ); + dependencies.set(`${name}@${version}`, { name, version }); + } + return [...dependencies.values()]; +} + +export function readResolvedProductionDependencies(path: string): ProductionDependency[] { + return parseResolvedProductionDependencies(JSON.parse(readFileSync(path, "utf8")) as unknown); +} + export function requiresPublicationCooldown(dependencyName: string): boolean { return !dependencyName.startsWith("@github/"); } diff --git a/nodejs/scripts/verify-dependency-policy.ts b/nodejs/scripts/verify-dependency-policy.ts index e102a91c36..b3cc2e59c6 100644 --- a/nodejs/scripts/verify-dependency-policy.ts +++ b/nodejs/scripts/verify-dependency-policy.ts @@ -5,20 +5,24 @@ import { assertMinimumPublicationAge, loadNpmPublicationTimes, readProductionDependencyManifest, + readResolvedProductionDependencies, requiresPublicationCooldown, } from "./dependency-policy.js"; async function main(): Promise { const nodeRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); const manifest = readProductionDependencyManifest(join(nodeRoot, "package.json")); - const dependencies = assertExactProductionDependencies(manifest); - const publicationTimes = await loadNpmPublicationTimes(dependencies); - assertMinimumPublicationAge(dependencies, publicationTimes); - const externalCount = dependencies.filter((dependency) => + const directDependencies = assertExactProductionDependencies(manifest); + const resolvedDependencies = readResolvedProductionDependencies( + join(nodeRoot, "package-lock.json") + ); + const publicationTimes = await loadNpmPublicationTimes(resolvedDependencies); + assertMinimumPublicationAge(resolvedDependencies, publicationTimes); + const externalCount = resolvedDependencies.filter((dependency) => requiresPublicationCooldown(dependency.name) ).length; console.log( - `Verified ${dependencies.length} exact production dependencies; ${externalCount} meet the seven-day npm publication age requirement.` + `Verified ${directDependencies.length} exact direct production dependencies; ${externalCount} resolved production package versions meet the seven-day npm publication age requirement.` ); } diff --git a/nodejs/test/dependency-policy.test.ts b/nodejs/test/dependency-policy.test.ts index 49a1b4f802..eaa1fc7c34 100644 --- a/nodejs/test/dependency-policy.test.ts +++ b/nodejs/test/dependency-policy.test.ts @@ -4,13 +4,13 @@ import { assertMinimumPublicationAge, loadNpmPublicationTimes, MINIMUM_DEPENDENCY_AGE_MS, + parseResolvedProductionDependencies, type ProductionDependency, type RegistryRequest, } from "../scripts/dependency-policy.js"; const dependency: ProductionDependency = { name: "koffi", - section: "dependencies", version: "3.2.1", }; const publishedAt = "2026-09-04T07:39:01.277Z"; @@ -26,7 +26,6 @@ describe("production dependency policy", () => { dependency, { name: "optional-package", - section: "optionalDependencies", version: "1.0.0-beta.1", }, ]); @@ -61,7 +60,6 @@ describe("production dependency policy", () => { [ { name: "@github/copilot", - section: "dependencies", version: "1.0.0", }, ], @@ -105,4 +103,85 @@ describe("production dependency policy", () => { "npm metadata request for koffi failed: 503 Service Unavailable" ); }); + + it("reads the complete resolved production graph from package-lock v3", () => { + expect( + parseResolvedProductionDependencies({ + lockfileVersion: 3, + packages: { + "": { + dependencies: { koffi: "3.2.1" }, + devDependencies: { eslint: "^9.0.0" }, + }, + "node_modules/koffi": { version: "3.2.1" }, + "node_modules/@koromix/koffi-linux-x64": { + version: "3.2.1", + optional: true, + }, + "node_modules/shared-optional": { + version: "2.0.0", + devOptional: true, + }, + "node_modules/eslint": { version: "9.0.0", dev: true }, + "node_modules/dev-optional": { + version: "1.0.0", + dev: true, + optional: true, + }, + "node_modules/local-package": { link: true }, + "node_modules/bundled-package": { + version: "1.0.0", + inBundle: true, + }, + "node_modules/parent/node_modules/nested": { version: "4.0.0" }, + }, + }) + ).toEqual([ + { name: "koffi", version: "3.2.1" }, + { + name: "@koromix/koffi-linux-x64", + version: "3.2.1", + }, + { name: "shared-optional", version: "2.0.0" }, + { name: "nested", version: "4.0.0" }, + ]); + }); + + it("keeps distinct resolved versions and removes duplicate package/version pairs", () => { + expect( + parseResolvedProductionDependencies({ + lockfileVersion: 3, + packages: { + "": {}, + "node_modules/example": { version: "1.0.0" }, + "node_modules/parent/node_modules/example": { version: "1.0.0" }, + "node_modules/other/node_modules/example": { version: "2.0.0" }, + }, + }) + ).toEqual([ + { name: "example", version: "1.0.0" }, + { name: "example", version: "2.0.0" }, + ]); + }); + + it("rejects unsupported production lockfile entries", () => { + expect(() => + parseResolvedProductionDependencies({ + lockfileVersion: 3, + packages: { + "": {}, + "packages/example": { version: "1.0.0" }, + }, + }) + ).toThrow("Unsupported production package-lock path 'packages/example'"); + expect(() => + parseResolvedProductionDependencies({ + lockfileVersion: 3, + packages: { + "": {}, + "node_modules/example": { version: "github:owner/example" }, + }, + }) + ).toThrow("Resolved production dependency example must use an exact SemVer version"); + }); });