| 32 | |
| 33 | /** Apply {env:VAR} and {file:path} substitutions to config text. */ |
| 34 | export async function substitute(input: SubstituteInput) { |
| 35 | const missing = input.missing ?? "error" |
| 36 | let text = input.text.replace(/\{env:([^}]+)\}/g, (_, varName) => { |
| 37 | return (input.env?.[varName] ?? process.env[varName]) || "" |
| 38 | }) |
| 39 | |
| 40 | const fileMatches = Array.from(text.matchAll(/\{file:[^}]+\}/g)) |
| 41 | if (!fileMatches.length) return text |
| 42 | |
| 43 | const configDir = dir(input) |
| 44 | const configSource = source(input) |
| 45 | let out = "" |
| 46 | let cursor = 0 |
| 47 | |
| 48 | for (const match of fileMatches) { |
| 49 | const token = match[0] |
| 50 | const index = match.index |
| 51 | out += text.slice(cursor, index) |
| 52 | |
| 53 | const lineStart = text.lastIndexOf("\n", index - 1) + 1 |
| 54 | const prefix = text.slice(lineStart, index).trimStart() |
| 55 | if (prefix.startsWith("//")) { |
| 56 | out += token |
| 57 | cursor = index + token.length |
| 58 | continue |
| 59 | } |
| 60 | |
| 61 | let filePath = token.replace(/^\{file:/, "").replace(/\}$/, "") |
| 62 | if (filePath.startsWith("~/")) { |
| 63 | filePath = path.join(os.homedir(), filePath.slice(2)) |
| 64 | } |
| 65 | |
| 66 | const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath) |
| 67 | const fileContent = ( |
| 68 | await Filesystem.readText(resolvedPath).catch((error: NodeJS.ErrnoException) => { |
| 69 | if (missing === "empty") return "" |
| 70 | |
| 71 | const errMsg = `bad file reference: "${token}"` |
| 72 | if (error.code === "ENOENT") { |
| 73 | throw new InvalidError( |
| 74 | { |
| 75 | path: configSource, |
| 76 | message: errMsg + ` ${resolvedPath} does not exist`, |
| 77 | }, |
| 78 | { cause: error }, |
| 79 | ) |
| 80 | } |
| 81 | throw new InvalidError({ path: configSource, message: errMsg }, { cause: error }) |
| 82 | }) |
| 83 | ).trim() |
| 84 | |
| 85 | out += JSON.stringify(fileContent).slice(1, -1) |
| 86 | cursor = index + token.length |
| 87 | } |
| 88 | |
| 89 | out += text.slice(cursor) |
| 90 | return out |
| 91 | } |