-
Notifications
You must be signed in to change notification settings - Fork 5
chore: generate and publish npm-shrinkwrap.json to lock full dependency tree #563
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| #!/usr/bin/env node | ||
| /** | ||
| * generate-shrinkwrap.js | ||
| * | ||
| * 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 | ||
| * 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') | ||
| const SHRINKWRAP_PATH = path.join(ROOT, 'npm-shrinkwrap.json') | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // 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() | ||
|
Comment on lines
+61
to
+67
|
||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // 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', | ||
| ) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This script claims to generate shrinkwrap for production deps only (and
verify-shrinkwrap.jsassumes--omit=dev), but the npm commands here don't omit dev dependencies. That will include dev packages inpackage-lock.json/npm-shrinkwrap.jsonand can publish a larger-than-intended dependency tree. Add--omit=dev(and ensure it applies to both the lock generation and shrinkwrap conversion).