From 81b4c132b5fa55a21af5ca90cacbaa2f69d15131 Mon Sep 17 00:00:00 2001 From: ethernet Date: Sat, 12 Sep 2026 14:06:35 -0400 Subject: [PATCH] fix(build): normalize npm config names in dependency receipts --- scripts/build/node-deps.mjs | 4 +++- tests-js/node-deps.test.mjs | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/scripts/build/node-deps.mjs b/scripts/build/node-deps.mjs index 3aa87eb3a4..e5566e9e1d 100644 --- a/scripts/build/node-deps.mjs +++ b/scripts/build/node-deps.mjs @@ -64,7 +64,9 @@ export function prepareNodeDependencies({ source, workspaces, env = process.env, const hiddenLock = join(source, 'node_modules/.package-lock.json') const inputs = createHash('sha256').update(JSON.stringify({ node: process.versions.node, npm: npmVersion, platform: process.platform, arch: process.arch, args, - config: Object.entries(env).filter(([key]) => /^npm_config_/i.test(key) && !/^npm_config_(cache|offline|prefer_offline)$/i.test(key)).sort(), + // npm names are case-insensitive; Windows Python uppercases inherited keys. + config: Object.entries(env).filter(([key]) => /^npm_config_/i.test(key) && !/^npm_config_(cache|offline|prefer_offline)$/i.test(key)) + .map(([key, value]) => [key.toLowerCase(), value]).sort(), })) const files = ['package-lock.json', '.npmrc', ...Object.keys(lock.packages) .filter(path => !path.split('/').includes('node_modules')) diff --git a/tests-js/node-deps.test.mjs b/tests-js/node-deps.test.mjs index 829da7a0c6..f2c38d58af 100644 --- a/tests-js/node-deps.test.mjs +++ b/tests-js/node-deps.test.mjs @@ -116,3 +116,21 @@ test('reuse respects lifecycle configuration and repairs missing installed packa prepareNodeDependencies({ ...options, reuse: false }) expect(readFileSync(artifact, 'utf8')).not.toBe(repaired) }, 30000) + +test('npm configuration name casing does not invalidate a completed install', async () => { + const { prepareNodeDependencies } = await import('../scripts/build/node-deps.mjs') + const source = fixture() + const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => key.toLowerCase() !== 'npm_config_prefix')) + Object.assign(env, { npm_config_offline: 'true', npm_config_cache: join(source, '.npm-cache') }) + const prefix = join(source, 'npm-prefix') + const options = { source, workspaces: ['web'], reuse: true } + prepareNodeDependencies({ ...options, env: { ...env, npm_config_prefix: prefix } }) + const artifact = join(source, 'node_modules/postinstall-output') + writeFileSync(artifact, 'built') + // The Windows runner sets lowercase; Python's os.environ returns uppercase. + prepareNodeDependencies({ ...options, env: { ...env, NPM_CONFIG_PREFIX: prefix } }) + expect(readFileSync(artifact, 'utf8')).toBe('built') + // Normalize names, not values: a genuinely changed setting still reinstalls. + prepareNodeDependencies({ ...options, env: { ...env, NPM_CONFIG_PREFIX: join(source, 'other-prefix') } }) + expect(existsSync(artifact)).toBe(false) +}, 30000)