From 40829ec40c33a6d23f18715e60e3395bdcb0467e Mon Sep 17 00:00:00 2001 From: Ruy Adorno Date: Thu, 10 Jun 2021 17:11:08 -0400 Subject: [PATCH 01/20] fix(link): do not prune packages `npm link ` is meant to be used as a way to link a local package to an install tree and it's very surprising to users that it may prune extraneous deps from the project. This change switches the default behavior to avoid pruning deps when reifying the dependencies in npm link. Fixes: https://github.com/npm/cli/issues/2554 PR-URL: https://github.com/npm/cli/pull/3399 Credit: @ruyadorno Close: #3399 Reviewed-by: @ljharb, @nlf --- lib/link.js | 2 ++ test/lib/link.js | 50 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/lib/link.js b/lib/link.js index 47fe4b17a272b..d6abf139730bd 100644 --- a/lib/link.js +++ b/lib/link.js @@ -134,12 +134,14 @@ class Link extends ArboristWorkspaceCmd { // reify all the pending names as symlinks there const localArb = new Arborist({ ...this.npm.flatOptions, + prune: false, log: this.npm.log, path: this.npm.prefix, save, }) await localArb.reify({ ...this.npm.flatOptions, + prune: false, path: this.npm.prefix, log: this.npm.log, add: names.map(l => `file:${resolve(globalTop, 'node_modules', l)}`), diff --git a/test/lib/link.js b/test/lib/link.js index 3cad0ff90362d..64375cfc13c2c 100644 --- a/test/lib/link.js +++ b/test/lib/link.js @@ -1,4 +1,5 @@ const { resolve } = require('path') +const fs = require('fs') const Arborist = require('@npmcli/arborist') const t = require('tap') @@ -485,6 +486,55 @@ t.test('link pkg already in global space when prefix is a symlink', (t) => { }) }) +t.test('should not prune dependencies when linking packages', async t => { + const testdir = t.testdir({ + 'global-prefix': { + lib: { + node_modules: { + linked: t.fixture('symlink', '../../../linked'), + }, + }, + }, + linked: { + 'package.json': JSON.stringify({ + name: 'linked', + version: '1.0.0', + }), + }, + 'my-project': { + node_modules: { + foo: { + 'package.json': JSON.stringify({ name: 'foo', version: '1.0.0' }), + }, + }, + 'package.json': JSON.stringify({ + name: 'my-project', + version: '1.0.0', + }), + }, + }) + npm.globalDir = resolve(testdir, 'global-prefix', 'lib', 'node_modules') + npm.prefix = resolve(testdir, 'my-project') + reifyOutput = () => {} + + const _cwd = process.cwd() + process.chdir(npm.prefix) + + await new Promise((res, rej) => { + link.exec(['linked'], (err) => { + if (err) + rej(err) + res() + }) + }) + + t.ok( + fs.statSync(resolve(testdir, 'my-project/node_modules/foo')), + 'should not prune any extraneous dep when running npm link' + ) + process.chdir(_cwd) +}) + t.test('completion', async t => { const testdir = t.testdir({ 'global-prefix': { From e5abf2a2171d95bafc0993f337230d2b6633a6ed Mon Sep 17 00:00:00 2001 From: Ruy Adorno Date: Mon, 7 Jun 2021 15:24:33 -0400 Subject: [PATCH 02/20] chore(libnpmdiff): added as workspace - Setup ./packages/* as workspaces - Moved source from: https://github.com/npm/libnpmdiff to ./packages/libnpmdiff - Added CI target for workspaces Relates to: https://github.com/npm/statusboard/issues/362 PR-URL: https://github.com/npm/cli/pull/3386 Credit: @ruyadorno Close: #3386 Reviewed-by: @wraithgar --- .github/workflows/ci.yml | 41 ++ node_modules/libnpmdiff | 1 + package-lock.json | 64 ++- package.json | 3 +- packages/libnpmdiff/.eslintrc.json | 207 ++++++++ packages/libnpmdiff/.gitignore | 99 ++++ packages/libnpmdiff/CHANGELOG.md | 30 ++ {node_modules => packages}/libnpmdiff/LICENSE | 0 packages/libnpmdiff/README.md | 98 ++++ .../libnpmdiff/index.js | 0 .../libnpmdiff/lib/format-diff.js | 0 .../libnpmdiff/lib/should-print-patch.js | 0 .../libnpmdiff/lib/tarball.js | 0 .../libnpmdiff/lib/untar.js | 0 .../libnpmdiff/package.json | 12 +- .../test/format-diff.js.test.cjs | 152 ++++++ .../tap-snapshots/test/index.js.test.cjs | 115 +++++ .../tap-snapshots/test/untar.js.test.cjs | 134 +++++ packages/libnpmdiff/test/fixtures/archive.tgz | Bin 0 -> 74564 bytes ...orno-simplistic-pkg-with-folders-1.0.0.tgz | Bin 0 -> 573 bytes .../test/fixtures/simple-output-2.2.1.tgz | Bin 0 -> 2227 bytes packages/libnpmdiff/test/format-diff.js | 483 ++++++++++++++++++ packages/libnpmdiff/test/index.js | 147 ++++++ .../libnpmdiff/test/should-print-patch.js | 28 + packages/libnpmdiff/test/tarball.js | 96 ++++ packages/libnpmdiff/test/untar.js | 231 +++++++++ scripts/bundle-and-gitignore-deps.js | 5 +- 27 files changed, 1912 insertions(+), 34 deletions(-) create mode 120000 node_modules/libnpmdiff create mode 100644 packages/libnpmdiff/.eslintrc.json create mode 100644 packages/libnpmdiff/.gitignore create mode 100644 packages/libnpmdiff/CHANGELOG.md rename {node_modules => packages}/libnpmdiff/LICENSE (100%) create mode 100644 packages/libnpmdiff/README.md rename {node_modules => packages}/libnpmdiff/index.js (100%) rename {node_modules => packages}/libnpmdiff/lib/format-diff.js (100%) rename {node_modules => packages}/libnpmdiff/lib/should-print-patch.js (100%) rename {node_modules => packages}/libnpmdiff/lib/tarball.js (100%) rename {node_modules => packages}/libnpmdiff/lib/untar.js (100%) rename {node_modules => packages}/libnpmdiff/package.json (87%) create mode 100644 packages/libnpmdiff/tap-snapshots/test/format-diff.js.test.cjs create mode 100644 packages/libnpmdiff/tap-snapshots/test/index.js.test.cjs create mode 100644 packages/libnpmdiff/tap-snapshots/test/untar.js.test.cjs create mode 100644 packages/libnpmdiff/test/fixtures/archive.tgz create mode 100644 packages/libnpmdiff/test/fixtures/ruyadorno-simplistic-pkg-with-folders-1.0.0.tgz create mode 100644 packages/libnpmdiff/test/fixtures/simple-output-2.2.1.tgz create mode 100644 packages/libnpmdiff/test/format-diff.js create mode 100644 packages/libnpmdiff/test/index.js create mode 100644 packages/libnpmdiff/test/should-print-patch.js create mode 100644 packages/libnpmdiff/test/tarball.js create mode 100644 packages/libnpmdiff/test/untar.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 78c2926afd6ed..3b622ed82fa78 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,6 +79,47 @@ jobs: env: DEPLOY_VERSION: testing + workspaces-tests: + strategy: + fail-fast: false + matrix: + node-version: [10.x, 12.x, 14.x, 16.x] + platform: + - os: ubuntu-latest + shell: bash + - os: macos-latest + shell: bash + - os: windows-latest + shell: bash + - os: windows-latest + shell: powershell + + runs-on: ${{ matrix.platform.os }} + defaults: + run: + shell: ${{ matrix.platform.shell }} + + steps: + # Checkout the npm/cli repo + - uses: actions/checkout@v2 + + # Installs the specific version of Node.js + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + + # Run the installer script + - name: Install dependencies + run: | + node . install --ignore-scripts --no-audit + node . rebuild + + - name: Run workspaces tests + run: node . test -w ./packages -- --no-check-coverage -t600 -Rbase -c + env: + DEPLOY_VERSION: testing + build: strategy: fail-fast: false diff --git a/node_modules/libnpmdiff b/node_modules/libnpmdiff new file mode 120000 index 0000000000000..ae8dd62893029 --- /dev/null +++ b/node_modules/libnpmdiff @@ -0,0 +1 @@ +../packages/libnpmdiff \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b393b7e3f18ef..3f3d1bf0f19ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -78,7 +78,8 @@ ], "license": "Artistic-2.0", "workspaces": [ - "docs" + "docs", + "packages/*" ], "dependencies": { "@npmcli/arborist": "^2.6.1", @@ -798,7 +799,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@npmcli/disparity-colors/-/disparity-colors-1.0.1.tgz", "integrity": "sha512-kQ1aCTTU45mPXN+pdAaRxlxr3OunkyztjbbxDY/aIcPS5CnCUrx+1+NvA6pTcYR7wmLZe37+Mi5v3nfbwPxq3A==", - "inBundle": true, "dependencies": { "ansi-styles": "^4.3.0" }, @@ -1380,7 +1380,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "inBundle": true, "engines": { "node": ">=8" } @@ -2236,7 +2235,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", - "inBundle": true, "engines": { "node": ">=0.3.1" } @@ -4632,23 +4630,8 @@ } }, "node_modules/libnpmdiff": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/libnpmdiff/-/libnpmdiff-2.0.4.tgz", - "integrity": "sha512-q3zWePOJLHwsLEUjZw3Kyu/MJMYfl4tWCg78Vl6QGSfm4aXBUSVzMzjJ6jGiyarsT4d+1NH4B1gxfs62/+y9iQ==", - "inBundle": true, - "dependencies": { - "@npmcli/disparity-colors": "^1.0.1", - "@npmcli/installed-package-contents": "^1.0.7", - "binary-extensions": "^2.2.0", - "diff": "^5.0.0", - "minimatch": "^3.0.4", - "npm-package-arg": "^8.1.1", - "pacote": "^11.3.0", - "tar": "^6.1.0" - }, - "engines": { - "node": ">=10" - } + "resolved": "packages/libnpmdiff", + "link": true }, "node_modules/libnpmexec": { "version": "1.2.0", @@ -10377,6 +10360,31 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } + }, + "packages/libnpmdiff": { + "version": "2.0.4", + "license": "ISC", + "dependencies": { + "@npmcli/disparity-colors": "^1.0.1", + "@npmcli/installed-package-contents": "^1.0.7", + "binary-extensions": "^2.2.0", + "diff": "^5.0.0", + "minimatch": "^3.0.4", + "npm-package-arg": "^8.1.4", + "pacote": "^11.3.4", + "tar": "^6.1.0" + }, + "devDependencies": { + "eslint": "^7.28.0", + "eslint-plugin-import": "^2.23.4", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^5.1.0", + "eslint-plugin-standard": "^5.0.0", + "tap": "^15.0.9" + }, + "engines": { + "node": ">=10" + } } }, "dependencies": { @@ -13706,17 +13714,21 @@ } }, "libnpmdiff": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/libnpmdiff/-/libnpmdiff-2.0.4.tgz", - "integrity": "sha512-q3zWePOJLHwsLEUjZw3Kyu/MJMYfl4tWCg78Vl6QGSfm4aXBUSVzMzjJ6jGiyarsT4d+1NH4B1gxfs62/+y9iQ==", + "version": "file:packages/libnpmdiff", "requires": { "@npmcli/disparity-colors": "^1.0.1", "@npmcli/installed-package-contents": "^1.0.7", "binary-extensions": "^2.2.0", "diff": "^5.0.0", + "eslint": "^7.28.0", + "eslint-plugin-import": "^2.23.4", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^5.1.0", + "eslint-plugin-standard": "^5.0.0", "minimatch": "^3.0.4", - "npm-package-arg": "^8.1.1", - "pacote": "^11.3.0", + "npm-package-arg": "^8.1.4", + "pacote": "^11.3.4", + "tap": "^15.0.9", "tar": "^6.1.0" } }, diff --git a/package.json b/package.json index 64568185861bd..a1f5159608cde 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "name": "npm", "description": "a package manager for JavaScript", "workspaces": [ - "docs" + "docs", + "packages/*" ], "files": [ "bin", diff --git a/packages/libnpmdiff/.eslintrc.json b/packages/libnpmdiff/.eslintrc.json new file mode 100644 index 0000000000000..6232a8f82187f --- /dev/null +++ b/packages/libnpmdiff/.eslintrc.json @@ -0,0 +1,207 @@ +{ + "parserOptions": { + "ecmaVersion": 2018, + "ecmaFeatures": {}, + "sourceType": "script" + }, + + "env": { + "es6": true, + "node": true + }, + + "plugins": [ + "import", + "node", + "promise", + "standard" + ], + + "globals": { + "document": "readonly", + "navigator": "readonly", + "window": "readonly" + }, + + "rules": { + "accessor-pairs": "error", + "array-bracket-spacing": ["error", "never"], + "arrow-spacing": ["error", { "before": true, "after": true }], + "block-spacing": ["error", "always"], + "brace-style": ["error", "1tbs", { "allowSingleLine": false }], + "camelcase": ["error", { "properties": "never" }], + "comma-dangle": ["error", { + "arrays": "always-multiline", + "objects": "always-multiline", + "imports": "always-multiline", + "exports": "always-multiline", + "functions": "never" + }], + "comma-spacing": ["error", { "before": false, "after": true }], + "comma-style": ["error", "last"], + "computed-property-spacing": ["error", "never"], + "constructor-super": "error", + "curly": ["error", "multi-or-nest"], + "dot-location": ["error", "property"], + "dot-notation": ["error", { "allowKeywords": true }], + "eol-last": "error", + "eqeqeq": ["error", "always", { "null": "ignore" }], + "func-call-spacing": ["error", "never"], + "generator-star-spacing": ["error", { "before": true, "after": true }], + "handle-callback-err": ["error", "^(err|error)$" ], + "indent": ["error", 2, { + "SwitchCase": 1, + "VariableDeclarator": 1, + "outerIIFEBody": 1, + "MemberExpression": 1, + "FunctionDeclaration": { "parameters": 1, "body": 1 }, + "FunctionExpression": { "parameters": 1, "body": 1 }, + "CallExpression": { "arguments": 1 }, + "ArrayExpression": 1, + "ObjectExpression": 1, + "ImportDeclaration": 1, + "flatTernaryExpressions": true, + "ignoreComments": false, + "ignoredNodes": ["TemplateLiteral *"] + }], + "key-spacing": ["error", { "beforeColon": false, "afterColon": true }], + "keyword-spacing": ["error", { "before": true, "after": true }], + "lines-between-class-members": ["error", "always", { "exceptAfterSingleLine": true }], + "new-cap": ["error", { "newIsCap": true, "capIsNew": false, "properties": true }], + "new-parens": "error", + "no-array-constructor": "error", + "no-async-promise-executor": "error", + "no-caller": "error", + "no-case-declarations": "error", + "no-class-assign": "error", + "no-compare-neg-zero": "error", + "no-cond-assign": "off", + "no-const-assign": "error", + "no-constant-condition": ["error", { "checkLoops": false }], + "no-control-regex": "error", + "no-debugger": "error", + "no-delete-var": "error", + "no-dupe-args": "error", + "no-dupe-class-members": "error", + "no-dupe-keys": "error", + "no-duplicate-case": "error", + "no-empty-character-class": "error", + "no-empty-pattern": "error", + "no-eval": "error", + "no-ex-assign": "error", + "no-extend-native": "error", + "no-extra-bind": "error", + "no-extra-boolean-cast": "error", + "no-extra-parens": ["error", "functions"], + "no-fallthrough": "error", + "no-floating-decimal": "error", + "no-func-assign": "error", + "no-global-assign": "error", + "no-implied-eval": "error", + "no-inner-declarations": ["error", "functions"], + "no-invalid-regexp": "error", + "no-irregular-whitespace": "error", + "no-iterator": "error", + "no-labels": ["error", { "allowLoop": true, "allowSwitch": false }], + "no-lone-blocks": "error", + "no-misleading-character-class": "error", + "no-prototype-builtins": "error", + "no-useless-catch": "error", + "no-mixed-operators": "off", + "no-mixed-spaces-and-tabs": "error", + "no-multi-spaces": "error", + "no-multi-str": "error", + "no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 0 }], + "no-negated-in-lhs": "error", + "no-new": "off", + "no-new-func": "error", + "no-new-object": "error", + "no-new-require": "error", + "no-new-symbol": "error", + "no-new-wrappers": "error", + "no-obj-calls": "error", + "no-octal": "error", + "no-octal-escape": "error", + "no-path-concat": "error", + "no-proto": "error", + "no-redeclare": ["error", { "builtinGlobals": false }], + "no-regex-spaces": "error", + "no-return-assign": "off", + "no-self-assign": "off", + "no-self-compare": "error", + "no-sequences": "error", + "no-shadow-restricted-names": "error", + "no-sparse-arrays": "error", + "no-tabs": "error", + "no-template-curly-in-string": "error", + "no-this-before-super": "error", + "no-throw-literal": "off", + "no-trailing-spaces": "error", + "no-undef": "error", + "no-undef-init": "error", + "no-unexpected-multiline": "error", + "no-unmodified-loop-condition": "error", + "no-unneeded-ternary": ["error", { "defaultAssignment": false }], + "no-unreachable": "error", + "no-unsafe-finally": 0, + "no-unsafe-negation": "error", + "no-unused-expressions": ["error", { "allowShortCircuit": true, "allowTernary": true, "allowTaggedTemplates": true }], + "no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": true }], + "no-use-before-define": ["error", { "functions": false, "classes": false, "variables": false }], + "no-useless-call": "error", + "no-useless-computed-key": "error", + "no-useless-constructor": "error", + "no-useless-escape": "error", + "no-useless-rename": "error", + "no-useless-return": "error", + "no-void": "error", + "no-whitespace-before-property": "error", + "no-with": "error", + "nonblock-statement-body-position": [2, "below"], + "object-curly-newline": "off", + "object-curly-spacing": "off", + "object-property-newline": ["error", { "allowMultiplePropertiesPerLine": true }], + "one-var": ["error", { "initialized": "never" }], + "operator-linebreak": "off", + "padded-blocks": ["error", { "blocks": "never", "switches": "never", "classes": "never" }], + "prefer-const": ["error", {"destructuring": "all"}], + "prefer-promise-reject-errors": "error", + "quote-props": ["error", "as-needed"], + "quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": true }], + "rest-spread-spacing": ["error", "never"], + "semi": ["error", "never"], + "semi-spacing": ["error", { "before": false, "after": true }], + "space-before-blocks": ["error", "always"], + "space-before-function-paren": ["error", "always"], + "space-in-parens": ["error", "never"], + "space-infix-ops": "error", + "space-unary-ops": ["error", { "words": true, "nonwords": false }], + "spaced-comment": ["error", "always", { + "line": { "markers": ["*package", "!", "/", ",", "="] }, + "block": { "balanced": true, "markers": ["*package", "!", ",", ":", "::", "flow-include"], "exceptions": ["*"] } + }], + "symbol-description": "error", + "template-curly-spacing": ["error", "never"], + "template-tag-spacing": ["error", "never"], + "unicode-bom": ["error", "never"], + "use-isnan": "error", + "valid-typeof": ["error", { "requireStringLiterals": true }], + "wrap-iife": ["error", "any", { "functionPrototypeMethods": true }], + "yield-star-spacing": ["error", "both"], + "yoda": ["error", "never"], + + "import/export": "error", + "import/first": "error", + "import/no-absolute-path": ["error", { "esmodule": true, "commonjs": true, "amd": false }], + "import/no-duplicates": "error", + "import/no-named-default": "error", + "import/no-webpack-loader-syntax": "error", + + "node/no-deprecated-api": "error", + "node/process-exit-as-throw": "error", + + "promise/param-names": "off", + + "standard/no-callback-literal": "error" + } +} diff --git a/packages/libnpmdiff/.gitignore b/packages/libnpmdiff/.gitignore new file mode 100644 index 0000000000000..0aba557bf2857 --- /dev/null +++ b/packages/libnpmdiff/.gitignore @@ -0,0 +1,99 @@ +# Logs +logs +*.log +npm-debug.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# gatsby files +.cache/ +public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# Editors +Session.vim diff --git a/packages/libnpmdiff/CHANGELOG.md b/packages/libnpmdiff/CHANGELOG.md new file mode 100644 index 0000000000000..b93b15b7b1113 --- /dev/null +++ b/packages/libnpmdiff/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog + +## 2.0.3 + +- fix name of options sent by the npm cli + +## 2.0.2 + +- fix matching basename file filter + +## 2.0.1 + +- fix for tarballs not listing folder names + +## 2.0.0 + +- API rewrite: + - normalized all options + - specs to compare are now an array +- fix context=0 +- added support to filtering by folder names + +## 1.0.1 + +- fixed nameOnly option + +## 1.0.0 + +- Initial release + diff --git a/node_modules/libnpmdiff/LICENSE b/packages/libnpmdiff/LICENSE similarity index 100% rename from node_modules/libnpmdiff/LICENSE rename to packages/libnpmdiff/LICENSE diff --git a/packages/libnpmdiff/README.md b/packages/libnpmdiff/README.md new file mode 100644 index 0000000000000..bc260ad15ce12 --- /dev/null +++ b/packages/libnpmdiff/README.md @@ -0,0 +1,98 @@ +# libnpmdiff + +[![npm version](https://img.shields.io/npm/v/libnpmdiff.svg)](https://npm.im/libnpmdiff) +[![license](https://img.shields.io/npm/l/libnpmdiff.svg)](https://npm.im/libnpmdiff) +[![GitHub Actions](https://github.com/npm/libnpmdiff/workflows/node-ci/badge.svg)](https://github.com/npm/libnpmdiff/actions?query=workflow%3Anode-ci) +[![Coverage Status](https://coveralls.io/repos/github/npm/libnpmdiff/badge.svg?branch=main)](https://coveralls.io/github/npm/libnpmdiff?branch=main) + +The registry diff lib. + +## Table of Contents + +* [Example](#example) +* [Install](#install) +* [Contributing](#contributing) +* [API](#api) +* [LICENSE](#license) + +## Example + +```js +const libdiff = require('libnpmdiff') + +const patch = await libdiff([ + 'abbrev@1.1.0', + 'abbrev@1.1.1' +]) +console.log( + patch +) +``` + +Returns: + +```patch +diff --git a/package.json b/package.json +index v1.1.0..v1.1.1 100644 +--- a/package.json ++++ b/package.json +@@ -1,6 +1,6 @@ + { + "name": "abbrev", +- "version": "1.1.0", ++ "version": "1.1.1", + "description": "Like ruby's abbrev module, but in js", + "author": "Isaac Z. Schlueter ", + "main": "abbrev.js", + +``` + +## Install + +`$ npm install libnpmdiff` + +### Contributing + +The npm team enthusiastically welcomes contributions and project participation! +There's a bunch of things you can do if you want to contribute! The +[Contributor Guide](https://github.com/npm/cli/blob/latest/CONTRIBUTING.md) +outlines the process for community interaction and contribution. Please don't +hesitate to jump in if you'd like to, or even ask us questions if something +isn't clear. + +All participants and maintainers in this project are expected to follow the +[npm Code of Conduct](https://www.npmjs.com/policies/conduct), and just +generally be excellent to each other. + +Please refer to the [Changelog](CHANGELOG.md) for project history details, too. + +Happy hacking! + +### API + +#### `> libnpmdif([ a, b ], [opts]) -> Promise` + +Fetches the registry tarballs and compare files between a spec `a` and spec `b`. **npm** spec types are usually described in `@` form but multiple other types are alsos supported, for more info on valid specs take a look at [`npm-package-arg`](https://github.com/npm/npm-package-arg). + +**Options**: + +- `color `: Should add ANSI colors to string output? Defaults to `false`. +- `tagVersionPrefix `: What prefix should be used to define version numbers. Defaults to `v` +- `diffUnified `: How many lines of code to print before/after each diff. Defaults to `3`. +- `diffFiles >`: If set only prints patches for the files listed in this array (also accepts globs). Defaults to `undefined`. +- `diffIgnoreAllSpace `: Whether or not should ignore changes in whitespace (very useful to avoid indentation changes extra diff lines). Defaults to `false`. +- `diffNameOnly `: Prints only file names and no patch diffs. Defaults to `false`. +- `diffNoPrefix `: If true then skips printing any prefixes in filenames. Defaults to `false`. +- `diffSrcPrefix `: Prefix to be used in the filenames from `a`. Defaults to `a/`. +- `diffDstPrefix `: Prefix to be used in the filenames from `b`. Defaults to `b/`. +- `diffText `: Should treat all files as text and try to print diff for binary files. Defaults to `false`. +- ...`cache`, `registry`, `where` and other common options accepted by [pacote](https://github.com/npm/pacote#options) + +Returns a `Promise` that fullfils with a `String` containing the resulting patch diffs. + +Throws an error if either `a` or `b` are missing or if trying to diff more than two specs. + +## LICENSE + +[ISC](./LICENSE) + diff --git a/node_modules/libnpmdiff/index.js b/packages/libnpmdiff/index.js similarity index 100% rename from node_modules/libnpmdiff/index.js rename to packages/libnpmdiff/index.js diff --git a/node_modules/libnpmdiff/lib/format-diff.js b/packages/libnpmdiff/lib/format-diff.js similarity index 100% rename from node_modules/libnpmdiff/lib/format-diff.js rename to packages/libnpmdiff/lib/format-diff.js diff --git a/node_modules/libnpmdiff/lib/should-print-patch.js b/packages/libnpmdiff/lib/should-print-patch.js similarity index 100% rename from node_modules/libnpmdiff/lib/should-print-patch.js rename to packages/libnpmdiff/lib/should-print-patch.js diff --git a/node_modules/libnpmdiff/lib/tarball.js b/packages/libnpmdiff/lib/tarball.js similarity index 100% rename from node_modules/libnpmdiff/lib/tarball.js rename to packages/libnpmdiff/lib/tarball.js diff --git a/node_modules/libnpmdiff/lib/untar.js b/packages/libnpmdiff/lib/untar.js similarity index 100% rename from node_modules/libnpmdiff/lib/untar.js rename to packages/libnpmdiff/lib/untar.js diff --git a/node_modules/libnpmdiff/package.json b/packages/libnpmdiff/package.json similarity index 87% rename from node_modules/libnpmdiff/package.json rename to packages/libnpmdiff/package.json index aa13954c63010..53fd5d4befd5e 100644 --- a/node_modules/libnpmdiff/package.json +++ b/packages/libnpmdiff/package.json @@ -46,12 +46,12 @@ ] }, "devDependencies": { - "eslint": "^7.18.0", - "eslint-plugin-import": "^2.22.1", + "eslint": "^7.28.0", + "eslint-plugin-import": "^2.23.4", "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^4.2.1", + "eslint-plugin-promise": "^5.1.0", "eslint-plugin-standard": "^5.0.0", - "tap": "^14.11.0" + "tap": "^15.0.9" }, "dependencies": { "@npmcli/disparity-colors": "^1.0.1", @@ -59,8 +59,8 @@ "binary-extensions": "^2.2.0", "diff": "^5.0.0", "minimatch": "^3.0.4", - "npm-package-arg": "^8.1.1", - "pacote": "^11.3.0", + "npm-package-arg": "^8.1.4", + "pacote": "^11.3.4", "tar": "^6.1.0" } } diff --git a/packages/libnpmdiff/tap-snapshots/test/format-diff.js.test.cjs b/packages/libnpmdiff/tap-snapshots/test/format-diff.js.test.cjs new file mode 100644 index 0000000000000..f735d8925820a --- /dev/null +++ b/packages/libnpmdiff/tap-snapshots/test/format-diff.js.test.cjs @@ -0,0 +1,152 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/format-diff.js TAP added file > should output expected added file diff result 1`] = ` +diff --git a/foo.js b/foo.js +new file mode 100755 +index v1.0.0..v2.0.0 +--- a/foo.js ++++ b/foo.js +@@ -0,0 +1,2 @@ ++"use strict" ++module.exports = "foo" +` + +exports[`test/format-diff.js TAP binary file > should output expected bin file diff result 1`] = ` +diff --git a/foo.jpg b/foo.jpg +index v1.0.0..v2.0.0 100644 +--- a/foo.jpg ++++ b/foo.jpg +` + +exports[`test/format-diff.js TAP changed file mode > should output expected changed file mode diff result 1`] = ` +diff --git a/foo.js b/foo.js +old mode 100644 +new mode 100755 +index v1.0.0..v2.0.0 +--- a/foo.js ++++ b/foo.js +` + +exports[`test/format-diff.js TAP colored output > should output expected colored diff result 1`] = ` +diff --git a/foo.js b/foo.js +index v1.0.0..v2.0.0 100644 +--- a/foo.js ++++ b/foo.js +@@ -1,2 +1,2 @@ + "use strict" +-module.exports = "foo" ++module.exports = "foobar" +` + +exports[`test/format-diff.js TAP diff options > should output expected diff result 1`] = ` +diff --git before/foo.js after/foo.js +index v1.0.0..v2.0.0 100644 +--- before/foo.js ++++ after/foo.js +@@ -4,4 +4,6 @@ + const c = "c" ++const d = "d" + module.exports = () => a+ + b+ +-c ++c+ ++d +` + +exports[`test/format-diff.js TAP diffUnified=0 > should output no context lines in output 1`] = ` +diff --git a/foo.js b/foo.js +index v1.0.0..v2.0.0 100644 +--- a/foo.js ++++ b/foo.js +@@ -3,2 +3,3 @@ +-const b = "b" +-const c = "c" ++ const b = "b" ++ const c = "c" ++ const d = "d" +@@ -7,1 +8,2 @@ +-c ++c+ ++d +` + +exports[`test/format-diff.js TAP format multiple files patch > should output expected result for multiple files 1`] = ` +diff --git a/foo.js b/foo.js +index v1.0.0..v1.1.1 100644 +--- a/foo.js ++++ b/foo.js +@@ -1,2 +1,2 @@ + "use strict" +-module.exports = "foo" ++module.exports = "foobar" +diff --git a/lib/utils.js b/lib/utils.js +index v1.0.0..v1.1.1 100644 +--- a/lib/utils.js ++++ b/lib/utils.js +@@ -1,3 +1,4 @@ + "use strict" + const bar = require("./bar.js") +-module.exports = () => bar ++module.exports = ++ () => bar + "util" +` + +exports[`test/format-diff.js TAP format removed file > should output expected removed file diff result 1`] = ` +diff --git a/foo.js b/foo.js +deleted file mode 100644 +index v1.0.0..v2.0.0 +--- a/foo.js ++++ b/foo.js +@@ -1,2 +0,0 @@ +-"use strict" +-module.exports = "foo" +/ No newline at end of file +` + +exports[`test/format-diff.js TAP format simple diff > should output expected diff result 1`] = ` +diff --git a/foo.js b/foo.js +index v1.0.0..v2.0.0 100644 +--- a/foo.js ++++ b/foo.js +@@ -1,2 +1,2 @@ + "use strict" +-module.exports = "foo" ++module.exports = "foobar" +` + +exports[`test/format-diff.js TAP noPrefix > should output result with no prefixes 1`] = ` +diff --git foo.js foo.js +index v1.0.0..v2.0.0 100644 +Index: foo.js +--- foo.js ++++ foo.js +@@ -1,2 +1,2 @@ + "use strict" +-module.exports = "foo" ++module.exports = "foobar" +` + +exports[`test/format-diff.js TAP nothing to diff > should output empty result 1`] = ` + +` + +exports[`test/format-diff.js TAP respect --tag-version-prefix option > should output expected diff result 1`] = ` +diff --git a/foo.js b/foo.js +index b1.0.0..b2.0.0 100644 +--- a/foo.js ++++ b/foo.js +@@ -1,2 +1,2 @@ + "use strict" +-module.exports = "foo" ++module.exports = "foobar" +` + +exports[`test/format-diff.js TAP using --name-only option > should output expected diff result 1`] = ` +foo.js +lib/utils.js +` diff --git a/packages/libnpmdiff/tap-snapshots/test/index.js.test.cjs b/packages/libnpmdiff/tap-snapshots/test/index.js.test.cjs new file mode 100644 index 0000000000000..21db3deac4d70 --- /dev/null +++ b/packages/libnpmdiff/tap-snapshots/test/index.js.test.cjs @@ -0,0 +1,115 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/index.js TAP compare two diff specs > should output expected diff 1`] = ` +diff --git a/index.js b/index.js +index v1.0.0..v2.0.0 100644 +--- a/index.js ++++ b/index.js +@@ -1,2 +1,2 @@ + module.exports = +- "a1" ++ "a2" +diff --git a/package.json b/package.json +index v1.0.0..v2.0.0 100644 +--- a/package.json ++++ b/package.json +@@ -1,4 +1,4 @@ + { + "name": "a", +- "version": "1.0.0" ++ "version": "2.0.0" + } +` + +exports[`test/index.js TAP folder in node_modules nested, absolute path > should output expected diff 1`] = ` +diff --git a/package.json b/package.json +index v2.0.0..v2.0.1 100644 +--- a/package.json ++++ b/package.json +@@ -1,6 +1,6 @@ + { + "name": "b", +- "version": "2.0.0", ++ "version": "2.0.1", + "scripts": { + "prepare": "node prepare.js" + } +diff --git a/prepare.js b/prepare.js +index v2.0.0..v2.0.1 100644 +--- a/prepare.js ++++ b/prepare.js +@@ -1,1 +0,0 @@ +-throw new Error("ERR") +/ No newline at end of file +` + +exports[`test/index.js TAP folder in node_modules nested, relative path > should output expected diff 1`] = ` +diff --git a/package.json b/package.json +index v2.0.0..v2.0.1 100644 +--- a/package.json ++++ b/package.json +@@ -1,6 +1,6 @@ + { + "name": "b", +- "version": "2.0.0", ++ "version": "2.0.1", + "scripts": { + "prepare": "node prepare.js" + } +diff --git a/prepare.js b/prepare.js +index v2.0.0..v2.0.1 100644 +--- a/prepare.js ++++ b/prepare.js +@@ -1,1 +0,0 @@ +-throw new Error("ERR") +/ No newline at end of file +` + +exports[`test/index.js TAP folder in node_modules top-level, absolute path > should output expected diff 1`] = ` +diff --git a/package.json b/package.json +index v1.0.0..v1.0.1 100644 +--- a/package.json ++++ b/package.json +@@ -1,6 +1,6 @@ + { + "name": "a", +- "version": "1.0.0", ++ "version": "1.0.1", + "scripts": { + "prepare": "node prepare.js" + } +diff --git a/prepare.js b/prepare.js +index v1.0.0..v1.0.1 100644 +--- a/prepare.js ++++ b/prepare.js +@@ -1,1 +0,0 @@ +-throw new Error("ERR") +/ No newline at end of file +` + +exports[`test/index.js TAP folder in node_modules top-level, relative path > should output expected diff 1`] = ` +diff --git a/package.json b/package.json +index v1.0.0..v1.0.1 100644 +--- a/package.json ++++ b/package.json +@@ -1,6 +1,6 @@ + { + "name": "a", +- "version": "1.0.0", ++ "version": "1.0.1", + "scripts": { + "prepare": "node prepare.js" + } +diff --git a/prepare.js b/prepare.js +index v1.0.0..v1.0.1 100644 +--- a/prepare.js ++++ b/prepare.js +@@ -1,1 +0,0 @@ +-throw new Error("ERR") +/ No newline at end of file +` diff --git a/packages/libnpmdiff/tap-snapshots/test/untar.js.test.cjs b/packages/libnpmdiff/tap-snapshots/test/untar.js.test.cjs new file mode 100644 index 0000000000000..b1092feb6ee8c --- /dev/null +++ b/packages/libnpmdiff/tap-snapshots/test/untar.js.test.cjs @@ -0,0 +1,134 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/untar.js TAP filter files > should return list of filenames 1`] = ` +LICENSE +README.md +` + +exports[`test/untar.js TAP filter files > should return map of filenames with valid contents 1`] = ` +a/LICENSE: true +a/README.md: true +` + +exports[`test/untar.js TAP filter files by exact filename > should return no filenames 1`] = ` + +` + +exports[`test/untar.js TAP filter files by exact filename > should return no filenames 2`] = ` + +` + +exports[`test/untar.js TAP filter files using glob expressions > should return list of filenames 1`] = ` +lib/index.js +lib/utils/b.js +package-lock.json +test/index.js +` + +exports[`test/untar.js TAP filter files using glob expressions > should return map of filenames with valid contents 1`] = ` +a/lib/index.js: true +a/lib/utils/b.js: true +a/package-lock.json: true +a/test/index.js: true +` + +exports[`test/untar.js TAP match files by end of filename > should return list of filenames 1`] = ` +lib/index.js +lib/utils/b.js +test/index.js +test/utils/b.js +` + +exports[`test/untar.js TAP match files by end of filename > should return map of filenames with valid contents 1`] = ` +a/lib/index.js: true +a/lib/utils/b.js: true +a/test/index.js: true +a/test/utils/b.js: true +` + +exports[`test/untar.js TAP match files by simple folder name > should return list of filenames 1`] = ` +lib/index.js +lib/utils/b.js +` + +exports[`test/untar.js TAP match files by simple folder name > should return map of filenames with valid contents 1`] = ` +a/lib/index.js: true +a/lib/utils/b.js: true +` + +exports[`test/untar.js TAP match files by simple folder name variation > should return list of filenames 1`] = ` +test/index.js +test/utils/b.js +` + +exports[`test/untar.js TAP match files by simple folder name variation > should return map of filenames with valid contents 1`] = ` +a/test/index.js: true +a/test/utils/b.js: true +` + +exports[`test/untar.js TAP untar package with folders > should have read contents 1`] = ` +module.exports = 'b' + +` + +exports[`test/untar.js TAP untar package with folders > should return list of filenames 1`] = ` +lib/index.js +lib/utils/b.js +package-lock.json +package.json +test/index.js +test/utils/b.js +` + +exports[`test/untar.js TAP untar package with folders > should return map of filenames to its contents 1`] = ` +a/lib/index.js: true +a/lib/utils/b.js: true +a/package-lock.json: true +a/package.json: true +a/test/index.js: true +a/test/utils/b.js: true +` + +exports[`test/untar.js TAP untar simple package > should have read contents 1`] = ` +The MIT License (MIT) + +Copyright (c) Ruy Adorno (ruyadorno.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +` + +exports[`test/untar.js TAP untar simple package > should return list of filenames 1`] = ` +LICENSE +index.js +package.json +README.md +` + +exports[`test/untar.js TAP untar simple package > should return map of filenames to its contents 1`] = ` +a/LICENSE: true +a/index.js: true +a/package.json: true +a/README.md: true +` diff --git a/packages/libnpmdiff/test/fixtures/archive.tgz b/packages/libnpmdiff/test/fixtures/archive.tgz new file mode 100644 index 0000000000000000000000000000000000000000..843a611239bbb9e8f3edc2477b7e49347297d850 GIT binary patch literal 74564 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%0D<47F@{kZsJ?ru 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 T+E@E(f06b-;D4KR0Qv*~$S*PX literal 0 HcmV?d00001 diff --git a/packages/libnpmdiff/test/fixtures/ruyadorno-simplistic-pkg-with-folders-1.0.0.tgz b/packages/libnpmdiff/test/fixtures/ruyadorno-simplistic-pkg-with-folders-1.0.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..11bbb44c4e9a42c832fee5d973207d50116aa826 GIT binary patch literal 573 zcmV-D0>b?tiwFP!00000|Ls=6j+-zL-7{Zdae-9CKo(eO)vmTuFFo{9^;Wf#0XMh- z+r=ikQI&tM4I~?yR2-VHisYv#CgT|xCwcD;N7)K3v0swZZzL&&pTeKw=Bx;UAc-Py zD&ArdgPYqp3BoW*0+pvO?xKZS&)ddnG(w_Uwer9B>5c;S>y@Blc~fGDpE;|g0Pld4 zI@W79WiT0d&wq)9yp#Zp@PDB>Kji-+j1B)MLB^<%Kq}(b_*X+}>^c&0XJ%Ov>i>l6 zh=Mg`4hpo!Aay*Q`v87h0Cc74VJU>ZdJVlq-3tH-RSGKI)YAgfM`-TOtnJLm%`1W* z_&=dJem=YVkMciU1V{cKC6VF(6zE8dwDBXyIz{Orm93+JK}!Mh)LZIVTf^lm!@&P9 zBmiUnpPZQggFD0jDKPH;vLJLRKtBdNv3hkp1PD^`k_bt%zovmhTNw-jFBK4ZT6_Mn zX$FSNV#oh}RX-D>=YGZr|KsrXi2rdE8~#rL&0(9O3fuRBWOtjuvOY;E@TT3r!Fby)}l7xMG_B&m$LZtn)6M`<p zz3=b$b=;qI&6Fg=RJ32hr;lHDXIX#1T!{{*8Nr8*BCZp^hY<|b+1nP)=wo1DaBcVl L*iDpP02lxO>6#Hd literal 0 HcmV?d00001 diff --git a/packages/libnpmdiff/test/fixtures/simple-output-2.2.1.tgz b/packages/libnpmdiff/test/fixtures/simple-output-2.2.1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..8d442f4c1c0780f8501206444db6eb7d855357be GIT binary patch literal 2227 zcmV;k2u$}MiwFP!00000|Lqy=ZrjN5e)B1&_Telw5+y6K4{+c+$Tl4l)rUdJmj-qY zrpT4F4#`z_mv)4!U#9;G^eOr%eUi@XlKOC*#A%uY0lct5?(TfgaAs#U3!^vTgf%+; zq1PLDKYWSNY&LhFJ%jt!akRFZP59yI&Teyidv~{qaLw&zYiAdqUqqTaBUd_W`JIg6ltI@YLWf5zQw%CNsXES~n9U`hOf^)ZDg^l)+Od$CfQroLp?P&9ks{8M0&s zCl+Wj!|{qmnpOyWERsarkZO_0Von;VJ;x!Jha+*#j4sy8nb0Vz6@eTuTY9zFQqwR= zV8jZ8Azz+3v{b4ii9LQyndV^vGa(JXyK1_~|HOkq|9JSa9eCgm;56v}<{x=Su+bji ze4_?0{ozUfYzPPuw0px>&_9ND?-l&T?;X{^`|D}o4F=E;9KU|# zob)?K9xfhuC|UcU<5^ay)M2OXcWZFe?zUfe2CI)$0*An?bnx=Tqb1_k#=pa%-|vwc zhyC6#z-bM&42BETm;S)3K|AmVWRT;a->o@hPQ>UNB8b=XEEO^}ta$`MXnr>E76jqQ zYj=t3lZ!Q&(KzuEfvo%;WC>#_cSm4TH(X;AZY zBoYPtSR4PAbIB@OiI~)j<*n*-$HHM5Pu{FTnFZQ+oJIrcb$pKV%#>Cv2T{b7s-$XC zg+CmiqM0YmWpAlCC|Bti%dLAOxn`Aum0KXe6W9X$djg0}lZ%YF+^&#kQVRJY;s|hu zxIuXUX`YS6r-@A%4!ju1sGi#4>xP^^BgyB@!y6a#|K8?)_?x> zkFDp6)il)6w2vp3Pz$L7HeqiOsB!-Hi;F*QE7kLh*T4Vxvgf`-R$a03sd(CA(9r||GX`onS5KCzfO4HeS z&F)}GukGO6>{#2;s|k8~p%|rS5rp(gkwj0DkDlaN=N7-_Z=IGN$5$F<{V$$)0ub5% zo5zRi|Mm{m`t|z1y|dGLtp8tQP=eXW!jx_7!G_}LEMaxbF0)*3P(s+a#x@i)IqkOG zmb-0NQ*7I5sXR5>Rnw2-^0`T%%T1iVHUKP}@XUbNM883Khv@vWzuhFTTLTs|6-hqR z1>e3Ar%YR>Bep59Ve_43jy*D);XYwZwVtWHMq`2UwS(e*qpJ&i_wayYJ-x=FYRn{_m@d zO<2njj&uH}^WJF}ioE~2Qs$RY7~}C^oXfe(MdM1{$t~)7Qe6UPv-C>2LQcwa#X5AA zxE$m>iD95aovS6Lmfx*Yd8)uGozX4+MBD=Feu$j z0bTIhU6_kpmS&Gb9l~gi{p1_k;<_-JBtN7gdIOj1Ry5b<9Q(CGC%OyA5HOygR!*@P z{U=$ND%1x(KE|HXUhMN|YQPN72}2sxFk;}~w(FozkydOulc;NQl4)cb+Mh9sPSp@w zixp}Pv2oROM^Ev9!dfMS{jGrFbnzpM=~iLUzE@Ds9D|%TEn%xfIOA0p2CP)k#1D*E zB5wA`=$p_b4%SNj;*&K({Vy>_FzLK&J+}hrx{Y*Rw;o-_qG3~X&R`YVTWGYri?Ab9 z_@$a4!gl^n$kP6;X@*PYuJ!M58tOB})W8VZ5fdLB;getSpp?uj%W&U>Gi41+7N@s_ zW7?AQ`^-@2pJS8q zmqx5Km{MO?%H#+C{wK7Wt!+489-9}L`Rqvh9>@1T{tK9ZW4Qn%007R& BVv_&> literal 0 HcmV?d00001 diff --git a/packages/libnpmdiff/test/format-diff.js b/packages/libnpmdiff/test/format-diff.js new file mode 100644 index 0000000000000..f2fc7c77d7f11 --- /dev/null +++ b/packages/libnpmdiff/test/format-diff.js @@ -0,0 +1,483 @@ +const t = require('tap') + +const formatDiff = require('../lib/format-diff.js') + +const normalizeWin = (str) => str + .replace(/\\+/g, '/') + .replace(/\r\n/g, '\n') + +t.cleanSnapshot = (str) => normalizeWin(str) + +t.test('format simple diff', t => { + const files = new Set([ + 'foo.js', + ]) + const refs = new Map(Object.entries({ + 'a/foo.js': { + content: '"use strict"\nmodule.exports = "foo"\n', + mode: '100644', + }, + 'b/foo.js': { + content: '"use strict"\nmodule.exports = "foobar"\n', + mode: '100644', + }, + })) + const versions = { + a: '1.0.0', + b: '2.0.0', + } + + t.matchSnapshot( + formatDiff({ + files, + refs, + versions, + }), + 'should output expected diff result' + ) + t.end() +}) + +t.test('nothing to diff', t => { + const files = new Set([ + 'foo.js', + ]) + const refs = new Map(Object.entries({ + 'a/foo.js': { + content: '"use strict"\nmodule.exports = "foo"\n', + mode: '100644', + }, + 'b/foo.js': { + content: '"use strict"\nmodule.exports = "foo"\n', + mode: '100644', + }, + })) + const versions = { + a: '1.0.0', + b: '1.0.0', + } + + t.matchSnapshot( + formatDiff({ + files, + refs, + versions, + }), + 'should output empty result' + ) + t.end() +}) + +t.test('format removed file', t => { + const files = new Set([ + 'foo.js', + ]) + const refs = new Map(Object.entries({ + 'a/foo.js': { + content: '"use strict"\nmodule.exports = "foo"\n', + mode: '100644', + }, + })) + const versions = { + a: '1.0.0', + b: '2.0.0', + } + + t.matchSnapshot( + formatDiff({ + files, + refs, + versions, + }), + 'should output expected removed file diff result' + ) + t.end() +}) + +t.test('changed file mode', t => { + const files = new Set([ + 'foo.js', + ]) + const refs = new Map(Object.entries({ + 'a/foo.js': { + content: '"use strict"\nmodule.exports = "foo"\n', + mode: '100644', + }, + 'b/foo.js': { + content: '"use strict"\nmodule.exports = "foo"\n', + mode: '100755', + }, + })) + const versions = { + a: '1.0.0', + b: '2.0.0', + } + + t.matchSnapshot( + formatDiff({ + files, + refs, + versions, + }), + 'should output expected changed file mode diff result' + ) + t.end() +}) + +t.test('added file', t => { + const files = new Set([ + 'foo.js', + ]) + const refs = new Map(Object.entries({ + 'b/foo.js': { + content: '"use strict"\nmodule.exports = "foo"\n', + mode: '100755', + }, + })) + const versions = { + a: '1.0.0', + b: '2.0.0', + } + + t.matchSnapshot( + formatDiff({ + files, + refs, + versions, + }), + 'should output expected added file diff result' + ) + t.end() +}) + +t.test('binary file', t => { + const files = new Set([ + 'foo.jpg', + ]) + const refs = new Map(Object.entries({ + 'a/foo.jpg': { + content: Buffer.from(''), + mode: '100644', + }, + 'b/foo.jpg': { + content: Buffer.from(''), + mode: '100644', + }, + })) + const versions = { + a: '1.0.0', + b: '2.0.0', + } + + t.matchSnapshot( + formatDiff({ + files, + refs, + versions, + }), + 'should output expected bin file diff result' + ) + t.end() +}) + +t.test('nothing to compare', t => { + const files = new Set([ + 'foo.jpg', + ]) + const refs = new Map(Object.entries({ + 'a/foo.jpg': {}, + 'b/foo.jpg': {}, + })) + const versions = { + a: '1.0.0', + b: '2.0.0', + } + + t.equal( + formatDiff({ + files, + refs, + versions, + }), + '', + 'should have no output' + ) + t.end() +}) + +t.test('colored output', t => { + const files = new Set([ + 'foo.js', + ]) + const refs = new Map(Object.entries({ + 'a/foo.js': { + content: '"use strict"\nmodule.exports = "foo"\n', + mode: '100644', + }, + 'b/foo.js': { + content: '"use strict"\nmodule.exports = "foobar"\n', + mode: '100644', + }, + })) + const versions = { + a: '1.0.0', + b: '2.0.0', + } + + t.matchSnapshot( + formatDiff({ + files, + refs, + versions, + opts: { + color: true, + }, + }), + 'should output expected colored diff result' + ) + t.end() +}) + +t.test('using --name-only option', t => { + const files = new Set([ + 'foo.js', + 'lib/bar.js', + 'lib/utils.js', + ]) + const refs = new Map(Object.entries({ + 'a/foo.js': { + content: '"use strict"\nmodule.exports = "foo"\n', + mode: '100644', + }, + 'b/foo.js': { + content: '"use strict"\nmodule.exports = "foobar"\n', + mode: '100644', + }, + 'a/lib/bar.js': { + content: '"use strict"\nmodule.exports = "bar"\n', + mode: '100644', + }, + 'b/lib/bar.js': { + content: '"use strict"\nmodule.exports = "bar"\n', + mode: '100644', + }, + 'a/lib/utils.js': { + content: '"use strict"\nconst bar = require("./bar.js")\n' + + 'module.exports = () => bar\n', + mode: '100644', + }, + 'b/lib/utils.js': { + content: '"use strict"\nconst bar = require("./bar.js")\n' + + 'module.exports =\n () => bar + "util"\n', + mode: '100644', + }, + })) + const versions = { + a: '1.0.0', + b: '2.0.0', + } + + t.matchSnapshot( + formatDiff({ + files, + refs, + versions, + opts: { + diffNameOnly: true, + }, + }), + 'should output expected diff result' + ) + t.end() +}) + +t.test('respect --tag-version-prefix option', t => { + const files = new Set([ + 'foo.js', + ]) + const refs = new Map(Object.entries({ + 'a/foo.js': { + content: '"use strict"\nmodule.exports = "foo"\n', + mode: '100644', + }, + 'b/foo.js': { + content: '"use strict"\nmodule.exports = "foobar"\n', + mode: '100644', + }, + })) + const versions = { + a: '1.0.0', + b: '2.0.0', + } + + t.matchSnapshot( + formatDiff({ + files, + refs, + versions, + opts: { + tagVersionPrefix: 'b', + }, + }), + 'should output expected diff result' + ) + t.end() +}) + +t.test('diff options', t => { + const files = new Set([ + 'foo.js', + ]) + const refs = new Map(Object.entries({ + 'a/foo.js': { + content: '"use strict"\nconst a = "a"\nconst b = "b"\n' + + 'const c = "c"\nmodule.exports = () => a+\nb+\nc\n', + mode: '100644', + }, + 'b/foo.js': { + content: '"use strict"\nconst a = "a"\n const b = "b"\n' + + ' const c = "c"\n const d = "d"\n' + + 'module.exports = () => a+\nb+\nc+\nd\n', + mode: '100644', + }, + })) + const versions = { + a: '1.0.0', + b: '2.0.0', + } + + t.matchSnapshot( + formatDiff({ + files, + refs, + versions, + opts: { + diffUnified: 1, + diffIgnoreAllSpace: true, + diffSrcPrefix: 'before/', + diffDstPrefix: 'after/', + }, + }), + 'should output expected diff result' + ) + t.end() +}) + +t.test('diffUnified=0', t => { + const files = new Set([ + 'foo.js', + ]) + const refs = new Map(Object.entries({ + 'a/foo.js': { + content: '"use strict"\nconst a = "a"\nconst b = "b"\n' + + 'const c = "c"\nmodule.exports = () => a+\nb+\nc\n', + mode: '100644', + }, + 'b/foo.js': { + content: '"use strict"\nconst a = "a"\n const b = "b"\n' + + ' const c = "c"\n const d = "d"\n' + + 'module.exports = () => a+\nb+\nc+\nd\n', + mode: '100644', + }, + })) + const versions = { + a: '1.0.0', + b: '2.0.0', + } + + t.matchSnapshot( + formatDiff({ + files, + refs, + versions, + opts: { + diffUnified: 0, + }, + }), + 'should output no context lines in output' + ) + t.end() +}) + +t.test('noPrefix', t => { + const files = new Set([ + 'foo.js', + ]) + const refs = new Map(Object.entries({ + 'a/foo.js': { + content: '"use strict"\nmodule.exports = "foo"\n', + mode: '100644', + }, + 'b/foo.js': { + content: '"use strict"\nmodule.exports = "foobar"\n', + mode: '100644', + }, + })) + const versions = { + a: '1.0.0', + b: '2.0.0', + } + + t.matchSnapshot( + formatDiff({ + files, + refs, + versions, + opts: { + diffNoPrefix: true, + }, + }), + 'should output result with no prefixes' + ) + t.end() +}) + +t.test('format multiple files patch', t => { + const files = new Set([ + 'foo.js', + 'lib/bar.js', + 'lib/utils.js', + ]) + const refs = new Map(Object.entries({ + 'a/foo.js': { + content: '"use strict"\nmodule.exports = "foo"\n', + mode: '100644', + }, + 'b/foo.js': { + content: '"use strict"\nmodule.exports = "foobar"\n', + mode: '100644', + }, + 'a/lib/bar.js': { + content: '"use strict"\nmodule.exports = "bar"\n', + mode: '100644', + }, + 'b/lib/bar.js': { + content: '"use strict"\nmodule.exports = "bar"\n', + mode: '100644', + }, + 'a/lib/utils.js': { + content: '"use strict"\nconst bar = require("./bar.js")\n' + + 'module.exports = () => bar\n', + mode: '100644', + }, + 'b/lib/utils.js': { + content: '"use strict"\nconst bar = require("./bar.js")\n' + + 'module.exports =\n () => bar + "util"\n', + mode: '100644', + }, + })) + const versions = { + a: '1.0.0', + b: '1.1.1', + } + + t.matchSnapshot( + formatDiff({ + files, + refs, + versions, + }), + 'should output expected result for multiple files' + ) + t.end() +}) diff --git a/packages/libnpmdiff/test/index.js b/packages/libnpmdiff/test/index.js new file mode 100644 index 0000000000000..88b474c111f15 --- /dev/null +++ b/packages/libnpmdiff/test/index.js @@ -0,0 +1,147 @@ +const { resolve } = require('path') + +const t = require('tap') + +const diff = require('../index.js') + +const normalizePath = p => p + .replace(/\\+/g, '/') + .replace(/\r\n/g, '\n') + +t.cleanSnapshot = (str) => normalizePath(str) + .replace(normalizePath(process.execPath), 'node') + +const json = (obj) => `${JSON.stringify(obj, null, 2)}\n` + +t.test('compare two diff specs', async t => { + const path = t.testdir({ + a1: { + 'package.json': json({ + name: 'a', + version: '1.0.0', + }), + 'index.js': 'module.exports =\n "a1"\n', + }, + a2: { + 'package.json': json({ + name: 'a', + version: '2.0.0', + }), + 'index.js': 'module.exports =\n "a2"\n', + }, + }) + + const a = `file:${resolve(path, 'a1')}` + const b = `file:${resolve(path, 'a2')}` + + t.resolveMatchSnapshot(diff([a, b], {}), 'should output expected diff') +}) + +t.test('using single arg', async t => { + await t.rejects( + diff(['abbrev@1.0.3']), + /libnpmdiff needs two arguments to compare/, + 'should throw EDIFFARGS error' + ) +}) + +t.test('too many args', async t => { + const args = ['abbrev@1.0.3', 'abbrev@1.0.4', 'abbrev@1.0.5'] + await t.rejects( + diff(args), + /libnpmdiff needs two arguments to compare/, + 'should output diff against cwd files' + ) +}) + +t.test('folder in node_modules', async t => { + const path = t.testdir({ + node_modules: { + a: { + 'package.json': json({ + name: 'a', + version: '1.0.0', + scripts: { + prepare: `${process.execPath} prepare.js`, + }, + }), + 'prepare.js': 'throw new Error("ERR")', + node_modules: { + b: { + 'package.json': json({ + name: 'b', + version: '2.0.0', + scripts: { + prepare: `${process.execPath} prepare.js`, + }, + }), + 'prepare.js': 'throw new Error("ERR")', + }, + }, + }, + }, + packages: { + a: { + 'package.json': json({ + name: 'a', + version: '1.0.1', + scripts: { + prepare: `${process.execPath} prepare.js`, + }, + }), + 'prepare.js': '', + }, + b: { + 'package.json': json({ + name: 'b', + version: '2.0.1', + scripts: { + prepare: `${process.execPath} prepare.js`, + }, + }), + 'prepare.js': '', + }, + }, + 'package.json': json({ + name: 'my-project', + version: '1.0.0', + }), + }) + + t.test('top-level, absolute path', async t => { + t.resolveMatchSnapshot(diff([ + `file:${resolve(path, 'node_modules/a')}`, + `file:${resolve(path, 'packages/a')}`, + ], { where: path }), 'should output expected diff') + }) + t.test('top-level, relative path', async t => { + const _cwd = process.cwd() + process.chdir(path) + t.teardown(() => { + process.chdir(_cwd) + }) + + t.resolveMatchSnapshot(diff([ + 'file:./node_modules/a', + 'file:./packages/a', + ], { where: path }), 'should output expected diff') + }) + t.test('nested, absolute path', async t => { + t.resolveMatchSnapshot(diff([ + `file:${resolve(path, 'node_modules/a/node_modules/b')}`, + `file:${resolve(path, 'packages/b')}`, + ], { where: path}), 'should output expected diff') + }) + t.test('nested, relative path', async t => { + const _cwd = process.cwd() + process.chdir(path) + t.teardown(() => { + process.chdir(_cwd) + }) + + t.resolveMatchSnapshot(diff([ + 'file:./node_modules/a/node_modules/b', + 'file:./packages/b', + ], { where: path }), 'should output expected diff') + }) +}) diff --git a/packages/libnpmdiff/test/should-print-patch.js b/packages/libnpmdiff/test/should-print-patch.js new file mode 100644 index 0000000000000..97b15787d3933 --- /dev/null +++ b/packages/libnpmdiff/test/should-print-patch.js @@ -0,0 +1,28 @@ +const t = require('tap') +const shouldPrintPatch = require('../lib/should-print-patch.js') + +t.test('valid filenames', t => { + t.ok(shouldPrintPatch('LICENSE')) + t.ok(shouldPrintPatch('.gitignore')) + t.ok(shouldPrintPatch('foo.md')) + t.ok(shouldPrintPatch('./bar.txt')) + t.ok(shouldPrintPatch('/a/b/c/bar.html')) + t.end() +}) + +t.test('invalid filenames', t => { + t.notOk(shouldPrintPatch('foo.exe')) + t.notOk(shouldPrintPatch('./foo.jpg')) + t.notOk(shouldPrintPatch('/a/b/c/bar.bin')) + t.end() +}) + +t.test('using --text/-a option', t => { + const opts = { + diffText: true, + } + t.ok(shouldPrintPatch('foo.exe', opts)) + t.ok(shouldPrintPatch('./foo.jpg', opts)) + t.ok(shouldPrintPatch('/a/b/c/bar.bin', opts)) + t.end() +}) diff --git a/packages/libnpmdiff/test/tarball.js b/packages/libnpmdiff/test/tarball.js new file mode 100644 index 0000000000000..3a959be6e53bc --- /dev/null +++ b/packages/libnpmdiff/test/tarball.js @@ -0,0 +1,96 @@ +const { resolve } = require('path') + +const t = require('tap') +const tar = require('tar') +const pacote = require('pacote') +pacote.tarball = () => { + throw new Error('Failed to detect node_modules tarball') +} + +const tarball = require('../lib/tarball.js') + +const json = (obj) => `${JSON.stringify(obj, null, 2)}\n` + +t.test('returns a tarball from node_modules', t => { + t.plan(2) + + const path = t.testdir({ + node_modules: { + a: { + 'package.json': json({ + name: 'a', + version: '1.0.0', + bin: { a: 'index.js' }, + }), + 'index.js': '', + }, + }, + }) + + const _cwd = process.cwd() + process.chdir(path) + t.teardown(() => { + process.chdir(_cwd) + }) + + tarball({ bin: { a: 'index.js' }, _resolved: resolve(path, 'node_modules/a') }, { where: path }) + .then(res => { + tar.list({ + filter: path => { + t.match( + path, + /package.json|index.js/, + 'should return tarball with expected files' + ) + }, + }) + .on('error', e => { + throw e + }) + .end(res) + }) +}) + +t.test('node_modules folder within a linked dir', async t => { + const path = t.testdir({ + node_modules: { + a: t.fixture('symlink', '../packages/a'), + }, + packages: { + a: { + node_modules: { + b: { + 'package.json': json({ + name: 'a', + version: '1.0.0', + }), + }, + }, + }, + }, + }) + + const link = await tarball({ _resolved: resolve(path, 'node_modules/a/node_modules/b') }, {}) + t.ok(link, 'should retrieve tarball from reading link') + + const target = await tarball({ _resolved: resolve(path, 'packages/a/node_modules/b') }, {}) + t.ok(target, 'should retrieve tarball from reading target') +}) + +t.test('pkg not in a node_modules folder', async t => { + const path = t.testdir({ + packages: { + a: { + 'package.json': json({ + name: 'a', + version: '1.0.0', + }), + }, + }, + }) + + t.throws( + () => tarball({ _resolved: resolve(path, 'packages/a') }, {}), + 'should call regular pacote.tarball method instead' + ) +}) diff --git a/packages/libnpmdiff/test/untar.js b/packages/libnpmdiff/test/untar.js new file mode 100644 index 0000000000000..62be1c6ba9003 --- /dev/null +++ b/packages/libnpmdiff/test/untar.js @@ -0,0 +1,231 @@ +const { resolve } = require('path') +const t = require('tap') +const pacote = require('pacote') +const untar = require('../lib/untar.js') + +t.test('untar simple package', async t => { + const item = + await pacote.tarball(resolve('./test/fixtures/simple-output-2.2.1.tgz')) + + const { + files, + refs, + } = await untar({ + item, + prefix: 'a/', + }) + + t.matchSnapshot([...files].join('\n'), 'should return list of filenames') + t.matchSnapshot( + [...refs.entries()].map(([k, v]) => `${k}: ${!!v}`).join('\n'), + 'should return map of filenames to its contents' + ) + t.matchSnapshot(refs.get('a/LICENSE').content, 'should have read contents') +}) + +t.test('untar package with folders', async t => { + const item = + await pacote.tarball(resolve('./test/fixtures/archive.tgz')) + + const { + files, + refs, + } = await untar({ + item, + prefix: 'a/', + }) + + t.matchSnapshot([...files].join('\n'), 'should return list of filenames') + t.matchSnapshot( + [...refs.entries()].map(([k, v]) => `${k}: ${!!v}`).join('\n'), + 'should return map of filenames to its contents' + ) + t.matchSnapshot( + refs.get('a/lib/utils/b.js').content, + 'should have read contents' + ) +}) + +t.test('filter files', async t => { + const item = + await pacote.tarball(resolve('./test/fixtures/simple-output-2.2.1.tgz')) + + const { + files, + refs, + } = await untar({ + item, + prefix: 'a/', + }, { + diffFiles: [ + './LICENSE', + 'missing-file', + 'README.md', + ], + }) + + t.matchSnapshot([...files].join('\n'), 'should return list of filenames') + t.matchSnapshot( + [...refs.entries()].map(([k, v]) => `${k}: ${!!v.content}`).join('\n'), + 'should return map of filenames with valid contents' + ) +}) + +t.test('filter files using glob expressions', async t => { + const item = + await pacote.tarball(resolve('./test/fixtures/archive.tgz')) + const cwd = t.testdir({ + lib: { + 'index.js': '', + utils: { + '/b.js': '', + }, + }, + 'package-lock.json': '', + 'package.json': '', + test: { + '/index.js': '', + utils: { + 'b.js': '', + }, + }, + }) + + const _cwd = process.cwd() + process.chdir(cwd) + t.teardown(() => { + process.chdir(_cwd) + }) + + const { + files, + refs, + } = await untar({ + item, + prefix: 'a/', + }, { + diffFiles: [ + './lib/**', + '*-lock.json', + 'test\\*', // windows-style sep should be normalized + ], + }) + + t.matchSnapshot([...files].join('\n'), 'should return list of filenames') + t.matchSnapshot( + [...refs.entries()].map(([k, v]) => `${k}: ${!!v.content}`).join('\n'), + 'should return map of filenames with valid contents' + ) +}) + +t.test('match files by end of filename', async t => { + const item = + await pacote.tarball(resolve('./test/fixtures/archive.tgz')) + + const { + files, + refs, + } = await untar({ + item, + prefix: 'a/', + }, { + diffFiles: [ + '*.js', + ], + }) + + t.matchSnapshot([...files].join('\n'), 'should return list of filenames') + t.matchSnapshot( + [...refs.entries()].map(([k, v]) => `${k}: ${!!v.content}`).join('\n'), + 'should return map of filenames with valid contents' + ) +}) + +t.test('filter files by exact filename', async t => { + const item = + await pacote.tarball(resolve('./test/fixtures/archive.tgz')) + + const { + files, + refs, + } = await untar({ + item, + prefix: 'a/', + }, { + diffFiles: [ + 'index.js', + ], + }) + + t.matchSnapshot([...files].join('\n'), 'should return no filenames') + t.matchSnapshot( + [...refs.entries()].map(([k, v]) => `${k}: ${!!v.content}`).join('\n'), + 'should return no filenames' + ) +}) + +t.test('match files by simple folder name', async t => { + const item = + await pacote.tarball(resolve('./test/fixtures/archive.tgz')) + + const { + files, + refs, + } = await untar({ + item, + prefix: 'a/', + }, { + diffFiles: [ + 'lib', + ], + }) + + t.matchSnapshot([...files].join('\n'), 'should return list of filenames') + t.matchSnapshot( + [...refs.entries()].map(([k, v]) => `${k}: ${!!v.content}`).join('\n'), + 'should return map of filenames with valid contents' + ) +}) + +t.test('match files by simple folder name variation', async t => { + const item = + await pacote.tarball(resolve('./test/fixtures/archive.tgz')) + + const { + files, + refs, + } = await untar({ + item, + prefix: 'a/', + }, { + diffFiles: [ + './test/', + ], + }) + + t.matchSnapshot([...files].join('\n'), 'should return list of filenames') + t.matchSnapshot( + [...refs.entries()].map(([k, v]) => `${k}: ${!!v.content}`).join('\n'), + 'should return map of filenames with valid contents' + ) +}) + +t.test('filter out all files', async t => { + const item = + await pacote.tarball(resolve('./test/fixtures/simple-output-2.2.1.tgz')) + + const { + files, + refs, + } = await untar({ + item, + prefix: 'a/', + }, { + diffFiles: [ + 'non-matching-pattern', + ], + }) + + t.equal(files.size, 0, 'should have no files') + t.equal(refs.size, 0, 'should have no refs') +}) diff --git a/scripts/bundle-and-gitignore-deps.js b/scripts/bundle-and-gitignore-deps.js index 84a3ab3ad9ef0..407b9e5982514 100644 --- a/scripts/bundle-and-gitignore-deps.js +++ b/scripts/bundle-and-gitignore-deps.js @@ -10,7 +10,10 @@ const shouldIgnore = [] arb.loadVirtual().then(tree => { for (const node of tree.children.values()) { - if (node.dev || node.isLink) { + const has = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key) + const nonProdWorkspace = + node.isWorkspace && !(has(tree.package.dependencies, node.name)) + if (node.dev || nonProdWorkspace) { console.error('ignore', node.name) shouldIgnore.push(node.name) } else if (tree.edgesOut.has(node.name)) { From c6a8734d7d6e4b6d061110a01e45e1d418d56489 Mon Sep 17 00:00:00 2001 From: Gar Date: Mon, 7 Jun 2021 15:55:25 -0700 Subject: [PATCH 03/20] chore(refactor): finish passing npm context No more requiring npm as a singleton. This will now allow us to move forward with the other refactoring we need to always use the npm object itself in tests, not a mocked one. PR-URL: https://github.com/npm/cli/pull/3388 Credit: @wraithgar Close: #3388 Reviewed-by: @ruyadorno --- lib/cli.js | 1 + lib/utils/cache-file.js | 66 --------- lib/utils/error-handler.js | 48 ++++-- lib/utils/error-message.js | 7 +- lib/utils/explain-eresolve.js | 19 +-- lib/utils/setup-log.js | 27 ++-- .../test/lib/utils/error-handler.js.test.cjs | 2 +- .../lib/utils/explain-eresolve.js.test.cjs | 139 ++---------------- test/lib/cli.js | 5 + test/lib/load-all.js | 1 + test/lib/utils/error-handler.js | 38 +++-- test/lib/utils/error-message.js | 57 +++---- test/lib/utils/explain-eresolve.js | 19 +-- test/lib/utils/setup-log.js | 2 +- 14 files changed, 137 insertions(+), 294 deletions(-) delete mode 100644 lib/utils/cache-file.js diff --git a/lib/cli.js b/lib/cli.js index d4a67645858ae..fbceb459db97c 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -20,6 +20,7 @@ module.exports = (process) => { const npm = require('../lib/npm.js') const errorHandler = require('../lib/utils/error-handler.js') + errorHandler.setNpm(npm) // if npm is called as "npmg" or "npm_g", then // run in global mode. diff --git a/lib/utils/cache-file.js b/lib/utils/cache-file.js deleted file mode 100644 index b33881e872ec2..0000000000000 --- a/lib/utils/cache-file.js +++ /dev/null @@ -1,66 +0,0 @@ -const npm = require('../npm.js') -const path = require('path') -const chownr = require('chownr') -const writeFileAtomic = require('write-file-atomic') -const mkdirp = require('mkdirp-infer-owner') -const fs = require('graceful-fs') - -let cache = null -let cacheUid = null -let cacheGid = null -let needChown = typeof process.getuid === 'function' - -const getCacheOwner = () => { - let st - try { - st = fs.lstatSync(cache) - } catch (er) { - if (er.code !== 'ENOENT') - throw er - - st = fs.lstatSync(path.dirname(cache)) - } - - cacheUid = st.uid - cacheGid = st.gid - - needChown = st.uid !== process.getuid() || - st.gid !== process.getgid() -} - -const writeOrAppend = (method, file, data) => { - if (!cache) - cache = npm.config.get('cache') - - // redundant if already absolute, but prevents non-absolute files - // from being written as if they're part of the cache. - file = path.resolve(cache, file) - - if (cacheUid === null && needChown) - getCacheOwner() - - const dir = path.dirname(file) - const firstMade = mkdirp.sync(dir) - - if (!needChown) - return method(file, data) - - let methodThrew = true - try { - method(file, data) - methodThrew = false - } finally { - // always try to leave it in the right ownership state, even on failure - // let the method error fail it instead of the chownr error, though - if (!methodThrew) - chownr.sync(firstMade || file, cacheUid, cacheGid) - else { - try { - chownr.sync(firstMade || file, cacheUid, cacheGid) - } catch (_) {} - } - } -} - -exports.append = (file, data) => writeOrAppend(fs.appendFileSync, file, data) -exports.write = (file, data) => writeOrAppend(writeFileAtomic.sync, file, data) diff --git a/lib/utils/error-handler.js b/lib/utils/error-handler.js index da716679d2705..f40e1f04fb180 100644 --- a/lib/utils/error-handler.js +++ b/lib/utils/error-handler.js @@ -1,28 +1,27 @@ +let npm // set by the cli let cbCalled = false const log = require('npmlog') -const npm = require('../npm.js') let itWorked = false const path = require('path') +const writeFileAtomic = require('write-file-atomic') +const mkdirp = require('mkdirp-infer-owner') +const fs = require('graceful-fs') let wroteLogFile = false let exitCode = 0 const errorMessage = require('./error-message.js') const replaceInfo = require('./replace-info.js') -const cacheFile = require('./cache-file.js') - let logFileName const getLogFile = () => { + // we call this multiple times, so we need to treat it as a singleton because + // the date is part of the name if (!logFileName) logFileName = path.resolve(npm.config.get('cache'), '_logs', (new Date()).toISOString().replace(/[.:]/g, '_') + '-debug.log') return logFileName } -const timings = { - version: npm.version, - command: process.argv.slice(2), - logfile: null, -} +const timings = {} process.on('timing', (name, value) => { if (timings[name]) timings[name] += value @@ -35,9 +34,21 @@ process.on('exit', code => { log.disableProgress() if (npm.config && npm.config.loaded && npm.config.get('timing')) { try { - timings.logfile = getLogFile() - cacheFile.append('_timing.json', JSON.stringify(timings) + '\n') - } catch (_) { + const file = path.resolve(npm.config.get('cache'), '_timing.json') + const dir = path.dirname(npm.config.get('cache')) + mkdirp.sync(dir) + + fs.appendFileSync(file, JSON.stringify({ + command: process.argv.slice(2), + logfile: getLogFile(), + version: npm.version, + ...timings, + }) + '\n') + + const st = fs.lstatSync(path.dirname(npm.config.get('cache'))) + fs.chownSync(dir, st.uid, st.gid) + fs.chownSync(file, st.uid, st.gid) + } catch (ex) { // ignore } } @@ -174,7 +185,7 @@ const errorHandler = (er) => { log.error(k, v) } - const msg = errorMessage(er) + const msg = errorMessage(er, npm) for (const errline of [...msg.summary, ...msg.detail]) log.error(...errline) @@ -214,7 +225,15 @@ const writeLogFile = () => { logOutput += line + os.EOL }) }) - cacheFile.write(getLogFile(), logOutput) + + const file = getLogFile() + const dir = path.dirname(file) + mkdirp.sync(dir) + writeFileAtomic.sync(file, logOutput) + + const st = fs.lstatSync(path.dirname(npm.config.get('cache'))) + fs.chownSync(dir, st.uid, st.gid) + fs.chownSync(file, st.uid, st.gid) // truncate once it's been written. log.record.length = 0 @@ -226,3 +245,6 @@ const writeLogFile = () => { module.exports = errorHandler module.exports.exit = exit +module.exports.setNpm = (n) => { + npm = n +} diff --git a/lib/utils/error-message.js b/lib/utils/error-message.js index ac5a935dc8770..125cdf8c53581 100644 --- a/lib/utils/error-message.js +++ b/lib/utils/error-message.js @@ -1,12 +1,11 @@ -const npm = require('../npm.js') const { format } = require('util') const { resolve } = require('path') const nameValidator = require('validate-npm-package-name') const npmlog = require('npmlog') const replaceInfo = require('./replace-info.js') -const { report: explainEresolve } = require('./explain-eresolve.js') +const { report } = require('./explain-eresolve.js') -module.exports = (er) => { +module.exports = (er, npm) => { const short = [] const detail = [] @@ -19,7 +18,7 @@ module.exports = (er) => { case 'ERESOLVE': short.push(['ERESOLVE', er.message]) detail.push(['', '']) - detail.push(['', explainEresolve(er)]) + detail.push(['', report(er, npm.color, resolve(npm.cache, 'eresolve-report.txt'))]) break case 'ENOLOCK': { diff --git a/lib/utils/explain-eresolve.js b/lib/utils/explain-eresolve.js index cda77aff94113..b35a32c6f935d 100644 --- a/lib/utils/explain-eresolve.js +++ b/lib/utils/explain-eresolve.js @@ -1,20 +1,14 @@ // this is called when an ERESOLVE error is caught in the error-handler, // or when there's a log.warn('eresolve', msg, explanation), to turn it // into a human-intelligible explanation of what's wrong and how to fix. -// -// TODO: abstract out the explainNode methods into a separate util for -// use by a future `npm explain ` command. - -const npm = require('../npm.js') const { writeFileSync } = require('fs') -const { resolve } = require('path') const { explainEdge, explainNode, printNode } = require('./explain-dep.js') // expl is an explanation object that comes from Arborist. It looks like: // Depth is how far we want to want to descend into the object making a report. // The full report (ie, depth=Infinity) is always written to the cache folder // at ${cache}/eresolve-report.txt along with full json. -const explainEresolve = (expl, color, depth) => { +const explain = (expl, color, depth) => { const { edge, current, peerConflict, currentEdge } = expl const out = [] @@ -42,9 +36,7 @@ const explainEresolve = (expl, color, depth) => { } // generate a full verbose report and tell the user how to fix it -const report = (expl, depth = 4) => { - const fullReport = resolve(npm.cache, 'eresolve-report.txt') - +const report = (expl, color, fullReport) => { const orNoStrict = expl.strictPeerDeps ? '--no-strict-peer-deps, ' : '' const fix = `Fix the upstream dependency conflict, or retry this command with ${orNoStrict}--force, or --legacy-peer-deps @@ -54,7 +46,7 @@ to accept an incorrect (and potentially broken) dependency resolution.` ${new Date().toISOString()} -${explainEresolve(expl, false, Infinity)} +${explain(expl, false, Infinity)} ${fix} @@ -63,13 +55,10 @@ Raw JSON explanation object: ${JSON.stringify(expl, null, 2)} `, 'utf8') - return explainEresolve(expl, npm.color, depth) + + return explain(expl, color, 4) + `\n\n${fix}\n\nSee ${fullReport} for a full report.` } -// the terser explain method for the warning when using --force -const explain = (expl, depth = 2) => explainEresolve(expl, npm.color, depth) - module.exports = { explain, report, diff --git a/lib/utils/setup-log.js b/lib/utils/setup-log.js index 44e612d50dc9f..9ee79d192d9ea 100644 --- a/lib/utils/setup-log.js +++ b/lib/utils/setup-log.js @@ -14,10 +14,22 @@ module.exports = (config) => { const { warn } = log + const stdoutTTY = process.stdout.isTTY + const stderrTTY = process.stderr.isTTY + const dumbTerm = process.env.TERM === 'dumb' + const stderrNotDumb = stderrTTY && !dumbTerm + const enableColorStderr = color === 'always' ? true + : color === false ? false + : stderrTTY + + const enableColorStdout = color === 'always' ? true + : color === false ? false + : stdoutTTY + log.warn = (heading, ...args) => { if (heading === 'ERESOLVE' && args[1] && typeof args[1] === 'object') { warn(heading, args[0]) - return warn('', explain(args[1])) + return warn('', explain(args[1], enableColorStdout, 2)) } return warn(heading, ...args) } @@ -29,19 +41,6 @@ module.exports = (config) => { log.heading = config.get('heading') || 'npm' - const stdoutTTY = process.stdout.isTTY - const stderrTTY = process.stderr.isTTY - const dumbTerm = process.env.TERM === 'dumb' - const stderrNotDumb = stderrTTY && !dumbTerm - - const enableColorStderr = color === 'always' ? true - : color === false ? false - : stderrTTY - - const enableColorStdout = color === 'always' ? true - : color === false ? false - : stdoutTTY - if (enableColorStderr) log.enableColor() else diff --git a/tap-snapshots/test/lib/utils/error-handler.js.test.cjs b/tap-snapshots/test/lib/utils/error-handler.js.test.cjs index 909051cdab506..78a9eef217f35 100644 --- a/tap-snapshots/test/lib/utils/error-handler.js.test.cjs +++ b/tap-snapshots/test/lib/utils/error-handler.js.test.cjs @@ -8,7 +8,7 @@ exports[`test/lib/utils/error-handler.js TAP handles unknown error > should have expected log contents for unknown error 1`] = ` 0 verbose code 1 1 error foo A complete log of this run can be found in: -1 error foo {CWD}/cachefolder/_logs/expecteddate-debug.log +1 error foo {CWD}/test/lib/utils/tap-testdir-error-handler/_logs/expecteddate-debug.log 2 verbose stack Error: ERROR 3 verbose cwd {CWD} 4 verbose Foo 1.0.0 diff --git a/tap-snapshots/test/lib/utils/explain-eresolve.js.test.cjs b/tap-snapshots/test/lib/utils/explain-eresolve.js.test.cjs index e066680153021..4c84aaca4ceeb 100644 --- a/tap-snapshots/test/lib/utils/explain-eresolve.js.test.cjs +++ b/tap-snapshots/test/lib/utils/explain-eresolve.js.test.cjs @@ -5,7 +5,7 @@ * Make sure to inspect the output below. Do not ignore changes! */ 'use strict' -exports[`test/lib/utils/explain-eresolve.js TAP chain-conflict > explain with color 1`] = ` +exports[`test/lib/utils/explain-eresolve.js TAP chain-conflict > explain with color, depth of 2 1`] = ` While resolving: project@1.2.3 Found: @isaacs/testing-peer-dep-conflict-chain-d@2.0.0 node_modules/@isaacs/testing-peer-dep-conflict-chain-d @@ -75,25 +75,7 @@ to accept an incorrect (and potentially broken) dependency resolution. See \${REPORT} for a full report. ` -exports[`test/lib/utils/explain-eresolve.js TAP chain-conflict > report with color, depth only 2 1`] = ` -While resolving: project@1.2.3 -Found: @isaacs/testing-peer-dep-conflict-chain-d@2.0.0 -node_modules/@isaacs/testing-peer-dep-conflict-chain-d - @isaacs/testing-peer-dep-conflict-chain-d@"2" from the root project - -Could not resolve dependency: -peer @isaacs/testing-peer-dep-conflict-chain-d@"1" from @isaacs/testing-peer-dep-conflict-chain-c@1.0.0 -node_modules/@isaacs/testing-peer-dep-conflict-chain-c - @isaacs/testing-peer-dep-conflict-chain-c@"1" from the root project - -Fix the upstream dependency conflict, or retry -this command with --force, or --legacy-peer-deps -to accept an incorrect (and potentially broken) dependency resolution. - -See \${REPORT} for a full report. -` - -exports[`test/lib/utils/explain-eresolve.js TAP chain-conflict > report with no color, depth of 6 1`] = ` +exports[`test/lib/utils/explain-eresolve.js TAP chain-conflict > report with no color 1`] = ` While resolving: project@1.2.3 Found: @isaacs/testing-peer-dep-conflict-chain-d@2.0.0 node_modules/@isaacs/testing-peer-dep-conflict-chain-d @@ -111,7 +93,7 @@ to accept an incorrect (and potentially broken) dependency resolution. See \${REPORT} for a full report. ` -exports[`test/lib/utils/explain-eresolve.js TAP cycleNested > explain with color 1`] = ` +exports[`test/lib/utils/explain-eresolve.js TAP cycleNested > explain with color, depth of 2 1`] = ` Found: @isaacs/peer-dep-cycle-c@2.0.0 node_modules/@isaacs/peer-dep-cycle-c @isaacs/peer-dep-cycle-c@"2.x" from the root project @@ -208,31 +190,7 @@ to accept an incorrect (and potentially broken) dependency resolution. See \${REPORT} for a full report. ` -exports[`test/lib/utils/explain-eresolve.js TAP cycleNested > report with color, depth only 2 1`] = ` -Found: @isaacs/peer-dep-cycle-c@2.0.0 -node_modules/@isaacs/peer-dep-cycle-c - @isaacs/peer-dep-cycle-c@"2.x" from the root project - -Could not resolve dependency: -peer @isaacs/peer-dep-cycle-b@"1" from @isaacs/peer-dep-cycle-a@1.0.0 -node_modules/@isaacs/peer-dep-cycle-a - @isaacs/peer-dep-cycle-a@"1.x" from the root project - -Conflicting peer dependency: @isaacs/peer-dep-cycle-c@1.0.0 -node_modules/@isaacs/peer-dep-cycle-c - peer @isaacs/peer-dep-cycle-c@"1" from @isaacs/peer-dep-cycle-b@1.0.0 - node_modules/@isaacs/peer-dep-cycle-b - peer @isaacs/peer-dep-cycle-b@"1" from @isaacs/peer-dep-cycle-a@1.0.0 - node_modules/@isaacs/peer-dep-cycle-a - -Fix the upstream dependency conflict, or retry -this command with --no-strict-peer-deps, --force, or --legacy-peer-deps -to accept an incorrect (and potentially broken) dependency resolution. - -See \${REPORT} for a full report. -` - -exports[`test/lib/utils/explain-eresolve.js TAP cycleNested > report with no color, depth of 6 1`] = ` +exports[`test/lib/utils/explain-eresolve.js TAP cycleNested > report with no color 1`] = ` Found: @isaacs/peer-dep-cycle-c@2.0.0 node_modules/@isaacs/peer-dep-cycle-c @isaacs/peer-dep-cycle-c@"2.x" from the root project @@ -257,7 +215,7 @@ to accept an incorrect (and potentially broken) dependency resolution. See \${REPORT} for a full report. ` -exports[`test/lib/utils/explain-eresolve.js TAP gatsby > explain with color 1`] = ` +exports[`test/lib/utils/explain-eresolve.js TAP gatsby > explain with color, depth of 2 1`] = ` While resolving: gatsby-recipes@0.2.31 Found: ink@3.0.0-7 node_modules/ink @@ -366,29 +324,7 @@ to accept an incorrect (and potentially broken) dependency resolution. See \${REPORT} for a full report. ` -exports[`test/lib/utils/explain-eresolve.js TAP gatsby > report with color, depth only 2 1`] = ` -While resolving: gatsby-recipes@0.2.31 -Found: ink@3.0.0-7 -node_modules/ink - dev ink@"next" from gatsby-recipes@0.2.31 - node_modules/gatsby-recipes - gatsby-recipes@"^0.2.31" from gatsby-cli@2.12.107 - node_modules/gatsby-cli - -Could not resolve dependency: -peer ink@">=2.0.0" from ink-box@1.0.0 -node_modules/ink-box - ink-box@"^1.0.0" from gatsby-recipes@0.2.31 - node_modules/gatsby-recipes - -Fix the upstream dependency conflict, or retry -this command with --no-strict-peer-deps, --force, or --legacy-peer-deps -to accept an incorrect (and potentially broken) dependency resolution. - -See \${REPORT} for a full report. -` - -exports[`test/lib/utils/explain-eresolve.js TAP gatsby > report with no color, depth of 6 1`] = ` +exports[`test/lib/utils/explain-eresolve.js TAP gatsby > report with no color 1`] = ` While resolving: gatsby-recipes@0.2.31 Found: ink@3.0.0-7 node_modules/ink @@ -409,7 +345,6 @@ node_modules/ink-box node_modules/gatsby-cli gatsby-cli@"^2.12.107" from gatsby@2.24.74 node_modules/gatsby - gatsby@"" from the root project Fix the upstream dependency conflict, or retry this command with --no-strict-peer-deps, --force, or --legacy-peer-deps @@ -418,7 +353,7 @@ to accept an incorrect (and potentially broken) dependency resolution. See \${REPORT} for a full report. ` -exports[`test/lib/utils/explain-eresolve.js TAP no current node, but has current edge > explain with color 1`] = ` +exports[`test/lib/utils/explain-eresolve.js TAP no current node, but has current edge > explain with color, depth of 2 1`] = ` While resolving: eslint@7.22.0 Found: dev eslint@"file:." from the root project @@ -480,23 +415,7 @@ to accept an incorrect (and potentially broken) dependency resolution. See \${REPORT} for a full report. ` -exports[`test/lib/utils/explain-eresolve.js TAP no current node, but has current edge > report with color, depth only 2 1`] = ` -While resolving: eslint@7.22.0 -Found: dev eslint@"file:." from the root project - -Could not resolve dependency: -peer eslint@"^6.0.0" from eslint-plugin-jsdoc@22.2.0 -node_modules/eslint-plugin-jsdoc - dev eslint-plugin-jsdoc@"^22.1.0" from the root project - -Fix the upstream dependency conflict, or retry -this command with --force, or --legacy-peer-deps -to accept an incorrect (and potentially broken) dependency resolution. - -See \${REPORT} for a full report. -` - -exports[`test/lib/utils/explain-eresolve.js TAP no current node, but has current edge > report with no color, depth of 6 1`] = ` +exports[`test/lib/utils/explain-eresolve.js TAP no current node, but has current edge > report with no color 1`] = ` While resolving: eslint@7.22.0 Found: dev eslint@"file:." from the root project @@ -512,7 +431,7 @@ to accept an incorrect (and potentially broken) dependency resolution. See \${REPORT} for a full report. ` -exports[`test/lib/utils/explain-eresolve.js TAP no current node, no current edge, idk > explain with color 1`] = ` +exports[`test/lib/utils/explain-eresolve.js TAP no current node, no current edge, idk > explain with color, depth of 2 1`] = ` While resolving: eslint@7.22.0 Could not resolve dependency: @@ -570,22 +489,7 @@ to accept an incorrect (and potentially broken) dependency resolution. See \${REPORT} for a full report. ` -exports[`test/lib/utils/explain-eresolve.js TAP no current node, no current edge, idk > report with color, depth only 2 1`] = ` -While resolving: eslint@7.22.0 - -Could not resolve dependency: -peer eslint@"^6.0.0" from eslint-plugin-jsdoc@22.2.0 -node_modules/eslint-plugin-jsdoc - dev eslint-plugin-jsdoc@"^22.1.0" from the root project - -Fix the upstream dependency conflict, or retry -this command with --force, or --legacy-peer-deps -to accept an incorrect (and potentially broken) dependency resolution. - -See \${REPORT} for a full report. -` - -exports[`test/lib/utils/explain-eresolve.js TAP no current node, no current edge, idk > report with no color, depth of 6 1`] = ` +exports[`test/lib/utils/explain-eresolve.js TAP no current node, no current edge, idk > report with no color 1`] = ` While resolving: eslint@7.22.0 Could not resolve dependency: @@ -600,7 +504,7 @@ to accept an incorrect (and potentially broken) dependency resolution. See \${REPORT} for a full report. ` -exports[`test/lib/utils/explain-eresolve.js TAP withShrinkwrap > explain with color 1`] = ` +exports[`test/lib/utils/explain-eresolve.js TAP withShrinkwrap > explain with color, depth of 2 1`] = ` While resolving: @isaacs/peer-dep-cycle-b@1.0.0 Found: @isaacs/peer-dep-cycle-c@2.0.0 node_modules/@isaacs/peer-dep-cycle-c @@ -677,26 +581,7 @@ to accept an incorrect (and potentially broken) dependency resolution. See \${REPORT} for a full report. ` -exports[`test/lib/utils/explain-eresolve.js TAP withShrinkwrap > report with color, depth only 2 1`] = ` -While resolving: @isaacs/peer-dep-cycle-b@1.0.0 -Found: @isaacs/peer-dep-cycle-c@2.0.0 -node_modules/@isaacs/peer-dep-cycle-c - @isaacs/peer-dep-cycle-c@"2.x" from the root project - -Could not resolve dependency: -peer @isaacs/peer-dep-cycle-c@"1" from @isaacs/peer-dep-cycle-b@1.0.0 -node_modules/@isaacs/peer-dep-cycle-b - peer @isaacs/peer-dep-cycle-b@"1" from @isaacs/peer-dep-cycle-a@1.0.0 - node_modules/@isaacs/peer-dep-cycle-a - -Fix the upstream dependency conflict, or retry -this command with --no-strict-peer-deps, --force, or --legacy-peer-deps -to accept an incorrect (and potentially broken) dependency resolution. - -See \${REPORT} for a full report. -` - -exports[`test/lib/utils/explain-eresolve.js TAP withShrinkwrap > report with no color, depth of 6 1`] = ` +exports[`test/lib/utils/explain-eresolve.js TAP withShrinkwrap > report with no color 1`] = ` While resolving: @isaacs/peer-dep-cycle-b@1.0.0 Found: @isaacs/peer-dep-cycle-c@2.0.0 node_modules/@isaacs/peer-dep-cycle-c diff --git a/test/lib/cli.js b/test/lib/cli.js index 42e05cc5d14c3..d0a9e0bd49200 100644 --- a/test/lib/cli.js +++ b/test/lib/cli.js @@ -25,6 +25,7 @@ const unsupportedMock = { } let errorHandlerCalled = null +let errorHandlerNpm = null let errorHandlerCb const errorHandlerMock = (...args) => { errorHandlerCalled = args @@ -35,6 +36,9 @@ let errorHandlerExitCalled = null errorHandlerMock.exit = code => { errorHandlerExitCalled = code } +errorHandlerMock.setNpm = npm => { + errorHandlerNpm = npm +} const logs = [] const npmlogMock = { @@ -181,6 +185,7 @@ t.test('gracefully handles error printing usage', t => { npmock.argv = [] errorHandlerCb = () => { t.match(errorHandlerCalled, [], 'should call errorHandler with no args') + t.match(errorHandlerNpm, npmock, 'errorHandler npm is set') t.end() } cli(proc) diff --git a/test/lib/load-all.js b/test/lib/load-all.js index 02736c18ccc38..4a975d49a490e 100644 --- a/test/lib/load-all.js +++ b/test/lib/load-all.js @@ -24,6 +24,7 @@ else { t.test('call the error handle so we dont freak out', t => { const errorHandler = require('../../lib/utils/error-handler.js') + errorHandler.setNpm(npm) errorHandler() t.end() }) diff --git a/test/lib/utils/error-handler.js b/test/lib/utils/error-handler.js index a00bac76e11c2..9a681e52ce5db 100644 --- a/test/lib/utils/error-handler.js +++ b/test/lib/utils/error-handler.js @@ -1,6 +1,7 @@ /* eslint-disable no-extend-native */ /* eslint-disable no-global-assign */ const EventEmitter = require('events') +const writeFileAtomic = require('write-file-atomic') const t = require('tap') // NOTE: Although these unit tests may look like the rest on the surface, @@ -23,13 +24,10 @@ const redactCwd = (path) => { t.cleanSnapshot = (str) => redactCwd(str) // internal modules mocks -const cacheFile = { - append: () => null, - write: () => null, -} +const cacheFolder = t.testdir({}) const config = { values: { - cache: 'cachefolder', + cache: cacheFolder, timing: true, }, loaded: true, @@ -111,20 +109,20 @@ process = Object.assign( // in order for tap to exit properly t.teardown(() => { process = _process + npmlog.record.length = 0 }) const mocks = { npmlog, - '../../../lib/npm.js': npm, '../../../lib/utils/error-message.js': (err) => ({ ...err, summary: [['ERR', err.message]], detail: [['ERR', err.message]], }), - '../../../lib/utils/cache-file.js': cacheFile, } let errorHandler = t.mock('../../../lib/utils/error-handler.js', mocks) +errorHandler.setNpm(npm) t.test('default exit code', (t) => { t.plan(1) @@ -160,10 +158,14 @@ t.test('default exit code', (t) => { t.test('handles unknown error', (t) => { t.plan(2) - cacheFile.write = (filename, content) => { + const _toISOString = Date.prototype.toISOString + Date.prototype.toISOString = () => 'expecteddate' + + const sync = writeFileAtomic.sync + writeFileAtomic.sync = (filename, content) => { t.equal( redactCwd(filename), - '{CWD}/cachefolder/_logs/expecteddate-debug.log', + '{CWD}/test/lib/utils/tap-testdir-error-handler/_logs/expecteddate-debug.log', 'should use expected log filename' ) t.matchSnapshot( @@ -175,7 +177,8 @@ t.test('handles unknown error', (t) => { errorHandler(err) t.teardown(() => { - cacheFile.write = () => null + writeFileAtomic.sync = sync + Date.prototype.toISOString = _toISOString }) t.end() }) @@ -205,11 +208,14 @@ t.test('npm.config not ready', (t) => { t.test('fail to write logfile', (t) => { t.plan(1) - cacheFile.write = () => { - throw err - } + const badDir = t.testdir({ + _logs: 'is a file', + }) + + config.values.cache = badDir + t.teardown(() => { - cacheFile.write = () => null + config.values.cache = cacheFolder }) t.doesNotThrow( @@ -372,6 +378,7 @@ t.test('uses code from errno', (t) => { t.plan(1) errorHandler = t.mock('../../../lib/utils/error-handler.js', mocks) + errorHandler.setNpm(npm) npmlog.level = 'silent' const _exit = process.exit @@ -396,6 +403,7 @@ t.test('uses exitCode as code if using a number', (t) => { t.plan(1) errorHandler = t.mock('../../../lib/utils/error-handler.js', mocks) + errorHandler.setNpm(npm) npmlog.level = 'silent' const _exit = process.exit @@ -420,6 +428,7 @@ t.test('call errorHandler with no error', (t) => { t.plan(1) errorHandler = t.mock('../../../lib/utils/error-handler.js', mocks) + errorHandler.setNpm(npm) const _exit = process.exit process.exit = (code) => { @@ -478,6 +487,7 @@ t.test('set it worked', (t) => { t.plan(1) errorHandler = t.mock('../../../lib/utils/error-handler.js', mocks) + errorHandler.setNpm(npm) const _exit = process.exit process.exit = () => { diff --git a/test/lib/utils/error-message.js b/test/lib/utils/error-message.js index 7529aac2d4a4b..4f94645a4542d 100644 --- a/test/lib/utils/error-message.js +++ b/test/lib/utils/error-message.js @@ -1,4 +1,5 @@ const t = require('tap') +const path = require('path') // make a bunch of stuff consistent for snapshots @@ -48,7 +49,7 @@ const mocks = { return 'explanation' }, }, - '../../../lib/npm.js': require('../../../lib/npm.js'), + // XXX ??? get '../../../lib/utils/is-windows.js' () { return process.platform === 'win32' }, @@ -110,7 +111,7 @@ t.test('just simple messages', t => { file, stack, }) - t.matchSnapshot(errorMessage(er)) + t.matchSnapshot(errorMessage(er, npm)) }) }) @@ -128,7 +129,7 @@ t.test('replace message/stack sensistive info', t => { file, stack, }) - t.matchSnapshot(errorMessage(er)) + t.matchSnapshot(errorMessage(er, npm)) t.end() }) @@ -148,7 +149,7 @@ t.test('bad engine with config loaded', t => { file, stack, }) - t.matchSnapshot(errorMessage(er)) + t.matchSnapshot(errorMessage(er, npm)) t.end() }) @@ -162,7 +163,7 @@ t.test('enoent without a file', t => { pkgid, stack, }) - t.matchSnapshot(errorMessage(er)) + t.matchSnapshot(errorMessage(er, npm)) t.end() }) @@ -179,20 +180,20 @@ t.test('enolock without a command', t => { file, stack, }) - t.matchSnapshot(errorMessage(er)) + t.matchSnapshot(errorMessage(er, npm)) t.end() }) t.test('default message', t => { - t.matchSnapshot(errorMessage(new Error('error object'))) - t.matchSnapshot(errorMessage('error string')) + t.matchSnapshot(errorMessage(new Error('error object'), npm)) + t.matchSnapshot(errorMessage('error string'), npm) t.matchSnapshot(errorMessage(Object.assign(new Error('cmd err'), { cmd: 'some command', signal: 'SIGYOLO', args: ['a', 'r', 'g', 's'], stdout: 'stdout', stderr: 'stderr', - }))) + }), npm)) t.end() }) @@ -213,7 +214,7 @@ t.test('eacces/eperm', t => { stack: 'dummy stack trace', }) verboseLogs.length = 0 - t.matchSnapshot(errorMessage(er)) + t.matchSnapshot(errorMessage(er, npm)) t.matchSnapshot(verboseLogs) t.end() verboseLogs.length = 0 @@ -288,7 +289,7 @@ t.test('json parse', t => { t.matchSnapshot(errorMessage(Object.assign(new Error('conflicted'), { code: 'EJSONPARSE', file: resolve(dir, 'package.json'), - }))) + }), npm)) t.end() }) @@ -310,7 +311,7 @@ t.test('json parse', t => { t.matchSnapshot(errorMessage(Object.assign(new Error('not json'), { code: 'EJSONPARSE', file: resolve(dir, 'package.json'), - }))) + }), npm)) t.end() }) @@ -326,7 +327,7 @@ t.test('json parse', t => { t.matchSnapshot(errorMessage(Object.assign(new Error('not json'), { code: 'EJSONPARSE', file: `${dir}/blerg.json`, - }))) + }), npm)) t.end() }) @@ -337,21 +338,21 @@ t.test('eotp/e401', t => { t.test('401, no auth headers', t => { t.matchSnapshot(errorMessage(Object.assign(new Error('nope'), { code: 'E401', - }))) + }), npm)) t.end() }) t.test('401, no message', t => { t.matchSnapshot(errorMessage({ code: 'E401', - })) + }, npm)) t.end() }) t.test('one-time pass challenge code', t => { t.matchSnapshot(errorMessage(Object.assign(new Error('nope'), { code: 'EOTP', - }))) + }), npm)) t.end() }) @@ -359,7 +360,7 @@ t.test('eotp/e401', t => { const message = 'one-time pass' t.matchSnapshot(errorMessage(Object.assign(new Error(message), { code: 'E401', - }))) + }), npm)) t.end() }) @@ -379,7 +380,7 @@ t.test('eotp/e401', t => { }, code: 'E401', }) - t.matchSnapshot(errorMessage(er)) + t.matchSnapshot(errorMessage(er, npm)) t.end() }) } @@ -391,7 +392,7 @@ t.test('eotp/e401', t => { t.test('404', t => { t.test('no package id', t => { const er = Object.assign(new Error('404 not found'), { code: 'E404' }) - t.matchSnapshot(errorMessage(er)) + t.matchSnapshot(errorMessage(er, npm)) t.end() }) t.test('you should publish it', t => { @@ -399,7 +400,7 @@ t.test('404', t => { pkgid: 'yolo', code: 'E404', }) - t.matchSnapshot(errorMessage(er)) + t.matchSnapshot(errorMessage(er, npm)) t.end() }) t.test('name with warning', t => { @@ -407,7 +408,7 @@ t.test('404', t => { pkgid: new Array(215).fill('x').join(''), code: 'E404', }) - t.matchSnapshot(errorMessage(er)) + t.matchSnapshot(errorMessage(er, npm)) t.end() }) t.test('name with error', t => { @@ -415,7 +416,7 @@ t.test('404', t => { pkgid: 'node_modules', code: 'E404', }) - t.matchSnapshot(errorMessage(er)) + t.matchSnapshot(errorMessage(er, npm)) t.end() }) t.end() @@ -435,7 +436,7 @@ t.test('bad platform', t => { }, code: 'EBADPLATFORM', }) - t.matchSnapshot(errorMessage(er)) + t.matchSnapshot(errorMessage(er, npm)) t.end() }) t.test('array os/arch', t => { @@ -451,7 +452,7 @@ t.test('bad platform', t => { }, code: 'EBADPLATFORM', }) - t.matchSnapshot(errorMessage(er)) + t.matchSnapshot(errorMessage(er, npm)) t.end() }) @@ -462,7 +463,11 @@ t.test('explain ERESOLVE errors', t => { const er = Object.assign(new Error('could not resolve'), { code: 'ERESOLVE', }) - t.matchSnapshot(errorMessage(er)) - t.strictSame(EXPLAIN_CALLED, [[er]]) + t.matchSnapshot(errorMessage(er, npm)) + t.match(EXPLAIN_CALLED, [[ + er, + undefined, + path.resolve(npm.cache, 'eresolve-report.txt'), + ]]) t.end() }) diff --git a/test/lib/utils/explain-eresolve.js b/test/lib/utils/explain-eresolve.js index 90795bb4470b2..f9710ee889ab1 100644 --- a/test/lib/utils/explain-eresolve.js +++ b/test/lib/utils/explain-eresolve.js @@ -1,8 +1,6 @@ const t = require('tap') const npm = {} -const { explain, report } = t.mock('../../../lib/utils/explain-eresolve.js', { - '../../../lib/npm.js': npm, -}) +const { explain, report } = require('../../../lib/utils/explain-eresolve.js') const { statSync, readFileSync, unlinkSync } = require('fs') // strip out timestamps from reports const read = f => readFileSync(f, 'utf8') @@ -25,23 +23,18 @@ for (const [name, expl] of Object.entries(cases)) { t.cleanSnapshot = str => str.split(reportFile).join('${REPORT}') npm.color = true - t.matchSnapshot(report(expl), 'report with color') + t.matchSnapshot(report(expl, true, reportFile), 'report with color') const reportData = read(reportFile) t.matchSnapshot(reportData, 'report') unlinkSync(reportFile) - t.matchSnapshot(report(expl, 2), 'report with color, depth only 2') + + t.matchSnapshot(report(expl, false, reportFile), 'report with no color') t.equal(read(reportFile), reportData, 'same report written for object') unlinkSync(reportFile) - npm.color = false - t.matchSnapshot(report(expl, 6), 'report with no color, depth of 6') - t.equal(read(reportFile), reportData, 'same report written for object') - unlinkSync(reportFile) - npm.color = true - t.matchSnapshot(explain(expl), 'explain with color') + t.matchSnapshot(explain(expl, true, 2), 'explain with color, depth of 2') t.throws(() => statSync(reportFile), { code: 'ENOENT' }, 'no report') - npm.color = false - t.matchSnapshot(explain(expl, 6), 'explain with no color, depth of 6') + t.matchSnapshot(explain(expl, false, 6), 'explain with no color, depth of 6') t.throws(() => statSync(reportFile), { code: 'ENOENT' }, 'no report') t.end() diff --git a/test/lib/utils/setup-log.js b/test/lib/utils/setup-log.js index 3daf3b8a52f53..86befe6e29297 100644 --- a/test/lib/utils/setup-log.js +++ b/test/lib/utils/setup-log.js @@ -92,7 +92,7 @@ t.test('setup with color=always and unicode', t => { })), true) npmlog.warn('ERESOLVE', 'hello', { some: { other: 'object' } }) - t.strictSame(EXPLAIN_CALLED, [[{ some: { other: 'object' } }]], + t.strictSame(EXPLAIN_CALLED, [[{ some: { other: 'object' } }, true, 2]], 'log.warn(ERESOLVE) patched to call explainEresolve()') t.strictSame(WARN_CALLED, [ ['ERESOLVE', 'hello'], From 102d4e6fb3c3b02148dbeee977a7d1e6372340d5 Mon Sep 17 00:00:00 2001 From: Gar Date: Tue, 15 Jun 2021 10:50:14 -0700 Subject: [PATCH 04/20] fix(workspaces): explicitly error in global mode Also includes a preliminary refactor to consolidate workspace logic now that every command that supports workspaces has it implemented. PR-URL: https://github.com/npm/cli/pull/3417 Credit: @wraithgar Close: #3417 Reviewed-by: @ruyadorno --- lib/audit.js | 2 +- lib/base-command.js | 9 +++++ lib/ci.js | 2 +- lib/dedupe.js | 2 +- lib/diff.js | 7 ++-- lib/dist-tag.js | 6 ++-- lib/docs.js | 6 ++-- lib/exec.js | 12 +++---- lib/explain.js | 4 +-- lib/fund.js | 2 +- lib/install.js | 2 +- lib/link.js | 2 +- lib/ls.js | 4 +-- lib/npm.js | 3 ++ lib/outdated.js | 6 ++-- lib/pack.js | 6 ++-- lib/prune.js | 2 +- lib/publish.js | 10 +++--- lib/rebuild.js | 2 +- lib/repo.js | 6 ++-- lib/run-script.js | 15 ++++---- lib/set-script.js | 20 +++++------ lib/uninstall.js | 2 +- lib/unpublish.js | 6 ++-- lib/update.js | 2 +- lib/version.js | 13 +++---- lib/view.js | 12 +++---- lib/workspaces/arborist-cmd.js | 7 ++-- test/lib/npm.js | 56 ++++++++++++++++++++++++++++- test/lib/workspaces/arborist-cmd.js | 12 +++---- 30 files changed, 139 insertions(+), 101 deletions(-) diff --git a/lib/audit.js b/lib/audit.js index a97fd9b30abd4..54480d1f0cbf9 100644 --- a/lib/audit.js +++ b/lib/audit.js @@ -58,7 +58,7 @@ class Audit extends ArboristWorkspaceCmd { audit: true, path: this.npm.prefix, reporter, - workspaces: this.workspaces, + workspaces: this.workspaceNames, } const arb = new Arborist(opts) diff --git a/lib/base-command.js b/lib/base-command.js index 843fb2d4b1358..4077733a934b0 100644 --- a/lib/base-command.js +++ b/lib/base-command.js @@ -1,6 +1,7 @@ // Base class for npm.commands[cmd] const usageUtil = require('./utils/usage.js') const ConfigDefinitions = require('./utils/config/definitions.js') +const getWorkspaces = require('./workspaces/get-workspaces.js') class BaseCommand { constructor (npm) { @@ -72,5 +73,13 @@ class BaseCommand { { code: 'ENOWORKSPACES' } ) } + + async setWorkspaces (filters) { + // TODO npm guards workspaces/global mode so we should use this.npm.prefix? + const ws = await getWorkspaces(filters, { path: this.npm.localPrefix }) + this.workspaces = ws + this.workspaceNames = [...ws.keys()] + this.workspacePaths = [...ws.values()] + } } module.exports = BaseCommand diff --git a/lib/ci.js b/lib/ci.js index 3a2a2b316a150..3ff4b65badb49 100644 --- a/lib/ci.js +++ b/lib/ci.js @@ -55,7 +55,7 @@ class CI extends ArboristWorkspaceCmd { path: where, log: this.npm.log, save: false, // npm ci should never modify the lockfile or package.json - workspaces: this.workspaces, + workspaces: this.workspaceNames, } const arb = new Arborist(opts) diff --git a/lib/dedupe.js b/lib/dedupe.js index 9a58316b80109..aaa7a30d10416 100644 --- a/lib/dedupe.js +++ b/lib/dedupe.js @@ -50,7 +50,7 @@ class Dedupe extends ArboristWorkspaceCmd { log: this.npm.log, path: where, dryRun, - workspaces: this.workspaces, + workspaces: this.workspaceNames, } const arb = new Arborist(opts) await arb.dedupe(opts) diff --git a/lib/diff.js b/lib/diff.js index d315551d443a5..01658c4664d05 100644 --- a/lib/diff.js +++ b/lib/diff.js @@ -8,7 +8,6 @@ const npmlog = require('npmlog') const pacote = require('pacote') const pickManifest = require('npm-pick-manifest') -const getWorkspaces = require('./workspaces/get-workspaces.js') const readPackageName = require('./utils/read-package-name.js') const BaseCommand = require('./base-command.js') @@ -90,9 +89,8 @@ class Diff extends BaseCommand { } async diffWorkspaces (args, filters) { - const workspaces = - await getWorkspaces(filters, { path: this.npm.localPrefix }) - for (const workspacePath of workspaces.values()) { + await this.setWorkspaces(filters) + for (const workspacePath of this.workspacePaths) { this.top = workspacePath this.prefix = workspacePath await this.diff(args) @@ -104,7 +102,6 @@ class Diff extends BaseCommand { async packageName (path) { let name try { - // TODO this won't work as expected in global mode name = await readPackageName(this.prefix) } catch (e) { npmlog.verbose('diff', 'could not read project dir package.json') diff --git a/lib/dist-tag.js b/lib/dist-tag.js index 11b1ad931e18b..e32dcf61fff80 100644 --- a/lib/dist-tag.js +++ b/lib/dist-tag.js @@ -5,7 +5,6 @@ const semver = require('semver') const otplease = require('./utils/otplease.js') const readPackageName = require('./utils/read-package-name.js') -const getWorkspaces = require('./workspaces/get-workspaces.js') const BaseCommand = require('./base-command.js') class DistTag extends BaseCommand { @@ -180,10 +179,9 @@ class DistTag extends BaseCommand { } async listWorkspaces (filters) { - const workspaces = - await getWorkspaces(filters, { path: this.npm.localPrefix }) + await this.setWorkspaces(filters) - for (const [name] of workspaces) { + for (const name of this.workspaceNames) { try { this.npm.output(`${name}:`) await this.list(npa(name), this.npm.flatOptions) diff --git a/lib/docs.js b/lib/docs.js index 24bbe9c854a62..69a19c35c3a13 100644 --- a/lib/docs.js +++ b/lib/docs.js @@ -2,7 +2,6 @@ const log = require('npmlog') const pacote = require('pacote') const openUrl = require('./utils/open-url.js') const hostedFromMani = require('./utils/hosted-git-info-from-manifest.js') -const getWorkspaces = require('./workspaces/get-workspaces.js') const BaseCommand = require('./base-command.js') class Docs extends BaseCommand { @@ -42,9 +41,8 @@ class Docs extends BaseCommand { } async docsWorkspaces (args, filters) { - const workspaces = - await getWorkspaces(filters, { path: this.npm.localPrefix }) - return this.docs([...workspaces.values()]) + await this.setWorkspaces(filters) + return this.docs(this.workspacePaths) } async getDocs (pkg) { diff --git a/lib/exec.js b/lib/exec.js index 8a87615d9749e..7e5f6886a3ed7 100644 --- a/lib/exec.js +++ b/lib/exec.js @@ -1,7 +1,6 @@ const libexec = require('libnpmexec') const BaseCommand = require('./base-command.js') const getLocationMsg = require('./exec/get-workspace-location-msg.js') -const getWorkspaces = require('./workspaces/get-workspaces.js') // it's like this: // @@ -105,16 +104,15 @@ class Exec extends BaseCommand { } async _execWorkspaces (args, filters) { - const workspaces = - await getWorkspaces(filters, { path: this.npm.localPrefix }) + await this.setWorkspaces(filters) const color = this.npm.config.get('color') - for (const workspacePath of workspaces.values()) { - const locationMsg = await getLocationMsg({ color, path: workspacePath }) + for (const path of this.workspacePaths) { + const locationMsg = await getLocationMsg({ color, path }) await this._exec(args, { locationMsg, - path: workspacePath, - runPath: workspacePath, + path, + runPath: path, }) } } diff --git a/lib/explain.js b/lib/explain.js index de04c69857240..7d785d7bfcf44 100644 --- a/lib/explain.js +++ b/lib/explain.js @@ -46,8 +46,8 @@ class Explain extends ArboristWorkspaceCmd { const arb = new Arborist({ path: this.npm.prefix, ...this.npm.flatOptions }) const tree = await arb.loadActual() - if (this.workspaces && this.workspaces.length) - this.filterSet = arb.workspaceDependencySet(tree, this.workspaces) + if (this.workspaceNames && this.workspaceNames.length) + this.filterSet = arb.workspaceDependencySet(tree, this.workspaceNames) const nodes = new Set() for (const arg of args) { diff --git a/lib/fund.js b/lib/fund.js index 55d2f65dc4b55..92580a756e8af 100644 --- a/lib/fund.js +++ b/lib/fund.js @@ -95,7 +95,7 @@ class Fund extends ArboristWorkspaceCmd { const fundingInfo = getFundingInfo(tree, { ...this.flatOptions, log: this.npm.log, - workspaces: this.workspaces, + workspaces: this.workspaceNames, }) if (this.npm.config.get('json')) diff --git a/lib/install.js b/lib/install.js index 7c5f55bb9de8a..6611763978e61 100644 --- a/lib/install.js +++ b/lib/install.js @@ -144,7 +144,7 @@ class Install extends ArboristWorkspaceCmd { auditLevel: null, path: where, add: args, - workspaces: this.workspaces, + workspaces: this.workspaceNames, } const arb = new Arborist(opts) await arb.reify(opts) diff --git a/lib/link.js b/lib/link.js index d6abf139730bd..febd908718be3 100644 --- a/lib/link.js +++ b/lib/link.js @@ -146,7 +146,7 @@ class Link extends ArboristWorkspaceCmd { log: this.npm.log, add: names.map(l => `file:${resolve(globalTop, 'node_modules', l)}`), save, - workspaces: this.workspaces, + workspaces: this.workspaceNames, }) await reifyFinish(this.npm, localArb) diff --git a/lib/ls.js b/lib/ls.js index d92b73ddfcdbb..78263554ca0a1 100644 --- a/lib/ls.js +++ b/lib/ls.js @@ -94,8 +94,8 @@ class LS extends ArboristWorkspaceCmd { // We only have to filter the first layer of edges, so we don't // explore anything that isn't part of the selected workspace set. let wsNodes - if (this.workspaces && this.workspaces.length) - wsNodes = arb.workspaceNodes(tree, this.workspaces) + if (this.workspaceNames && this.workspaceNames.length) + wsNodes = arb.workspaceNodes(tree, this.workspaceNames) const filterBySelectedWorkspaces = edge => { if (!wsNodes || !wsNodes.length) return true diff --git a/lib/npm.js b/lib/npm.js index 5f8b2ff3d703d..937459501c0a5 100644 --- a/lib/npm.js +++ b/lib/npm.js @@ -108,6 +108,9 @@ const npm = module.exports = new class extends EventEmitter { this.output(impl.usage) cb() } else if (filterByWorkspaces) { + if (this.config.get('global')) + return cb(new Error('Workspaces not supported for global packages')) + impl.execWorkspaces(args, this.config.get('workspace'), er => { process.emit('timeEnd', `command:${cmd}`) cb(er) diff --git a/lib/outdated.js b/lib/outdated.js index 1be92b9349fe7..9d60d143d71ce 100644 --- a/lib/outdated.js +++ b/lib/outdated.js @@ -59,8 +59,10 @@ class Outdated extends ArboristWorkspaceCmd { this.list = [] this.tree = await arb.loadActual() - if (this.workspaces && this.workspaces.length) - this.filterSet = arb.workspaceDependencySet(this.tree, this.workspaces) + if (this.workspaceNames && this.workspaceNames.length) { + this.filterSet = + arb.workspaceDependencySet(this.tree, this.workspaceNames) + } if (args.length !== 0) { // specific deps diff --git a/lib/pack.js b/lib/pack.js index 52d4c3e7f900a..f4364d29033c4 100644 --- a/lib/pack.js +++ b/lib/pack.js @@ -3,7 +3,6 @@ const log = require('npmlog') const pacote = require('pacote') const libpack = require('libnpmpack') const npa = require('npm-package-arg') -const getWorkspaces = require('./workspaces/get-workspaces.js') const { getContents, logTar } = require('./utils/tar.js') @@ -97,9 +96,8 @@ class Pack extends BaseCommand { return this.pack(args) } - const workspaces = - await getWorkspaces(filters, { path: this.npm.localPrefix }) - return this.pack([...workspaces.values(), ...args.filter(a => a !== '.')]) + await this.setWorkspaces(filters) + return this.pack([...this.workspacePaths, ...args.filter(a => a !== '.')]) } } module.exports = Pack diff --git a/lib/prune.js b/lib/prune.js index a90e7595421ec..a91276fc4fa27 100644 --- a/lib/prune.js +++ b/lib/prune.js @@ -34,7 +34,7 @@ class Prune extends ArboristWorkspaceCmd { ...this.npm.flatOptions, path: where, log: this.npm.log, - workspaces: this.workspaces, + workspaces: this.workspaceNames, } const arb = new Arborist(opts) await arb.prune(opts) diff --git a/lib/publish.js b/lib/publish.js index 3cb8b0627e974..f35388a30f4ed 100644 --- a/lib/publish.js +++ b/lib/publish.js @@ -11,7 +11,6 @@ const chalk = require('chalk') const otplease = require('./utils/otplease.js') const { getContents, logTar } = require('./utils/tar.js') -const getWorkspaces = require('./workspaces/get-workspaces.js') // for historical reasons, publishConfig in package.json can contain ANY config // keys that npm supports in .npmrc files and elsewhere. We *may* want to @@ -138,7 +137,7 @@ class Publish extends BaseCommand { }) } - if (!this.workspaces) { + if (!this.suppressOutput) { if (!silent && json) this.npm.output(JSON.stringify(pkgContents, null, 2)) else if (!silent) @@ -150,17 +149,16 @@ class Publish extends BaseCommand { async publishWorkspaces (args, filters) { // Suppresses JSON output in publish() so we can handle it here - this.workspaces = true + this.suppressOutput = true const results = {} const json = this.npm.config.get('json') const silent = log.level === 'silent' const noop = a => a const color = this.npm.color ? chalk : { green: noop, bold: noop } - const workspaces = - await getWorkspaces(filters, { path: this.npm.localPrefix }) + await this.setWorkspaces(filters) - for (const [name, workspace] of workspaces.entries()) { + for (const [name, workspace] of this.workspaces.entries()) { let pkgContents try { pkgContents = await this.publish([workspace]) diff --git a/lib/rebuild.js b/lib/rebuild.js index ef88dc5168d0c..9aa0e27f87eb4 100644 --- a/lib/rebuild.js +++ b/lib/rebuild.js @@ -47,7 +47,7 @@ class Rebuild extends ArboristWorkspaceCmd { ...this.npm.flatOptions, path: where, // TODO when extending ReifyCmd - // workspaces: this.workspaces, + // workspaces: this.workspaceNames, }) if (args.length) { diff --git a/lib/repo.js b/lib/repo.js index 645c0eeae32fe..e0172d01f63d1 100644 --- a/lib/repo.js +++ b/lib/repo.js @@ -1,6 +1,5 @@ const log = require('npmlog') const pacote = require('pacote') -const getWorkspaces = require('./workspaces/get-workspaces.js') const { URL } = require('url') const hostedFromMani = require('./utils/hosted-git-info-from-manifest.js') @@ -44,9 +43,8 @@ class Repo extends BaseCommand { } async repoWorkspaces (args, filters) { - const workspaces = - await getWorkspaces(filters, { path: this.npm.localPrefix }) - return this.repo([...workspaces.values()]) + await this.setWorkspaces(filters) + return this.repo(this.workspacePaths) } async get (pkg) { diff --git a/lib/run-script.js b/lib/run-script.js index ec1042828fad8..b94d2fce07180 100644 --- a/lib/run-script.js +++ b/lib/run-script.js @@ -6,7 +6,6 @@ const rpj = require('read-package-json-fast') const log = require('npmlog') const didYouMean = require('./utils/did-you-mean.js') const isWindowsShell = require('./utils/is-windows-shell.js') -const getWorkspaces = require('./workspaces/get-workspaces.js') const cmdList = [ 'publish', @@ -195,10 +194,9 @@ class RunScript extends BaseCommand { async runWorkspaces (args, filters) { const res = [] - const workspaces = - await getWorkspaces(filters, { path: this.npm.localPrefix }) + await this.setWorkspaces(filters) - for (const workspacePath of workspaces.values()) { + for (const workspacePath of this.workspacePaths) { const pkg = await rpj(`${workspacePath}/package.json`) const runResult = await this.run(args, { path: workspacePath, @@ -227,15 +225,14 @@ class RunScript extends BaseCommand { } async listWorkspaces (args, filters) { - const workspaces = - await getWorkspaces(filters, { path: this.npm.localPrefix }) + await this.setWorkspaces(filters) if (log.level === 'silent') return if (this.npm.config.get('json')) { const res = {} - for (const workspacePath of workspaces.values()) { + for (const workspacePath of this.workspacePaths) { const { scripts, name } = await rpj(`${workspacePath}/package.json`) res[name] = { ...scripts } } @@ -244,7 +241,7 @@ class RunScript extends BaseCommand { } if (this.npm.config.get('parseable')) { - for (const workspacePath of workspaces.values()) { + for (const workspacePath of this.workspacePaths) { const { scripts, name } = await rpj(`${workspacePath}/package.json`) for (const [script, cmd] of Object.entries(scripts || {})) this.npm.output(`${name}:${script}:${cmd}`) @@ -252,7 +249,7 @@ class RunScript extends BaseCommand { return } - for (const workspacePath of workspaces.values()) + for (const workspacePath of this.workspacePaths) await this.list(args, workspacePath) } } diff --git a/lib/set-script.js b/lib/set-script.js index b31e123becd8b..cd01e28b56b06 100644 --- a/lib/set-script.js +++ b/lib/set-script.js @@ -3,7 +3,6 @@ const fs = require('fs') const parseJSON = require('json-parse-even-better-errors') const rpj = require('read-package-json-fast') const { resolve } = require('path') -const getWorkspaces = require('./workspaces/get-workspaces.js') const BaseCommand = require('./base-command.js') class SetScript extends BaseCommand { @@ -47,28 +46,27 @@ class SetScript extends BaseCommand { } exec (args, cb) { - this.set(args).then(() => cb()).catch(cb) + this.setScript(args).then(() => cb()).catch(cb) } - async set (args) { + async setScript (args) { this.validate(args) - const warn = this.setScript(this.npm.localPrefix, args[0], args[1]) + const warn = this.doSetScript(this.npm.localPrefix, args[0], args[1]) if (warn) log.warn('set-script', `Script "${args[0]}" was overwritten`) } execWorkspaces (args, filters, cb) { - this.setWorkspaces(args, filters).then(() => cb()).catch(cb) + this.setScriptWorkspaces(args, filters).then(() => cb()).catch(cb) } - async setWorkspaces (args, filters) { + async setScriptWorkspaces (args, filters) { this.validate(args) - const workspaces = - await getWorkspaces(filters, { path: this.npm.localPrefix }) + await this.setWorkspaces(filters) - for (const [name, path] of workspaces) { + for (const [name, path] of this.workspaces) { try { - const warn = this.setScript(path, args[0], args[1]) + const warn = this.doSetScript(path, args[0], args[1]) if (warn) { log.warn('set-script', `Script "${args[0]}" was overwritten`) log.warn(` in workspace: ${name}`) @@ -86,7 +84,7 @@ class SetScript extends BaseCommand { // returns a Boolean that will be true if // the requested script was overwritten // and false if it was set as a new script - setScript (path, name, value) { + doSetScript (path, name, value) { // Set the script let manifest let warn = false diff --git a/lib/uninstall.js b/lib/uninstall.js index cbbc62c2a4a75..fbb2cef0fbf18 100644 --- a/lib/uninstall.js +++ b/lib/uninstall.js @@ -66,7 +66,7 @@ class Uninstall extends ArboristWorkspaceCmd { path, log: this.npm.log, rm: args, - workspaces: this.workspaces, + workspaces: this.workspaceNames, } const arb = new Arborist(opts) await arb.reify(opts) diff --git a/lib/unpublish.js b/lib/unpublish.js index 1571fd88ef781..32a634013a7c4 100644 --- a/lib/unpublish.js +++ b/lib/unpublish.js @@ -6,7 +6,6 @@ const npmFetch = require('npm-registry-fetch') const libunpub = require('libnpmpublish').unpublish const readJson = util.promisify(require('read-package-json')) -const getWorkspaces = require('./workspaces/get-workspaces.js') const otplease = require('./utils/otplease.js') const getIdentity = require('./utils/get-identity.js') @@ -129,8 +128,7 @@ class Unpublish extends BaseCommand { } async unpublishWorkspaces (args, filters) { - const workspaces = - await getWorkspaces(filters, { path: this.npm.localPrefix }) + await this.setWorkspaces(filters) const force = this.npm.config.get('force') if (!force) { @@ -140,7 +138,7 @@ class Unpublish extends BaseCommand { ) } - for (const [name] of workspaces.entries()) + for (const name of this.workspaceNames) await this.unpublish([name]) } } diff --git a/lib/update.js b/lib/update.js index 3cdeb8ea7dd31..393c8f0f67e5f 100644 --- a/lib/update.js +++ b/lib/update.js @@ -66,7 +66,7 @@ class Update extends ArboristWorkspaceCmd { ...this.npm.flatOptions, log: this.npm.log, path: where, - workspaces: this.workspaces, + workspaces: this.workspaceNames, }) await arb.reify({ update }) diff --git a/lib/version.js b/lib/version.js index 3ef3801e74e81..f3680fe8b7a01 100644 --- a/lib/version.js +++ b/lib/version.js @@ -3,7 +3,6 @@ const { resolve } = require('path') const { promisify } = require('util') const readFile = promisify(require('fs').readFile) -const getWorkspaces = require('./workspaces/get-workspaces.js') const BaseCommand = require('./base-command.js') class Version extends BaseCommand { @@ -93,9 +92,8 @@ class Version extends BaseCommand { async changeWorkspaces (args, filters) { const prefix = this.npm.config.get('tag-version-prefix') - const workspaces = - await getWorkspaces(filters, { path: this.npm.localPrefix }) - for (const [name, path] of workspaces) { + await this.setWorkspaces(filters) + for (const [name, path] of this.workspaces) { this.npm.output(name) const version = await libnpmversion(args[0], { ...this.npm.flatOptions, @@ -128,11 +126,10 @@ class Version extends BaseCommand { async listWorkspaces (filters) { const results = {} - const workspaces = - await getWorkspaces(filters, { path: this.npm.localPrefix }) - for (const [, path] of workspaces) { + await this.setWorkspaces(filters) + for (const path of this.workspacePaths) { const pj = resolve(path, 'package.json') - // getWorkspaces has already parsed this so we know it won't error + // setWorkspaces has already parsed package.json so we know it won't error const pkg = await readFile(pj, 'utf8') .then(data => JSON.parse(data)) diff --git a/lib/view.js b/lib/view.js index 9cc1aed914488..788df3ed0b4d8 100644 --- a/lib/view.js +++ b/lib/view.js @@ -13,7 +13,6 @@ const semver = require('semver') const style = require('ansistyles') const { inspect, promisify } = require('util') const { packument } = require('pacote') -const getWorkspaces = require('./workspaces/get-workspaces.js') const readFile = promisify(fs.readFile) const readJson = async file => jsonParse(await readFile(file, 'utf8')) @@ -160,10 +159,9 @@ class View extends BaseCommand { args = [''] // getData relies on this } const results = {} - const workspaces = - await getWorkspaces(filters, { path: this.npm.localPrefix }) - for (const workspace of [...workspaces.entries()]) { - const wsPkg = `${workspace[0]}${pkg.slice(1)}` + await this.setWorkspaces(filters) + for (const name of this.workspaceNames) { + const wsPkg = `${name}${pkg.slice(1)}` const [pckmnt, data] = await this.getData(wsPkg, args) let reducedData = data.reduce(reducer, {}) @@ -177,7 +175,7 @@ class View extends BaseCommand { if (wholePackument) data.map((v) => this.prettyView(pckmnt, v[Object.keys(v)[0]][''])) else { - console.log(`${workspace[0]}:`) + console.log(`${name}:`) const msg = await this.jsonData(reducedData, pckmnt._id) if (msg !== '') console.log(msg) @@ -185,7 +183,7 @@ class View extends BaseCommand { } else { const msg = await this.jsonData(reducedData, pckmnt._id) if (msg !== '') - results[workspace[0]] = JSON.parse(msg) + results[name] = JSON.parse(msg) } } if (Object.keys(results).length > 0) diff --git a/lib/workspaces/arborist-cmd.js b/lib/workspaces/arborist-cmd.js index 337e7f9d8f932..cb6b66b8cb257 100644 --- a/lib/workspaces/arborist-cmd.js +++ b/lib/workspaces/arborist-cmd.js @@ -3,7 +3,6 @@ // be able to run a filtered Arborist.reify() at some point. const BaseCommand = require('../base-command.js') -const getWorkspaces = require('./get-workspaces.js') class ArboristCmd extends BaseCommand { /* istanbul ignore next - see test/lib/load-all-commands.js */ static get params () { @@ -14,10 +13,8 @@ class ArboristCmd extends BaseCommand { } execWorkspaces (args, filters, cb) { - getWorkspaces(filters, { path: this.npm.localPrefix }) - .then(workspaces => { - this.workspaces = [...workspaces.keys()] - this.workspacePaths = [...workspaces.values()] + this.setWorkspaces(filters) + .then(() => { this.exec(args, cb) }) .catch(er => cb(er)) diff --git a/test/lib/npm.js b/test/lib/npm.js index 6f8f8936d3f73..6909c43e4ff0e 100644 --- a/test/lib/npm.js +++ b/test/lib/npm.js @@ -355,7 +355,7 @@ t.test('npm.load', t => { await new Promise((res) => setTimeout(res)) }) - t.test('workpaces-aware configs and commands', async t => { + t.test('workspace-aware configs and commands', async t => { const dir = t.testdir({ packages: { a: { @@ -438,6 +438,60 @@ t.test('npm.load', t => { }) }) + t.test('workspaces in global mode', async t => { + const dir = t.testdir({ + packages: { + a: { + 'package.json': JSON.stringify({ + name: 'a', + version: '1.0.0', + scripts: { test: 'echo test a' }, + }), + }, + b: { + 'package.json': JSON.stringify({ + name: 'b', + version: '1.0.0', + scripts: { test: 'echo test b' }, + }), + }, + }, + 'package.json': JSON.stringify({ + name: 'root', + version: '1.0.0', + workspaces: ['./packages/*'], + }), + }) + const { execPath } = process + freshConfig({ + argv: [ + execPath, + process.argv[1], + '--userconfig', + resolve(dir, '.npmrc'), + '--color', + 'false', + '--workspaces', + '--global', + 'true', + ], + }) + await npm.load(er => { + if (er) + throw er + }) + npm.localPrefix = dir + await new Promise((res, rej) => { + // verify that calling the command with a short name still sets + // the npm.command property to the full canonical name of the cmd. + npm.command = null + npm.commands.run([], er => { + t.match(er, /Workspaces not supported for global packages/) + res() + }) + }) + }) + t.end() }) diff --git a/test/lib/workspaces/arborist-cmd.js b/test/lib/workspaces/arborist-cmd.js index 740ddb1ff0dc5..75ac8f4ebf804 100644 --- a/test/lib/workspaces/arborist-cmd.js +++ b/test/lib/workspaces/arborist-cmd.js @@ -49,7 +49,7 @@ t.test('arborist-cmd', async t => { // check filtering for a single workspace name cmd.exec = function (args, cb) { - t.same(this.workspaces, ['a'], 'should set array with single ws name') + t.same(this.workspaceNames, ['a'], 'should set array with single ws name') t.same(args, ['foo'], 'should get received args') cb() } @@ -59,7 +59,7 @@ t.test('arborist-cmd', async t => { // check filtering single workspace by path cmd.exec = function (args, cb) { - t.same(this.workspaces, ['a'], + t.same(this.workspaceNames, ['a'], 'should set array with single ws name from path') cb() } @@ -69,7 +69,7 @@ t.test('arborist-cmd', async t => { // check filtering single workspace by full path cmd.exec = function (args, cb) { - t.same(this.workspaces, ['a'], + t.same(this.workspaceNames, ['a'], 'should set array with single ws name from full path') cb() } @@ -79,7 +79,7 @@ t.test('arborist-cmd', async t => { // filtering multiple workspaces by name cmd.exec = function (args, cb) { - t.same(this.workspaces, ['a', 'c'], + t.same(this.workspaceNames, ['a', 'c'], 'should set array with multiple listed ws names') cb() } @@ -89,7 +89,7 @@ t.test('arborist-cmd', async t => { // filtering multiple workspaces by path names cmd.exec = function (args, cb) { - t.same(this.workspaces, ['a', 'c'], + t.same(this.workspaceNames, ['a', 'c'], 'should set array with multiple ws names from paths') cb() } @@ -99,7 +99,7 @@ t.test('arborist-cmd', async t => { // filtering multiple workspaces by parent path name cmd.exec = function (args, cb) { - t.same(this.workspaces, ['c', 'd'], + t.same(this.workspaceNames, ['c', 'd'], 'should set array with multiple ws names from a parent folder name') cb() } From 993df3041f5bdaa496c3c8d80f00d16b9cf0a1e6 Mon Sep 17 00:00:00 2001 From: Vlad GURDIGA Date: Wed, 16 Jun 2021 12:37:54 +0300 Subject: [PATCH 05/20] fix(docs): ls command usage instructions PR-URL: https://github.com/npm/cli/pull/3423 Credit: @gurdiga Close: #3423 Reviewed-by: @ruyadorno --- lib/ls.js | 2 +- tap-snapshots/test/lib/load-all-commands.js.test.cjs | 2 +- tap-snapshots/test/lib/utils/npm-usage.js.test.cjs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ls.js b/lib/ls.js index 78263554ca0a1..319439fcc8f48 100644 --- a/lib/ls.js +++ b/lib/ls.js @@ -36,7 +36,7 @@ class LS extends ArboristWorkspaceCmd { /* istanbul ignore next - see test/lib/load-all-commands.js */ static get usage () { - return ['npm ls [[<@scope>/] ...]'] + return ['[[<@scope>/] ...]'] } /* istanbul ignore next - see test/lib/load-all-commands.js */ diff --git a/tap-snapshots/test/lib/load-all-commands.js.test.cjs b/tap-snapshots/test/lib/load-all-commands.js.test.cjs index 097123d46a3cc..ff6a5fb321e94 100644 --- a/tap-snapshots/test/lib/load-all-commands.js.test.cjs +++ b/tap-snapshots/test/lib/load-all-commands.js.test.cjs @@ -583,7 +583,7 @@ npm ls List installed packages Usage: -npm ls npm ls [[<@scope>/] ...] +npm ls [[<@scope>/] ...] Options: [-a|--all] [--json] [-l|--long] [-p|--parseable] [-g|--global] [--depth ] diff --git a/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs b/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs index 54f6c3d2feb2a..e5ae7d827a6ca 100644 --- a/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs +++ b/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs @@ -678,7 +678,7 @@ All commands: List installed packages Usage: - npm ls npm ls [[<@scope>/] ...] + npm ls [[<@scope>/] ...] Options: [-a|--all] [--json] [-l|--long] [-p|--parseable] [-g|--global] [--depth ] From 6b951c042084e639be929a7ea783c2d85b311bad Mon Sep 17 00:00:00 2001 From: Gar Date: Wed, 16 Jun 2021 06:58:37 -0700 Subject: [PATCH 06/20] libnpmversion@1.2.1 * fix(retrieve-tag): pass match in a way git accepts --- node_modules/libnpmversion/lib/retrieve-tag.js | 2 +- node_modules/libnpmversion/package.json | 2 +- package-lock.json | 14 +++++++------- package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/node_modules/libnpmversion/lib/retrieve-tag.js b/node_modules/libnpmversion/lib/retrieve-tag.js index 7aa2abfda8651..6adb6df317a8d 100644 --- a/node_modules/libnpmversion/lib/retrieve-tag.js +++ b/node_modules/libnpmversion/lib/retrieve-tag.js @@ -2,7 +2,7 @@ const { spawn } = require('@npmcli/git') const semver = require('semver') module.exports = async opts => { - const tag = (await spawn(['describe', '--tags', '--abbrev=0', '--match=\'*.*.*\''], opts)).stdout.trim() + const tag = (await spawn(['describe', '--tags', '--abbrev=0', '--match=*.*.*'], opts)).stdout.trim() const ver = semver.coerce(tag, { loose: true }) if (ver) { return ver.version diff --git a/node_modules/libnpmversion/package.json b/node_modules/libnpmversion/package.json index ebc88a1fc5754..1ee2ee5995a52 100644 --- a/node_modules/libnpmversion/package.json +++ b/node_modules/libnpmversion/package.json @@ -1,6 +1,6 @@ { "name": "libnpmversion", - "version": "1.2.0", + "version": "1.2.1", "main": "lib/index.js", "files": [ "lib/*.js" diff --git a/package-lock.json b/package-lock.json index 3f3d1bf0f19ed..0c74e93d0100a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -115,7 +115,7 @@ "libnpmpublish": "^4.0.1", "libnpmsearch": "^3.1.1", "libnpmteam": "^2.0.3", - "libnpmversion": "^1.2.0", + "libnpmversion": "^1.2.1", "make-fetch-happen": "^9.0.1", "minipass": "^3.1.3", "minipass-pipeline": "^1.2.4", @@ -4746,9 +4746,9 @@ } }, "node_modules/libnpmversion": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/libnpmversion/-/libnpmversion-1.2.0.tgz", - "integrity": "sha512-0pfmobLZbOvq1cLIONZk8ISvEM1k3JdkNXWhMDZvUeH+ijBNvMVdPu/CPUr1eDFbNINS3b6R/0PbTIZDVz7thg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/libnpmversion/-/libnpmversion-1.2.1.tgz", + "integrity": "sha512-AA7x5CFgBFN+L4/JWobnY5t4OAHjQuPbAwUYJ7/NtHuyLut5meb+ne/aj0n7PWNiTGCJcRw/W6Zd2LoLT7EZuQ==", "inBundle": true, "dependencies": { "@npmcli/git": "^2.0.7", @@ -13816,9 +13816,9 @@ } }, "libnpmversion": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/libnpmversion/-/libnpmversion-1.2.0.tgz", - "integrity": "sha512-0pfmobLZbOvq1cLIONZk8ISvEM1k3JdkNXWhMDZvUeH+ijBNvMVdPu/CPUr1eDFbNINS3b6R/0PbTIZDVz7thg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/libnpmversion/-/libnpmversion-1.2.1.tgz", + "integrity": "sha512-AA7x5CFgBFN+L4/JWobnY5t4OAHjQuPbAwUYJ7/NtHuyLut5meb+ne/aj0n7PWNiTGCJcRw/W6Zd2LoLT7EZuQ==", "requires": { "@npmcli/git": "^2.0.7", "@npmcli/run-script": "^1.8.4", diff --git a/package.json b/package.json index a1f5159608cde..9ae066fb4eb2b 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "libnpmpublish": "^4.0.1", "libnpmsearch": "^3.1.1", "libnpmteam": "^2.0.3", - "libnpmversion": "^1.2.0", + "libnpmversion": "^1.2.1", "make-fetch-happen": "^9.0.1", "minipass": "^3.1.3", "minipass-pipeline": "^1.2.4", From ae285b39191f3a0c4edfb045a334057bef4567b5 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Sun, 13 Jun 2021 10:16:22 +1200 Subject: [PATCH 07/20] feat(ls): support `--package-lock-only` flag This enables using the virtual tree instead of node_modules. PR-URL: https://github.com/npm/cli/pull/3408 Credit: @G-Rath Close: #3408 Reviewed-by: @isaacs --- lib/ls.js | 13 +- lib/utils/config/definitions.js | 10 +- .../test/lib/load-all-commands.js.test.cjs | 4 +- .../lib/utils/config/describe-all.js.test.cjs | 10 +- .../test/lib/utils/npm-usage.js.test.cjs | 4 +- test/lib/ls.js | 786 ++++++++++++++++++ 6 files changed, 816 insertions(+), 11 deletions(-) diff --git a/lib/ls.js b/lib/ls.js index 319439fcc8f48..b425bd620b38e 100644 --- a/lib/ls.js +++ b/lib/ls.js @@ -50,6 +50,7 @@ class LS extends ArboristWorkspaceCmd { 'depth', 'omit', 'link', + 'package-lock-only', 'unicode', ...super.params, ] @@ -79,6 +80,7 @@ class LS extends ArboristWorkspaceCmd { const prod = this.npm.config.get('prod') const production = this.npm.config.get('production') const unicode = this.npm.config.get('unicode') + const packageLockOnly = this.npm.config.get('package-lock-only') const path = global ? resolve(this.npm.globalDir, '..') : this.npm.prefix @@ -88,7 +90,7 @@ class LS extends ArboristWorkspaceCmd { legacyPeerDeps: false, path, }) - const tree = await this.initTree({arb, args }) + const tree = await this.initTree({arb, args, packageLockOnly }) // filters by workspaces nodes when using -w // We only have to filter the first layer of edges, so we don't @@ -216,8 +218,13 @@ class LS extends ArboristWorkspaceCmd { } } - async initTree ({ arb, args }) { - const tree = await arb.loadActual() + async initTree ({ arb, args, packageLockOnly }) { + const tree = await ( + packageLockOnly + ? arb.loadVirtual() + : arb.loadActual() + ) + tree[_include] = args.length === 0 tree[_depth] = 0 diff --git a/lib/utils/config/definitions.js b/lib/utils/config/definitions.js index ce7702aaa4f79..b6f7c84317c6a 100644 --- a/lib/utils/config/definitions.js +++ b/lib/utils/config/definitions.js @@ -1333,8 +1333,14 @@ define('package-lock-only', { default: false, type: Boolean, description: ` - If set to true, it will update only the \`package-lock.json\`, instead of - checking \`node_modules\` and downloading dependencies. + If set to true, the current operation will only use the \`package-lock.json\`, + ignoring \`node_modules\`. + + For \`update\` this means only the \`package-lock.json\` will be updated, + instead of checking \`node_modules\` and downloading dependencies. + + For \`list\` this means the output will be based on the tree described by the + \`package-lock.json\`, rather than the contents of \`node_modules\`. `, flatten, }) diff --git a/tap-snapshots/test/lib/load-all-commands.js.test.cjs b/tap-snapshots/test/lib/load-all-commands.js.test.cjs index ff6a5fb321e94..70902ba10cf33 100644 --- a/tap-snapshots/test/lib/load-all-commands.js.test.cjs +++ b/tap-snapshots/test/lib/load-all-commands.js.test.cjs @@ -538,7 +538,7 @@ npm ll [[<@scope>/] ...] Options: [-a|--all] [--json] [-l|--long] [-p|--parseable] [-g|--global] [--depth ] [--omit [--omit ...]] [--link] -[--unicode] +[--package-lock-only] [--unicode] [-w|--workspace [-w|--workspace ...]] [-ws|--workspaces] @@ -588,7 +588,7 @@ npm ls [[<@scope>/] ...] Options: [-a|--all] [--json] [-l|--long] [-p|--parseable] [-g|--global] [--depth ] [--omit [--omit ...]] [--link] -[--unicode] +[--package-lock-only] [--unicode] [-w|--workspace [-w|--workspace ...]] [-ws|--workspaces] diff --git a/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs b/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs index da8cd1794f2ac..b7bcca8d539a1 100644 --- a/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs +++ b/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs @@ -781,8 +781,14 @@ package-locks disabled use \`npm prune\`. * Default: false * Type: Boolean -If set to true, it will update only the \`package-lock.json\`, instead of -checking \`node_modules\` and downloading dependencies. +If set to true, the current operation will only use the \`package-lock.json\`, +ignoring \`node_modules\`. + +For \`update\` this means only the \`package-lock.json\` will be updated, +instead of checking \`node_modules\` and downloading dependencies. + +For \`list\` this means the output will be based on the tree described by the +\`package-lock.json\`, rather than the contents of \`node_modules\`. #### \`parseable\` diff --git a/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs b/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs index e5ae7d827a6ca..dc10b43739b15 100644 --- a/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs +++ b/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs @@ -639,7 +639,7 @@ All commands: Options: [-a|--all] [--json] [-l|--long] [-p|--parseable] [-g|--global] [--depth ] [--omit [--omit ...]] [--link] - [--unicode] + [--package-lock-only] [--unicode] [-w|--workspace [-w|--workspace ...]] [-ws|--workspaces] @@ -683,7 +683,7 @@ All commands: Options: [-a|--all] [--json] [-l|--long] [-p|--parseable] [-g|--global] [--depth ] [--omit [--omit ...]] [--link] - [--unicode] + [--package-lock-only] [--unicode] [-w|--workspace [-w|--workspace ...]] [-ws|--workspaces] diff --git a/test/lib/ls.js b/test/lib/ls.js index ecdede809df20..582416f4aa2d2 100644 --- a/test/lib/ls.js +++ b/test/lib/ls.js @@ -107,6 +107,7 @@ const config = { only: null, parseable: false, production: false, + 'package-lock-only': false, } const flatOptions = { } @@ -4152,3 +4153,788 @@ t.test('ls --json', (t) => { t.end() }) + +t.test('ls --package-lock-only', (t) => { + config['package-lock-only'] = true + t.test('ls --package-lock-only --json', (t) => { + t.beforeEach(cleanUpResult) + config.json = true + config.parseable = false + t.test('no args', (t) => { + npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: '^1.0.0', + chai: '^1.0.0', + }, + }), + 'package-lock.json': JSON.stringify({ + dependencies: { + foo: { + version: '1.0.0', + requires: { + dog: '^1.0.0', + }, + }, + dog: { + version: '1.0.0', + }, + chai: { + version: '1.0.0', + }, + }, + }), + }) + ls.exec([], (err) => { + t.error(err, 'npm ls') + t.same( + jsonParse(result), + { + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: { + version: '1.0.0', + dependencies: { + dog: { + version: '1.0.0', + }, + }, + }, + chai: { + version: '1.0.0', + }, + }, + }, + 'should output json representation of dependencies structure' + ) + t.end() + }) + }) + + t.test('extraneous deps', (t) => { + npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: '^1.0.0', + }, + }), + 'package-lock.json': JSON.stringify({ + dependencies: { + foo: { + version: '1.0.0', + requires: { + dog: '^1.0.0', + }, + }, + dog: { + version: '1.0.0', + }, + chai: { + version: '1.0.0', + }, + }, + }), + }) + ls.exec([], (err) => { + t.error(err) // should not error for extraneous + t.same( + jsonParse(result), + { + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: { + version: '1.0.0', + dependencies: { + dog: { + version: '1.0.0', + }, + }, + }, + }, + }, + 'should output json containing no problem info' + ) + t.end() + }) + }) + + t.test('missing deps --long', (t) => { + config.long = true + npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: '^1.0.0', + dog: '^1.0.0', + chai: '^1.0.0', + ipsum: '^1.0.0', + }, + }), + 'package-lock.json': JSON.stringify({ + dependencies: { + foo: { + version: '1.0.0', + requires: { + dog: '^1.0.0', + }, + }, + dog: { + version: '1.0.0', + }, + chai: { + version: '1.0.0', + }, + ipsum: { + version: '1.0.0', + }, + }, + }), + }) + ls.exec([], (err) => { + t.error(err, 'npm ls') + t.match( + jsonParse(result), + { + name: 'test-npm-ls', + version: '1.0.0', + }, + 'should output json containing no problems info' + ) + config.long = false + t.end() + }) + }) + + t.test('with filter arg', (t) => { + npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: '^1.0.0', + chai: '^1.0.0', + }, + }), + 'package-lock.json': JSON.stringify({ + dependencies: { + foo: { + version: '1.0.0', + requires: { + dog: '^1.0.0', + }, + }, + dog: { + version: '1.0.0', + }, + chai: { + version: '1.0.0', + }, + ipsum: { + version: '1.0.0', + }, + }, + }), + }) + ls.exec(['chai'], (err) => { + t.error(err, 'npm ls') + t.same( + jsonParse(result), + { + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + chai: { + version: '1.0.0', + }, + }, + }, + 'should output json contaning only occurrences of filtered by package' + ) + t.equal( + process.exitCode, + 0, + 'should exit with error code 0' + ) + t.end() + }) + }) + + t.test('with filter arg nested dep', (t) => { + npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: '^1.0.0', + chai: '^1.0.0', + }, + }), + 'package-lock.json': JSON.stringify({ + dependencies: { + foo: { + version: '1.0.0', + requires: { + dog: '^1.0.0', + }, + }, + dog: { + version: '1.0.0', + }, + chai: { + version: '1.0.0', + }, + ipsum: { + version: '1.0.0', + }, + }, + }), + }) + ls.exec(['dog'], (err) => { + t.error(err, 'npm ls') + t.same( + jsonParse(result), + { + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: { + version: '1.0.0', + dependencies: { + dog: { + version: '1.0.0', + }, + }, + }, + }, + }, + 'should output json contaning only occurrences of filtered by package' + ) + t.end() + }) + }) + + t.test('with multiple filter args', (t) => { + npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: '^1.0.0', + chai: '^1.0.0', + ipsum: '^1.0.0', + }, + }), + 'package-lock.json': JSON.stringify({ + dependencies: { + foo: { + version: '1.0.0', + requires: { + dog: '^1.0.0', + }, + }, + dog: { + version: '1.0.0', + }, + chai: { + version: '1.0.0', + }, + ipsum: { + version: '1.0.0', + }, + }, + }), + }) + ls.exec(['dog@*', 'chai@1.0.0'], (err) => { + t.error(err, 'npm ls') + t.same( + jsonParse(result), + { + version: '1.0.0', + name: 'test-npm-ls', + dependencies: { + foo: { + version: '1.0.0', + dependencies: { + dog: { + version: '1.0.0', + }, + }, + }, + chai: { + version: '1.0.0', + }, + }, + }, + 'should output json contaning only occurrences of multiple filtered packages and their ancestors' + ) + t.end() + }) + }) + + t.test('with missing filter arg', (t) => { + npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: '^1.0.0', + chai: '^1.0.0', + }, + }), + 'package-lock.json': JSON.stringify({ + dependencies: { + foo: { + version: '1.0.0', + requires: { + dog: '^1.0.0', + }, + }, + dog: { + version: '1.0.0', + }, + chai: { + version: '1.0.0', + }, + }, + }), + }) + ls.exec(['notadep'], (err) => { + t.error(err, 'npm ls') + t.same( + jsonParse(result), + { + name: 'test-npm-ls', + version: '1.0.0', + }, + 'should output json containing no dependencies info' + ) + t.equal( + process.exitCode, + 1, + 'should exit with error code 1' + ) + process.exitCode = 0 + t.end() + }) + }) + + t.test('default --depth value should now be 0', (t) => { + config.all = false + config.depth = undefined + npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: '^1.0.0', + chai: '^1.0.0', + }, + }), + 'package-lock.json': JSON.stringify({ + dependencies: { + foo: { + version: '1.0.0', + requires: { + dog: '^1.0.0', + }, + }, + dog: { + version: '1.0.0', + }, + chai: { + version: '1.0.0', + }, + }, + }), + }) + ls.exec([], (err) => { + t.error(err, 'npm ls') + t.same( + jsonParse(result), + { + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: { + version: '1.0.0', + }, + chai: { + version: '1.0.0', + }, + }, + }, + 'should output json containing only top-level dependencies' + ) + config.all = true + config.depth = Infinity + t.end() + }) + }) + + t.test('--depth=0', (t) => { + config.all = false + config.depth = 0 + npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: '^1.0.0', + chai: '^1.0.0', + }, + }), + 'package-lock.json': JSON.stringify({ + dependencies: { + foo: { + version: '1.0.0', + requires: { + dog: '^1.0.0', + }, + }, + dog: { + version: '1.0.0', + }, + chai: { + version: '1.0.0', + }, + }, + }), + }) + ls.exec([], (err) => { + t.error(err, 'npm ls') + t.same( + jsonParse(result), + { + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: { + version: '1.0.0', + }, + chai: { + version: '1.0.0', + }, + }, + }, + 'should output json containing only top-level dependencies' + ) + config.all = true + config.depth = Infinity + t.end() + }) + }) + + t.test('--depth=1', (t) => { + config.all = false + config.depth = 1 + npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: '^1.0.0', + chai: '^1.0.0', + }, + }), + 'package-lock.json': JSON.stringify({ + dependencies: { + foo: { + version: '1.0.0', + requires: { + dog: '^1.0.0', + }, + }, + dog: { + version: '1.0.0', + }, + chai: { + version: '1.0.0', + }, + }, + }), + }) + ls.exec([], (err) => { + t.error(err, 'npm ls') + t.same( + jsonParse(result), + { + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: { + version: '1.0.0', + dependencies: { + dog: { + version: '1.0.0', + }, + }, + }, + chai: { + version: '1.0.0', + }, + }, + }, + 'should output json containing top-level deps and their deps only' + ) + config.all = true + config.depth = Infinity + t.end() + }) + }) + + t.test('missing/invalid/extraneous', (t) => { + npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: '^2.0.0', + ipsum: '^1.0.0', + }, + }), + 'package-lock.json': JSON.stringify({ + dependencies: { + foo: { + version: '1.0.0', + requires: { + dog: '^1.0.0', + }, + }, + dog: { + version: '1.0.0', + }, + chai: { + version: '1.0.0', + }, + }, + }), + }) + ls.exec([], (err) => { + t.match(err, { code: 'ELSPROBLEMS' }, 'should list dep problems') + t.same( + jsonParse(result), + { + name: 'test-npm-ls', + version: '1.0.0', + problems: [ + 'invalid: foo@1.0.0 {CWD}/tap-testdir-ls-ls---package-lock-only-ls---package-lock-only---json-missing-invalid-extraneous/node_modules/foo', + 'missing: ipsum@^1.0.0, required by test-npm-ls@1.0.0', + ], + dependencies: { + foo: { + version: '1.0.0', + invalid: true, + problems: [ + 'invalid: foo@1.0.0 {CWD}/tap-testdir-ls-ls---package-lock-only-ls---package-lock-only---json-missing-invalid-extraneous/node_modules/foo', + ], + dependencies: { + dog: { + version: '1.0.0', + }, + }, + }, + ipsum: { + required: '^1.0.0', + missing: true, + problems: [ + 'missing: ipsum@^1.0.0, required by test-npm-ls@1.0.0', + ], + }, + }, + }, + 'should output json containing top-level deps and their deps only' + ) + t.end() + }) + }) + + t.test('from lockfile', (t) => { + npm.prefix = t.testdir({ + 'package-lock.json': JSON.stringify({ + name: 'dedupe-lockfile', + version: '1.0.0', + lockfileVersion: 2, + requires: true, + packages: { + '': { + name: 'dedupe-lockfile', + version: '1.0.0', + dependencies: { + '@isaacs/dedupe-tests-a': '1.0.1', + '@isaacs/dedupe-tests-b': '1||2', + }, + }, + 'node_modules/@isaacs/dedupe-tests-a': { + name: '@isaacs/dedupe-tests-a', + version: '1.0.1', + resolved: 'https://registry.npmjs.org/@isaacs/dedupe-tests-a/-/dedupe-tests-a-1.0.1.tgz', + integrity: 'sha512-8AN9lNCcBt5Xeje7fMEEpp5K3rgcAzIpTtAjYb/YMUYu8SbIVF6wz0WqACDVKvpQOUcSfNHZQNLNmue0QSwXOQ==', + dependencies: { + '@isaacs/dedupe-tests-b': '1', + }, + }, + 'node_modules/@isaacs/dedupe-tests-a/node_modules/@isaacs/dedupe-tests-b': { + name: '@isaacs/dedupe-tests-b', + version: '1.0.0', + resolved: 'https://registry.npmjs.org/@isaacs/dedupe-tests-b/-/dedupe-tests-b-1.0.0.tgz', + integrity: 'sha512-3nmvzIb8QL8OXODzipwoV3U8h9OQD9g9RwOPuSBQqjqSg9JZR1CCFOWNsDUtOfmwY8HFUJV9EAZ124uhqVxq+w==', + }, + 'node_modules/@isaacs/dedupe-tests-b': { + name: '@isaacs/dedupe-tests-b', + version: '2.0.0', + resolved: 'https://registry.npmjs.org/@isaacs/dedupe-tests-b/-/dedupe-tests-b-2.0.0.tgz', + integrity: 'sha512-KTYkpRv9EzlmCg4Gsm/jpclWmRYFCXow8GZKJXjK08sIZBlElTZEa5Bw/UQxIvEfcKmWXczSqItD49Kr8Ax4UA==', + }, + }, + dependencies: { + '@isaacs/dedupe-tests-a': { + version: '1.0.1', + resolved: 'https://registry.npmjs.org/@isaacs/dedupe-tests-a/-/dedupe-tests-a-1.0.1.tgz', + integrity: 'sha512-8AN9lNCcBt5Xeje7fMEEpp5K3rgcAzIpTtAjYb/YMUYu8SbIVF6wz0WqACDVKvpQOUcSfNHZQNLNmue0QSwXOQ==', + requires: { + '@isaacs/dedupe-tests-b': '1', + }, + dependencies: { + '@isaacs/dedupe-tests-b': { + version: '1.0.0', + resolved: 'https://registry.npmjs.org/@isaacs/dedupe-tests-b/-/dedupe-tests-b-1.0.0.tgz', + integrity: 'sha512-3nmvzIb8QL8OXODzipwoV3U8h9OQD9g9RwOPuSBQqjqSg9JZR1CCFOWNsDUtOfmwY8HFUJV9EAZ124uhqVxq+w==', + }, + }, + }, + '@isaacs/dedupe-tests-b': { + version: '2.0.0', + resolved: 'https://registry.npmjs.org/@isaacs/dedupe-tests-b/-/dedupe-tests-b-2.0.0.tgz', + integrity: 'sha512-KTYkpRv9EzlmCg4Gsm/jpclWmRYFCXow8GZKJXjK08sIZBlElTZEa5Bw/UQxIvEfcKmWXczSqItD49Kr8Ax4UA==', + }, + }, + }), + 'package.json': JSON.stringify({ + name: 'dedupe-lockfile', + version: '1.0.0', + dependencies: { + '@isaacs/dedupe-tests-a': '1.0.1', + '@isaacs/dedupe-tests-b': '1||2', + }, + }), + }) + ls.exec([], () => { + t.same( + jsonParse(result), + { + version: '1.0.0', + name: 'dedupe-lockfile', + dependencies: { + '@isaacs/dedupe-tests-a': { + version: '1.0.1', + resolved: 'https://registry.npmjs.org/@isaacs/dedupe-tests-a/-/dedupe-tests-a-1.0.1.tgz', + dependencies: { + '@isaacs/dedupe-tests-b': { + version: '1.0.0', + resolved: 'https://registry.npmjs.org/@isaacs/dedupe-tests-b/-/dedupe-tests-b-1.0.0.tgz', + }, + }, + }, + '@isaacs/dedupe-tests-b': { + version: '2.0.0', + resolved: 'https://registry.npmjs.org/@isaacs/dedupe-tests-b/-/dedupe-tests-b-2.0.0.tgz', + }, + }, + }, + 'should output json containing only prod deps' + ) + t.end() + }) + }) + + t.test('using aliases', (t) => { + npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + a: 'npm:b@1.0.0', + }, + }), + 'package-lock.json': JSON.stringify({ + dependencies: { + a: { + version: 'npm:b@1.0.0', + resolved: 'https://localhost:8080/abbrev/-/abbrev-1.0.0.tgz', + }, + }, + }), + }) + ls.exec([], () => { + t.same( + jsonParse(result), + { + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + a: { + version: '1.0.0', + resolved: 'https://localhost:8080/abbrev/-/abbrev-1.0.0.tgz', + }, + }, + }, + 'should output json containing aliases' + ) + t.end() + }) + }) + + t.test('resolved points to git ref', (t) => { + config.long = false + npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + abbrev: 'git+https://github.com/isaacs/abbrev-js.git', + }, + }), + 'package-lock.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + lockfileVersion: 2, + requires: true, + dependencies: { + abbrev: { + version: 'git+ssh://git@github.com/isaacs/abbrev-js.git#b8f3a2fc0c3bb8ffd8b0d0072cc6b5a3667e963c', + from: 'abbrev@git+https://github.com/isaacs/abbrev-js.git', + }, + }, + } + ), + }) + ls.exec([], () => { + t.same( + jsonParse(result), + { + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + abbrev: { + resolved: 'git+ssh://git@github.com/isaacs/abbrev-js.git#b8f3a2fc0c3bb8ffd8b0d0072cc6b5a3667e963c', + }, + }, + }, + 'should output json containing git refs' + ) + t.end() + }) + }) + + t.end() + }) + + t.end() +}) From de820a0213f54bbcd155dff25b05d072d5c4a57a Mon Sep 17 00:00:00 2001 From: Gar Date: Wed, 16 Jun 2021 07:45:55 -0700 Subject: [PATCH 08/20] npm-package-arg@8.1.5 * fix: Make file: URLs (mostly) RFC 8909 compliant --- node_modules/npm-package-arg/npa.js | 104 +++++++++++++++------- node_modules/npm-package-arg/package.json | 2 +- package-lock.json | 14 +-- package.json | 2 +- 4 files changed, 83 insertions(+), 39 deletions(-) diff --git a/node_modules/npm-package-arg/npa.js b/node_modules/npm-package-arg/npa.js index 3a01d4d907192..191befeb5e69d 100644 --- a/node_modules/npm-package-arg/npa.js +++ b/node_modules/npm-package-arg/npa.js @@ -6,7 +6,7 @@ module.exports.Result = Result const url = require('url') const HostedGit = require('hosted-git-info') const semver = require('semver') -const path = require('path') +const path = global.FAKE_WINDOWS ? require('path').win32 : require('path') const validatePackageName = require('validate-npm-package-name') const { homedir } = require('os') @@ -151,42 +151,86 @@ function setGitCommittish (res, committish) { return res } -const isAbsolutePath = /^[/]|^[A-Za-z]:/ - -function resolvePath (where, spec) { - if (isAbsolutePath.test(spec)) - return spec - return path.resolve(where, spec) -} - -function isAbsolute (dir) { - if (dir[0] === '/') - return true - if (/^[A-Za-z]:/.test(dir)) - return true - return false -} - function fromFile (res, where) { if (!where) where = process.cwd() res.type = isFilename.test(res.rawSpec) ? 'file' : 'directory' res.where = where - const spec = res.rawSpec.replace(/\\/g, '/') - .replace(/^file:[/]*([A-Za-z]:)/, '$1') // drive name paths on windows - .replace(/^file:(?:[/]*(~\/|\.*\/|[/]))?/, '$1') - if (/^~[/]/.test(spec)) { - // this is needed for windows and for file:~/foo/bar - res.fetchSpec = resolvePath(homedir(), spec.slice(2)) - res.saveSpec = 'file:' + spec - } else { - res.fetchSpec = resolvePath(where, spec) - if (isAbsolute(spec)) - res.saveSpec = 'file:' + spec - else - res.saveSpec = 'file:' + path.relative(where, res.fetchSpec) + // always put the '/' on where when resolving urls, or else + // file:foo from /path/to/bar goes to /path/to/foo, when we want + // it to be /path/to/foo/bar + + let specUrl + let resolvedUrl + const prefix = (!/^file:/.test(res.rawSpec) ? 'file:' : '') + const rawWithPrefix = prefix + res.rawSpec + let rawNoPrefix = rawWithPrefix.replace(/^file:/, '') + try { + resolvedUrl = new url.URL(rawWithPrefix, `file://${path.resolve(where)}/`) + specUrl = new url.URL(rawWithPrefix) + } catch (originalError) { + const er = new Error('Invalid file: URL, must comply with RFC 8909') + throw Object.assign(er, { + raw: res.rawSpec, + spec: res, + where, + originalError, + }) + } + + // environment switch for testing + if (process.env.NPM_PACKAGE_ARG_8909_STRICT !== '1') { + // XXX backwards compatibility lack of compliance with 8909 + // Remove when we want a breaking change to come into RFC compliance. + if (resolvedUrl.host && resolvedUrl.host !== 'localhost') { + const rawSpec = res.rawSpec.replace(/^file:\/\//, 'file:///') + resolvedUrl = new url.URL(rawSpec, `file://${path.resolve(where)}/`) + specUrl = new url.URL(rawSpec) + rawNoPrefix = rawSpec.replace(/^file:/, '') + } + // turn file:/../foo into file:../foo + if (/^\/\.\.?(\/|$)/.test(rawNoPrefix)) { + const rawSpec = res.rawSpec.replace(/^file:\//, 'file:') + resolvedUrl = new url.URL(rawSpec, `file://${path.resolve(where)}/`) + specUrl = new url.URL(rawSpec) + rawNoPrefix = rawSpec.replace(/^file:/, '') + } + // XXX end 8909 violation backwards compatibility section } + + // file:foo - relative url to ./foo + // file:/foo - absolute path /foo + // file:///foo - absolute path to /foo, no authority host + // file://localhost/foo - absolute path to /foo, on localhost + // file://foo - absolute path to / on foo host (error!) + if (resolvedUrl.host && resolvedUrl.host !== 'localhost') { + const msg = `Invalid file: URL, must be absolute if // present` + throw Object.assign(new Error(msg), { + raw: res.rawSpec, + parsed: resolvedUrl, + }) + } + + // turn /C:/blah into just C:/blah on windows + let specPath = decodeURIComponent(specUrl.pathname) + let resolvedPath = decodeURIComponent(resolvedUrl.pathname) + if (isWindows) { + specPath = specPath.replace(/^\/+([a-z]:\/)/i, '$1') + resolvedPath = resolvedPath.replace(/^\/+([a-z]:\/)/i, '$1') + } + + // replace ~ with homedir, but keep the ~ in the saveSpec + // otherwise, make it relative to where param + if (/^\/~(\/|$)/.test(specPath)) { + res.saveSpec = `file:${specPath.substr(1)}` + resolvedPath = path.resolve(homedir(), specPath.substr(3)) + } else if (!path.isAbsolute(rawNoPrefix)) + res.saveSpec = `file:${path.relative(where, resolvedPath)}` + else + res.saveSpec = `file:${path.resolve(resolvedPath)}` + + res.fetchSpec = path.resolve(where, resolvedPath) return res } diff --git a/node_modules/npm-package-arg/package.json b/node_modules/npm-package-arg/package.json index a237928943ccb..bf5f597e6d8df 100644 --- a/node_modules/npm-package-arg/package.json +++ b/node_modules/npm-package-arg/package.json @@ -1,6 +1,6 @@ { "name": "npm-package-arg", - "version": "8.1.4", + "version": "8.1.5", "description": "Parse the things that can be arguments to `npm install`", "main": "npa.js", "directories": { diff --git a/package-lock.json b/package-lock.json index 0c74e93d0100a..edcbe26ae3b58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -125,7 +125,7 @@ "node-gyp": "^7.1.2", "nopt": "^5.0.0", "npm-audit-report": "^2.1.5", - "npm-package-arg": "^8.1.4", + "npm-package-arg": "^8.1.5", "npm-pick-manifest": "^6.1.1", "npm-profile": "^5.0.3", "npm-registry-fetch": "^11.0.0", @@ -5432,9 +5432,9 @@ "inBundle": true }, "node_modules/npm-package-arg": { - "version": "8.1.4", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.1.4.tgz", - "integrity": "sha512-xLokoCFqj/rPdr3LvcdDL6Kj6ipXGEDHD/QGpzwU6/pibYUOXmp5DBmg76yukFyx4ZDbrXNOTn+BPyd8TD4Jlw==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.1.5.tgz", + "integrity": "sha512-LhgZrg0n0VgvzVdSm1oiZworPbTxYHUJCgtsJW8mGvlDpxTM1vSJc3m5QZeUkhAHIzbz3VCHd/R4osi1L1Tg/Q==", "inBundle": true, "dependencies": { "hosted-git-info": "^4.0.1", @@ -14335,9 +14335,9 @@ "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" }, "npm-package-arg": { - "version": "8.1.4", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.1.4.tgz", - "integrity": "sha512-xLokoCFqj/rPdr3LvcdDL6Kj6ipXGEDHD/QGpzwU6/pibYUOXmp5DBmg76yukFyx4ZDbrXNOTn+BPyd8TD4Jlw==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.1.5.tgz", + "integrity": "sha512-LhgZrg0n0VgvzVdSm1oiZworPbTxYHUJCgtsJW8mGvlDpxTM1vSJc3m5QZeUkhAHIzbz3VCHd/R4osi1L1Tg/Q==", "requires": { "hosted-git-info": "^4.0.1", "semver": "^7.3.4", diff --git a/package.json b/package.json index 9ae066fb4eb2b..42e93f50b2891 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "node-gyp": "^7.1.2", "nopt": "^5.0.0", "npm-audit-report": "^2.1.5", - "npm-package-arg": "^8.1.4", + "npm-package-arg": "^8.1.5", "npm-pick-manifest": "^6.1.1", "npm-profile": "^5.0.3", "npm-registry-fetch": "^11.0.0", From d16ee452a4a034caada4e9b96faf5c453a658876 Mon Sep 17 00:00:00 2001 From: Gar Date: Wed, 16 Jun 2021 08:31:49 -0700 Subject: [PATCH 09/20] chore(tests): use path.resolve npm-package-arg is doing things more properly now so the tests should reflect that PR-URL: https://github.com/npm/cli/pull/3426 Credit: @wraithgar Close: #3426 Reviewed-by: @ruyadorno --- test/lib/diff.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/lib/diff.js b/test/lib/diff.js index 993dfa4d60718..2fb38c9b127e4 100644 --- a/test/lib/diff.js +++ b/test/lib/diff.js @@ -549,12 +549,13 @@ t.test('single arg', t => { t.test('dir spec type', t => { t.plan(2) + const otherPath = resolve('/path/to/other-dir') libnpmdiff = async ([a, b], opts) => { - t.equal(a, 'file:/path/to/other-dir', 'should target dir') + t.equal(a, `file:${otherPath}`, 'should target dir') t.equal(b, `file:${fooPath}`, 'should compare to cwd') } - config.diff = ['/path/to/other-dir'] + config.diff = [otherPath] diff.exec([], err => { if (err) throw err From 16a95c64731609c69630c17c45b16edb53ee81b2 Mon Sep 17 00:00:00 2001 From: Gar Date: Wed, 16 Jun 2021 09:39:05 -0700 Subject: [PATCH 10/20] @npmcli/arborist@2.6.3 * fix(inventory) handle old and british forms of 'license' * fix: removes [_complete] check to apply correct metadata * ensure node.fsParent is not set to node itself * fix extraneous deps on load-actual --- .../arborist/lib/arborist/build-ideal-tree.js | 14 ++++---- .../@npmcli/arborist/lib/arborist/index.js | 2 +- .../arborist/lib/arborist/load-actual.js | 20 ++++++++--- .../@npmcli/arborist/lib/arborist/reify.js | 35 ------------------- .../@npmcli/arborist/lib/audit-report.js | 2 +- .../@npmcli/arborist/lib/calc-dep-flags.js | 16 +++++++-- .../@npmcli/arborist/lib/inventory.js | 18 +++++++++- node_modules/@npmcli/arborist/lib/node.js | 18 ++++++++-- node_modules/@npmcli/arborist/lib/proc-log.js | 21 ----------- .../@npmcli/arborist/lib/shrinkwrap.js | 2 +- node_modules/@npmcli/arborist/lib/tracker.js | 2 +- node_modules/@npmcli/arborist/package.json | 3 +- package-lock.json | 16 +++++---- package.json | 2 +- 14 files changed, 87 insertions(+), 84 deletions(-) delete mode 100644 node_modules/@npmcli/arborist/lib/proc-log.js diff --git a/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js b/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js index 73a6f667e35db..5db11eb3832eb 100644 --- a/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js +++ b/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js @@ -7,7 +7,7 @@ const semver = require('semver') const promiseCallLimit = require('promise-call-limit') const getPeerSet = require('../peer-set.js') const realpath = require('../../lib/realpath.js') -const { resolve } = require('path') +const { resolve, dirname } = require('path') const { promisify } = require('util') const treeCheck = require('../tree-check.js') const readdir = promisify(require('readdir-scoped-modules')) @@ -661,7 +661,7 @@ module.exports = cls => class IdealTreeBuilder extends cls { const ancient = meta.ancientLockfile const old = meta.loadedFromDisk && !(meta.originalLockfileVersion >= 2) - if (inventory.size === 0 || !ancient && !(old && this[_complete])) + if (inventory.size === 0 || !ancient && !old) return // if the lockfile is from node v5 or earlier, then we'll have to reload @@ -688,10 +688,12 @@ This is a one-time fix-up, please be patient... this.log.silly('inflate', node.location) const { resolved, version, path, name, location, integrity } = node // don't try to hit the registry for linked deps - const useResolved = !version || - resolved && resolved.startsWith('file:') - const id = useResolved ? resolved : version - const spec = npa.resolve(name, id, path) + const useResolved = resolved && ( + !version || resolved.startsWith('file:') + ) + const id = useResolved ? resolved + : version || `file:${node.path}` + const spec = npa.resolve(name, id, dirname(path)) const sloc = location.substr('node_modules/'.length) const t = `idealTree:inflate:${sloc}` this.addTracker(t) diff --git a/node_modules/@npmcli/arborist/lib/arborist/index.js b/node_modules/@npmcli/arborist/lib/arborist/index.js index b119d3117bc23..94501cae12c84 100644 --- a/node_modules/@npmcli/arborist/lib/arborist/index.js +++ b/node_modules/@npmcli/arborist/lib/arborist/index.js @@ -28,7 +28,7 @@ const {resolve} = require('path') const {homedir} = require('os') -const procLog = require('../proc-log.js') +const procLog = require('proc-log') const { saveTypeMap } = require('../add-rm-pkg-deps.js') const mixins = [ diff --git a/node_modules/@npmcli/arborist/lib/arborist/load-actual.js b/node_modules/@npmcli/arborist/lib/arborist/load-actual.js index d9e7fb46d6df9..9fca7d6425da0 100644 --- a/node_modules/@npmcli/arborist/lib/arborist/load-actual.js +++ b/node_modules/@npmcli/arborist/lib/arborist/load-actual.js @@ -22,6 +22,7 @@ const _loadFSTree = Symbol('loadFSTree') const _loadFSChildren = Symbol('loadFSChildren') const _findMissingEdges = Symbol('findMissingEdges') const _findFSParents = Symbol('findFSParents') +const _resetDepFlags = Symbol('resetDepFlags') const _actualTreeLoaded = Symbol('actualTreeLoaded') const _rpcache = Symbol.for('realpathCache') @@ -74,6 +75,19 @@ module.exports = cls => class ActualLoader extends cls { this[_topNodes] = new Set() } + [_resetDepFlags] (tree, root) { + // reset all deps to extraneous prior to recalc + if (!root) { + for (const node of tree.inventory.values()) + node.extraneous = true + } + + // only reset root flags if we're not re-rooting, + // otherwise leave as-is + calcDepFlags(tree, !root) + return tree + } + // public method async loadActual (options = {}) { // allow the user to set options on the ctor as well. @@ -88,6 +102,7 @@ module.exports = cls => class ActualLoader extends cls { return this.actualTree ? this.actualTree : this[_actualTreePromise] ? this[_actualTreePromise] : this[_actualTreePromise] = this[_loadActual](options) + .then(tree => this[_resetDepFlags](tree, options.root)) .then(tree => this.actualTree = treeCheck(tree)) } @@ -152,8 +167,7 @@ module.exports = cls => class ActualLoader extends cls { root: this[_actualTree], }) await this[_loadWorkspaces](this[_actualTree]) - if (this[_actualTree].workspaces && this[_actualTree].workspaces.size) - calcDepFlags(this[_actualTree], !root) + this[_transplant](root) return this[_actualTree] } @@ -178,8 +192,6 @@ module.exports = cls => class ActualLoader extends cls { dependencies[name] = dependencies[name] || '*' actualRoot.package = { ...actualRoot.package, dependencies } } - // only reset root flags if we're not re-rooting, otherwise leave as-is - calcDepFlags(this[_actualTree], !root) return this[_actualTree] } diff --git a/node_modules/@npmcli/arborist/lib/arborist/reify.js b/node_modules/@npmcli/arborist/lib/arborist/reify.js index 58dc7c68a4a40..55360538b901a 100644 --- a/node_modules/@npmcli/arborist/lib/arborist/reify.js +++ b/node_modules/@npmcli/arborist/lib/arborist/reify.js @@ -2,7 +2,6 @@ const onExit = require('../signal-handling.js') const pacote = require('pacote') -const rpj = require('read-package-json-fast') const AuditReport = require('../audit-report.js') const {subset, intersects} = require('semver') const npa = require('npm-package-arg') @@ -57,7 +56,6 @@ const _extractOrLink = Symbol('extractOrLink') const _checkBins = Symbol.for('checkBins') const _symlink = Symbol('symlink') const _warnDeprecated = Symbol('warnDeprecated') -const _loadAncientPackageDetails = Symbol('loadAncientPackageDetails') const _loadBundlesAndUpdateTrees = Symbol.for('loadBundlesAndUpdateTrees') const _submitQuickAudit = Symbol('submitQuickAudit') const _awaitQuickAudit = Symbol('awaitQuickAudit') @@ -522,7 +520,6 @@ module.exports = cls => class Reifier extends cls { await this[_checkBins](node) await this[_extractOrLink](node) await this[_warnDeprecated](node) - await this[_loadAncientPackageDetails](node) }) return this[_handleOptionalFailure](node, p) @@ -583,32 +580,6 @@ module.exports = cls => class Reifier extends cls { this.log.warn('deprecated', `${_id}: ${deprecated}`) } - async [_loadAncientPackageDetails] (node, forceReload = false) { - // If we're loading from a v1 lockfile, load details from the package.json - // that weren't recorded in the old format. - const {meta} = this.idealTree - const ancient = meta.ancientLockfile - const old = meta.loadedFromDisk && !(meta.originalLockfileVersion >= 2) - - // already replaced with the manifest if it's truly ancient - if (node.path && (forceReload || (old && !ancient))) { - // XXX should have a shared location where package.json is read, - // so we don't ever read the same pj more than necessary. - let pkg - try { - pkg = await rpj(node.path + '/package.json') - } catch (err) {} - - if (pkg) { - node.package.bin = pkg.bin - node.package.os = pkg.os - node.package.cpu = pkg.cpu - node.package.engines = pkg.engines - meta.add(node) - } - } - } - // if the node is optional, then the failure of the promise is nonfatal // just add it and its optional set to the trash list. [_handleOptionalFailure] (node, p) { @@ -1079,12 +1050,6 @@ module.exports = cls => class Reifier extends cls { const { meta } = this.idealTree - // might have to update metadata for bins and stuff that gets lost - if (meta.loadedFromDisk && !(meta.originalLockfileVersion >= 2)) { - for (const node of this.idealTree.inventory.values()) - await this[_loadAncientPackageDetails](node, true) - } - return meta.save(saveOpt) } diff --git a/node_modules/@npmcli/arborist/lib/audit-report.js b/node_modules/@npmcli/arborist/lib/audit-report.js index 139a7aefd2489..8f7d6546d64f4 100644 --- a/node_modules/@npmcli/arborist/lib/audit-report.js +++ b/node_modules/@npmcli/arborist/lib/audit-report.js @@ -12,7 +12,7 @@ const _fixAvailable = Symbol('fixAvailable') const _checkTopNode = Symbol('checkTopNode') const _init = Symbol('init') const _omit = Symbol('omit') -const procLog = require('./proc-log.js') +const procLog = require('proc-log') const fetch = require('npm-registry-fetch') diff --git a/node_modules/@npmcli/arborist/lib/calc-dep-flags.js b/node_modules/@npmcli/arborist/lib/calc-dep-flags.js index d6ae266db3bb0..21d8ddcf7b442 100644 --- a/node_modules/@npmcli/arborist/lib/calc-dep-flags.js +++ b/node_modules/@npmcli/arborist/lib/calc-dep-flags.js @@ -22,6 +22,11 @@ const calcDepFlagsStep = (node) => { // Since we're only walking through deps that are not already flagged // as non-dev/non-optional, it's typically a very shallow traversal node.extraneous = false + resetParents(node, 'extraneous') + resetParents(node, 'dev') + resetParents(node, 'peer') + resetParents(node, 'devOptional') + resetParents(node, 'optional') // for links, map their hierarchy appropriately if (node.target) { @@ -29,8 +34,7 @@ const calcDepFlagsStep = (node) => { node.target.optional = node.optional node.target.devOptional = node.devOptional node.target.peer = node.peer - node.target.extraneous = false - node = node.target + return calcDepFlagsStep(node.target) } node.edgesOut.forEach(({peer, optional, dev, to}) => { @@ -71,6 +75,14 @@ const calcDepFlagsStep = (node) => { return node } +const resetParents = (node, flag) => { + if (node[flag]) + return + + for (let p = node; p && (p === node || p[flag]); p = p.resolveParent) + p[flag] = false +} + // typically a short walk, since it only traverses deps that // have the flag set. const unsetFlag = (node, flag) => { diff --git a/node_modules/@npmcli/arborist/lib/inventory.js b/node_modules/@npmcli/arborist/lib/inventory.js index 7578291885223..a4ae11c2ab41e 100644 --- a/node_modules/@npmcli/arborist/lib/inventory.js +++ b/node_modules/@npmcli/arborist/lib/inventory.js @@ -7,6 +7,20 @@ const _index = Symbol('_index') const defaultKeys = ['name', 'license', 'funding', 'realpath', 'packageName'] const { hasOwnProperty } = Object.prototype const debug = require('./debug.js') + +// handling for the outdated "licenses" array, just pick the first one +// also support the alternative spelling "licence" +const getLicense = pkg => { + if (pkg) { + const lic = pkg.license || pkg.licence + if (lic) + return lic + const lics = pkg.licenses || pkg.licences + if (Array.isArray(lics)) + return lics[0] + } +} + class Inventory extends Map { constructor (opt = {}) { const { primary, keys } = opt @@ -56,7 +70,9 @@ class Inventory extends Map { for (const [key, map] of this[_index].entries()) { // if the node has the value, but it's false, then use that const val_ = hasOwnProperty.call(node, key) ? node[key] - : node[key] || (node.package && node.package[key]) + : key === 'license' ? getLicense(node.package) + : node[key] ? node[key] + : node.package && node.package[key] const val = typeof val_ === 'string' ? val_ : !val_ || typeof val_ !== 'object' ? val_ : key === 'license' ? val_.type diff --git a/node_modules/@npmcli/arborist/lib/node.js b/node_modules/@npmcli/arborist/lib/node.js index 1683c7049504e..c21bc46cfb539 100644 --- a/node_modules/@npmcli/arborist/lib/node.js +++ b/node_modules/@npmcli/arborist/lib/node.js @@ -547,6 +547,8 @@ class Node { // try to find our parent/fsParent in the new root inventory for (const p of walkUp(dirname(this.path))) { + if (p === this.path) + continue const ploc = relpath(root.realpath, p) const parent = root.inventory.get(ploc) if (parent) { @@ -783,7 +785,13 @@ class Node { } get fsParent () { - return this[_fsParent] + const parent = this[_fsParent] + /* istanbul ignore next - should be impossible */ + debug(() => { + if (parent === this) + throw new Error('node set to its own fsParent') + }) + return parent } set fsParent (fsParent) { @@ -1009,7 +1017,13 @@ class Node { } get parent () { - return this[_parent] + const parent = this[_parent] + /* istanbul ignore next - should be impossible */ + debug(() => { + if (parent === this) + throw new Error('node set to its own parent') + }) + return parent } // This setter keeps everything in order when we move a node from diff --git a/node_modules/@npmcli/arborist/lib/proc-log.js b/node_modules/@npmcli/arborist/lib/proc-log.js deleted file mode 100644 index 52e0e466798ee..0000000000000 --- a/node_modules/@npmcli/arborist/lib/proc-log.js +++ /dev/null @@ -1,21 +0,0 @@ -// default logger. -// emits 'log' events on the process -const LEVELS = [ - 'notice', - 'error', - 'warn', - 'info', - 'verbose', - 'http', - 'silly', - 'pause', - 'resume', -] - -const log = level => (...args) => process.emit('log', level, ...args) - -const logger = {} -for (const level of LEVELS) - logger[level] = log(level) - -module.exports = logger diff --git a/node_modules/@npmcli/arborist/lib/shrinkwrap.js b/node_modules/@npmcli/arborist/lib/shrinkwrap.js index 0a19ef93005ad..9fb0528db497c 100644 --- a/node_modules/@npmcli/arborist/lib/shrinkwrap.js +++ b/node_modules/@npmcli/arborist/lib/shrinkwrap.js @@ -32,7 +32,7 @@ const mismatch = (a, b) => a && b && a !== b // After calling this.commit(), any nodes not present in the tree will have // been removed from the shrinkwrap data as well. -const procLog = require('./proc-log.js') +const procLog = require('proc-log') const YarnLock = require('./yarn-lock.js') const {promisify} = require('util') const rimraf = promisify(require('rimraf')) diff --git a/node_modules/@npmcli/arborist/lib/tracker.js b/node_modules/@npmcli/arborist/lib/tracker.js index 47267872ce780..aefd5fe1bbf58 100644 --- a/node_modules/@npmcli/arborist/lib/tracker.js +++ b/node_modules/@npmcli/arborist/lib/tracker.js @@ -1,6 +1,6 @@ const _progress = Symbol('_progress') const _onError = Symbol('_onError') -const procLog = require('./proc-log.js') +const procLog = require('proc-log') module.exports = cls => class Tracker extends cls { constructor (options = {}) { diff --git a/node_modules/@npmcli/arborist/package.json b/node_modules/@npmcli/arborist/package.json index 7c2622f49e93e..bd27e4bbffa20 100644 --- a/node_modules/@npmcli/arborist/package.json +++ b/node_modules/@npmcli/arborist/package.json @@ -1,6 +1,6 @@ { "name": "@npmcli/arborist", - "version": "2.6.2", + "version": "2.6.3", "description": "Manage node_modules trees", "dependencies": { "@npmcli/installed-package-contents": "^1.0.7", @@ -22,6 +22,7 @@ "npm-registry-fetch": "^11.0.0", "pacote": "^11.2.6", "parse-conflict-json": "^1.1.1", + "proc-log": "^1.0.0", "promise-all-reject-late": "^1.0.0", "promise-call-limit": "^1.0.1", "read-package-json-fast": "^2.0.2", diff --git a/package-lock.json b/package-lock.json index edcbe26ae3b58..e49153959c919 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,7 +82,7 @@ "packages/*" ], "dependencies": { - "@npmcli/arborist": "^2.6.1", + "@npmcli/arborist": "^2.6.3", "@npmcli/ci-detect": "^1.2.0", "@npmcli/config": "^2.2.0", "@npmcli/run-script": "^1.8.5", @@ -733,9 +733,9 @@ } }, "node_modules/@npmcli/arborist": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-2.6.2.tgz", - "integrity": "sha512-CAo0HSziRdlpGUUheERmOrADnKHfBYpLAl/HmWGwGCtWKB3BCxfgb0rJ7MsFg38wy7YF3+fDs7R9dMVCH89K/A==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-2.6.3.tgz", + "integrity": "sha512-R8U2dZ8+jeE7go+qNU4Mt6aiXyBu3mM75iRIugNCA4P0OWlsLOpuDPPhsaRcOVbtXheOGZXrqe36qP1g+M68KQ==", "inBundle": true, "dependencies": { "@npmcli/installed-package-contents": "^1.0.7", @@ -757,6 +757,7 @@ "npm-registry-fetch": "^11.0.0", "pacote": "^11.2.6", "parse-conflict-json": "^1.1.1", + "proc-log": "^1.0.0", "promise-all-reject-late": "^1.0.0", "promise-call-limit": "^1.0.1", "read-package-json-fast": "^2.0.2", @@ -10856,9 +10857,9 @@ "dev": true }, "@npmcli/arborist": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-2.6.2.tgz", - "integrity": "sha512-CAo0HSziRdlpGUUheERmOrADnKHfBYpLAl/HmWGwGCtWKB3BCxfgb0rJ7MsFg38wy7YF3+fDs7R9dMVCH89K/A==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-2.6.3.tgz", + "integrity": "sha512-R8U2dZ8+jeE7go+qNU4Mt6aiXyBu3mM75iRIugNCA4P0OWlsLOpuDPPhsaRcOVbtXheOGZXrqe36qP1g+M68KQ==", "requires": { "@npmcli/installed-package-contents": "^1.0.7", "@npmcli/map-workspaces": "^1.0.2", @@ -10879,6 +10880,7 @@ "npm-registry-fetch": "^11.0.0", "pacote": "^11.2.6", "parse-conflict-json": "^1.1.1", + "proc-log": "^1.0.0", "promise-all-reject-late": "^1.0.0", "promise-call-limit": "^1.0.1", "read-package-json-fast": "^2.0.2", diff --git a/package.json b/package.json index 42e93f50b2891..851c4a2c6924a 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "./package.json": "./package.json" }, "dependencies": { - "@npmcli/arborist": "^2.6.1", + "@npmcli/arborist": "^2.6.3", "@npmcli/ci-detect": "^1.2.0", "@npmcli/config": "^2.2.0", "@npmcli/run-script": "^1.8.5", From dcc13662c1d3e22eaf392647a9cddbb5b0710d24 Mon Sep 17 00:00:00 2001 From: Gar Date: Tue, 15 Jun 2021 13:24:56 -0700 Subject: [PATCH 11/20] fix(config): update link definition The behavior for installing global versions if found has never been part of npm@7. PR-URL: https://github.com/npm/cli/pull/3418 Credit: @wraithgar Close: #3418 Reviewed-by: @lukekarrys --- docs/content/commands/npm-ls.md | 13 +------------ docs/content/using-npm/config.md | 13 +------------ lib/utils/config/definitions.js | 14 ++------------ .../test/lib/utils/config/describe-all.js.test.cjs | 13 +------------ 4 files changed, 5 insertions(+), 48 deletions(-) diff --git a/docs/content/commands/npm-ls.md b/docs/content/commands/npm-ls.md index 3c662176327bf..21a3c3eec8025 100644 --- a/docs/content/commands/npm-ls.md +++ b/docs/content/commands/npm-ls.md @@ -155,18 +155,7 @@ variable will be set to `'production'` for all lifecycle scripts. * Default: false * Type: Boolean -If true, then local installs will link if there is a suitable globally -installed package. - -Note that this means that local installs can cause things to be installed -into the global space at the same time. The link is only done if one of the -two conditions are met: - -* The package is not already installed globally, or -* the globally installed version is identical to the version that is being - installed locally. - -When used with `npm ls`, only show packages that are linked. +Used with `npm ls`, limiting output to only those packages that are linked. #### `unicode` diff --git a/docs/content/using-npm/config.md b/docs/content/using-npm/config.md index 44b79a801f15e..95f89a5848d23 100644 --- a/docs/content/using-npm/config.md +++ b/docs/content/using-npm/config.md @@ -743,18 +743,7 @@ Use of `legacy-peer-deps` is not recommended, as it will not enforce the * Default: false * Type: Boolean -If true, then local installs will link if there is a suitable globally -installed package. - -Note that this means that local installs can cause things to be installed -into the global space at the same time. The link is only done if one of the -two conditions are met: - -* The package is not already installed globally, or -* the globally installed version is identical to the version that is being - installed locally. - -When used with `npm ls`, only show packages that are linked. +Used with `npm ls`, limiting output to only those packages that are linked. #### `local-address` diff --git a/lib/utils/config/definitions.js b/lib/utils/config/definitions.js index b6f7c84317c6a..ce1ea766c9404 100644 --- a/lib/utils/config/definitions.js +++ b/lib/utils/config/definitions.js @@ -1086,18 +1086,8 @@ define('link', { default: false, type: Boolean, description: ` - If true, then local installs will link if there is a suitable globally - installed package. - - Note that this means that local installs can cause things to be installed - into the global space at the same time. The link is only done if one of - the two conditions are met: - - * The package is not already installed globally, or - * the globally installed version is identical to the version that is - being installed locally. - - When used with \`npm ls\`, only show packages that are linked. + Used with \`npm ls\`, limiting output to only those packages that are + linked. `, }) diff --git a/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs b/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs index b7bcca8d539a1..9e3ba4d1af050 100644 --- a/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs +++ b/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs @@ -622,18 +622,7 @@ Use of \`legacy-peer-deps\` is not recommended, as it will not enforce the * Default: false * Type: Boolean -If true, then local installs will link if there is a suitable globally -installed package. - -Note that this means that local installs can cause things to be installed -into the global space at the same time. The link is only done if one of the -two conditions are met: - -* The package is not already installed globally, or -* the globally installed version is identical to the version that is being - installed locally. - -When used with \`npm ls\`, only show packages that are linked. +Used with \`npm ls\`, limiting output to only those packages that are linked. #### \`local-address\` From d341bd86ce05fabe44f3be5888ba2611b61914b4 Mon Sep 17 00:00:00 2001 From: Gar Date: Thu, 17 Jun 2021 07:06:05 -0700 Subject: [PATCH 12/20] make-fetch-happen@9.0.3 * fix: implement cache modes correctly --- .../make-fetch-happen/lib/cache/entry.js | 6 ++++++ .../make-fetch-happen/lib/cache/index.js | 17 ++++++++--------- node_modules/make-fetch-happen/package.json | 2 +- package-lock.json | 14 +++++++------- package.json | 2 +- 5 files changed, 23 insertions(+), 18 deletions(-) diff --git a/node_modules/make-fetch-happen/lib/cache/entry.js b/node_modules/make-fetch-happen/lib/cache/entry.js index 0df006fe34a3f..41f8a3d215ee1 100644 --- a/node_modules/make-fetch-happen/lib/cache/entry.js +++ b/node_modules/make-fetch-happen/lib/cache/entry.js @@ -145,6 +145,12 @@ class CacheEntry { return } + // a cache mode of 'reload' means to behave as though we have no cache + // on the way to the network. return undefined to allow cacheFetch to + // create a brand new request no matter what. + if (options.cache === 'reload') + return + // find the specific entry that satisfies the request let match for (const entry of matches) { diff --git a/node_modules/make-fetch-happen/lib/cache/index.js b/node_modules/make-fetch-happen/lib/cache/index.js index 00df31dd15023..cca93d9b4eb5d 100644 --- a/node_modules/make-fetch-happen/lib/cache/index.js +++ b/node_modules/make-fetch-happen/lib/cache/index.js @@ -7,7 +7,7 @@ const cacheFetch = async (request, options) => { // try to find a cached entry that satisfies this request const entry = await CacheEntry.find(request, options) if (!entry) { - // no cached result, if the cache mode is only-if-cached that's a failure + // no cached result, if the cache mode is 'only-if-cached' that's a failure if (options.cache === 'only-if-cached') throw new NotCachedError(request.url) @@ -17,22 +17,21 @@ const cacheFetch = async (request, options) => { return entry.store('miss') } - // we have a cached response that satisfies this request, however - // if the cache mode is reload the user explicitly wants us to revalidate - if (options.cache === 'reload') + // we have a cached response that satisfies this request, however if the cache + // mode is 'no-cache' then we send the revalidation request no matter what + if (options.cache === 'no-cache') return entry.revalidate(request, options) - // if the cache mode is either force-cache or only-if-cached we will only - // respond with a cached entry, even if it's stale. set the status to the - // appropriate value based on whether revalidation is needed and respond - // from the cache + // if the cached entry is not stale, or if the cache mode is 'force-cache' or + // 'only-if-cached' we can respond with the cached entry. set the status + // based on the result of needsRevalidation and respond const _needsRevalidation = entry.policy.needsRevalidation(request) if (options.cache === 'force-cache' || options.cache === 'only-if-cached' || !_needsRevalidation) return entry.respond(request.method, options, _needsRevalidation ? 'stale' : 'hit') - // cache entry might be stale, revalidate it and return a response + // if we got here, the cache entry is stale so revalidate it return entry.revalidate(request, options) } diff --git a/node_modules/make-fetch-happen/package.json b/node_modules/make-fetch-happen/package.json index af97a161c6088..44330998bb02f 100644 --- a/node_modules/make-fetch-happen/package.json +++ b/node_modules/make-fetch-happen/package.json @@ -1,6 +1,6 @@ { "name": "make-fetch-happen", - "version": "9.0.2", + "version": "9.0.3", "description": "Opinionated, caching, retrying fetch client", "main": "lib/index.js", "files": [ diff --git a/package-lock.json b/package-lock.json index e49153959c919..88b1797e90ab9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -116,7 +116,7 @@ "libnpmsearch": "^3.1.1", "libnpmteam": "^2.0.3", "libnpmversion": "^1.2.1", - "make-fetch-happen": "^9.0.1", + "make-fetch-happen": "^9.0.3", "minipass": "^3.1.3", "minipass-pipeline": "^1.2.4", "mkdirp": "^1.0.4", @@ -4953,9 +4953,9 @@ } }, "node_modules/make-fetch-happen": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.0.2.tgz", - "integrity": "sha512-UkAWAuXPXSSlVviTjH2We20mtj1NnZW2Qq/oTY2dyMbRQ5CR3Xed3akCDMnM7j6axrMY80lhgM7loNE132PfAw==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.0.3.tgz", + "integrity": "sha512-uZ/9Cf2vKqsSWZyXhZ9wHHyckBrkntgbnqV68Bfe8zZenlf7D6yuGMXvHZQ+jSnzPkjosuNP1HGasj1J4h8OlQ==", "inBundle": true, "dependencies": { "agentkeepalive": "^4.1.3", @@ -13989,9 +13989,9 @@ } }, "make-fetch-happen": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.0.2.tgz", - "integrity": "sha512-UkAWAuXPXSSlVviTjH2We20mtj1NnZW2Qq/oTY2dyMbRQ5CR3Xed3akCDMnM7j6axrMY80lhgM7loNE132PfAw==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.0.3.tgz", + "integrity": "sha512-uZ/9Cf2vKqsSWZyXhZ9wHHyckBrkntgbnqV68Bfe8zZenlf7D6yuGMXvHZQ+jSnzPkjosuNP1HGasj1J4h8OlQ==", "requires": { "agentkeepalive": "^4.1.3", "cacache": "^15.2.0", diff --git a/package.json b/package.json index 851c4a2c6924a..a8f7047924913 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "libnpmsearch": "^3.1.1", "libnpmteam": "^2.0.3", "libnpmversion": "^1.2.1", - "make-fetch-happen": "^9.0.1", + "make-fetch-happen": "^9.0.3", "minipass": "^3.1.3", "minipass-pipeline": "^1.2.4", "mkdirp": "^1.0.4", From b19e56c2e54c035518165470c10480201cefa997 Mon Sep 17 00:00:00 2001 From: Ruy Adorno Date: Wed, 16 Jun 2021 15:52:57 -0400 Subject: [PATCH 13/20] fix(ls): respect prod config for workspaces `npm ls --prod` is currently not omitting devDependencies for configured workspaces, this changes it by properly checking for the tweaked `currentDepth` value instead of root check that was in place. Fixes: https://github.com/npm/cli/issues/3382 PR-URL: https://github.com/npm/cli/pull/3429 Credit: @ruyadorno Close: #3429 Reviewed-by: @wraithgar --- lib/ls.js | 8 +++++--- tap-snapshots/test/lib/ls.js.test.cjs | 17 +++++++++++++++++ test/lib/ls.js | 21 +++++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/lib/ls.js b/lib/ls.js index b425bd620b38e..f146928a96b41 100644 --- a/lib/ls.js +++ b/lib/ls.js @@ -141,6 +141,7 @@ class LS extends ArboristWorkspaceCmd { : [...(node.target || node).edgesOut.values()] .filter(filterBySelectedWorkspaces) .filter(filterByEdgesTypes({ + currentDepth, dev, development, link, @@ -387,6 +388,7 @@ const getJsonOutputItem = (node, { global, long }) => { } const filterByEdgesTypes = ({ + currentDepth, dev, development, link, @@ -398,11 +400,11 @@ const filterByEdgesTypes = ({ }) => { // filter deps by type, allows for: `npm ls --dev`, `npm ls --prod`, // `npm ls --link`, `npm ls --only=dev`, etc - const filterDev = node === tree && + const filterDev = currentDepth === 0 && (dev || development || /^dev(elopment)?$/.test(only)) - const filterProd = node === tree && + const filterProd = currentDepth === 0 && (prod || production || /^prod(uction)?$/.test(only)) - const filterLink = node === tree && link + const filterLink = currentDepth === 0 && link return (edge) => (filterDev ? edge.dev : true) && diff --git a/tap-snapshots/test/lib/ls.js.test.cjs b/tap-snapshots/test/lib/ls.js.test.cjs index 2ed0b4b001376..3d56d1f432731 100644 --- a/tap-snapshots/test/lib/ls.js.test.cjs +++ b/tap-snapshots/test/lib/ls.js.test.cjs @@ -496,6 +496,7 @@ workspaces-tree@1.0.0 {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspac exports[`test/lib/ls.js TAP ls loading a tree containing workspaces > should filter using workspace config 1`] = ` workspaces-tree@1.0.0 {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces \`-- a@1.0.0 -> ./a + +-- baz@1.0.0 +-- c@1.0.0 \`-- d@1.0.0 -> ./d \`-- foo@1.1.1 @@ -506,6 +507,21 @@ workspaces-tree@1.0.0 {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspac exports[`test/lib/ls.js TAP ls loading a tree containing workspaces > should list --all workspaces properly 1`] = ` workspaces-tree@1.0.0 {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces +-- a@1.0.0 -> ./a +| +-- baz@1.0.0 +| +-- c@1.0.0 +| \`-- d@1.0.0 deduped -> ./d ++-- b@1.0.0 -> ./b ++-- d@1.0.0 -> ./d +| \`-- foo@1.1.1 +| \`-- bar@1.0.0 ++-- e@1.0.0 -> ./group/e +\`-- f@1.0.0 -> ./group/f + +` + +exports[`test/lib/ls.js TAP ls loading a tree containing workspaces > should list only prod deps of workspaces 1`] = ` +workspaces-tree@1.0.0 {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces ++-- a@1.0.0 -> ./a | +-- c@1.0.0 | \`-- d@1.0.0 deduped -> ./d +-- b@1.0.0 -> ./b @@ -520,6 +536,7 @@ workspaces-tree@1.0.0 {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspac exports[`test/lib/ls.js TAP ls loading a tree containing workspaces > should list workspaces properly with default configs 1`] = ` workspaces-tree@1.0.0 {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces +-- a@1.0.0 -> ./a +| +-- baz@1.0.0 | +-- c@1.0.0 | \`-- d@1.0.0 deduped -> ./d +-- b@1.0.0 -> ./b diff --git a/test/lib/ls.js b/test/lib/ls.js index 582416f4aa2d2..64ece6bd8a3fd 100644 --- a/test/lib/ls.js +++ b/test/lib/ls.js @@ -1449,6 +1449,9 @@ t.test('ls', (t) => { bar: { 'package.json': JSON.stringify({ name: 'bar', version: '1.0.0' }), }, + baz: { + 'package.json': JSON.stringify({ name: 'baz', version: '1.0.0' }), + }, }, a: { 'package.json': JSON.stringify({ @@ -1458,6 +1461,9 @@ t.test('ls', (t) => { c: '^1.0.0', d: '^1.0.0', }, + devDependencies: { + baz: '^1.0.0', + }, }), }, b: { @@ -1520,6 +1526,21 @@ t.test('ls', (t) => { }) }) + // --production + await new Promise((res, rej) => { + config.production = true + ls.exec([], (err) => { + if (err) + rej(err) + + t.matchSnapshot(redactCwd(result), + 'should list only prod deps of workspaces') + + config.production = false + res() + }) + }) + // filter out a single workspace using args await new Promise((res, rej) => { ls.exec(['d'], (err) => { From c99b8b53c3d7a9b0daa6d4416e9c40202ddd59a2 Mon Sep 17 00:00:00 2001 From: Gar Date: Wed, 16 Jun 2021 13:31:51 -0700 Subject: [PATCH 14/20] fix(config): add flatOptions.npxCache This adds a new `npxCache` flatOption for libnpmexec to look for to put its `_npx` content. libnpmexec will have to be patched to use that value, and continue to pass `flatOptions.cache` to pacote et al. The `flatOptions.cache` is the one that is intended to be passed down into other modules, as it has the `_cacache` suffix attached. What was happening before was that `npx` was creating a new alternate cache one directory up from where everything else was, and also putting the `_npx` content there. It is possible this is the source of at least some of our "npx doesn't find the right versions" bugs. PR-URL: https://github.com/npm/cli/pull/3430 Credit: @wraithgar Close: #3430 Reviewed-by: @ruyadorno --- lib/exec.js | 2 -- lib/init.js | 2 -- lib/utils/config/definitions.js | 1 + test/lib/exec.js | 6 +++++- test/lib/init.js | 12 ++++++++++-- test/lib/utils/config/definitions.js | 1 + 6 files changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/exec.js b/lib/exec.js index 7e5f6886a3ed7..959fab66634bd 100644 --- a/lib/exec.js +++ b/lib/exec.js @@ -67,7 +67,6 @@ class Exec extends BaseCommand { // can be named correctly async _exec (_args, { locationMsg, path, runPath }) { const args = [..._args] - const cache = this.npm.config.get('cache') const call = this.npm.config.get('call') const color = this.npm.config.get('color') const { @@ -88,7 +87,6 @@ class Exec extends BaseCommand { ...flatOptions, args, call, - cache, color, localBin, locationMsg, diff --git a/lib/init.js b/lib/init.js index 4dd091601e191..d34f92b882b32 100644 --- a/lib/init.js +++ b/lib/init.js @@ -106,7 +106,6 @@ class Init extends BaseCommand { } const newArgs = [packageName, ...otherArgs] - const cache = this.npm.config.get('cache') const { color } = this.npm.flatOptions const { flatOptions, @@ -128,7 +127,6 @@ class Init extends BaseCommand { await libexec({ ...flatOptions, args: newArgs, - cache, color, localBin, locationMsg, diff --git a/lib/utils/config/definitions.js b/lib/utils/config/definitions.js index ce1ea766c9404..40120498aa2bf 100644 --- a/lib/utils/config/definitions.js +++ b/lib/utils/config/definitions.js @@ -322,6 +322,7 @@ define('cache', { `, flatten (key, obj, flatOptions) { flatOptions.cache = join(obj.cache, '_cacache') + flatOptions.npxCache = join(obj.cache, '_npx') }, }) diff --git a/test/lib/exec.js b/test/lib/exec.js index 6924783239b49..857a6f8634560 100644 --- a/test/lib/exec.js +++ b/test/lib/exec.js @@ -24,11 +24,13 @@ let PROGRESS_ENABLED = true const LOG_WARN = [] let PROGRESS_IGNORED = false const flatOptions = { + npxCache: 'npx-cache-dir', + cache: 'cache-dir', legacyPeerDeps: false, package: [], } const config = { - cache: 'cache-dir', + cache: 'bad-cache-dir', // this should never show up passed into libnpmexec yes: true, call: '', package: [], @@ -134,6 +136,8 @@ t.test('npx foo, bin already exists locally', t => { t.match(RUN_SCRIPTS, [{ pkg: { scripts: { npx: 'foo' }}, args: ['one arg', 'two arg'], + cache: flatOptions.cache, + npxCache: flatOptions.npxCache, banner: false, path: process.cwd(), stdioString: true, diff --git a/test/lib/init.js b/test/lib/init.js index 268b170cb4839..44a2af5bcc02b 100644 --- a/test/lib/init.js +++ b/test/lib/init.js @@ -12,10 +12,16 @@ const npmLog = { silly: () => null, } const config = { + cache: 'bad-cache-dir', 'init-module': '~/.npm-init.js', yes: true, } +const flatOptions = { + cache: 'test-config-dir/_cacache', + npxCache: 'test-config-dir/_npx', +} const npm = mockNpm({ + flatOptions, config, log: npmLog, }) @@ -82,16 +88,18 @@ t.test('classic interactive npm init', t => { }) t.test('npm init ', t => { - t.plan(1) + t.plan(3) npm.localPrefix = t.testdir({}) const Init = t.mock('../../lib/init.js', { - libnpmexec: ({ args }) => { + libnpmexec: ({ args, cache, npxCache }) => { t.same( args, ['create-react-app'], 'should npx with listed packages' ) + t.same(cache, flatOptions.cache) + t.same(npxCache, flatOptions.npxCache) }, }) const init = new Init(npm) diff --git a/test/lib/utils/config/definitions.js b/test/lib/utils/config/definitions.js index 49e4152883795..a87698c181359 100644 --- a/test/lib/utils/config/definitions.js +++ b/test/lib/utils/config/definitions.js @@ -181,6 +181,7 @@ t.test('cache', t => { defsNix.cache.flatten('cache', { cache: '/some/cache/value' }, flat) const {join} = require('path') t.equal(flat.cache, join('/some/cache/value', '_cacache')) + t.equal(flat.npxCache, join('/some/cache/value', '_npx')) t.end() }) From c90612cf566d563199553749900d8b05367e2532 Mon Sep 17 00:00:00 2001 From: Gar Date: Thu, 17 Jun 2021 09:27:26 -0700 Subject: [PATCH 15/20] libnpmexec@2.0.0 * use new npxCache option --- .../libnpmexec/lib/cache-install-dir.js | 8 +++---- node_modules/libnpmexec/lib/index.js | 4 ++-- node_modules/libnpmexec/package.json | 2 +- package-lock.json | 14 ++++++------ package.json | 2 +- test/lib/exec.js | 22 +++++++++---------- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/node_modules/libnpmexec/lib/cache-install-dir.js b/node_modules/libnpmexec/lib/cache-install-dir.js index 9e30d62a1e102..4fb534f7dfe12 100644 --- a/node_modules/libnpmexec/lib/cache-install-dir.js +++ b/node_modules/libnpmexec/lib/cache-install-dir.js @@ -2,12 +2,12 @@ const crypto = require('crypto') const { resolve } = require('path') -const cacheInstallDir = ({ cache, packages }) => { - if (!cache) - throw new Error('Must provide a valid cache path') +const cacheInstallDir = ({ npxCache, packages }) => { + if (!npxCache) + throw new Error('Must provide a valid npxCache path') // only packages not found in ${prefix}/node_modules - return resolve(cache, '_npx', getHash(packages)) + return resolve(npxCache, getHash(packages)) } const getHash = (packages) => diff --git a/node_modules/libnpmexec/lib/index.js b/node_modules/libnpmexec/lib/index.js index 8c5181f397519..57c2a148d3489 100644 --- a/node_modules/libnpmexec/lib/index.js +++ b/node_modules/libnpmexec/lib/index.js @@ -124,8 +124,8 @@ const exec = async (opts) => { manis.some(manifest => manifestMissing({ tree, manifest })) if (needInstall) { - const { cache } = flatOptions - const installDir = cacheInstallDir({ cache, packages }) + const { npxCache } = flatOptions + const installDir = cacheInstallDir({ npxCache, packages }) await mkdirp(installDir) const arb = new Arborist({ ...flatOptions, diff --git a/node_modules/libnpmexec/package.json b/node_modules/libnpmexec/package.json index 2b3b488cf079f..dff91077d148a 100644 --- a/node_modules/libnpmexec/package.json +++ b/node_modules/libnpmexec/package.json @@ -1,6 +1,6 @@ { "name": "libnpmexec", - "version": "1.2.0", + "version": "2.0.0", "files": [ "lib" ], diff --git a/package-lock.json b/package-lock.json index 88b1797e90ab9..5ef82e0fdb28b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -107,7 +107,7 @@ "leven": "^3.1.0", "libnpmaccess": "^4.0.2", "libnpmdiff": "^2.0.4", - "libnpmexec": "^1.2.0", + "libnpmexec": "^2.0.0", "libnpmfund": "^1.1.0", "libnpmhook": "^6.0.2", "libnpmorg": "^2.0.2", @@ -4635,9 +4635,9 @@ "link": true }, "node_modules/libnpmexec": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/libnpmexec/-/libnpmexec-1.2.0.tgz", - "integrity": "sha512-LkxnH2wsMUI4thsgUK0r+EFZ5iCjKlp21J68dFY7AzD5uaaIPqO3lqVvYbyl1Umz1R4rY9t3vFa1fF3hzo6Y2Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/libnpmexec/-/libnpmexec-2.0.0.tgz", + "integrity": "sha512-9zHswx//Lp2ao+huWF2aL+6v4haMncyxNusk6Us2fbLNnPh3+rgSkv38LJ2v8gmKS2kAnkUmQf8pHjcZ+7Z3NA==", "inBundle": true, "dependencies": { "@npmcli/arborist": "^2.3.0", @@ -13735,9 +13735,9 @@ } }, "libnpmexec": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/libnpmexec/-/libnpmexec-1.2.0.tgz", - "integrity": "sha512-LkxnH2wsMUI4thsgUK0r+EFZ5iCjKlp21J68dFY7AzD5uaaIPqO3lqVvYbyl1Umz1R4rY9t3vFa1fF3hzo6Y2Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/libnpmexec/-/libnpmexec-2.0.0.tgz", + "integrity": "sha512-9zHswx//Lp2ao+huWF2aL+6v4haMncyxNusk6Us2fbLNnPh3+rgSkv38LJ2v8gmKS2kAnkUmQf8pHjcZ+7Z3NA==", "requires": { "@npmcli/arborist": "^2.3.0", "@npmcli/ci-detect": "^1.3.0", diff --git a/package.json b/package.json index a8f7047924913..628c44a3f74f7 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "leven": "^3.1.0", "libnpmaccess": "^4.0.2", "libnpmdiff": "^2.0.4", - "libnpmexec": "^1.2.0", + "libnpmexec": "^2.0.0", "libnpmfund": "^1.1.0", "libnpmhook": "^6.0.2", "libnpmorg": "^2.0.2", diff --git a/test/lib/exec.js b/test/lib/exec.js index 857a6f8634560..dff067619f2ce 100644 --- a/test/lib/exec.js +++ b/test/lib/exec.js @@ -329,7 +329,7 @@ t.test('npm exec , run interactive shell', t => { t.test('npm exec foo, not present locally or in central loc', t => { const path = t.testdir() - const installDir = resolve('cache-dir/_npx/f7fbba6e0636f890') + const installDir = resolve('npx-cache-dir/f7fbba6e0636f890') npm.localPrefix = path ARB_ACTUAL_TREE[path] = { children: new Map(), @@ -369,7 +369,7 @@ t.test('npm exec foo, not present locally or in central loc', t => { t.test('npm exec foo, not present locally but in central loc', t => { const path = t.testdir() - const installDir = resolve('cache-dir/_npx/f7fbba6e0636f890') + const installDir = resolve('npx-cache-dir/f7fbba6e0636f890') npm.localPrefix = path ARB_ACTUAL_TREE[path] = { children: new Map(), @@ -409,7 +409,7 @@ t.test('npm exec foo, not present locally but in central loc', t => { t.test('npm exec foo, present locally but wrong version', t => { const path = t.testdir() - const installDir = resolve('cache-dir/_npx/2badf4630f1cfaad') + const installDir = resolve('npx-cache-dir/2badf4630f1cfaad') npm.localPrefix = path ARB_ACTUAL_TREE[path] = { children: new Map(), @@ -605,7 +605,7 @@ t.test('run command with 2 packages, need install, verify sort', t => { config.package = packages const add = packages.map(p => `${p}@`).sort((a, b) => a.localeCompare(b, 'en')) const path = t.testdir() - const installDir = resolve('cache-dir/_npx/07de77790e5f40f2') + const installDir = resolve('npx-cache-dir/07de77790e5f40f2') npm.localPrefix = path ARB_ACTUAL_TREE[path] = { children: new Map(), @@ -760,7 +760,7 @@ t.test('prompt when installs are needed if not already present and shell is a TT const add = packages.map(p => `${p}@`).sort((a, b) => a.localeCompare(b, 'en')) const path = t.testdir() - const installDir = resolve('cache-dir/_npx/07de77790e5f40f2') + const installDir = resolve('npx-cache-dir/07de77790e5f40f2') npm.localPrefix = path ARB_ACTUAL_TREE[path] = { children: new Map(), @@ -829,7 +829,7 @@ t.test('skip prompt when installs are needed if not already present and shell is const add = packages.map(p => `${p}@`).sort((a, b) => a.localeCompare(b, 'en')) const path = t.testdir() - const installDir = resolve('cache-dir/_npx/07de77790e5f40f2') + const installDir = resolve('npx-cache-dir/07de77790e5f40f2') npm.localPrefix = path ARB_ACTUAL_TREE[path] = { children: new Map(), @@ -896,7 +896,7 @@ t.test('skip prompt when installs are needed if not already present and shell is const add = packages.map(p => `${p}@`).sort((a, b) => a.localeCompare(b, 'en')) const path = t.testdir() - const installDir = resolve('cache-dir/_npx/f7fbba6e0636f890') + const installDir = resolve('npx-cache-dir/f7fbba6e0636f890') npm.localPrefix = path ARB_ACTUAL_TREE[path] = { children: new Map(), @@ -954,7 +954,7 @@ t.test('abort if prompt rejected', t => { config.yes = undefined const path = t.testdir() - const installDir = resolve('cache-dir/_npx/07de77790e5f40f2') + const installDir = resolve('npx-cache-dir/07de77790e5f40f2') npm.localPrefix = path ARB_ACTUAL_TREE[path] = { children: new Map(), @@ -1012,7 +1012,7 @@ t.test('abort if prompt false', t => { config.yes = undefined const path = t.testdir() - const installDir = resolve('cache-dir/_npx/07de77790e5f40f2') + const installDir = resolve('npx-cache-dir/07de77790e5f40f2') npm.localPrefix = path ARB_ACTUAL_TREE[path] = { children: new Map(), @@ -1069,7 +1069,7 @@ t.test('abort if -n provided', t => { config.yes = false const path = t.testdir() - const installDir = resolve('cache-dir/_npx/07de77790e5f40f2') + const installDir = resolve('npx-cache-dir/07de77790e5f40f2') npm.localPrefix = path ARB_ACTUAL_TREE[path] = { children: new Map(), @@ -1107,7 +1107,7 @@ t.test('abort if -n provided', t => { t.test('forward legacyPeerDeps opt', t => { const path = t.testdir() - const installDir = resolve('cache-dir/_npx/f7fbba6e0636f890') + const installDir = resolve('npx-cache-dir/f7fbba6e0636f890') npm.localPrefix = path ARB_ACTUAL_TREE[path] = { children: new Map(), From c984fb59c5af087b91acd927cbbacad7c6a46576 Mon Sep 17 00:00:00 2001 From: Gar Date: Tue, 15 Jun 2021 14:23:00 -0700 Subject: [PATCH 16/20] feat(pack): add pack-destination config This will allow users to specify a folder in which to save their tarballs. PR-URL: https://github.com/npm/cli/pull/3420 Credit: @wraithgar Close: #3420 Reviewed-by: @ruyadorno --- docs/content/commands/npm-pack.md | 7 +++ docs/content/using-npm/config.md | 7 +++ lib/pack.js | 12 ++++- lib/utils/config/definitions.js | 8 ++++ .../test/lib/load-all-commands.js.test.cjs | 2 +- .../lib/utils/config/definitions.js.test.cjs | 1 + .../lib/utils/config/describe-all.js.test.cjs | 7 +++ .../test/lib/utils/npm-usage.js.test.cjs | 2 +- test/lib/pack.js | 44 ++++++++++++++++--- 9 files changed, 81 insertions(+), 9 deletions(-) diff --git a/docs/content/commands/npm-pack.md b/docs/content/commands/npm-pack.md index 04a22a5d854b4..9507026278437 100644 --- a/docs/content/commands/npm-pack.md +++ b/docs/content/commands/npm-pack.md @@ -36,6 +36,13 @@ Whether or not to output JSON data, rather than the normal output. Not supported by all npm commands. +#### `pack-destination` + +* Default: "." +* Type: String + +Directory in which `npm pack` will save tarballs. + #### `workspace` * Default: diff --git a/docs/content/using-npm/config.md b/docs/content/using-npm/config.md index 95f89a5848d23..a33c66e145cd9 100644 --- a/docs/content/using-npm/config.md +++ b/docs/content/using-npm/config.md @@ -867,6 +867,13 @@ when publishing or changing package permissions with `npm access`. If not set, and a registry response fails with a challenge for a one-time password, npm will prompt on the command line for one. +#### `pack-destination` + +* Default: "." +* Type: String + +Directory in which `npm pack` will save tarballs. + #### `package` * Default: diff --git a/lib/pack.js b/lib/pack.js index f4364d29033c4..8fc89db1a0b2b 100644 --- a/lib/pack.js +++ b/lib/pack.js @@ -3,6 +3,7 @@ const log = require('npmlog') const pacote = require('pacote') const libpack = require('libnpmpack') const npa = require('npm-package-arg') +const path = require('path') const { getContents, logTar } = require('./utils/tar.js') @@ -23,7 +24,13 @@ class Pack extends BaseCommand { /* istanbul ignore next - see test/lib/load-all-commands.js */ static get params () { - return ['dry-run', 'json', 'workspace', 'workspaces'] + return [ + 'dry-run', + 'json', + 'pack-destination', + 'workspace', + 'workspaces', + ] } /* istanbul ignore next - see test/lib/load-all-commands.js */ @@ -67,9 +74,10 @@ class Pack extends BaseCommand { for (const { arg, filename, manifest } of manifests) { const tarballData = await libpack(arg, this.npm.flatOptions) const pkgContents = await getContents(manifest, tarballData) + const tarballFilename = path.resolve(this.npm.config.get('pack-destination'), filename) if (!dryRun) - await writeFile(filename, tarballData) + await writeFile(tarballFilename, tarballData) tarballs.push(pkgContents) } diff --git a/lib/utils/config/definitions.js b/lib/utils/config/definitions.js index 40120498aa2bf..d540b0fc67e82 100644 --- a/lib/utils/config/definitions.js +++ b/lib/utils/config/definitions.js @@ -1336,6 +1336,14 @@ define('package-lock-only', { flatten, }) +define('pack-destination', { + default: '.', + type: String, + description: ` + Directory in which \`npm pack\` will save tarballs. + `, +}) + define('parseable', { default: false, type: Boolean, diff --git a/tap-snapshots/test/lib/load-all-commands.js.test.cjs b/tap-snapshots/test/lib/load-all-commands.js.test.cjs index 70902ba10cf33..3575783a644b2 100644 --- a/tap-snapshots/test/lib/load-all-commands.js.test.cjs +++ b/tap-snapshots/test/lib/load-all-commands.js.test.cjs @@ -657,7 +657,7 @@ Usage: npm pack [[<@scope>/]...] Options: -[--dry-run] [--json] +[--dry-run] [--json] [--pack-destination ] [-w|--workspace [-w|--workspace ...]] [-ws|--workspaces] diff --git a/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs b/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs index 32443c57af35b..35942fea64683 100644 --- a/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs +++ b/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs @@ -98,6 +98,7 @@ Array [ "package", "package-lock", "package-lock-only", + "pack-destination", "parseable", "prefer-offline", "prefer-online", diff --git a/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs b/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs index 9e3ba4d1af050..daa071b642e94 100644 --- a/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs +++ b/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs @@ -746,6 +746,13 @@ when publishing or changing package permissions with \`npm access\`. If not set, and a registry response fails with a challenge for a one-time password, npm will prompt on the command line for one. +#### \`pack-destination\` + +* Default: "." +* Type: String + +Directory in which \`npm pack\` will save tarballs. + #### \`package\` * Default: diff --git a/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs b/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs index dc10b43739b15..3987f6a732da5 100644 --- a/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs +++ b/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs @@ -744,7 +744,7 @@ All commands: npm pack [[<@scope>/]...] Options: - [--dry-run] [--json] + [--dry-run] [--json] [--pack-destination ] [-w|--workspace [-w|--workspace ...]] [-ws|--workspaces] diff --git a/test/lib/pack.js b/test/lib/pack.js index ad5bbf3359182..523ba5d6b535d 100644 --- a/test/lib/pack.js +++ b/test/lib/pack.js @@ -1,6 +1,7 @@ const t = require('tap') const mockNpm = require('../fixtures/mock-npm') const pacote = require('pacote') +const path = require('path') const OUTPUT = [] const output = (...msg) => OUTPUT.push(msg) @@ -27,6 +28,7 @@ const mockPacote = { t.afterEach(() => OUTPUT.length = 0) t.test('should pack current directory with no arguments', (t) => { + let tarballFileName const Pack = t.mock('../../lib/pack.js', { libnpmpack, npmlog: { @@ -35,14 +37,46 @@ t.test('should pack current directory with no arguments', (t) => { clearProgress: () => {}, }, fs: { - writeFile: (file, data, cb) => cb(), + writeFile: (file, data, cb) => { + tarballFileName = file + cb() + }, + }, + }) + const npm = mockNpm({ + output, + }) + const pack = new Pack(npm) + + pack.exec([], err => { + t.error(err, { bail: true }) + + const filename = `npm-${require('../../package.json').version}.tgz` + t.strictSame(OUTPUT, [[filename]]) + t.strictSame(tarballFileName, path.resolve(filename)) + t.end() + }) +}) + +t.test('follows pack-destination config', (t) => { + let tarballFileName + const Pack = t.mock('../../lib/pack.js', { + libnpmpack, + npmlog: { + notice: () => {}, + showProgress: () => {}, + clearProgress: () => {}, + }, + fs: { + writeFile: (file, data, cb) => { + tarballFileName = file + cb() + }, }, }) const npm = mockNpm({ config: { - unicode: false, - json: false, - 'dry-run': false, + 'pack-destination': '/tmp/test', }, output, }) @@ -53,10 +87,10 @@ t.test('should pack current directory with no arguments', (t) => { const filename = `npm-${require('../../package.json').version}.tgz` t.strictSame(OUTPUT, [[filename]]) + t.strictSame(tarballFileName, path.resolve('/tmp/test', filename)) t.end() }) }) - t.test('should pack given directory', (t) => { const testDir = t.testdir({ 'package.json': JSON.stringify({ From ad641d4917f0141b7fa5492b68471e4369f2073a Mon Sep 17 00:00:00 2001 From: Luke Karrys Date: Thu, 17 Jun 2021 10:35:58 -0700 Subject: [PATCH 17/20] docs: changelog for v7.18.0 --- CHANGELOG.md | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e627d72ba2834..64f4e071ed259 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,79 @@ +## v7.18.0 (2021-06-17) + +## FEATURES + +* [`ae285b391`](https://github.com/npm/cli/commit/ae285b39191f3a0c4edfb045a334057bef4567b5) + [#3408](https://github.com/npm/cli/issues/3408) + feat(ls): support `--package-lock-only` flag + ([@G-Rath](https://github.com/G-Rath)) +* [`c984fb59c`](https://github.com/npm/cli/commit/c984fb59c5af087b91acd927cbbacad7c6a46576) + [#3420](https://github.com/npm/cli/issues/3420) + feat(pack): add pack-destination config + ([@wraithgar](https://github.com/wraithgar)) + +## BUG FIXES + +* [`40829ec40`](https://github.com/npm/cli/commit/40829ec40c33a6d23f18715e60e3395bdcb0467e) + [#2554](https://github.com/npm/cli/issues/2554) + [#3399](https://github.com/npm/cli/issues/3399) + fix(link): do not prune packages + ([@ruyadorno](https://github.com/ruyadorno)) +* [`102d4e6fb`](https://github.com/npm/cli/commit/102d4e6fb3c3b02148dbeee977a7d1e6372340d5) + [#3417](https://github.com/npm/cli/issues/3417) + fix(workspaces): explicitly error in global mode + ([@wraithgar](https://github.com/wraithgar)) +* [`993df3041`](https://github.com/npm/cli/commit/993df3041f5bdaa496c3c8d80f00d16b9cf0a1e6) + [#3423](https://github.com/npm/cli/issues/3423) + fix(docs): ls command usage instructions + ([@gurdiga](https://github.com/gurdiga)) +* [`dcc13662c`](https://github.com/npm/cli/commit/dcc13662c1d3e22eaf392647a9cddbb5b0710d24) + [#3418](https://github.com/npm/cli/issues/3418) + fix(config): update link definition + ([@wraithgar](https://github.com/wraithgar)) +* [`b19e56c2e`](https://github.com/npm/cli/commit/b19e56c2e54c035518165470c10480201cefa997) + [#3382](https://github.com/npm/cli/issues/3382) + [#3429](https://github.com/npm/cli/issues/3429) + fix(ls): respect prod config for workspaces + ([@ruyadorno](https://github.com/ruyadorno)) +* [`c99b8b53c`](https://github.com/npm/cli/commit/c99b8b53c3d7a9b0daa6d4416e9c40202ddd59a2) + [#3430](https://github.com/npm/cli/issues/3430) + fix(config): add flatOptions.npxCache + ([@wraithgar](https://github.com/wraithgar)) +* [`e5abf2a21`](https://github.com/npm/cli/commit/e5abf2a2171d95bafc0993f337230d2b6633a6ed) + [#3386](https://github.com/npm/cli/issues/3386) + chore(libnpmdiff): added as workspace + ([@ruyadorno](https://github.com/ruyadorno)) +* [`c6a8734d7`](https://github.com/npm/cli/commit/c6a8734d7d6e4b6d061110a01e45e1d418d56489) + [#3388](https://github.com/npm/cli/issues/3388) + chore(refactor): finish passing npm context + ([@wraithgar](https://github.com/wraithgar)) +* [`d16ee452a`](https://github.com/npm/cli/commit/d16ee452a4a034caada4e9b96faf5c453a658876) + [#3426](https://github.com/npm/cli/issues/3426) + chore(tests): use path.resolve + ([@wraithgar](https://github.com/wraithgar)) + +## DEPENDENCIES + +* [`6b951c042`](https://github.com/npm/cli/commit/6b951c042084e639be929a7ea783c2d85b311bad) + `libnpmversion@1.2.1`: + * fix(retrieve-tag): pass match in a way git accepts +* [`de820a021`](https://github.com/npm/cli/commit/de820a0213f54bbcd155dff25b05d072d5c4a57a) + `npm-package-arg@8.1.5`: + * fix: Make file: URLs (mostly) RFC 8909 compliant +* [`16a95c647`](https://github.com/npm/cli/commit/16a95c64731609c69630c17c45b16edb53ee81b2) + `@npmcli/arborist@2.6.3`: + * fix(inventory) handle old and british forms of 'license' + * fix: removes [_complete] check to apply correct metadata + * ensure node.fsParent is not set to node itself + * fix extraneous deps on load-actual +* [`d341bd86c`](https://github.com/npm/cli/commit/d341bd86ce05fabe44f3be5888ba2611b61914b4) + `make-fetch-happen@9.0.3`: + * fix: implement cache modes correctly +* [`c90612cf5`](https://github.com/npm/cli/commit/c90612cf566d563199553749900d8b05367e2532) + `libnpmexec@2.0.0`: + * use new npxCache option + + ## v7.17.0 (2021-06-10) ## FEATURES From 872ea9ea1bc0f2a3ef9b0881ca50a05bc7c23fea Mon Sep 17 00:00:00 2001 From: Luke Karrys Date: Thu, 17 Jun 2021 10:36:40 -0700 Subject: [PATCH 18/20] update AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index a8dfd8b6be682..1ee244b9f5838 100644 --- a/AUTHORS +++ b/AUTHORS @@ -783,3 +783,4 @@ rethab Spencer Wilson <5624115+spencerwilson@users.noreply.github.com> Daniel Park Daniel Park +Luke Karrys From 1fb377609300314cb896e7c7c9377833855cf31b Mon Sep 17 00:00:00 2001 From: Luke Karrys Date: Thu, 17 Jun 2021 11:00:13 -0700 Subject: [PATCH 19/20] fix(docs): rebuild docs --- docs/content/commands/npm-audit.md | 10 ++++++++-- docs/content/commands/npm-ls.md | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/docs/content/commands/npm-audit.md b/docs/content/commands/npm-audit.md index 0771d897df90d..704d7a15fb8f1 100644 --- a/docs/content/commands/npm-audit.md +++ b/docs/content/commands/npm-audit.md @@ -250,8 +250,14 @@ Not supported by all npm commands. * Default: false * Type: Boolean -If set to true, it will update only the `package-lock.json`, instead of -checking `node_modules` and downloading dependencies. +If set to true, the current operation will only use the `package-lock.json`, +ignoring `node_modules`. + +For `update` this means only the `package-lock.json` will be updated, +instead of checking `node_modules` and downloading dependencies. + +For `list` this means the output will be based on the tree described by the +`package-lock.json`, rather than the contents of `node_modules`. #### `omit` diff --git a/docs/content/commands/npm-ls.md b/docs/content/commands/npm-ls.md index 21a3c3eec8025..1f401fa956ff8 100644 --- a/docs/content/commands/npm-ls.md +++ b/docs/content/commands/npm-ls.md @@ -157,6 +157,20 @@ variable will be set to `'production'` for all lifecycle scripts. Used with `npm ls`, limiting output to only those packages that are linked. +#### `package-lock-only` + +* Default: false +* Type: Boolean + +If set to true, the current operation will only use the `package-lock.json`, +ignoring `node_modules`. + +For `update` this means only the `package-lock.json` will be updated, +instead of checking `node_modules` and downloading dependencies. + +For `list` this means the output will be based on the tree described by the +`package-lock.json`, rather than the contents of `node_modules`. + #### `unicode` * Default: false on windows, true on mac/unix systems with a unicode locale, From 699c2d708d2a24b4f495a74974b2a345f33ee08a Mon Sep 17 00:00:00 2001 From: Luke Karrys Date: Thu, 17 Jun 2021 10:36:40 -0700 Subject: [PATCH 20/20] 7.18.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5ef82e0fdb28b..b6c93b183bfd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "npm", - "version": "7.17.0", + "version": "7.18.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "npm", - "version": "7.17.0", + "version": "7.18.0", "bundleDependencies": [ "@npmcli/arborist", "@npmcli/ci-detect", diff --git a/package.json b/package.json index 628c44a3f74f7..37562f0e86acf 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "7.17.0", + "version": "7.18.0", "name": "npm", "description": "a package manager for JavaScript", "workspaces": [