Files
bun.sh/test/js/bun/ini/ini.test.ts
robobun bc47f87450 fix(ini): support env var expansion in quoted .npmrc values (#25518)
## Summary

Fixes environment variable expansion in quoted `.npmrc` values and adds
support for the `?` optional modifier.

### Changes

**Simplified quoted value handling:**
- Removed unnecessary `isProperlyQuoted` check that added complexity
without benefit
- When JSON.parse succeeds for quoted strings, expand env vars in the
result
- When JSON.parse fails for single-quoted strings like `'${VAR}'`, still
expand env vars

**Added `?` modifier support (matching npm behavior):**
- `${VAR}` - if VAR is undefined, leaves as `${VAR}` (no expansion)
- `${VAR?}` - if VAR is undefined, expands to empty string

This applies consistently to both quoted and unquoted values.

### Examples

```ini
# Env var found - all expand to the value
token = ${NPM_TOKEN}
token = "${NPM_TOKEN}"
token = '${NPM_TOKEN}'

# Env var NOT found - left as-is
token = ${NPM_TOKEN}         # → ${NPM_TOKEN}
token = "${NPM_TOKEN}"       # → ${NPM_TOKEN}
token = '${NPM_TOKEN}'       # → ${NPM_TOKEN}

# Optional modifier (?) - expands to empty if not found
token = ${NPM_TOKEN?}        # → (empty)
token = "${NPM_TOKEN?}"      # → (empty)
auth = "Bearer ${TOKEN?}"    # → Bearer 
```

### Test Plan

- Added 8 new tests for the `?` modifier covering quoted and unquoted
values
- Verified all expected values match `npm config get` behavior
- All 30 ini tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
2025-12-16 19:49:23 -08:00

533 lines
12 KiB
TypeScript

const { iniInternals } = require("bun:internal-for-testing");
const { parse } = iniInternals;
import { describe, expect, it, test } from "bun:test";
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
describe("parse ini", () => {
test("weird section", () => {
const ini = /* ini */ `
[foo\\]]
lol = true
`;
expect(parse(ini)).toEqual({ "[foo\\]]": true, "lol": true });
});
test("really long input", () => {
const ini = /* ini */ `
[${Array(1024).fill("a").join("")}.lol.this.be.long]
wow = 'hi'
`;
expect(parse(ini)).toEqual({
[`${Array(1024).fill("a").join("")}`]: {
lol: {
this: {
be: {
long: {
wow: "hi",
},
},
},
},
},
});
});
describe("env vars", () => {
// Tests translated from npm's workspaces/config/test/env-replace.js
envVarTest({
name: "replaces multiple defined variables",
ini: "hi = ${FOO}${BAR}",
env: { FOO: "bar", BAR: "baz" },
expected: { hi: "barbaz" },
});
envVarTest({
name: "replaces mixed defined/undefined variables with ? modifier",
ini: "hi = ${FOO?}${BAZ?}",
env: { FOO: "bar" },
expected: { hi: "bar" },
});
envVarTest({
name: "escapes normal variable",
ini: "hi = \\${FOO}",
env: { FOO: "bar" },
expected: { hi: "${FOO}" },
});
envVarTest({
name: "double escape allows replacement",
ini: "hi = \\\\${FOO}",
env: { FOO: "bar" },
expected: { hi: "\\bar" },
});
envVarTest({
name: "triple escape prevents replacement",
ini: "hi = \\\\\\${FOO}",
env: { FOO: "bar" },
expected: { hi: "\\${FOO}" },
});
envVarTest({
name: "leaves undefined variable unreplaced",
ini: "hi = ${BAZ}",
env: { FOO: "bar" },
expected: { hi: "${BAZ}" },
});
envVarTest({
name: "escapes undefined variable",
ini: "hi = \\${BAZ}",
env: { FOO: "bar" },
expected: { hi: "${BAZ}" },
});
envVarTest({
name: "double escape with undefined variable",
ini: "hi = \\\\${BAZ}",
env: { FOO: "bar" },
expected: { hi: "\\${BAZ}" },
});
envVarTest({
name: "escapes optional variable",
ini: "hi = \\${FOO?}",
env: { FOO: "bar" },
expected: { hi: "${FOO?}" },
});
envVarTest({
name: "double escape allows optional replacement",
ini: "hi = \\\\${FOO?}",
env: { FOO: "bar" },
expected: { hi: "\\bar" },
});
envVarTest({
name: "replaces undefined variable with empty string when using ? modifier",
ini: "hi = ${BAZ?}",
env: { FOO: "bar" },
expected: { hi: "" },
});
envVarTest({
name: "escapes undefined optional variable",
ini: "hi = \\${BAZ?}",
env: { FOO: "bar" },
expected: { hi: "${BAZ?}" },
});
envVarTest({
name: "double escape with undefined optional variable results in empty replacement",
ini: "hi = \\\\${BAZ?}",
env: { FOO: "bar" },
expected: { hi: "\\" },
});
// Original bun tests
envVarTest({
name: "escaped",
ini: "hi = \\${NODE_ENV}",
env: { NODE_ENV: "production" },
expected: { hi: "${NODE_ENV}" },
});
envVarTest({
name: "escaped2",
ini: "hi = \\\\${NODE_ENV}",
env: { NODE_ENV: "production" },
expected: { hi: "\\production" },
});
envVarTest({
name: "backslashes",
ini: "filepath=C:\\Home\\someuser\\My Documents\nfilepath2=\\\\\\\\TwoBackslashes",
env: {},
expected: { filepath: "C:\\Home\\someuser\\My Documents", filepath2: "\\\\TwoBackslashes" },
});
envVarTest({
name: "basic",
ini: /* ini */ `
hello = \${LOL}
`,
env: { LOL: "hi" },
expected: { hello: "hi" },
});
envVarTest({
name: "no val",
ini: /* ini */ `
hello = \${oooooooooooooooogaboga}
`,
env: {},
expected: { hello: "${oooooooooooooooogaboga}" },
});
envVarTest({
name: "concat",
ini: /* ini */ `
hello = greeting: \${LOL}
`,
env: { LOL: "hi" },
expected: { hello: "greeting: hi" },
});
envVarTest({
name: "nesting selects the inner most",
ini: /* ini */ `
hello = greeting: \${what\${LOL}lol}
`,
env: { LOL: "hi" },
expected: { hello: "greeting: ${whathilol}" },
});
envVarTest({
name: "nesting 2 selects the inner most",
ini: /* ini */ `
hello = greeting: \${what\${omg\${LOL}why}lol}
`,
env: { LOL: "hi" },
expected: { hello: "greeting: ${what${omghiwhy}lol}" },
});
envVarTest({
name: "unclosed",
ini: /* ini */ `
hello = greeting: \${LOL
`,
env: { LOL: "hi" },
expected: { hello: "greeting: ${LOL" },
});
envVarTest({
name: "double quoted env var",
ini: /* ini */ `
hello = "\${LOL}"
`,
env: { LOL: "hi" },
expected: { hello: "hi" },
});
envVarTest({
name: "single quoted env var",
ini: /* ini */ `
hello = '\${LOL}'
`,
env: { LOL: "hi" },
expected: { hello: "hi" },
});
envVarTest({
name: "double quoted env var with prefix",
ini: /* ini */ `
hello = "Bearer \${TOKEN}"
`,
env: { TOKEN: "secret123" },
expected: { hello: "Bearer secret123" },
});
envVarTest({
name: "double quoted env var not found leaves as-is",
ini: /* ini */ `
hello = "\${NOTFOUND}"
`,
env: {},
expected: { hello: "${NOTFOUND}" },
});
envVarTest({
name: "unquoted optional env var expands to empty when not found",
ini: /* ini */ `
hello = \${NOTFOUND?}
`,
env: {},
expected: { hello: "" },
});
envVarTest({
name: "unquoted optional env var expands to value when found",
ini: /* ini */ `
hello = \${TOKEN?}
`,
env: { TOKEN: "secret" },
expected: { hello: "secret" },
});
envVarTest({
name: "double quoted optional env var expands to empty when not found",
ini: /* ini */ `
hello = "\${NOTFOUND?}"
`,
env: {},
expected: { hello: "" },
});
envVarTest({
name: "double quoted optional env var expands to value when found",
ini: /* ini */ `
hello = "\${TOKEN?}"
`,
env: { TOKEN: "secret" },
expected: { hello: "secret" },
});
envVarTest({
name: "single quoted optional env var expands to empty when not found",
ini: /* ini */ `
hello = '\${NOTFOUND?}'
`,
env: {},
expected: { hello: "" },
});
envVarTest({
name: "unquoted optional env var with prefix",
ini: /* ini */ `
hello = Bearer \${TOKEN?}
`,
env: {},
expected: { hello: "Bearer " },
});
envVarTest({
name: "double quoted optional env var with prefix",
ini: /* ini */ `
hello = "Bearer \${TOKEN?}"
`,
env: {},
expected: { hello: "Bearer " },
});
// Note: In JSON strings, \$ is just $ (backslash doesn't escape $)
// So "\\${LOL}" in .npmrc becomes "\${LOL}" after JSON parsing, which expands to "\hi"
// This matches npm behavior where escaping env vars in quoted strings requires \\$
envVarTest({
name: "double quoted with backslash before env var",
ini: /* ini */ `
hello = "\\\\$\{LOL}"
`,
env: { LOL: "hi" },
expected: { hello: "\\hi" },
});
function envVarTest(args: { name: string; ini: string; env: Record<string, string>; expected: any }) {
const { name, ini, env, expected } = args;
test(name, async () => {
const tempdir = tempDirWithFiles("hi", { "foo.ini": ini });
const inipath = `${tempdir}/foo.ini`.replaceAll("\\", "/");
const code = /* ts */ `
const { iniInternals } = require("bun:internal-for-testing");
const { parse } = iniInternals;
const ini = await Bun.$\`cat ${inipath}\`.text()
console.log(JSON.stringify(parse(ini)))
`;
const result = await Bun.$`${bunExe()} -e ${code}`.env({ ...bunEnv, ...env }).json();
expect(result).toEqual(expected);
});
}
});
it("works with unicode in the .ini file", () => {
let ini /* ini */ = `
hi👋lol = 'lol hi 👋'
`;
expect(parse(ini)).toEqual({
"hi👋lol": "lol hi 👋",
});
ini = /* ini */ `
[😎.🫒.🤦‍♀️]
lol = 'wtf'
`;
expect(parse(ini)).toEqual({
"😎": {
"🫒": {
"🤦‍♀️": {
lol: "wtf",
},
},
},
});
});
it("matches stupid npm/ini behavior", () => {
let ini /* ini */ = `
'{ "what": "is this" }' = seriously?
`;
let result = parse(ini);
expect(result).toEqual({
"[Object object]": "seriously?",
});
ini = /* ini */ `
'[1, 2, 3]' = cmon man
`;
result = parse(ini);
expect(result).toEqual({
"1,2,3": "cmon man",
});
});
test("basic", () => {
const ini = /* ini */ `
hello = 'friends'
`;
expect(parse(ini)).toEqual({
hello: "friends",
});
});
test("basic sections", () => {
const ini = /* ini */ `
hello = 'friends'
[foo]
bar = 'baz'
`;
expect(parse(ini)).toEqual({
hello: "friends",
foo: {
bar: "baz",
},
});
});
test("key and then section edgecase", () => {
const ini = /* ini */ `
foo = 'hihihi'
[foo]
isbar = 'lol'
`;
expect(parse(ini)).toEqual({
foo: "hihihi",
});
});
describe("duplicate properties", () => {
test("decode with duplicate properties", () => {
const ini = /* ini */ `
zr[] = deedee
zr=123
ar[] = one
ar[] = three
str = 3
brr = 1
brr = 2
brr = 3
brr = 3
`;
expect(parse(ini)).toEqual({
zr: ["deedee", "123"],
ar: ["one", "three"],
str: "3",
brr: "3",
});
});
});
test("bigboi", async () => {
const foo = await Bun.$`cat ${__dirname}/foo.ini`.text();
const result = parse(foo);
console.log(JSON.stringify(result));
expect(result).toEqual({
" xa n p ": '"\r\nyoyoyo\r\r\n',
"[disturbing]": "hey you never know",
"a": {
"[]": "a square?",
"av": "a val",
"b": {
"c": {
"e": "1",
"j": "2",
},
},
"cr": ["four", "eight"],
"e": '{ o: p, a: { av: a val, b: { c: { e: "this [value]" } } } }',
"j": '"{ o: "p", a: { av: "a val", b: { c: { e: "this [value]" } } } }"',
},
"a with spaces": "b c",
"ar": ["one", "three", "this is included"],
"b": {},
"br": "warm",
"eq": "eq=eq",
"false": false,
"null": null,
"o": "p",
"s": "something",
"s1": "\"something'",
"s2": "something else",
"s3": "",
"s4": "",
"s5": " ",
"s6": " a ",
"s7": true,
"true": true,
"undefined": "undefined",
"x.y.z": {
"a.b.c": {
"a.b.c": "abc",
"nocomment": "this; this is not a comment",
"noHashComment": "this# this is not a comment",
},
"x.y.z": "xyz",
},
"zr": ["deedee"],
});
});
});
const wtf = {
"o": "p",
"a with spaces": "b c",
" xa n p ": '"\r\nyoyoyo\r\r\n',
"[disturbing]": "hey you never know",
"s": "something",
"s1": "\"something'",
"s2": "something else",
"s3": true,
"s4": true,
"s5": " ",
"s6": " a ",
"s7": true,
"true": true,
"false": false,
"null": null,
"undefined": "undefined",
"zr": ["deedee"],
"ar": [["one"], "three", "this is included"],
"br": "warm",
"eq": "eq=eq",
"a": {
"av": "a val",
"e": '{ o: p, a: { av: a val, b: { c: { e: "this [value]" } } } }',
"j": '"{ o: "p", a: { av: "a val", b: { c: { e: "this [value]" } } } }"',
"[]": "a square?",
"cr": [["four"], "eight"],
"b": { "c": { "e": "1", "j": "2" } },
},
"b": {},
"x.y.z": {
"x.y.z": "xyz",
"a.b.c": {
"a.b.c": "abc",
"nocomment": "this; this is not a comment",
"noHashComment": "this# this is not a comment",
},
},
};