Skip to content

Commit 43ec44b

Browse files
Fix import removal for omitted route chunks
1 parent c6efa78 commit 43ec44b

File tree

2 files changed

+104
-34
lines changed

2 files changed

+104
-34
lines changed

packages/react-router-dev/vite/route-chunks-test.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,13 +178,58 @@ describe("route chunks", () => {
178178
"import { shared } from "./shared";
179179
export const chunk = shared("chunk");"
180180
`);
181-
expect(getChunkedExport(code, "main", {}, ...cache)?.code)
181+
expect(omitChunkedExports(code, ["chunk"], {}, ...cache)?.code)
182182
.toMatchInlineSnapshot(`
183183
"import { shared } from "./shared";
184184
export const main = shared("main");"
185185
`);
186186
});
187187

188+
test("shared imports across chunks but not main chunk", () => {
189+
const code = dedent`
190+
import { shared } from "./shared";
191+
export const chunk1 = shared("chunk1");
192+
export const chunk2 = shared("chunk2");
193+
export const main = "main";
194+
`;
195+
expect(hasChunkableExport(code, "chunk1", ...cache)).toBe(true);
196+
expect(hasChunkableExport(code, "chunk2", ...cache)).toBe(true);
197+
expect(hasChunkableExport(code, "main", ...cache)).toBe(true);
198+
expect(getChunkedExport(code, "chunk1", {}, ...cache)?.code)
199+
.toMatchInlineSnapshot(`
200+
"import { shared } from "./shared";
201+
export const chunk1 = shared("chunk1");"
202+
`);
203+
expect(getChunkedExport(code, "chunk2", {}, ...cache)?.code)
204+
.toMatchInlineSnapshot(`
205+
"import { shared } from "./shared";
206+
export const chunk2 = shared("chunk2");"
207+
`);
208+
expect(
209+
omitChunkedExports(code, ["chunk1", "chunk2"], {}, ...cache)?.code
210+
).toMatchInlineSnapshot(`"export const main = "main";"`);
211+
});
212+
213+
test("import with side effect usage", () => {
214+
const code = dedent`
215+
import { sideEffect } from "./side-effect";
216+
sideEffect();
217+
export const chunk = "chunk";
218+
export const main = "main";
219+
`;
220+
expect(hasChunkableExport(code, "chunk", ...cache)).toBe(true);
221+
expect(hasChunkableExport(code, "main", ...cache)).toBe(true);
222+
expect(
223+
getChunkedExport(code, "chunk", {}, ...cache)?.code
224+
).toMatchInlineSnapshot(`"export const chunk = "chunk";"`);
225+
expect(omitChunkedExports(code, ["chunk"], {}, ...cache)?.code)
226+
.toMatchInlineSnapshot(`
227+
"import { sideEffect } from "./side-effect";
228+
sideEffect();
229+
export const main = "main";"
230+
`);
231+
});
232+
188233
test("re-exports using shared statement", () => {
189234
const code = dedent`
190235
export { chunk1, chunk2, main } from "./shared";
@@ -927,6 +972,29 @@ describe("route chunks", () => {
927972
export const main = getMainMessage();"
928973
`);
929974
});
975+
976+
test("shared imports across chunks but not main chunk with shared side effect usage", () => {
977+
const code = dedent`
978+
import { shared } from "./shared";
979+
shared("side-effect");
980+
export const chunk1 = shared("chunk1");
981+
export const chunk2 = shared("chunk2");
982+
export const main = "main";
983+
`;
984+
expect(hasChunkableExport(code, "chunk1", ...cache)).toBe(false);
985+
expect(hasChunkableExport(code, "chunk2", ...cache)).toBe(false);
986+
expect(hasChunkableExport(code, "main", ...cache)).toBe(true);
987+
expect(getChunkedExport(code, "chunk1", {}, ...cache)).toBeUndefined();
988+
expect(getChunkedExport(code, "chunk2", {}, ...cache)).toBeUndefined();
989+
expect(omitChunkedExports(code, ["chunk1", "chunk2"], {}, ...cache)?.code)
990+
.toMatchInlineSnapshot(`
991+
"import { shared } from "./shared";
992+
shared("side-effect");
993+
export const chunk1 = shared("chunk1");
994+
export const chunk2 = shared("chunk2");
995+
export const main = "main";"
996+
`);
997+
});
930998
});
931999

9321000
describe("export dependency analysis", () => {

packages/react-router-dev/vite/route-chunks.ts

Lines changed: 35 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -702,22 +702,31 @@ export function omitChunkedExports(
702702
)}::${JSON.stringify(generateOptions)}`,
703703
code,
704704
() => {
705+
const isChunkable = (exportName: string): boolean =>
706+
hasChunkableExport(code, exportName, cache, cacheKey);
707+
708+
const isOmitted = (exportName: string): boolean =>
709+
exportNames.includes(exportName) && isChunkable(exportName);
710+
711+
const isRetained = (exportName: string): boolean =>
712+
!isOmitted(exportName);
713+
705714
let exportDependencies = getExportDependencies(code, cache, cacheKey);
706715

716+
let allExportNames = Array.from(exportDependencies.keys());
717+
let omittedExportNames = allExportNames.filter(isOmitted);
718+
let retainedExportNames = allExportNames.filter(isRetained);
719+
707720
let omittedStatements = new Set<Statement>();
708721
let omittedExportedVariableDeclarators = new Set<VariableDeclarator>();
709722

710-
for (let exportName of exportNames) {
711-
let dependencies = exportDependencies.get(exportName);
723+
for (let omittedExportName of omittedExportNames) {
724+
let dependencies = exportDependencies.get(omittedExportName);
712725

713-
// If the export is not chunkable then its code will still remain in the
714-
// main chunk, so we need to keep its top level statements.
715-
if (
716-
!dependencies ||
717-
!hasChunkableExport(code, exportName, cache, cacheKey)
718-
) {
719-
continue;
720-
}
726+
invariant(
727+
dependencies,
728+
`Expected dependencies for ${omittedExportName}`
729+
);
721730

722731
// Now that we know the export is chunkable, add all of its top level
723732
// non-module statements to the set of statements to be omitted from the
@@ -742,14 +751,6 @@ export function omitChunkedExports(
742751
omittedExportedVariableDeclarators
743752
);
744753

745-
function isChunkable(exportName: string): boolean {
746-
return hasChunkableExport(code, exportName, cache, cacheKey);
747-
}
748-
749-
function isOmitted(exportName: string): boolean {
750-
return exportNames.includes(exportName) && isChunkable(exportName);
751-
}
752-
753754
ast.program.body = ast.program.body
754755
// Remove top level statements that belong solely to the chunked
755756
// exports that are being omitted.
@@ -774,27 +775,28 @@ export function omitChunkedExports(
774775
// Remove import specifiers that are only used by the omitted chunks.
775776
// This ensures only the necessary imports remain in the main chunk.
776777
node.specifiers = node.specifiers.filter((specifier) => {
777-
// Check the imported identifiers that each export depends on to see
778-
// if it includes the specifier's local name.
779-
for (let exportName of exportNames) {
780-
// If the export is not chunkable then its code will still remain
781-
// in the main chunk, so we need to keep its imports.
782-
if (!isChunkable(exportName)) {
783-
continue;
778+
let importedName = specifier.local.name;
779+
780+
// Keep the import specifier if it's depended on by any of the
781+
// retained exports.
782+
for (let retainedExportName of retainedExportNames) {
783+
let dependencies = exportDependencies.get(retainedExportName);
784+
if (dependencies?.importedIdentifierNames?.has(importedName)) {
785+
return true;
784786
}
787+
}
785788

786-
let importedIdentifierNames =
787-
exportDependencies.get(exportName)?.importedIdentifierNames;
788-
789-
// If the import specifier's local name is in the set of imported
790-
// identifiers for the chunked export, we filter it out.
791-
if (importedIdentifierNames?.has(specifier.local.name)) {
789+
// Now that we've bailed out early and kept the import specifier if
790+
// any retained exports depend on it, remove the import specifier if
791+
// it's depended on by any of the omitted exports.
792+
for (let omittedExportName of omittedExportNames) {
793+
let dependencies = exportDependencies.get(omittedExportName);
794+
if (dependencies?.importedIdentifierNames?.has(importedName)) {
792795
return false;
793796
}
794797
}
795798

796-
// If we didn't return false, the specifier is not in the set of
797-
// imported identifiers for any chunked export, so we keep it.
799+
// Keep the import specifier if it isn't depended on by any export.
798800
return true;
799801
});
800802

0 commit comments

Comments
 (0)