From 48327b2b74a203ebe37990972e3f969073ea70d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Nison?= Date: Thu, 28 Sep 2023 15:49:23 +0200 Subject: [PATCH] Improves the output of "yarn config" (#5713) **What's the problem this PR addresses?** It's a little difficult to ask people to check specific values of their configuration at the moment. They need to run `yarn config get ` to see the value, or `yarn config --why | grep `, but that isn't very visible. **How did you fix it?** It's a bit of an experiment to try to change the output from: image To the new tree display: image To offset the lack of compaction, the command now also accepts a list of setting names on the command line, so you can write the following: ``` yarn config nodeLinker ``` Or even multiple parameters: ``` yarn config supportedArchitectures enableGlobalCache ``` image **Checklist** - [x] I have read the [Contributing Guide](https://yarnpkg.com/advanced/contributing). - [x] I have set the packages that need to be released for my changes to be effective. - [x] I will check that all automated PR checks pass before the PR gets reviewed. --- .yarn/versions/b9bd2984.yml | 34 +++ .../__snapshots__/config.test.js.snap | 241 ------------------ .../__snapshots__/config.test.ts.snap | 199 +++++++++++++++ .../{config.test.js => config.test.ts} | 80 +++--- .../sources/commands/config.ts | 162 ++++++++---- .../sources/commands/config/get.ts | 4 + .../sources/commands/install.ts | 195 ++++++-------- packages/plugin-nm/sources/index.ts | 6 +- .../yarnpkg-core/sources/Configuration.ts | 34 ++- packages/yarnpkg-core/sources/StreamReport.ts | 38 +++ packages/yarnpkg-core/sources/index.ts | 2 +- packages/yarnpkg-core/sources/treeUtils.ts | 4 +- 12 files changed, 533 insertions(+), 466 deletions(-) create mode 100644 .yarn/versions/b9bd2984.yml delete mode 100644 packages/acceptance-tests/pkg-tests-specs/sources/commands/__snapshots__/config.test.js.snap create mode 100644 packages/acceptance-tests/pkg-tests-specs/sources/commands/__snapshots__/config.test.ts.snap rename packages/acceptance-tests/pkg-tests-specs/sources/commands/{config.test.js => config.test.ts} (54%) diff --git a/.yarn/versions/b9bd2984.yml b/.yarn/versions/b9bd2984.yml new file mode 100644 index 000000000000..f8d20a359f5f --- /dev/null +++ b/.yarn/versions/b9bd2984.yml @@ -0,0 +1,34 @@ +releases: + "@yarnpkg/cli": major + "@yarnpkg/core": major + "@yarnpkg/plugin-essentials": major + "@yarnpkg/plugin-nm": patch + +declined: + - "@yarnpkg/plugin-compat" + - "@yarnpkg/plugin-constraints" + - "@yarnpkg/plugin-dlx" + - "@yarnpkg/plugin-exec" + - "@yarnpkg/plugin-file" + - "@yarnpkg/plugin-git" + - "@yarnpkg/plugin-github" + - "@yarnpkg/plugin-http" + - "@yarnpkg/plugin-init" + - "@yarnpkg/plugin-interactive-tools" + - "@yarnpkg/plugin-link" + - "@yarnpkg/plugin-npm" + - "@yarnpkg/plugin-npm-cli" + - "@yarnpkg/plugin-pack" + - "@yarnpkg/plugin-patch" + - "@yarnpkg/plugin-pnp" + - "@yarnpkg/plugin-pnpm" + - "@yarnpkg/plugin-stage" + - "@yarnpkg/plugin-typescript" + - "@yarnpkg/plugin-version" + - "@yarnpkg/plugin-workspace-tools" + - "@yarnpkg/builder" + - "@yarnpkg/doctor" + - "@yarnpkg/extensions" + - "@yarnpkg/nm" + - "@yarnpkg/pnpify" + - "@yarnpkg/sdks" diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/commands/__snapshots__/config.test.js.snap b/packages/acceptance-tests/pkg-tests-specs/sources/commands/__snapshots__/config.test.js.snap deleted file mode 100644 index 3d0fe71d5c66..000000000000 --- a/packages/acceptance-tests/pkg-tests-specs/sources/commands/__snapshots__/config.test.js.snap +++ /dev/null @@ -1,241 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Commands config test (folder with rcfile / as json) 1`] = ` -{ - "code": 0, - "stderr": "", - "stdout": "{"key":"defaultLanguageName","effective":"node","description":"Default language mode that should be used when a package doesn't offer any insight","type":"STRING","default":"node"} -{"key":"initScope","effective":"my-test","source":"WORKSPACE_ROOT/subfolder/subfolder/.yarnrc.yml","description":"Scope used when creating packages via the init command","type":"STRING","default":null} -{"key":"lastUpdateCheck","effective":null,"description":"Last timestamp we checked whether new Yarn versions were available","type":"STRING","default":null}", -} -`; - -exports[`Commands config test (folder with rcfile / showing explanation) 1`] = ` -{ - "code": 0, - "stderr": "", - "stdout": "➤ YN0000: defaultLanguageName - Default language mode that should be used when a package doesn't offer any insight - 'node' -➤ YN0000: initScope - Scope used when creating packages via the init command - 'my-test' -➤ YN0000: lastUpdateCheck - Last timestamp we checked whether new Yarn versions were available - null", -} -`; - -exports[`Commands config test (folder with rcfile / showing the source) 1`] = ` -{ - "code": 0, - "stderr": "", - "stdout": "➤ YN0000: defaultLanguageName - - 'node' -➤ YN0000: initScope - WORKSPACE_ROOT/subfolder/subfolder/.yarnrc.yml - 'my-test' -➤ YN0000: lastUpdateCheck - - null", -} -`; - -exports[`Commands config test (folder with rcfile / without flags) 1`] = ` -{ - "code": 0, - "stderr": "", - "stdout": "➤ YN0000: defaultLanguageName - 'node' -➤ YN0000: initScope - 'my-test' -➤ YN0000: lastUpdateCheck - null", -} -`; - -exports[`Commands config test (folder with rcfile and rc in ancestor parent / as json) 1`] = ` -{ - "code": 0, - "stderr": "", - "stdout": "{"key":"defaultLanguageName","effective":"node","description":"Default language mode that should be used when a package doesn't offer any insight","type":"STRING","default":"node"} -{"key":"initScope","effective":"my-test","source":"WORKSPACE_ROOT/subfolder/subfolder/.yarnrc.yml","description":"Scope used when creating packages via the init command","type":"STRING","default":null} -{"key":"lastUpdateCheck","effective":"1555784893958","source":"WORKSPACE_ROOT/.yarnrc.yml","description":"Last timestamp we checked whether new Yarn versions were available","type":"STRING","default":null}", -} -`; - -exports[`Commands config test (folder with rcfile and rc in ancestor parent / showing explanation) 1`] = ` -{ - "code": 0, - "stderr": "", - "stdout": "➤ YN0000: defaultLanguageName - Default language mode that should be used when a package doesn't offer any insight - 'node' -➤ YN0000: initScope - Scope used when creating packages via the init command - 'my-test' -➤ YN0000: lastUpdateCheck - Last timestamp we checked whether new Yarn versions were available - '1555784893958'", -} -`; - -exports[`Commands config test (folder with rcfile and rc in ancestor parent / showing the source) 1`] = ` -{ - "code": 0, - "stderr": "", - "stdout": "➤ YN0000: defaultLanguageName - - 'node' -➤ YN0000: initScope - WORKSPACE_ROOT/subfolder/subfolder/.yarnrc.yml - 'my-test' -➤ YN0000: lastUpdateCheck - WORKSPACE_ROOT/.yarnrc.yml - '1555784893958'", -} -`; - -exports[`Commands config test (folder with rcfile and rc in ancestor parent / without flags) 1`] = ` -{ - "code": 0, - "stderr": "", - "stdout": "➤ YN0000: defaultLanguageName - 'node' -➤ YN0000: initScope - 'my-test' -➤ YN0000: lastUpdateCheck - '1555784893958'", -} -`; - -exports[`Commands config test (folder with rcfile and rc in home folder / as json) 1`] = ` -{ - "code": 0, - "stderr": "", - "stdout": "{"key":"defaultLanguageName","effective":"python","source":"HOME/.yarnrc.yml","description":"Default language mode that should be used when a package doesn't offer any insight","type":"STRING","default":"node"} -{"key":"initScope","effective":"my-test","source":"WORKSPACE_ROOT/.yarnrc.yml","description":"Scope used when creating packages via the init command","type":"STRING","default":null} -{"key":"lastUpdateCheck","effective":"1555784893958","source":"WORKSPACE_ROOT/.yarnrc.yml","description":"Last timestamp we checked whether new Yarn versions were available","type":"STRING","default":null}", -} -`; - -exports[`Commands config test (folder with rcfile and rc in home folder / showing explanation) 1`] = ` -{ - "code": 0, - "stderr": "", - "stdout": "➤ YN0000: defaultLanguageName - Default language mode that should be used when a package doesn't offer any insight - 'python' -➤ YN0000: initScope - Scope used when creating packages via the init command - 'my-test' -➤ YN0000: lastUpdateCheck - Last timestamp we checked whether new Yarn versions were available - '1555784893958'", -} -`; - -exports[`Commands config test (folder with rcfile and rc in home folder / showing the source) 1`] = ` -{ - "code": 0, - "stderr": "", - "stdout": "➤ YN0000: defaultLanguageName - HOME/.yarnrc.yml - 'python' -➤ YN0000: initScope - WORKSPACE_ROOT/.yarnrc.yml - 'my-test' -➤ YN0000: lastUpdateCheck - WORKSPACE_ROOT/.yarnrc.yml - '1555784893958'", -} -`; - -exports[`Commands config test (folder with rcfile and rc in home folder / without flags) 1`] = ` -{ - "code": 0, - "stderr": "", - "stdout": "➤ YN0000: defaultLanguageName - 'python' -➤ YN0000: initScope - 'my-test' -➤ YN0000: lastUpdateCheck - '1555784893958'", -} -`; - -exports[`Commands config test (folder with rcfile and rc in parent / as json) 1`] = ` -{ - "code": 0, - "stderr": "", - "stdout": "{"key":"defaultLanguageName","effective":"node","description":"Default language mode that should be used when a package doesn't offer any insight","type":"STRING","default":"node"} -{"key":"initScope","effective":"my-test","source":"WORKSPACE_ROOT/subfolder/subfolder/.yarnrc.yml","description":"Scope used when creating packages via the init command","type":"STRING","default":null} -{"key":"lastUpdateCheck","effective":"1555784893958","source":"WORKSPACE_ROOT/subfolder/.yarnrc.yml","description":"Last timestamp we checked whether new Yarn versions were available","type":"STRING","default":null}", -} -`; - -exports[`Commands config test (folder with rcfile and rc in parent / showing explanation) 1`] = ` -{ - "code": 0, - "stderr": "", - "stdout": "➤ YN0000: defaultLanguageName - Default language mode that should be used when a package doesn't offer any insight - 'node' -➤ YN0000: initScope - Scope used when creating packages via the init command - 'my-test' -➤ YN0000: lastUpdateCheck - Last timestamp we checked whether new Yarn versions were available - '1555784893958'", -} -`; - -exports[`Commands config test (folder with rcfile and rc in parent / showing the source) 1`] = ` -{ - "code": 0, - "stderr": "", - "stdout": "➤ YN0000: defaultLanguageName - - 'node' -➤ YN0000: initScope - WORKSPACE_ROOT/subfolder/subfolder/.yarnrc.yml - 'my-test' -➤ YN0000: lastUpdateCheck - WORKSPACE_ROOT/subfolder/.yarnrc.yml - '1555784893958'", -} -`; - -exports[`Commands config test (folder with rcfile and rc in parent / without flags) 1`] = ` -{ - "code": 0, - "stderr": "", - "stdout": "➤ YN0000: defaultLanguageName - 'node' -➤ YN0000: initScope - 'my-test' -➤ YN0000: lastUpdateCheck - '1555784893958'", -} -`; - -exports[`Commands config test (folder with rcfile without trailing newline / as json) 1`] = ` -{ - "code": 0, - "stderr": "", - "stdout": "{"key":"defaultLanguageName","effective":"node","description":"Default language mode that should be used when a package doesn't offer any insight","type":"STRING","default":"node"} -{"key":"initScope","effective":"my-test","source":"WORKSPACE_ROOT/subfolder/subfolder/.yarnrc.yml","description":"Scope used when creating packages via the init command","type":"STRING","default":null} -{"key":"lastUpdateCheck","effective":null,"description":"Last timestamp we checked whether new Yarn versions were available","type":"STRING","default":null}", -} -`; - -exports[`Commands config test (folder with rcfile without trailing newline / showing explanation) 1`] = ` -{ - "code": 0, - "stderr": "", - "stdout": "➤ YN0000: defaultLanguageName - Default language mode that should be used when a package doesn't offer any insight - 'node' -➤ YN0000: initScope - Scope used when creating packages via the init command - 'my-test' -➤ YN0000: lastUpdateCheck - Last timestamp we checked whether new Yarn versions were available - null", -} -`; - -exports[`Commands config test (folder with rcfile without trailing newline / showing the source) 1`] = ` -{ - "code": 0, - "stderr": "", - "stdout": "➤ YN0000: defaultLanguageName - - 'node' -➤ YN0000: initScope - WORKSPACE_ROOT/subfolder/subfolder/.yarnrc.yml - 'my-test' -➤ YN0000: lastUpdateCheck - - null", -} -`; - -exports[`Commands config test (folder with rcfile without trailing newline / without flags) 1`] = ` -{ - "code": 0, - "stderr": "", - "stdout": "➤ YN0000: defaultLanguageName - 'node' -➤ YN0000: initScope - 'my-test' -➤ YN0000: lastUpdateCheck - null", -} -`; - -exports[`Commands config test (folder without rcfile in ancestry / as json) 1`] = ` -{ - "code": 0, - "stderr": "", - "stdout": "{"key":"defaultLanguageName","effective":"node","description":"Default language mode that should be used when a package doesn't offer any insight","type":"STRING","default":"node"} -{"key":"initScope","effective":null,"description":"Scope used when creating packages via the init command","type":"STRING","default":null} -{"key":"lastUpdateCheck","effective":null,"description":"Last timestamp we checked whether new Yarn versions were available","type":"STRING","default":null}", -} -`; - -exports[`Commands config test (folder without rcfile in ancestry / showing explanation) 1`] = ` -{ - "code": 0, - "stderr": "", - "stdout": "➤ YN0000: defaultLanguageName - Default language mode that should be used when a package doesn't offer any insight - 'node' -➤ YN0000: initScope - Scope used when creating packages via the init command - null -➤ YN0000: lastUpdateCheck - Last timestamp we checked whether new Yarn versions were available - null", -} -`; - -exports[`Commands config test (folder without rcfile in ancestry / showing the source) 1`] = ` -{ - "code": 0, - "stderr": "", - "stdout": "➤ YN0000: defaultLanguageName - - 'node' -➤ YN0000: initScope - - null -➤ YN0000: lastUpdateCheck - - null", -} -`; - -exports[`Commands config test (folder without rcfile in ancestry / without flags) 1`] = ` -{ - "code": 0, - "stderr": "", - "stdout": "➤ YN0000: defaultLanguageName - 'node' -➤ YN0000: initScope - null -➤ YN0000: lastUpdateCheck - null", -} -`; diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/commands/__snapshots__/config.test.ts.snap b/packages/acceptance-tests/pkg-tests-specs/sources/commands/__snapshots__/config.test.ts.snap new file mode 100644 index 000000000000..82ad7018b2a9 --- /dev/null +++ b/packages/acceptance-tests/pkg-tests-specs/sources/commands/__snapshots__/config.test.ts.snap @@ -0,0 +1,199 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Commands config test (folder with rcfile / as json) 1`] = ` +{ + "code": 0, + "stderr": "", + "stdout": "{"key":"defaultLanguageName","effective":"node","source":"","description":"Default language mode that should be used when a package doesn't offer any insight","type":"STRING","default":"node"} +{"key":"initScope","effective":"my-test","source":"WORKSPACE_ROOT/subfolder/subfolder/.yarnrc.yml","description":"Scope used when creating packages via the init command","type":"STRING","default":null} +{"key":"lastUpdateCheck","effective":null,"source":"","description":"Last timestamp we checked whether new Yarn versions were available","type":"STRING","default":null} +", +} +`; + +exports[`Commands config test (folder with rcfile / without flags) 1`] = ` +{ + "code": 0, + "stderr": "", + "stdout": "├─ defaultLanguageName +│ - ├─ Description: Default language mode that should be used when a package doesn't offer any insight +│ - ├─ Source: +│ - └─ Value: 'node' +│ +├─ initScope +│ - ├─ Description: Scope used when creating packages via the init command +│ - ├─ Source: WORKSPACE_ROOT/subfolder/subfolder/.yarnrc.yml +│ - └─ Value: 'my-test' +│ +└─ lastUpdateCheck + - ├─ Description: Last timestamp we checked whether new Yarn versions were available + - ├─ Source: + - └─ Value: null +", +} +`; + +exports[`Commands config test (folder with rcfile and rc in ancestor parent / as json) 1`] = ` +{ + "code": 0, + "stderr": "", + "stdout": "{"key":"defaultLanguageName","effective":"node","source":"","description":"Default language mode that should be used when a package doesn't offer any insight","type":"STRING","default":"node"} +{"key":"initScope","effective":"my-test","source":"WORKSPACE_ROOT/subfolder/subfolder/.yarnrc.yml","description":"Scope used when creating packages via the init command","type":"STRING","default":null} +{"key":"lastUpdateCheck","effective":"1555784893958","source":"WORKSPACE_ROOT/.yarnrc.yml","description":"Last timestamp we checked whether new Yarn versions were available","type":"STRING","default":null} +", +} +`; + +exports[`Commands config test (folder with rcfile and rc in ancestor parent / without flags) 1`] = ` +{ + "code": 0, + "stderr": "", + "stdout": "├─ defaultLanguageName +│ - ├─ Description: Default language mode that should be used when a package doesn't offer any insight +│ - ├─ Source: +│ - └─ Value: 'node' +│ +├─ initScope +│ - ├─ Description: Scope used when creating packages via the init command +│ - ├─ Source: WORKSPACE_ROOT/subfolder/subfolder/.yarnrc.yml +│ - └─ Value: 'my-test' +│ +└─ lastUpdateCheck + - ├─ Description: Last timestamp we checked whether new Yarn versions were available + - ├─ Source: WORKSPACE_ROOT/.yarnrc.yml + - └─ Value: '1555784893958' +", +} +`; + +exports[`Commands config test (folder with rcfile and rc in home folder / as json) 1`] = ` +{ + "code": 0, + "stderr": "", + "stdout": "{"key":"defaultLanguageName","effective":"node","source":"","description":"Default language mode that should be used when a package doesn't offer any insight","type":"STRING","default":"node"} +{"key":"initScope","effective":"my-test","source":"WORKSPACE_ROOT/.yarnrc.yml","description":"Scope used when creating packages via the init command","type":"STRING","default":null} +{"key":"lastUpdateCheck","effective":"1555784893958","source":"WORKSPACE_ROOT/.yarnrc.yml","description":"Last timestamp we checked whether new Yarn versions were available","type":"STRING","default":null} +", +} +`; + +exports[`Commands config test (folder with rcfile and rc in home folder / without flags) 1`] = ` +{ + "code": 0, + "stderr": "", + "stdout": "├─ defaultLanguageName +│ - ├─ Description: Default language mode that should be used when a package doesn't offer any insight +│ - ├─ Source: +│ - └─ Value: 'node' +│ +├─ initScope +│ - ├─ Description: Scope used when creating packages via the init command +│ - ├─ Source: WORKSPACE_ROOT/.yarnrc.yml +│ - └─ Value: 'my-test' +│ +└─ lastUpdateCheck + - ├─ Description: Last timestamp we checked whether new Yarn versions were available + - ├─ Source: WORKSPACE_ROOT/.yarnrc.yml + - └─ Value: '1555784893958' +", +} +`; + +exports[`Commands config test (folder with rcfile and rc in parent / as json) 1`] = ` +{ + "code": 0, + "stderr": "", + "stdout": "{"key":"defaultLanguageName","effective":"node","source":"","description":"Default language mode that should be used when a package doesn't offer any insight","type":"STRING","default":"node"} +{"key":"initScope","effective":"my-test","source":"WORKSPACE_ROOT/subfolder/subfolder/.yarnrc.yml","description":"Scope used when creating packages via the init command","type":"STRING","default":null} +{"key":"lastUpdateCheck","effective":"1555784893958","source":"WORKSPACE_ROOT/subfolder/.yarnrc.yml","description":"Last timestamp we checked whether new Yarn versions were available","type":"STRING","default":null} +", +} +`; + +exports[`Commands config test (folder with rcfile and rc in parent / without flags) 1`] = ` +{ + "code": 0, + "stderr": "", + "stdout": "├─ defaultLanguageName +│ - ├─ Description: Default language mode that should be used when a package doesn't offer any insight +│ - ├─ Source: +│ - └─ Value: 'node' +│ +├─ initScope +│ - ├─ Description: Scope used when creating packages via the init command +│ - ├─ Source: WORKSPACE_ROOT/subfolder/subfolder/.yarnrc.yml +│ - └─ Value: 'my-test' +│ +└─ lastUpdateCheck + - ├─ Description: Last timestamp we checked whether new Yarn versions were available + - ├─ Source: WORKSPACE_ROOT/subfolder/.yarnrc.yml + - └─ Value: '1555784893958' +", +} +`; + +exports[`Commands config test (folder with rcfile without trailing newline / as json) 1`] = ` +{ + "code": 0, + "stderr": "", + "stdout": "{"key":"defaultLanguageName","effective":"node","source":"","description":"Default language mode that should be used when a package doesn't offer any insight","type":"STRING","default":"node"} +{"key":"initScope","effective":"my-test","source":"WORKSPACE_ROOT/subfolder/subfolder/.yarnrc.yml","description":"Scope used when creating packages via the init command","type":"STRING","default":null} +{"key":"lastUpdateCheck","effective":null,"source":"","description":"Last timestamp we checked whether new Yarn versions were available","type":"STRING","default":null} +", +} +`; + +exports[`Commands config test (folder with rcfile without trailing newline / without flags) 1`] = ` +{ + "code": 0, + "stderr": "", + "stdout": "├─ defaultLanguageName +│ - ├─ Description: Default language mode that should be used when a package doesn't offer any insight +│ - ├─ Source: +│ - └─ Value: 'node' +│ +├─ initScope +│ - ├─ Description: Scope used when creating packages via the init command +│ - ├─ Source: WORKSPACE_ROOT/subfolder/subfolder/.yarnrc.yml +│ - └─ Value: 'my-test' +│ +└─ lastUpdateCheck + - ├─ Description: Last timestamp we checked whether new Yarn versions were available + - ├─ Source: + - └─ Value: null +", +} +`; + +exports[`Commands config test (folder without rcfile in ancestry / as json) 1`] = ` +{ + "code": 0, + "stderr": "", + "stdout": "{"key":"defaultLanguageName","effective":"node","source":"","description":"Default language mode that should be used when a package doesn't offer any insight","type":"STRING","default":"node"} +{"key":"initScope","effective":null,"source":"","description":"Scope used when creating packages via the init command","type":"STRING","default":null} +{"key":"lastUpdateCheck","effective":null,"source":"","description":"Last timestamp we checked whether new Yarn versions were available","type":"STRING","default":null} +", +} +`; + +exports[`Commands config test (folder without rcfile in ancestry / without flags) 1`] = ` +{ + "code": 0, + "stderr": "", + "stdout": "├─ defaultLanguageName +│ - ├─ Description: Default language mode that should be used when a package doesn't offer any insight +│ - ├─ Source: +│ - └─ Value: 'node' +│ +├─ initScope +│ - ├─ Description: Scope used when creating packages via the init command +│ - ├─ Source: +│ - └─ Value: null +│ +└─ lastUpdateCheck + - ├─ Description: Last timestamp we checked whether new Yarn versions were available + - ├─ Source: + - └─ Value: null +", +} +`; diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/commands/config.test.js b/packages/acceptance-tests/pkg-tests-specs/sources/commands/config.test.ts similarity index 54% rename from packages/acceptance-tests/pkg-tests-specs/sources/commands/config.test.js rename to packages/acceptance-tests/pkg-tests-specs/sources/commands/config.test.ts index f5903246e598..f48bfbcde35a 100644 --- a/packages/acceptance-tests/pkg-tests-specs/sources/commands/config.test.js +++ b/packages/acceptance-tests/pkg-tests-specs/sources/commands/config.test.ts @@ -1,7 +1,4 @@ -const {xfs} = require(`@yarnpkg/fslib`); -const { - fs: {writeFile}, -} = require(`pkg-tests-core`); +import {PortablePath, npath, ppath, xfs} from '@yarnpkg/fslib'; const RC_FILENAME = `.yarnrc.yml`; const SUBFOLDER = `subfolder`; @@ -10,59 +7,65 @@ const FAKE_LOCAL_APP_DATA = `LOCAL_APP_DATA`; const FAKE_WORKSPACE_ROOT = `WORKSPACE_ROOT`; const FAKE_HOME = `HOME`; -const FILTER = new RegExp([ +const FILTER = [ `initScope`, `lastUpdateCheck`, `defaultLanguageName`, -].join(`|`)); +]; -const environments = { +const environments: Record Promise> = { [`folder without rcfile in ancestry`]: async () => { // Nothing to do }, [`folder with rcfile`]: async ({path}) => { - await writeFile(`${path}/${SUBFOLDER}/${SUBFOLDER}/${RC_FILENAME}`, `initScope: my-test\n`); + await xfs.writeFilePromise(ppath.join(path, `${SUBFOLDER}/${SUBFOLDER}/${RC_FILENAME}`), `initScope: my-test\n`); }, [`folder with rcfile without trailing newline`]: async ({path}) => { - await writeFile(`${path}/${SUBFOLDER}/${SUBFOLDER}/${RC_FILENAME}`, `initScope: my-test`); + await xfs.writeFilePromise(ppath.join(path, `${SUBFOLDER}/${SUBFOLDER}/${RC_FILENAME}`), `initScope: my-test`); }, [`folder with rcfile and rc in parent`]: async ({path}) => { - await writeFile(`${path}/${SUBFOLDER}/${SUBFOLDER}/${RC_FILENAME}`, `initScope: my-test\n`); - await writeFile(`${path}/${SUBFOLDER}/${RC_FILENAME}`, `initScope: value-to-override\nlastUpdateCheck: 1555784893958\n`); + await xfs.writeFilePromise(ppath.join(path, `${SUBFOLDER}/${SUBFOLDER}/${RC_FILENAME}`), `initScope: my-test`); + await xfs.writeFilePromise(ppath.join(path, `${SUBFOLDER}/${RC_FILENAME}`), `initScope: value-to-override\nlastUpdateCheck: 1555784893958\n`); }, [`folder with rcfile and rc in ancestor parent`]: async ({path}) => { - await writeFile(`${path}/${SUBFOLDER}/${SUBFOLDER}/${RC_FILENAME}`, `initScope: my-test\n`); - await writeFile(`${path}/${RC_FILENAME}`, `initScope: value-to-override\nlastUpdateCheck: 1555784893958\n`); + await xfs.writeFilePromise(ppath.join(path, `${SUBFOLDER}/${SUBFOLDER}/${RC_FILENAME}`), `initScope: my-test`); + await xfs.writeFilePromise(ppath.join(path, `${RC_FILENAME}`), `initScope: value-to-override\nlastUpdateCheck: 1555784893958\n`); }, [`folder with rcfile and rc in home folder`]: async ({path, homePath}) => { - await writeFile(`${homePath}/${RC_FILENAME}`, `initScope: value-to-override\ndefaultLanguageName: python\n`); - await writeFile(`${path}/${RC_FILENAME}`, `initScope: my-test\nlastUpdateCheck: 1555784893958\n`); + await xfs.writeFilePromise(ppath.join(homePath, RC_FILENAME), `initScope: my-test`); + await xfs.writeFilePromise(ppath.join(path, `${RC_FILENAME}`), `initScope: my-test\nlastUpdateCheck: 1555784893958\n`); }, }; -function cleanupPlainOutput(output, path, homePath) { +function cleanupPlainOutput(output: string, path: PortablePath, homePath: PortablePath) { // Replace multiple consecutive spaces with one space. // The output of the config command is aligned according to the longest value, which probably // contains `path`. In other words, the formatting depends on the length of `path`. output = output.replace(/ +/g, ` - `); + // The JSON output contains escaped backslashes on Windows; we need to unescape them for + // them to be matched against the paths in the two following replacements. + output = output.replaceAll(`\\\\`, `\\`); + // replace the generated workspace root with a constant - output = output.replace(new RegExp(path, `g`), FAKE_WORKSPACE_ROOT); + output = output.replaceAll(npath.fromPortablePath(path), FAKE_WORKSPACE_ROOT); // replace the generated home folder with a constant - output = output.replace(new RegExp(homePath, `g`), FAKE_HOME); + output = output.replaceAll(npath.fromPortablePath(homePath), FAKE_HOME); + + // replace the windows backslashes by forward slashes + output = output.replaceAll(`\\`, `/`); // replace the default global folder with a constant output = output.replace(/[^"]+\/\.?yarn\/berry/ig, FAKE_LOCAL_APP_DATA); - output = output.split(/\n/).filter(line => { - return line.match(FILTER) !== null; - }).join(`\n`); - return output; } -function cleanupJsonOutput(output, path, homePath) { +function cleanupJsonOutput(output: string, path: PortablePath, homePath: PortablePath) { let outputObject; try { outputObject = JSON.parse(output); @@ -76,23 +79,22 @@ function cleanupJsonOutput(output, path, homePath) { // replace the generated registry server URL with a constant outputObject.npmRegistryServer.effective = FAKE_REGISTRY_URL; - const pathRegExp = new RegExp(path, `g`); - const homePathRegExp = new RegExp(homePath, `g`); + const pathN = npath.fromPortablePath(path); + const homePathN = npath.fromPortablePath(homePath); - for (const setting of Object.values(outputObject)) { - if (typeof setting.source === `string`) { - setting.source = setting.source.replace(pathRegExp, FAKE_WORKSPACE_ROOT); - setting.source = setting.source.replace(homePathRegExp, FAKE_HOME); - } + const cleanPath = (input: string) => input + .replaceAll(pathN, FAKE_WORKSPACE_ROOT) + .replaceAll(homePathN, FAKE_HOME); - if (typeof setting.default === `string`) { - setting.default = setting.default.replace(pathRegExp, FAKE_WORKSPACE_ROOT); - setting.default = setting.default.replace(homePathRegExp, FAKE_HOME); - } + for (const setting of Object.values(outputObject)) { + if (typeof setting.source === `string`) + setting.source = cleanPath(setting.source); + + if (typeof setting.default === `string`) + setting.default = cleanPath(setting.default); if (typeof setting.effective === `string`) { - setting.effective = setting.effective.replace(pathRegExp, FAKE_WORKSPACE_ROOT); - setting.effective = setting.effective.replace(homePathRegExp, FAKE_HOME); + setting.effective = cleanPath(setting.effective); } } @@ -101,8 +103,6 @@ function cleanupJsonOutput(output, path, homePath) { const options = { [`without flags`]: {cleanupStdout: cleanupPlainOutput, flags: []}, - [`showing the source`]: {cleanupStdout: cleanupPlainOutput, flags: [`--why`]}, - [`showing explanation`]: {cleanupStdout: cleanupPlainOutput, flags: [`--verbose`]}, [`as json`]: {cleanupStdout: cleanupJsonOutput, flags: [`--json`]}, }; @@ -111,7 +111,7 @@ describe(`Commands`, () => { for (const [environmentDescription, environment] of Object.entries(environments)) { for (const [optionDescription, {flags, cleanupStdout}] of Object.entries(options)) { test(`test (${environmentDescription} / ${optionDescription})`, makeTemporaryEnv({}, async ({path, run, source}) => { - const cwd = `${path}/${SUBFOLDER}/${SUBFOLDER}`; + const cwd = ppath.join(path, `${SUBFOLDER}/${SUBFOLDER}`); const homePath = await xfs.mktempPromise(); await xfs.mkdirPromise(cwd, {recursive: true}); @@ -122,7 +122,7 @@ describe(`Commands`, () => { let stderr; try { - ({code, stdout, stderr} = await run(`config`, ...flags, {cwd, env: {HOME: homePath, USERPROFILE: homePath}})); + ({code, stdout, stderr} = await run(`config`, ...flags, ...FILTER, {cwd, env: {HOME: homePath, USERPROFILE: homePath}})); } catch (error) { ({code, stdout, stderr} = error); } diff --git a/packages/plugin-essentials/sources/commands/config.ts b/packages/plugin-essentials/sources/commands/config.ts index 81f8e8f60f5e..895ca949fb5b 100644 --- a/packages/plugin-essentials/sources/commands/config.ts +++ b/packages/plugin-essentials/sources/commands/config.ts @@ -1,8 +1,8 @@ -import {BaseCommand} from '@yarnpkg/cli'; -import {Configuration, MessageName, StreamReport} from '@yarnpkg/core'; -import {miscUtils} from '@yarnpkg/core'; -import {Command, Option, Usage} from 'clipanion'; -import {inspect} from 'util'; +import {BaseCommand} from '@yarnpkg/cli'; +import {Configuration, MessageName, StreamReport, formatUtils, reportOptionDeprecations, treeUtils} from '@yarnpkg/core'; +import {npath} from '@yarnpkg/fslib'; +import {Command, Option, Usage} from 'clipanion'; +import {inspect} from 'util'; // eslint-disable-next-line arca/no-default-export export default class ConfigCommand extends BaseCommand { @@ -21,27 +21,51 @@ export default class ConfigCommand extends BaseCommand { ]], }); - verbose = Option.Boolean(`-v,--verbose`, false, { - description: `Print the setting description on top of the regular key/value information`, - }); - - why = Option.Boolean(`--why`, false, { - description: `Print the reason why a setting is set a particular way`, + noDefaults = Option.Boolean(`--no-defaults`, false, { + description: `Omit the default values from the display`, }); json = Option.Boolean(`--json`, false, { description: `Format the output as an NDJSON stream`, }); + // Legacy flags; will emit errors or warnings when used + verbose = Option.Boolean(`-v,--verbose`, {hidden: true}); + why = Option.Boolean(`--why`, {hidden: true}); + + names = Option.Rest(); + async execute() { const configuration = await Configuration.find(this.context.cwd, this.context.plugins, { strict: false, }); + const deprecationExitCode = await reportOptionDeprecations({ + configuration, + stdout: this.context.stdout, + forceError: this.json, + }, [{ + option: this.verbose, + message: `The --verbose option is deprecated, the settings' descriptions are now always displayed`, + }, { + option: this.why, + message: `The --why option is deprecated, the settings' sources are now always displayed`, + }]); + + if (deprecationExitCode !== null) + return deprecationExitCode; + + const names = this.names.length > 0 + ? [...new Set(this.names)].sort() + : [...configuration.settings.keys()].sort(); + + let trailingValue: any; + const report = await StreamReport.start({ configuration, json: this.json, stdout: this.context.stdout, + includeFooter: false, }, async report => { if (configuration.invalid.size > 0 && !this.json) { for (const [key, source] of configuration.invalid) @@ -51,62 +75,104 @@ export default class ConfigCommand extends BaseCommand { } if (this.json) { - const keys = miscUtils.sortMap(configuration.settings.keys(), key => key); + for (const name of names) { + const data = configuration.settings.get(name); + if (typeof data === `undefined`) + report.reportError(MessageName.INVALID_CONFIGURATION_KEY, `No configuration key named "${name}"`); - for (const key of keys) { - const data = configuration.settings.get(key); - - const effective = configuration.getSpecial(key, { + const effective = configuration.getSpecial(name, { hideSecrets: true, getNativePaths: true, }); - const source = configuration.sources.get(key); + const source = configuration.sources.get(name) ?? ``; + const sourceAsNativePath = source && source[0] !== `<` + ? npath.fromPortablePath(source) + : source; - if (this.verbose) { - report.reportJson({key, effective, source}); - } else { - report.reportJson({key, effective, source, ...data}); - } + report.reportJson({key: name, effective, source: sourceAsNativePath, ...data}); } } else { - const keys = miscUtils.sortMap(configuration.settings.keys(), key => key); - const maxKeyLength = keys.reduce((max, key) => Math.max(max, key.length), 0); - const inspectConfig = { breakLength: Infinity, colors: configuration.get(`enableColors`), maxArrayLength: 2, }; - if (this.why || this.verbose) { - const keysAndDescriptions = keys.map(key => { - const setting = configuration.settings.get(key); + const configTreeChildren: treeUtils.TreeMap = {}; + const configTree: treeUtils.TreeNode = {children: configTreeChildren}; + + for (const name of names) { + if (this.noDefaults && !configuration.sources.has(name)) + continue; + + const setting = configuration.settings.get(name)!; + const source = configuration.sources.get(name) ?? ``; + const value = configuration.getSpecial(name, {hideSecrets: true, getNativePaths: true}); + + const fields: treeUtils.TreeMap = { + Description: { + label: `Description`, + value: formatUtils.tuple(formatUtils.Type.MARKDOWN, {text: setting.description, format: this.cli.format(), paragraphs: false}), + }, + Source: { + label: `Source`, + value: formatUtils.tuple(source[0] === `<` ? formatUtils.Type.CODE : formatUtils.Type.PATH, source), + }, + }; + + configTreeChildren[name] = { + value: formatUtils.tuple(formatUtils.Type.CODE, name), + children: fields, + }; + + const setValueTo = (node: treeUtils.TreeMap, value: Map) => { + for (const [key, subValue] of value) { + if (subValue instanceof Map) { + const subFields: treeUtils.TreeMap = {}; + node[key] = {children: subFields}; + setValueTo(subFields, subValue); + } else { + node[key] = { + label: key, + value: formatUtils.tuple(formatUtils.Type.NO_HINT, inspect(subValue, inspectConfig)), + }; + } + } + }; + + if (value instanceof Map) { + setValueTo(fields, value); + } else { + fields.Value = { + label: `Value`, + value: formatUtils.tuple(formatUtils.Type.NO_HINT, inspect(value, inspectConfig)), + }; + } + } - if (!setting) - throw new Error(`Assertion failed: This settings ("${key}") should have been registered`); + if (names.length !== 1) + trailingValue = undefined; - const description = this.why - ? configuration.sources.get(key) || `` - : setting.description; + treeUtils.emitTree(configTree, { + configuration, + json: this.json, + stdout: this.context.stdout, + separators: 2, + }); + } + }); - return [key, description] as [string, string]; - }); + if (!this.json && typeof trailingValue !== `undefined`) { + const name = names[0]; - const maxDescriptionLength = keysAndDescriptions.reduce((max, [, description]) => { - return Math.max(max, description.length); - }, 0); + const value = inspect(configuration.getSpecial(name, {hideSecrets: true, getNativePaths: true}), { + colors: configuration.get(`enableColors`), + }); - for (const [key, description] of keysAndDescriptions) { - report.reportInfo(null, `${key.padEnd(maxKeyLength, ` `)} ${description.padEnd(maxDescriptionLength, ` `)} ${inspect(configuration.getSpecial(key, {hideSecrets: true, getNativePaths: true}), inspectConfig)}`); - } - } else { - for (const key of keys) { - report.reportInfo(null, `${key.padEnd(maxKeyLength, ` `)} ${inspect(configuration.getSpecial(key, {hideSecrets: true, getNativePaths: true}), inspectConfig)}`); - } - } - } - }); + this.context.stdout.write(`\n`); + this.context.stdout.write(`${value}\n`); + } return report.exitCode(); } diff --git a/packages/plugin-essentials/sources/commands/config/get.ts b/packages/plugin-essentials/sources/commands/config/get.ts index 270f583f6ff6..c2de800151c2 100644 --- a/packages/plugin-essentials/sources/commands/config/get.ts +++ b/packages/plugin-essentials/sources/commands/config/get.ts @@ -35,6 +35,10 @@ export default class ConfigGetCommand extends BaseCommand { ]], }); + why = Option.Boolean(`--why`, false, { + description: `Print the explanation for why a setting has its value`, + }); + json = Option.Boolean(`--json`, false, { description: `Format the output as an NDJSON stream`, }); diff --git a/packages/plugin-essentials/sources/commands/install.ts b/packages/plugin-essentials/sources/commands/install.ts index 85f10b1e9ddb..b25271c19d87 100644 --- a/packages/plugin-essentials/sources/commands/install.ts +++ b/packages/plugin-essentials/sources/commands/install.ts @@ -1,11 +1,11 @@ -import {BaseCommand, WorkspaceRequiredError} from '@yarnpkg/cli'; -import {Configuration, Cache, MessageName, Project, ReportError, StreamReport, formatUtils, InstallMode, execUtils, structUtils, LEGACY_PLUGINS, ConfigurationValueMap, YarnVersion, httpUtils} from '@yarnpkg/core'; -import {xfs, ppath, Filename, PortablePath} from '@yarnpkg/fslib'; -import {parseSyml, stringifySyml} from '@yarnpkg/parsers'; -import CI from 'ci-info'; -import {Command, Option, Usage, UsageError} from 'clipanion'; -import semver from 'semver'; -import * as t from 'typanion'; +import {BaseCommand, WorkspaceRequiredError} from '@yarnpkg/cli'; +import {Configuration, Cache, MessageName, Project, ReportError, StreamReport, formatUtils, InstallMode, execUtils, structUtils, LEGACY_PLUGINS, ConfigurationValueMap, YarnVersion, httpUtils, reportOptionDeprecations} from '@yarnpkg/core'; +import {xfs, ppath, Filename, PortablePath} from '@yarnpkg/fslib'; +import {parseSyml, stringifySyml} from '@yarnpkg/parsers'; +import CI from 'ci-info'; +import {Command, Option, Usage, UsageError} from 'clipanion'; +import semver from 'semver'; +import * as t from 'typanion'; const LOCKFILE_MIGRATION_RULES: Array<{ selector: (version: number) => boolean; @@ -130,122 +130,69 @@ export default class YarnCommand extends BaseCommand { // Google App Engine const isGCP = !!process.env.FUNCTION_TARGET || !!process.env.GOOGLE_RUNTIME; - const reportDeprecation = async (message: string, {error}: {error: boolean}) => { - const deprecationReport = await StreamReport.start({ - configuration, - stdout: this.context.stdout, - includeFooter: false, - }, async report => { - if (error) { - report.reportError(MessageName.DEPRECATED_CLI_SETTINGS, message); - } else { - report.reportWarning(MessageName.DEPRECATED_CLI_SETTINGS, message); - } - }); - - if (deprecationReport.hasErrors()) { - return deprecationReport.exitCode(); - } else { - return null; - } - }; - - // The ignoreEngines flag isn't implemented at the moment. I'm still - // considering how it should work in the context of plugins - would it - // make sense to allow them (or direct dependencies) to define new - // "engine check"? Since it has implications regarding the architecture, - // I prefer to postpone the decision to later. Also it wouldn't be a flag, - // it would definitely be a configuration setting. - if (typeof this.ignoreEngines !== `undefined`) { - const exitCode = await reportDeprecation(`The --ignore-engines option is deprecated; engine checking isn't a core feature anymore`, { - error: !CI.VERCEL, - }); - - if (exitCode !== null) { - return exitCode; - } - } - - // The registry flag isn't supported anymore because it makes little sense - // to use a registry for a single install. You instead want to configure it - // for all installs inside a project, so through the .yarnrc.yml file. Note - // that if absolutely necessary, the old behavior can be emulated by adding - // the YARN_NPM_REGISTRY_SERVER variable to the environment. - if (typeof this.registry !== `undefined`) { - const exitCode = await reportDeprecation(`The --registry option is deprecated; prefer setting npmRegistryServer in your .yarnrc.yml file`, { - error: false, - }); - - if (exitCode !== null) { - return exitCode; - } - } - - // The preferOffline flag doesn't make much sense with our architecture. - // It would require the fetchers to also act as resolvers, which is - // doable but quirky. Since a similar behavior is available via the - // --cached flag in yarn add, I prefer to move it outside of the core and - // let someone implement this "resolver-that-reads-the-cache" logic. - if (typeof this.preferOffline !== `undefined`) { - const exitCode = await reportDeprecation(`The --prefer-offline flag is deprecated; use the --cached flag with 'yarn add' instead`, { - error: !CI.VERCEL, - }); - - if (exitCode !== null) { - return exitCode; - } - } - - // Since the production flag would yield a different lockfile than the - // regular installs, it's not part of the regular `install` command anymore. - // Instead, we expect users to use it with `yarn workspaces focus` (which can - // be used even outside of monorepos). - if (typeof this.production !== `undefined`) { - const exitCode = await reportDeprecation(`The --production option is deprecated on 'install'; use 'yarn workspaces focus' instead`, { - error: true, - }); - - if (exitCode !== null) { - return exitCode; - } - } - - // Yarn 2 isn't interactive during installs anyway, so there's no real point - // to this flag at the moment. - if (typeof this.nonInteractive !== `undefined`) { - const exitCode = await reportDeprecation(`The --non-interactive option is deprecated`, { - error: !isGCP, - }); - - if (exitCode !== null) { - return exitCode; - } - } - - // We want to prevent people from using --frozen-lockfile - // Note: it's been deprecated because we're now locking more than just the - // lockfile - for example the PnP artifacts will also be locked. - if (typeof this.frozenLockfile !== `undefined`) { - await reportDeprecation(`The --frozen-lockfile option is deprecated; use --immutable and/or --immutable-cache instead`, { - error: false, - }); - - this.immutable = this.frozenLockfile; - } - - // We also want to prevent them from using --cache-folder - // Note: it's been deprecated because the cache folder should be set from - // the settings. Otherwise there would be a very high chance that multiple - // Yarn commands would use different caches, causing unexpected behaviors. - if (typeof this.cacheFolder !== `undefined`) { - const exitCode = await reportDeprecation(`The cache-folder option has been deprecated; use rc settings instead`, { - error: !CI.NETLIFY, - }); - - if (exitCode !== null) { - return exitCode; - } - } + const deprecationExitCode = await reportOptionDeprecations({ + configuration, + stdout: this.context.stdout, + }, [{ + // The ignoreEngines flag isn't implemented at the moment. I'm still + // considering how it should work in the context of plugins - would it + // make sense to allow them (or direct dependencies) to define new + // "engine check"? Since it has implications regarding the architecture, + // I prefer to postpone the decision to later. Also it wouldn't be a flag, + // it would definitely be a configuration setting. + option: this.ignoreEngines, + message: `The --ignore-engines option is deprecated; engine checking isn't a core feature anymore`, + error: !CI.VERCEL, + }, { + // The registry flag isn't supported anymore because it makes little sense + // to use a registry for a single install. You instead want to configure it + // for all installs inside a project, so through the .yarnrc.yml file. Note + // that if absolutely necessary, the old behavior can be emulated by adding + // the YARN_NPM_REGISTRY_SERVER variable to the environment. + option: this.registry, + message: `The --registry option is deprecated; prefer setting npmRegistryServer in your .yarnrc.yml file`, + }, { + // The preferOffline flag doesn't make much sense with our architecture. + // It would require the fetchers to also act as resolvers, which is + // doable but quirky. Since a similar behavior is available via the + // --cached flag in yarn add, I prefer to move it outside of the core and + // let someone implement this "resolver-that-reads-the-cache" logic. + option: this.preferOffline, + message: `The --prefer-offline flag is deprecated; use the --cached flag with 'yarn add' instead`, + error: !CI.VERCEL, + }, { + // Since the production flag would yield a different lockfile than the + // regular installs, it's not part of the regular `install` command anymore. + // Instead, we expect users to use it with `yarn workspaces focus` (which can + // be used even outside of monorepos). + option: this.production, + message: `The --production option is deprecated on 'install'; use 'yarn workspaces focus' instead`, + error: true, + }, { + // Yarn 2+ isn't interactive during installs anyway, so there's no real point + // to this flag at the moment. + option: this.nonInteractive, + message: `The --non-interactive option is deprecated`, + error: !isGCP, + }, { + // We want to prevent people from using --frozen-lockfile + // Note: it's been deprecated because we're now locking more than just the + // lockfile - for example the PnP artifacts will also be locked. + option: this.frozenLockfile, + message: `The --frozen-lockfile option is deprecated; use --immutable and/or --immutable-cache instead`, + callback: () => this.immutable = this.frozenLockfile, + }, { + // We also want to prevent them from using --cache-folder + // Note: it's been deprecated because the cache folder should be set from + // the settings. Otherwise there would be a very high chance that multiple + // Yarn commands would use different caches, causing unexpected behaviors. + option: this.cacheFolder, + message: `The cache-folder option has been deprecated; use rc settings instead`, + error: !CI.NETLIFY, + }]); + + if (deprecationExitCode !== null) + return deprecationExitCode; const updateMode = this.mode === InstallMode.UpdateLockfile; if (updateMode && (this.immutable || this.immutableCache)) diff --git a/packages/plugin-nm/sources/index.ts b/packages/plugin-nm/sources/index.ts index 986618e7c786..4287af526c57 100644 --- a/packages/plugin-nm/sources/index.ts +++ b/packages/plugin-nm/sources/index.ts @@ -27,7 +27,7 @@ const plugin: Plugin = { }, configuration: { nmHoistingLimits: { - description: `Prevent packages to be hoisted past specific levels`, + description: `Prevents packages to be hoisted past specific levels`, type: SettingsType.STRING, values: [ NodeModulesHoistingLimits.WORKSPACES, @@ -37,7 +37,7 @@ const plugin: Plugin = { default: NodeModulesHoistingLimits.NONE, }, nmMode: { - description: `If set to "hardlinks-local" Yarn will utilize hardlinks to reduce disk space consumption inside "node_modules" directories. With "hardlinks-global" Yarn will use global content addressable storage to reduce "node_modules" size across all the projects using this option.`, + description: `Defines in which measure Yarn must use hardlinks and symlinks when generated \`node_modules\` directories.`, type: SettingsType.STRING, values: [ NodeModulesMode.CLASSIC, @@ -47,7 +47,7 @@ const plugin: Plugin = { default: NodeModulesMode.CLASSIC, }, nmSelfReferences: { - description: `If set to 'false' the workspace will not be allowed to require itself and corresponding self-referencing symlink will not be created`, + description: `Defines whether the linker should generate self-referencing symlinks for workspaces.`, type: SettingsType.BOOLEAN, default: true, }, diff --git a/packages/yarnpkg-core/sources/Configuration.ts b/packages/yarnpkg-core/sources/Configuration.ts index 99f7f711e32e..1cbeee94c9c3 100644 --- a/packages/yarnpkg-core/sources/Configuration.ts +++ b/packages/yarnpkg-core/sources/Configuration.ts @@ -759,8 +759,8 @@ function parseSingleValue(configuration: Configuration, path: string, valueBase: // singleValue's source should be a single file path, if it exists const source = configUtils.getSource(valueBase); - if (source) - cwd = ppath.resolve(source as PortablePath, `..`); + if (source && source[0] !== `<`) + cwd = ppath.dirname(source as PortablePath); return ppath.resolve(cwd, npath.toPortablePath(valueWithReplacedVariables)); } @@ -909,20 +909,34 @@ function transformConfiguration(rawValue: unknown, definition: SettingsDefinitio } if (definition.type === SettingsType.MAP && rawValue instanceof Map) { + if (rawValue.size === 0) + return undefined; + const newValue: Map = new Map(); - for (const [key, value] of rawValue.entries()) - newValue.set(key, transformConfiguration(value, definition.valueDefinition, transforms)); + for (const [key, value] of rawValue.entries()) { + const transformedValue = transformConfiguration(value, definition.valueDefinition, transforms); + if (typeof transformedValue !== `undefined`) { + newValue.set(key, transformedValue); + } + } return newValue; } if (definition.type === SettingsType.SHAPE && rawValue instanceof Map) { + if (rawValue.size === 0) + return undefined; + const newValue: Map = new Map(); for (const [key, value] of rawValue.entries()) { const propertyDefinition = definition.properties[key]; - newValue.set(key, transformConfiguration(value, propertyDefinition, transforms)); + + const transformedValue = transformConfiguration(value, propertyDefinition, transforms); + if (typeof transformedValue !== `undefined`) { + newValue.set(key, transformedValue); + } } return newValue; @@ -1577,8 +1591,14 @@ export class Configuration { const definition = this.settings.get(key); if (!definition) { const homeFolder = folderUtils.getHomeFolder(); - const rcFileFolder = ppath.resolve(source as PortablePath, `..`); - const isHomeRcFile = homeFolder === rcFileFolder; + + const rcFileFolder = source[0] !== `<` + ? ppath.dirname(source as PortablePath) + : null; + + const isHomeRcFile = rcFileFolder !== null + ? homeFolder === rcFileFolder + : false; if (strict && !isHomeRcFile) { throw new UsageError(`Unrecognized or legacy configuration settings found: ${key} - run "yarn config -v" to see the list of settings supported in Yarn`); diff --git a/packages/yarnpkg-core/sources/StreamReport.ts b/packages/yarnpkg-core/sources/StreamReport.ts index a1b8b320b9e3..366e860bbc9f 100644 --- a/packages/yarnpkg-core/sources/StreamReport.ts +++ b/packages/yarnpkg-core/sources/StreamReport.ts @@ -111,6 +111,44 @@ export function formatNameWithHyperlink(name: MessageName | null, {configuration return formatUtils.applyHyperlink(configuration, code, href); } +/** + * @internal + */ +export async function reportOptionDeprecations({configuration, stdout, forceError}: {configuration: Configuration, stdout: Writable, forceError?: boolean}, checks: Array<{option: unknown, message: string, error?: boolean, callback?: () => void}>) { + const deprecationReport = await StreamReport.start({ + configuration, + stdout, + includeFooter: false, + }, async report => { + let hasWarnings = false; + let hasErrors = false; + + for (const check of checks) { + if (typeof check.option !== `undefined`) { + if (check.error || forceError) { + hasErrors = true; + report.reportError(MessageName.DEPRECATED_CLI_SETTINGS, check.message); + } else { + hasWarnings = true; + report.reportWarning(MessageName.DEPRECATED_CLI_SETTINGS, check.message); + } + + check.callback?.(); + } + } + + if (hasWarnings && !hasErrors) { + report.reportSeparator(); + } + }); + + if (deprecationReport.hasErrors()) { + return deprecationReport.exitCode(); + } else { + return null; + } +} + export class StreamReport extends Report { static async start(opts: StreamReportOptions, cb: (report: StreamReport) => Promise) { const report = new this(opts); diff --git a/packages/yarnpkg-core/sources/index.ts b/packages/yarnpkg-core/sources/index.ts index cf5d787b61ea..b82e871aa7f5 100644 --- a/packages/yarnpkg-core/sources/index.ts +++ b/packages/yarnpkg-core/sources/index.ts @@ -32,7 +32,7 @@ export type {PeerRequirement} export {LOCKFILE_VERSION, Project, InstallMode} from './Project'; export {ReportError, Report} from './Report'; export type {Resolver, ResolveOptions, MinimalResolveOptions} from './Resolver'; -export {StreamReport} from './StreamReport'; +export {StreamReport, reportOptionDeprecations} from './StreamReport'; export {TelemetryManager} from './TelemetryManager'; export {ThrowReport} from './ThrowReport'; export {VirtualFetcher} from './VirtualFetcher'; diff --git a/packages/yarnpkg-core/sources/treeUtils.ts b/packages/yarnpkg-core/sources/treeUtils.ts index 888fd9e93950..af6e0d701b14 100644 --- a/packages/yarnpkg-core/sources/treeUtils.ts +++ b/packages/yarnpkg-core/sources/treeUtils.ts @@ -45,7 +45,7 @@ export function treeNodeToTreeify(printTree: TreeNode, {configuration}: {configu if (finalParts.length === 0) finalParts.push(formatUtils.applyStyle(configuration, `${key}`, formatUtils.Style.BOLD)); - const finalLabel = finalParts.join(`: `); + const finalLabel = finalParts.join(`: `).trim(); // The library we use, treeify, doesn't support having multiple nodes with // the same label. To work around that, we prefix each label with a unique @@ -128,7 +128,7 @@ export function emitTree(tree: TreeNode, {configuration, stdout, json, separator // Another one for the second level fields. We run it twice because in some pathological cases the regex matches would if (separators >= 2) for (let t = 0; t < 2; ++t) - treeOutput = treeOutput.replace(/^([│ ].{2}[├│ ].{2}[^\n]+\n)(([│ ]).{2}[├└].{2}[^\n]*\n[│ ].{2}[│ ].{2}[├└]─)/gm, `$1$3 │\n$2`).replace(/^│\n/, ``); + treeOutput = treeOutput.replace(/^([│ ].{2}[├│ ].{2}[^\n]+\n)(([│ ]).{2}[├└].{2}[^\n]*\n[│ ].{2}[│ ].{2}[├└]─)/gm, `$1$3 │ (\\n)?\n$2`).replace(/^│\n/, ``); if (separators >= 3) throw new Error(`Only the first two levels are accepted by treeUtils.emitTree`);