Skip to content

Commit

Permalink
fix(router-plugin): do not split if known ident is exported (#2334)
Browse files Browse the repository at this point in the history
  • Loading branch information
SeanCassiere authored Sep 18, 2024
1 parent db02bef commit df67018
Show file tree
Hide file tree
Showing 13 changed files with 491 additions and 65 deletions.
278 changes: 213 additions & 65 deletions packages/router-plugin/src/core/code-splitter/compilers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,95 +98,118 @@ export function compileCodeSplitReferenceRoute(opts: ParseAstOptions) {
if (prop.key.name === 'component') {
const value = prop.value

let shouldSplit = true

if (t.isIdentifier(value)) {
existingCompImportPath =
getImportSpecifierAndPathFromLocalName(
programPath,
value.name,
).path

removeIdentifierLiteral(path, value)
}

// Prepend the import statement to the program along with the importer function
// Check to see if lazyRouteComponent is already imported before attempting
// to import it again
// exported identifiers should not be split
// since they are already being imported
// and need to be retained in the compiled file
const isExported = hasExport(ast, value)
shouldSplit = !isExported

if (
!hasImportedOrDefinedIdentifier(
'lazyRouteComponent',
)
) {
programPath.unshiftContainer('body', [
template.statement(
`import { lazyRouteComponent } from '@tanstack/react-router'`,
)(),
])
if (shouldSplit) {
removeIdentifierLiteral(path, value)
}
}

if (
!hasImportedOrDefinedIdentifier(
'$$splitComponentImporter',
)
) {
programPath.unshiftContainer('body', [
if (shouldSplit) {
// Prepend the import statement to the program along with the importer function
// Check to see if lazyRouteComponent is already imported before attempting
// to import it again

if (
!hasImportedOrDefinedIdentifier(
'lazyRouteComponent',
)
) {
programPath.unshiftContainer('body', [
template.statement(
`import { lazyRouteComponent } from '@tanstack/react-router'`,
)(),
])
}

if (
!hasImportedOrDefinedIdentifier(
'$$splitComponentImporter',
)
) {
programPath.unshiftContainer('body', [
template.statement(
`const $$splitComponentImporter = () => import('${splitUrl}')`,
)(),
])
}

prop.value = template.expression(
`lazyRouteComponent($$splitComponentImporter, 'component')`,
)()

programPath.pushContainer('body', [
template.statement(
`const $$splitComponentImporter = () => import('${splitUrl}')`,
`function DummyComponent() { return null }`,
)(),
])
}

prop.value = template.expression(
`lazyRouteComponent($$splitComponentImporter, 'component')`,
)()

programPath.pushContainer('body', [
template.statement(
`function DummyComponent() { return null }`,
)(),
])

found = true
found = true
}
} else if (prop.key.name === 'loader') {
const value = prop.value

let shouldSplit = true

if (t.isIdentifier(value)) {
existingLoaderImportPath =
getImportSpecifierAndPathFromLocalName(
programPath,
value.name,
).path

removeIdentifierLiteral(path, value)
}

// Prepend the import statement to the program along with the importer function
// exported identifiers should not be split
// since they are already being imported
// and need to be retained in the compiled file
const isExported = hasExport(ast, value)
shouldSplit = !isExported

if (!hasImportedOrDefinedIdentifier('lazyFn')) {
programPath.unshiftContainer('body', [
template.smart(
`import { lazyFn } from '@tanstack/react-router'`,
)() as t.Statement,
])
if (shouldSplit) {
removeIdentifierLiteral(path, value)
}
}

if (
!hasImportedOrDefinedIdentifier(
'$$splitLoaderImporter',
)
) {
programPath.unshiftContainer('body', [
template.statement(
`const $$splitLoaderImporter = () => import('${splitUrl}')`,
)(),
])
if (shouldSplit) {
// Prepend the import statement to the program along with the importer function
if (!hasImportedOrDefinedIdentifier('lazyFn')) {
programPath.unshiftContainer('body', [
template.smart(
`import { lazyFn } from '@tanstack/react-router'`,
)() as t.Statement,
])
}

if (
!hasImportedOrDefinedIdentifier(
'$$splitLoaderImporter',
)
) {
programPath.unshiftContainer('body', [
template.statement(
`const $$splitLoaderImporter = () => import('${splitUrl}')`,
)(),
])
}

prop.value = template.expression(
`lazyFn($$splitLoaderImporter, 'loader')`,
)()

found = true
}

prop.value = template.expression(
`lazyFn($$splitLoaderImporter, 'loader')`,
)()

found = true
}
}
}
Expand Down Expand Up @@ -251,6 +274,8 @@ export function compileCodeSplitVirtualRoute(opts: ParseAstOptions) {
)
}

const knownExportedIdents = new Set<string>()

babel.traverse(ast, {
Program: {
enter(programPath, programState) {
Expand Down Expand Up @@ -287,12 +312,31 @@ export function compileCodeSplitVirtualRoute(opts: ParseAstOptions) {
if (t.isObjectExpression(options)) {
options.properties.forEach((prop) => {
if (t.isObjectProperty(prop)) {
splitNodeTypes.forEach((type) => {
if (t.isIdentifier(prop.key)) {
if (prop.key.name === type) {
splitNodesByType[type] = prop.value
splitNodeTypes.forEach((splitType) => {
if (
!t.isIdentifier(prop.key) ||
prop.key.name !== splitType
) {
return
}

const value = prop.value

let isExported = false
if (t.isIdentifier(value)) {
isExported = hasExport(ast, value)
if (isExported) {
knownExportedIdents.add(value.name)
}
}

// If the node is exported, we need to remove
// the export from the split file
if (isExported && t.isIdentifier(value)) {
removeExports(ast, value)
} else {
splitNodesByType[splitType] = prop.value
}
})
}
})
Expand Down Expand Up @@ -448,6 +492,28 @@ export function compileCodeSplitVirtualRoute(opts: ParseAstOptions) {

deadCodeElimination(ast)

// if there are exported identifiers, then we need to add a warning
// to the file to let the user know that the exported identifiers
// will not in the split file but in the original file, therefore
// increasing the bundle size
if (knownExportedIdents.size > 0) {
const list = Array.from(knownExportedIdents).reduce((str, ident) => {
str += `\n- ${ident}`
return str
}, '')

const warningMessage = `These exports from "${opts.filename.replace('?' + splitPrefix, '')}" are not being code-split and will increase your bundle size: ${list}\nThese should either have their export statements removed or be imported from another file that is not a route.`
console.warn(warningMessage)

// append this warning to the file using a template
if (process.env.NODE_ENV !== 'production') {
const warningTemplate = template.statement(
`console.warn(${JSON.stringify(warningMessage)})`,
)()
ast.program.body.unshift(warningTemplate)
}
}

return generate(ast, {
sourceMaps: true,
})
Expand Down Expand Up @@ -515,3 +581,85 @@ function removeIdentifierLiteral(path: any, node: any) {
}
}
}

function hasExport(ast: t.File, node: t.Identifier): boolean {
let found = false

babel.traverse(ast, {
ExportNamedDeclaration(path) {
if (path.node.declaration) {
// declared as `const loaderFn = () => {}`
if (t.isVariableDeclaration(path.node.declaration)) {
path.node.declaration.declarations.forEach((decl) => {
if (t.isVariableDeclarator(decl)) {
if (t.isIdentifier(decl.id)) {
if (decl.id.name === node.name) {
found = true
}
}
}
})
}

// declared as `function loaderFn() {}`
if (t.isFunctionDeclaration(path.node.declaration)) {
if (t.isIdentifier(path.node.declaration.id)) {
if (path.node.declaration.id.name === node.name) {
found = true
}
}
}
}
},
ExportDefaultDeclaration(path) {
if (t.isIdentifier(path.node.declaration)) {
if (path.node.declaration.name === node.name) {
found = true
}
}
},
})

return found
}

function removeExports(ast: t.File, node: t.Identifier): boolean {
let removed = false

babel.traverse(ast, {
ExportNamedDeclaration(path) {
if (path.node.declaration) {
// declared as `const loaderFn = () => {}`
if (t.isVariableDeclaration(path.node.declaration)) {
path.node.declaration.declarations.forEach((decl) => {
if (t.isVariableDeclarator(decl)) {
if (t.isIdentifier(decl.id)) {
if (decl.id.name === node.name) {
path.remove()
removed = true
}
}
}
})
} else if (t.isFunctionDeclaration(path.node.declaration)) {
if (t.isIdentifier(path.node.declaration.id)) {
if (path.node.declaration.id.name === node.name) {
path.remove()
removed = true
}
}
}
}
},
ExportDefaultDeclaration(path) {
if (t.isIdentifier(path.node.declaration)) {
if (path.node.declaration.name === node.name) {
path.remove()
removed = true
}
}
},
})

return removed
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const $$splitLoaderImporter = () => import('tsr-split:retain-export-component.tsx?tsr-split');
import { lazyFn } from '@tanstack/react-router';
import { createFileRoute, Outlet } from '@tanstack/react-router';
import { importedComponent as ImportedComponent } from '../shared/imported';
export function Layout() {
return <main>
<header style={{
height: HEADER_HEIGHT
}}>
<nav>
<ul>
<li>
<a href="/">Home</a>
</li>
</ul>
</nav>
</header>
<ImportedComponent />
<Outlet />
</main>;
}
export const Route = createFileRoute('/_layout')({
component: Layout,
loader: lazyFn($$splitLoaderImporter, 'loader')
});
const HEADER_HEIGHT = '63px';
export const SIDEBAR_WIDTH = '150px';
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
console.warn("These exports from \"retain-export-component.tsx\" are not being code-split and will increase your bundle size: \n- Layout\nThese should either have their export statements removed or be imported from another file that is not a route.");
import { importedLoader } from '../shared/imported';
const loader = importedLoader;
export { loader };
Loading

0 comments on commit df67018

Please sign in to comment.