Repository navigation
Code Analysis Vite Config Merge
Vite #21164 changes how external: true combines with an external-module list. This study asks what changed, which configuration path reaches it, and what remains unknown. Source answers were fixed before running Archify. No Archify implementation defect was demonstrated, and this study adds no product change or PR.
The fixed versions are parent 74559c947483a8ee24da052ac2d9568f7cb3546a and head 5ef557a96c4a1f2b3a3aa25c12df3ee87b4a03f5. mergeConfigRecursively adds external to its true-dominates rule for ssr/resolve paths. Previously, true and ['bar'] fell through to array merging. The existing environment-path handling also applies this rule under environments.ssr.resolve. Changed predicate · Upstream expectation
An isolated Node 22.23.2 VM executed exact extracted source for four inspected functions and their environment-path regex, transpiled with TypeScript 5.9.3. Unmodeled alias/worker-plugin branches throw, rather than returning fabricated values.
| Inputs and path | Parent observation | Head observation |
|---|---|---|
true then ['bar'], at each of three supported paths |
[true, 'bar'] |
true |
['bar'] then true, at each of three supported paths |
['bar', true] |
true |
Twelve controls: existing noExternal, array merging, unrelated nested paths and allowedHosts
|
Expected prior behavior | Same behavior |
These 18 cases per side check merge values only. No Vite dependency installation, startup, upstream test suite or module externalization was executed.
The relevant unchanged path is config.ts's SSR/environment merge at lines 1184–1197 → exported mergeConfig → local mergeConfigRecursively. The { resolve: ... } wrapper preserves the root semantics. Configuration caller · Public wrapper
The analyzer is Draft #1229 at 3dda561d5db3323b00e8df20627ada96aec95900, call model v2 / adapter revision 11. Each root is packages/vite, with include: ["src/**"] and entry src/node/utils.ts. This is a package source scope using its captured compiler settings, not a whole-monorepo build.
- Each side captures 321 files. Two deliberately invalid SSR error fixtures remain unavailable; normal CLI returns exit 1 / partial and identifies both. The selected entry remains valid. Upstream error-fixture tests
- One changed definition is identified:
mergeConfigRecursively, lines 1236–1300. Its two supported references are the recursive call at 1286 and unchanged wrapper call at 1314. They do not constitute a transitive caller list. - The module reading list includes
config.ts's import at line 69, includingmergeConfig. It shows 40 of 74 references and explicitly reports 34 omissions. Source reading is still needed to connect the wrapper to configuration callsites at 1184 and 1235. - Both sides report the same observed resolution settings. Source and unpacked archive paired JSON match byte for byte. A fresh public reproduction also passes; the archive SHA256 is
e3142fb4b967a9ebde65bc1bbfe30f4019a6015885ebe4c12cb4e4206d445d00.
A separate agent, given the artifact and source but not the prewritten answer or oracle, explained the merge behavior and unchanged wrapper path and retained the runtime and coverage limits. This is one qualitative reading, with no source-only control or measured speed, accuracy or adoption claim.
A correct merge value does not establish which module is externalized or reproduce the reported server failure. Final environment configuration, module IDs, resolution, builtins and other conditions remain relevant. Externalization consumer
The result supports using changed-definition references and module imports as reading locations, then inspecting unchanged wrappers and downstream conditions. It does not justify adding automatic transitive or runtime-impact claims.
Use Git, Node 22, npm and Python 3 in a Unix shell. The script fetches the pinned analyzer/archive and complete Vite checkouts, verifies frozen source hashes, runs the inspected helper matrix, and compares normal source/archive CLI output. Dependencies are installed only in temporary analyzer copies. Expected CLI exit 1 is asserted as partial capture; the outer script exits 0 when every check passes.
Run the pinned Vite source, helper and archive checks
#!/usr/bin/env bash
set -euo pipefail
# Public replay: Node v22.23.2, Git, npm and unzip required. Vite dependencies are not installed.
node -e "if(process.version!=='v22.23.2') throw Error('Node v22.23.2 required: '+process.version)"
WORK=$(mktemp -d "${TMPDIR:-/tmp}/archify-vite-replay.XXXXXXXX")
echo "Results and checkouts: $WORK"
export ANALYZER_ROOT="$WORK/analyzer"
cat > "$WORK/expected.json" <<'ARCHIFY_EXPECTED'
{
"frozenBeforeArchify": true,
"frozenAtUtc": "2026-11-06T11:11:12.786856+00:00",
"analyzer": "3dda561d5db3323b00e8df20627ada96aec95900",
"upstream": "https://github.com/vitejs/vite",
"base": "74559c947483a8ee24da052ac2d9568f7cb3546a",
"head": "5ef557a96c4a1f2b3a3aa25c12df3ee87b4a03f5",
"entry": "src/node/utils.ts",
"questions": [
{
"id": "changedbehavior",
"question": "When a true external setting and an external list are merged, what changed, and under which configuration paths?",
"expected": "At ssr or resolve roots, including environment roots normalized by recursive merging, external now shares noExternal true-dominates behavior. Before merging true and [bar] produced [true,bar] or [bar,true]; after either order gives true. Other paths and two arrays retain existing merging.",
"source": [
"utils.ts:1263-1269",
"utils.ts:1281-1293",
"config.spec.ts:168-192"
]
},
{
"id": "affectedsourcepath",
"question": "Which concrete source path connects the changed helper to SSR/environment configuration?",
"expected": "config.ts imports mergeConfig from ./utils; resolveConfig merges config.ssr.external with environments.ssr.resolve at 1184-1197 under resolve root. Exported utils.mergeConfig at 1302-1315 calls mergeConfigRecursively at1314; recursion1286 routes to changed predicate1263. config.ts1235 also merges per-environment defaults. These are source reading routes, not complete impact or guaranteed runtime execution.",
"source": [
"config.ts:69-86",
"config.ts:1184-1197",
"config.ts:1234-1240",
"utils.ts:1302-1315",
"utils.ts:1285-1293"
]
},
{
"id": "remainingunknown",
"question": "Does this change prove which modules become external or that the reported dev-server crash is fixed?",
"expected": "No. Upstream unit test establishes intended merge-result shape and source explains predicate. Actual resolution/externalization depends on final config, environment, module IDs, resolver/plugin dispatch and installed dependencies. We have not run upstream tests, full Vite, rolldown, Vitest or the reported reproduction. Paired orientation cannot substitute for these.",
"source": [
"config.spec.ts:168-192",
"upstream PR21164 reproduction",
"config.ts:1184-1197"
]
}
],
"rootSelection": "Each complete Vite checkout/packages/vite, analysis include src/** retaining package tsconfig and local extends"
}
ARCHIFY_EXPECTED
cat > "$WORK/public-helper.mjs" <<'ARCHIFY_HELPER'
import fs from 'node:fs';
import path from 'node:path';
import vm from 'node:vm';
import assert from 'node:assert/strict';
import {createHash} from 'node:crypto';
import {createRequire} from 'node:module';
import {spawnSync} from 'node:child_process';
const evidence=import.meta.dirname;
const analyzer=process.env.ANALYZER_ROOT;
const ts=createRequire(analyzer+'/archify/modules/code-analysis/package.json')('typescript');
const hash=text=>createHash('sha256').update(text).digest('hex');
const pins={base:'74559c947483a8ee24da052ac2d9568f7cb3546a',head:'5ef557a96c4a1f2b3a3aa25c12df3ee87b4a03f5'};
const wrap=(parts,value)=>parts.reduceRight((v,key)=>({[key]:v}),value);
const cases=[];
for(const parts of [['ssr'],['resolve'],['environments','ssr','resolve']]) {
const label=parts.join('.');
for(const reverse of [false,true]) {
const values=reverse?[['bar'],true]:[true,['bar']];
cases.push({id:`external-${label}-${reverse?'list-first':'true-first'}`,defaults:wrap([...parts,'external'],values[0]),overrides:wrap([...parts,'external'],values[1]),expectedBase:wrap([...parts,'external'],reverse?['bar',true]:[true,'bar']),expectedHead:wrap([...parts,'external'],true)});
cases.push({id:`noExternal-${label}-${reverse?'list-first':'true-first'}`,defaults:wrap([...parts,'noExternal'],values[0]),overrides:wrap([...parts,'noExternal'],values[1]),expectedBase:wrap([...parts,'noExternal'],true),expectedHead:wrap([...parts,'noExternal'],true)});
}
cases.push({id:`array-${label}`,defaults:wrap([...parts,'external'],['foo']),overrides:wrap([...parts,'external'],['bar']),expectedBase:wrap([...parts,'external'],['foo','bar']),expectedHead:wrap([...parts,'external'],['foo','bar'])});
}
for(const reverse of [false,true]) {
const values=reverse?[['bar'],true]:[true,['bar']];
const expected=wrap(['unrelated','external'],reverse?['bar',true]:[true,'bar']);
cases.push({id:`unrelated-${reverse?'list-first':'true-first'}`,defaults:wrap(['unrelated','external'],values[0]),overrides:wrap(['unrelated','external'],values[1]),expectedBase:expected,expectedHead:expected});
}
cases.push({id:'allowedHosts-existing-control',defaults:{server:{allowedHosts:true}},overrides:{server:{allowedHosts:['example.test']}},expectedBase:{server:{allowedHosts:true}},expectedHead:{server:{allowedHosts:true}}});
// Persist independent expected values before loading/transpiling/executing helpers.
fs.writeFileSync(path.join(evidence,'helper-expected.json'),JSON.stringify(cases,null,2)+'\n');
assert.equal(hash(fs.readFileSync(path.join(evidence,'helper-expected.json'))),'065e2db390c64656683857bfede094adcb4345f68f2cb9bf0b81e91d72527359');
assert.equal(ts.version,'5.9.3');
const result={node:process.version,executable:process.execPath,typescript:ts.version,analyzerPin:'3dda561d5db3323b00e8df20627ada96aec95900',expectedSha256:hash(fs.readFileSync(path.join(evidence,'helper-expected.json'))),variants:{},limits:'Only inspected exact mergeConfig/mergeConfigRecursively/arraify/isObject and environmentPathRE are executed. Alias and worker plugin helpers are deliberately unavailable and throw if reached. No whole Vite import, dependencies, startup, module externalization or upstream tests executed.'};
for(const [side,pin] of Object.entries(pins)) {
const root=path.join(evidence,side);
const git=spawnSync('git',['-C',root,'rev-parse','HEAD'],{encoding:'utf8'});
assert.equal(git.status,0);assert.equal(git.stdout.trim(),pin);
const sourceFile=path.join(root,'packages/vite/src/node/utils.ts');
const sourceText=fs.readFileSync(sourceFile,'utf8');
assert.equal(hash(sourceText),side==='base'?'89d589193040af29042d6cb88c3e45bb1abc2e092f78501f9627d1bd68c28752':'8bd21f14321dfdee876423dbe4735494d8ec423f32211aef10baef2e4cbc7f8c');
const source=ts.createSourceFile(sourceFile,sourceText,ts.ScriptTarget.Latest,true);
const names=['mergeConfig','mergeConfigRecursively','arraify','isObject'];
const nodes=names.map(name=>{
const matches=source.statements.filter(n=>ts.isFunctionDeclaration(n)&&n.name?.text===name);
assert.equal(matches.length,1,name);return {name,node:matches[0]};
});
const constant=source.statements.filter(n=>ts.isVariableStatement(n)&&n.declarationList.declarations.some(d=>ts.isIdentifier(d.name)&&d.name.text==='environmentPathRE'));
assert.equal(constant.length,1);
nodes.unshift({name:'environmentPathRE',node:constant[0]});
const declarations=nodes.map(({node})=>sourceText.slice(node.getStart(source),node.getEnd())).join('\n\n');
assert.equal(hash(declarations),side==='base'?'a9bf4245994496da186ef45fb2e3801b9800db1c1f1bcd4d2d327885346d4244':'45cd574d51e6bb2b9cd41f6a345715223586a707affaaa09d563dff63dfc1281');
fs.writeFileSync(path.join(evidence,`helper-${side}-closure.ts`),declarations+'\n');
const emitted=ts.transpileModule(declarations,{compilerOptions:{target:ts.ScriptTarget.ES2023,module:ts.ModuleKind.CommonJS},reportDiagnostics:true});
assert.ok(!emitted.diagnostics?.some(d=>d.category===ts.DiagnosticCategory.Error));
assert.equal(hash(emitted.outputText),side==='base'?'c343cade657942fac529b23103325b1b1e6737ec6879b43b0046015ab15e0d27':'59689d80ca6837ba921268b03786d148a1efb7861511446ab3610a0fde955ba6');
fs.writeFileSync(path.join(evidence,`helper-${side}-closure.js`),emitted.outputText);
const context=vm.createContext({exports:{}});
vm.runInContext(`function mergeAlias(){throw new Error('UNMODELED_ALIAS_BRANCH')}; function backwardCompatibleWorkerPlugins(){throw new Error('UNMODELED_WORKER_PLUGIN_BRANCH')};`,context,{timeout:1000});
vm.runInContext(emitted.outputText,context,{timeout:1000});
// Prove omitted helper paths fail rather than yielding invented results.
assert.throws(()=>vm.runInContext(`mergeConfig({resolve:{alias:{x:'a'}}},{resolve:{alias:{x:'b'}}})`,context,{timeout:1000}),/UNMODELED_ALIAS_BRANCH/);
assert.throws(()=>vm.runInContext(`mergeConfig({worker:{plugins:[]}},{worker:{plugins:[]}}).worker.plugins()`,context,{timeout:1000}),/UNMODELED_WORKER_PLUGIN_BRANCH/);
const observations=[];
for(const item of cases) {
context.__defaults=JSON.stringify(item.defaults);context.__overrides=JSON.stringify(item.overrides);
const actual=JSON.parse(vm.runInContext(`JSON.stringify(mergeConfig(JSON.parse(__defaults),JSON.parse(__overrides)))`,context,{timeout:1000}));
const expected=item[side==='base'?'expectedBase':'expectedHead'];
observations.push({id:item.id,expected,actual});
// Preserve observations up to the first failure if any assertion fails.
fs.writeFileSync(path.join(evidence,`helper-${side}-observations.json`),JSON.stringify(observations,null,2)+'\n');
assert.deepEqual(actual,expected,`${side}: ${item.id}`);
}
result.variants[side]={pin,sourceFile,sourceSha256:hash(sourceText),closureSha256:hash(declarations),emittedSha256:hash(emitted.outputText),functions:nodes.map(({name,node})=>({name,line:source.getLineAndCharacterOfPosition(node.getStart(source)).line+1,endLine:source.getLineAndCharacterOfPosition(node.getEnd()).line+1,sha256:hash(sourceText.slice(node.getStart(source),node.getEnd()))})),observations,unmodeledBranchGuards:'alias and worker-plugin guards both throw as expected'};
}
fs.writeFileSync(path.join(evidence,'helper-result.json'),JSON.stringify(result,null,2)+'\n');
console.log(JSON.stringify({node:result.node,typescript:result.typescript,casesPerSide:cases.length,changedCases:cases.filter(c=>JSON.stringify(c.expectedBase)!==JSON.stringify(c.expectedHead)).length,unchangedControls:cases.filter(c=>JSON.stringify(c.expectedBase)===JSON.stringify(c.expectedHead)).length,verified:true},null,2));
ARCHIFY_HELPER
cat > "$WORK/public-orientation.mjs" <<'ARCHIFY_ORIENTATION'
import fs from 'node:fs';
import path from 'node:path';
import assert from 'node:assert/strict';
import {spawnSync} from 'node:child_process';
import {createHash} from 'node:crypto';
import {fileURLToPath} from 'node:url';
const e=path.dirname(fileURLToPath(import.meta.url));
const label=process.argv[2]||'source';
const file=path.join(e,label==='source'?'paired.json':`paired-${label}.json`);
const sha=x=>createHash('sha256').update(x).digest('hex');
if(process.argv[3]){
const args=[process.argv[3],'code-analysis','task','--base-root',`${e}/base/packages/vite`,'--root',`${e}/head/packages/vite`,'--entry','src/node/utils.ts','--config',`${e}/analysis-config.json`,'--question','What changed when true and an external list are merged, which SSR/environment source path reaches this helper, and what remains unknown about module externalization and the reported server crash?','--format','json','--out',file,'--json'];
const r=spawnSync(process.execPath,args,{encoding:'utf8',timeout:240000,maxBuffer:4*1024*1024});
fs.writeFileSync(path.join(e,`${label}-receipt.json`),r.stdout);fs.writeFileSync(path.join(e,`${label}.stderr`),r.stderr);
assert.equal(r.status,1,r.stderr||r.stdout); // Explicit partial capture, not a failed finding gate.
}
const b=JSON.parse(fs.readFileSync(file));
assert.equal(b.status,'partial');assert.equal(b.stage,'orientation-only');
assert.equal(b.comparison.bindingAttribution,'same-observed-settings');
assert.equal(b.comparison.sourceText,'reading-signals-only');
assert.deepEqual(b.comparison.reasons.map(x=>[x.code,x.side,x.count]),[['source-regions-unavailable','base',2],['source-regions-unavailable','head',2]]);
assert.deepEqual(b.fileChanges.items.map(x=>x.file),['src/node/__tests__/config.spec.ts','src/node/utils.ts']);
assert.equal(b.changedDefinitions.total,1);assert.equal(b.unpairedDefinitions.total,0);
const result=[];
for(const [side,pin] of [['base','74559c947483a8ee24da052ac2d9568f7cb3546a'],['head','5ef557a96c4a1f2b3a3aa25c12df3ee87b4a03f5']]){
const root=path.join(e,side);
const git=spawnSync('git',['-C',root,'status','--porcelain','--untracked-files=no'],{encoding:'utf8'});assert.equal(git.status,0);assert.equal(git.stdout,'');
const s=b.sides[side],d=b.changedDefinitions.items[0][side];
assert.equal(s.snapshot.baseRevision,pin);assert.equal(s.snapshot.adapterRevision,11);assert.equal(s.callModel,'js-ts-lexical-v2');assert.equal(s.capturedFiles,321);assert.equal(s.sourceIdentity,'valid');assert.equal(s.parseErrors,2);
assert.deepEqual(s.unavailableFiles.items.map(x=>x.file),['src/node/ssr/__tests__/fixtures/errors/syntax-error.js','src/node/ssr/__tests__/fixtures/errors/syntax-error.ts']);
assert.equal(d.definition.name,'mergeConfigRecursively');assert.equal(d.definition.line,1236);assert.equal(d.definition.endLine,1300);
assert.equal(d.knownReferences.total,2);assert.equal(d.knownReferences.omitted,0);
assert.deepEqual(d.knownReferences.items.map(x=>[x.fact.file,x.fact.line,x.navigation.status]),[['src/node/utils.ts',1286,'valid'],['src/node/utils.ts',1314,'valid']]);
assert.equal(d.unresolvedWithinSourceRange.total,4);
const imports=b.moduleReading[side].resolvedReferences;
assert.equal(imports.total,74);assert.equal(imports.included,40);assert.equal(imports.omitted,34);
const config=imports.items.find(x=>x.fact.from==='src/node/config.ts'&&x.fact.line===69);
assert.ok(config?.fact.names.includes('mergeConfig'));assert.equal(config.navigation.status,'valid');
const source=fs.readFileSync(path.join(root,'packages/vite/src/node/config.ts'),'utf8').split('\n');
assert.ok(source[1183].includes('mergeConfig('));assert.ok(source[1234].includes('mergeConfig('));
result.push({side,pin,files:321,parseErrors:2,changedHelper:d.definition.name,helperReferences:[1286,1314],configImport:69,sourceCheckedWrapperCallers:[1184,1235],referenceList:{total:74,included:40,omitted:34}});
}
if(label==='archive')assert.equal(sha(fs.readFileSync(file)),sha(fs.readFileSync(path.join(e,'paired.json'))),'Source/archive paired artifact byte parity');
fs.writeFileSync(path.join(e,`${label}-verification.json`),JSON.stringify({node:process.version,artifactSha256:sha(fs.readFileSync(file)),expectedExit:1,status:'partial',result,archiveParity:label==='archive',claim:'Reading locations agree with source; cross-file wrapper path still requires source reading, not complete runtime impact.'},null,2)+'\n');
console.log(JSON.stringify({label,status:'partial',sides:result,archiveParity:label==='archive'}));
ARCHIFY_ORIENTATION
printf '%s\n' '{"include":["src/**"]}' > "$WORK/analysis-config.json"
fetch_checkout() {
local url=$1 revision=$2 dest=$3 pattern=$4
git init -q "$dest"
git -C "$dest" remote add origin "$url"
git -C "$dest" config http.version HTTP/1.1
if [[ -n "$pattern" ]]; then
git -C "$dest" config core.sparseCheckout true
printf '%s\n' "$pattern" > "$dest/.git/info/sparse-checkout"
fi
git -C "$dest" fetch --quiet --depth=2 --filter=blob:none origin "$revision"
git -C "$dest" checkout -q --detach FETCH_HEAD
test "$(git -C "$dest" rev-parse HEAD)" = "$revision"
}
fetch_checkout https://github.com/tt-a1i/archify.git 3dda561d5db3323b00e8df20627ada96aec95900 "$WORK/analyzer" $'/archify/\n/archify.zip'
node - "$WORK/analyzer/archify.zip" <<'JS'
const fs=require('node:fs'),c=require('node:crypto'),assert=require('node:assert/strict');
const sha=c.createHash('sha256').update(fs.readFileSync(process.argv[2])).digest('hex');
assert.equal(sha,'e3142fb4b967a9ebde65bc1bbfe30f4019a6015885ebe4c12cb4e4206d445d00'); console.log('Archive SHA256:',sha);
JS
mkdir "$WORK/archive"
unzip -q "$WORK/analyzer/archify.zip" -d "$WORK/archive"
for dir in "$WORK/analyzer/archify/modules/code-analysis" "$WORK/archive/archify/modules/code-analysis"; do
(cd "$dir" && npm ci --ignore-scripts --no-audit --no-fund)
done
# Full tracked Vite checkouts retain tsconfig and source scope; no target dependencies are installed.
fetch_checkout https://github.com/vitejs/vite.git 74559c947483a8ee24da052ac2d9568f7cb3546a "$WORK/base" ''
fetch_checkout https://github.com/vitejs/vite.git 5ef557a96c4a1f2b3a3aa25c12df3ee87b4a03f5 "$WORK/head" ''
git -C "$WORK/head" -c color.ui=false diff --no-ext-diff --abbrev=9 74559c947483a8ee24da052ac2d9568f7cb3546a 5ef557a96c4a1f2b3a3aa25c12df3ee87b4a03f5 > "$WORK/upstream.patch"
node - "$WORK" <<'JS'
const fs=require('node:fs'),path=require('node:path'),c=require('node:crypto'),assert=require('node:assert/strict'); const w=process.argv[2];
for(const [name,sha] of [
['expected.json','3233a6600bae0c4363832fe5e235931a8a44d9b602356f092aabdb9061687712'],
['upstream.patch','a64d043f78dff51f177f07d41663620eedfb00ffee361da18eb687e3ca0e60f8'],
['head/packages/vite/src/node/utils.ts','8bd21f14321dfdee876423dbe4735494d8ec423f32211aef10baef2e4cbc7f8c'],
['head/packages/vite/src/node/config.ts','f6077a2115e2c906e5fab179c22970b496cb376fec38584b0f5078cd45b95f0d'],
['head/packages/vite/src/node/__tests__/config.spec.ts','14b9941068ec0b34db9416d6f3b289b279184e00725b9c89cd7e8ce578c4f5ed']
]) { const actual=c.createHash('sha256').update(fs.readFileSync(path.join(w,name))).digest('hex');assert.equal(actual,sha,name); }
console.log('Frozen input hashes verified');
JS
node "$WORK/public-helper.mjs"
node "$WORK/public-orientation.mjs" source "$WORK/analyzer/archify/bin/archify.mjs"
node "$WORK/public-orientation.mjs" archive "$WORK/archive/archify/bin/archify.mjs"
echo "Verified; results retained at $WORK"