one
Version:
One is a new React Framework that makes Vite serve both native and web.
168 lines (147 loc) • 6.48 kB
text/typescript
/**
* Babel plugin to remove server-only code (loader, generateStaticParams) from native bundles.
*
* This is the Metro equivalent of clientTreeShakePlugin. It:
* 1. Captures referenced identifiers BEFORE removing exports (critical for DCE)
* 2. Removes server-only exports (loader, generateStaticParams)
* 3. Re-parses the modified code and runs standalone DCE with the pre-removal references
* 4. Adds empty stubs back to prevent "missing export" errors
*
* The re-parse step is necessary because babel-dead-code-elimination uses NodePath
* identity (Set.has) which breaks when called within a babel plugin's traversal context
* due to different NodePath instances across traversal boundaries.
*/
import type { NodePath, PluginObj } from '@babel/core'
import BabelGenerate from '@babel/generator'
import { parse } from '@babel/parser'
import BabelTraverse from '@babel/traverse'
import * as t from '@babel/types'
import {
deadCodeElimination,
findReferencedIdentifiers,
} from 'babel-dead-code-elimination'
const generate = (BabelGenerate['default'] ||
BabelGenerate) as any as typeof BabelGenerate
const traverse = (BabelTraverse['default'] || BabelTraverse) as typeof BabelTraverse
const SERVER_EXPORTS = ['loader', 'generateStaticParams'] as const
type ServerExport = (typeof SERVER_EXPORTS)[number]
type PluginOptions = { routerRoot?: string }
function removeServerCodePlugin(_: unknown, options: PluginOptions): PluginObj {
const { routerRoot = 'app' } = options
return {
name: 'one-remove-server-code',
visitor: {
Program: {
exit(path: NodePath<t.Program>, state: { filename?: string }) {
const filename = state.filename
if (!filename) return
const routerRootPattern = new RegExp(`[/\\\\]${routerRoot}[/\\\\]`)
if (!routerRootPattern.test(filename)) return
if (filename.includes('node_modules')) return
const code = path.toString()
if (!/generateStaticParams|loader/.test(code)) return
// mirror the clientTreeShakePlugin approach exactly:
// 1. parse fresh AST from the current code
// 2. capture referenced identifiers BEFORE removing exports
// 3. remove server exports
// 4. run DCE with pre-removal references
// 5. replace the program body
try {
const freshAst = parse(code, {
sourceType: 'module',
plugins: ['typescript', 'jsx'],
}) as any
// capture references BEFORE removal (critical for DCE to work)
const referenced = findReferencedIdentifiers(freshAst)
const removed = { loader: false, generateStaticParams: false }
// remove server exports from the fresh AST
traverse(freshAst, {
ExportNamedDeclaration(expPath) {
const declaration = expPath.node.declaration
if (!declaration) return
if (declaration.type === 'FunctionDeclaration' && declaration.id) {
const name = declaration.id.name
if (name === 'loader' || name === 'generateStaticParams') {
expPath.remove()
removed[name] = true
}
} else if (declaration.type === 'VariableDeclaration') {
const decl = expPath.get(
'declaration'
) as NodePath<t.VariableDeclaration>
const declarators = decl.get('declarations')
// iterate in reverse so indices stay valid after removal
for (let i = declarators.length - 1; i >= 0; i--) {
const declarator = declarators[i]
const id = declarator.node.id
if (
id.type === 'Identifier' &&
(id.name === 'loader' || id.name === 'generateStaticParams')
) {
declarator.remove()
removed[id.name as ServerExport] = true
}
}
// if all declarators were removed, clean up the empty export
if (decl.node && decl.node.declarations.length === 0) {
expPath.remove()
}
}
},
})
const removedFunctions = Object.keys(removed).filter(
(key) => removed[key as keyof typeof removed]
)
if (removedFunctions.length === 0) return
// run DCE with pre-removal references (same as clientTreeShakePlugin)
deadCodeElimination(freshAst, referenced)
// add empty stubs to prevent "missing export" errors
if (removed.loader) {
freshAst.program.body.push(
t.exportNamedDeclaration(
t.functionDeclaration(
t.identifier('loader'),
[],
t.blockStatement([
t.returnStatement(t.stringLiteral('__vxrn__loader__')),
])
)
)
)
}
if (removed.generateStaticParams) {
freshAst.program.body.push(
t.exportNamedDeclaration(
t.functionDeclaration(
t.identifier('generateStaticParams'),
[],
t.blockStatement([])
)
)
)
}
// generate cleaned code and re-parse to get proper NodePaths for babel
const out = generate(freshAst, { retainLines: true })
const finalAst = parse(out.code, {
sourceType: 'module',
plugins: ['typescript', 'jsx'],
}) as any
path.node.body = finalAst.program.body
path.node.directives = finalAst.program.directives
if (process.env.DEBUG) {
console.info(
` 🧹 [one/metro] ${filename} removed ${removedFunctions.length} server-only exports`
)
}
} catch (error) {
console.warn(
`[one/metro] Tree shaking failed for ${filename}:`,
error instanceof Error ? error.message : String(error)
)
}
},
},
},
}
}
export default removeServerCodePlugin