Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
pcattori committed Jun 4, 2024
1 parent f29d42a commit 6427d97
Show file tree
Hide file tree
Showing 2 changed files with 191 additions and 76 deletions.
161 changes: 118 additions & 43 deletions packages/remix-dev/vite/define-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,109 @@ import { parse, traverse, generate, t } from "./babel";
import type { GeneratorResult } from "@babel/generator";
import { deadCodeElimination, findReferencedIdentifiers } from "./dce";

// given a route module filepath
// determine if its using `defineRoute$`
// if not, just do the old stuff that looks at exports

const MACRO = "defineRoute$";
const MACRO_PKG = "react-router";
const SERVER_ONLY_PROPERTIES = ["loader", "action"];

type Field = NodePath<t.ObjectProperty | t.ObjectMethod>;
type Fields = {
params?: NodePath<t.ObjectProperty>;
loader?: Field;
clientLoader?: Field;
action?: Field;
clientAction?: Field;
Component?: Field;
ErrorBoundary?: Field;
};

export type HasFields = {
hasLoader: boolean;
hasClientLoader: boolean;
hasAction: boolean;
hasClientAction: boolean;
hasErrorBoundary: boolean;
};

function analyzeRoute(path: NodePath<t.CallExpression>): Fields {
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<t.Node>;
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<t.Node>;
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<t.ObjectProperty>;
checkRouteParams(paramsPath);
fields["params"] = paramsPath;
continue;
}

if (
key === "loader" ||
key === "clientLoader" ||
key === "action" ||
key === "clientAction" ||
key === "Component" ||
key === "ErrorBoundary"
) {
fields[key] = propertyPath;
}
}
return fields;
}

