UNPKG

one

Version:

One is a new React Framework that makes Vite serve both native and web.

217 lines (191 loc) 6.45 kB
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 } } }