one
Version:
One is a new React Framework that makes Vite serve both native and web.
217 lines (191 loc) • 6.45 kB
text/typescript
import { extname, relative } from 'node:path'
import BabelGenerate from '@babel/generator'
import { parse } from '@babel/parser'
import BabelTraverse from '@babel/traverse'
import type * as t from '@babel/types'
import {
deadCodeElimination,
findReferencedIdentifiers,
} from 'babel-dead-code-elimination'
import type { Plugin } from 'vite'
import { EMPTY_LOADER_STRING } from '../constants'
const traverse = (BabelTraverse['default'] || BabelTraverse) as typeof BabelTraverse
const generate = (BabelGenerate['default'] ||
BabelGenerate) as any as typeof BabelGenerate
// Collect type-only imports before dead code elimination runs
// These should never be removed since TypeScript erases them at compile time
function collectTypeImports(ast: t.File): t.ImportDeclaration[] {
const typeImports: t.ImportDeclaration[] = []
traverse(ast, {
ImportDeclaration(path) {
// Check if the entire import is type-only: `import type { X } from '...'`
if (path.node.importKind === 'type') {
typeImports.push(path.node)
}
},
})
return typeImports
}
// Restore type-only imports that may have been removed by dead code elimination
function restoreTypeImports(ast: t.File, typeImports: t.ImportDeclaration[]) {
if (typeImports.length === 0) return
// Get existing import sources to avoid duplicates
const existingSources = new Set<string>()
traverse(ast, {
ImportDeclaration(path) {
if (path.node.importKind === 'type') {
existingSources.add(path.node.source.value)
}
},
})
// Add back any type imports that were removed
for (const typeImport of typeImports) {
if (!existingSources.has(typeImport.source.value)) {
ast.program.body.unshift(typeImport)
}
}
}
export const clientTreeShakePlugin = (): Plugin => {
return {
name: 'one-client-tree-shake',
enforce: 'pre',
applyToEnvironment(env) {
return env.name === 'client' || env.name === 'ios' || env.name === 'android'
},
transform: {
order: 'pre',
async handler(code, id, settings) {
if (this.environment.name === 'ssr') {
return
}
if (!/\.(js|jsx|ts|tsx)/.test(extname(id))) {
return
}
if (/node_modules/.test(id)) {
return
}
const out = await transformTreeShakeClient(code, id)
return out
},
},
} satisfies Plugin
}
export async function transformTreeShakeClient(code: string, id: string) {
if (!/generateStaticParams|loader/.test(code)) {
return
}
let ast: any
try {
// `as any` because babel-dead-code-elimination using @types and it conflicts :/
ast = parse(code, {
sourceType: 'module',
plugins: ['typescript', 'jsx'],
}) as any
} catch (error) {
// If there's a syntax error, skip transformation and let Vite handle the error
// This prevents the dev server from crashing on syntax errors
const errorMessage = error instanceof Error ? error.message : String(error)
console.warn(
`[one] Skipping tree shaking for ${id} due to syntax error:`,
errorMessage
)
return
}
let referenced: any
try {
referenced = findReferencedIdentifiers(ast)
} catch (error) {
// If finding referenced identifiers fails, skip transformation
const errorMessage = error instanceof Error ? error.message : String(error)
console.warn(
`[one] Skipping tree shaking for ${id} due to identifier analysis error:`,
errorMessage
)
return
}
const removed = {
loader: false,
generateStaticParams: false,
}
try {
traverse(ast, {
ExportNamedDeclaration(path) {
if (
path.node.declaration &&
path.node.declaration.type === 'FunctionDeclaration'
) {
if (!path.node.declaration.id) return
const functionName = path.node.declaration.id.name
if (functionName === 'loader' || functionName === 'generateStaticParams') {
path.remove()
removed[functionName] = true
}
} else if (
path.node.declaration &&
path.node.declaration.type === 'VariableDeclaration'
) {
path.node.declaration.declarations.forEach((declarator, index) => {
if (
declarator.id.type === 'Identifier' &&
(declarator.id.name === 'loader' ||
declarator.id.name === 'generateStaticParams')
) {
const declaration = path.get('declaration.declarations.' + index)
if (!Array.isArray(declaration) && declaration) {
;(declaration as any).remove()
removed[declarator.id.name] = true
}
}
})
}
},
})
} catch (error) {
// If traversal fails, skip transformation
const errorMessage = error instanceof Error ? error.message : String(error)
console.warn(
`[one] Skipping tree shaking for ${id} due to traversal error:`,
errorMessage
)
return
}
const removedFunctions = Object.keys(removed).filter((key) => removed[key])
if (removedFunctions.length) {
try {
// Collect type-only imports before dead code elimination
// These should be preserved since TypeScript erases them at compile time
const typeImports = collectTypeImports(ast)
deadCodeElimination(ast, referenced)
// Restore any type imports that were incorrectly removed
restoreTypeImports(ast, typeImports)
const out = generate(ast)
// add back in empty or filled loader and genparams
const codeOut =
out.code +
'\n\n' +
removedFunctions
.map((key) => {
if (key === 'loader') {
return EMPTY_LOADER_STRING
}
return `export function generateStaticParams() {};`
})
.join('\n')
console.info(
` 🧹 [one] ${relative(process.cwd(), id)} removed ${removedFunctions.length} server-only exports`
)
return {
code: codeOut,
map: out.map,
}
} catch (error) {
// If code generation fails, skip transformation
const errorMessage = error instanceof Error ? error.message : String(error)
console.warn(
`[one] Skipping tree shaking for ${id} due to code generation error:`,
errorMessage
)
return
}
}
}