export function getDefineRouteHasFields(source: string): HasFields | null {
let ast = parse(source, { sourceType: "module" });
let foundMacro = false;
let metadata: HasFields = {
hasLoader: false,
hasClientAction: false,
hasAction: false,
hasClientLoader: false,
hasErrorBoundary: false,
};
traverse(ast, {
CallExpression(path) {
if (!isMacro(path)) return;

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,
Expand All @@ -31,48 +126,18 @@ export function transform(
);
},
CallExpression(path) {
if (!t.isIdentifier(path.node.callee)) return false;
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<t.Node>;
if (!t.isObjectExpression(arg)) {
throw argPath.buildCodeFrameError(
`'${macro}' macro argument must be a literal object`
);
}
if (!isMacro(path)) return;

let markedForRemoval: NodePath<t.Node>[] = [];
for (let [i, property] of arg.properties.entries()) {
let propertyPath = argPath.get(`properties.${i}`) as NodePath<t.Node>;
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<t.ObjectProperty>);
}

if (options.ssr && SERVER_ONLY_PROPERTIES.includes(property.key.name)) {
markedForRemoval.push(propertyPath);
let fields = analyzeRoute(path);
if (options.ssr) {
let markedForRemoval: NodePath<t.Node>[] = [];
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);
Expand All @@ -96,6 +161,16 @@ function checkRouteParams(path: NodePath<t.ObjectProperty>) {
}
}

function isMacro(path: NodePath<t.CallExpression>): boolean {
if (!t.isIdentifier(path.node.callee)) return false;
let name = path.node.callee.name;
let binding = path.scope.getBinding(name);

if (!binding) return false;
if (!isMacroBinding(binding)) return false;
return true;
}

function isMacroBinding(binding: Binding): boolean {
// import source
if (!t.isImportDeclaration(binding?.path.parent)) return false;
Expand Down
106 changes: 73 additions & 33 deletions packages/remix-dev/vite/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import {
resolveEntryFiles,
resolvePublicPath,
} from "../config";
import type { HasFields } from "./define-route";
import { getDefineRouteHasFields } from "./define-route";

export async function resolveViteConfig({
configFile,
Expand Down Expand Up @@ -283,18 +285,21 @@ const writeFileSafe = async (file: string, contents: string): Promise<void> => {
await fse.writeFile(file, contents);
};

// TODO: change this to return <Reco
const getRouteManifestModuleExports = async (
viteChildCompiler: Vite.ViteDevServer | null,
ctx: ReactRouterPluginContext
): Promise<Record<string, string[]>> => {
): Promise<Record<string, HasFields>> => {
let entries = await Promise.all(
Object.entries(ctx.reactRouterConfig.routes).map(async ([key, route]) => {
let sourceExports = await getRouteModuleExports(
// do the old thing when `defineRoute$` isn't present
// otherwise use the new AST-based `has*` fields detector
let hasFields = await getRouteModuleHasFields({
viteChildCompiler,
ctx,
route.file
);
return [key, sourceExports] as const;
routeFile: route.file,
});
return [key, hasFields] as const;
})
);
return Object.fromEntries(entries);
Expand Down Expand Up @@ -588,7 +593,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = (_config) => {
ctx.reactRouterConfig.appDirectory,
route.file
);
let sourceExports = routeManifestExports[key];
let hasFields = routeManifestExports[key];
let isRootRoute = route.parentId === undefined;

let routeManifestEntry = {
Expand All @@ -597,11 +602,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = (_config) => {
path: route.path,
index: route.index,
caseSensitive: route.caseSensitive,
hasAction: sourceExports.includes("action"),
hasLoader: sourceExports.includes("loader"),
hasClientAction: sourceExports.includes("clientAction"),
hasClientLoader: sourceExports.includes("clientLoader"),
hasErrorBoundary: sourceExports.includes("ErrorBoundary"),
...hasFields,
...getReactRouterManifestBuildAssets(
ctx,
viteManifest,
Expand Down Expand Up @@ -665,7 +666,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = (_config) => {
);

for (let [key, route] of Object.entries(ctx.reactRouterConfig.routes)) {
let sourceExports = routeManifestExports[key];
let hasFields = routeManifestExports[key];
routes[key] = {
id: route.id,
parentId: route.parentId,
Expand All @@ -679,11 +680,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = (_config) => {
resolveRelativeRouteFilePath(route, ctx.reactRouterConfig)
)}`
),
hasAction: sourceExports.includes("action"),
hasLoader: sourceExports.includes("loader"),
hasClientAction: sourceExports.includes("clientAction"),
hasClientLoader: sourceExports.includes("clientLoader"),
hasErrorBoundary: sourceExports.includes("ErrorBoundary"),
...hasFields,
imports: [],
};
}
Expand Down Expand Up @@ -987,16 +984,21 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = (_config) => {

if (id.endsWith(BUILD_CLIENT_ROUTE_QUERY_STRING)) {
let routeModuleId = id.replace(BUILD_CLIENT_ROUTE_QUERY_STRING, "");
let sourceExports = await getRouteModuleExports(
// TODO: make this work with new hasFields codepaths for defineRoute$ compat
let hasFields = await getRouteModuleHasFields({
viteChildCompiler,
ctx,
routeModuleId
);
routeFile: routeModuleId,
});

let clientExports = CLIENT_ROUTE_EXPORTS.filter(
(xport) => xport // TODO: make a fields util and a hasFields util, don't precompute hasFields, precompute _fields_
).join(", ");

let routeFileName = path.basename(routeModuleId);
let clientExports = sourceExports
.filter((exportName) => CLIENT_ROUTE_EXPORTS.includes(exportName))
.join(", ");
// let clientExports = sourceExports
// .filter((exportName) => CLIENT_ROUTE_EXPORTS.includes(exportName))
// .join(", ");

return `export { ${clientExports} } from "./${routeFileName}";`;
}
Expand Down Expand Up @@ -1612,12 +1614,12 @@ async function getRouteMetadata(
route: ConfigRoute,
readRouteFile?: () => string | Promise<string>
) {
let sourceExports = await getRouteModuleExports(
viteChildCompiler,
let hasFields = await getRouteModuleHasFields({
ctx,
route.file,
readRouteFile
);
viteChildCompiler,
routeFile: route.file,
readRouteFile,
});

let info = {
id: route.id,
Expand All @@ -1640,11 +1642,7 @@ async function getRouteMetadata(
resolveRelativeRouteFilePath(route, ctx.reactRouterConfig)
)}?import`
), // Ensure the Vite dev server responds with a JS module
hasAction: sourceExports.includes("action"),
hasClientAction: sourceExports.includes("clientAction"),
hasLoader: sourceExports.includes("loader"),
hasClientLoader: sourceExports.includes("clientLoader"),
hasErrorBoundary: sourceExports.includes("ErrorBoundary"),
...hasFields,
imports: [],
};
return info;
Expand Down Expand Up @@ -1877,3 +1875,45 @@ function createPrerenderRoutes(
};
});
}

async function getRouteModuleHasFieldsFromExports(args: {
viteChildCompiler: Vite.ViteDevServer | null;
ctx: ReactRouterPluginContext;
routeFile: string;
readRouteFile?: () => string | Promise<string>;
}): Promise<HasFields> {
let xports = await getRouteModuleExports(
args.viteChildCompiler,
args.ctx,
args.routeFile,
args.readRouteFile
);

return {
hasLoader: xports.includes("loader"),
hasClientLoader: xports.includes("clientLoader"),
hasAction: xports.includes("action"),
hasClientAction: xports.includes("clientAction"),
hasErrorBoundary: xports.includes("ErrorBoundary"),
};
}

async function getRouteModuleHasFields(args: {
routeFile: string;
readRouteFile?: () => string | Promise<string>;

// optional for back compat
viteChildCompiler: Vite.ViteDevServer | null;
ctx: ReactRouterPluginContext;
}): Promise<HasFields> {
if (!args.routeFile.includes(`defineRoute$`)) {
return getRouteModuleHasFieldsFromExports(args);
}

// TODO: probably also need to pass `readRouteFile` to `getDefineRouteHasFields`
let fields = getDefineRouteHasFields(args.routeFile);
if (!fields) {
return getRouteModuleHasFieldsFromExports(args);
}
return fields;
}

0 comments on commit 6427d97

Please sign in to comment.