Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
pcattori committed Jun 6, 2024
1 parent 89fb2f7 commit 7b3ba79
Show file tree
Hide file tree
Showing 5 changed files with 298 additions and 120 deletions.
2 changes: 1 addition & 1 deletion packages/react-router/lib/router/define-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ type Component<P extends string, L extends Loader<P>> = (args: {
// loader -> data for component
// loader -> serverLoader for clientLoader -> data for component
// TODO: clientLoader and all the other route module export APIs (meta, handle, ErrorBoundary, etc.)
export const defineRoute$ = <
export const defineRoute = <
const P extends string,
L extends Loader<P>,
A extends Action<P>
Expand Down
2 changes: 1 addition & 1 deletion packages/remix-dev/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"@react-router/node": "workspace:*",
"@react-router/server-runtime": "workspace:*",
"arg": "^5.0.1",
"babel-dead-code-elimination": "^1.0.0",
"babel-dead-code-elimination": "^1.0.1",
"chalk": "^4.1.2",
"es-module-lexer": "^1.3.1",
"exit-hook": "2.2.1",
Expand Down
227 changes: 227 additions & 0 deletions packages/remix-dev/vite/define-route2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import * as babel from "@babel/core";
import type { Binding, NodePath } from "@babel/traverse";
import type { GeneratorResult } from "@babel/generator";
import {
deadCodeElimination,
findReferencedIdentifiers,
} from "babel-dead-code-elimination";

import { generate, parse, t, traverse } from "./babel";

function parseRouteSource(source: string) {
console.log(source);
let ast = parse(source, {
sourceType: "module",
plugins: ["jsx", ["typescript", {}]],
});

// Workaround for `path.buildCodeFrameError`
// See:
// - https://github.com/babel/babel/issues/11889
// - https://github.com/babel/babel/issues/11350#issuecomment-606169054
// @ts-expect-error `@types/babel__core` is missing types for `File`
new babel.File({ filename: undefined }, { code: source, ast });

return ast;
}

let fields = [
"loader",
"clientLoader",
"action",
"clientAction",
"Component",
"ErrorBoundary",
] as const;

type Field = (typeof fields)[number];
type FieldPath = NodePath<t.ObjectProperty | t.ObjectMethod>;

function isField(field: string): field is Field {
return (fields as readonly string[]).includes(field);
}

type Analysis = Record<Field, FieldPath | null>;

export function getRouteFields(source: string): string[] {
let ast = parseRouteSource(source);

let fieldNames: Field[] = [];
traverse(ast, {
ExportDefaultDeclaration(path) {
let fields = analyzeRouteExport(path);
console.log({ fields });
if (fields instanceof Error) throw fields;
console.log("after error check");
for (let [key, fieldPath] of Object.entries(fields)) {
if (!fieldPath) continue;
fieldNames.push(key as Field);
}
},
});
return fieldNames;
}

export function toMetadata(fields: string[]) {
return {
hasLoader: fields.includes("loader"),
hasClientLoader: fields.includes("clientLoader"),
hasAction: fields.includes("action"),
hasClientAction: fields.includes("clientAction"),
hasErrorBoundary: fields.includes("ErrorBoundary"),
};
}

export function transform(
source: string,
id: string,
options: { ssr: boolean }
): GeneratorResult {
let ast = parseRouteSource(source);

let refs = findReferencedIdentifiers(ast);
traverse(ast, {
ExportDefaultDeclaration(path) {
let fields = analyzeRouteExport(path);
if (fields instanceof Error) throw fields;
if (options.ssr) return;

let markedForRemoval: NodePath<t.Node>[] = [];
for (let [key, fieldPath] of Object.entries(fields)) {
if (["loader", "action"].includes(key)) {
if (!fieldPath) continue;
markedForRemoval.push(fieldPath);
}
}
markedForRemoval.forEach((path) => path.remove());
},
});
deadCodeElimination(ast, refs);
return generate(ast, { sourceMaps: true, sourceFileName: id }, source);
}

function analyzeRouteExport(
path: NodePath<t.ExportDefaultDeclaration>
): Analysis | Error {
let route = path.node.declaration;

// export default {...}
if (t.isObjectExpression(route)) {
let routePath = path.get("declaration") as NodePath<t.ObjectExpression>;
return analyzeRoute(routePath);
}

// export default defineRoute({...})
if (t.isCallExpression(route)) {
let routePath = path.get("declaration") as NodePath<t.CallExpression>;
if (!isDefineRoute(routePath)) {
return routePath.buildCodeFrameError("TODO");
}

if (routePath.node.arguments.length !== 1) {
return path.buildCodeFrameError(
`defineRoute must take exactly one argument`
);
}
let arg = routePath.node.arguments[0];
let argPath = path.get("arguments.0") as NodePath<t.ObjectExpression>;
if (!t.isObjectExpression(arg)) {
return argPath.buildCodeFrameError(
"defineRoute argument must be a literal object"
);
}
return analyzeRoute(argPath);
}

console.log({ path });
console.log("BEFORE");
let err = path.get("declaration").buildCodeFrameError("TODO");
console.log("AFTER");
return err;
}

function analyzeRoute(path: NodePath<t.ObjectExpression>): Analysis {
let analysis: Analysis = {
loader: null,
clientLoader: null,
action: null,
clientAction: null,
Component: null,
ErrorBoundary: null,
};

for (let [i, property] of path.node.properties.entries()) {
if (!t.isObjectProperty(property) && !t.isObjectMethod(property)) {
let propertyPath = path.get(`properties.${i}`) as NodePath<t.Node>;
throw propertyPath.buildCodeFrameError("todo");
}

let propertyPath = path.get(`properties.${i}`) as NodePath<
t.ObjectProperty | t.ObjectMethod
>;
if (property.computed || !t.isIdentifier(property.key)) {
throw propertyPath.buildCodeFrameError("todo");
}

let key = property.key.name;
if (key === "params") {
let paramsPath = propertyPath as NodePath<t.ObjectProperty>;
checkRouteParams(paramsPath);
continue;
}
if (isField(key)) {
analysis[key] = propertyPath;
continue;
}
}
return analysis;
}

function checkRouteParams(path: NodePath<t.ObjectProperty>) {
if (t.isObjectMethod(path.node)) {
throw path.buildCodeFrameError(`params must be statically analyzable`);
}
if (!t.isArrayExpression(path.node.value)) {
throw path.buildCodeFrameError(`params must be statically analyzable`);
}
for (let [i, element] of path.node.value.elements.entries()) {
if (!t.isStringLiteral(element)) {
let elementPath = path.get(`value.elements.${i}`) as NodePath<t.Node>;
throw elementPath.buildCodeFrameError(
`params must be statically analyzable`
);
}
}
}

function isDefineRoute(path: NodePath<t.CallExpression>): boolean {
if (!t.isIdentifier(path.node.callee)) return false;
let binding = path.scope.getBinding(path.node.callee.name);
if (!binding) return false;
return isCanonicallyImportedAs(binding, {
imported: "defineRoute",
source: "react-router",
});
}

function isCanonicallyImportedAs(
binding: Binding,
{
source: sourceName,
imported: importedName,
}: {
source: string;
imported: string;
}
): boolean {
// import source
if (!t.isImportDeclaration(binding?.path.parent)) return false;
if (binding.path.parent.source.value !== sourceName) return false;

// import specifier
if (!t.isImportSpecifier(binding?.path.node)) return false;
let { imported } = binding.path.node;
if (!t.isIdentifier(imported)) return false;
if (imported.name !== importedName) return false;
return true;
}
Loading

0 comments on commit 7b3ba79

Please sign in to comment.