|
1 | 1 | const ENV_EXPR = /(?<!\\)(\\*)\$\{([^${}]+)\}/g |
| 2 | +const ENV_VALUE = /([^:-]+)(:?)-(.+)/ |
2 | 3 |
|
3 | | -export function envReplace(settingValue: string, env: NodeJS.ProcessEnv): string { |
4 | | - return settingValue.replace(ENV_EXPR, replaceEnvMatch.bind(null, env)) |
| 4 | +/** |
| 5 | + * Replace every `${VAR}` (or `${VAR-default}` / `${VAR:-default}`) placeholder |
| 6 | + * in `settingValue` with the env value resolved from `env`. Throws on the first |
| 7 | + * placeholder that has no value and no default. |
| 8 | + */ |
| 9 | +export function envReplace (settingValue: string, env: NodeJS.ProcessEnv): string { |
| 10 | + return replaceWith(settingValue, env, (orig) => { |
| 11 | + throw new Error(`Failed to replace env in config: ${orig}`) |
| 12 | + }) |
5 | 13 | } |
6 | 14 |
|
7 | | -function replaceEnvMatch (env: NodeJS.ProcessEnv, orig: string, escape: string, name: string): string { |
8 | | - if (escape.length % 2) { |
9 | | - return orig.slice((escape.length + 1) / 2) |
10 | | - } |
11 | | - const envValue = getEnvValue(env, name) |
12 | | - if (envValue === undefined) { |
13 | | - throw new Error(`Failed to replace env in config: ${orig}`) |
14 | | - } |
15 | | - return `${(escape.slice(escape.length / 2))}${envValue}` |
| 15 | +export interface EnvReplaceLossyResult { |
| 16 | + /** The substituted text. Unresolved placeholders are replaced with `''`. */ |
| 17 | + value: string |
| 18 | + /** |
| 19 | + * The list of unresolved placeholders (each including its surrounding `${...}`, |
| 20 | + * in source order). One entry per occurrence — duplicates appear multiple times. |
| 21 | + * Callers typically surface each as a warning. |
| 22 | + */ |
| 23 | + unresolved: string[] |
16 | 24 | } |
17 | 25 |
|
18 | | -const ENV_VALUE = /([^:-]+)(:?)-(.+)/ |
| 26 | +/** |
| 27 | + * Like {@link envReplace}, but replaces unresolved `${VAR}` placeholders with |
| 28 | + * `''` and reports them in {@link EnvReplaceLossyResult.unresolved} instead of |
| 29 | + * throwing. Resolvable placeholders and `${VAR-default}` / `${VAR:-default}` |
| 30 | + * fallbacks still expand normally — only the genuinely unresolved bare ones are |
| 31 | + * dropped. |
| 32 | + * |
| 33 | + * Use this when leaving the literal `${VAR}` in the substituted value would be |
| 34 | + * worse than dropping it (e.g. auth tokens in `.npmrc` under OIDC trusted |
| 35 | + * publishing — see https://github.com/pnpm/pnpm/issues/11513). |
| 36 | + */ |
| 37 | +export function envReplaceLossy (settingValue: string, env: NodeJS.ProcessEnv): EnvReplaceLossyResult { |
| 38 | + const unresolved: string[] = [] |
| 39 | + const value = replaceWith(settingValue, env, (orig) => { |
| 40 | + unresolved.push(orig) |
| 41 | + return '' |
| 42 | + }) |
| 43 | + return { value, unresolved } |
| 44 | +} |
| 45 | + |
| 46 | +// Shared substitution loop. `onUnresolved` decides what to splice in (and may |
| 47 | +// throw) when a placeholder has no value and no default. |
| 48 | +function replaceWith ( |
| 49 | + settingValue: string, |
| 50 | + env: NodeJS.ProcessEnv, |
| 51 | + onUnresolved: (orig: string) => string |
| 52 | +): string { |
| 53 | + return settingValue.replace(ENV_EXPR, (orig: string, escape: string, name: string) => { |
| 54 | + if (escape.length % 2) return orig.slice((escape.length + 1) / 2) |
| 55 | + const halfEscape = escape.slice(escape.length / 2) |
| 56 | + const envValue = getEnvValue(env, name) |
| 57 | + if (envValue === undefined) return `${halfEscape}${onUnresolved(orig)}` |
| 58 | + return `${halfEscape}${envValue}` |
| 59 | + }) |
| 60 | +} |
19 | 61 |
|
20 | 62 | function getEnvValue (env: NodeJS.ProcessEnv, name: string): string | undefined { |
21 | 63 | const matched = name.match(ENV_VALUE) |
22 | 64 | if (!matched) return env[name] |
23 | 65 | const [, variableName, colon, fallback] = matched |
24 | | - if (Object.prototype.hasOwnProperty.call(env, variableName)) { |
25 | | - return !env[variableName] && colon ? fallback : env[variableName] |
26 | | - } |
27 | | - return fallback |
| 66 | + // Treat `{ KEY: undefined }` as unset rather than "explicitly empty": the |
| 67 | + // `NodeJS.ProcessEnv` (= `Record<string, string | undefined>`) signature lets |
| 68 | + // callers represent an unset variable as a present-but-undefined property, |
| 69 | + // and `${KEY-default}` must reach the fallback in that case. Using |
| 70 | + // `hasOwnProperty` would treat the property as set and return `undefined` |
| 71 | + // instead of `fallback`. |
| 72 | + const v = env[variableName] |
| 73 | + if (v === undefined) return fallback |
| 74 | + return !v && colon ? fallback : v |
28 | 75 | } |
0 commit comments