From 077924a17793d18cb655e17e3ff397c85a779499 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 28 Mar 2025 12:14:48 +0100 Subject: [PATCH 1/3] Fix for augmentations --- package.json | 2 +- src/vite/utils/data-functions-augment.test.ts | 611 +++++++++++------- src/vite/utils/data-functions-augment.ts | 60 +- 3 files changed, 411 insertions(+), 262 deletions(-) diff --git a/package.json b/package.json index c5e7c54c..c5354848 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "react-router-devtools", "description": "Devtools for React Router - debug, trace, find hydration errors, catch bugs and inspect server/client data with react-router-devtools", "author": "Alem Tuzlak", - "version": "1.1.7", + "version": "1.1.8", "license": "MIT", "keywords": [ "react-router", diff --git a/src/vite/utils/data-functions-augment.test.ts b/src/vite/utils/data-functions-augment.test.ts index d2c3b679..a68e9605 100644 --- a/src/vite/utils/data-functions-augment.test.ts +++ b/src/vite/utils/data-functions-augment.test.ts @@ -3,383 +3,512 @@ import { augmentDataFetchingFunctions } from "./data-functions-augment" const removeWhitespace = (str: string) => str.replace(/\s/g, "") describe("transform", () => { - it("should transform the loader export when it's a function", () => { - const result = augmentDataFetchingFunctions( - ` + describe("loader transformations", () => { + it("should transform the loader export when it's a function", () => { + const result = augmentDataFetchingFunctions( + ` export function loader() {} `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withLoaderWrapper as _withLoaderWrapper } from "react-router-devtools/server"; export const loader = _withLoaderWrapper(function loader() {}, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the loader export when it's a const variable", () => { - const result = augmentDataFetchingFunctions( - ` + it("should transform the loader export when it's a const variable", () => { + const result = augmentDataFetchingFunctions( + ` export const loader = async ({ request }) => { return {};} `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withLoaderWrapper as _withLoaderWrapper } from "react-router-devtools/server"; export const loader = _withLoaderWrapper(async ({ request }) => { return {};}, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the loader export when it's a let variable", () => { - const result = augmentDataFetchingFunctions( - ` + it("should transform the loader export when it's a let variable", () => { + const result = augmentDataFetchingFunctions( + ` export let loader = async ({ request }) => { return {};} `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withLoaderWrapper as _withLoaderWrapper } from "react-router-devtools/server"; export let loader = _withLoaderWrapper(async ({ request }) => { return {};}, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the loader export when it's a var variable", () => { - const result = augmentDataFetchingFunctions( - ` + it("should transform the loader export when it's a var variable", () => { + const result = augmentDataFetchingFunctions( + ` export var loader = async ({ request }) => { return {};} `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withLoaderWrapper as _withLoaderWrapper } from "react-router-devtools/server"; export var loader = _withLoaderWrapper(async ({ request }) => { return {};}, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the loader export when it's re-exported from another file", () => { - const result = augmentDataFetchingFunctions( - ` + it("should transform the loader export when it's re-exported from another file", () => { + const result = augmentDataFetchingFunctions( + ` export { loader } from "./loader.js"; `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withLoaderWrapper as _withLoaderWrapper } from "react-router-devtools/server"; export { loader as _loader } from "./loader.js"; export const loader = _withLoaderWrapper(_loader, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should wrap the loader export when it's imported from another file and exported", () => { - const result = augmentDataFetchingFunctions( - ` + it("should wrap the loader export when it's imported from another file and exported", () => { + const result = augmentDataFetchingFunctions( + ` import { loader } from "./loader.js"; export { loader }; `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withLoaderWrapper as _withLoaderWrapper } from "react-router-devtools/server"; import { loader } from "./loader.js"; export { loader as _loader }; export const loader = _withLoaderWrapper(_loader, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should wrap the client loader export when it's a function", () => { - const result = augmentDataFetchingFunctions( - ` - export function clientLoader() {} - `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` - import { withClientLoaderWrapper as _withClientLoaderWrapper } from "react-router-devtools/client"; - export const clientLoader = _withClientLoaderWrapper(function clientLoader() {}, "test"); - `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + it("should wrap the loader export when it's exported via export { loader } and declared within the file", () => { + const result = augmentDataFetchingFunctions( + ` - it("should wrap the client loader export when it's a const variable", () => { - const result = augmentDataFetchingFunctions( - ` - export const clientLoader = async ({ request }) => { return {};} - `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` - import { withClientLoaderWrapper as _withClientLoaderWrapper } from "react-router-devtools/client"; - export const clientLoader = _withClientLoaderWrapper(async ({ request }) => { return {};}, "test"); - `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + function loader() { + return { name: 'React Router' }; + } - it("should wrap the client loader export when it's a let variable", () => { - const result = augmentDataFetchingFunctions( - ` - export let clientLoader = async ({ request }) => { return {};} - `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` - import { withClientLoaderWrapper as _withClientLoaderWrapper } from "react-router-devtools/client"; - export let clientLoader = _withClientLoaderWrapper(async ({ request }) => { return {};}, "test"); - `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) - it("should wrap the client loader export when it's a var variable", () => { - const result = augmentDataFetchingFunctions( - ` - export var clientLoader = async ({ request }) => { return {};} + export { loader }; + `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` - import { withClientLoaderWrapper as _withClientLoaderWrapper } from "react-router-devtools/client"; - export var clientLoader = _withClientLoaderWrapper(async ({ request }) => { return {};}, "test"); + "test", + "/file/path" + ) + const expected = removeWhitespace(` + import { withLoaderWrapper as _withLoaderWrapper } from "react-router-devtools/server"; + const loader = _withLoaderWrapper(function loader() { + return { name: 'React Router' }; + }, "test"); + export { loader }; + `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should wrap the client loader export when it's re-exported from another file", () => { - const result = augmentDataFetchingFunctions( - ` - import { clientLoader } from "./client-loader.js"; - export { clientLoader }; + /* it("should work with export { loader } from 'x' pattern", () => { + const result = augmentDataFetchingFunctions( + ` + + export { loader } from "./$slug"; `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` - import { withClientLoaderWrapper as _withClientLoaderWrapper } from "react-router-devtools/client"; - import { clientLoader } from "./client-loader.js"; - export { clientLoader as _clientLoader }; - export const clientLoader = _withClientLoaderWrapper(_clientLoader, "test"); + "test", + "/file/path" + ) + const expected = removeWhitespace(` + import { withLoaderWrapper as _withLoaderWrapper } from "react-router-devtools/server"; + import { loader as _loader } from "./$slug"; + const loader = _withLoaderWrapper(_loader, "test"); + export { loader }; + `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) */ }) - it("should wrap the client loader export when it's imported from another file and exported", () => { - const result = augmentDataFetchingFunctions( - ` - import { clientLoader } from "./client-loader.js"; + describe("clientLoader transformations", () => { + it("should wrap the client loader export when it's a function", () => { + const result = augmentDataFetchingFunctions( + ` + export function clientLoader() {} + `, + "test", + "/file/path" + ) + const expected = removeWhitespace(` + import { withClientLoaderWrapper as _withClientLoaderWrapper } from "react-router-devtools/client"; + export const clientLoader = _withClientLoaderWrapper(function clientLoader() {}, "test"); + `) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) + + it("should wrap the client loader export when it's a const variable", () => { + const result = augmentDataFetchingFunctions( + ` + export const clientLoader = async ({ request }) => { return {};} + `, + "test", + "/file/path" + ) + const expected = removeWhitespace(` + import { withClientLoaderWrapper as _withClientLoaderWrapper } from "react-router-devtools/client"; + export const clientLoader = _withClientLoaderWrapper(async ({ request }) => { return {};}, "test"); + `) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) + + it("should wrap the client loader export when it's a let variable", () => { + const result = augmentDataFetchingFunctions( + ` + export let clientLoader = async ({ request }) => { return {};} + `, + "test", + "/file/path" + ) + const expected = removeWhitespace(` + import { withClientLoaderWrapper as _withClientLoaderWrapper } from "react-router-devtools/client"; + export let clientLoader = _withClientLoaderWrapper(async ({ request }) => { return {};}, "test"); + `) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) + + it("should wrap the client loader export when it's a var variable", () => { + const result = augmentDataFetchingFunctions( + ` + export var clientLoader = async ({ request }) => { return {};} + `, + "test", + "/file/path" + ) + const expected = removeWhitespace(` + import { withClientLoaderWrapper as _withClientLoaderWrapper } from "react-router-devtools/client"; + export var clientLoader = _withClientLoaderWrapper(async ({ request }) => { return {};}, "test"); + `) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) + + it("should wrap the client loader export when it's re-exported from another file", () => { + const result = augmentDataFetchingFunctions( + ` + import { clientLoader } from "./client-loader.js"; + export { clientLoader }; + `, + "test", + "/file/path" + ) + const expected = removeWhitespace(` + import { withClientLoaderWrapper as _withClientLoaderWrapper } from "react-router-devtools/client"; + import { clientLoader } from "./client-loader.js"; + export { clientLoader as _clientLoader }; + export const clientLoader = _withClientLoaderWrapper(_clientLoader, "test"); + `) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) + + it("should wrap the client loader export when it's imported from another file and exported", () => { + const result = augmentDataFetchingFunctions( + ` + import { clientLoader } from "./client-loader.js"; + export { clientLoader }; + `, + "test", + "/file/path" + ) + const expected = removeWhitespace(` + import { withClientLoaderWrapper as _withClientLoaderWrapper } from "react-router-devtools/client"; + import { clientLoader } from "./client-loader.js"; + export { clientLoader as _clientLoader }; + export const clientLoader = _withClientLoaderWrapper(_clientLoader, "test"); + `) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) + it("should wrap the clientLoader export when it's exported via export { clientLoader } and declared within the file", () => { + const result = augmentDataFetchingFunctions( + ` + + function clientLoader() { + return { name: 'React Router' }; + } + + export { clientLoader }; + `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withClientLoaderWrapper as _withClientLoaderWrapper } from "react-router-devtools/client"; - import { clientLoader } from "./client-loader.js"; - export { clientLoader as _clientLoader }; - export const clientLoader = _withClientLoaderWrapper(_clientLoader, "test"); + const clientLoader = _withClientLoaderWrapper(function clientLoader() { + return { name: 'React Router' }; + }, "test"); + export { clientLoader }; + `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) }) - it("should transform the action export when it's a function", () => { - const result = augmentDataFetchingFunctions( - ` + describe("action transformations", () => { + it("should transform the action export when it's a function", () => { + const result = augmentDataFetchingFunctions( + ` export function action() {} `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withActionWrapper as _withActionWrapper } from "react-router-devtools/server"; export const action = _withActionWrapper(function action() {}, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the action export when it's a const variable", () => { - const result = augmentDataFetchingFunctions( - ` + it("should transform the action export when it's a const variable", () => { + const result = augmentDataFetchingFunctions( + ` export const action = async ({ request }) => { return {};} `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withActionWrapper as _withActionWrapper } from "react-router-devtools/server"; export const action = _withActionWrapper(async ({ request }) => { return {};}, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the action export when it's a let variable", () => { - const result = augmentDataFetchingFunctions( - ` + it("should transform the action export when it's a let variable", () => { + const result = augmentDataFetchingFunctions( + ` export let action = async ({ request }) => { return {};} `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withActionWrapper as _withActionWrapper } from "react-router-devtools/server"; export let action = _withActionWrapper(async ({ request }) => { return {};}, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the action export when it's a var variable", () => { - const result = augmentDataFetchingFunctions( - ` + it("should transform the action export when it's a var variable", () => { + const result = augmentDataFetchingFunctions( + ` export var action = async ({ request }) => { return {};} `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withActionWrapper as _withActionWrapper } from "react-router-devtools/server"; export var action = _withActionWrapper(async ({ request }) => { return {};}, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the action export when it's re-exported from another file", () => { - const result = augmentDataFetchingFunctions( - ` + it("should transform the action export when it's re-exported from another file", () => { + const result = augmentDataFetchingFunctions( + ` export { action } from "./action.js"; `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withActionWrapper as _withActionWrapper } from "react-router-devtools/server"; export { action as _action } from "./action.js"; export const action = _withActionWrapper(_action, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should wrap the action export when it's imported from another file and exported", () => { - const result = augmentDataFetchingFunctions( - ` + it("should wrap the action export when it's imported from another file and exported", () => { + const result = augmentDataFetchingFunctions( + ` import { action } from "./action.js"; export { action }; `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withActionWrapper as _withActionWrapper } from "react-router-devtools/server"; import { action } from "./action.js"; export { action as _action }; export const action = _withActionWrapper(_action, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) + + it("should wrap the action export when it's exported via export { action } and declared within the file", () => { + const result = augmentDataFetchingFunctions( + ` + + function action() { + return { name: 'React Router' }; + } + + + export { action }; - it("should transform the client action export when it's a function", () => { - const result = augmentDataFetchingFunctions( - ` + `, + "test", + "/file/path" + ) + const expected = removeWhitespace(` + import { withActionWrapper as _withActionWrapper } from "react-router-devtools/server"; + const action = _withActionWrapper(function action() { + return { name: 'React Router' }; + }, "test"); + export { action }; + + `) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) + }) + describe("client action transformations", () => { + it("should transform the client action export when it's a function", () => { + const result = augmentDataFetchingFunctions( + ` export function clientAction() {} `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withClientActionWrapper as _withClientActionWrapper } from "react-router-devtools/client"; export const clientAction = _withClientActionWrapper(function clientAction() {}, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the client action export when it's a const variable", () => { - const result = augmentDataFetchingFunctions( - ` + it("should transform the client action export when it's a const variable", () => { + const result = augmentDataFetchingFunctions( + ` export const clientAction = async ({ request }) => { return {};} `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withClientActionWrapper as _withClientActionWrapper } from "react-router-devtools/client"; export const clientAction = _withClientActionWrapper(async ({ request }) => { return {};}, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the client action export when it's a let variable", () => { - const result = augmentDataFetchingFunctions( - ` + it("should transform the client action export when it's a let variable", () => { + const result = augmentDataFetchingFunctions( + ` export let clientAction = async ({ request }) => { return {};} `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withClientActionWrapper as _withClientActionWrapper } from "react-router-devtools/client"; export let clientAction = _withClientActionWrapper(async ({ request }) => { return {};}, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the client action export when it's a var variable", () => { - const result = augmentDataFetchingFunctions( - ` + it("should transform the client action export when it's a var variable", () => { + const result = augmentDataFetchingFunctions( + ` export var clientAction = async ({ request }) => { return {};} `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withClientActionWrapper as _withClientActionWrapper } from "react-router-devtools/client"; export var clientAction = _withClientActionWrapper(async ({ request }) => { return {};}, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the client action export when it's re-exported from another file", () => { - const result = augmentDataFetchingFunctions( - ` + it("should transform the client action export when it's re-exported from another file", () => { + const result = augmentDataFetchingFunctions( + ` import { clientAction } from "./client-action.js"; export { clientAction }; `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withClientActionWrapper as _withClientActionWrapper } from "react-router-devtools/client"; import { clientAction } from "./client-action.js"; export { clientAction as _clientAction }; export const clientAction = _withClientActionWrapper(_clientAction, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the client action export when it's imported from another file and exported", () => { - const result = augmentDataFetchingFunctions( - ` + it("should transform the client action export when it's imported from another file and exported", () => { + const result = augmentDataFetchingFunctions( + ` import { clientAction } from "./client-action.js"; export { clientAction }; `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withClientActionWrapper as _withClientActionWrapper } from "react-router-devtools/client"; import { clientAction } from "./client-action.js"; export { clientAction as _clientAction }; export const clientAction = _withClientActionWrapper(_clientAction, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) + + it("should wrap the clientAction export when it's exported via export { clientAction } and declared within the file", () => { + const result = augmentDataFetchingFunctions( + ` + + function clientAction() { + return { name: 'React Router' }; + } + + + export { clientAction }; + + `, + "test", + "/file/path" + ) + const expected = removeWhitespace(` + import { withClientActionWrapper as _withClientActionWrapper } from "react-router-devtools/client"; + const clientAction = _withClientActionWrapper(function clientAction() { + return { name: 'React Router' }; + }, "test"); + export { clientAction }; + + `) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) }) }) diff --git a/src/vite/utils/data-functions-augment.ts b/src/vite/utils/data-functions-augment.ts index 8a43620b..571deaec 100644 --- a/src/vite/utils/data-functions-augment.ts +++ b/src/vite/utils/data-functions-augment.ts @@ -86,32 +86,52 @@ const transform = (ast: ParseResult, routeId: string) => { if (!ALL_EXPORTS.includes(name)) { return } + const uid = CLIENT_COMPONENT_EXPORTS.includes(name) ? getClientHocId(path, `with${uppercaseFirstLetter(name)}Wrapper`) : getServerHocId(path, `with${uppercaseFirstLetter(name)}Wrapper`) - transformations.push(() => { - const uniqueName = path.scope.generateUidIdentifier(name).name - path.replaceWith( - t.exportNamedDeclaration( - null, - [t.exportSpecifier(t.identifier(name), t.identifier(uniqueName))], - path.node.source - ) + const binding = path.scope.getBinding(name) + if (binding?.path.isFunctionDeclaration()) { + // Replace the function declaration with a wrapped version + binding.path.replaceWith( + t.variableDeclaration("const", [ + t.variableDeclarator( + t.identifier(name), + t.callExpression(uid, [toFunctionExpression(binding.path.node), t.stringLiteral(routeId)]) + ), + ]) ) + } else if (binding?.path.isVariableDeclarator()) { + // Wrap the variable declarator's initializer + const init = binding.path.get("init") + if (init.node) { + init.replaceWith(t.callExpression(uid, [init.node, t.stringLiteral(routeId)])) + } + } else { + transformations.push(() => { + const uniqueName = path.scope.generateUidIdentifier(name).name + path.replaceWith( + t.exportNamedDeclaration( + null, + [t.exportSpecifier(t.identifier(name), t.identifier(uniqueName))], + path.node.source + ) + ) - // Insert the wrapped export after the modified export statement - path.insertAfter( - t.exportNamedDeclaration( - t.variableDeclaration("const", [ - t.variableDeclarator( - t.identifier(name), - t.callExpression(uid, [t.identifier(uniqueName), t.stringLiteral(routeId)]) - ), - ]), - [] + // Insert the wrapped export after the modified export statement + path.insertAfter( + t.exportNamedDeclaration( + t.variableDeclaration("const", [ + t.variableDeclarator( + t.identifier(name), + t.callExpression(uid, [t.identifier(uniqueName), t.stringLiteral(routeId)]) + ), + ]), + [] + ) ) - ) - }) + }) + } } }, }) From 75e2a18062e31b0e5785bc3559c793d780f014ae Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 28 Mar 2025 14:09:04 +0100 Subject: [PATCH 2/3] Fix for augmentations --- src/vite/plugin.tsx | 13 +-- src/vite/utils/data-functions-augment.test.ts | 79 ++++++++++++++++++- src/vite/utils/data-functions-augment.ts | 39 ++++++++- .../react-router-vite/app/routes/exports.tsx | 1 + 4 files changed, 123 insertions(+), 9 deletions(-) create mode 100644 test-apps/react-router-vite/app/routes/exports.tsx diff --git a/src/vite/plugin.tsx b/src/vite/plugin.tsx index 666e0e67..f1fb6631 100644 --- a/src/vite/plugin.tsx +++ b/src/vite/plugin.tsx @@ -115,31 +115,32 @@ export const reactRouterDevTools: (args?: ReactRouterViteConfig) => Plugin[] = ( return injectRdtClient(code, config, pluginImports, id) }, }, + { - name: "react-router-devtools-inject-context", + name: "react-router-devtools-data-function-augment", apply(config) { - return shouldInject(config.mode, includeDevtools) + return shouldInject(config.mode, includeServer) }, transform(code, id) { const routeId = isTransformable(id) if (!routeId) { return } - const finalCode = injectContext(code, routeId, id) + const finalCode = augmentDataFetchingFunctions(code, routeId, id) return finalCode }, }, { - name: "react-router-devtools-data-function-augment", + name: "react-router-devtools-inject-context", apply(config) { - return shouldInject(config.mode, includeServer) + return shouldInject(config.mode, includeDevtools) }, transform(code, id) { const routeId = isTransformable(id) if (!routeId) { return } - const finalCode = augmentDataFetchingFunctions(code, routeId, id) + const finalCode = injectContext(code, routeId, id) return finalCode }, }, diff --git a/src/vite/utils/data-functions-augment.test.ts b/src/vite/utils/data-functions-augment.test.ts index a68e9605..8efef82c 100644 --- a/src/vite/utils/data-functions-augment.test.ts +++ b/src/vite/utils/data-functions-augment.test.ts @@ -74,8 +74,9 @@ describe("transform", () => { ) const expected = removeWhitespace(` import { withLoaderWrapper as _withLoaderWrapper } from "react-router-devtools/server"; - export { loader as _loader } from "./loader.js"; + import { loader as _loader } from "./loader.js"; export const loader = _withLoaderWrapper(_loader, "test"); + export {} from "./loader.js"; `) expect(removeWhitespace(result.code)).toStrictEqual(expected) }) @@ -240,6 +241,24 @@ describe("transform", () => { `) expect(removeWhitespace(result.code)).toStrictEqual(expected) }) + + it("should transform the client action export when it's re-exported from another file", () => { + const result = augmentDataFetchingFunctions( + ` + export { clientLoader } from "./clientLoader.js"; + `, + "test", + "/file/path" + ) + const expected = removeWhitespace(` + import { withClientLoaderWrapper as _withClientLoaderWrapper } from "react-router-devtools/client"; + import { clientLoader as _clientLoader } from "./clientLoader.js"; + export const clientLoader = _withClientLoaderWrapper(_clientLoader, "test"); + export {} from "./clientLoader.js"; + `) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) + it("should wrap the clientLoader export when it's exported via export { clientLoader } and declared within the file", () => { const result = augmentDataFetchingFunctions( ` @@ -338,8 +357,9 @@ describe("transform", () => { ) const expected = removeWhitespace(` import { withActionWrapper as _withActionWrapper } from "react-router-devtools/server"; - export { action as _action } from "./action.js"; + import { action as _action } from "./action.js"; export const action = _withActionWrapper(_action, "test"); + export {} from "./action.js"; `) expect(removeWhitespace(result.code)).toStrictEqual(expected) }) @@ -467,6 +487,23 @@ describe("transform", () => { expect(removeWhitespace(result.code)).toStrictEqual(expected) }) + it("should transform the client action export when it's re-exported from another file", () => { + const result = augmentDataFetchingFunctions( + ` + export { clientAction } from "./clientAction.js"; + `, + "test", + "/file/path" + ) + const expected = removeWhitespace(` + import { withClientActionWrapper as _withClientActionWrapper } from "react-router-devtools/client"; + import { clientAction as _clientAction } from "./clientAction.js"; + export const clientAction = _withClientActionWrapper(_clientAction, "test"); + export {} from "./clientAction.js"; + `) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) + it("should transform the client action export when it's imported from another file and exported", () => { const result = augmentDataFetchingFunctions( ` @@ -512,3 +549,41 @@ describe("transform", () => { }) }) }) + +it("should transform the re-exports when it's re-exported from another file with multiple re-exports", () => { + const result = augmentDataFetchingFunctions( + ` + export { action, loader, default } from "./action.js"; + `, + "test", + "/file/path" + ) + const expected = removeWhitespace(` + import { withActionWrapper as _withActionWrapper, withLoaderWrapper as _withLoaderWrapper } from "react-router-devtools/server"; + import { action as _action } from "./action.js"; + import { loader as _loader } from "./action.js"; + export const action = _withActionWrapper(_action, "test"); + export const loader = _withLoaderWrapper(_loader, "test"); + export { default } from "./action.js"; +`) + expect(removeWhitespace(result.code)).toStrictEqual(expected) +}) + +it("should transform the re-exports when it's re-exported from another file with multiple re-exports", () => { + const result = augmentDataFetchingFunctions( + ` + export { action, loader, default, blah } from "./action.js"; + `, + "test", + "/file/path" + ) + const expected = removeWhitespace(` + import { withActionWrapper as _withActionWrapper, withLoaderWrapper as _withLoaderWrapper } from "react-router-devtools/server"; + import { action as _action } from "./action.js"; + import { loader as _loader } from "./action.js"; + export const action = _withActionWrapper(_action, "test"); + export const loader = _withLoaderWrapper(_loader, "test"); + export { default, blah } from "./action.js"; +`) + expect(removeWhitespace(result.code)).toStrictEqual(expected) +}) diff --git a/src/vite/utils/data-functions-augment.ts b/src/vite/utils/data-functions-augment.ts index 571deaec..5f2ffac8 100644 --- a/src/vite/utils/data-functions-augment.ts +++ b/src/vite/utils/data-functions-augment.ts @@ -32,6 +32,8 @@ const transform = (ast: ParseResult, routeId: string) => { return str.charAt(0).toUpperCase() + str.slice(1) } const transformations: Array<() => void> = [] + + const importDeclarations: Babel.ImportDeclaration[] = [] trav(ast, { ExportDeclaration(path) { if (path.isExportNamedDeclaration()) { @@ -91,7 +93,39 @@ const transform = (ast: ParseResult, routeId: string) => { ? getClientHocId(path, `with${uppercaseFirstLetter(name)}Wrapper`) : getServerHocId(path, `with${uppercaseFirstLetter(name)}Wrapper`) const binding = path.scope.getBinding(name) - if (binding?.path.isFunctionDeclaration()) { + + if (path.node.source) { + // Special condition: export { loader, action } from "./path" + const source = path.node.source.value + + importDeclarations.push( + t.importDeclaration( + [t.importSpecifier(t.identifier(`_${name}`), t.identifier(name))], + t.stringLiteral(source) + ) + ) + transformations.push(() => { + path.insertBefore( + t.exportNamedDeclaration( + t.variableDeclaration("const", [ + t.variableDeclarator( + t.identifier(name), + t.callExpression(uid, [t.identifier(`_${name}`), t.stringLiteral(routeId)]) + ), + ]) + ) + ) + }) + + // Remove the specifier from the exports and add a manual export + transformations.push(() => { + const remainingSpecifiers = path.node.specifiers.filter( + (exportSpecifier) => !(t.isIdentifier(exportSpecifier.exported) && exportSpecifier.exported.name === name) + ) + + path.replaceWith(t.exportNamedDeclaration(null, remainingSpecifiers, path.node.source)) + }) + } else if (binding?.path.isFunctionDeclaration()) { // Replace the function declaration with a wrapped version binding.path.replaceWith( t.variableDeclaration("const", [ @@ -138,6 +172,9 @@ const transform = (ast: ParseResult, routeId: string) => { for (const transformation of transformations) { transformation() } + if (importDeclarations.length > 0) { + ast.program.body.unshift(...importDeclarations) + } if (serverHocs.length > 0) { ast.program.body.unshift( t.importDeclaration( diff --git a/test-apps/react-router-vite/app/routes/exports.tsx b/test-apps/react-router-vite/app/routes/exports.tsx new file mode 100644 index 00000000..c12558cd --- /dev/null +++ b/test-apps/react-router-vite/app/routes/exports.tsx @@ -0,0 +1 @@ +export { loader, default }from "./_index"; From b84ab774d597d802185fbf14b1f93bcdef56c6cf Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 28 Mar 2025 15:47:28 +0100 Subject: [PATCH 3/3] update --- src/vite/plugin.tsx | 13 +- src/vite/utils/inject-context.test.ts | 579 ++++++++++-------- src/vite/utils/inject-context.ts | 92 ++- .../app/routes/unexported.tsx | 20 + 4 files changed, 429 insertions(+), 275 deletions(-) create mode 100644 test-apps/react-router-vite/app/routes/unexported.tsx diff --git a/src/vite/plugin.tsx b/src/vite/plugin.tsx index f1fb6631..666e0e67 100644 --- a/src/vite/plugin.tsx +++ b/src/vite/plugin.tsx @@ -115,32 +115,31 @@ export const reactRouterDevTools: (args?: ReactRouterViteConfig) => Plugin[] = ( return injectRdtClient(code, config, pluginImports, id) }, }, - { - name: "react-router-devtools-data-function-augment", + name: "react-router-devtools-inject-context", apply(config) { - return shouldInject(config.mode, includeServer) + return shouldInject(config.mode, includeDevtools) }, transform(code, id) { const routeId = isTransformable(id) if (!routeId) { return } - const finalCode = augmentDataFetchingFunctions(code, routeId, id) + const finalCode = injectContext(code, routeId, id) return finalCode }, }, { - name: "react-router-devtools-inject-context", + name: "react-router-devtools-data-function-augment", apply(config) { - return shouldInject(config.mode, includeDevtools) + return shouldInject(config.mode, includeServer) }, transform(code, id) { const routeId = isTransformable(id) if (!routeId) { return } - const finalCode = injectContext(code, routeId, id) + const finalCode = augmentDataFetchingFunctions(code, routeId, id) return finalCode }, }, diff --git a/src/vite/utils/inject-context.test.ts b/src/vite/utils/inject-context.test.ts index 5499a79d..32764cdb 100644 --- a/src/vite/utils/inject-context.test.ts +++ b/src/vite/utils/inject-context.test.ts @@ -3,383 +3,462 @@ import { injectContext } from "./inject-context" const removeWhitespace = (str: string) => str.replace(/\s/g, "") describe("transform", () => { - it("should transform the loader export when it's a function", () => { - const result = injectContext( - ` - export function loader() {} - `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` - import { withLoaderContextWrapper as _withLoaderContextWrapper } from "react-router-devtools/context"; - export const loader = _withLoaderContextWrapper(function loader() {}, "test"); - `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + describe("loader transforms", () => { + it("should transform the loader export when it's a function", () => { + const result = injectContext( + ` + export function loader() {} + `, + "test", + "/file/path" + ) + const expected = removeWhitespace(` + import { withLoaderContextWrapper as _withLoaderContextWrapper } from "react-router-devtools/context"; + export const loader = _withLoaderContextWrapper(function loader() {}, "test"); + `) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the loader export when it's a const variable", () => { - const result = injectContext( - ` - export const loader = async ({ request }) => { return {};} - `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` - import { withLoaderContextWrapper as _withLoaderContextWrapper } from "react-router-devtools/context"; - export const loader = _withLoaderContextWrapper(async ({ request }) => { return {};}, "test"); - `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + it("should transform the loader export when it's a const variable", () => { + const result = injectContext( + ` + export const loader = async ({ request }) => { return {};} + `, + "test", + "/file/path" + ) + const expected = removeWhitespace(` + import { withLoaderContextWrapper as _withLoaderContextWrapper } from "react-router-devtools/context"; + export const loader = _withLoaderContextWrapper(async ({ request }) => { return {};}, "test"); + `) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the loader export when it's a let variable", () => { - const result = injectContext( - ` - export let loader = async ({ request }) => { return {};} - `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` - import { withLoaderContextWrapper as _withLoaderContextWrapper } from "react-router-devtools/context"; - export let loader = _withLoaderContextWrapper(async ({ request }) => { return {};}, "test"); - `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + it("should transform the loader export when it's a let variable", () => { + const result = injectContext( + ` + export let loader = async ({ request }) => { return {};} + `, + "test", + "/file/path" + ) + const expected = removeWhitespace(` + import { withLoaderContextWrapper as _withLoaderContextWrapper } from "react-router-devtools/context"; + export let loader = _withLoaderContextWrapper(async ({ request }) => { return {};}, "test"); + `) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the loader export when it's a var variable", () => { - const result = injectContext( - ` - export var loader = async ({ request }) => { return {};} - `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` - import { withLoaderContextWrapper as _withLoaderContextWrapper } from "react-router-devtools/context"; - export var loader = _withLoaderContextWrapper(async ({ request }) => { return {};}, "test"); - `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + it("should transform the loader export when it's a var variable", () => { + const result = injectContext( + ` + export var loader = async ({ request }) => { return {};} + `, + "test", + "/file/path" + ) + const expected = removeWhitespace(` + import { withLoaderContextWrapper as _withLoaderContextWrapper } from "react-router-devtools/context"; + export var loader = _withLoaderContextWrapper(async ({ request }) => { return {};}, "test"); + `) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the loader export when it's re-exported from another file", () => { - const result = injectContext( - ` - export { loader } from "./loader.js"; - `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` - import { withLoaderContextWrapper as _withLoaderContextWrapper } from "react-router-devtools/context"; - export { loader as _loader } from "./loader.js"; - export const loader = _withLoaderContextWrapper(_loader, "test"); - `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + it("should transform the loader export when it's re-exported from another file", () => { + const result = injectContext( + ` + export { loader } from "./loader.js"; + `, + "test", + "/file/path" + ) + const expected = removeWhitespace(` + import { withLoaderContextWrapper as _withLoaderContextWrapper } from "react-router-devtools/context"; + import { loader as _loader } from "./loader.js"; + export const loader = _withLoaderContextWrapper(_loader, "test"); + export {} from "./loader.js"; + `) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should wrap the loader export when it's imported from another file and exported", () => { - const result = injectContext( - ` - import { loader } from "./loader.js"; - export { loader }; - `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` - import { withLoaderContextWrapper as _withLoaderContextWrapper } from "react-router-devtools/context"; - import { loader } from "./loader.js"; - export { loader as _loader }; - export const loader = _withLoaderContextWrapper(_loader, "test"); - `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) + it("should wrap the loader export when it's imported from another file and exported", () => { + const result = injectContext( + ` + import { loader } from "./loader.js"; + export { loader }; + `, + "test", + "/file/path" + ) + const expected = removeWhitespace(` + import { withLoaderContextWrapper as _withLoaderContextWrapper } from "react-router-devtools/context"; + import { loader } from "./loader.js"; + export { loader as _loader }; + export const loader = _withLoaderContextWrapper(_loader, "test"); + `) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) }) - - it("should wrap the client loader export when it's a function", () => { - const result = injectContext( - ` + describe("client loader transforms", () => { + it("should wrap the client loader export when it's a function", () => { + const result = injectContext( + ` export function clientLoader() {} `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withClientLoaderContextWrapper as _withClientLoaderContextWrapper } from "react-router-devtools/context"; export const clientLoader = _withClientLoaderContextWrapper(function clientLoader() {}, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should wrap the client loader export when it's a const variable", () => { - const result = injectContext( - ` + it("should wrap the client loader export when it's a const variable", () => { + const result = injectContext( + ` export const clientLoader = async ({ request }) => { return {};} `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withClientLoaderContextWrapper as _withClientLoaderContextWrapper } from "react-router-devtools/context"; export const clientLoader = _withClientLoaderContextWrapper(async ({ request }) => { return {};}, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should wrap the client loader export when it's a let variable", () => { - const result = injectContext( - ` + it("should wrap the client loader export when it's a let variable", () => { + const result = injectContext( + ` export let clientLoader = async ({ request }) => { return {};} `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withClientLoaderContextWrapper as _withClientLoaderContextWrapper } from "react-router-devtools/context"; export let clientLoader = _withClientLoaderContextWrapper(async ({ request }) => { return {};}, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should wrap the client loader export when it's a var variable", () => { - const result = injectContext( - ` + it("should wrap the client loader export when it's a var variable", () => { + const result = injectContext( + ` export var clientLoader = async ({ request }) => { return {};} `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withClientLoaderContextWrapper as _withClientLoaderContextWrapper } from "react-router-devtools/context"; export var clientLoader = _withClientLoaderContextWrapper(async ({ request }) => { return {};}, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should wrap the client loader export when it's re-exported from another file", () => { - const result = injectContext( - ` + it("should wrap the client loader export when it's re-exported from another file", () => { + const result = injectContext( + ` import { clientLoader } from "./client-loader.js"; export { clientLoader }; `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withClientLoaderContextWrapper as _withClientLoaderContextWrapper } from "react-router-devtools/context"; import { clientLoader } from "./client-loader.js"; export { clientLoader as _clientLoader }; export const clientLoader = _withClientLoaderContextWrapper(_clientLoader, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should wrap the client loader export when it's imported from another file and exported", () => { - const result = injectContext( - ` + it("should wrap the client loader export when it's imported from another file and exported", () => { + const result = injectContext( + ` import { clientLoader } from "./client-loader.js"; export { clientLoader }; `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withClientLoaderContextWrapper as _withClientLoaderContextWrapper } from "react-router-devtools/context"; import { clientLoader } from "./client-loader.js"; export { clientLoader as _clientLoader }; export const clientLoader = _withClientLoaderContextWrapper(_clientLoader, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the action export when it's a function", () => { - const result = injectContext( - ` + it("should transform the client loader export when it's re-exported from another file", () => { + const result = injectContext( + ` + export { clientLoader } from "./clientLoader.js"; + `, + "test", + "/file/path" + ) + const expected = removeWhitespace(` + import { withClientLoaderContextWrapper as _withClientLoaderContextWrapper } from "react-router-devtools/context"; + import { clientLoader as _clientLoader } from "./clientLoader.js"; + export const clientLoader = _withClientLoaderContextWrapper(_clientLoader, "test"); + export {} from "./clientLoader.js"; + `) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) + }) + describe("action transforms", () => { + it("should transform the action export when it's a function", () => { + const result = injectContext( + ` export function action() {} `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withActionContextWrapper as _withActionContextWrapper } from "react-router-devtools/context"; export const action = _withActionContextWrapper(function action() {}, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the action export when it's a const variable", () => { - const result = injectContext( - ` + it("should transform the action export when it's a const variable", () => { + const result = injectContext( + ` export const action = async ({ request }) => { return {};} `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withActionContextWrapper as _withActionContextWrapper } from "react-router-devtools/context"; export const action = _withActionContextWrapper(async ({ request }) => { return {};}, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the action export when it's a let variable", () => { - const result = injectContext( - ` + it("should transform the action export when it's a let variable", () => { + const result = injectContext( + ` export let action = async ({ request }) => { return {};} `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withActionContextWrapper as _withActionContextWrapper } from "react-router-devtools/context"; export let action = _withActionContextWrapper(async ({ request }) => { return {};}, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the action export when it's a var variable", () => { - const result = injectContext( - ` + it("should transform the action export when it's a var variable", () => { + const result = injectContext( + ` export var action = async ({ request }) => { return {};} `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withActionContextWrapper as _withActionContextWrapper } from "react-router-devtools/context"; export var action = _withActionContextWrapper(async ({ request }) => { return {};}, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the action export when it's re-exported from another file", () => { - const result = injectContext( - ` + it("should transform the action export when it's re-exported from another file", () => { + const result = injectContext( + ` export { action } from "./action.js"; `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withActionContextWrapper as _withActionContextWrapper } from "react-router-devtools/context"; - export { action as _action } from "./action.js"; + import { action as _action } from "./action.js"; export const action = _withActionContextWrapper(_action, "test"); + export {} from "./action.js"; `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should wrap the action export when it's imported from another file and exported", () => { - const result = injectContext( - ` + it("should wrap the action export when it's imported from another file and exported", () => { + const result = injectContext( + ` import { action } from "./action.js"; export { action }; `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withActionContextWrapper as _withActionContextWrapper } from "react-router-devtools/context"; import { action } from "./action.js"; export { action as _action }; export const action = _withActionContextWrapper(_action, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) }) - - it("should transform the client action export when it's a function", () => { - const result = injectContext( - ` + describe("client action transforms", () => { + it("should transform the client action export when it's a function", () => { + const result = injectContext( + ` export function clientAction() {} `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withClientActionContextWrapper as _withClientActionContextWrapper } from "react-router-devtools/context"; export const clientAction = _withClientActionContextWrapper(function clientAction() {}, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the client action export when it's a const variable", () => { - const result = injectContext( - ` + it("should transform the client action export when it's a const variable", () => { + const result = injectContext( + ` export const clientAction = async ({ request }) => { return {};} `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withClientActionContextWrapper as _withClientActionContextWrapper } from "react-router-devtools/context"; export const clientAction = _withClientActionContextWrapper(async ({ request }) => { return {};}, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the client action export when it's a let variable", () => { - const result = injectContext( - ` + it("should transform the client action export when it's a let variable", () => { + const result = injectContext( + ` export let clientAction = async ({ request }) => { return {};} `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withClientActionContextWrapper as _withClientActionContextWrapper } from "react-router-devtools/context"; export let clientAction = _withClientActionContextWrapper(async ({ request }) => { return {};}, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the client action export when it's a var variable", () => { - const result = injectContext( - ` + it("should transform the client action export when it's a var variable", () => { + const result = injectContext( + ` export var clientAction = async ({ request }) => { return {};} `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withClientActionContextWrapper as _withClientActionContextWrapper } from "react-router-devtools/context"; export var clientAction = _withClientActionContextWrapper(async ({ request }) => { return {};}, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the client action export when it's re-exported from another file", () => { - const result = injectContext( - ` + it("should transform the client action export when it's re-exported from another file", () => { + const result = injectContext( + ` import { clientAction } from "./client-action.js"; export { clientAction }; `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withClientActionContextWrapper as _withClientActionContextWrapper } from "react-router-devtools/context"; import { clientAction } from "./client-action.js"; export { clientAction as _clientAction }; export const clientAction = _withClientActionContextWrapper(_clientAction, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) - }) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) - it("should transform the client action export when it's imported from another file and exported", () => { - const result = injectContext( - ` + it("should transform the client action export when it's imported from another file and exported", () => { + const result = injectContext( + ` import { clientAction } from "./client-action.js"; export { clientAction }; `, - "test", - "/file/path" - ) - const expected = removeWhitespace(` + "test", + "/file/path" + ) + const expected = removeWhitespace(` import { withClientActionContextWrapper as _withClientActionContextWrapper } from "react-router-devtools/context"; import { clientAction } from "./client-action.js"; export { clientAction as _clientAction }; export const clientAction = _withClientActionContextWrapper(_clientAction, "test"); `) - expect(removeWhitespace(result.code)).toStrictEqual(expected) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) + + it("should transform the clientAction export when it's re-exported from another file", () => { + const result = injectContext( + ` + export { clientAction } from "./clientAction.js"; + `, + "test", + "/file/path" + ) + const expected = removeWhitespace(` + import { withClientActionContextWrapper as _withClientActionContextWrapper } from "react-router-devtools/context"; + import { clientAction as _clientAction } from "./clientAction.js"; + export const clientAction = _withClientActionContextWrapper(_clientAction, "test"); + export {} from "./clientAction.js"; + `) + expect(removeWhitespace(result.code)).toStrictEqual(expected) + }) }) }) + +it("should transform the re-exports when it's re-exported from another file with multiple re-exports", () => { + const result = injectContext( + ` + export { action, loader, default } from "./action.js"; + `, + "test", + "/file/path" + ) + const expected = removeWhitespace(` + import { withActionContextWrapper as _withActionContextWrapper, withLoaderContextWrapper as _withLoaderContextWrapper } from "react-router-devtools/context"; + import { action as _action } from "./action.js"; + import { loader as _loader } from "./action.js"; + export const action = _withActionContextWrapper(_action, "test"); + export const loader = _withLoaderContextWrapper(_loader, "test"); + export { default } from "./action.js"; +`) + expect(removeWhitespace(result.code)).toStrictEqual(expected) +}) + +it("should transform the re-exports when it's re-exported from another file with multiple re-exports", () => { + const result = injectContext( + ` + export { action, loader, default, blah } from "./action.js"; + `, + "test", + "/file/path" + ) + const expected = removeWhitespace(` + import { withActionContextWrapper as _withActionContextWrapper, withLoaderContextWrapper as _withLoaderContextWrapper } from "react-router-devtools/context"; + import { action as _action } from "./action.js"; + import { loader as _loader } from "./action.js"; + export const action = _withActionContextWrapper(_action, "test"); + export const loader = _withLoaderContextWrapper(_loader, "test"); + export { default, blah } from "./action.js"; +`) + expect(removeWhitespace(result.code)).toStrictEqual(expected) +}) diff --git a/src/vite/utils/inject-context.ts b/src/vite/utils/inject-context.ts index 2cff4ec2..7a8a2c43 100644 --- a/src/vite/utils/inject-context.ts +++ b/src/vite/utils/inject-context.ts @@ -23,6 +23,7 @@ const transform = (ast: ParseResult, routeId: string) => { return str.charAt(0).toUpperCase() + str.slice(1) } const transformations: Array<() => void> = [] + const importDeclarations: Babel.ImportDeclaration[] = [] trav(ast, { ExportDeclaration(path) { if (path.isExportNamedDeclaration()) { @@ -73,36 +74,91 @@ const transform = (ast: ParseResult, routeId: string) => { if (!ALL_EXPORTS.includes(name)) { return } + const uid = getHocId(path, `with${uppercaseFirstLetter(name)}ContextWrapper`) - transformations.push(() => { - const uniqueName = path.scope.generateUidIdentifier(name).name - path.replaceWith( - t.exportNamedDeclaration( - null, - [t.exportSpecifier(t.identifier(name), t.identifier(uniqueName))], - path.node.source + const binding = path.scope.getBinding(name) + + if (path.node.source) { + // Special condition: export { loader, action } from "./path" + const source = path.node.source.value + + importDeclarations.push( + t.importDeclaration( + [t.importSpecifier(t.identifier(`_${name}`), t.identifier(name))], + t.stringLiteral(source) ) ) + transformations.push(() => { + path.insertBefore( + t.exportNamedDeclaration( + t.variableDeclaration("const", [ + t.variableDeclarator( + t.identifier(name), + t.callExpression(uid, [t.identifier(`_${name}`), t.stringLiteral(routeId)]) + ), + ]) + ) + ) + }) - // Insert the wrapped export after the modified export statement - path.insertAfter( - t.exportNamedDeclaration( - t.variableDeclaration("const", [ - t.variableDeclarator( - t.identifier(name), - t.callExpression(uid, [t.identifier(uniqueName), t.stringLiteral(routeId)]) - ), - ]), - [] + // Remove the specifier from the exports and add a manual export + transformations.push(() => { + const remainingSpecifiers = path.node.specifiers.filter( + (exportSpecifier) => !(t.isIdentifier(exportSpecifier.exported) && exportSpecifier.exported.name === name) ) + + path.replaceWith(t.exportNamedDeclaration(null, remainingSpecifiers, path.node.source)) + }) + } else if (binding?.path.isFunctionDeclaration()) { + // Replace the function declaration with a wrapped version + binding.path.replaceWith( + t.variableDeclaration("const", [ + t.variableDeclarator( + t.identifier(name), + t.callExpression(uid, [toFunctionExpression(binding.path.node), t.stringLiteral(routeId)]) + ), + ]) ) - }) + } else if (binding?.path.isVariableDeclarator()) { + // Wrap the variable declarator's initializer + const init = binding.path.get("init") + if (init.node) { + init.replaceWith(t.callExpression(uid, [init.node, t.stringLiteral(routeId)])) + } + } else { + transformations.push(() => { + const uniqueName = path.scope.generateUidIdentifier(name).name + path.replaceWith( + t.exportNamedDeclaration( + null, + [t.exportSpecifier(t.identifier(name), t.identifier(uniqueName))], + path.node.source + ) + ) + + // Insert the wrapped export after the modified export statement + path.insertAfter( + t.exportNamedDeclaration( + t.variableDeclaration("const", [ + t.variableDeclarator( + t.identifier(name), + t.callExpression(uid, [t.identifier(uniqueName), t.stringLiteral(routeId)]) + ), + ]), + [] + ) + ) + }) + } } }, }) for (const transformation of transformations) { transformation() } + if (importDeclarations.length > 0) { + ast.program.body.unshift(...importDeclarations) + } if (hocs.length > 0) { ast.program.body.unshift( t.importDeclaration( diff --git a/test-apps/react-router-vite/app/routes/unexported.tsx b/test-apps/react-router-vite/app/routes/unexported.tsx new file mode 100644 index 00000000..cad5878d --- /dev/null +++ b/test-apps/react-router-vite/app/routes/unexported.tsx @@ -0,0 +1,20 @@ + +function loader() { + return { name: 'React Router' }; +} + +function Home({ loaderData }: Route.ComponentProps) { + return ( +
+

Hello, {loaderData.name}

+ + React Router Docs + +
+ ); +} + +export { loader, Home as default };