diff --git a/.yarn/versions/7297fb1f.yml b/.yarn/versions/7297fb1f.yml new file mode 100644 index 000000000000..ba29c37171c6 --- /dev/null +++ b/.yarn/versions/7297fb1f.yml @@ -0,0 +1,32 @@ +releases: + "@yarnpkg/core": prerelease + "@yarnpkg/parsers": prerelease + +declined: + - "@yarnpkg/plugin-compat" + - "@yarnpkg/plugin-constraints" + - "@yarnpkg/plugin-dlx" + - "@yarnpkg/plugin-essentials" + - "@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-node-modules" + - "@yarnpkg/plugin-npm" + - "@yarnpkg/plugin-npm-cli" + - "@yarnpkg/plugin-pack" + - "@yarnpkg/plugin-patch" + - "@yarnpkg/plugin-pnp" + - "@yarnpkg/plugin-stage" + - "@yarnpkg/plugin-typescript" + - "@yarnpkg/plugin-version" + - "@yarnpkg/plugin-workspace-tools" + - "@yarnpkg/builder" + - "@yarnpkg/cli" + - "@yarnpkg/doctor" + - "@yarnpkg/pnpify" + - "@yarnpkg/shell" diff --git a/packages/gatsby/src/pages/configuration/yarnrc.json b/packages/gatsby/src/pages/configuration/yarnrc.json index 82bd1f4eca40..e55f2d30198b 100644 --- a/packages/gatsby/src/pages/configuration/yarnrc.json +++ b/packages/gatsby/src/pages/configuration/yarnrc.json @@ -1,7 +1,7 @@ { "title": "JSON Schema for Yarnrc files", "$schema": "https://json-schema.org/draft/2019-09/schema#", - "description": "Yarnrc files (named this way because they must be called `.yarnrc.yml`) are the one place where you'll be able to configure Yarn's internal settings. While Yarn will automatically find them in the parent directories, they should usually be kept at the root of your project (often your repository). Starting from the v2, they **must** be written in valid Yaml and have the right extension (simply calling your file `.yarnrc` won't do).\n\nThose settings can also be defined through environment variables (at least for the simpler ones; arrays and objects aren't supported yet). To do this, just prefix the names and write them in snake case: `YARN_CACHE_FOLDER` will set the cache folder (such values will overwrite any that might have been defined in the RC files - use them sparingly).", + "description": "Yarnrc files (named this way because they must be called `.yarnrc.yml`) are the one place where you'll be able to configure Yarn's internal settings. While Yarn will automatically find them in the parent directories, they should usually be kept at the root of your project (often your repository). Starting from the v2, they **must** be written in valid Yaml and have the right extension (simply calling your file `.yarnrc` won't do).\n\nEnvironment variables can be accessed from setting definitions by using the `${NAME}` syntax when defining the values. By default Yarn will require the variables to be present, but this can be turned off by using either `${NAME-fallback}` (which will return `fallback` if `NAME` isn't set) or `${NAME:-fallback}` (which will return `fallback` if `NAME` isn't set, or is an empty string).\n\nFinally, note that most settings can also be defined through environment variables (at least for the simpler ones; arrays and objects aren't supported yet). To do this, just prefix the names and write them in snake case: `YARN_CACHE_FOLDER` will set the cache folder (such values will overwrite any that might have been defined in the RC files - use them sparingly).", "type": "object", "properties": { "bstatePath": { diff --git a/packages/yarnpkg-core/sources/Configuration.ts b/packages/yarnpkg-core/sources/Configuration.ts index a4a874a3782b..e51152a639a3 100644 --- a/packages/yarnpkg-core/sources/Configuration.ts +++ b/packages/yarnpkg-core/sources/Configuration.ts @@ -438,17 +438,21 @@ function parseSingleValue(configuration: Configuration, path: string, value: unk if (typeof value !== `string`) throw new Error(`Expected value (${value}) to be a string`); + const valueWithReplacedVariables = miscUtils.replaceEnvVariables(value, { + env: process.env, + }); + switch (definition.type) { case SettingsType.ABSOLUTE_PATH: - return ppath.resolve(folder, npath.toPortablePath(value)); + return ppath.resolve(folder, npath.toPortablePath(valueWithReplacedVariables)); case SettingsType.LOCATOR_LOOSE: - return structUtils.parseLocator(value, false); + return structUtils.parseLocator(valueWithReplacedVariables, false); case SettingsType.NUMBER: - return parseInt(value); + return parseInt(valueWithReplacedVariables); case SettingsType.LOCATOR: - return structUtils.parseLocator(value); + return structUtils.parseLocator(valueWithReplacedVariables); default: - return value; + return valueWithReplacedVariables; } }; @@ -1049,7 +1053,15 @@ export class Configuration { if (this.sources.has(key) && !overwrite) continue; - this.values.set(key, parseValue(this, key, value, definition, folder)); + let parsed; + try { + parsed = parseValue(this, key, data[key], definition, folder); + } catch (error) { + error.message += ` in ${source}`; + throw error; + } + + this.values.set(key, parsed); this.sources.set(key, source); } } diff --git a/packages/yarnpkg-core/sources/miscUtils.ts b/packages/yarnpkg-core/sources/miscUtils.ts index 4d153d6a9e4e..eaaae3438584 100644 --- a/packages/yarnpkg-core/sources/miscUtils.ts +++ b/packages/yarnpkg-core/sources/miscUtils.ts @@ -1,4 +1,5 @@ import {PortablePath, npath} from '@yarnpkg/fslib'; +import {UsageError} from 'clipanion'; import micromatch from 'micromatch'; import {Readable, Transform} from 'stream'; @@ -263,7 +264,7 @@ export function sortMap(values: Iterable, mappers: ((value: T) => string) * * @returns A `string` representing a regular expression or `null` if no glob patterns are provided */ -export const buildIgnorePattern = (ignorePatterns: Array) => { +export function buildIgnorePattern(ignorePatterns: Array) { if (ignorePatterns.length === 0) return null; @@ -273,4 +274,27 @@ export const buildIgnorePattern = (ignorePatterns: Array) => { windows: false, }).source})`; }).join(`|`); -}; +} + +export function replaceEnvVariables(value: string, {env}: {env: {[key: string]: string | undefined}}) { + const regex = /\${(?[\d\w_]+)(?:)?-?(?[^}]+)?}/g; + + return value.replace(regex, (...args) => { + const {variableName, colon, fallback} = args[args.length - 1]; + + const variableExist = Object.prototype.hasOwnProperty.call(env, variableName); + const variableValue = process.env[variableName]; + + if (variableValue) + return variableValue; + if (variableExist && !variableValue && colon) + return fallback; + if (variableExist) + return variableValue; + if (fallback) + return fallback; + + throw new UsageError(`Environment variable not found (${variableName})`); + }); +} + diff --git a/packages/yarnpkg-core/tests/Configuration.test.ts b/packages/yarnpkg-core/tests/Configuration.test.ts index 2849ef58379f..27a979754250 100644 --- a/packages/yarnpkg-core/tests/Configuration.test.ts +++ b/packages/yarnpkg-core/tests/Configuration.test.ts @@ -33,4 +33,75 @@ describe(`Configuration`, () => { expect(secondToken).toEqual(SECRET); }); }); + + describe(`Environment variables`, () => { + it(`should replace env variables`, async () => { + process.env.ENV_AUTH_TOKEN = `AAA-BBB-CCC`; + process.env.EMPTY_VARIABLE = ``; + + await initializeConfiguration({ + npmScopes: { + onlyEnv: { + npmAuthToken: `\${ENV_AUTH_TOKEN}`, + }, + multipleEnvs: { + npmAuthToken: `\${ENV_AUTH_TOKEN}-separator-\${ENV_AUTH_TOKEN}`, + }, + envInString: { + npmAuthToken: `beforeEnv-\${ENV_AUTH_TOKEN}-after-env`, + }, + envSetWithFallback: { + npmAuthToken: `\${ENV_AUTH_TOKEN-fallback-value}`, + }, + unsetEnvWithFallback: { + npmAuthToken: `\${NOT_EXISTING_ENV-fallback-value}`, + }, + emptyEnvWithStrictFallback: { + npmAuthToken: `\${EMPTY_VARIABLE-fallback-value}`, + }, + emptyEnvWithFallback: { + npmAuthToken: `\${EMPTY_VARIABLE:-fallback-for-empty-value}`, + }, + }, + }, async dir => { + const configuration = await Configuration.find(dir, { + modules: new Map([[`@yarnpkg/plugin-npm`, NpmPlugin]]), + plugins: new Set([`@yarnpkg/plugin-npm`]), + }); + + const getToken = (scope: string) => configuration.get(`npmScopes`).get(scope).get(`npmAuthToken`); + + const onlyEnv = getToken(`onlyEnv`); + const multipleEnvs = getToken(`multipleEnvs`); + const envInString = getToken(`envInString`); + const envSetWithFallback = getToken(`envSetWithFallback`); + const unsetEnvWithFallback = getToken(`unsetEnvWithFallback`); + const emptyEnvWithStrictFallback = getToken(`emptyEnvWithStrictFallback`); + const emptyEnvWithFallback = getToken(`emptyEnvWithFallback`); + + expect(onlyEnv).toEqual(`AAA-BBB-CCC`); + expect(multipleEnvs).toEqual(`AAA-BBB-CCC-separator-AAA-BBB-CCC`); + expect(envInString).toEqual(`beforeEnv-AAA-BBB-CCC-after-env`); + expect(envSetWithFallback).toEqual(`AAA-BBB-CCC`); + expect(unsetEnvWithFallback).toEqual(`fallback-value`); + expect(emptyEnvWithStrictFallback).toEqual(``); + expect(emptyEnvWithFallback).toEqual(`fallback-for-empty-value`); + }); + }); + + it(`should forbid unset variables`, async () => { + await initializeConfiguration({ + npmScopes: { + onlyEnv: { + npmAuthToken: `\${A_VARIABLE_THAT_DEFINITELY_DOESNT_EXIST}`, + }, + }, + }, async dir => { + await expect(Configuration.find(dir, { + modules: new Map([[`@yarnpkg/plugin-npm`, NpmPlugin]]), + plugins: new Set([`@yarnpkg/plugin-npm`]), + })).rejects.toThrow(); + }); + }); + }); });