From 8b3a95f3b4975c9efb1a98126753cf86abf7f402 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Fri, 3 Apr 2026 17:09:23 -0400 Subject: [PATCH 1/2] chore: generate and publish npm-shrinkwrap.json to lock full dependency tree --- .gitignore | 5 + package.json | 59 +++++----- scripts/generate-shrinkwrap.js | 68 +++++++++++ scripts/verify-shrinkwrap.js | 203 +++++++++++++++++++++++++++++++++ yarn.lock | 154 ++++++++++++------------- 5 files changed, 383 insertions(+), 106 deletions(-) create mode 100644 scripts/generate-shrinkwrap.js create mode 100644 scripts/verify-shrinkwrap.js diff --git a/.gitignore b/.gitignore index 2afb3596..32ae089f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ +# Generated at publish time by `npm shrinkwrap`; not committed (yarn.lock is source of truth) +npm-shrinkwrap.json +package-lock.json +yarn.lock.shrinkwrap-bak + # MCP validation schema cache .schema/ .DS_Store diff --git a/package.json b/package.json index 16061a9b..b4f3d81e 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,10 @@ "format": "prettier --write \"src/**/*.{ts,js,json}\" \"test/**/*.{ts,js,json}\" \"test-utils/**/*.{ts,js,json}\" \"*.{ts,js,json,md}\"", "format:check": "prettier --check \"src/**/*.{ts,js,json}\" \"test/**/*.{ts,js,json}\" \"test-utils/**/*.{ts,js,json}\" \"*.{ts,js,json,md}\"", "lint": "eslint . --config eslint.config.mjs", - "postpack": "shx rm -f oclif.manifest.json", + "postpack": "shx rm -f oclif.manifest.json npm-shrinkwrap.json package-lock.json", "posttest": "yarn lint", - "prepack": "yarn build && oclif readme --multi", + "prepack": "yarn build && oclif readme --multi && node scripts/generate-shrinkwrap.js", + "shrinkwrap": "node scripts/generate-shrinkwrap.js && yarn install", "pretest": "yarn format:check", "test": "vitest run --config vitest.config.ts && yarn workspace @devcycle/mcp-worker test", "test:ci": "vitest run --config vitest.config.ts && yarn workspace @devcycle/mcp-worker test", @@ -40,34 +41,34 @@ "version": "oclif readme --multi && git add README.md" }, "dependencies": { - "@babel/parser": "^7.28.0", - "@modelcontextprotocol/sdk": "^1.27.1", - "@oclif/core": "^2.16.0", - "@oclif/plugin-autocomplete": "^2.3.10", - "@oclif/plugin-help": "^6.2.27", - "@types/estraverse": "^5.1.7", - "@types/inquirer": "^8.2.10", - "@types/inquirer-autocomplete-prompt": "^2.0.2", - "@types/js-yaml": "^4.0.9", - "@types/validator": "^13.12.2", - "@zodios/core": "^10.9.6", + "@babel/parser": "7.28.0", + "@modelcontextprotocol/sdk": "1.27.1", + "@oclif/core": "2.16.0", + "@oclif/plugin-autocomplete": "2.3.10", + "@oclif/plugin-help": "6.2.27", + "@types/estraverse": "5.1.7", + "@types/inquirer": "8.2.10", + "@types/inquirer-autocomplete-prompt": "2.0.2", + "@types/js-yaml": "4.0.9", + "@types/validator": "13.12.2", + "@zodios/core": "10.9.6", "axios": "1.13.6", - "chalk": "^4.1.2", - "class-transformer": "^0.5.1", - "class-validator": "^0.14.2", - "estraverse": "^5.3.0", - "fuzzy": "^0.1.3", - "inquirer": "^8.2.6", - "inquirer-autocomplete-prompt": "^2.0.1", - "js-sha256": "^0.11.0", - "js-yaml": "^4.1.0", - "lodash": "^4.17.23", - "minimatch": "^9.0.7", - "open": "^8.4.2", - "parse-diff": "^0.9.0", - "recast": "^0.21.5", - "reflect-metadata": "^0.1.14", - "zod": "~3.25.76" + "chalk": "4.1.2", + "class-transformer": "0.5.1", + "class-validator": "0.14.2", + "estraverse": "5.3.0", + "fuzzy": "0.1.3", + "inquirer": "8.2.6", + "inquirer-autocomplete-prompt": "2.0.1", + "js-sha256": "0.11.0", + "js-yaml": "4.1.1", + "lodash": "4.17.23", + "minimatch": "9.0.9", + "open": "8.4.2", + "parse-diff": "0.9.0", + "recast": "0.21.5", + "reflect-metadata": "0.1.14", + "zod": "3.25.76" }, "devDependencies": { "@anthropic-ai/tokenizer": "^0.0.4", diff --git a/scripts/generate-shrinkwrap.js b/scripts/generate-shrinkwrap.js new file mode 100644 index 00000000..7092f4c9 --- /dev/null +++ b/scripts/generate-shrinkwrap.js @@ -0,0 +1,68 @@ +#!/usr/bin/env node +/** + * generate-shrinkwrap.js + * + * Generates npm-shrinkwrap.json for production dependencies only, while + * preserving yarn.lock intact. + * + * Why the yarn.lock protection is needed: + * `npm install --package-lock-only` rewrites yarn.lock with a Yarn v1 + * format file, corrupting the Yarn Berry lockfile that the project uses + * for development. We save and restore it around the npm operation. + */ + +'use strict' + +const { execSync } = require('child_process') +const fs = require('fs') +const path = require('path') + +const ROOT = path.resolve(__dirname, '..') +const YARN_LOCK = path.join(ROOT, 'yarn.lock') +const YARN_LOCK_BAK = path.join(ROOT, 'yarn.lock.shrinkwrap-bak') + +// --------------------------------------------------------------------------- +// Save yarn.lock before npm touches it +// --------------------------------------------------------------------------- + +if (!fs.existsSync(YARN_LOCK)) { + console.error( + 'Error: yarn.lock not found. Cannot protect it from npm overwrite.', + ) + process.exit(1) +} + +const yarnLockContent = fs.readFileSync(YARN_LOCK) + +function restoreYarnLock() { + fs.writeFileSync(YARN_LOCK, yarnLockContent) + if (fs.existsSync(YARN_LOCK_BAK)) fs.unlinkSync(YARN_LOCK_BAK) +} + +// Write a physical backup too, so a crash mid-run doesn't lose the file. +fs.writeFileSync(YARN_LOCK_BAK, yarnLockContent) + +// --------------------------------------------------------------------------- +// Run npm operations, always restoring yarn.lock afterwards +// --------------------------------------------------------------------------- + +try { + console.log('Generating package-lock.json from installed node_modules...') + execSync('npm install --package-lock-only --ignore-scripts', { + cwd: ROOT, + stdio: 'inherit', + }) + + console.log('Converting package-lock.json to npm-shrinkwrap.json...') + execSync('npm shrinkwrap', { + cwd: ROOT, + stdio: 'inherit', + }) +} catch (err) { + console.error('Error during shrinkwrap generation:', err.message) + process.exit(1) +} finally { + // Always restore yarn.lock. npm operations rewrite it to Yarn v1 format, + // which corrupts the Yarn Berry lockfile used for development. + restoreYarnLock() +} diff --git a/scripts/verify-shrinkwrap.js b/scripts/verify-shrinkwrap.js new file mode 100644 index 00000000..b3b87074 --- /dev/null +++ b/scripts/verify-shrinkwrap.js @@ -0,0 +1,203 @@ +#!/usr/bin/env node +/** + * verify-shrinkwrap.js + * + * 1. Strips workspace-specific entries from npm-shrinkwrap.json (they're not + * published with the package and would confuse consumers' npm installs). + * 2. Verifies that every production package version in npm-shrinkwrap.json + * matches the version actually installed in node_modules by Yarn. + * + * Dev packages are excluded from both the shrinkwrap (via --omit=dev in + * generate-shrinkwrap.js) and this check — consumers never install them. + * + * Comparing against node_modules directly (rather than `yarn info`) avoids the + * issue where Yarn Berry throws an error when npm-shrinkwrap.json is present. + * + * Exits 0 if all versions match, 1 if any mismatches are found. + */ + +'use strict' + +const fs = require('fs') +const path = require('path') + +const ROOT = path.resolve(__dirname, '..') +const SHRINKWRAP_PATH = path.join(ROOT, 'npm-shrinkwrap.json') +const NODE_MODULES = path.join(ROOT, 'node_modules') + +// --------------------------------------------------------------------------- +// 1. Read npm-shrinkwrap.json +// --------------------------------------------------------------------------- + +if (!fs.existsSync(SHRINKWRAP_PATH)) { + console.error( + 'Error: npm-shrinkwrap.json not found. Run `npm shrinkwrap` first.', + ) + process.exit(1) +} + +const shrinkwrap = JSON.parse(fs.readFileSync(SHRINKWRAP_PATH, 'utf8')) +const allPackages = shrinkwrap.packages || {} + +// --------------------------------------------------------------------------- +// 2. Strip workspace entries and write cleaned shrinkwrap back +// +// Workspace entries arise because npm reads the `workspaces` field from +// package.json. They take two forms: +// - Keys without a "node_modules/" prefix (e.g. "mcp-worker") +// - Symlink entries with { "link": true } (e.g. "node_modules/@devcycle/mcp-worker") +// +// These entries reference a local directory that does not exist when a +// consumer installs the published package, so they must be removed. +// --------------------------------------------------------------------------- + +let strippedCount = 0 +const cleanedPackages = {} + +for (const [key, value] of Object.entries(allPackages)) { + // Always keep the root entry (empty string key) + if (key === '') { + cleanedPackages[key] = value + continue + } + + // Drop workspace root entries (not under node_modules/) + if (!key.startsWith('node_modules/')) { + strippedCount++ + continue + } + + // Drop workspace symlink entries + if (value.link === true) { + strippedCount++ + continue + } + + cleanedPackages[key] = value +} + +if (strippedCount > 0) { + console.log( + `Stripped ${strippedCount} workspace entries from npm-shrinkwrap.json.`, + ) + shrinkwrap.packages = cleanedPackages + fs.writeFileSync( + SHRINKWRAP_PATH, + JSON.stringify(shrinkwrap, null, 2) + '\n', + ) +} + +// --------------------------------------------------------------------------- +// 3. Build a map of package name → version from node_modules +// (what Yarn actually installed) +// --------------------------------------------------------------------------- + +/** + * Read the installed version for a package from node_modules. + * Returns null if the package is not found (e.g. optional deps not installed + * on this platform). + */ +function getInstalledVersion(pkgName) { + const pkgJsonPath = path.join(NODE_MODULES, pkgName, 'package.json') + if (!fs.existsSync(pkgJsonPath)) return null + try { + const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')) + return pkg.version || null + } catch { + return null + } +} + +// --------------------------------------------------------------------------- +// 4. Compare shrinkwrap versions against installed versions +// --------------------------------------------------------------------------- + +const mismatches = [] +const missing = [] + +for (const [key, value] of Object.entries(cleanedPackages)) { + if (!key.startsWith('node_modules/')) continue + const shrinkwrapVersion = value.version + if (!shrinkwrapVersion) continue + + // Strip "node_modules/" prefix; skip nested installs (contain a second "node_modules/") + const pkgName = key.slice('node_modules/'.length) + if (pkgName.includes('/node_modules/')) continue + + // Skip dev-only packages — the shrinkwrap is generated with --omit=dev so + // these should not appear, but guard against any that slip through. + if (value.dev === true) continue + + // Skip @types/* packages: pure TypeScript type definitions, stripped at + // compile time and absent from the distributed dist/ output. Version + // differences have no runtime or security impact. + if (pkgName.startsWith('@types/')) continue + + const installedVersion = getInstalledVersion(pkgName) + + if (installedVersion === null) { + // Platform-specific optional dep not installed on this OS; warn only + missing.push({ pkg: pkgName, shrinkwrapVersion }) + continue + } + + if (installedVersion !== shrinkwrapVersion) { + mismatches.push({ pkg: pkgName, shrinkwrapVersion, installedVersion }) + } +} + +// --------------------------------------------------------------------------- +// 5. Report +// --------------------------------------------------------------------------- + +const totalChecked = Object.keys(cleanedPackages).filter((k) => { + if (!k.startsWith('node_modules/')) return false + const name = k.slice('node_modules/'.length) + if (name.includes('/node_modules/')) return false + if (name.startsWith('@types/')) return false + const pkg = cleanedPackages[k] + if (pkg.dev === true) return false + return true +}).length + +if (missing.length > 0) { + console.warn( + `\nWarning: ${missing.length} package(s) in npm-shrinkwrap.json were not found in node_modules.`, + ) + console.warn( + 'These may be optional/platform-specific packages not installed on this OS:', + ) + for (const { pkg, shrinkwrapVersion } of missing) { + console.warn(` ${pkg}@${shrinkwrapVersion}`) + } +} + +if (mismatches.length === 0) { + console.log( + `\n✓ Shrinkwrap verified: all ${totalChecked} packages match installed versions in node_modules.`, + ) + process.exit(0) +} + +console.error( + `\n✗ Shrinkwrap verification failed: ${mismatches.length} version mismatch(es) detected.`, +) +console.error( + 'npm-shrinkwrap.json and node_modules have different versions for these packages:\n', +) +console.error( + `${'Package'.padEnd(50)} ${'npm-shrinkwrap'.padEnd(20)} ${'node_modules'.padEnd(20)}`, +) +console.error('-'.repeat(90)) +for (const { pkg, shrinkwrapVersion, installedVersion } of mismatches) { + console.error( + `${pkg.padEnd(50)} ${shrinkwrapVersion.padEnd(20)} ${installedVersion.padEnd(20)}`, + ) +} +console.error( + '\nThis means the npm-shrinkwrap.json does not match what Yarn installed in node_modules.', +) +console.error( + 'Run `yarn install` to ensure node_modules is up to date with yarn.lock, then retry.', +) +process.exit(1) diff --git a/yarn.lock b/yarn.lock index b0c7e1c4..9515ffab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -215,7 +215,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.15.7, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.0": +"@babel/parser@npm:7.28.0, @babel/parser@npm:^7.15.7, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.0": version: 7.28.0 resolution: "@babel/parser@npm:7.28.0" dependencies: @@ -365,59 +365,59 @@ __metadata: "@babel/core": "npm:^7.28.0" "@babel/generator": "npm:^7.28.0" "@babel/helper-validator-identifier": "npm:^7.27.1" - "@babel/parser": "npm:^7.28.0" + "@babel/parser": "npm:7.28.0" "@babel/template": "npm:^7.27.2" "@babel/traverse": "npm:^7.28.0" "@babel/types": "npm:^7.28.0" "@eslint/js": "npm:^9.18.0" - "@modelcontextprotocol/sdk": "npm:^1.27.1" - "@oclif/core": "npm:^2.16.0" - "@oclif/plugin-autocomplete": "npm:^2.3.10" - "@oclif/plugin-help": "npm:^6.2.27" + "@modelcontextprotocol/sdk": "npm:1.27.1" + "@oclif/core": "npm:2.16.0" + "@oclif/plugin-autocomplete": "npm:2.3.10" + "@oclif/plugin-help": "npm:6.2.27" "@oclif/test": "npm:^2.5.6" - "@types/estraverse": "npm:^5.1.7" - "@types/inquirer": "npm:^8.2.10" - "@types/inquirer-autocomplete-prompt": "npm:^2.0.2" - "@types/js-yaml": "npm:^4.0.9" + "@types/estraverse": "npm:5.1.7" + "@types/inquirer": "npm:8.2.10" + "@types/inquirer-autocomplete-prompt": "npm:2.0.2" + "@types/js-yaml": "npm:4.0.9" "@types/minimatch": "npm:^5.1.2" "@types/node": "npm:^18.19.68" - "@types/validator": "npm:^13.12.2" + "@types/validator": "npm:13.12.2" "@typescript-eslint/eslint-plugin": "npm:^8.21.0" "@typescript-eslint/parser": "npm:^8.21.0" - "@zodios/core": "npm:^10.9.6" + "@zodios/core": "npm:10.9.6" ajv: "npm:^8.18.0" ajv-cli: "npm:^5.0.0" ajv-formats: "npm:^3.0.1" axios: "npm:1.13.6" - chalk: "npm:^4.1.2" - class-transformer: "npm:^0.5.1" - class-validator: "npm:^0.14.2" + chalk: "npm:4.1.2" + class-transformer: "npm:0.5.1" + class-validator: "npm:0.14.2" eslint: "npm:^9.18.0" eslint-config-prettier: "npm:^9.1.0" - estraverse: "npm:^5.3.0" - fuzzy: "npm:^0.1.3" + estraverse: "npm:5.3.0" + fuzzy: "npm:0.1.3" gpt-tokenizer: "npm:^3.0.1" - inquirer: "npm:^8.2.6" - inquirer-autocomplete-prompt: "npm:^2.0.1" - js-sha256: "npm:^0.11.0" - js-yaml: "npm:^4.1.0" - lodash: "npm:^4.17.23" - minimatch: "npm:^9.0.7" + inquirer: "npm:8.2.6" + inquirer-autocomplete-prompt: "npm:2.0.1" + js-sha256: "npm:0.11.0" + js-yaml: "npm:4.1.1" + lodash: "npm:4.17.23" + minimatch: "npm:9.0.9" nock: "npm:^13.5.6" oclif: "npm:^3.17.2" - open: "npm:^8.4.2" + open: "npm:8.4.2" openapi-zod-client: "npm:^1.18.3" - parse-diff: "npm:^0.9.0" + parse-diff: "npm:0.9.0" prettier: "npm:^3.5.3" - recast: "npm:^0.21.5" - reflect-metadata: "npm:^0.1.14" + recast: "npm:0.21.5" + reflect-metadata: "npm:0.1.14" shx: "npm:^0.3.4" sinon: "npm:^19.0.2" ts-node: "npm:^10.9.2" typescript: "npm:^5.7.2" typescript-eslint: "npm:^8.21.0" vitest: "npm:^3.2.4" - zod: "npm:~3.25.76" + zod: "npm:3.25.76" bin: dvc: ./bin/run dvc-mcp: ./bin/mcp @@ -1271,7 +1271,7 @@ __metadata: languageName: node linkType: hard -"@modelcontextprotocol/sdk@npm:1.27.1, @modelcontextprotocol/sdk@npm:^1.27.1": +"@modelcontextprotocol/sdk@npm:1.27.1": version: 1.27.1 resolution: "@modelcontextprotocol/sdk@npm:1.27.1" dependencies: @@ -1588,7 +1588,7 @@ __metadata: languageName: node linkType: hard -"@oclif/core@npm:^2.11.4, @oclif/core@npm:^2.15.0, @oclif/core@npm:^2.16.0": +"@oclif/core@npm:2.16.0, @oclif/core@npm:^2.11.4, @oclif/core@npm:^2.15.0": version: 2.16.0 resolution: "@oclif/core@npm:2.16.0" dependencies: @@ -1650,7 +1650,7 @@ __metadata: languageName: node linkType: hard -"@oclif/plugin-autocomplete@npm:^2.3.10": +"@oclif/plugin-autocomplete@npm:2.3.10": version: 2.3.10 resolution: "@oclif/plugin-autocomplete@npm:2.3.10" dependencies: @@ -1661,6 +1661,15 @@ __metadata: languageName: node linkType: hard +"@oclif/plugin-help@npm:6.2.27": + version: 6.2.27 + resolution: "@oclif/plugin-help@npm:6.2.27" + dependencies: + "@oclif/core": "npm:^4" + checksum: 10c0/1c88b5c64550dad4d0dc1898cf12509498033d6b455013331ca4e623e75786d2492e9fdb53ae005c3bec72239de0c6a88530d3ba54f70d63c593c30c3070cda6 + languageName: node + linkType: hard + "@oclif/plugin-help@npm:^5.2.14": version: 5.2.20 resolution: "@oclif/plugin-help@npm:5.2.20" @@ -1670,15 +1679,6 @@ __metadata: languageName: node linkType: hard -"@oclif/plugin-help@npm:^6.2.27": - version: 6.2.27 - resolution: "@oclif/plugin-help@npm:6.2.27" - dependencies: - "@oclif/core": "npm:^4" - checksum: 10c0/1c88b5c64550dad4d0dc1898cf12509498033d6b455013331ca4e623e75786d2492e9fdb53ae005c3bec72239de0c6a88530d3ba54f70d63c593c30c3070cda6 - languageName: node - linkType: hard - "@oclif/plugin-not-found@npm:^2.3.32": version: 2.4.3 resolution: "@oclif/plugin-not-found@npm:2.4.3" @@ -2246,7 +2246,7 @@ __metadata: languageName: node linkType: hard -"@types/estraverse@npm:^5.1.7": +"@types/estraverse@npm:5.1.7": version: 5.1.7 resolution: "@types/estraverse@npm:5.1.7" dependencies: @@ -2285,7 +2285,7 @@ __metadata: languageName: node linkType: hard -"@types/inquirer-autocomplete-prompt@npm:^2.0.2": +"@types/inquirer-autocomplete-prompt@npm:2.0.2": version: 2.0.2 resolution: "@types/inquirer-autocomplete-prompt@npm:2.0.2" dependencies: @@ -2294,7 +2294,7 @@ __metadata: languageName: node linkType: hard -"@types/inquirer@npm:^8, @types/inquirer@npm:^8.2.10": +"@types/inquirer@npm:8.2.10, @types/inquirer@npm:^8": version: 8.2.10 resolution: "@types/inquirer@npm:8.2.10" dependencies: @@ -2304,7 +2304,7 @@ __metadata: languageName: node linkType: hard -"@types/js-yaml@npm:^4.0.9": +"@types/js-yaml@npm:4.0.9": version: 4.0.9 resolution: "@types/js-yaml@npm:4.0.9" checksum: 10c0/24de857aa8d61526bbfbbaa383aa538283ad17363fcd5bb5148e2c7f604547db36646440e739d78241ed008702a8920665d1add5618687b6743858fae00da211 @@ -2423,7 +2423,7 @@ __metadata: languageName: node linkType: hard -"@types/validator@npm:^13.11.8, @types/validator@npm:^13.12.2": +"@types/validator@npm:13.12.2, @types/validator@npm:^13.11.8": version: 13.12.2 resolution: "@types/validator@npm:13.12.2" checksum: 10c0/64f1326c768947d756ab5bcd73f3f11a6f07dc76292aea83890d0390a9b9acb374f8df6b24af2c783271f276d3d613b78fc79491fe87edee62108d54be2e3c31 @@ -2660,7 +2660,7 @@ __metadata: languageName: node linkType: hard -"@zodios/core@npm:^10.3.1, @zodios/core@npm:^10.9.6": +"@zodios/core@npm:10.9.6, @zodios/core@npm:^10.3.1": version: 10.9.6 resolution: "@zodios/core@npm:10.9.6" peerDependencies: @@ -3528,7 +3528,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^4, chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2": +"chalk@npm:4.1.2, chalk@npm:^4, chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -3566,14 +3566,14 @@ __metadata: languageName: node linkType: hard -"class-transformer@npm:^0.5.1": +"class-transformer@npm:0.5.1": version: 0.5.1 resolution: "class-transformer@npm:0.5.1" checksum: 10c0/19809914e51c6db42c036166839906420bb60367df14e15f49c45c8c1231bf25ae661ebe94736ee29cc688b77101ef851a8acca299375cc52fc141b64acde18a languageName: node linkType: hard -"class-validator@npm:^0.14.2": +"class-validator@npm:0.14.2": version: 0.14.2 resolution: "class-validator@npm:0.14.2" dependencies: @@ -5124,7 +5124,7 @@ __metadata: languageName: node linkType: hard -"fuzzy@npm:^0.1.3": +"fuzzy@npm:0.1.3": version: 0.1.3 resolution: "fuzzy@npm:0.1.3" checksum: 10c0/584fcd57a03431707a6d0c1c4a41f17368cdb23d37dcb176d6cbbeeaecaac51be15dec229b3547acfb7db052cb066fcd86db907d40112ac4a3d3a368f88e7105 @@ -5726,7 +5726,7 @@ __metadata: languageName: node linkType: hard -"inquirer-autocomplete-prompt@npm:^2.0.1": +"inquirer-autocomplete-prompt@npm:2.0.1": version: 2.0.1 resolution: "inquirer-autocomplete-prompt@npm:2.0.1" dependencies: @@ -5741,7 +5741,7 @@ __metadata: languageName: node linkType: hard -"inquirer@npm:^8.0.0, inquirer@npm:^8.2.6": +"inquirer@npm:8.2.6, inquirer@npm:^8.0.0": version: 8.2.6 resolution: "inquirer@npm:8.2.6" dependencies: @@ -6049,7 +6049,7 @@ __metadata: languageName: node linkType: hard -"js-sha256@npm:^0.11.0": +"js-sha256@npm:0.11.0": version: 0.11.0 resolution: "js-sha256@npm:0.11.0" checksum: 10c0/90980fe01ca01fbd166751fb16c4caa09c1ab997e8bf77c0764cc05c772c6044946f4c1b3bad266ce78357280d2131d3dc0cf2dd7646e78272996bd4d590aa4f @@ -6070,26 +6070,26 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:^3.13.0, js-yaml@npm:^3.13.1, js-yaml@npm:^3.14.0, js-yaml@npm:^3.14.1": - version: 3.14.1 - resolution: "js-yaml@npm:3.14.1" +"js-yaml@npm:4.1.1, js-yaml@npm:^4.1.0": + version: 4.1.1 + resolution: "js-yaml@npm:4.1.1" dependencies: - argparse: "npm:^1.0.7" - esprima: "npm:^4.0.0" + argparse: "npm:^2.0.1" bin: js-yaml: bin/js-yaml.js - checksum: 10c0/6746baaaeac312c4db8e75fa22331d9a04cccb7792d126ed8ce6a0bbcfef0cedaddd0c5098fade53db067c09fe00aa1c957674b4765610a8b06a5a189e46433b + checksum: 10c0/561c7d7088c40a9bb53cc75becbfb1df6ae49b34b5e6e5a81744b14ae8667ec564ad2527709d1a6e7d5e5fa6d483aa0f373a50ad98d42fde368ec4a190d4fae7 languageName: node linkType: hard -"js-yaml@npm:^4.1.0": - version: 4.1.1 - resolution: "js-yaml@npm:4.1.1" +"js-yaml@npm:^3.13.0, js-yaml@npm:^3.13.1, js-yaml@npm:^3.14.0, js-yaml@npm:^3.14.1": + version: 3.14.1 + resolution: "js-yaml@npm:3.14.1" dependencies: - argparse: "npm:^2.0.1" + argparse: "npm:^1.0.7" + esprima: "npm:^4.0.0" bin: js-yaml: bin/js-yaml.js - checksum: 10c0/561c7d7088c40a9bb53cc75becbfb1df6ae49b34b5e6e5a81744b14ae8667ec564ad2527709d1a6e7d5e5fa6d483aa0f373a50ad98d42fde368ec4a190d4fae7 + checksum: 10c0/6746baaaeac312c4db8e75fa22331d9a04cccb7792d126ed8ce6a0bbcfef0cedaddd0c5098fade53db067c09fe00aa1c957674b4765610a8b06a5a189e46433b languageName: node linkType: hard @@ -6393,6 +6393,13 @@ __metadata: languageName: node linkType: hard +"lodash@npm:4.17.23": + version: 4.17.23 + resolution: "lodash@npm:4.17.23" + checksum: 10c0/1264a90469f5bb95d4739c43eb6277d15b6d9e186df4ac68c3620443160fc669e2f14c11e7d8b2ccf078b81d06147c01a8ccced9aab9f9f63d50dcf8cace6bf6 + languageName: node + linkType: hard + "lodash@npm:^4.17.10, lodash@npm:^4.17.11, lodash@npm:^4.17.13, lodash@npm:^4.17.21": version: 4.17.21 resolution: "lodash@npm:4.17.21" @@ -6400,13 +6407,6 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.17.23": - version: 4.17.23 - resolution: "lodash@npm:4.17.23" - checksum: 10c0/1264a90469f5bb95d4739c43eb6277d15b6d9e186df4ac68c3620443160fc669e2f14c11e7d8b2ccf078b81d06147c01a8ccced9aab9f9f63d50dcf8cace6bf6 - languageName: node - linkType: hard - "log-symbols@npm:^4.0.0, log-symbols@npm:^4.1.0": version: 4.1.0 resolution: "log-symbols@npm:4.1.0" @@ -6758,7 +6758,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^9.0.0, minimatch@npm:^9.0.4, minimatch@npm:^9.0.5, minimatch@npm:^9.0.7": +"minimatch@npm:9.0.9, minimatch@npm:^9.0.0, minimatch@npm:^9.0.4, minimatch@npm:^9.0.5": version: 9.0.9 resolution: "minimatch@npm:9.0.9" dependencies: @@ -7492,7 +7492,7 @@ __metadata: languageName: node linkType: hard -"open@npm:^8.4.2": +"open@npm:8.4.2": version: 8.4.2 resolution: "open@npm:8.4.2" dependencies: @@ -7753,7 +7753,7 @@ __metadata: languageName: node linkType: hard -"parse-diff@npm:^0.9.0": +"parse-diff@npm:0.9.0": version: 0.9.0 resolution: "parse-diff@npm:0.9.0" checksum: 10c0/600b2460ae1b172f31dcfded7878a3c89f401f7c31c4f62fb42572899083618834ea718aa9f5555c8829e8e45fd25f947ea77787e68a62e08b41771810d2daca @@ -8305,7 +8305,7 @@ __metadata: languageName: node linkType: hard -"recast@npm:^0.21.5": +"recast@npm:0.21.5": version: 0.21.5 resolution: "recast@npm:0.21.5" dependencies: @@ -8335,7 +8335,7 @@ __metadata: languageName: node linkType: hard -"reflect-metadata@npm:^0.1.14": +"reflect-metadata@npm:0.1.14": version: 0.1.14 resolution: "reflect-metadata@npm:0.1.14" checksum: 10c0/3a6190c7f6cb224f26a012d11f9e329360c01c1945e2cbefea23976a8bacf9db6b794aeb5bf18adcb673c448a234fbc06fc41853c00a6c206b30f0777ecf019e @@ -10606,7 +10606,7 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.19.1, zod@npm:~3.25.76": +"zod@npm:3.25.76, zod@npm:^3.19.1": version: 3.25.76 resolution: "zod@npm:3.25.76" checksum: 10c0/5718ec35e3c40b600316c5b4c5e4976f7fee68151bc8f8d90ec18a469be9571f072e1bbaace10f1e85cf8892ea12d90821b200e980ab46916a6166a4260a983c From d2f1d09cd12ecb0efdedc870d3c5696bb352ed2b Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Fri, 3 Apr 2026 17:14:22 -0400 Subject: [PATCH 2/2] refactor: move workspace stripping into generate-shrinkwrap.js, drop verify script --- scripts/generate-shrinkwrap.js | 55 ++++++++- scripts/verify-shrinkwrap.js | 203 --------------------------------- 2 files changed, 53 insertions(+), 205 deletions(-) delete mode 100644 scripts/verify-shrinkwrap.js diff --git a/scripts/generate-shrinkwrap.js b/scripts/generate-shrinkwrap.js index 7092f4c9..204606aa 100644 --- a/scripts/generate-shrinkwrap.js +++ b/scripts/generate-shrinkwrap.js @@ -2,8 +2,7 @@ /** * generate-shrinkwrap.js * - * Generates npm-shrinkwrap.json for production dependencies only, while - * preserving yarn.lock intact. + * Generates npm-shrinkwrap.json while preserving yarn.lock intact. * * Why the yarn.lock protection is needed: * `npm install --package-lock-only` rewrites yarn.lock with a Yarn v1 @@ -20,6 +19,7 @@ const path = require('path') const ROOT = path.resolve(__dirname, '..') const YARN_LOCK = path.join(ROOT, 'yarn.lock') const YARN_LOCK_BAK = path.join(ROOT, 'yarn.lock.shrinkwrap-bak') +const SHRINKWRAP_PATH = path.join(ROOT, 'npm-shrinkwrap.json') // --------------------------------------------------------------------------- // Save yarn.lock before npm touches it @@ -66,3 +66,54 @@ try { // which corrupts the Yarn Berry lockfile used for development. restoreYarnLock() } + +// --------------------------------------------------------------------------- +// Strip workspace entries from npm-shrinkwrap.json +// +// Workspace entries arise because npm reads the `workspaces` field from +// package.json. They take two forms: +// - Keys without a "node_modules/" prefix (e.g. "mcp-worker") +// - Symlink entries with { "link": true } (e.g. "node_modules/@devcycle/mcp-worker") +// +// These entries reference a local directory that does not exist when a +// consumer installs the published package, so they must be removed. +// --------------------------------------------------------------------------- + +const shrinkwrap = JSON.parse(fs.readFileSync(SHRINKWRAP_PATH, 'utf8')) +const allPackages = shrinkwrap.packages || {} + +let strippedCount = 0 +const cleanedPackages = {} + +for (const [key, value] of Object.entries(allPackages)) { + // Always keep the root entry (empty string key) + if (key === '') { + cleanedPackages[key] = value + continue + } + + // Drop workspace root entries (not under node_modules/) + if (!key.startsWith('node_modules/')) { + strippedCount++ + continue + } + + // Drop workspace symlink entries + if (value.link === true) { + strippedCount++ + continue + } + + cleanedPackages[key] = value +} + +if (strippedCount > 0) { + console.log( + `Stripped ${strippedCount} workspace entries from npm-shrinkwrap.json.`, + ) + shrinkwrap.packages = cleanedPackages + fs.writeFileSync( + SHRINKWRAP_PATH, + JSON.stringify(shrinkwrap, null, 2) + '\n', + ) +} diff --git a/scripts/verify-shrinkwrap.js b/scripts/verify-shrinkwrap.js deleted file mode 100644 index b3b87074..00000000 --- a/scripts/verify-shrinkwrap.js +++ /dev/null @@ -1,203 +0,0 @@ -#!/usr/bin/env node -/** - * verify-shrinkwrap.js - * - * 1. Strips workspace-specific entries from npm-shrinkwrap.json (they're not - * published with the package and would confuse consumers' npm installs). - * 2. Verifies that every production package version in npm-shrinkwrap.json - * matches the version actually installed in node_modules by Yarn. - * - * Dev packages are excluded from both the shrinkwrap (via --omit=dev in - * generate-shrinkwrap.js) and this check — consumers never install them. - * - * Comparing against node_modules directly (rather than `yarn info`) avoids the - * issue where Yarn Berry throws an error when npm-shrinkwrap.json is present. - * - * Exits 0 if all versions match, 1 if any mismatches are found. - */ - -'use strict' - -const fs = require('fs') -const path = require('path') - -const ROOT = path.resolve(__dirname, '..') -const SHRINKWRAP_PATH = path.join(ROOT, 'npm-shrinkwrap.json') -const NODE_MODULES = path.join(ROOT, 'node_modules') - -// --------------------------------------------------------------------------- -// 1. Read npm-shrinkwrap.json -// --------------------------------------------------------------------------- - -if (!fs.existsSync(SHRINKWRAP_PATH)) { - console.error( - 'Error: npm-shrinkwrap.json not found. Run `npm shrinkwrap` first.', - ) - process.exit(1) -} - -const shrinkwrap = JSON.parse(fs.readFileSync(SHRINKWRAP_PATH, 'utf8')) -const allPackages = shrinkwrap.packages || {} - -// --------------------------------------------------------------------------- -// 2. Strip workspace entries and write cleaned shrinkwrap back -// -// Workspace entries arise because npm reads the `workspaces` field from -// package.json. They take two forms: -// - Keys without a "node_modules/" prefix (e.g. "mcp-worker") -// - Symlink entries with { "link": true } (e.g. "node_modules/@devcycle/mcp-worker") -// -// These entries reference a local directory that does not exist when a -// consumer installs the published package, so they must be removed. -// --------------------------------------------------------------------------- - -let strippedCount = 0 -const cleanedPackages = {} - -for (const [key, value] of Object.entries(allPackages)) { - // Always keep the root entry (empty string key) - if (key === '') { - cleanedPackages[key] = value - continue - } - - // Drop workspace root entries (not under node_modules/) - if (!key.startsWith('node_modules/')) { - strippedCount++ - continue - } - - // Drop workspace symlink entries - if (value.link === true) { - strippedCount++ - continue - } - - cleanedPackages[key] = value -} - -if (strippedCount > 0) { - console.log( - `Stripped ${strippedCount} workspace entries from npm-shrinkwrap.json.`, - ) - shrinkwrap.packages = cleanedPackages - fs.writeFileSync( - SHRINKWRAP_PATH, - JSON.stringify(shrinkwrap, null, 2) + '\n', - ) -} - -// --------------------------------------------------------------------------- -// 3. Build a map of package name → version from node_modules -// (what Yarn actually installed) -// --------------------------------------------------------------------------- - -/** - * Read the installed version for a package from node_modules. - * Returns null if the package is not found (e.g. optional deps not installed - * on this platform). - */ -function getInstalledVersion(pkgName) { - const pkgJsonPath = path.join(NODE_MODULES, pkgName, 'package.json') - if (!fs.existsSync(pkgJsonPath)) return null - try { - const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')) - return pkg.version || null - } catch { - return null - } -} - -// --------------------------------------------------------------------------- -// 4. Compare shrinkwrap versions against installed versions -// --------------------------------------------------------------------------- - -const mismatches = [] -const missing = [] - -for (const [key, value] of Object.entries(cleanedPackages)) { - if (!key.startsWith('node_modules/')) continue - const shrinkwrapVersion = value.version - if (!shrinkwrapVersion) continue - - // Strip "node_modules/" prefix; skip nested installs (contain a second "node_modules/") - const pkgName = key.slice('node_modules/'.length) - if (pkgName.includes('/node_modules/')) continue - - // Skip dev-only packages — the shrinkwrap is generated with --omit=dev so - // these should not appear, but guard against any that slip through. - if (value.dev === true) continue - - // Skip @types/* packages: pure TypeScript type definitions, stripped at - // compile time and absent from the distributed dist/ output. Version - // differences have no runtime or security impact. - if (pkgName.startsWith('@types/')) continue - - const installedVersion = getInstalledVersion(pkgName) - - if (installedVersion === null) { - // Platform-specific optional dep not installed on this OS; warn only - missing.push({ pkg: pkgName, shrinkwrapVersion }) - continue - } - - if (installedVersion !== shrinkwrapVersion) { - mismatches.push({ pkg: pkgName, shrinkwrapVersion, installedVersion }) - } -} - -// --------------------------------------------------------------------------- -// 5. Report -// --------------------------------------------------------------------------- - -const totalChecked = Object.keys(cleanedPackages).filter((k) => { - if (!k.startsWith('node_modules/')) return false - const name = k.slice('node_modules/'.length) - if (name.includes('/node_modules/')) return false - if (name.startsWith('@types/')) return false - const pkg = cleanedPackages[k] - if (pkg.dev === true) return false - return true -}).length - -if (missing.length > 0) { - console.warn( - `\nWarning: ${missing.length} package(s) in npm-shrinkwrap.json were not found in node_modules.`, - ) - console.warn( - 'These may be optional/platform-specific packages not installed on this OS:', - ) - for (const { pkg, shrinkwrapVersion } of missing) { - console.warn(` ${pkg}@${shrinkwrapVersion}`) - } -} - -if (mismatches.length === 0) { - console.log( - `\n✓ Shrinkwrap verified: all ${totalChecked} packages match installed versions in node_modules.`, - ) - process.exit(0) -} - -console.error( - `\n✗ Shrinkwrap verification failed: ${mismatches.length} version mismatch(es) detected.`, -) -console.error( - 'npm-shrinkwrap.json and node_modules have different versions for these packages:\n', -) -console.error( - `${'Package'.padEnd(50)} ${'npm-shrinkwrap'.padEnd(20)} ${'node_modules'.padEnd(20)}`, -) -console.error('-'.repeat(90)) -for (const { pkg, shrinkwrapVersion, installedVersion } of mismatches) { - console.error( - `${pkg.padEnd(50)} ${shrinkwrapVersion.padEnd(20)} ${installedVersion.padEnd(20)}`, - ) -} -console.error( - '\nThis means the npm-shrinkwrap.json does not match what Yarn installed in node_modules.', -) -console.error( - 'Run `yarn install` to ensure node_modules is up to date with yarn.lock, then retry.', -) -process.exit(1)