From a8402442de1b4ecfb2da92720f5ee579b3414907 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Mon, 3 Aug 2026 19:21:25 +0900 Subject: [PATCH] refactor(rsc): resolve local function exports Co-authored-by: OpenCode --- packages/plugin-rsc/e2e/source-map.test.ts | 4 +- .../canonical-vs-wrapped-binding.js | 2 +- ...anonical-vs-wrapped-binding.js.map.snap.md | 6 +- .../canonical-vs-wrapped-binding.js.snap.md | 14 +-- .../default-identifier.js.map.snap.md | 14 +-- .../wrap-export/default-identifier.js.snap.md | 13 ++- .../wrap-export/local-alias.js.map.snap.md | 12 ++- .../wrap-export/local-alias.js.snap.md | 13 ++- ...l-export-before-declaration.js.map.snap.md | 8 +- ...local-export-before-declaration.js.snap.md | 9 +- .../transforms/module-export-effect.test.ts | 13 ++- .../src/transforms/module-export-scan.test.ts | 46 ++++++++ .../src/transforms/module-export-scan.ts | 102 ++++++++++++++++-- .../src/transforms/module-export-wrap.test.ts | 62 ++++++++++- .../src/transforms/module-export-wrap.ts | 77 ++++++++++--- .../src/transforms/wrap-export.test.ts | 7 +- 16 files changed, 326 insertions(+), 76 deletions(-) diff --git a/packages/plugin-rsc/e2e/source-map.test.ts b/packages/plugin-rsc/e2e/source-map.test.ts index ba3578710..ac51e2c8c 100644 --- a/packages/plugin-rsc/e2e/source-map.test.ts +++ b/packages/plugin-rsc/e2e/source-map.test.ts @@ -65,8 +65,8 @@ const serverReferenceCases: { }, { route: '/specifiers', - // Registration effects for export specifiers are appended without explicit - // mappings. These expectations record bundler-specific adjacent fallbacks. + // Re-export registration effects are appended without explicit mappings. + // These expectations record bundler-specific adjacent fallbacks. references: [ { name: 'local-alias', diff --git a/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/canonical-vs-wrapped-binding.js b/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/canonical-vs-wrapped-binding.js index 06e0bcbf2..3fe495e7a 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/canonical-vs-wrapped-binding.js +++ b/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/canonical-vs-wrapped-binding.js @@ -1,6 +1,6 @@ export async function direct() {} -const indirect = async () => {} +let indirect = async () => {} export { indirect } consume(direct, indirect) diff --git a/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/canonical-vs-wrapped-binding.js.map.snap.md b/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/canonical-vs-wrapped-binding.js.map.snap.md index 2627e5e80..8795e224d 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/canonical-vs-wrapped-binding.js.map.snap.md +++ b/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/canonical-vs-wrapped-binding.js.map.snap.md @@ -2,7 +2,7 @@ ```txt (0:7) "async function direct() {}\n" --> (0:0) "async function direct() {}\n" -(2:0) "const indirect = async () => {}\n" --> (2:0) "const indirect = async () => {}\n" +(2:0) "let indirect = async () => {}\n" --> (2:0) "let indirect = async () => {}\n" (5:0) "consume(direct, indirect)\n" --> (5:0) "consume(direct, indirect)\n" (0:0) "export async function direct() {}\n" --> (6:0) "direct = /* #__PURE__ */ registerServerReference(direct, \"direct\");\n" (0:0) "export async function direct() {}\n" --> (7:0) "export { direct };\n" @@ -15,7 +15,7 @@ ```txt (0:7) "async function direct() {}\n" --> (0:0) "async function direct() {}\n" -(2:0) "const indirect = async () => {}\n" --> (2:0) "const indirect = async () => {}\n" +(2:0) "let indirect = async () => {}\n" --> (2:0) "let indirect = async () => {}\n" (3:0) "export { indirect }\n" --> (3:0) "export { indirect }\n" (5:0) "consume(direct, indirect)\n" --> (5:0) "consume(direct, indirect)\n" (0:0) "export async function direct() {}\n" --> (6:0) "\n" @@ -31,7 +31,7 @@ (0:7) "async function direct() " --> (1:0) "const $$module_0_implementation_direct = async function $$module_0_implementation_direct() " (0:31) "{}\n" --> (1:91) "{};\n" [unmapped] --> (2:0) "export const direct = /* #__PURE__ */ registerServerReference(Object.defineProperty($$module_0_implementation_direct, \"name\", { value: \"direct\" }), \"direct\");\n" -(2:0) "const indirect = async () => {}\n" --> (4:0) "const indirect = async () => {}\n" +(2:0) "let indirect = async () => {}\n" --> (4:0) "let indirect = async () => {}\n" (5:0) "consume(direct, indirect)\n" --> (7:0) "consume(direct, indirect)\n" [unmapped] --> (9:0) "const $$module_1_binding_indirect = /* #__PURE__ */ registerServerReference(indirect, \"indirect\");\n" [unmapped] --> (10:0) "export { $$module_1_binding_indirect as indirect };\n" diff --git a/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/canonical-vs-wrapped-binding.js.snap.md b/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/canonical-vs-wrapped-binding.js.snap.md index 4d21726b6..e3f30a0cd 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/canonical-vs-wrapped-binding.js.snap.md +++ b/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/canonical-vs-wrapped-binding.js.snap.md @@ -3,7 +3,7 @@ ```js export async function direct() {} -const indirect = async () => {} +let indirect = async () => {} export { indirect } consume(direct, indirect) @@ -15,12 +15,12 @@ consume(direct, indirect) **References:** direct, indirect -[Source map visualization](https://evanw.github.io/source-map-visualization/#MzA0AGFzeW5jIGZ1bmN0aW9uIGRpcmVjdCgpIHt9Cgpjb25zdCBpbmRpcmVjdCA9IGFzeW5jICgpID0+IHt9CgoKY29uc3VtZShkaXJlY3QsIGluZGlyZWN0KQpkaXJlY3QgPSAvKiAjX19QVVJFX18gKi8gcmVnaXN0ZXJTZXJ2ZXJSZWZlcmVuY2UoZGlyZWN0LCAiZGlyZWN0Iik7CmV4cG9ydCB7IGRpcmVjdCB9Owo7CmNvbnN0ICQkd3JhcF9pbmRpcmVjdCA9IC8qICNfX1BVUkVfXyAqLyByZWdpc3RlclNlcnZlclJlZmVyZW5jZShpbmRpcmVjdCwgImluZGlyZWN0Iik7CmV4cG9ydCB7ICQkd3JhcF9pbmRpcmVjdCBhcyBpbmRpcmVjdCB9OwozNzkAeyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIiJdLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgYXN5bmMgZnVuY3Rpb24gZGlyZWN0KCkge31cblxuY29uc3QgaW5kaXJlY3QgPSBhc3luYyAoKSA9PiB7fVxuZXhwb3J0IHsgaW5kaXJlY3QgfVxuXG5jb25zdW1lKGRpcmVjdCwgaW5kaXJlY3QpXG4iXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQU8sS0FBSyxDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLENBQUM7O0FBRWhDLEtBQUssQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDOzs7QUFHOUIsT0FBTyxDQUFDLE1BQU0sQ0FBQyxDQUFDLFFBQVE7QUFMeEI7QUFBQTs7OzsifQ==) +[Source map visualization](https://evanw.github.io/source-map-visualization/#MzAyAGFzeW5jIGZ1bmN0aW9uIGRpcmVjdCgpIHt9CgpsZXQgaW5kaXJlY3QgPSBhc3luYyAoKSA9PiB7fQoKCmNvbnN1bWUoZGlyZWN0LCBpbmRpcmVjdCkKZGlyZWN0ID0gLyogI19fUFVSRV9fICovIHJlZ2lzdGVyU2VydmVyUmVmZXJlbmNlKGRpcmVjdCwgImRpcmVjdCIpOwpleHBvcnQgeyBkaXJlY3QgfTsKOwpjb25zdCAkJHdyYXBfaW5kaXJlY3QgPSAvKiAjX19QVVJFX18gKi8gcmVnaXN0ZXJTZXJ2ZXJSZWZlcmVuY2UoaW5kaXJlY3QsICJpbmRpcmVjdCIpOwpleHBvcnQgeyAkJHdyYXBfaW5kaXJlY3QgYXMgaW5kaXJlY3QgfTsKMzc3AHsidmVyc2lvbiI6Mywic291cmNlcyI6WyIiXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGRpcmVjdCgpIHt9XG5cbmxldCBpbmRpcmVjdCA9IGFzeW5jICgpID0+IHt9XG5leHBvcnQgeyBpbmRpcmVjdCB9XG5cbmNvbnN1bWUoZGlyZWN0LCBpbmRpcmVjdClcbiJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBTyxLQUFLLENBQUMsUUFBUSxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsQ0FBQzs7QUFFaEMsR0FBRyxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7OztBQUc1QixPQUFPLENBQUMsTUFBTSxDQUFDLENBQUMsUUFBUTtBQUx4QjtBQUFBOzs7OyJ9) ```js async function direct() {} -const indirect = async () => {} +let indirect = async () => {} consume(direct, indirect) @@ -37,12 +37,12 @@ export { $$wrap_indirect as indirect }; **References:** direct, indirect -[Source map visualization](https://evanw.github.io/source-map-visualization/#MjE3AGFzeW5jIGZ1bmN0aW9uIGRpcmVjdCgpIHt9Cgpjb25zdCBpbmRpcmVjdCA9IGFzeW5jICgpID0+IHt9CmV4cG9ydCB7IGluZGlyZWN0IH0KCmNvbnN1bWUoZGlyZWN0LCBpbmRpcmVjdCkKCnJlZ2lzdGVyU2VydmVyUmVmZXJlbmNlKGRpcmVjdCwgImRpcmVjdCIpOwpleHBvcnQgeyBkaXJlY3QgfTsKCnJlZ2lzdGVyU2VydmVyUmVmZXJlbmNlKGluZGlyZWN0LCAiaW5kaXJlY3QiKTs0MTcAeyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIiJdLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgYXN5bmMgZnVuY3Rpb24gZGlyZWN0KCkge31cblxuY29uc3QgaW5kaXJlY3QgPSBhc3luYyAoKSA9PiB7fVxuZXhwb3J0IHsgaW5kaXJlY3QgfVxuXG5jb25zdW1lKGRpcmVjdCwgaW5kaXJlY3QpXG4iXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQU8sS0FBSyxDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLENBQUM7O0FBRWhDLEtBQUssQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBQzlCLE1BQU0sQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDOztBQUVsQixPQUFPLENBQUMsTUFBTSxDQUFDLENBQUMsUUFBUTtBQUx4QjtBQUFBO0FBQUE7OyJ9) +[Source map visualization](https://evanw.github.io/source-map-visualization/#MjE1AGFzeW5jIGZ1bmN0aW9uIGRpcmVjdCgpIHt9CgpsZXQgaW5kaXJlY3QgPSBhc3luYyAoKSA9PiB7fQpleHBvcnQgeyBpbmRpcmVjdCB9Cgpjb25zdW1lKGRpcmVjdCwgaW5kaXJlY3QpCgpyZWdpc3RlclNlcnZlclJlZmVyZW5jZShkaXJlY3QsICJkaXJlY3QiKTsKZXhwb3J0IHsgZGlyZWN0IH07CgpyZWdpc3RlclNlcnZlclJlZmVyZW5jZShpbmRpcmVjdCwgImluZGlyZWN0Iik7NDE1AHsidmVyc2lvbiI6Mywic291cmNlcyI6WyIiXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGRpcmVjdCgpIHt9XG5cbmxldCBpbmRpcmVjdCA9IGFzeW5jICgpID0+IHt9XG5leHBvcnQgeyBpbmRpcmVjdCB9XG5cbmNvbnN1bWUoZGlyZWN0LCBpbmRpcmVjdClcbiJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBTyxLQUFLLENBQUMsUUFBUSxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsQ0FBQzs7QUFFaEMsR0FBRyxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFDNUIsTUFBTSxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUM7O0FBRWxCLE9BQU8sQ0FBQyxNQUFNLENBQUMsQ0FBQyxRQUFRO0FBTHhCO0FBQUE7QUFBQTs7In0=) ```js async function direct() {} -const indirect = async () => {} +let indirect = async () => {} export { indirect } consume(direct, indirect) @@ -59,14 +59,14 @@ registerServerReference(indirect, "indirect"); **References:** direct, indirect -[Source map visualization](https://evanw.github.io/source-map-visualization/#NDY4AApjb25zdCAkJG1vZHVsZV8wX2ltcGxlbWVudGF0aW9uX2RpcmVjdCA9IGFzeW5jIGZ1bmN0aW9uICQkbW9kdWxlXzBfaW1wbGVtZW50YXRpb25fZGlyZWN0KCkge307CmV4cG9ydCBjb25zdCBkaXJlY3QgPSAvKiAjX19QVVJFX18gKi8gcmVnaXN0ZXJTZXJ2ZXJSZWZlcmVuY2UoT2JqZWN0LmRlZmluZVByb3BlcnR5KCQkbW9kdWxlXzBfaW1wbGVtZW50YXRpb25fZGlyZWN0LCAibmFtZSIsIHsgdmFsdWU6ICJkaXJlY3QiIH0pLCAiZGlyZWN0Iik7Cgpjb25zdCBpbmRpcmVjdCA9IGFzeW5jICgpID0+IHt9CgoKY29uc3VtZShkaXJlY3QsIGluZGlyZWN0KQoKY29uc3QgJCRtb2R1bGVfMV9iaW5kaW5nX2luZGlyZWN0ID0gLyogI19fUFVSRV9fICovIHJlZ2lzdGVyU2VydmVyUmVmZXJlbmNlKGluZGlyZWN0LCAiaW5kaXJlY3QiKTsKZXhwb3J0IHsgJCRtb2R1bGVfMV9iaW5kaW5nX2luZGlyZWN0IGFzIGluZGlyZWN0IH07CjM0MQB7InZlcnNpb24iOjMsInNvdXJjZXMiOlsiIl0sInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBhc3luYyBmdW5jdGlvbiBkaXJlY3QoKSB7fVxuXG5jb25zdCBpbmRpcmVjdCA9IGFzeW5jICgpID0+IHt9XG5leHBvcnQgeyBpbmRpcmVjdCB9XG5cbmNvbnN1bWUoZGlyZWN0LCBpbmRpcmVjdClcbiJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBTztBQUFBLDJGQUF3QixDQUFDOzs7QUFFaEMsS0FBSyxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7OztBQUc5QixPQUFPLENBQUMsTUFBTSxDQUFDLENBQUMsUUFBUTs7OzsifQ==) +[Source map visualization](https://evanw.github.io/source-map-visualization/#NDY2AApjb25zdCAkJG1vZHVsZV8wX2ltcGxlbWVudGF0aW9uX2RpcmVjdCA9IGFzeW5jIGZ1bmN0aW9uICQkbW9kdWxlXzBfaW1wbGVtZW50YXRpb25fZGlyZWN0KCkge307CmV4cG9ydCBjb25zdCBkaXJlY3QgPSAvKiAjX19QVVJFX18gKi8gcmVnaXN0ZXJTZXJ2ZXJSZWZlcmVuY2UoT2JqZWN0LmRlZmluZVByb3BlcnR5KCQkbW9kdWxlXzBfaW1wbGVtZW50YXRpb25fZGlyZWN0LCAibmFtZSIsIHsgdmFsdWU6ICJkaXJlY3QiIH0pLCAiZGlyZWN0Iik7CgpsZXQgaW5kaXJlY3QgPSBhc3luYyAoKSA9PiB7fQoKCmNvbnN1bWUoZGlyZWN0LCBpbmRpcmVjdCkKCmNvbnN0ICQkbW9kdWxlXzFfYmluZGluZ19pbmRpcmVjdCA9IC8qICNfX1BVUkVfXyAqLyByZWdpc3RlclNlcnZlclJlZmVyZW5jZShpbmRpcmVjdCwgImluZGlyZWN0Iik7CmV4cG9ydCB7ICQkbW9kdWxlXzFfYmluZGluZ19pbmRpcmVjdCBhcyBpbmRpcmVjdCB9OwozMzkAeyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIiJdLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgYXN5bmMgZnVuY3Rpb24gZGlyZWN0KCkge31cblxubGV0IGluZGlyZWN0ID0gYXN5bmMgKCkgPT4ge31cbmV4cG9ydCB7IGluZGlyZWN0IH1cblxuY29uc3VtZShkaXJlY3QsIGluZGlyZWN0KVxuIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFPO0FBQUEsMkZBQXdCLENBQUM7OztBQUVoQyxHQUFHLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQzs7O0FBRzVCLE9BQU8sQ0FBQyxNQUFNLENBQUMsQ0FBQyxRQUFROzs7OyJ9) ```js const $$module_0_implementation_direct = async function $$module_0_implementation_direct() {}; export const direct = /* #__PURE__ */ registerServerReference(Object.defineProperty($$module_0_implementation_direct, "name", { value: "direct" }), "direct"); -const indirect = async () => {} +let indirect = async () => {} consume(direct, indirect) diff --git a/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/default-identifier.js.map.snap.md b/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/default-identifier.js.map.snap.md index 4346ba119..a83d0de31 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/default-identifier.js.map.snap.md +++ b/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/default-identifier.js.map.snap.md @@ -29,11 +29,11 @@ ```txt (0:0) "'use server'\n" --> (0:0) "'use server'\n" -(2:0) "async function defaultIdentifier() {\n" --> (2:0) "async function defaultIdentifier() {\n" -(3:0) " return 'default identifier called'\n" --> (3:0) " return 'default identifier called'\n" -(4:0) "}\n" --> (4:0) "}\n" -(6:0) "export default " --> (6:0) "const $$module_0_implementation_default = " -(6:15) "defaultIdentifier\n" --> (6:42) "defaultIdentifier\n" -[unmapped] --> (8:0) "const $$module_0_binding_default = /* #__PURE__ */ registerServerReference($$module_0_implementation_default, \"default\");\n" -[unmapped] --> (9:0) "export { $$module_0_binding_default as default };\n" +(2:0) "async function defaultIdentifier() {\n" --> (2:0) "\n" +(2:0) "async function defaultIdentifier() " --> (3:0) "const $$module_0_implementation_defaultIdentifier = async function $$module_0_implementation_defaultIdentifier() " +(2:35) "{\n" --> (3:113) "{\n" +(3:0) " return 'default identifier called'\n" --> (4:0) " return 'default identifier called'\n" +(4:0) "}\n" --> (5:0) "};\n" +[unmapped] --> (6:0) "const defaultIdentifier = /* #__PURE__ */ registerServerReference(Object.defineProperty($$module_0_implementation_defaultIdentifier, \"name\", { value: \"defaultIdentifier\" }), \"default\");\n" +(6:0) "export default defaultIdentifier\n" --> (8:0) "export default defaultIdentifier\n" ``` diff --git a/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/default-identifier.js.snap.md b/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/default-identifier.js.snap.md index b453ed846..4e34e17b5 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/default-identifier.js.snap.md +++ b/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/default-identifier.js.snap.md @@ -58,17 +58,16 @@ export default $$effect_default; **References:** default -[Source map visualization](https://evanw.github.io/source-map-visualization/#MzI0ACd1c2Ugc2VydmVyJwoKYXN5bmMgZnVuY3Rpb24gZGVmYXVsdElkZW50aWZpZXIoKSB7CiAgcmV0dXJuICdkZWZhdWx0IGlkZW50aWZpZXIgY2FsbGVkJwp9Cgpjb25zdCAkJG1vZHVsZV8wX2ltcGxlbWVudGF0aW9uX2RlZmF1bHQgPSBkZWZhdWx0SWRlbnRpZmllcgoKY29uc3QgJCRtb2R1bGVfMF9iaW5kaW5nX2RlZmF1bHQgPSAvKiAjX19QVVJFX18gKi8gcmVnaXN0ZXJTZXJ2ZXJSZWZlcmVuY2UoJCRtb2R1bGVfMF9pbXBsZW1lbnRhdGlvbl9kZWZhdWx0LCAiZGVmYXVsdCIpOwpleHBvcnQgeyAkJG1vZHVsZV8wX2JpbmRpbmdfZGVmYXVsdCBhcyBkZWZhdWx0IH07CjM1NgB7InZlcnNpb24iOjMsInNvdXJjZXMiOlsiIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc2VydmVyJ1xuXG5hc3luYyBmdW5jdGlvbiBkZWZhdWx0SWRlbnRpZmllcigpIHtcbiAgcmV0dXJuICdkZWZhdWx0IGlkZW50aWZpZXIgY2FsbGVkJ1xufVxuXG5leHBvcnQgZGVmYXVsdCBkZWZhdWx0SWRlbnRpZmllclxuIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLENBQUMsR0FBRyxDQUFDLE1BQU07O0FBRVgsS0FBSyxDQUFDLFFBQVEsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDLENBQUM7QUFDbkMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLE9BQU8sQ0FBQyxVQUFVLENBQUMsTUFBTTtBQUNuQzs7QUFFQSwwQ0FBZTs7OzsifQ==) +[Source map visualization](https://evanw.github.io/source-map-visualization/#MzkwACd1c2Ugc2VydmVyJwoKCmNvbnN0ICQkbW9kdWxlXzBfaW1wbGVtZW50YXRpb25fZGVmYXVsdElkZW50aWZpZXIgPSBhc3luYyBmdW5jdGlvbiAkJG1vZHVsZV8wX2ltcGxlbWVudGF0aW9uX2RlZmF1bHRJZGVudGlmaWVyKCkgewogIHJldHVybiAnZGVmYXVsdCBpZGVudGlmaWVyIGNhbGxlZCcKfTsKY29uc3QgZGVmYXVsdElkZW50aWZpZXIgPSAvKiAjX19QVVJFX18gKi8gcmVnaXN0ZXJTZXJ2ZXJSZWZlcmVuY2UoT2JqZWN0LmRlZmluZVByb3BlcnR5KCQkbW9kdWxlXzBfaW1wbGVtZW50YXRpb25fZGVmYXVsdElkZW50aWZpZXIsICJuYW1lIiwgeyB2YWx1ZTogImRlZmF1bHRJZGVudGlmaWVyIiB9KSwgImRlZmF1bHQiKTsKCmV4cG9ydCBkZWZhdWx0IGRlZmF1bHRJZGVudGlmaWVyCjMzOAB7InZlcnNpb24iOjMsInNvdXJjZXMiOlsiIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc2VydmVyJ1xuXG5hc3luYyBmdW5jdGlvbiBkZWZhdWx0SWRlbnRpZmllcigpIHtcbiAgcmV0dXJuICdkZWZhdWx0IGlkZW50aWZpZXIgY2FsbGVkJ1xufVxuXG5leHBvcnQgZGVmYXVsdCBkZWZhdWx0SWRlbnRpZmllclxuIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLENBQUMsR0FBRyxDQUFDLE1BQU07O0FBRVg7QUFBQSxpSEFBbUM7QUFDbkMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLE9BQU8sQ0FBQyxVQUFVLENBQUMsTUFBTTtBQUNuQzs7O0FBRUEsTUFBTSxDQUFDLE9BQU8sQ0FBQzsifQ==) ```js 'use server' -async function defaultIdentifier() { - return 'default identifier called' -} -const $$module_0_implementation_default = defaultIdentifier +const $$module_0_implementation_defaultIdentifier = async function $$module_0_implementation_defaultIdentifier() { + return 'default identifier called' +}; +const defaultIdentifier = /* #__PURE__ */ registerServerReference(Object.defineProperty($$module_0_implementation_defaultIdentifier, "name", { value: "defaultIdentifier" }), "default"); -const $$module_0_binding_default = /* #__PURE__ */ registerServerReference($$module_0_implementation_default, "default"); -export { $$module_0_binding_default as default }; +export default defaultIdentifier ``` diff --git a/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/local-alias.js.map.snap.md b/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/local-alias.js.map.snap.md index 3a5c81178..3cfffcd40 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/local-alias.js.map.snap.md +++ b/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/local-alias.js.map.snap.md @@ -25,9 +25,11 @@ ```txt (0:0) "'use server'\n" --> (0:0) "'use server'\n" -(2:0) "async function localAlias() {\n" --> (2:0) "async function localAlias() {\n" -(3:0) " return 'local alias called'\n" --> (3:0) " return 'local alias called'\n" -(4:0) "}\n" --> (4:0) "}\n" -[unmapped] --> (8:0) "const $$module_0_binding_aliasedAction = /* #__PURE__ */ registerServerReference(localAlias, \"aliasedAction\");\n" -[unmapped] --> (9:0) "export { $$module_0_binding_aliasedAction as aliasedAction };\n" +(2:0) "async function localAlias() {\n" --> (2:0) "\n" +(2:0) "async function localAlias() " --> (3:0) "const $$module_0_implementation_localAlias = async function $$module_0_implementation_localAlias() " +(2:28) "{\n" --> (3:99) "{\n" +(3:0) " return 'local alias called'\n" --> (4:0) " return 'local alias called'\n" +(4:0) "}\n" --> (5:0) "};\n" +[unmapped] --> (6:0) "const localAlias = /* #__PURE__ */ registerServerReference(Object.defineProperty($$module_0_implementation_localAlias, \"name\", { value: \"localAlias\" }), \"aliasedAction\");\n" +(6:0) "export { localAlias as aliasedAction }\n" --> (8:0) "export { localAlias as aliasedAction }\n" ``` diff --git a/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/local-alias.js.snap.md b/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/local-alias.js.snap.md index bfbf8706e..183e23b12 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/local-alias.js.snap.md +++ b/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/local-alias.js.snap.md @@ -57,17 +57,16 @@ registerServerReference(localAlias, "aliasedAction"); **References:** aliasedAction -[Source map visualization](https://evanw.github.io/source-map-visualization/#MjUyACd1c2Ugc2VydmVyJwoKYXN5bmMgZnVuY3Rpb24gbG9jYWxBbGlhcygpIHsKICByZXR1cm4gJ2xvY2FsIGFsaWFzIGNhbGxlZCcKfQoKCgpjb25zdCAkJG1vZHVsZV8wX2JpbmRpbmdfYWxpYXNlZEFjdGlvbiA9IC8qICNfX1BVUkVfXyAqLyByZWdpc3RlclNlcnZlclJlZmVyZW5jZShsb2NhbEFsaWFzLCAiYWxpYXNlZEFjdGlvbiIpOwpleHBvcnQgeyAkJG1vZHVsZV8wX2JpbmRpbmdfYWxpYXNlZEFjdGlvbiBhcyBhbGlhc2VkQWN0aW9uIH07CjMzNgB7InZlcnNpb24iOjMsInNvdXJjZXMiOlsiIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc2VydmVyJ1xuXG5hc3luYyBmdW5jdGlvbiBsb2NhbEFsaWFzKCkge1xuICByZXR1cm4gJ2xvY2FsIGFsaWFzIGNhbGxlZCdcbn1cblxuZXhwb3J0IHsgbG9jYWxBbGlhcyBhcyBhbGlhc2VkQWN0aW9uIH1cbiJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxDQUFDLEdBQUcsQ0FBQyxNQUFNOztBQUVYLEtBQUssQ0FBQyxRQUFRLENBQUMsVUFBVSxDQUFDLENBQUMsQ0FBQztBQUM1QixDQUFDLENBQUMsTUFBTSxDQUFDLENBQUMsS0FBSyxDQUFDLEtBQUssQ0FBQyxNQUFNO0FBQzVCOzs7Ozs7In0=) +[Source map visualization](https://evanw.github.io/source-map-visualization/#MzYwACd1c2Ugc2VydmVyJwoKCmNvbnN0ICQkbW9kdWxlXzBfaW1wbGVtZW50YXRpb25fbG9jYWxBbGlhcyA9IGFzeW5jIGZ1bmN0aW9uICQkbW9kdWxlXzBfaW1wbGVtZW50YXRpb25fbG9jYWxBbGlhcygpIHsKICByZXR1cm4gJ2xvY2FsIGFsaWFzIGNhbGxlZCcKfTsKY29uc3QgbG9jYWxBbGlhcyA9IC8qICNfX1BVUkVfXyAqLyByZWdpc3RlclNlcnZlclJlZmVyZW5jZShPYmplY3QuZGVmaW5lUHJvcGVydHkoJCRtb2R1bGVfMF9pbXBsZW1lbnRhdGlvbl9sb2NhbEFsaWFzLCAibmFtZSIsIHsgdmFsdWU6ICJsb2NhbEFsaWFzIiB9KSwgImFsaWFzZWRBY3Rpb24iKTsKCmV4cG9ydCB7IGxvY2FsQWxpYXMgYXMgYWxpYXNlZEFjdGlvbiB9CjM2MAB7InZlcnNpb24iOjMsInNvdXJjZXMiOlsiIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc2VydmVyJ1xuXG5hc3luYyBmdW5jdGlvbiBsb2NhbEFsaWFzKCkge1xuICByZXR1cm4gJ2xvY2FsIGFsaWFzIGNhbGxlZCdcbn1cblxuZXhwb3J0IHsgbG9jYWxBbGlhcyBhcyBhbGlhc2VkQWN0aW9uIH1cbiJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxDQUFDLEdBQUcsQ0FBQyxNQUFNOztBQUVYO0FBQUEsbUdBQTRCO0FBQzVCLENBQUMsQ0FBQyxNQUFNLENBQUMsQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLE1BQU07QUFDNUI7OztBQUVBLE1BQU0sQ0FBQyxDQUFDLENBQUMsVUFBVSxDQUFDLEVBQUUsQ0FBQyxhQUFhLENBQUM7In0=) ```js 'use server' -async function localAlias() { - return 'local alias called' -} - +const $$module_0_implementation_localAlias = async function $$module_0_implementation_localAlias() { + return 'local alias called' +}; +const localAlias = /* #__PURE__ */ registerServerReference(Object.defineProperty($$module_0_implementation_localAlias, "name", { value: "localAlias" }), "aliasedAction"); -const $$module_0_binding_aliasedAction = /* #__PURE__ */ registerServerReference(localAlias, "aliasedAction"); -export { $$module_0_binding_aliasedAction as aliasedAction }; +export { localAlias as aliasedAction } ``` diff --git a/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/local-export-before-declaration.js.map.snap.md b/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/local-export-before-declaration.js.map.snap.md index 553b224b4..d7739371a 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/local-export-before-declaration.js.map.snap.md +++ b/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/local-export-before-declaration.js.map.snap.md @@ -21,7 +21,9 @@ ```txt (0:0) "'use server'\n" --> (0:0) "'use server'\n" -(3:0) "const action = async () => 'action called'\n" --> (3:0) "const action = async () => 'action called'\n" -[unmapped] --> (5:0) "const $$module_0_binding_action = /* #__PURE__ */ registerServerReference(action, \"action\");\n" -[unmapped] --> (6:0) "export { $$module_0_binding_action as action };\n" +(3:15) "async () => 'action called'\n" --> (2:0) "\n" +(3:15) "async () => " --> (3:0) "const $$module_0_implementation_action = async () => " +(3:27) "'action called'\n" --> (3:53) "'action called';\n" +(2:0) "export { action }\n" --> (4:0) "export { action }\n" +(3:0) "const action = async () => 'action called'\n" --> (5:0) "const action = /* #__PURE__ */ registerServerReference(Object.defineProperty($$module_0_implementation_action, \"name\", { value: \"action\" }), \"action\")\n" ``` diff --git a/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/local-export-before-declaration.js.snap.md b/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/local-export-before-declaration.js.snap.md index 4dd9d56de..71ef77b59 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/local-export-before-declaration.js.snap.md +++ b/packages/plugin-rsc/src/transforms/fixtures/source-map/wrap-export/local-export-before-declaration.js.snap.md @@ -48,14 +48,13 @@ registerServerReference(action, "action"); **References:** action -[Source map visualization](https://evanw.github.io/source-map-visualization/#MjAwACd1c2Ugc2VydmVyJwoKCmNvbnN0IGFjdGlvbiA9IGFzeW5jICgpID0+ICdhY3Rpb24gY2FsbGVkJwoKY29uc3QgJCRtb2R1bGVfMF9iaW5kaW5nX2FjdGlvbiA9IC8qICNfX1BVUkVfXyAqLyByZWdpc3RlclNlcnZlclJlZmVyZW5jZShhY3Rpb24sICJhY3Rpb24iKTsKZXhwb3J0IHsgJCRtb2R1bGVfMF9iaW5kaW5nX2FjdGlvbiBhcyBhY3Rpb24gfTsKMjc5AHsidmVyc2lvbiI6Mywic291cmNlcyI6WyIiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzZXJ2ZXInXG5cbmV4cG9ydCB7IGFjdGlvbiB9XG5jb25zdCBhY3Rpb24gPSBhc3luYyAoKSA9PiAnYWN0aW9uIGNhbGxlZCdcbiJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxDQUFDLEdBQUcsQ0FBQyxNQUFNOzs7QUFHWCxLQUFLLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsTUFBTTs7OzsifQ==) +[Source map visualization](https://evanw.github.io/source-map-visualization/#MjU0ACd1c2Ugc2VydmVyJwoKCmNvbnN0ICQkbW9kdWxlXzBfaW1wbGVtZW50YXRpb25fYWN0aW9uID0gYXN5bmMgKCkgPT4gJ2FjdGlvbiBjYWxsZWQnOwpleHBvcnQgeyBhY3Rpb24gfQpjb25zdCBhY3Rpb24gPSAvKiAjX19QVVJFX18gKi8gcmVnaXN0ZXJTZXJ2ZXJSZWZlcmVuY2UoT2JqZWN0LmRlZmluZVByb3BlcnR5KCQkbW9kdWxlXzBfaW1wbGVtZW50YXRpb25fYWN0aW9uLCAibmFtZSIsIHsgdmFsdWU6ICJhY3Rpb24iIH0pLCAiYWN0aW9uIikKMjgzAHsidmVyc2lvbiI6Mywic291cmNlcyI6WyIiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzZXJ2ZXInXG5cbmV4cG9ydCB7IGFjdGlvbiB9XG5jb25zdCBhY3Rpb24gPSBhc3luYyAoKSA9PiAnYWN0aW9uIGNhbGxlZCdcbiJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxDQUFDLEdBQUcsQ0FBQyxNQUFNOztBQUdJO0FBQUEscURBQVksQ0FBQyxNQUFNLENBQUMsTUFBTTtBQUR6QyxNQUFNLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQztBQUNoQixLQUFLLENBQUMsTUFBTSxDQUFDLENBQUM7In0=) ```js 'use server' -const action = async () => 'action called' - -const $$module_0_binding_action = /* #__PURE__ */ registerServerReference(action, "action"); -export { $$module_0_binding_action as action }; +const $$module_0_implementation_action = async () => 'action called'; +export { action } +const action = /* #__PURE__ */ registerServerReference(Object.defineProperty($$module_0_implementation_action, "name", { value: "action" }), "action") ``` diff --git a/packages/plugin-rsc/src/transforms/module-export-effect.test.ts b/packages/plugin-rsc/src/transforms/module-export-effect.test.ts index a253a664c..f4213ae63 100644 --- a/packages/plugin-rsc/src/transforms/module-export-effect.test.ts +++ b/packages/plugin-rsc/src/transforms/module-export-effect.test.ts @@ -225,9 +225,18 @@ export default async function Page() {} [`export default 1`, [{ isFunction: false }]], [ `const Page = () => {}; export default Page`, - [{ defaultExportIdentifierName: 'Page' }], + [ + { + localName: 'Page', + isFunction: true, + defaultExportIdentifierName: 'Page', + }, + ], + ], + [ + `const id = async () => {}; export { id }`, + [{ localName: 'id', isFunction: true }], ], - [`const id = async () => {}; export { id }`, [{}]], [`export { id } from './dep'`, [{}]], ] diff --git a/packages/plugin-rsc/src/transforms/module-export-scan.test.ts b/packages/plugin-rsc/src/transforms/module-export-scan.test.ts index c1891e4e5..2390fe6ef 100644 --- a/packages/plugin-rsc/src/transforms/module-export-scan.test.ts +++ b/packages/plugin-rsc/src/transforms/module-export-scan.test.ts @@ -152,6 +152,52 @@ test('records explicit function expression names', async () => { ]) }) +test('resolves stable local function exports', async () => { + const ast = await parseAstAsync(` +const named = async () => {} +function defaultValue() {} +let mutable = async () => {} +export { named as action, mutable } +export default defaultValue +`) + + expect(scanModuleExports(ast)).toMatchObject([ + { + type: 'specifiers', + exports: [ + { + localName: 'named', + exportName: 'action', + directFunction: { + node: { type: 'ArrowFunctionExpression' }, + originalName: 'named', + }, + meta: { localName: 'named', isFunction: true }, + }, + { + localName: 'mutable', + exportName: 'mutable', + directFunction: undefined, + meta: { localName: 'mutable', isFunction: true }, + }, + ], + }, + { + type: 'default', + kind: 'identifier', + directFunction: { + node: { type: 'FunctionDeclaration' }, + originalName: 'defaultValue', + }, + meta: { + localName: 'defaultValue', + isFunction: true, + defaultExportIdentifierName: 'defaultValue', + }, + }, + ]) +}) + test('flags string literal export names as unsupported', async () => { const ast = await parseAstAsync(` export { local as "public name" } diff --git a/packages/plugin-rsc/src/transforms/module-export-scan.ts b/packages/plugin-rsc/src/transforms/module-export-scan.ts index 0a55b2242..d3116e673 100644 --- a/packages/plugin-rsc/src/transforms/module-export-scan.ts +++ b/packages/plugin-rsc/src/transforms/module-export-scan.ts @@ -36,7 +36,8 @@ export type ModuleExportMeta = { * - `"Page"` for `export function Page() {}` * - `"Page"` for `export const Page = () => {}` * - `undefined` for `export default () => {}` - * - `undefined` for `export { Page }` + * - `"Page"` for `const Page = () => {}; export { Page }` + * - `undefined` for `export { imported }` */ localName?: string /** @@ -45,7 +46,8 @@ export type ModuleExportMeta = { * - `true` for `export const Page = () => {}` * - `false` for `export const value = 1` * - `undefined` for `export const value = getValue()` - * - `undefined` for `export default Page` + * - `true` for `const Page = () => {}; export default Page` + * - `undefined` for `export default imported` */ isFunction?: boolean /** @@ -68,6 +70,7 @@ export type ModuleExportSpecifier = { node: ExportSpecifier localName: string exportName: string + directFunction?: ModuleExportDirectFunction meta: ModuleExportMeta } @@ -138,6 +141,7 @@ export function scanModuleExports( ): ModuleExportGroup[] { const ast = viteAst as unknown as Program const groups: ModuleExportGroup[] = [] + const localBindings = scanLocalBindings(ast, countLocalExports(ast)) for (const node of ast.body) { if (node.type === 'ExportNamedDeclaration') { @@ -206,17 +210,22 @@ export function scanModuleExports( exports: node.specifiers.map((specifier) => { // String-literal export names are unsupported. Callers must check // the returned node's local and exported types before rewriting. + const localName = + specifier.local.type === 'Identifier' + ? specifier.local.name + : '__unsupported_string_export__' + const localBinding = node.source + ? undefined + : localBindings.get(localName) return { node: specifier, - localName: - specifier.local.type === 'Identifier' - ? specifier.local.name - : '__unsupported_string_export__', + localName, exportName: specifier.exported.type === 'Identifier' ? specifier.exported.name : '__unsupported_string_export__', - meta: {}, + directFunction: localBinding?.directFunction, + meta: localBinding?.meta ?? {}, } }), }) @@ -231,7 +240,7 @@ export function scanModuleExports( let kind: ModuleExportDefaultKind let localName: string | undefined let meta: ModuleExportMeta - const directFunction = getDirectFunction(node.declaration, 'default') + let directFunction = getDirectFunction(node.declaration, 'default') if ( (node.declaration.type === 'FunctionDeclaration' || node.declaration.type === 'ClassDeclaration') && @@ -245,7 +254,12 @@ export function scanModuleExports( } } else if (node.declaration.type === 'Identifier') { kind = 'identifier' - meta = { defaultExportIdentifierName: node.declaration.name } + const localBinding = localBindings.get(node.declaration.name) + directFunction = localBinding?.directFunction + meta = { + ...localBinding?.meta, + defaultExportIdentifierName: node.declaration.name, + } } else { // export default function () {} // export default () => {} @@ -266,6 +280,76 @@ export function scanModuleExports( return groups } +type ModuleLocalBinding = { + directFunction?: ModuleExportDirectFunction + meta: ModuleExportMeta +} + +function countLocalExports(ast: Program): Map { + const counts = new Map() + const add = (name: string) => counts.set(name, (counts.get(name) ?? 0) + 1) + for (const node of ast.body) { + if (node.type === 'ExportNamedDeclaration' && !node.source) { + for (const specifier of node.specifiers) { + if (specifier.local.type === 'Identifier') add(specifier.local.name) + } + } else if ( + node.type === 'ExportDefaultDeclaration' && + node.declaration.type === 'Identifier' + ) { + add(node.declaration.name) + } + } + return counts +} + +function scanLocalBindings( + ast: Program, + exportCounts: Map, +): Map { + const bindings = new Map() + for (const node of ast.body) { + if ( + (node.type === 'FunctionDeclaration' || + node.type === 'ClassDeclaration') && + node.id + ) { + bindings.set(node.id.name, { + directFunction: + node.type === 'FunctionDeclaration' && + exportCounts.get(node.id.name) === 1 + ? getDirectFunction(node, node.id.name) + : undefined, + meta: { + localName: node.id.name, + isFunction: getIsFunction(node), + }, + }) + } else if (node.type === 'VariableDeclaration') { + for (const declarator of node.declarations) { + const directFunction = + node.kind === 'const' && + declarator.id.type === 'Identifier' && + exportCounts.get(declarator.id.name) === 1 && + declarator.init + ? getDirectFunction(declarator.init, declarator.id.name) + : undefined + const isFunction = + declarator.id.type === 'Identifier' && declarator.init + ? getIsFunction(declarator.init) + : undefined + for (const name of extractNames(declarator.id)) { + bindings.set(name, { + directFunction, + meta: { localName: name, isFunction }, + }) + } + } + } + } + return bindings +} + function getIsFunction( node: Node | ExportDefaultDeclaration['declaration'], ): boolean | undefined { diff --git a/packages/plugin-rsc/src/transforms/module-export-wrap.test.ts b/packages/plugin-rsc/src/transforms/module-export-wrap.test.ts index 2d021a919..817ba8592 100644 --- a/packages/plugin-rsc/src/transforms/module-export-wrap.test.ts +++ b/packages/plugin-rsc/src/transforms/module-export-wrap.test.ts @@ -59,8 +59,8 @@ export default async function Page() {} exportName: 'loader', }, { - implementation: 'local', - originalName: undefined, + implementation: '$$module_2_implementation_local', + originalName: 'local', exportName: 'renamed', }, { @@ -125,6 +125,64 @@ export default async function Page() {} expect(filtered.references).toEqual([]) }) + test('uses canonical bindings for stable local function exports', async () => { + const named = await transform(`\ +const action = async () => {} +export { action as renamed } +`) + expect(named.output.toString()).toMatchInlineSnapshot(` + " + const $$module_0_implementation_action = async () => {}; + const action = /* #__PURE__ */ wrap(Object.defineProperty($$module_0_implementation_action, \"name\", { value: \"action\" }), \"renamed\") + export { action as renamed } + " + `) + + const defaultExport = await transform(`\ +async function action() {} +export default action +`) + expect(defaultExport.output.toString()).toMatchInlineSnapshot(` + " + const $$module_0_implementation_action = async function $$module_0_implementation_action() {}; + const action = /* #__PURE__ */ wrap(Object.defineProperty($$module_0_implementation_action, \"name\", { value: \"action\" }), \"default\"); + export default action + " + `) + }) + + test('keeps mutable and multiply exported locals on wrapped bindings', async () => { + const mutable = await transform(`\ +let action = async () => {} +export { action } +action = async () => {} +`) + expect(mutable.references.map(formatContext)).toEqual([ + { + implementation: 'action', + originalName: undefined, + exportName: 'action', + }, + ]) + + const multiple = await transform(`\ +const action = async () => {} +export { action, action as renamed } +`) + expect(multiple.references.map(formatContext)).toEqual([ + { + implementation: 'action', + originalName: undefined, + exportName: 'action', + }, + { + implementation: 'action', + originalName: undefined, + exportName: 'renamed', + }, + ]) + }) + test('controls export-all preservation', async () => { const input = `export * from './dep'` diff --git a/packages/plugin-rsc/src/transforms/module-export-wrap.ts b/packages/plugin-rsc/src/transforms/module-export-wrap.ts index 80cfada74..1526f09a6 100644 --- a/packages/plugin-rsc/src/transforms/module-export-wrap.ts +++ b/packages/plugin-rsc/src/transforms/module-export-wrap.ts @@ -19,7 +19,7 @@ export type TransformModuleExportWrapContext = { * - `"action"` for `export const action = () => {}` * - `"implementation"` for `export const action = function implementation() {}` * - `"default"` for `export default () => {}` - * - `undefined` for an indirect export such as `export { action }` + * - `undefined` for an unresolved or mutable export such as `export { imported }` */ originalName?: string /** The public export name, or `"default"` for a default export. */ @@ -90,10 +90,11 @@ export type TransformModuleExportWrapResult = { * `references` returns those contexts in wrapper creation order, while * `referenceNames` returns only their export names. * - * A direct function export makes its source name the canonical wrapper binding, - * so local and imported reads of `someFn` receive the same value. Other exports - * retain their source binding and receive a separate wrapper at the export - * boundary, so the local `someValue` above remains the unwrapped value. + * A direct function export, including a stable local function exported through + * a specifier or default identifier, makes its source name the canonical wrapper + * binding. Other exports retain their source binding and receive a separate + * wrapper at the export boundary, so the local `someValue` above remains the + * unwrapped value. * * Generated `$$module_*` names are not deconflicted from user bindings, * consistent with the other transform helpers. @@ -115,6 +116,7 @@ export function transformModuleExportWrap( const filter = options.filter ?? (() => true) const references: TransformModuleExportWrapContext[] = [] const wrappedBindingCode: string[] = [] + const hoistedLocalFunctions = new Set() const hoistPosition = getDirectivePrologueEnd(ast) function createContext( @@ -237,7 +239,9 @@ export function transformModuleExportWrap( `\nconst ${implementation} = ${originalPrefix}`, ) output.appendLeft(node.end, ';\n') - output.move(node.start, node.end, hoistPosition) + if (node.start !== hoistPosition) { + output.move(node.start, node.end, hoistPosition) + } const context = createContext( implementation, @@ -248,6 +252,35 @@ export function transformModuleExportWrap( return generate(context) } + function hoistLocalFunction( + directFunction: ModuleExportDirectFunction, + sourceName: string, + exportName: string, + meta: ModuleExportMeta, + ): void { + if (hoistedLocalFunctions.has(directFunction.node.start)) { + throw Object.assign( + new Error('unsupported multiple exports for local function'), + { pos: directFunction.node.start }, + ) + } + hoistedLocalFunctions.add(directFunction.node.start) + const replacement = hoistFunction( + directFunction, + sourceName, + exportName, + meta, + ) + output.appendLeft( + directFunction.node.start === hoistPosition + ? directFunction.node.end + : directFunction.node.start, + directFunction.node.type === 'FunctionDeclaration' + ? `const ${sourceName} = ${replacement};` + : replacement, + ) + } + for (const group of scanModuleExports(viteAst)) { if (group.type === 'declaration') { const [entry] = group.exports @@ -352,7 +385,7 @@ export function transformModuleExportWrap( // export { $$module_0_binding_action as action } // export { skipped } const preserved: string[] = [] - let selected = false + let rewrite = false for (const entry of group.exports) { tinyassert(entry.node.local.type === 'Identifier') if (entry.node.exported.type !== 'Identifier') { @@ -362,15 +395,20 @@ export function transformModuleExportWrap( ) } const { localName, exportName, meta } = entry + const specifier = + localName === exportName ? localName : `${localName} as ${exportName}` if (!filter(exportName, meta)) { - preserved.push( - localName === exportName - ? localName - : `${localName} as ${exportName}`, - ) + preserved.push(specifier) continue } - selected = true + + if (entry.directFunction) { + hoistLocalFunction(entry.directFunction, localName, exportName, meta) + preserved.push(specifier) + continue + } + + rewrite = true let implementation = localName if (group.node.source) { @@ -390,7 +428,7 @@ export function transformModuleExportWrap( emitWrappedBinding(implementation, exportName, meta) } - if (selected) { + if (rewrite) { output.remove(group.node.start, group.node.end) if (preserved.length > 0) { const source = group.node.source @@ -411,7 +449,16 @@ export function transformModuleExportWrap( } else if (group.type === 'default') { let meta: ModuleExportMeta - if (group.directFunction) { + if (group.kind === 'identifier' && group.directFunction) { + meta = group.meta + if (!filter('default', meta)) continue + hoistLocalFunction( + group.directFunction, + group.directFunction.originalName, + 'default', + meta, + ) + } else if (group.directFunction) { const declaration = group.directFunction.node // export default async function Page() {} // ⬇️ diff --git a/packages/plugin-rsc/src/transforms/wrap-export.test.ts b/packages/plugin-rsc/src/transforms/wrap-export.test.ts index 69a726c0d..d703038df 100644 --- a/packages/plugin-rsc/src/transforms/wrap-export.test.ts +++ b/packages/plugin-rsc/src/transforms/wrap-export.test.ts @@ -452,11 +452,16 @@ export default cached; `const Page = () => {}; export default Page`, [ { + isFunction: true, + declName: 'Page', defaultExportIdentifierName: 'Page', }, ], ], - [`const id = async () => {}; export { id }`, [{}]], + [ + `const id = async () => {}; export { id }`, + [{ isFunction: true, declName: 'id' }], + ], [`export { id } from './dep'`, [{}]], ]