UNPKG

one

Version:

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

661 lines (660 loc) 23.2 kB
import { extname, relative } from "node:path"; import MagicString from "magic-string"; import { parseSync } from "oxc-parser"; import { EMPTY_LOADER_STRING, makeLoaderRouteIdStub } from "../constants.mjs"; const clientTreeShakePlugin = opts => { const runtime = opts?.runtime ?? "vite"; const routerRoot = opts?.routerRoot ?? "app"; return { name: "one-client-tree-shake", enforce: "pre", ...(runtime === "vite" && { applyToEnvironment(env) { return env.name === "client" || env.name === "ios" || env.name === "android"; } }), transform: { order: "pre", async handler(code, id, settings) { if (runtime === "vite" && 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, process.cwd(), routerRoot); return out; } } }; }; function extractPatternBindings(node, names) { if (!node) return; switch (node.type) { case "Identifier": names.add(node.name); break; case "ObjectPattern": for (const prop of node.properties) { if (prop.type === "Property") { extractPatternBindings(prop.value, names); } else if (prop.type === "RestElement") { extractPatternBindings(prop.argument, names); } } break; case "ArrayPattern": for (const element of node.elements) { if (element) { extractPatternBindings(element, names); } } break; case "AssignmentPattern": extractPatternBindings(node.left, names); break; case "RestElement": extractPatternBindings(node.argument, names); break; case "TSParameterProperty": extractPatternBindings(node.parameter, names); break; } } function getReferencedIdentifiers(rootNode, candidateNames) { const referenced = /* @__PURE__ */new Set(); const scopeStack = []; function isShadowed(name) { for (let i = scopeStack.length - 1; i >= 0; i--) { if (scopeStack[i].has(name)) return true; } return false; } function checkIdentifier(name) { if (!isShadowed(name) && candidateNames.has(name)) { referenced.add(name); } } function walk(node) { if (!node || typeof node !== "object") return; if (node.type === "TSTypeAnnotation" || node.type === "TSTypeReference" || node.type === "TSTypeAliasDeclaration" || node.type === "TSInterfaceDeclaration" || node.type === "TSTypeParameterDeclaration" || node.type === "TSTypeParameterInstantiation") { return; } if (node.type === "TSAsExpression" || node.type === "TSTypeAssertion" || node.type === "TSSatisfiesExpression" || node.type === "TSNonNullExpression") { walk(node.expression); return; } function walkPatternExpressions(pat) { if (!pat || typeof pat !== "object") return; switch (pat.type) { case "AssignmentPattern": walk(pat.right); walkPatternExpressions(pat.left); break; case "ObjectPattern": for (const prop of pat.properties) { if (prop.type === "Property") { if (prop.computed) { walk(prop.key); } walkPatternExpressions(prop.value); } else if (prop.type === "RestElement") { walkPatternExpressions(prop.argument); } } break; case "ArrayPattern": for (const element of pat.elements) { if (element) { walkPatternExpressions(element); } } break; case "RestElement": walkPatternExpressions(pat.argument); break; case "TSParameterProperty": walkPatternExpressions(pat.parameter); break; } } if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") { let addVars = function (n) { if (!n || typeof n !== "object") return; if (n.type === "FunctionDeclaration" || n.type === "FunctionExpression" || n.type === "ArrowFunctionExpression") { return; } if (n.type === "VariableDeclaration" && n.kind === "var") { for (const d of n.declarations) { extractPatternBindings(d.id, scope); } } for (const k of Object.keys(n)) { if (k === "parent") continue; const val = n[k]; if (Array.isArray(val)) { for (const item of val) addVars(item); } else if (val && typeof val === "object") { addVars(val); } } }; const scope = /* @__PURE__ */new Set(); if (node.type === "FunctionExpression" && node.id) { scope.add(node.id.name); } if (node.params) { for (const param of node.params) { walkPatternExpressions(param); extractPatternBindings(param, scope); } } addVars(node.body); scopeStack.push(scope); walk(node.body); scopeStack.pop(); return; } if (node.type === "BlockStatement") { const scope = /* @__PURE__ */new Set(); for (const stmt of node.body) { if (stmt.type === "VariableDeclaration" && (stmt.kind === "let" || stmt.kind === "const")) { for (const decl of stmt.declarations) { extractPatternBindings(decl.id, scope); } } else if (stmt.type === "FunctionDeclaration" && stmt.id) { scope.add(stmt.id.name); } else if (stmt.type === "ClassDeclaration" && stmt.id) { scope.add(stmt.id.name); } } scopeStack.push(scope); for (const stmt of node.body) { walk(stmt); } scopeStack.pop(); return; } if (node.type === "CatchClause") { const scope = /* @__PURE__ */new Set(); if (node.param) { extractPatternBindings(node.param, scope); } scopeStack.push(scope); walk(node.body); scopeStack.pop(); return; } if (node.type === "ForStatement" || node.type === "ForInStatement" || node.type === "ForOfStatement") { const scope = /* @__PURE__ */new Set(); const initOrLeft = node.init || node.left; if (initOrLeft && initOrLeft.type === "VariableDeclaration" && initOrLeft.kind !== "var") { for (const decl of initOrLeft.declarations) { extractPatternBindings(decl.id, scope); } } scopeStack.push(scope); if (node.init) walk(node.init); if (node.left) walk(node.left); if (node.right) walk(node.right); if (node.test) walk(node.test); if (node.update) walk(node.update); walk(node.body); scopeStack.pop(); return; } if (node.type === "Identifier") { checkIdentifier(node.name); return; } if (node.type === "JSXOpeningElement") { if (node.name) { if (node.name.type === "JSXIdentifier") { if (node.name.name && /^[A-Z]/.test(node.name.name)) { checkIdentifier(node.name.name); } } else if (node.name.type === "JSXMemberExpression") { let rootObj = node.name.object; while (rootObj && rootObj.type === "JSXMemberExpression") { rootObj = rootObj.object; } if (rootObj && rootObj.type === "JSXIdentifier") { checkIdentifier(rootObj.name); } } } if (node.attributes) { for (const attr of node.attributes) { walk(attr); } } return; } if (node.type === "JSXAttribute") { if (node.value) walk(node.value); return; } if (node.type === "JSXClosingElement") { return; } if (node.type === "Property") { if (node.computed) { walk(node.key); } walk(node.value); return; } if (node.type === "MemberExpression") { walk(node.object); if (node.computed) { walk(node.property); } return; } if (node.type === "VariableDeclarator") { walkPatternExpressions(node.id); if (node.init) { walk(node.init); } return; } if (node.type === "ExportSpecifier") { if (node.local && node.local.type === "Identifier") { checkIdentifier(node.local.name); } return; } for (const key of Object.keys(node)) { if (key === "parent") continue; const child = node[key]; if (Array.isArray(child)) { for (const item of child) { if (item && typeof item === "object") walk(item); } } else if (child && typeof child === "object") { walk(child); } } } walk(rootNode); return referenced; } async function transformTreeShakeClient(code, id, root, routerRoot = "app") { if (!/generateStaticParams|loader/.test(code)) { return; } const isProd = process.env.NODE_ENV === "production"; const filename = (id || "file.tsx").split("?")[0].split("#")[0]; let parseResult; try { parseResult = parseSync(filename, code); if (parseResult.errors && parseResult.errors.length > 0) { const errorMessage = parseResult.errors.map(e => e.message).join("\n"); if (isProd) { throw new Error(`[one] Failed to parse ${id} with server exports: ${errorMessage}`); } console.warn(`[one] Skipping tree shaking for ${id} due to syntax error:`, errorMessage); return; } } catch (error) { if (isProd) { throw error instanceof Error ? error : new Error(String(error)); } const errorMessage = error instanceof Error ? error.message : String(error); console.warn(`[one] Skipping tree shaking for ${id} due to syntax error:`, errorMessage); return; } const ast = parseResult.program; try { return doTreeShakeClient(code, id, root, ast, routerRoot); } catch (error) { if (isProd) { throw error instanceof Error ? error : new Error(String(error)); } const errorMessage = error instanceof Error ? error.message : String(error); console.warn(`[one] Skipping tree shaking for ${id} due to analysis error:`, errorMessage); return; } } function doTreeShakeClient(code, id, root, ast, routerRoot) { const removed = { loader: false, generateStaticParams: false }; const serverExportStmts = /* @__PURE__ */new Map(); for (const stmt of ast.body) { if (stmt.type === "ExportNamedDeclaration" && stmt.declaration) { if (stmt.declaration.type === "FunctionDeclaration") { const fnName = stmt.declaration.id?.name; if (fnName === "loader" || fnName === "generateStaticParams") { removed[fnName] = true; serverExportStmts.set(stmt, { names: /* @__PURE__ */new Set([fnName]), replaceAll: true, declaratorsToRemove: /* @__PURE__ */new Set() }); } } else if (stmt.declaration.type === "VariableDeclaration") { const found = []; for (const d of stmt.declaration.declarations) { if (d.id?.type === "Identifier" && (d.id.name === "loader" || d.id.name === "generateStaticParams")) { found.push(d); removed[d.id.name] = true; } } if (found.length > 0) { serverExportStmts.set(stmt, { names: new Set(found.map(d => d.id.name)), replaceAll: found.length === stmt.declaration.declarations.length, declaratorsToRemove: new Set(found) }); } } } } const removedFunctions = Object.keys(removed).filter(k => removed[k]); if (removedFunctions.length === 0) { return; } const moduleBindings = /* @__PURE__ */new Map(); for (const stmt of ast.body) { if (stmt.type === "ImportDeclaration") { const isDeclType = stmt.importKind === "type"; for (const sp of stmt.specifiers) { const isType = isDeclType || sp.importKind === "type"; moduleBindings.set(sp.local.name, { kind: "import", stmt, node: sp, isType }); } } else if (stmt.type === "ExportNamedDeclaration" && stmt.declaration) { const decl = stmt.declaration; if (decl.type === "FunctionDeclaration" && decl.id) { moduleBindings.set(decl.id.name, { kind: "function", stmt, node: decl }); } else if (decl.type === "VariableDeclaration") { for (const d of decl.declarations) { const names = /* @__PURE__ */new Set(); extractPatternBindings(d.id, names); for (const name of names) { moduleBindings.set(name, { kind: "variable", stmt, node: d }); } } } else if (decl.type === "ClassDeclaration" && decl.id) { moduleBindings.set(decl.id.name, { kind: "class", stmt, node: decl }); } } else if (stmt.type === "FunctionDeclaration" && stmt.id) { moduleBindings.set(stmt.id.name, { kind: "function", stmt, node: stmt }); } else if (stmt.type === "VariableDeclaration") { for (const d of stmt.declarations) { const names = /* @__PURE__ */new Set(); extractPatternBindings(d.id, names); for (const name of names) { moduleBindings.set(name, { kind: "variable", stmt, node: d }); } } } else if (stmt.type === "ClassDeclaration" && stmt.id) { moduleBindings.set(stmt.id.name, { kind: "class", stmt, node: stmt }); } else if (stmt.type === "TSEnumDeclaration" && stmt.id) { moduleBindings.set(stmt.id.name, { kind: "enum", stmt, node: stmt }); } } const allBindingNames = new Set(moduleBindings.keys()); const topLevelDeps = /* @__PURE__ */new Map(); const clientRootDeps = /* @__PURE__ */new Set(); for (const [name, info] of moduleBindings) { if (info.kind === "import") { topLevelDeps.set(name, /* @__PURE__ */new Set()); } else if (info.kind === "function") { topLevelDeps.set(name, getReferencedIdentifiers(info.node, allBindingNames)); } else if (info.kind === "variable") { const deps = getReferencedIdentifiers(info.node, allBindingNames); deps.delete(name); topLevelDeps.set(name, deps); } else if (info.kind === "class") { topLevelDeps.set(name, getReferencedIdentifiers(info.node, allBindingNames)); } else if (info.kind === "enum") { topLevelDeps.set(name, getReferencedIdentifiers(info.node, allBindingNames)); } } const serverNeeded = /* @__PURE__ */new Set(); const serverQueue = []; for (const fn of removedFunctions) { const deps = topLevelDeps.get(fn); if (deps) { for (const dep of deps) serverQueue.push(dep); } } while (serverQueue.length > 0) { const name = serverQueue.shift(); if (serverNeeded.has(name)) continue; serverNeeded.add(name); const deps = topLevelDeps.get(name); if (deps) { for (const dep of deps) { if (!serverNeeded.has(dep)) serverQueue.push(dep); } } } for (const stmt of ast.body) { if (stmt.type === "ImportDeclaration") { continue; } if (serverExportStmts.has(stmt)) { const info = serverExportStmts.get(stmt); if (!info.replaceAll && stmt.declaration?.declarations) { for (const d of stmt.declaration.declarations) { if (!info.declaratorsToRemove.has(d)) { const deps2 = getReferencedIdentifiers(d, allBindingNames); for (const dep of deps2) clientRootDeps.add(dep); } } } continue; } if (stmt.type === "VariableDeclaration") { const allInServer = stmt.declarations.every(d => { const names = /* @__PURE__ */new Set(); extractPatternBindings(d.id, names); return names.size > 0 && Array.from(names).every(n => serverNeeded.has(n)); }); if (!allInServer) { for (const d of stmt.declarations) { const names = /* @__PURE__ */new Set(); extractPatternBindings(d.id, names); const inServer = names.size > 0 && Array.from(names).every(n => serverNeeded.has(n)); if (!inServer) { const deps2 = getReferencedIdentifiers(d, allBindingNames); for (const dep of deps2) clientRootDeps.add(dep); } } } continue; } if ((stmt.type === "FunctionDeclaration" || stmt.type === "ClassDeclaration" || stmt.type === "TSEnumDeclaration") && stmt.id && serverNeeded.has(stmt.id.name)) { continue; } const deps = getReferencedIdentifiers(stmt, allBindingNames); for (const dep of deps) clientRootDeps.add(dep); } const clientNeeded = /* @__PURE__ */new Set(); const clientQueue = Array.from(clientRootDeps); while (clientQueue.length > 0) { const name = clientQueue.shift(); if (clientNeeded.has(name)) continue; clientNeeded.add(name); if (name === "loader" || name === "generateStaticParams") continue; const deps = topLevelDeps.get(name); if (deps) { for (const dep of deps) { if (!clientNeeded.has(dep)) clientQueue.push(dep); } } } const exclusiveServerBindings = /* @__PURE__ */new Set(); for (const name of serverNeeded) { if (!clientNeeded.has(name)) { exclusiveServerBindings.add(name); } } const s = new MagicString(code); for (const [stmt, info] of serverExportStmts) { const stubs = []; if (info.names.has("loader")) { if (root) { const fromRoot = relative(root, id).replace(/\\/g, "/"); const prefix = routerRoot.replace(/^\.\//, "").replace(/\/$/, "") + "/"; const routeId = "./" + (fromRoot.startsWith(prefix) ? fromRoot.slice(prefix.length) : fromRoot); stubs.push(makeLoaderRouteIdStub(routeId)); } else { stubs.push(EMPTY_LOADER_STRING); } } if (info.names.has("generateStaticParams")) { stubs.push("export function generateStaticParams() {};"); } if (info.replaceAll) { s.overwrite(stmt.start, stmt.end, stubs.join("\n")); } else { const decl = stmt.declaration; for (let i = decl.declarations.length - 1; i >= 0; i--) { const d = decl.declarations[i]; if (info.declaratorsToRemove.has(d)) { if (i === 0) { const next = decl.declarations[1]; s.remove(d.start, next.start); } else { const prev = decl.declarations[i - 1]; s.remove(prev.end, d.end); } } } s.appendRight(stmt.end, "\n" + stubs.join("\n")); } } for (const stmt of ast.body) { if (serverExportStmts.has(stmt) || stmt.type === "ImportDeclaration") continue; if ((stmt.type === "FunctionDeclaration" || stmt.type === "ClassDeclaration" || stmt.type === "TSEnumDeclaration") && stmt.id && exclusiveServerBindings.has(stmt.id.name)) { let end = stmt.end; if (code[end] === ";") end++; if (code[end] === "\r" && code[end + 1] === "\n") end += 2;else if (code[end] === "\n") end += 1; s.remove(stmt.start, end); } else if (stmt.type === "VariableDeclaration") { const allExclusive = stmt.declarations.every(d => { const names = /* @__PURE__ */new Set(); extractPatternBindings(d.id, names); return Array.from(names).every(n => exclusiveServerBindings.has(n)); }); if (allExclusive) { let end = stmt.end; if (code[end] === ";") end++; if (code[end] === "\r" && code[end + 1] === "\n") end += 2;else if (code[end] === "\n") end += 1; s.remove(stmt.start, end); } else { for (let i = stmt.declarations.length - 1; i >= 0; i--) { const d = stmt.declarations[i]; const names = /* @__PURE__ */new Set(); extractPatternBindings(d.id, names); if (Array.from(names).every(n => exclusiveServerBindings.has(n))) { if (i === 0) { const next = stmt.declarations[1]; s.remove(d.start, next.start); } else { const prev = stmt.declarations[i - 1]; s.remove(prev.end, d.end); } } } } } else if (stmt.type === "ClassDeclaration" && stmt.id && exclusiveServerBindings.has(stmt.id.name)) { let end = stmt.end; if (code[end] === ";") end++; if (code[end] === "\r" && code[end + 1] === "\n") end += 2;else if (code[end] === "\n") end += 1; s.remove(stmt.start, end); } } for (const stmt of ast.body) { if (stmt.type !== "ImportDeclaration") continue; if (stmt.importKind === "type") continue; if (!stmt.specifiers || stmt.specifiers.length === 0) continue; const kept = stmt.specifiers.filter(sp => { if (sp.importKind === "type") return true; return !exclusiveServerBindings.has(sp.local.name); }); if (kept.length === 0) { let end = stmt.end; if (code[end] === ";") end++; if (code[end] === "\r" && code[end + 1] === "\n") end += 2;else if (code[end] === "\n") end += 1; s.remove(stmt.start, end); } else if (kept.length < stmt.specifiers.length) { const declCode = code.slice(stmt.start, stmt.source.start); const prefixMatch = declCode.match(/^import\s+/); const prefixEnd = stmt.start + (prefixMatch ? prefixMatch[0].length : 7); let pos = stmt.source.start - 1; while (pos >= 0 && /\s/.test(code[pos])) pos--; const fromStart = pos - 3; const defaultSpec = kept.find(sp => sp.type === "ImportDefaultSpecifier"); const namespaceSpec = kept.find(sp => sp.type === "ImportNamespaceSpecifier"); const namedSpecs = kept.filter(sp => sp.type === "ImportSpecifier"); const parts = []; if (defaultSpec) parts.push(defaultSpec.local.name); if (namespaceSpec) parts.push("* as " + namespaceSpec.local.name); if (namedSpecs.length > 0) { const namedStr = namedSpecs.map(sp => { const typePrefix = sp.importKind === "type" ? "type " : ""; const importedName = sp.imported.raw || sp.imported.name; if (importedName === sp.local.name) { return typePrefix + sp.local.name; } return typePrefix + importedName + " as " + sp.local.name; }).join(", "); parts.push("{ " + namedStr + " }"); } s.overwrite(prefixEnd, fromStart, parts.join(", ") + " "); } } console.info(` \u{1F9F9} [one] ${relative(process.cwd(), id)} removed ${removedFunctions.length} server-only exports`); return { code: s.toString(), map: s.generateMap({ hires: "boundary", source: id, includeContent: true }) }; } export { clientTreeShakePlugin, transformTreeShakeClient }; //# sourceMappingURL=clientTreeShakePlugin.mjs.map