// util.parseEnv: a .env-file parser matching Node's src/node_dotenv.cc ParseContent // (reimplemented from its documented behavior). No variable interpolation: `${FOO}` // is kept literally, as Node's own parser does. function trimSpaces(s: string): string { const start = s.search(/[^ \t\n]/); if (start === -1) return ""; let end = s.length - 1; while (end >= 0 && " \t\n".includes(s[end]!)) end--; return s.slice(start, end + 1); } export function parseEnv(content: unknown): Record { if (typeof content !== "string") { const got = content === null || content === undefined ? ` Received ${content}` : typeof content === "function" ? ` Received function ${(content as { name?: string }).name || "anonymous"}` : typeof content === "object" ? ` Received an instance of ${(content as object).constructor?.name ?? "Object"}` : ` Received type ${typeof content} (${String(content)})`; throw Object.assign(new TypeError(`The "content" argument must be of type string.${got}`), { code: "ERR_INVALID_ARG_TYPE" }); } const store: Record = Object.create(null); const text = content.replace(/\r/g, ""); let rest = trimSpaces(text); while (rest.length > 0) { if (rest[0] === "\n" || rest[0] === "#") { const nl = rest.indexOf("\n"); rest = nl === -1 ? "" : rest.slice(nl + 1); continue; } const stop = (() => { for (let i = 0; i < rest.length; i++) { if (rest[i] === "=" || rest[i] === "\n") return i; } return -1; })(); if (stop === -1 || rest[stop] === "\n") { if (stop !== -1) { rest = trimSpaces(rest.slice(stop + 1)); continue; } break; } let key = trimSpaces(rest.slice(0, stop)); rest = rest.slice(stop + 1); if (rest.length === 0 || rest[0] === "\n") { if (key) store[key] = ""; rest = rest.length === 0 ? "" : rest.slice(1); continue; } rest = trimSpaces(rest); if (key === "") continue; if (key.startsWith("export ")) key = trimSpaces(key.slice(7)); if (rest.length === 0) { store[key] = ""; break; } // A double-quoted value may span lines; \n inside it becomes a real newline. if (rest[0] === '"') { const closing = rest.indexOf('"', 1); if (closing !== -1) { const value = rest.slice(1, closing).replace(/\\n/g, "\n"); store[key] = value; const nl = rest.indexOf("\n", closing + 1); rest = nl === -1 ? "" : rest.slice(nl + 1); continue; } } if (rest[0] === "'" || rest[0] === '"' || rest[0] === "`") { const quote = rest[0]; const closing = rest.indexOf(quote, 1); if (closing === -1) { const nl = rest.indexOf("\n"); if (nl !== -1) { store[key] = rest.slice(0, nl); rest = rest.slice(nl + 1); } else { store[key] = rest; break; } } else { store[key] = rest.slice(1, closing); const nl = rest.indexOf("\n", closing + 1); rest = nl === -1 ? "" : rest.slice(nl + 1); continue; } } else { const nl = rest.indexOf("\n"); let value = nl === -1 ? rest : rest.slice(0, nl); const hash = value.indexOf("#"); if (hash !== -1) value = value.slice(0, hash); store[key] = trimSpaces(value); rest = nl === -1 ? "" : rest.slice(nl + 1); } rest = trimSpaces(rest); } return store; }