Skip to content

fix: Aliased exports to already exported component for "use client" #493

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { normalizeModulePath } from "./normalizeModulePath.mjs";
const log = debug("rwsdk:vite:rsc-directives-plugin");
const verboseLog = debug("verbose:rwsdk:vite:rsc-directives-plugin");

export const rscDirectivesPlugin = ({
export const directivesPlugin = ({
projectRootDir,
clientFiles,
serverFiles,
Expand Down
4 changes: 2 additions & 2 deletions sdk/src/vite/redwoodPlugin.mts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import reactPlugin from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";

import { transformJsxScriptTagsPlugin } from "./transformJsxScriptTagsPlugin.mjs";
import { rscDirectivesPlugin } from "./rscDirectivesPlugin.mjs";
import { directivesPlugin } from "./directivesPlugin.mjs";
import { useClientLookupPlugin } from "./useClientLookupPlugin.mjs";
import { useServerLookupPlugin } from "./useServerLookupPlugin.mjs";
import { miniflarePlugin } from "./miniflarePlugin.mjs";
Expand Down Expand Up @@ -87,7 +87,7 @@ export const redwoodPlugin = async (
options.configPath ?? (await findWranglerConfig(projectRootDir)),
}),
reactPlugin(),
rscDirectivesPlugin({
directivesPlugin({
projectRootDir,
clientFiles,
serverFiles,
Expand Down
43 changes: 28 additions & 15 deletions sdk/src/vite/transformClientComponents.mts
Original file line number Diff line number Diff line change
Expand Up @@ -100,21 +100,29 @@ export async function transformClientComponents(
exported: string;
isDefault: boolean;
statementIdx: number;
alias?: string;
};

const exportInfos: ExportInfo[] = [];
let defaultExportInfo: ExportInfo | undefined;

// Helper to get the computed local name (with alias suffix if present)
function getComputedLocalName(info: ExportInfo): string {
return `${info.local}${info.alias ? `_${info.alias}` : ""}`;
}

// Helper to add export info
function addExport(
local: string,
exported: string,
isDefault: boolean,
statementIdx: number,
alias?: string,
) {
if (isDefault) {
defaultExportInfo = { local, exported, isDefault, statementIdx };
} else {
exportInfos.push({ local, exported, isDefault, statementIdx });
exportInfos.push({ local, exported, isDefault, statementIdx, alias });
}
}

Expand Down Expand Up @@ -174,13 +182,10 @@ export async function transformClientComponents(
const namedExports = stmt.getNamedExports();
if (namedExports.length > 0) {
namedExports.forEach((exp) => {
const local = exp.getAliasNode()
? exp.getNameNode().getText()
: exp.getName();
const exported = exp.getAliasNode()
? exp.getAliasNode()!.getText()
: exp.getName();
addExport(local, exported, exported === "default", idx);
const alias = exp.getAliasNode()?.getText();
const local = alias ? exp.getNameNode().getText() : exp.getName();
const exported = alias ? alias : exp.getName();
addExport(local, exported, exported === "default", idx, alias);
});
}
return;
Expand Down Expand Up @@ -236,23 +241,31 @@ export async function transformClientComponents(
namedImports: [{ name: "registerClientReference" }],
});

// Add registerClientReference assignments for named exports in order
for (const info of exportInfos) {
// Compute unique computed local names first
const computedLocalNames = new Map(
exportInfos.map((info) => [getComputedLocalName(info), info]),
);

// Add registerClientReference assignments for unique names
for (const [computedLocalName, correspondingInfo] of computedLocalNames) {
log(
":isEsbuild=%s: Registering client reference for named export: %s as %s",
!!ctx.isEsbuild,
info.local,
info.exported,
correspondingInfo.local,
correspondingInfo.exported,
);
sourceFile.addStatements(
`const ${info.local} = registerClientReference("${normalizedId}", "${info.exported}");`,
`const ${computedLocalName} = registerClientReference("${normalizedId}", "${correspondingInfo.exported}");`,
);
}

// Add grouped export statement for named exports (preserving order and alias)
if (exportInfos.length > 0) {
const exportNames = exportInfos.map((e) =>
e.local === e.exported ? e.local : `${e.local} as ${e.exported}`,
const exportNames = Array.from(computedLocalNames.entries()).map(
([computedLocalName, correspondingInfo]) =>
correspondingInfo.local === correspondingInfo.exported
? computedLocalName
: `${computedLocalName} as ${correspondingInfo.exported}`,
);
log(
":isEsbuild=%s: Exporting named exports: %O",
Expand Down
27 changes: 23 additions & 4 deletions sdk/src/vite/transformClientComponents.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ export { Fourth as AnotherName }`)) ?? "",
const First = registerClientReference("/test/file.tsx", "First");
const Second = registerClientReference("/test/file.tsx", "Second");
const Third = registerClientReference("/test/file.tsx", "Third");
const Fourth = registerClientReference("/test/file.tsx", "AnotherName");
export { First, Second, Third, Fourth as AnotherName };
const Fourth_AnotherName = registerClientReference("/test/file.tsx", "AnotherName");
export { First, Second, Third, Fourth_AnotherName as AnotherName };
export default registerClientReference("/test/file.tsx", "default");
`,
);
Expand Down Expand Up @@ -246,8 +246,8 @@ const MyComponent = () => {
export { MyComponent as CustomName }`)) ?? "",
).toEqual(
`import { registerClientReference } from "rwsdk/worker";
const MyComponent = registerClientReference("/test/file.tsx", "CustomName");
export { MyComponent as CustomName };
const MyComponent_CustomName = registerClientReference("/test/file.tsx", "CustomName");
export { MyComponent_CustomName as CustomName };
`,
);
});
Expand Down Expand Up @@ -286,6 +286,25 @@ const Component = registerClientReference("/test/file.tsx", "Component");
const data = registerClientReference("/test/file.tsx", "data");
const helper = registerClientReference("/test/file.tsx", "helper");
export { Component, data, helper };
`,
);
});

it("transforms multiple exports aliases for the same component", async () => {
expect(
(await transform(`"use client"

export const Slot = () => {
return jsx('div', { children: 'Slot' });
}

export { Slot, Slot as Root }
`)) ?? "",
).toEqual(
`import { registerClientReference } from "rwsdk/worker";
const Slot = registerClientReference("/test/file.tsx", "Slot");
const Slot_Root = registerClientReference("/test/file.tsx", "Root");
export { Slot, Slot_Root as Root };
`,
);
});
Expand Down