(input: SubstituteInput)
| 77 | * the subsequent JSONC parse. |
| 78 | */ |
| 79 | export function substituteConfigVariables(input: SubstituteInput): SubstituteResult { |
| 80 | const warnings: string[] = []; |
| 81 | let text = input.text; |
| 82 | |
| 83 | if (input.isProjectConfig) { |
| 84 | const hasEnvTokens = ENV_PATTERN.test(text); |
| 85 | const hasFileTokens = FILE_PATTERN.test(text); |
| 86 | ENV_PATTERN.lastIndex = 0; |
| 87 | FILE_PATTERN.lastIndex = 0; |
| 88 | if (hasEnvTokens || hasFileTokens) { |
| 89 | const tokenTypes = [ |
| 90 | hasEnvTokens ? "{env:}" : undefined, |
| 91 | hasFileTokens ? "{file:}" : undefined, |
| 92 | ] |
| 93 | .filter(Boolean) |
| 94 | .join(" and "); |
| 95 | warnings.push( |
| 96 | `Project-level config no longer supports ${tokenTypes} tokens for security reasons; leaving tokens literal. Move secret expansion to user-level config.`, |
| 97 | ); |
| 98 | } |
| 99 | return { text, warnings }; |
| 100 | } |
| 101 | |
| 102 | // Strip JSONC comments before substitution so tokens in comments cannot |
| 103 | // trigger env/file side effects. The shared parser helper is string-aware, |
| 104 | // so URL strings and literal comment markers inside strings are preserved. |
| 105 | text = stripJsonComments(text); |
| 106 | |
| 107 | text = text.replace(ENV_PATTERN, (_, rawName: string) => { |
| 108 | const varName = rawName.trim(); |
| 109 | const value = varName ? process.env[varName] : undefined; |
| 110 | if (value === undefined || value === "") { |
| 111 | warnings.push( |
| 112 | `Environment variable ${varName} is not set (referenced via {env:${varName}}); using empty string`, |
| 113 | ); |
| 114 | return ""; |
| 115 | } |
| 116 | |
| 117 | return JSON.stringify(value).slice(1, -1); |
| 118 | }); |
| 119 | |
| 120 | const fileMatches = Array.from(text.matchAll(FILE_PATTERN)); |
| 121 | if (fileMatches.length === 0) { |
| 122 | return { text, warnings }; |
| 123 | } |
| 124 | |
| 125 | const configDir = input.configPath ? dirname(input.configPath) : process.cwd(); |
| 126 | |
| 127 | let output = ""; |
| 128 | let cursor = 0; |
| 129 | |
| 130 | for (const match of fileMatches) { |
| 131 | const token = match[0]; |
| 132 | const rawPath = match[1] ?? ""; |
| 133 | const index = match.index ?? 0; |
| 134 | |
| 135 | output += text.slice(cursor, index); |
| 136 | cursor = index + token.length; |
no test coverage detected