From a02bf93b252246ffaa64a2a3e8817dca85814181 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Thu, 30 May 2024 10:40:03 -0400 Subject: [PATCH] wip --- packages/remix-dev/vite/define-route.ts | 149 ++++++++++++++++++------ packages/remix-dev/vite/plugin.ts | 16 +++ 2 files changed, 129 insertions(+), 36 deletions(-) diff --git a/packages/remix-dev/vite/define-route.ts b/packages/remix-dev/vite/define-route.ts index 2274fdc7cc..3713ad9dfa 100644 --- a/packages/remix-dev/vite/define-route.ts +++ b/packages/remix-dev/vite/define-route.ts @@ -11,6 +11,108 @@ const MACRO = "defineRoute$"; const MACRO_PKG = "react-router"; const SERVER_ONLY_PROPERTIES = ["loader", "action"]; +type Fields = { + params?: NodePath; + loader?: NodePath; + clientLoader?: NodePath; + action?: NodePath; + clientAction?: NodePath; + Component?: NodePath; + ErrorBoundary?: NodePath; +}; + +function analyzeRoute(path: NodePath) { + if (path.node.arguments.length !== 1) { + throw path.buildCodeFrameError(`macro must take exactly one argument`); + } + let arg = path.node.arguments[0]; + let argPath = path.get("arguments.0") as NodePath; + if (!t.isObjectExpression(arg)) { + throw argPath.buildCodeFrameError( + "macro argument must be a literal object" + ); + } + + let fields: Fields = {}; + for (let [i, property] of arg.properties.entries()) { + if (!t.isObjectProperty(property) && !t.isObjectMethod(property)) { + let propertyPath = argPath.get(`properties.${i}`) as NodePath; + throw propertyPath.buildCodeFrameError( + "macro argument must only have statically analyzable properties" + ); + } + let propertyPath = argPath.get(`properties.${i}`) as NodePath< + t.ObjectProperty | t.ObjectMethod + >; + if (property.computed || !t.isIdentifier(property.key)) { + throw propertyPath.buildCodeFrameError( + "macro argument must only have statically analyzable fields" + ); + } + + let key = property.key.name; + if (key === "params") { + let paramsPath = propertyPath as NodePath; + checkRouteParams(paramsPath); + fields["params"] = paramsPath; + continue; + } + + if ( + key === "loader" || + key === "clientLoader" || + key === "action" || + key === "clientAction" || + key === "Component" || + key === "ErrorBoundary" + ) { + fields[key] = propertyPath; + } + } + return fields; +} + +// analyze route module: +// - using defineRoute$ ? +// - throw if macro not used correctly (validation) +// - return PATHS to the fields (params, loader, clientLoader, action, clientAction, ErrorBoundary, Component) +// +export function getRouteHasFields(source: string) { + let ast = parse(source, { sourceType: "module" }); + let foundMacro = false; + let metadata = { + hasLoader: false, + hasClientAction: false, + hasAction: false, + hasClientLoader: false, + hasErrorBoundary: false, + }; + traverse(ast, { + CallExpression(path) { + // detect macro BEGIN + if (!t.isIdentifier(path.node.callee)) return; + let macro = path.node.callee.name; + let binding = path.scope.getBinding(macro); + + if (!binding) return; + if (!isMacroBinding(binding)) return; + // detect macro END + + foundMacro = true; + let fields = analyzeRoute(path); + metadata = { + hasLoader: fields.loader !== undefined, + hasClientLoader: fields.clientLoader !== undefined, + hasAction: fields.action !== undefined, + hasClientAction: fields.clientAction !== undefined, + hasErrorBoundary: fields.ErrorBoundary !== undefined, + }; + }, + }); + if (!foundMacro) return null; + return metadata; +} + export function transform( source: string, id: string, @@ -31,48 +133,23 @@ export function transform( ); }, CallExpression(path) { - if (!t.isIdentifier(path.node.callee)) return false; + if (!t.isIdentifier(path.node.callee)) return; let macro = path.node.callee.name; let binding = path.scope.getBinding(macro); - if (!binding) return false; - if (!isMacroBinding(binding)) return false; - if (path.node.arguments.length !== 1) { - throw path.buildCodeFrameError( - `'${macro}' macro must take exactly one argument` - ); - } - let arg = path.node.arguments[0]; - let argPath = path.get("arguments.0") as NodePath; - if (!t.isObjectExpression(arg)) { - throw argPath.buildCodeFrameError( - `'${macro}' macro argument must be a literal object` - ); - } - - let markedForRemoval: NodePath[] = []; - for (let [i, property] of arg.properties.entries()) { - let propertyPath = argPath.get(`properties.${i}`) as NodePath; - if (!t.isObjectProperty(property) && !t.isObjectMethod(property)) { - throw propertyPath.buildCodeFrameError( - `'${macro}' macro argument must only have statically analyzable properties` - ); - } - if (property.computed || !t.isIdentifier(property.key)) { - throw propertyPath.buildCodeFrameError( - `'${macro}' macro argument must only have statically analyzable fields` - ); - } - - if (property.key.name === "params") { - checkRouteParams(propertyPath as NodePath); - } + if (!binding) return; + if (!isMacroBinding(binding)) return; - if (options.ssr && SERVER_ONLY_PROPERTIES.includes(property.key.name)) { - markedForRemoval.push(propertyPath); + let fields = analyzeRoute(path); + if (options.ssr) { + let markedForRemoval: NodePath[] = []; + for (let [key, fieldPath] of Object.entries(fields)) { + if (SERVER_ONLY_PROPERTIES.includes(key)) { + markedForRemoval.push(fieldPath); + } } + markedForRemoval.forEach((path) => path.remove()); } - markedForRemoval.forEach((path) => path.remove()); }, }); deadCodeElimination(ast, refs); diff --git a/packages/remix-dev/vite/plugin.ts b/packages/remix-dev/vite/plugin.ts index 9e876daae0..4361654f0a 100644 --- a/packages/remix-dev/vite/plugin.ts +++ b/packages/remix-dev/vite/plugin.ts @@ -38,6 +38,7 @@ import { resolveEntryFiles, resolvePublicPath, } from "../config"; +import { getRouteHasFields } from "./define-route"; export async function resolveViteConfig({ configFile, @@ -289,6 +290,8 @@ const getRouteManifestModuleExports = async ( ): Promise> => { let entries = await Promise.all( Object.entries(ctx.reactRouterConfig.routes).map(async ([key, route]) => { + // do the old thing when `defineRoute$` isn't present + // otherwise use the new AST-based `has*` fields detector let sourceExports = await getRouteModuleExports( viteChildCompiler, ctx, @@ -665,6 +668,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = (_config) => { ); for (let [key, route] of Object.entries(ctx.reactRouterConfig.routes)) { + let metadata = await getDefineRouteFields(ctx, route); let sourceExports = routeManifestExports[key]; routes[key] = { id: route.id, @@ -1877,3 +1881,15 @@ function createPrerenderRoutes( }; }); } + +// TODO precompute the define route fields for all routes in parallel +// for all routes in the route tree! +async function getDefineRouteFields( + ctx: ReactRouterPluginContext, + route: ConfigRoute, + readRouteFile?: () => string | Promise +) { + let routePath = path.resolve(ctx.reactRouterConfig.appDirectory, route.file); + let routeSource = readRouteFile?.() ?? fse.readFile(routePath, "utf-8"); + return getRouteHasFields(await routeSource); +}