diff --git a/.pnp.js b/.pnp.js index 9d9158487eb..fee3b034550 100755 --- a/.pnp.js +++ b/.pnp.js @@ -9627,6 +9627,10 @@ function $$SETUP_STATE(hydrateRuntimeState) { "@berry/pnpify", "workspace:packages/berry-pnpify" ], + [ + "js-yaml", + "npm:3.12.2" + ], [ "pegjs", "npm:0.10.0" diff --git a/.yarnrc b/.yarnrc index 0eb9d296a42..a8e9950f565 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,12 +1,16 @@ +## General settings + init-scope: berry npm-publish-access: public +yarn-path: scripts/run-yarn.js + +## List of plugins enabled in the current repository + plugins: - scripts/plugin-exec.js - scripts/plugin-stage.js - scripts/plugin-typescript.js - scripts/plugin-version.js - scripts/plugin-workspace-tools.js - -yarn-path: ./scripts/run-yarn.js diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/auth.test.js b/packages/acceptance-tests/pkg-tests-specs/sources/auth.test.js index ffc6f37c25c..293810134c4 100644 --- a/packages/acceptance-tests/pkg-tests-specs/sources/auth.test.js +++ b/packages/acceptance-tests/pkg-tests-specs/sources/auth.test.js @@ -43,7 +43,7 @@ describe(`Auth tests`, () => { dependencies: {[`no-deps`]: `1.0.0`}, }, async ({path, run, source}) => { - await writeFile(`${path}/.yarnrc`, `npmAlwaysAuth true\n`); + await writeFile(`${path}/.yarnrc`, `npmAlwaysAuth: true\n`); await expect(run(`install`)).rejects.toThrowError(/No authentication configured for request/); }, @@ -57,7 +57,7 @@ describe(`Auth tests`, () => { dependencies: {[`private-package`]: `1.0.0`}, }, async ({path, run, source}) => { - await writeFile(`${path}/.yarnrc`, `npmAuthToken "${AUTH_TOKEN}"\n`); + await writeFile(`${path}/.yarnrc`, `npmAuthToken: "${AUTH_TOKEN}"\n`); // Rejected by 401 error from registry so no validation on the error message await expect(run(`install`)).rejects.toThrow(); @@ -72,7 +72,7 @@ describe(`Auth tests`, () => { dependencies: {[`@private/package`]: `1.0.0`}, }, async ({path, run, source}) => { - await writeFile(`${path}/.yarnrc`, `npmAuthToken "${AUTH_TOKEN}"\n`); + await writeFile(`${path}/.yarnrc`, `npmAuthToken: "${AUTH_TOKEN}"\n`); await run(`install`); @@ -91,7 +91,7 @@ describe(`Auth tests`, () => { dependencies: {[`private-package`]: `1.0.0`}, }, async ({path, run, source}) => { - await writeFile(`${path}/.yarnrc`, `npmAuthToken "${AUTH_TOKEN}"\nnpmAlwaysAuth true\n`); + await writeFile(`${path}/.yarnrc`, `npmAuthToken: "${AUTH_TOKEN}"\nnpmAlwaysAuth: true\n`); await run(`install`); @@ -110,7 +110,7 @@ describe(`Auth tests`, () => { dependencies: {[`private-package`]: `1.0.0`}, }, async ({path, run, source}) => { - await writeFile(`${path}/.yarnrc`, `npmAuthIdent "${AUTH_IDENT}"\n`); + await writeFile(`${path}/.yarnrc`, `npmAuthIdent: "${AUTH_IDENT}"\n`); // Rejected by 401 error from registry so no validation on the error message await expect(run(`install`)).rejects.toThrow(); @@ -125,7 +125,7 @@ describe(`Auth tests`, () => { dependencies: {[`@private/package`]: `1.0.0`}, }, async ({path, run, source}) => { - await writeFile(`${path}/.yarnrc`, `npmAuthIdent "${AUTH_IDENT}"\n`); + await writeFile(`${path}/.yarnrc`, `npmAuthIdent: "${AUTH_IDENT}"\n`); await run(`install`); @@ -144,7 +144,7 @@ describe(`Auth tests`, () => { dependencies: {[`private-package`]: `1.0.0`}, }, async ({path, run, source}) => { - await writeFile(`${path}/.yarnrc`, `npmAuthIdent "${AUTH_IDENT}"\nnpmAlwaysAuth true\n`); + await writeFile(`${path}/.yarnrc`, `npmAuthIdent: "${AUTH_IDENT}"\nnpmAlwaysAuth: true\n`); await run(`install`); @@ -163,7 +163,7 @@ describe(`Auth tests`, () => { dependencies: {[`private-package`]: `1.0.0`}, }, async ({path, run, source}) => { - await writeFile(`${path}/.yarnrc`, `npmAuthToken "${INVALID_AUTH_TOKEN}"\nnpmAlwaysAuth true\n`); + await writeFile(`${path}/.yarnrc`, `npmAuthToken: "${INVALID_AUTH_TOKEN}"\nnpmAlwaysAuth: true\n`); await expect(run(`install`)).rejects.toThrow(); }, @@ -177,7 +177,7 @@ describe(`Auth tests`, () => { dependencies: {[`private-package`]: `1.0.0`}, }, async ({path, run, source}) => { - await writeFile(`${path}/.yarnrc`, `npmAuthIdent "${INVALID_AUTH_IDENT}"\nnpmAlwaysAuth true\n`); + await writeFile(`${path}/.yarnrc`, `npmAuthIdent: "${INVALID_AUTH_IDENT}"\nnpmAlwaysAuth: true\n`); await expect(run(`install`)).rejects.toThrow(); }, 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.js index 666b974fd4a..a469c37df05 100644 --- a/packages/acceptance-tests/pkg-tests-specs/sources/commands/config.test.js +++ b/packages/acceptance-tests/pkg-tests-specs/sources/commands/config.test.js @@ -14,22 +14,22 @@ const environments = { // Nothing to do }, [`folder with rcfile`]: async ({path}) => { - await writeFile(`${path}/${SUBFOLDER}/${SUBFOLDER}/${RC_FILENAME}`, `init-scope berry-test\n`); + await writeFile(`${path}/${SUBFOLDER}/${SUBFOLDER}/${RC_FILENAME}`, `init-scope: berry-test\n`); }, [`folder with rcfile without trailing newline`]: async ({path}) => { - await writeFile(`${path}/${SUBFOLDER}/${SUBFOLDER}/${RC_FILENAME}`, `init-scope berry-test`); + await writeFile(`${path}/${SUBFOLDER}/${SUBFOLDER}/${RC_FILENAME}`, `init-scope: berry-test`); }, [`folder with rcfile and rc in parent`]: async ({path}) => { - await writeFile(`${path}/${SUBFOLDER}/${SUBFOLDER}/${RC_FILENAME}`, `init-scope berry-test\n`); - await writeFile(`${path}/${SUBFOLDER}/${RC_FILENAME}`, `init-scope value-to-override\nlastUpdateCheck 1555784893958\n`); + await writeFile(`${path}/${SUBFOLDER}/${SUBFOLDER}/${RC_FILENAME}`, `init-scope: berry-test\n`); + await writeFile(`${path}/${SUBFOLDER}/${RC_FILENAME}`, `init-scope: value-to-override\nlast-update-check: 1555784893958\n`); }, [`folder with rcfile and rc in ancestor parent`]: async ({path}) => { - await writeFile(`${path}/${SUBFOLDER}/${SUBFOLDER}/${RC_FILENAME}`, `init-scope berry-test\n`); - await writeFile(`${path}/${RC_FILENAME}`, `init-scope value-to-override\nlastUpdateCheck 1555784893958\n`); + await writeFile(`${path}/${SUBFOLDER}/${SUBFOLDER}/${RC_FILENAME}`, `init-scope: berry-test\n`); + await writeFile(`${path}/${RC_FILENAME}`, `init-scope: value-to-override\nlast-update-check: 1555784893958\n`); }, [`folder with rcfile and rc in home folder`]: async ({path, homePath}) => { - await writeFile(`${homePath}/${RC_FILENAME}`, `init-scope value-to-override\ndefaultLanguageName python\n`); - await writeFile(`${path}/${RC_FILENAME}`, `init-scope berry-test\nlastUpdateCheck 1555784893958\n`); + await writeFile(`${homePath}/${RC_FILENAME}`, `init-scope: value-to-override\ndefault-language-name: python\n`); + await writeFile(`${path}/${RC_FILENAME}`, `init-scope: berry-test\nlast-update-check: 1555784893958\n`); }, }; diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/commands/npm/__snapshots__/login.test.js.snap b/packages/acceptance-tests/pkg-tests-specs/sources/commands/npm/__snapshots__/login.test.js.snap new file mode 100644 index 00000000000..4bdc82b8da4 --- /dev/null +++ b/packages/acceptance-tests/pkg-tests-specs/sources/commands/npm/__snapshots__/login.test.js.snap @@ -0,0 +1,71 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Commands npm login it should login a user with OTP setup 1`] = ` +"npmAuthToken: 686159dc-64b3-413e-a244-2de2b8d1c36f +" +`; + +exports[`Commands npm login it should login a user with OTP setup 2`] = ` +Object { + "code": 0, + "stderr": "", + "stdout": "➤ YN0000: Successfully logged in +➤ YN0000: Done +", +} +`; + +exports[`Commands npm login it should login a user with OTP to a specific scope 1`] = ` +"init-scope: berry-test + +npmScopes: + testScope: + npmAuthToken: 686159dc-64b3-413e-a244-2de2b8d1c36f + npmRegistryServer: \\"http://yarn.test.registry\\" +" +`; + +exports[`Commands npm login it should login a user with OTP to a specific scope 2`] = ` +Object { + "code": 0, + "stderr": "", + "stdout": "➤ YN0000: Successfully logged in +➤ YN0000: Done +", +} +`; + +exports[`Commands npm login it should login a user with no OTP setup 1`] = ` +"npmAuthToken: 316158de-64b3-413e-a244-2de2b8d1c80f +" +`; + +exports[`Commands npm login it should login a user with no OTP setup 2`] = ` +Object { + "code": 0, + "stderr": "", + "stdout": "➤ YN0000: Successfully logged in +➤ YN0000: Done +", +} +`; + +exports[`Commands npm login it should login a user with no OTP setup to a specific scope 1`] = ` +"init-scope: berry-test + +npmScopes: + testScope: + npmAuthToken: 316158de-64b3-413e-a244-2de2b8d1c80f + npmRegistryServer: \\"http://yarn.test.registry\\" +" +`; + +exports[`Commands npm login it should login a user with no OTP setup to a specific scope 2`] = ` +Object { + "code": 0, + "stderr": "", + "stdout": "➤ YN0000: Successfully logged in +➤ YN0000: Done +", +} +`; diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/commands/npm/__snapshots__/whoami.test.js.snap b/packages/acceptance-tests/pkg-tests-specs/sources/commands/npm/__snapshots__/whoami.test.js.snap new file mode 100644 index 00000000000..beeeb6baaca --- /dev/null +++ b/packages/acceptance-tests/pkg-tests-specs/sources/commands/npm/__snapshots__/whoami.test.js.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Commands npm whoami it should print the npm registry username for a given scope 1`] = ` +Object { + "code": 0, + "stderr": "", + "stdout": "➤ YN0000: username +➤ YN0000: Done +", +} +`; + +exports[`Commands npm whoami it should print the npm registry username when config has a valid npmAuthIdent 1`] = ` +Object { + "code": 0, + "stderr": "", + "stdout": "➤ YN0000: username +➤ YN0000: Done +", +} +`; + +exports[`Commands npm whoami it should print the npm registry username when config has a valid npmAuthToken 1`] = ` +Object { + "code": 0, + "stderr": "", + "stdout": "➤ YN0000: username +➤ YN0000: Done +", +} +`; diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/commands/npm/login.test.js b/packages/acceptance-tests/pkg-tests-specs/sources/commands/npm/login.test.js new file mode 100644 index 00000000000..05d2a93f342 --- /dev/null +++ b/packages/acceptance-tests/pkg-tests-specs/sources/commands/npm/login.test.js @@ -0,0 +1,191 @@ +const { + fs: {createTemporaryFolder, readFile, writeFile}, + tests: {startPackageServer} +} = require('pkg-tests-core'); + +const SPEC_RC_FILENAME = `.spec-yarnrc`; +const FAKE_REGISTRY_URL = `http://yarn.test.registry`; + +function cleanupFileContent(fileContent) { + return fileContent.replace(/http:\/\/localhost:\d+/g, FAKE_REGISTRY_URL); +} + +describe(`Commands`, () => { + describe(`npm login`, () => { + test( + `it should login a user with no OTP setup`, + makeTemporaryEnv({}, async ({ path, run, source }) => { + const homePath = await createTemporaryFolder(); + await writeFile(`${homePath}/${SPEC_RC_FILENAME}`, ``); + + let code; + let stdout; + let stderr; + + try { + ({ code, stdout, stderr } = await run(`npm`, `login`, { + env: { + HOME: homePath, + USERPROFILE: homePath, + TEST_NPM_USER: `anotherTestUser`, + TEST_NPM_PASSWORD: `password123`, + YARN_RC_FILENAME: SPEC_RC_FILENAME, + } + })); + } catch (error) { + ({ code, stdout, stderr } = error); + } + + const finalRcFileContent = await readFile(`${homePath}/${SPEC_RC_FILENAME}`, `utf8`); + const cleanFileContent = cleanupFileContent(finalRcFileContent); + + expect(cleanFileContent).toMatchSnapshot(); + expect({code, stdout, stderr}).toMatchSnapshot(); + }) + ); + + test( + `it should login a user with OTP setup`, + makeTemporaryEnv({}, async ({ path, run, source }) => { + const homePath = await createTemporaryFolder(); + + await writeFile(`${homePath}/${SPEC_RC_FILENAME}`, ``); + + let code; + let stdout; + let stderr; + + try { + ({ code, stdout, stderr } = await run(`npm`, `login`, { + env: { + HOME: homePath, + USERPROFILE: homePath, + TEST_NPM_USER: `testUser`, + TEST_NPM_PASSWORD: `password`, + TEST_NPM_2FA_TOKEN: `1234`, + YARN_RC_FILENAME: SPEC_RC_FILENAME, + } + })); + } catch (error) { + ({code, stdout, stderr} = error); + } + + const finalRcFileContent = await readFile(`${homePath}/${SPEC_RC_FILENAME}`, `utf8`); + const cleanFileContent = cleanupFileContent(finalRcFileContent); + + expect(cleanFileContent).toMatchSnapshot(); + expect({code, stdout, stderr}).toMatchSnapshot(); + }) + ); + + test( + `it should throw an error when credentials are incorrect`, + makeTemporaryEnv({}, async ({ path, run, source }) => { + await expect( + run(`npm`, `login`, { + env: { + TEST_NPM_USER: `anotherTestUser`, + TEST_NPM_PASSWORD: `incorrect password` + } + }) + ).rejects.toThrowError(/Invalid Authentication/); + }) + ); + + test( + `it should throw an error with incorrect OTP`, + makeTemporaryEnv({}, async ({ path, run, source }) => { + await expect( + run(`npm`, `login`, { + env: { + TEST_NPM_USER: `testUser`, + TEST_NPM_PASSWORD: `password`, + TEST_NPM_2FA_TOKEN: `incorrect OTP`, + } + }) + ).rejects.toThrowError(/Invalid Authentication/); + }) + ); + + test( + `it should login a user with no OTP setup to a specific scope`, + makeTemporaryEnv({}, async ({ path, run, source }) => { + const url = startPackageServer(); + const homePath = await createTemporaryFolder(); + + await writeFile(`${homePath}/${SPEC_RC_FILENAME}`, [ + `init-scope: berry-test\n`, + `npmScopes:\n`, + ` testScope:\n`, + ` npmRegistryServer: "${url}"\n`, + ].join(``)); + + let code; + let stdout; + let stderr; + + try { + ({ code, stdout, stderr } = await run(`npm`, `login`, `--scope`, `testScope`, { + env: { + HOME: homePath, + USERPROFILE: homePath, + TEST_NPM_USER: `anotherTestUser`, + TEST_NPM_PASSWORD: `password123`, + YARN_RC_FILENAME: SPEC_RC_FILENAME, + } + })); + } catch (error) { + ({ code, stdout, stderr } = error); + } + + const finalRcFileContent = await readFile(`${homePath}/${SPEC_RC_FILENAME}`, `utf8`); + const cleanFileContent = cleanupFileContent(finalRcFileContent); + + expect(cleanFileContent).toMatchSnapshot(); + expect({code, stdout, stderr}).toMatchSnapshot(); + }) + ); + + test( + `it should login a user with OTP to a specific scope`, + makeTemporaryEnv({}, async ({ path, run, source }) => { + const url = startPackageServer(); + const homePath = await createTemporaryFolder(); + + const initialRcFileContent = [ + `init-scope: berry-test\n`, + `npmScopes:\n`, + ` testScope:\n`, + ` npmRegistryServer: "${url}"\n`, + ].join(``); + + await writeFile(`${homePath}/${SPEC_RC_FILENAME}`, initialRcFileContent); + + let code; + let stdout; + let stderr; + + try { + ({ code, stdout, stderr } = await run(`npm`, `login`, `--scope`, `testScope`, { + env: { + HOME: homePath, + USERPROFILE: homePath, + TEST_NPM_USER: `testUser`, + TEST_NPM_PASSWORD: `password`, + TEST_NPM_2FA_TOKEN: `1234`, + YARN_RC_FILENAME: SPEC_RC_FILENAME + } + })); + } catch (error) { + ({ code, stdout, stderr } = error); + } + + const finalRcFileContent = await readFile(`${homePath}/${SPEC_RC_FILENAME}`, `utf8`); + const cleanFileContent = cleanupFileContent(finalRcFileContent); + + expect(cleanFileContent).toMatchSnapshot(); + expect({code, stdout, stderr}).toMatchSnapshot(); + }) + ); + }); +}); diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/commands/npm/whoami.test.js b/packages/acceptance-tests/pkg-tests-specs/sources/commands/npm/whoami.test.js new file mode 100644 index 00000000000..623a94e2f28 --- /dev/null +++ b/packages/acceptance-tests/pkg-tests-specs/sources/commands/npm/whoami.test.js @@ -0,0 +1,122 @@ +const { + fs: {createTemporaryFolder, readFile, writeFile}, + tests: {startPackageServer} +} = require('pkg-tests-core'); + +const AUTH_TOKEN = `686159dc-64b3-413e-a244-2de2b8d1c36f`; +const AUTH_IDENT = `dXNlcm5hbWU6YSB2ZXJ5IHNlY3VyZSBwYXNzd29yZA==`; + +const INVALID_AUTH_TOKEN = `a24cb960-e6a5-45fc-b9ab-0f9fe0aaae57`; +const INVALID_AUTH_IDENT = `dXNlcm5hbWU6bm90IHRoZSByaWdodCBwYXNzd29yZA==`; // username:not the right password + +describe(`Commands`, () => { + describe(`npm whoami`, () => { + test( + `it should print the npm registry username when config has a valid npmAuthToken`, + makeTemporaryEnv({}, async ({ path, run, source }) => { + await writeFile(`${path}/.yarnrc`, `npmAuthToken: "${AUTH_TOKEN}"\n`); + + let code; + let stdout; + let stderr; + + try { + ({code, stdout, stderr} = await run(`npm`, `whoami`)); + } catch (error) { + ({code, stdout, stderr} = error); + } + + expect({code, stdout, stderr}).toMatchSnapshot(); + }) + ); + + test( + `it should print the npm registry username when config has a valid npmAuthIdent`, + makeTemporaryEnv({}, async ({ path, run, source }) => { + await writeFile(`${path}/.yarnrc`, `npmAuthIdent: "${AUTH_IDENT}"\n`); + + let code; + let stdout; + let stderr; + + try { + ({code, stdout, stderr} = await run(`npm`, `whoami`)); + } catch (error) { + ({code, stdout, stderr} = error); + } + + expect({code, stdout, stderr}).toMatchSnapshot(); + }) + ); + + test( + `it should print the npm registry username for a given scope`, + makeTemporaryEnv({}, async ({ path, run, source }) => { + const url = startPackageServer(); + + await writeFile(`${path}/.yarnrc`, [ + `npmScopes:\n`, + ` testScope:\n`, + ` npmRegistryServer: "${url}"\n`, + `npmRegistries:\n`, + ` "${url}":\n`, + ` npmAuthToken: ${AUTH_TOKEN}`, + ].join(``)); + + let code; + let stdout; + let stderr; + + try { + ({code, stdout, stderr} = await run(`npm`, `whoami`, `--scope`, `testScope`)); + } catch (error) { + ({code, stdout, stderr} = error); + } + + expect({code, stdout, stderr}).toMatchSnapshot(); + }) + ); + + test( + `it should throw an error when no auth config is found`, + makeTemporaryEnv({}, async ({ path, run, source }) => { + await expect(run(`npm`, `whoami`)).rejects.toThrowError(/No authentication configured/); + }) + ); + + test( + `it should throw an error when config has an invalid npmAuthToken`, + makeTemporaryEnv({}, async ({ path, run, source }) => { + await writeFile(`${path}/.yarnrc`, `npmAuthToken: "${INVALID_AUTH_TOKEN}"\n`); + await expect(run(`npm`, `whoami`)).rejects.toThrowError(/Authentication failed/); + }) + ); + + test( + `it should throw an error when config has an invalid npmAuthIdent`, + makeTemporaryEnv({}, async ({ path, run, source }) => { + await writeFile(`${path}/.yarnrc`, `npmAuthIdent: "${INVALID_AUTH_IDENT}"\n`); + await expect(run(`npm`, `whoami`)).rejects.toThrowError(/Authentication failed/); + }) + ); + + test( + `it should thow an error when invalid auth config is found for a scope`, + makeTemporaryEnv({}, async ({ path, run, source }) => { + const url = startPackageServer(); + + await writeFile(`${path}/.yarnrc`, [ + `npmScopes:\n`, + ` testScope:\n`, + ` npmRegistryServer: "${url}"\n`, + `npmRegistries:\n`, + ` "${url}":\n`, + ` npmAuthToken: ${INVALID_AUTH_TOKEN}`, + ].join(``)); + + await expect(run(`npm`, `whoami`, `--scope`, `testScope`)).rejects.toThrowError(/Authentication failed/); + await expect(run(`npm`, `whoami`)).rejects.toThrowError(/Authentication failed/); + }) + ); + }); +}); diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/features/rcFiles.test.js b/packages/acceptance-tests/pkg-tests-specs/sources/features/rcFiles.test.js index 96589e856bf..e11488777fa 100644 --- a/packages/acceptance-tests/pkg-tests-specs/sources/features/rcFiles.test.js +++ b/packages/acceptance-tests/pkg-tests-specs/sources/features/rcFiles.test.js @@ -10,7 +10,7 @@ describe(`Features`, () => { makeTemporaryEnv( {}, async ({path, run, source}) => { - await writeFile(`${path}/.yarnrc`, `pnp-shebang "Hello World!"\n`); + await writeFile(`${path}/.yarnrc`, `pnp-shebang: "Hello World!"\n`); expect(parseJsonStream( (await run(`config`, `--json`)).stdout, @@ -29,7 +29,7 @@ describe(`Features`, () => { makeTemporaryEnv( {}, async ({path, run, source}) => { - await writeFile(`${path}/.foobarrc`, `pnp-shebang "Hello World!"\n`); + await writeFile(`${path}/.foobarrc`, `pnp-shebang: "Hello World!"\n`); expect(parseJsonStream( (await run(`config`, `--json`, {rcFilename: `.foobarrc`})).stdout, diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/plugins/__snapshots__/plugin-npm-cli.test.js.snap b/packages/acceptance-tests/pkg-tests-specs/sources/plugins/__snapshots__/plugin-npm-cli.test.js.snap deleted file mode 100644 index b8e5b8539cc..00000000000 --- a/packages/acceptance-tests/pkg-tests-specs/sources/plugins/__snapshots__/plugin-npm-cli.test.js.snap +++ /dev/null @@ -1,105 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Plugins npm-cli login should login a user with OTP setup 1`] = ` -"init-scope: berry-test - -npm-auth-token: 686159dc-64b3-413e-a244-2de2b8d1c36f -" -`; - -exports[`Plugins npm-cli login should login a user with OTP setup 2`] = ` -Object { - "code": 0, - "stderr": "", - "stdout": "➤ YN0000: Successfully logged in -➤ YN0000: Done -", -} -`; - -exports[`Plugins npm-cli login should login a user with OTP to a specific scope 1`] = ` -"init-scope: berry-test - -npmScopes: - testScope: - npmAuthToken: 686159dc-64b3-413e-a244-2de2b8d1c36f - npmRegistryServer: \\"http://yarn.test.registry\\" -" -`; - -exports[`Plugins npm-cli login should login a user with OTP to a specific scope 2`] = ` -Object { - "code": 0, - "stderr": "", - "stdout": "➤ YN0000: Successfully logged in -➤ YN0000: Done -", -} -`; - -exports[`Plugins npm-cli login should login a user with no OTP setup 1`] = ` -"init-scope: berry-test - -npm-auth-token: 316158de-64b3-413e-a244-2de2b8d1c80f -" -`; - -exports[`Plugins npm-cli login should login a user with no OTP setup 2`] = ` -Object { - "code": 0, - "stderr": "", - "stdout": "➤ YN0000: Successfully logged in -➤ YN0000: Done -", -} -`; - -exports[`Plugins npm-cli login should login a user with no OTP setup to a specific scope 1`] = ` -"init-scope: berry-test - -npmScopes: - testScope: - npmAuthToken: 316158de-64b3-413e-a244-2de2b8d1c80f - npmRegistryServer: \\"http://yarn.test.registry\\" -" -`; - -exports[`Plugins npm-cli login should login a user with no OTP setup to a specific scope 2`] = ` -Object { - "code": 0, - "stderr": "", - "stdout": "➤ YN0000: Successfully logged in -➤ YN0000: Done -", -} -`; - -exports[`Plugins npm-cli whoami should print the npm registry username for a given scope 1`] = ` -Object { - "code": 0, - "stderr": "", - "stdout": "➤ YN0000: username -➤ YN0000: Done -", -} -`; - -exports[`Plugins npm-cli whoami should print the npm registry username when config has a valid npmAuthIdent 1`] = ` -Object { - "code": 0, - "stderr": "", - "stdout": "➤ YN0000: username -➤ YN0000: Done -", -} -`; - -exports[`Plugins npm-cli whoami should print the npm registry username when config has a valid npmAuthToken 1`] = ` -Object { - "code": 0, - "stderr": "", - "stdout": "➤ YN0000: username -➤ YN0000: Done -", -} -`; diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/plugins/plugin-npm-cli.test.js b/packages/acceptance-tests/pkg-tests-specs/sources/plugins/plugin-npm-cli.test.js deleted file mode 100644 index 2e0a0edb642..00000000000 --- a/packages/acceptance-tests/pkg-tests-specs/sources/plugins/plugin-npm-cli.test.js +++ /dev/null @@ -1,307 +0,0 @@ -const { - fs: {createTemporaryFolder, readFile, writeFile}, - tests: {startPackageServer} -} = require('pkg-tests-core'); - -const AUTH_TOKEN = `686159dc-64b3-413e-a244-2de2b8d1c36f`; -const AUTH_IDENT = `dXNlcm5hbWU6YSB2ZXJ5IHNlY3VyZSBwYXNzd29yZA==`; - -const INVALID_AUTH_TOKEN = `a24cb960-e6a5-45fc-b9ab-0f9fe0aaae57`; -const INVALID_AUTH_IDENT = `dXNlcm5hbWU6bm90IHRoZSByaWdodCBwYXNzd29yZA==`; // username:not the right password - -const RC_FILENAME = `.spec-yarnrc`; -const FAKE_REGISTRY_URL = `http://yarn.test.registry`; - -describe(`Plugins`, () => { - describe(`npm-cli whoami`, () => { - test( - `should print the npm registry username when config has a valid npmAuthToken`, - makeTemporaryEnv({}, async ({ path, run, source }) => { - await writeFile(`${path}/.yarnrc`, `npmAuthToken "${AUTH_TOKEN}"\n`); - - let code; - let stdout; - let stderr; - - try { - ({code, stdout, stderr} = await run(`npm`, `whoami`)); - } catch (error) { - ({code, stdout, stderr} = error); - } - - expect({code, stdout, stderr}).toMatchSnapshot(); - }) - ); - - test( - `should print the npm registry username when config has a valid npmAuthIdent`, - makeTemporaryEnv({}, async ({ path, run, source }) => { - await writeFile(`${path}/.yarnrc`, `npmAuthIdent "${AUTH_IDENT}"\n`); - - let code; - let stdout; - let stderr; - - try { - ({code, stdout, stderr} = await run(`npm`, `whoami`)); - } catch (error) { - ({code, stdout, stderr} = error); - } - - expect({code, stdout, stderr}).toMatchSnapshot(); - }) - ); - - test( - `should print the npm registry username for a given scope`, - makeTemporaryEnv({}, async ({ path, run, source }) => { - const url = startPackageServer(); - const rcFileContent = [ - `npmScopes:\n`, - ` testScope:\n`, - ` npmRegistryServer: "${url}"\n`, - `npmRegistries:\n`, - ` "${url}":\n`, - ` npmAuthToken: ${AUTH_TOKEN}`, - ].join(``); - - await writeFile(`${path}/.yarnrc`, rcFileContent); - - let code; - let stdout; - let stderr; - - try { - ({code, stdout, stderr} = await run(`npm`, `whoami`, `--scope`, `testScope`)); - } catch (error) { - ({code, stdout, stderr} = error); - } - - expect({code, stdout, stderr}).toMatchSnapshot(); - }) - ); - - test( - `should throw an error when no auth config is found`, - makeTemporaryEnv({}, async ({ path, run, source }) => { - await expect(run(`npm`, `whoami`)).rejects.toThrowError(/No authentication configured/); - }) - ); - - test( - `should throw an error when config has an invalid npmAuthToken`, - makeTemporaryEnv({}, async ({ path, run, source }) => { - await writeFile(`${path}/.yarnrc`, `npmAuthToken "${INVALID_AUTH_TOKEN}"\n`); - await expect(run(`npm`, `whoami`)).rejects.toThrowError(/Authentication failed/); - }) - ); - - test( - `should throw an error when config has an invalid npmAuthIdent`, - makeTemporaryEnv({}, async ({ path, run, source }) => { - await writeFile(`${path}/.yarnrc`, `npmAuthIdent "${INVALID_AUTH_IDENT}"\n`); - await expect(run(`npm`, `whoami`)).rejects.toThrowError(/Authentication failed/); - }) - ); - - test( - `should thow an error when invalid auth config is found for a scope`, - makeTemporaryEnv({}, async ({ path, run, source }) => { - const url = startPackageServer(); - const rcFileContent = [ - `npmScopes:\n`, - ` testScope:\n`, - ` npmRegistryServer: "${url}"\n`, - `npmRegistries:\n`, - ` "${url}":\n`, - ` npmAuthToken: ${INVALID_AUTH_TOKEN}`, - ].join(``); - - await writeFile(`${path}/.yarnrc`, rcFileContent); - await expect(run(`npm`, `whoami`, `--scope`, `testScope`)).rejects.toThrowError(/Authentication failed/); - await expect(run(`npm`, `whoami`)).rejects.toThrowError(/Authentication failed/); - }) - ); - }); - - describe(`npm-cli login`, () => { - test( - `should login a user with no OTP setup`, - makeTemporaryEnv({}, async ({ path, run, source }) => { - const homePath = await createTemporaryFolder(); - await writeFile(`${homePath}/${RC_FILENAME}`, `init-scope: berry-test\n`); - - let code; - let stdout; - let stderr; - - try { - ({ code, stdout, stderr } = await run(`npm`, `login`, { - env: { - HOME: homePath, - USERPROFILE: homePath, - TEST_NPM_USER: `anotherTestUser`, - TEST_NPM_PASSWORD: `password123`, - YARN_RC_FILENAME: RC_FILENAME - } - })); - } catch (error) { - ({ code, stdout, stderr } = error); - } - - const rcFileContent = await readFile(`${homePath}/${RC_FILENAME}`); - - expect(rcFileContent.toString()).toMatchSnapshot(); - expect({code, stdout, stderr}).toMatchSnapshot(); - }) - ); - - test( - `should login a user with OTP setup`, - makeTemporaryEnv({}, async ({ path, run, source }) => { - const homePath = await createTemporaryFolder(); - await writeFile(`${homePath}/${RC_FILENAME}`, `init-scope berry-test\n`); - - let code; - let stdout; - let stderr; - - try { - ({ code, stdout, stderr } = await run(`npm`, `login`, { - env: { - HOME: homePath, - USERPROFILE: homePath, - TEST_NPM_USER: `testUser`, - TEST_NPM_PASSWORD: `password`, - TEST_NPM_2FA_TOKEN: `1234`, - YARN_RC_FILENAME: RC_FILENAME - } - })); - } catch (error) { - ({code, stdout, stderr} = error); - } - - const rcFileContent = await readFile(`${homePath}/${RC_FILENAME}`); - - expect(rcFileContent.toString()).toMatchSnapshot(); - expect({code, stdout, stderr}).toMatchSnapshot(); - }) - ); - - test( - `should throw an error when credentials are incorrect`, - makeTemporaryEnv({}, async ({ path, run, source }) => { - await expect( - run(`npm`, `login`, { - env: { - TEST_NPM_USER: `anotherTestUser`, - TEST_NPM_PASSWORD: `incorrect password` - } - }) - ).rejects.toThrowError(/Invalid Authentication/); - }) - ); - - test( - `should throw an error with incorrect OTP`, - makeTemporaryEnv({}, async ({ path, run, source }) => { - await expect( - run(`npm`, `login`, { - env: { - TEST_NPM_USER: `testUser`, - TEST_NPM_PASSWORD: `password`, - TEST_NPM_2FA_TOKEN: `incorrect OTP`, - } - }) - ).rejects.toThrowError(/Invalid Authentication/); - }) - ); - - test( - `should login a user with no OTP setup to a specific scope`, - makeTemporaryEnv({}, async ({ path, run, source }) => { - const url = startPackageServer(); - const homePath = await createTemporaryFolder(); - - const initialRcFileContent = [ - `init-scope berry-test\n`, - `npmScopes:\n`, - ` testScope:\n`, - ` npmRegistryServer: "${url}"\n`, - ].join(``); - - await writeFile(`${homePath}/${RC_FILENAME}`, initialRcFileContent); - - let code; - let stdout; - let stderr; - - try { - ({ code, stdout, stderr } = await run(`npm`, `login`, `--scope`, `testScope`, { - env: { - HOME: homePath, - USERPROFILE: homePath, - TEST_NPM_USER: `anotherTestUser`, - TEST_NPM_PASSWORD: `password123`, - YARN_RC_FILENAME: RC_FILENAME - } - })); - } catch (error) { - ({ code, stdout, stderr } = error); - } - - const finalRcFileContent = await readFile(`${homePath}/${RC_FILENAME}`); - const cleanFileContent = cleanupFileContent(finalRcFileContent.toString()); - - expect(cleanFileContent).toMatchSnapshot(); - expect({code, stdout, stderr}).toMatchSnapshot(); - }) - ); - - test( - `should login a user with OTP to a specific scope`, - makeTemporaryEnv({}, async ({ path, run, source }) => { - const url = startPackageServer(); - const homePath = await createTemporaryFolder(); - - const initialRcFileContent = [ - `init-scope berry-test\n`, - `npmScopes:\n`, - ` testScope:\n`, - ` npmRegistryServer: "${url}"\n`, - ].join(``); - - await writeFile(`${homePath}/${RC_FILENAME}`, initialRcFileContent); - - let code; - let stdout; - let stderr; - - try { - ({ code, stdout, stderr } = await run(`npm`, `login`, `--scope`, `testScope`, { - env: { - HOME: homePath, - USERPROFILE: homePath, - TEST_NPM_USER: `testUser`, - TEST_NPM_PASSWORD: `password`, - TEST_NPM_2FA_TOKEN: `1234`, - YARN_RC_FILENAME: RC_FILENAME - } - })); - } catch (error) { - ({ code, stdout, stderr } = error); - } - - const finalRcFileContent = await readFile(`${homePath}/${RC_FILENAME}`); - const cleanFileContent = cleanupFileContent(finalRcFileContent.toString()); - - expect(cleanFileContent).toMatchSnapshot(); - expect({code, stdout, stderr}).toMatchSnapshot(); - }) - ); - }); -}); - -function cleanupFileContent(fileContent) { - return fileContent.replace(/http:\/\/localhost:\d+/g, FAKE_REGISTRY_URL); -} diff --git a/packages/berry-core/sources/Configuration.ts b/packages/berry-core/sources/Configuration.ts index 1f4fcbc0d20..50aecac1a4c 100644 --- a/packages/berry-core/sources/Configuration.ts +++ b/packages/berry-core/sources/Configuration.ts @@ -572,7 +572,18 @@ export class Configuration { if (xfs.existsSync(rcPath)) { const content = await xfs.readFilePromise(rcPath, `utf8`); - const data = parseSyml(content) as any; + + let data; + try { + data = parseSyml(content) as any; + } catch (error) { + let tip = ``; + + if (content.match(/^\s+(?!-)[^:]+\s+\S+/m)) + tip = ` (in particular, make sure you list the colons after each key name)` + + throw new UsageError(`Parse error when loading ${rcPath}; please check it's proper Yaml${tip}`); + } rcFiles.push({path: rcPath, cwd: currentCwd, data}); } diff --git a/packages/berry-core/sources/Project.ts b/packages/berry-core/sources/Project.ts index 25bd718ea7f..3c849d9350a 100644 --- a/packages/berry-core/sources/Project.ts +++ b/packages/berry-core/sources/Project.ts @@ -33,6 +33,8 @@ import {LinkType} // the Package type; no more no less. const LOCKFILE_VERSION = 3; +const MULTIPLE_KEYS_REGEXP = / *, */g; + export type InstallOptions = { cache: Cache, fetcher?: Fetcher, @@ -116,6 +118,7 @@ export class Project { this.storedPackages = new Map(); const lockfilePath = ppath.join(this.cwd, this.configuration.get(`lockfileFilename`)); + const defaultLanguageName = this.configuration.get(`defaultLanguageName`); if (xfs.existsSync(lockfilePath)) { const content = await xfs.readFilePromise(lockfilePath, `utf8`); @@ -137,7 +140,7 @@ export class Project { const version = manifest.version; - const languageName = manifest.languageName || this.configuration.get(`defaultLanguageName`); + const languageName = manifest.languageName || defaultLanguageName; const linkType = data.linkType as LinkType; const dependencies = manifest.dependencies; @@ -156,7 +159,7 @@ export class Project { this.storedPackages.set(pkg.locatorHash, pkg); } - for (const entry of key.split(/ *, */g)) { + for (const entry of key.split(MULTIPLE_KEYS_REGEXP)) { const descriptor = structUtils.parseDescriptor(entry); this.storedDescriptors.set(descriptor.descriptorHash, descriptor); diff --git a/packages/berry-parsers/package.json b/packages/berry-parsers/package.json index 9f4f1cd47dd..b5f6c9e2bf4 100644 --- a/packages/berry-parsers/package.json +++ b/packages/berry-parsers/package.json @@ -16,6 +16,9 @@ "release": "yarn npm publish", "test:parsers": "run test:unit packages/berry-parsers" }, + "dependencies": { + "js-yaml": "npm:^3.10.0" + }, "publishConfig": { "main": "./lib/index.js", "typings": "./lib/index.d.ts" diff --git a/packages/berry-parsers/sources/syml.ts b/packages/berry-parsers/sources/syml.ts index b9d340218de..bc811d32d40 100644 --- a/packages/berry-parsers/sources/syml.ts +++ b/packages/berry-parsers/sources/syml.ts @@ -1,4 +1,7 @@ -import {parse} from './grammars/syml'; +// @ts-ignore +import {safeLoad, FAILSAFE_SCHEMA} from 'js-yaml'; + +import {parse} from './grammars/syml'; const simpleStringPattern = /^(?![-?:,\][{}#&*!|>'"%@` \t\r\n]).([ \t]*(?![,\][{}:# \t\r\n]).)*$/; @@ -81,15 +84,46 @@ function stringifyValue(value: any, indentLevel: number): string { } export function stringifySyml(value: any) { - return stringifyValue(value, 0); -} - -export function parseSyml(source: string) { try { - return parse(source.endsWith(`\n`) ? source : `${source}\n`); + return stringifyValue(value, 0); } catch (error) { if (error.location) error.message = error.message.replace(/(\.)?$/, ` (line ${error.location.start.line}, column ${error.location.start.column})$1`); throw error; } } + +function parseViaPeg(source: string) { + if (!source.endsWith(`\n`)) + source += `\n`; + + return parse(source); +} + +const LEGACY_REGEXP = /^(#.*\n)*?#\s+yarn\s+lockfile\s+v1\n/i; + +function parseViaJsYaml(source: string) { + if (LEGACY_REGEXP.test(source)) + return parseViaPeg(source); + + let value = safeLoad(source, { + schema: FAILSAFE_SCHEMA, + }); + + // Empty files are parsed as `undefined` instead of an empty object + // Empty files with 2 newlines or more are `null` instead + if (value === undefined || value === null) + return {} as {[key: string]: string}; + + if (typeof value !== `object`) + throw new Error(`Expected an indexed object, got a ${typeof value} instead. Does your file follow Yaml's rules?`); + + if (Array.isArray(value)) + throw new Error(`Expected an indexed object, got an array instead. Does your file follow Yaml's rules?`); + + return value as {[key: string]: string}; +} + +export function parseSyml(source: string) { + return parseViaJsYaml(source); +} diff --git a/packages/berry-parsers/tests/syml.test.js b/packages/berry-parsers/tests/syml.test.js index 02f31a8f5f8..c940a6036c0 100644 --- a/packages/berry-parsers/tests/syml.test.js +++ b/packages/berry-parsers/tests/syml.test.js @@ -2,13 +2,19 @@ import {parseSyml} from '@berry/parsers'; describe(`Syml parser`, () => { it(`shouldn't confuse old-style values with new-style keys`, () => { - expect(parseSyml(`foo "C:\\\\foobar"\n`)).toEqual({ + expect(parseSyml(`# yarn lockfile v1\nfoo "C:\\\\foobar"\n`)).toEqual({ foo: `C:\\foobar`, }); }); - it(`should parse objects`, () => { - expect(parseSyml(`foo:\n bar true\n baz "quux"\n`)).toEqual({ + it(`should parse new-style objects`, () => { + expect(parseSyml(`foo:\n bar: true\n baz: "quux"\n`)).toEqual({ + foo: {bar: `true`, baz: `quux`}, + }) + }); + + it(`should parse old-style objects`, () => { + expect(parseSyml(`# yarn lockfile v1\nfoo:\n bar true\n baz "quux"\n`)).toEqual({ foo: {bar: `true`, baz: `quux`}, }) }); diff --git a/packages/gatsby/content/advanced/migration.md b/packages/gatsby/content/advanced/migration.md index b72dd060886..fdab2042458 100644 --- a/packages/gatsby/content/advanced/migration.md +++ b/packages/gatsby/content/advanced/migration.md @@ -16,6 +16,32 @@ The long term fix is of course to submit a pull request upstream to add the miss Note that the short-term fix isn't meant to be long-term: you'll need to reapply it each time the package version changes and its metadata are downloaded from the registry again. +## Yarn & lockfile format + +> Parse error when loading {path}; please check it's proper Yaml (in particular, make sure you list the colons after each key name) + +Starting from the v2 Yarn expects proper Yaml files by default. We still can read old-style files, but they must start with a particular comment: + +```yaml +# yarn lockfile v1 +``` + +Old-style files are very similar to Yaml, but with one major distinction: keys don't contain colons when their value is on the same line. The following old-style file: + +``` +hello-world: + setting-a foo + setting-b bar +``` + +Would look like the following once converted into Yaml: + +```yaml +hello-world: + settings-a: foo + settings-b: foo +``` + ## Yarnrc file detection The Yarnrc files mechanisms have been changed and simplified. In particular: diff --git a/packages/plugin-dlx/sources/commands/dlx.ts b/packages/plugin-dlx/sources/commands/dlx.ts index f7f9be35f08..10af4e2ed96 100644 --- a/packages/plugin-dlx/sources/commands/dlx.ts +++ b/packages/plugin-dlx/sources/commands/dlx.ts @@ -34,7 +34,7 @@ export default (clipanion: any, pluginConfiguration: PluginConfiguration) => cli try { await xfs.writeFilePromise(ppath.join(tmpDir, toFilename(`package.json`)), `{}\n`); await xfs.writeFilePromise(ppath.join(tmpDir, toFilename(`yarn.lock`)), ``); - await xfs.writeFilePromise(ppath.join(tmpDir, toFilename(`.yarnrc`)), `enable-global-cache true\n`); + await xfs.writeFilePromise(ppath.join(tmpDir, toFilename(`.yarnrc`)), `enable-global-cache: true\n`); if (packages.length === 0) { packages = [command]; diff --git a/packages/plugin-npm-cli/sources/commands/npm/whoami.ts b/packages/plugin-npm-cli/sources/commands/npm/whoami.ts index cdcb51d924f..67b5a26cbab 100644 --- a/packages/plugin-npm-cli/sources/commands/npm/whoami.ts +++ b/packages/plugin-npm-cli/sources/commands/npm/whoami.ts @@ -37,7 +37,6 @@ export default (clipanion: Clipanion, pluginConfiguration: PluginConfiguration) if (scope) ident = structUtils.makeIdent(scope, ``); - try { const response = await npmHttpUtils.get(`/-/whoami`, {configuration, ident, authType: npmHttpUtils.AuthType.ALWAYS_AUTH, json: true}); diff --git a/yarn.lock b/yarn.lock index 08fa0c93e97..2f552494989 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1724,6 +1724,7 @@ __metadata: resolution: "@berry/parsers@workspace:packages/berry-parsers" dependencies: "@berry/pnpify": "workspace:0.0.3" + js-yaml: "npm:^3.10.0" pegjs: "npm:^0.10.0" typescript: "npm:^3.3.3333" languageName: unknown