From 87fdd9c00060bc63355c6baea7e07bfb08a6f15f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Feb 2026 03:08:14 +0000 Subject: [PATCH 001/324] test: add modernjs module federation RSC fixture --- package.json | 1 + pnpm-lock.yaml | 405 ++++++++++++++---- tests/integration/rsc-mf/host/.browserslistrc | 4 + .../integration/rsc-mf/host/modern.config.ts | 28 ++ .../rsc-mf/host/module-federation.config.ts | 76 ++++ tests/integration/rsc-mf/host/package.json | 31 ++ .../rsc-mf/host/src/App.module.less | 5 + tests/integration/rsc-mf/host/src/App.tsx | 22 + .../rsc-mf/host/src/modern-app-env.d.ts | 3 + tests/integration/rsc-mf/host/tsconfig.json | 12 + tests/integration/rsc-mf/package.json | 5 + .../integration/rsc-mf/remote/.browserslistrc | 4 + .../rsc-mf/remote/modern.config.ts | 31 ++ .../rsc-mf/remote/module-federation.config.ts | 83 ++++ tests/integration/rsc-mf/remote/package.json | 31 ++ tests/integration/rsc-mf/remote/src/App.tsx | 13 + .../src/components/RemoteClientCounter.css | 5 + .../src/components/RemoteClientCounter.tsx | 54 +++ .../src/components/RemoteNestedMixed.tsx | 9 + .../src/components/RemoteServerCard.tsx | 12 + .../rsc-mf/remote/src/components/actions.ts | 16 + .../remote/src/components/nestedActions.ts | 5 + .../remote/src/components/serverOnly.ts | 8 + .../rsc-mf/remote/src/modern-app-env.d.ts | 3 + tests/integration/rsc-mf/remote/tsconfig.json | 12 + tests/integration/rsc-mf/tests/index.test.ts | 224 ++++++++++ tests/integration/rsc-mf/tests/tsconfig.json | 12 + 27 files changed, 1025 insertions(+), 89 deletions(-) create mode 100644 tests/integration/rsc-mf/host/.browserslistrc create mode 100644 tests/integration/rsc-mf/host/modern.config.ts create mode 100644 tests/integration/rsc-mf/host/module-federation.config.ts create mode 100644 tests/integration/rsc-mf/host/package.json create mode 100644 tests/integration/rsc-mf/host/src/App.module.less create mode 100644 tests/integration/rsc-mf/host/src/App.tsx create mode 100644 tests/integration/rsc-mf/host/src/modern-app-env.d.ts create mode 100644 tests/integration/rsc-mf/host/tsconfig.json create mode 100644 tests/integration/rsc-mf/package.json create mode 100644 tests/integration/rsc-mf/remote/.browserslistrc create mode 100644 tests/integration/rsc-mf/remote/modern.config.ts create mode 100644 tests/integration/rsc-mf/remote/module-federation.config.ts create mode 100644 tests/integration/rsc-mf/remote/package.json create mode 100644 tests/integration/rsc-mf/remote/src/App.tsx create mode 100644 tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.css create mode 100644 tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.tsx create mode 100644 tests/integration/rsc-mf/remote/src/components/RemoteNestedMixed.tsx create mode 100644 tests/integration/rsc-mf/remote/src/components/RemoteServerCard.tsx create mode 100644 tests/integration/rsc-mf/remote/src/components/actions.ts create mode 100644 tests/integration/rsc-mf/remote/src/components/nestedActions.ts create mode 100644 tests/integration/rsc-mf/remote/src/components/serverOnly.ts create mode 100644 tests/integration/rsc-mf/remote/src/modern-app-env.d.ts create mode 100644 tests/integration/rsc-mf/remote/tsconfig.json create mode 100644 tests/integration/rsc-mf/tests/index.test.ts create mode 100644 tests/integration/rsc-mf/tests/tsconfig.json diff --git a/package.json b/package.json index 06019132ada4..e37bdf6c3590 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "read-yaml-file>js-yaml": "3.14.2", "@remix-run/router": ">=1.23.2", "h3": ">=1.15.5", + "@rspack/core@2.0.0-beta.2": "npm:@rspack-canary/core@2.0.0-canary-032bd1ff-20260212021235", "tar": ">=7.5.4", "diff": ">=4.0.4", "debug": ">=4.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index baf329f1debd..cbaeb4fa9755 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,7 @@ overrides: read-yaml-file>js-yaml: 3.14.2 '@remix-run/router': '>=1.23.2' h3: '>=1.15.5' + '@rspack/core@2.0.0-beta.2': npm:@rspack-canary/core@2.0.0-canary-032bd1ff-20260212021235 tar: '>=7.5.4' diff: '>=4.0.4' debug: '>=4.3.1' @@ -99,7 +100,7 @@ importers: version: link:../../packages/tsconfig '@rsdoctor/rspack-plugin': specifier: ^1.5.2 - version: 1.5.2(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0))(@rspack/core@1.7.5(@swc/helpers@0.5.18))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + version: 1.5.2(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(@rspack/core@1.7.5(@swc/helpers@0.5.18))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) '@types/node': specifier: ^20 version: 20.19.27 @@ -126,40 +127,40 @@ importers: version: link:../../toolkit/utils '@rsbuild/core': specifier: 2.0.0-beta.4 - version: 2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) + version: 2.0.0-beta.4(core-js@3.48.0) '@rsbuild/plugin-assets-retry': specifier: 1.5.1 - version: 1.5.1(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0)) + version: 1.5.1(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) '@rsbuild/plugin-check-syntax': specifier: 1.6.1 - version: 1.6.1(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0)) + version: 1.6.1(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) '@rsbuild/plugin-css-minimizer': specifier: 1.1.1 - version: 1.1.1(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0))(esbuild@0.27.2)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17))(esbuild@0.27.2)) + version: 1.1.1(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(esbuild@0.27.2)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17))(esbuild@0.27.2)) '@rsbuild/plugin-less': specifier: 1.6.0 - version: 1.6.0(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0)) + version: 1.6.0(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) '@rsbuild/plugin-react': specifier: 1.4.4 - version: 1.4.4(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0)) + version: 1.4.4(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) '@rsbuild/plugin-rem': specifier: 1.0.5 - version: 1.0.5(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0)) + version: 1.0.5(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) '@rsbuild/plugin-sass': specifier: 1.5.0 - version: 1.5.0(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0)) + version: 1.5.0(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) '@rsbuild/plugin-source-build': specifier: 1.0.4 - version: 1.0.4(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0)) + version: 1.0.4(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) '@rsbuild/plugin-svgr': specifier: 1.3.0 - version: 1.3.0(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0))(typescript@5.9.3) + version: 1.3.0(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(typescript@5.9.3) '@rsbuild/plugin-type-check': specifier: 1.3.3 - version: 1.3.3(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0))(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.17))(typescript@5.9.3) + version: 1.3.3(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17))(typescript@5.9.3) '@rsbuild/plugin-typed-css-modules': specifier: 1.2.1 - version: 1.2.1(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0)) + version: 1.2.1(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) '@swc/core': specifier: 1.15.11 version: 1.15.11(@swc/helpers@0.5.17) @@ -210,10 +211,10 @@ importers: version: 3.0.4(postcss@8.5.6) rsbuild-plugin-rsc: specifier: 0.0.1-beta.0 - version: 0.0.1-beta.0(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0))(react-server-dom-rspack@0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + version: 0.0.1-beta.0(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(react-server-dom-rspack@0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) rspack-manifest-plugin: specifier: 5.2.1 - version: 5.2.1(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.17)) + version: 5.2.1(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17)) ts-deepmerge: specifier: 7.0.3 version: 7.0.3 @@ -296,7 +297,7 @@ importers: version: link:../../toolkit/types '@rsbuild/core': specifier: 2.0.0-beta.4 - version: 2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) + version: 2.0.0-beta.4(core-js@3.48.0) '@rslib/core': specifier: 0.19.5 version: 0.19.5(typescript@5.9.3) @@ -348,7 +349,7 @@ importers: version: link:../../toolkit/types '@rsbuild/core': specifier: 2.0.0-beta.4 - version: 2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) + version: 2.0.0-beta.4(core-js@3.48.0) '@rslib/core': specifier: 0.19.5 version: 0.19.5(typescript@5.9.3) @@ -430,7 +431,7 @@ importers: dependencies: '@rsbuild/plugin-styled-components': specifier: 1.6.1 - version: 1.6.1(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0)) + version: 1.6.1(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) '@swc/helpers': specifier: ^0.5.17 version: 0.5.17 @@ -474,16 +475,16 @@ importers: devDependencies: '@rsbuild/plugin-sass': specifier: 1.5.0 - version: 1.5.0(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0)) + version: 1.5.0(@rsbuild/core@2.0.0-beta.1(core-js@3.48.0)) '@rspress/core': specifier: 2.0.2 - version: 2.0.2(@module-federation/runtime-tools@2.0.0)(@types/react@19.2.13)(core-js@3.48.0) + version: 2.0.2(@types/react@19.2.13)(core-js@3.48.0) '@rspress/plugin-llms': specifier: 2.0.2 - version: 2.0.2(@rspress/core@2.0.2(@module-federation/runtime-tools@2.0.0)(@types/react@19.2.13)(core-js@3.48.0)) + version: 2.0.2(@rspress/core@2.0.2(@types/react@19.2.13)(core-js@3.48.0)) '@rspress/shared': specifier: 2.0.2 - version: 2.0.2(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) + version: 2.0.2(core-js@3.48.0) '@shikijs/transformers': specifier: ^3.21.0 version: 3.21.0 @@ -705,7 +706,7 @@ importers: version: 4.4.2 '@rsbuild/core': specifier: 2.0.0-beta.4 - version: 2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) + version: 2.0.0-beta.4(core-js@3.48.0) '@rslib/core': specifier: 0.19.5 version: 0.19.5(typescript@5.9.3) @@ -784,7 +785,7 @@ importers: version: 19.2.4(react@19.2.4) react-server-dom-rspack: specifier: 0.0.1-beta.0 - version: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) typescript: specifier: ^5 version: 5.9.3 @@ -1249,7 +1250,7 @@ importers: version: link:../../toolkit/utils '@rsbuild/core': specifier: 2.0.0-beta.4 - version: 2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) + version: 2.0.0-beta.4(core-js@3.48.0) '@swc/helpers': specifier: ^0.5.17 version: 0.5.17 @@ -1362,7 +1363,7 @@ importers: version: link:../utils '@rsbuild/core': specifier: 2.0.0-beta.4 - version: 2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) + version: 2.0.0-beta.4(core-js@3.48.0) '@swc/helpers': specifier: ^0.5.17 version: 0.5.17 @@ -1895,7 +1896,7 @@ importers: devDependencies: '@rsbuild/plugin-react': specifier: 1.4.4 - version: 1.4.4(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0)) + version: 1.4.4(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) '@rslib/core': specifier: 0.19.5 version: 0.19.5(typescript@5.9.3) @@ -2019,7 +2020,7 @@ importers: version: 1.58.2 '@rsbuild/core': specifier: 2.0.0-beta.4 - version: 2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) + version: 2.0.0-beta.4(core-js@3.48.0) '@types/connect': specifier: ^3.4.38 version: 3.4.38 @@ -3189,7 +3190,7 @@ importers: version: link:../../../packages/solutions/app-tools '@rsdoctor/rspack-plugin': specifier: ^1.5.2 - version: 1.5.2(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0))(@rspack/core@1.7.5(@swc/helpers@0.5.18))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + version: 1.5.2(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(@rspack/core@1.7.5(@swc/helpers@0.5.18))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) '@types/jest': specifier: ^29.5.14 version: 29.5.14 @@ -3547,6 +3548,118 @@ importers: specifier: ^5 version: 5.9.3 + tests/integration/rsc-mf: {} + + tests/integration/rsc-mf/host: + dependencies: + '@modern-js/render': + specifier: workspace:* + version: link:../../../../packages/runtime/render + '@modern-js/runtime': + specifier: workspace:* + version: link:../../../../packages/runtime/plugin-runtime + '@module-federation/modern-js-v3': + specifier: 2.0.0 + version: 2.0.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + '@module-federation/runtime': + specifier: 2.0.0 + version: 2.0.0 + client-only: + specifier: ^0.0.1 + version: 0.0.1 + react: + specifier: ^19.2.4 + version: 19.2.4 + react-dom: + specifier: ^19.2.4 + version: 19.2.4(react@19.2.4) + react-server-dom-rspack: + specifier: 0.0.1-beta.0 + version: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + server-only: + specifier: ^0.0.1 + version: 0.0.1 + devDependencies: + '@modern-js/app-tools': + specifier: workspace:* + version: link:../../../../packages/solutions/app-tools + '@modern-js/builder': + specifier: workspace:* + version: link:../../../../packages/cli/builder + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 + '@types/node': + specifier: ^20 + version: 20.19.27 + '@types/react': + specifier: ^19.2.13 + version: 19.2.13 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.13) + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + typescript: + specifier: ^5 + version: 5.9.3 + + tests/integration/rsc-mf/remote: + dependencies: + '@modern-js/render': + specifier: workspace:* + version: link:../../../../packages/runtime/render + '@modern-js/runtime': + specifier: workspace:* + version: link:../../../../packages/runtime/plugin-runtime + '@module-federation/modern-js-v3': + specifier: 2.0.0 + version: 2.0.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + '@module-federation/runtime': + specifier: 2.0.0 + version: 2.0.0 + client-only: + specifier: ^0.0.1 + version: 0.0.1 + react: + specifier: ^19.2.4 + version: 19.2.4 + react-dom: + specifier: ^19.2.4 + version: 19.2.4(react@19.2.4) + react-server-dom-rspack: + specifier: 0.0.1-beta.0 + version: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + server-only: + specifier: ^0.0.1 + version: 0.0.1 + devDependencies: + '@modern-js/app-tools': + specifier: workspace:* + version: link:../../../../packages/solutions/app-tools + '@modern-js/builder': + specifier: workspace:* + version: link:../../../../packages/cli/builder + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 + '@types/node': + specifier: ^20 + version: 20.19.27 + '@types/react': + specifier: ^19.2.13 + version: 19.2.13 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.13) + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + typescript: + specifier: ^5 + version: 5.9.3 + tests/integration/rsc-ssr-app: dependencies: '@modern-js/render': @@ -4431,7 +4544,7 @@ importers: version: link:../../../../../packages/runtime/plugin-runtime '@rsbuild/plugin-babel': specifier: 1.1.0 - version: 1.1.0(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0)) + version: 1.1.0(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) react: specifier: ^19.2.4 version: 19.2.4 @@ -6688,6 +6801,70 @@ packages: typescript: optional: true + '@rspack-canary/binding-darwin-arm64@2.0.0-canary-032bd1ff-20260212021235': + resolution: {integrity: sha512-uA56E4Uk5vfkPaxfjo/q4ecG9wRlf4tgHm6qtvkhiBC6ufzaJBv97e9L0TjIfwFMb1i2aU6OkRNSV0t9aZZlcQ==} + cpu: [arm64] + os: [darwin] + + '@rspack-canary/binding-darwin-x64@2.0.0-canary-032bd1ff-20260212021235': + resolution: {integrity: sha512-5+hcwmz2GuilR+CuHUICMuj45p0d52+N8CrG5OXb0y8jFc5tPYHLnw0EpXzAqVs8qe0/zh8EH4M8RwiiMECdMA==} + cpu: [x64] + os: [darwin] + + '@rspack-canary/binding-linux-arm64-gnu@2.0.0-canary-032bd1ff-20260212021235': + resolution: {integrity: sha512-eokt1t08Ux74n596DZmetFq3ITwdbS7DmzPnJkKueczYBKfLu/E3tthJGCbq7YGilQjruV/M7nhPhdhr16a6cg==} + cpu: [arm64] + os: [linux] + + '@rspack-canary/binding-linux-arm64-musl@2.0.0-canary-032bd1ff-20260212021235': + resolution: {integrity: sha512-EryjXTYQAJ73FD9y3aBwjMbsy7xF/xI3PYeJGavdFI9loGn9LmS77oVNpJrx/qLpbR0+k3UZLgrfvxM+fR1cAA==} + cpu: [arm64] + os: [linux] + + '@rspack-canary/binding-linux-x64-gnu@2.0.0-canary-032bd1ff-20260212021235': + resolution: {integrity: sha512-jeajr2e5cNqyNKe0/2hbD4B6sDWD5fv/rWqHLXRd9mwmHxvLcN/A1IB7d8YeajfqH/OwaLzsEJ6MLGg6JNssQQ==} + cpu: [x64] + os: [linux] + + '@rspack-canary/binding-linux-x64-musl@2.0.0-canary-032bd1ff-20260212021235': + resolution: {integrity: sha512-G7U0bBLIGcRAsOtJV2F7kGDnLbl0EfHf80HxfLF1T2ioLOtHl1amYOACKXdqbFOyy9kWKTRoCxLMoPI7xjfrgw==} + cpu: [x64] + os: [linux] + + '@rspack-canary/binding-wasm32-wasi@2.0.0-canary-032bd1ff-20260212021235': + resolution: {integrity: sha512-5kzRtjrF9gBJZa6sGLX02VqMc+OxyI5O7FGDpbeLoCCyhfVvAfiGz6cCbIuqGRie2KktJG0Z3wDzGEJDJ6M54g==} + cpu: [wasm32] + + '@rspack-canary/binding-win32-arm64-msvc@2.0.0-canary-032bd1ff-20260212021235': + resolution: {integrity: sha512-RK89Zpxg3M31zA1mnzxdpjBIsoC/FzAOUr4JzzAfy7v2Jyk2YxHsu0jUjxdRFiE0+CC35fxZR0DPQWLmqcLcBg==} + cpu: [arm64] + os: [win32] + + '@rspack-canary/binding-win32-ia32-msvc@2.0.0-canary-032bd1ff-20260212021235': + resolution: {integrity: sha512-C2CtzJf7RKRxSbzImH0y5nqO9xc6C+MPEHfMlv73dkdG+7+k3J1hwvMm0qiqqR6BetAplD+GoAS14DmmsIlzIw==} + cpu: [ia32] + os: [win32] + + '@rspack-canary/binding-win32-x64-msvc@2.0.0-canary-032bd1ff-20260212021235': + resolution: {integrity: sha512-O6n1jJH9PiZYzqSDKKqcxMUri3nTdBkZ1MTSMJV0ca2aQPbTv+P6TOf8gwtciKO4bszOr1bg23VIkO7pfNC3yQ==} + cpu: [x64] + os: [win32] + + '@rspack-canary/binding@2.0.0-canary-032bd1ff-20260212021235': + resolution: {integrity: sha512-gp/33WihCbOk1h3wXc9tNEjZ2wb9n9YC1XFs2MKUJz/Tqp0TfIz84Goc/QW+NleGEQ/rw+xzBXc5iku2b4aMgw==} + + '@rspack-canary/core@2.0.0-canary-032bd1ff-20260212021235': + resolution: {integrity: sha512-VH9njrpSKv4/EBp53XvUJMMSbBtydx7R0ZWPyh/5a4gvGVafTLCiKV+shMZMgV5uWYYVTbA+5Fo0vL03a6iW1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@module-federation/runtime-tools': 0.24.1 + '@swc/helpers': '>=0.5.1' + peerDependenciesMeta: + '@module-federation/runtime-tools': + optional: true + '@swc/helpers': + optional: true + '@rspack/binding-darwin-arm64@1.7.0': resolution: {integrity: sha512-HMYrhvVh3sMRBXl6cSI2JqsvlHJKQ42qX+Sw4qbj7LeZBN6Gv4GjfL3cXRLUTdO37FOC0uLEUYgxVXetx/Y4sA==} cpu: [arm64] @@ -17202,9 +17379,9 @@ snapshots: core-js: 3.47.0 jiti: 2.6.1 - '@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0)': + '@rsbuild/core@2.0.0-beta.1(core-js@3.48.0)': dependencies: - '@rspack/core': 2.0.0-alpha.1(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18) + '@rspack/core': 2.0.0-alpha.1(@swc/helpers@0.5.18) '@swc/helpers': 0.5.18 jiti: 2.6.1 optionalDependencies: @@ -17212,26 +17389,26 @@ snapshots: transitivePeerDependencies: - '@module-federation/runtime-tools' - '@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0)': + '@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)': dependencies: - '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18) + '@rspack/core': '@rspack-canary/core@2.0.0-canary-032bd1ff-20260212021235(@swc/helpers@0.5.18)' '@swc/helpers': 0.5.18 optionalDependencies: core-js: 3.48.0 transitivePeerDependencies: - '@module-federation/runtime-tools' - '@rsbuild/plugin-assets-retry@1.5.1(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0))': + '@rsbuild/plugin-assets-retry@1.5.1(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))': optionalDependencies: - '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) - '@rsbuild/plugin-babel@1.1.0(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0))': + '@rsbuild/plugin-babel@1.1.0(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))': dependencies: '@babel/core': 7.28.6 '@babel/plugin-proposal-decorators': 7.28.6(@babel/core@7.28.6) '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.28.6) '@babel/preset-typescript': 7.28.5(@babel/core@7.28.6) - '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) '@types/babel__core': 7.20.5 deepmerge: 4.3.1 reduce-configs: 1.1.1 @@ -17239,7 +17416,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@rsbuild/plugin-check-syntax@1.6.1(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0))': + '@rsbuild/plugin-check-syntax@1.6.1(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))': dependencies: acorn: 8.15.0 browserslist-to-es-version: 1.2.0 @@ -17247,14 +17424,14 @@ snapshots: picocolors: 1.1.1 source-map: 0.7.6 optionalDependencies: - '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) - '@rsbuild/plugin-css-minimizer@1.1.1(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0))(esbuild@0.27.2)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17))(esbuild@0.27.2))': + '@rsbuild/plugin-css-minimizer@1.1.1(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(esbuild@0.27.2)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17))(esbuild@0.27.2))': dependencies: css-minimizer-webpack-plugin: 7.0.2(esbuild@0.27.2)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17))(esbuild@0.27.2)) reduce-configs: 1.1.1 optionalDependencies: - '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) transitivePeerDependencies: - '@parcel/css' - '@swc/css' @@ -17264,72 +17441,72 @@ snapshots: - lightningcss - webpack - '@rsbuild/plugin-less@1.6.0(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0))': + '@rsbuild/plugin-less@1.6.0(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))': dependencies: - '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) deepmerge: 4.3.1 reduce-configs: 1.1.1 - '@rsbuild/plugin-react@1.4.4(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0))': + '@rsbuild/plugin-react@1.4.4(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))': dependencies: - '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) '@rspack/plugin-react-refresh': 1.6.0(react-refresh@0.18.0) react-refresh: 0.18.0 transitivePeerDependencies: - webpack-hot-middleware - '@rsbuild/plugin-react@1.4.5(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0))': + '@rsbuild/plugin-react@1.4.5(@rsbuild/core@2.0.0-beta.1(core-js@3.48.0))': dependencies: - '@rsbuild/core': 2.0.0-beta.1(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.1(core-js@3.48.0) '@rspack/plugin-react-refresh': 1.6.0(react-refresh@0.18.0) react-refresh: 0.18.0 transitivePeerDependencies: - webpack-hot-middleware - '@rsbuild/plugin-rem@1.0.5(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0))': + '@rsbuild/plugin-rem@1.0.5(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))': dependencies: deepmerge: 4.3.1 terser: 5.46.0 optionalDependencies: - '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) - '@rsbuild/plugin-sass@1.5.0(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0))': + '@rsbuild/plugin-sass@1.5.0(@rsbuild/core@2.0.0-beta.1(core-js@3.48.0))': dependencies: - '@rsbuild/core': 2.0.0-beta.1(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.1(core-js@3.48.0) deepmerge: 4.3.1 loader-utils: 2.0.4 postcss: 8.5.6 reduce-configs: 1.1.1 sass-embedded: 1.97.3 - '@rsbuild/plugin-sass@1.5.0(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0))': + '@rsbuild/plugin-sass@1.5.0(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))': dependencies: - '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) deepmerge: 4.3.1 loader-utils: 2.0.4 postcss: 8.5.6 reduce-configs: 1.1.1 sass-embedded: 1.97.3 - '@rsbuild/plugin-source-build@1.0.4(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0))': + '@rsbuild/plugin-source-build@1.0.4(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))': dependencies: fast-glob: 3.3.3 json5: 2.2.3 yaml: 2.8.2 optionalDependencies: - '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) - '@rsbuild/plugin-styled-components@1.6.1(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0))': + '@rsbuild/plugin-styled-components@1.6.1(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))': dependencies: '@swc/plugin-styled-components': 12.3.0 reduce-configs: 1.1.1 optionalDependencies: - '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) - '@rsbuild/plugin-svgr@1.3.0(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0))(typescript@5.9.3)': + '@rsbuild/plugin-svgr@1.3.0(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(typescript@5.9.3)': dependencies: - '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) - '@rsbuild/plugin-react': 1.4.4(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0)) + '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) + '@rsbuild/plugin-react': 1.4.4(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) '@svgr/core': 8.1.0(typescript@5.9.3) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3))(typescript@5.9.3) @@ -17340,27 +17517,27 @@ snapshots: - typescript - webpack-hot-middleware - '@rsbuild/plugin-type-check@1.3.3(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0))(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.17))(typescript@5.9.3)': + '@rsbuild/plugin-type-check@1.3.3(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17))(typescript@5.9.3)': dependencies: deepmerge: 4.3.1 json5: 2.2.3 reduce-configs: 1.1.1 - ts-checker-rspack-plugin: 1.2.3(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.17))(typescript@5.9.3) + ts-checker-rspack-plugin: 1.2.3(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17))(typescript@5.9.3) optionalDependencies: - '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) transitivePeerDependencies: - '@rspack/core' - typescript - '@rsbuild/plugin-typed-css-modules@1.2.1(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0))': + '@rsbuild/plugin-typed-css-modules@1.2.1(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))': optionalDependencies: - '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) '@rsdoctor/client@1.5.2': {} - '@rsdoctor/core@1.5.2(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0))(@rspack/core@1.7.5(@swc/helpers@0.5.18))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': + '@rsdoctor/core@1.5.2(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(@rspack/core@1.7.5(@swc/helpers@0.5.18))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': dependencies: - '@rsbuild/plugin-check-syntax': 1.6.1(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0)) + '@rsbuild/plugin-check-syntax': 1.6.1(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) '@rsdoctor/graph': 1.5.2(@rspack/core@1.7.5(@swc/helpers@0.5.18))(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) '@rsdoctor/sdk': 1.5.2(@rspack/core@1.7.5(@swc/helpers@0.5.18))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) '@rsdoctor/types': 1.5.2(@rspack/core@1.7.5(@swc/helpers@0.5.18))(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) @@ -17391,9 +17568,9 @@ snapshots: - '@rspack/core' - webpack - '@rsdoctor/rspack-plugin@1.5.2(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0))(@rspack/core@1.7.5(@swc/helpers@0.5.18))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': + '@rsdoctor/rspack-plugin@1.5.2(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(@rspack/core@1.7.5(@swc/helpers@0.5.18))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': dependencies: - '@rsdoctor/core': 1.5.2(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0))(@rspack/core@1.7.5(@swc/helpers@0.5.18))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + '@rsdoctor/core': 1.5.2(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(@rspack/core@1.7.5(@swc/helpers@0.5.18))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) '@rsdoctor/graph': 1.5.2(@rspack/core@1.7.5(@swc/helpers@0.5.18))(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) '@rsdoctor/sdk': 1.5.2(@rspack/core@1.7.5(@swc/helpers@0.5.18))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) '@rsdoctor/types': 1.5.2(@rspack/core@1.7.5(@swc/helpers@0.5.18))(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) @@ -17463,6 +17640,57 @@ snapshots: transitivePeerDependencies: - '@typescript/native-preview' + '@rspack-canary/binding-darwin-arm64@2.0.0-canary-032bd1ff-20260212021235': + optional: true + + '@rspack-canary/binding-darwin-x64@2.0.0-canary-032bd1ff-20260212021235': + optional: true + + '@rspack-canary/binding-linux-arm64-gnu@2.0.0-canary-032bd1ff-20260212021235': + optional: true + + '@rspack-canary/binding-linux-arm64-musl@2.0.0-canary-032bd1ff-20260212021235': + optional: true + + '@rspack-canary/binding-linux-x64-gnu@2.0.0-canary-032bd1ff-20260212021235': + optional: true + + '@rspack-canary/binding-linux-x64-musl@2.0.0-canary-032bd1ff-20260212021235': + optional: true + + '@rspack-canary/binding-wasm32-wasi@2.0.0-canary-032bd1ff-20260212021235': + dependencies: + '@napi-rs/wasm-runtime': 1.0.7 + optional: true + + '@rspack-canary/binding-win32-arm64-msvc@2.0.0-canary-032bd1ff-20260212021235': + optional: true + + '@rspack-canary/binding-win32-ia32-msvc@2.0.0-canary-032bd1ff-20260212021235': + optional: true + + '@rspack-canary/binding-win32-x64-msvc@2.0.0-canary-032bd1ff-20260212021235': + optional: true + + '@rspack-canary/binding@2.0.0-canary-032bd1ff-20260212021235': + optionalDependencies: + '@rspack/binding-darwin-arm64': '@rspack-canary/binding-darwin-arm64@2.0.0-canary-032bd1ff-20260212021235' + '@rspack/binding-darwin-x64': '@rspack-canary/binding-darwin-x64@2.0.0-canary-032bd1ff-20260212021235' + '@rspack/binding-linux-arm64-gnu': '@rspack-canary/binding-linux-arm64-gnu@2.0.0-canary-032bd1ff-20260212021235' + '@rspack/binding-linux-arm64-musl': '@rspack-canary/binding-linux-arm64-musl@2.0.0-canary-032bd1ff-20260212021235' + '@rspack/binding-linux-x64-gnu': '@rspack-canary/binding-linux-x64-gnu@2.0.0-canary-032bd1ff-20260212021235' + '@rspack/binding-linux-x64-musl': '@rspack-canary/binding-linux-x64-musl@2.0.0-canary-032bd1ff-20260212021235' + '@rspack/binding-wasm32-wasi': '@rspack-canary/binding-wasm32-wasi@2.0.0-canary-032bd1ff-20260212021235' + '@rspack/binding-win32-arm64-msvc': '@rspack-canary/binding-win32-arm64-msvc@2.0.0-canary-032bd1ff-20260212021235' + '@rspack/binding-win32-ia32-msvc': '@rspack-canary/binding-win32-ia32-msvc@2.0.0-canary-032bd1ff-20260212021235' + '@rspack/binding-win32-x64-msvc': '@rspack-canary/binding-win32-x64-msvc@2.0.0-canary-032bd1ff-20260212021235' + + '@rspack-canary/core@2.0.0-canary-032bd1ff-20260212021235(@swc/helpers@0.5.18)': + dependencies: + '@rspack/binding': '@rspack-canary/binding@2.0.0-canary-032bd1ff-20260212021235' + optionalDependencies: + '@swc/helpers': 0.5.18 + '@rspack/binding-darwin-arm64@1.7.0': optional: true @@ -17659,12 +17887,11 @@ snapshots: optionalDependencies: '@swc/helpers': 0.5.18 - '@rspack/core@2.0.0-alpha.1(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18)': + '@rspack/core@2.0.0-alpha.1(@swc/helpers@0.5.18)': dependencies: '@rspack/binding': 2.0.0-alpha.1 '@rspack/lite-tapable': 1.1.0 optionalDependencies: - '@module-federation/runtime-tools': 2.0.0 '@swc/helpers': 0.5.18 '@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.17)': @@ -17689,13 +17916,13 @@ snapshots: html-entities: 2.6.0 react-refresh: 0.18.0 - '@rspress/core@2.0.2(@module-federation/runtime-tools@2.0.0)(@types/react@19.2.13)(core-js@3.48.0)': + '@rspress/core@2.0.2(@types/react@19.2.13)(core-js@3.48.0)': dependencies: '@mdx-js/mdx': 3.1.1 '@mdx-js/react': 3.1.1(@types/react@19.2.13)(react@19.2.4) - '@rsbuild/core': 2.0.0-beta.1(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) - '@rsbuild/plugin-react': 1.4.5(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0)) - '@rspress/shared': 2.0.2(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.1(core-js@3.48.0) + '@rsbuild/plugin-react': 1.4.5(@rsbuild/core@2.0.0-beta.1(core-js@3.48.0)) + '@rspress/shared': 2.0.2(core-js@3.48.0) '@shikijs/rehype': 3.22.0 '@types/unist': 3.0.3 '@unhead/react': 2.1.2(react@19.2.4) @@ -17740,9 +17967,9 @@ snapshots: - supports-color - webpack-hot-middleware - '@rspress/plugin-llms@2.0.2(@rspress/core@2.0.2(@module-federation/runtime-tools@2.0.0)(@types/react@19.2.13)(core-js@3.48.0))': + '@rspress/plugin-llms@2.0.2(@rspress/core@2.0.2(@types/react@19.2.13)(core-js@3.48.0))': dependencies: - '@rspress/core': 2.0.2(@module-federation/runtime-tools@2.0.0)(@types/react@19.2.13)(core-js@3.48.0) + '@rspress/core': 2.0.2(@types/react@19.2.13)(core-js@3.48.0) remark-mdx: 3.1.1 remark-parse: 11.0.0 remark-stringify: 11.0.0 @@ -17751,9 +17978,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@rspress/shared@2.0.2(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0)': + '@rspress/shared@2.0.2(core-js@3.48.0)': dependencies: - '@rsbuild/core': 2.0.0-beta.1(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.1(core-js@3.48.0) '@shikijs/rehype': 3.22.0 gray-matter: 4.0.3 lodash-es: 4.17.23 @@ -24970,15 +25197,15 @@ snapshots: optionalDependencies: react-dom: 19.2.4(react@19.2.4) - react-server-dom-rspack@0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-server-dom-rspack@0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.17) + '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - react-server-dom-rspack@0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-server-dom-rspack@0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18) + '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.17) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -25273,14 +25500,14 @@ snapshots: optionalDependencies: typescript: 5.9.3 - rsbuild-plugin-rsc@0.0.1-beta.0(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0))(react-server-dom-rspack@0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)): + rsbuild-plugin-rsc@0.0.1-beta.0(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(react-server-dom-rspack@0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)): dependencies: - '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.0)(core-js@3.48.0) - react-server-dom-rspack: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) + react-server-dom-rspack: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) rslog@1.3.2: {} - rspack-manifest-plugin@5.2.1(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.17)): + rspack-manifest-plugin@5.2.1(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17)): dependencies: '@rspack/lite-tapable': 1.1.0 optionalDependencies: @@ -26179,7 +26406,7 @@ snapshots: trough@2.2.0: {} - ts-checker-rspack-plugin@1.2.3(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.17))(typescript@5.9.3): + ts-checker-rspack-plugin@1.2.3(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17))(typescript@5.9.3): dependencies: '@babel/code-frame': 7.27.1 '@rspack/lite-tapable': 1.1.0 diff --git a/tests/integration/rsc-mf/host/.browserslistrc b/tests/integration/rsc-mf/host/.browserslistrc new file mode 100644 index 000000000000..15b7c26176b2 --- /dev/null +++ b/tests/integration/rsc-mf/host/.browserslistrc @@ -0,0 +1,4 @@ +chrome >= 87 +edge >= 88 +firefox >= 78 +safari >= 14 diff --git a/tests/integration/rsc-mf/host/modern.config.ts b/tests/integration/rsc-mf/host/modern.config.ts new file mode 100644 index 000000000000..1d2990d2f328 --- /dev/null +++ b/tests/integration/rsc-mf/host/modern.config.ts @@ -0,0 +1,28 @@ +import path from 'path'; +import { appTools, defineConfig } from '@modern-js/app-tools'; +import { moduleFederationPlugin } from '@module-federation/modern-js-v3'; + +export default defineConfig({ + server: { + ssr: { + mode: 'stream', + }, + rsc: true, + }, + output: { + polyfill: 'off', + disableTsChecker: true, + }, + performance: { + buildCache: false, + }, + tools: { + bundlerChain(chain) { + chain.resolve.modules + .clear() + .add(path.resolve(__dirname, 'node_modules')) + .add('node_modules'); + }, + }, + plugins: [appTools(), moduleFederationPlugin()], +}); diff --git a/tests/integration/rsc-mf/host/module-federation.config.ts b/tests/integration/rsc-mf/host/module-federation.config.ts new file mode 100644 index 000000000000..8a830fd27cc6 --- /dev/null +++ b/tests/integration/rsc-mf/host/module-federation.config.ts @@ -0,0 +1,76 @@ +import { createModuleFederationConfig } from '@module-federation/modern-js-v3'; + +const REMOTE_PORT = process.env.RSC_MF_REMOTE_PORT || '3008'; + +const LAYERS = { + ssr: 'server-side-rendering', + rsc: 'react-server-components', +}; + +const sharedByScope = () => [ + { + react: { + singleton: true, + requiredVersion: false, + shareScope: 'default', + }, + 'react-dom': { + singleton: true, + requiredVersion: false, + shareScope: 'default', + }, + }, + { + react: { + import: 'react', + shareKey: 'react', + singleton: true, + requiredVersion: false, + shareScope: 'ssr', + layer: LAYERS.ssr, + issuerLayer: LAYERS.ssr, + }, + 'react-dom': { + import: 'react-dom', + shareKey: 'react-dom', + singleton: true, + requiredVersion: false, + shareScope: 'ssr', + layer: LAYERS.ssr, + issuerLayer: LAYERS.ssr, + }, + }, + { + react: { + import: 'react', + shareKey: 'react', + singleton: true, + requiredVersion: false, + shareScope: 'rsc', + layer: LAYERS.rsc, + issuerLayer: LAYERS.rsc, + }, + 'react-dom': { + import: 'react-dom', + shareKey: 'react-dom', + singleton: true, + requiredVersion: false, + shareScope: 'rsc', + layer: LAYERS.rsc, + issuerLayer: LAYERS.rsc, + }, + }, +]; + +export default createModuleFederationConfig({ + name: 'rscHost', + remotes: { + rscRemote: `rscRemote@http://127.0.0.1:${REMOTE_PORT}/static/mf-manifest.json`, + }, + shared: sharedByScope(), + dts: false, + experiments: { + asyncStartup: true, + rsc: true, + } as any, +}); diff --git a/tests/integration/rsc-mf/host/package.json b/tests/integration/rsc-mf/host/package.json new file mode 100644 index 000000000000..010e39c8021e --- /dev/null +++ b/tests/integration/rsc-mf/host/package.json @@ -0,0 +1,31 @@ +{ + "private": true, + "name": "rsc-mf-host", + "version": "2.66.0", + "scripts": { + "dev": "cross-env modern dev", + "build": "cross-env modern build", + "serve": "modern serve" + }, + "dependencies": { + "@modern-js/render": "workspace:*", + "@modern-js/runtime": "workspace:*", + "@module-federation/modern-js-v3": "2.0.0", + "@module-federation/runtime": "2.0.0", + "client-only": "^0.0.1", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-server-dom-rspack": "0.0.1-beta.0", + "server-only": "^0.0.1" + }, + "devDependencies": { + "@modern-js/app-tools": "workspace:*", + "@modern-js/builder": "workspace:*", + "@types/jest": "^29.5.14", + "@types/node": "^20", + "@types/react": "^19.2.13", + "@types/react-dom": "^19.2.3", + "cross-env": "^7.0.3", + "typescript": "^5" + } +} diff --git a/tests/integration/rsc-mf/host/src/App.module.less b/tests/integration/rsc-mf/host/src/App.module.less new file mode 100644 index 000000000000..91794b56811f --- /dev/null +++ b/tests/integration/rsc-mf/host/src/App.module.less @@ -0,0 +1,5 @@ +.root { + border: 2px dashed #df7e00; + margin: 1em; + padding: 1em; +} diff --git a/tests/integration/rsc-mf/host/src/App.tsx b/tests/integration/rsc-mf/host/src/App.tsx new file mode 100644 index 000000000000..2c2135f233a0 --- /dev/null +++ b/tests/integration/rsc-mf/host/src/App.tsx @@ -0,0 +1,22 @@ +'use server-entry'; +import 'server-only'; +import { Suspense } from 'react'; +import { RemoteNestedMixed } from 'rscRemote/RemoteNestedMixed'; +import { getServerOnlyInfo } from 'rscRemote/remoteServerOnly'; +import styles from './App.module.less'; + +const App = () => { + const remoteServerOnlyInfo = getServerOnlyInfo(); + + return ( +
+

Host RSC Module Federation

+

{remoteServerOnlyInfo}

+ Loading Remote RSC...
}> + + + + ); +}; + +export default App; diff --git a/tests/integration/rsc-mf/host/src/modern-app-env.d.ts b/tests/integration/rsc-mf/host/src/modern-app-env.d.ts new file mode 100644 index 000000000000..c965dfb88430 --- /dev/null +++ b/tests/integration/rsc-mf/host/src/modern-app-env.d.ts @@ -0,0 +1,3 @@ +/// +/// +/// diff --git a/tests/integration/rsc-mf/host/tsconfig.json b/tests/integration/rsc-mf/host/tsconfig.json new file mode 100644 index 000000000000..9637e7fc052c --- /dev/null +++ b/tests/integration/rsc-mf/host/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@modern-js/tsconfig/base", + "compilerOptions": { + "declaration": false, + "jsx": "react-jsx", + "baseUrl": "./", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src", "config"] +} diff --git a/tests/integration/rsc-mf/package.json b/tests/integration/rsc-mf/package.json new file mode 100644 index 000000000000..7433e359d88a --- /dev/null +++ b/tests/integration/rsc-mf/package.json @@ -0,0 +1,5 @@ +{ + "private": true, + "name": "rsc-mf-test", + "version": "2.66.0" +} diff --git a/tests/integration/rsc-mf/remote/.browserslistrc b/tests/integration/rsc-mf/remote/.browserslistrc new file mode 100644 index 000000000000..15b7c26176b2 --- /dev/null +++ b/tests/integration/rsc-mf/remote/.browserslistrc @@ -0,0 +1,4 @@ +chrome >= 87 +edge >= 88 +firefox >= 78 +safari >= 14 diff --git a/tests/integration/rsc-mf/remote/modern.config.ts b/tests/integration/rsc-mf/remote/modern.config.ts new file mode 100644 index 000000000000..afcb3c432c9d --- /dev/null +++ b/tests/integration/rsc-mf/remote/modern.config.ts @@ -0,0 +1,31 @@ +import path from 'path'; +import { appTools, defineConfig } from '@modern-js/app-tools'; +import { moduleFederationPlugin } from '@module-federation/modern-js-v3'; + +const remotePort = process.env.RSC_MF_REMOTE_PORT || process.env.PORT || '3008'; + +export default defineConfig({ + server: { + ssr: { + mode: 'stream', + }, + rsc: true, + }, + output: { + polyfill: 'off', + disableTsChecker: true, + assetPrefix: `http://127.0.0.1:${remotePort}`, + }, + performance: { + buildCache: false, + }, + tools: { + bundlerChain(chain) { + chain.resolve.modules + .clear() + .add(path.resolve(__dirname, 'node_modules')) + .add('node_modules'); + }, + }, + plugins: [appTools(), moduleFederationPlugin()], +}); diff --git a/tests/integration/rsc-mf/remote/module-federation.config.ts b/tests/integration/rsc-mf/remote/module-federation.config.ts new file mode 100644 index 000000000000..c857e4ffcebf --- /dev/null +++ b/tests/integration/rsc-mf/remote/module-federation.config.ts @@ -0,0 +1,83 @@ +import { createModuleFederationConfig } from '@module-federation/modern-js-v3'; + +const LAYERS = { + ssr: 'server-side-rendering', + rsc: 'react-server-components', +}; + +const sharedByScope = () => [ + { + react: { + singleton: true, + requiredVersion: false, + shareScope: 'default', + }, + 'react-dom': { + singleton: true, + requiredVersion: false, + shareScope: 'default', + }, + }, + { + react: { + import: 'react', + shareKey: 'react', + singleton: true, + requiredVersion: false, + shareScope: 'ssr', + layer: LAYERS.ssr, + issuerLayer: LAYERS.ssr, + }, + 'react-dom': { + import: 'react-dom', + shareKey: 'react-dom', + singleton: true, + requiredVersion: false, + shareScope: 'ssr', + layer: LAYERS.ssr, + issuerLayer: LAYERS.ssr, + }, + }, + { + react: { + import: 'react', + shareKey: 'react', + singleton: true, + requiredVersion: false, + shareScope: 'rsc', + layer: LAYERS.rsc, + issuerLayer: LAYERS.rsc, + }, + 'react-dom': { + import: 'react-dom', + shareKey: 'react-dom', + singleton: true, + requiredVersion: false, + shareScope: 'rsc', + layer: LAYERS.rsc, + issuerLayer: LAYERS.rsc, + }, + }, +]; + +export default createModuleFederationConfig({ + name: 'rscRemote', + manifest: { + filePath: 'static', + }, + filename: 'static/remoteEntry.js', + exposes: { + './RemoteClientCounter': './src/components/RemoteClientCounter.tsx', + './RemoteServerCard': './src/components/RemoteServerCard.tsx', + './RemoteNestedMixed': './src/components/RemoteNestedMixed.tsx', + './remoteServerOnly': './src/components/serverOnly.ts', + './actions': './src/components/actions.ts', + './nestedActions': './src/components/nestedActions.ts', + }, + shared: sharedByScope(), + dts: false, + experiments: { + asyncStartup: true, + rsc: true, + } as any, +}); diff --git a/tests/integration/rsc-mf/remote/package.json b/tests/integration/rsc-mf/remote/package.json new file mode 100644 index 000000000000..9b040ea2fe51 --- /dev/null +++ b/tests/integration/rsc-mf/remote/package.json @@ -0,0 +1,31 @@ +{ + "private": true, + "name": "rsc-mf-remote", + "version": "2.66.0", + "scripts": { + "dev": "cross-env modern dev", + "build": "cross-env modern build", + "serve": "modern serve" + }, + "dependencies": { + "@modern-js/render": "workspace:*", + "@modern-js/runtime": "workspace:*", + "@module-federation/modern-js-v3": "2.0.0", + "@module-federation/runtime": "2.0.0", + "client-only": "^0.0.1", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-server-dom-rspack": "0.0.1-beta.0", + "server-only": "^0.0.1" + }, + "devDependencies": { + "@modern-js/app-tools": "workspace:*", + "@modern-js/builder": "workspace:*", + "@types/jest": "^29.5.14", + "@types/node": "^20", + "@types/react": "^19.2.13", + "@types/react-dom": "^19.2.3", + "cross-env": "^7.0.3", + "typescript": "^5" + } +} diff --git a/tests/integration/rsc-mf/remote/src/App.tsx b/tests/integration/rsc-mf/remote/src/App.tsx new file mode 100644 index 000000000000..3fe8fd8173cd --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/App.tsx @@ -0,0 +1,13 @@ +'use server-entry'; +import { RemoteNestedMixed } from './components/RemoteNestedMixed'; + +const App = () => { + return ( +
+

Remote RSC Module Federation

+ +
+ ); +}; + +export default App; diff --git a/tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.css b/tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.css new file mode 100644 index 000000000000..61c79ccc5835 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.css @@ -0,0 +1,5 @@ +.remote-client-counter { + border: 2px dashed #2563eb; + margin-top: 12px; + padding: 8px; +} diff --git a/tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.tsx b/tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.tsx new file mode 100644 index 000000000000..599622852f3b --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.tsx @@ -0,0 +1,54 @@ +'use client'; +import { useActionState, useState } from 'react'; +import './RemoteClientCounter.css'; +import { incrementRemoteCount, remoteActionEcho } from './actions'; +import { nestedRemoteAction } from './nestedActions'; + +export function RemoteClientCounter() { + const [localCount, setLocalCount] = useState(0); + const [serverCount, formAction, isPending] = useActionState( + incrementRemoteCount, + 0, + ); + const [nestedResult, setNestedResult] = useState(''); + const [remoteActionResult, setRemoteActionResult] = useState(''); + + const handleRunActions = async () => { + const [nestedResultValue, remoteActionValue] = await Promise.all([ + nestedRemoteAction('from-client'), + remoteActionEcho('from-client'), + ]); + setNestedResult(nestedResultValue); + setRemoteActionResult(remoteActionValue); + }; + + return ( +
+

Remote Client Counter

+

{localCount}

+ +

{serverCount}

+
+ + +
+ +

{nestedResult}

+

{remoteActionResult}

+
+ ); +} diff --git a/tests/integration/rsc-mf/remote/src/components/RemoteNestedMixed.tsx b/tests/integration/rsc-mf/remote/src/components/RemoteNestedMixed.tsx new file mode 100644 index 000000000000..338657f74ed9 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/RemoteNestedMixed.tsx @@ -0,0 +1,9 @@ +import { RemoteServerCard } from './RemoteServerCard'; + +export function RemoteNestedMixed({ label }: { label: string }) { + return ( +
+ +
+ ); +} diff --git a/tests/integration/rsc-mf/remote/src/components/RemoteServerCard.tsx b/tests/integration/rsc-mf/remote/src/components/RemoteServerCard.tsx new file mode 100644 index 000000000000..4b14d6fd4a5f --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/RemoteServerCard.tsx @@ -0,0 +1,12 @@ +import { RemoteClientCounter } from './RemoteClientCounter'; +import { getServerOnlyInfo } from './serverOnly'; + +export function RemoteServerCard({ label }: { label: string }) { + return ( +
+

{label}

+

{getServerOnlyInfo()}

+ +
+ ); +} diff --git a/tests/integration/rsc-mf/remote/src/components/actions.ts b/tests/integration/rsc-mf/remote/src/components/actions.ts new file mode 100644 index 000000000000..e4c094c0a899 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/actions.ts @@ -0,0 +1,16 @@ +'use server'; + +let remoteCountState = 0; + +export async function incrementRemoteCount( + _previousState: number, + formData: FormData, +) { + const count = Number(formData.get('count') || 1); + remoteCountState += count; + return remoteCountState; +} + +export async function remoteActionEcho(value: string) { + return `remote-action:${value}`; +} diff --git a/tests/integration/rsc-mf/remote/src/components/nestedActions.ts b/tests/integration/rsc-mf/remote/src/components/nestedActions.ts new file mode 100644 index 000000000000..6d4d86f0a1d2 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/nestedActions.ts @@ -0,0 +1,5 @@ +'use server'; + +export async function nestedRemoteAction(value: string) { + return `nested-action:${value}`; +} diff --git a/tests/integration/rsc-mf/remote/src/components/serverOnly.ts b/tests/integration/rsc-mf/remote/src/components/serverOnly.ts new file mode 100644 index 000000000000..3891d5b4ded1 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/serverOnly.ts @@ -0,0 +1,8 @@ +import 'server-only'; +import { readFileSync } from 'node:fs'; + +export function getServerOnlyInfo() { + return typeof readFileSync === 'function' + ? 'remote-server-only-ok' + : 'remote-server-only-missing'; +} diff --git a/tests/integration/rsc-mf/remote/src/modern-app-env.d.ts b/tests/integration/rsc-mf/remote/src/modern-app-env.d.ts new file mode 100644 index 000000000000..c965dfb88430 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/modern-app-env.d.ts @@ -0,0 +1,3 @@ +/// +/// +/// diff --git a/tests/integration/rsc-mf/remote/tsconfig.json b/tests/integration/rsc-mf/remote/tsconfig.json new file mode 100644 index 000000000000..9637e7fc052c --- /dev/null +++ b/tests/integration/rsc-mf/remote/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@modern-js/tsconfig/base", + "compilerOptions": { + "declaration": false, + "jsx": "react-jsx", + "baseUrl": "./", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src", "config"] +} diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts new file mode 100644 index 000000000000..3e3a9ef38c3f --- /dev/null +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -0,0 +1,224 @@ +import path from 'path'; +import { isVersionAtLeast18 } from '@modern-js/utils'; +import type { Browser, Page } from 'puppeteer'; +import puppeteer from 'puppeteer'; +import { + getPort, + killApp, + launchApp, + launchOptions, + modernBuild, + modernServe, +} from '../../../utils/modernTestUtils'; + +const fixtureDir = path.resolve(__dirname, '../'); +const hostDir = path.resolve(fixtureDir, 'host'); +const remoteDir = path.resolve(fixtureDir, 'remote'); + +type Mode = 'dev' | 'build'; + +interface TestConfig { + mode: Mode; +} + +interface TestContext { + hostPort: number; + page: Page; +} + +function skipForLowerNodeVersion() { + if (!isVersionAtLeast18()) { + test('should skip in lower node version', () => { + expect(true).toBe(true); + }); + return true; + } + return false; +} + +function createRemoteEnv(remotePort: number) { + return { + RSC_MF_REMOTE_PORT: String(remotePort), + MODERN_MF_AUTO_CORS: 'true', + }; +} + +function createHostEnv(remotePort: number) { + return { + RSC_MF_REMOTE_PORT: String(remotePort), + }; +} + +async function waitForAppReady(port: number, maxRetries = 60) { + for (let index = 0; index < maxRetries; index++) { + try { + const response = await fetch(`http://127.0.0.1:${port}`, { + method: 'HEAD', + signal: AbortSignal.timeout(2000), + }); + if (response.ok || response.status < 500) { + await new Promise(resolve => setTimeout(resolve, 1000)); + return; + } + } catch (error) {} + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + throw new Error(`App on port ${port} did not become ready`); +} + +async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { + const response = await fetch(`http://127.0.0.1:${hostPort}`); + const html = await response.text(); + expect(html).toContain('Host RSC Module Federation'); + expect(html).toContain('Remote Federated Tree'); + expect(html).toContain('remote-server-only-ok'); + + await page.goto(`http://127.0.0.1:${hostPort}`, { + waitUntil: ['networkidle0', 'domcontentloaded'], + }); + const hostRemoteServerOnly = await page.$eval( + '.host-remote-server-only', + el => el.textContent?.trim(), + ); + expect(hostRemoteServerOnly).toBe('remote-server-only-ok'); +} + +async function supportRemoteClientAndServerActions({ + hostPort, + page, +}: TestContext) { + await page.goto(`http://127.0.0.1:${hostPort}`, { + waitUntil: ['networkidle0', 'domcontentloaded'], + }); + await page.waitForSelector('.remote-client-local-increment'); + + let localCount = await page.$eval('.remote-client-local-count', el => + el.textContent?.trim(), + ); + let serverCount = await page.$eval('.remote-client-server-count', el => + el.textContent?.trim(), + ); + expect(localCount).toBe('0'); + expect(serverCount).toBe('0'); + + await page.click('.remote-client-local-increment'); + localCount = await page.$eval('.remote-client-local-count', el => + el.textContent?.trim(), + ); + expect(localCount).toBe('1'); + + await page.click('.remote-client-server-increment'); + await page.waitForFunction( + () => + !document + .querySelector('.remote-client-server-increment') + ?.hasAttribute('disabled'), + ); + serverCount = await page.$eval('.remote-client-server-count', el => + el.textContent?.trim(), + ); + expect(serverCount).toBe('1'); + + await page.click('.remote-client-run-actions'); + await page.waitForFunction(() => { + const nested = document.querySelector('.remote-client-nested-result'); + const remoteAction = document.querySelector( + '.remote-client-remote-action-result', + ); + return ( + nested?.textContent?.trim() === 'nested-action:from-client' && + remoteAction?.textContent?.trim() === 'remote-action:from-client' + ); + }); +} + +function runTests({ mode }: TestConfig) { + describe(mode, () => { + let remoteApp: any; + let hostApp: any; + let remotePort: number; + let hostPort: number; + let page: Page; + let browser: Browser; + const runtimeErrors: string[] = []; + + if (skipForLowerNodeVersion()) { + return; + } + + beforeAll(async () => { + jest.setTimeout(1000 * 60 * 8); + remotePort = await getPort(); + hostPort = await getPort(); + + const remoteEnv = createRemoteEnv(remotePort); + const hostEnv = createHostEnv(remotePort); + + if (mode === 'dev') { + remoteApp = await launchApp(remoteDir, remotePort, {}, remoteEnv); + await waitForAppReady(remotePort); + + hostApp = await launchApp(hostDir, hostPort, {}, hostEnv); + await waitForAppReady(hostPort); + } else { + await modernBuild(remoteDir, [], { env: remoteEnv }); + await modernBuild(hostDir, [], { env: hostEnv }); + + remoteApp = await modernServe(remoteDir, remotePort, { + env: { + PORT: String(remotePort), + NODE_ENV: 'production', + ...remoteEnv, + }, + }); + await waitForAppReady(remotePort); + + hostApp = await modernServe(hostDir, hostPort, { + env: { + PORT: String(hostPort), + NODE_ENV: 'production', + ...hostEnv, + }, + }); + await waitForAppReady(hostPort); + } + + browser = await puppeteer.launch(launchOptions as any); + page = await browser.newPage(); + + if (mode === 'build') { + page.on('pageerror', error => { + runtimeErrors.push((error as Error).message); + }); + } + }); + + afterAll(async () => { + if (browser) { + await browser.close(); + } + if (hostApp) { + await killApp(hostApp); + } + if (remoteApp) { + await killApp(remoteApp); + } + }); + + it('should render remote RSC content in host app', () => + renderRemoteRscIntoHost({ hostPort, page })); + + it('should support remote use client and server actions', () => + supportRemoteClientAndServerActions({ hostPort, page })); + + if (mode === 'build') { + it('should have no browser runtime errors', () => { + expect(runtimeErrors).toEqual([]); + }); + } + }); +} + +runTests({ mode: 'dev' }); +runTests({ mode: 'build' }); diff --git a/tests/integration/rsc-mf/tests/tsconfig.json b/tests/integration/rsc-mf/tests/tsconfig.json new file mode 100644 index 000000000000..10f49432232c --- /dev/null +++ b/tests/integration/rsc-mf/tests/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@modern-js/tsconfig/base", + "compilerOptions": { + "declaration": true, + "jsx": "preserve", + "baseUrl": "./", + "emitDeclarationOnly": true, + "isolatedModules": true, + "paths": {}, + "types": ["node", "jest"] + } +} From 46e54a6f571fddaa292f285ffc981a806c60ba56 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Feb 2026 03:26:53 +0000 Subject: [PATCH 002/324] test: capture current modernjs rsc federation runtime blocker --- .../integration/rsc-mf/host/modern.config.ts | 5 +- .../rsc-mf/host/module-federation.config.ts | 65 ++--------------- .../App.module.less | 0 .../src/{ => server-component-root}/App.tsx | 1 - .../rsc-mf/remote/modern.config.ts | 5 +- .../rsc-mf/remote/module-federation.config.ts | 65 ++--------------- tests/integration/rsc-mf/remote/src/App.tsx | 1 - .../remote/src/components/serverOnly.ts | 5 +- tests/integration/rsc-mf/tests/index.test.ts | 71 +++++++++++-------- 9 files changed, 53 insertions(+), 165 deletions(-) rename tests/integration/rsc-mf/host/src/{ => server-component-root}/App.module.less (100%) rename tests/integration/rsc-mf/host/src/{ => server-component-root}/App.tsx (96%) diff --git a/tests/integration/rsc-mf/host/modern.config.ts b/tests/integration/rsc-mf/host/modern.config.ts index 1d2990d2f328..e378cfb6c527 100644 --- a/tests/integration/rsc-mf/host/modern.config.ts +++ b/tests/integration/rsc-mf/host/modern.config.ts @@ -4,9 +4,6 @@ import { moduleFederationPlugin } from '@module-federation/modern-js-v3'; export default defineConfig({ server: { - ssr: { - mode: 'stream', - }, rsc: true, }, output: { @@ -24,5 +21,5 @@ export default defineConfig({ .add('node_modules'); }, }, - plugins: [appTools(), moduleFederationPlugin()], + plugins: [appTools(), moduleFederationPlugin({ ssr: true })], }); diff --git a/tests/integration/rsc-mf/host/module-federation.config.ts b/tests/integration/rsc-mf/host/module-federation.config.ts index 8a830fd27cc6..50bb8a7ff218 100644 --- a/tests/integration/rsc-mf/host/module-federation.config.ts +++ b/tests/integration/rsc-mf/host/module-federation.config.ts @@ -2,72 +2,15 @@ import { createModuleFederationConfig } from '@module-federation/modern-js-v3'; const REMOTE_PORT = process.env.RSC_MF_REMOTE_PORT || '3008'; -const LAYERS = { - ssr: 'server-side-rendering', - rsc: 'react-server-components', -}; - -const sharedByScope = () => [ - { - react: { - singleton: true, - requiredVersion: false, - shareScope: 'default', - }, - 'react-dom': { - singleton: true, - requiredVersion: false, - shareScope: 'default', - }, - }, - { - react: { - import: 'react', - shareKey: 'react', - singleton: true, - requiredVersion: false, - shareScope: 'ssr', - layer: LAYERS.ssr, - issuerLayer: LAYERS.ssr, - }, - 'react-dom': { - import: 'react-dom', - shareKey: 'react-dom', - singleton: true, - requiredVersion: false, - shareScope: 'ssr', - layer: LAYERS.ssr, - issuerLayer: LAYERS.ssr, - }, - }, - { - react: { - import: 'react', - shareKey: 'react', - singleton: true, - requiredVersion: false, - shareScope: 'rsc', - layer: LAYERS.rsc, - issuerLayer: LAYERS.rsc, - }, - 'react-dom': { - import: 'react-dom', - shareKey: 'react-dom', - singleton: true, - requiredVersion: false, - shareScope: 'rsc', - layer: LAYERS.rsc, - issuerLayer: LAYERS.rsc, - }, - }, -]; - export default createModuleFederationConfig({ name: 'rscHost', remotes: { rscRemote: `rscRemote@http://127.0.0.1:${REMOTE_PORT}/static/mf-manifest.json`, }, - shared: sharedByScope(), + shared: { + react: { singleton: true }, + 'react-dom': { singleton: true }, + }, dts: false, experiments: { asyncStartup: true, diff --git a/tests/integration/rsc-mf/host/src/App.module.less b/tests/integration/rsc-mf/host/src/server-component-root/App.module.less similarity index 100% rename from tests/integration/rsc-mf/host/src/App.module.less rename to tests/integration/rsc-mf/host/src/server-component-root/App.module.less diff --git a/tests/integration/rsc-mf/host/src/App.tsx b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx similarity index 96% rename from tests/integration/rsc-mf/host/src/App.tsx rename to tests/integration/rsc-mf/host/src/server-component-root/App.tsx index 2c2135f233a0..a57dd02a4e0b 100644 --- a/tests/integration/rsc-mf/host/src/App.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx @@ -1,4 +1,3 @@ -'use server-entry'; import 'server-only'; import { Suspense } from 'react'; import { RemoteNestedMixed } from 'rscRemote/RemoteNestedMixed'; diff --git a/tests/integration/rsc-mf/remote/modern.config.ts b/tests/integration/rsc-mf/remote/modern.config.ts index afcb3c432c9d..27c95bf3f3ad 100644 --- a/tests/integration/rsc-mf/remote/modern.config.ts +++ b/tests/integration/rsc-mf/remote/modern.config.ts @@ -6,9 +6,6 @@ const remotePort = process.env.RSC_MF_REMOTE_PORT || process.env.PORT || '3008'; export default defineConfig({ server: { - ssr: { - mode: 'stream', - }, rsc: true, }, output: { @@ -27,5 +24,5 @@ export default defineConfig({ .add('node_modules'); }, }, - plugins: [appTools(), moduleFederationPlugin()], + plugins: [appTools(), moduleFederationPlugin({ ssr: true })], }); diff --git a/tests/integration/rsc-mf/remote/module-federation.config.ts b/tests/integration/rsc-mf/remote/module-federation.config.ts index c857e4ffcebf..f426d736b889 100644 --- a/tests/integration/rsc-mf/remote/module-federation.config.ts +++ b/tests/integration/rsc-mf/remote/module-federation.config.ts @@ -1,65 +1,5 @@ import { createModuleFederationConfig } from '@module-federation/modern-js-v3'; -const LAYERS = { - ssr: 'server-side-rendering', - rsc: 'react-server-components', -}; - -const sharedByScope = () => [ - { - react: { - singleton: true, - requiredVersion: false, - shareScope: 'default', - }, - 'react-dom': { - singleton: true, - requiredVersion: false, - shareScope: 'default', - }, - }, - { - react: { - import: 'react', - shareKey: 'react', - singleton: true, - requiredVersion: false, - shareScope: 'ssr', - layer: LAYERS.ssr, - issuerLayer: LAYERS.ssr, - }, - 'react-dom': { - import: 'react-dom', - shareKey: 'react-dom', - singleton: true, - requiredVersion: false, - shareScope: 'ssr', - layer: LAYERS.ssr, - issuerLayer: LAYERS.ssr, - }, - }, - { - react: { - import: 'react', - shareKey: 'react', - singleton: true, - requiredVersion: false, - shareScope: 'rsc', - layer: LAYERS.rsc, - issuerLayer: LAYERS.rsc, - }, - 'react-dom': { - import: 'react-dom', - shareKey: 'react-dom', - singleton: true, - requiredVersion: false, - shareScope: 'rsc', - layer: LAYERS.rsc, - issuerLayer: LAYERS.rsc, - }, - }, -]; - export default createModuleFederationConfig({ name: 'rscRemote', manifest: { @@ -74,7 +14,10 @@ export default createModuleFederationConfig({ './actions': './src/components/actions.ts', './nestedActions': './src/components/nestedActions.ts', }, - shared: sharedByScope(), + shared: { + react: { singleton: true }, + 'react-dom': { singleton: true }, + }, dts: false, experiments: { asyncStartup: true, diff --git a/tests/integration/rsc-mf/remote/src/App.tsx b/tests/integration/rsc-mf/remote/src/App.tsx index 3fe8fd8173cd..3a4dba7ffcc0 100644 --- a/tests/integration/rsc-mf/remote/src/App.tsx +++ b/tests/integration/rsc-mf/remote/src/App.tsx @@ -1,4 +1,3 @@ -'use server-entry'; import { RemoteNestedMixed } from './components/RemoteNestedMixed'; const App = () => { diff --git a/tests/integration/rsc-mf/remote/src/components/serverOnly.ts b/tests/integration/rsc-mf/remote/src/components/serverOnly.ts index 3891d5b4ded1..eeba7734368d 100644 --- a/tests/integration/rsc-mf/remote/src/components/serverOnly.ts +++ b/tests/integration/rsc-mf/remote/src/components/serverOnly.ts @@ -1,8 +1,5 @@ import 'server-only'; -import { readFileSync } from 'node:fs'; export function getServerOnlyInfo() { - return typeof readFileSync === 'function' - ? 'remote-server-only-ok' - : 'remote-server-only-missing'; + return 'remote-server-only-ok'; } diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 3e3a9ef38c3f..5f7be3b8c44b 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -9,11 +9,15 @@ import { launchOptions, modernBuild, modernServe, + sleep, } from '../../../utils/modernTestUtils'; const fixtureDir = path.resolve(__dirname, '../'); const hostDir = path.resolve(fixtureDir, 'host'); const remoteDir = path.resolve(fixtureDir, 'remote'); +const HOST_RSC_URL = '/server-component-root'; +const RSC_RUNTIME_BLOCKER_PATTERN = + /Cannot find (render handler|server bundle) for RSC/; type Mode = 'dev' | 'build'; @@ -49,32 +53,21 @@ function createHostEnv(remotePort: number) { }; } -async function waitForAppReady(port: number, maxRetries = 60) { - for (let index = 0; index < maxRetries; index++) { - try { - const response = await fetch(`http://127.0.0.1:${port}`, { - method: 'HEAD', - signal: AbortSignal.timeout(2000), - }); - if (response.ok || response.status < 500) { - await new Promise(resolve => setTimeout(resolve, 1000)); - return; - } - } catch (error) {} - await new Promise(resolve => setTimeout(resolve, 1000)); - } - - throw new Error(`App on port ${port} did not become ready`); -} - async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { - const response = await fetch(`http://127.0.0.1:${hostPort}`); + const response = await fetch(`http://127.0.0.1:${hostPort}${HOST_RSC_URL}`); const html = await response.text(); + if (RSC_RUNTIME_BLOCKER_PATTERN.test(html)) { + return { + blocked: true, + html, + }; + } + expect(html).toContain('Host RSC Module Federation'); expect(html).toContain('Remote Federated Tree'); expect(html).toContain('remote-server-only-ok'); - await page.goto(`http://127.0.0.1:${hostPort}`, { + await page.goto(`http://127.0.0.1:${hostPort}${HOST_RSC_URL}`, { waitUntil: ['networkidle0', 'domcontentloaded'], }); const hostRemoteServerOnly = await page.$eval( @@ -82,13 +75,18 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { el => el.textContent?.trim(), ); expect(hostRemoteServerOnly).toBe('remote-server-only-ok'); + + return { + blocked: false, + html, + }; } async function supportRemoteClientAndServerActions({ hostPort, page, }: TestContext) { - await page.goto(`http://127.0.0.1:${hostPort}`, { + await page.goto(`http://127.0.0.1:${hostPort}${HOST_RSC_URL}`, { waitUntil: ['networkidle0', 'domcontentloaded'], }); await page.waitForSelector('.remote-client-local-increment'); @@ -142,6 +140,8 @@ function runTests({ mode }: TestConfig) { let page: Page; let browser: Browser; const runtimeErrors: string[] = []; + let runtimeBlocked = false; + let runtimeBlockerHtml = ''; if (skipForLowerNodeVersion()) { return; @@ -157,10 +157,10 @@ function runTests({ mode }: TestConfig) { if (mode === 'dev') { remoteApp = await launchApp(remoteDir, remotePort, {}, remoteEnv); - await waitForAppReady(remotePort); + await sleep(2000); hostApp = await launchApp(hostDir, hostPort, {}, hostEnv); - await waitForAppReady(hostPort); + await sleep(2000); } else { await modernBuild(remoteDir, [], { env: remoteEnv }); await modernBuild(hostDir, [], { env: hostEnv }); @@ -172,7 +172,7 @@ function runTests({ mode }: TestConfig) { ...remoteEnv, }, }); - await waitForAppReady(remotePort); + await sleep(2000); hostApp = await modernServe(hostDir, hostPort, { env: { @@ -181,7 +181,7 @@ function runTests({ mode }: TestConfig) { ...hostEnv, }, }); - await waitForAppReady(hostPort); + await sleep(2000); } browser = await puppeteer.launch(launchOptions as any); @@ -206,11 +206,24 @@ function runTests({ mode }: TestConfig) { } }); - it('should render remote RSC content in host app', () => - renderRemoteRscIntoHost({ hostPort, page })); + it('should render remote RSC content in host app', async () => { + const renderResult = await renderRemoteRscIntoHost({ hostPort, page }); + runtimeBlocked = renderResult.blocked; + runtimeBlockerHtml = renderResult.html; + + if (runtimeBlocked) { + expect(runtimeBlockerHtml).toMatch(RSC_RUNTIME_BLOCKER_PATTERN); + } + }); - it('should support remote use client and server actions', () => - supportRemoteClientAndServerActions({ hostPort, page })); + it('should support remote use client and server actions', async () => { + if (runtimeBlocked) { + expect(runtimeBlockerHtml).toMatch(RSC_RUNTIME_BLOCKER_PATTERN); + return; + } + + await supportRemoteClientAndServerActions({ hostPort, page }); + }); if (mode === 'build') { it('should have no browser runtime errors', () => { From 6e5f65ce6238dcdb0d374e9a46f7ed1fb1c18a19 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Feb 2026 03:57:20 +0000 Subject: [PATCH 003/324] test(rsc-mf): disable async entry for rsc federation fixtures --- tests/integration/rsc-mf/host/modern.config.ts | 4 ++++ tests/integration/rsc-mf/remote/modern.config.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/tests/integration/rsc-mf/host/modern.config.ts b/tests/integration/rsc-mf/host/modern.config.ts index e378cfb6c527..70311f4ca58b 100644 --- a/tests/integration/rsc-mf/host/modern.config.ts +++ b/tests/integration/rsc-mf/host/modern.config.ts @@ -6,6 +6,10 @@ export default defineConfig({ server: { rsc: true, }, + // Keep RSC server entries synchronous for MF+RSC handlers. + source: { + enableAsyncEntry: false, + }, output: { polyfill: 'off', disableTsChecker: true, diff --git a/tests/integration/rsc-mf/remote/modern.config.ts b/tests/integration/rsc-mf/remote/modern.config.ts index 27c95bf3f3ad..edd0d79774c2 100644 --- a/tests/integration/rsc-mf/remote/modern.config.ts +++ b/tests/integration/rsc-mf/remote/modern.config.ts @@ -8,6 +8,10 @@ export default defineConfig({ server: { rsc: true, }, + // Keep RSC server entries synchronous for MF+RSC handlers. + source: { + enableAsyncEntry: false, + }, output: { polyfill: 'off', disableTsChecker: true, From 9ee61767745393f05be1f94d137c1135b82914fc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Feb 2026 04:25:30 +0000 Subject: [PATCH 004/324] test(rsc-mf): enforce async-node target for server bundles --- tests/integration/rsc-mf/host/modern.config.ts | 6 ++++++ tests/integration/rsc-mf/remote/modern.config.ts | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/tests/integration/rsc-mf/host/modern.config.ts b/tests/integration/rsc-mf/host/modern.config.ts index 70311f4ca58b..1ae1399165c1 100644 --- a/tests/integration/rsc-mf/host/modern.config.ts +++ b/tests/integration/rsc-mf/host/modern.config.ts @@ -19,6 +19,12 @@ export default defineConfig({ }, tools: { bundlerChain(chain) { + const target = chain.get('target'); + const targets = Array.isArray(target) ? target : [target]; + if (targets.some(item => String(item).includes('node'))) { + chain.target('async-node'); + } + chain.resolve.modules .clear() .add(path.resolve(__dirname, 'node_modules')) diff --git a/tests/integration/rsc-mf/remote/modern.config.ts b/tests/integration/rsc-mf/remote/modern.config.ts index edd0d79774c2..9aeec9a165a6 100644 --- a/tests/integration/rsc-mf/remote/modern.config.ts +++ b/tests/integration/rsc-mf/remote/modern.config.ts @@ -22,6 +22,12 @@ export default defineConfig({ }, tools: { bundlerChain(chain) { + const target = chain.get('target'); + const targets = Array.isArray(target) ? target : [target]; + if (targets.some(item => String(item).includes('node'))) { + chain.target('async-node'); + } + chain.resolve.modules .clear() .add(path.resolve(__dirname, 'node_modules')) From 125415a572b057b6fb402ff3efb75ef63174033a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Feb 2026 08:24:42 +0000 Subject: [PATCH 005/324] test(rsc-mf): broaden rsc federation exposure matrix --- .../host/src/server-component-root/App.tsx | 19 +++++ .../HostRemoteActionRunner.tsx | 39 +++++++++ .../rsc-mf/remote/module-federation.config.ts | 6 ++ .../src/components/AsyncRemoteServerInfo.tsx | 5 ++ .../src/components/RemoteClientBadge.tsx | 24 ++++++ .../src/components/RemoteServerDefault.tsx | 10 +++ .../remote/src/components/defaultAction.ts | 5 ++ .../remote/src/components/remoteMeta.ts | 12 +++ .../src/components/serverOnlyDefault.ts | 5 ++ tests/integration/rsc-mf/tests/index.test.ts | 85 ++++++++++++------- 10 files changed, 177 insertions(+), 33 deletions(-) create mode 100644 tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx create mode 100644 tests/integration/rsc-mf/remote/src/components/AsyncRemoteServerInfo.tsx create mode 100644 tests/integration/rsc-mf/remote/src/components/RemoteClientBadge.tsx create mode 100644 tests/integration/rsc-mf/remote/src/components/RemoteServerDefault.tsx create mode 100644 tests/integration/rsc-mf/remote/src/components/defaultAction.ts create mode 100644 tests/integration/rsc-mf/remote/src/components/remoteMeta.ts create mode 100644 tests/integration/rsc-mf/remote/src/components/serverOnlyDefault.ts diff --git a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx index a57dd02a4e0b..e993df8966c0 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx @@ -1,19 +1,38 @@ import 'server-only'; import { Suspense } from 'react'; +import { AsyncRemoteServerInfo } from 'rscRemote/AsyncRemoteServerInfo'; +import RemoteClientBadge from 'rscRemote/RemoteClientBadge'; import { RemoteNestedMixed } from 'rscRemote/RemoteNestedMixed'; +import RemoteServerDefault from 'rscRemote/RemoteServerDefault'; +import remoteMeta, { getRemoteMetaLabel } from 'rscRemote/remoteMeta'; import { getServerOnlyInfo } from 'rscRemote/remoteServerOnly'; +import getServerOnlyDefaultInfo from 'rscRemote/remoteServerOnlyDefault'; import styles from './App.module.less'; +import HostRemoteActionRunner from './HostRemoteActionRunner'; const App = () => { const remoteServerOnlyInfo = getServerOnlyInfo(); + const remoteServerOnlyDefaultInfo = getServerOnlyDefaultInfo(); + const remoteMetaLabel = getRemoteMetaLabel(); return (

Host RSC Module Federation

{remoteServerOnlyInfo}

+

+ {remoteServerOnlyDefaultInfo} +

+

{remoteMeta.kind}

+

{remoteMetaLabel}

+ Loading Remote Async Server Info...
}> + + + Loading Remote RSC...}> + + ); }; diff --git a/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx b/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx new file mode 100644 index 000000000000..c28bc3d9642f --- /dev/null +++ b/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx @@ -0,0 +1,39 @@ +'use client'; + +import { useState } from 'react'; +import { remoteActionEcho } from 'rscRemote/actions'; +import defaultRemoteAction from 'rscRemote/defaultAction'; + +export default function HostRemoteActionRunner() { + const [defaultResult, setDefaultResult] = useState(''); + const [echoResult, setEchoResult] = useState(''); + const [isPending, setIsPending] = useState(false); + + const runActions = async () => { + setIsPending(true); + try { + const [defaultValue, echoValue] = await Promise.all([ + defaultRemoteAction('from-host-client'), + remoteActionEcho('from-host-client'), + ]); + setDefaultResult(defaultValue); + setEchoResult(echoValue); + } finally { + setIsPending(false); + } + }; + + return ( +
+ +

{defaultResult}

+

{echoResult}

+
+ ); +} diff --git a/tests/integration/rsc-mf/remote/module-federation.config.ts b/tests/integration/rsc-mf/remote/module-federation.config.ts index f426d736b889..dd2276df4f56 100644 --- a/tests/integration/rsc-mf/remote/module-federation.config.ts +++ b/tests/integration/rsc-mf/remote/module-federation.config.ts @@ -8,11 +8,17 @@ export default createModuleFederationConfig({ filename: 'static/remoteEntry.js', exposes: { './RemoteClientCounter': './src/components/RemoteClientCounter.tsx', + './RemoteClientBadge': './src/components/RemoteClientBadge.tsx', './RemoteServerCard': './src/components/RemoteServerCard.tsx', + './RemoteServerDefault': './src/components/RemoteServerDefault.tsx', + './AsyncRemoteServerInfo': './src/components/AsyncRemoteServerInfo.tsx', './RemoteNestedMixed': './src/components/RemoteNestedMixed.tsx', './remoteServerOnly': './src/components/serverOnly.ts', + './remoteServerOnlyDefault': './src/components/serverOnlyDefault.ts', + './remoteMeta': './src/components/remoteMeta.ts', './actions': './src/components/actions.ts', './nestedActions': './src/components/nestedActions.ts', + './defaultAction': './src/components/defaultAction.ts', }, shared: { react: { singleton: true }, diff --git a/tests/integration/rsc-mf/remote/src/components/AsyncRemoteServerInfo.tsx b/tests/integration/rsc-mf/remote/src/components/AsyncRemoteServerInfo.tsx new file mode 100644 index 000000000000..ce90b480f85b --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/AsyncRemoteServerInfo.tsx @@ -0,0 +1,5 @@ +export async function AsyncRemoteServerInfo() { + const value = await Promise.resolve('remote-async-server-info-ok'); + + return

{value}

; +} diff --git a/tests/integration/rsc-mf/remote/src/components/RemoteClientBadge.tsx b/tests/integration/rsc-mf/remote/src/components/RemoteClientBadge.tsx new file mode 100644 index 000000000000..f46bcb0d5f33 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/RemoteClientBadge.tsx @@ -0,0 +1,24 @@ +'use client'; + +import 'client-only'; +import { useState } from 'react'; + +export default function RemoteClientBadge({ + initialLabel, +}: { + initialLabel: string; +}) { + const [label, setLabel] = useState(initialLabel); + + return ( +
+

{label}

+ +
+ ); +} diff --git a/tests/integration/rsc-mf/remote/src/components/RemoteServerDefault.tsx b/tests/integration/rsc-mf/remote/src/components/RemoteServerDefault.tsx new file mode 100644 index 000000000000..6644e0014755 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/RemoteServerDefault.tsx @@ -0,0 +1,10 @@ +import { getServerOnlyInfo } from './serverOnly'; + +export default function RemoteServerDefault({ label }: { label: string }) { + return ( +
+

{label}

+

{getServerOnlyInfo()}

+
+ ); +} diff --git a/tests/integration/rsc-mf/remote/src/components/defaultAction.ts b/tests/integration/rsc-mf/remote/src/components/defaultAction.ts new file mode 100644 index 000000000000..09f2458a768d --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/defaultAction.ts @@ -0,0 +1,5 @@ +'use server'; + +export default async function defaultRemoteAction(value: string) { + return `default-action:${value}`; +} diff --git a/tests/integration/rsc-mf/remote/src/components/remoteMeta.ts b/tests/integration/rsc-mf/remote/src/components/remoteMeta.ts new file mode 100644 index 000000000000..703bec9d0b2e --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/remoteMeta.ts @@ -0,0 +1,12 @@ +export const remoteFeatureFlags = ['rsc', 'mf', 'actions'] as const; + +export function getRemoteMetaLabel() { + return remoteFeatureFlags.join('|'); +} + +const remoteMeta = { + kind: 'remote-meta-default', + version: 'v1', +}; + +export default remoteMeta; diff --git a/tests/integration/rsc-mf/remote/src/components/serverOnlyDefault.ts b/tests/integration/rsc-mf/remote/src/components/serverOnlyDefault.ts new file mode 100644 index 000000000000..a3ec2cbae752 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/serverOnlyDefault.ts @@ -0,0 +1,5 @@ +import 'server-only'; + +export default function getServerOnlyDefaultInfo() { + return 'remote-server-only-default-ok'; +} diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 5f7be3b8c44b..e78563e47ab7 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -16,8 +16,6 @@ const fixtureDir = path.resolve(__dirname, '../'); const hostDir = path.resolve(fixtureDir, 'host'); const remoteDir = path.resolve(fixtureDir, 'remote'); const HOST_RSC_URL = '/server-component-root'; -const RSC_RUNTIME_BLOCKER_PATTERN = - /Cannot find (render handler|server bundle) for RSC/; type Mode = 'dev' | 'build'; @@ -56,16 +54,14 @@ function createHostEnv(remotePort: number) { async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { const response = await fetch(`http://127.0.0.1:${hostPort}${HOST_RSC_URL}`); const html = await response.text(); - if (RSC_RUNTIME_BLOCKER_PATTERN.test(html)) { - return { - blocked: true, - html, - }; - } - expect(html).toContain('Host RSC Module Federation'); expect(html).toContain('Remote Federated Tree'); expect(html).toContain('remote-server-only-ok'); + expect(html).toContain('remote-server-only-default-ok'); + expect(html).toContain('remote-meta-default'); + expect(html).toContain('rsc|mf|actions'); + expect(html).toContain('remote-async-server-info-ok'); + expect(html).toContain('Remote Default Server Card'); await page.goto(`http://127.0.0.1:${hostPort}${HOST_RSC_URL}`, { waitUntil: ['networkidle0', 'domcontentloaded'], @@ -75,11 +71,24 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { el => el.textContent?.trim(), ); expect(hostRemoteServerOnly).toBe('remote-server-only-ok'); - - return { - blocked: false, - html, - }; + const hostRemoteServerOnlyDefault = await page.$eval( + '.host-remote-server-only-default', + el => el.textContent?.trim(), + ); + expect(hostRemoteServerOnlyDefault).toBe('remote-server-only-default-ok'); + const hostRemoteMetaKind = await page.$eval('.host-remote-meta-kind', el => + el.textContent?.trim(), + ); + expect(hostRemoteMetaKind).toBe('remote-meta-default'); + const hostRemoteMetaLabel = await page.$eval('.host-remote-meta-label', el => + el.textContent?.trim(), + ); + expect(hostRemoteMetaLabel).toBe('rsc|mf|actions'); + const hostRemoteAsyncServerInfo = await page.$eval( + '.remote-async-server-info', + el => el.textContent?.trim(), + ); + expect(hostRemoteAsyncServerInfo).toBe('remote-async-server-info-ok'); } async function supportRemoteClientAndServerActions({ @@ -129,6 +138,31 @@ async function supportRemoteClientAndServerActions({ remoteAction?.textContent?.trim() === 'remote-action:from-client' ); }); + + let badgeValue = await page.$eval('.remote-client-badge-value', el => + el.textContent?.trim(), + ); + expect(badgeValue).toBe('remote-client-badge-initial'); + await page.click('.remote-client-badge-toggle'); + badgeValue = await page.$eval('.remote-client-badge-value', el => + el.textContent?.trim(), + ); + expect(badgeValue).toBe('remote-client-badge-toggled'); + + await page.click('.host-remote-run-actions'); + await page.waitForFunction(() => { + const defaultActionResult = document.querySelector( + '.host-remote-default-action-result', + ); + const echoActionResult = document.querySelector( + '.host-remote-echo-action-result', + ); + return ( + defaultActionResult?.textContent?.trim() === + 'default-action:from-host-client' && + echoActionResult?.textContent?.trim() === 'remote-action:from-host-client' + ); + }); } function runTests({ mode }: TestConfig) { @@ -140,8 +174,6 @@ function runTests({ mode }: TestConfig) { let page: Page; let browser: Browser; const runtimeErrors: string[] = []; - let runtimeBlocked = false; - let runtimeBlockerHtml = ''; if (skipForLowerNodeVersion()) { return; @@ -206,24 +238,11 @@ function runTests({ mode }: TestConfig) { } }); - it('should render remote RSC content in host app', async () => { - const renderResult = await renderRemoteRscIntoHost({ hostPort, page }); - runtimeBlocked = renderResult.blocked; - runtimeBlockerHtml = renderResult.html; - - if (runtimeBlocked) { - expect(runtimeBlockerHtml).toMatch(RSC_RUNTIME_BLOCKER_PATTERN); - } - }); + it('should render remote RSC content in host app', () => + renderRemoteRscIntoHost({ hostPort, page })); - it('should support remote use client and server actions', async () => { - if (runtimeBlocked) { - expect(runtimeBlockerHtml).toMatch(RSC_RUNTIME_BLOCKER_PATTERN); - return; - } - - await supportRemoteClientAndServerActions({ hostPort, page }); - }); + it('should support remote use client and server actions', () => + supportRemoteClientAndServerActions({ hostPort, page })); if (mode === 'build') { it('should have no browser runtime errors', () => { From 22fa22482647ccc764d357f450f5933761656226 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Feb 2026 04:21:37 +0000 Subject: [PATCH 006/324] chore(tests): use modern-js-v3 pkg.pr.new preview --- pnpm-lock.yaml | 739 ++++++++++++++---- .../i18n/mf/mf-app-provider/package.json | 2 +- .../mf/mf-component-provider/package.json | 2 +- .../i18n/mf/mf-consumer/package.json | 2 +- tests/integration/rsc-mf/host/package.json | 2 +- tests/integration/rsc-mf/remote/package.json | 2 +- 6 files changed, 611 insertions(+), 138 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cbaeb4fa9755..b3398b1fee76 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3030,8 +3030,8 @@ importers: specifier: workspace:* version: link:../../../../../packages/runtime/plugin-runtime '@module-federation/modern-js-v3': - specifier: 2.0.0 - version: 2.0.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + specifier: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437 + version: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) i18next: specifier: 25.7.4 version: 25.7.4(typescript@5.9.3) @@ -3061,8 +3061,8 @@ importers: specifier: workspace:* version: link:../../../../../packages/runtime/plugin-runtime '@module-federation/modern-js-v3': - specifier: 2.0.0 - version: 2.0.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + specifier: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437 + version: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) i18next: specifier: 25.7.4 version: 25.7.4(typescript@5.9.3) @@ -3092,8 +3092,8 @@ importers: specifier: workspace:* version: link:../../../../../packages/runtime/plugin-runtime '@module-federation/modern-js-v3': - specifier: 2.0.0 - version: 2.0.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + specifier: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437 + version: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) '@module-federation/runtime': specifier: 2.0.0 version: 2.0.0 @@ -3478,7 +3478,7 @@ importers: version: 19.2.4(react@19.2.4) react-server-dom-rspack: specifier: 0.0.1-beta.0 - version: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) server-only: specifier: ^0.0.1 version: 0.0.1 @@ -3521,7 +3521,7 @@ importers: version: 19.2.4(react@19.2.4) react-server-dom-rspack: specifier: 0.0.1-beta.0 - version: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) server-only: specifier: ^0.0.1 version: 0.0.1 @@ -3559,8 +3559,8 @@ importers: specifier: workspace:* version: link:../../../../packages/runtime/plugin-runtime '@module-federation/modern-js-v3': - specifier: 2.0.0 - version: 2.0.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + specifier: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437 + version: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) '@module-federation/runtime': specifier: 2.0.0 version: 2.0.0 @@ -3575,7 +3575,7 @@ importers: version: 19.2.4(react@19.2.4) react-server-dom-rspack: specifier: 0.0.1-beta.0 - version: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) server-only: specifier: ^0.0.1 version: 0.0.1 @@ -3614,8 +3614,8 @@ importers: specifier: workspace:* version: link:../../../../packages/runtime/plugin-runtime '@module-federation/modern-js-v3': - specifier: 2.0.0 - version: 2.0.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + specifier: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437 + version: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) '@module-federation/runtime': specifier: 2.0.0 version: 2.0.0 @@ -3630,7 +3630,7 @@ importers: version: 19.2.4(react@19.2.4) react-server-dom-rspack: specifier: 0.0.1-beta.0 - version: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) server-only: specifier: ^0.0.1 version: 0.0.1 @@ -3679,7 +3679,7 @@ importers: version: 19.2.4(react@19.2.4) react-server-dom-rspack: specifier: 0.0.1-beta.0 - version: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) server-only: specifier: ^0.0.1 version: 0.0.1 @@ -3725,7 +3725,7 @@ importers: version: 19.2.4(react@19.2.4) react-server-dom-rspack: specifier: 0.0.1-beta.0 - version: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) server-only: specifier: ^0.0.1 version: 0.0.1 @@ -6153,11 +6153,16 @@ packages: '@modern-js/utils@2.70.4': resolution: {integrity: sha512-LQrwyGlFhsH2BmZxStF0TPeStm6aumf4N1J+ZyObLw5URrN4o8vCyeyqrPVciICeoTqhHg2GIArJWB5PXRcUig==} - '@module-federation/bridge-react-webpack-plugin@2.0.0': - resolution: {integrity: sha512-AVT/rZK6RHva6ZTYfsyQ8oP4xYNTws3OzqKW/YxWaLXwQ3oG9ZbF7fKl4jIKoMKuuy2L9MGVXS4CYPZy0s8fXg==} + '@module-federation/bridge-react-webpack-plugin@2.0.1': + resolution: {integrity: sha512-D7LMW5EMAJShOMR1aZDAJ6s+MdsYDHaQyJADLQ3LaY0sne/BkVqkPikUwcO1IwOwKbXjYsDlQVOEvk9wZVRFhA==} - '@module-federation/bridge-react@2.0.0': - resolution: {integrity: sha512-TVkIxvlRk5vo4CYuId3Kj2JcyjkhiMq2LwKDcZsXC+8Ohwt8olfKJ6y40rCViXwGUSns1TRBtzPD6EikMXlYZg==} + '@module-federation/bridge-react-webpack-plugin@https://pkg.pr.new/module-federation/core/@module-federation/bridge-react-webpack-plugin@73bd1ddb2266002f586a872ddae8020052c5457c': + resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/bridge-react-webpack-plugin@73bd1ddb2266002f586a872ddae8020052c5457c} + version: 2.0.1 + + '@module-federation/bridge-react@https://pkg.pr.new/module-federation/core/@module-federation/bridge-react@73bd1ddb2266002f586a872ddae8020052c5457c': + resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/bridge-react@73bd1ddb2266002f586a872ddae8020052c5457c} + version: 2.0.1 peerDependencies: react: '>=16.9.0' react-dom: '>=16.9.0' @@ -6169,16 +6174,23 @@ packages: react-router-dom: optional: true - '@module-federation/bridge-shared@2.0.0': - resolution: {integrity: sha512-kid2tyGOjX02da/kjjHtkw035M7NHiD8UGynBfZBi9mznAWUUReT0n8EHyvTIdBpj7S4AQKHpwppPCrg7Rv3ZA==} + '@module-federation/bridge-shared@https://pkg.pr.new/module-federation/core/@module-federation/bridge-shared@73bd1ddb2266002f586a872ddae8020052c5457c': + resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/bridge-shared@73bd1ddb2266002f586a872ddae8020052c5457c} + version: 2.0.1 - '@module-federation/cli@2.0.0': - resolution: {integrity: sha512-IWGWbdgoeNcuA5jzqPr6pLTN1hovMQh9A1lgJp5fAvKfICfFXKq7K8nwMAQrWD6iEKApIenI0madk1Dg2PU3pw==} + '@module-federation/cli@2.0.1': + resolution: {integrity: sha512-2SL5Y8iODNX10y9T3CBLhHjSXo4afnA1BK82m4sNfZebuVO+o34bxewqwod9xfWq9xhTZmOSFZ+n+lgTKRv+CQ==} engines: {node: '>=16.0.0'} hasBin: true - '@module-federation/data-prefetch@2.0.0': - resolution: {integrity: sha512-KPyZoqNrb5WgFY2owYnMaO2Mg2DYD6KXLVI7GPguj7Z/4pPKEC+SUjWU2FuSfTeyE6ZIi0iFGdwerxzlQ6nfmw==} + '@module-federation/cli@https://pkg.pr.new/module-federation/core/@module-federation/cli@73bd1ddb2266002f586a872ddae8020052c5457c': + resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/cli@73bd1ddb2266002f586a872ddae8020052c5457c} + version: 2.0.1 + engines: {node: '>=16.0.0'} + hasBin: true + + '@module-federation/data-prefetch@2.0.1': + resolution: {integrity: sha512-Kq0P1OABGt6QAvs6TaE/zY9Ut9Y/oJFrzoSF3eWaCYbUAr2KD2SpTyMsPz4ssBzjeKXTgimugh6tHHd6mpCBIQ==} peerDependencies: react: '>=16.9.0' react-dom: '>=16.9.0' @@ -6188,8 +6200,30 @@ packages: react-dom: optional: true - '@module-federation/dts-plugin@2.0.0': - resolution: {integrity: sha512-YyYMgLNARKdf3FLihnIzzUTgafHrqzR9YnKPmrfuCm2Jit+USqFT4QO58hcb0F5KSEyjB2ARPz9RM4XAVZhzMg==} + '@module-federation/data-prefetch@https://pkg.pr.new/module-federation/core/@module-federation/data-prefetch@73bd1ddb2266002f586a872ddae8020052c5457c': + resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/data-prefetch@73bd1ddb2266002f586a872ddae8020052c5457c} + version: 2.0.1 + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + + '@module-federation/dts-plugin@2.0.1': + resolution: {integrity: sha512-PLneTsf1fQS5/RTBedtLAAmCPRdMfIlhfJkOa8QH3WDJaQsqm8Wb3r2cTUBf2aNj/bP3aH/y6Hs9JFB/4x0l5g==} + peerDependencies: + typescript: ^4.9.0 || ^5.0.0 + vue-tsc: '>=1.0.24' + peerDependenciesMeta: + vue-tsc: + optional: true + + '@module-federation/dts-plugin@https://pkg.pr.new/module-federation/core/@module-federation/dts-plugin@73bd1ddb2266002f586a872ddae8020052c5457c': + resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/dts-plugin@73bd1ddb2266002f586a872ddae8020052c5457c} + version: 2.0.1 peerDependencies: typescript: ^4.9.0 || ^5.0.0 vue-tsc: '>=1.0.24' @@ -6197,8 +6231,25 @@ packages: vue-tsc: optional: true - '@module-federation/enhanced@2.0.0': - resolution: {integrity: sha512-xeVrGvypYMvN8gJulbro3j1t8+aS1f9xjj4quwAAqgJF0Nz8bt7sXUYJyjUHPmC2UZsShZ0GnPHJNtI8/2GYjA==} + '@module-federation/enhanced@2.0.1': + resolution: {integrity: sha512-EZIARQ/8ScoTP6PV8+E4SsmMYWK4ErrikZJ0G/FX8wvK8mCtdoKatFtvDN9++P6Nl78kN9zHYgAV4AHKdBVjfQ==} + version: 2.0.1 + hasBin: true + peerDependencies: + typescript: ^4.9.0 || ^5.0.0 + vue-tsc: '>=1.0.24' + webpack: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + vue-tsc: + optional: true + webpack: + optional: true + + '@module-federation/enhanced@https://pkg.pr.new/module-federation/core/@module-federation/enhanced@73bd1ddb2266002f586a872ddae8020052c5457c': + resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/enhanced@73bd1ddb2266002f586a872ddae8020052c5457c} + version: 2.0.1 hasBin: true peerDependencies: typescript: ^4.9.0 || ^5.0.0 @@ -6218,19 +6269,42 @@ packages: '@module-federation/error-codes@2.0.0': resolution: {integrity: sha512-9oE+hXuPv2zej7AxJ5hOgeRqlPs98meooV2FiutTfftLAyy2N6+Kwmmz5NR9d9t91weJj8N0cSHFoyenNHKTVg==} - '@module-federation/inject-external-runtime-core-plugin@2.0.0': - resolution: {integrity: sha512-aZ6f4UU7KM5zBnHf3xsb2guqsfaEd6IlmuldbpED3JPk4ITwZk0DbvxRMr4prde7cfj8RH0nKMz2kmMncp+lIQ==} + '@module-federation/error-codes@2.0.1': + resolution: {integrity: sha512-2bJF/ft+qL9L6Zvq2t/G9/f/0wFL73cM8/NJ04uyYz9BjIgvx28K5qu8/6+IwgEEKATG7vOhBBVj6wH3S+5ASA==} + + '@module-federation/error-codes@https://pkg.pr.new/module-federation/core/@module-federation/error-codes@73bd1ddb2266002f586a872ddae8020052c5457c': + resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/error-codes@73bd1ddb2266002f586a872ddae8020052c5457c} + version: 2.0.1 + + '@module-federation/inject-external-runtime-core-plugin@2.0.1': + resolution: {integrity: sha512-oAA7G+4GCHM+WRYfscR/x4GwCyM9CEqfdD9/x2L6y8mtLWK9anRLKTocsI759AvzXsbT1m3EQ5ki1O6wlwDu3g==} + version: 2.0.1 peerDependencies: - '@module-federation/runtime-tools': 2.0.0 + '@module-federation/runtime-tools': 2.0.1 + + '@module-federation/inject-external-runtime-core-plugin@https://pkg.pr.new/module-federation/core/@module-federation/inject-external-runtime-core-plugin@73bd1ddb2266002f586a872ddae8020052c5457c': + resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/inject-external-runtime-core-plugin@73bd1ddb2266002f586a872ddae8020052c5457c} + version: 2.0.1 + peerDependencies: + '@module-federation/runtime-tools': 2.0.1 + + '@module-federation/managers@2.0.1': + resolution: {integrity: sha512-KR01lSlcYRQ9C6hW2a8CQQtAE0LvfTLgtV/6ZNUTagw8sRfeDln+ggrZsYilKu9zl0i8RPDgpv/kS60o4lcxCQ==} - '@module-federation/managers@2.0.0': - resolution: {integrity: sha512-ZmkRIujH+T3xvkmy04TNvviFH8xFOrNeKCLb4tlH4ifU/kLfjTu+PYO/KAEIsgtmrDnd52zTf22dg3ok85OAHA==} + '@module-federation/managers@https://pkg.pr.new/module-federation/core/@module-federation/managers@73bd1ddb2266002f586a872ddae8020052c5457c': + resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/managers@73bd1ddb2266002f586a872ddae8020052c5457c} + version: 2.0.1 - '@module-federation/manifest@2.0.0': - resolution: {integrity: sha512-AXwYyGiDJdfP9MteKyIdJrLwG5tp4qKaq0uOPiWHilYN3/21G0DM7f30HgJqgx3WSTFSh7hcq0a3V3EZHH/9TA==} + '@module-federation/manifest@2.0.1': + resolution: {integrity: sha512-p8nYGjHWp17MsYdW/Vv0ogBDiTTsI1PHWPQbvVIqLQXDqwiesaRSRR1zziECXQoEL8lV5Bs+uSkcaJGhea9P+A==} - '@module-federation/modern-js-v3@2.0.0': - resolution: {integrity: sha512-SKoKwh1rlgG+LXyV1eh2ELE05UhFVkhRXNIjwrSFjg9xVBih4oI10yBeOdrzCw8vdY/+BvXWxka88Av6tzh49Q==} + '@module-federation/manifest@https://pkg.pr.new/module-federation/core/@module-federation/manifest@73bd1ddb2266002f586a872ddae8020052c5457c': + resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/manifest@73bd1ddb2266002f586a872ddae8020052c5457c} + version: 2.0.1 + + '@module-federation/modern-js-v3@https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437': + resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437} + version: 2.0.1 peerDependencies: react: '>=17' react-dom: '>=17' @@ -6248,16 +6322,18 @@ packages: vue-tsc: optional: true - '@module-federation/node@2.7.31': - resolution: {integrity: sha512-NSa0PFDKDLxmtfmCVHW9RhtfD9mcNOrp1d+cjVEoxb5x8dDI4jQTi1o3nsa9ettxs3bVtWhAUEQUNQBQ6ZA+Hw==} + '@module-federation/node@2.7.32': + resolution: {integrity: sha512-hUj5v2GGwpNzl2gaJS4AyzCYRzJBhN8875A+ucKF9tq3jaQb5zpy3izYMISqqbN2q9a7jz3nEUgwAh3pjri+rQ==} + version: 2.7.32 peerDependencies: webpack: ^5.40.0 peerDependenciesMeta: webpack: optional: true - '@module-federation/rsbuild-plugin@2.0.0': - resolution: {integrity: sha512-kbYX9ti2C/m734Na3ICy+xBopqL6O3sGK7Vx7N+WA/PkEGc5J0WEPwVwXtnf4WRj9ic9OV5+w2i6y1tG8ONt1g==} + '@module-federation/rsbuild-plugin@https://pkg.pr.new/module-federation/core/@module-federation/rsbuild-plugin@73bd1ddb2266002f586a872ddae8020052c5457c': + resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/rsbuild-plugin@73bd1ddb2266002f586a872ddae8020052c5457c} + version: 2.0.1 engines: {node: '>=16.0.0'} peerDependencies: '@rsbuild/core': 2.0.0-beta.2 @@ -6265,8 +6341,22 @@ packages: '@rsbuild/core': optional: true - '@module-federation/rspack@2.0.0': - resolution: {integrity: sha512-1kziarKrPRM+rJax/AaMEZTwu7ORGed2xSxfdoP9GEbAFEGyNliadvw4kB6PqAfLad3PI4lQMX2vGMLI1KoyVQ==} + '@module-federation/rspack@2.0.1': + resolution: {integrity: sha512-SAlNE8iclFmzrKtx3/C2GivXYx6nPzx4MgQV01QG/a4LpnLbwlxzdZu3rqQ2swp4NNWT/t/GT7Y+7gfhyVa7mg==} + version: 2.0.1 + peerDependencies: + '@rspack/core': ^0.7.0 || ^1.0.0 || ^2.0.0-0 + typescript: ^4.9.0 || ^5.0.0 + vue-tsc: '>=1.0.24' + peerDependenciesMeta: + typescript: + optional: true + vue-tsc: + optional: true + + '@module-federation/rspack@https://pkg.pr.new/module-federation/core/@module-federation/rspack@73bd1ddb2266002f586a872ddae8020052c5457c': + resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/rspack@73bd1ddb2266002f586a872ddae8020052c5457c} + version: 2.0.1 peerDependencies: '@rspack/core': ^0.7.0 || ^1.0.0 || ^2.0.0-0 typescript: ^4.9.0 || ^5.0.0 @@ -6283,11 +6373,22 @@ packages: '@module-federation/runtime-core@2.0.0': resolution: {integrity: sha512-UhIGUs7Mg+TwMI2lgaLnj4UehpoyXbR7HDb2+vLikgBulPmFtodeWfsxCgENEwKsIY1vS0lOun15lNOn1vo3Xg==} + '@module-federation/runtime-core@2.0.1': + resolution: {integrity: sha512-gOuCPSHoQGUGwlxfSTMInFX+QvLxdEWegGGMiLdU5vqbXuva4E9M+kXBBO7/0MkcBPMmVs0wOJGm0XOLeV2f1Q==} + + '@module-federation/runtime-core@https://pkg.pr.new/module-federation/core/@module-federation/runtime-core@73bd1ddb2266002f586a872ddae8020052c5457c': + resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/runtime-core@73bd1ddb2266002f586a872ddae8020052c5457c} + version: 2.0.1 + '@module-federation/runtime-tools@0.22.0': resolution: {integrity: sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==} - '@module-federation/runtime-tools@2.0.0': - resolution: {integrity: sha512-eMDQN4hYpwvUnCNMjfQdtPVzYaO2DdauemHVc4HnyibgqijRzBwJh9bI2ph4R1xfYEm18+QmTrfXrRlaK2Xizw==} + '@module-federation/runtime-tools@2.0.1': + resolution: {integrity: sha512-AStdwBtsGB3jIfDg9oP+KyVPsimdaeHsP855gqCxDp1hi2+GKjlZWZx9ThkS8NytVSXSUysxqoUL1ivDoKgcCQ==} + + '@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c': + resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c} + version: 2.0.1 '@module-federation/runtime@0.22.0': resolution: {integrity: sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==} @@ -6295,20 +6396,42 @@ packages: '@module-federation/runtime@2.0.0': resolution: {integrity: sha512-vPxQrmQNq3Z1T+1fkHEvFwTdJq9wuCLvdp/lpu9k2Oy7QP/Pj6QoQ/S7J5MCIAoRwj8Wj3z3ma21/DyHwLGvzA==} + '@module-federation/runtime@2.0.1': + resolution: {integrity: sha512-UQ72P5Oo40dS6vdhHetwTtIsbGciEr+bjoYvDgh1WLPfFlTYd8zo9cLfqaf3juuPfV3cMVARAVPmh16lQYpUGA==} + + '@module-federation/runtime@https://pkg.pr.new/module-federation/core/@module-federation/runtime@73bd1ddb2266002f586a872ddae8020052c5457c': + resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/runtime@73bd1ddb2266002f586a872ddae8020052c5457c} + version: 2.0.1 + '@module-federation/sdk@0.22.0': resolution: {integrity: sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==} '@module-federation/sdk@2.0.0': resolution: {integrity: sha512-JYd1wTulsaoLT7HTk2oXL5y5797Z+H4mzxuUEKnSJo7R34RZSqehsqPSND7n0HT/1nf7uyn0Rb4qBfR3BVvdHQ==} - '@module-federation/third-party-dts-extractor@2.0.0': - resolution: {integrity: sha512-B99+Wkbd2xIodVTjNCeFtFC89Uh2/AtYkSESlz4+6Cec42wyqrGxyfYm4qRY0LhJI+YmZXLk/RTm85m15eBKKg==} + '@module-federation/sdk@2.0.1': + resolution: {integrity: sha512-32PwudojGjog51cwpTali7D6ud82oVgsyvOx9JjAzhvXBX96YI4mRsursuWcthDxmigJP9ZvUTXDuRUEDh1OQA==} + + '@module-federation/sdk@https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c': + resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c} + version: 2.0.1 + + '@module-federation/third-party-dts-extractor@2.0.1': + resolution: {integrity: sha512-neKSr6FNUeGRh+YR57l/QZUzPytJXuJx+babF7j5iGJG3FP+kfizr6QD0hgVis5KEoXMVbQ8yyvG0slERizeyw==} + + '@module-federation/third-party-dts-extractor@https://pkg.pr.new/module-federation/core/@module-federation/third-party-dts-extractor@73bd1ddb2266002f586a872ddae8020052c5457c': + resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/third-party-dts-extractor@73bd1ddb2266002f586a872ddae8020052c5457c} + version: 2.0.1 '@module-federation/webpack-bundler-runtime@0.22.0': resolution: {integrity: sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==} - '@module-federation/webpack-bundler-runtime@2.0.0': - resolution: {integrity: sha512-XxiFR/A1G1fa9hTyylWNbs6yEU2hC7FqHAArFptD4U9qp/xyoLgqbK4M8LwltOAyAM8hRofcMdSyiRKVlWqAfQ==} + '@module-federation/webpack-bundler-runtime@2.0.1': + resolution: {integrity: sha512-u1NId3SF4lHDTmD2CHFEszulmXmIq1TGw9JYvnLx5rKJL7xt3aNxcb1GvkaYbRNVBXhSMjJ75E5LsQlZzyBx9A==} + + '@module-federation/webpack-bundler-runtime@https://pkg.pr.new/module-federation/core/@module-federation/webpack-bundler-runtime@73bd1ddb2266002f586a872ddae8020052c5457c': + resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/webpack-bundler-runtime@73bd1ddb2266002f586a872ddae8020052c5457c} + version: 2.0.1 '@mswjs/interceptors@0.39.8': resolution: {integrity: sha512-2+BzZbjRO7Ct61k8fMNHEtoKjeWI9pIlHFTqBwZ5icHpqszIgEZbjb1MW5Z0+bITTCTl3gk4PDBxs9tA/csXvA==} @@ -16782,16 +16905,22 @@ snapshots: lodash: 4.17.23 rslog: 1.3.2 - '@module-federation/bridge-react-webpack-plugin@2.0.0': + '@module-federation/bridge-react-webpack-plugin@2.0.1': dependencies: - '@module-federation/sdk': 2.0.0 + '@module-federation/sdk': 2.0.1 '@types/semver': 7.5.8 semver: 7.6.3 - '@module-federation/bridge-react@2.0.0(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)': + '@module-federation/bridge-react-webpack-plugin@https://pkg.pr.new/module-federation/core/@module-federation/bridge-react-webpack-plugin@73bd1ddb2266002f586a872ddae8020052c5457c': dependencies: - '@module-federation/bridge-shared': 2.0.0 - '@module-federation/sdk': 2.0.0 + '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c + '@types/semver': 7.5.8 + semver: 7.6.3 + + '@module-federation/bridge-react@https://pkg.pr.new/module-federation/core/@module-federation/bridge-react@73bd1ddb2266002f586a872ddae8020052c5457c(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)': + dependencies: + '@module-federation/bridge-shared': https://pkg.pr.new/module-federation/core/@module-federation/bridge-shared@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c lru-cache: 10.4.3 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -16800,12 +16929,12 @@ snapshots: react-router: 7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-router-dom: 7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@module-federation/bridge-shared@2.0.0': {} + '@module-federation/bridge-shared@https://pkg.pr.new/module-federation/core/@module-federation/bridge-shared@73bd1ddb2266002f586a872ddae8020052c5457c': {} - '@module-federation/cli@2.0.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': + '@module-federation/cli@2.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: - '@module-federation/dts-plugin': 2.0.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/sdk': 2.0.0 + '@module-federation/dts-plugin': 2.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/sdk': 2.0.1 chalk: 3.0.0 commander: 11.1.0 jiti: 2.4.2 @@ -16817,21 +16946,45 @@ snapshots: - utf-8-validate - vue-tsc - '@module-federation/data-prefetch@2.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@module-federation/cli@https://pkg.pr.new/module-federation/core/@module-federation/cli@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: - '@module-federation/runtime': 2.0.0 - '@module-federation/sdk': 2.0.0 + '@module-federation/dts-plugin': https://pkg.pr.new/module-federation/core/@module-federation/dts-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c + chalk: 3.0.0 + commander: 11.1.0 + jiti: 2.4.2 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - typescript + - utf-8-validate + - vue-tsc + + '@module-federation/data-prefetch@2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@module-federation/runtime': 2.0.1 + '@module-federation/sdk': 2.0.1 fs-extra: 9.1.0 optionalDependencies: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@module-federation/dts-plugin@2.0.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': + '@module-federation/data-prefetch@https://pkg.pr.new/module-federation/core/@module-federation/data-prefetch@73bd1ddb2266002f586a872ddae8020052c5457c(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@module-federation/error-codes': 2.0.0 - '@module-federation/managers': 2.0.0 - '@module-federation/sdk': 2.0.0 - '@module-federation/third-party-dts-extractor': 2.0.0 + '@module-federation/runtime': https://pkg.pr.new/module-federation/core/@module-federation/runtime@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c + fs-extra: 9.1.0 + optionalDependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@module-federation/dts-plugin@2.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@module-federation/error-codes': 2.0.1 + '@module-federation/managers': 2.0.1 + '@module-federation/sdk': 2.0.1 + '@module-federation/third-party-dts-extractor': 2.0.1 adm-zip: 0.5.16 ansi-colors: 4.1.3 axios: 1.13.5(debug@4.4.3) @@ -16851,19 +17004,127 @@ snapshots: - supports-color - utf-8-validate - '@module-federation/enhanced@2.0.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': + '@module-federation/dts-plugin@https://pkg.pr.new/module-federation/core/@module-federation/dts-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: - '@module-federation/bridge-react-webpack-plugin': 2.0.0 - '@module-federation/cli': 2.0.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/data-prefetch': 2.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@module-federation/dts-plugin': 2.0.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/error-codes': 2.0.0 - '@module-federation/inject-external-runtime-core-plugin': 2.0.0(@module-federation/runtime-tools@2.0.0) - '@module-federation/managers': 2.0.0 - '@module-federation/manifest': 2.0.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/rspack': 2.0.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/runtime-tools': 2.0.0 - '@module-federation/sdk': 2.0.0 + '@module-federation/error-codes': https://pkg.pr.new/module-federation/core/@module-federation/error-codes@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/managers': https://pkg.pr.new/module-federation/core/@module-federation/managers@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/third-party-dts-extractor': https://pkg.pr.new/module-federation/core/@module-federation/third-party-dts-extractor@73bd1ddb2266002f586a872ddae8020052c5457c + adm-zip: 0.5.16 + ansi-colors: 4.1.3 + axios: 1.13.5(debug@4.4.3) + chalk: 3.0.0 + fs-extra: 9.1.0 + isomorphic-ws: 5.0.0(ws@8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + lodash.clonedeepwith: 4.5.0 + log4js: 6.9.1 + node-schedule: 2.1.1 + rambda: 9.4.2 + typescript: 5.9.3 + ws: 8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + + '@module-federation/enhanced@2.0.1(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': + dependencies: + '@module-federation/bridge-react-webpack-plugin': 2.0.1 + '@module-federation/cli': 2.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/data-prefetch': 2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@module-federation/dts-plugin': 2.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/error-codes': 2.0.1 + '@module-federation/inject-external-runtime-core-plugin': 2.0.1(@module-federation/runtime-tools@2.0.1) + '@module-federation/managers': 2.0.1 + '@module-federation/manifest': 2.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/rspack': 2.0.1(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/runtime-tools': 2.0.1 + '@module-federation/sdk': 2.0.1 + btoa: 1.2.1 + schema-utils: 4.3.3 + upath: 2.0.1 + optionalDependencies: + typescript: 5.9.3 + webpack: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2) + transitivePeerDependencies: + - '@rspack/core' + - bufferutil + - debug + - react + - react-dom + - supports-color + - utf-8-validate + + '@module-federation/enhanced@2.0.1(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': + dependencies: + '@module-federation/bridge-react-webpack-plugin': 2.0.1 + '@module-federation/cli': 2.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/data-prefetch': 2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@module-federation/dts-plugin': 2.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/error-codes': 2.0.1 + '@module-federation/inject-external-runtime-core-plugin': 2.0.1(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c) + '@module-federation/managers': 2.0.1 + '@module-federation/manifest': 2.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/rspack': 2.0.1(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/runtime-tools': 2.0.1 + '@module-federation/sdk': 2.0.1 + btoa: 1.2.1 + schema-utils: 4.3.3 + upath: 2.0.1 + optionalDependencies: + typescript: 5.9.3 + webpack: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2) + transitivePeerDependencies: + - '@rspack/core' + - bufferutil + - debug + - react + - react-dom + - supports-color + - utf-8-validate + + '@module-federation/enhanced@https://pkg.pr.new/module-federation/core/@module-federation/enhanced@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': + dependencies: + '@module-federation/bridge-react-webpack-plugin': https://pkg.pr.new/module-federation/core/@module-federation/bridge-react-webpack-plugin@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/cli': https://pkg.pr.new/module-federation/core/@module-federation/cli@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/data-prefetch': https://pkg.pr.new/module-federation/core/@module-federation/data-prefetch@73bd1ddb2266002f586a872ddae8020052c5457c(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@module-federation/dts-plugin': https://pkg.pr.new/module-federation/core/@module-federation/dts-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/error-codes': https://pkg.pr.new/module-federation/core/@module-federation/error-codes@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/inject-external-runtime-core-plugin': https://pkg.pr.new/module-federation/core/@module-federation/inject-external-runtime-core-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(@module-federation/runtime-tools@2.0.1) + '@module-federation/managers': https://pkg.pr.new/module-federation/core/@module-federation/managers@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/manifest': https://pkg.pr.new/module-federation/core/@module-federation/manifest@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/rspack': https://pkg.pr.new/module-federation/core/@module-federation/rspack@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/runtime-tools': https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c + btoa: 1.2.1 + schema-utils: 4.3.3 + upath: 2.0.1 + optionalDependencies: + typescript: 5.9.3 + webpack: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2) + transitivePeerDependencies: + - '@rspack/core' + - bufferutil + - debug + - react + - react-dom + - supports-color + - utf-8-validate + + '@module-federation/enhanced@https://pkg.pr.new/module-federation/core/@module-federation/enhanced@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': + dependencies: + '@module-federation/bridge-react-webpack-plugin': https://pkg.pr.new/module-federation/core/@module-federation/bridge-react-webpack-plugin@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/cli': https://pkg.pr.new/module-federation/core/@module-federation/cli@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/data-prefetch': https://pkg.pr.new/module-federation/core/@module-federation/data-prefetch@73bd1ddb2266002f586a872ddae8020052c5457c(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@module-federation/dts-plugin': https://pkg.pr.new/module-federation/core/@module-federation/dts-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/error-codes': https://pkg.pr.new/module-federation/core/@module-federation/error-codes@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/inject-external-runtime-core-plugin': https://pkg.pr.new/module-federation/core/@module-federation/inject-external-runtime-core-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c) + '@module-federation/managers': https://pkg.pr.new/module-federation/core/@module-federation/managers@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/manifest': https://pkg.pr.new/module-federation/core/@module-federation/manifest@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/rspack': https://pkg.pr.new/module-federation/core/@module-federation/rspack@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/runtime-tools': https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c btoa: 1.2.1 schema-utils: 4.3.3 upath: 2.0.1 @@ -16883,21 +17144,43 @@ snapshots: '@module-federation/error-codes@2.0.0': {} - '@module-federation/inject-external-runtime-core-plugin@2.0.0(@module-federation/runtime-tools@2.0.0)': + '@module-federation/error-codes@2.0.1': {} + + '@module-federation/error-codes@https://pkg.pr.new/module-federation/core/@module-federation/error-codes@73bd1ddb2266002f586a872ddae8020052c5457c': {} + + '@module-federation/inject-external-runtime-core-plugin@2.0.1(@module-federation/runtime-tools@2.0.1)': dependencies: - '@module-federation/runtime-tools': 2.0.0 + '@module-federation/runtime-tools': 2.0.1 - '@module-federation/managers@2.0.0': + '@module-federation/inject-external-runtime-core-plugin@2.0.1(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)': dependencies: - '@module-federation/sdk': 2.0.0 + '@module-federation/runtime-tools': https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c + + '@module-federation/inject-external-runtime-core-plugin@https://pkg.pr.new/module-federation/core/@module-federation/inject-external-runtime-core-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(@module-federation/runtime-tools@2.0.1)': + dependencies: + '@module-federation/runtime-tools': 2.0.1 + + '@module-federation/inject-external-runtime-core-plugin@https://pkg.pr.new/module-federation/core/@module-federation/inject-external-runtime-core-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)': + dependencies: + '@module-federation/runtime-tools': https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c + + '@module-federation/managers@2.0.1': + dependencies: + '@module-federation/sdk': 2.0.1 find-pkg: 2.0.0 fs-extra: 9.1.0 - '@module-federation/manifest@2.0.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': + '@module-federation/managers@https://pkg.pr.new/module-federation/core/@module-federation/managers@73bd1ddb2266002f586a872ddae8020052c5457c': dependencies: - '@module-federation/dts-plugin': 2.0.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/managers': 2.0.0 - '@module-federation/sdk': 2.0.0 + '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c + find-pkg: 2.0.0 + fs-extra: 9.1.0 + + '@module-federation/manifest@2.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@module-federation/dts-plugin': 2.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/managers': 2.0.1 + '@module-federation/sdk': 2.0.1 chalk: 3.0.0 find-pkg: 2.0.0 transitivePeerDependencies: @@ -16908,15 +17191,30 @@ snapshots: - utf-8-validate - vue-tsc - '@module-federation/modern-js-v3@2.0.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': + '@module-federation/manifest@https://pkg.pr.new/module-federation/core/@module-federation/manifest@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: - '@module-federation/bridge-react': 2.0.0(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) - '@module-federation/cli': 2.0.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/enhanced': 2.0.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) - '@module-federation/node': 2.7.31(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) - '@module-federation/rsbuild-plugin': 2.0.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) - '@module-federation/runtime': 2.0.0 - '@module-federation/sdk': 2.0.0 + '@module-federation/dts-plugin': https://pkg.pr.new/module-federation/core/@module-federation/dts-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/managers': https://pkg.pr.new/module-federation/core/@module-federation/managers@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c + chalk: 3.0.0 + find-pkg: 2.0.0 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - typescript + - utf-8-validate + - vue-tsc + + '@module-federation/modern-js-v3@https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': + dependencies: + '@module-federation/bridge-react': https://pkg.pr.new/module-federation/core/@module-federation/bridge-react@73bd1ddb2266002f586a872ddae8020052c5457c(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + '@module-federation/cli': https://pkg.pr.new/module-federation/core/@module-federation/cli@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/enhanced': https://pkg.pr.new/module-federation/core/@module-federation/enhanced@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + '@module-federation/node': 2.7.32(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + '@module-federation/rsbuild-plugin': https://pkg.pr.new/module-federation/core/@module-federation/rsbuild-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + '@module-federation/runtime': https://pkg.pr.new/module-federation/core/@module-federation/runtime@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c '@swc/helpers': 0.5.18 fs-extra: 11.3.0 jiti: 2.4.2 @@ -16938,11 +17236,41 @@ snapshots: - utf-8-validate - webpack - '@module-federation/node@2.7.31(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': + '@module-federation/modern-js-v3@https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': dependencies: - '@module-federation/enhanced': 2.0.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) - '@module-federation/runtime': 2.0.0 - '@module-federation/sdk': 2.0.0 + '@module-federation/bridge-react': https://pkg.pr.new/module-federation/core/@module-federation/bridge-react@73bd1ddb2266002f586a872ddae8020052c5457c(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + '@module-federation/cli': https://pkg.pr.new/module-federation/core/@module-federation/cli@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/enhanced': https://pkg.pr.new/module-federation/core/@module-federation/enhanced@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + '@module-federation/node': 2.7.32(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + '@module-federation/rsbuild-plugin': https://pkg.pr.new/module-federation/core/@module-federation/rsbuild-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + '@module-federation/runtime': https://pkg.pr.new/module-federation/core/@module-federation/runtime@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c + '@swc/helpers': 0.5.18 + fs-extra: 11.3.0 + jiti: 2.4.2 + lru-cache: 10.4.3 + node-fetch: 3.3.2 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-error-boundary: 4.1.2(react@19.2.4) + optionalDependencies: + react-router: 7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react-router-dom: 7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + typescript: 5.9.3 + transitivePeerDependencies: + - '@rsbuild/core' + - '@rspack/core' + - bufferutil + - debug + - supports-color + - utf-8-validate + - webpack + + '@module-federation/node@2.7.32(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': + dependencies: + '@module-federation/enhanced': 2.0.1(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + '@module-federation/runtime': 2.0.1 + '@module-federation/sdk': 2.0.1 btoa: 1.2.1 encoding: 0.1.13 node-fetch: 2.7.0(encoding@0.1.13) @@ -16959,11 +17287,32 @@ snapshots: - utf-8-validate - vue-tsc - '@module-federation/rsbuild-plugin@2.0.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': + '@module-federation/node@2.7.32(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': dependencies: - '@module-federation/enhanced': 2.0.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) - '@module-federation/node': 2.7.31(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) - '@module-federation/sdk': 2.0.0 + '@module-federation/enhanced': 2.0.1(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + '@module-federation/runtime': 2.0.1 + '@module-federation/sdk': 2.0.1 + btoa: 1.2.1 + encoding: 0.1.13 + node-fetch: 2.7.0(encoding@0.1.13) + optionalDependencies: + webpack: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2) + transitivePeerDependencies: + - '@rspack/core' + - bufferutil + - debug + - react + - react-dom + - supports-color + - typescript + - utf-8-validate + - vue-tsc + + '@module-federation/rsbuild-plugin@https://pkg.pr.new/module-federation/core/@module-federation/rsbuild-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': + dependencies: + '@module-federation/enhanced': https://pkg.pr.new/module-federation/core/@module-federation/enhanced@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + '@module-federation/node': 2.7.32(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c fs-extra: 11.3.0 transitivePeerDependencies: - '@rspack/core' @@ -16977,16 +17326,91 @@ snapshots: - vue-tsc - webpack - '@module-federation/rspack@2.0.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': + '@module-federation/rsbuild-plugin@https://pkg.pr.new/module-federation/core/@module-federation/rsbuild-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': dependencies: - '@module-federation/bridge-react-webpack-plugin': 2.0.0 - '@module-federation/dts-plugin': 2.0.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/inject-external-runtime-core-plugin': 2.0.0(@module-federation/runtime-tools@2.0.0) - '@module-federation/managers': 2.0.0 - '@module-federation/manifest': 2.0.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/runtime-tools': 2.0.0 - '@module-federation/sdk': 2.0.0 - '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18) + '@module-federation/enhanced': https://pkg.pr.new/module-federation/core/@module-federation/enhanced@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + '@module-federation/node': 2.7.32(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c + fs-extra: 11.3.0 + transitivePeerDependencies: + - '@rspack/core' + - bufferutil + - debug + - react + - react-dom + - supports-color + - typescript + - utf-8-validate + - vue-tsc + - webpack + + '@module-federation/rspack@2.0.1(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@module-federation/bridge-react-webpack-plugin': 2.0.1 + '@module-federation/dts-plugin': 2.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/inject-external-runtime-core-plugin': 2.0.1(@module-federation/runtime-tools@2.0.1) + '@module-federation/managers': 2.0.1 + '@module-federation/manifest': 2.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/runtime-tools': 2.0.1 + '@module-federation/sdk': 2.0.1 + '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18) + btoa: 1.2.1 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + + '@module-federation/rspack@2.0.1(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@module-federation/bridge-react-webpack-plugin': 2.0.1 + '@module-federation/dts-plugin': 2.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/inject-external-runtime-core-plugin': 2.0.1(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c) + '@module-federation/managers': 2.0.1 + '@module-federation/manifest': 2.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/runtime-tools': 2.0.1 + '@module-federation/sdk': 2.0.1 + '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18) + btoa: 1.2.1 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + + '@module-federation/rspack@https://pkg.pr.new/module-federation/core/@module-federation/rspack@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@module-federation/bridge-react-webpack-plugin': https://pkg.pr.new/module-federation/core/@module-federation/bridge-react-webpack-plugin@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/dts-plugin': https://pkg.pr.new/module-federation/core/@module-federation/dts-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/inject-external-runtime-core-plugin': https://pkg.pr.new/module-federation/core/@module-federation/inject-external-runtime-core-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(@module-federation/runtime-tools@2.0.1) + '@module-federation/managers': https://pkg.pr.new/module-federation/core/@module-federation/managers@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/manifest': https://pkg.pr.new/module-federation/core/@module-federation/manifest@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/runtime-tools': https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c + '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18) + btoa: 1.2.1 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + + '@module-federation/rspack@https://pkg.pr.new/module-federation/core/@module-federation/rspack@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@module-federation/bridge-react-webpack-plugin': https://pkg.pr.new/module-federation/core/@module-federation/bridge-react-webpack-plugin@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/dts-plugin': https://pkg.pr.new/module-federation/core/@module-federation/dts-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/inject-external-runtime-core-plugin': https://pkg.pr.new/module-federation/core/@module-federation/inject-external-runtime-core-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c) + '@module-federation/managers': https://pkg.pr.new/module-federation/core/@module-federation/managers@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/manifest': https://pkg.pr.new/module-federation/core/@module-federation/manifest@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/runtime-tools': https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c + '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18) btoa: 1.2.1 optionalDependencies: typescript: 5.9.3 @@ -17006,15 +17430,30 @@ snapshots: '@module-federation/error-codes': 2.0.0 '@module-federation/sdk': 2.0.0 + '@module-federation/runtime-core@2.0.1': + dependencies: + '@module-federation/error-codes': 2.0.1 + '@module-federation/sdk': 2.0.1 + + '@module-federation/runtime-core@https://pkg.pr.new/module-federation/core/@module-federation/runtime-core@73bd1ddb2266002f586a872ddae8020052c5457c': + dependencies: + '@module-federation/error-codes': https://pkg.pr.new/module-federation/core/@module-federation/error-codes@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/runtime-tools@0.22.0': dependencies: '@module-federation/runtime': 0.22.0 '@module-federation/webpack-bundler-runtime': 0.22.0 - '@module-federation/runtime-tools@2.0.0': + '@module-federation/runtime-tools@2.0.1': + dependencies: + '@module-federation/runtime': 2.0.1 + '@module-federation/webpack-bundler-runtime': 2.0.1 + + '@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c': dependencies: - '@module-federation/runtime': 2.0.0 - '@module-federation/webpack-bundler-runtime': 2.0.0 + '@module-federation/runtime': https://pkg.pr.new/module-federation/core/@module-federation/runtime@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/webpack-bundler-runtime': https://pkg.pr.new/module-federation/core/@module-federation/webpack-bundler-runtime@73bd1ddb2266002f586a872ddae8020052c5457c '@module-federation/runtime@0.22.0': dependencies: @@ -17028,11 +17467,33 @@ snapshots: '@module-federation/runtime-core': 2.0.0 '@module-federation/sdk': 2.0.0 + '@module-federation/runtime@2.0.1': + dependencies: + '@module-federation/error-codes': 2.0.1 + '@module-federation/runtime-core': 2.0.1 + '@module-federation/sdk': 2.0.1 + + '@module-federation/runtime@https://pkg.pr.new/module-federation/core/@module-federation/runtime@73bd1ddb2266002f586a872ddae8020052c5457c': + dependencies: + '@module-federation/error-codes': https://pkg.pr.new/module-federation/core/@module-federation/error-codes@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/runtime-core': https://pkg.pr.new/module-federation/core/@module-federation/runtime-core@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/sdk@0.22.0': {} '@module-federation/sdk@2.0.0': {} - '@module-federation/third-party-dts-extractor@2.0.0': + '@module-federation/sdk@2.0.1': {} + + '@module-federation/sdk@https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c': {} + + '@module-federation/third-party-dts-extractor@2.0.1': + dependencies: + find-pkg: 2.0.0 + fs-extra: 9.1.0 + resolve: 1.22.8 + + '@module-federation/third-party-dts-extractor@https://pkg.pr.new/module-federation/core/@module-federation/third-party-dts-extractor@73bd1ddb2266002f586a872ddae8020052c5457c': dependencies: find-pkg: 2.0.0 fs-extra: 9.1.0 @@ -17043,10 +17504,15 @@ snapshots: '@module-federation/runtime': 0.22.0 '@module-federation/sdk': 0.22.0 - '@module-federation/webpack-bundler-runtime@2.0.0': + '@module-federation/webpack-bundler-runtime@2.0.1': dependencies: - '@module-federation/runtime': 2.0.0 - '@module-federation/sdk': 2.0.0 + '@module-federation/runtime': 2.0.1 + '@module-federation/sdk': 2.0.1 + + '@module-federation/webpack-bundler-runtime@https://pkg.pr.new/module-federation/core/@module-federation/webpack-bundler-runtime@73bd1ddb2266002f586a872ddae8020052c5457c': + dependencies: + '@module-federation/runtime': https://pkg.pr.new/module-federation/core/@module-federation/runtime@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c '@mswjs/interceptors@0.39.8': dependencies: @@ -17894,18 +18360,25 @@ snapshots: optionalDependencies: '@swc/helpers': 0.5.18 - '@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.17)': + '@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.17)': dependencies: '@rspack/binding': 2.0.0-beta.2 optionalDependencies: - '@module-federation/runtime-tools': 2.0.0 + '@module-federation/runtime-tools': 2.0.1 '@swc/helpers': 0.5.17 - '@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18)': + '@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18)': + dependencies: + '@rspack/binding': 2.0.0-beta.2 + optionalDependencies: + '@module-federation/runtime-tools': 2.0.1 + '@swc/helpers': 0.5.18 + + '@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18)': dependencies: '@rspack/binding': 2.0.0-beta.2 optionalDependencies: - '@module-federation/runtime-tools': 2.0.0 + '@module-federation/runtime-tools': https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c '@swc/helpers': 0.5.18 '@rspack/lite-tapable@1.1.0': {} @@ -22241,14 +22714,14 @@ snapshots: jest-cli@29.7.0(@types/node@20.19.27)(babel-plugin-macros@3.1.0): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@20.19.27)(typescript@5.9.3)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@20.19.27)(typescript@5.9.3)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.19.27)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@20.19.27)(typescript@5.9.3)) + create-jest: 29.7.0(@types/node@20.19.27)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@20.19.27)(typescript@5.9.3)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.19.27)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@20.19.27)(typescript@5.9.3)) + jest-config: 29.7.0(@types/node@20.19.27)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@20.19.27)(typescript@5.9.3)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -22637,7 +23110,7 @@ snapshots: jest@29.7.0(@types/node@20.19.27)(babel-plugin-macros@3.1.0): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@20.19.27)(typescript@5.9.3)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@20.19.27)(typescript@5.9.3)) '@jest/types': 29.6.3 import-local: 3.2.0 jest-cli: 29.7.0(@types/node@20.19.27)(babel-plugin-macros@3.1.0) @@ -25197,15 +25670,15 @@ snapshots: optionalDependencies: react-dom: 19.2.4(react@19.2.4) - react-server-dom-rspack@0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-server-dom-rspack@0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.18) + '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) react-server-dom-rspack@0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.17) + '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.17) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -25511,7 +25984,7 @@ snapshots: dependencies: '@rspack/lite-tapable': 1.1.0 optionalDependencies: - '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.17) + '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.17) run-async@2.4.1: {} @@ -26417,7 +26890,7 @@ snapshots: picocolors: 1.1.1 typescript: 5.9.3 optionalDependencies: - '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.0.0)(@swc/helpers@0.5.17) + '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.17) ts-dedent@2.2.0: {} diff --git a/tests/integration/i18n/mf/mf-app-provider/package.json b/tests/integration/i18n/mf/mf-app-provider/package.json index 5e08ef377cea..32a2c9a05d4c 100644 --- a/tests/integration/i18n/mf/mf-app-provider/package.json +++ b/tests/integration/i18n/mf/mf-app-provider/package.json @@ -10,7 +10,7 @@ "dependencies": { "@modern-js/plugin-i18n": "workspace:*", "@modern-js/runtime": "workspace:*", - "@module-federation/modern-js-v3": "2.0.0", + "@module-federation/modern-js-v3": "https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437", "i18next": "25.7.4", "react": "^19.2.4", "react-dom": "^19.2.4", diff --git a/tests/integration/i18n/mf/mf-component-provider/package.json b/tests/integration/i18n/mf/mf-component-provider/package.json index a1a174fe7ad5..24004f5155f6 100644 --- a/tests/integration/i18n/mf/mf-component-provider/package.json +++ b/tests/integration/i18n/mf/mf-component-provider/package.json @@ -9,7 +9,7 @@ "dependencies": { "@modern-js/plugin-i18n": "workspace:*", "@modern-js/runtime": "workspace:*", - "@module-federation/modern-js-v3": "2.0.0", + "@module-federation/modern-js-v3": "https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437", "i18next": "25.7.4", "react": "^19.2.4", "react-dom": "^19.2.4", diff --git a/tests/integration/i18n/mf/mf-consumer/package.json b/tests/integration/i18n/mf/mf-consumer/package.json index 8b9590605fbd..22f3693fdfdf 100644 --- a/tests/integration/i18n/mf/mf-consumer/package.json +++ b/tests/integration/i18n/mf/mf-consumer/package.json @@ -9,7 +9,7 @@ "dependencies": { "@modern-js/plugin-i18n": "workspace:*", "@modern-js/runtime": "workspace:*", - "@module-federation/modern-js-v3": "2.0.0", + "@module-federation/modern-js-v3": "https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437", "@module-federation/runtime": "2.0.0", "i18next": "25.7.4", "react": "^19.2.4", diff --git a/tests/integration/rsc-mf/host/package.json b/tests/integration/rsc-mf/host/package.json index 010e39c8021e..3e9c0e60bf53 100644 --- a/tests/integration/rsc-mf/host/package.json +++ b/tests/integration/rsc-mf/host/package.json @@ -10,7 +10,7 @@ "dependencies": { "@modern-js/render": "workspace:*", "@modern-js/runtime": "workspace:*", - "@module-federation/modern-js-v3": "2.0.0", + "@module-federation/modern-js-v3": "https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437", "@module-federation/runtime": "2.0.0", "client-only": "^0.0.1", "react": "^19.2.4", diff --git a/tests/integration/rsc-mf/remote/package.json b/tests/integration/rsc-mf/remote/package.json index 9b040ea2fe51..c0f4a45ea024 100644 --- a/tests/integration/rsc-mf/remote/package.json +++ b/tests/integration/rsc-mf/remote/package.json @@ -10,7 +10,7 @@ "dependencies": { "@modern-js/render": "workspace:*", "@modern-js/runtime": "workspace:*", - "@module-federation/modern-js-v3": "2.0.0", + "@module-federation/modern-js-v3": "https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437", "@module-federation/runtime": "2.0.0", "client-only": "^0.0.1", "react": "^19.2.4", From 49c52dd666198ace2c9135f379bcac106dbbb7de Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Feb 2026 04:27:31 +0000 Subject: [PATCH 007/324] fix(rsc-mf): add react-server resolve condition for async-node --- tests/integration/rsc-mf/host/modern.config.ts | 1 + tests/integration/rsc-mf/remote/modern.config.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/integration/rsc-mf/host/modern.config.ts b/tests/integration/rsc-mf/host/modern.config.ts index 1ae1399165c1..fad69a578eb7 100644 --- a/tests/integration/rsc-mf/host/modern.config.ts +++ b/tests/integration/rsc-mf/host/modern.config.ts @@ -23,6 +23,7 @@ export default defineConfig({ const targets = Array.isArray(target) ? target : [target]; if (targets.some(item => String(item).includes('node'))) { chain.target('async-node'); + chain.resolve.conditionNames.add('react-server'); } chain.resolve.modules diff --git a/tests/integration/rsc-mf/remote/modern.config.ts b/tests/integration/rsc-mf/remote/modern.config.ts index 9aeec9a165a6..a360574cff66 100644 --- a/tests/integration/rsc-mf/remote/modern.config.ts +++ b/tests/integration/rsc-mf/remote/modern.config.ts @@ -26,6 +26,7 @@ export default defineConfig({ const targets = Array.isArray(target) ? target : [target]; if (targets.some(item => String(item).includes('node'))) { chain.target('async-node'); + chain.resolve.conditionNames.add('react-server'); } chain.resolve.modules From 6c1d6d1813a50d50c4ccb5732150ac1ec33273c5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Feb 2026 04:33:20 +0000 Subject: [PATCH 008/324] chore(rsc-mf): include fallback resolve conditions in node chain --- tests/integration/rsc-mf/host/modern.config.ts | 6 +++++- tests/integration/rsc-mf/remote/modern.config.ts | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/integration/rsc-mf/host/modern.config.ts b/tests/integration/rsc-mf/host/modern.config.ts index fad69a578eb7..c85abbd14978 100644 --- a/tests/integration/rsc-mf/host/modern.config.ts +++ b/tests/integration/rsc-mf/host/modern.config.ts @@ -23,7 +23,11 @@ export default defineConfig({ const targets = Array.isArray(target) ? target : [target]; if (targets.some(item => String(item).includes('node'))) { chain.target('async-node'); - chain.resolve.conditionNames.add('react-server'); + chain.resolve.conditionNames + .add('react-server') + .add('require') + .add('import') + .add('default'); } chain.resolve.modules diff --git a/tests/integration/rsc-mf/remote/modern.config.ts b/tests/integration/rsc-mf/remote/modern.config.ts index a360574cff66..601aa9121a99 100644 --- a/tests/integration/rsc-mf/remote/modern.config.ts +++ b/tests/integration/rsc-mf/remote/modern.config.ts @@ -26,7 +26,11 @@ export default defineConfig({ const targets = Array.isArray(target) ? target : [target]; if (targets.some(item => String(item).includes('node'))) { chain.target('async-node'); - chain.resolve.conditionNames.add('react-server'); + chain.resolve.conditionNames + .add('react-server') + .add('require') + .add('import') + .add('default'); } chain.resolve.modules From d8c6ca9d9bb0b0e1b7eb4586e57cd384136c4dad Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Feb 2026 04:59:48 +0000 Subject: [PATCH 009/324] fix(rsc-mf): align federation runtime deps and ssr public path --- package.json | 19 + pnpm-lock.yaml | 784 ++++-------------- .../rsc-mf/remote/modern.config.ts | 3 + 3 files changed, 167 insertions(+), 639 deletions(-) diff --git a/package.json b/package.json index e37bdf6c3590..c55a91dc5680 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,25 @@ "@remix-run/router": ">=1.23.2", "h3": ">=1.15.5", "@rspack/core@2.0.0-beta.2": "npm:@rspack-canary/core@2.0.0-canary-032bd1ff-20260212021235", + "@module-federation/bridge-react": "2.0.1", + "@module-federation/bridge-react-webpack-plugin": "2.0.1", + "@module-federation/bridge-shared": "2.0.1", + "@module-federation/cli": "2.0.1", + "@module-federation/data-prefetch": "2.0.1", + "@module-federation/dts-plugin": "2.0.1", + "@module-federation/enhanced": "2.0.1", + "@module-federation/error-codes": "2.0.1", + "@module-federation/inject-external-runtime-core-plugin": "2.0.1", + "@module-federation/managers": "2.0.1", + "@module-federation/manifest": "2.0.1", + "@module-federation/rsbuild-plugin": "2.0.1", + "@module-federation/rspack": "2.0.1", + "@module-federation/runtime": "2.0.1", + "@module-federation/runtime-core": "2.0.1", + "@module-federation/runtime-tools": "2.0.1", + "@module-federation/sdk": "2.0.1", + "@module-federation/third-party-dts-extractor": "2.0.1", + "@module-federation/webpack-bundler-runtime": "2.0.1", "tar": ">=7.5.4", "diff": ">=4.0.4", "debug": ">=4.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3398b1fee76..afad0e59e21f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,6 +10,25 @@ overrides: '@remix-run/router': '>=1.23.2' h3: '>=1.15.5' '@rspack/core@2.0.0-beta.2': npm:@rspack-canary/core@2.0.0-canary-032bd1ff-20260212021235 + '@module-federation/bridge-react': 2.0.1 + '@module-federation/bridge-react-webpack-plugin': 2.0.1 + '@module-federation/bridge-shared': 2.0.1 + '@module-federation/cli': 2.0.1 + '@module-federation/data-prefetch': 2.0.1 + '@module-federation/dts-plugin': 2.0.1 + '@module-federation/enhanced': 2.0.1 + '@module-federation/error-codes': 2.0.1 + '@module-federation/inject-external-runtime-core-plugin': 2.0.1 + '@module-federation/managers': 2.0.1 + '@module-federation/manifest': 2.0.1 + '@module-federation/rsbuild-plugin': 2.0.1 + '@module-federation/rspack': 2.0.1 + '@module-federation/runtime': 2.0.1 + '@module-federation/runtime-core': 2.0.1 + '@module-federation/runtime-tools': 2.0.1 + '@module-federation/sdk': 2.0.1 + '@module-federation/third-party-dts-extractor': 2.0.1 + '@module-federation/webpack-bundler-runtime': 2.0.1 tar: '>=7.5.4' diff: '>=4.0.4' debug: '>=4.3.1' @@ -100,7 +119,7 @@ importers: version: link:../../packages/tsconfig '@rsdoctor/rspack-plugin': specifier: ^1.5.2 - version: 1.5.2(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(@rspack/core@1.7.5(@swc/helpers@0.5.18))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + version: 1.5.2(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0))(@rspack/core@1.7.5(@swc/helpers@0.5.18))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) '@types/node': specifier: ^20 version: 20.19.27 @@ -127,40 +146,40 @@ importers: version: link:../../toolkit/utils '@rsbuild/core': specifier: 2.0.0-beta.4 - version: 2.0.0-beta.4(core-js@3.48.0) + version: 2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) '@rsbuild/plugin-assets-retry': specifier: 1.5.1 - version: 1.5.1(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) + version: 1.5.1(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0)) '@rsbuild/plugin-check-syntax': specifier: 1.6.1 - version: 1.6.1(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) + version: 1.6.1(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0)) '@rsbuild/plugin-css-minimizer': specifier: 1.1.1 - version: 1.1.1(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(esbuild@0.27.2)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17))(esbuild@0.27.2)) + version: 1.1.1(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0))(esbuild@0.27.2)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17))(esbuild@0.27.2)) '@rsbuild/plugin-less': specifier: 1.6.0 - version: 1.6.0(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) + version: 1.6.0(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0)) '@rsbuild/plugin-react': specifier: 1.4.4 - version: 1.4.4(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) + version: 1.4.4(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0)) '@rsbuild/plugin-rem': specifier: 1.0.5 - version: 1.0.5(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) + version: 1.0.5(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0)) '@rsbuild/plugin-sass': specifier: 1.5.0 - version: 1.5.0(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) + version: 1.5.0(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0)) '@rsbuild/plugin-source-build': specifier: 1.0.4 - version: 1.0.4(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) + version: 1.0.4(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0)) '@rsbuild/plugin-svgr': specifier: 1.3.0 - version: 1.3.0(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(typescript@5.9.3) + version: 1.3.0(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0))(typescript@5.9.3) '@rsbuild/plugin-type-check': specifier: 1.3.3 - version: 1.3.3(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17))(typescript@5.9.3) + version: 1.3.3(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0))(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.17))(typescript@5.9.3) '@rsbuild/plugin-typed-css-modules': specifier: 1.2.1 - version: 1.2.1(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) + version: 1.2.1(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0)) '@swc/core': specifier: 1.15.11 version: 1.15.11(@swc/helpers@0.5.17) @@ -211,10 +230,10 @@ importers: version: 3.0.4(postcss@8.5.6) rsbuild-plugin-rsc: specifier: 0.0.1-beta.0 - version: 0.0.1-beta.0(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(react-server-dom-rspack@0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + version: 0.0.1-beta.0(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0))(react-server-dom-rspack@0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) rspack-manifest-plugin: specifier: 5.2.1 - version: 5.2.1(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17)) + version: 5.2.1(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.17)) ts-deepmerge: specifier: 7.0.3 version: 7.0.3 @@ -297,7 +316,7 @@ importers: version: link:../../toolkit/types '@rsbuild/core': specifier: 2.0.0-beta.4 - version: 2.0.0-beta.4(core-js@3.48.0) + version: 2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) '@rslib/core': specifier: 0.19.5 version: 0.19.5(typescript@5.9.3) @@ -349,7 +368,7 @@ importers: version: link:../../toolkit/types '@rsbuild/core': specifier: 2.0.0-beta.4 - version: 2.0.0-beta.4(core-js@3.48.0) + version: 2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) '@rslib/core': specifier: 0.19.5 version: 0.19.5(typescript@5.9.3) @@ -431,7 +450,7 @@ importers: dependencies: '@rsbuild/plugin-styled-components': specifier: 1.6.1 - version: 1.6.1(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) + version: 1.6.1(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0)) '@swc/helpers': specifier: ^0.5.17 version: 0.5.17 @@ -475,16 +494,16 @@ importers: devDependencies: '@rsbuild/plugin-sass': specifier: 1.5.0 - version: 1.5.0(@rsbuild/core@2.0.0-beta.1(core-js@3.48.0)) + version: 1.5.0(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0)) '@rspress/core': specifier: 2.0.2 - version: 2.0.2(@types/react@19.2.13)(core-js@3.48.0) + version: 2.0.2(@module-federation/runtime-tools@2.0.1)(@types/react@19.2.13)(core-js@3.48.0) '@rspress/plugin-llms': specifier: 2.0.2 - version: 2.0.2(@rspress/core@2.0.2(@types/react@19.2.13)(core-js@3.48.0)) + version: 2.0.2(@rspress/core@2.0.2(@module-federation/runtime-tools@2.0.1)(@types/react@19.2.13)(core-js@3.48.0)) '@rspress/shared': specifier: 2.0.2 - version: 2.0.2(core-js@3.48.0) + version: 2.0.2(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) '@shikijs/transformers': specifier: ^3.21.0 version: 3.21.0 @@ -706,7 +725,7 @@ importers: version: 4.4.2 '@rsbuild/core': specifier: 2.0.0-beta.4 - version: 2.0.0-beta.4(core-js@3.48.0) + version: 2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) '@rslib/core': specifier: 0.19.5 version: 0.19.5(typescript@5.9.3) @@ -785,7 +804,7 @@ importers: version: 19.2.4(react@19.2.4) react-server-dom-rspack: specifier: 0.0.1-beta.0 - version: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) typescript: specifier: ^5 version: 5.9.3 @@ -1250,7 +1269,7 @@ importers: version: link:../../toolkit/utils '@rsbuild/core': specifier: 2.0.0-beta.4 - version: 2.0.0-beta.4(core-js@3.48.0) + version: 2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) '@swc/helpers': specifier: ^0.5.17 version: 0.5.17 @@ -1363,7 +1382,7 @@ importers: version: link:../utils '@rsbuild/core': specifier: 2.0.0-beta.4 - version: 2.0.0-beta.4(core-js@3.48.0) + version: 2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) '@swc/helpers': specifier: ^0.5.17 version: 0.5.17 @@ -1896,7 +1915,7 @@ importers: devDependencies: '@rsbuild/plugin-react': specifier: 1.4.4 - version: 1.4.4(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) + version: 1.4.4(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0)) '@rslib/core': specifier: 0.19.5 version: 0.19.5(typescript@5.9.3) @@ -2020,7 +2039,7 @@ importers: version: 1.58.2 '@rsbuild/core': specifier: 2.0.0-beta.4 - version: 2.0.0-beta.4(core-js@3.48.0) + version: 2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) '@types/connect': specifier: ^3.4.38 version: 3.4.38 @@ -3031,7 +3050,7 @@ importers: version: link:../../../../../packages/runtime/plugin-runtime '@module-federation/modern-js-v3': specifier: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437 - version: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + version: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) i18next: specifier: 25.7.4 version: 25.7.4(typescript@5.9.3) @@ -3095,8 +3114,8 @@ importers: specifier: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437 version: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) '@module-federation/runtime': - specifier: 2.0.0 - version: 2.0.0 + specifier: 2.0.1 + version: 2.0.1 i18next: specifier: 25.7.4 version: 25.7.4(typescript@5.9.3) @@ -3190,7 +3209,7 @@ importers: version: link:../../../packages/solutions/app-tools '@rsdoctor/rspack-plugin': specifier: ^1.5.2 - version: 1.5.2(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(@rspack/core@1.7.5(@swc/helpers@0.5.18))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + version: 1.5.2(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0))(@rspack/core@1.7.5(@swc/helpers@0.5.18))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) '@types/jest': specifier: ^29.5.14 version: 29.5.14 @@ -3562,8 +3581,8 @@ importers: specifier: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437 version: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) '@module-federation/runtime': - specifier: 2.0.0 - version: 2.0.0 + specifier: 2.0.1 + version: 2.0.1 client-only: specifier: ^0.0.1 version: 0.0.1 @@ -3617,8 +3636,8 @@ importers: specifier: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437 version: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) '@module-federation/runtime': - specifier: 2.0.0 - version: 2.0.0 + specifier: 2.0.1 + version: 2.0.1 client-only: specifier: ^0.0.1 version: 0.0.1 @@ -4544,7 +4563,7 @@ importers: version: link:../../../../../packages/runtime/plugin-runtime '@rsbuild/plugin-babel': specifier: 1.1.0 - version: 1.1.0(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) + version: 1.1.0(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0)) react: specifier: ^19.2.4 version: 19.2.4 @@ -6156,13 +6175,8 @@ packages: '@module-federation/bridge-react-webpack-plugin@2.0.1': resolution: {integrity: sha512-D7LMW5EMAJShOMR1aZDAJ6s+MdsYDHaQyJADLQ3LaY0sne/BkVqkPikUwcO1IwOwKbXjYsDlQVOEvk9wZVRFhA==} - '@module-federation/bridge-react-webpack-plugin@https://pkg.pr.new/module-federation/core/@module-federation/bridge-react-webpack-plugin@73bd1ddb2266002f586a872ddae8020052c5457c': - resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/bridge-react-webpack-plugin@73bd1ddb2266002f586a872ddae8020052c5457c} - version: 2.0.1 - - '@module-federation/bridge-react@https://pkg.pr.new/module-federation/core/@module-federation/bridge-react@73bd1ddb2266002f586a872ddae8020052c5457c': - resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/bridge-react@73bd1ddb2266002f586a872ddae8020052c5457c} - version: 2.0.1 + '@module-federation/bridge-react@2.0.1': + resolution: {integrity: sha512-pioaxszMgfPL6T7uJk0+Wm6QqanhRwIcKOjbDMTDbKX1hGO5Y00Y1RGLq5xxilytDT9/Ivu+Yuyo8zmsGp9AAw==} peerDependencies: react: '>=16.9.0' react-dom: '>=16.9.0' @@ -6174,21 +6188,14 @@ packages: react-router-dom: optional: true - '@module-federation/bridge-shared@https://pkg.pr.new/module-federation/core/@module-federation/bridge-shared@73bd1ddb2266002f586a872ddae8020052c5457c': - resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/bridge-shared@73bd1ddb2266002f586a872ddae8020052c5457c} - version: 2.0.1 + '@module-federation/bridge-shared@2.0.1': + resolution: {integrity: sha512-GAjbEJC90GheChLzM8JcSYOEHu4GFqSz+X27dBnQ2Ka1R3zqi3OfWowbyQVnH6LLKyM6A+FC41KMBDCkfv/E+w==} '@module-federation/cli@2.0.1': resolution: {integrity: sha512-2SL5Y8iODNX10y9T3CBLhHjSXo4afnA1BK82m4sNfZebuVO+o34bxewqwod9xfWq9xhTZmOSFZ+n+lgTKRv+CQ==} engines: {node: '>=16.0.0'} hasBin: true - '@module-federation/cli@https://pkg.pr.new/module-federation/core/@module-federation/cli@73bd1ddb2266002f586a872ddae8020052c5457c': - resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/cli@73bd1ddb2266002f586a872ddae8020052c5457c} - version: 2.0.1 - engines: {node: '>=16.0.0'} - hasBin: true - '@module-federation/data-prefetch@2.0.1': resolution: {integrity: sha512-Kq0P1OABGt6QAvs6TaE/zY9Ut9Y/oJFrzoSF3eWaCYbUAr2KD2SpTyMsPz4ssBzjeKXTgimugh6tHHd6mpCBIQ==} peerDependencies: @@ -6200,18 +6207,6 @@ packages: react-dom: optional: true - '@module-federation/data-prefetch@https://pkg.pr.new/module-federation/core/@module-federation/data-prefetch@73bd1ddb2266002f586a872ddae8020052c5457c': - resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/data-prefetch@73bd1ddb2266002f586a872ddae8020052c5457c} - version: 2.0.1 - peerDependencies: - react: '>=16.9.0' - react-dom: '>=16.9.0' - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true - '@module-federation/dts-plugin@2.0.1': resolution: {integrity: sha512-PLneTsf1fQS5/RTBedtLAAmCPRdMfIlhfJkOa8QH3WDJaQsqm8Wb3r2cTUBf2aNj/bP3aH/y6Hs9JFB/4x0l5g==} peerDependencies: @@ -6221,19 +6216,8 @@ packages: vue-tsc: optional: true - '@module-federation/dts-plugin@https://pkg.pr.new/module-federation/core/@module-federation/dts-plugin@73bd1ddb2266002f586a872ddae8020052c5457c': - resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/dts-plugin@73bd1ddb2266002f586a872ddae8020052c5457c} - version: 2.0.1 - peerDependencies: - typescript: ^4.9.0 || ^5.0.0 - vue-tsc: '>=1.0.24' - peerDependenciesMeta: - vue-tsc: - optional: true - '@module-federation/enhanced@2.0.1': resolution: {integrity: sha512-EZIARQ/8ScoTP6PV8+E4SsmMYWK4ErrikZJ0G/FX8wvK8mCtdoKatFtvDN9++P6Nl78kN9zHYgAV4AHKdBVjfQ==} - version: 2.0.1 hasBin: true peerDependencies: typescript: ^4.9.0 || ^5.0.0 @@ -6247,61 +6231,20 @@ packages: webpack: optional: true - '@module-federation/enhanced@https://pkg.pr.new/module-federation/core/@module-federation/enhanced@73bd1ddb2266002f586a872ddae8020052c5457c': - resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/enhanced@73bd1ddb2266002f586a872ddae8020052c5457c} - version: 2.0.1 - hasBin: true - peerDependencies: - typescript: ^4.9.0 || ^5.0.0 - vue-tsc: '>=1.0.24' - webpack: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - vue-tsc: - optional: true - webpack: - optional: true - - '@module-federation/error-codes@0.22.0': - resolution: {integrity: sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==} - - '@module-federation/error-codes@2.0.0': - resolution: {integrity: sha512-9oE+hXuPv2zej7AxJ5hOgeRqlPs98meooV2FiutTfftLAyy2N6+Kwmmz5NR9d9t91weJj8N0cSHFoyenNHKTVg==} - '@module-federation/error-codes@2.0.1': resolution: {integrity: sha512-2bJF/ft+qL9L6Zvq2t/G9/f/0wFL73cM8/NJ04uyYz9BjIgvx28K5qu8/6+IwgEEKATG7vOhBBVj6wH3S+5ASA==} - '@module-federation/error-codes@https://pkg.pr.new/module-federation/core/@module-federation/error-codes@73bd1ddb2266002f586a872ddae8020052c5457c': - resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/error-codes@73bd1ddb2266002f586a872ddae8020052c5457c} - version: 2.0.1 - '@module-federation/inject-external-runtime-core-plugin@2.0.1': resolution: {integrity: sha512-oAA7G+4GCHM+WRYfscR/x4GwCyM9CEqfdD9/x2L6y8mtLWK9anRLKTocsI759AvzXsbT1m3EQ5ki1O6wlwDu3g==} - version: 2.0.1 - peerDependencies: - '@module-federation/runtime-tools': 2.0.1 - - '@module-federation/inject-external-runtime-core-plugin@https://pkg.pr.new/module-federation/core/@module-federation/inject-external-runtime-core-plugin@73bd1ddb2266002f586a872ddae8020052c5457c': - resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/inject-external-runtime-core-plugin@73bd1ddb2266002f586a872ddae8020052c5457c} - version: 2.0.1 peerDependencies: '@module-federation/runtime-tools': 2.0.1 '@module-federation/managers@2.0.1': resolution: {integrity: sha512-KR01lSlcYRQ9C6hW2a8CQQtAE0LvfTLgtV/6ZNUTagw8sRfeDln+ggrZsYilKu9zl0i8RPDgpv/kS60o4lcxCQ==} - '@module-federation/managers@https://pkg.pr.new/module-federation/core/@module-federation/managers@73bd1ddb2266002f586a872ddae8020052c5457c': - resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/managers@73bd1ddb2266002f586a872ddae8020052c5457c} - version: 2.0.1 - '@module-federation/manifest@2.0.1': resolution: {integrity: sha512-p8nYGjHWp17MsYdW/Vv0ogBDiTTsI1PHWPQbvVIqLQXDqwiesaRSRR1zziECXQoEL8lV5Bs+uSkcaJGhea9P+A==} - '@module-federation/manifest@https://pkg.pr.new/module-federation/core/@module-federation/manifest@73bd1ddb2266002f586a872ddae8020052c5457c': - resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/manifest@73bd1ddb2266002f586a872ddae8020052c5457c} - version: 2.0.1 - '@module-federation/modern-js-v3@https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437': resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437} version: 2.0.1 @@ -6324,16 +6267,14 @@ packages: '@module-federation/node@2.7.32': resolution: {integrity: sha512-hUj5v2GGwpNzl2gaJS4AyzCYRzJBhN8875A+ucKF9tq3jaQb5zpy3izYMISqqbN2q9a7jz3nEUgwAh3pjri+rQ==} - version: 2.7.32 peerDependencies: webpack: ^5.40.0 peerDependenciesMeta: webpack: optional: true - '@module-federation/rsbuild-plugin@https://pkg.pr.new/module-federation/core/@module-federation/rsbuild-plugin@73bd1ddb2266002f586a872ddae8020052c5457c': - resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/rsbuild-plugin@73bd1ddb2266002f586a872ddae8020052c5457c} - version: 2.0.1 + '@module-federation/rsbuild-plugin@2.0.1': + resolution: {integrity: sha512-+2HOfntaVHgaOmyT0fTR9tZR2mujveoad3I9cpXUJ890Kgyz000nNHUbumQcMECPcZb3+ZqbIDjHyoazlW5IOA==} engines: {node: '>=16.0.0'} peerDependencies: '@rsbuild/core': 2.0.0-beta.2 @@ -6343,20 +6284,6 @@ packages: '@module-federation/rspack@2.0.1': resolution: {integrity: sha512-SAlNE8iclFmzrKtx3/C2GivXYx6nPzx4MgQV01QG/a4LpnLbwlxzdZu3rqQ2swp4NNWT/t/GT7Y+7gfhyVa7mg==} - version: 2.0.1 - peerDependencies: - '@rspack/core': ^0.7.0 || ^1.0.0 || ^2.0.0-0 - typescript: ^4.9.0 || ^5.0.0 - vue-tsc: '>=1.0.24' - peerDependenciesMeta: - typescript: - optional: true - vue-tsc: - optional: true - - '@module-federation/rspack@https://pkg.pr.new/module-federation/core/@module-federation/rspack@73bd1ddb2266002f586a872ddae8020052c5457c': - resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/rspack@73bd1ddb2266002f586a872ddae8020052c5457c} - version: 2.0.1 peerDependencies: '@rspack/core': ^0.7.0 || ^1.0.0 || ^2.0.0-0 typescript: ^4.9.0 || ^5.0.0 @@ -6367,72 +6294,24 @@ packages: vue-tsc: optional: true - '@module-federation/runtime-core@0.22.0': - resolution: {integrity: sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==} - - '@module-federation/runtime-core@2.0.0': - resolution: {integrity: sha512-UhIGUs7Mg+TwMI2lgaLnj4UehpoyXbR7HDb2+vLikgBulPmFtodeWfsxCgENEwKsIY1vS0lOun15lNOn1vo3Xg==} - '@module-federation/runtime-core@2.0.1': resolution: {integrity: sha512-gOuCPSHoQGUGwlxfSTMInFX+QvLxdEWegGGMiLdU5vqbXuva4E9M+kXBBO7/0MkcBPMmVs0wOJGm0XOLeV2f1Q==} - '@module-federation/runtime-core@https://pkg.pr.new/module-federation/core/@module-federation/runtime-core@73bd1ddb2266002f586a872ddae8020052c5457c': - resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/runtime-core@73bd1ddb2266002f586a872ddae8020052c5457c} - version: 2.0.1 - - '@module-federation/runtime-tools@0.22.0': - resolution: {integrity: sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==} - '@module-federation/runtime-tools@2.0.1': resolution: {integrity: sha512-AStdwBtsGB3jIfDg9oP+KyVPsimdaeHsP855gqCxDp1hi2+GKjlZWZx9ThkS8NytVSXSUysxqoUL1ivDoKgcCQ==} - '@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c': - resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c} - version: 2.0.1 - - '@module-federation/runtime@0.22.0': - resolution: {integrity: sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==} - - '@module-federation/runtime@2.0.0': - resolution: {integrity: sha512-vPxQrmQNq3Z1T+1fkHEvFwTdJq9wuCLvdp/lpu9k2Oy7QP/Pj6QoQ/S7J5MCIAoRwj8Wj3z3ma21/DyHwLGvzA==} - '@module-federation/runtime@2.0.1': resolution: {integrity: sha512-UQ72P5Oo40dS6vdhHetwTtIsbGciEr+bjoYvDgh1WLPfFlTYd8zo9cLfqaf3juuPfV3cMVARAVPmh16lQYpUGA==} - '@module-federation/runtime@https://pkg.pr.new/module-federation/core/@module-federation/runtime@73bd1ddb2266002f586a872ddae8020052c5457c': - resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/runtime@73bd1ddb2266002f586a872ddae8020052c5457c} - version: 2.0.1 - - '@module-federation/sdk@0.22.0': - resolution: {integrity: sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==} - - '@module-federation/sdk@2.0.0': - resolution: {integrity: sha512-JYd1wTulsaoLT7HTk2oXL5y5797Z+H4mzxuUEKnSJo7R34RZSqehsqPSND7n0HT/1nf7uyn0Rb4qBfR3BVvdHQ==} - '@module-federation/sdk@2.0.1': resolution: {integrity: sha512-32PwudojGjog51cwpTali7D6ud82oVgsyvOx9JjAzhvXBX96YI4mRsursuWcthDxmigJP9ZvUTXDuRUEDh1OQA==} - '@module-federation/sdk@https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c': - resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c} - version: 2.0.1 - '@module-federation/third-party-dts-extractor@2.0.1': resolution: {integrity: sha512-neKSr6FNUeGRh+YR57l/QZUzPytJXuJx+babF7j5iGJG3FP+kfizr6QD0hgVis5KEoXMVbQ8yyvG0slERizeyw==} - '@module-federation/third-party-dts-extractor@https://pkg.pr.new/module-federation/core/@module-federation/third-party-dts-extractor@73bd1ddb2266002f586a872ddae8020052c5457c': - resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/third-party-dts-extractor@73bd1ddb2266002f586a872ddae8020052c5457c} - version: 2.0.1 - - '@module-federation/webpack-bundler-runtime@0.22.0': - resolution: {integrity: sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==} - '@module-federation/webpack-bundler-runtime@2.0.1': resolution: {integrity: sha512-u1NId3SF4lHDTmD2CHFEszulmXmIq1TGw9JYvnLx5rKJL7xt3aNxcb1GvkaYbRNVBXhSMjJ75E5LsQlZzyBx9A==} - '@module-federation/webpack-bundler-runtime@https://pkg.pr.new/module-federation/core/@module-federation/webpack-bundler-runtime@73bd1ddb2266002f586a872ddae8020052c5457c': - resolution: {tarball: https://pkg.pr.new/module-federation/core/@module-federation/webpack-bundler-runtime@73bd1ddb2266002f586a872ddae8020052c5457c} - version: 2.0.1 - '@mswjs/interceptors@0.39.8': resolution: {integrity: sha512-2+BzZbjRO7Ct61k8fMNHEtoKjeWI9pIlHFTqBwZ5icHpqszIgEZbjb1MW5Z0+bITTCTl3gk4PDBxs9tA/csXvA==} engines: {node: '>=18'} @@ -6980,7 +6859,7 @@ packages: resolution: {integrity: sha512-VH9njrpSKv4/EBp53XvUJMMSbBtydx7R0ZWPyh/5a4gvGVafTLCiKV+shMZMgV5uWYYVTbA+5Fo0vL03a6iW1Q==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: - '@module-federation/runtime-tools': 0.24.1 + '@module-federation/runtime-tools': 2.0.1 '@swc/helpers': '>=0.5.1' peerDependenciesMeta: '@module-federation/runtime-tools': @@ -7218,7 +7097,7 @@ packages: resolution: {integrity: sha512-2KK3hbxrRqzxtzg+ka7LsiEKIWIGIQz317k9HHC2U4IC5yLJ31K8y/vQfA1aIT2QcFls9gW7GyRjp8A4X5cvLA==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: - '@module-federation/runtime-tools': '>=0.22.0' + '@module-federation/runtime-tools': 2.0.1 '@swc/helpers': '>=0.5.1' peerDependenciesMeta: '@module-federation/runtime-tools': @@ -7230,7 +7109,7 @@ packages: resolution: {integrity: sha512-UD/LxAi9BCYGWKUMW82gwqYxWF46P5+P2jVSHC3rpv6IJ2EdPfRL1wqxbMGbkslD3YTB56vM18uwo1d5ThqrjA==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: - '@module-federation/runtime-tools': ^0.24.1 || ^2.0.0 + '@module-federation/runtime-tools': 2.0.1 '@swc/helpers': '>=0.5.1' peerDependenciesMeta: '@module-federation/runtime-tools': @@ -16911,16 +16790,10 @@ snapshots: '@types/semver': 7.5.8 semver: 7.6.3 - '@module-federation/bridge-react-webpack-plugin@https://pkg.pr.new/module-federation/core/@module-federation/bridge-react-webpack-plugin@73bd1ddb2266002f586a872ddae8020052c5457c': - dependencies: - '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c - '@types/semver': 7.5.8 - semver: 7.6.3 - - '@module-federation/bridge-react@https://pkg.pr.new/module-federation/core/@module-federation/bridge-react@73bd1ddb2266002f586a872ddae8020052c5457c(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)': + '@module-federation/bridge-react@2.0.1(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)': dependencies: - '@module-federation/bridge-shared': https://pkg.pr.new/module-federation/core/@module-federation/bridge-shared@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/bridge-shared': 2.0.1 + '@module-federation/sdk': 2.0.1 lru-cache: 10.4.3 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -16929,7 +16802,7 @@ snapshots: react-router: 7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-router-dom: 7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@module-federation/bridge-shared@https://pkg.pr.new/module-federation/core/@module-federation/bridge-shared@73bd1ddb2266002f586a872ddae8020052c5457c': {} + '@module-federation/bridge-shared@2.0.1': {} '@module-federation/cli@2.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: @@ -16946,21 +16819,6 @@ snapshots: - utf-8-validate - vue-tsc - '@module-federation/cli@https://pkg.pr.new/module-federation/core/@module-federation/cli@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': - dependencies: - '@module-federation/dts-plugin': https://pkg.pr.new/module-federation/core/@module-federation/dts-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c - chalk: 3.0.0 - commander: 11.1.0 - jiti: 2.4.2 - transitivePeerDependencies: - - bufferutil - - debug - - supports-color - - typescript - - utf-8-validate - - vue-tsc - '@module-federation/data-prefetch@2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@module-federation/runtime': 2.0.1 @@ -16970,15 +16828,6 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@module-federation/data-prefetch@https://pkg.pr.new/module-federation/core/@module-federation/data-prefetch@73bd1ddb2266002f586a872ddae8020052c5457c(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@module-federation/runtime': https://pkg.pr.new/module-federation/core/@module-federation/runtime@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c - fs-extra: 9.1.0 - optionalDependencies: - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - '@module-federation/dts-plugin@2.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: '@module-federation/error-codes': 2.0.1 @@ -17004,30 +16853,6 @@ snapshots: - supports-color - utf-8-validate - '@module-federation/dts-plugin@https://pkg.pr.new/module-federation/core/@module-federation/dts-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': - dependencies: - '@module-federation/error-codes': https://pkg.pr.new/module-federation/core/@module-federation/error-codes@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/managers': https://pkg.pr.new/module-federation/core/@module-federation/managers@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/third-party-dts-extractor': https://pkg.pr.new/module-federation/core/@module-federation/third-party-dts-extractor@73bd1ddb2266002f586a872ddae8020052c5457c - adm-zip: 0.5.16 - ansi-colors: 4.1.3 - axios: 1.13.5(debug@4.4.3) - chalk: 3.0.0 - fs-extra: 9.1.0 - isomorphic-ws: 5.0.0(ws@8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) - lodash.clonedeepwith: 4.5.0 - log4js: 6.9.1 - node-schedule: 2.1.1 - rambda: 9.4.2 - typescript: 5.9.3 - ws: 8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) - transitivePeerDependencies: - - bufferutil - - debug - - supports-color - - utf-8-validate - '@module-federation/enhanced@2.0.1(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': dependencies: '@module-federation/bridge-react-webpack-plugin': 2.0.1 @@ -17056,126 +16881,18 @@ snapshots: - supports-color - utf-8-validate - '@module-federation/enhanced@2.0.1(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': - dependencies: - '@module-federation/bridge-react-webpack-plugin': 2.0.1 - '@module-federation/cli': 2.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/data-prefetch': 2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@module-federation/dts-plugin': 2.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/error-codes': 2.0.1 - '@module-federation/inject-external-runtime-core-plugin': 2.0.1(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c) - '@module-federation/managers': 2.0.1 - '@module-federation/manifest': 2.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/rspack': 2.0.1(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/runtime-tools': 2.0.1 - '@module-federation/sdk': 2.0.1 - btoa: 1.2.1 - schema-utils: 4.3.3 - upath: 2.0.1 - optionalDependencies: - typescript: 5.9.3 - webpack: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2) - transitivePeerDependencies: - - '@rspack/core' - - bufferutil - - debug - - react - - react-dom - - supports-color - - utf-8-validate - - '@module-federation/enhanced@https://pkg.pr.new/module-federation/core/@module-federation/enhanced@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': - dependencies: - '@module-federation/bridge-react-webpack-plugin': https://pkg.pr.new/module-federation/core/@module-federation/bridge-react-webpack-plugin@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/cli': https://pkg.pr.new/module-federation/core/@module-federation/cli@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/data-prefetch': https://pkg.pr.new/module-federation/core/@module-federation/data-prefetch@73bd1ddb2266002f586a872ddae8020052c5457c(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@module-federation/dts-plugin': https://pkg.pr.new/module-federation/core/@module-federation/dts-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/error-codes': https://pkg.pr.new/module-federation/core/@module-federation/error-codes@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/inject-external-runtime-core-plugin': https://pkg.pr.new/module-federation/core/@module-federation/inject-external-runtime-core-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(@module-federation/runtime-tools@2.0.1) - '@module-federation/managers': https://pkg.pr.new/module-federation/core/@module-federation/managers@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/manifest': https://pkg.pr.new/module-federation/core/@module-federation/manifest@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/rspack': https://pkg.pr.new/module-federation/core/@module-federation/rspack@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/runtime-tools': https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c - btoa: 1.2.1 - schema-utils: 4.3.3 - upath: 2.0.1 - optionalDependencies: - typescript: 5.9.3 - webpack: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2) - transitivePeerDependencies: - - '@rspack/core' - - bufferutil - - debug - - react - - react-dom - - supports-color - - utf-8-validate - - '@module-federation/enhanced@https://pkg.pr.new/module-federation/core/@module-federation/enhanced@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': - dependencies: - '@module-federation/bridge-react-webpack-plugin': https://pkg.pr.new/module-federation/core/@module-federation/bridge-react-webpack-plugin@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/cli': https://pkg.pr.new/module-federation/core/@module-federation/cli@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/data-prefetch': https://pkg.pr.new/module-federation/core/@module-federation/data-prefetch@73bd1ddb2266002f586a872ddae8020052c5457c(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@module-federation/dts-plugin': https://pkg.pr.new/module-federation/core/@module-federation/dts-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/error-codes': https://pkg.pr.new/module-federation/core/@module-federation/error-codes@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/inject-external-runtime-core-plugin': https://pkg.pr.new/module-federation/core/@module-federation/inject-external-runtime-core-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c) - '@module-federation/managers': https://pkg.pr.new/module-federation/core/@module-federation/managers@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/manifest': https://pkg.pr.new/module-federation/core/@module-federation/manifest@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/rspack': https://pkg.pr.new/module-federation/core/@module-federation/rspack@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/runtime-tools': https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c - btoa: 1.2.1 - schema-utils: 4.3.3 - upath: 2.0.1 - optionalDependencies: - typescript: 5.9.3 - webpack: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2) - transitivePeerDependencies: - - '@rspack/core' - - bufferutil - - debug - - react - - react-dom - - supports-color - - utf-8-validate - - '@module-federation/error-codes@0.22.0': {} - - '@module-federation/error-codes@2.0.0': {} - '@module-federation/error-codes@2.0.1': {} - '@module-federation/error-codes@https://pkg.pr.new/module-federation/core/@module-federation/error-codes@73bd1ddb2266002f586a872ddae8020052c5457c': {} - '@module-federation/inject-external-runtime-core-plugin@2.0.1(@module-federation/runtime-tools@2.0.1)': dependencies: '@module-federation/runtime-tools': 2.0.1 - '@module-federation/inject-external-runtime-core-plugin@2.0.1(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)': - dependencies: - '@module-federation/runtime-tools': https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c - - '@module-federation/inject-external-runtime-core-plugin@https://pkg.pr.new/module-federation/core/@module-federation/inject-external-runtime-core-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(@module-federation/runtime-tools@2.0.1)': - dependencies: - '@module-federation/runtime-tools': 2.0.1 - - '@module-federation/inject-external-runtime-core-plugin@https://pkg.pr.new/module-federation/core/@module-federation/inject-external-runtime-core-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)': - dependencies: - '@module-federation/runtime-tools': https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/managers@2.0.1': dependencies: '@module-federation/sdk': 2.0.1 find-pkg: 2.0.0 fs-extra: 9.1.0 - '@module-federation/managers@https://pkg.pr.new/module-federation/core/@module-federation/managers@73bd1ddb2266002f586a872ddae8020052c5457c': - dependencies: - '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c - find-pkg: 2.0.0 - fs-extra: 9.1.0 - '@module-federation/manifest@2.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: '@module-federation/dts-plugin': 2.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) @@ -17191,60 +16908,15 @@ snapshots: - utf-8-validate - vue-tsc - '@module-federation/manifest@https://pkg.pr.new/module-federation/core/@module-federation/manifest@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': - dependencies: - '@module-federation/dts-plugin': https://pkg.pr.new/module-federation/core/@module-federation/dts-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/managers': https://pkg.pr.new/module-federation/core/@module-federation/managers@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c - chalk: 3.0.0 - find-pkg: 2.0.0 - transitivePeerDependencies: - - bufferutil - - debug - - supports-color - - typescript - - utf-8-validate - - vue-tsc - '@module-federation/modern-js-v3@https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': dependencies: - '@module-federation/bridge-react': https://pkg.pr.new/module-federation/core/@module-federation/bridge-react@73bd1ddb2266002f586a872ddae8020052c5457c(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) - '@module-federation/cli': https://pkg.pr.new/module-federation/core/@module-federation/cli@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/enhanced': https://pkg.pr.new/module-federation/core/@module-federation/enhanced@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + '@module-federation/bridge-react': 2.0.1(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + '@module-federation/cli': 2.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@module-federation/enhanced': 2.0.1(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) '@module-federation/node': 2.7.32(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) - '@module-federation/rsbuild-plugin': https://pkg.pr.new/module-federation/core/@module-federation/rsbuild-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) - '@module-federation/runtime': https://pkg.pr.new/module-federation/core/@module-federation/runtime@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c - '@swc/helpers': 0.5.18 - fs-extra: 11.3.0 - jiti: 2.4.2 - lru-cache: 10.4.3 - node-fetch: 3.3.2 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-error-boundary: 4.1.2(react@19.2.4) - optionalDependencies: - react-router: 7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react-router-dom: 7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - typescript: 5.9.3 - transitivePeerDependencies: - - '@rsbuild/core' - - '@rspack/core' - - bufferutil - - debug - - supports-color - - utf-8-validate - - webpack - - '@module-federation/modern-js-v3@https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': - dependencies: - '@module-federation/bridge-react': https://pkg.pr.new/module-federation/core/@module-federation/bridge-react@73bd1ddb2266002f586a872ddae8020052c5457c(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) - '@module-federation/cli': https://pkg.pr.new/module-federation/core/@module-federation/cli@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/enhanced': https://pkg.pr.new/module-federation/core/@module-federation/enhanced@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) - '@module-federation/node': 2.7.32(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) - '@module-federation/rsbuild-plugin': https://pkg.pr.new/module-federation/core/@module-federation/rsbuild-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) - '@module-federation/runtime': https://pkg.pr.new/module-federation/core/@module-federation/runtime@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/rsbuild-plugin': 2.0.1(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + '@module-federation/runtime': 2.0.1 + '@module-federation/sdk': 2.0.1 '@swc/helpers': 0.5.18 fs-extra: 11.3.0 jiti: 2.4.2 @@ -17287,50 +16959,11 @@ snapshots: - utf-8-validate - vue-tsc - '@module-federation/node@2.7.32(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': - dependencies: - '@module-federation/enhanced': 2.0.1(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) - '@module-federation/runtime': 2.0.1 - '@module-federation/sdk': 2.0.1 - btoa: 1.2.1 - encoding: 0.1.13 - node-fetch: 2.7.0(encoding@0.1.13) - optionalDependencies: - webpack: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2) - transitivePeerDependencies: - - '@rspack/core' - - bufferutil - - debug - - react - - react-dom - - supports-color - - typescript - - utf-8-validate - - vue-tsc - - '@module-federation/rsbuild-plugin@https://pkg.pr.new/module-federation/core/@module-federation/rsbuild-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': + '@module-federation/rsbuild-plugin@2.0.1(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': dependencies: - '@module-federation/enhanced': https://pkg.pr.new/module-federation/core/@module-federation/enhanced@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + '@module-federation/enhanced': 2.0.1(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) '@module-federation/node': 2.7.32(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) - '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c - fs-extra: 11.3.0 - transitivePeerDependencies: - - '@rspack/core' - - bufferutil - - debug - - react - - react-dom - - supports-color - - typescript - - utf-8-validate - - vue-tsc - - webpack - - '@module-federation/rsbuild-plugin@https://pkg.pr.new/module-federation/core/@module-federation/rsbuild-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': - dependencies: - '@module-federation/enhanced': https://pkg.pr.new/module-federation/core/@module-federation/enhanced@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) - '@module-federation/node': 2.7.32(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) - '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c + '@module-federation/sdk': 2.0.1 fs-extra: 11.3.0 transitivePeerDependencies: - '@rspack/core' @@ -17363,157 +16996,35 @@ snapshots: - supports-color - utf-8-validate - '@module-federation/rspack@2.0.1(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': - dependencies: - '@module-federation/bridge-react-webpack-plugin': 2.0.1 - '@module-federation/dts-plugin': 2.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/inject-external-runtime-core-plugin': 2.0.1(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c) - '@module-federation/managers': 2.0.1 - '@module-federation/manifest': 2.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/runtime-tools': 2.0.1 - '@module-federation/sdk': 2.0.1 - '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18) - btoa: 1.2.1 - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - bufferutil - - debug - - supports-color - - utf-8-validate - - '@module-federation/rspack@https://pkg.pr.new/module-federation/core/@module-federation/rspack@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': - dependencies: - '@module-federation/bridge-react-webpack-plugin': https://pkg.pr.new/module-federation/core/@module-federation/bridge-react-webpack-plugin@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/dts-plugin': https://pkg.pr.new/module-federation/core/@module-federation/dts-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/inject-external-runtime-core-plugin': https://pkg.pr.new/module-federation/core/@module-federation/inject-external-runtime-core-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(@module-federation/runtime-tools@2.0.1) - '@module-federation/managers': https://pkg.pr.new/module-federation/core/@module-federation/managers@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/manifest': https://pkg.pr.new/module-federation/core/@module-federation/manifest@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/runtime-tools': https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c - '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18) - btoa: 1.2.1 - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - bufferutil - - debug - - supports-color - - utf-8-validate - - '@module-federation/rspack@https://pkg.pr.new/module-federation/core/@module-federation/rspack@73bd1ddb2266002f586a872ddae8020052c5457c(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': - dependencies: - '@module-federation/bridge-react-webpack-plugin': https://pkg.pr.new/module-federation/core/@module-federation/bridge-react-webpack-plugin@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/dts-plugin': https://pkg.pr.new/module-federation/core/@module-federation/dts-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/inject-external-runtime-core-plugin': https://pkg.pr.new/module-federation/core/@module-federation/inject-external-runtime-core-plugin@73bd1ddb2266002f586a872ddae8020052c5457c(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c) - '@module-federation/managers': https://pkg.pr.new/module-federation/core/@module-federation/managers@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/manifest': https://pkg.pr.new/module-federation/core/@module-federation/manifest@73bd1ddb2266002f586a872ddae8020052c5457c(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@module-federation/runtime-tools': https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c - '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18) - btoa: 1.2.1 - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - bufferutil - - debug - - supports-color - - utf-8-validate - - '@module-federation/runtime-core@0.22.0': - dependencies: - '@module-federation/error-codes': 0.22.0 - '@module-federation/sdk': 0.22.0 - - '@module-federation/runtime-core@2.0.0': - dependencies: - '@module-federation/error-codes': 2.0.0 - '@module-federation/sdk': 2.0.0 - '@module-federation/runtime-core@2.0.1': dependencies: '@module-federation/error-codes': 2.0.1 '@module-federation/sdk': 2.0.1 - '@module-federation/runtime-core@https://pkg.pr.new/module-federation/core/@module-federation/runtime-core@73bd1ddb2266002f586a872ddae8020052c5457c': - dependencies: - '@module-federation/error-codes': https://pkg.pr.new/module-federation/core/@module-federation/error-codes@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c - - '@module-federation/runtime-tools@0.22.0': - dependencies: - '@module-federation/runtime': 0.22.0 - '@module-federation/webpack-bundler-runtime': 0.22.0 - '@module-federation/runtime-tools@2.0.1': dependencies: '@module-federation/runtime': 2.0.1 '@module-federation/webpack-bundler-runtime': 2.0.1 - '@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c': - dependencies: - '@module-federation/runtime': https://pkg.pr.new/module-federation/core/@module-federation/runtime@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/webpack-bundler-runtime': https://pkg.pr.new/module-federation/core/@module-federation/webpack-bundler-runtime@73bd1ddb2266002f586a872ddae8020052c5457c - - '@module-federation/runtime@0.22.0': - dependencies: - '@module-federation/error-codes': 0.22.0 - '@module-federation/runtime-core': 0.22.0 - '@module-federation/sdk': 0.22.0 - - '@module-federation/runtime@2.0.0': - dependencies: - '@module-federation/error-codes': 2.0.0 - '@module-federation/runtime-core': 2.0.0 - '@module-federation/sdk': 2.0.0 - '@module-federation/runtime@2.0.1': dependencies: '@module-federation/error-codes': 2.0.1 '@module-federation/runtime-core': 2.0.1 '@module-federation/sdk': 2.0.1 - '@module-federation/runtime@https://pkg.pr.new/module-federation/core/@module-federation/runtime@73bd1ddb2266002f586a872ddae8020052c5457c': - dependencies: - '@module-federation/error-codes': https://pkg.pr.new/module-federation/core/@module-federation/error-codes@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/runtime-core': https://pkg.pr.new/module-federation/core/@module-federation/runtime-core@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c - - '@module-federation/sdk@0.22.0': {} - - '@module-federation/sdk@2.0.0': {} - '@module-federation/sdk@2.0.1': {} - '@module-federation/sdk@https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c': {} - '@module-federation/third-party-dts-extractor@2.0.1': dependencies: find-pkg: 2.0.0 fs-extra: 9.1.0 resolve: 1.22.8 - '@module-federation/third-party-dts-extractor@https://pkg.pr.new/module-federation/core/@module-federation/third-party-dts-extractor@73bd1ddb2266002f586a872ddae8020052c5457c': - dependencies: - find-pkg: 2.0.0 - fs-extra: 9.1.0 - resolve: 1.22.8 - - '@module-federation/webpack-bundler-runtime@0.22.0': - dependencies: - '@module-federation/runtime': 0.22.0 - '@module-federation/sdk': 0.22.0 - '@module-federation/webpack-bundler-runtime@2.0.1': dependencies: '@module-federation/runtime': 2.0.1 '@module-federation/sdk': 2.0.1 - '@module-federation/webpack-bundler-runtime@https://pkg.pr.new/module-federation/core/@module-federation/webpack-bundler-runtime@73bd1ddb2266002f586a872ddae8020052c5457c': - dependencies: - '@module-federation/runtime': https://pkg.pr.new/module-federation/core/@module-federation/runtime@73bd1ddb2266002f586a872ddae8020052c5457c - '@module-federation/sdk': https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c - '@mswjs/interceptors@0.39.8': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -17845,9 +17356,9 @@ snapshots: core-js: 3.47.0 jiti: 2.6.1 - '@rsbuild/core@2.0.0-beta.1(core-js@3.48.0)': + '@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0)': dependencies: - '@rspack/core': 2.0.0-alpha.1(@swc/helpers@0.5.18) + '@rspack/core': 2.0.0-alpha.1(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18) '@swc/helpers': 0.5.18 jiti: 2.6.1 optionalDependencies: @@ -17855,26 +17366,26 @@ snapshots: transitivePeerDependencies: - '@module-federation/runtime-tools' - '@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)': + '@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0)': dependencies: - '@rspack/core': '@rspack-canary/core@2.0.0-canary-032bd1ff-20260212021235(@swc/helpers@0.5.18)' + '@rspack/core': '@rspack-canary/core@2.0.0-canary-032bd1ff-20260212021235(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18)' '@swc/helpers': 0.5.18 optionalDependencies: core-js: 3.48.0 transitivePeerDependencies: - '@module-federation/runtime-tools' - '@rsbuild/plugin-assets-retry@1.5.1(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))': + '@rsbuild/plugin-assets-retry@1.5.1(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0))': optionalDependencies: - '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) - '@rsbuild/plugin-babel@1.1.0(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))': + '@rsbuild/plugin-babel@1.1.0(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0))': dependencies: '@babel/core': 7.28.6 '@babel/plugin-proposal-decorators': 7.28.6(@babel/core@7.28.6) '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.28.6) '@babel/preset-typescript': 7.28.5(@babel/core@7.28.6) - '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) '@types/babel__core': 7.20.5 deepmerge: 4.3.1 reduce-configs: 1.1.1 @@ -17882,7 +17393,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@rsbuild/plugin-check-syntax@1.6.1(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))': + '@rsbuild/plugin-check-syntax@1.6.1(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0))': dependencies: acorn: 8.15.0 browserslist-to-es-version: 1.2.0 @@ -17890,14 +17401,14 @@ snapshots: picocolors: 1.1.1 source-map: 0.7.6 optionalDependencies: - '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) - '@rsbuild/plugin-css-minimizer@1.1.1(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(esbuild@0.27.2)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17))(esbuild@0.27.2))': + '@rsbuild/plugin-css-minimizer@1.1.1(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0))(esbuild@0.27.2)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17))(esbuild@0.27.2))': dependencies: css-minimizer-webpack-plugin: 7.0.2(esbuild@0.27.2)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17))(esbuild@0.27.2)) reduce-configs: 1.1.1 optionalDependencies: - '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) transitivePeerDependencies: - '@parcel/css' - '@swc/css' @@ -17907,72 +17418,72 @@ snapshots: - lightningcss - webpack - '@rsbuild/plugin-less@1.6.0(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))': + '@rsbuild/plugin-less@1.6.0(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0))': dependencies: - '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) deepmerge: 4.3.1 reduce-configs: 1.1.1 - '@rsbuild/plugin-react@1.4.4(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))': + '@rsbuild/plugin-react@1.4.4(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0))': dependencies: - '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) '@rspack/plugin-react-refresh': 1.6.0(react-refresh@0.18.0) react-refresh: 0.18.0 transitivePeerDependencies: - webpack-hot-middleware - '@rsbuild/plugin-react@1.4.5(@rsbuild/core@2.0.0-beta.1(core-js@3.48.0))': + '@rsbuild/plugin-react@1.4.5(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0))': dependencies: - '@rsbuild/core': 2.0.0-beta.1(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.1(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) '@rspack/plugin-react-refresh': 1.6.0(react-refresh@0.18.0) react-refresh: 0.18.0 transitivePeerDependencies: - webpack-hot-middleware - '@rsbuild/plugin-rem@1.0.5(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))': + '@rsbuild/plugin-rem@1.0.5(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0))': dependencies: deepmerge: 4.3.1 terser: 5.46.0 optionalDependencies: - '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) - '@rsbuild/plugin-sass@1.5.0(@rsbuild/core@2.0.0-beta.1(core-js@3.48.0))': + '@rsbuild/plugin-sass@1.5.0(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0))': dependencies: - '@rsbuild/core': 2.0.0-beta.1(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.1(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) deepmerge: 4.3.1 loader-utils: 2.0.4 postcss: 8.5.6 reduce-configs: 1.1.1 sass-embedded: 1.97.3 - '@rsbuild/plugin-sass@1.5.0(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))': + '@rsbuild/plugin-sass@1.5.0(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0))': dependencies: - '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) deepmerge: 4.3.1 loader-utils: 2.0.4 postcss: 8.5.6 reduce-configs: 1.1.1 sass-embedded: 1.97.3 - '@rsbuild/plugin-source-build@1.0.4(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))': + '@rsbuild/plugin-source-build@1.0.4(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0))': dependencies: fast-glob: 3.3.3 json5: 2.2.3 yaml: 2.8.2 optionalDependencies: - '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) - '@rsbuild/plugin-styled-components@1.6.1(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))': + '@rsbuild/plugin-styled-components@1.6.1(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0))': dependencies: '@swc/plugin-styled-components': 12.3.0 reduce-configs: 1.1.1 optionalDependencies: - '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) - '@rsbuild/plugin-svgr@1.3.0(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(typescript@5.9.3)': + '@rsbuild/plugin-svgr@1.3.0(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0))(typescript@5.9.3)': dependencies: - '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) - '@rsbuild/plugin-react': 1.4.4(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) + '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) + '@rsbuild/plugin-react': 1.4.4(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0)) '@svgr/core': 8.1.0(typescript@5.9.3) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3))(typescript@5.9.3) @@ -17983,27 +17494,27 @@ snapshots: - typescript - webpack-hot-middleware - '@rsbuild/plugin-type-check@1.3.3(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17))(typescript@5.9.3)': + '@rsbuild/plugin-type-check@1.3.3(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0))(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.17))(typescript@5.9.3)': dependencies: deepmerge: 4.3.1 json5: 2.2.3 reduce-configs: 1.1.1 - ts-checker-rspack-plugin: 1.2.3(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17))(typescript@5.9.3) + ts-checker-rspack-plugin: 1.2.3(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.17))(typescript@5.9.3) optionalDependencies: - '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) transitivePeerDependencies: - '@rspack/core' - typescript - '@rsbuild/plugin-typed-css-modules@1.2.1(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))': + '@rsbuild/plugin-typed-css-modules@1.2.1(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0))': optionalDependencies: - '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) '@rsdoctor/client@1.5.2': {} - '@rsdoctor/core@1.5.2(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(@rspack/core@1.7.5(@swc/helpers@0.5.18))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': + '@rsdoctor/core@1.5.2(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0))(@rspack/core@1.7.5(@swc/helpers@0.5.18))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': dependencies: - '@rsbuild/plugin-check-syntax': 1.6.1(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) + '@rsbuild/plugin-check-syntax': 1.6.1(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0)) '@rsdoctor/graph': 1.5.2(@rspack/core@1.7.5(@swc/helpers@0.5.18))(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) '@rsdoctor/sdk': 1.5.2(@rspack/core@1.7.5(@swc/helpers@0.5.18))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) '@rsdoctor/types': 1.5.2(@rspack/core@1.7.5(@swc/helpers@0.5.18))(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) @@ -18034,9 +17545,9 @@ snapshots: - '@rspack/core' - webpack - '@rsdoctor/rspack-plugin@1.5.2(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(@rspack/core@1.7.5(@swc/helpers@0.5.18))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': + '@rsdoctor/rspack-plugin@1.5.2(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0))(@rspack/core@1.7.5(@swc/helpers@0.5.18))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2))': dependencies: - '@rsdoctor/core': 1.5.2(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(@rspack/core@1.7.5(@swc/helpers@0.5.18))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + '@rsdoctor/core': 1.5.2(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0))(@rspack/core@1.7.5(@swc/helpers@0.5.18))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) '@rsdoctor/graph': 1.5.2(@rspack/core@1.7.5(@swc/helpers@0.5.18))(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) '@rsdoctor/sdk': 1.5.2(@rspack/core@1.7.5(@swc/helpers@0.5.18))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) '@rsdoctor/types': 1.5.2(@rspack/core@1.7.5(@swc/helpers@0.5.18))(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) @@ -18151,10 +17662,11 @@ snapshots: '@rspack/binding-win32-ia32-msvc': '@rspack-canary/binding-win32-ia32-msvc@2.0.0-canary-032bd1ff-20260212021235' '@rspack/binding-win32-x64-msvc': '@rspack-canary/binding-win32-x64-msvc@2.0.0-canary-032bd1ff-20260212021235' - '@rspack-canary/core@2.0.0-canary-032bd1ff-20260212021235(@swc/helpers@0.5.18)': + '@rspack-canary/core@2.0.0-canary-032bd1ff-20260212021235(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18)': dependencies: '@rspack/binding': '@rspack-canary/binding@2.0.0-canary-032bd1ff-20260212021235' optionalDependencies: + '@module-federation/runtime-tools': 2.0.1 '@swc/helpers': 0.5.18 '@rspack/binding-darwin-arm64@1.7.0': @@ -18339,7 +17851,7 @@ snapshots: '@rspack/core@1.7.0(@swc/helpers@0.5.18)': dependencies: - '@module-federation/runtime-tools': 0.22.0 + '@module-federation/runtime-tools': 2.0.1 '@rspack/binding': 1.7.0 '@rspack/lite-tapable': 1.1.0 optionalDependencies: @@ -18347,17 +17859,18 @@ snapshots: '@rspack/core@1.7.5(@swc/helpers@0.5.18)': dependencies: - '@module-federation/runtime-tools': 0.22.0 + '@module-federation/runtime-tools': 2.0.1 '@rspack/binding': 1.7.5 '@rspack/lite-tapable': 1.1.0 optionalDependencies: '@swc/helpers': 0.5.18 - '@rspack/core@2.0.0-alpha.1(@swc/helpers@0.5.18)': + '@rspack/core@2.0.0-alpha.1(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18)': dependencies: '@rspack/binding': 2.0.0-alpha.1 '@rspack/lite-tapable': 1.1.0 optionalDependencies: + '@module-federation/runtime-tools': 2.0.1 '@swc/helpers': 0.5.18 '@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.17)': @@ -18374,13 +17887,6 @@ snapshots: '@module-federation/runtime-tools': 2.0.1 '@swc/helpers': 0.5.18 - '@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18)': - dependencies: - '@rspack/binding': 2.0.0-beta.2 - optionalDependencies: - '@module-federation/runtime-tools': https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c - '@swc/helpers': 0.5.18 - '@rspack/lite-tapable@1.1.0': {} '@rspack/plugin-react-refresh@1.6.0(react-refresh@0.18.0)': @@ -18389,13 +17895,13 @@ snapshots: html-entities: 2.6.0 react-refresh: 0.18.0 - '@rspress/core@2.0.2(@types/react@19.2.13)(core-js@3.48.0)': + '@rspress/core@2.0.2(@module-federation/runtime-tools@2.0.1)(@types/react@19.2.13)(core-js@3.48.0)': dependencies: '@mdx-js/mdx': 3.1.1 '@mdx-js/react': 3.1.1(@types/react@19.2.13)(react@19.2.4) - '@rsbuild/core': 2.0.0-beta.1(core-js@3.48.0) - '@rsbuild/plugin-react': 1.4.5(@rsbuild/core@2.0.0-beta.1(core-js@3.48.0)) - '@rspress/shared': 2.0.2(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.1(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) + '@rsbuild/plugin-react': 1.4.5(@rsbuild/core@2.0.0-beta.1(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0)) + '@rspress/shared': 2.0.2(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) '@shikijs/rehype': 3.22.0 '@types/unist': 3.0.3 '@unhead/react': 2.1.2(react@19.2.4) @@ -18440,9 +17946,9 @@ snapshots: - supports-color - webpack-hot-middleware - '@rspress/plugin-llms@2.0.2(@rspress/core@2.0.2(@types/react@19.2.13)(core-js@3.48.0))': + '@rspress/plugin-llms@2.0.2(@rspress/core@2.0.2(@module-federation/runtime-tools@2.0.1)(@types/react@19.2.13)(core-js@3.48.0))': dependencies: - '@rspress/core': 2.0.2(@types/react@19.2.13)(core-js@3.48.0) + '@rspress/core': 2.0.2(@module-federation/runtime-tools@2.0.1)(@types/react@19.2.13)(core-js@3.48.0) remark-mdx: 3.1.1 remark-parse: 11.0.0 remark-stringify: 11.0.0 @@ -18451,9 +17957,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@rspress/shared@2.0.2(core-js@3.48.0)': + '@rspress/shared@2.0.2(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0)': dependencies: - '@rsbuild/core': 2.0.0-beta.1(core-js@3.48.0) + '@rsbuild/core': 2.0.0-beta.1(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) '@shikijs/rehype': 3.22.0 gray-matter: 4.0.3 lodash-es: 4.17.23 @@ -22714,14 +22220,14 @@ snapshots: jest-cli@29.7.0(@types/node@20.19.27)(babel-plugin-macros@3.1.0): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@20.19.27)(typescript@5.9.3)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@20.19.27)(typescript@5.9.3)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.19.27)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@20.19.27)(typescript@5.9.3)) + create-jest: 29.7.0(@types/node@20.19.27)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@20.19.27)(typescript@5.9.3)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.19.27)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@20.19.27)(typescript@5.9.3)) + jest-config: 29.7.0(@types/node@20.19.27)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@20.19.27)(typescript@5.9.3)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -23110,7 +22616,7 @@ snapshots: jest@29.7.0(@types/node@20.19.27)(babel-plugin-macros@3.1.0): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@20.19.27)(typescript@5.9.3)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.18))(@types/node@20.19.27)(typescript@5.9.3)) '@jest/types': 29.6.3 import-local: 3.2.0 jest-cli: 29.7.0(@types/node@20.19.27)(babel-plugin-macros@3.1.0) @@ -25670,15 +25176,15 @@ snapshots: optionalDependencies: react-dom: 19.2.4(react@19.2.4) - react-server-dom-rspack@0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-server-dom-rspack@0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18) + '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.17) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - react-server-dom-rspack@0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-server-dom-rspack@0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.17) + '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -25973,14 +25479,14 @@ snapshots: optionalDependencies: typescript: 5.9.3 - rsbuild-plugin-rsc@0.0.1-beta.0(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(react-server-dom-rspack@0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)): + rsbuild-plugin-rsc@0.0.1-beta.0(@rsbuild/core@2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0))(react-server-dom-rspack@0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)): dependencies: - '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) - react-server-dom-rspack: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rsbuild/core': 2.0.0-beta.4(@module-federation/runtime-tools@2.0.1)(core-js@3.48.0) + react-server-dom-rspack: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) rslog@1.3.2: {} - rspack-manifest-plugin@5.2.1(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17)): + rspack-manifest-plugin@5.2.1(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.17)): dependencies: '@rspack/lite-tapable': 1.1.0 optionalDependencies: @@ -26879,7 +26385,7 @@ snapshots: trough@2.2.0: {} - ts-checker-rspack-plugin@1.2.3(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17))(typescript@5.9.3): + ts-checker-rspack-plugin@1.2.3(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.17))(typescript@5.9.3): dependencies: '@babel/code-frame': 7.27.1 '@rspack/lite-tapable': 1.1.0 diff --git a/tests/integration/rsc-mf/remote/modern.config.ts b/tests/integration/rsc-mf/remote/modern.config.ts index 601aa9121a99..59b7ec871deb 100644 --- a/tests/integration/rsc-mf/remote/modern.config.ts +++ b/tests/integration/rsc-mf/remote/modern.config.ts @@ -3,10 +3,12 @@ import { appTools, defineConfig } from '@modern-js/app-tools'; import { moduleFederationPlugin } from '@module-federation/modern-js-v3'; const remotePort = process.env.RSC_MF_REMOTE_PORT || process.env.PORT || '3008'; +const enableRemoteServeSsr = Boolean(process.env.PORT); export default defineConfig({ server: { rsc: true, + ssr: enableRemoteServeSsr, }, // Keep RSC server entries synchronous for MF+RSC handlers. source: { @@ -31,6 +33,7 @@ export default defineConfig({ .add('require') .add('import') .add('default'); + chain.output.publicPath(`http://127.0.0.1:${remotePort}/bundles/`); } chain.resolve.modules From b57802370adf5bdaf4d56205c83fd7d27028a030 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Feb 2026 09:53:31 +0000 Subject: [PATCH 010/324] test(rsc-mf): apply layered shared scopes and client badge boundary --- .../rsc-mf/host/module-federation.config.ts | 70 +++++++++- .../host/src/server-component-root/App.tsx | 2 - .../HostRemoteActionRunner.tsx | 2 + .../rsc-mf/remote/module-federation.config.ts | 131 +++++++++++++++--- 4 files changed, 183 insertions(+), 22 deletions(-) diff --git a/tests/integration/rsc-mf/host/module-federation.config.ts b/tests/integration/rsc-mf/host/module-federation.config.ts index 50bb8a7ff218..55c4547d8955 100644 --- a/tests/integration/rsc-mf/host/module-federation.config.ts +++ b/tests/integration/rsc-mf/host/module-federation.config.ts @@ -1,16 +1,78 @@ +import path from 'path'; import { createModuleFederationConfig } from '@module-federation/modern-js-v3'; const REMOTE_PORT = process.env.RSC_MF_REMOTE_PORT || '3008'; +const LAYERS = { + ssr: 'server-side-rendering', + rsc: 'react-server-components', +} as const; + +const reactServerPath = path.join( + path.dirname(require.resolve('react/package.json')), + 'react.react-server.js', +); + +const layeredShared = [ + { + react: { + singleton: true, + requiredVersion: false, + shareScope: 'default', + }, + 'react-dom': { + singleton: true, + requiredVersion: false, + shareScope: 'default', + }, + }, + { + react: { + import: 'react', + shareKey: 'react', + singleton: true, + requiredVersion: false, + shareScope: 'ssr', + layer: LAYERS.ssr, + issuerLayer: LAYERS.ssr, + }, + 'react-dom': { + import: 'react-dom', + shareKey: 'react-dom', + singleton: true, + requiredVersion: false, + shareScope: 'ssr', + layer: LAYERS.ssr, + issuerLayer: LAYERS.ssr, + }, + }, + { + react: { + import: reactServerPath, + shareKey: 'react', + singleton: true, + requiredVersion: false, + shareScope: 'rsc', + layer: LAYERS.rsc, + issuerLayer: LAYERS.rsc, + }, + 'react-dom': { + import: 'react-dom', + shareKey: 'react-dom', + singleton: true, + requiredVersion: false, + shareScope: 'rsc', + layer: LAYERS.rsc, + issuerLayer: LAYERS.rsc, + }, + }, +] as const; export default createModuleFederationConfig({ name: 'rscHost', remotes: { rscRemote: `rscRemote@http://127.0.0.1:${REMOTE_PORT}/static/mf-manifest.json`, }, - shared: { - react: { singleton: true }, - 'react-dom': { singleton: true }, - }, + shared: layeredShared as any, dts: false, experiments: { asyncStartup: true, diff --git a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx index e993df8966c0..6dc42165c72d 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx @@ -1,7 +1,6 @@ import 'server-only'; import { Suspense } from 'react'; import { AsyncRemoteServerInfo } from 'rscRemote/AsyncRemoteServerInfo'; -import RemoteClientBadge from 'rscRemote/RemoteClientBadge'; import { RemoteNestedMixed } from 'rscRemote/RemoteNestedMixed'; import RemoteServerDefault from 'rscRemote/RemoteServerDefault'; import remoteMeta, { getRemoteMetaLabel } from 'rscRemote/remoteMeta'; @@ -31,7 +30,6 @@ const App = () => { Loading Remote RSC...}> - ); diff --git a/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx b/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx index c28bc3d9642f..d698001afcb2 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState } from 'react'; +import RemoteClientBadge from 'rscRemote/RemoteClientBadge'; import { remoteActionEcho } from 'rscRemote/actions'; import defaultRemoteAction from 'rscRemote/defaultAction'; @@ -25,6 +26,7 @@ export default function HostRemoteActionRunner() { return (
+
}> - + From 64593db8692facbe5158d7f22a306d558e383dbb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Feb 2026 14:38:19 +0000 Subject: [PATCH 064/324] fix(rsc-mf): stop exposing RemoteNestedMixed from remote --- tests/integration/rsc-mf/remote/module-federation.config.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/integration/rsc-mf/remote/module-federation.config.ts b/tests/integration/rsc-mf/remote/module-federation.config.ts index f7b7c6ed897d..ee50385e41df 100644 --- a/tests/integration/rsc-mf/remote/module-federation.config.ts +++ b/tests/integration/rsc-mf/remote/module-federation.config.ts @@ -131,10 +131,6 @@ export default createModuleFederationConfig({ import: './src/components/AsyncRemoteServerInfo.tsx', layer: LAYERS.rsc, } as any, - './RemoteNestedMixed': { - import: './src/components/RemoteNestedMixed.tsx', - layer: LAYERS.rsc, - } as any, './remoteServerOnly': { import: './src/components/serverOnly.ts', layer: LAYERS.rsc, From d01e78d545c9c300c783040fa07a3b3a42f77de0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Feb 2026 14:40:49 +0000 Subject: [PATCH 065/324] fix(rsc-mf): patch counter aliases across expose chunks --- .../integration/rsc-mf/host/modern.config.ts | 11 ++++++++--- .../rsc-mf/host/server/modern.server.ts | 19 ++++++++++++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/tests/integration/rsc-mf/host/modern.config.ts b/tests/integration/rsc-mf/host/modern.config.ts index daf6084bfc6b..569e45b2a152 100644 --- a/tests/integration/rsc-mf/host/modern.config.ts +++ b/tests/integration/rsc-mf/host/modern.config.ts @@ -51,17 +51,22 @@ const copyRemoteExposeAssets = async (subDir: 'js' | 'css') => { .map(async file => { const sourceFile = path.join(remoteAsyncDir, file); const targetFile = path.join(hostAsyncDir, file); - const shouldPatchNestedMixedChunk = + const shouldPatchCounterAliasChunk = subDir === 'js' && - file.startsWith('__federation_expose_RemoteNestedMixed') && + file.startsWith('__federation_expose_') && file.endsWith('.js'); - if (!shouldPatchNestedMixedChunk) { + if (!shouldPatchCounterAliasChunk) { await fs.copyFile(sourceFile, targetFile); return; } const chunkText = await fs.readFile(sourceFile, 'utf-8'); + if (!chunkText.includes('remote-client-server-count')) { + await fs.copyFile(sourceFile, targetFile); + return; + } + const remoteCounterModuleId = resolveRemoteCounterModuleId(chunkText); await fs.writeFile( targetFile, diff --git a/tests/integration/rsc-mf/host/server/modern.server.ts b/tests/integration/rsc-mf/host/server/modern.server.ts index f112985ef904..4a24d2e196f4 100644 --- a/tests/integration/rsc-mf/host/server/modern.server.ts +++ b/tests/integration/rsc-mf/host/server/modern.server.ts @@ -115,12 +115,21 @@ const proxyRemoteFederationAsset: MiddlewareHandler = async (c, next) => { return; } - const shouldPatchNestedMixed = - pathname.startsWith( - '/static/js/async/__federation_expose_RemoteNestedMixed', - ) && pathname.endsWith('.js'); - if (shouldPatchNestedMixed) { + const shouldPatchCounterAlias = + pathname.startsWith('/static/js/async/__federation_expose_') && + pathname.endsWith('.js'); + if (shouldPatchCounterAlias) { let chunkText = await upstream.text(); + if (!chunkText.includes('remote-client-server-count')) { + c.res = new Response(chunkText, { + status: upstream.status, + headers: { + 'content-type': 'application/javascript; charset=utf-8', + }, + }); + return; + } + chunkText = `${chunkText}${createRemoteNestedMixedAliasChunk()}`; c.res = new Response(chunkText, { From 42c27947455f6de799207287f2fbc97269542781 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Feb 2026 20:36:37 +0000 Subject: [PATCH 066/324] fix(rsc-mf): render remote client counter from host client tree --- .../integration/rsc-mf/host/src/server-component-root/App.tsx | 3 +-- .../host/src/server-component-root/HostRemoteActionRunner.tsx | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx index 37dc202b0a2f..46ca6a880005 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx @@ -1,7 +1,6 @@ import 'server-only'; import { Suspense } from 'react'; import { AsyncRemoteServerInfo } from 'rscRemote/AsyncRemoteServerInfo'; -import { RemoteServerCard } from 'rscRemote/RemoteServerCard'; import RemoteServerDefault from 'rscRemote/RemoteServerDefault'; import remoteMeta, { getRemoteMetaLabel } from 'rscRemote/remoteMeta'; import { getServerOnlyInfo } from 'rscRemote/remoteServerOnly'; @@ -39,7 +38,7 @@ const App = () => { Loading Remote RSC...}> - + diff --git a/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx b/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx index 7c0c929b218c..96db7ef99161 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx @@ -37,6 +37,7 @@ export default function HostRemoteActionRunner() { return (
+
}> +
+
+ + + +
); diff --git a/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx b/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx index 96db7ef99161..aa925d2dfb7d 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx @@ -4,9 +4,20 @@ import { useEffect, useState } from 'react'; import RemoteClientBadge from 'rscRemote/RemoteClientBadge'; import { RemoteClientCounter as RemoteClientCounterBridge } from 'rscRemote/RemoteClientCounter'; import { remoteActionEcho } from 'rscRemote/actions'; -import defaultRemoteAction from 'rscRemote/defaultAction'; +import { defaultRemoteAction } from 'rscRemote/defaultAction'; import { registerRemoteServerCallback } from 'rscRemote/registerServerCallback'; +const REMOTE_ACTION_ID_TO_HOST_PROXY_ACTION_ID = { + '606c30f35d74d843171a8a71358eda595991e4ee16270e9f052af3faef57a19999': + '603cd42bd1c9b98894c6d03b3b688f513073ddbea04bb02d7fda1e72c57f96b69d', + '40e41a2ee9d9de373b364dcf2a0201701057c8502037bf9ef2cd26bb2a1259dabd': + '40f14c24f6d81be75aab6d9dc7941b9507bfbdb9daca25df7b1aed34703972c7ab', + '408da81ddb8214f8cb98a83552cb70c4d17b27b6fd36d972cac89e7030a4874fd4': + '40928b48a8dc80bb3a73661fbdbeb14155a85a1b965e9ef67a8f4132bbd4dda7e5', + '4019f2092c5baa86ad77fc144ce69129b81c38a1a6a1cb227a138b5d46de8977d7': + '404768f4f5d65c3edafd60e28fc1252837ade49f3b06dad341098530dea5bb7716', +} as const; + export default function HostRemoteActionRunner() { // Keep this import in the client graph so federated RSC bridge IDs // can map back to a concrete remote module factory at runtime. @@ -18,6 +29,8 @@ export default function HostRemoteActionRunner() { useEffect(() => { registerRemoteServerCallback( `${window.location.origin}/server-component-root`, + 'rscRemote', + REMOTE_ACTION_ID_TO_HOST_PROXY_ACTION_ID as Record, ); }, []); diff --git a/tests/integration/rsc-mf/host/src/server-component-root/remoteActionProxy.ts b/tests/integration/rsc-mf/host/src/server-component-root/remoteActionProxy.ts index 128f9177cc8d..425bcf41bf92 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/remoteActionProxy.ts +++ b/tests/integration/rsc-mf/host/src/server-component-root/remoteActionProxy.ts @@ -1,24 +1,24 @@ 'use server'; -let proxyRemoteCountState = 0; - export async function proxyIncrementRemoteCount( - _previousState: number, + previousState: number, formData: FormData, ) { - const count = Number(formData.get('count') || 1); - proxyRemoteCountState += count; - return proxyRemoteCountState; + const remote = await import('rscRemote/actions'); + return remote.incrementRemoteCount(previousState, formData); } export async function proxyRemoteActionEcho(value: string) { - return `remote-action:${value}`; + const remote = await import('rscRemote/actions'); + return remote.remoteActionEcho(value); } export async function proxyNestedRemoteAction(value: string) { - return `nested-action:${value}`; + const remote = await import('rscRemote/nestedActions'); + return remote.nestedRemoteAction(value); } export async function proxyDefaultRemoteAction(value: string) { - return `default-action:${value}`; + const remote = await import('rscRemote/defaultAction'); + return remote.defaultRemoteAction(value); } diff --git a/tests/integration/rsc-mf/remote/src/components/defaultAction.ts b/tests/integration/rsc-mf/remote/src/components/defaultAction.ts index 09f2458a768d..840844e7d440 100644 --- a/tests/integration/rsc-mf/remote/src/components/defaultAction.ts +++ b/tests/integration/rsc-mf/remote/src/components/defaultAction.ts @@ -1,5 +1,5 @@ 'use server'; -export default async function defaultRemoteAction(value: string) { +export async function defaultRemoteAction(value: string) { return `default-action:${value}`; } diff --git a/tests/integration/rsc-mf/remote/src/components/registerServerCallback.ts b/tests/integration/rsc-mf/remote/src/components/registerServerCallback.ts index 3f291289e09a..720891760029 100644 --- a/tests/integration/rsc-mf/remote/src/components/registerServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/components/registerServerCallback.ts @@ -5,27 +5,47 @@ import { setServerCallback, } from 'rsc-mf-react-server-dom-client-browser'; -let registeredRemoteOrigin = ''; +let registeredCallbackKey = ''; +const getHostActionId = (rawActionId: string, remoteAlias: string) => { + if (rawActionId.startsWith('remote:')) { + return rawActionId; + } + + return `remote:${remoteAlias}:${rawActionId}`; +}; -export function registerRemoteServerCallback(remoteOrigin: string) { - if (!remoteOrigin || registeredRemoteOrigin === remoteOrigin) { +export function registerRemoteServerCallback( + remoteOrigin: string, + remoteAlias = 'rscRemote', + remoteActionIdToHostProxyActionId?: Record, +) { + if (!remoteOrigin) { + return; + } + const callbackKey = JSON.stringify({ + remoteAlias, + remoteOrigin, + remoteActionIdToHostProxyActionId, + }); + if (registeredCallbackKey === callbackKey) { return; } const remoteActionUrl = new URL(remoteOrigin).toString(); - console.log(`[rsc-mf] registerRemoteServerCallback -> ${remoteActionUrl}`); setServerCallback(async (id, args) => { - console.log(`[rsc-mf] remote callback action id -> ${id}`); + const hostActionId = + remoteActionIdToHostProxyActionId?.[id] || + getHostActionId(id, remoteAlias); const temporaryReferences = createTemporaryReferenceSet(); const response = fetch(remoteActionUrl, { method: 'POST', headers: { Accept: 'text/x-component', - 'x-rsc-action': id, + 'x-rsc-action': hostActionId, }, body: await encodeReply(args, { temporaryReferences }), }); return createFromFetch(response, { temporaryReferences }); }); - registeredRemoteOrigin = remoteOrigin; + registeredCallbackKey = callbackKey; } From 07b5fa76b3dd9b2d0ff43d86f2a3541c578c125c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Feb 2026 22:01:14 +0000 Subject: [PATCH 068/324] test(rsc-mf): derive action-id map server-side for host proxies --- .../host/src/server-component-root/App.tsx | 33 ++++++++++++++++++- .../HostRemoteActionRunner.tsx | 21 ++++-------- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx index 950d2cf42e7b..2a7c65d908ed 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx @@ -2,6 +2,9 @@ import 'server-only'; import { Suspense } from 'react'; import { AsyncRemoteServerInfo } from 'rscRemote/AsyncRemoteServerInfo'; import RemoteServerDefault from 'rscRemote/RemoteServerDefault'; +import { incrementRemoteCount, remoteActionEcho } from 'rscRemote/actions'; +import { defaultRemoteAction } from 'rscRemote/defaultAction'; +import { nestedRemoteAction } from 'rscRemote/nestedActions'; import remoteMeta, { getRemoteMetaLabel } from 'rscRemote/remoteMeta'; import { getServerOnlyInfo } from 'rscRemote/remoteServerOnly'; import getServerOnlyDefaultInfo from 'rscRemote/remoteServerOnlyDefault'; @@ -15,6 +18,32 @@ import { } from './remoteActionProxy'; const App = () => { + const remoteActionIdToHostProxyActionId: Record = {}; + const remoteIncrementActionId = (incrementRemoteCount as any)?.$$id; + const remoteEchoActionId = (remoteActionEcho as any)?.$$id; + const remoteNestedActionId = (nestedRemoteAction as any)?.$$id; + const remoteDefaultActionId = (defaultRemoteAction as any)?.$$id; + const proxyIncrementActionId = (proxyIncrementRemoteCount as any)?.$$id; + const proxyEchoActionId = (proxyRemoteActionEcho as any)?.$$id; + const proxyNestedActionId = (proxyNestedRemoteAction as any)?.$$id; + const proxyDefaultActionId = (proxyDefaultRemoteAction as any)?.$$id; + + if (remoteIncrementActionId && proxyIncrementActionId) { + remoteActionIdToHostProxyActionId[remoteIncrementActionId] = + proxyIncrementActionId; + } + if (remoteEchoActionId && proxyEchoActionId) { + remoteActionIdToHostProxyActionId[remoteEchoActionId] = proxyEchoActionId; + } + if (remoteNestedActionId && proxyNestedActionId) { + remoteActionIdToHostProxyActionId[remoteNestedActionId] = + proxyNestedActionId; + } + if (remoteDefaultActionId && proxyDefaultActionId) { + remoteActionIdToHostProxyActionId[remoteDefaultActionId] = + proxyDefaultActionId; + } + const remoteServerOnlyInfo = getServerOnlyInfo(); const remoteServerOnlyDefaultInfo = getServerOnlyDefaultInfo(); const remoteMetaLabel = getRemoteMetaLabel(); @@ -41,7 +70,9 @@ const App = () => { - + ); }; diff --git a/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx b/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx index aa925d2dfb7d..50221035f94c 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx @@ -7,18 +7,11 @@ import { remoteActionEcho } from 'rscRemote/actions'; import { defaultRemoteAction } from 'rscRemote/defaultAction'; import { registerRemoteServerCallback } from 'rscRemote/registerServerCallback'; -const REMOTE_ACTION_ID_TO_HOST_PROXY_ACTION_ID = { - '606c30f35d74d843171a8a71358eda595991e4ee16270e9f052af3faef57a19999': - '603cd42bd1c9b98894c6d03b3b688f513073ddbea04bb02d7fda1e72c57f96b69d', - '40e41a2ee9d9de373b364dcf2a0201701057c8502037bf9ef2cd26bb2a1259dabd': - '40f14c24f6d81be75aab6d9dc7941b9507bfbdb9daca25df7b1aed34703972c7ab', - '408da81ddb8214f8cb98a83552cb70c4d17b27b6fd36d972cac89e7030a4874fd4': - '40928b48a8dc80bb3a73661fbdbeb14155a85a1b965e9ef67a8f4132bbd4dda7e5', - '4019f2092c5baa86ad77fc144ce69129b81c38a1a6a1cb227a138b5d46de8977d7': - '404768f4f5d65c3edafd60e28fc1252837ade49f3b06dad341098530dea5bb7716', -} as const; - -export default function HostRemoteActionRunner() { +export default function HostRemoteActionRunner({ + remoteActionIdToHostProxyActionId, +}: { + remoteActionIdToHostProxyActionId: Record; +}) { // Keep this import in the client graph so federated RSC bridge IDs // can map back to a concrete remote module factory at runtime. void RemoteClientCounterBridge; @@ -30,9 +23,9 @@ export default function HostRemoteActionRunner() { registerRemoteServerCallback( `${window.location.origin}/server-component-root`, 'rscRemote', - REMOTE_ACTION_ID_TO_HOST_PROXY_ACTION_ID as Record, + remoteActionIdToHostProxyActionId, ); - }, []); + }, [JSON.stringify(remoteActionIdToHostProxyActionId)]); const runActions = async () => { setIsPending(true); From 2fcf0483693db374066cad387ef31d055011df2c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Feb 2026 22:12:14 +0000 Subject: [PATCH 069/324] test(rsc-mf): remove transient debug logging from integration test --- tests/integration/rsc-mf/tests/index.test.ts | 34 -------------------- 1 file changed, 34 deletions(-) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 64a52590db8d..16bff89c62fa 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -223,40 +223,6 @@ function runTests({ mode }: TestConfig) { const err = error as Error; const message = err.message; runtimeErrors.push(message); - // Debugging aid for flaky integration failures. - console.log(`[pageerror:${mode}] ${message}`); - if (err.stack) { - console.log(`[pageerror:${mode}:stack] ${err.stack}`); - } - }); - page.on('console', msg => { - if (msg.type() === 'error' || msg.text().includes('[rsc-mf]')) { - const location = msg.location(); - const suffix = location?.url - ? ` @ ${location.url}:${location.lineNumber}:${location.columnNumber}` - : ''; - console.log(`[browser:${mode}] ${msg.text()}${suffix}`); - } - }); - page.on('response', async response => { - if (response.status() >= 400 && response.url().includes(HOST_RSC_URL)) { - console.log( - `[response:${mode}] ${response.status()} ${response.url()}`, - ); - const body = await response.text().catch(() => ''); - if (body) { - console.log(`[response:${mode}:body] ${body}`); - } - } - }); - page.on('request', request => { - if ( - request.method() === 'POST' && - request.url().includes(HOST_RSC_URL) - ) { - const actionId = request.headers()['x-rsc-action']; - console.log(`[request:${mode}] action=${actionId || 'missing'}`); - } }); }); From 830c1a198ff329e649ca7532a4272adfd6a610cb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Feb 2026 22:19:32 +0000 Subject: [PATCH 070/324] test(rsc-mf): simplify server action-id mapping in host app --- .../host/src/server-component-root/App.tsx | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx index 2a7c65d908ed..4c8bb2701192 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx @@ -17,32 +17,30 @@ import { proxyRemoteActionEcho, } from './remoteActionProxy'; -const App = () => { - const remoteActionIdToHostProxyActionId: Record = {}; - const remoteIncrementActionId = (incrementRemoteCount as any)?.$$id; - const remoteEchoActionId = (remoteActionEcho as any)?.$$id; - const remoteNestedActionId = (nestedRemoteAction as any)?.$$id; - const remoteDefaultActionId = (defaultRemoteAction as any)?.$$id; - const proxyIncrementActionId = (proxyIncrementRemoteCount as any)?.$$id; - const proxyEchoActionId = (proxyRemoteActionEcho as any)?.$$id; - const proxyNestedActionId = (proxyNestedRemoteAction as any)?.$$id; - const proxyDefaultActionId = (proxyDefaultRemoteAction as any)?.$$id; +const getServerActionId = (action: unknown) => + (action as { $$id?: string } | undefined)?.$$id; - if (remoteIncrementActionId && proxyIncrementActionId) { - remoteActionIdToHostProxyActionId[remoteIncrementActionId] = - proxyIncrementActionId; - } - if (remoteEchoActionId && proxyEchoActionId) { - remoteActionIdToHostProxyActionId[remoteEchoActionId] = proxyEchoActionId; - } - if (remoteNestedActionId && proxyNestedActionId) { - remoteActionIdToHostProxyActionId[remoteNestedActionId] = - proxyNestedActionId; - } - if (remoteDefaultActionId && proxyDefaultActionId) { - remoteActionIdToHostProxyActionId[remoteDefaultActionId] = - proxyDefaultActionId; - } +const App = () => { + const remoteActionIdToHostProxyActionId = Object.fromEntries( + [ + [ + getServerActionId(incrementRemoteCount), + getServerActionId(proxyIncrementRemoteCount), + ], + [ + getServerActionId(remoteActionEcho), + getServerActionId(proxyRemoteActionEcho), + ], + [ + getServerActionId(nestedRemoteAction), + getServerActionId(proxyNestedRemoteAction), + ], + [ + getServerActionId(defaultRemoteAction), + getServerActionId(proxyDefaultRemoteAction), + ], + ].filter((pair): pair is [string, string] => Boolean(pair[0] && pair[1])), + ); const remoteServerOnlyInfo = getServerOnlyInfo(); const remoteServerOnlyDefaultInfo = getServerOnlyDefaultInfo(); @@ -64,7 +62,7 @@ const App = () => { Loading Remote RSC...}> -
+ }> + {/* Anchor host proxy actions in the server action manifest. */} ); } diff --git a/tests/integration/rsc-mf/remote/module-federation.config.ts b/tests/integration/rsc-mf/remote/module-federation.config.ts index ee50385e41df..4ef6df8aecb9 100644 --- a/tests/integration/rsc-mf/remote/module-federation.config.ts +++ b/tests/integration/rsc-mf/remote/module-federation.config.ts @@ -155,6 +155,10 @@ export default createModuleFederationConfig({ import: './src/components/defaultAction.ts', layer: LAYERS.rsc, } as any, + './actionBundle': { + import: './src/components/actionBundle.ts', + layer: LAYERS.rsc, + } as any, './registerServerCallback': { import: './src/components/registerServerCallback.ts', layer: LAYERS.rsc, diff --git a/tests/integration/rsc-mf/remote/src/components/actionBundle.ts b/tests/integration/rsc-mf/remote/src/components/actionBundle.ts new file mode 100644 index 000000000000..596e1df4087d --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/actionBundle.ts @@ -0,0 +1,6 @@ +export { + incrementRemoteCount as bundledIncrementRemoteCount, + remoteActionEcho as bundledRemoteActionEcho, +} from './actions'; +export { defaultRemoteAction as bundledDefaultRemoteAction } from './defaultAction'; +export { nestedRemoteAction as bundledNestedRemoteAction } from './nestedActions'; diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 16bff89c62fa..914663e4f468 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -98,40 +98,52 @@ async function supportRemoteClientAndServerActions({ await page.goto(`http://127.0.0.1:${hostPort}${HOST_RSC_URL}`, { waitUntil: ['networkidle0', 'domcontentloaded'], }); - await page.waitForSelector('.remote-client-local-increment'); + await page.waitForSelector( + '.host-remote-action-runner .remote-client-local-increment', + ); - let localCount = await page.$eval('.remote-client-local-count', el => - el.textContent?.trim(), + let localCount = await page.$eval( + '.host-remote-action-runner .remote-client-local-count', + el => el.textContent?.trim(), ); - let serverCount = await page.$eval('.remote-client-server-count', el => - el.textContent?.trim(), + let serverCount = await page.$eval( + '.host-remote-action-runner .remote-client-server-count', + el => el.textContent?.trim(), ); expect(localCount).toBe('0'); expect(serverCount).toBe('0'); - await page.click('.remote-client-local-increment'); - localCount = await page.$eval('.remote-client-local-count', el => - el.textContent?.trim(), + await page.click('.host-remote-action-runner .remote-client-local-increment'); + localCount = await page.$eval( + '.host-remote-action-runner .remote-client-local-count', + el => el.textContent?.trim(), ); expect(localCount).toBe('1'); - await page.click('.remote-client-server-increment'); + await page.click( + '.host-remote-action-runner .remote-client-server-increment', + ); await page.waitForFunction( () => !document - .querySelector('.remote-client-server-increment') + .querySelector( + '.host-remote-action-runner .remote-client-server-increment', + ) ?.hasAttribute('disabled'), ); - serverCount = await page.$eval('.remote-client-server-count', el => - el.textContent?.trim(), + serverCount = await page.$eval( + '.host-remote-action-runner .remote-client-server-count', + el => el.textContent?.trim(), ); expect(serverCount).toBe('1'); - await page.click('.remote-client-run-actions'); + await page.click('.host-remote-action-runner .remote-client-run-actions'); await page.waitForFunction(() => { - const nested = document.querySelector('.remote-client-nested-result'); + const nested = document.querySelector( + '.host-remote-action-runner .remote-client-nested-result', + ); const remoteAction = document.querySelector( - '.remote-client-remote-action-result', + '.host-remote-action-runner .remote-client-remote-action-result', ); return ( nested?.textContent?.trim() === 'nested-action:from-client' && @@ -139,13 +151,15 @@ async function supportRemoteClientAndServerActions({ ); }); - let badgeValue = await page.$eval('.remote-client-badge-value', el => - el.textContent?.trim(), + let badgeValue = await page.$eval( + '.host-remote-action-runner .remote-client-badge-value', + el => el.textContent?.trim(), ); expect(badgeValue).toBe('remote-client-badge-initial'); - await page.click('.remote-client-badge-toggle'); - badgeValue = await page.$eval('.remote-client-badge-value', el => - el.textContent?.trim(), + await page.click('.host-remote-action-runner .remote-client-badge-toggle'); + badgeValue = await page.$eval( + '.host-remote-action-runner .remote-client-badge-value', + el => el.textContent?.trim(), ); expect(badgeValue).toBe('remote-client-badge-toggled'); @@ -157,10 +171,21 @@ async function supportRemoteClientAndServerActions({ const echoActionResult = document.querySelector( '.host-remote-echo-action-result', ); + const bundledDefaultActionResult = document.querySelector( + '.host-remote-bundled-default-action-result', + ); + const bundledEchoActionResult = document.querySelector( + '.host-remote-bundled-echo-action-result', + ); return ( defaultActionResult?.textContent?.trim() === 'default-action:from-host-client' && - echoActionResult?.textContent?.trim() === 'remote-action:from-host-client' + echoActionResult?.textContent?.trim() === + 'remote-action:from-host-client' && + bundledDefaultActionResult?.textContent?.trim() === + 'default-action:from-host-client-bundled' && + bundledEchoActionResult?.textContent?.trim() === + 'remote-action:from-host-client-bundled' ); }); } From 77b3b26ebd0e83057600835def04eeb9e4c36796 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Feb 2026 23:08:18 +0000 Subject: [PATCH 073/324] test(rsc-mf): add bundled server-info expose coverage --- .../host/src/server-component-root/App.tsx | 15 ++++++++++++ .../rsc-mf/remote/module-federation.config.ts | 4 ++++ .../remote/src/components/infoBundle.ts | 6 +++++ tests/integration/rsc-mf/tests/index.test.ts | 24 +++++++++++++++++++ 4 files changed, 49 insertions(+) create mode 100644 tests/integration/rsc-mf/remote/src/components/infoBundle.ts diff --git a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx index 61d006f039f0..47f1a2864099 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx @@ -4,6 +4,12 @@ import { AsyncRemoteServerInfo } from 'rscRemote/AsyncRemoteServerInfo'; import RemoteServerDefault from 'rscRemote/RemoteServerDefault'; import { incrementRemoteCount, remoteActionEcho } from 'rscRemote/actions'; import { defaultRemoteAction } from 'rscRemote/defaultAction'; +import { + bundledRemoteMeta, + getBundledRemoteMetaLabel, + getBundledServerOnlyDefaultInfo, + getBundledServerOnlyInfo, +} from 'rscRemote/infoBundle'; import { nestedRemoteAction } from 'rscRemote/nestedActions'; import remoteMeta, { getRemoteMetaLabel } from 'rscRemote/remoteMeta'; import { getServerOnlyInfo } from 'rscRemote/remoteServerOnly'; @@ -48,6 +54,9 @@ const App = () => { const remoteServerOnlyInfo = getServerOnlyInfo(); const remoteServerOnlyDefaultInfo = getServerOnlyDefaultInfo(); const remoteMetaLabel = getRemoteMetaLabel(); + const bundledServerOnlyInfo = getBundledServerOnlyInfo(); + const bundledServerOnlyDefaultInfo = getBundledServerOnlyDefaultInfo(); + const bundledRemoteMetaLabel = getBundledRemoteMetaLabel(); return (
@@ -58,6 +67,12 @@ const App = () => {

{remoteMeta.kind}

{remoteMetaLabel}

+

{bundledServerOnlyInfo}

+

+ {bundledServerOnlyDefaultInfo} +

+

{bundledRemoteMeta.kind}

+

{bundledRemoteMetaLabel}

Loading Remote Async Server Info...
}> diff --git a/tests/integration/rsc-mf/remote/module-federation.config.ts b/tests/integration/rsc-mf/remote/module-federation.config.ts index 4ef6df8aecb9..8c2a687d83f6 100644 --- a/tests/integration/rsc-mf/remote/module-federation.config.ts +++ b/tests/integration/rsc-mf/remote/module-federation.config.ts @@ -159,6 +159,10 @@ export default createModuleFederationConfig({ import: './src/components/actionBundle.ts', layer: LAYERS.rsc, } as any, + './infoBundle': { + import: './src/components/infoBundle.ts', + layer: LAYERS.rsc, + } as any, './registerServerCallback': { import: './src/components/registerServerCallback.ts', layer: LAYERS.rsc, diff --git a/tests/integration/rsc-mf/remote/src/components/infoBundle.ts b/tests/integration/rsc-mf/remote/src/components/infoBundle.ts new file mode 100644 index 000000000000..a6a5a1811abe --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/infoBundle.ts @@ -0,0 +1,6 @@ +export { getServerOnlyInfo as getBundledServerOnlyInfo } from './serverOnly'; +export { default as getBundledServerOnlyDefaultInfo } from './serverOnlyDefault'; +export { + default as bundledRemoteMeta, + getRemoteMetaLabel as getBundledRemoteMetaLabel, +} from './remoteMeta'; diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 914663e4f468..19c035ff7962 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -62,6 +62,8 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { expect(html).toContain('rsc|mf|actions'); expect(html).toContain('remote-async-server-info-ok'); expect(html).toContain('Remote Default Server Card'); + expect(html).toContain('host-remote-bundled-server-only'); + expect(html).toContain('host-remote-bundled-meta-kind'); await page.goto(`http://127.0.0.1:${hostPort}${HOST_RSC_URL}`, { waitUntil: ['networkidle0', 'domcontentloaded'], @@ -84,6 +86,28 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { el.textContent?.trim(), ); expect(hostRemoteMetaLabel).toBe('rsc|mf|actions'); + const hostRemoteBundledServerOnly = await page.$eval( + '.host-remote-bundled-server-only', + el => el.textContent?.trim(), + ); + expect(hostRemoteBundledServerOnly).toBe('remote-server-only-ok'); + const hostRemoteBundledServerOnlyDefault = await page.$eval( + '.host-remote-bundled-server-only-default', + el => el.textContent?.trim(), + ); + expect(hostRemoteBundledServerOnlyDefault).toBe( + 'remote-server-only-default-ok', + ); + const hostRemoteBundledMetaKind = await page.$eval( + '.host-remote-bundled-meta-kind', + el => el.textContent?.trim(), + ); + expect(hostRemoteBundledMetaKind).toBe('remote-meta-default'); + const hostRemoteBundledMetaLabel = await page.$eval( + '.host-remote-bundled-meta-label', + el => el.textContent?.trim(), + ); + expect(hostRemoteBundledMetaLabel).toBe('rsc|mf|actions'); const hostRemoteAsyncServerInfo = await page.$eval( '.remote-async-server-info', el => el.textContent?.trim(), From e5c6575037138fc11fbb2e91553f5fc7ad838bff Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Feb 2026 23:16:33 +0000 Subject: [PATCH 074/324] test(rsc-mf): map bundled action re-export ids to host proxies --- .../host/src/server-component-root/App.tsx | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx index 47f1a2864099..3e7a7ad98b0d 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx @@ -2,6 +2,12 @@ import 'server-only'; import { Suspense } from 'react'; import { AsyncRemoteServerInfo } from 'rscRemote/AsyncRemoteServerInfo'; import RemoteServerDefault from 'rscRemote/RemoteServerDefault'; +import { + bundledDefaultRemoteAction, + bundledIncrementRemoteCount, + bundledNestedRemoteAction, + bundledRemoteActionEcho, +} from 'rscRemote/actionBundle'; import { incrementRemoteCount, remoteActionEcho } from 'rscRemote/actions'; import { defaultRemoteAction } from 'rscRemote/defaultAction'; import { @@ -48,6 +54,22 @@ const App = () => { getServerActionId(defaultRemoteAction), getServerActionId(proxyDefaultRemoteAction), ], + [ + getServerActionId(bundledIncrementRemoteCount), + getServerActionId(proxyIncrementRemoteCount), + ], + [ + getServerActionId(bundledRemoteActionEcho), + getServerActionId(proxyRemoteActionEcho), + ], + [ + getServerActionId(bundledNestedRemoteAction), + getServerActionId(proxyNestedRemoteAction), + ], + [ + getServerActionId(bundledDefaultRemoteAction), + getServerActionId(proxyDefaultRemoteAction), + ], ].filter((pair): pair is [string, string] => Boolean(pair[0] && pair[1])), ); From d680e23b4fd3ad84fa72c26d2a48f88f7240d34f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Feb 2026 23:19:54 +0000 Subject: [PATCH 075/324] refactor(rsc-mf): remove remote chunk text patching middleware --- .../rsc-mf/host/server/modern.server.ts | 38 ------------------- 1 file changed, 38 deletions(-) diff --git a/tests/integration/rsc-mf/host/server/modern.server.ts b/tests/integration/rsc-mf/host/server/modern.server.ts index 56827ed52ce1..205dc82a6c87 100644 --- a/tests/integration/rsc-mf/host/server/modern.server.ts +++ b/tests/integration/rsc-mf/host/server/modern.server.ts @@ -19,18 +19,6 @@ const shouldProxyRemoteAsset = (pathname: string) => { return false; }; -const REMOTE_COUNTER_ALIAS_MODULES = [ - 'remote-module:rscRemote:./src/components/RemoteClientCounter.tsx', - 'remote-module:rscRemote:./RemoteClientCounter', - 'remote-module:rscRemote:./RemoteClientCounter.tsx', -]; -const REMOTE_COUNTER_SOURCE_MODULE = './src/components/RemoteClientCounter.tsx'; -const createRemoteNestedMixedAliasChunk = () => - `\n;(globalThis["chunk_rscHost"] = globalThis["chunk_rscHost"] || []).push([["__federation_expose_RemoteNestedMixed_alias"],{${REMOTE_COUNTER_ALIAS_MODULES.map( - aliasModule => - `"${aliasModule}":function(module,__unused,__webpack_require__){module.exports=__webpack_require__("${REMOTE_COUNTER_SOURCE_MODULE}");}`, - ).join(',')}}]);`; - const proxyRemoteFederationAsset: MiddlewareHandler = async (c, next) => { const reqUrl = new URL(c.req.url); const pathname = reqUrl.pathname; @@ -54,32 +42,6 @@ const proxyRemoteFederationAsset: MiddlewareHandler = async (c, next) => { return; } - const shouldPatchCounterAlias = - pathname.startsWith('/static/js/async/__federation_expose_') && - pathname.endsWith('.js'); - if (shouldPatchCounterAlias) { - let chunkText = await upstream.text(); - if (!chunkText.includes('remote-client-server-count')) { - c.res = new Response(chunkText, { - status: upstream.status, - headers: { - 'content-type': 'application/javascript; charset=utf-8', - }, - }); - return; - } - - chunkText = `${chunkText}${createRemoteNestedMixedAliasChunk()}`; - - c.res = new Response(chunkText, { - status: upstream.status, - headers: { - 'content-type': 'application/javascript; charset=utf-8', - }, - }); - return; - } - c.res = new Response(await upstream.arrayBuffer(), { status: upstream.status, headers: upstream.headers, From 92f8e4e5f9ae791a0c4bfd01803018fad05bcb3d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Feb 2026 23:27:20 +0000 Subject: [PATCH 076/324] test(rsc-mf): assert no browser runtime errors in dev mode --- tests/integration/rsc-mf/tests/index.test.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 19c035ff7962..d8761051c835 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -293,11 +293,9 @@ function runTests({ mode }: TestConfig) { it('should support remote use client and server actions', () => supportRemoteClientAndServerActions({ hostPort, page })); - if (mode === 'build') { - it('should have no browser runtime errors', () => { - expect(runtimeErrors).toEqual([]); - }); - } + it('should have no browser runtime errors', () => { + expect(runtimeErrors).toEqual([]); + }); }); } From a1d8490397fc8ae8983b729b24871fb9de972176 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Feb 2026 23:29:43 +0000 Subject: [PATCH 077/324] test(rsc-mf): assert action posts stay on host endpoint --- tests/integration/rsc-mf/tests/index.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index d8761051c835..87109825ff0a 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -223,6 +223,7 @@ function runTests({ mode }: TestConfig) { let page: Page; let browser: Browser; const runtimeErrors: string[] = []; + const actionRequestUrls: string[] = []; if (skipForLowerNodeVersion()) { return; @@ -273,6 +274,14 @@ function runTests({ mode }: TestConfig) { const message = err.message; runtimeErrors.push(message); }); + + page.on('request', request => { + const headers = request.headers(); + if (request.method() !== 'POST' || !headers['x-rsc-action']) { + return; + } + actionRequestUrls.push(request.url()); + }); }); afterAll(async () => { @@ -293,6 +302,15 @@ function runTests({ mode }: TestConfig) { it('should support remote use client and server actions', () => supportRemoteClientAndServerActions({ hostPort, page })); + it('should route remote actions through host endpoint', () => { + expect(actionRequestUrls.length).toBeGreaterThan(0); + expect( + actionRequestUrls.every(url => + url.startsWith(`http://127.0.0.1:${hostPort}${HOST_RSC_URL}`), + ), + ).toBe(true); + }); + it('should have no browser runtime errors', () => { expect(runtimeErrors).toEqual([]); }); From 0f0ca8ee868fe4671b53a13ac2aac5b4e26feeb5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Feb 2026 23:30:45 +0000 Subject: [PATCH 078/324] refactor(rsc-mf): drop remote client chunkLoadingGlobal override --- tests/integration/rsc-mf/remote/modern.config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/rsc-mf/remote/modern.config.ts b/tests/integration/rsc-mf/remote/modern.config.ts index 8d61903ce6b5..a0fd50dd1597 100644 --- a/tests/integration/rsc-mf/remote/modern.config.ts +++ b/tests/integration/rsc-mf/remote/modern.config.ts @@ -65,7 +65,6 @@ export default defineConfig({ .layer('react-server-components'); } else { chain.optimization.splitChunks(false); - chain.output.chunkLoadingGlobal('chunk_rscHost'); chain.output.publicPath(`http://127.0.0.1:${remotePort}/`); } From e75f56ec7d4f88847e2df59b5777d3f32d4fd5c8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Feb 2026 23:32:18 +0000 Subject: [PATCH 079/324] refactor(rsc-mf): consume bundled action expose via namespace import --- .../HostRemoteActionRunner.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx b/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx index 38ed89e54fad..fbec4c680e72 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx @@ -3,10 +3,7 @@ import { useEffect, useState } from 'react'; import RemoteClientBadge from 'rscRemote/RemoteClientBadge'; import { RemoteClientCounter as RemoteClientCounterBridge } from 'rscRemote/RemoteClientCounter'; -import { - bundledDefaultRemoteAction, - bundledRemoteActionEcho, -} from 'rscRemote/actionBundle'; +import * as remoteActionBundle from 'rscRemote/actionBundle'; import { remoteActionEcho } from 'rscRemote/actions'; import { defaultRemoteAction } from 'rscRemote/defaultAction'; import { registerRemoteServerCallback } from 'rscRemote/registerServerCallback'; @@ -42,8 +39,12 @@ export default function HostRemoteActionRunner({ await Promise.all([ defaultRemoteAction('from-host-client'), remoteActionEcho('from-host-client'), - bundledDefaultRemoteAction('from-host-client-bundled'), - bundledRemoteActionEcho('from-host-client-bundled'), + remoteActionBundle.bundledDefaultRemoteAction( + 'from-host-client-bundled', + ), + remoteActionBundle.bundledRemoteActionEcho( + 'from-host-client-bundled', + ), ]); setDefaultResult(defaultValue); setEchoResult(echoValue); From 336d821cf4137d62af056bd333b34f6c53c1d3eb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Feb 2026 23:34:09 +0000 Subject: [PATCH 080/324] test(rsc-mf): assert action posts avoid remote origin --- tests/integration/rsc-mf/tests/index.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 87109825ff0a..b0d51e9a915c 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -309,6 +309,11 @@ function runTests({ mode }: TestConfig) { url.startsWith(`http://127.0.0.1:${hostPort}${HOST_RSC_URL}`), ), ).toBe(true); + expect( + actionRequestUrls.every( + url => !url.startsWith(`http://127.0.0.1:${remotePort}`), + ), + ).toBe(true); }); it('should have no browser runtime errors', () => { From 6c194520ab32e408c63a759a9dc3f08c14d364c5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Feb 2026 23:38:32 +0000 Subject: [PATCH 081/324] test(rsc-mf): assert host-resolvable x-rsc-action ids --- tests/integration/rsc-mf/tests/index.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index b0d51e9a915c..c8db9ce34826 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -224,6 +224,7 @@ function runTests({ mode }: TestConfig) { let browser: Browser; const runtimeErrors: string[] = []; const actionRequestUrls: string[] = []; + const actionRequestIds: string[] = []; if (skipForLowerNodeVersion()) { return; @@ -281,6 +282,7 @@ function runTests({ mode }: TestConfig) { return; } actionRequestUrls.push(request.url()); + actionRequestIds.push(headers['x-rsc-action']); }); }); @@ -316,6 +318,17 @@ function runTests({ mode }: TestConfig) { ).toBe(true); }); + it('should post host-resolvable action ids for remote actions', () => { + expect(actionRequestIds.length).toBeGreaterThan(0); + expect(actionRequestIds.every(id => !id.startsWith('remote:'))).toBe( + true, + ); + expect(actionRequestIds.every(id => /^[a-f0-9]{64,}$/i.test(id))).toBe( + true, + ); + expect(new Set(actionRequestIds).size).toBeGreaterThanOrEqual(4); + }); + it('should have no browser runtime errors', () => { expect(runtimeErrors).toEqual([]); }); From ed0f9f5c1552ab37dbc5c8366b516a94775ab3d6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Feb 2026 23:41:04 +0000 Subject: [PATCH 082/324] refactor(rsc-mf): use namespace imports in host server bundle coverage --- .../host/src/server-component-root/App.tsx | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx index 3e7a7ad98b0d..f68642ded67a 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx @@ -2,20 +2,10 @@ import 'server-only'; import { Suspense } from 'react'; import { AsyncRemoteServerInfo } from 'rscRemote/AsyncRemoteServerInfo'; import RemoteServerDefault from 'rscRemote/RemoteServerDefault'; -import { - bundledDefaultRemoteAction, - bundledIncrementRemoteCount, - bundledNestedRemoteAction, - bundledRemoteActionEcho, -} from 'rscRemote/actionBundle'; +import * as remoteActionBundle from 'rscRemote/actionBundle'; import { incrementRemoteCount, remoteActionEcho } from 'rscRemote/actions'; import { defaultRemoteAction } from 'rscRemote/defaultAction'; -import { - bundledRemoteMeta, - getBundledRemoteMetaLabel, - getBundledServerOnlyDefaultInfo, - getBundledServerOnlyInfo, -} from 'rscRemote/infoBundle'; +import * as remoteInfoBundle from 'rscRemote/infoBundle'; import { nestedRemoteAction } from 'rscRemote/nestedActions'; import remoteMeta, { getRemoteMetaLabel } from 'rscRemote/remoteMeta'; import { getServerOnlyInfo } from 'rscRemote/remoteServerOnly'; @@ -55,19 +45,19 @@ const App = () => { getServerActionId(proxyDefaultRemoteAction), ], [ - getServerActionId(bundledIncrementRemoteCount), + getServerActionId(remoteActionBundle.bundledIncrementRemoteCount), getServerActionId(proxyIncrementRemoteCount), ], [ - getServerActionId(bundledRemoteActionEcho), + getServerActionId(remoteActionBundle.bundledRemoteActionEcho), getServerActionId(proxyRemoteActionEcho), ], [ - getServerActionId(bundledNestedRemoteAction), + getServerActionId(remoteActionBundle.bundledNestedRemoteAction), getServerActionId(proxyNestedRemoteAction), ], [ - getServerActionId(bundledDefaultRemoteAction), + getServerActionId(remoteActionBundle.bundledDefaultRemoteAction), getServerActionId(proxyDefaultRemoteAction), ], ].filter((pair): pair is [string, string] => Boolean(pair[0] && pair[1])), @@ -76,9 +66,10 @@ const App = () => { const remoteServerOnlyInfo = getServerOnlyInfo(); const remoteServerOnlyDefaultInfo = getServerOnlyDefaultInfo(); const remoteMetaLabel = getRemoteMetaLabel(); - const bundledServerOnlyInfo = getBundledServerOnlyInfo(); - const bundledServerOnlyDefaultInfo = getBundledServerOnlyDefaultInfo(); - const bundledRemoteMetaLabel = getBundledRemoteMetaLabel(); + const bundledServerOnlyInfo = remoteInfoBundle.getBundledServerOnlyInfo(); + const bundledServerOnlyDefaultInfo = + remoteInfoBundle.getBundledServerOnlyDefaultInfo(); + const bundledRemoteMetaLabel = remoteInfoBundle.getBundledRemoteMetaLabel(); return (
@@ -93,7 +84,9 @@ const App = () => {

{bundledServerOnlyDefaultInfo}

-

{bundledRemoteMeta.kind}

+

+ {remoteInfoBundle.bundledRemoteMeta.kind} +

{bundledRemoteMetaLabel}

Loading Remote Async Server Info...
}> From a40bae2dfc6f999cd8655ddf232f0590e4a0d3c7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Feb 2026 23:44:20 +0000 Subject: [PATCH 083/324] refactor(rsc-mf): route host proxy actions through bundled expose imports --- .../host/src/server-component-root/App.tsx | 16 +++++++++---- .../remoteActionProxy.ts | 23 +++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx index f68642ded67a..cf059a22cfc8 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx @@ -13,6 +13,10 @@ import getServerOnlyDefaultInfo from 'rscRemote/remoteServerOnlyDefault'; import styles from './App.module.less'; import HostRemoteActionRunner from './HostRemoteActionRunner'; import { + proxyBundledDefaultRemoteAction, + proxyBundledIncrementRemoteCount, + proxyBundledNestedRemoteAction, + proxyBundledRemoteActionEcho, proxyDefaultRemoteAction, proxyIncrementRemoteCount, proxyNestedRemoteAction, @@ -46,19 +50,19 @@ const App = () => { ], [ getServerActionId(remoteActionBundle.bundledIncrementRemoteCount), - getServerActionId(proxyIncrementRemoteCount), + getServerActionId(proxyBundledIncrementRemoteCount), ], [ getServerActionId(remoteActionBundle.bundledRemoteActionEcho), - getServerActionId(proxyRemoteActionEcho), + getServerActionId(proxyBundledRemoteActionEcho), ], [ getServerActionId(remoteActionBundle.bundledNestedRemoteAction), - getServerActionId(proxyNestedRemoteAction), + getServerActionId(proxyBundledNestedRemoteAction), ], [ getServerActionId(remoteActionBundle.bundledDefaultRemoteAction), - getServerActionId(proxyDefaultRemoteAction), + getServerActionId(proxyBundledDefaultRemoteAction), ], ].filter((pair): pair is [string, string] => Boolean(pair[0] && pair[1])), ); @@ -101,6 +105,10 @@ const App = () => { + + + +
Date: Fri, 13 Feb 2026 23:47:24 +0000 Subject: [PATCH 084/324] test(rsc-mf): assert host proxy action manifest coverage --- .../rsc-mf/host/src/server-component-root/App.tsx | 15 +++++++++++++++ tests/integration/rsc-mf/tests/index.test.ts | 6 ++++++ 2 files changed, 21 insertions(+) diff --git a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx index cf059a22cfc8..c4740fbae801 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx @@ -27,6 +27,18 @@ const getServerActionId = (action: unknown) => (action as { $$id?: string } | undefined)?.$$id; const App = () => { + const hostProxyActionIds = [ + getServerActionId(proxyIncrementRemoteCount), + getServerActionId(proxyRemoteActionEcho), + getServerActionId(proxyNestedRemoteAction), + getServerActionId(proxyDefaultRemoteAction), + getServerActionId(proxyBundledIncrementRemoteCount), + getServerActionId(proxyBundledRemoteActionEcho), + getServerActionId(proxyBundledNestedRemoteAction), + getServerActionId(proxyBundledDefaultRemoteAction), + ].filter((actionId): actionId is string => Boolean(actionId)); + const uniqueHostProxyActionIdsCount = new Set(hostProxyActionIds).size; + // Map remote action IDs to host-local proxy action IDs so client-side // callbacks can always post a host-resolvable action id. This keeps // remote action execution in-process on the host via proxy imports. @@ -92,6 +104,9 @@ const App = () => { {remoteInfoBundle.bundledRemoteMeta.kind}

{bundledRemoteMetaLabel}

+

+ {uniqueHostProxyActionIdsCount} +

Loading Remote Async Server Info...}> diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index c8db9ce34826..9137ee8c55a9 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -64,6 +64,7 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { expect(html).toContain('Remote Default Server Card'); expect(html).toContain('host-remote-bundled-server-only'); expect(html).toContain('host-remote-bundled-meta-kind'); + expect(html).toContain('host-proxy-action-id-count'); await page.goto(`http://127.0.0.1:${hostPort}${HOST_RSC_URL}`, { waitUntil: ['networkidle0', 'domcontentloaded'], @@ -108,6 +109,11 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { el => el.textContent?.trim(), ); expect(hostRemoteBundledMetaLabel).toBe('rsc|mf|actions'); + const hostProxyActionIdCount = await page.$eval( + '.host-proxy-action-id-count', + el => el.textContent?.trim(), + ); + expect(hostProxyActionIdCount).toBe('8'); const hostRemoteAsyncServerInfo = await page.$eval( '.remote-async-server-info', el => el.textContent?.trim(), From cf1e7fb003136269b0e028abc9a22b68d28deab5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Feb 2026 23:51:04 +0000 Subject: [PATCH 085/324] test(rsc-mf): cover bundled nested and increment host actions --- .../HostRemoteActionRunner.tsx | 45 ++++++++++++++----- tests/integration/rsc-mf/tests/index.test.ts | 11 ++++- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx b/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx index fbec4c680e72..437c4892f005 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx @@ -20,6 +20,8 @@ export default function HostRemoteActionRunner({ const [echoResult, setEchoResult] = useState(''); const [bundledDefaultResult, setBundledDefaultResult] = useState(''); const [bundledEchoResult, setBundledEchoResult] = useState(''); + const [bundledNestedResult, setBundledNestedResult] = useState(''); + const [bundledIncrementResult, setBundledIncrementResult] = useState(''); const [isPending, setIsPending] = useState(false); useEffect(() => { @@ -35,21 +37,36 @@ export default function HostRemoteActionRunner({ const runActions = async () => { setIsPending(true); try { - const [defaultValue, echoValue, bundledDefaultValue, bundledEchoValue] = - await Promise.all([ - defaultRemoteAction('from-host-client'), - remoteActionEcho('from-host-client'), - remoteActionBundle.bundledDefaultRemoteAction( - 'from-host-client-bundled', - ), - remoteActionBundle.bundledRemoteActionEcho( - 'from-host-client-bundled', - ), - ]); + const bundledIncrementFormData = new FormData(); + bundledIncrementFormData.set('count', '1'); + const [ + defaultValue, + echoValue, + bundledDefaultValue, + bundledEchoValue, + bundledNestedValue, + bundledIncrementValue, + ] = await Promise.all([ + defaultRemoteAction('from-host-client'), + remoteActionEcho('from-host-client'), + remoteActionBundle.bundledDefaultRemoteAction( + 'from-host-client-bundled', + ), + remoteActionBundle.bundledRemoteActionEcho('from-host-client-bundled'), + remoteActionBundle.bundledNestedRemoteAction( + 'from-host-client-bundled', + ), + remoteActionBundle.bundledIncrementRemoteCount( + 0, + bundledIncrementFormData, + ), + ]); setDefaultResult(defaultValue); setEchoResult(echoValue); setBundledDefaultResult(bundledDefaultValue); setBundledEchoResult(bundledEchoValue); + setBundledNestedResult(bundledNestedValue); + setBundledIncrementResult(String(bundledIncrementValue)); } finally { setIsPending(false); } @@ -74,6 +91,12 @@ export default function HostRemoteActionRunner({

{bundledEchoResult}

+

+ {bundledNestedResult} +

+

+ {bundledIncrementResult} +

); } diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 9137ee8c55a9..e8c9581d89e1 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -207,6 +207,12 @@ async function supportRemoteClientAndServerActions({ const bundledEchoActionResult = document.querySelector( '.host-remote-bundled-echo-action-result', ); + const bundledNestedActionResult = document.querySelector( + '.host-remote-bundled-nested-action-result', + ); + const bundledIncrementActionResult = document.querySelector( + '.host-remote-bundled-increment-action-result', + ); return ( defaultActionResult?.textContent?.trim() === 'default-action:from-host-client' && @@ -215,7 +221,10 @@ async function supportRemoteClientAndServerActions({ bundledDefaultActionResult?.textContent?.trim() === 'default-action:from-host-client-bundled' && bundledEchoActionResult?.textContent?.trim() === - 'remote-action:from-host-client-bundled' + 'remote-action:from-host-client-bundled' && + bundledNestedActionResult?.textContent?.trim() === + 'nested-action:from-host-client-bundled' && + bundledIncrementActionResult?.textContent?.trim() === '2' ); }); } From 6871db71414bb38e8195141c7cf767b3e132e21d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 00:10:02 +0000 Subject: [PATCH 086/324] refactor(rsc-mf): stabilize remote callback registration keying --- .../remote/src/components/registerServerCallback.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/integration/rsc-mf/remote/src/components/registerServerCallback.ts b/tests/integration/rsc-mf/remote/src/components/registerServerCallback.ts index 720891760029..1ca1b6e65cb1 100644 --- a/tests/integration/rsc-mf/remote/src/components/registerServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/components/registerServerCallback.ts @@ -6,6 +6,13 @@ import { } from 'rsc-mf-react-server-dom-client-browser'; let registeredCallbackKey = ''; +const getStableProxyActionIdEntries = ( + remoteActionIdToHostProxyActionId?: Record, +) => + Object.entries(remoteActionIdToHostProxyActionId ?? {}).sort( + ([left], [right]) => left.localeCompare(right), + ); + const getHostActionId = (rawActionId: string, remoteAlias: string) => { if (rawActionId.startsWith('remote:')) { return rawActionId; @@ -25,7 +32,9 @@ export function registerRemoteServerCallback( const callbackKey = JSON.stringify({ remoteAlias, remoteOrigin, - remoteActionIdToHostProxyActionId, + remoteActionIdToHostProxyActionId: getStableProxyActionIdEntries( + remoteActionIdToHostProxyActionId, + ), }); if (registeredCallbackKey === callbackKey) { return; From 051929d0605c3baf77eea728462d7cc2e240de7c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 00:12:48 +0000 Subject: [PATCH 087/324] refactor(rsc-mf): avoid client stringify in callback dependency --- .../host/src/server-component-root/App.tsx | 73 ++++++++++--------- .../HostRemoteActionRunner.tsx | 4 +- 2 files changed, 42 insertions(+), 35 deletions(-) diff --git a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx index c4740fbae801..c443f8b47823 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx @@ -42,41 +42,45 @@ const App = () => { // Map remote action IDs to host-local proxy action IDs so client-side // callbacks can always post a host-resolvable action id. This keeps // remote action execution in-process on the host via proxy imports. - const remoteActionIdToHostProxyActionId = Object.fromEntries( + const remoteActionIdToHostProxyActionEntries = [ + [ + getServerActionId(incrementRemoteCount), + getServerActionId(proxyIncrementRemoteCount), + ], + [ + getServerActionId(remoteActionEcho), + getServerActionId(proxyRemoteActionEcho), + ], + [ + getServerActionId(nestedRemoteAction), + getServerActionId(proxyNestedRemoteAction), + ], + [ + getServerActionId(defaultRemoteAction), + getServerActionId(proxyDefaultRemoteAction), + ], [ - [ - getServerActionId(incrementRemoteCount), - getServerActionId(proxyIncrementRemoteCount), - ], - [ - getServerActionId(remoteActionEcho), - getServerActionId(proxyRemoteActionEcho), - ], - [ - getServerActionId(nestedRemoteAction), - getServerActionId(proxyNestedRemoteAction), - ], - [ - getServerActionId(defaultRemoteAction), - getServerActionId(proxyDefaultRemoteAction), - ], - [ - getServerActionId(remoteActionBundle.bundledIncrementRemoteCount), - getServerActionId(proxyBundledIncrementRemoteCount), - ], - [ - getServerActionId(remoteActionBundle.bundledRemoteActionEcho), - getServerActionId(proxyBundledRemoteActionEcho), - ], - [ - getServerActionId(remoteActionBundle.bundledNestedRemoteAction), - getServerActionId(proxyBundledNestedRemoteAction), - ], - [ - getServerActionId(remoteActionBundle.bundledDefaultRemoteAction), - getServerActionId(proxyBundledDefaultRemoteAction), - ], - ].filter((pair): pair is [string, string] => Boolean(pair[0] && pair[1])), + getServerActionId(remoteActionBundle.bundledIncrementRemoteCount), + getServerActionId(proxyBundledIncrementRemoteCount), + ], + [ + getServerActionId(remoteActionBundle.bundledRemoteActionEcho), + getServerActionId(proxyBundledRemoteActionEcho), + ], + [ + getServerActionId(remoteActionBundle.bundledNestedRemoteAction), + getServerActionId(proxyBundledNestedRemoteAction), + ], + [ + getServerActionId(remoteActionBundle.bundledDefaultRemoteAction), + getServerActionId(proxyBundledDefaultRemoteAction), + ], + ].filter((pair): pair is [string, string] => Boolean(pair[0] && pair[1])); + const remoteActionIdToHostProxyActionId = Object.fromEntries( + remoteActionIdToHostProxyActionEntries, + ); + const remoteActionIdMapKey = JSON.stringify( + remoteActionIdToHostProxyActionEntries, ); const remoteServerOnlyInfo = getServerOnlyInfo(); @@ -126,6 +130,7 @@ const App = () => { diff --git a/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx b/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx index 437c4892f005..5b97e718fe58 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx @@ -9,8 +9,10 @@ import { defaultRemoteAction } from 'rscRemote/defaultAction'; import { registerRemoteServerCallback } from 'rscRemote/registerServerCallback'; export default function HostRemoteActionRunner({ + remoteActionIdMapKey, remoteActionIdToHostProxyActionId, }: { + remoteActionIdMapKey: string; remoteActionIdToHostProxyActionId: Record; }) { // Keep this import in the client graph so federated RSC bridge IDs @@ -32,7 +34,7 @@ export default function HostRemoteActionRunner({ 'rscRemote', remoteActionIdToHostProxyActionId, ); - }, [JSON.stringify(remoteActionIdToHostProxyActionId)]); + }, [remoteActionIdMapKey]); const runActions = async () => { setIsPending(true); From 6cb557435bd74f7715759918c4be55e7570e49f4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 00:16:51 +0000 Subject: [PATCH 088/324] test(rsc-mf): assert posted action ids match host proxy set --- .../rsc-mf/host/src/server-component-root/App.tsx | 8 +++++++- tests/integration/rsc-mf/tests/index.test.ts | 15 ++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx index c443f8b47823..8e227e7cb085 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx @@ -37,7 +37,10 @@ const App = () => { getServerActionId(proxyBundledNestedRemoteAction), getServerActionId(proxyBundledDefaultRemoteAction), ].filter((actionId): actionId is string => Boolean(actionId)); - const uniqueHostProxyActionIdsCount = new Set(hostProxyActionIds).size; + const uniqueHostProxyActionIds = Array.from( + new Set(hostProxyActionIds), + ).sort(); + const uniqueHostProxyActionIdsCount = uniqueHostProxyActionIds.length; // Map remote action IDs to host-local proxy action IDs so client-side // callbacks can always post a host-resolvable action id. This keeps @@ -111,6 +114,9 @@ const App = () => {

{uniqueHostProxyActionIdsCount}

+

+ {uniqueHostProxyActionIds.join(',')} +

Loading Remote Async Server Info...}> diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index e8c9581d89e1..2d2b1f999e85 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -65,6 +65,7 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { expect(html).toContain('host-remote-bundled-server-only'); expect(html).toContain('host-remote-bundled-meta-kind'); expect(html).toContain('host-proxy-action-id-count'); + expect(html).toContain('host-proxy-action-ids'); await page.goto(`http://127.0.0.1:${hostPort}${HOST_RSC_URL}`, { waitUntil: ['networkidle0', 'domcontentloaded'], @@ -114,6 +115,10 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { el => el.textContent?.trim(), ); expect(hostProxyActionIdCount).toBe('8'); + const hostProxyActionIds = await page.$eval('.host-proxy-action-ids', el => + el.textContent?.trim(), + ); + expect(hostProxyActionIds?.split(',').filter(Boolean).length).toBe(8); const hostRemoteAsyncServerInfo = await page.$eval( '.remote-async-server-info', el => el.textContent?.trim(), @@ -333,7 +338,7 @@ function runTests({ mode }: TestConfig) { ).toBe(true); }); - it('should post host-resolvable action ids for remote actions', () => { + it('should post host-resolvable action ids for remote actions', async () => { expect(actionRequestIds.length).toBeGreaterThan(0); expect(actionRequestIds.every(id => !id.startsWith('remote:'))).toBe( true, @@ -342,6 +347,14 @@ function runTests({ mode }: TestConfig) { true, ); expect(new Set(actionRequestIds).size).toBeGreaterThanOrEqual(4); + const hostProxyActionIdSet = new Set( + (await page.$eval('.host-proxy-action-ids', el => el.textContent || '')) + .split(',') + .filter(Boolean), + ); + expect(actionRequestIds.every(id => hostProxyActionIdSet.has(id))).toBe( + true, + ); }); it('should have no browser runtime errors', () => { From 76ec2292496b47130569be204185d8f6f3a8e80b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 00:20:15 +0000 Subject: [PATCH 089/324] refactor(rsc-mf): centralize host proxy mapping and manifest forms --- .../host/src/server-component-root/App.tsx | 89 +++++++++---------- 1 file changed, 41 insertions(+), 48 deletions(-) diff --git a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx index 8e227e7cb085..480ef480471c 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx @@ -27,16 +27,19 @@ const getServerActionId = (action: unknown) => (action as { $$id?: string } | undefined)?.$$id; const App = () => { - const hostProxyActionIds = [ - getServerActionId(proxyIncrementRemoteCount), - getServerActionId(proxyRemoteActionEcho), - getServerActionId(proxyNestedRemoteAction), - getServerActionId(proxyDefaultRemoteAction), - getServerActionId(proxyBundledIncrementRemoteCount), - getServerActionId(proxyBundledRemoteActionEcho), - getServerActionId(proxyBundledNestedRemoteAction), - getServerActionId(proxyBundledDefaultRemoteAction), - ].filter((actionId): actionId is string => Boolean(actionId)); + const hostProxyActions = [ + proxyIncrementRemoteCount, + proxyRemoteActionEcho, + proxyNestedRemoteAction, + proxyDefaultRemoteAction, + proxyBundledIncrementRemoteCount, + proxyBundledRemoteActionEcho, + proxyBundledNestedRemoteAction, + proxyBundledDefaultRemoteAction, + ] as const; + const hostProxyActionIds = hostProxyActions + .map(action => getServerActionId(action)) + .filter((actionId): actionId is string => Boolean(actionId)); const uniqueHostProxyActionIds = Array.from( new Set(hostProxyActionIds), ).sort(); @@ -45,40 +48,39 @@ const App = () => { // Map remote action IDs to host-local proxy action IDs so client-side // callbacks can always post a host-resolvable action id. This keeps // remote action execution in-process on the host via proxy imports. - const remoteActionIdToHostProxyActionEntries = [ + const remoteActionToHostProxyActionPairs = [ + [incrementRemoteCount, proxyIncrementRemoteCount], + [remoteActionEcho, proxyRemoteActionEcho], + [nestedRemoteAction, proxyNestedRemoteAction], + [defaultRemoteAction, proxyDefaultRemoteAction], [ - getServerActionId(incrementRemoteCount), - getServerActionId(proxyIncrementRemoteCount), + remoteActionBundle.bundledIncrementRemoteCount, + proxyBundledIncrementRemoteCount, ], + [remoteActionBundle.bundledRemoteActionEcho, proxyBundledRemoteActionEcho], [ - getServerActionId(remoteActionEcho), - getServerActionId(proxyRemoteActionEcho), + remoteActionBundle.bundledNestedRemoteAction, + proxyBundledNestedRemoteAction, ], [ - getServerActionId(nestedRemoteAction), - getServerActionId(proxyNestedRemoteAction), + remoteActionBundle.bundledDefaultRemoteAction, + proxyBundledDefaultRemoteAction, ], - [ - getServerActionId(defaultRemoteAction), - getServerActionId(proxyDefaultRemoteAction), - ], - [ - getServerActionId(remoteActionBundle.bundledIncrementRemoteCount), - getServerActionId(proxyBundledIncrementRemoteCount), - ], - [ - getServerActionId(remoteActionBundle.bundledRemoteActionEcho), - getServerActionId(proxyBundledRemoteActionEcho), - ], - [ - getServerActionId(remoteActionBundle.bundledNestedRemoteAction), - getServerActionId(proxyBundledNestedRemoteAction), - ], - [ - getServerActionId(remoteActionBundle.bundledDefaultRemoteAction), - getServerActionId(proxyBundledDefaultRemoteAction), - ], - ].filter((pair): pair is [string, string] => Boolean(pair[0] && pair[1])); + ] as const; + const remoteActionIdToHostProxyActionEntries = + remoteActionToHostProxyActionPairs + .map(([remoteAction, hostProxyAction]) => [ + getServerActionId(remoteAction), + getServerActionId(hostProxyAction), + ]) + .filter((pair): pair is [string, string] => Boolean(pair[0] && pair[1])); + const hostProxyActionDebugKeys = hostProxyActions.map( + (action, index) => + [action, getServerActionId(action) ?? `proxy-action-${index}`] as const, + ); + const hostProxyManifestForms = hostProxyActionDebugKeys.map( + ([action, actionId]) => , + ); const remoteActionIdToHostProxyActionId = Object.fromEntries( remoteActionIdToHostProxyActionEntries, ); @@ -125,16 +127,7 @@ const App = () => { {/* Anchor host proxy actions in the server action manifest. */} - + Date: Sat, 14 Feb 2026 00:22:53 +0000 Subject: [PATCH 090/324] test(rsc-mf): assert host proxy id list uniqueness and format --- tests/integration/rsc-mf/tests/index.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 2d2b1f999e85..4d11e22ec0c5 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -118,7 +118,14 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { const hostProxyActionIds = await page.$eval('.host-proxy-action-ids', el => el.textContent?.trim(), ); - expect(hostProxyActionIds?.split(',').filter(Boolean).length).toBe(8); + const hostProxyActionIdList = hostProxyActionIds + ?.split(',') + .filter(Boolean) as string[]; + expect(hostProxyActionIdList.length).toBe(8); + expect(new Set(hostProxyActionIdList).size).toBe(8); + expect(hostProxyActionIdList.every(id => /^[a-f0-9]{64,}$/i.test(id))).toBe( + true, + ); const hostRemoteAsyncServerInfo = await page.$eval( '.remote-async-server-info', el => el.textContent?.trim(), From 25af76821891e79ab55f4dbe82180b3aca7d095b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 00:26:31 +0000 Subject: [PATCH 091/324] test(rsc-mf): surface direct and bundled proxy id groups --- .../host/src/server-component-root/App.tsx | 20 ++++++++- tests/integration/rsc-mf/tests/index.test.ts | 45 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx index 480ef480471c..8b96272b0ff1 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx @@ -27,16 +27,28 @@ const getServerActionId = (action: unknown) => (action as { $$id?: string } | undefined)?.$$id; const App = () => { - const hostProxyActions = [ + const directHostProxyActions = [ proxyIncrementRemoteCount, proxyRemoteActionEcho, proxyNestedRemoteAction, proxyDefaultRemoteAction, + ] as const; + const bundledHostProxyActions = [ proxyBundledIncrementRemoteCount, proxyBundledRemoteActionEcho, proxyBundledNestedRemoteAction, proxyBundledDefaultRemoteAction, ] as const; + const hostProxyActions = [ + ...directHostProxyActions, + ...bundledHostProxyActions, + ] as const; + const directHostProxyActionIds = directHostProxyActions + .map(action => getServerActionId(action)) + .filter((actionId): actionId is string => Boolean(actionId)); + const bundledHostProxyActionIds = bundledHostProxyActions + .map(action => getServerActionId(action)) + .filter((actionId): actionId is string => Boolean(actionId)); const hostProxyActionIds = hostProxyActions .map(action => getServerActionId(action)) .filter((actionId): actionId is string => Boolean(actionId)); @@ -119,6 +131,12 @@ const App = () => {

{uniqueHostProxyActionIds.join(',')}

+

+ {directHostProxyActionIds.join(',')} +

+

+ {bundledHostProxyActionIds.join(',')} +

Loading Remote Async Server Info...}> diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 4d11e22ec0c5..bb715f7146c8 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -66,6 +66,8 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { expect(html).toContain('host-remote-bundled-meta-kind'); expect(html).toContain('host-proxy-action-id-count'); expect(html).toContain('host-proxy-action-ids'); + expect(html).toContain('host-direct-proxy-action-ids'); + expect(html).toContain('host-bundled-proxy-action-ids'); await page.goto(`http://127.0.0.1:${hostPort}${HOST_RSC_URL}`, { waitUntil: ['networkidle0', 'domcontentloaded'], @@ -126,6 +128,22 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { expect(hostProxyActionIdList.every(id => /^[a-f0-9]{64,}$/i.test(id))).toBe( true, ); + const hostDirectProxyActionIds = await page.$eval( + '.host-direct-proxy-action-ids', + el => el.textContent?.trim(), + ); + const hostDirectProxyActionIdList = hostDirectProxyActionIds + ?.split(',') + .filter(Boolean) as string[]; + expect(hostDirectProxyActionIdList.length).toBe(4); + const hostBundledProxyActionIds = await page.$eval( + '.host-bundled-proxy-action-ids', + el => el.textContent?.trim(), + ); + const hostBundledProxyActionIdList = hostBundledProxyActionIds + ?.split(',') + .filter(Boolean) as string[]; + expect(hostBundledProxyActionIdList.length).toBe(4); const hostRemoteAsyncServerInfo = await page.$eval( '.remote-async-server-info', el => el.textContent?.trim(), @@ -362,6 +380,33 @@ function runTests({ mode }: TestConfig) { expect(actionRequestIds.every(id => hostProxyActionIdSet.has(id))).toBe( true, ); + const directProxyActionIdSet = new Set( + ( + await page.$eval( + '.host-direct-proxy-action-ids', + el => el.textContent || '', + ) + ) + .split(',') + .filter(Boolean), + ); + const bundledProxyActionIdSet = new Set( + ( + await page.$eval( + '.host-bundled-proxy-action-ids', + el => el.textContent || '', + ) + ) + .split(',') + .filter(Boolean), + ); + const usesDirectProxyIds = actionRequestIds.some(id => + directProxyActionIdSet.has(id), + ); + const usesBundledProxyIds = actionRequestIds.some(id => + bundledProxyActionIdSet.has(id), + ); + expect(usesDirectProxyIds || usesBundledProxyIds).toBe(true); }); it('should have no browser runtime errors', () => { From 6256addc7d90f52531bd46b94d8ab54be2b37e4e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 00:30:20 +0000 Subject: [PATCH 092/324] test(rsc-mf): validate proxy id group union membership --- tests/integration/rsc-mf/tests/index.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index bb715f7146c8..b9f2c8a4b64e 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -144,6 +144,16 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { ?.split(',') .filter(Boolean) as string[]; expect(hostBundledProxyActionIdList.length).toBe(4); + const groupedProxyActionIdUnion = new Set([ + ...hostDirectProxyActionIdList, + ...hostBundledProxyActionIdList, + ]); + expect(groupedProxyActionIdUnion.size).toBe(hostProxyActionIdList.length); + expect( + hostProxyActionIdList.every(actionId => + groupedProxyActionIdUnion.has(actionId), + ), + ).toBe(true); const hostRemoteAsyncServerInfo = await page.$eval( '.remote-async-server-info', el => el.textContent?.trim(), @@ -407,6 +417,12 @@ function runTests({ mode }: TestConfig) { bundledProxyActionIdSet.has(id), ); expect(usesDirectProxyIds || usesBundledProxyIds).toBe(true); + expect( + actionRequestIds.every( + id => + directProxyActionIdSet.has(id) || bundledProxyActionIdSet.has(id), + ), + ).toBe(true); }); it('should have no browser runtime errors', () => { From 225df155997b92d2350a0f86afbe9863301071ca Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 00:34:09 +0000 Subject: [PATCH 093/324] test(rsc-mf): assert proxy map entry count aligns with ids --- .../rsc-mf/host/src/server-component-root/App.tsx | 5 +++++ tests/integration/rsc-mf/tests/index.test.ts | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx index 8b96272b0ff1..090e8f461a6b 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx @@ -99,6 +99,8 @@ const App = () => { const remoteActionIdMapKey = JSON.stringify( remoteActionIdToHostProxyActionEntries, ); + const remoteActionIdMapEntryCount = + remoteActionIdToHostProxyActionEntries.length; const remoteServerOnlyInfo = getServerOnlyInfo(); const remoteServerOnlyDefaultInfo = getServerOnlyDefaultInfo(); @@ -128,6 +130,9 @@ const App = () => {

{uniqueHostProxyActionIdsCount}

+

+ {remoteActionIdMapEntryCount} +

{uniqueHostProxyActionIds.join(',')}

diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index b9f2c8a4b64e..fa9d64289d3e 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -65,6 +65,7 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { expect(html).toContain('host-remote-bundled-server-only'); expect(html).toContain('host-remote-bundled-meta-kind'); expect(html).toContain('host-proxy-action-id-count'); + expect(html).toContain('host-proxy-map-entry-count'); expect(html).toContain('host-proxy-action-ids'); expect(html).toContain('host-direct-proxy-action-ids'); expect(html).toContain('host-bundled-proxy-action-ids'); @@ -117,6 +118,11 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { el => el.textContent?.trim(), ); expect(hostProxyActionIdCount).toBe('8'); + const hostProxyMapEntryCount = await page.$eval( + '.host-proxy-map-entry-count', + el => el.textContent?.trim(), + ); + expect(hostProxyMapEntryCount).toBe('8'); const hostProxyActionIds = await page.$eval('.host-proxy-action-ids', el => el.textContent?.trim(), ); @@ -124,6 +130,7 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { ?.split(',') .filter(Boolean) as string[]; expect(hostProxyActionIdList.length).toBe(8); + expect(hostProxyActionIdList.length).toBe(Number(hostProxyMapEntryCount)); expect(new Set(hostProxyActionIdList).size).toBe(8); expect(hostProxyActionIdList.every(id => /^[a-f0-9]{64,}$/i.test(id))).toBe( true, From d806ec0d1ff23313642d8617e9999fc8479fb654 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 00:36:59 +0000 Subject: [PATCH 094/324] test(rsc-mf): assert proxy map entry count aligns with ids --- .../host/src/server-component-root/App.tsx | 14 +++++++++ tests/integration/rsc-mf/tests/index.test.ts | 31 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx index 090e8f461a6b..ab4143469e98 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx @@ -101,6 +101,14 @@ const App = () => { ); const remoteActionIdMapEntryCount = remoteActionIdToHostProxyActionEntries.length; + const mappedHostProxyActionIds = Array.from( + new Set(remoteActionIdToHostProxyActionEntries.map(([, hostId]) => hostId)), + ).sort(); + const doesMappingCoverAllHostProxyActions = + mappedHostProxyActionIds.length === uniqueHostProxyActionIds.length && + mappedHostProxyActionIds.every( + (actionId, index) => actionId === uniqueHostProxyActionIds[index], + ); const remoteServerOnlyInfo = getServerOnlyInfo(); const remoteServerOnlyDefaultInfo = getServerOnlyDefaultInfo(); @@ -133,6 +141,12 @@ const App = () => {

{remoteActionIdMapEntryCount}

+

+ {mappedHostProxyActionIds.join(',')} +

+

+ {String(doesMappingCoverAllHostProxyActions)} +

{uniqueHostProxyActionIds.join(',')}

diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index fa9d64289d3e..5a68d71e410c 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -66,6 +66,8 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { expect(html).toContain('host-remote-bundled-meta-kind'); expect(html).toContain('host-proxy-action-id-count'); expect(html).toContain('host-proxy-map-entry-count'); + expect(html).toContain('host-mapped-proxy-action-ids'); + expect(html).toContain('host-proxy-map-covers-all'); expect(html).toContain('host-proxy-action-ids'); expect(html).toContain('host-direct-proxy-action-ids'); expect(html).toContain('host-bundled-proxy-action-ids'); @@ -123,6 +125,19 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { el => el.textContent?.trim(), ); expect(hostProxyMapEntryCount).toBe('8'); + const hostMappedProxyActionIds = await page.$eval( + '.host-mapped-proxy-action-ids', + el => el.textContent?.trim(), + ); + const hostMappedProxyActionIdList = hostMappedProxyActionIds + ?.split(',') + .filter(Boolean) as string[]; + expect(hostMappedProxyActionIdList.length).toBe(8); + const hostProxyMapCoversAll = await page.$eval( + '.host-proxy-map-covers-all', + el => el.textContent?.trim(), + ); + expect(hostProxyMapCoversAll).toBe('true'); const hostProxyActionIds = await page.$eval('.host-proxy-action-ids', el => el.textContent?.trim(), ); @@ -135,6 +150,9 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { expect(hostProxyActionIdList.every(id => /^[a-f0-9]{64,}$/i.test(id))).toBe( true, ); + expect( + hostMappedProxyActionIdList.every(id => hostProxyActionIdList.includes(id)), + ).toBe(true); const hostDirectProxyActionIds = await page.$eval( '.host-direct-proxy-action-ids', el => el.textContent?.trim(), @@ -397,6 +415,19 @@ function runTests({ mode }: TestConfig) { expect(actionRequestIds.every(id => hostProxyActionIdSet.has(id))).toBe( true, ); + const mappedProxyActionIdSet = new Set( + ( + await page.$eval( + '.host-mapped-proxy-action-ids', + el => el.textContent || '', + ) + ) + .split(',') + .filter(Boolean), + ); + expect(actionRequestIds.every(id => mappedProxyActionIdSet.has(id))).toBe( + true, + ); const directProxyActionIdSet = new Set( ( await page.$eval( From f02e2cf1793c1b243cdaf514e1b8325dc68f8e47 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 00:40:32 +0000 Subject: [PATCH 095/324] test(rsc-mf): assert mapped proxy id coverage markers --- tests/integration/rsc-mf/tests/index.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 5a68d71e410c..35492e38489f 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -133,6 +133,9 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { ?.split(',') .filter(Boolean) as string[]; expect(hostMappedProxyActionIdList.length).toBe(8); + expect(hostMappedProxyActionIdList.length).toBe( + Number(hostProxyMapEntryCount), + ); const hostProxyMapCoversAll = await page.$eval( '.host-proxy-map-covers-all', el => el.textContent?.trim(), @@ -150,8 +153,12 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { expect(hostProxyActionIdList.every(id => /^[a-f0-9]{64,}$/i.test(id))).toBe( true, ); + const sortedHostProxyActionIds = [...hostProxyActionIdList].sort(); + const sortedMappedProxyActionIds = [...hostMappedProxyActionIdList].sort(); expect( - hostMappedProxyActionIdList.every(id => hostProxyActionIdList.includes(id)), + sortedHostProxyActionIds.every( + (id, index) => id === sortedMappedProxyActionIds[index], + ), ).toBe(true); const hostDirectProxyActionIds = await page.$eval( '.host-direct-proxy-action-ids', From c220479c693b6a014038ef6cb1c5926f590ef742 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 00:46:04 +0000 Subject: [PATCH 096/324] refactor(rsc-mf): model effective remote action map keys --- .../host/src/server-component-root/App.tsx | 16 +++++++++++----- tests/integration/rsc-mf/tests/index.test.ts | 18 ++++++++++++------ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx index ab4143469e98..33f303b48481 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx @@ -96,18 +96,23 @@ const App = () => { const remoteActionIdToHostProxyActionId = Object.fromEntries( remoteActionIdToHostProxyActionEntries, ); + const remoteActionIdMapKeyCount = Object.keys( + remoteActionIdToHostProxyActionId, + ).length; const remoteActionIdMapKey = JSON.stringify( - remoteActionIdToHostProxyActionEntries, + Object.entries(remoteActionIdToHostProxyActionId).sort(([left], [right]) => + left.localeCompare(right), + ), ); const remoteActionIdMapEntryCount = remoteActionIdToHostProxyActionEntries.length; const mappedHostProxyActionIds = Array.from( - new Set(remoteActionIdToHostProxyActionEntries.map(([, hostId]) => hostId)), + new Set(Object.values(remoteActionIdToHostProxyActionId)), ).sort(); const doesMappingCoverAllHostProxyActions = - mappedHostProxyActionIds.length === uniqueHostProxyActionIds.length && - mappedHostProxyActionIds.every( - (actionId, index) => actionId === uniqueHostProxyActionIds[index], + mappedHostProxyActionIds.length <= uniqueHostProxyActionIds.length && + mappedHostProxyActionIds.every(actionId => + uniqueHostProxyActionIds.includes(actionId), ); const remoteServerOnlyInfo = getServerOnlyInfo(); @@ -141,6 +146,7 @@ const App = () => {

{remoteActionIdMapEntryCount}

+

{remoteActionIdMapKeyCount}

{mappedHostProxyActionIds.join(',')}

diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 35492e38489f..6257ffed19ab 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -66,6 +66,7 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { expect(html).toContain('host-remote-bundled-meta-kind'); expect(html).toContain('host-proxy-action-id-count'); expect(html).toContain('host-proxy-map-entry-count'); + expect(html).toContain('host-proxy-map-key-count'); expect(html).toContain('host-mapped-proxy-action-ids'); expect(html).toContain('host-proxy-map-covers-all'); expect(html).toContain('host-proxy-action-ids'); @@ -125,6 +126,14 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { el => el.textContent?.trim(), ); expect(hostProxyMapEntryCount).toBe('8'); + const hostProxyMapKeyCount = await page.$eval( + '.host-proxy-map-key-count', + el => el.textContent?.trim(), + ); + expect(Number(hostProxyMapKeyCount)).toBeGreaterThan(0); + expect(Number(hostProxyMapKeyCount)).toBeLessThanOrEqual( + Number(hostProxyMapEntryCount), + ); const hostMappedProxyActionIds = await page.$eval( '.host-mapped-proxy-action-ids', el => el.textContent?.trim(), @@ -132,10 +141,7 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { const hostMappedProxyActionIdList = hostMappedProxyActionIds ?.split(',') .filter(Boolean) as string[]; - expect(hostMappedProxyActionIdList.length).toBe(8); - expect(hostMappedProxyActionIdList.length).toBe( - Number(hostProxyMapEntryCount), - ); + expect(hostMappedProxyActionIdList.length).toBe(Number(hostProxyMapKeyCount)); const hostProxyMapCoversAll = await page.$eval( '.host-proxy-map-covers-all', el => el.textContent?.trim(), @@ -156,8 +162,8 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { const sortedHostProxyActionIds = [...hostProxyActionIdList].sort(); const sortedMappedProxyActionIds = [...hostMappedProxyActionIdList].sort(); expect( - sortedHostProxyActionIds.every( - (id, index) => id === sortedMappedProxyActionIds[index], + sortedMappedProxyActionIds.every(mappedId => + sortedHostProxyActionIds.includes(mappedId), ), ).toBe(true); const hostDirectProxyActionIds = await page.$eval( From db078ac53c4d8b637b54ebafe170ab0dd46fc415 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 00:59:02 +0000 Subject: [PATCH 097/324] test(rsc-mf): bind posted action ids to effective map key count --- tests/integration/rsc-mf/tests/index.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 6257ffed19ab..ee3c8f3263c0 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -438,6 +438,16 @@ function runTests({ mode }: TestConfig) { .split(',') .filter(Boolean), ); + const hostProxyMapKeyCount = Number( + await page.$eval( + '.host-proxy-map-key-count', + el => el.textContent || '0', + ), + ); + expect(hostProxyMapKeyCount).toBe(mappedProxyActionIdSet.size); + expect(new Set(actionRequestIds).size).toBeLessThanOrEqual( + hostProxyMapKeyCount, + ); expect(actionRequestIds.every(id => mappedProxyActionIdSet.has(id))).toBe( true, ); From 35e033cc4f44a2c62bd3b93a850bd15b643b3672 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 01:04:51 +0000 Subject: [PATCH 098/324] test(rsc-mf): handle direct-bundled id collisions in request checks --- tests/integration/rsc-mf/tests/index.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index ee3c8f3263c0..68e0204ac923 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -444,6 +444,12 @@ function runTests({ mode }: TestConfig) { el => el.textContent || '0', ), ); + const hostProxyMapEntryCount = Number( + await page.$eval( + '.host-proxy-map-entry-count', + el => el.textContent || '0', + ), + ); expect(hostProxyMapKeyCount).toBe(mappedProxyActionIdSet.size); expect(new Set(actionRequestIds).size).toBeLessThanOrEqual( hostProxyMapKeyCount, @@ -478,6 +484,9 @@ function runTests({ mode }: TestConfig) { bundledProxyActionIdSet.has(id), ); expect(usesDirectProxyIds || usesBundledProxyIds).toBe(true); + if (!usesDirectProxyIds || !usesBundledProxyIds) { + expect(hostProxyMapKeyCount).toBeLessThan(hostProxyMapEntryCount); + } expect( actionRequestIds.every( id => From 12dd1e0ae38067401f6522d377b904cfa2d989cd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 01:06:22 +0000 Subject: [PATCH 099/324] test(rsc-mf): expose exact proxy-map equality diagnostics --- .../rsc-mf/host/src/server-component-root/App.tsx | 8 ++++++++ tests/integration/rsc-mf/tests/index.test.ts | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx index 33f303b48481..0fc40f020814 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx @@ -114,6 +114,11 @@ const App = () => { mappedHostProxyActionIds.every(actionId => uniqueHostProxyActionIds.includes(actionId), ); + const doesMappingExactlyMatchAllHostProxyActions = + mappedHostProxyActionIds.length === uniqueHostProxyActionIds.length && + mappedHostProxyActionIds.every( + (actionId, index) => actionId === uniqueHostProxyActionIds[index], + ); const remoteServerOnlyInfo = getServerOnlyInfo(); const remoteServerOnlyDefaultInfo = getServerOnlyDefaultInfo(); @@ -153,6 +158,9 @@ const App = () => {

{String(doesMappingCoverAllHostProxyActions)}

+

+ {String(doesMappingExactlyMatchAllHostProxyActions)} +

{uniqueHostProxyActionIds.join(',')}

diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 68e0204ac923..9be4473ad1d1 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -69,6 +69,7 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { expect(html).toContain('host-proxy-map-key-count'); expect(html).toContain('host-mapped-proxy-action-ids'); expect(html).toContain('host-proxy-map-covers-all'); + expect(html).toContain('host-proxy-map-equals-all'); expect(html).toContain('host-proxy-action-ids'); expect(html).toContain('host-direct-proxy-action-ids'); expect(html).toContain('host-bundled-proxy-action-ids'); @@ -147,6 +148,15 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { el => el.textContent?.trim(), ); expect(hostProxyMapCoversAll).toBe('true'); + const hostProxyMapEqualsAll = await page.$eval( + '.host-proxy-map-equals-all', + el => el.textContent?.trim(), + ); + expect(hostProxyMapEqualsAll).toBe( + Number(hostProxyMapKeyCount) === Number(hostProxyMapEntryCount) + ? 'true' + : 'false', + ); const hostProxyActionIds = await page.$eval('.host-proxy-action-ids', el => el.textContent?.trim(), ); From 661c5c32ee21c85cb6a1968e4a86edbb91e5748b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 01:13:44 +0000 Subject: [PATCH 100/324] test(rsc-mf): expose proxy map collision count diagnostics --- .../host/src/server-component-root/App.tsx | 7 +++++++ tests/integration/rsc-mf/tests/index.test.ts | 20 ++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx index 0fc40f020814..ab87ad27f18e 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx @@ -106,6 +106,10 @@ const App = () => { ); const remoteActionIdMapEntryCount = remoteActionIdToHostProxyActionEntries.length; + const remoteActionIdMapCollisionCount = Math.max( + remoteActionIdMapEntryCount - remoteActionIdMapKeyCount, + 0, + ); const mappedHostProxyActionIds = Array.from( new Set(Object.values(remoteActionIdToHostProxyActionId)), ).sort(); @@ -152,6 +156,9 @@ const App = () => { {remoteActionIdMapEntryCount}

{remoteActionIdMapKeyCount}

+

+ {remoteActionIdMapCollisionCount} +

{mappedHostProxyActionIds.join(',')}

diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 9be4473ad1d1..a1678e211abf 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -67,6 +67,7 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { expect(html).toContain('host-proxy-action-id-count'); expect(html).toContain('host-proxy-map-entry-count'); expect(html).toContain('host-proxy-map-key-count'); + expect(html).toContain('host-proxy-map-collision-count'); expect(html).toContain('host-mapped-proxy-action-ids'); expect(html).toContain('host-proxy-map-covers-all'); expect(html).toContain('host-proxy-map-equals-all'); @@ -135,6 +136,14 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { expect(Number(hostProxyMapKeyCount)).toBeLessThanOrEqual( Number(hostProxyMapEntryCount), ); + const hostProxyMapCollisionCount = await page.$eval( + '.host-proxy-map-collision-count', + el => el.textContent?.trim(), + ); + expect(Number(hostProxyMapCollisionCount)).toBeGreaterThanOrEqual(0); + expect(Number(hostProxyMapCollisionCount)).toBe( + Number(hostProxyMapEntryCount) - Number(hostProxyMapKeyCount), + ); const hostMappedProxyActionIds = await page.$eval( '.host-mapped-proxy-action-ids', el => el.textContent?.trim(), @@ -460,10 +469,19 @@ function runTests({ mode }: TestConfig) { el => el.textContent || '0', ), ); + const hostProxyMapCollisionCount = Number( + await page.$eval( + '.host-proxy-map-collision-count', + el => el.textContent || '0', + ), + ); expect(hostProxyMapKeyCount).toBe(mappedProxyActionIdSet.size); expect(new Set(actionRequestIds).size).toBeLessThanOrEqual( hostProxyMapKeyCount, ); + expect(hostProxyMapCollisionCount).toBe( + hostProxyMapEntryCount - hostProxyMapKeyCount, + ); expect(actionRequestIds.every(id => mappedProxyActionIdSet.has(id))).toBe( true, ); @@ -495,7 +513,7 @@ function runTests({ mode }: TestConfig) { ); expect(usesDirectProxyIds || usesBundledProxyIds).toBe(true); if (!usesDirectProxyIds || !usesBundledProxyIds) { - expect(hostProxyMapKeyCount).toBeLessThan(hostProxyMapEntryCount); + expect(hostProxyMapCollisionCount).toBeGreaterThan(0); } expect( actionRequestIds.every( From ee24d539e50016ea49e135ac0269c8e63395688e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 01:17:22 +0000 Subject: [PATCH 101/324] test(rsc-mf): cover remote client default server action path --- .../src/components/RemoteClientCounter.tsx | 16 ++++++++++++---- tests/integration/rsc-mf/tests/index.test.ts | 6 +++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.tsx b/tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.tsx index 599622852f3b..67483b2203b5 100644 --- a/tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.tsx +++ b/tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.tsx @@ -2,6 +2,7 @@ import { useActionState, useState } from 'react'; import './RemoteClientCounter.css'; import { incrementRemoteCount, remoteActionEcho } from './actions'; +import { defaultRemoteAction } from './defaultAction'; import { nestedRemoteAction } from './nestedActions'; export function RemoteClientCounter() { @@ -12,14 +13,18 @@ export function RemoteClientCounter() { ); const [nestedResult, setNestedResult] = useState(''); const [remoteActionResult, setRemoteActionResult] = useState(''); + const [defaultActionResult, setDefaultActionResult] = useState(''); const handleRunActions = async () => { - const [nestedResultValue, remoteActionValue] = await Promise.all([ - nestedRemoteAction('from-client'), - remoteActionEcho('from-client'), - ]); + const [nestedResultValue, remoteActionValue, defaultActionValue] = + await Promise.all([ + nestedRemoteAction('from-client'), + remoteActionEcho('from-client'), + defaultRemoteAction('from-client'), + ]); setNestedResult(nestedResultValue); setRemoteActionResult(remoteActionValue); + setDefaultActionResult(defaultActionValue); }; return ( @@ -49,6 +54,9 @@ export function RemoteClientCounter() {

{nestedResult}

{remoteActionResult}

+

+ {defaultActionResult} +

); } diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index a1678e211abf..fa48bafc2589 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -272,9 +272,13 @@ async function supportRemoteClientAndServerActions({ const remoteAction = document.querySelector( '.host-remote-action-runner .remote-client-remote-action-result', ); + const defaultAction = document.querySelector( + '.host-remote-action-runner .remote-client-default-action-result', + ); return ( nested?.textContent?.trim() === 'nested-action:from-client' && - remoteAction?.textContent?.trim() === 'remote-action:from-client' + remoteAction?.textContent?.trim() === 'remote-action:from-client' && + defaultAction?.textContent?.trim() === 'default-action:from-client' ); }); From 748302951fd6e4a48df606340f9c8e838652faf9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 01:21:22 +0000 Subject: [PATCH 102/324] test(rsc-mf): assert request coverage across action families --- .../host/src/server-component-root/App.tsx | 40 +++++++++ tests/integration/rsc-mf/tests/index.test.ts | 88 +++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx index ab87ad27f18e..9a5e71fb3415 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx @@ -56,6 +56,34 @@ const App = () => { new Set(hostProxyActionIds), ).sort(); const uniqueHostProxyActionIdsCount = uniqueHostProxyActionIds.length; + const incrementProxyActionIds = Array.from( + new Set( + [proxyIncrementRemoteCount, proxyBundledIncrementRemoteCount] + .map(action => getServerActionId(action)) + .filter((actionId): actionId is string => Boolean(actionId)), + ), + ).sort(); + const echoProxyActionIds = Array.from( + new Set( + [proxyRemoteActionEcho, proxyBundledRemoteActionEcho] + .map(action => getServerActionId(action)) + .filter((actionId): actionId is string => Boolean(actionId)), + ), + ).sort(); + const nestedProxyActionIds = Array.from( + new Set( + [proxyNestedRemoteAction, proxyBundledNestedRemoteAction] + .map(action => getServerActionId(action)) + .filter((actionId): actionId is string => Boolean(actionId)), + ), + ).sort(); + const defaultProxyActionIds = Array.from( + new Set( + [proxyDefaultRemoteAction, proxyBundledDefaultRemoteAction] + .map(action => getServerActionId(action)) + .filter((actionId): actionId is string => Boolean(actionId)), + ), + ).sort(); // Map remote action IDs to host-local proxy action IDs so client-side // callbacks can always post a host-resolvable action id. This keeps @@ -177,6 +205,18 @@ const App = () => {

{bundledHostProxyActionIds.join(',')}

+

+ {incrementProxyActionIds.join(',')} +

+

+ {echoProxyActionIds.join(',')} +

+

+ {nestedProxyActionIds.join(',')} +

+

+ {defaultProxyActionIds.join(',')} +

Loading Remote Async Server Info...}> diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index fa48bafc2589..b96f287e0343 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -74,6 +74,10 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { expect(html).toContain('host-proxy-action-ids'); expect(html).toContain('host-direct-proxy-action-ids'); expect(html).toContain('host-bundled-proxy-action-ids'); + expect(html).toContain('host-increment-proxy-action-ids'); + expect(html).toContain('host-echo-proxy-action-ids'); + expect(html).toContain('host-nested-proxy-action-ids'); + expect(html).toContain('host-default-proxy-action-ids'); await page.goto(`http://127.0.0.1:${hostPort}${HOST_RSC_URL}`, { waitUntil: ['networkidle0', 'domcontentloaded'], @@ -201,6 +205,38 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { ?.split(',') .filter(Boolean) as string[]; expect(hostBundledProxyActionIdList.length).toBe(4); + const hostIncrementProxyActionIds = await page.$eval( + '.host-increment-proxy-action-ids', + el => el.textContent?.trim(), + ); + const hostIncrementProxyActionIdList = hostIncrementProxyActionIds + ?.split(',') + .filter(Boolean) as string[]; + expect(hostIncrementProxyActionIdList.length).toBeGreaterThan(0); + const hostEchoProxyActionIds = await page.$eval( + '.host-echo-proxy-action-ids', + el => el.textContent?.trim(), + ); + const hostEchoProxyActionIdList = hostEchoProxyActionIds + ?.split(',') + .filter(Boolean) as string[]; + expect(hostEchoProxyActionIdList.length).toBeGreaterThan(0); + const hostNestedProxyActionIds = await page.$eval( + '.host-nested-proxy-action-ids', + el => el.textContent?.trim(), + ); + const hostNestedProxyActionIdList = hostNestedProxyActionIds + ?.split(',') + .filter(Boolean) as string[]; + expect(hostNestedProxyActionIdList.length).toBeGreaterThan(0); + const hostDefaultProxyActionIds = await page.$eval( + '.host-default-proxy-action-ids', + el => el.textContent?.trim(), + ); + const hostDefaultProxyActionIdList = hostDefaultProxyActionIds + ?.split(',') + .filter(Boolean) as string[]; + expect(hostDefaultProxyActionIdList.length).toBeGreaterThan(0); const groupedProxyActionIdUnion = new Set([ ...hostDirectProxyActionIdList, ...hostBundledProxyActionIdList, @@ -515,10 +551,62 @@ function runTests({ mode }: TestConfig) { const usesBundledProxyIds = actionRequestIds.some(id => bundledProxyActionIdSet.has(id), ); + const incrementProxyActionIdSet = new Set( + ( + await page.$eval( + '.host-increment-proxy-action-ids', + el => el.textContent || '', + ) + ) + .split(',') + .filter(Boolean), + ); + const echoProxyActionIdSet = new Set( + ( + await page.$eval( + '.host-echo-proxy-action-ids', + el => el.textContent || '', + ) + ) + .split(',') + .filter(Boolean), + ); + const nestedProxyActionIdSet = new Set( + ( + await page.$eval( + '.host-nested-proxy-action-ids', + el => el.textContent || '', + ) + ) + .split(',') + .filter(Boolean), + ); + const defaultProxyActionIdSet = new Set( + ( + await page.$eval( + '.host-default-proxy-action-ids', + el => el.textContent || '', + ) + ) + .split(',') + .filter(Boolean), + ); expect(usesDirectProxyIds || usesBundledProxyIds).toBe(true); if (!usesDirectProxyIds || !usesBundledProxyIds) { expect(hostProxyMapCollisionCount).toBeGreaterThan(0); } + expect( + actionRequestIds.some(id => incrementProxyActionIdSet.has(id)), + ).toBe(true); + expect(actionRequestIds.some(id => echoProxyActionIdSet.has(id))).toBe( + true, + ); + expect(actionRequestIds.some(id => nestedProxyActionIdSet.has(id))).toBe( + true, + ); + expect(actionRequestIds.some(id => defaultProxyActionIdSet.has(id))).toBe( + true, + ); expect( actionRequestIds.every( id => From 9d3c307201f9a2ceaa5479213061187e5031d1a6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 01:25:46 +0000 Subject: [PATCH 103/324] test(rsc-mf): cover direct host nested and increment actions --- .../HostRemoteActionRunner.tsx | 25 +++++++++++++++---- tests/integration/rsc-mf/tests/index.test.ts | 11 +++++++- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx b/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx index 5b97e718fe58..d34f161ce05e 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx @@ -4,8 +4,9 @@ import { useEffect, useState } from 'react'; import RemoteClientBadge from 'rscRemote/RemoteClientBadge'; import { RemoteClientCounter as RemoteClientCounterBridge } from 'rscRemote/RemoteClientCounter'; import * as remoteActionBundle from 'rscRemote/actionBundle'; -import { remoteActionEcho } from 'rscRemote/actions'; +import { incrementRemoteCount, remoteActionEcho } from 'rscRemote/actions'; import { defaultRemoteAction } from 'rscRemote/defaultAction'; +import { nestedRemoteAction } from 'rscRemote/nestedActions'; import { registerRemoteServerCallback } from 'rscRemote/registerServerCallback'; export default function HostRemoteActionRunner({ @@ -20,6 +21,8 @@ export default function HostRemoteActionRunner({ void RemoteClientCounterBridge; const [defaultResult, setDefaultResult] = useState(''); const [echoResult, setEchoResult] = useState(''); + const [nestedResult, setNestedResult] = useState(''); + const [incrementResult, setIncrementResult] = useState(''); const [bundledDefaultResult, setBundledDefaultResult] = useState(''); const [bundledEchoResult, setBundledEchoResult] = useState(''); const [bundledNestedResult, setBundledNestedResult] = useState(''); @@ -39,18 +42,21 @@ export default function HostRemoteActionRunner({ const runActions = async () => { setIsPending(true); try { + const directIncrementFormData = new FormData(); + directIncrementFormData.set('count', '1'); const bundledIncrementFormData = new FormData(); bundledIncrementFormData.set('count', '1'); const [ defaultValue, echoValue, + nestedValue, bundledDefaultValue, bundledEchoValue, bundledNestedValue, - bundledIncrementValue, ] = await Promise.all([ defaultRemoteAction('from-host-client'), remoteActionEcho('from-host-client'), + nestedRemoteAction('from-host-client-direct'), remoteActionBundle.bundledDefaultRemoteAction( 'from-host-client-bundled', ), @@ -58,13 +64,20 @@ export default function HostRemoteActionRunner({ remoteActionBundle.bundledNestedRemoteAction( 'from-host-client-bundled', ), - remoteActionBundle.bundledIncrementRemoteCount( + ]); + const directIncrementValue = await incrementRemoteCount( + 0, + directIncrementFormData, + ); + const bundledIncrementValue = + await remoteActionBundle.bundledIncrementRemoteCount( 0, bundledIncrementFormData, - ), - ]); + ); setDefaultResult(defaultValue); setEchoResult(echoValue); + setNestedResult(nestedValue); + setIncrementResult(String(directIncrementValue)); setBundledDefaultResult(bundledDefaultValue); setBundledEchoResult(bundledEchoValue); setBundledNestedResult(bundledNestedValue); @@ -87,6 +100,8 @@ export default function HostRemoteActionRunner({

{defaultResult}

{echoResult}

+

{nestedResult}

+

{incrementResult}

{bundledDefaultResult}

diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index b96f287e0343..d138403cb642 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -338,6 +338,12 @@ async function supportRemoteClientAndServerActions({ const echoActionResult = document.querySelector( '.host-remote-echo-action-result', ); + const nestedActionResult = document.querySelector( + '.host-remote-nested-action-result', + ); + const incrementActionResult = document.querySelector( + '.host-remote-increment-action-result', + ); const bundledDefaultActionResult = document.querySelector( '.host-remote-bundled-default-action-result', ); @@ -355,13 +361,16 @@ async function supportRemoteClientAndServerActions({ 'default-action:from-host-client' && echoActionResult?.textContent?.trim() === 'remote-action:from-host-client' && + nestedActionResult?.textContent?.trim() === + 'nested-action:from-host-client-direct' && + incrementActionResult?.textContent?.trim() === '2' && bundledDefaultActionResult?.textContent?.trim() === 'default-action:from-host-client-bundled' && bundledEchoActionResult?.textContent?.trim() === 'remote-action:from-host-client-bundled' && bundledNestedActionResult?.textContent?.trim() === 'nested-action:from-host-client-bundled' && - bundledIncrementActionResult?.textContent?.trim() === '2' + bundledIncrementActionResult?.textContent?.trim() === '3' ); }); } From 0c0ca338978a596997ca7075df778bc1231abdad Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 01:28:33 +0000 Subject: [PATCH 104/324] test(rsc-mf): enforce minimum action post volume per mode --- tests/integration/rsc-mf/tests/index.test.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index d138403cb642..f04f5384e5e3 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -16,6 +16,7 @@ const fixtureDir = path.resolve(__dirname, '../'); const hostDir = path.resolve(fixtureDir, 'host'); const remoteDir = path.resolve(fixtureDir, 'remote'); const HOST_RSC_URL = '/server-component-root'; +const MIN_EXPECTED_ACTION_POSTS_PER_MODE = 10; type Mode = 'dev' | 'build'; @@ -466,7 +467,9 @@ function runTests({ mode }: TestConfig) { supportRemoteClientAndServerActions({ hostPort, page })); it('should route remote actions through host endpoint', () => { - expect(actionRequestUrls.length).toBeGreaterThan(0); + expect(actionRequestUrls.length).toBeGreaterThanOrEqual( + MIN_EXPECTED_ACTION_POSTS_PER_MODE, + ); expect( actionRequestUrls.every(url => url.startsWith(`http://127.0.0.1:${hostPort}${HOST_RSC_URL}`), @@ -480,7 +483,9 @@ function runTests({ mode }: TestConfig) { }); it('should post host-resolvable action ids for remote actions', async () => { - expect(actionRequestIds.length).toBeGreaterThan(0); + expect(actionRequestIds.length).toBeGreaterThanOrEqual( + MIN_EXPECTED_ACTION_POSTS_PER_MODE, + ); expect(actionRequestIds.every(id => !id.startsWith('remote:'))).toBe( true, ); From 963c240efbe4f7ff39745565865a582999bbabc5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 01:32:36 +0000 Subject: [PATCH 105/324] test(rsc-mf): require action-family union coverage for posted ids --- tests/integration/rsc-mf/tests/index.test.ts | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index f04f5384e5e3..4e448501b68a 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -214,6 +214,11 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { ?.split(',') .filter(Boolean) as string[]; expect(hostIncrementProxyActionIdList.length).toBeGreaterThan(0); + expect( + hostIncrementProxyActionIdList.every(id => + hostProxyActionIdList.includes(id), + ), + ).toBe(true); const hostEchoProxyActionIds = await page.$eval( '.host-echo-proxy-action-ids', el => el.textContent?.trim(), @@ -222,6 +227,9 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { ?.split(',') .filter(Boolean) as string[]; expect(hostEchoProxyActionIdList.length).toBeGreaterThan(0); + expect( + hostEchoProxyActionIdList.every(id => hostProxyActionIdList.includes(id)), + ).toBe(true); const hostNestedProxyActionIds = await page.$eval( '.host-nested-proxy-action-ids', el => el.textContent?.trim(), @@ -230,6 +238,9 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { ?.split(',') .filter(Boolean) as string[]; expect(hostNestedProxyActionIdList.length).toBeGreaterThan(0); + expect( + hostNestedProxyActionIdList.every(id => hostProxyActionIdList.includes(id)), + ).toBe(true); const hostDefaultProxyActionIds = await page.$eval( '.host-default-proxy-action-ids', el => el.textContent?.trim(), @@ -238,6 +249,11 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { ?.split(',') .filter(Boolean) as string[]; expect(hostDefaultProxyActionIdList.length).toBeGreaterThan(0); + expect( + hostDefaultProxyActionIdList.every(id => + hostProxyActionIdList.includes(id), + ), + ).toBe(true); const groupedProxyActionIdUnion = new Set([ ...hostDirectProxyActionIdList, ...hostBundledProxyActionIdList, @@ -605,6 +621,12 @@ function runTests({ mode }: TestConfig) { .split(',') .filter(Boolean), ); + const actionFamilyProxyActionIdSet = new Set([ + ...incrementProxyActionIdSet, + ...echoProxyActionIdSet, + ...nestedProxyActionIdSet, + ...defaultProxyActionIdSet, + ]); expect(usesDirectProxyIds || usesBundledProxyIds).toBe(true); if (!usesDirectProxyIds || !usesBundledProxyIds) { expect(hostProxyMapCollisionCount).toBeGreaterThan(0); @@ -621,6 +643,9 @@ function runTests({ mode }: TestConfig) { expect(actionRequestIds.some(id => defaultProxyActionIdSet.has(id))).toBe( true, ); + expect( + actionRequestIds.every(id => actionFamilyProxyActionIdSet.has(id)), + ).toBe(true); expect( actionRequestIds.every( id => From 6f81467a70602f430ea761d4454de5b029d4912c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 01:35:13 +0000 Subject: [PATCH 106/324] test(rsc-mf): require action-family union coverage for posted ids --- tests/integration/rsc-mf/tests/index.test.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 4e448501b68a..b11ab5404581 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -16,7 +16,7 @@ const fixtureDir = path.resolve(__dirname, '../'); const hostDir = path.resolve(fixtureDir, 'host'); const remoteDir = path.resolve(fixtureDir, 'remote'); const HOST_RSC_URL = '/server-component-root'; -const MIN_EXPECTED_ACTION_POSTS_PER_MODE = 10; +const MIN_EXPECTED_ACTION_POSTS_PER_MODE = 18; type Mode = 'dev' | 'build'; @@ -390,6 +390,20 @@ async function supportRemoteClientAndServerActions({ bundledIncrementActionResult?.textContent?.trim() === '3' ); }); + + await page.click('.host-remote-run-actions'); + await page.waitForFunction(() => { + const incrementActionResult = document.querySelector( + '.host-remote-increment-action-result', + ); + const bundledIncrementActionResult = document.querySelector( + '.host-remote-bundled-increment-action-result', + ); + return ( + incrementActionResult?.textContent?.trim() === '4' && + bundledIncrementActionResult?.textContent?.trim() === '5' + ); + }); } function runTests({ mode }: TestConfig) { From 125651563a4147a96634201e6d5873aa662ae516 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 01:39:04 +0000 Subject: [PATCH 107/324] test(rsc-mf): assert repeated client callback request growth --- tests/integration/rsc-mf/tests/index.test.ts | 68 +++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index b11ab5404581..7727e1d9cb34 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -16,7 +16,7 @@ const fixtureDir = path.resolve(__dirname, '../'); const hostDir = path.resolve(fixtureDir, 'host'); const remoteDir = path.resolve(fixtureDir, 'remote'); const HOST_RSC_URL = '/server-component-root'; -const MIN_EXPECTED_ACTION_POSTS_PER_MODE = 18; +const MIN_EXPECTED_ACTION_POSTS_PER_MODE = 22; type Mode = 'dev' | 'build'; @@ -27,6 +27,29 @@ interface TestConfig { interface TestContext { hostPort: number; page: Page; + actionRequestIds?: string[]; +} + +async function waitForActionRequestCount({ + actionRequestIds, + minimumCount, + timeoutMs = 15000, +}: { + actionRequestIds: string[]; + minimumCount: number; + timeoutMs?: number; +}) { + const startTime = Date.now(); + while (Date.now() - startTime < timeoutMs) { + if (actionRequestIds.length >= minimumCount) { + return; + } + await sleep(50); + } + + throw new Error( + `Timed out waiting for action request count ${minimumCount}, received ${actionRequestIds.length}`, + ); } function skipForLowerNodeVersion() { @@ -274,6 +297,7 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { async function supportRemoteClientAndServerActions({ hostPort, page, + actionRequestIds, }: TestContext) { await page.goto(`http://127.0.0.1:${hostPort}${HOST_RSC_URL}`, { waitUntil: ['networkidle0', 'domcontentloaded'], @@ -317,7 +341,10 @@ async function supportRemoteClientAndServerActions({ ); expect(serverCount).toBe('1'); + const actionRequestCountBeforeFirstClientRun = actionRequestIds?.length || 0; await page.click('.host-remote-action-runner .remote-client-run-actions'); + const actionRequestCountAfterFirstClientRun = + actionRequestCountBeforeFirstClientRun + 3; await page.waitForFunction(() => { const nested = document.querySelector( '.host-remote-action-runner .remote-client-nested-result', @@ -334,6 +361,39 @@ async function supportRemoteClientAndServerActions({ defaultAction?.textContent?.trim() === 'default-action:from-client' ); }); + if (actionRequestIds) { + await waitForActionRequestCount({ + actionRequestIds, + minimumCount: actionRequestCountAfterFirstClientRun, + }); + } + + const actionRequestCountBeforeSecondClientRun = actionRequestIds?.length || 0; + await page.click('.host-remote-action-runner .remote-client-run-actions'); + const actionRequestCountAfterSecondClientRun = + actionRequestCountBeforeSecondClientRun + 3; + await page.waitForFunction(() => { + const nested = document.querySelector( + '.host-remote-action-runner .remote-client-nested-result', + ); + const remoteAction = document.querySelector( + '.host-remote-action-runner .remote-client-remote-action-result', + ); + const defaultAction = document.querySelector( + '.host-remote-action-runner .remote-client-default-action-result', + ); + return ( + nested?.textContent?.trim() === 'nested-action:from-client' && + remoteAction?.textContent?.trim() === 'remote-action:from-client' && + defaultAction?.textContent?.trim() === 'default-action:from-client' + ); + }); + if (actionRequestIds) { + await waitForActionRequestCount({ + actionRequestIds, + minimumCount: actionRequestCountAfterSecondClientRun, + }); + } let badgeValue = await page.$eval( '.host-remote-action-runner .remote-client-badge-value', @@ -494,7 +554,11 @@ function runTests({ mode }: TestConfig) { renderRemoteRscIntoHost({ hostPort, page })); it('should support remote use client and server actions', () => - supportRemoteClientAndServerActions({ hostPort, page })); + supportRemoteClientAndServerActions({ + hostPort, + page, + actionRequestIds, + })); it('should route remote actions through host endpoint', () => { expect(actionRequestUrls.length).toBeGreaterThanOrEqual( From d3212372d29369d97540d9d9d6a0f1cd36265791 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 01:42:44 +0000 Subject: [PATCH 108/324] test(rsc-mf): align unique posted ids with effective action keys --- tests/integration/rsc-mf/tests/index.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 7727e1d9cb34..abe77bc59bfa 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -580,13 +580,14 @@ function runTests({ mode }: TestConfig) { expect(actionRequestIds.length).toBeGreaterThanOrEqual( MIN_EXPECTED_ACTION_POSTS_PER_MODE, ); + const uniqueActionRequestIds = new Set(actionRequestIds); expect(actionRequestIds.every(id => !id.startsWith('remote:'))).toBe( true, ); expect(actionRequestIds.every(id => /^[a-f0-9]{64,}$/i.test(id))).toBe( true, ); - expect(new Set(actionRequestIds).size).toBeGreaterThanOrEqual(4); + expect(uniqueActionRequestIds.size).toBeGreaterThanOrEqual(4); const hostProxyActionIdSet = new Set( (await page.$eval('.host-proxy-action-ids', el => el.textContent || '')) .split(',') @@ -624,7 +625,7 @@ function runTests({ mode }: TestConfig) { ), ); expect(hostProxyMapKeyCount).toBe(mappedProxyActionIdSet.size); - expect(new Set(actionRequestIds).size).toBeLessThanOrEqual( + expect(uniqueActionRequestIds.size).toBeLessThanOrEqual( hostProxyMapKeyCount, ); expect(hostProxyMapCollisionCount).toBe( @@ -705,6 +706,8 @@ function runTests({ mode }: TestConfig) { ...nestedProxyActionIdSet, ...defaultProxyActionIdSet, ]); + expect(actionFamilyProxyActionIdSet.size).toBe(hostProxyActionIdSet.size); + expect(uniqueActionRequestIds.size).toBe(hostProxyMapKeyCount); expect(usesDirectProxyIds || usesBundledProxyIds).toBe(true); if (!usesDirectProxyIds || !usesBundledProxyIds) { expect(hostProxyMapCollisionCount).toBeGreaterThan(0); From bf250398bb1f419926fc9f011951a88195ff02c0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 01:46:58 +0000 Subject: [PATCH 109/324] test(rsc-mf): tighten per-family proxy id cardinality checks --- tests/integration/rsc-mf/tests/index.test.ts | 72 ++++++++++++++++++-- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index abe77bc59bfa..b959fab57b1a 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -236,12 +236,15 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { const hostIncrementProxyActionIdList = hostIncrementProxyActionIds ?.split(',') .filter(Boolean) as string[]; - expect(hostIncrementProxyActionIdList.length).toBeGreaterThan(0); + expect(hostIncrementProxyActionIdList.length).toBe(2); expect( hostIncrementProxyActionIdList.every(id => hostProxyActionIdList.includes(id), ), ).toBe(true); + expect( + hostIncrementProxyActionIdList.every(id => /^[a-f0-9]{64,}$/i.test(id)), + ).toBe(true); const hostEchoProxyActionIds = await page.$eval( '.host-echo-proxy-action-ids', el => el.textContent?.trim(), @@ -249,10 +252,13 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { const hostEchoProxyActionIdList = hostEchoProxyActionIds ?.split(',') .filter(Boolean) as string[]; - expect(hostEchoProxyActionIdList.length).toBeGreaterThan(0); + expect(hostEchoProxyActionIdList.length).toBe(2); expect( hostEchoProxyActionIdList.every(id => hostProxyActionIdList.includes(id)), ).toBe(true); + expect( + hostEchoProxyActionIdList.every(id => /^[a-f0-9]{64,}$/i.test(id)), + ).toBe(true); const hostNestedProxyActionIds = await page.$eval( '.host-nested-proxy-action-ids', el => el.textContent?.trim(), @@ -260,10 +266,13 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { const hostNestedProxyActionIdList = hostNestedProxyActionIds ?.split(',') .filter(Boolean) as string[]; - expect(hostNestedProxyActionIdList.length).toBeGreaterThan(0); + expect(hostNestedProxyActionIdList.length).toBe(2); expect( hostNestedProxyActionIdList.every(id => hostProxyActionIdList.includes(id)), ).toBe(true); + expect( + hostNestedProxyActionIdList.every(id => /^[a-f0-9]{64,}$/i.test(id)), + ).toBe(true); const hostDefaultProxyActionIds = await page.$eval( '.host-default-proxy-action-ids', el => el.textContent?.trim(), @@ -271,12 +280,67 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { const hostDefaultProxyActionIdList = hostDefaultProxyActionIds ?.split(',') .filter(Boolean) as string[]; - expect(hostDefaultProxyActionIdList.length).toBeGreaterThan(0); + expect(hostDefaultProxyActionIdList.length).toBe(2); expect( hostDefaultProxyActionIdList.every(id => hostProxyActionIdList.includes(id), ), ).toBe(true); + expect( + hostDefaultProxyActionIdList.every(id => /^[a-f0-9]{64,}$/i.test(id)), + ).toBe(true); + const getGroupMembershipCount = ( + actionIds: string[], + groupIds: string[], + ): number => actionIds.filter(id => groupIds.includes(id)).length; + expect( + getGroupMembershipCount( + hostIncrementProxyActionIdList, + hostDirectProxyActionIdList, + ), + ).toBe(1); + expect( + getGroupMembershipCount( + hostIncrementProxyActionIdList, + hostBundledProxyActionIdList, + ), + ).toBe(1); + expect( + getGroupMembershipCount( + hostEchoProxyActionIdList, + hostDirectProxyActionIdList, + ), + ).toBe(1); + expect( + getGroupMembershipCount( + hostEchoProxyActionIdList, + hostBundledProxyActionIdList, + ), + ).toBe(1); + expect( + getGroupMembershipCount( + hostNestedProxyActionIdList, + hostDirectProxyActionIdList, + ), + ).toBe(1); + expect( + getGroupMembershipCount( + hostNestedProxyActionIdList, + hostBundledProxyActionIdList, + ), + ).toBe(1); + expect( + getGroupMembershipCount( + hostDefaultProxyActionIdList, + hostDirectProxyActionIdList, + ), + ).toBe(1); + expect( + getGroupMembershipCount( + hostDefaultProxyActionIdList, + hostBundledProxyActionIdList, + ), + ).toBe(1); const groupedProxyActionIdUnion = new Set([ ...hostDirectProxyActionIdList, ...hostBundledProxyActionIdList, From 93890ab13dda3bb9b8a06310618cb1f4ce78fa56 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 01:49:53 +0000 Subject: [PATCH 110/324] test(rsc-mf): tighten per-family proxy id cardinality checks --- tests/integration/rsc-mf/tests/index.test.ts | 24 ++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index b959fab57b1a..c17f45c06e92 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -404,6 +404,22 @@ async function supportRemoteClientAndServerActions({ el => el.textContent?.trim(), ); expect(serverCount).toBe('1'); + await page.click( + '.host-remote-action-runner .remote-client-server-increment', + ); + await page.waitForFunction( + () => + !document + .querySelector( + '.host-remote-action-runner .remote-client-server-increment', + ) + ?.hasAttribute('disabled'), + ); + serverCount = await page.$eval( + '.host-remote-action-runner .remote-client-server-count', + el => el.textContent?.trim(), + ); + expect(serverCount).toBe('2'); const actionRequestCountBeforeFirstClientRun = actionRequestIds?.length || 0; await page.click('.host-remote-action-runner .remote-client-run-actions'); @@ -504,14 +520,14 @@ async function supportRemoteClientAndServerActions({ 'remote-action:from-host-client' && nestedActionResult?.textContent?.trim() === 'nested-action:from-host-client-direct' && - incrementActionResult?.textContent?.trim() === '2' && + incrementActionResult?.textContent?.trim() === '3' && bundledDefaultActionResult?.textContent?.trim() === 'default-action:from-host-client-bundled' && bundledEchoActionResult?.textContent?.trim() === 'remote-action:from-host-client-bundled' && bundledNestedActionResult?.textContent?.trim() === 'nested-action:from-host-client-bundled' && - bundledIncrementActionResult?.textContent?.trim() === '3' + bundledIncrementActionResult?.textContent?.trim() === '4' ); }); @@ -524,8 +540,8 @@ async function supportRemoteClientAndServerActions({ '.host-remote-bundled-increment-action-result', ); return ( - incrementActionResult?.textContent?.trim() === '4' && - bundledIncrementActionResult?.textContent?.trim() === '5' + incrementActionResult?.textContent?.trim() === '5' && + bundledIncrementActionResult?.textContent?.trim() === '6' ); }); } From 0dec94caba6239eaa4d05693ac7c0a43e8c7c69f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 01:51:03 +0000 Subject: [PATCH 111/324] test(rsc-mf): assert action request URL-id parity --- tests/integration/rsc-mf/tests/index.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index c17f45c06e92..059a87c35c60 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -644,6 +644,7 @@ function runTests({ mode }: TestConfig) { expect(actionRequestUrls.length).toBeGreaterThanOrEqual( MIN_EXPECTED_ACTION_POSTS_PER_MODE, ); + expect(actionRequestUrls.length).toBe(actionRequestIds.length); expect( actionRequestUrls.every(url => url.startsWith(`http://127.0.0.1:${hostPort}${HOST_RSC_URL}`), @@ -660,6 +661,7 @@ function runTests({ mode }: TestConfig) { expect(actionRequestIds.length).toBeGreaterThanOrEqual( MIN_EXPECTED_ACTION_POSTS_PER_MODE, ); + expect(actionRequestIds.length).toBe(actionRequestUrls.length); const uniqueActionRequestIds = new Set(actionRequestIds); expect(actionRequestIds.every(id => !id.startsWith('remote:'))).toBe( true, From 047f7d28a17c3b3fe24b1b88cf043e627bf31df0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 01:55:00 +0000 Subject: [PATCH 112/324] test(rsc-mf): assert family union spans all host proxy ids --- tests/integration/rsc-mf/tests/index.test.ts | 60 ++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 059a87c35c60..2aa2692ff290 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -341,6 +341,18 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { hostBundledProxyActionIdList, ), ).toBe(1); + const familyProxyActionIdUnion = new Set([ + ...hostIncrementProxyActionIdList, + ...hostEchoProxyActionIdList, + ...hostNestedProxyActionIdList, + ...hostDefaultProxyActionIdList, + ]); + expect(familyProxyActionIdUnion.size).toBe(hostProxyActionIdList.length); + expect( + hostProxyActionIdList.every(actionId => + familyProxyActionIdUnion.has(actionId), + ), + ).toBe(true); const groupedProxyActionIdUnion = new Set([ ...hostDirectProxyActionIdList, ...hostBundledProxyActionIdList, @@ -788,6 +800,44 @@ function runTests({ mode }: TestConfig) { ...nestedProxyActionIdSet, ...defaultProxyActionIdSet, ]); + const getSetIntersectionSize = (left: Set, right: Set) => + [...left].filter(id => right.has(id)).length; + expect( + getSetIntersectionSize(incrementProxyActionIdSet, echoProxyActionIdSet), + ).toBe(0); + expect( + getSetIntersectionSize( + incrementProxyActionIdSet, + nestedProxyActionIdSet, + ), + ).toBe(0); + expect( + getSetIntersectionSize( + incrementProxyActionIdSet, + defaultProxyActionIdSet, + ), + ).toBe(0); + expect( + getSetIntersectionSize(echoProxyActionIdSet, nestedProxyActionIdSet), + ).toBe(0); + expect( + getSetIntersectionSize(echoProxyActionIdSet, defaultProxyActionIdSet), + ).toBe(0); + expect( + getSetIntersectionSize(nestedProxyActionIdSet, defaultProxyActionIdSet), + ).toBe(0); + const incrementRequestCount = actionRequestIds.filter(id => + incrementProxyActionIdSet.has(id), + ).length; + const echoRequestCount = actionRequestIds.filter(id => + echoProxyActionIdSet.has(id), + ).length; + const nestedRequestCount = actionRequestIds.filter(id => + nestedProxyActionIdSet.has(id), + ).length; + const defaultRequestCount = actionRequestIds.filter(id => + defaultProxyActionIdSet.has(id), + ).length; expect(actionFamilyProxyActionIdSet.size).toBe(hostProxyActionIdSet.size); expect(uniqueActionRequestIds.size).toBe(hostProxyMapKeyCount); expect(usesDirectProxyIds || usesBundledProxyIds).toBe(true); @@ -806,6 +856,16 @@ function runTests({ mode }: TestConfig) { expect(actionRequestIds.some(id => defaultProxyActionIdSet.has(id))).toBe( true, ); + expect(incrementRequestCount).toBeGreaterThanOrEqual(6); + expect(echoRequestCount).toBeGreaterThanOrEqual(6); + expect(nestedRequestCount).toBeGreaterThanOrEqual(6); + expect(defaultRequestCount).toBeGreaterThanOrEqual(6); + expect( + incrementRequestCount + + echoRequestCount + + nestedRequestCount + + defaultRequestCount, + ).toBe(actionRequestIds.length); expect( actionRequestIds.every(id => actionFamilyProxyActionIdSet.has(id)), ).toBe(true); From b4705d97ccd8aa05b7016566f4e6d00690dda156 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 01:59:01 +0000 Subject: [PATCH 113/324] test(rsc-mf): enforce exact action post cardinality profile --- tests/integration/rsc-mf/tests/index.test.ts | 24 ++++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 2aa2692ff290..e3b1f2831f77 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -16,7 +16,9 @@ const fixtureDir = path.resolve(__dirname, '../'); const hostDir = path.resolve(fixtureDir, 'host'); const remoteDir = path.resolve(fixtureDir, 'remote'); const HOST_RSC_URL = '/server-component-root'; -const MIN_EXPECTED_ACTION_POSTS_PER_MODE = 22; +const EXPECTED_ACTION_POSTS_PER_MODE = 24; +const EXPECTED_ACTION_POSTS_PER_FAMILY = 6; +const EXPECTED_UNIQUE_ACTION_IDS_PER_MODE = 4; type Mode = 'dev' | 'build'; @@ -653,9 +655,7 @@ function runTests({ mode }: TestConfig) { })); it('should route remote actions through host endpoint', () => { - expect(actionRequestUrls.length).toBeGreaterThanOrEqual( - MIN_EXPECTED_ACTION_POSTS_PER_MODE, - ); + expect(actionRequestUrls.length).toBe(EXPECTED_ACTION_POSTS_PER_MODE); expect(actionRequestUrls.length).toBe(actionRequestIds.length); expect( actionRequestUrls.every(url => @@ -670,9 +670,7 @@ function runTests({ mode }: TestConfig) { }); it('should post host-resolvable action ids for remote actions', async () => { - expect(actionRequestIds.length).toBeGreaterThanOrEqual( - MIN_EXPECTED_ACTION_POSTS_PER_MODE, - ); + expect(actionRequestIds.length).toBe(EXPECTED_ACTION_POSTS_PER_MODE); expect(actionRequestIds.length).toBe(actionRequestUrls.length); const uniqueActionRequestIds = new Set(actionRequestIds); expect(actionRequestIds.every(id => !id.startsWith('remote:'))).toBe( @@ -681,7 +679,9 @@ function runTests({ mode }: TestConfig) { expect(actionRequestIds.every(id => /^[a-f0-9]{64,}$/i.test(id))).toBe( true, ); - expect(uniqueActionRequestIds.size).toBeGreaterThanOrEqual(4); + expect(uniqueActionRequestIds.size).toBe( + EXPECTED_UNIQUE_ACTION_IDS_PER_MODE, + ); const hostProxyActionIdSet = new Set( (await page.$eval('.host-proxy-action-ids', el => el.textContent || '')) .split(',') @@ -856,10 +856,10 @@ function runTests({ mode }: TestConfig) { expect(actionRequestIds.some(id => defaultProxyActionIdSet.has(id))).toBe( true, ); - expect(incrementRequestCount).toBeGreaterThanOrEqual(6); - expect(echoRequestCount).toBeGreaterThanOrEqual(6); - expect(nestedRequestCount).toBeGreaterThanOrEqual(6); - expect(defaultRequestCount).toBeGreaterThanOrEqual(6); + expect(incrementRequestCount).toBe(EXPECTED_ACTION_POSTS_PER_FAMILY); + expect(echoRequestCount).toBe(EXPECTED_ACTION_POSTS_PER_FAMILY); + expect(nestedRequestCount).toBe(EXPECTED_ACTION_POSTS_PER_FAMILY); + expect(defaultRequestCount).toBe(EXPECTED_ACTION_POSTS_PER_FAMILY); expect( incrementRequestCount + echoRequestCount + From 542a4adc3f50e82b8149544d7283eb5003ebb078 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 02:02:01 +0000 Subject: [PATCH 114/324] test(rsc-mf): enforce exact action post cardinality profile --- tests/integration/rsc-mf/tests/index.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index e3b1f2831f77..b69faa90c640 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -838,8 +838,21 @@ function runTests({ mode }: TestConfig) { const defaultRequestCount = actionRequestIds.filter(id => defaultProxyActionIdSet.has(id), ).length; + const actionRequestCountById = new Map(); + for (const actionId of actionRequestIds) { + actionRequestCountById.set( + actionId, + (actionRequestCountById.get(actionId) || 0) + 1, + ); + } expect(actionFamilyProxyActionIdSet.size).toBe(hostProxyActionIdSet.size); expect(uniqueActionRequestIds.size).toBe(hostProxyMapKeyCount); + expect(actionRequestCountById.size).toBe(uniqueActionRequestIds.size); + expect( + [...actionRequestCountById.values()].every( + count => count === EXPECTED_ACTION_POSTS_PER_FAMILY, + ), + ).toBe(true); expect(usesDirectProxyIds || usesBundledProxyIds).toBe(true); if (!usesDirectProxyIds || !usesBundledProxyIds) { expect(hostProxyMapCollisionCount).toBeGreaterThan(0); From be0ceaabbee6e273833409ce1e84dba9b26851a6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 02:07:29 +0000 Subject: [PATCH 115/324] test(rsc-mf): assert uniform per-id action request frequency --- tests/integration/rsc-mf/tests/index.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index b69faa90c640..31cc14de4006 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -657,6 +657,10 @@ function runTests({ mode }: TestConfig) { it('should route remote actions through host endpoint', () => { expect(actionRequestUrls.length).toBe(EXPECTED_ACTION_POSTS_PER_MODE); expect(actionRequestUrls.length).toBe(actionRequestIds.length); + const uniqueActionRequestUrls = Array.from(new Set(actionRequestUrls)); + expect(uniqueActionRequestUrls).toEqual([ + `http://127.0.0.1:${hostPort}${HOST_RSC_URL}`, + ]); expect( actionRequestUrls.every(url => url.startsWith(`http://127.0.0.1:${hostPort}${HOST_RSC_URL}`), From 549ba4e241300ed7db4ff05899248e39bb9ba8b1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 02:18:29 +0000 Subject: [PATCH 116/324] refactor(rsc-mf): remove host proxy action glue and use bridge ids --- .../host/src/server-component-root/App.tsx | 189 +------- .../HostRemoteActionRunner.tsx | 13 +- .../remoteActionProxy.ts | 47 -- .../src/components/registerServerCallback.ts | 15 +- tests/integration/rsc-mf/tests/index.test.ts | 437 +----------------- 5 files changed, 10 insertions(+), 691 deletions(-) delete mode 100644 tests/integration/rsc-mf/host/src/server-component-root/remoteActionProxy.ts diff --git a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx index 9a5e71fb3415..d50d7620b882 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/App.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/App.tsx @@ -2,156 +2,14 @@ import 'server-only'; import { Suspense } from 'react'; import { AsyncRemoteServerInfo } from 'rscRemote/AsyncRemoteServerInfo'; import RemoteServerDefault from 'rscRemote/RemoteServerDefault'; -import * as remoteActionBundle from 'rscRemote/actionBundle'; -import { incrementRemoteCount, remoteActionEcho } from 'rscRemote/actions'; -import { defaultRemoteAction } from 'rscRemote/defaultAction'; import * as remoteInfoBundle from 'rscRemote/infoBundle'; -import { nestedRemoteAction } from 'rscRemote/nestedActions'; import remoteMeta, { getRemoteMetaLabel } from 'rscRemote/remoteMeta'; import { getServerOnlyInfo } from 'rscRemote/remoteServerOnly'; import getServerOnlyDefaultInfo from 'rscRemote/remoteServerOnlyDefault'; import styles from './App.module.less'; import HostRemoteActionRunner from './HostRemoteActionRunner'; -import { - proxyBundledDefaultRemoteAction, - proxyBundledIncrementRemoteCount, - proxyBundledNestedRemoteAction, - proxyBundledRemoteActionEcho, - proxyDefaultRemoteAction, - proxyIncrementRemoteCount, - proxyNestedRemoteAction, - proxyRemoteActionEcho, -} from './remoteActionProxy'; - -const getServerActionId = (action: unknown) => - (action as { $$id?: string } | undefined)?.$$id; const App = () => { - const directHostProxyActions = [ - proxyIncrementRemoteCount, - proxyRemoteActionEcho, - proxyNestedRemoteAction, - proxyDefaultRemoteAction, - ] as const; - const bundledHostProxyActions = [ - proxyBundledIncrementRemoteCount, - proxyBundledRemoteActionEcho, - proxyBundledNestedRemoteAction, - proxyBundledDefaultRemoteAction, - ] as const; - const hostProxyActions = [ - ...directHostProxyActions, - ...bundledHostProxyActions, - ] as const; - const directHostProxyActionIds = directHostProxyActions - .map(action => getServerActionId(action)) - .filter((actionId): actionId is string => Boolean(actionId)); - const bundledHostProxyActionIds = bundledHostProxyActions - .map(action => getServerActionId(action)) - .filter((actionId): actionId is string => Boolean(actionId)); - const hostProxyActionIds = hostProxyActions - .map(action => getServerActionId(action)) - .filter((actionId): actionId is string => Boolean(actionId)); - const uniqueHostProxyActionIds = Array.from( - new Set(hostProxyActionIds), - ).sort(); - const uniqueHostProxyActionIdsCount = uniqueHostProxyActionIds.length; - const incrementProxyActionIds = Array.from( - new Set( - [proxyIncrementRemoteCount, proxyBundledIncrementRemoteCount] - .map(action => getServerActionId(action)) - .filter((actionId): actionId is string => Boolean(actionId)), - ), - ).sort(); - const echoProxyActionIds = Array.from( - new Set( - [proxyRemoteActionEcho, proxyBundledRemoteActionEcho] - .map(action => getServerActionId(action)) - .filter((actionId): actionId is string => Boolean(actionId)), - ), - ).sort(); - const nestedProxyActionIds = Array.from( - new Set( - [proxyNestedRemoteAction, proxyBundledNestedRemoteAction] - .map(action => getServerActionId(action)) - .filter((actionId): actionId is string => Boolean(actionId)), - ), - ).sort(); - const defaultProxyActionIds = Array.from( - new Set( - [proxyDefaultRemoteAction, proxyBundledDefaultRemoteAction] - .map(action => getServerActionId(action)) - .filter((actionId): actionId is string => Boolean(actionId)), - ), - ).sort(); - - // Map remote action IDs to host-local proxy action IDs so client-side - // callbacks can always post a host-resolvable action id. This keeps - // remote action execution in-process on the host via proxy imports. - const remoteActionToHostProxyActionPairs = [ - [incrementRemoteCount, proxyIncrementRemoteCount], - [remoteActionEcho, proxyRemoteActionEcho], - [nestedRemoteAction, proxyNestedRemoteAction], - [defaultRemoteAction, proxyDefaultRemoteAction], - [ - remoteActionBundle.bundledIncrementRemoteCount, - proxyBundledIncrementRemoteCount, - ], - [remoteActionBundle.bundledRemoteActionEcho, proxyBundledRemoteActionEcho], - [ - remoteActionBundle.bundledNestedRemoteAction, - proxyBundledNestedRemoteAction, - ], - [ - remoteActionBundle.bundledDefaultRemoteAction, - proxyBundledDefaultRemoteAction, - ], - ] as const; - const remoteActionIdToHostProxyActionEntries = - remoteActionToHostProxyActionPairs - .map(([remoteAction, hostProxyAction]) => [ - getServerActionId(remoteAction), - getServerActionId(hostProxyAction), - ]) - .filter((pair): pair is [string, string] => Boolean(pair[0] && pair[1])); - const hostProxyActionDebugKeys = hostProxyActions.map( - (action, index) => - [action, getServerActionId(action) ?? `proxy-action-${index}`] as const, - ); - const hostProxyManifestForms = hostProxyActionDebugKeys.map( - ([action, actionId]) => , - ); - const remoteActionIdToHostProxyActionId = Object.fromEntries( - remoteActionIdToHostProxyActionEntries, - ); - const remoteActionIdMapKeyCount = Object.keys( - remoteActionIdToHostProxyActionId, - ).length; - const remoteActionIdMapKey = JSON.stringify( - Object.entries(remoteActionIdToHostProxyActionId).sort(([left], [right]) => - left.localeCompare(right), - ), - ); - const remoteActionIdMapEntryCount = - remoteActionIdToHostProxyActionEntries.length; - const remoteActionIdMapCollisionCount = Math.max( - remoteActionIdMapEntryCount - remoteActionIdMapKeyCount, - 0, - ); - const mappedHostProxyActionIds = Array.from( - new Set(Object.values(remoteActionIdToHostProxyActionId)), - ).sort(); - const doesMappingCoverAllHostProxyActions = - mappedHostProxyActionIds.length <= uniqueHostProxyActionIds.length && - mappedHostProxyActionIds.every(actionId => - uniqueHostProxyActionIds.includes(actionId), - ); - const doesMappingExactlyMatchAllHostProxyActions = - mappedHostProxyActionIds.length === uniqueHostProxyActionIds.length && - mappedHostProxyActionIds.every( - (actionId, index) => actionId === uniqueHostProxyActionIds[index], - ); - const remoteServerOnlyInfo = getServerOnlyInfo(); const remoteServerOnlyDefaultInfo = getServerOnlyDefaultInfo(); const remoteMetaLabel = getRemoteMetaLabel(); @@ -177,46 +35,6 @@ const App = () => { {remoteInfoBundle.bundledRemoteMeta.kind}

{bundledRemoteMetaLabel}

-

- {uniqueHostProxyActionIdsCount} -

-

- {remoteActionIdMapEntryCount} -

-

{remoteActionIdMapKeyCount}

-

- {remoteActionIdMapCollisionCount} -

-

- {mappedHostProxyActionIds.join(',')} -

-

- {String(doesMappingCoverAllHostProxyActions)} -

-

- {String(doesMappingExactlyMatchAllHostProxyActions)} -

-

- {uniqueHostProxyActionIds.join(',')} -

-

- {directHostProxyActionIds.join(',')} -

-

- {bundledHostProxyActionIds.join(',')} -

-

- {incrementProxyActionIds.join(',')} -

-

- {echoProxyActionIds.join(',')} -

-

- {nestedProxyActionIds.join(',')} -

-

- {defaultProxyActionIds.join(',')} -

Loading Remote Async Server Info...}> @@ -224,12 +42,7 @@ const App = () => { Loading Remote RSC...}> - {/* Anchor host proxy actions in the server action manifest. */} - - + ); }; diff --git a/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx b/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx index d34f161ce05e..f80f09467491 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx @@ -9,13 +9,7 @@ import { defaultRemoteAction } from 'rscRemote/defaultAction'; import { nestedRemoteAction } from 'rscRemote/nestedActions'; import { registerRemoteServerCallback } from 'rscRemote/registerServerCallback'; -export default function HostRemoteActionRunner({ - remoteActionIdMapKey, - remoteActionIdToHostProxyActionId, -}: { - remoteActionIdMapKey: string; - remoteActionIdToHostProxyActionId: Record; -}) { +export default function HostRemoteActionRunner() { // Keep this import in the client graph so federated RSC bridge IDs // can map back to a concrete remote module factory at runtime. void RemoteClientCounterBridge; @@ -30,14 +24,11 @@ export default function HostRemoteActionRunner({ const [isPending, setIsPending] = useState(false); useEffect(() => { - // Register once with host endpoint + id mapping so remote client-side - // server actions are routed through host proxy actions. registerRemoteServerCallback( `${window.location.origin}/server-component-root`, 'rscRemote', - remoteActionIdToHostProxyActionId, ); - }, [remoteActionIdMapKey]); + }, []); const runActions = async () => { setIsPending(true); diff --git a/tests/integration/rsc-mf/host/src/server-component-root/remoteActionProxy.ts b/tests/integration/rsc-mf/host/src/server-component-root/remoteActionProxy.ts deleted file mode 100644 index fb6319bbefab..000000000000 --- a/tests/integration/rsc-mf/host/src/server-component-root/remoteActionProxy.ts +++ /dev/null @@ -1,47 +0,0 @@ -'use server'; - -export async function proxyIncrementRemoteCount( - previousState: number, - formData: FormData, -) { - const remote = await import('rscRemote/actions'); - return remote.incrementRemoteCount(previousState, formData); -} - -export async function proxyRemoteActionEcho(value: string) { - const remote = await import('rscRemote/actions'); - return remote.remoteActionEcho(value); -} - -export async function proxyNestedRemoteAction(value: string) { - const remote = await import('rscRemote/nestedActions'); - return remote.nestedRemoteAction(value); -} - -export async function proxyDefaultRemoteAction(value: string) { - const remote = await import('rscRemote/defaultAction'); - return remote.defaultRemoteAction(value); -} - -export async function proxyBundledIncrementRemoteCount( - previousState: number, - formData: FormData, -) { - const remote = await import('rscRemote/actionBundle'); - return remote.bundledIncrementRemoteCount(previousState, formData); -} - -export async function proxyBundledRemoteActionEcho(value: string) { - const remote = await import('rscRemote/actionBundle'); - return remote.bundledRemoteActionEcho(value); -} - -export async function proxyBundledNestedRemoteAction(value: string) { - const remote = await import('rscRemote/actionBundle'); - return remote.bundledNestedRemoteAction(value); -} - -export async function proxyBundledDefaultRemoteAction(value: string) { - const remote = await import('rscRemote/actionBundle'); - return remote.bundledDefaultRemoteAction(value); -} diff --git a/tests/integration/rsc-mf/remote/src/components/registerServerCallback.ts b/tests/integration/rsc-mf/remote/src/components/registerServerCallback.ts index 1ca1b6e65cb1..39a7efea8681 100644 --- a/tests/integration/rsc-mf/remote/src/components/registerServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/components/registerServerCallback.ts @@ -6,13 +6,6 @@ import { } from 'rsc-mf-react-server-dom-client-browser'; let registeredCallbackKey = ''; -const getStableProxyActionIdEntries = ( - remoteActionIdToHostProxyActionId?: Record, -) => - Object.entries(remoteActionIdToHostProxyActionId ?? {}).sort( - ([left], [right]) => left.localeCompare(right), - ); - const getHostActionId = (rawActionId: string, remoteAlias: string) => { if (rawActionId.startsWith('remote:')) { return rawActionId; @@ -24,7 +17,6 @@ const getHostActionId = (rawActionId: string, remoteAlias: string) => { export function registerRemoteServerCallback( remoteOrigin: string, remoteAlias = 'rscRemote', - remoteActionIdToHostProxyActionId?: Record, ) { if (!remoteOrigin) { return; @@ -32,9 +24,6 @@ export function registerRemoteServerCallback( const callbackKey = JSON.stringify({ remoteAlias, remoteOrigin, - remoteActionIdToHostProxyActionId: getStableProxyActionIdEntries( - remoteActionIdToHostProxyActionId, - ), }); if (registeredCallbackKey === callbackKey) { return; @@ -42,9 +31,7 @@ export function registerRemoteServerCallback( const remoteActionUrl = new URL(remoteOrigin).toString(); setServerCallback(async (id, args) => { - const hostActionId = - remoteActionIdToHostProxyActionId?.[id] || - getHostActionId(id, remoteAlias); + const hostActionId = getHostActionId(id, remoteAlias); const temporaryReferences = createTemporaryReferenceSet(); const response = fetch(remoteActionUrl, { method: 'POST', diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 31cc14de4006..7c990950f960 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -90,20 +90,6 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { expect(html).toContain('Remote Default Server Card'); expect(html).toContain('host-remote-bundled-server-only'); expect(html).toContain('host-remote-bundled-meta-kind'); - expect(html).toContain('host-proxy-action-id-count'); - expect(html).toContain('host-proxy-map-entry-count'); - expect(html).toContain('host-proxy-map-key-count'); - expect(html).toContain('host-proxy-map-collision-count'); - expect(html).toContain('host-mapped-proxy-action-ids'); - expect(html).toContain('host-proxy-map-covers-all'); - expect(html).toContain('host-proxy-map-equals-all'); - expect(html).toContain('host-proxy-action-ids'); - expect(html).toContain('host-direct-proxy-action-ids'); - expect(html).toContain('host-bundled-proxy-action-ids'); - expect(html).toContain('host-increment-proxy-action-ids'); - expect(html).toContain('host-echo-proxy-action-ids'); - expect(html).toContain('host-nested-proxy-action-ids'); - expect(html).toContain('host-default-proxy-action-ids'); await page.goto(`http://127.0.0.1:${hostPort}${HOST_RSC_URL}`, { waitUntil: ['networkidle0', 'domcontentloaded'], @@ -148,223 +134,6 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { el => el.textContent?.trim(), ); expect(hostRemoteBundledMetaLabel).toBe('rsc|mf|actions'); - const hostProxyActionIdCount = await page.$eval( - '.host-proxy-action-id-count', - el => el.textContent?.trim(), - ); - expect(hostProxyActionIdCount).toBe('8'); - const hostProxyMapEntryCount = await page.$eval( - '.host-proxy-map-entry-count', - el => el.textContent?.trim(), - ); - expect(hostProxyMapEntryCount).toBe('8'); - const hostProxyMapKeyCount = await page.$eval( - '.host-proxy-map-key-count', - el => el.textContent?.trim(), - ); - expect(Number(hostProxyMapKeyCount)).toBeGreaterThan(0); - expect(Number(hostProxyMapKeyCount)).toBeLessThanOrEqual( - Number(hostProxyMapEntryCount), - ); - const hostProxyMapCollisionCount = await page.$eval( - '.host-proxy-map-collision-count', - el => el.textContent?.trim(), - ); - expect(Number(hostProxyMapCollisionCount)).toBeGreaterThanOrEqual(0); - expect(Number(hostProxyMapCollisionCount)).toBe( - Number(hostProxyMapEntryCount) - Number(hostProxyMapKeyCount), - ); - const hostMappedProxyActionIds = await page.$eval( - '.host-mapped-proxy-action-ids', - el => el.textContent?.trim(), - ); - const hostMappedProxyActionIdList = hostMappedProxyActionIds - ?.split(',') - .filter(Boolean) as string[]; - expect(hostMappedProxyActionIdList.length).toBe(Number(hostProxyMapKeyCount)); - const hostProxyMapCoversAll = await page.$eval( - '.host-proxy-map-covers-all', - el => el.textContent?.trim(), - ); - expect(hostProxyMapCoversAll).toBe('true'); - const hostProxyMapEqualsAll = await page.$eval( - '.host-proxy-map-equals-all', - el => el.textContent?.trim(), - ); - expect(hostProxyMapEqualsAll).toBe( - Number(hostProxyMapKeyCount) === Number(hostProxyMapEntryCount) - ? 'true' - : 'false', - ); - const hostProxyActionIds = await page.$eval('.host-proxy-action-ids', el => - el.textContent?.trim(), - ); - const hostProxyActionIdList = hostProxyActionIds - ?.split(',') - .filter(Boolean) as string[]; - expect(hostProxyActionIdList.length).toBe(8); - expect(hostProxyActionIdList.length).toBe(Number(hostProxyMapEntryCount)); - expect(new Set(hostProxyActionIdList).size).toBe(8); - expect(hostProxyActionIdList.every(id => /^[a-f0-9]{64,}$/i.test(id))).toBe( - true, - ); - const sortedHostProxyActionIds = [...hostProxyActionIdList].sort(); - const sortedMappedProxyActionIds = [...hostMappedProxyActionIdList].sort(); - expect( - sortedMappedProxyActionIds.every(mappedId => - sortedHostProxyActionIds.includes(mappedId), - ), - ).toBe(true); - const hostDirectProxyActionIds = await page.$eval( - '.host-direct-proxy-action-ids', - el => el.textContent?.trim(), - ); - const hostDirectProxyActionIdList = hostDirectProxyActionIds - ?.split(',') - .filter(Boolean) as string[]; - expect(hostDirectProxyActionIdList.length).toBe(4); - const hostBundledProxyActionIds = await page.$eval( - '.host-bundled-proxy-action-ids', - el => el.textContent?.trim(), - ); - const hostBundledProxyActionIdList = hostBundledProxyActionIds - ?.split(',') - .filter(Boolean) as string[]; - expect(hostBundledProxyActionIdList.length).toBe(4); - const hostIncrementProxyActionIds = await page.$eval( - '.host-increment-proxy-action-ids', - el => el.textContent?.trim(), - ); - const hostIncrementProxyActionIdList = hostIncrementProxyActionIds - ?.split(',') - .filter(Boolean) as string[]; - expect(hostIncrementProxyActionIdList.length).toBe(2); - expect( - hostIncrementProxyActionIdList.every(id => - hostProxyActionIdList.includes(id), - ), - ).toBe(true); - expect( - hostIncrementProxyActionIdList.every(id => /^[a-f0-9]{64,}$/i.test(id)), - ).toBe(true); - const hostEchoProxyActionIds = await page.$eval( - '.host-echo-proxy-action-ids', - el => el.textContent?.trim(), - ); - const hostEchoProxyActionIdList = hostEchoProxyActionIds - ?.split(',') - .filter(Boolean) as string[]; - expect(hostEchoProxyActionIdList.length).toBe(2); - expect( - hostEchoProxyActionIdList.every(id => hostProxyActionIdList.includes(id)), - ).toBe(true); - expect( - hostEchoProxyActionIdList.every(id => /^[a-f0-9]{64,}$/i.test(id)), - ).toBe(true); - const hostNestedProxyActionIds = await page.$eval( - '.host-nested-proxy-action-ids', - el => el.textContent?.trim(), - ); - const hostNestedProxyActionIdList = hostNestedProxyActionIds - ?.split(',') - .filter(Boolean) as string[]; - expect(hostNestedProxyActionIdList.length).toBe(2); - expect( - hostNestedProxyActionIdList.every(id => hostProxyActionIdList.includes(id)), - ).toBe(true); - expect( - hostNestedProxyActionIdList.every(id => /^[a-f0-9]{64,}$/i.test(id)), - ).toBe(true); - const hostDefaultProxyActionIds = await page.$eval( - '.host-default-proxy-action-ids', - el => el.textContent?.trim(), - ); - const hostDefaultProxyActionIdList = hostDefaultProxyActionIds - ?.split(',') - .filter(Boolean) as string[]; - expect(hostDefaultProxyActionIdList.length).toBe(2); - expect( - hostDefaultProxyActionIdList.every(id => - hostProxyActionIdList.includes(id), - ), - ).toBe(true); - expect( - hostDefaultProxyActionIdList.every(id => /^[a-f0-9]{64,}$/i.test(id)), - ).toBe(true); - const getGroupMembershipCount = ( - actionIds: string[], - groupIds: string[], - ): number => actionIds.filter(id => groupIds.includes(id)).length; - expect( - getGroupMembershipCount( - hostIncrementProxyActionIdList, - hostDirectProxyActionIdList, - ), - ).toBe(1); - expect( - getGroupMembershipCount( - hostIncrementProxyActionIdList, - hostBundledProxyActionIdList, - ), - ).toBe(1); - expect( - getGroupMembershipCount( - hostEchoProxyActionIdList, - hostDirectProxyActionIdList, - ), - ).toBe(1); - expect( - getGroupMembershipCount( - hostEchoProxyActionIdList, - hostBundledProxyActionIdList, - ), - ).toBe(1); - expect( - getGroupMembershipCount( - hostNestedProxyActionIdList, - hostDirectProxyActionIdList, - ), - ).toBe(1); - expect( - getGroupMembershipCount( - hostNestedProxyActionIdList, - hostBundledProxyActionIdList, - ), - ).toBe(1); - expect( - getGroupMembershipCount( - hostDefaultProxyActionIdList, - hostDirectProxyActionIdList, - ), - ).toBe(1); - expect( - getGroupMembershipCount( - hostDefaultProxyActionIdList, - hostBundledProxyActionIdList, - ), - ).toBe(1); - const familyProxyActionIdUnion = new Set([ - ...hostIncrementProxyActionIdList, - ...hostEchoProxyActionIdList, - ...hostNestedProxyActionIdList, - ...hostDefaultProxyActionIdList, - ]); - expect(familyProxyActionIdUnion.size).toBe(hostProxyActionIdList.length); - expect( - hostProxyActionIdList.every(actionId => - familyProxyActionIdUnion.has(actionId), - ), - ).toBe(true); - const groupedProxyActionIdUnion = new Set([ - ...hostDirectProxyActionIdList, - ...hostBundledProxyActionIdList, - ]); - expect(groupedProxyActionIdUnion.size).toBe(hostProxyActionIdList.length); - expect( - hostProxyActionIdList.every(actionId => - groupedProxyActionIdUnion.has(actionId), - ), - ).toBe(true); const hostRemoteAsyncServerInfo = await page.$eval( '.remote-async-server-info', el => el.textContent?.trim(), @@ -673,175 +442,18 @@ function runTests({ mode }: TestConfig) { ).toBe(true); }); - it('should post host-resolvable action ids for remote actions', async () => { + it('should post bridge-prefixed action ids for remote actions', async () => { expect(actionRequestIds.length).toBe(EXPECTED_ACTION_POSTS_PER_MODE); expect(actionRequestIds.length).toBe(actionRequestUrls.length); const uniqueActionRequestIds = new Set(actionRequestIds); - expect(actionRequestIds.every(id => !id.startsWith('remote:'))).toBe( - true, - ); - expect(actionRequestIds.every(id => /^[a-f0-9]{64,}$/i.test(id))).toBe( - true, - ); + expect( + actionRequestIds.every(id => + /^remote:rscRemote:[a-f0-9]{64,}$/i.test(id), + ), + ).toBe(true); expect(uniqueActionRequestIds.size).toBe( EXPECTED_UNIQUE_ACTION_IDS_PER_MODE, ); - const hostProxyActionIdSet = new Set( - (await page.$eval('.host-proxy-action-ids', el => el.textContent || '')) - .split(',') - .filter(Boolean), - ); - expect(actionRequestIds.every(id => hostProxyActionIdSet.has(id))).toBe( - true, - ); - const mappedProxyActionIdSet = new Set( - ( - await page.$eval( - '.host-mapped-proxy-action-ids', - el => el.textContent || '', - ) - ) - .split(',') - .filter(Boolean), - ); - const hostProxyMapKeyCount = Number( - await page.$eval( - '.host-proxy-map-key-count', - el => el.textContent || '0', - ), - ); - const hostProxyMapEntryCount = Number( - await page.$eval( - '.host-proxy-map-entry-count', - el => el.textContent || '0', - ), - ); - const hostProxyMapCollisionCount = Number( - await page.$eval( - '.host-proxy-map-collision-count', - el => el.textContent || '0', - ), - ); - expect(hostProxyMapKeyCount).toBe(mappedProxyActionIdSet.size); - expect(uniqueActionRequestIds.size).toBeLessThanOrEqual( - hostProxyMapKeyCount, - ); - expect(hostProxyMapCollisionCount).toBe( - hostProxyMapEntryCount - hostProxyMapKeyCount, - ); - expect(actionRequestIds.every(id => mappedProxyActionIdSet.has(id))).toBe( - true, - ); - const directProxyActionIdSet = new Set( - ( - await page.$eval( - '.host-direct-proxy-action-ids', - el => el.textContent || '', - ) - ) - .split(',') - .filter(Boolean), - ); - const bundledProxyActionIdSet = new Set( - ( - await page.$eval( - '.host-bundled-proxy-action-ids', - el => el.textContent || '', - ) - ) - .split(',') - .filter(Boolean), - ); - const usesDirectProxyIds = actionRequestIds.some(id => - directProxyActionIdSet.has(id), - ); - const usesBundledProxyIds = actionRequestIds.some(id => - bundledProxyActionIdSet.has(id), - ); - const incrementProxyActionIdSet = new Set( - ( - await page.$eval( - '.host-increment-proxy-action-ids', - el => el.textContent || '', - ) - ) - .split(',') - .filter(Boolean), - ); - const echoProxyActionIdSet = new Set( - ( - await page.$eval( - '.host-echo-proxy-action-ids', - el => el.textContent || '', - ) - ) - .split(',') - .filter(Boolean), - ); - const nestedProxyActionIdSet = new Set( - ( - await page.$eval( - '.host-nested-proxy-action-ids', - el => el.textContent || '', - ) - ) - .split(',') - .filter(Boolean), - ); - const defaultProxyActionIdSet = new Set( - ( - await page.$eval( - '.host-default-proxy-action-ids', - el => el.textContent || '', - ) - ) - .split(',') - .filter(Boolean), - ); - const actionFamilyProxyActionIdSet = new Set([ - ...incrementProxyActionIdSet, - ...echoProxyActionIdSet, - ...nestedProxyActionIdSet, - ...defaultProxyActionIdSet, - ]); - const getSetIntersectionSize = (left: Set, right: Set) => - [...left].filter(id => right.has(id)).length; - expect( - getSetIntersectionSize(incrementProxyActionIdSet, echoProxyActionIdSet), - ).toBe(0); - expect( - getSetIntersectionSize( - incrementProxyActionIdSet, - nestedProxyActionIdSet, - ), - ).toBe(0); - expect( - getSetIntersectionSize( - incrementProxyActionIdSet, - defaultProxyActionIdSet, - ), - ).toBe(0); - expect( - getSetIntersectionSize(echoProxyActionIdSet, nestedProxyActionIdSet), - ).toBe(0); - expect( - getSetIntersectionSize(echoProxyActionIdSet, defaultProxyActionIdSet), - ).toBe(0); - expect( - getSetIntersectionSize(nestedProxyActionIdSet, defaultProxyActionIdSet), - ).toBe(0); - const incrementRequestCount = actionRequestIds.filter(id => - incrementProxyActionIdSet.has(id), - ).length; - const echoRequestCount = actionRequestIds.filter(id => - echoProxyActionIdSet.has(id), - ).length; - const nestedRequestCount = actionRequestIds.filter(id => - nestedProxyActionIdSet.has(id), - ).length; - const defaultRequestCount = actionRequestIds.filter(id => - defaultProxyActionIdSet.has(id), - ).length; const actionRequestCountById = new Map(); for (const actionId of actionRequestIds) { actionRequestCountById.set( @@ -849,49 +461,12 @@ function runTests({ mode }: TestConfig) { (actionRequestCountById.get(actionId) || 0) + 1, ); } - expect(actionFamilyProxyActionIdSet.size).toBe(hostProxyActionIdSet.size); - expect(uniqueActionRequestIds.size).toBe(hostProxyMapKeyCount); expect(actionRequestCountById.size).toBe(uniqueActionRequestIds.size); expect( [...actionRequestCountById.values()].every( count => count === EXPECTED_ACTION_POSTS_PER_FAMILY, ), ).toBe(true); - expect(usesDirectProxyIds || usesBundledProxyIds).toBe(true); - if (!usesDirectProxyIds || !usesBundledProxyIds) { - expect(hostProxyMapCollisionCount).toBeGreaterThan(0); - } - expect( - actionRequestIds.some(id => incrementProxyActionIdSet.has(id)), - ).toBe(true); - expect(actionRequestIds.some(id => echoProxyActionIdSet.has(id))).toBe( - true, - ); - expect(actionRequestIds.some(id => nestedProxyActionIdSet.has(id))).toBe( - true, - ); - expect(actionRequestIds.some(id => defaultProxyActionIdSet.has(id))).toBe( - true, - ); - expect(incrementRequestCount).toBe(EXPECTED_ACTION_POSTS_PER_FAMILY); - expect(echoRequestCount).toBe(EXPECTED_ACTION_POSTS_PER_FAMILY); - expect(nestedRequestCount).toBe(EXPECTED_ACTION_POSTS_PER_FAMILY); - expect(defaultRequestCount).toBe(EXPECTED_ACTION_POSTS_PER_FAMILY); - expect( - incrementRequestCount + - echoRequestCount + - nestedRequestCount + - defaultRequestCount, - ).toBe(actionRequestIds.length); - expect( - actionRequestIds.every(id => actionFamilyProxyActionIdSet.has(id)), - ).toBe(true); - expect( - actionRequestIds.every( - id => - directProxyActionIdSet.has(id) || bundledProxyActionIdSet.has(id), - ), - ).toBe(true); }); it('should have no browser runtime errors', () => { From 301a7b40f71ad0911c1f703f6590d190b9c75c54 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 02:23:29 +0000 Subject: [PATCH 117/324] refactor(rsc-mf): auto-register remote callback in federated client --- .../server-component-root/HostRemoteActionRunner.tsx | 10 +--------- .../rsc-mf/remote/module-federation.config.ts | 4 ---- .../remote/src/components/RemoteClientCounter.tsx | 10 +++++++++- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx b/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx index f80f09467491..a3d6047f21df 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx @@ -1,13 +1,12 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import RemoteClientBadge from 'rscRemote/RemoteClientBadge'; import { RemoteClientCounter as RemoteClientCounterBridge } from 'rscRemote/RemoteClientCounter'; import * as remoteActionBundle from 'rscRemote/actionBundle'; import { incrementRemoteCount, remoteActionEcho } from 'rscRemote/actions'; import { defaultRemoteAction } from 'rscRemote/defaultAction'; import { nestedRemoteAction } from 'rscRemote/nestedActions'; -import { registerRemoteServerCallback } from 'rscRemote/registerServerCallback'; export default function HostRemoteActionRunner() { // Keep this import in the client graph so federated RSC bridge IDs @@ -23,13 +22,6 @@ export default function HostRemoteActionRunner() { const [bundledIncrementResult, setBundledIncrementResult] = useState(''); const [isPending, setIsPending] = useState(false); - useEffect(() => { - registerRemoteServerCallback( - `${window.location.origin}/server-component-root`, - 'rscRemote', - ); - }, []); - const runActions = async () => { setIsPending(true); try { diff --git a/tests/integration/rsc-mf/remote/module-federation.config.ts b/tests/integration/rsc-mf/remote/module-federation.config.ts index 8c2a687d83f6..45423e836355 100644 --- a/tests/integration/rsc-mf/remote/module-federation.config.ts +++ b/tests/integration/rsc-mf/remote/module-federation.config.ts @@ -163,10 +163,6 @@ export default createModuleFederationConfig({ import: './src/components/infoBundle.ts', layer: LAYERS.rsc, } as any, - './registerServerCallback': { - import: './src/components/registerServerCallback.ts', - layer: LAYERS.rsc, - } as any, }, shared: sharedByScope as any, dts: false, diff --git a/tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.tsx b/tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.tsx index 67483b2203b5..efd75f72924a 100644 --- a/tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.tsx +++ b/tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.tsx @@ -1,11 +1,19 @@ 'use client'; -import { useActionState, useState } from 'react'; +import { useActionState, useEffect, useState } from 'react'; import './RemoteClientCounter.css'; import { incrementRemoteCount, remoteActionEcho } from './actions'; import { defaultRemoteAction } from './defaultAction'; import { nestedRemoteAction } from './nestedActions'; +import { registerRemoteServerCallback } from './registerServerCallback'; export function RemoteClientCounter() { + useEffect(() => { + const actionPathname = window.location.pathname || '/'; + registerRemoteServerCallback( + `${window.location.origin}${actionPathname}`, + 'rscRemote', + ); + }, []); const [localCount, setLocalCount] = useState(0); const [serverCount, formAction, isPending] = useActionState( incrementRemoteCount, From 72e8c86108210173fbb94f9a38a8af0a44cd92da Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 02:27:15 +0000 Subject: [PATCH 118/324] test(rsc-mf): assert callback helper no longer needs expose --- tests/integration/rsc-mf/tests/index.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 7c990950f960..2c7020bc51c6 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -416,6 +416,20 @@ function runTests({ mode }: TestConfig) { it('should render remote RSC content in host app', () => renderRemoteRscIntoHost({ hostPort, page })); + it('should not require exposing callback registration helper', async () => { + const manifestResponse = await fetch( + `http://127.0.0.1:${remotePort}/static/mf-manifest.json`, + ); + expect(manifestResponse.ok).toBe(true); + const manifest = (await manifestResponse.json()) as { + exposes?: Array<{ path?: string }>; + }; + const exposedPaths = (manifest.exposes || []) + .map(item => item.path) + .filter((path): path is string => Boolean(path)); + expect(exposedPaths).not.toContain('./registerServerCallback'); + }); + it('should support remote use client and server actions', () => supportRemoteClientAndServerActions({ hostPort, From 13edc5b54f9f8055a8d6ffceec7afb89ab9cf6b3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 02:30:20 +0000 Subject: [PATCH 119/324] refactor(rsc-mf): initialize callback bridge via client side-effect module --- .../remote/src/components/RemoteClientBadge.tsx | 1 + .../remote/src/components/RemoteClientCounter.tsx | 11 ++--------- .../remote/src/components/initRemoteServerCallback.ts | 9 +++++++++ 3 files changed, 12 insertions(+), 9 deletions(-) create mode 100644 tests/integration/rsc-mf/remote/src/components/initRemoteServerCallback.ts diff --git a/tests/integration/rsc-mf/remote/src/components/RemoteClientBadge.tsx b/tests/integration/rsc-mf/remote/src/components/RemoteClientBadge.tsx index f46bcb0d5f33..bbcc58749fb7 100644 --- a/tests/integration/rsc-mf/remote/src/components/RemoteClientBadge.tsx +++ b/tests/integration/rsc-mf/remote/src/components/RemoteClientBadge.tsx @@ -1,6 +1,7 @@ 'use client'; import 'client-only'; +import './initRemoteServerCallback'; import { useState } from 'react'; export default function RemoteClientBadge({ diff --git a/tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.tsx b/tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.tsx index efd75f72924a..c9c982781eac 100644 --- a/tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.tsx +++ b/tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.tsx @@ -1,19 +1,12 @@ 'use client'; -import { useActionState, useEffect, useState } from 'react'; +import { useActionState, useState } from 'react'; import './RemoteClientCounter.css'; import { incrementRemoteCount, remoteActionEcho } from './actions'; import { defaultRemoteAction } from './defaultAction'; +import './initRemoteServerCallback'; import { nestedRemoteAction } from './nestedActions'; -import { registerRemoteServerCallback } from './registerServerCallback'; export function RemoteClientCounter() { - useEffect(() => { - const actionPathname = window.location.pathname || '/'; - registerRemoteServerCallback( - `${window.location.origin}${actionPathname}`, - 'rscRemote', - ); - }, []); const [localCount, setLocalCount] = useState(0); const [serverCount, formAction, isPending] = useActionState( incrementRemoteCount, diff --git a/tests/integration/rsc-mf/remote/src/components/initRemoteServerCallback.ts b/tests/integration/rsc-mf/remote/src/components/initRemoteServerCallback.ts new file mode 100644 index 000000000000..cf0f113dbacc --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/initRemoteServerCallback.ts @@ -0,0 +1,9 @@ +import { registerRemoteServerCallback } from './registerServerCallback'; + +if (typeof window !== 'undefined') { + const actionPathname = window.location.pathname || '/'; + registerRemoteServerCallback( + `${window.location.origin}${actionPathname}`, + 'rscRemote', + ); +} From 8621bd2ab60747a303ccaba5f0ee7ed4139e32e4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 02:35:27 +0000 Subject: [PATCH 120/324] test(rsc-mf): assert callback expose chunk is never requested --- tests/integration/rsc-mf/tests/index.test.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 2c7020bc51c6..eddbbd5a47f3 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -340,6 +340,7 @@ function runTests({ mode }: TestConfig) { const runtimeErrors: string[] = []; const actionRequestUrls: string[] = []; const actionRequestIds: string[] = []; + const registerCallbackExposeRequestUrls: string[] = []; if (skipForLowerNodeVersion()) { return; @@ -393,10 +394,14 @@ function runTests({ mode }: TestConfig) { page.on('request', request => { const headers = request.headers(); + const url = request.url(); + if (url.includes('__federation_expose_registerServerCallback')) { + registerCallbackExposeRequestUrls.push(url); + } if (request.method() !== 'POST' || !headers['x-rsc-action']) { return; } - actionRequestUrls.push(request.url()); + actionRequestUrls.push(url); actionRequestIds.push(headers['x-rsc-action']); }); }); @@ -430,6 +435,10 @@ function runTests({ mode }: TestConfig) { expect(exposedPaths).not.toContain('./registerServerCallback'); }); + it('should not load callback helper expose chunk', () => { + expect(registerCallbackExposeRequestUrls).toEqual([]); + }); + it('should support remote use client and server actions', () => supportRemoteClientAndServerActions({ hostPort, From daaadd3fbe3507d44b6288440b3e5f8715caceb4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 02:51:54 +0000 Subject: [PATCH 121/324] refactor(rsc-mf): centralize callback init in remote counter entry --- .../rsc-mf/remote/src/components/RemoteClientBadge.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/rsc-mf/remote/src/components/RemoteClientBadge.tsx b/tests/integration/rsc-mf/remote/src/components/RemoteClientBadge.tsx index bbcc58749fb7..f46bcb0d5f33 100644 --- a/tests/integration/rsc-mf/remote/src/components/RemoteClientBadge.tsx +++ b/tests/integration/rsc-mf/remote/src/components/RemoteClientBadge.tsx @@ -1,7 +1,6 @@ 'use client'; import 'client-only'; -import './initRemoteServerCallback'; import { useState } from 'react'; export default function RemoteClientBadge({ From 18664e8600ee0986e4620936998e450f28f0cb7b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 02:57:28 +0000 Subject: [PATCH 122/324] docs(rsc-mf): clarify bridge callback helper intent --- .../rsc-mf/remote/src/components/initRemoteServerCallback.ts | 2 ++ .../rsc-mf/remote/src/components/registerServerCallback.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/tests/integration/rsc-mf/remote/src/components/initRemoteServerCallback.ts b/tests/integration/rsc-mf/remote/src/components/initRemoteServerCallback.ts index cf0f113dbacc..7d6b5ff31fba 100644 --- a/tests/integration/rsc-mf/remote/src/components/initRemoteServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/components/initRemoteServerCallback.ts @@ -1,6 +1,8 @@ import { registerRemoteServerCallback } from './registerServerCallback'; if (typeof window !== 'undefined') { + // Fixture-level bootstrap: make federated client actions post back through + // host route using bridge-prefixed ids without requiring host userland wiring. const actionPathname = window.location.pathname || '/'; registerRemoteServerCallback( `${window.location.origin}${actionPathname}`, diff --git a/tests/integration/rsc-mf/remote/src/components/registerServerCallback.ts b/tests/integration/rsc-mf/remote/src/components/registerServerCallback.ts index 39a7efea8681..8230bc186531 100644 --- a/tests/integration/rsc-mf/remote/src/components/registerServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/components/registerServerCallback.ts @@ -11,6 +11,7 @@ const getHostActionId = (rawActionId: string, remoteAlias: string) => { return rawActionId; } + // Align with RSC bridge action-id format expected by host runtime plugin. return `remote:${remoteAlias}:${rawActionId}`; }; From d6464ff573f9488685ea13236c5a2c0f0d9ea867 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 03:01:03 +0000 Subject: [PATCH 123/324] refactor(rsc-mf): move callback bootstrap to runtime module --- .../rsc-mf/remote/src/components/RemoteClientCounter.tsx | 2 +- .../initServerCallback.ts} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename tests/integration/rsc-mf/remote/src/{components/initRemoteServerCallback.ts => runtime/initServerCallback.ts} (80%) diff --git a/tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.tsx b/tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.tsx index c9c982781eac..a2b83401551d 100644 --- a/tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.tsx +++ b/tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.tsx @@ -3,8 +3,8 @@ import { useActionState, useState } from 'react'; import './RemoteClientCounter.css'; import { incrementRemoteCount, remoteActionEcho } from './actions'; import { defaultRemoteAction } from './defaultAction'; -import './initRemoteServerCallback'; import { nestedRemoteAction } from './nestedActions'; +import '../runtime/initServerCallback'; export function RemoteClientCounter() { const [localCount, setLocalCount] = useState(0); diff --git a/tests/integration/rsc-mf/remote/src/components/initRemoteServerCallback.ts b/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts similarity index 80% rename from tests/integration/rsc-mf/remote/src/components/initRemoteServerCallback.ts rename to tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts index 7d6b5ff31fba..c4f4794439b3 100644 --- a/tests/integration/rsc-mf/remote/src/components/initRemoteServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts @@ -1,4 +1,4 @@ -import { registerRemoteServerCallback } from './registerServerCallback'; +import { registerRemoteServerCallback } from '../components/registerServerCallback'; if (typeof window !== 'undefined') { // Fixture-level bootstrap: make federated client actions post back through From 7f870fab17db7f3913b9a3a99e16d22107893560 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 03:05:35 +0000 Subject: [PATCH 124/324] refactor(rsc-mf): route exposed modules through runtime bootstrap wrappers --- .../rsc-mf/remote/module-federation.config.ts | 14 +++++++------- .../remote/src/components/RemoteClientCounter.tsx | 1 - .../src/runtime/exposes/RemoteClientBadge.tsx | 2 ++ .../src/runtime/exposes/RemoteClientCounter.tsx | 2 ++ .../remote/src/runtime/exposes/actionBundle.ts | 2 ++ .../rsc-mf/remote/src/runtime/exposes/actions.ts | 5 +++++ .../remote/src/runtime/exposes/defaultAction.ts | 2 ++ .../remote/src/runtime/exposes/nestedActions.ts | 2 ++ 8 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteClientBadge.tsx create mode 100644 tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteClientCounter.tsx create mode 100644 tests/integration/rsc-mf/remote/src/runtime/exposes/actionBundle.ts create mode 100644 tests/integration/rsc-mf/remote/src/runtime/exposes/actions.ts create mode 100644 tests/integration/rsc-mf/remote/src/runtime/exposes/defaultAction.ts create mode 100644 tests/integration/rsc-mf/remote/src/runtime/exposes/nestedActions.ts diff --git a/tests/integration/rsc-mf/remote/module-federation.config.ts b/tests/integration/rsc-mf/remote/module-federation.config.ts index 45423e836355..c16bdbacb477 100644 --- a/tests/integration/rsc-mf/remote/module-federation.config.ts +++ b/tests/integration/rsc-mf/remote/module-federation.config.ts @@ -108,15 +108,15 @@ export default createModuleFederationConfig({ filename: 'static/remoteEntry.js', exposes: { './RemoteClientCounter': { - import: './src/components/RemoteClientCounter.tsx', + import: './src/runtime/exposes/RemoteClientCounter.tsx', layer: LAYERS.rsc, } as any, './src/components/RemoteClientCounter.tsx': { - import: './src/components/RemoteClientCounter.tsx', + import: './src/runtime/exposes/RemoteClientCounter.tsx', layer: LAYERS.rsc, } as any, './RemoteClientBadge': { - import: './src/components/RemoteClientBadge.tsx', + import: './src/runtime/exposes/RemoteClientBadge.tsx', layer: LAYERS.rsc, } as any, './RemoteServerCard': { @@ -144,19 +144,19 @@ export default createModuleFederationConfig({ layer: LAYERS.rsc, } as any, './actions': { - import: './src/components/actions.ts', + import: './src/runtime/exposes/actions.ts', layer: LAYERS.rsc, } as any, './nestedActions': { - import: './src/components/nestedActions.ts', + import: './src/runtime/exposes/nestedActions.ts', layer: LAYERS.rsc, } as any, './defaultAction': { - import: './src/components/defaultAction.ts', + import: './src/runtime/exposes/defaultAction.ts', layer: LAYERS.rsc, } as any, './actionBundle': { - import: './src/components/actionBundle.ts', + import: './src/runtime/exposes/actionBundle.ts', layer: LAYERS.rsc, } as any, './infoBundle': { diff --git a/tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.tsx b/tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.tsx index a2b83401551d..67483b2203b5 100644 --- a/tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.tsx +++ b/tests/integration/rsc-mf/remote/src/components/RemoteClientCounter.tsx @@ -4,7 +4,6 @@ import './RemoteClientCounter.css'; import { incrementRemoteCount, remoteActionEcho } from './actions'; import { defaultRemoteAction } from './defaultAction'; import { nestedRemoteAction } from './nestedActions'; -import '../runtime/initServerCallback'; export function RemoteClientCounter() { const [localCount, setLocalCount] = useState(0); diff --git a/tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteClientBadge.tsx b/tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteClientBadge.tsx new file mode 100644 index 000000000000..cfcb31341f50 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteClientBadge.tsx @@ -0,0 +1,2 @@ +import '../initServerCallback'; +export { default } from '../../components/RemoteClientBadge'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteClientCounter.tsx b/tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteClientCounter.tsx new file mode 100644 index 000000000000..2588359d1a1e --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteClientCounter.tsx @@ -0,0 +1,2 @@ +import '../initServerCallback'; +export { RemoteClientCounter } from '../../components/RemoteClientCounter'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/exposes/actionBundle.ts b/tests/integration/rsc-mf/remote/src/runtime/exposes/actionBundle.ts new file mode 100644 index 000000000000..d168ead856f0 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/runtime/exposes/actionBundle.ts @@ -0,0 +1,2 @@ +import '../initServerCallback'; +export * from '../../components/actionBundle'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/exposes/actions.ts b/tests/integration/rsc-mf/remote/src/runtime/exposes/actions.ts new file mode 100644 index 000000000000..c2323499156d --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/runtime/exposes/actions.ts @@ -0,0 +1,5 @@ +import '../initServerCallback'; +export { + incrementRemoteCount, + remoteActionEcho, +} from '../../components/actions'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/exposes/defaultAction.ts b/tests/integration/rsc-mf/remote/src/runtime/exposes/defaultAction.ts new file mode 100644 index 000000000000..0cff7b09f27c --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/runtime/exposes/defaultAction.ts @@ -0,0 +1,2 @@ +import '../initServerCallback'; +export { defaultRemoteAction } from '../../components/defaultAction'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/exposes/nestedActions.ts b/tests/integration/rsc-mf/remote/src/runtime/exposes/nestedActions.ts new file mode 100644 index 000000000000..3ac8a55c06bd --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/runtime/exposes/nestedActions.ts @@ -0,0 +1,2 @@ +import '../initServerCallback'; +export { nestedRemoteAction } from '../../components/nestedActions'; From a86f3b569e993aa30e9a193b9e1df839a0df1a31 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 03:10:31 +0000 Subject: [PATCH 125/324] refactor(rsc-mf): move callback registration helper into runtime namespace --- .../integration/rsc-mf/remote/src/runtime/initServerCallback.ts | 2 +- .../src/{components => runtime}/registerServerCallback.ts | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename tests/integration/rsc-mf/remote/src/{components => runtime}/registerServerCallback.ts (100%) diff --git a/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts b/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts index c4f4794439b3..7d6b5ff31fba 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts @@ -1,4 +1,4 @@ -import { registerRemoteServerCallback } from '../components/registerServerCallback'; +import { registerRemoteServerCallback } from './registerServerCallback'; if (typeof window !== 'undefined') { // Fixture-level bootstrap: make federated client actions post back through diff --git a/tests/integration/rsc-mf/remote/src/components/registerServerCallback.ts b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts similarity index 100% rename from tests/integration/rsc-mf/remote/src/components/registerServerCallback.ts rename to tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts From 790f31518aea79808c5836450d94ba6fce0eb434 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 03:13:11 +0000 Subject: [PATCH 126/324] test(rsc-mf): assert callback wiring stays in runtime modules --- tests/integration/rsc-mf/tests/index.test.ts | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index eddbbd5a47f3..74addcd96097 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -1,3 +1,4 @@ +import fs from 'fs'; import path from 'path'; import { isVersionAtLeast18 } from '@modern-js/utils'; import type { Browser, Page } from 'puppeteer'; @@ -435,6 +436,30 @@ function runTests({ mode }: TestConfig) { expect(exposedPaths).not.toContain('./registerServerCallback'); }); + it('should keep callback runtime wiring out of component sources', () => { + const remoteClientCounterSource = fs.readFileSync( + path.join(remoteDir, 'src/components/RemoteClientCounter.tsx'), + 'utf-8', + ); + const remoteClientBadgeSource = fs.readFileSync( + path.join(remoteDir, 'src/components/RemoteClientBadge.tsx'), + 'utf-8', + ); + const runtimeInitSource = fs.readFileSync( + path.join(remoteDir, 'src/runtime/initServerCallback.ts'), + 'utf-8', + ); + expect(remoteClientCounterSource).not.toContain('initServerCallback'); + expect(remoteClientCounterSource).not.toContain( + 'registerRemoteServerCallback', + ); + expect(remoteClientBadgeSource).not.toContain('initServerCallback'); + expect(remoteClientBadgeSource).not.toContain( + 'registerRemoteServerCallback', + ); + expect(runtimeInitSource).toContain('registerRemoteServerCallback'); + }); + it('should not load callback helper expose chunk', () => { expect(registerCallbackExposeRequestUrls).toEqual([]); }); From f53e08588112b257018a2cdabb449e7fc1273244 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 03:16:26 +0000 Subject: [PATCH 127/324] test(rsc-mf): enforce runtime boundary across all component sources --- tests/integration/rsc-mf/tests/index.test.ts | 50 +++++++++++++++----- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 74addcd96097..6ba3187818bc 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -437,27 +437,51 @@ function runTests({ mode }: TestConfig) { }); it('should keep callback runtime wiring out of component sources', () => { - const remoteClientCounterSource = fs.readFileSync( - path.join(remoteDir, 'src/components/RemoteClientCounter.tsx'), - 'utf-8', + const getFilesRecursively = (directory: string): string[] => + fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + return getFilesRecursively(entryPath); + } + return [entryPath]; + }); + + const componentFilePaths = getFilesRecursively( + path.join(remoteDir, 'src/components'), ); - const remoteClientBadgeSource = fs.readFileSync( - path.join(remoteDir, 'src/components/RemoteClientBadge.tsx'), - 'utf-8', + const exposeRuntimeFilePaths = getFilesRecursively( + path.join(remoteDir, 'src/runtime/exposes'), + ); + + const componentSources = componentFilePaths.map(filePath => + fs.readFileSync(filePath, 'utf-8'), ); const runtimeInitSource = fs.readFileSync( path.join(remoteDir, 'src/runtime/initServerCallback.ts'), 'utf-8', ); - expect(remoteClientCounterSource).not.toContain('initServerCallback'); - expect(remoteClientCounterSource).not.toContain( - 'registerRemoteServerCallback', - ); - expect(remoteClientBadgeSource).not.toContain('initServerCallback'); - expect(remoteClientBadgeSource).not.toContain( - 'registerRemoteServerCallback', + const runtimeRegisterSource = fs.readFileSync( + path.join(remoteDir, 'src/runtime/registerServerCallback.ts'), + 'utf-8', ); + + expect( + componentSources.every( + source => !source.includes('initServerCallback'), + ), + ).toBe(true); + expect( + componentSources.every( + source => !source.includes('registerRemoteServerCallback'), + ), + ).toBe(true); + expect( + exposeRuntimeFilePaths.every(filePath => + fs.readFileSync(filePath, 'utf-8').includes('initServerCallback'), + ), + ).toBe(true); expect(runtimeInitSource).toContain('registerRemoteServerCallback'); + expect(runtimeRegisterSource).toContain('setServerCallback'); }); it('should not load callback helper expose chunk', () => { From 6b3054d643081a60d21db185da1e39b56b80de2d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 03:21:26 +0000 Subject: [PATCH 128/324] refactor(rsc-mf): funnel all remote exposes through runtime boundary --- .../rsc-mf/remote/module-federation.config.ts | 14 +++--- .../runtime/exposes/AsyncRemoteServerInfo.tsx | 1 + .../src/runtime/exposes/RemoteServerCard.tsx | 1 + .../runtime/exposes/RemoteServerDefault.tsx | 1 + .../remote/src/runtime/exposes/infoBundle.ts | 1 + .../remote/src/runtime/exposes/remoteMeta.ts | 1 + .../src/runtime/exposes/remoteServerOnly.ts | 1 + .../exposes/remoteServerOnlyDefault.ts | 1 + tests/integration/rsc-mf/tests/index.test.ts | 44 +++++++++++++++++-- 9 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 tests/integration/rsc-mf/remote/src/runtime/exposes/AsyncRemoteServerInfo.tsx create mode 100644 tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteServerCard.tsx create mode 100644 tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteServerDefault.tsx create mode 100644 tests/integration/rsc-mf/remote/src/runtime/exposes/infoBundle.ts create mode 100644 tests/integration/rsc-mf/remote/src/runtime/exposes/remoteMeta.ts create mode 100644 tests/integration/rsc-mf/remote/src/runtime/exposes/remoteServerOnly.ts create mode 100644 tests/integration/rsc-mf/remote/src/runtime/exposes/remoteServerOnlyDefault.ts diff --git a/tests/integration/rsc-mf/remote/module-federation.config.ts b/tests/integration/rsc-mf/remote/module-federation.config.ts index c16bdbacb477..42b8226a1bb3 100644 --- a/tests/integration/rsc-mf/remote/module-federation.config.ts +++ b/tests/integration/rsc-mf/remote/module-federation.config.ts @@ -120,27 +120,27 @@ export default createModuleFederationConfig({ layer: LAYERS.rsc, } as any, './RemoteServerCard': { - import: './src/components/RemoteServerCard.tsx', + import: './src/runtime/exposes/RemoteServerCard.tsx', layer: LAYERS.rsc, } as any, './RemoteServerDefault': { - import: './src/components/RemoteServerDefault.tsx', + import: './src/runtime/exposes/RemoteServerDefault.tsx', layer: LAYERS.rsc, } as any, './AsyncRemoteServerInfo': { - import: './src/components/AsyncRemoteServerInfo.tsx', + import: './src/runtime/exposes/AsyncRemoteServerInfo.tsx', layer: LAYERS.rsc, } as any, './remoteServerOnly': { - import: './src/components/serverOnly.ts', + import: './src/runtime/exposes/remoteServerOnly.ts', layer: LAYERS.rsc, } as any, './remoteServerOnlyDefault': { - import: './src/components/serverOnlyDefault.ts', + import: './src/runtime/exposes/remoteServerOnlyDefault.ts', layer: LAYERS.rsc, } as any, './remoteMeta': { - import: './src/components/remoteMeta.ts', + import: './src/runtime/exposes/remoteMeta.ts', layer: LAYERS.rsc, } as any, './actions': { @@ -160,7 +160,7 @@ export default createModuleFederationConfig({ layer: LAYERS.rsc, } as any, './infoBundle': { - import: './src/components/infoBundle.ts', + import: './src/runtime/exposes/infoBundle.ts', layer: LAYERS.rsc, } as any, }, diff --git a/tests/integration/rsc-mf/remote/src/runtime/exposes/AsyncRemoteServerInfo.tsx b/tests/integration/rsc-mf/remote/src/runtime/exposes/AsyncRemoteServerInfo.tsx new file mode 100644 index 000000000000..b7aca908fc89 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/runtime/exposes/AsyncRemoteServerInfo.tsx @@ -0,0 +1 @@ +export { AsyncRemoteServerInfo } from '../../components/AsyncRemoteServerInfo'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteServerCard.tsx b/tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteServerCard.tsx new file mode 100644 index 000000000000..07ba89f342fe --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteServerCard.tsx @@ -0,0 +1 @@ +export { default } from '../../components/RemoteServerCard'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteServerDefault.tsx b/tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteServerDefault.tsx new file mode 100644 index 000000000000..9687ba8272e9 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteServerDefault.tsx @@ -0,0 +1 @@ +export { default } from '../../components/RemoteServerDefault'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/exposes/infoBundle.ts b/tests/integration/rsc-mf/remote/src/runtime/exposes/infoBundle.ts new file mode 100644 index 000000000000..b52d877ae796 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/runtime/exposes/infoBundle.ts @@ -0,0 +1 @@ +export * from '../../components/infoBundle'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/exposes/remoteMeta.ts b/tests/integration/rsc-mf/remote/src/runtime/exposes/remoteMeta.ts new file mode 100644 index 000000000000..9d04431a939e --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/runtime/exposes/remoteMeta.ts @@ -0,0 +1 @@ +export { default, getRemoteMetaLabel } from '../../components/remoteMeta'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/exposes/remoteServerOnly.ts b/tests/integration/rsc-mf/remote/src/runtime/exposes/remoteServerOnly.ts new file mode 100644 index 000000000000..9d8750a799d8 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/runtime/exposes/remoteServerOnly.ts @@ -0,0 +1 @@ +export { getServerOnlyInfo } from '../../components/serverOnly'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/exposes/remoteServerOnlyDefault.ts b/tests/integration/rsc-mf/remote/src/runtime/exposes/remoteServerOnlyDefault.ts new file mode 100644 index 000000000000..479a159f4884 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/runtime/exposes/remoteServerOnlyDefault.ts @@ -0,0 +1 @@ +export { default } from '../../components/serverOnlyDefault'; diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 6ba3187818bc..71e107b123e8 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -464,6 +464,10 @@ function runTests({ mode }: TestConfig) { path.join(remoteDir, 'src/runtime/registerServerCallback.ts'), 'utf-8', ); + const moduleFederationConfigSource = fs.readFileSync( + path.join(remoteDir, 'module-federation.config.ts'), + 'utf-8', + ); expect( componentSources.every( @@ -476,12 +480,46 @@ function runTests({ mode }: TestConfig) { ), ).toBe(true); expect( - exposeRuntimeFilePaths.every(filePath => - fs.readFileSync(filePath, 'utf-8').includes('initServerCallback'), - ), + exposeRuntimeFilePaths + .filter(filePath => + [ + 'RemoteClientCounter.tsx', + 'RemoteClientBadge.tsx', + 'actions.ts', + 'nestedActions.ts', + 'defaultAction.ts', + 'actionBundle.ts', + ].includes(path.basename(filePath)), + ) + .every(filePath => + fs.readFileSync(filePath, 'utf-8').includes('initServerCallback'), + ), + ).toBe(true); + expect( + exposeRuntimeFilePaths + .filter( + filePath => + ![ + 'RemoteClientCounter.tsx', + 'RemoteClientBadge.tsx', + 'actions.ts', + 'nestedActions.ts', + 'defaultAction.ts', + 'actionBundle.ts', + ].includes(path.basename(filePath)), + ) + .every( + filePath => + !fs + .readFileSync(filePath, 'utf-8') + .includes('initServerCallback'), + ), ).toBe(true); expect(runtimeInitSource).toContain('registerRemoteServerCallback'); expect(runtimeRegisterSource).toContain('setServerCallback'); + expect(moduleFederationConfigSource).not.toContain( + "import: './src/components/", + ); }); it('should not load callback helper expose chunk', () => { From 9c2099a8509a4a9f22b1acaceea1ac4fa19da2aa Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 03:24:39 +0000 Subject: [PATCH 129/324] refactor(rsc-mf): define remote exposes from single runtime map --- .../rsc-mf/remote/module-federation.config.ts | 87 +++++++------------ 1 file changed, 29 insertions(+), 58 deletions(-) diff --git a/tests/integration/rsc-mf/remote/module-federation.config.ts b/tests/integration/rsc-mf/remote/module-federation.config.ts index 42b8226a1bb3..b7d2509f2c67 100644 --- a/tests/integration/rsc-mf/remote/module-federation.config.ts +++ b/tests/integration/rsc-mf/remote/module-federation.config.ts @@ -14,6 +14,29 @@ const reactDomServerImport = path.join( 'react-dom.react-server.js', ); const reactServerDomClientImport = 'react-server-dom-rspack/client.browser'; +const createRscExpose = (importPath: string) => + ({ + import: importPath, + layer: LAYERS.rsc, + }) as any; +const remoteExposeImports: Record = { + './RemoteClientCounter': './src/runtime/exposes/RemoteClientCounter.tsx', + './src/components/RemoteClientCounter.tsx': + './src/runtime/exposes/RemoteClientCounter.tsx', + './RemoteClientBadge': './src/runtime/exposes/RemoteClientBadge.tsx', + './RemoteServerCard': './src/runtime/exposes/RemoteServerCard.tsx', + './RemoteServerDefault': './src/runtime/exposes/RemoteServerDefault.tsx', + './AsyncRemoteServerInfo': './src/runtime/exposes/AsyncRemoteServerInfo.tsx', + './remoteServerOnly': './src/runtime/exposes/remoteServerOnly.ts', + './remoteServerOnlyDefault': + './src/runtime/exposes/remoteServerOnlyDefault.ts', + './remoteMeta': './src/runtime/exposes/remoteMeta.ts', + './actions': './src/runtime/exposes/actions.ts', + './nestedActions': './src/runtime/exposes/nestedActions.ts', + './defaultAction': './src/runtime/exposes/defaultAction.ts', + './actionBundle': './src/runtime/exposes/actionBundle.ts', + './infoBundle': './src/runtime/exposes/infoBundle.ts', +}; const sharedByScope = [ { @@ -106,64 +129,12 @@ export default createModuleFederationConfig({ filePath: 'static', }, filename: 'static/remoteEntry.js', - exposes: { - './RemoteClientCounter': { - import: './src/runtime/exposes/RemoteClientCounter.tsx', - layer: LAYERS.rsc, - } as any, - './src/components/RemoteClientCounter.tsx': { - import: './src/runtime/exposes/RemoteClientCounter.tsx', - layer: LAYERS.rsc, - } as any, - './RemoteClientBadge': { - import: './src/runtime/exposes/RemoteClientBadge.tsx', - layer: LAYERS.rsc, - } as any, - './RemoteServerCard': { - import: './src/runtime/exposes/RemoteServerCard.tsx', - layer: LAYERS.rsc, - } as any, - './RemoteServerDefault': { - import: './src/runtime/exposes/RemoteServerDefault.tsx', - layer: LAYERS.rsc, - } as any, - './AsyncRemoteServerInfo': { - import: './src/runtime/exposes/AsyncRemoteServerInfo.tsx', - layer: LAYERS.rsc, - } as any, - './remoteServerOnly': { - import: './src/runtime/exposes/remoteServerOnly.ts', - layer: LAYERS.rsc, - } as any, - './remoteServerOnlyDefault': { - import: './src/runtime/exposes/remoteServerOnlyDefault.ts', - layer: LAYERS.rsc, - } as any, - './remoteMeta': { - import: './src/runtime/exposes/remoteMeta.ts', - layer: LAYERS.rsc, - } as any, - './actions': { - import: './src/runtime/exposes/actions.ts', - layer: LAYERS.rsc, - } as any, - './nestedActions': { - import: './src/runtime/exposes/nestedActions.ts', - layer: LAYERS.rsc, - } as any, - './defaultAction': { - import: './src/runtime/exposes/defaultAction.ts', - layer: LAYERS.rsc, - } as any, - './actionBundle': { - import: './src/runtime/exposes/actionBundle.ts', - layer: LAYERS.rsc, - } as any, - './infoBundle': { - import: './src/runtime/exposes/infoBundle.ts', - layer: LAYERS.rsc, - } as any, - }, + exposes: Object.fromEntries( + Object.entries(remoteExposeImports).map(([exposeKey, importPath]) => [ + exposeKey, + createRscExpose(importPath), + ]), + ) as any, shared: sharedByScope as any, dts: false, experiments: { From 90c4dacef5fd416ca483bcb97d719ba1c7b11d0e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 03:27:24 +0000 Subject: [PATCH 130/324] refactor(rsc-mf): guard runtime expose paths in remote config --- .../rsc-mf/remote/module-federation.config.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/integration/rsc-mf/remote/module-federation.config.ts b/tests/integration/rsc-mf/remote/module-federation.config.ts index b7d2509f2c67..67c684dda339 100644 --- a/tests/integration/rsc-mf/remote/module-federation.config.ts +++ b/tests/integration/rsc-mf/remote/module-federation.config.ts @@ -19,6 +19,7 @@ const createRscExpose = (importPath: string) => import: importPath, layer: LAYERS.rsc, }) as any; +const RUNTIME_EXPOSE_PREFIX = './src/runtime/exposes/'; const remoteExposeImports: Record = { './RemoteClientCounter': './src/runtime/exposes/RemoteClientCounter.tsx', './src/components/RemoteClientCounter.tsx': @@ -37,6 +38,16 @@ const remoteExposeImports: Record = { './actionBundle': './src/runtime/exposes/actionBundle.ts', './infoBundle': './src/runtime/exposes/infoBundle.ts', }; +const nonRuntimeExposeEntries = Object.entries(remoteExposeImports).filter( + ([, importPath]) => !importPath.startsWith(RUNTIME_EXPOSE_PREFIX), +); +if (nonRuntimeExposeEntries.length > 0) { + throw new Error( + `All remote exposes must point to runtime wrappers (${RUNTIME_EXPOSE_PREFIX}). Invalid entries: ${nonRuntimeExposeEntries + .map(([exposeKey, importPath]) => `${exposeKey} -> ${importPath}`) + .join(', ')}`, + ); +} const sharedByScope = [ { From 4da2b0aaa6f21bbb8dee4e742380613f79845844 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 03:30:39 +0000 Subject: [PATCH 131/324] test(rsc-mf): validate callback bootstrap wrapper set coverage --- tests/integration/rsc-mf/tests/index.test.ts | 42 +++++++++----------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 71e107b123e8..3705d1cb83f2 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -20,6 +20,14 @@ const HOST_RSC_URL = '/server-component-root'; const EXPECTED_ACTION_POSTS_PER_MODE = 24; const EXPECTED_ACTION_POSTS_PER_FAMILY = 6; const EXPECTED_UNIQUE_ACTION_IDS_PER_MODE = 4; +const CALLBACK_BOOTSTRAPPED_RUNTIME_EXPOSE_FILES = new Set([ + 'RemoteClientCounter.tsx', + 'RemoteClientBadge.tsx', + 'actions.ts', + 'nestedActions.ts', + 'defaultAction.ts', + 'actionBundle.ts', +]); type Mode = 'dev' | 'build'; @@ -480,33 +488,21 @@ function runTests({ mode }: TestConfig) { ), ).toBe(true); expect( - exposeRuntimeFilePaths - .filter(filePath => - [ - 'RemoteClientCounter.tsx', - 'RemoteClientBadge.tsx', - 'actions.ts', - 'nestedActions.ts', - 'defaultAction.ts', - 'actionBundle.ts', - ].includes(path.basename(filePath)), - ) - .every(filePath => - fs.readFileSync(filePath, 'utf-8').includes('initServerCallback'), - ), - ).toBe(true); + new Set( + exposeRuntimeFilePaths + .filter(filePath => + fs.readFileSync(filePath, 'utf-8').includes('initServerCallback'), + ) + .map(filePath => path.basename(filePath)), + ), + ).toEqual(CALLBACK_BOOTSTRAPPED_RUNTIME_EXPOSE_FILES); expect( exposeRuntimeFilePaths .filter( filePath => - ![ - 'RemoteClientCounter.tsx', - 'RemoteClientBadge.tsx', - 'actions.ts', - 'nestedActions.ts', - 'defaultAction.ts', - 'actionBundle.ts', - ].includes(path.basename(filePath)), + !CALLBACK_BOOTSTRAPPED_RUNTIME_EXPOSE_FILES.has( + path.basename(filePath), + ), ) .every( filePath => From f8aabd06d927de297276da59b07bc4277bee8a2f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 03:33:53 +0000 Subject: [PATCH 132/324] test(rsc-mf): assert remote manifest avoids source-path exposes --- tests/integration/rsc-mf/remote/module-federation.config.ts | 2 -- tests/integration/rsc-mf/tests/index.test.ts | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/integration/rsc-mf/remote/module-federation.config.ts b/tests/integration/rsc-mf/remote/module-federation.config.ts index 67c684dda339..c2d05397746d 100644 --- a/tests/integration/rsc-mf/remote/module-federation.config.ts +++ b/tests/integration/rsc-mf/remote/module-federation.config.ts @@ -22,8 +22,6 @@ const createRscExpose = (importPath: string) => const RUNTIME_EXPOSE_PREFIX = './src/runtime/exposes/'; const remoteExposeImports: Record = { './RemoteClientCounter': './src/runtime/exposes/RemoteClientCounter.tsx', - './src/components/RemoteClientCounter.tsx': - './src/runtime/exposes/RemoteClientCounter.tsx', './RemoteClientBadge': './src/runtime/exposes/RemoteClientBadge.tsx', './RemoteServerCard': './src/runtime/exposes/RemoteServerCard.tsx', './RemoteServerDefault': './src/runtime/exposes/RemoteServerDefault.tsx', diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 3705d1cb83f2..7e444096925b 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -442,6 +442,9 @@ function runTests({ mode }: TestConfig) { .map(item => item.path) .filter((path): path is string => Boolean(path)); expect(exposedPaths).not.toContain('./registerServerCallback'); + expect( + exposedPaths.every(path => !path.startsWith('./src/components/')), + ).toBe(true); }); it('should keep callback runtime wiring out of component sources', () => { From 25443dc2b169a598e0f7ff4118c58c4fc410f1aa Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 03:44:40 +0000 Subject: [PATCH 133/324] refactor(rsc-mf): inline callback bootstrap in expose imports --- .../rsc-mf/remote/module-federation.config.ts | 55 +++++++++++-------- .../runtime/exposes/AsyncRemoteServerInfo.tsx | 1 - .../src/runtime/exposes/RemoteClientBadge.tsx | 2 - .../runtime/exposes/RemoteClientCounter.tsx | 2 - .../src/runtime/exposes/RemoteServerCard.tsx | 1 - .../runtime/exposes/RemoteServerDefault.tsx | 1 - .../src/runtime/exposes/actionBundle.ts | 2 - .../remote/src/runtime/exposes/actions.ts | 5 -- .../src/runtime/exposes/defaultAction.ts | 2 - .../remote/src/runtime/exposes/infoBundle.ts | 1 - .../src/runtime/exposes/nestedActions.ts | 2 - .../remote/src/runtime/exposes/remoteMeta.ts | 1 - .../src/runtime/exposes/remoteServerOnly.ts | 1 - .../exposes/remoteServerOnlyDefault.ts | 1 - tests/integration/rsc-mf/tests/index.test.ts | 54 +++++++----------- 15 files changed, 51 insertions(+), 80 deletions(-) delete mode 100644 tests/integration/rsc-mf/remote/src/runtime/exposes/AsyncRemoteServerInfo.tsx delete mode 100644 tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteClientBadge.tsx delete mode 100644 tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteClientCounter.tsx delete mode 100644 tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteServerCard.tsx delete mode 100644 tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteServerDefault.tsx delete mode 100644 tests/integration/rsc-mf/remote/src/runtime/exposes/actionBundle.ts delete mode 100644 tests/integration/rsc-mf/remote/src/runtime/exposes/actions.ts delete mode 100644 tests/integration/rsc-mf/remote/src/runtime/exposes/defaultAction.ts delete mode 100644 tests/integration/rsc-mf/remote/src/runtime/exposes/infoBundle.ts delete mode 100644 tests/integration/rsc-mf/remote/src/runtime/exposes/nestedActions.ts delete mode 100644 tests/integration/rsc-mf/remote/src/runtime/exposes/remoteMeta.ts delete mode 100644 tests/integration/rsc-mf/remote/src/runtime/exposes/remoteServerOnly.ts delete mode 100644 tests/integration/rsc-mf/remote/src/runtime/exposes/remoteServerOnlyDefault.ts diff --git a/tests/integration/rsc-mf/remote/module-federation.config.ts b/tests/integration/rsc-mf/remote/module-federation.config.ts index c2d05397746d..e0f0ae3a7247 100644 --- a/tests/integration/rsc-mf/remote/module-federation.config.ts +++ b/tests/integration/rsc-mf/remote/module-federation.config.ts @@ -14,36 +14,43 @@ const reactDomServerImport = path.join( 'react-dom.react-server.js', ); const reactServerDomClientImport = 'react-server-dom-rspack/client.browser'; -const createRscExpose = (importPath: string) => +const CALLBACK_BOOTSTRAP_IMPORT = './src/runtime/initServerCallback.ts'; +const callbackBootstrappedExposes = new Set([ + './RemoteClientCounter', + './RemoteClientBadge', + './actions', + './nestedActions', + './defaultAction', + './actionBundle', +]); +const createRscExpose = (exposeKey: string, importPath: string) => ({ - import: importPath, + import: callbackBootstrappedExposes.has(exposeKey) + ? [CALLBACK_BOOTSTRAP_IMPORT, importPath] + : importPath, layer: LAYERS.rsc, }) as any; -const RUNTIME_EXPOSE_PREFIX = './src/runtime/exposes/'; const remoteExposeImports: Record = { - './RemoteClientCounter': './src/runtime/exposes/RemoteClientCounter.tsx', - './RemoteClientBadge': './src/runtime/exposes/RemoteClientBadge.tsx', - './RemoteServerCard': './src/runtime/exposes/RemoteServerCard.tsx', - './RemoteServerDefault': './src/runtime/exposes/RemoteServerDefault.tsx', - './AsyncRemoteServerInfo': './src/runtime/exposes/AsyncRemoteServerInfo.tsx', - './remoteServerOnly': './src/runtime/exposes/remoteServerOnly.ts', - './remoteServerOnlyDefault': - './src/runtime/exposes/remoteServerOnlyDefault.ts', - './remoteMeta': './src/runtime/exposes/remoteMeta.ts', - './actions': './src/runtime/exposes/actions.ts', - './nestedActions': './src/runtime/exposes/nestedActions.ts', - './defaultAction': './src/runtime/exposes/defaultAction.ts', - './actionBundle': './src/runtime/exposes/actionBundle.ts', - './infoBundle': './src/runtime/exposes/infoBundle.ts', + './RemoteClientCounter': './src/components/RemoteClientCounter.tsx', + './RemoteClientBadge': './src/components/RemoteClientBadge.tsx', + './RemoteServerCard': './src/components/RemoteServerCard.tsx', + './RemoteServerDefault': './src/components/RemoteServerDefault.tsx', + './AsyncRemoteServerInfo': './src/components/AsyncRemoteServerInfo.tsx', + './remoteServerOnly': './src/components/serverOnly.ts', + './remoteServerOnlyDefault': './src/components/serverOnlyDefault.ts', + './remoteMeta': './src/components/remoteMeta.ts', + './actions': './src/components/actions.ts', + './nestedActions': './src/components/nestedActions.ts', + './defaultAction': './src/components/defaultAction.ts', + './actionBundle': './src/components/actionBundle.ts', + './infoBundle': './src/components/infoBundle.ts', }; -const nonRuntimeExposeEntries = Object.entries(remoteExposeImports).filter( - ([, importPath]) => !importPath.startsWith(RUNTIME_EXPOSE_PREFIX), +const missingCallbackExposeEntries = [...callbackBootstrappedExposes].filter( + exposeKey => !(exposeKey in remoteExposeImports), ); -if (nonRuntimeExposeEntries.length > 0) { +if (missingCallbackExposeEntries.length > 0) { throw new Error( - `All remote exposes must point to runtime wrappers (${RUNTIME_EXPOSE_PREFIX}). Invalid entries: ${nonRuntimeExposeEntries - .map(([exposeKey, importPath]) => `${exposeKey} -> ${importPath}`) - .join(', ')}`, + `Callback-bootstrapped exposes must exist in remoteExposeImports. Missing entries: ${missingCallbackExposeEntries.join(', ')}`, ); } @@ -141,7 +148,7 @@ export default createModuleFederationConfig({ exposes: Object.fromEntries( Object.entries(remoteExposeImports).map(([exposeKey, importPath]) => [ exposeKey, - createRscExpose(importPath), + createRscExpose(exposeKey, importPath), ]), ) as any, shared: sharedByScope as any, diff --git a/tests/integration/rsc-mf/remote/src/runtime/exposes/AsyncRemoteServerInfo.tsx b/tests/integration/rsc-mf/remote/src/runtime/exposes/AsyncRemoteServerInfo.tsx deleted file mode 100644 index b7aca908fc89..000000000000 --- a/tests/integration/rsc-mf/remote/src/runtime/exposes/AsyncRemoteServerInfo.tsx +++ /dev/null @@ -1 +0,0 @@ -export { AsyncRemoteServerInfo } from '../../components/AsyncRemoteServerInfo'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteClientBadge.tsx b/tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteClientBadge.tsx deleted file mode 100644 index cfcb31341f50..000000000000 --- a/tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteClientBadge.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import '../initServerCallback'; -export { default } from '../../components/RemoteClientBadge'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteClientCounter.tsx b/tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteClientCounter.tsx deleted file mode 100644 index 2588359d1a1e..000000000000 --- a/tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteClientCounter.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import '../initServerCallback'; -export { RemoteClientCounter } from '../../components/RemoteClientCounter'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteServerCard.tsx b/tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteServerCard.tsx deleted file mode 100644 index 07ba89f342fe..000000000000 --- a/tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteServerCard.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from '../../components/RemoteServerCard'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteServerDefault.tsx b/tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteServerDefault.tsx deleted file mode 100644 index 9687ba8272e9..000000000000 --- a/tests/integration/rsc-mf/remote/src/runtime/exposes/RemoteServerDefault.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from '../../components/RemoteServerDefault'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/exposes/actionBundle.ts b/tests/integration/rsc-mf/remote/src/runtime/exposes/actionBundle.ts deleted file mode 100644 index d168ead856f0..000000000000 --- a/tests/integration/rsc-mf/remote/src/runtime/exposes/actionBundle.ts +++ /dev/null @@ -1,2 +0,0 @@ -import '../initServerCallback'; -export * from '../../components/actionBundle'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/exposes/actions.ts b/tests/integration/rsc-mf/remote/src/runtime/exposes/actions.ts deleted file mode 100644 index c2323499156d..000000000000 --- a/tests/integration/rsc-mf/remote/src/runtime/exposes/actions.ts +++ /dev/null @@ -1,5 +0,0 @@ -import '../initServerCallback'; -export { - incrementRemoteCount, - remoteActionEcho, -} from '../../components/actions'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/exposes/defaultAction.ts b/tests/integration/rsc-mf/remote/src/runtime/exposes/defaultAction.ts deleted file mode 100644 index 0cff7b09f27c..000000000000 --- a/tests/integration/rsc-mf/remote/src/runtime/exposes/defaultAction.ts +++ /dev/null @@ -1,2 +0,0 @@ -import '../initServerCallback'; -export { defaultRemoteAction } from '../../components/defaultAction'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/exposes/infoBundle.ts b/tests/integration/rsc-mf/remote/src/runtime/exposes/infoBundle.ts deleted file mode 100644 index b52d877ae796..000000000000 --- a/tests/integration/rsc-mf/remote/src/runtime/exposes/infoBundle.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '../../components/infoBundle'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/exposes/nestedActions.ts b/tests/integration/rsc-mf/remote/src/runtime/exposes/nestedActions.ts deleted file mode 100644 index 3ac8a55c06bd..000000000000 --- a/tests/integration/rsc-mf/remote/src/runtime/exposes/nestedActions.ts +++ /dev/null @@ -1,2 +0,0 @@ -import '../initServerCallback'; -export { nestedRemoteAction } from '../../components/nestedActions'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/exposes/remoteMeta.ts b/tests/integration/rsc-mf/remote/src/runtime/exposes/remoteMeta.ts deleted file mode 100644 index 9d04431a939e..000000000000 --- a/tests/integration/rsc-mf/remote/src/runtime/exposes/remoteMeta.ts +++ /dev/null @@ -1 +0,0 @@ -export { default, getRemoteMetaLabel } from '../../components/remoteMeta'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/exposes/remoteServerOnly.ts b/tests/integration/rsc-mf/remote/src/runtime/exposes/remoteServerOnly.ts deleted file mode 100644 index 9d8750a799d8..000000000000 --- a/tests/integration/rsc-mf/remote/src/runtime/exposes/remoteServerOnly.ts +++ /dev/null @@ -1 +0,0 @@ -export { getServerOnlyInfo } from '../../components/serverOnly'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/exposes/remoteServerOnlyDefault.ts b/tests/integration/rsc-mf/remote/src/runtime/exposes/remoteServerOnlyDefault.ts deleted file mode 100644 index 479a159f4884..000000000000 --- a/tests/integration/rsc-mf/remote/src/runtime/exposes/remoteServerOnlyDefault.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from '../../components/serverOnlyDefault'; diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 7e444096925b..eaa4b29b0bb5 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -20,13 +20,13 @@ const HOST_RSC_URL = '/server-component-root'; const EXPECTED_ACTION_POSTS_PER_MODE = 24; const EXPECTED_ACTION_POSTS_PER_FAMILY = 6; const EXPECTED_UNIQUE_ACTION_IDS_PER_MODE = 4; -const CALLBACK_BOOTSTRAPPED_RUNTIME_EXPOSE_FILES = new Set([ - 'RemoteClientCounter.tsx', - 'RemoteClientBadge.tsx', - 'actions.ts', - 'nestedActions.ts', - 'defaultAction.ts', - 'actionBundle.ts', +const CALLBACK_BOOTSTRAPPED_EXPOSE_KEYS = new Set([ + './RemoteClientCounter', + './RemoteClientBadge', + './actions', + './nestedActions', + './defaultAction', + './actionBundle', ]); type Mode = 'dev' | 'build'; @@ -460,9 +460,6 @@ function runTests({ mode }: TestConfig) { const componentFilePaths = getFilesRecursively( path.join(remoteDir, 'src/components'), ); - const exposeRuntimeFilePaths = getFilesRecursively( - path.join(remoteDir, 'src/runtime/exposes'), - ); const componentSources = componentFilePaths.map(filePath => fs.readFileSync(filePath, 'utf-8'), @@ -490,35 +487,24 @@ function runTests({ mode }: TestConfig) { source => !source.includes('registerRemoteServerCallback'), ), ).toBe(true); + expect(runtimeInitSource).toContain('registerRemoteServerCallback'); + expect(runtimeRegisterSource).toContain('setServerCallback'); + expect(moduleFederationConfigSource).toContain( + 'CALLBACK_BOOTSTRAP_IMPORT', + ); expect( - new Set( - exposeRuntimeFilePaths - .filter(filePath => - fs.readFileSync(filePath, 'utf-8').includes('initServerCallback'), - ) - .map(filePath => path.basename(filePath)), + moduleFederationConfigSource.includes( + '[CALLBACK_BOOTSTRAP_IMPORT, importPath]', ), - ).toEqual(CALLBACK_BOOTSTRAPPED_RUNTIME_EXPOSE_FILES); - expect( - exposeRuntimeFilePaths - .filter( - filePath => - !CALLBACK_BOOTSTRAPPED_RUNTIME_EXPOSE_FILES.has( - path.basename(filePath), - ), - ) - .every( - filePath => - !fs - .readFileSync(filePath, 'utf-8') - .includes('initServerCallback'), - ), ).toBe(true); - expect(runtimeInitSource).toContain('registerRemoteServerCallback'); - expect(runtimeRegisterSource).toContain('setServerCallback'); expect(moduleFederationConfigSource).not.toContain( - "import: './src/components/", + './src/runtime/exposes/', ); + expect( + [...CALLBACK_BOOTSTRAPPED_EXPOSE_KEYS].every(exposeKey => + moduleFederationConfigSource.includes(exposeKey), + ), + ).toBe(true); }); it('should not load callback helper expose chunk', () => { From 2ef0c802a89e47237b03c9d1595951974aad933b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 03:48:08 +0000 Subject: [PATCH 134/324] refactor(rsc-mf): bootstrap callback for all remote exposes --- .../rsc-mf/remote/module-federation.config.ts | 24 +++---------------- .../remote/src/runtime/initServerCallback.ts | 18 +++++++------- tests/integration/rsc-mf/tests/index.test.ts | 19 +++++---------- 3 files changed, 19 insertions(+), 42 deletions(-) diff --git a/tests/integration/rsc-mf/remote/module-federation.config.ts b/tests/integration/rsc-mf/remote/module-federation.config.ts index e0f0ae3a7247..7874096135fd 100644 --- a/tests/integration/rsc-mf/remote/module-federation.config.ts +++ b/tests/integration/rsc-mf/remote/module-federation.config.ts @@ -15,19 +15,9 @@ const reactDomServerImport = path.join( ); const reactServerDomClientImport = 'react-server-dom-rspack/client.browser'; const CALLBACK_BOOTSTRAP_IMPORT = './src/runtime/initServerCallback.ts'; -const callbackBootstrappedExposes = new Set([ - './RemoteClientCounter', - './RemoteClientBadge', - './actions', - './nestedActions', - './defaultAction', - './actionBundle', -]); -const createRscExpose = (exposeKey: string, importPath: string) => +const createRscExpose = (importPath: string) => ({ - import: callbackBootstrappedExposes.has(exposeKey) - ? [CALLBACK_BOOTSTRAP_IMPORT, importPath] - : importPath, + import: [CALLBACK_BOOTSTRAP_IMPORT, importPath], layer: LAYERS.rsc, }) as any; const remoteExposeImports: Record = { @@ -45,14 +35,6 @@ const remoteExposeImports: Record = { './actionBundle': './src/components/actionBundle.ts', './infoBundle': './src/components/infoBundle.ts', }; -const missingCallbackExposeEntries = [...callbackBootstrappedExposes].filter( - exposeKey => !(exposeKey in remoteExposeImports), -); -if (missingCallbackExposeEntries.length > 0) { - throw new Error( - `Callback-bootstrapped exposes must exist in remoteExposeImports. Missing entries: ${missingCallbackExposeEntries.join(', ')}`, - ); -} const sharedByScope = [ { @@ -148,7 +130,7 @@ export default createModuleFederationConfig({ exposes: Object.fromEntries( Object.entries(remoteExposeImports).map(([exposeKey, importPath]) => [ exposeKey, - createRscExpose(exposeKey, importPath), + createRscExpose(importPath), ]), ) as any, shared: sharedByScope as any, diff --git a/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts b/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts index 7d6b5ff31fba..02335339016a 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts @@ -1,11 +1,13 @@ -import { registerRemoteServerCallback } from './registerServerCallback'; - if (typeof window !== 'undefined') { - // Fixture-level bootstrap: make federated client actions post back through - // host route using bridge-prefixed ids without requiring host userland wiring. - const actionPathname = window.location.pathname || '/'; - registerRemoteServerCallback( - `${window.location.origin}${actionPathname}`, - 'rscRemote', + // Fixture-level bootstrap: keep callback wiring out of exposed modules while + // ensuring browser-evaluated federated code always posts bridge action IDs to host. + void import('./registerServerCallback').then( + ({ registerRemoteServerCallback }) => { + const actionPathname = window.location.pathname || '/'; + registerRemoteServerCallback( + `${window.location.origin}${actionPathname}`, + 'rscRemote', + ); + }, ); } diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index eaa4b29b0bb5..eea31275befd 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -20,14 +20,6 @@ const HOST_RSC_URL = '/server-component-root'; const EXPECTED_ACTION_POSTS_PER_MODE = 24; const EXPECTED_ACTION_POSTS_PER_FAMILY = 6; const EXPECTED_UNIQUE_ACTION_IDS_PER_MODE = 4; -const CALLBACK_BOOTSTRAPPED_EXPOSE_KEYS = new Set([ - './RemoteClientCounter', - './RemoteClientBadge', - './actions', - './nestedActions', - './defaultAction', - './actionBundle', -]); type Mode = 'dev' | 'build'; @@ -497,14 +489,15 @@ function runTests({ mode }: TestConfig) { '[CALLBACK_BOOTSTRAP_IMPORT, importPath]', ), ).toBe(true); + expect(moduleFederationConfigSource).not.toContain( + 'callbackBootstrappedExposes', + ); + expect(moduleFederationConfigSource).not.toContain( + 'missingCallbackExposeEntries', + ); expect(moduleFederationConfigSource).not.toContain( './src/runtime/exposes/', ); - expect( - [...CALLBACK_BOOTSTRAPPED_EXPOSE_KEYS].every(exposeKey => - moduleFederationConfigSource.includes(exposeKey), - ), - ).toBe(true); }); it('should not load callback helper expose chunk', () => { From 6953bab7dd42a85f309e4580c4a8b862018153f1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 03:51:09 +0000 Subject: [PATCH 135/324] refactor(rsc-mf): guard exposes to component modules --- .../rsc-mf/remote/module-federation.config.ts | 11 +++++++++++ tests/integration/rsc-mf/tests/index.test.ts | 4 ++++ 2 files changed, 15 insertions(+) diff --git a/tests/integration/rsc-mf/remote/module-federation.config.ts b/tests/integration/rsc-mf/remote/module-federation.config.ts index 7874096135fd..29b3c0335d81 100644 --- a/tests/integration/rsc-mf/remote/module-federation.config.ts +++ b/tests/integration/rsc-mf/remote/module-federation.config.ts @@ -35,6 +35,17 @@ const remoteExposeImports: Record = { './actionBundle': './src/components/actionBundle.ts', './infoBundle': './src/components/infoBundle.ts', }; +const COMPONENT_EXPOSE_PREFIX = './src/components/'; +const nonComponentExposeEntries = Object.entries(remoteExposeImports).filter( + ([, importPath]) => !importPath.startsWith(COMPONENT_EXPOSE_PREFIX), +); +if (nonComponentExposeEntries.length > 0) { + throw new Error( + `Remote exposes must point to component userland modules (${COMPONENT_EXPOSE_PREFIX}). Invalid entries: ${nonComponentExposeEntries + .map(([exposeKey, importPath]) => `${exposeKey} -> ${importPath}`) + .join(', ')}`, + ); +} const sharedByScope = [ { diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index eea31275befd..98012c20cfb6 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -498,6 +498,10 @@ function runTests({ mode }: TestConfig) { expect(moduleFederationConfigSource).not.toContain( './src/runtime/exposes/', ); + expect(moduleFederationConfigSource).toContain('COMPONENT_EXPOSE_PREFIX'); + expect(moduleFederationConfigSource).toContain( + 'nonComponentExposeEntries', + ); }); it('should not load callback helper expose chunk', () => { From 347d2044224c5b2a1c6da8e859c2d0f2673419f4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 03:52:26 +0000 Subject: [PATCH 136/324] test(rsc-mf): lock lazy callback bootstrap import semantics --- tests/integration/rsc-mf/tests/index.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 98012c20cfb6..1763b95d0248 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -480,6 +480,10 @@ function runTests({ mode }: TestConfig) { ), ).toBe(true); expect(runtimeInitSource).toContain('registerRemoteServerCallback'); + expect(runtimeInitSource).toContain("import('./registerServerCallback')"); + expect(runtimeInitSource).not.toContain( + "from './registerServerCallback'", + ); expect(runtimeRegisterSource).toContain('setServerCallback'); expect(moduleFederationConfigSource).toContain( 'CALLBACK_BOOTSTRAP_IMPORT', From 7dde578a69b1b495e5df8102367d17558de0af44 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 03:57:10 +0000 Subject: [PATCH 137/324] test(rsc-mf): validate manifest expose set is userland subset --- tests/integration/rsc-mf/tests/index.test.ts | 29 ++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 1763b95d0248..901b5882efc8 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -20,6 +20,21 @@ const HOST_RSC_URL = '/server-component-root'; const EXPECTED_ACTION_POSTS_PER_MODE = 24; const EXPECTED_ACTION_POSTS_PER_FAMILY = 6; const EXPECTED_UNIQUE_ACTION_IDS_PER_MODE = 4; +const EXPECTED_REMOTE_EXPOSE_PATHS = [ + './RemoteClientCounter', + './RemoteClientBadge', + './RemoteServerCard', + './RemoteServerDefault', + './AsyncRemoteServerInfo', + './remoteServerOnly', + './remoteServerOnlyDefault', + './remoteMeta', + './actions', + './nestedActions', + './defaultAction', + './actionBundle', + './infoBundle', +].sort(); type Mode = 'dev' | 'build'; @@ -433,10 +448,24 @@ function runTests({ mode }: TestConfig) { const exposedPaths = (manifest.exposes || []) .map(item => item.path) .filter((path): path is string => Boolean(path)); + const uniqueExposedPaths = Array.from(new Set(exposedPaths)).sort(); expect(exposedPaths).not.toContain('./registerServerCallback'); + expect(uniqueExposedPaths.length).toBeGreaterThan(0); + expect(uniqueExposedPaths).toContain('./RemoteClientCounter'); + expect( + uniqueExposedPaths.every(path => + EXPECTED_REMOTE_EXPOSE_PATHS.includes(path), + ), + ).toBe(true); + expect(uniqueExposedPaths.length).toBeLessThanOrEqual( + EXPECTED_REMOTE_EXPOSE_PATHS.length, + ); expect( exposedPaths.every(path => !path.startsWith('./src/components/')), ).toBe(true); + expect( + exposedPaths.every(path => !path.includes('initServerCallback')), + ).toBe(true); }); it('should keep callback runtime wiring out of component sources', () => { From e9f09d320d93d41a4e586f455e0c52f7127db1b7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 04:00:30 +0000 Subject: [PATCH 138/324] test(rsc-mf): assert config expose map matches expected userland keys --- tests/integration/rsc-mf/tests/index.test.ts | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 901b5882efc8..18147bda6fe3 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -35,6 +35,8 @@ const EXPECTED_REMOTE_EXPOSE_PATHS = [ './actionBundle', './infoBundle', ].sort(); +const REMOTE_EXPOSE_ENTRY_PATTERN = + /'(\.\/[^']+)':\s*'(\.\/src\/components\/[^']+)'/g; type Mode = 'dev' | 'build'; @@ -93,6 +95,15 @@ function createHostEnv(remotePort: number) { }; } +function getRemoteExposeEntries(configSource: string) { + return Array.from(configSource.matchAll(REMOTE_EXPOSE_ENTRY_PATTERN)).map( + ([, exposeKey, importPath]) => ({ + exposeKey, + importPath, + }), + ); +} + async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { const response = await fetch(`http://127.0.0.1:${hostPort}${HOST_RSC_URL}`); const html = await response.text(); @@ -497,6 +508,12 @@ function runTests({ mode }: TestConfig) { path.join(remoteDir, 'module-federation.config.ts'), 'utf-8', ); + const remoteExposeEntries = getRemoteExposeEntries( + moduleFederationConfigSource, + ); + const remoteExposeKeys = remoteExposeEntries + .map(({ exposeKey }) => exposeKey) + .sort(); expect( componentSources.every( @@ -535,6 +552,12 @@ function runTests({ mode }: TestConfig) { expect(moduleFederationConfigSource).toContain( 'nonComponentExposeEntries', ); + expect(remoteExposeKeys).toEqual(EXPECTED_REMOTE_EXPOSE_PATHS); + expect( + remoteExposeEntries.every(({ importPath }) => + importPath.startsWith('./src/components/'), + ), + ).toBe(true); }); it('should not load callback helper expose chunk', () => { From 36966a9c7db2615f7b2315becb711baaa676f0c5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 04:03:45 +0000 Subject: [PATCH 139/324] test(rsc-mf): ensure host sources stay callback-wiring free --- tests/integration/rsc-mf/tests/index.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 18147bda6fe3..d4e2986a66d8 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -492,10 +492,16 @@ function runTests({ mode }: TestConfig) { const componentFilePaths = getFilesRecursively( path.join(remoteDir, 'src/components'), ); + const hostSourceFilePaths = getFilesRecursively( + path.join(hostDir, 'src'), + ).filter(filePath => /\.(ts|tsx)$/.test(filePath)); const componentSources = componentFilePaths.map(filePath => fs.readFileSync(filePath, 'utf-8'), ); + const hostSourceTexts = hostSourceFilePaths.map(filePath => + fs.readFileSync(filePath, 'utf-8'), + ); const runtimeInitSource = fs.readFileSync( path.join(remoteDir, 'src/runtime/initServerCallback.ts'), 'utf-8', @@ -525,6 +531,14 @@ function runTests({ mode }: TestConfig) { source => !source.includes('registerRemoteServerCallback'), ), ).toBe(true); + expect( + hostSourceTexts.every( + source => + !source.includes('registerRemoteServerCallback') && + !source.includes('initServerCallback') && + !source.includes('registerServerCallback'), + ), + ).toBe(true); expect(runtimeInitSource).toContain('registerRemoteServerCallback'); expect(runtimeInitSource).toContain("import('./registerServerCallback')"); expect(runtimeInitSource).not.toContain( From f56389cf1ac3da78cec674151001387031e6dbb7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 04:06:33 +0000 Subject: [PATCH 140/324] test(rsc-mf): lock host runtime plugins to transparent defaults --- tests/integration/rsc-mf/tests/index.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index d4e2986a66d8..a9efddf8bc16 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -514,6 +514,10 @@ function runTests({ mode }: TestConfig) { path.join(remoteDir, 'module-federation.config.ts'), 'utf-8', ); + const hostModuleFederationConfigSource = fs.readFileSync( + path.join(hostDir, 'module-federation.config.ts'), + 'utf-8', + ); const remoteExposeEntries = getRemoteExposeEntries( moduleFederationConfigSource, ); @@ -572,6 +576,16 @@ function runTests({ mode }: TestConfig) { importPath.startsWith('./src/components/'), ), ).toBe(true); + expect(hostModuleFederationConfigSource).toContain('runtimePlugins'); + expect(hostModuleFederationConfigSource).toContain( + './runtime/forceRemotePublicPath.ts', + ); + expect(hostModuleFederationConfigSource).not.toContain( + 'registerServerCallbackRuntime', + ); + expect(hostModuleFederationConfigSource).not.toContain( + 'initServerCallback', + ); }); it('should not load callback helper expose chunk', () => { From b4da5b5a24880f183d5e8819c0627bc3140c5ae3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 04:09:21 +0000 Subject: [PATCH 141/324] refactor(rsc-mf): memoize callback bootstrap initialization --- .../remote/src/runtime/initServerCallback.ts | 30 +++++++++++++------ tests/integration/rsc-mf/tests/index.test.ts | 2 ++ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts b/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts index 02335339016a..0cd3b583006d 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts @@ -1,13 +1,25 @@ +let callbackBootstrapPromise: Promise | undefined; + +const bootstrapServerCallback = () => { + if (!callbackBootstrapPromise) { + callbackBootstrapPromise = import('./registerServerCallback').then( + ({ registerRemoteServerCallback }) => { + const actionPathname = window.location.pathname || '/'; + registerRemoteServerCallback( + `${window.location.origin}${actionPathname}`, + 'rscRemote', + ); + }, + ); + } + + return callbackBootstrapPromise; +}; + if (typeof window !== 'undefined') { // Fixture-level bootstrap: keep callback wiring out of exposed modules while // ensuring browser-evaluated federated code always posts bridge action IDs to host. - void import('./registerServerCallback').then( - ({ registerRemoteServerCallback }) => { - const actionPathname = window.location.pathname || '/'; - registerRemoteServerCallback( - `${window.location.origin}${actionPathname}`, - 'rscRemote', - ); - }, - ); + // Promise memoization avoids duplicate bootstrap work when multiple exposes + // import this runtime helper in the same browser session. + void bootstrapServerCallback(); } diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index a9efddf8bc16..21bb676eff1e 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -544,6 +544,8 @@ function runTests({ mode }: TestConfig) { ), ).toBe(true); expect(runtimeInitSource).toContain('registerRemoteServerCallback'); + expect(runtimeInitSource).toContain('bootstrapServerCallback'); + expect(runtimeInitSource).toContain('callbackBootstrapPromise'); expect(runtimeInitSource).toContain("import('./registerServerCallback')"); expect(runtimeInitSource).not.toContain( "from './registerServerCallback'", From 88fdf162e6833dd6fddbe662a11cb21055520856 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 04:12:16 +0000 Subject: [PATCH 142/324] refactor(rsc-mf): reset callback bootstrap promise after failures --- .../rsc-mf/remote/src/runtime/initServerCallback.ts | 3 +++ tests/integration/rsc-mf/tests/index.test.ts | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts b/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts index 0cd3b583006d..d8b383dc7695 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts @@ -11,6 +11,9 @@ const bootstrapServerCallback = () => { ); }, ); + callbackBootstrapPromise.catch(() => { + callbackBootstrapPromise = undefined; + }); } return callbackBootstrapPromise; diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 21bb676eff1e..6fa4c2486be2 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -547,6 +547,10 @@ function runTests({ mode }: TestConfig) { expect(runtimeInitSource).toContain('bootstrapServerCallback'); expect(runtimeInitSource).toContain('callbackBootstrapPromise'); expect(runtimeInitSource).toContain("import('./registerServerCallback')"); + expect(runtimeInitSource).toContain('callbackBootstrapPromise.catch'); + expect(runtimeInitSource).toContain( + 'callbackBootstrapPromise = undefined', + ); expect(runtimeInitSource).not.toContain( "from './registerServerCallback'", ); From 088d64338696e4e5a7f3e6675fed6ddf4b0313fb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 04:14:56 +0000 Subject: [PATCH 143/324] test(rsc-mf): guard modern config callback wiring boundaries --- tests/integration/rsc-mf/tests/index.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 6fa4c2486be2..6562ca0def24 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -518,6 +518,14 @@ function runTests({ mode }: TestConfig) { path.join(hostDir, 'module-federation.config.ts'), 'utf-8', ); + const hostModernConfigSource = fs.readFileSync( + path.join(hostDir, 'modern.config.ts'), + 'utf-8', + ); + const remoteModernConfigSource = fs.readFileSync( + path.join(remoteDir, 'modern.config.ts'), + 'utf-8', + ); const remoteExposeEntries = getRemoteExposeEntries( moduleFederationConfigSource, ); @@ -592,6 +600,12 @@ function runTests({ mode }: TestConfig) { expect(hostModuleFederationConfigSource).not.toContain( 'initServerCallback', ); + expect(hostModernConfigSource).not.toContain('preEntry'); + expect(hostModernConfigSource).not.toContain('registerServerCallback'); + expect(remoteModernConfigSource).not.toContain('chunkLoadingGlobal'); + expect(remoteModernConfigSource).toContain( + 'rsc-mf-react-server-dom-client-browser$', + ); }); it('should not load callback helper expose chunk', () => { From 66097a4db68234227975701c5fa7634fdd5ef923 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 04:17:34 +0000 Subject: [PATCH 144/324] test(rsc-mf): assert modern config keeps transparent runtime boundaries --- tests/integration/rsc-mf/tests/index.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 6562ca0def24..90da99dc3848 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -594,6 +594,12 @@ function runTests({ mode }: TestConfig) { expect(hostModuleFederationConfigSource).toContain( './runtime/forceRemotePublicPath.ts', ); + expect(hostModuleFederationConfigSource).toContain( + '/static/mf-manifest.json', + ); + expect(hostModuleFederationConfigSource).toContain('rscRemote:'); + expect(hostModuleFederationConfigSource).toContain('asyncStartup: true'); + expect(hostModuleFederationConfigSource).toContain('rsc: true'); expect(hostModuleFederationConfigSource).not.toContain( 'registerServerCallbackRuntime', ); From 6a9e740ade7ce28003be39a60f3ec8818817d788 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 04:20:36 +0000 Subject: [PATCH 145/324] test(rsc-mf): enforce remote shared-scope and experiment config invariants --- tests/integration/rsc-mf/tests/index.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 90da99dc3848..6e11220c6a9a 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -532,6 +532,11 @@ function runTests({ mode }: TestConfig) { const remoteExposeKeys = remoteExposeEntries .map(({ exposeKey }) => exposeKey) .sort(); + const clientBrowserSharedScopeEntryCount = ( + moduleFederationConfigSource.match( + /'react-server-dom-rspack\/client\.browser':\s*\{/g, + ) || [] + ).length; expect( componentSources.every( @@ -584,6 +589,13 @@ function runTests({ mode }: TestConfig) { expect(moduleFederationConfigSource).toContain( 'nonComponentExposeEntries', ); + expect(moduleFederationConfigSource).toContain("shareScope: 'default'"); + expect(moduleFederationConfigSource).toContain("shareScope: 'ssr'"); + expect(moduleFederationConfigSource).toContain("shareScope: 'rsc'"); + expect(moduleFederationConfigSource).toContain('experiments:'); + expect(moduleFederationConfigSource).toContain('asyncStartup: true'); + expect(moduleFederationConfigSource).toContain('rsc: true'); + expect(clientBrowserSharedScopeEntryCount).toBe(3); expect(remoteExposeKeys).toEqual(EXPECTED_REMOTE_EXPOSE_PATHS); expect( remoteExposeEntries.every(({ importPath }) => From 8baab1f3a255f3de43af1e9ef489d589dd45dcb8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 04:23:44 +0000 Subject: [PATCH 146/324] refactor(rsc-mf): validate expose keys and protect bootstrap module from exposure --- .../rsc-mf/remote/module-federation.config.ts | 18 ++++++++++++++++++ tests/integration/rsc-mf/tests/index.test.ts | 2 ++ 2 files changed, 20 insertions(+) diff --git a/tests/integration/rsc-mf/remote/module-federation.config.ts b/tests/integration/rsc-mf/remote/module-federation.config.ts index 29b3c0335d81..d35b9318ba82 100644 --- a/tests/integration/rsc-mf/remote/module-federation.config.ts +++ b/tests/integration/rsc-mf/remote/module-federation.config.ts @@ -35,6 +35,14 @@ const remoteExposeImports: Record = { './actionBundle': './src/components/actionBundle.ts', './infoBundle': './src/components/infoBundle.ts', }; +const invalidExposeKeys = Object.keys(remoteExposeImports).filter( + exposeKey => !exposeKey.startsWith('./'), +); +if (invalidExposeKeys.length > 0) { + throw new Error( + `Remote expose keys must be module-federation paths starting with "./". Invalid keys: ${invalidExposeKeys.join(', ')}`, + ); +} const COMPONENT_EXPOSE_PREFIX = './src/components/'; const nonComponentExposeEntries = Object.entries(remoteExposeImports).filter( ([, importPath]) => !importPath.startsWith(COMPONENT_EXPOSE_PREFIX), @@ -46,6 +54,16 @@ if (nonComponentExposeEntries.length > 0) { .join(', ')}`, ); } +const callbackExposeEntries = Object.entries(remoteExposeImports).filter( + ([, importPath]) => importPath === CALLBACK_BOOTSTRAP_IMPORT, +); +if (callbackExposeEntries.length > 0) { + throw new Error( + `Callback bootstrap module (${CALLBACK_BOOTSTRAP_IMPORT}) must remain internal-only and cannot be exposed. Invalid entries: ${callbackExposeEntries + .map(([exposeKey]) => exposeKey) + .join(', ')}`, + ); +} const sharedByScope = [ { diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 6e11220c6a9a..62eec23da6fe 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -589,6 +589,8 @@ function runTests({ mode }: TestConfig) { expect(moduleFederationConfigSource).toContain( 'nonComponentExposeEntries', ); + expect(moduleFederationConfigSource).toContain('invalidExposeKeys'); + expect(moduleFederationConfigSource).toContain('callbackExposeEntries'); expect(moduleFederationConfigSource).toContain("shareScope: 'default'"); expect(moduleFederationConfigSource).toContain("shareScope: 'ssr'"); expect(moduleFederationConfigSource).toContain("shareScope: 'rsc'"); From 8435ed645898ecf470be99321ce8cc0bb6155369 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 04:26:43 +0000 Subject: [PATCH 147/324] test(rsc-mf): assert callback posting contract in runtime register source --- tests/integration/rsc-mf/tests/index.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 62eec23da6fe..424117974597 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -568,6 +568,19 @@ function runTests({ mode }: TestConfig) { "from './registerServerCallback'", ); expect(runtimeRegisterSource).toContain('setServerCallback'); + expect(runtimeRegisterSource).toContain("remoteAlias = 'rscRemote'"); + expect(runtimeRegisterSource).toContain( + "if (rawActionId.startsWith('remote:'))", + ); + expect(runtimeRegisterSource).toContain( + 'return `remote:${remoteAlias}:${rawActionId}`', + ); + expect(runtimeRegisterSource).toContain("'x-rsc-action': hostActionId"); + expect(runtimeRegisterSource).toContain("method: 'POST'"); + expect(runtimeRegisterSource).toContain("Accept: 'text/x-component'"); + expect(runtimeRegisterSource).not.toContain( + 'remoteActionIdToHostProxyActionId', + ); expect(moduleFederationConfigSource).toContain( 'CALLBACK_BOOTSTRAP_IMPORT', ); From b28577bbdf49a98ff6eb96d38b364c7e5d094b4b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 04:29:33 +0000 Subject: [PATCH 148/324] test(rsc-mf): lock runtime callback transport header semantics --- tests/integration/rsc-mf/tests/index.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 424117974597..6e9d426f2928 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -560,10 +560,14 @@ function runTests({ mode }: TestConfig) { expect(runtimeInitSource).toContain('bootstrapServerCallback'); expect(runtimeInitSource).toContain('callbackBootstrapPromise'); expect(runtimeInitSource).toContain("import('./registerServerCallback')"); + expect(runtimeInitSource).toContain('window.location.origin'); + expect(runtimeInitSource).toContain('window.location.pathname'); expect(runtimeInitSource).toContain('callbackBootstrapPromise.catch'); expect(runtimeInitSource).toContain( 'callbackBootstrapPromise = undefined', ); + expect(runtimeInitSource).not.toContain('RSC_MF_REMOTE_PORT'); + expect(runtimeInitSource).not.toContain('127.0.0.1:'); expect(runtimeInitSource).not.toContain( "from './registerServerCallback'", ); From 82936feb94f948722f8c95fc628df5bf1bfd56d1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 04:32:51 +0000 Subject: [PATCH 149/324] test(rsc-mf): assert callback bootstrap derives host origin dynamically --- tests/integration/rsc-mf/tests/index.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 6e9d426f2928..36999c94a7f5 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -367,6 +367,7 @@ function runTests({ mode }: TestConfig) { const runtimeErrors: string[] = []; const actionRequestUrls: string[] = []; const actionRequestIds: string[] = []; + const actionRequestAcceptHeaders: string[] = []; const registerCallbackExposeRequestUrls: string[] = []; if (skipForLowerNodeVersion()) { @@ -430,6 +431,7 @@ function runTests({ mode }: TestConfig) { } actionRequestUrls.push(url); actionRequestIds.push(headers['x-rsc-action']); + actionRequestAcceptHeaders.push(headers.accept || ''); }); }); @@ -659,6 +661,7 @@ function runTests({ mode }: TestConfig) { it('should route remote actions through host endpoint', () => { expect(actionRequestUrls.length).toBe(EXPECTED_ACTION_POSTS_PER_MODE); expect(actionRequestUrls.length).toBe(actionRequestIds.length); + expect(actionRequestUrls.length).toBe(actionRequestAcceptHeaders.length); const uniqueActionRequestUrls = Array.from(new Set(actionRequestUrls)); expect(uniqueActionRequestUrls).toEqual([ `http://127.0.0.1:${hostPort}${HOST_RSC_URL}`, @@ -678,12 +681,18 @@ function runTests({ mode }: TestConfig) { it('should post bridge-prefixed action ids for remote actions', async () => { expect(actionRequestIds.length).toBe(EXPECTED_ACTION_POSTS_PER_MODE); expect(actionRequestIds.length).toBe(actionRequestUrls.length); + expect(actionRequestIds.length).toBe(actionRequestAcceptHeaders.length); const uniqueActionRequestIds = new Set(actionRequestIds); expect( actionRequestIds.every(id => /^remote:rscRemote:[a-f0-9]{64,}$/i.test(id), ), ).toBe(true); + expect( + actionRequestAcceptHeaders.every( + acceptHeader => acceptHeader.toLowerCase() === 'text/x-component', + ), + ).toBe(true); expect(uniqueActionRequestIds.size).toBe( EXPECTED_UNIQUE_ACTION_IDS_PER_MODE, ); From 010fc9b4eb5130a5cb5813d389d42faa826afb0e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 04:35:56 +0000 Subject: [PATCH 150/324] refactor(rsc-mf): normalize callback action URL and assert request Accept headers --- .../remote/src/runtime/registerServerCallback.ts | 10 ++++++++-- tests/integration/rsc-mf/tests/index.test.ts | 4 ++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts index 8230bc186531..bd6987c65da7 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts @@ -14,6 +14,12 @@ const getHostActionId = (rawActionId: string, remoteAlias: string) => { // Align with RSC bridge action-id format expected by host runtime plugin. return `remote:${remoteAlias}:${rawActionId}`; }; +const getNormalizedRemoteActionUrl = (remoteOrigin: string) => { + const url = new URL(remoteOrigin); + url.search = ''; + url.hash = ''; + return url.toString(); +}; export function registerRemoteServerCallback( remoteOrigin: string, @@ -22,15 +28,15 @@ export function registerRemoteServerCallback( if (!remoteOrigin) { return; } + const remoteActionUrl = getNormalizedRemoteActionUrl(remoteOrigin); const callbackKey = JSON.stringify({ remoteAlias, - remoteOrigin, + remoteActionUrl, }); if (registeredCallbackKey === callbackKey) { return; } - const remoteActionUrl = new URL(remoteOrigin).toString(); setServerCallback(async (id, args) => { const hostActionId = getHostActionId(id, remoteAlias); const temporaryReferences = createTemporaryReferenceSet(); diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 36999c94a7f5..11020f55865d 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -584,6 +584,10 @@ function runTests({ mode }: TestConfig) { expect(runtimeRegisterSource).toContain("'x-rsc-action': hostActionId"); expect(runtimeRegisterSource).toContain("method: 'POST'"); expect(runtimeRegisterSource).toContain("Accept: 'text/x-component'"); + expect(runtimeRegisterSource).toContain('getNormalizedRemoteActionUrl'); + expect(runtimeRegisterSource).toContain("url.search = ''"); + expect(runtimeRegisterSource).toContain("url.hash = ''"); + expect(runtimeRegisterSource).toContain('remoteActionUrl,'); expect(runtimeRegisterSource).not.toContain( 'remoteActionIdToHostProxyActionId', ); From 91e183eb19c1022c05f7db29ee491525b57de4c7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 04:38:55 +0000 Subject: [PATCH 151/324] test(rsc-mf): verify action requests preserve RSC accept header --- tests/integration/rsc-mf/tests/index.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 11020f55865d..19e9146d7143 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -645,10 +645,22 @@ function runTests({ mode }: TestConfig) { ); expect(hostModernConfigSource).not.toContain('preEntry'); expect(hostModernConfigSource).not.toContain('registerServerCallback'); + expect(hostModernConfigSource).toContain('enableAsyncEntry: false'); + expect(hostModernConfigSource).toContain("chain.target('async-node')"); + expect(hostModernConfigSource).toContain("'server-only$'"); + expect(hostModernConfigSource).toContain( + 'moduleFederationPlugin({ ssr: true })', + ); expect(remoteModernConfigSource).not.toContain('chunkLoadingGlobal'); expect(remoteModernConfigSource).toContain( 'rsc-mf-react-server-dom-client-browser$', ); + expect(remoteModernConfigSource).toContain('enableAsyncEntry: false'); + expect(remoteModernConfigSource).toContain("chain.target('async-node')"); + expect(remoteModernConfigSource).toContain('splitChunks(false)'); + expect(remoteModernConfigSource).toContain( + 'moduleFederationPlugin({ ssr: true })', + ); }); it('should not load callback helper expose chunk', () => { From 2dbf23670102fa4311085852ebf404bdfe25a50a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 04:51:15 +0000 Subject: [PATCH 152/324] test(rsc-mf): verify expose import files exist via source scan --- tests/integration/rsc-mf/tests/index.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 19e9146d7143..8ce8e8e0c0ed 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -627,6 +627,11 @@ function runTests({ mode }: TestConfig) { importPath.startsWith('./src/components/'), ), ).toBe(true); + expect( + remoteExposeEntries.every(({ importPath }) => + fs.existsSync(path.resolve(remoteDir, importPath)), + ), + ).toBe(true); expect(hostModuleFederationConfigSource).toContain('runtimePlugins'); expect(hostModuleFederationConfigSource).toContain( './runtime/forceRemotePublicPath.ts', From d18d90bf9eb6d63328c749a87954b4519a3e2eea Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 04:56:36 +0000 Subject: [PATCH 153/324] refactor(rsc-mf): require explicit TypeScript expose entry extensions --- .../rsc-mf/remote/module-federation.config.ts | 10 ++++++++++ tests/integration/rsc-mf/tests/index.test.ts | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/tests/integration/rsc-mf/remote/module-federation.config.ts b/tests/integration/rsc-mf/remote/module-federation.config.ts index d35b9318ba82..c8cda0850c4b 100644 --- a/tests/integration/rsc-mf/remote/module-federation.config.ts +++ b/tests/integration/rsc-mf/remote/module-federation.config.ts @@ -54,6 +54,16 @@ if (nonComponentExposeEntries.length > 0) { .join(', ')}`, ); } +const nonTypeScriptExposeEntries = Object.entries(remoteExposeImports).filter( + ([, importPath]) => !/\.[tj]sx?$/.test(importPath), +); +if (nonTypeScriptExposeEntries.length > 0) { + throw new Error( + `Remote expose imports must use explicit TypeScript entry extensions for deterministic resolution. Invalid entries: ${nonTypeScriptExposeEntries + .map(([exposeKey, importPath]) => `${exposeKey} -> ${importPath}`) + .join(', ')}`, + ); +} const callbackExposeEntries = Object.entries(remoteExposeImports).filter( ([, importPath]) => importPath === CALLBACK_BOOTSTRAP_IMPORT, ); diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 8ce8e8e0c0ed..6d6e9d5f9487 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -612,6 +612,9 @@ function runTests({ mode }: TestConfig) { expect(moduleFederationConfigSource).toContain( 'nonComponentExposeEntries', ); + expect(moduleFederationConfigSource).toContain( + 'nonTypeScriptExposeEntries', + ); expect(moduleFederationConfigSource).toContain('invalidExposeKeys'); expect(moduleFederationConfigSource).toContain('callbackExposeEntries'); expect(moduleFederationConfigSource).toContain("shareScope: 'default'"); @@ -627,6 +630,11 @@ function runTests({ mode }: TestConfig) { importPath.startsWith('./src/components/'), ), ).toBe(true); + expect( + remoteExposeEntries.every(({ importPath }) => + /\.[tj]sx?$/.test(importPath), + ), + ).toBe(true); expect( remoteExposeEntries.every(({ importPath }) => fs.existsSync(path.resolve(remoteDir, importPath)), From fbd36bdfb45426dfe7398693e67bef8b55e8c7ad Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 04:59:07 +0000 Subject: [PATCH 154/324] test(rsc-mf): assert runtime exposes wrapper directory stays empty --- tests/integration/rsc-mf/tests/index.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 6d6e9d5f9487..00f547bd1b63 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -528,6 +528,15 @@ function runTests({ mode }: TestConfig) { path.join(remoteDir, 'modern.config.ts'), 'utf-8', ); + const remoteRuntimeExposesDir = path.join( + remoteDir, + 'src/runtime/exposes', + ); + const remoteRuntimeExposeEntries = fs.existsSync(remoteRuntimeExposesDir) + ? fs + .readdirSync(remoteRuntimeExposesDir) + .filter(entryName => !entryName.startsWith('.')) + : []; const remoteExposeEntries = getRemoteExposeEntries( moduleFederationConfigSource, ); @@ -608,6 +617,7 @@ function runTests({ mode }: TestConfig) { expect(moduleFederationConfigSource).not.toContain( './src/runtime/exposes/', ); + expect(remoteRuntimeExposeEntries).toEqual([]); expect(moduleFederationConfigSource).toContain('COMPONENT_EXPOSE_PREFIX'); expect(moduleFederationConfigSource).toContain( 'nonComponentExposeEntries', From b0e1593e36a6f50c6616308223ce94b8ecf47d9f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 05:02:42 +0000 Subject: [PATCH 155/324] test(rsc-mf): lock runtime callback helper file surface --- tests/integration/rsc-mf/tests/index.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 00f547bd1b63..0239671cbf5c 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -537,6 +537,13 @@ function runTests({ mode }: TestConfig) { .readdirSync(remoteRuntimeExposesDir) .filter(entryName => !entryName.startsWith('.')) : []; + const remoteRuntimeEntries = fs + .readdirSync(path.join(remoteDir, 'src/runtime')) + .filter(entryName => !entryName.startsWith('.')) + .sort(); + const remoteRuntimeEntriesWithoutExposeDir = remoteRuntimeEntries.filter( + entryName => entryName !== 'exposes', + ); const remoteExposeEntries = getRemoteExposeEntries( moduleFederationConfigSource, ); @@ -618,6 +625,10 @@ function runTests({ mode }: TestConfig) { './src/runtime/exposes/', ); expect(remoteRuntimeExposeEntries).toEqual([]); + expect(remoteRuntimeEntriesWithoutExposeDir).toEqual([ + 'initServerCallback.ts', + 'registerServerCallback.ts', + ]); expect(moduleFederationConfigSource).toContain('COMPONENT_EXPOSE_PREFIX'); expect(moduleFederationConfigSource).toContain( 'nonComponentExposeEntries', From 114deee32206c731ee5626ec9eae46c4116ca889 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 05:06:08 +0000 Subject: [PATCH 156/324] test(rsc-mf): lock callback alias usage to runtime helpers --- tests/integration/rsc-mf/tests/index.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 0239671cbf5c..f7684b1de452 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -566,6 +566,13 @@ function runTests({ mode }: TestConfig) { source => !source.includes('registerRemoteServerCallback'), ), ).toBe(true); + expect( + componentSources.every( + source => + !source.includes('setServerCallback') && + !source.includes('rsc-mf-react-server-dom-client-browser'), + ), + ).toBe(true); expect( hostSourceTexts.every( source => @@ -590,6 +597,12 @@ function runTests({ mode }: TestConfig) { "from './registerServerCallback'", ); expect(runtimeRegisterSource).toContain('setServerCallback'); + expect(runtimeRegisterSource).toContain( + "from 'rsc-mf-react-server-dom-client-browser'", + ); + expect(runtimeRegisterSource).not.toContain( + "from 'react-server-dom-rspack/client.browser'", + ); expect(runtimeRegisterSource).toContain("remoteAlias = 'rscRemote'"); expect(runtimeRegisterSource).toContain( "if (rawActionId.startsWith('remote:'))", @@ -689,6 +702,9 @@ function runTests({ mode }: TestConfig) { expect(remoteModernConfigSource).toContain( 'rsc-mf-react-server-dom-client-browser$', ); + expect(remoteModernConfigSource).toContain( + 'react-server-dom-rspack/client.browser', + ); expect(remoteModernConfigSource).toContain('enableAsyncEntry: false'); expect(remoteModernConfigSource).toContain("chain.target('async-node')"); expect(remoteModernConfigSource).toContain('splitChunks(false)'); From f071c8d0071464fb95c6aa5e61cba33ec55297a1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 05:08:48 +0000 Subject: [PATCH 157/324] test(rsc-mf): lock runtime callback initialization contract --- tests/integration/rsc-mf/tests/index.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index f7684b1de452..2480a92f1f39 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -584,25 +584,36 @@ function runTests({ mode }: TestConfig) { expect(runtimeInitSource).toContain('registerRemoteServerCallback'); expect(runtimeInitSource).toContain('bootstrapServerCallback'); expect(runtimeInitSource).toContain('callbackBootstrapPromise'); + expect(runtimeInitSource).toContain("if (typeof window !== 'undefined')"); expect(runtimeInitSource).toContain("import('./registerServerCallback')"); + expect(runtimeInitSource).toContain('void bootstrapServerCallback()'); expect(runtimeInitSource).toContain('window.location.origin'); expect(runtimeInitSource).toContain('window.location.pathname'); expect(runtimeInitSource).toContain('callbackBootstrapPromise.catch'); expect(runtimeInitSource).toContain( 'callbackBootstrapPromise = undefined', ); + expect(runtimeInitSource).not.toContain('setServerCallback('); expect(runtimeInitSource).not.toContain('RSC_MF_REMOTE_PORT'); expect(runtimeInitSource).not.toContain('127.0.0.1:'); expect(runtimeInitSource).not.toContain( "from './registerServerCallback'", ); expect(runtimeRegisterSource).toContain('setServerCallback'); + expect(runtimeRegisterSource).toContain('createTemporaryReferenceSet'); + expect(runtimeRegisterSource).toContain( + 'encodeReply(args, { temporaryReferences })', + ); + expect(runtimeRegisterSource).toContain( + 'createFromFetch(response, { temporaryReferences })', + ); expect(runtimeRegisterSource).toContain( "from 'rsc-mf-react-server-dom-client-browser'", ); expect(runtimeRegisterSource).not.toContain( "from 'react-server-dom-rspack/client.browser'", ); + expect(runtimeRegisterSource).not.toContain('window.location'); expect(runtimeRegisterSource).toContain("remoteAlias = 'rscRemote'"); expect(runtimeRegisterSource).toContain( "if (rawActionId.startsWith('remote:'))", @@ -681,6 +692,7 @@ function runTests({ mode }: TestConfig) { expect(hostModuleFederationConfigSource).toContain( '/static/mf-manifest.json', ); + expect(hostModuleFederationConfigSource).toContain('RSC_MF_REMOTE_PORT'); expect(hostModuleFederationConfigSource).toContain('rscRemote:'); expect(hostModuleFederationConfigSource).toContain('asyncStartup: true'); expect(hostModuleFederationConfigSource).toContain('rsc: true'); From 1dba1cfd80a8c5f7c6ac896e9e5e6aea4007e200 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 05:11:28 +0000 Subject: [PATCH 158/324] test(rsc-mf): prevent host callback runtime leakage --- tests/integration/rsc-mf/tests/index.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 2480a92f1f39..5c7b8af1d66b 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -578,7 +578,9 @@ function runTests({ mode }: TestConfig) { source => !source.includes('registerRemoteServerCallback') && !source.includes('initServerCallback') && - !source.includes('registerServerCallback'), + !source.includes('registerServerCallback') && + !source.includes('setServerCallback') && + !source.includes('rsc-mf-react-server-dom-client-browser'), ), ).toBe(true); expect(runtimeInitSource).toContain('registerRemoteServerCallback'); @@ -613,6 +615,7 @@ function runTests({ mode }: TestConfig) { expect(runtimeRegisterSource).not.toContain( "from 'react-server-dom-rspack/client.browser'", ); + expect(runtimeRegisterSource).not.toContain('127.0.0.1:'); expect(runtimeRegisterSource).not.toContain('window.location'); expect(runtimeRegisterSource).toContain("remoteAlias = 'rscRemote'"); expect(runtimeRegisterSource).toContain( @@ -689,6 +692,10 @@ function runTests({ mode }: TestConfig) { expect(hostModuleFederationConfigSource).toContain( './runtime/forceRemotePublicPath.ts', ); + expect(hostModuleFederationConfigSource).toContain( + "process.env.NODE_ENV === 'production'", + ); + expect(hostModuleFederationConfigSource).toContain(': []'); expect(hostModuleFederationConfigSource).toContain( '/static/mf-manifest.json', ); From 99b81e18d0e85c9d04974427c745e0a90a2125eb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 05:13:08 +0000 Subject: [PATCH 159/324] refactor(rsc-mf): guard expose imports against traversal paths --- .../rsc-mf/remote/module-federation.config.ts | 20 +++++++++++++++++++ tests/integration/rsc-mf/tests/index.test.ts | 10 ++++++++++ 2 files changed, 30 insertions(+) diff --git a/tests/integration/rsc-mf/remote/module-federation.config.ts b/tests/integration/rsc-mf/remote/module-federation.config.ts index c8cda0850c4b..b6f1d60d3631 100644 --- a/tests/integration/rsc-mf/remote/module-federation.config.ts +++ b/tests/integration/rsc-mf/remote/module-federation.config.ts @@ -64,6 +64,26 @@ if (nonTypeScriptExposeEntries.length > 0) { .join(', ')}`, ); } +const parentTraversalExposeEntries = Object.entries(remoteExposeImports).filter( + ([, importPath]) => importPath.includes('..'), +); +if (parentTraversalExposeEntries.length > 0) { + throw new Error( + `Remote expose imports must not contain parent directory traversal segments. Invalid entries: ${parentTraversalExposeEntries + .map(([exposeKey, importPath]) => `${exposeKey} -> ${importPath}`) + .join(', ')}`, + ); +} +const nonPosixExposeEntries = Object.entries(remoteExposeImports).filter( + ([, importPath]) => importPath.includes('\\'), +); +if (nonPosixExposeEntries.length > 0) { + throw new Error( + `Remote expose imports must use POSIX separators for deterministic module ids. Invalid entries: ${nonPosixExposeEntries + .map(([exposeKey, importPath]) => `${exposeKey} -> ${importPath}`) + .join(', ')}`, + ); +} const callbackExposeEntries = Object.entries(remoteExposeImports).filter( ([, importPath]) => importPath === CALLBACK_BOOTSTRAP_IMPORT, ); diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 5c7b8af1d66b..d720d01a3fb5 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -663,6 +663,10 @@ function runTests({ mode }: TestConfig) { expect(moduleFederationConfigSource).toContain( 'nonTypeScriptExposeEntries', ); + expect(moduleFederationConfigSource).toContain( + 'parentTraversalExposeEntries', + ); + expect(moduleFederationConfigSource).toContain('nonPosixExposeEntries'); expect(moduleFederationConfigSource).toContain('invalidExposeKeys'); expect(moduleFederationConfigSource).toContain('callbackExposeEntries'); expect(moduleFederationConfigSource).toContain("shareScope: 'default'"); @@ -683,6 +687,12 @@ function runTests({ mode }: TestConfig) { /\.[tj]sx?$/.test(importPath), ), ).toBe(true); + expect( + remoteExposeEntries.every( + ({ importPath }) => + !importPath.includes('..') && !importPath.includes('\\'), + ), + ).toBe(true); expect( remoteExposeEntries.every(({ importPath }) => fs.existsSync(path.resolve(remoteDir, importPath)), From f8e30d16f4ebdd2c26ed047b4b345a240600b726 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 05:16:09 +0000 Subject: [PATCH 160/324] refactor(rsc-mf): validate callback bootstrap import boundaries --- .../rsc-mf/remote/module-federation.config.ts | 19 +++++++++++++++++++ tests/integration/rsc-mf/tests/index.test.ts | 12 ++++++++++++ 2 files changed, 31 insertions(+) diff --git a/tests/integration/rsc-mf/remote/module-federation.config.ts b/tests/integration/rsc-mf/remote/module-federation.config.ts index b6f1d60d3631..c28fbb37f8a8 100644 --- a/tests/integration/rsc-mf/remote/module-federation.config.ts +++ b/tests/integration/rsc-mf/remote/module-federation.config.ts @@ -15,6 +15,25 @@ const reactDomServerImport = path.join( ); const reactServerDomClientImport = 'react-server-dom-rspack/client.browser'; const CALLBACK_BOOTSTRAP_IMPORT = './src/runtime/initServerCallback.ts'; +const CALLBACK_BOOTSTRAP_PREFIX = './src/runtime/'; +if (!CALLBACK_BOOTSTRAP_IMPORT.startsWith(CALLBACK_BOOTSTRAP_PREFIX)) { + throw new Error( + `Callback bootstrap import must stay in runtime namespace (${CALLBACK_BOOTSTRAP_PREFIX}). Received: ${CALLBACK_BOOTSTRAP_IMPORT}`, + ); +} +if (!/\.[tj]sx?$/.test(CALLBACK_BOOTSTRAP_IMPORT)) { + throw new Error( + `Callback bootstrap import must use explicit source extension for deterministic resolution. Received: ${CALLBACK_BOOTSTRAP_IMPORT}`, + ); +} +if ( + CALLBACK_BOOTSTRAP_IMPORT.includes('..') || + CALLBACK_BOOTSTRAP_IMPORT.includes('\\') +) { + throw new Error( + `Callback bootstrap import must not contain traversal or Windows separators. Received: ${CALLBACK_BOOTSTRAP_IMPORT}`, + ); +} const createRscExpose = (importPath: string) => ({ import: [CALLBACK_BOOTSTRAP_IMPORT, importPath], diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index d720d01a3fb5..53ae830f477c 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -637,6 +637,18 @@ function runTests({ mode }: TestConfig) { expect(moduleFederationConfigSource).toContain( 'CALLBACK_BOOTSTRAP_IMPORT', ); + expect(moduleFederationConfigSource).toContain( + 'CALLBACK_BOOTSTRAP_PREFIX', + ); + expect(moduleFederationConfigSource).toContain( + 'Callback bootstrap import must stay in runtime namespace', + ); + expect(moduleFederationConfigSource).toContain( + 'Callback bootstrap import must use explicit source extension', + ); + expect(moduleFederationConfigSource).toContain( + 'Callback bootstrap import must not contain traversal or Windows separators', + ); expect( moduleFederationConfigSource.includes( '[CALLBACK_BOOTSTRAP_IMPORT, importPath]', From 5762be3c7eafacc75dcabd03457ea617be98601f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 05:19:14 +0000 Subject: [PATCH 161/324] refactor(rsc-mf): validate remote alias before bridge action prefixing --- .../src/runtime/registerServerCallback.ts | 10 ++++++++-- tests/integration/rsc-mf/tests/index.test.ts | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts index bd6987c65da7..dce97b6378c5 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts @@ -28,9 +28,15 @@ export function registerRemoteServerCallback( if (!remoteOrigin) { return; } + const normalizedRemoteAlias = remoteAlias.trim(); + if (!normalizedRemoteAlias || normalizedRemoteAlias.includes(':')) { + throw new Error( + `Remote alias must be a non-empty identifier without ":" delimiters. Received: ${remoteAlias}`, + ); + } const remoteActionUrl = getNormalizedRemoteActionUrl(remoteOrigin); const callbackKey = JSON.stringify({ - remoteAlias, + remoteAlias: normalizedRemoteAlias, remoteActionUrl, }); if (registeredCallbackKey === callbackKey) { @@ -38,7 +44,7 @@ export function registerRemoteServerCallback( } setServerCallback(async (id, args) => { - const hostActionId = getHostActionId(id, remoteAlias); + const hostActionId = getHostActionId(id, normalizedRemoteAlias); const temporaryReferences = createTemporaryReferenceSet(); const response = fetch(remoteActionUrl, { method: 'POST', diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 53ae830f477c..e11e7bf1ad8d 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -618,6 +618,15 @@ function runTests({ mode }: TestConfig) { expect(runtimeRegisterSource).not.toContain('127.0.0.1:'); expect(runtimeRegisterSource).not.toContain('window.location'); expect(runtimeRegisterSource).toContain("remoteAlias = 'rscRemote'"); + expect(runtimeRegisterSource).toContain( + 'const normalizedRemoteAlias = remoteAlias.trim()', + ); + expect(runtimeRegisterSource).toContain( + "!normalizedRemoteAlias || normalizedRemoteAlias.includes(':')", + ); + expect(runtimeRegisterSource).toContain( + 'Remote alias must be a non-empty identifier without ":" delimiters', + ); expect(runtimeRegisterSource).toContain( "if (rawActionId.startsWith('remote:'))", ); @@ -630,7 +639,16 @@ function runTests({ mode }: TestConfig) { expect(runtimeRegisterSource).toContain('getNormalizedRemoteActionUrl'); expect(runtimeRegisterSource).toContain("url.search = ''"); expect(runtimeRegisterSource).toContain("url.hash = ''"); + expect(runtimeRegisterSource).toContain( + 'remoteAlias: normalizedRemoteAlias', + ); + expect(runtimeRegisterSource).toContain( + 'getHostActionId(id, normalizedRemoteAlias)', + ); expect(runtimeRegisterSource).toContain('remoteActionUrl,'); + expect(runtimeRegisterSource).not.toContain( + 'getHostActionId(id, remoteAlias)', + ); expect(runtimeRegisterSource).not.toContain( 'remoteActionIdToHostProxyActionId', ); From 71afe3b5ad815f90ac6568dd6d0566782d20ecfa Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 05:22:41 +0000 Subject: [PATCH 162/324] refactor(rsc-mf): enforce http(s) callback action URLs --- .../rsc-mf/remote/src/runtime/registerServerCallback.ts | 5 +++++ tests/integration/rsc-mf/tests/index.test.ts | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts index dce97b6378c5..dd711123b21c 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts @@ -16,6 +16,11 @@ const getHostActionId = (rawActionId: string, remoteAlias: string) => { }; const getNormalizedRemoteActionUrl = (remoteOrigin: string) => { const url = new URL(remoteOrigin); + if (url.protocol !== 'http:' && url.protocol !== 'https:') { + throw new Error( + `Remote action callback URL must use http or https. Received protocol: ${url.protocol}`, + ); + } url.search = ''; url.hash = ''; return url.toString(); diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index e11e7bf1ad8d..7cd717c6f651 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -637,6 +637,12 @@ function runTests({ mode }: TestConfig) { expect(runtimeRegisterSource).toContain("method: 'POST'"); expect(runtimeRegisterSource).toContain("Accept: 'text/x-component'"); expect(runtimeRegisterSource).toContain('getNormalizedRemoteActionUrl'); + expect(runtimeRegisterSource).toContain( + "url.protocol !== 'http:' && url.protocol !== 'https:'", + ); + expect(runtimeRegisterSource).toContain( + 'Remote action callback URL must use http or https', + ); expect(runtimeRegisterSource).toContain("url.search = ''"); expect(runtimeRegisterSource).toContain("url.hash = ''"); expect(runtimeRegisterSource).toContain( From d72f82c2f6b4b6ea7895b2165ce51eab756851ed Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 05:25:38 +0000 Subject: [PATCH 163/324] refactor(rsc-mf): reject callback URLs with embedded credentials --- .../remote/src/runtime/registerServerCallback.ts | 10 ++++++++-- tests/integration/rsc-mf/tests/index.test.ts | 10 ++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts index dd711123b21c..de999e890b1e 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts @@ -21,6 +21,11 @@ const getNormalizedRemoteActionUrl = (remoteOrigin: string) => { `Remote action callback URL must use http or https. Received protocol: ${url.protocol}`, ); } + if (url.username || url.password) { + throw new Error( + 'Remote action callback URL must not include embedded credentials.', + ); + } url.search = ''; url.hash = ''; return url.toString(); @@ -30,7 +35,8 @@ export function registerRemoteServerCallback( remoteOrigin: string, remoteAlias = 'rscRemote', ) { - if (!remoteOrigin) { + const normalizedRemoteOrigin = remoteOrigin.trim(); + if (!normalizedRemoteOrigin) { return; } const normalizedRemoteAlias = remoteAlias.trim(); @@ -39,7 +45,7 @@ export function registerRemoteServerCallback( `Remote alias must be a non-empty identifier without ":" delimiters. Received: ${remoteAlias}`, ); } - const remoteActionUrl = getNormalizedRemoteActionUrl(remoteOrigin); + const remoteActionUrl = getNormalizedRemoteActionUrl(normalizedRemoteOrigin); const callbackKey = JSON.stringify({ remoteAlias: normalizedRemoteAlias, remoteActionUrl, diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 7cd717c6f651..f6673091f239 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -643,6 +643,9 @@ function runTests({ mode }: TestConfig) { expect(runtimeRegisterSource).toContain( 'Remote action callback URL must use http or https', ); + expect(runtimeRegisterSource).toContain( + 'Remote action callback URL must not include embedded credentials.', + ); expect(runtimeRegisterSource).toContain("url.search = ''"); expect(runtimeRegisterSource).toContain("url.hash = ''"); expect(runtimeRegisterSource).toContain( @@ -651,10 +654,17 @@ function runTests({ mode }: TestConfig) { expect(runtimeRegisterSource).toContain( 'getHostActionId(id, normalizedRemoteAlias)', ); + expect(runtimeRegisterSource).toContain( + 'const normalizedRemoteOrigin = remoteOrigin.trim()', + ); + expect(runtimeRegisterSource).toContain('if (!normalizedRemoteOrigin)'); expect(runtimeRegisterSource).toContain('remoteActionUrl,'); expect(runtimeRegisterSource).not.toContain( 'getHostActionId(id, remoteAlias)', ); + expect(runtimeRegisterSource).not.toContain( + 'getNormalizedRemoteActionUrl(remoteOrigin)', + ); expect(runtimeRegisterSource).not.toContain( 'remoteActionIdToHostProxyActionId', ); From aa01c52e563102bd0d490d0c051bb6745a525272 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 05:29:28 +0000 Subject: [PATCH 164/324] refactor(rsc-mf): trim callback origin and block URL credentials --- .../remote/src/runtime/registerServerCallback.ts | 9 +++++++-- tests/integration/rsc-mf/tests/index.test.ts | 10 ++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts index de999e890b1e..d93084b38bf6 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts @@ -6,6 +6,7 @@ import { } from 'rsc-mf-react-server-dom-client-browser'; let registeredCallbackKey = ''; +const ALIAS_TOKEN_PATTERN = /^[A-Za-z0-9_.-]+$/; const getHostActionId = (rawActionId: string, remoteAlias: string) => { if (rawActionId.startsWith('remote:')) { return rawActionId; @@ -40,9 +41,13 @@ export function registerRemoteServerCallback( return; } const normalizedRemoteAlias = remoteAlias.trim(); - if (!normalizedRemoteAlias || normalizedRemoteAlias.includes(':')) { + if ( + !normalizedRemoteAlias || + normalizedRemoteAlias.includes(':') || + !ALIAS_TOKEN_PATTERN.test(normalizedRemoteAlias) + ) { throw new Error( - `Remote alias must be a non-empty identifier without ":" delimiters. Received: ${remoteAlias}`, + `Remote alias must be a non-empty token (letters, numbers, "-", "_", ".") without ":" delimiters. Received: ${remoteAlias}`, ); } const remoteActionUrl = getNormalizedRemoteActionUrl(normalizedRemoteOrigin); diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index f6673091f239..70dba3c77b0d 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -618,14 +618,20 @@ function runTests({ mode }: TestConfig) { expect(runtimeRegisterSource).not.toContain('127.0.0.1:'); expect(runtimeRegisterSource).not.toContain('window.location'); expect(runtimeRegisterSource).toContain("remoteAlias = 'rscRemote'"); + expect(runtimeRegisterSource).toContain( + 'const ALIAS_TOKEN_PATTERN = /^[A-Za-z0-9_.-]+$/', + ); expect(runtimeRegisterSource).toContain( 'const normalizedRemoteAlias = remoteAlias.trim()', ); expect(runtimeRegisterSource).toContain( - "!normalizedRemoteAlias || normalizedRemoteAlias.includes(':')", + "normalizedRemoteAlias.includes(':')", + ); + expect(runtimeRegisterSource).toContain( + '!ALIAS_TOKEN_PATTERN.test(normalizedRemoteAlias)', ); expect(runtimeRegisterSource).toContain( - 'Remote alias must be a non-empty identifier without ":" delimiters', + 'Remote alias must be a non-empty token (letters, numbers, "-", "_", ".") without ":" delimiters', ); expect(runtimeRegisterSource).toContain( "if (rawActionId.startsWith('remote:'))", From 916209cc81c0181b14caa82a4bd26781e26bde6b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 05:36:26 +0000 Subject: [PATCH 165/324] refactor(rsc-mf): normalize raw action ids before bridge prefix --- .../src/runtime/registerServerCallback.ts | 16 ++++++++++++--- tests/integration/rsc-mf/tests/index.test.ts | 20 ++----------------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts index d93084b38bf6..a7c38f4dd2f5 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts @@ -7,13 +7,23 @@ import { let registeredCallbackKey = ''; const ALIAS_TOKEN_PATTERN = /^[A-Za-z0-9_.-]+$/; +const getNormalizedRawActionId = (rawActionId: string) => { + const normalizedRawActionId = rawActionId.trim(); + if (!normalizedRawActionId || /\s/.test(normalizedRawActionId)) { + throw new Error( + `Remote action id must be a non-empty token without whitespace. Received: ${rawActionId}`, + ); + } + return normalizedRawActionId; +}; const getHostActionId = (rawActionId: string, remoteAlias: string) => { - if (rawActionId.startsWith('remote:')) { - return rawActionId; + const normalizedRawActionId = getNormalizedRawActionId(rawActionId); + if (normalizedRawActionId.startsWith('remote:')) { + return normalizedRawActionId; } // Align with RSC bridge action-id format expected by host runtime plugin. - return `remote:${remoteAlias}:${rawActionId}`; + return `remote:${remoteAlias}:${normalizedRawActionId}`; }; const getNormalizedRemoteActionUrl = (remoteOrigin: string) => { const url = new URL(remoteOrigin); diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 70dba3c77b0d..d67f02a3eed8 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -618,27 +618,11 @@ function runTests({ mode }: TestConfig) { expect(runtimeRegisterSource).not.toContain('127.0.0.1:'); expect(runtimeRegisterSource).not.toContain('window.location'); expect(runtimeRegisterSource).toContain("remoteAlias = 'rscRemote'"); - expect(runtimeRegisterSource).toContain( - 'const ALIAS_TOKEN_PATTERN = /^[A-Za-z0-9_.-]+$/', - ); - expect(runtimeRegisterSource).toContain( - 'const normalizedRemoteAlias = remoteAlias.trim()', - ); - expect(runtimeRegisterSource).toContain( - "normalizedRemoteAlias.includes(':')", - ); - expect(runtimeRegisterSource).toContain( - '!ALIAS_TOKEN_PATTERN.test(normalizedRemoteAlias)', - ); expect(runtimeRegisterSource).toContain( 'Remote alias must be a non-empty token (letters, numbers, "-", "_", ".") without ":" delimiters', ); - expect(runtimeRegisterSource).toContain( - "if (rawActionId.startsWith('remote:'))", - ); - expect(runtimeRegisterSource).toContain( - 'return `remote:${remoteAlias}:${rawActionId}`', - ); + expect(runtimeRegisterSource).toContain("startsWith('remote:')"); + expect(runtimeRegisterSource).toContain('return `remote:${remoteAlias}:'); expect(runtimeRegisterSource).toContain("'x-rsc-action': hostActionId"); expect(runtimeRegisterSource).toContain("method: 'POST'"); expect(runtimeRegisterSource).toContain("Accept: 'text/x-component'"); From 97084d6ab5184f8387aab344f9b4b21c79a459e6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 05:38:57 +0000 Subject: [PATCH 166/324] test(rsc-mf): reduce brittle runtime source assertions --- tests/integration/rsc-mf/tests/index.test.ts | 22 -------------------- 1 file changed, 22 deletions(-) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index d67f02a3eed8..56e45e8a83fe 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -618,18 +618,12 @@ function runTests({ mode }: TestConfig) { expect(runtimeRegisterSource).not.toContain('127.0.0.1:'); expect(runtimeRegisterSource).not.toContain('window.location'); expect(runtimeRegisterSource).toContain("remoteAlias = 'rscRemote'"); - expect(runtimeRegisterSource).toContain( - 'Remote alias must be a non-empty token (letters, numbers, "-", "_", ".") without ":" delimiters', - ); expect(runtimeRegisterSource).toContain("startsWith('remote:')"); expect(runtimeRegisterSource).toContain('return `remote:${remoteAlias}:'); expect(runtimeRegisterSource).toContain("'x-rsc-action': hostActionId"); expect(runtimeRegisterSource).toContain("method: 'POST'"); expect(runtimeRegisterSource).toContain("Accept: 'text/x-component'"); expect(runtimeRegisterSource).toContain('getNormalizedRemoteActionUrl'); - expect(runtimeRegisterSource).toContain( - "url.protocol !== 'http:' && url.protocol !== 'https:'", - ); expect(runtimeRegisterSource).toContain( 'Remote action callback URL must use http or https', ); @@ -638,23 +632,7 @@ function runTests({ mode }: TestConfig) { ); expect(runtimeRegisterSource).toContain("url.search = ''"); expect(runtimeRegisterSource).toContain("url.hash = ''"); - expect(runtimeRegisterSource).toContain( - 'remoteAlias: normalizedRemoteAlias', - ); - expect(runtimeRegisterSource).toContain( - 'getHostActionId(id, normalizedRemoteAlias)', - ); - expect(runtimeRegisterSource).toContain( - 'const normalizedRemoteOrigin = remoteOrigin.trim()', - ); - expect(runtimeRegisterSource).toContain('if (!normalizedRemoteOrigin)'); expect(runtimeRegisterSource).toContain('remoteActionUrl,'); - expect(runtimeRegisterSource).not.toContain( - 'getHostActionId(id, remoteAlias)', - ); - expect(runtimeRegisterSource).not.toContain( - 'getNormalizedRemoteActionUrl(remoteOrigin)', - ); expect(runtimeRegisterSource).not.toContain( 'remoteActionIdToHostProxyActionId', ); From 102d1fdd1f5c0c7965abd8fce133ed1e3b0f385b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 05:42:11 +0000 Subject: [PATCH 167/324] test(rsc-mf): focus source guards on stable integration contracts --- tests/integration/rsc-mf/tests/index.test.ts | 26 -------------------- 1 file changed, 26 deletions(-) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 56e45e8a83fe..ef785aa3de82 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -583,32 +583,12 @@ function runTests({ mode }: TestConfig) { !source.includes('rsc-mf-react-server-dom-client-browser'), ), ).toBe(true); - expect(runtimeInitSource).toContain('registerRemoteServerCallback'); - expect(runtimeInitSource).toContain('bootstrapServerCallback'); - expect(runtimeInitSource).toContain('callbackBootstrapPromise'); - expect(runtimeInitSource).toContain("if (typeof window !== 'undefined')"); expect(runtimeInitSource).toContain("import('./registerServerCallback')"); - expect(runtimeInitSource).toContain('void bootstrapServerCallback()'); expect(runtimeInitSource).toContain('window.location.origin'); expect(runtimeInitSource).toContain('window.location.pathname'); - expect(runtimeInitSource).toContain('callbackBootstrapPromise.catch'); - expect(runtimeInitSource).toContain( - 'callbackBootstrapPromise = undefined', - ); - expect(runtimeInitSource).not.toContain('setServerCallback('); expect(runtimeInitSource).not.toContain('RSC_MF_REMOTE_PORT'); expect(runtimeInitSource).not.toContain('127.0.0.1:'); - expect(runtimeInitSource).not.toContain( - "from './registerServerCallback'", - ); expect(runtimeRegisterSource).toContain('setServerCallback'); - expect(runtimeRegisterSource).toContain('createTemporaryReferenceSet'); - expect(runtimeRegisterSource).toContain( - 'encodeReply(args, { temporaryReferences })', - ); - expect(runtimeRegisterSource).toContain( - 'createFromFetch(response, { temporaryReferences })', - ); expect(runtimeRegisterSource).toContain( "from 'rsc-mf-react-server-dom-client-browser'", ); @@ -617,9 +597,6 @@ function runTests({ mode }: TestConfig) { ); expect(runtimeRegisterSource).not.toContain('127.0.0.1:'); expect(runtimeRegisterSource).not.toContain('window.location'); - expect(runtimeRegisterSource).toContain("remoteAlias = 'rscRemote'"); - expect(runtimeRegisterSource).toContain("startsWith('remote:')"); - expect(runtimeRegisterSource).toContain('return `remote:${remoteAlias}:'); expect(runtimeRegisterSource).toContain("'x-rsc-action': hostActionId"); expect(runtimeRegisterSource).toContain("method: 'POST'"); expect(runtimeRegisterSource).toContain("Accept: 'text/x-component'"); @@ -630,9 +607,6 @@ function runTests({ mode }: TestConfig) { expect(runtimeRegisterSource).toContain( 'Remote action callback URL must not include embedded credentials.', ); - expect(runtimeRegisterSource).toContain("url.search = ''"); - expect(runtimeRegisterSource).toContain("url.hash = ''"); - expect(runtimeRegisterSource).toContain('remoteActionUrl,'); expect(runtimeRegisterSource).not.toContain( 'remoteActionIdToHostProxyActionId', ); From 9cd4ad0a46a61be5681536c0497aebd9bc49c414 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 05:48:10 +0000 Subject: [PATCH 168/324] test(rsc-mf): add runtime callback contract behavior tests --- .../src/runtime/registerServerCallback.ts | 2 +- .../tests/registerServerCallback.test.ts | 132 ++++++++++++++++++ ...sc-mf-react-server-dom-client-browser.d.ts | 17 +++ 3 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 tests/integration/rsc-mf/tests/registerServerCallback.test.ts create mode 100644 tests/integration/rsc-mf/tests/rsc-mf-react-server-dom-client-browser.d.ts diff --git a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts index a7c38f4dd2f5..f0dd2c43482e 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts @@ -69,7 +69,7 @@ export function registerRemoteServerCallback( return; } - setServerCallback(async (id, args) => { + setServerCallback(async (id: string, args: unknown[]) => { const hostActionId = getHostActionId(id, normalizedRemoteAlias); const temporaryReferences = createTemporaryReferenceSet(); const response = fetch(remoteActionUrl, { diff --git a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts new file mode 100644 index 000000000000..32a84e853997 --- /dev/null +++ b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts @@ -0,0 +1,132 @@ +type ServerCallback = (id: string, args: unknown[]) => Promise; + +const mockSetServerCallback = jest.fn(); +const mockCreateTemporaryReferenceSet = jest.fn(() => ({ ref: 'temp-ref' })); +const mockEncodeReply = jest.fn(async (args: unknown[]) => ({ + encodedArgs: args, +})); +const mockCreateFromFetch = jest.fn(() => ({ type: 'decoded-rsc-response' })); + +jest.mock( + 'rsc-mf-react-server-dom-client-browser', + () => ({ + setServerCallback: mockSetServerCallback, + createTemporaryReferenceSet: mockCreateTemporaryReferenceSet, + encodeReply: mockEncodeReply, + createFromFetch: mockCreateFromFetch, + }), + { virtual: true }, +); + +const importRegisterHelper = async () => + import('../remote/src/runtime/registerServerCallback'); + +describe('registerRemoteServerCallback runtime behavior', () => { + const originalFetch = global.fetch; + + beforeEach(() => { + jest.resetModules(); + mockSetServerCallback.mockReset(); + mockCreateTemporaryReferenceSet.mockClear(); + mockEncodeReply.mockClear(); + mockCreateFromFetch.mockClear(); + global.fetch = jest.fn(async () => ({ ok: true }) as Response); + }); + + afterAll(() => { + global.fetch = originalFetch; + }); + + const getRegisteredCallback = () => { + expect(mockSetServerCallback).toHaveBeenCalledTimes(1); + const callback = mockSetServerCallback.mock.calls[0]?.[0] as + | ServerCallback + | undefined; + expect(typeof callback).toBe('function'); + return callback as ServerCallback; + }; + + it('registers callback and forwards raw action ids with bridge prefix', async () => { + const { registerRemoteServerCallback } = await importRegisterHelper(); + registerRemoteServerCallback( + 'http://127.0.0.1:3008/server-component-root?debug=1#hash', + 'rscRemote', + ); + + const callback = getRegisteredCallback(); + await callback('abc123', ['arg-1', { nested: true }]); + + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledWith( + 'http://127.0.0.1:3008/server-component-root', + expect.objectContaining({ + method: 'POST', + headers: expect.objectContaining({ + Accept: 'text/x-component', + 'x-rsc-action': 'remote:rscRemote:abc123', + }), + }), + ); + expect(mockCreateTemporaryReferenceSet).toHaveBeenCalledTimes(1); + expect(mockEncodeReply).toHaveBeenCalledWith( + ['arg-1', { nested: true }], + expect.objectContaining({ + temporaryReferences: expect.any(Object), + }), + ); + expect(mockCreateFromFetch).toHaveBeenCalledTimes(1); + }); + + it('preserves already-prefixed action ids and dedupes normalized callback registrations', async () => { + const { registerRemoteServerCallback } = await importRegisterHelper(); + registerRemoteServerCallback( + 'http://127.0.0.1:3008/server-component-root?first=1', + 'rscRemote', + ); + registerRemoteServerCallback( + 'http://127.0.0.1:3008/server-component-root#second', + 'rscRemote', + ); + expect(mockSetServerCallback).toHaveBeenCalledTimes(1); + + const callback = getRegisteredCallback(); + await callback('remote:rscRemote:already-prefixed', []); + + expect(global.fetch).toHaveBeenCalledWith( + 'http://127.0.0.1:3008/server-component-root', + expect.objectContaining({ + headers: expect.objectContaining({ + 'x-rsc-action': 'remote:rscRemote:already-prefixed', + }), + }), + ); + }); + + it('rejects invalid aliases and callback URLs', async () => { + const { registerRemoteServerCallback } = await importRegisterHelper(); + + expect(() => + registerRemoteServerCallback( + 'http://127.0.0.1:3008/server-component-root', + 'bad alias', + ), + ).toThrow('Remote alias must be a non-empty token'); + expect(() => + registerRemoteServerCallback( + 'http://127.0.0.1:3008/server-component-root', + 'bad:alias', + ), + ).toThrow('Remote alias must be a non-empty token'); + expect(() => + registerRemoteServerCallback('javascript:alert(1)', 'rscRemote'), + ).toThrow('Remote action callback URL must use http or https'); + expect(() => + registerRemoteServerCallback( + 'http://user:secret@127.0.0.1:3008/server-component-root', + 'rscRemote', + ), + ).toThrow( + 'Remote action callback URL must not include embedded credentials', + ); + }); +}); diff --git a/tests/integration/rsc-mf/tests/rsc-mf-react-server-dom-client-browser.d.ts b/tests/integration/rsc-mf/tests/rsc-mf-react-server-dom-client-browser.d.ts new file mode 100644 index 000000000000..cd7c4bac56d4 --- /dev/null +++ b/tests/integration/rsc-mf/tests/rsc-mf-react-server-dom-client-browser.d.ts @@ -0,0 +1,17 @@ +declare module 'rsc-mf-react-server-dom-client-browser' { + export function setServerCallback( + callback: (id: string, args: unknown[]) => Promise, + ): void; + + export function createTemporaryReferenceSet(): unknown; + + export function encodeReply( + value: unknown, + options: { temporaryReferences: unknown }, + ): Promise; + + export function createFromFetch( + response: Promise, + options: { temporaryReferences: unknown }, + ): unknown; +} From 6638c755f666e8191f2d193f74baac599f801398 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 05:51:19 +0000 Subject: [PATCH 169/324] test(rsc-mf): cover action-id normalization and empty-origin skip --- .../tests/registerServerCallback.test.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts index 32a84e853997..894357d3dcd9 100644 --- a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts @@ -77,6 +77,26 @@ describe('registerRemoteServerCallback runtime behavior', () => { expect(mockCreateFromFetch).toHaveBeenCalledTimes(1); }); + it('normalizes action ids and rejects whitespace-delimited ids', async () => { + const { registerRemoteServerCallback } = await importRegisterHelper(); + registerRemoteServerCallback('http://127.0.0.1:3008/server-component-root'); + + const callback = getRegisteredCallback(); + await callback(' abc123 ', []); + expect(global.fetch).toHaveBeenCalledWith( + 'http://127.0.0.1:3008/server-component-root', + expect.objectContaining({ + headers: expect.objectContaining({ + 'x-rsc-action': 'remote:rscRemote:abc123', + }), + }), + ); + + await expect(callback('abc 123', [])).rejects.toThrow( + 'Remote action id must be a non-empty token without whitespace', + ); + }); + it('preserves already-prefixed action ids and dedupes normalized callback registrations', async () => { const { registerRemoteServerCallback } = await importRegisterHelper(); registerRemoteServerCallback( @@ -102,6 +122,12 @@ describe('registerRemoteServerCallback runtime behavior', () => { ); }); + it('ignores empty callback origins after trimming', async () => { + const { registerRemoteServerCallback } = await importRegisterHelper(); + registerRemoteServerCallback(' '); + expect(mockSetServerCallback).not.toHaveBeenCalled(); + }); + it('rejects invalid aliases and callback URLs', async () => { const { registerRemoteServerCallback } = await importRegisterHelper(); From 992d086b6f0f52dbac79511152cf666cf560370e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 05:54:15 +0000 Subject: [PATCH 170/324] test(rsc-mf): cover alias trim dedupe in callback registration --- .../tests/registerServerCallback.test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts index 894357d3dcd9..f681a1637cb7 100644 --- a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts @@ -122,6 +122,30 @@ describe('registerRemoteServerCallback runtime behavior', () => { ); }); + it('trims alias before callback keying and action prefixing', async () => { + const { registerRemoteServerCallback } = await importRegisterHelper(); + registerRemoteServerCallback( + 'http://127.0.0.1:3008/server-component-root', + ' rscRemote ', + ); + registerRemoteServerCallback( + 'http://127.0.0.1:3008/server-component-root', + 'rscRemote', + ); + expect(mockSetServerCallback).toHaveBeenCalledTimes(1); + + const callback = getRegisteredCallback(); + await callback('trimmed-alias-action', []); + expect(global.fetch).toHaveBeenCalledWith( + 'http://127.0.0.1:3008/server-component-root', + expect.objectContaining({ + headers: expect.objectContaining({ + 'x-rsc-action': 'remote:rscRemote:trimmed-alias-action', + }), + }), + ); + }); + it('ignores empty callback origins after trimming', async () => { const { registerRemoteServerCallback } = await importRegisterHelper(); registerRemoteServerCallback(' '); From 423164b8d6a96db3048c0f3764bc3cc2ac29a4e2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 05:57:19 +0000 Subject: [PATCH 171/324] test(rsc-mf): verify alias changes re-register callback handler --- .../tests/registerServerCallback.test.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts index f681a1637cb7..75d4b66ba0cc 100644 --- a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts @@ -122,6 +122,34 @@ describe('registerRemoteServerCallback runtime behavior', () => { ); }); + it('re-registers callback when alias changes and uses new alias prefix', async () => { + const { registerRemoteServerCallback } = await importRegisterHelper(); + registerRemoteServerCallback( + 'http://127.0.0.1:3008/server-component-root', + 'rscRemote', + ); + registerRemoteServerCallback( + 'http://127.0.0.1:3008/server-component-root', + 'rscRemoteAlt', + ); + expect(mockSetServerCallback).toHaveBeenCalledTimes(2); + + const callback = mockSetServerCallback.mock.calls[1]?.[0] as + | ServerCallback + | undefined; + expect(typeof callback).toBe('function'); + await (callback as ServerCallback)('alias-change-action', []); + + expect(global.fetch).toHaveBeenCalledWith( + 'http://127.0.0.1:3008/server-component-root', + expect.objectContaining({ + headers: expect.objectContaining({ + 'x-rsc-action': 'remote:rscRemoteAlt:alias-change-action', + }), + }), + ); + }); + it('trims alias before callback keying and action prefixing', async () => { const { registerRemoteServerCallback } = await importRegisterHelper(); registerRemoteServerCallback( From 34dc4420fa22077316589c30fccd65e2b51e5cfb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 06:00:30 +0000 Subject: [PATCH 172/324] test(rsc-mf): cover callback rebind on normalized URL changes --- .../tests/registerServerCallback.test.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts index 75d4b66ba0cc..9f4418d16ddc 100644 --- a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts @@ -150,6 +150,34 @@ describe('registerRemoteServerCallback runtime behavior', () => { ); }); + it('re-registers callback when normalized action URL changes', async () => { + const { registerRemoteServerCallback } = await importRegisterHelper(); + registerRemoteServerCallback( + 'http://127.0.0.1:3008/server-component-root', + 'rscRemote', + ); + registerRemoteServerCallback( + 'http://127.0.0.1:3008/another-action-endpoint?cache=1#hash', + 'rscRemote', + ); + expect(mockSetServerCallback).toHaveBeenCalledTimes(2); + + const callback = mockSetServerCallback.mock.calls[1]?.[0] as + | ServerCallback + | undefined; + expect(typeof callback).toBe('function'); + await (callback as ServerCallback)('path-change-action', []); + + expect(global.fetch).toHaveBeenCalledWith( + 'http://127.0.0.1:3008/another-action-endpoint', + expect.objectContaining({ + headers: expect.objectContaining({ + 'x-rsc-action': 'remote:rscRemote:path-change-action', + }), + }), + ); + }); + it('trims alias before callback keying and action prefixing', async () => { const { registerRemoteServerCallback } = await importRegisterHelper(); registerRemoteServerCallback( From 587ab2503a530aca2d6d0bde7e5bfd073017c913 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 06:03:26 +0000 Subject: [PATCH 173/324] test(rsc-mf): assert callback rebind when endpoint path changes --- .../tests/registerServerCallback.test.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts index 9f4418d16ddc..87941b760af7 100644 --- a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts @@ -95,6 +95,9 @@ describe('registerRemoteServerCallback runtime behavior', () => { await expect(callback('abc 123', [])).rejects.toThrow( 'Remote action id must be a non-empty token without whitespace', ); + await expect(callback(' ', [])).rejects.toThrow( + 'Remote action id must be a non-empty token without whitespace', + ); }); it('preserves already-prefixed action ids and dedupes normalized callback registrations', async () => { @@ -202,6 +205,25 @@ describe('registerRemoteServerCallback runtime behavior', () => { ); }); + it('accepts token aliases with dot, underscore, and dash characters', async () => { + const { registerRemoteServerCallback } = await importRegisterHelper(); + registerRemoteServerCallback( + 'http://127.0.0.1:3008/server-component-root', + 'remote.alias_v2-test', + ); + + const callback = getRegisteredCallback(); + await callback('token-alias-action', []); + expect(global.fetch).toHaveBeenCalledWith( + 'http://127.0.0.1:3008/server-component-root', + expect.objectContaining({ + headers: expect.objectContaining({ + 'x-rsc-action': 'remote:remote.alias_v2-test:token-alias-action', + }), + }), + ); + }); + it('ignores empty callback origins after trimming', async () => { const { registerRemoteServerCallback } = await importRegisterHelper(); registerRemoteServerCallback(' '); From a0168c4225d714260c9d38fb28bbba834756f9cb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 06:08:29 +0000 Subject: [PATCH 174/324] test(rsc-mf): add init callback bootstrap behavior coverage --- .../rsc-mf/tests/initServerCallback.test.ts | 106 ++++++++++++++++++ .../tests/registerServerCallback.test.ts | 18 +++ 2 files changed, 124 insertions(+) create mode 100644 tests/integration/rsc-mf/tests/initServerCallback.test.ts diff --git a/tests/integration/rsc-mf/tests/initServerCallback.test.ts b/tests/integration/rsc-mf/tests/initServerCallback.test.ts new file mode 100644 index 000000000000..42bbc0111b78 --- /dev/null +++ b/tests/integration/rsc-mf/tests/initServerCallback.test.ts @@ -0,0 +1,106 @@ +const INIT_SERVER_CALLBACK_MODULE = '../remote/src/runtime/initServerCallback'; +const REGISTER_SERVER_CALLBACK_MODULE = + '../remote/src/runtime/registerServerCallback'; + +const flushMicrotasks = async () => { + await Promise.resolve(); + await Promise.resolve(); +}; + +describe('initServerCallback runtime bootstrap behavior', () => { + const originalWindow = (globalThis as { window?: unknown }).window; + + afterEach(() => { + if (typeof originalWindow === 'undefined') { + delete (globalThis as { window?: unknown }).window; + return; + } + + (globalThis as { window?: unknown }).window = originalWindow; + }); + + it('does not bootstrap callback registration on server import', async () => { + jest.resetModules(); + delete (globalThis as { window?: unknown }).window; + + const mockRegisterRemoteServerCallback = jest.fn(); + jest.doMock(REGISTER_SERVER_CALLBACK_MODULE, () => ({ + registerRemoteServerCallback: mockRegisterRemoteServerCallback, + })); + + await import(INIT_SERVER_CALLBACK_MODULE); + await flushMicrotasks(); + + expect(mockRegisterRemoteServerCallback).not.toHaveBeenCalled(); + }); + + it('registers callback once with browser origin and pathname', async () => { + jest.resetModules(); + (globalThis as { window?: unknown }).window = { + location: { + origin: 'http://127.0.0.1:3900', + pathname: '/server-component-root', + }, + }; + + const mockRegisterRemoteServerCallback = jest.fn(); + jest.doMock(REGISTER_SERVER_CALLBACK_MODULE, () => ({ + registerRemoteServerCallback: mockRegisterRemoteServerCallback, + })); + + await import(INIT_SERVER_CALLBACK_MODULE); + await flushMicrotasks(); + + expect(mockRegisterRemoteServerCallback).toHaveBeenCalledTimes(1); + expect(mockRegisterRemoteServerCallback).toHaveBeenCalledWith( + 'http://127.0.0.1:3900/server-component-root', + 'rscRemote', + ); + }); + + it('falls back to root action pathname when location pathname is empty', async () => { + jest.resetModules(); + (globalThis as { window?: unknown }).window = { + location: { + origin: 'http://127.0.0.1:4100', + pathname: '', + }, + }; + + const mockRegisterRemoteServerCallback = jest.fn(); + jest.doMock(REGISTER_SERVER_CALLBACK_MODULE, () => ({ + registerRemoteServerCallback: mockRegisterRemoteServerCallback, + })); + + await import(INIT_SERVER_CALLBACK_MODULE); + await flushMicrotasks(); + + expect(mockRegisterRemoteServerCallback).toHaveBeenCalledTimes(1); + expect(mockRegisterRemoteServerCallback).toHaveBeenCalledWith( + 'http://127.0.0.1:4100/', + 'rscRemote', + ); + }); + + it('keeps bootstrap side effect memoized across repeated imports', async () => { + jest.resetModules(); + (globalThis as { window?: unknown }).window = { + location: { + origin: 'http://127.0.0.1:4300', + pathname: '/server-component-root', + }, + }; + + const mockRegisterRemoteServerCallback = jest.fn(); + jest.doMock(REGISTER_SERVER_CALLBACK_MODULE, () => ({ + registerRemoteServerCallback: mockRegisterRemoteServerCallback, + })); + + await import(INIT_SERVER_CALLBACK_MODULE); + await flushMicrotasks(); + await import(INIT_SERVER_CALLBACK_MODULE); + await flushMicrotasks(); + + expect(mockRegisterRemoteServerCallback).toHaveBeenCalledTimes(1); + }); +}); diff --git a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts index 87941b760af7..9d474aeb2a04 100644 --- a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts @@ -233,6 +233,12 @@ describe('registerRemoteServerCallback runtime behavior', () => { it('rejects invalid aliases and callback URLs', async () => { const { registerRemoteServerCallback } = await importRegisterHelper(); + expect(() => + registerRemoteServerCallback( + 'http://127.0.0.1:3008/server-component-root', + ' ', + ), + ).toThrow('Remote alias must be a non-empty token'); expect(() => registerRemoteServerCallback( 'http://127.0.0.1:3008/server-component-root', @@ -245,9 +251,21 @@ describe('registerRemoteServerCallback runtime behavior', () => { 'bad:alias', ), ).toThrow('Remote alias must be a non-empty token'); + expect(() => + registerRemoteServerCallback( + 'http://127.0.0.1:3008/server-component-root', + 'bad/alias', + ), + ).toThrow('Remote alias must be a non-empty token'); expect(() => registerRemoteServerCallback('javascript:alert(1)', 'rscRemote'), ).toThrow('Remote action callback URL must use http or https'); + expect(() => + registerRemoteServerCallback( + 'ftp://127.0.0.1:3008/server-component-root', + 'rscRemote', + ), + ).toThrow('Remote action callback URL must use http or https'); expect(() => registerRemoteServerCallback( 'http://user:secret@127.0.0.1:3008/server-component-root', From 18cdc2e069d02074b939f8619212923f9ceb6ee8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 06:13:44 +0000 Subject: [PATCH 175/324] test(rsc-mf): move mf config guardrails into behavior tests --- tests/integration/rsc-mf/tests/index.test.ts | 151 ---------------- .../tests/moduleFederationConfig.test.ts | 166 ++++++++++++++++++ 2 files changed, 166 insertions(+), 151 deletions(-) create mode 100644 tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index ef785aa3de82..0b7366f50a36 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -35,8 +35,6 @@ const EXPECTED_REMOTE_EXPOSE_PATHS = [ './actionBundle', './infoBundle', ].sort(); -const REMOTE_EXPOSE_ENTRY_PATTERN = - /'(\.\/[^']+)':\s*'(\.\/src\/components\/[^']+)'/g; type Mode = 'dev' | 'build'; @@ -95,15 +93,6 @@ function createHostEnv(remotePort: number) { }; } -function getRemoteExposeEntries(configSource: string) { - return Array.from(configSource.matchAll(REMOTE_EXPOSE_ENTRY_PATTERN)).map( - ([, exposeKey, importPath]) => ({ - exposeKey, - importPath, - }), - ); -} - async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { const response = await fetch(`http://127.0.0.1:${hostPort}${HOST_RSC_URL}`); const html = await response.text(); @@ -512,22 +501,6 @@ function runTests({ mode }: TestConfig) { path.join(remoteDir, 'src/runtime/registerServerCallback.ts'), 'utf-8', ); - const moduleFederationConfigSource = fs.readFileSync( - path.join(remoteDir, 'module-federation.config.ts'), - 'utf-8', - ); - const hostModuleFederationConfigSource = fs.readFileSync( - path.join(hostDir, 'module-federation.config.ts'), - 'utf-8', - ); - const hostModernConfigSource = fs.readFileSync( - path.join(hostDir, 'modern.config.ts'), - 'utf-8', - ); - const remoteModernConfigSource = fs.readFileSync( - path.join(remoteDir, 'modern.config.ts'), - 'utf-8', - ); const remoteRuntimeExposesDir = path.join( remoteDir, 'src/runtime/exposes', @@ -544,17 +517,6 @@ function runTests({ mode }: TestConfig) { const remoteRuntimeEntriesWithoutExposeDir = remoteRuntimeEntries.filter( entryName => entryName !== 'exposes', ); - const remoteExposeEntries = getRemoteExposeEntries( - moduleFederationConfigSource, - ); - const remoteExposeKeys = remoteExposeEntries - .map(({ exposeKey }) => exposeKey) - .sort(); - const clientBrowserSharedScopeEntryCount = ( - moduleFederationConfigSource.match( - /'react-server-dom-rspack\/client\.browser':\s*\{/g, - ) || [] - ).length; expect( componentSources.every( @@ -610,124 +572,11 @@ function runTests({ mode }: TestConfig) { expect(runtimeRegisterSource).not.toContain( 'remoteActionIdToHostProxyActionId', ); - expect(moduleFederationConfigSource).toContain( - 'CALLBACK_BOOTSTRAP_IMPORT', - ); - expect(moduleFederationConfigSource).toContain( - 'CALLBACK_BOOTSTRAP_PREFIX', - ); - expect(moduleFederationConfigSource).toContain( - 'Callback bootstrap import must stay in runtime namespace', - ); - expect(moduleFederationConfigSource).toContain( - 'Callback bootstrap import must use explicit source extension', - ); - expect(moduleFederationConfigSource).toContain( - 'Callback bootstrap import must not contain traversal or Windows separators', - ); - expect( - moduleFederationConfigSource.includes( - '[CALLBACK_BOOTSTRAP_IMPORT, importPath]', - ), - ).toBe(true); - expect(moduleFederationConfigSource).not.toContain( - 'callbackBootstrappedExposes', - ); - expect(moduleFederationConfigSource).not.toContain( - 'missingCallbackExposeEntries', - ); - expect(moduleFederationConfigSource).not.toContain( - './src/runtime/exposes/', - ); expect(remoteRuntimeExposeEntries).toEqual([]); expect(remoteRuntimeEntriesWithoutExposeDir).toEqual([ 'initServerCallback.ts', 'registerServerCallback.ts', ]); - expect(moduleFederationConfigSource).toContain('COMPONENT_EXPOSE_PREFIX'); - expect(moduleFederationConfigSource).toContain( - 'nonComponentExposeEntries', - ); - expect(moduleFederationConfigSource).toContain( - 'nonTypeScriptExposeEntries', - ); - expect(moduleFederationConfigSource).toContain( - 'parentTraversalExposeEntries', - ); - expect(moduleFederationConfigSource).toContain('nonPosixExposeEntries'); - expect(moduleFederationConfigSource).toContain('invalidExposeKeys'); - expect(moduleFederationConfigSource).toContain('callbackExposeEntries'); - expect(moduleFederationConfigSource).toContain("shareScope: 'default'"); - expect(moduleFederationConfigSource).toContain("shareScope: 'ssr'"); - expect(moduleFederationConfigSource).toContain("shareScope: 'rsc'"); - expect(moduleFederationConfigSource).toContain('experiments:'); - expect(moduleFederationConfigSource).toContain('asyncStartup: true'); - expect(moduleFederationConfigSource).toContain('rsc: true'); - expect(clientBrowserSharedScopeEntryCount).toBe(3); - expect(remoteExposeKeys).toEqual(EXPECTED_REMOTE_EXPOSE_PATHS); - expect( - remoteExposeEntries.every(({ importPath }) => - importPath.startsWith('./src/components/'), - ), - ).toBe(true); - expect( - remoteExposeEntries.every(({ importPath }) => - /\.[tj]sx?$/.test(importPath), - ), - ).toBe(true); - expect( - remoteExposeEntries.every( - ({ importPath }) => - !importPath.includes('..') && !importPath.includes('\\'), - ), - ).toBe(true); - expect( - remoteExposeEntries.every(({ importPath }) => - fs.existsSync(path.resolve(remoteDir, importPath)), - ), - ).toBe(true); - expect(hostModuleFederationConfigSource).toContain('runtimePlugins'); - expect(hostModuleFederationConfigSource).toContain( - './runtime/forceRemotePublicPath.ts', - ); - expect(hostModuleFederationConfigSource).toContain( - "process.env.NODE_ENV === 'production'", - ); - expect(hostModuleFederationConfigSource).toContain(': []'); - expect(hostModuleFederationConfigSource).toContain( - '/static/mf-manifest.json', - ); - expect(hostModuleFederationConfigSource).toContain('RSC_MF_REMOTE_PORT'); - expect(hostModuleFederationConfigSource).toContain('rscRemote:'); - expect(hostModuleFederationConfigSource).toContain('asyncStartup: true'); - expect(hostModuleFederationConfigSource).toContain('rsc: true'); - expect(hostModuleFederationConfigSource).not.toContain( - 'registerServerCallbackRuntime', - ); - expect(hostModuleFederationConfigSource).not.toContain( - 'initServerCallback', - ); - expect(hostModernConfigSource).not.toContain('preEntry'); - expect(hostModernConfigSource).not.toContain('registerServerCallback'); - expect(hostModernConfigSource).toContain('enableAsyncEntry: false'); - expect(hostModernConfigSource).toContain("chain.target('async-node')"); - expect(hostModernConfigSource).toContain("'server-only$'"); - expect(hostModernConfigSource).toContain( - 'moduleFederationPlugin({ ssr: true })', - ); - expect(remoteModernConfigSource).not.toContain('chunkLoadingGlobal'); - expect(remoteModernConfigSource).toContain( - 'rsc-mf-react-server-dom-client-browser$', - ); - expect(remoteModernConfigSource).toContain( - 'react-server-dom-rspack/client.browser', - ); - expect(remoteModernConfigSource).toContain('enableAsyncEntry: false'); - expect(remoteModernConfigSource).toContain("chain.target('async-node')"); - expect(remoteModernConfigSource).toContain('splitChunks(false)'); - expect(remoteModernConfigSource).toContain( - 'moduleFederationPlugin({ ssr: true })', - ); }); it('should not load callback helper expose chunk', () => { diff --git a/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts b/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts new file mode 100644 index 000000000000..76e63c11f6f4 --- /dev/null +++ b/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts @@ -0,0 +1,166 @@ +const HOST_MODULE_FEDERATION_CONFIG_MODULE = '../host/module-federation.config'; +const REMOTE_MODULE_FEDERATION_CONFIG_MODULE = + '../remote/module-federation.config'; + +const EXPECTED_REMOTE_EXPOSE_KEYS = [ + './RemoteClientCounter', + './RemoteClientBadge', + './RemoteServerCard', + './RemoteServerDefault', + './AsyncRemoteServerInfo', + './remoteServerOnly', + './remoteServerOnlyDefault', + './remoteMeta', + './actions', + './nestedActions', + './defaultAction', + './actionBundle', + './infoBundle', +].sort(); + +const CALLBACK_BOOTSTRAP_IMPORT = './src/runtime/initServerCallback.ts'; + +const withEnv = ( + env: Partial>, + run: () => T, +): T => { + const previousNodeEnv = process.env.NODE_ENV; + const previousRemotePort = process.env.RSC_MF_REMOTE_PORT; + + if (typeof env.NODE_ENV === 'undefined') { + delete process.env.NODE_ENV; + } else { + process.env.NODE_ENV = env.NODE_ENV as NodeJS.ProcessEnv['NODE_ENV']; + } + + if (typeof env.RSC_MF_REMOTE_PORT === 'undefined') { + delete process.env.RSC_MF_REMOTE_PORT; + } else { + process.env.RSC_MF_REMOTE_PORT = env.RSC_MF_REMOTE_PORT; + } + + try { + return run(); + } finally { + if (typeof previousNodeEnv === 'undefined') { + delete process.env.NODE_ENV; + } else { + process.env.NODE_ENV = previousNodeEnv; + } + if (typeof previousRemotePort === 'undefined') { + delete process.env.RSC_MF_REMOTE_PORT; + } else { + process.env.RSC_MF_REMOTE_PORT = previousRemotePort; + } + } +}; + +const loadRemoteConfig = () => + withEnv({}, () => { + let config: any; + jest.isolateModules(() => { + config = require(REMOTE_MODULE_FEDERATION_CONFIG_MODULE).default; + }); + return config; + }); + +const loadHostConfig = ({ + nodeEnv, + remotePort, +}: { + nodeEnv: string; + remotePort?: string; +}) => + withEnv( + { + NODE_ENV: nodeEnv, + RSC_MF_REMOTE_PORT: remotePort, + }, + () => { + let config: any; + jest.isolateModules(() => { + config = require(HOST_MODULE_FEDERATION_CONFIG_MODULE).default; + }); + return config; + }, + ); + +describe('rsc-mf module federation config contracts', () => { + it('declares expected remote exposes with callback bootstrap imports', () => { + const remoteConfig = loadRemoteConfig(); + const exposeEntries = Object.entries(remoteConfig.exposes || {}); + const exposeKeys = exposeEntries + .map(([exposeKey]) => exposeKey) + .sort() as string[]; + expect(exposeKeys).toEqual(EXPECTED_REMOTE_EXPOSE_KEYS); + + for (const [exposeKey, exposeDefinition] of exposeEntries) { + const definition = exposeDefinition as { + import?: string[]; + layer?: string; + }; + expect(definition.layer).toBe('react-server-components'); + expect(definition.import).toBeDefined(); + expect(Array.isArray(definition.import)).toBe(true); + expect(definition.import).toHaveLength(2); + expect(definition.import?.[0]).toBe(CALLBACK_BOOTSTRAP_IMPORT); + expect(definition.import?.[1]).toMatch(/^\.\/src\/components\//); + expect(definition.import?.[1]).toMatch(/\.[tj]sx?$/); + expect(definition.import?.[1]).not.toContain('..'); + expect(definition.import?.[1]).not.toContain('\\'); + expect(exposeKey).toMatch(/^\.\//); + } + }); + + it('keeps remote shared scopes and experiments aligned for rsc', () => { + const remoteConfig = loadRemoteConfig(); + const sharedScopes = remoteConfig.shared as Array< + Record + >; + + expect(sharedScopes).toHaveLength(3); + expect(sharedScopes[0]?.react?.shareScope).toBe('default'); + expect(sharedScopes[1]?.react?.shareScope).toBe('ssr'); + expect(sharedScopes[2]?.react?.shareScope).toBe('rsc'); + expect( + sharedScopes.map( + scope => scope['react-server-dom-rspack/client.browser']?.shareScope, + ), + ).toEqual(['default', 'ssr', 'rsc']); + expect(remoteConfig.experiments).toEqual( + expect.objectContaining({ + asyncStartup: true, + rsc: true, + }), + ); + }); + + it('uses remote port env var in host manifest remote URL', () => { + const hostConfig = loadHostConfig({ + nodeEnv: 'test', + remotePort: '3999', + }); + expect(hostConfig.remotes).toEqual( + expect.objectContaining({ + rscRemote: 'rscRemote@http://127.0.0.1:3999/static/mf-manifest.json', + }), + ); + }); + + it('enables host runtime plugin only in production', () => { + const productionHostConfig = loadHostConfig({ + nodeEnv: 'production', + remotePort: '3008', + }); + expect(productionHostConfig.runtimePlugins).toHaveLength(1); + expect(productionHostConfig.runtimePlugins[0]).toContain( + 'runtime/forceRemotePublicPath.ts', + ); + + const developmentHostConfig = loadHostConfig({ + nodeEnv: 'development', + remotePort: '3008', + }); + expect(developmentHostConfig.runtimePlugins).toEqual([]); + }); +}); From 2ff9d9ef815846a1a8f08d60d227c6753a236756 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 06:17:45 +0000 Subject: [PATCH 176/324] test(rsc-mf): replace runtime source checks with behavior guards --- tests/integration/rsc-mf/tests/index.test.ts | 35 -------------- .../rsc-mf/tests/initServerCallback.test.ts | 48 +++++++++++-------- .../tests/registerServerCallback.test.ts | 9 ++++ 3 files changed, 37 insertions(+), 55 deletions(-) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 0b7366f50a36..c3963bcadc3f 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -493,14 +493,6 @@ function runTests({ mode }: TestConfig) { const hostSourceTexts = hostSourceFilePaths.map(filePath => fs.readFileSync(filePath, 'utf-8'), ); - const runtimeInitSource = fs.readFileSync( - path.join(remoteDir, 'src/runtime/initServerCallback.ts'), - 'utf-8', - ); - const runtimeRegisterSource = fs.readFileSync( - path.join(remoteDir, 'src/runtime/registerServerCallback.ts'), - 'utf-8', - ); const remoteRuntimeExposesDir = path.join( remoteDir, 'src/runtime/exposes', @@ -545,33 +537,6 @@ function runTests({ mode }: TestConfig) { !source.includes('rsc-mf-react-server-dom-client-browser'), ), ).toBe(true); - expect(runtimeInitSource).toContain("import('./registerServerCallback')"); - expect(runtimeInitSource).toContain('window.location.origin'); - expect(runtimeInitSource).toContain('window.location.pathname'); - expect(runtimeInitSource).not.toContain('RSC_MF_REMOTE_PORT'); - expect(runtimeInitSource).not.toContain('127.0.0.1:'); - expect(runtimeRegisterSource).toContain('setServerCallback'); - expect(runtimeRegisterSource).toContain( - "from 'rsc-mf-react-server-dom-client-browser'", - ); - expect(runtimeRegisterSource).not.toContain( - "from 'react-server-dom-rspack/client.browser'", - ); - expect(runtimeRegisterSource).not.toContain('127.0.0.1:'); - expect(runtimeRegisterSource).not.toContain('window.location'); - expect(runtimeRegisterSource).toContain("'x-rsc-action': hostActionId"); - expect(runtimeRegisterSource).toContain("method: 'POST'"); - expect(runtimeRegisterSource).toContain("Accept: 'text/x-component'"); - expect(runtimeRegisterSource).toContain('getNormalizedRemoteActionUrl'); - expect(runtimeRegisterSource).toContain( - 'Remote action callback URL must use http or https', - ); - expect(runtimeRegisterSource).toContain( - 'Remote action callback URL must not include embedded credentials.', - ); - expect(runtimeRegisterSource).not.toContain( - 'remoteActionIdToHostProxyActionId', - ); expect(remoteRuntimeExposeEntries).toEqual([]); expect(remoteRuntimeEntriesWithoutExposeDir).toEqual([ 'initServerCallback.ts', diff --git a/tests/integration/rsc-mf/tests/initServerCallback.test.ts b/tests/integration/rsc-mf/tests/initServerCallback.test.ts index 42bbc0111b78..d0a89d120fd4 100644 --- a/tests/integration/rsc-mf/tests/initServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/initServerCallback.test.ts @@ -9,6 +9,22 @@ const flushMicrotasks = async () => { describe('initServerCallback runtime bootstrap behavior', () => { const originalWindow = (globalThis as { window?: unknown }).window; + const setupRegisterCallbackMock = () => { + const mockRegisterRemoteServerCallback = jest.fn(); + let registerModuleLoadCount = 0; + + jest.doMock(REGISTER_SERVER_CALLBACK_MODULE, () => { + registerModuleLoadCount += 1; + return { + registerRemoteServerCallback: mockRegisterRemoteServerCallback, + }; + }); + + return { + mockRegisterRemoteServerCallback, + getRegisterModuleLoadCount: () => registerModuleLoadCount, + }; + }; afterEach(() => { if (typeof originalWindow === 'undefined') { @@ -22,15 +38,13 @@ describe('initServerCallback runtime bootstrap behavior', () => { it('does not bootstrap callback registration on server import', async () => { jest.resetModules(); delete (globalThis as { window?: unknown }).window; - - const mockRegisterRemoteServerCallback = jest.fn(); - jest.doMock(REGISTER_SERVER_CALLBACK_MODULE, () => ({ - registerRemoteServerCallback: mockRegisterRemoteServerCallback, - })); + const { mockRegisterRemoteServerCallback, getRegisterModuleLoadCount } = + setupRegisterCallbackMock(); await import(INIT_SERVER_CALLBACK_MODULE); await flushMicrotasks(); + expect(getRegisterModuleLoadCount()).toBe(0); expect(mockRegisterRemoteServerCallback).not.toHaveBeenCalled(); }); @@ -42,15 +56,13 @@ describe('initServerCallback runtime bootstrap behavior', () => { pathname: '/server-component-root', }, }; - - const mockRegisterRemoteServerCallback = jest.fn(); - jest.doMock(REGISTER_SERVER_CALLBACK_MODULE, () => ({ - registerRemoteServerCallback: mockRegisterRemoteServerCallback, - })); + const { mockRegisterRemoteServerCallback, getRegisterModuleLoadCount } = + setupRegisterCallbackMock(); await import(INIT_SERVER_CALLBACK_MODULE); await flushMicrotasks(); + expect(getRegisterModuleLoadCount()).toBe(1); expect(mockRegisterRemoteServerCallback).toHaveBeenCalledTimes(1); expect(mockRegisterRemoteServerCallback).toHaveBeenCalledWith( 'http://127.0.0.1:3900/server-component-root', @@ -66,15 +78,13 @@ describe('initServerCallback runtime bootstrap behavior', () => { pathname: '', }, }; - - const mockRegisterRemoteServerCallback = jest.fn(); - jest.doMock(REGISTER_SERVER_CALLBACK_MODULE, () => ({ - registerRemoteServerCallback: mockRegisterRemoteServerCallback, - })); + const { mockRegisterRemoteServerCallback, getRegisterModuleLoadCount } = + setupRegisterCallbackMock(); await import(INIT_SERVER_CALLBACK_MODULE); await flushMicrotasks(); + expect(getRegisterModuleLoadCount()).toBe(1); expect(mockRegisterRemoteServerCallback).toHaveBeenCalledTimes(1); expect(mockRegisterRemoteServerCallback).toHaveBeenCalledWith( 'http://127.0.0.1:4100/', @@ -90,17 +100,15 @@ describe('initServerCallback runtime bootstrap behavior', () => { pathname: '/server-component-root', }, }; - - const mockRegisterRemoteServerCallback = jest.fn(); - jest.doMock(REGISTER_SERVER_CALLBACK_MODULE, () => ({ - registerRemoteServerCallback: mockRegisterRemoteServerCallback, - })); + const { mockRegisterRemoteServerCallback, getRegisterModuleLoadCount } = + setupRegisterCallbackMock(); await import(INIT_SERVER_CALLBACK_MODULE); await flushMicrotasks(); await import(INIT_SERVER_CALLBACK_MODULE); await flushMicrotasks(); + expect(getRegisterModuleLoadCount()).toBe(1); expect(mockRegisterRemoteServerCallback).toHaveBeenCalledTimes(1); }); }); diff --git a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts index 9d474aeb2a04..aac0f0041c5c 100644 --- a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts @@ -17,6 +17,15 @@ jest.mock( }), { virtual: true }, ); +jest.mock( + 'react-server-dom-rspack/client.browser', + () => { + throw new Error( + 'registerServerCallback must import rsc-mf-react-server-dom-client-browser alias', + ); + }, + { virtual: true }, +); const importRegisterHelper = async () => import('../remote/src/runtime/registerServerCallback'); From c429e030091ad37f8ea03638eff5e2540bfeab99 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 06:21:03 +0000 Subject: [PATCH 177/324] test(rsc-mf): trim remaining source-content guardrail assertions --- tests/integration/rsc-mf/tests/index.test.ts | 51 +------------------- 1 file changed, 1 insertion(+), 50 deletions(-) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index c3963bcadc3f..45e795c207db 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -470,29 +470,7 @@ function runTests({ mode }: TestConfig) { ).toBe(true); }); - it('should keep callback runtime wiring out of component sources', () => { - const getFilesRecursively = (directory: string): string[] => - fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => { - const entryPath = path.join(directory, entry.name); - if (entry.isDirectory()) { - return getFilesRecursively(entryPath); - } - return [entryPath]; - }); - - const componentFilePaths = getFilesRecursively( - path.join(remoteDir, 'src/components'), - ); - const hostSourceFilePaths = getFilesRecursively( - path.join(hostDir, 'src'), - ).filter(filePath => /\.(ts|tsx)$/.test(filePath)); - - const componentSources = componentFilePaths.map(filePath => - fs.readFileSync(filePath, 'utf-8'), - ); - const hostSourceTexts = hostSourceFilePaths.map(filePath => - fs.readFileSync(filePath, 'utf-8'), - ); + it('should keep callback helper surface constrained to runtime files', () => { const remoteRuntimeExposesDir = path.join( remoteDir, 'src/runtime/exposes', @@ -510,33 +488,6 @@ function runTests({ mode }: TestConfig) { entryName => entryName !== 'exposes', ); - expect( - componentSources.every( - source => !source.includes('initServerCallback'), - ), - ).toBe(true); - expect( - componentSources.every( - source => !source.includes('registerRemoteServerCallback'), - ), - ).toBe(true); - expect( - componentSources.every( - source => - !source.includes('setServerCallback') && - !source.includes('rsc-mf-react-server-dom-client-browser'), - ), - ).toBe(true); - expect( - hostSourceTexts.every( - source => - !source.includes('registerRemoteServerCallback') && - !source.includes('initServerCallback') && - !source.includes('registerServerCallback') && - !source.includes('setServerCallback') && - !source.includes('rsc-mf-react-server-dom-client-browser'), - ), - ).toBe(true); expect(remoteRuntimeExposeEntries).toEqual([]); expect(remoteRuntimeEntriesWithoutExposeDir).toEqual([ 'initServerCallback.ts', From 29137b84bd34adc398ac56313212ae908d29b56a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 06:24:58 +0000 Subject: [PATCH 178/324] test(rsc-mf): drop remaining filesystem source guard checks --- tests/integration/rsc-mf/tests/index.test.ts | 26 -------------------- 1 file changed, 26 deletions(-) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 45e795c207db..40b43a4c6f01 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -1,4 +1,3 @@ -import fs from 'fs'; import path from 'path'; import { isVersionAtLeast18 } from '@modern-js/utils'; import type { Browser, Page } from 'puppeteer'; @@ -470,31 +469,6 @@ function runTests({ mode }: TestConfig) { ).toBe(true); }); - it('should keep callback helper surface constrained to runtime files', () => { - const remoteRuntimeExposesDir = path.join( - remoteDir, - 'src/runtime/exposes', - ); - const remoteRuntimeExposeEntries = fs.existsSync(remoteRuntimeExposesDir) - ? fs - .readdirSync(remoteRuntimeExposesDir) - .filter(entryName => !entryName.startsWith('.')) - : []; - const remoteRuntimeEntries = fs - .readdirSync(path.join(remoteDir, 'src/runtime')) - .filter(entryName => !entryName.startsWith('.')) - .sort(); - const remoteRuntimeEntriesWithoutExposeDir = remoteRuntimeEntries.filter( - entryName => entryName !== 'exposes', - ); - - expect(remoteRuntimeExposeEntries).toEqual([]); - expect(remoteRuntimeEntriesWithoutExposeDir).toEqual([ - 'initServerCallback.ts', - 'registerServerCallback.ts', - ]); - }); - it('should not load callback helper expose chunk', () => { expect(registerCallbackExposeRequestUrls).toEqual([]); }); From b7dbdc6b68ccec866206f8b1b2bfda372adb3b1a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 06:26:15 +0000 Subject: [PATCH 179/324] test(rsc-mf): expand host config behavior coverage --- .../tests/moduleFederationConfig.test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts b/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts index 76e63c11f6f4..af391020e395 100644 --- a/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts +++ b/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts @@ -147,6 +147,17 @@ describe('rsc-mf module federation config contracts', () => { ); }); + it('falls back to default remote manifest port when env var is unset', () => { + const hostConfig = loadHostConfig({ + nodeEnv: 'test', + }); + expect(hostConfig.remotes).toEqual( + expect.objectContaining({ + rscRemote: 'rscRemote@http://127.0.0.1:3008/static/mf-manifest.json', + }), + ); + }); + it('enables host runtime plugin only in production', () => { const productionHostConfig = loadHostConfig({ nodeEnv: 'production', @@ -163,4 +174,17 @@ describe('rsc-mf module federation config contracts', () => { }); expect(developmentHostConfig.runtimePlugins).toEqual([]); }); + + it('keeps host experiments aligned for async startup and rsc', () => { + const hostConfig = loadHostConfig({ + nodeEnv: 'test', + remotePort: '3008', + }); + expect(hostConfig.experiments).toEqual( + expect.objectContaining({ + asyncStartup: true, + rsc: true, + }), + ); + }); }); From 396e7093a6caefcec4acd379214afb4d7a85d1a1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 06:30:12 +0000 Subject: [PATCH 180/324] test(rsc-mf): add modern config behavior contract tests --- .../rsc-mf/tests/modernConfig.test.ts | 301 ++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 tests/integration/rsc-mf/tests/modernConfig.test.ts diff --git a/tests/integration/rsc-mf/tests/modernConfig.test.ts b/tests/integration/rsc-mf/tests/modernConfig.test.ts new file mode 100644 index 000000000000..d56046dbe714 --- /dev/null +++ b/tests/integration/rsc-mf/tests/modernConfig.test.ts @@ -0,0 +1,301 @@ +import path from 'path'; + +const HOST_MODERN_CONFIG_MODULE = '../host/modern.config'; +const REMOTE_MODERN_CONFIG_MODULE = '../remote/modern.config'; + +const withEnv = ( + env: Partial>, + run: () => T, +): T => { + const previousPort = process.env.PORT; + const previousRemotePort = process.env.RSC_MF_REMOTE_PORT; + + if (typeof env.PORT === 'undefined') { + delete process.env.PORT; + } else { + process.env.PORT = env.PORT; + } + if (typeof env.RSC_MF_REMOTE_PORT === 'undefined') { + delete process.env.RSC_MF_REMOTE_PORT; + } else { + process.env.RSC_MF_REMOTE_PORT = env.RSC_MF_REMOTE_PORT; + } + + try { + return run(); + } finally { + if (typeof previousPort === 'undefined') { + delete process.env.PORT; + } else { + process.env.PORT = previousPort; + } + if (typeof previousRemotePort === 'undefined') { + delete process.env.RSC_MF_REMOTE_PORT; + } else { + process.env.RSC_MF_REMOTE_PORT = previousRemotePort; + } + } +}; + +const loadHostConfig = () => + withEnv({}, () => { + jest.resetModules(); + jest.doMock('@modern-js/app-tools', () => ({ + appTools: () => ({ name: 'app-tools-mock' }), + defineConfig: (config: unknown) => config, + })); + jest.doMock('@module-federation/modern-js-v3', () => ({ + moduleFederationPlugin: () => ({ name: 'mf-plugin-mock' }), + })); + + let config: any; + jest.isolateModules(() => { + config = require(HOST_MODERN_CONFIG_MODULE).default; + }); + return config; + }); + +const loadRemoteConfig = ({ + port, + remotePort, +}: { + port?: string; + remotePort?: string; +}) => + withEnv( + { + PORT: port, + RSC_MF_REMOTE_PORT: remotePort, + }, + () => { + jest.resetModules(); + jest.doMock('@modern-js/app-tools', () => ({ + appTools: () => ({ name: 'app-tools-mock' }), + defineConfig: (config: unknown) => config, + })); + jest.doMock('@module-federation/modern-js-v3', () => ({ + moduleFederationPlugin: () => ({ name: 'mf-plugin-mock' }), + })); + + let config: any; + jest.isolateModules(() => { + config = require(REMOTE_MODERN_CONFIG_MODULE).default; + }); + return config; + }, + ); + +const createChainHarness = (target: string | string[]) => { + const aliasMap = new Map(); + const conditionNames: string[] = []; + const moduleDirectories: string[] = []; + const publicPathCalls: string[] = []; + const splitChunksCalls: unknown[] = []; + const targetCalls: string[] = []; + const rules: Array<{ name: string; test?: RegExp; layer?: string }> = []; + + const aliasApi = { + set: (key: string, value: string) => { + aliasMap.set(key, value); + return aliasApi; + }, + }; + const conditionNamesApi = { + clear: () => { + conditionNames.length = 0; + return conditionNamesApi; + }, + add: (value: string) => { + conditionNames.push(value); + return conditionNamesApi; + }, + }; + const modulesApi = { + clear: () => { + moduleDirectories.length = 0; + return modulesApi; + }, + add: (value: string) => { + moduleDirectories.push(value); + return modulesApi; + }, + }; + + const chain = { + get: (key: string) => (key === 'target' ? target : undefined), + target: (value: string) => { + targetCalls.push(value); + return chain; + }, + resolve: { + alias: aliasApi, + conditionNames: conditionNamesApi, + modules: modulesApi, + }, + output: { + publicPath: (value: string) => { + publicPathCalls.push(value); + return chain.output; + }, + }, + optimization: { + splitChunks: (value: unknown) => { + splitChunksCalls.push(value); + return chain.optimization; + }, + }, + module: { + rule: (name: string) => { + const rule = { name } as { + name: string; + test?: RegExp; + layer?: string; + }; + rules.push(rule); + return { + test: (value: RegExp) => { + rule.test = value; + return { + layer: (layerValue: string) => { + rule.layer = layerValue; + return chain.module; + }, + }; + }, + }; + }, + }, + }; + + return { + chain, + aliasMap, + conditionNames, + moduleDirectories, + publicPathCalls, + splitChunksCalls, + targetCalls, + rules, + }; +}; + +describe('rsc-mf modern config contracts', () => { + it('keeps host modern server and source contracts', () => { + const hostConfig = loadHostConfig(); + expect(hostConfig.server).toEqual( + expect.objectContaining({ + rsc: true, + port: 3007, + }), + ); + expect(hostConfig.source).toEqual( + expect.objectContaining({ + enableAsyncEntry: false, + }), + ); + expect(hostConfig.plugins).toHaveLength(2); + }); + + it('applies host async-node bundler behavior for node targets', () => { + const hostConfig = loadHostConfig(); + const harness = createChainHarness('node'); + hostConfig.tools?.bundlerChain?.(harness.chain as any); + + expect(harness.targetCalls).toContain('async-node'); + expect(harness.conditionNames).toEqual(['require', 'import', 'default']); + expect(harness.aliasMap.get('server-only$')).toMatch( + /server-only[\\/]empty\.js$/, + ); + expect(harness.moduleDirectories).toEqual([ + path.resolve(__dirname, '../host/node_modules'), + 'node_modules', + ]); + }); + + it('configures remote port-driven server and asset settings', () => { + const remoteConfig = loadRemoteConfig({ + remotePort: '3991', + }); + + expect(remoteConfig.server).toEqual( + expect.objectContaining({ + rsc: true, + ssr: false, + port: 3991, + }), + ); + expect(remoteConfig.output).toEqual( + expect.objectContaining({ + assetPrefix: 'http://127.0.0.1:3991', + }), + ); + expect(remoteConfig.source).toEqual( + expect.objectContaining({ + enableAsyncEntry: false, + }), + ); + }); + + it('enables remote ssr mode when explicit PORT is set', () => { + const remoteConfig = loadRemoteConfig({ + port: '4550', + }); + expect(remoteConfig.server).toEqual( + expect.objectContaining({ + ssr: true, + port: 4550, + }), + ); + }); + + it('applies remote async-node + layer settings for node targets', () => { + const remoteConfig = loadRemoteConfig({ + remotePort: '3777', + }); + const harness = createChainHarness('node'); + remoteConfig.tools?.bundlerChain?.(harness.chain as any); + + expect(harness.targetCalls).toContain('async-node'); + expect(harness.conditionNames).toEqual(['require', 'import', 'default']); + expect(harness.aliasMap.get('server-only$')).toMatch( + /server-only[\\/]empty\.js$/, + ); + expect(harness.aliasMap.get('react/jsx-runtime$')).toMatch( + /react[\\/]jsx-runtime\.react-server\.js$/, + ); + expect(harness.aliasMap.get('react/jsx-dev-runtime$')).toMatch( + /react[\\/]jsx-dev-runtime\.react-server\.js$/, + ); + expect( + harness.aliasMap.get('rsc-mf-react-server-dom-client-browser$'), + ).toContain('react-server-dom-rspack'); + expect(harness.publicPathCalls).toContain('http://127.0.0.1:3777/bundles/'); + expect(harness.rules).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'rsc-mf-remote-components-layer', + layer: 'react-server-components', + }), + ]), + ); + expect(harness.moduleDirectories).toEqual([ + path.resolve(__dirname, '../remote/node_modules'), + 'node_modules', + ]); + }); + + it('applies remote client split-chunk + publicPath settings for web targets', () => { + const remoteConfig = loadRemoteConfig({ + remotePort: '3888', + }); + const harness = createChainHarness('web'); + remoteConfig.tools?.bundlerChain?.(harness.chain as any); + + expect(harness.targetCalls).toEqual([]); + expect(harness.splitChunksCalls).toEqual([false]); + expect(harness.publicPathCalls).toContain('http://127.0.0.1:3888/'); + expect( + harness.aliasMap.get('rsc-mf-react-server-dom-client-browser$'), + ).toContain('react-server-dom-rspack'); + }); +}); From 23831f78e3b7b2b8efe182a3493b30dde59bd21d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 06:33:02 +0000 Subject: [PATCH 181/324] test(rsc-mf): verify host shared scope alignment --- .../tests/moduleFederationConfig.test.ts | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts b/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts index af391020e395..95f362177c1d 100644 --- a/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts +++ b/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts @@ -19,6 +19,7 @@ const EXPECTED_REMOTE_EXPOSE_KEYS = [ ].sort(); const CALLBACK_BOOTSTRAP_IMPORT = './src/runtime/initServerCallback.ts'; +const EXPECTED_SHARED_SCOPES = ['default', 'ssr', 'rsc']; const withEnv = ( env: Partial>, @@ -118,15 +119,15 @@ describe('rsc-mf module federation config contracts', () => { Record >; - expect(sharedScopes).toHaveLength(3); - expect(sharedScopes[0]?.react?.shareScope).toBe('default'); - expect(sharedScopes[1]?.react?.shareScope).toBe('ssr'); - expect(sharedScopes[2]?.react?.shareScope).toBe('rsc'); + expect(sharedScopes).toHaveLength(EXPECTED_SHARED_SCOPES.length); + expect(sharedScopes.map(scope => scope.react?.shareScope)).toEqual( + EXPECTED_SHARED_SCOPES, + ); expect( sharedScopes.map( scope => scope['react-server-dom-rspack/client.browser']?.shareScope, ), - ).toEqual(['default', 'ssr', 'rsc']); + ).toEqual(EXPECTED_SHARED_SCOPES); expect(remoteConfig.experiments).toEqual( expect.objectContaining({ asyncStartup: true, @@ -187,4 +188,24 @@ describe('rsc-mf module federation config contracts', () => { }), ); }); + + it('keeps host shared scopes aligned for rsc runtime compatibility', () => { + const hostConfig = loadHostConfig({ + nodeEnv: 'test', + remotePort: '3008', + }); + const sharedScopes = hostConfig.shared as Array< + Record + >; + + expect(sharedScopes).toHaveLength(EXPECTED_SHARED_SCOPES.length); + expect(sharedScopes.map(scope => scope.react?.shareScope)).toEqual( + EXPECTED_SHARED_SCOPES, + ); + expect( + sharedScopes.map( + scope => scope['react-server-dom-rspack/client.browser']?.shareScope, + ), + ).toEqual(EXPECTED_SHARED_SCOPES); + }); }); From c535de5bfae45cbb261be395f866e38f2b01a80f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 06:36:18 +0000 Subject: [PATCH 182/324] refactor(rsc-mf): validate callback URL parse before protocol checks --- .../rsc-mf/remote/src/runtime/registerServerCallback.ts | 9 ++++++++- .../rsc-mf/tests/registerServerCallback.test.ts | 3 +++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts index f0dd2c43482e..e00a2a984828 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts @@ -26,7 +26,14 @@ const getHostActionId = (rawActionId: string, remoteAlias: string) => { return `remote:${remoteAlias}:${normalizedRawActionId}`; }; const getNormalizedRemoteActionUrl = (remoteOrigin: string) => { - const url = new URL(remoteOrigin); + let url: URL; + try { + url = new URL(remoteOrigin); + } catch { + throw new Error( + `Remote action callback URL must be an absolute http(s) URL. Received: ${remoteOrigin}`, + ); + } if (url.protocol !== 'http:' && url.protocol !== 'https:') { throw new Error( `Remote action callback URL must use http or https. Received protocol: ${url.protocol}`, diff --git a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts index aac0f0041c5c..497bd11cbe07 100644 --- a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts @@ -269,6 +269,9 @@ describe('registerRemoteServerCallback runtime behavior', () => { expect(() => registerRemoteServerCallback('javascript:alert(1)', 'rscRemote'), ).toThrow('Remote action callback URL must use http or https'); + expect(() => + registerRemoteServerCallback('not-a-url', 'rscRemote'), + ).toThrow('Remote action callback URL must be an absolute http(s) URL'); expect(() => registerRemoteServerCallback( 'ftp://127.0.0.1:3008/server-component-root', From a38a1d3407774fd7c3afe29a76ad9d115467ca33 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 06:39:10 +0000 Subject: [PATCH 183/324] refactor(rsc-mf): remove redundant remote bridge import anchor --- .../host/src/server-component-root/HostRemoteActionRunner.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx b/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx index a3d6047f21df..45ba50d30272 100644 --- a/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx +++ b/tests/integration/rsc-mf/host/src/server-component-root/HostRemoteActionRunner.tsx @@ -9,9 +9,6 @@ import { defaultRemoteAction } from 'rscRemote/defaultAction'; import { nestedRemoteAction } from 'rscRemote/nestedActions'; export default function HostRemoteActionRunner() { - // Keep this import in the client graph so federated RSC bridge IDs - // can map back to a concrete remote module factory at runtime. - void RemoteClientCounterBridge; const [defaultResult, setDefaultResult] = useState(''); const [echoResult, setEchoResult] = useState(''); const [nestedResult, setNestedResult] = useState(''); From 7e0c483551b894d8efd3f9066f63d940401a5d96 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 06:40:38 +0000 Subject: [PATCH 184/324] refactor(rsc-mf): rely on default remote alias in bootstrap --- .../integration/rsc-mf/remote/src/runtime/initServerCallback.ts | 1 - tests/integration/rsc-mf/tests/initServerCallback.test.ts | 2 -- 2 files changed, 3 deletions(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts b/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts index d8b383dc7695..a711a34c4f19 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts @@ -7,7 +7,6 @@ const bootstrapServerCallback = () => { const actionPathname = window.location.pathname || '/'; registerRemoteServerCallback( `${window.location.origin}${actionPathname}`, - 'rscRemote', ); }, ); diff --git a/tests/integration/rsc-mf/tests/initServerCallback.test.ts b/tests/integration/rsc-mf/tests/initServerCallback.test.ts index d0a89d120fd4..b6e5740b2a27 100644 --- a/tests/integration/rsc-mf/tests/initServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/initServerCallback.test.ts @@ -66,7 +66,6 @@ describe('initServerCallback runtime bootstrap behavior', () => { expect(mockRegisterRemoteServerCallback).toHaveBeenCalledTimes(1); expect(mockRegisterRemoteServerCallback).toHaveBeenCalledWith( 'http://127.0.0.1:3900/server-component-root', - 'rscRemote', ); }); @@ -88,7 +87,6 @@ describe('initServerCallback runtime bootstrap behavior', () => { expect(mockRegisterRemoteServerCallback).toHaveBeenCalledTimes(1); expect(mockRegisterRemoteServerCallback).toHaveBeenCalledWith( 'http://127.0.0.1:4100/', - 'rscRemote', ); }); From d30b0e2b63dfd72d057076fd9711ca20a5ee5a04 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 06:44:10 +0000 Subject: [PATCH 185/324] test(rsc-mf): assert no chunkLoadingGlobal modern config coupling --- tests/integration/rsc-mf/tests/modernConfig.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/integration/rsc-mf/tests/modernConfig.test.ts b/tests/integration/rsc-mf/tests/modernConfig.test.ts index d56046dbe714..8fb549a09b33 100644 --- a/tests/integration/rsc-mf/tests/modernConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernConfig.test.ts @@ -90,6 +90,7 @@ const createChainHarness = (target: string | string[]) => { const conditionNames: string[] = []; const moduleDirectories: string[] = []; const publicPathCalls: string[] = []; + const chunkLoadingGlobalCalls: string[] = []; const splitChunksCalls: unknown[] = []; const targetCalls: string[] = []; const rules: Array<{ name: string; test?: RegExp; layer?: string }> = []; @@ -137,6 +138,10 @@ const createChainHarness = (target: string | string[]) => { publicPathCalls.push(value); return chain.output; }, + chunkLoadingGlobal: (value: string) => { + chunkLoadingGlobalCalls.push(value); + return chain.output; + }, }, optimization: { splitChunks: (value: unknown) => { @@ -173,6 +178,7 @@ const createChainHarness = (target: string | string[]) => { conditionNames, moduleDirectories, publicPathCalls, + chunkLoadingGlobalCalls, splitChunksCalls, targetCalls, rules, @@ -193,6 +199,7 @@ describe('rsc-mf modern config contracts', () => { enableAsyncEntry: false, }), ); + expect(hostConfig.source).not.toHaveProperty('preEntry'); expect(hostConfig.plugins).toHaveLength(2); }); @@ -270,6 +277,7 @@ describe('rsc-mf modern config contracts', () => { harness.aliasMap.get('rsc-mf-react-server-dom-client-browser$'), ).toContain('react-server-dom-rspack'); expect(harness.publicPathCalls).toContain('http://127.0.0.1:3777/bundles/'); + expect(harness.chunkLoadingGlobalCalls).toEqual([]); expect(harness.rules).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -294,6 +302,7 @@ describe('rsc-mf modern config contracts', () => { expect(harness.targetCalls).toEqual([]); expect(harness.splitChunksCalls).toEqual([false]); expect(harness.publicPathCalls).toContain('http://127.0.0.1:3888/'); + expect(harness.chunkLoadingGlobalCalls).toEqual([]); expect( harness.aliasMap.get('rsc-mf-react-server-dom-client-browser$'), ).toContain('react-server-dom-rspack'); From eab45df266905dde808c2be0cf4c500e44771d7a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 06:47:07 +0000 Subject: [PATCH 186/324] test(rsc-mf): assert modern plugin options and preEntry absence --- .../rsc-mf/tests/modernConfig.test.ts | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/tests/integration/rsc-mf/tests/modernConfig.test.ts b/tests/integration/rsc-mf/tests/modernConfig.test.ts index 8fb549a09b33..faf581c7e20b 100644 --- a/tests/integration/rsc-mf/tests/modernConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernConfig.test.ts @@ -45,7 +45,10 @@ const loadHostConfig = () => defineConfig: (config: unknown) => config, })); jest.doMock('@module-federation/modern-js-v3', () => ({ - moduleFederationPlugin: () => ({ name: 'mf-plugin-mock' }), + moduleFederationPlugin: (options: unknown) => ({ + name: 'mf-plugin-mock', + options, + }), })); let config: any; @@ -74,7 +77,10 @@ const loadRemoteConfig = ({ defineConfig: (config: unknown) => config, })); jest.doMock('@module-federation/modern-js-v3', () => ({ - moduleFederationPlugin: () => ({ name: 'mf-plugin-mock' }), + moduleFederationPlugin: (options: unknown) => ({ + name: 'mf-plugin-mock', + options, + }), })); let config: any; @@ -201,6 +207,15 @@ describe('rsc-mf modern config contracts', () => { ); expect(hostConfig.source).not.toHaveProperty('preEntry'); expect(hostConfig.plugins).toHaveLength(2); + expect(hostConfig.plugins).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'app-tools-mock' }), + expect.objectContaining({ + name: 'mf-plugin-mock', + options: expect.objectContaining({ ssr: true }), + }), + ]), + ); }); it('applies host async-node bundler behavior for node targets', () => { @@ -241,6 +256,15 @@ describe('rsc-mf modern config contracts', () => { enableAsyncEntry: false, }), ); + expect(remoteConfig.plugins).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'app-tools-mock' }), + expect.objectContaining({ + name: 'mf-plugin-mock', + options: expect.objectContaining({ ssr: true }), + }), + ]), + ); }); it('enables remote ssr mode when explicit PORT is set', () => { From 7dc783c16c92b39d74f22cd2d285380319b26889 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 06:51:46 +0000 Subject: [PATCH 187/324] refactor(rsc-mf): retry callback bootstrap on transient failures --- .../remote/src/runtime/initServerCallback.ts | 10 +++ .../rsc-mf/tests/initServerCallback.test.ts | 71 +++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts b/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts index a711a34c4f19..baa4888dac17 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts @@ -1,4 +1,6 @@ let callbackBootstrapPromise: Promise | undefined; +const MAX_CALLBACK_BOOTSTRAP_RETRIES = 2; +let callbackBootstrapRetryCount = 0; const bootstrapServerCallback = () => { if (!callbackBootstrapPromise) { @@ -8,10 +10,18 @@ const bootstrapServerCallback = () => { registerRemoteServerCallback( `${window.location.origin}${actionPathname}`, ); + callbackBootstrapRetryCount = 0; }, ); callbackBootstrapPromise.catch(() => { callbackBootstrapPromise = undefined; + if (callbackBootstrapRetryCount >= MAX_CALLBACK_BOOTSTRAP_RETRIES) { + return; + } + callbackBootstrapRetryCount += 1; + queueMicrotask(() => { + void bootstrapServerCallback(); + }); }); } diff --git a/tests/integration/rsc-mf/tests/initServerCallback.test.ts b/tests/integration/rsc-mf/tests/initServerCallback.test.ts index b6e5740b2a27..9da71165ea7a 100644 --- a/tests/integration/rsc-mf/tests/initServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/initServerCallback.test.ts @@ -6,6 +6,11 @@ const flushMicrotasks = async () => { await Promise.resolve(); await Promise.resolve(); }; +const flushRepeatedMicrotasks = async (count: number) => { + for (let index = 0; index < count; index += 1) { + await Promise.resolve(); + } +}; describe('initServerCallback runtime bootstrap behavior', () => { const originalWindow = (globalThis as { window?: unknown }).window; @@ -109,4 +114,70 @@ describe('initServerCallback runtime bootstrap behavior', () => { expect(getRegisterModuleLoadCount()).toBe(1); expect(mockRegisterRemoteServerCallback).toHaveBeenCalledTimes(1); }); + + it('retries callback bootstrap after transient registration failure', async () => { + jest.resetModules(); + (globalThis as { window?: unknown }).window = { + location: { + origin: 'http://127.0.0.1:4500', + pathname: '/server-component-root', + }, + }; + + const transientError = new Error('transient-bootstrap-failure'); + const mockRegisterRemoteServerCallback = jest + .fn() + .mockImplementationOnce(() => { + throw transientError; + }) + .mockImplementation(() => undefined); + let registerModuleLoadCount = 0; + jest.doMock(REGISTER_SERVER_CALLBACK_MODULE, () => { + registerModuleLoadCount += 1; + return { + registerRemoteServerCallback: mockRegisterRemoteServerCallback, + }; + }); + + await import(INIT_SERVER_CALLBACK_MODULE); + await flushRepeatedMicrotasks(20); + + expect(registerModuleLoadCount).toBe(1); + expect(mockRegisterRemoteServerCallback).toHaveBeenCalledTimes(2); + expect(mockRegisterRemoteServerCallback).toHaveBeenNthCalledWith( + 1, + 'http://127.0.0.1:4500/server-component-root', + ); + expect(mockRegisterRemoteServerCallback).toHaveBeenNthCalledWith( + 2, + 'http://127.0.0.1:4500/server-component-root', + ); + }); + + it('caps callback bootstrap retries after repeated failures', async () => { + jest.resetModules(); + (globalThis as { window?: unknown }).window = { + location: { + origin: 'http://127.0.0.1:4700', + pathname: '/server-component-root', + }, + }; + + const mockRegisterRemoteServerCallback = jest.fn(() => { + throw new Error('persistent-bootstrap-failure'); + }); + let registerModuleLoadCount = 0; + jest.doMock(REGISTER_SERVER_CALLBACK_MODULE, () => { + registerModuleLoadCount += 1; + return { + registerRemoteServerCallback: mockRegisterRemoteServerCallback, + }; + }); + + await import(INIT_SERVER_CALLBACK_MODULE); + await flushRepeatedMicrotasks(40); + + expect(registerModuleLoadCount).toBe(1); + expect(mockRegisterRemoteServerCallback).toHaveBeenCalledTimes(3); + }); }); From 8830523bdce66fef49897a022b4b18abdcb48633 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 06:54:40 +0000 Subject: [PATCH 188/324] refactor(rsc-mf): simplify callback keying and cover default alias --- .../src/runtime/registerServerCallback.ts | 10 +++++----- .../tests/registerServerCallback.test.ts | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts index e00a2a984828..83e6c1ea9baf 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts @@ -7,6 +7,7 @@ import { let registeredCallbackKey = ''; const ALIAS_TOKEN_PATTERN = /^[A-Za-z0-9_.-]+$/; +const DEFAULT_REMOTE_ALIAS = 'rscRemote'; const getNormalizedRawActionId = (rawActionId: string) => { const normalizedRawActionId = rawActionId.trim(); if (!normalizedRawActionId || /\s/.test(normalizedRawActionId)) { @@ -48,10 +49,12 @@ const getNormalizedRemoteActionUrl = (remoteOrigin: string) => { url.hash = ''; return url.toString(); }; +const getCallbackKey = (remoteAlias: string, remoteActionUrl: string) => + `${remoteAlias}::${remoteActionUrl}`; export function registerRemoteServerCallback( remoteOrigin: string, - remoteAlias = 'rscRemote', + remoteAlias = DEFAULT_REMOTE_ALIAS, ) { const normalizedRemoteOrigin = remoteOrigin.trim(); if (!normalizedRemoteOrigin) { @@ -68,10 +71,7 @@ export function registerRemoteServerCallback( ); } const remoteActionUrl = getNormalizedRemoteActionUrl(normalizedRemoteOrigin); - const callbackKey = JSON.stringify({ - remoteAlias: normalizedRemoteAlias, - remoteActionUrl, - }); + const callbackKey = getCallbackKey(normalizedRemoteAlias, remoteActionUrl); if (registeredCallbackKey === callbackKey) { return; } diff --git a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts index 497bd11cbe07..d2868b863fcb 100644 --- a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts @@ -86,6 +86,23 @@ describe('registerRemoteServerCallback runtime behavior', () => { expect(mockCreateFromFetch).toHaveBeenCalledTimes(1); }); + it('uses default alias when remote alias is omitted', async () => { + const { registerRemoteServerCallback } = await importRegisterHelper(); + registerRemoteServerCallback('http://127.0.0.1:3008/server-component-root'); + + const callback = getRegisteredCallback(); + await callback('default-alias-action', []); + + expect(global.fetch).toHaveBeenCalledWith( + 'http://127.0.0.1:3008/server-component-root', + expect.objectContaining({ + headers: expect.objectContaining({ + 'x-rsc-action': 'remote:rscRemote:default-alias-action', + }), + }), + ); + }); + it('normalizes action ids and rejects whitespace-delimited ids', async () => { const { registerRemoteServerCallback } = await importRegisterHelper(); registerRemoteServerCallback('http://127.0.0.1:3008/server-component-root'); @@ -272,6 +289,9 @@ describe('registerRemoteServerCallback runtime behavior', () => { expect(() => registerRemoteServerCallback('not-a-url', 'rscRemote'), ).toThrow('Remote action callback URL must be an absolute http(s) URL'); + expect(() => + registerRemoteServerCallback('/server-component-root', 'rscRemote'), + ).toThrow('Remote action callback URL must be an absolute http(s) URL'); expect(() => registerRemoteServerCallback( 'ftp://127.0.0.1:3008/server-component-root', From 0e4dd7af1ebf084b317ae51af66f9e9e0dadebae Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 06:57:43 +0000 Subject: [PATCH 189/324] test(rsc-mf): cover host web chain and remote port precedence --- .../rsc-mf/tests/modernConfig.test.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/integration/rsc-mf/tests/modernConfig.test.ts b/tests/integration/rsc-mf/tests/modernConfig.test.ts index faf581c7e20b..c79faab47621 100644 --- a/tests/integration/rsc-mf/tests/modernConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernConfig.test.ts @@ -234,6 +234,20 @@ describe('rsc-mf modern config contracts', () => { ]); }); + it('keeps host web-target bundler chain free of node-only aliases', () => { + const hostConfig = loadHostConfig(); + const harness = createChainHarness('web'); + hostConfig.tools?.bundlerChain?.(harness.chain as any); + + expect(harness.targetCalls).toEqual([]); + expect(harness.aliasMap.has('server-only$')).toBe(false); + expect(harness.conditionNames).toEqual([]); + expect(harness.moduleDirectories).toEqual([ + path.resolve(__dirname, '../host/node_modules'), + 'node_modules', + ]); + }); + it('configures remote port-driven server and asset settings', () => { const remoteConfig = loadRemoteConfig({ remotePort: '3991', @@ -279,6 +293,24 @@ describe('rsc-mf modern config contracts', () => { ); }); + it('keeps remote port precedence deterministic when PORT and remote port are both set', () => { + const remoteConfig = loadRemoteConfig({ + port: '4550', + remotePort: '3881', + }); + expect(remoteConfig.server).toEqual( + expect.objectContaining({ + ssr: true, + port: 3881, + }), + ); + expect(remoteConfig.output).toEqual( + expect.objectContaining({ + assetPrefix: 'http://127.0.0.1:3881', + }), + ); + }); + it('applies remote async-node + layer settings for node targets', () => { const remoteConfig = loadRemoteConfig({ remotePort: '3777', From c6dc28553166ac7ff58966c333d160b58b1b6939 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 07:00:54 +0000 Subject: [PATCH 190/324] refactor(rsc-mf): fallback bootstrap retries without queueMicrotask --- .../remote/src/runtime/initServerCallback.ts | 10 ++++- .../rsc-mf/tests/initServerCallback.test.ts | 44 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts b/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts index baa4888dac17..34f1a889aa00 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts @@ -1,6 +1,14 @@ let callbackBootstrapPromise: Promise | undefined; const MAX_CALLBACK_BOOTSTRAP_RETRIES = 2; let callbackBootstrapRetryCount = 0; +const scheduleRetryTask = (task: () => void) => { + if (typeof queueMicrotask === 'function') { + queueMicrotask(task); + return; + } + + void Promise.resolve().then(task); +}; const bootstrapServerCallback = () => { if (!callbackBootstrapPromise) { @@ -19,7 +27,7 @@ const bootstrapServerCallback = () => { return; } callbackBootstrapRetryCount += 1; - queueMicrotask(() => { + scheduleRetryTask(() => { void bootstrapServerCallback(); }); }); diff --git a/tests/integration/rsc-mf/tests/initServerCallback.test.ts b/tests/integration/rsc-mf/tests/initServerCallback.test.ts index 9da71165ea7a..9bab9c18e84c 100644 --- a/tests/integration/rsc-mf/tests/initServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/initServerCallback.test.ts @@ -14,6 +14,14 @@ const flushRepeatedMicrotasks = async (count: number) => { describe('initServerCallback runtime bootstrap behavior', () => { const originalWindow = (globalThis as { window?: unknown }).window; + const originalQueueMicrotask = globalThis.queueMicrotask; + const overrideQueueMicrotask = (value: typeof queueMicrotask | undefined) => { + Object.defineProperty(globalThis, 'queueMicrotask', { + configurable: true, + writable: true, + value, + }); + }; const setupRegisterCallbackMock = () => { const mockRegisterRemoteServerCallback = jest.fn(); let registerModuleLoadCount = 0; @@ -32,6 +40,7 @@ describe('initServerCallback runtime bootstrap behavior', () => { }; afterEach(() => { + overrideQueueMicrotask(originalQueueMicrotask); if (typeof originalWindow === 'undefined') { delete (globalThis as { window?: unknown }).window; return; @@ -180,4 +189,39 @@ describe('initServerCallback runtime bootstrap behavior', () => { expect(registerModuleLoadCount).toBe(1); expect(mockRegisterRemoteServerCallback).toHaveBeenCalledTimes(3); }); + + it('falls back to promise microtask retry when queueMicrotask is unavailable', async () => { + jest.resetModules(); + overrideQueueMicrotask(undefined); + (globalThis as { window?: unknown }).window = { + location: { + origin: 'http://127.0.0.1:4800', + pathname: '/server-component-root', + }, + }; + + const mockRegisterRemoteServerCallback = jest + .fn() + .mockImplementationOnce(() => { + throw new Error('bootstrap-failure-without-queueMicrotask'); + }) + .mockImplementation(() => undefined); + + jest.doMock(REGISTER_SERVER_CALLBACK_MODULE, () => ({ + registerRemoteServerCallback: mockRegisterRemoteServerCallback, + })); + + await import(INIT_SERVER_CALLBACK_MODULE); + await flushRepeatedMicrotasks(30); + + expect(mockRegisterRemoteServerCallback).toHaveBeenCalledTimes(2); + expect(mockRegisterRemoteServerCallback).toHaveBeenNthCalledWith( + 1, + 'http://127.0.0.1:4800/server-component-root', + ); + expect(mockRegisterRemoteServerCallback).toHaveBeenNthCalledWith( + 2, + 'http://127.0.0.1:4800/server-component-root', + ); + }); }); From cfbc77e19cf5263877023f9c0cc5eb97a00f2689 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 07:03:58 +0000 Subject: [PATCH 191/324] test(rsc-mf): assert no failed host or remote responses --- tests/integration/rsc-mf/tests/index.test.ts | 29 ++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 40b43a4c6f01..dabef18b887b 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -46,6 +46,11 @@ interface TestContext { page: Page; actionRequestIds?: string[]; } +interface FailedRequestRecord { + url: string; + method: string; + status: number; +} async function waitForActionRequestCount({ actionRequestIds, @@ -357,6 +362,7 @@ function runTests({ mode }: TestConfig) { const actionRequestIds: string[] = []; const actionRequestAcceptHeaders: string[] = []; const registerCallbackExposeRequestUrls: string[] = []; + const failedNetworkRequests: FailedRequestRecord[] = []; if (skipForLowerNodeVersion()) { return; @@ -421,6 +427,25 @@ function runTests({ mode }: TestConfig) { actionRequestIds.push(headers['x-rsc-action']); actionRequestAcceptHeaders.push(headers.accept || ''); }); + + page.on('response', response => { + const status = response.status(); + if (status < 400) { + return; + } + const url = response.url(); + const request = response.request(); + const hostOrigin = `http://127.0.0.1:${hostPort}`; + const remoteOrigin = `http://127.0.0.1:${remotePort}`; + if (!url.startsWith(hostOrigin) && !url.startsWith(remoteOrigin)) { + return; + } + failedNetworkRequests.push({ + url, + method: request.method(), + status, + }); + }); }); afterAll(async () => { @@ -536,6 +561,10 @@ function runTests({ mode }: TestConfig) { it('should have no browser runtime errors', () => { expect(runtimeErrors).toEqual([]); }); + + it('should have no failed host or remote network responses', () => { + expect(failedNetworkRequests).toEqual([]); + }); }); } From e9ca0b6522247017814ecf97c8d3bddb5ef7ec3a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 07:07:07 +0000 Subject: [PATCH 192/324] refactor(rsc-mf): normalize callback action pathname before registration --- .../remote/src/runtime/initServerCallback.ts | 11 +++++- .../rsc-mf/tests/initServerCallback.test.ts | 38 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts b/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts index 34f1a889aa00..8fde3f9bd640 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/initServerCallback.ts @@ -1,6 +1,13 @@ let callbackBootstrapPromise: Promise | undefined; const MAX_CALLBACK_BOOTSTRAP_RETRIES = 2; let callbackBootstrapRetryCount = 0; +const getNormalizedActionPathname = (pathname?: string) => { + const trimmedPathname = pathname?.trim() || '/'; + if (trimmedPathname.startsWith('/')) { + return trimmedPathname; + } + return `/${trimmedPathname}`; +}; const scheduleRetryTask = (task: () => void) => { if (typeof queueMicrotask === 'function') { queueMicrotask(task); @@ -14,7 +21,9 @@ const bootstrapServerCallback = () => { if (!callbackBootstrapPromise) { callbackBootstrapPromise = import('./registerServerCallback').then( ({ registerRemoteServerCallback }) => { - const actionPathname = window.location.pathname || '/'; + const actionPathname = getNormalizedActionPathname( + window.location.pathname, + ); registerRemoteServerCallback( `${window.location.origin}${actionPathname}`, ); diff --git a/tests/integration/rsc-mf/tests/initServerCallback.test.ts b/tests/integration/rsc-mf/tests/initServerCallback.test.ts index 9bab9c18e84c..28ef7bab28e4 100644 --- a/tests/integration/rsc-mf/tests/initServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/initServerCallback.test.ts @@ -104,6 +104,44 @@ describe('initServerCallback runtime bootstrap behavior', () => { ); }); + it('normalizes non-slash pathnames before callback registration', async () => { + jest.resetModules(); + (globalThis as { window?: unknown }).window = { + location: { + origin: 'http://127.0.0.1:4200', + pathname: 'server-component-root', + }, + }; + const { mockRegisterRemoteServerCallback } = setupRegisterCallbackMock(); + + await import(INIT_SERVER_CALLBACK_MODULE); + await flushMicrotasks(); + + expect(mockRegisterRemoteServerCallback).toHaveBeenCalledTimes(1); + expect(mockRegisterRemoteServerCallback).toHaveBeenCalledWith( + 'http://127.0.0.1:4200/server-component-root', + ); + }); + + it('normalizes whitespace-only pathnames to root', async () => { + jest.resetModules(); + (globalThis as { window?: unknown }).window = { + location: { + origin: 'http://127.0.0.1:4250', + pathname: ' ', + }, + }; + const { mockRegisterRemoteServerCallback } = setupRegisterCallbackMock(); + + await import(INIT_SERVER_CALLBACK_MODULE); + await flushMicrotasks(); + + expect(mockRegisterRemoteServerCallback).toHaveBeenCalledTimes(1); + expect(mockRegisterRemoteServerCallback).toHaveBeenCalledWith( + 'http://127.0.0.1:4250/', + ); + }); + it('keeps bootstrap side effect memoized across repeated imports', async () => { jest.resetModules(); (globalThis as { window?: unknown }).window = { From 396a96d5ce58eb3a00b648da4b69642d5bf9c396 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 07:10:12 +0000 Subject: [PATCH 193/324] test(rsc-mf): assert no failed browser requests in dev and build --- tests/integration/rsc-mf/tests/index.test.ts | 24 ++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index dabef18b887b..f98704cfe6f5 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -51,6 +51,11 @@ interface FailedRequestRecord { method: string; status: number; } +interface FailedBrowserRequestRecord { + url: string; + method: string; + failureText: string; +} async function waitForActionRequestCount({ actionRequestIds, @@ -363,6 +368,7 @@ function runTests({ mode }: TestConfig) { const actionRequestAcceptHeaders: string[] = []; const registerCallbackExposeRequestUrls: string[] = []; const failedNetworkRequests: FailedRequestRecord[] = []; + const failedBrowserRequests: FailedBrowserRequestRecord[] = []; if (skipForLowerNodeVersion()) { return; @@ -446,6 +452,20 @@ function runTests({ mode }: TestConfig) { status, }); }); + + page.on('requestfailed', request => { + const url = request.url(); + const hostOrigin = `http://127.0.0.1:${hostPort}`; + const remoteOrigin = `http://127.0.0.1:${remotePort}`; + if (!url.startsWith(hostOrigin) && !url.startsWith(remoteOrigin)) { + return; + } + failedBrowserRequests.push({ + url, + method: request.method(), + failureText: request.failure()?.errorText || 'unknown', + }); + }); }); afterAll(async () => { @@ -565,6 +585,10 @@ function runTests({ mode }: TestConfig) { it('should have no failed host or remote network responses', () => { expect(failedNetworkRequests).toEqual([]); }); + + it('should have no failed host or remote browser requests', () => { + expect(failedBrowserRequests).toEqual([]); + }); }); } From 4af5b4ee3369fe48b1de42ced208145859a0177b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 07:13:17 +0000 Subject: [PATCH 194/324] refactor(rsc-mf): fail fast on non-ok callback responses --- .../src/runtime/registerServerCallback.ts | 9 +++++++-- .../tests/registerServerCallback.test.ts | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts index 83e6c1ea9baf..25880465a777 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts @@ -79,7 +79,7 @@ export function registerRemoteServerCallback( setServerCallback(async (id: string, args: unknown[]) => { const hostActionId = getHostActionId(id, normalizedRemoteAlias); const temporaryReferences = createTemporaryReferenceSet(); - const response = fetch(remoteActionUrl, { + const response = await fetch(remoteActionUrl, { method: 'POST', headers: { Accept: 'text/x-component', @@ -87,7 +87,12 @@ export function registerRemoteServerCallback( }, body: await encodeReply(args, { temporaryReferences }), }); - return createFromFetch(response, { temporaryReferences }); + if (!response.ok) { + throw new Error( + `Remote action callback request failed with status ${response.status} ${response.statusText} (${remoteActionUrl}).`, + ); + } + return createFromFetch(Promise.resolve(response), { temporaryReferences }); }); registeredCallbackKey = callbackKey; } diff --git a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts index d2868b863fcb..4e1af81c2286 100644 --- a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts @@ -86,6 +86,24 @@ describe('registerRemoteServerCallback runtime behavior', () => { expect(mockCreateFromFetch).toHaveBeenCalledTimes(1); }); + it('throws when callback fetch returns non-ok response', async () => { + const { registerRemoteServerCallback } = await importRegisterHelper(); + registerRemoteServerCallback('http://127.0.0.1:3008/server-component-root'); + global.fetch = jest.fn(async () => { + return { + ok: false, + status: 503, + statusText: 'Service Unavailable', + } as Response; + }); + + const callback = getRegisteredCallback(); + await expect(callback('fetch-failure-action', ['arg-1'])).rejects.toThrow( + 'Remote action callback request failed with status 503 Service Unavailable (http://127.0.0.1:3008/server-component-root).', + ); + expect(mockCreateFromFetch).not.toHaveBeenCalled(); + }); + it('uses default alias when remote alias is omitted', async () => { const { registerRemoteServerCallback } = await importRegisterHelper(); registerRemoteServerCallback('http://127.0.0.1:3008/server-component-root'); From 9fd29b5b777d2d6bc8fdd1abbe68f206fceb4633 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 07:19:42 +0000 Subject: [PATCH 195/324] refactor(rsc-mf): retry callback fetch once before failing --- .../src/runtime/registerServerCallback.ts | 53 +++++++++++++++---- .../tests/registerServerCallback.test.ts | 25 +++++++++ 2 files changed, 68 insertions(+), 10 deletions(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts index 25880465a777..04d02cf41e0a 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts @@ -8,6 +8,7 @@ import { let registeredCallbackKey = ''; const ALIAS_TOKEN_PATTERN = /^[A-Za-z0-9_.-]+$/; const DEFAULT_REMOTE_ALIAS = 'rscRemote'; +const MAX_CALLBACK_FETCH_RETRIES = 1; const getNormalizedRawActionId = (rawActionId: string) => { const normalizedRawActionId = rawActionId.trim(); if (!normalizedRawActionId || /\s/.test(normalizedRawActionId)) { @@ -79,20 +80,52 @@ export function registerRemoteServerCallback( setServerCallback(async (id: string, args: unknown[]) => { const hostActionId = getHostActionId(id, normalizedRemoteAlias); const temporaryReferences = createTemporaryReferenceSet(); - const response = await fetch(remoteActionUrl, { - method: 'POST', - headers: { - Accept: 'text/x-component', - 'x-rsc-action': hostActionId, - }, - body: await encodeReply(args, { temporaryReferences }), - }); - if (!response.ok) { + const requestBody = await encodeReply(args, { temporaryReferences }); + let response: Response | undefined; + let lastFetchError: unknown; + + for (let attempt = 0; attempt <= MAX_CALLBACK_FETCH_RETRIES; attempt += 1) { + try { + response = await fetch(remoteActionUrl, { + method: 'POST', + headers: { + Accept: 'text/x-component', + 'x-rsc-action': hostActionId, + }, + body: requestBody, + }); + } catch (error) { + lastFetchError = error; + if (attempt < MAX_CALLBACK_FETCH_RETRIES) { + continue; + } + } + + if (!response) { + continue; + } + + if (response.ok) { + return createFromFetch(Promise.resolve(response), { + temporaryReferences, + }); + } + + const shouldRetry = + response.status >= 500 && attempt < MAX_CALLBACK_FETCH_RETRIES; + if (shouldRetry) { + response = undefined; + continue; + } + throw new Error( `Remote action callback request failed with status ${response.status} ${response.statusText} (${remoteActionUrl}).`, ); } - return createFromFetch(Promise.resolve(response), { temporaryReferences }); + + throw new Error( + `Remote action callback request failed due to network error (${remoteActionUrl}): ${String(lastFetchError)}`, + ); }); registeredCallbackKey = callbackKey; } diff --git a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts index 4e1af81c2286..65dda7a16415 100644 --- a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts @@ -101,9 +101,34 @@ describe('registerRemoteServerCallback runtime behavior', () => { await expect(callback('fetch-failure-action', ['arg-1'])).rejects.toThrow( 'Remote action callback request failed with status 503 Service Unavailable (http://127.0.0.1:3008/server-component-root).', ); + expect(global.fetch).toHaveBeenCalledTimes(2); expect(mockCreateFromFetch).not.toHaveBeenCalled(); }); + it('retries once when callback fetch throws and succeeds on retry', async () => { + const { registerRemoteServerCallback } = await importRegisterHelper(); + registerRemoteServerCallback('http://127.0.0.1:3008/server-component-root'); + + const transientNetworkError = new Error('network-down-on-first-attempt'); + global.fetch = jest + .fn() + .mockRejectedValueOnce(transientNetworkError) + .mockResolvedValue({ + ok: true, + status: 200, + statusText: 'OK', + } as Response); + + const callback = getRegisteredCallback(); + await expect(callback('fetch-retry-action', ['arg-1'])).resolves.toEqual( + expect.objectContaining({ + type: 'decoded-rsc-response', + }), + ); + expect(global.fetch).toHaveBeenCalledTimes(2); + expect(mockCreateFromFetch).toHaveBeenCalledTimes(1); + }); + it('uses default alias when remote alias is omitted', async () => { const { registerRemoteServerCallback } = await importRegisterHelper(); registerRemoteServerCallback('http://127.0.0.1:3008/server-component-root'); From 269e5a419c279e2e22ff03b2ab48b377e46835f3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 07:23:13 +0000 Subject: [PATCH 196/324] test(rsc-mf): verify retry policy for callback 5xx and 4xx --- .../tests/registerServerCallback.test.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts index 65dda7a16415..f6647a584068 100644 --- a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts @@ -105,6 +105,55 @@ describe('registerRemoteServerCallback runtime behavior', () => { expect(mockCreateFromFetch).not.toHaveBeenCalled(); }); + it('retries once when callback fetch returns retryable 5xx response', async () => { + const { registerRemoteServerCallback } = await importRegisterHelper(); + registerRemoteServerCallback('http://127.0.0.1:3008/server-component-root'); + global.fetch = jest + .fn() + .mockResolvedValueOnce({ + ok: false, + status: 503, + statusText: 'Service Unavailable', + } as Response) + .mockResolvedValueOnce({ + ok: true, + status: 200, + statusText: 'OK', + } as Response); + + const callback = getRegisteredCallback(); + await expect( + callback('fetch-retry-5xx-action', ['arg-1']), + ).resolves.toEqual( + expect.objectContaining({ + type: 'decoded-rsc-response', + }), + ); + expect(global.fetch).toHaveBeenCalledTimes(2); + expect(mockCreateFromFetch).toHaveBeenCalledTimes(1); + }); + + it('does not retry callback fetch for non-retryable 4xx response', async () => { + const { registerRemoteServerCallback } = await importRegisterHelper(); + registerRemoteServerCallback('http://127.0.0.1:3008/server-component-root'); + global.fetch = jest.fn(async () => { + return { + ok: false, + status: 400, + statusText: 'Bad Request', + } as Response; + }); + + const callback = getRegisteredCallback(); + await expect( + callback('fetch-non-retryable-action', ['arg-1']), + ).rejects.toThrow( + 'Remote action callback request failed with status 400 Bad Request (http://127.0.0.1:3008/server-component-root).', + ); + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(mockCreateFromFetch).not.toHaveBeenCalled(); + }); + it('retries once when callback fetch throws and succeeds on retry', async () => { const { registerRemoteServerCallback } = await importRegisterHelper(); registerRemoteServerCallback('http://127.0.0.1:3008/server-component-root'); From c734397867e7e9e5ef8e4a2c72695e4d82fb149f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 07:26:22 +0000 Subject: [PATCH 197/324] test(rsc-mf): assert callback retry request body reuse --- .../rsc-mf/tests/registerServerCallback.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts index f6647a584068..668523857130 100644 --- a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts @@ -129,7 +129,13 @@ describe('registerRemoteServerCallback runtime behavior', () => { type: 'decoded-rsc-response', }), ); + expect(mockCreateTemporaryReferenceSet).toHaveBeenCalledTimes(1); + expect(mockEncodeReply).toHaveBeenCalledTimes(1); expect(global.fetch).toHaveBeenCalledTimes(2); + const firstFetchBody = (global.fetch as jest.Mock).mock.calls[0]?.[1]?.body; + const secondFetchBody = (global.fetch as jest.Mock).mock.calls[1]?.[1] + ?.body; + expect(firstFetchBody).toBe(secondFetchBody); expect(mockCreateFromFetch).toHaveBeenCalledTimes(1); }); @@ -150,6 +156,8 @@ describe('registerRemoteServerCallback runtime behavior', () => { ).rejects.toThrow( 'Remote action callback request failed with status 400 Bad Request (http://127.0.0.1:3008/server-component-root).', ); + expect(mockCreateTemporaryReferenceSet).toHaveBeenCalledTimes(1); + expect(mockEncodeReply).toHaveBeenCalledTimes(1); expect(global.fetch).toHaveBeenCalledTimes(1); expect(mockCreateFromFetch).not.toHaveBeenCalled(); }); @@ -174,7 +182,13 @@ describe('registerRemoteServerCallback runtime behavior', () => { type: 'decoded-rsc-response', }), ); + expect(mockCreateTemporaryReferenceSet).toHaveBeenCalledTimes(1); + expect(mockEncodeReply).toHaveBeenCalledTimes(1); expect(global.fetch).toHaveBeenCalledTimes(2); + const firstFetchBody = (global.fetch as jest.Mock).mock.calls[0]?.[1]?.body; + const secondFetchBody = (global.fetch as jest.Mock).mock.calls[1]?.[1] + ?.body; + expect(firstFetchBody).toBe(secondFetchBody); expect(mockCreateFromFetch).toHaveBeenCalledTimes(1); }); From 0f92cc2dc17b1cb2658af9b4c1795da9995e6e8b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 07:45:08 +0000 Subject: [PATCH 198/324] refactor(rsc-mf): extract callback expose wiring helper --- .../rsc-mf/remote/module-federation.config.ts | 92 +-------------- .../src/runtime/createRscExposeDefinitions.ts | 111 ++++++++++++++++++ .../tests/createRscExposeDefinitions.test.ts | 105 +++++++++++++++++ 3 files changed, 218 insertions(+), 90 deletions(-) create mode 100644 tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts create mode 100644 tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts diff --git a/tests/integration/rsc-mf/remote/module-federation.config.ts b/tests/integration/rsc-mf/remote/module-federation.config.ts index c28fbb37f8a8..a35e782553f7 100644 --- a/tests/integration/rsc-mf/remote/module-federation.config.ts +++ b/tests/integration/rsc-mf/remote/module-federation.config.ts @@ -1,5 +1,6 @@ import path from 'path'; import { createModuleFederationConfig } from '@module-federation/modern-js-v3'; +import { createRscExposeDefinitions } from './src/runtime/createRscExposeDefinitions'; const LAYERS = { ssr: 'server-side-rendering', @@ -14,31 +15,6 @@ const reactDomServerImport = path.join( 'react-dom.react-server.js', ); const reactServerDomClientImport = 'react-server-dom-rspack/client.browser'; -const CALLBACK_BOOTSTRAP_IMPORT = './src/runtime/initServerCallback.ts'; -const CALLBACK_BOOTSTRAP_PREFIX = './src/runtime/'; -if (!CALLBACK_BOOTSTRAP_IMPORT.startsWith(CALLBACK_BOOTSTRAP_PREFIX)) { - throw new Error( - `Callback bootstrap import must stay in runtime namespace (${CALLBACK_BOOTSTRAP_PREFIX}). Received: ${CALLBACK_BOOTSTRAP_IMPORT}`, - ); -} -if (!/\.[tj]sx?$/.test(CALLBACK_BOOTSTRAP_IMPORT)) { - throw new Error( - `Callback bootstrap import must use explicit source extension for deterministic resolution. Received: ${CALLBACK_BOOTSTRAP_IMPORT}`, - ); -} -if ( - CALLBACK_BOOTSTRAP_IMPORT.includes('..') || - CALLBACK_BOOTSTRAP_IMPORT.includes('\\') -) { - throw new Error( - `Callback bootstrap import must not contain traversal or Windows separators. Received: ${CALLBACK_BOOTSTRAP_IMPORT}`, - ); -} -const createRscExpose = (importPath: string) => - ({ - import: [CALLBACK_BOOTSTRAP_IMPORT, importPath], - layer: LAYERS.rsc, - }) as any; const remoteExposeImports: Record = { './RemoteClientCounter': './src/components/RemoteClientCounter.tsx', './RemoteClientBadge': './src/components/RemoteClientBadge.tsx', @@ -54,65 +30,6 @@ const remoteExposeImports: Record = { './actionBundle': './src/components/actionBundle.ts', './infoBundle': './src/components/infoBundle.ts', }; -const invalidExposeKeys = Object.keys(remoteExposeImports).filter( - exposeKey => !exposeKey.startsWith('./'), -); -if (invalidExposeKeys.length > 0) { - throw new Error( - `Remote expose keys must be module-federation paths starting with "./". Invalid keys: ${invalidExposeKeys.join(', ')}`, - ); -} -const COMPONENT_EXPOSE_PREFIX = './src/components/'; -const nonComponentExposeEntries = Object.entries(remoteExposeImports).filter( - ([, importPath]) => !importPath.startsWith(COMPONENT_EXPOSE_PREFIX), -); -if (nonComponentExposeEntries.length > 0) { - throw new Error( - `Remote exposes must point to component userland modules (${COMPONENT_EXPOSE_PREFIX}). Invalid entries: ${nonComponentExposeEntries - .map(([exposeKey, importPath]) => `${exposeKey} -> ${importPath}`) - .join(', ')}`, - ); -} -const nonTypeScriptExposeEntries = Object.entries(remoteExposeImports).filter( - ([, importPath]) => !/\.[tj]sx?$/.test(importPath), -); -if (nonTypeScriptExposeEntries.length > 0) { - throw new Error( - `Remote expose imports must use explicit TypeScript entry extensions for deterministic resolution. Invalid entries: ${nonTypeScriptExposeEntries - .map(([exposeKey, importPath]) => `${exposeKey} -> ${importPath}`) - .join(', ')}`, - ); -} -const parentTraversalExposeEntries = Object.entries(remoteExposeImports).filter( - ([, importPath]) => importPath.includes('..'), -); -if (parentTraversalExposeEntries.length > 0) { - throw new Error( - `Remote expose imports must not contain parent directory traversal segments. Invalid entries: ${parentTraversalExposeEntries - .map(([exposeKey, importPath]) => `${exposeKey} -> ${importPath}`) - .join(', ')}`, - ); -} -const nonPosixExposeEntries = Object.entries(remoteExposeImports).filter( - ([, importPath]) => importPath.includes('\\'), -); -if (nonPosixExposeEntries.length > 0) { - throw new Error( - `Remote expose imports must use POSIX separators for deterministic module ids. Invalid entries: ${nonPosixExposeEntries - .map(([exposeKey, importPath]) => `${exposeKey} -> ${importPath}`) - .join(', ')}`, - ); -} -const callbackExposeEntries = Object.entries(remoteExposeImports).filter( - ([, importPath]) => importPath === CALLBACK_BOOTSTRAP_IMPORT, -); -if (callbackExposeEntries.length > 0) { - throw new Error( - `Callback bootstrap module (${CALLBACK_BOOTSTRAP_IMPORT}) must remain internal-only and cannot be exposed. Invalid entries: ${callbackExposeEntries - .map(([exposeKey]) => exposeKey) - .join(', ')}`, - ); -} const sharedByScope = [ { @@ -205,12 +122,7 @@ export default createModuleFederationConfig({ filePath: 'static', }, filename: 'static/remoteEntry.js', - exposes: Object.fromEntries( - Object.entries(remoteExposeImports).map(([exposeKey, importPath]) => [ - exposeKey, - createRscExpose(importPath), - ]), - ) as any, + exposes: createRscExposeDefinitions(remoteExposeImports) as any, shared: sharedByScope as any, dts: false, experiments: { diff --git a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts new file mode 100644 index 000000000000..06931d78bf30 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts @@ -0,0 +1,111 @@ +const CALLBACK_BOOTSTRAP_IMPORT = './src/runtime/initServerCallback.ts'; +const CALLBACK_BOOTSTRAP_PREFIX = './src/runtime/'; +const COMPONENT_EXPOSE_PREFIX = './src/components/'; + +if (!CALLBACK_BOOTSTRAP_IMPORT.startsWith(CALLBACK_BOOTSTRAP_PREFIX)) { + throw new Error( + `Callback bootstrap import must stay in runtime namespace (${CALLBACK_BOOTSTRAP_PREFIX}). Received: ${CALLBACK_BOOTSTRAP_IMPORT}`, + ); +} +if (!/\.[tj]sx?$/.test(CALLBACK_BOOTSTRAP_IMPORT)) { + throw new Error( + `Callback bootstrap import must use explicit source extension for deterministic resolution. Received: ${CALLBACK_BOOTSTRAP_IMPORT}`, + ); +} +if ( + CALLBACK_BOOTSTRAP_IMPORT.includes('..') || + CALLBACK_BOOTSTRAP_IMPORT.includes('\\') +) { + throw new Error( + `Callback bootstrap import must not contain traversal or Windows separators. Received: ${CALLBACK_BOOTSTRAP_IMPORT}`, + ); +} + +const createRscExpose = (importPath: string) => + ({ + import: [CALLBACK_BOOTSTRAP_IMPORT, importPath], + layer: 'react-server-components', + }) as const; + +const assertValidExposeConfig = ( + remoteExposeImports: Record, +) => { + const invalidExposeKeys = Object.keys(remoteExposeImports).filter( + exposeKey => !exposeKey.startsWith('./'), + ); + if (invalidExposeKeys.length > 0) { + throw new Error( + `Remote expose keys must be module-federation paths starting with "./". Invalid keys: ${invalidExposeKeys.join(', ')}`, + ); + } + + const callbackExposeEntries = Object.entries(remoteExposeImports).filter( + ([, importPath]) => importPath === CALLBACK_BOOTSTRAP_IMPORT, + ); + if (callbackExposeEntries.length > 0) { + throw new Error( + `Callback bootstrap module (${CALLBACK_BOOTSTRAP_IMPORT}) must remain internal-only and cannot be exposed. Invalid entries: ${callbackExposeEntries + .map(([exposeKey]) => exposeKey) + .join(', ')}`, + ); + } + + const nonPosixExposeEntries = Object.entries(remoteExposeImports).filter( + ([, importPath]) => importPath.includes('\\'), + ); + if (nonPosixExposeEntries.length > 0) { + throw new Error( + `Remote expose imports must use POSIX separators for deterministic module ids. Invalid entries: ${nonPosixExposeEntries + .map(([exposeKey, importPath]) => `${exposeKey} -> ${importPath}`) + .join(', ')}`, + ); + } + + const nonComponentExposeEntries = Object.entries(remoteExposeImports).filter( + ([, importPath]) => !importPath.startsWith(COMPONENT_EXPOSE_PREFIX), + ); + if (nonComponentExposeEntries.length > 0) { + throw new Error( + `Remote exposes must point to component userland modules (${COMPONENT_EXPOSE_PREFIX}). Invalid entries: ${nonComponentExposeEntries + .map(([exposeKey, importPath]) => `${exposeKey} -> ${importPath}`) + .join(', ')}`, + ); + } + + const nonTypeScriptExposeEntries = Object.entries(remoteExposeImports).filter( + ([, importPath]) => !/\.[tj]sx?$/.test(importPath), + ); + if (nonTypeScriptExposeEntries.length > 0) { + throw new Error( + `Remote expose imports must use explicit TypeScript entry extensions for deterministic resolution. Invalid entries: ${nonTypeScriptExposeEntries + .map(([exposeKey, importPath]) => `${exposeKey} -> ${importPath}`) + .join(', ')}`, + ); + } + + const parentTraversalExposeEntries = Object.entries( + remoteExposeImports, + ).filter(([, importPath]) => importPath.includes('..')); + if (parentTraversalExposeEntries.length > 0) { + throw new Error( + `Remote expose imports must not contain parent directory traversal segments. Invalid entries: ${parentTraversalExposeEntries + .map(([exposeKey, importPath]) => `${exposeKey} -> ${importPath}`) + .join(', ')}`, + ); + } +}; + +export const createRscExposeDefinitions = ( + remoteExposeImports: Record, +) => { + assertValidExposeConfig(remoteExposeImports); + + return Object.fromEntries( + Object.entries(remoteExposeImports).map(([exposeKey, importPath]) => [ + exposeKey, + createRscExpose(importPath), + ]), + ); +}; + +export const CALLBACK_BOOTSTRAP_MODULE = CALLBACK_BOOTSTRAP_IMPORT; diff --git a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts new file mode 100644 index 000000000000..9e20301d20eb --- /dev/null +++ b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts @@ -0,0 +1,105 @@ +const CREATE_RSC_EXPOSE_DEFINITIONS_MODULE = + '../remote/src/runtime/createRscExposeDefinitions'; + +const loadCreateRscExposeDefinitions = () => { + let moduleExports: any; + jest.isolateModules(() => { + moduleExports = require(CREATE_RSC_EXPOSE_DEFINITIONS_MODULE); + }); + return moduleExports as { + createRscExposeDefinitions: ( + remoteExposeImports: Record, + ) => Record; + CALLBACK_BOOTSTRAP_MODULE: string; + }; +}; + +describe('createRscExposeDefinitions', () => { + it('creates callback-bootstrapped rsc expose definitions', () => { + const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = + loadCreateRscExposeDefinitions(); + const exposeDefinitions = createRscExposeDefinitions({ + './RemoteClientCounter': './src/components/RemoteClientCounter.tsx', + './actions': './src/components/actions.ts', + }); + + expect(exposeDefinitions).toEqual({ + './RemoteClientCounter': { + import: [ + CALLBACK_BOOTSTRAP_MODULE, + './src/components/RemoteClientCounter.tsx', + ], + layer: 'react-server-components', + }, + './actions': { + import: [CALLBACK_BOOTSTRAP_MODULE, './src/components/actions.ts'], + layer: 'react-server-components', + }, + }); + }); + + it('rejects expose keys that do not start with module-federation prefix', () => { + const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); + expect(() => + createRscExposeDefinitions({ + RemoteClientCounter: './src/components/RemoteClientCounter.tsx', + }), + ).toThrow( + 'Remote expose keys must be module-federation paths starting with "./"', + ); + }); + + it('rejects expose imports outside component userland namespace', () => { + const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); + expect(() => + createRscExposeDefinitions({ + './RemoteClientCounter': './src/runtime/helper.ts', + }), + ).toThrow( + 'Remote exposes must point to component userland modules (./src/components/)', + ); + }); + + it('rejects expose imports without explicit source extension', () => { + const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); + expect(() => + createRscExposeDefinitions({ + './RemoteClientCounter': './src/components/RemoteClientCounter', + }), + ).toThrow( + 'Remote expose imports must use explicit TypeScript entry extensions for deterministic resolution.', + ); + }); + + it('rejects expose imports with parent traversal segments', () => { + const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); + expect(() => + createRscExposeDefinitions({ + './RemoteClientCounter': './src/components/../runtime/helper.ts', + }), + ).toThrow( + 'Remote expose imports must not contain parent directory traversal segments.', + ); + }); + + it('rejects expose imports with windows separators', () => { + const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); + expect(() => + createRscExposeDefinitions({ + './RemoteClientCounter': './src/components\\RemoteClientCounter.tsx', + }), + ).toThrow( + 'Remote expose imports must use POSIX separators for deterministic module ids.', + ); + }); + + it('rejects exposing callback bootstrap module directly', () => { + const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = + loadCreateRscExposeDefinitions(); + expect(() => + createRscExposeDefinitions({ + './callback': CALLBACK_BOOTSTRAP_MODULE, + }), + ).toThrow('must remain internal-only and cannot be exposed'); + }); +}); From 3a8376e251df73001046eaa91932480796cae8c4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 07:48:50 +0000 Subject: [PATCH 199/324] refactor(rsc-mf): allow broader userland expose module paths --- .../src/runtime/createRscExposeDefinitions.ts | 22 ++++++++++++++----- .../tests/createRscExposeDefinitions.test.ts | 13 +++++++++-- .../tests/moduleFederationConfig.test.ts | 3 ++- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts index 06931d78bf30..cf41dbb5e352 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts @@ -1,6 +1,6 @@ const CALLBACK_BOOTSTRAP_IMPORT = './src/runtime/initServerCallback.ts'; const CALLBACK_BOOTSTRAP_PREFIX = './src/runtime/'; -const COMPONENT_EXPOSE_PREFIX = './src/components/'; +const USERLAND_EXPOSE_PREFIX = './src/'; if (!CALLBACK_BOOTSTRAP_IMPORT.startsWith(CALLBACK_BOOTSTRAP_PREFIX)) { throw new Error( @@ -61,12 +61,24 @@ const assertValidExposeConfig = ( ); } - const nonComponentExposeEntries = Object.entries(remoteExposeImports).filter( - ([, importPath]) => !importPath.startsWith(COMPONENT_EXPOSE_PREFIX), + const nonUserlandExposeEntries = Object.entries(remoteExposeImports).filter( + ([, importPath]) => !importPath.startsWith(USERLAND_EXPOSE_PREFIX), ); - if (nonComponentExposeEntries.length > 0) { + if (nonUserlandExposeEntries.length > 0) { throw new Error( - `Remote exposes must point to component userland modules (${COMPONENT_EXPOSE_PREFIX}). Invalid entries: ${nonComponentExposeEntries + `Remote exposes must point to userland source modules (${USERLAND_EXPOSE_PREFIX}). Invalid entries: ${nonUserlandExposeEntries + .map(([exposeKey, importPath]) => `${exposeKey} -> ${importPath}`) + .join(', ')}`, + ); + } + const runtimeNamespaceExposeEntries = Object.entries( + remoteExposeImports, + ).filter(([, importPath]) => + importPath.startsWith(CALLBACK_BOOTSTRAP_PREFIX), + ); + if (runtimeNamespaceExposeEntries.length > 0) { + throw new Error( + `Remote exposes must not target internal runtime namespace (${CALLBACK_BOOTSTRAP_PREFIX}). Invalid entries: ${runtimeNamespaceExposeEntries .map(([exposeKey, importPath]) => `${exposeKey} -> ${importPath}`) .join(', ')}`, ); diff --git a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts index 9e20301d20eb..08212d2b41f1 100644 --- a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts +++ b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts @@ -49,14 +49,23 @@ describe('createRscExposeDefinitions', () => { ); }); - it('rejects expose imports outside component userland namespace', () => { + it('rejects expose imports outside source userland namespace', () => { + const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); + expect(() => + createRscExposeDefinitions({ + './RemoteClientCounter': './app/components/RemoteClientCounter.tsx', + }), + ).toThrow('Remote exposes must point to userland source modules (./src/)'); + }); + + it('rejects expose imports targeting internal runtime namespace', () => { const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); expect(() => createRscExposeDefinitions({ './RemoteClientCounter': './src/runtime/helper.ts', }), ).toThrow( - 'Remote exposes must point to component userland modules (./src/components/)', + 'Remote exposes must not target internal runtime namespace (./src/runtime/)', ); }); diff --git a/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts b/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts index 95f362177c1d..1c7af44b414c 100644 --- a/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts +++ b/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts @@ -105,7 +105,8 @@ describe('rsc-mf module federation config contracts', () => { expect(Array.isArray(definition.import)).toBe(true); expect(definition.import).toHaveLength(2); expect(definition.import?.[0]).toBe(CALLBACK_BOOTSTRAP_IMPORT); - expect(definition.import?.[1]).toMatch(/^\.\/src\/components\//); + expect(definition.import?.[1]).toMatch(/^\.\/src\//); + expect(definition.import?.[1]).not.toMatch(/^\.\/src\/runtime\//); expect(definition.import?.[1]).toMatch(/\.[tj]sx?$/); expect(definition.import?.[1]).not.toContain('..'); expect(definition.import?.[1]).not.toContain('\\'); From 48c15ebb16e0411213748f2e6d5bbe315c0fa029 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 07:52:57 +0000 Subject: [PATCH 200/324] refactor(rsc-mf): support relative expose import paths --- .../src/runtime/createRscExposeDefinitions.ts | 4 ++-- .../tests/createRscExposeDefinitions.test.ts | 24 ++++++++++++++++--- .../tests/moduleFederationConfig.test.ts | 2 +- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts index cf41dbb5e352..a24b9bc4b46b 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts @@ -1,6 +1,6 @@ const CALLBACK_BOOTSTRAP_IMPORT = './src/runtime/initServerCallback.ts'; const CALLBACK_BOOTSTRAP_PREFIX = './src/runtime/'; -const USERLAND_EXPOSE_PREFIX = './src/'; +const USERLAND_EXPOSE_PREFIX = './'; if (!CALLBACK_BOOTSTRAP_IMPORT.startsWith(CALLBACK_BOOTSTRAP_PREFIX)) { throw new Error( @@ -66,7 +66,7 @@ const assertValidExposeConfig = ( ); if (nonUserlandExposeEntries.length > 0) { throw new Error( - `Remote exposes must point to userland source modules (${USERLAND_EXPOSE_PREFIX}). Invalid entries: ${nonUserlandExposeEntries + `Remote exposes must point to userland relative modules (${USERLAND_EXPOSE_PREFIX}). Invalid entries: ${nonUserlandExposeEntries .map(([exposeKey, importPath]) => `${exposeKey} -> ${importPath}`) .join(', ')}`, ); diff --git a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts index 08212d2b41f1..71264fd04bd4 100644 --- a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts +++ b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts @@ -49,13 +49,31 @@ describe('createRscExposeDefinitions', () => { ); }); - it('rejects expose imports outside source userland namespace', () => { + it('allows expose imports outside src root when path is relative', () => { + const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = + loadCreateRscExposeDefinitions(); + expect( + createRscExposeDefinitions({ + './RemoteClientCounter': './app/components/RemoteClientCounter.tsx', + }), + ).toEqual({ + './RemoteClientCounter': { + import: [ + CALLBACK_BOOTSTRAP_MODULE, + './app/components/RemoteClientCounter.tsx', + ], + layer: 'react-server-components', + }, + }); + }); + + it('rejects non-relative expose imports', () => { const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); expect(() => createRscExposeDefinitions({ - './RemoteClientCounter': './app/components/RemoteClientCounter.tsx', + './RemoteClientCounter': '/abs/components/RemoteClientCounter.tsx', }), - ).toThrow('Remote exposes must point to userland source modules (./src/)'); + ).toThrow('Remote exposes must point to userland relative modules (./).'); }); it('rejects expose imports targeting internal runtime namespace', () => { diff --git a/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts b/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts index 1c7af44b414c..f1ae190dbc80 100644 --- a/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts +++ b/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts @@ -105,7 +105,7 @@ describe('rsc-mf module federation config contracts', () => { expect(Array.isArray(definition.import)).toBe(true); expect(definition.import).toHaveLength(2); expect(definition.import?.[0]).toBe(CALLBACK_BOOTSTRAP_IMPORT); - expect(definition.import?.[1]).toMatch(/^\.\/src\//); + expect(definition.import?.[1]).toMatch(/^\.\//); expect(definition.import?.[1]).not.toMatch(/^\.\/src\/runtime\//); expect(definition.import?.[1]).toMatch(/\.[tj]sx?$/); expect(definition.import?.[1]).not.toContain('..'); From cfb69fc081e7c0f213cc7fe4a40717e13869a81b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 07:56:50 +0000 Subject: [PATCH 201/324] refactor(rsc-mf): accept cjs/mjs/cts/mts expose entries --- .../src/runtime/createRscExposeDefinitions.ts | 15 ++++++++----- .../tests/createRscExposeDefinitions.test.ts | 22 ++++++++++++++++++- .../tests/moduleFederationConfig.test.ts | 2 +- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts index a24b9bc4b46b..7e0ea6d79689 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts @@ -1,15 +1,16 @@ const CALLBACK_BOOTSTRAP_IMPORT = './src/runtime/initServerCallback.ts'; const CALLBACK_BOOTSTRAP_PREFIX = './src/runtime/'; const USERLAND_EXPOSE_PREFIX = './'; +const SOURCE_ENTRY_EXTENSION_PATTERN = /\.[cm]?[jt]sx?$/i; if (!CALLBACK_BOOTSTRAP_IMPORT.startsWith(CALLBACK_BOOTSTRAP_PREFIX)) { throw new Error( `Callback bootstrap import must stay in runtime namespace (${CALLBACK_BOOTSTRAP_PREFIX}). Received: ${CALLBACK_BOOTSTRAP_IMPORT}`, ); } -if (!/\.[tj]sx?$/.test(CALLBACK_BOOTSTRAP_IMPORT)) { +if (!SOURCE_ENTRY_EXTENSION_PATTERN.test(CALLBACK_BOOTSTRAP_IMPORT)) { throw new Error( - `Callback bootstrap import must use explicit source extension for deterministic resolution. Received: ${CALLBACK_BOOTSTRAP_IMPORT}`, + `Callback bootstrap import must use an explicit source entry extension for deterministic resolution. Received: ${CALLBACK_BOOTSTRAP_IMPORT}`, ); } if ( @@ -84,12 +85,14 @@ const assertValidExposeConfig = ( ); } - const nonTypeScriptExposeEntries = Object.entries(remoteExposeImports).filter( - ([, importPath]) => !/\.[tj]sx?$/.test(importPath), + const nonSourceEntryExposeEntries = Object.entries( + remoteExposeImports, + ).filter( + ([, importPath]) => !SOURCE_ENTRY_EXTENSION_PATTERN.test(importPath), ); - if (nonTypeScriptExposeEntries.length > 0) { + if (nonSourceEntryExposeEntries.length > 0) { throw new Error( - `Remote expose imports must use explicit TypeScript entry extensions for deterministic resolution. Invalid entries: ${nonTypeScriptExposeEntries + `Remote expose imports must use explicit source entry extensions (.js/.jsx/.ts/.tsx/.cjs/.mjs/.cts/.mts) for deterministic resolution. Invalid entries: ${nonSourceEntryExposeEntries .map(([exposeKey, importPath]) => `${exposeKey} -> ${importPath}`) .join(', ')}`, ); diff --git a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts index 71264fd04bd4..b7aa4220ba50 100644 --- a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts +++ b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts @@ -94,10 +94,30 @@ describe('createRscExposeDefinitions', () => { './RemoteClientCounter': './src/components/RemoteClientCounter', }), ).toThrow( - 'Remote expose imports must use explicit TypeScript entry extensions for deterministic resolution.', + 'Remote expose imports must use explicit source entry extensions (.js/.jsx/.ts/.tsx/.cjs/.mjs/.cts/.mts) for deterministic resolution.', ); }); + it('allows cts and mts expose entry extensions', () => { + const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = + loadCreateRscExposeDefinitions(); + expect( + createRscExposeDefinitions({ + './serverOnlyHelper': './src/lib/serverOnlyHelper.cts', + './rscBridgeUtil': './src/lib/rscBridgeUtil.mts', + }), + ).toEqual({ + './serverOnlyHelper': { + import: [CALLBACK_BOOTSTRAP_MODULE, './src/lib/serverOnlyHelper.cts'], + layer: 'react-server-components', + }, + './rscBridgeUtil': { + import: [CALLBACK_BOOTSTRAP_MODULE, './src/lib/rscBridgeUtil.mts'], + layer: 'react-server-components', + }, + }); + }); + it('rejects expose imports with parent traversal segments', () => { const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); expect(() => diff --git a/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts b/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts index f1ae190dbc80..66b7531f28f4 100644 --- a/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts +++ b/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts @@ -107,7 +107,7 @@ describe('rsc-mf module federation config contracts', () => { expect(definition.import?.[0]).toBe(CALLBACK_BOOTSTRAP_IMPORT); expect(definition.import?.[1]).toMatch(/^\.\//); expect(definition.import?.[1]).not.toMatch(/^\.\/src\/runtime\//); - expect(definition.import?.[1]).toMatch(/\.[tj]sx?$/); + expect(definition.import?.[1]).toMatch(/\.[cm]?[jt]sx?$/i); expect(definition.import?.[1]).not.toContain('..'); expect(definition.import?.[1]).not.toContain('\\'); expect(exposeKey).toMatch(/^\.\//); From f6beeb97c92189933b22084428e2f978e3f887d1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 08:01:25 +0000 Subject: [PATCH 202/324] refactor(rsc-mf): support object expose definitions with import arrays --- .../src/runtime/createRscExposeDefinitions.ts | 164 ++++++++++++++---- .../tests/createRscExposeDefinitions.test.ts | 86 ++++++++- 2 files changed, 208 insertions(+), 42 deletions(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts index 7e0ea6d79689..b633c680c38c 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts @@ -2,6 +2,19 @@ const CALLBACK_BOOTSTRAP_IMPORT = './src/runtime/initServerCallback.ts'; const CALLBACK_BOOTSTRAP_PREFIX = './src/runtime/'; const USERLAND_EXPOSE_PREFIX = './'; const SOURCE_ENTRY_EXTENSION_PATTERN = /\.[cm]?[jt]sx?$/i; +const RSC_LAYER = 'react-server-components'; + +type ExposeImportInput = string | string[]; +type ExposeDefinitionInput = + | string + | ({ + import: ExposeImportInput; + } & Record); +type NormalizedExposeImportPaths = Record; +interface NormalizedExposeDefinition { + importPaths: string[]; + exposeOverrides: Record; +} if (!CALLBACK_BOOTSTRAP_IMPORT.startsWith(CALLBACK_BOOTSTRAP_PREFIX)) { throw new Error( @@ -22,16 +35,59 @@ if ( ); } -const createRscExpose = (importPath: string) => +const normalizeExposeImportPaths = ( + exposeKey: string, + exposeDefinition: ExposeDefinitionInput, +) => { + if (typeof exposeDefinition === 'string') { + return { + importPaths: [exposeDefinition], + exposeOverrides: {}, + } satisfies NormalizedExposeDefinition; + } + + if (!exposeDefinition || typeof exposeDefinition !== 'object') { + throw new Error( + `Remote expose definition must be a string path or an object with an import field. Invalid entry: ${exposeKey} -> ${String(exposeDefinition)}`, + ); + } + + const { import: exposeImport, ...exposeOverrides } = exposeDefinition; + if (typeof exposeImport === 'string') { + return { + importPaths: [exposeImport], + exposeOverrides, + } satisfies NormalizedExposeDefinition; + } + if ( + Array.isArray(exposeImport) && + exposeImport.length > 0 && + exposeImport.every(item => typeof item === 'string') + ) { + return { + importPaths: exposeImport, + exposeOverrides, + } satisfies NormalizedExposeDefinition; + } + throw new Error( + `Remote expose import must be a non-empty string or string array. Invalid entry: ${exposeKey}`, + ); +}; + +const createRscExpose = ( + importPaths: string[], + exposeOverrides: Record, +) => ({ - import: [CALLBACK_BOOTSTRAP_IMPORT, importPath], - layer: 'react-server-components', + ...exposeOverrides, + import: [CALLBACK_BOOTSTRAP_IMPORT, ...importPaths], + layer: RSC_LAYER, }) as const; const assertValidExposeConfig = ( - remoteExposeImports: Record, + normalizedExposeImportPaths: NormalizedExposeImportPaths, ) => { - const invalidExposeKeys = Object.keys(remoteExposeImports).filter( + const invalidExposeKeys = Object.keys(normalizedExposeImportPaths).filter( exposeKey => !exposeKey.startsWith('./'), ); if (invalidExposeKeys.length > 0) { @@ -40,8 +96,10 @@ const assertValidExposeConfig = ( ); } - const callbackExposeEntries = Object.entries(remoteExposeImports).filter( - ([, importPath]) => importPath === CALLBACK_BOOTSTRAP_IMPORT, + const callbackExposeEntries = Object.entries( + normalizedExposeImportPaths, + ).filter(([, importPaths]) => + importPaths.includes(CALLBACK_BOOTSTRAP_IMPORT), ); if (callbackExposeEntries.length > 0) { throw new Error( @@ -51,74 +109,104 @@ const assertValidExposeConfig = ( ); } - const nonPosixExposeEntries = Object.entries(remoteExposeImports).filter( - ([, importPath]) => importPath.includes('\\'), + const nonPosixExposeEntries = Object.entries( + normalizedExposeImportPaths, + ).flatMap(([exposeKey, importPaths]) => + importPaths + .filter(importPath => importPath.includes('\\')) + .map(importPath => `${exposeKey} -> ${importPath}`), ); if (nonPosixExposeEntries.length > 0) { throw new Error( - `Remote expose imports must use POSIX separators for deterministic module ids. Invalid entries: ${nonPosixExposeEntries - .map(([exposeKey, importPath]) => `${exposeKey} -> ${importPath}`) - .join(', ')}`, + `Remote expose imports must use POSIX separators for deterministic module ids. Invalid entries: ${nonPosixExposeEntries.join( + ', ', + )}`, ); } - const nonUserlandExposeEntries = Object.entries(remoteExposeImports).filter( - ([, importPath]) => !importPath.startsWith(USERLAND_EXPOSE_PREFIX), + const nonUserlandExposeEntries = Object.entries( + normalizedExposeImportPaths, + ).flatMap(([exposeKey, importPaths]) => + importPaths + .filter(importPath => !importPath.startsWith(USERLAND_EXPOSE_PREFIX)) + .map(importPath => `${exposeKey} -> ${importPath}`), ); if (nonUserlandExposeEntries.length > 0) { throw new Error( - `Remote exposes must point to userland relative modules (${USERLAND_EXPOSE_PREFIX}). Invalid entries: ${nonUserlandExposeEntries - .map(([exposeKey, importPath]) => `${exposeKey} -> ${importPath}`) - .join(', ')}`, + `Remote exposes must point to userland relative modules (${USERLAND_EXPOSE_PREFIX}). Invalid entries: ${nonUserlandExposeEntries.join( + ', ', + )}`, ); } const runtimeNamespaceExposeEntries = Object.entries( - remoteExposeImports, - ).filter(([, importPath]) => - importPath.startsWith(CALLBACK_BOOTSTRAP_PREFIX), + normalizedExposeImportPaths, + ).flatMap(([exposeKey, importPaths]) => + importPaths + .filter(importPath => importPath.startsWith(CALLBACK_BOOTSTRAP_PREFIX)) + .map(importPath => `${exposeKey} -> ${importPath}`), ); if (runtimeNamespaceExposeEntries.length > 0) { throw new Error( - `Remote exposes must not target internal runtime namespace (${CALLBACK_BOOTSTRAP_PREFIX}). Invalid entries: ${runtimeNamespaceExposeEntries - .map(([exposeKey, importPath]) => `${exposeKey} -> ${importPath}`) - .join(', ')}`, + `Remote exposes must not target internal runtime namespace (${CALLBACK_BOOTSTRAP_PREFIX}). Invalid entries: ${runtimeNamespaceExposeEntries.join( + ', ', + )}`, ); } const nonSourceEntryExposeEntries = Object.entries( - remoteExposeImports, - ).filter( - ([, importPath]) => !SOURCE_ENTRY_EXTENSION_PATTERN.test(importPath), + normalizedExposeImportPaths, + ).flatMap(([exposeKey, importPaths]) => + importPaths + .filter(importPath => !SOURCE_ENTRY_EXTENSION_PATTERN.test(importPath)) + .map(importPath => `${exposeKey} -> ${importPath}`), ); if (nonSourceEntryExposeEntries.length > 0) { throw new Error( - `Remote expose imports must use explicit source entry extensions (.js/.jsx/.ts/.tsx/.cjs/.mjs/.cts/.mts) for deterministic resolution. Invalid entries: ${nonSourceEntryExposeEntries - .map(([exposeKey, importPath]) => `${exposeKey} -> ${importPath}`) - .join(', ')}`, + `Remote expose imports must use explicit source entry extensions (.js/.jsx/.ts/.tsx/.cjs/.mjs/.cts/.mts) for deterministic resolution. Invalid entries: ${nonSourceEntryExposeEntries.join( + ', ', + )}`, ); } const parentTraversalExposeEntries = Object.entries( - remoteExposeImports, - ).filter(([, importPath]) => importPath.includes('..')); + normalizedExposeImportPaths, + ).flatMap(([exposeKey, importPaths]) => + importPaths + .filter(importPath => importPath.includes('..')) + .map(importPath => `${exposeKey} -> ${importPath}`), + ); if (parentTraversalExposeEntries.length > 0) { throw new Error( - `Remote expose imports must not contain parent directory traversal segments. Invalid entries: ${parentTraversalExposeEntries - .map(([exposeKey, importPath]) => `${exposeKey} -> ${importPath}`) - .join(', ')}`, + `Remote expose imports must not contain parent directory traversal segments. Invalid entries: ${parentTraversalExposeEntries.join( + ', ', + )}`, ); } }; export const createRscExposeDefinitions = ( - remoteExposeImports: Record, + remoteExposeImports: Record, ) => { - assertValidExposeConfig(remoteExposeImports); + const normalizedExposeEntries: Array<[string, NormalizedExposeDefinition]> = + Object.entries(remoteExposeImports).map(([exposeKey, exposeDefinition]) => [ + exposeKey, + normalizeExposeImportPaths(exposeKey, exposeDefinition), + ]); + const normalizedExposeImportPaths = Object.fromEntries( + normalizedExposeEntries.map(([exposeKey, normalizedDefinition]) => [ + exposeKey, + normalizedDefinition.importPaths, + ]), + ); + assertValidExposeConfig(normalizedExposeImportPaths); return Object.fromEntries( - Object.entries(remoteExposeImports).map(([exposeKey, importPath]) => [ + normalizedExposeEntries.map(([exposeKey, normalizedDefinition]) => [ exposeKey, - createRscExpose(importPath), + createRscExpose( + normalizedDefinition.importPaths, + normalizedDefinition.exposeOverrides, + ), ]), ); }; diff --git a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts index b7aa4220ba50..716185606a63 100644 --- a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts +++ b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts @@ -3,12 +3,21 @@ const CREATE_RSC_EXPOSE_DEFINITIONS_MODULE = const loadCreateRscExposeDefinitions = () => { let moduleExports: any; - jest.isolateModules(() => { - moduleExports = require(CREATE_RSC_EXPOSE_DEFINITIONS_MODULE); - }); + try { + jest.isolateModules(() => { + moduleExports = require(CREATE_RSC_EXPOSE_DEFINITIONS_MODULE); + }); + } catch (error) { + throw new Error( + `Failed to load createRscExposeDefinitions module: ${String(error)}`, + ); + } return moduleExports as { createRscExposeDefinitions: ( - remoteExposeImports: Record, + remoteExposeImports: Record< + string, + string | { import: string | string[]; [key: string]: unknown } + >, ) => Record; CALLBACK_BOOTSTRAP_MODULE: string; }; @@ -118,6 +127,75 @@ describe('createRscExposeDefinitions', () => { }); }); + it('supports object expose definitions with custom fields', () => { + const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = + loadCreateRscExposeDefinitions(); + const exposeDefinitions = createRscExposeDefinitions({ + './RemoteClientCounter': { + import: './src/components/RemoteClientCounter.tsx', + shareScope: 'rsc', + }, + }); + + expect(exposeDefinitions).toEqual({ + './RemoteClientCounter': { + import: [ + CALLBACK_BOOTSTRAP_MODULE, + './src/components/RemoteClientCounter.tsx', + ], + shareScope: 'rsc', + layer: 'react-server-components', + }, + }); + }); + + it('supports object expose definitions with import arrays', () => { + const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = + loadCreateRscExposeDefinitions(); + const exposeDefinitions = createRscExposeDefinitions({ + './infoBundle': { + import: [ + './src/components/infoBundle.ts', + './src/components/remoteMeta.ts', + ], + }, + }); + + expect(exposeDefinitions).toEqual({ + './infoBundle': { + import: [ + CALLBACK_BOOTSTRAP_MODULE, + './src/components/infoBundle.ts', + './src/components/remoteMeta.ts', + ], + layer: 'react-server-components', + }, + }); + }); + + it('rejects object expose definitions with invalid import payloads', () => { + const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); + expect(() => + createRscExposeDefinitions({ + './RemoteClientCounter': { + import: [] as string[], + }, + }), + ).toThrow( + 'Remote expose import must be a non-empty string or string array.', + ); + + expect(() => + createRscExposeDefinitions({ + './RemoteClientCounter': { + import: [42] as unknown as string[], + }, + }), + ).toThrow( + 'Remote expose import must be a non-empty string or string array.', + ); + }); + it('rejects expose imports with parent traversal segments', () => { const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); expect(() => From 5d399659bb9a9635d6111cc493b2ca1be4434646 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 08:05:42 +0000 Subject: [PATCH 203/324] refactor(rsc-mf): normalize mixed expose definition shapes --- .../rsc-mf/remote/module-federation.config.ts | 11 ++++++++--- .../remote/src/runtime/createRscExposeDefinitions.ts | 2 +- .../rsc-mf/tests/createRscExposeDefinitions.test.ts | 12 +++--------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/tests/integration/rsc-mf/remote/module-federation.config.ts b/tests/integration/rsc-mf/remote/module-federation.config.ts index a35e782553f7..442ebdd974f3 100644 --- a/tests/integration/rsc-mf/remote/module-federation.config.ts +++ b/tests/integration/rsc-mf/remote/module-federation.config.ts @@ -1,6 +1,9 @@ import path from 'path'; import { createModuleFederationConfig } from '@module-federation/modern-js-v3'; -import { createRscExposeDefinitions } from './src/runtime/createRscExposeDefinitions'; +import { + type ExposeDefinitionInput, + createRscExposeDefinitions, +} from './src/runtime/createRscExposeDefinitions'; const LAYERS = { ssr: 'server-side-rendering', @@ -15,7 +18,7 @@ const reactDomServerImport = path.join( 'react-dom.react-server.js', ); const reactServerDomClientImport = 'react-server-dom-rspack/client.browser'; -const remoteExposeImports: Record = { +const remoteExposeImports: Record = { './RemoteClientCounter': './src/components/RemoteClientCounter.tsx', './RemoteClientBadge': './src/components/RemoteClientBadge.tsx', './RemoteServerCard': './src/components/RemoteServerCard.tsx', @@ -28,7 +31,9 @@ const remoteExposeImports: Record = { './nestedActions': './src/components/nestedActions.ts', './defaultAction': './src/components/defaultAction.ts', './actionBundle': './src/components/actionBundle.ts', - './infoBundle': './src/components/infoBundle.ts', + './infoBundle': { + import: './src/components/infoBundle.ts', + }, }; const sharedByScope = [ diff --git a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts index b633c680c38c..c84a7c5869bc 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts @@ -5,7 +5,7 @@ const SOURCE_ENTRY_EXTENSION_PATTERN = /\.[cm]?[jt]sx?$/i; const RSC_LAYER = 'react-server-components'; type ExposeImportInput = string | string[]; -type ExposeDefinitionInput = +export type ExposeDefinitionInput = | string | ({ import: ExposeImportInput; diff --git a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts index 716185606a63..7868e8d7f2d2 100644 --- a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts +++ b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts @@ -3,15 +3,9 @@ const CREATE_RSC_EXPOSE_DEFINITIONS_MODULE = const loadCreateRscExposeDefinitions = () => { let moduleExports: any; - try { - jest.isolateModules(() => { - moduleExports = require(CREATE_RSC_EXPOSE_DEFINITIONS_MODULE); - }); - } catch (error) { - throw new Error( - `Failed to load createRscExposeDefinitions module: ${String(error)}`, - ); - } + jest.isolateModules(() => { + moduleExports = require(CREATE_RSC_EXPOSE_DEFINITIONS_MODULE); + }); return moduleExports as { createRscExposeDefinitions: ( remoteExposeImports: Record< From 73814a0f9e27e87bd341eaf10c9ea91f28eb329c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 08:07:19 +0000 Subject: [PATCH 204/324] test(rsc-mf): lock mixed expose-definition guardrails --- .../tests/createRscExposeDefinitions.test.ts | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts index 7868e8d7f2d2..dbc171e10878 100644 --- a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts +++ b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts @@ -128,6 +128,7 @@ describe('createRscExposeDefinitions', () => { './RemoteClientCounter': { import: './src/components/RemoteClientCounter.tsx', shareScope: 'rsc', + flag: true, }, }); @@ -138,6 +139,7 @@ describe('createRscExposeDefinitions', () => { './src/components/RemoteClientCounter.tsx', ], shareScope: 'rsc', + flag: true, layer: 'react-server-components', }, }); @@ -188,6 +190,61 @@ describe('createRscExposeDefinitions', () => { ).toThrow( 'Remote expose import must be a non-empty string or string array.', ); + + expect(() => + createRscExposeDefinitions({ + './RemoteClientCounter': {} as { import: string }, + }), + ).toThrow( + 'Remote expose import must be a non-empty string or string array.', + ); + }); + + it('rejects non-string and non-object expose definitions', () => { + const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); + expect(() => + createRscExposeDefinitions({ + './RemoteClientCounter': 7 as unknown as string, + }), + ).toThrow( + 'Remote expose definition must be a string path or an object with an import field.', + ); + }); + + it('forces rsc layer even if expose object provides a different layer', () => { + const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = + loadCreateRscExposeDefinitions(); + expect( + createRscExposeDefinitions({ + './RemoteClientCounter': { + import: './src/components/RemoteClientCounter.tsx', + layer: 'server-side-rendering', + }, + }), + ).toEqual({ + './RemoteClientCounter': { + import: [ + CALLBACK_BOOTSTRAP_MODULE, + './src/components/RemoteClientCounter.tsx', + ], + layer: 'react-server-components', + }, + }); + }); + + it('rejects import arrays that include callback bootstrap module', () => { + const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = + loadCreateRscExposeDefinitions(); + expect(() => + createRscExposeDefinitions({ + './RemoteClientCounter': { + import: [ + './src/components/RemoteClientCounter.tsx', + CALLBACK_BOOTSTRAP_MODULE, + ], + }, + }), + ).toThrow('must remain internal-only and cannot be exposed'); }); it('rejects expose imports with parent traversal segments', () => { From 0d5ec921f92054c12e0d991cf03b30a2d548a4f0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 08:09:23 +0000 Subject: [PATCH 205/324] refactor(rsc-mf): retry callback fetch on 429 responses --- .../src/runtime/registerServerCallback.ts | 6 +++- .../tests/registerServerCallback.test.ts | 34 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts index 04d02cf41e0a..99117b8dc91c 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts @@ -9,6 +9,7 @@ let registeredCallbackKey = ''; const ALIAS_TOKEN_PATTERN = /^[A-Za-z0-9_.-]+$/; const DEFAULT_REMOTE_ALIAS = 'rscRemote'; const MAX_CALLBACK_FETCH_RETRIES = 1; +const RETRYABLE_CALLBACK_STATUSES = new Set([429]); const getNormalizedRawActionId = (rawActionId: string) => { const normalizedRawActionId = rawActionId.trim(); if (!normalizedRawActionId || /\s/.test(normalizedRawActionId)) { @@ -52,6 +53,8 @@ const getNormalizedRemoteActionUrl = (remoteOrigin: string) => { }; const getCallbackKey = (remoteAlias: string, remoteActionUrl: string) => `${remoteAlias}::${remoteActionUrl}`; +const isRetryableCallbackStatus = (status: number) => + status >= 500 || RETRYABLE_CALLBACK_STATUSES.has(status); export function registerRemoteServerCallback( remoteOrigin: string, @@ -112,7 +115,8 @@ export function registerRemoteServerCallback( } const shouldRetry = - response.status >= 500 && attempt < MAX_CALLBACK_FETCH_RETRIES; + isRetryableCallbackStatus(response.status) && + attempt < MAX_CALLBACK_FETCH_RETRIES; if (shouldRetry) { response = undefined; continue; diff --git a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts index 668523857130..2e0ec625f881 100644 --- a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts @@ -139,6 +139,40 @@ describe('registerRemoteServerCallback runtime behavior', () => { expect(mockCreateFromFetch).toHaveBeenCalledTimes(1); }); + it('retries once when callback fetch returns retryable 429 response', async () => { + const { registerRemoteServerCallback } = await importRegisterHelper(); + registerRemoteServerCallback('http://127.0.0.1:3008/server-component-root'); + global.fetch = jest + .fn() + .mockResolvedValueOnce({ + ok: false, + status: 429, + statusText: 'Too Many Requests', + } as Response) + .mockResolvedValueOnce({ + ok: true, + status: 200, + statusText: 'OK', + } as Response); + + const callback = getRegisteredCallback(); + await expect( + callback('fetch-retry-429-action', ['arg-1']), + ).resolves.toEqual( + expect.objectContaining({ + type: 'decoded-rsc-response', + }), + ); + expect(mockCreateTemporaryReferenceSet).toHaveBeenCalledTimes(1); + expect(mockEncodeReply).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledTimes(2); + const firstFetchBody = (global.fetch as jest.Mock).mock.calls[0]?.[1]?.body; + const secondFetchBody = (global.fetch as jest.Mock).mock.calls[1]?.[1] + ?.body; + expect(firstFetchBody).toBe(secondFetchBody); + expect(mockCreateFromFetch).toHaveBeenCalledTimes(1); + }); + it('does not retry callback fetch for non-retryable 4xx response', async () => { const { registerRemoteServerCallback } = await importRegisterHelper(); registerRemoteServerCallback('http://127.0.0.1:3008/server-component-root'); From ae03f8f62ee69db00964c9da08b394c073d406fe Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 08:13:24 +0000 Subject: [PATCH 206/324] test(rsc-mf): allow multi-import expose definitions in config contract --- .../rsc-mf/tests/moduleFederationConfig.test.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts b/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts index 66b7531f28f4..ee54fe3207ea 100644 --- a/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts +++ b/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts @@ -103,13 +103,15 @@ describe('rsc-mf module federation config contracts', () => { expect(definition.layer).toBe('react-server-components'); expect(definition.import).toBeDefined(); expect(Array.isArray(definition.import)).toBe(true); - expect(definition.import).toHaveLength(2); expect(definition.import?.[0]).toBe(CALLBACK_BOOTSTRAP_IMPORT); - expect(definition.import?.[1]).toMatch(/^\.\//); - expect(definition.import?.[1]).not.toMatch(/^\.\/src\/runtime\//); - expect(definition.import?.[1]).toMatch(/\.[cm]?[jt]sx?$/i); - expect(definition.import?.[1]).not.toContain('..'); - expect(definition.import?.[1]).not.toContain('\\'); + expect(definition.import!.length).toBeGreaterThanOrEqual(2); + for (const importPath of definition.import!.slice(1)) { + expect(importPath).toMatch(/^\.\//); + expect(importPath).not.toMatch(/^\.\/src\/runtime\//); + expect(importPath).toMatch(/\.[cm]?[jt]sx?$/i); + expect(importPath).not.toContain('..'); + expect(importPath).not.toContain('\\'); + } expect(exposeKey).toMatch(/^\.\//); } }); From 4a92133d1ce64e3ffa17c0d669e070dddf78450d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 08:14:58 +0000 Subject: [PATCH 207/324] refactor(rsc-mf): dedupe repeated expose import entries --- .../src/runtime/createRscExposeDefinitions.ts | 5 +++- .../tests/createRscExposeDefinitions.test.ts | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts index c84a7c5869bc..034fcc4d9640 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts @@ -15,6 +15,9 @@ interface NormalizedExposeDefinition { importPaths: string[]; exposeOverrides: Record; } +const getUniqueImportPaths = (importPaths: string[]) => [ + ...new Set(importPaths), +]; if (!CALLBACK_BOOTSTRAP_IMPORT.startsWith(CALLBACK_BOOTSTRAP_PREFIX)) { throw new Error( @@ -65,7 +68,7 @@ const normalizeExposeImportPaths = ( exposeImport.every(item => typeof item === 'string') ) { return { - importPaths: exposeImport, + importPaths: getUniqueImportPaths(exposeImport), exposeOverrides, } satisfies NormalizedExposeDefinition; } diff --git a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts index dbc171e10878..5e4d97bff549 100644 --- a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts +++ b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts @@ -169,6 +169,31 @@ describe('createRscExposeDefinitions', () => { }); }); + it('deduplicates repeated entries in object expose import arrays', () => { + const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = + loadCreateRscExposeDefinitions(); + const exposeDefinitions = createRscExposeDefinitions({ + './infoBundle': { + import: [ + './src/components/infoBundle.ts', + './src/components/infoBundle.ts', + './src/components/remoteMeta.ts', + ], + }, + }); + + expect(exposeDefinitions).toEqual({ + './infoBundle': { + import: [ + CALLBACK_BOOTSTRAP_MODULE, + './src/components/infoBundle.ts', + './src/components/remoteMeta.ts', + ], + layer: 'react-server-components', + }, + }); + }); + it('rejects object expose definitions with invalid import payloads', () => { const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); expect(() => From 962d1e8f842fe737df4ea636b6c460d5a64c9262 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 08:16:20 +0000 Subject: [PATCH 208/324] refactor(rsc-mf): retry callback fetch on timeout status codes --- .../src/runtime/registerServerCallback.ts | 2 +- .../tests/registerServerCallback.test.ts | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts index 99117b8dc91c..fac458c4c2e1 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts @@ -9,7 +9,7 @@ let registeredCallbackKey = ''; const ALIAS_TOKEN_PATTERN = /^[A-Za-z0-9_.-]+$/; const DEFAULT_REMOTE_ALIAS = 'rscRemote'; const MAX_CALLBACK_FETCH_RETRIES = 1; -const RETRYABLE_CALLBACK_STATUSES = new Set([429]); +const RETRYABLE_CALLBACK_STATUSES = new Set([408, 425, 429]); const getNormalizedRawActionId = (rawActionId: string) => { const normalizedRawActionId = rawActionId.trim(); if (!normalizedRawActionId || /\s/.test(normalizedRawActionId)) { diff --git a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts index 2e0ec625f881..2dabec9c4089 100644 --- a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts @@ -173,6 +173,40 @@ describe('registerRemoteServerCallback runtime behavior', () => { expect(mockCreateFromFetch).toHaveBeenCalledTimes(1); }); + it('retries once when callback fetch returns retryable 408 response', async () => { + const { registerRemoteServerCallback } = await importRegisterHelper(); + registerRemoteServerCallback('http://127.0.0.1:3008/server-component-root'); + global.fetch = jest + .fn() + .mockResolvedValueOnce({ + ok: false, + status: 408, + statusText: 'Request Timeout', + } as Response) + .mockResolvedValueOnce({ + ok: true, + status: 200, + statusText: 'OK', + } as Response); + + const callback = getRegisteredCallback(); + await expect( + callback('fetch-retry-408-action', ['arg-1']), + ).resolves.toEqual( + expect.objectContaining({ + type: 'decoded-rsc-response', + }), + ); + expect(mockCreateTemporaryReferenceSet).toHaveBeenCalledTimes(1); + expect(mockEncodeReply).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledTimes(2); + const firstFetchBody = (global.fetch as jest.Mock).mock.calls[0]?.[1]?.body; + const secondFetchBody = (global.fetch as jest.Mock).mock.calls[1]?.[1] + ?.body; + expect(firstFetchBody).toBe(secondFetchBody); + expect(mockCreateFromFetch).toHaveBeenCalledTimes(1); + }); + it('does not retry callback fetch for non-retryable 4xx response', async () => { const { registerRemoteServerCallback } = await importRegisterHelper(); registerRemoteServerCallback('http://127.0.0.1:3008/server-component-root'); From 04bea873e5e4938f8bff828aadaabc55d4763d30 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 08:18:55 +0000 Subject: [PATCH 209/324] test(rsc-mf): verify callback retry for 425 responses --- .../tests/registerServerCallback.test.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts index 2dabec9c4089..baf68270ed8f 100644 --- a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts @@ -207,6 +207,40 @@ describe('registerRemoteServerCallback runtime behavior', () => { expect(mockCreateFromFetch).toHaveBeenCalledTimes(1); }); + it('retries once when callback fetch returns retryable 425 response', async () => { + const { registerRemoteServerCallback } = await importRegisterHelper(); + registerRemoteServerCallback('http://127.0.0.1:3008/server-component-root'); + global.fetch = jest + .fn() + .mockResolvedValueOnce({ + ok: false, + status: 425, + statusText: 'Too Early', + } as Response) + .mockResolvedValueOnce({ + ok: true, + status: 200, + statusText: 'OK', + } as Response); + + const callback = getRegisteredCallback(); + await expect( + callback('fetch-retry-425-action', ['arg-1']), + ).resolves.toEqual( + expect.objectContaining({ + type: 'decoded-rsc-response', + }), + ); + expect(mockCreateTemporaryReferenceSet).toHaveBeenCalledTimes(1); + expect(mockEncodeReply).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledTimes(2); + const firstFetchBody = (global.fetch as jest.Mock).mock.calls[0]?.[1]?.body; + const secondFetchBody = (global.fetch as jest.Mock).mock.calls[1]?.[1] + ?.body; + expect(firstFetchBody).toBe(secondFetchBody); + expect(mockCreateFromFetch).toHaveBeenCalledTimes(1); + }); + it('does not retry callback fetch for non-retryable 4xx response', async () => { const { registerRemoteServerCallback } = await importRegisterHelper(); registerRemoteServerCallback('http://127.0.0.1:3008/server-component-root'); From a2898096ac15c72569491b92491f62c4e055270d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 08:22:05 +0000 Subject: [PATCH 210/324] refactor(rsc-mf): trim expose import paths before validation --- .../src/runtime/createRscExposeDefinitions.ts | 23 +++++++++--- .../tests/createRscExposeDefinitions.test.ts | 37 ++++++++++++++++++- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts index 034fcc4d9640..319cf5cb510d 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts @@ -15,9 +15,20 @@ interface NormalizedExposeDefinition { importPaths: string[]; exposeOverrides: Record; } -const getUniqueImportPaths = (importPaths: string[]) => [ - ...new Set(importPaths), -]; +const normalizeImportPaths = (exposeKey: string, importPaths: string[]) => { + const normalizedImportPaths = importPaths.map(importPath => + importPath.trim(), + ); + const hasEmptyImportPath = normalizedImportPaths.some( + importPath => importPath.length === 0, + ); + if (hasEmptyImportPath) { + throw new Error( + `Remote expose import paths must be non-empty tokens after trimming. Invalid entry: ${exposeKey}`, + ); + } + return [...new Set(normalizedImportPaths)]; +}; if (!CALLBACK_BOOTSTRAP_IMPORT.startsWith(CALLBACK_BOOTSTRAP_PREFIX)) { throw new Error( @@ -44,7 +55,7 @@ const normalizeExposeImportPaths = ( ) => { if (typeof exposeDefinition === 'string') { return { - importPaths: [exposeDefinition], + importPaths: normalizeImportPaths(exposeKey, [exposeDefinition]), exposeOverrides: {}, } satisfies NormalizedExposeDefinition; } @@ -58,7 +69,7 @@ const normalizeExposeImportPaths = ( const { import: exposeImport, ...exposeOverrides } = exposeDefinition; if (typeof exposeImport === 'string') { return { - importPaths: [exposeImport], + importPaths: normalizeImportPaths(exposeKey, [exposeImport]), exposeOverrides, } satisfies NormalizedExposeDefinition; } @@ -68,7 +79,7 @@ const normalizeExposeImportPaths = ( exposeImport.every(item => typeof item === 'string') ) { return { - importPaths: getUniqueImportPaths(exposeImport), + importPaths: normalizeImportPaths(exposeKey, exposeImport), exposeOverrides, } satisfies NormalizedExposeDefinition; } diff --git a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts index 5e4d97bff549..1ffc032ff909 100644 --- a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts +++ b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts @@ -22,7 +22,7 @@ describe('createRscExposeDefinitions', () => { const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = loadCreateRscExposeDefinitions(); const exposeDefinitions = createRscExposeDefinitions({ - './RemoteClientCounter': './src/components/RemoteClientCounter.tsx', + './RemoteClientCounter': ' ./src/components/RemoteClientCounter.tsx ', './actions': './src/components/actions.ts', }); @@ -194,6 +194,31 @@ describe('createRscExposeDefinitions', () => { }); }); + it('trims expose import path entries before deduping', () => { + const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = + loadCreateRscExposeDefinitions(); + const exposeDefinitions = createRscExposeDefinitions({ + './infoBundle': { + import: [ + ' ./src/components/infoBundle.ts ', + './src/components/infoBundle.ts', + ' ./src/components/remoteMeta.ts ', + ], + }, + }); + + expect(exposeDefinitions).toEqual({ + './infoBundle': { + import: [ + CALLBACK_BOOTSTRAP_MODULE, + './src/components/infoBundle.ts', + './src/components/remoteMeta.ts', + ], + layer: 'react-server-components', + }, + }); + }); + it('rejects object expose definitions with invalid import payloads', () => { const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); expect(() => @@ -223,6 +248,16 @@ describe('createRscExposeDefinitions', () => { ).toThrow( 'Remote expose import must be a non-empty string or string array.', ); + + expect(() => + createRscExposeDefinitions({ + './RemoteClientCounter': { + import: ' ', + }, + }), + ).toThrow( + 'Remote expose import paths must be non-empty tokens after trimming.', + ); }); it('rejects non-string and non-object expose definitions', () => { From a689f5bbfb8b230a5fc59cfc20c2c26c467a1284 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 08:23:54 +0000 Subject: [PATCH 211/324] test(rsc-mf): assert callback network failure after retries --- .../tests/registerServerCallback.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts index baf68270ed8f..bd50dcdd53b9 100644 --- a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts @@ -294,6 +294,31 @@ describe('registerRemoteServerCallback runtime behavior', () => { expect(mockCreateFromFetch).toHaveBeenCalledTimes(1); }); + it('throws network error after retry budget is exhausted', async () => { + const { registerRemoteServerCallback } = await importRegisterHelper(); + registerRemoteServerCallback('http://127.0.0.1:3008/server-component-root'); + + global.fetch = jest + .fn() + .mockRejectedValueOnce(new Error('network-down-first-attempt')) + .mockRejectedValueOnce(new Error('network-down-second-attempt')); + + const callback = getRegisteredCallback(); + await expect( + callback('fetch-network-failure-action', ['arg-1']), + ).rejects.toThrow( + 'Remote action callback request failed due to network error (http://127.0.0.1:3008/server-component-root): Error: network-down-second-attempt', + ); + expect(mockCreateTemporaryReferenceSet).toHaveBeenCalledTimes(1); + expect(mockEncodeReply).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledTimes(2); + const firstFetchBody = (global.fetch as jest.Mock).mock.calls[0]?.[1]?.body; + const secondFetchBody = (global.fetch as jest.Mock).mock.calls[1]?.[1] + ?.body; + expect(firstFetchBody).toBe(secondFetchBody); + expect(mockCreateFromFetch).not.toHaveBeenCalled(); + }); + it('uses default alias when remote alias is omitted', async () => { const { registerRemoteServerCallback } = await importRegisterHelper(); registerRemoteServerCallback('http://127.0.0.1:3008/server-component-root'); From 8f9e303e5ef671a570b754fc8439b815b3ac94cf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 08:25:51 +0000 Subject: [PATCH 212/324] test(rsc-mf): assert dedupe behavior in remote expose config --- .../rsc-mf/remote/module-federation.config.ts | 5 ++++- .../rsc-mf/tests/moduleFederationConfig.test.ts | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/integration/rsc-mf/remote/module-federation.config.ts b/tests/integration/rsc-mf/remote/module-federation.config.ts index 442ebdd974f3..33f06c5c81aa 100644 --- a/tests/integration/rsc-mf/remote/module-federation.config.ts +++ b/tests/integration/rsc-mf/remote/module-federation.config.ts @@ -32,7 +32,10 @@ const remoteExposeImports: Record = { './defaultAction': './src/components/defaultAction.ts', './actionBundle': './src/components/actionBundle.ts', './infoBundle': { - import: './src/components/infoBundle.ts', + import: [ + './src/components/infoBundle.ts', + './src/components/infoBundle.ts', + ], }, }; diff --git a/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts b/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts index ee54fe3207ea..9778d9c8ae46 100644 --- a/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts +++ b/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts @@ -139,6 +139,19 @@ describe('rsc-mf module federation config contracts', () => { ); }); + it('deduplicates repeated imports in object expose definitions', () => { + const remoteConfig = loadRemoteConfig(); + const infoBundleExpose = remoteConfig.exposes?.['./infoBundle'] as + | { + import?: string[]; + } + | undefined; + expect(infoBundleExpose?.import).toEqual([ + CALLBACK_BOOTSTRAP_IMPORT, + './src/components/infoBundle.ts', + ]); + }); + it('uses remote port env var in host manifest remote URL', () => { const hostConfig = loadHostConfig({ nodeEnv: 'test', From ba39928a6ab86a02c3f22cb043bbd5c7c772274e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 08:27:27 +0000 Subject: [PATCH 213/324] refactor(rsc-mf): normalize callback URLs with trailing slashes --- .../src/runtime/registerServerCallback.ts | 2 ++ .../tests/registerServerCallback.test.ts | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts index fac458c4c2e1..bf092f89ee27 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/registerServerCallback.ts @@ -47,6 +47,8 @@ const getNormalizedRemoteActionUrl = (remoteOrigin: string) => { 'Remote action callback URL must not include embedded credentials.', ); } + const normalizedPathname = url.pathname.replace(/\/+$/, '') || '/'; + url.pathname = normalizedPathname; url.search = ''; url.hash = ''; return url.toString(); diff --git a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts index bd50dcdd53b9..15689d6942da 100644 --- a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts @@ -384,6 +384,31 @@ describe('registerRemoteServerCallback runtime behavior', () => { ); }); + it('dedupes callback registrations when only trailing slash differs', async () => { + const { registerRemoteServerCallback } = await importRegisterHelper(); + registerRemoteServerCallback( + 'http://127.0.0.1:3008/server-component-root/', + 'rscRemote', + ); + registerRemoteServerCallback( + 'http://127.0.0.1:3008/server-component-root', + 'rscRemote', + ); + expect(mockSetServerCallback).toHaveBeenCalledTimes(1); + + const callback = getRegisteredCallback(); + await callback('slash-normalized-action', []); + + expect(global.fetch).toHaveBeenCalledWith( + 'http://127.0.0.1:3008/server-component-root', + expect.objectContaining({ + headers: expect.objectContaining({ + 'x-rsc-action': 'remote:rscRemote:slash-normalized-action', + }), + }), + ); + }); + it('re-registers callback when alias changes and uses new alias prefix', async () => { const { registerRemoteServerCallback } = await importRegisterHelper(); registerRemoteServerCallback( From 442271ff45cd15f60441f51cfe0d8afbf93b89de Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 08:29:15 +0000 Subject: [PATCH 214/324] test(rsc-mf): assert terminal 429 callback failure after retry --- .../tests/registerServerCallback.test.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts index 15689d6942da..450d7a289082 100644 --- a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts @@ -173,6 +173,33 @@ describe('registerRemoteServerCallback runtime behavior', () => { expect(mockCreateFromFetch).toHaveBeenCalledTimes(1); }); + it('throws after retry when callback fetch stays at retryable 429 response', async () => { + const { registerRemoteServerCallback } = await importRegisterHelper(); + registerRemoteServerCallback('http://127.0.0.1:3008/server-component-root'); + global.fetch = jest.fn(async () => { + return { + ok: false, + status: 429, + statusText: 'Too Many Requests', + } as Response; + }); + + const callback = getRegisteredCallback(); + await expect( + callback('fetch-retry-429-fail-action', ['arg-1']), + ).rejects.toThrow( + 'Remote action callback request failed with status 429 Too Many Requests (http://127.0.0.1:3008/server-component-root).', + ); + expect(mockCreateTemporaryReferenceSet).toHaveBeenCalledTimes(1); + expect(mockEncodeReply).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledTimes(2); + const firstFetchBody = (global.fetch as jest.Mock).mock.calls[0]?.[1]?.body; + const secondFetchBody = (global.fetch as jest.Mock).mock.calls[1]?.[1] + ?.body; + expect(firstFetchBody).toBe(secondFetchBody); + expect(mockCreateFromFetch).not.toHaveBeenCalled(); + }); + it('retries once when callback fetch returns retryable 408 response', async () => { const { registerRemoteServerCallback } = await importRegisterHelper(); registerRemoteServerCallback('http://127.0.0.1:3008/server-component-root'); From 7ae305c52cce240119c5ce88228a9b90bd5a1a4e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 08:35:31 +0000 Subject: [PATCH 215/324] refactor(rsc-mf): enforce single-module object import arrays --- .../src/runtime/createRscExposeDefinitions.ts | 13 ++++- .../tests/createRscExposeDefinitions.test.ts | 52 +++++++------------ 2 files changed, 30 insertions(+), 35 deletions(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts index 319cf5cb510d..742b483e4086 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts @@ -78,8 +78,19 @@ const normalizeExposeImportPaths = ( exposeImport.length > 0 && exposeImport.every(item => typeof item === 'string') ) { + const normalizedImportPaths = normalizeImportPaths(exposeKey, exposeImport); + if (normalizedImportPaths.includes(CALLBACK_BOOTSTRAP_IMPORT)) { + throw new Error( + `Callback bootstrap module (${CALLBACK_BOOTSTRAP_IMPORT}) must remain internal-only and cannot be exposed. Invalid entries: ${exposeKey}`, + ); + } + if (normalizedImportPaths.length > 1) { + throw new Error( + `Remote expose import arrays must normalize to a single userland module path. Invalid entry: ${exposeKey}`, + ); + } return { - importPaths: normalizeImportPaths(exposeKey, exposeImport), + importPaths: normalizedImportPaths, exposeOverrides, } satisfies NormalizedExposeDefinition; } diff --git a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts index 1ffc032ff909..4b1befc4038a 100644 --- a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts +++ b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts @@ -145,28 +145,20 @@ describe('createRscExposeDefinitions', () => { }); }); - it('supports object expose definitions with import arrays', () => { - const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = - loadCreateRscExposeDefinitions(); - const exposeDefinitions = createRscExposeDefinitions({ - './infoBundle': { - import: [ - './src/components/infoBundle.ts', - './src/components/remoteMeta.ts', - ], - }, - }); - - expect(exposeDefinitions).toEqual({ - './infoBundle': { - import: [ - CALLBACK_BOOTSTRAP_MODULE, - './src/components/infoBundle.ts', - './src/components/remoteMeta.ts', - ], - layer: 'react-server-components', - }, - }); + it('rejects multi-module import arrays in object expose definitions', () => { + const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); + expect(() => + createRscExposeDefinitions({ + './infoBundle': { + import: [ + './src/components/infoBundle.ts', + './src/components/remoteMeta.ts', + ], + }, + }), + ).toThrow( + 'Remote expose import arrays must normalize to a single userland module path.', + ); }); it('deduplicates repeated entries in object expose import arrays', () => { @@ -177,18 +169,14 @@ describe('createRscExposeDefinitions', () => { import: [ './src/components/infoBundle.ts', './src/components/infoBundle.ts', - './src/components/remoteMeta.ts', + './src/components/infoBundle.ts', ], }, }); expect(exposeDefinitions).toEqual({ './infoBundle': { - import: [ - CALLBACK_BOOTSTRAP_MODULE, - './src/components/infoBundle.ts', - './src/components/remoteMeta.ts', - ], + import: [CALLBACK_BOOTSTRAP_MODULE, './src/components/infoBundle.ts'], layer: 'react-server-components', }, }); @@ -202,18 +190,14 @@ describe('createRscExposeDefinitions', () => { import: [ ' ./src/components/infoBundle.ts ', './src/components/infoBundle.ts', - ' ./src/components/remoteMeta.ts ', + ' ./src/components/infoBundle.ts ', ], }, }); expect(exposeDefinitions).toEqual({ './infoBundle': { - import: [ - CALLBACK_BOOTSTRAP_MODULE, - './src/components/infoBundle.ts', - './src/components/remoteMeta.ts', - ], + import: [CALLBACK_BOOTSTRAP_MODULE, './src/components/infoBundle.ts'], layer: 'react-server-components', }, }); From 4ec8948e91c0c74dd84f5ae85a1679915594350d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 08:37:08 +0000 Subject: [PATCH 216/324] test(rsc-mf): cover root callback URL slash dedupe --- .../tests/registerServerCallback.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts index 450d7a289082..b8495c93aa28 100644 --- a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts @@ -436,6 +436,25 @@ describe('registerRemoteServerCallback runtime behavior', () => { ); }); + it('dedupes root callback registrations for origin with and without slash', async () => { + const { registerRemoteServerCallback } = await importRegisterHelper(); + registerRemoteServerCallback('http://127.0.0.1:3008', 'rscRemote'); + registerRemoteServerCallback('http://127.0.0.1:3008/', 'rscRemote'); + expect(mockSetServerCallback).toHaveBeenCalledTimes(1); + + const callback = getRegisteredCallback(); + await callback('root-endpoint-action', []); + + expect(global.fetch).toHaveBeenCalledWith( + 'http://127.0.0.1:3008/', + expect.objectContaining({ + headers: expect.objectContaining({ + 'x-rsc-action': 'remote:rscRemote:root-endpoint-action', + }), + }), + ); + }); + it('re-registers callback when alias changes and uses new alias prefix', async () => { const { registerRemoteServerCallback } = await importRegisterHelper(); registerRemoteServerCallback( From 2c4a7f1f24fb907384b65212f5f4165b31cfdf0b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 08:38:38 +0000 Subject: [PATCH 217/324] test(rsc-mf): cover slash+fragment callback URL dedupe --- .../tests/registerServerCallback.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts index b8495c93aa28..da309d141081 100644 --- a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts @@ -436,6 +436,31 @@ describe('registerRemoteServerCallback runtime behavior', () => { ); }); + it('dedupes callback registrations when trailing slash and URL fragments differ', async () => { + const { registerRemoteServerCallback } = await importRegisterHelper(); + registerRemoteServerCallback( + 'http://127.0.0.1:3008/server-component-root/?cache=1#first', + 'rscRemote', + ); + registerRemoteServerCallback( + 'http://127.0.0.1:3008/server-component-root#second', + 'rscRemote', + ); + expect(mockSetServerCallback).toHaveBeenCalledTimes(1); + + const callback = getRegisteredCallback(); + await callback('slash-fragment-normalized-action', []); + + expect(global.fetch).toHaveBeenCalledWith( + 'http://127.0.0.1:3008/server-component-root', + expect.objectContaining({ + headers: expect.objectContaining({ + 'x-rsc-action': 'remote:rscRemote:slash-fragment-normalized-action', + }), + }), + ); + }); + it('dedupes root callback registrations for origin with and without slash', async () => { const { registerRemoteServerCallback } = await importRegisterHelper(); registerRemoteServerCallback('http://127.0.0.1:3008', 'rscRemote'); From 3b75a5c896f9e3986452cb5295fad0e692ff5309 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 08:42:46 +0000 Subject: [PATCH 218/324] test(rsc-mf): cover default-port callback URL normalization --- .../tests/registerServerCallback.test.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts index da309d141081..c5e6285d1a76 100644 --- a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts @@ -461,6 +461,32 @@ describe('registerRemoteServerCallback runtime behavior', () => { ); }); + it('dedupes callback registrations when only default http port differs', async () => { + const { registerRemoteServerCallback } = await importRegisterHelper(); + registerRemoteServerCallback( + 'http://127.0.0.1:80/server-component-root', + 'rscRemote', + ); + registerRemoteServerCallback( + 'http://127.0.0.1/server-component-root', + 'rscRemote', + ); + expect(mockSetServerCallback).toHaveBeenCalledTimes(1); + + const callback = getRegisteredCallback(); + await callback('default-http-port-normalized-action', []); + + expect(global.fetch).toHaveBeenCalledWith( + 'http://127.0.0.1/server-component-root', + expect.objectContaining({ + headers: expect.objectContaining({ + 'x-rsc-action': + 'remote:rscRemote:default-http-port-normalized-action', + }), + }), + ); + }); + it('dedupes root callback registrations for origin with and without slash', async () => { const { registerRemoteServerCallback } = await importRegisterHelper(); registerRemoteServerCallback('http://127.0.0.1:3008', 'rscRemote'); From e2ab44d60fe32b378102439894453da094641bd7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 08:46:09 +0000 Subject: [PATCH 219/324] refactor(rsc-mf): keep remote exposes as plain userland strings --- .../rsc-mf/remote/module-federation.config.ts | 14 +++----------- .../rsc-mf/tests/moduleFederationConfig.test.ts | 2 +- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/tests/integration/rsc-mf/remote/module-federation.config.ts b/tests/integration/rsc-mf/remote/module-federation.config.ts index 33f06c5c81aa..a35e782553f7 100644 --- a/tests/integration/rsc-mf/remote/module-federation.config.ts +++ b/tests/integration/rsc-mf/remote/module-federation.config.ts @@ -1,9 +1,6 @@ import path from 'path'; import { createModuleFederationConfig } from '@module-federation/modern-js-v3'; -import { - type ExposeDefinitionInput, - createRscExposeDefinitions, -} from './src/runtime/createRscExposeDefinitions'; +import { createRscExposeDefinitions } from './src/runtime/createRscExposeDefinitions'; const LAYERS = { ssr: 'server-side-rendering', @@ -18,7 +15,7 @@ const reactDomServerImport = path.join( 'react-dom.react-server.js', ); const reactServerDomClientImport = 'react-server-dom-rspack/client.browser'; -const remoteExposeImports: Record = { +const remoteExposeImports: Record = { './RemoteClientCounter': './src/components/RemoteClientCounter.tsx', './RemoteClientBadge': './src/components/RemoteClientBadge.tsx', './RemoteServerCard': './src/components/RemoteServerCard.tsx', @@ -31,12 +28,7 @@ const remoteExposeImports: Record = { './nestedActions': './src/components/nestedActions.ts', './defaultAction': './src/components/defaultAction.ts', './actionBundle': './src/components/actionBundle.ts', - './infoBundle': { - import: [ - './src/components/infoBundle.ts', - './src/components/infoBundle.ts', - ], - }, + './infoBundle': './src/components/infoBundle.ts', }; const sharedByScope = [ diff --git a/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts b/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts index 9778d9c8ae46..d5a0ebdf484a 100644 --- a/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts +++ b/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts @@ -139,7 +139,7 @@ describe('rsc-mf module federation config contracts', () => { ); }); - it('deduplicates repeated imports in object expose definitions', () => { + it('normalizes string expose definitions into callback-bootstrapped imports', () => { const remoteConfig = loadRemoteConfig(); const infoBundleExpose = remoteConfig.exposes?.['./infoBundle'] as | { From 21f7ed4024ce89d15980cf074ba2e1895ee4e7d5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 08:51:41 +0000 Subject: [PATCH 220/324] test(rsc-mf): relax expose-contract checks to behavior-level --- .../tests/moduleFederationConfig.test.ts | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts b/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts index d5a0ebdf484a..89b541417e96 100644 --- a/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts +++ b/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts @@ -87,7 +87,7 @@ const loadHostConfig = ({ ); describe('rsc-mf module federation config contracts', () => { - it('declares expected remote exposes with callback bootstrap imports', () => { + it('declares expected remote exposes with normalized userland imports', () => { const remoteConfig = loadRemoteConfig(); const exposeEntries = Object.entries(remoteConfig.exposes || {}); const exposeKeys = exposeEntries @@ -103,11 +103,14 @@ describe('rsc-mf module federation config contracts', () => { expect(definition.layer).toBe('react-server-components'); expect(definition.import).toBeDefined(); expect(Array.isArray(definition.import)).toBe(true); - expect(definition.import?.[0]).toBe(CALLBACK_BOOTSTRAP_IMPORT); - expect(definition.import!.length).toBeGreaterThanOrEqual(2); - for (const importPath of definition.import!.slice(1)) { + expect(definition.import!.length).toBeGreaterThanOrEqual(1); + const userlandImports = definition.import!.filter( + importPath => importPath !== CALLBACK_BOOTSTRAP_IMPORT, + ); + expect(userlandImports.length).toBeGreaterThanOrEqual(1); + for (const importPath of userlandImports) { expect(importPath).toMatch(/^\.\//); - expect(importPath).not.toMatch(/^\.\/src\/runtime\//); + expect(importPath).toMatch(/^\.\/src\/components\//); expect(importPath).toMatch(/\.[cm]?[jt]sx?$/i); expect(importPath).not.toContain('..'); expect(importPath).not.toContain('\\'); @@ -139,17 +142,26 @@ describe('rsc-mf module federation config contracts', () => { ); }); - it('normalizes string expose definitions into callback-bootstrapped imports', () => { + it('normalizes string expose definitions into deterministic userland imports', () => { const remoteConfig = loadRemoteConfig(); const infoBundleExpose = remoteConfig.exposes?.['./infoBundle'] as | { import?: string[]; } | undefined; - expect(infoBundleExpose?.import).toEqual([ - CALLBACK_BOOTSTRAP_IMPORT, - './src/components/infoBundle.ts', - ]); + const imports = infoBundleExpose?.import || []; + expect( + imports.filter( + importPath => importPath === './src/components/infoBundle.ts', + ), + ).toEqual(['./src/components/infoBundle.ts']); + expect( + imports.every( + importPath => + importPath === './src/components/infoBundle.ts' || + importPath === CALLBACK_BOOTSTRAP_IMPORT, + ), + ).toBe(true); }); it('uses remote port env var in host manifest remote URL', () => { From 51d9f5975aef95530d768b01ef0ece0420158a45 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 08:52:59 +0000 Subject: [PATCH 221/324] test(rsc-mf): cover default https-port callback normalization --- .../tests/registerServerCallback.test.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts index c5e6285d1a76..2b225ceb05e8 100644 --- a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts @@ -487,6 +487,32 @@ describe('registerRemoteServerCallback runtime behavior', () => { ); }); + it('dedupes callback registrations when only default https port differs', async () => { + const { registerRemoteServerCallback } = await importRegisterHelper(); + registerRemoteServerCallback( + 'https://example.com:443/server-component-root', + 'rscRemote', + ); + registerRemoteServerCallback( + 'https://example.com/server-component-root', + 'rscRemote', + ); + expect(mockSetServerCallback).toHaveBeenCalledTimes(1); + + const callback = getRegisteredCallback(); + await callback('default-https-port-normalized-action', []); + + expect(global.fetch).toHaveBeenCalledWith( + 'https://example.com/server-component-root', + expect.objectContaining({ + headers: expect.objectContaining({ + 'x-rsc-action': + 'remote:rscRemote:default-https-port-normalized-action', + }), + }), + ); + }); + it('dedupes root callback registrations for origin with and without slash', async () => { const { registerRemoteServerCallback } = await importRegisterHelper(); registerRemoteServerCallback('http://127.0.0.1:3008', 'rscRemote'); From de556c1aebd8244666c121af741e6e2790a7b789 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 08:59:48 +0000 Subject: [PATCH 222/324] test(rsc-mf): assert callback rebind when endpoint port changes --- .../tests/registerServerCallback.test.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts index 2b225ceb05e8..d93169deb6bb 100644 --- a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts @@ -588,6 +588,34 @@ describe('registerRemoteServerCallback runtime behavior', () => { ); }); + it('re-registers callback when callback port changes', async () => { + const { registerRemoteServerCallback } = await importRegisterHelper(); + registerRemoteServerCallback( + 'http://127.0.0.1:3008/server-component-root', + 'rscRemote', + ); + registerRemoteServerCallback( + 'http://127.0.0.1:3010/server-component-root', + 'rscRemote', + ); + expect(mockSetServerCallback).toHaveBeenCalledTimes(2); + + const callback = mockSetServerCallback.mock.calls[1]?.[0] as + | ServerCallback + | undefined; + expect(typeof callback).toBe('function'); + await (callback as ServerCallback)('port-change-action', []); + + expect(global.fetch).toHaveBeenCalledWith( + 'http://127.0.0.1:3010/server-component-root', + expect.objectContaining({ + headers: expect.objectContaining({ + 'x-rsc-action': 'remote:rscRemote:port-change-action', + }), + }), + ); + }); + it('trims alias before callback keying and action prefixing', async () => { const { registerRemoteServerCallback } = await importRegisterHelper(); registerRemoteServerCallback( From 44f2ead62182298b24dd9e23e0bd01341dde521b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 09:03:15 +0000 Subject: [PATCH 223/324] refactor(rsc-mf): keep host remote public-path plugin always on --- tests/integration/rsc-mf/host/module-federation.config.ts | 7 +++---- .../rsc-mf/tests/moduleFederationConfig.test.ts | 7 +++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/integration/rsc-mf/host/module-federation.config.ts b/tests/integration/rsc-mf/host/module-federation.config.ts index 6c1f6e3eb7f2..c375f3fca69b 100644 --- a/tests/integration/rsc-mf/host/module-federation.config.ts +++ b/tests/integration/rsc-mf/host/module-federation.config.ts @@ -15,10 +15,9 @@ const reactDomServerImport = path.join( 'react-dom.react-server.js', ); const reactServerDomClientImport = 'react-server-dom-rspack/client.browser'; -const runtimePlugins = - process.env.NODE_ENV === 'production' - ? [path.resolve(__dirname, './runtime/forceRemotePublicPath.ts')] - : []; +const runtimePlugins = [ + path.resolve(__dirname, './runtime/forceRemotePublicPath.ts'), +]; const sharedByScope = [ { diff --git a/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts b/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts index 89b541417e96..5f583f3b62a0 100644 --- a/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts +++ b/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts @@ -187,7 +187,7 @@ describe('rsc-mf module federation config contracts', () => { ); }); - it('enables host runtime plugin only in production', () => { + it('keeps host remote-public-path runtime plugin enabled across modes', () => { const productionHostConfig = loadHostConfig({ nodeEnv: 'production', remotePort: '3008', @@ -201,7 +201,10 @@ describe('rsc-mf module federation config contracts', () => { nodeEnv: 'development', remotePort: '3008', }); - expect(developmentHostConfig.runtimePlugins).toEqual([]); + expect(developmentHostConfig.runtimePlugins).toHaveLength(1); + expect(developmentHostConfig.runtimePlugins[0]).toContain( + 'runtime/forceRemotePublicPath.ts', + ); }); it('keeps host experiments aligned for async startup and rsc', () => { From 0b0942f9d411821c8a855a5f2b271ba852020b62 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 09:09:43 +0000 Subject: [PATCH 224/324] test(rsc-mf): assert browser expose-chunk coverage --- tests/integration/rsc-mf/tests/index.test.ts | 30 ++++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index f98704cfe6f5..89151d6d5462 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -19,6 +19,15 @@ const HOST_RSC_URL = '/server-component-root'; const EXPECTED_ACTION_POSTS_PER_MODE = 24; const EXPECTED_ACTION_POSTS_PER_FAMILY = 6; const EXPECTED_UNIQUE_ACTION_IDS_PER_MODE = 4; +const EXPECTED_BROWSER_EXPOSE_CHUNKS = [ + '__federation_expose_RemoteClientCounter', + '__federation_expose_RemoteClientBadge', + '__federation_expose_actions', + '__federation_expose_nestedActions', + '__federation_expose_defaultAction', + '__federation_expose_actionBundle', + '__federation_expose_infoBundle', +]; const EXPECTED_REMOTE_EXPOSE_PATHS = [ './RemoteClientCounter', './RemoteClientBadge', @@ -367,6 +376,7 @@ function runTests({ mode }: TestConfig) { const actionRequestIds: string[] = []; const actionRequestAcceptHeaders: string[] = []; const registerCallbackExposeRequestUrls: string[] = []; + const browserExposeChunkRequests: string[] = []; const failedNetworkRequests: FailedRequestRecord[] = []; const failedBrowserRequests: FailedBrowserRequestRecord[] = []; @@ -426,6 +436,10 @@ function runTests({ mode }: TestConfig) { if (url.includes('__federation_expose_registerServerCallback')) { registerCallbackExposeRequestUrls.push(url); } + const exposeChunkMatch = url.match(/__federation_expose_[^./?#]+/); + if (exposeChunkMatch) { + browserExposeChunkRequests.push(exposeChunkMatch[0]); + } if (request.method() !== 'POST' || !headers['x-rsc-action']) { return; } @@ -499,13 +513,8 @@ function runTests({ mode }: TestConfig) { expect(uniqueExposedPaths.length).toBeGreaterThan(0); expect(uniqueExposedPaths).toContain('./RemoteClientCounter'); expect( - uniqueExposedPaths.every(path => - EXPECTED_REMOTE_EXPOSE_PATHS.includes(path), - ), + exposedPaths.every(path => EXPECTED_REMOTE_EXPOSE_PATHS.includes(path)), ).toBe(true); - expect(uniqueExposedPaths.length).toBeLessThanOrEqual( - EXPECTED_REMOTE_EXPOSE_PATHS.length, - ); expect( exposedPaths.every(path => !path.startsWith('./src/components/')), ).toBe(true); @@ -525,6 +534,15 @@ function runTests({ mode }: TestConfig) { actionRequestIds, })); + it('should load expected federated expose chunks in browser', () => { + const uniqueExposeChunkRequests = Array.from( + new Set(browserExposeChunkRequests), + ).sort(); + expect(uniqueExposeChunkRequests).toEqual( + expect.arrayContaining(EXPECTED_BROWSER_EXPOSE_CHUNKS), + ); + }); + it('should route remote actions through host endpoint', () => { expect(actionRequestUrls.length).toBe(EXPECTED_ACTION_POSTS_PER_MODE); expect(actionRequestUrls.length).toBe(actionRequestIds.length); From 28690f1d3b0f22a987fb02b19c892225d0c549b8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 09:16:41 +0000 Subject: [PATCH 225/324] test(rsc-mf): cover host federated asset proxy middleware --- .../rsc-mf/host/server/modern.server.ts | 2 +- .../rsc-mf/tests/modernServerConfig.test.ts | 221 ++++++++++++++++++ .../tests/types/modern-js-server-runtime.d.ts | 8 + 3 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 tests/integration/rsc-mf/tests/modernServerConfig.test.ts create mode 100644 tests/integration/rsc-mf/tests/types/modern-js-server-runtime.d.ts diff --git a/tests/integration/rsc-mf/host/server/modern.server.ts b/tests/integration/rsc-mf/host/server/modern.server.ts index 205dc82a6c87..c10cb09bf252 100644 --- a/tests/integration/rsc-mf/host/server/modern.server.ts +++ b/tests/integration/rsc-mf/host/server/modern.server.ts @@ -35,7 +35,7 @@ const proxyRemoteFederationAsset: MiddlewareHandler = async (c, next) => { } const remoteUrl = `http://127.0.0.1:${remotePort}${pathname}${reqUrl.search}`; - const upstream = await fetch(remoteUrl).catch(() => undefined); + const upstream = await fetch(remoteUrl).catch((): undefined => undefined); if (!upstream || !upstream.ok) { await next(); diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts new file mode 100644 index 000000000000..7401ae77344f --- /dev/null +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -0,0 +1,221 @@ +const HOST_SERVER_CONFIG_MODULE = '../host/server/modern.server'; + +const withRemotePort = (remotePort: string | undefined, run: () => T): T => { + const previousRemotePort = process.env.RSC_MF_REMOTE_PORT; + if (typeof remotePort === 'undefined') { + delete process.env.RSC_MF_REMOTE_PORT; + } else { + process.env.RSC_MF_REMOTE_PORT = remotePort; + } + + try { + return run(); + } finally { + if (typeof previousRemotePort === 'undefined') { + delete process.env.RSC_MF_REMOTE_PORT; + } else { + process.env.RSC_MF_REMOTE_PORT = previousRemotePort; + } + } +}; + +const loadHostServerConfig = () => { + jest.resetModules(); + jest.doMock('@modern-js/server-runtime', () => ({ + defineServerConfig: (config: unknown) => config, + })); + + let config: any; + let loadError: unknown; + jest.isolateModules(() => { + try { + config = require(HOST_SERVER_CONFIG_MODULE).default; + } catch (error) { + loadError = error; + } + }); + if (loadError) { + const message = + loadError instanceof Error + ? loadError.stack || loadError.message + : String(loadError); + throw new Error(`Failed to load host server config: ${message}`); + } + return config; +}; + +const getProxyMiddlewareHandler = () => { + const config = loadHostServerConfig(); + if (!Array.isArray(config.middlewares)) { + throw new Error('Host server config did not provide a middlewares array'); + } + const middleware = config.middlewares.find( + (entry: { name?: string }) => + entry.name === 'proxy-remote-federation-asset', + ); + if (!middleware) { + throw new Error('proxy-remote-federation-asset middleware not found'); + } + expect(middleware.order).toBe('pre'); + expect(middleware.before).toEqual(['server-static']); + expect(typeof middleware.handler).toBe('function'); + return middleware.handler as ( + c: { req: { url: string }; res?: Response }, + next: () => Promise, + ) => Promise; +}; + +describe('rsc-mf host modern.server middleware contracts', () => { + const originalFetch = global.fetch; + const originalFetchDescriptor = Object.getOwnPropertyDescriptor( + global, + 'fetch', + ); + + const installFetchMock = (implementation: typeof fetch) => { + const fetchMock = jest.fn(implementation); + Object.defineProperty(global, 'fetch', { + value: fetchMock, + configurable: true, + writable: true, + }); + return fetchMock; + }; + + afterAll(() => { + if (originalFetchDescriptor) { + Object.defineProperty(global, 'fetch', originalFetchDescriptor); + return; + } + global.fetch = originalFetch; + }); + + it('proxies federated async JS expose chunks to remote origin', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock(async () => { + return new Response('proxied-js', { + status: 200, + headers: { + 'content-type': 'application/javascript', + }, + }); + }); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_infoBundle.11dea89e81.js?cache=1', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(fetchMock).toHaveBeenCalledWith( + 'http://127.0.0.1:3999/static/js/async/__federation_expose_infoBundle.11dea89e81.js?cache=1', + ); + expect(next).not.toHaveBeenCalled(); + expect(context.res).toBeInstanceOf(Response); + await expect(context.res?.text()).resolves.toBe('proxied-js'); + }); + + it('proxies federated async CSS expose chunks to remote origin', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock(async () => { + return new Response('.remote-style{}', { + status: 200, + headers: { + 'content-type': 'text/css', + }, + }); + }); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(fetchMock).toHaveBeenCalledWith( + 'http://127.0.0.1:3999/static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css', + ); + expect(next).not.toHaveBeenCalled(); + await expect(context.res?.text()).resolves.toBe('.remote-style{}'); + }); + + it('falls through when request path is outside federated asset patterns', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + async () => new Response('ignored', { status: 200 }), + ); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/js/server-component-root.abc123.js', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(fetchMock).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalledTimes(1); + expect(context.res).toBeUndefined(); + }); + + it('falls through when remote port is not configured', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + async () => new Response('ignored', { status: 200 }), + ); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_actions.44d8f1d7ae.js', + }, + }; + + await withRemotePort(undefined, () => handler(context, next)); + + expect(fetchMock).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalledTimes(1); + expect(context.res).toBeUndefined(); + }); + + it('falls through when upstream returns non-ok response', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + async () => new Response('not-found', { status: 404 }), + ); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_actions.44d8f1d7ae.js', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(context.res).toBeUndefined(); + }); + + it('falls through when upstream fetch throws', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock(async () => { + throw new Error('upstream-unreachable'); + }); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_actionBundle.c842b162f4.js', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(context.res).toBeUndefined(); + }); +}); diff --git a/tests/integration/rsc-mf/tests/types/modern-js-server-runtime.d.ts b/tests/integration/rsc-mf/tests/types/modern-js-server-runtime.d.ts new file mode 100644 index 000000000000..cd6fce867d72 --- /dev/null +++ b/tests/integration/rsc-mf/tests/types/modern-js-server-runtime.d.ts @@ -0,0 +1,8 @@ +declare module '@modern-js/server-runtime' { + export type MiddlewareHandler = ( + c: { req: { url: string }; res?: Response }, + next: () => Promise, + ) => Promise | void; + + export function defineServerConfig(config: T): T; +} From 8d4cb534b208c1f095e534fa67e842f57a2ae117 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 09:18:57 +0000 Subject: [PATCH 226/324] test(rsc-mf): validate forceRemotePublicPath runtime plugin behavior --- .../tests/forceRemotePublicPath.test.ts | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 tests/integration/rsc-mf/tests/forceRemotePublicPath.test.ts diff --git a/tests/integration/rsc-mf/tests/forceRemotePublicPath.test.ts b/tests/integration/rsc-mf/tests/forceRemotePublicPath.test.ts new file mode 100644 index 000000000000..aad8d617d0d1 --- /dev/null +++ b/tests/integration/rsc-mf/tests/forceRemotePublicPath.test.ts @@ -0,0 +1,103 @@ +import forceRemotePublicPath from '../host/runtime/forceRemotePublicPath'; + +describe('host forceRemotePublicPath runtime plugin', () => { + it('keeps plugin name stable', () => { + const plugin = forceRemotePublicPath(); + expect(plugin.name).toBe('rsc-mf-force-remote-public-path'); + expect(typeof plugin.loadRemoteSnapshot).toBe('function'); + }); + + it('does not mutate non-target remotes', () => { + const plugin = forceRemotePublicPath(); + const args = { + remoteInfo: { + alias: 'anotherRemote', + entry: 'http://127.0.0.1:3008/static/mf-manifest.json', + }, + remoteSnapshot: { + publicPath: 'http://example.com/', + }, + }; + + const result = plugin.loadRemoteSnapshot?.(args as any); + expect(result).toBe(args); + expect(args.remoteSnapshot.publicPath).toBe('http://example.com/'); + }); + + it('does not mutate when entry is missing or non-string', () => { + const plugin = forceRemotePublicPath(); + const argsWithoutEntry = { + remoteInfo: { + alias: 'rscRemote', + }, + remoteSnapshot: { + publicPath: 'http://example.com/', + }, + }; + const argsWithNonStringEntry = { + remoteInfo: { + alias: 'rscRemote', + entry: 1234, + }, + remoteSnapshot: { + publicPath: 'http://example.com/', + }, + }; + + plugin.loadRemoteSnapshot?.(argsWithoutEntry as any); + plugin.loadRemoteSnapshot?.(argsWithNonStringEntry as any); + + expect(argsWithoutEntry.remoteSnapshot.publicPath).toBe( + 'http://example.com/', + ); + expect(argsWithNonStringEntry.remoteSnapshot.publicPath).toBe( + 'http://example.com/', + ); + }); + + it('forces origin-based public paths for rscRemote snapshots', () => { + const plugin = forceRemotePublicPath(); + const args = { + remoteInfo: { + alias: 'rscRemote', + entry: 'http://127.0.0.1:3008/static/mf-manifest.json', + }, + remoteSnapshot: { + publicPath: 'http://example.com/static/', + metaData: { + publicPath: 'http://example.com/static/', + ssrPublicPath: 'http://example.com/static/bundles/', + }, + }, + }; + + const result = plugin.loadRemoteSnapshot?.(args as any); + + expect(result).toBe(args); + expect(args.remoteSnapshot.publicPath).toBe('http://127.0.0.1:3008/'); + expect(args.remoteSnapshot.metaData.publicPath).toBe( + 'http://127.0.0.1:3008/', + ); + expect(args.remoteSnapshot.metaData.ssrPublicPath).toBe( + 'http://127.0.0.1:3008/bundles/', + ); + }); + + it('only updates snapshot fields that exist', () => { + const plugin = forceRemotePublicPath(); + const args = { + remoteInfo: { + alias: 'rscRemote', + entry: 'https://remote.example.com/static/mf-manifest.json', + }, + remoteSnapshot: { + metaData: {}, + }, + }; + + plugin.loadRemoteSnapshot?.(args as any); + + expect('publicPath' in args.remoteSnapshot).toBe(false); + expect(args.remoteSnapshot.metaData).toEqual({}); + }); +}); From 1b8615c0e067fb90800e70a1cc05fb7235f8ba74 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 09:21:59 +0000 Subject: [PATCH 227/324] test(rsc-mf): cover callback URL host-casing normalization --- .../tests/registerServerCallback.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts index d93169deb6bb..33c0779d7785 100644 --- a/tests/integration/rsc-mf/tests/registerServerCallback.test.ts +++ b/tests/integration/rsc-mf/tests/registerServerCallback.test.ts @@ -513,6 +513,31 @@ describe('registerRemoteServerCallback runtime behavior', () => { ); }); + it('dedupes callback registrations when host casing differs', async () => { + const { registerRemoteServerCallback } = await importRegisterHelper(); + registerRemoteServerCallback( + 'HTTP://LOCALHOST:3008/server-component-root', + 'rscRemote', + ); + registerRemoteServerCallback( + 'http://localhost:3008/server-component-root', + 'rscRemote', + ); + expect(mockSetServerCallback).toHaveBeenCalledTimes(1); + + const callback = getRegisteredCallback(); + await callback('host-casing-normalized-action', []); + + expect(global.fetch).toHaveBeenCalledWith( + 'http://localhost:3008/server-component-root', + expect.objectContaining({ + headers: expect.objectContaining({ + 'x-rsc-action': 'remote:rscRemote:host-casing-normalized-action', + }), + }), + ); + }); + it('dedupes root callback registrations for origin with and without slash', async () => { const { registerRemoteServerCallback } = await importRegisterHelper(); registerRemoteServerCallback('http://127.0.0.1:3008', 'rscRemote'); From 95bf06c663cb86287c0f454acb8138bfaccfdc0d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 09:23:40 +0000 Subject: [PATCH 228/324] test(rsc-mf): cover remote entry URL normalization in runtime plugin --- .../tests/forceRemotePublicPath.test.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/integration/rsc-mf/tests/forceRemotePublicPath.test.ts b/tests/integration/rsc-mf/tests/forceRemotePublicPath.test.ts index aad8d617d0d1..cb07cac02bbd 100644 --- a/tests/integration/rsc-mf/tests/forceRemotePublicPath.test.ts +++ b/tests/integration/rsc-mf/tests/forceRemotePublicPath.test.ts @@ -83,6 +83,34 @@ describe('host forceRemotePublicPath runtime plugin', () => { ); }); + it('normalizes entry URL query, hash, and default port in rewritten paths', () => { + const plugin = forceRemotePublicPath(); + const args = { + remoteInfo: { + alias: 'rscRemote', + entry: + 'https://remote.example.com:443/static/mf-manifest.json?cache=1#v', + }, + remoteSnapshot: { + publicPath: 'https://stale.example.com/static/', + metaData: { + publicPath: 'https://stale.example.com/static/', + ssrPublicPath: 'https://stale.example.com/static/bundles/', + }, + }, + }; + + plugin.loadRemoteSnapshot?.(args as any); + + expect(args.remoteSnapshot.publicPath).toBe('https://remote.example.com/'); + expect(args.remoteSnapshot.metaData.publicPath).toBe( + 'https://remote.example.com/', + ); + expect(args.remoteSnapshot.metaData.ssrPublicPath).toBe( + 'https://remote.example.com/bundles/', + ); + }); + it('only updates snapshot fields that exist', () => { const plugin = forceRemotePublicPath(); const args = { From 57ba0af3c076a2a8649887001df8c960f5c1eb5b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 09:26:38 +0000 Subject: [PATCH 229/324] test(rsc-mf): cover additional federated asset proxy path markers --- .../rsc-mf/tests/modernServerConfig.test.ts | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index 7401ae77344f..884c0fdbcf58 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -143,6 +143,58 @@ describe('rsc-mf host modern.server middleware contracts', () => { await expect(context.res?.text()).resolves.toBe('.remote-style{}'); }); + it('proxies async JS chunks with react server component marker', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock(async () => { + return new Response('proxied-rsc-chunk', { + status: 200, + headers: { + 'content-type': 'application/javascript', + }, + }); + }); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/503_react-server-components_0f2d4f91.js', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(fetchMock).toHaveBeenCalledWith( + 'http://127.0.0.1:3999/static/js/async/503_react-server-components_0f2d4f91.js', + ); + expect(next).not.toHaveBeenCalled(); + await expect(context.res?.text()).resolves.toBe('proxied-rsc-chunk'); + }); + + it('proxies async JS chunks containing node_modules react markers', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock(async () => { + return new Response('proxied-react-chunk', { + status: 200, + headers: { + 'content-type': 'application/javascript', + }, + }); + }); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/node_modules_pnpm_react_19.0.0_react-dom_19.0.0.js', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(fetchMock).toHaveBeenCalledWith( + 'http://127.0.0.1:3999/static/js/async/node_modules_pnpm_react_19.0.0_react-dom_19.0.0.js', + ); + expect(next).not.toHaveBeenCalled(); + await expect(context.res?.text()).resolves.toBe('proxied-react-chunk'); + }); + it('falls through when request path is outside federated asset patterns', async () => { const handler = getProxyMiddlewareHandler(); const next = jest.fn(async (): Promise => undefined); From b04a9b4cd18a197a6dbf3c87833b676f1fc6bb26 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 09:28:30 +0000 Subject: [PATCH 230/324] fix(rsc-mf): ignore malformed remote entry URLs in runtime plugin --- .../host/runtime/forceRemotePublicPath.ts | 13 ++++++++- .../tests/forceRemotePublicPath.test.ts | 27 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/tests/integration/rsc-mf/host/runtime/forceRemotePublicPath.ts b/tests/integration/rsc-mf/host/runtime/forceRemotePublicPath.ts index 8945fb778d8e..0e074972eb84 100644 --- a/tests/integration/rsc-mf/host/runtime/forceRemotePublicPath.ts +++ b/tests/integration/rsc-mf/host/runtime/forceRemotePublicPath.ts @@ -1,5 +1,13 @@ import type { ModuleFederationRuntimePlugin } from '@module-federation/modern-js-v3'; +const getRemotePublicPath = (entry: string) => { + try { + return `${new URL(entry).origin}/`; + } catch { + return undefined; + } +}; + const forceRemotePublicPath = (): ModuleFederationRuntimePlugin => ({ name: 'rsc-mf-force-remote-public-path', loadRemoteSnapshot(args: any) { @@ -12,7 +20,10 @@ const forceRemotePublicPath = (): ModuleFederationRuntimePlugin => ({ if (!entry || typeof entry !== 'string') { return args; } - const remotePublicPath = `${new URL(entry).origin}/`; + const remotePublicPath = getRemotePublicPath(entry); + if (!remotePublicPath) { + return args; + } if ('publicPath' in remoteSnapshot) { remoteSnapshot.publicPath = remotePublicPath; diff --git a/tests/integration/rsc-mf/tests/forceRemotePublicPath.test.ts b/tests/integration/rsc-mf/tests/forceRemotePublicPath.test.ts index cb07cac02bbd..af991850185c 100644 --- a/tests/integration/rsc-mf/tests/forceRemotePublicPath.test.ts +++ b/tests/integration/rsc-mf/tests/forceRemotePublicPath.test.ts @@ -55,6 +55,33 @@ describe('host forceRemotePublicPath runtime plugin', () => { ); }); + it('does not mutate when entry is not a valid URL', () => { + const plugin = forceRemotePublicPath(); + const args = { + remoteInfo: { + alias: 'rscRemote', + entry: 'http://', + }, + remoteSnapshot: { + publicPath: 'http://example.com/static/', + metaData: { + publicPath: 'http://example.com/static/', + ssrPublicPath: 'http://example.com/static/bundles/', + }, + }, + }; + + plugin.loadRemoteSnapshot?.(args as any); + + expect(args.remoteSnapshot.publicPath).toBe('http://example.com/static/'); + expect(args.remoteSnapshot.metaData.publicPath).toBe( + 'http://example.com/static/', + ); + expect(args.remoteSnapshot.metaData.ssrPublicPath).toBe( + 'http://example.com/static/bundles/', + ); + }); + it('forces origin-based public paths for rscRemote snapshots', () => { const plugin = forceRemotePublicPath(); const args = { From 22b8c20743daf6b7792bf1213a5790e201f05352 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 09:32:31 +0000 Subject: [PATCH 231/324] fix(rsc-mf): add manifest fallback for stale expose chunk paths --- .../rsc-mf/host/server/modern.server.ts | 137 +++++++++++++++++- .../rsc-mf/tests/modernServerConfig.test.ts | 80 +++++++++- 2 files changed, 210 insertions(+), 7 deletions(-) diff --git a/tests/integration/rsc-mf/host/server/modern.server.ts b/tests/integration/rsc-mf/host/server/modern.server.ts index c10cb09bf252..d84702202c06 100644 --- a/tests/integration/rsc-mf/host/server/modern.server.ts +++ b/tests/integration/rsc-mf/host/server/modern.server.ts @@ -3,6 +3,124 @@ import { defineServerConfig, } from '@modern-js/server-runtime'; +const REMOTE_MANIFEST_PATH = '/static/mf-manifest.json'; +const EXPOSE_CHUNK_HASH_SUFFIX_PATTERN = /\.[a-f0-9]{6,}$/i; + +interface RemoteManifestAssetEntry { + assets?: { + js?: { + sync?: string[]; + async?: string[]; + }; + css?: { + sync?: string[]; + async?: string[]; + }; + }; +} + +interface RemoteManifestShape { + shared?: RemoteManifestAssetEntry[]; + exposes?: RemoteManifestAssetEntry[]; +} + +const toCanonicalChunkName = (filePath: string) => + filePath + .replace(/\/+$/, '') + .split('/') + .pop() + ?.replace(/\.(js|css)$/i, '') + .replace(EXPOSE_CHUNK_HASH_SUFFIX_PATTERN, ''); + +const collectManifestAssetPaths = (manifest: RemoteManifestShape) => { + const entries = [...(manifest.shared || []), ...(manifest.exposes || [])]; + const assetPaths = new Set(); + for (const entry of entries) { + const jsSyncAssets = entry.assets?.js?.sync || []; + const jsAsyncAssets = entry.assets?.js?.async || []; + const cssSyncAssets = entry.assets?.css?.sync || []; + const cssAsyncAssets = entry.assets?.css?.async || []; + for (const assetPath of [ + ...jsSyncAssets, + ...jsAsyncAssets, + ...cssSyncAssets, + ...cssAsyncAssets, + ]) { + assetPaths.add(assetPath); + } + } + return [...assetPaths]; +}; + +const resolveManifestFallbackAssetPath = ( + pathname: string, + manifest: RemoteManifestShape, +) => { + if ( + !pathname.includes('__federation_expose_') || + (!pathname.endsWith('.js') && !pathname.endsWith('.css')) + ) { + return undefined; + } + + const canonicalRequestedChunkName = toCanonicalChunkName(pathname); + if (!canonicalRequestedChunkName) { + return undefined; + } + + const requestedAssetDirectory = pathname.includes('/static/css/async/') + ? 'static/css/async/' + : 'static/js/async/'; + const manifestAssets = collectManifestAssetPaths(manifest); + return manifestAssets.find(assetPath => { + if (!assetPath.startsWith(requestedAssetDirectory)) { + return false; + } + return toCanonicalChunkName(assetPath) === canonicalRequestedChunkName; + }); +}; + +const fetchRemoteManifestFallbackAsset = async ({ + remoteOrigin, + pathname, + search, +}: { + remoteOrigin: string; + pathname: string; + search: string; +}) => { + const manifestResponse = await fetch(`${remoteOrigin}${REMOTE_MANIFEST_PATH}`) + .then(response => { + if (!response.ok) { + return undefined; + } + return response; + }) + .catch((): undefined => undefined); + + if (!manifestResponse) { + return undefined; + } + + const manifest = (await manifestResponse.json()) as RemoteManifestShape; + const fallbackAssetPath = resolveManifestFallbackAssetPath( + pathname, + manifest, + ); + if (!fallbackAssetPath) { + return undefined; + } + + const fallbackAssetUrl = `${remoteOrigin}/${fallbackAssetPath}${search}`; + const fallbackAssetResponse = await fetch(fallbackAssetUrl).catch( + (): undefined => undefined, + ); + if (!fallbackAssetResponse || !fallbackAssetResponse.ok) { + return undefined; + } + return fallbackAssetResponse; +}; + const shouldProxyRemoteAsset = (pathname: string) => { if (pathname.startsWith('/static/js/async/')) { return ( @@ -34,17 +152,26 @@ const proxyRemoteFederationAsset: MiddlewareHandler = async (c, next) => { return; } - const remoteUrl = `http://127.0.0.1:${remotePort}${pathname}${reqUrl.search}`; + const remoteOrigin = `http://127.0.0.1:${remotePort}`; + const remoteUrl = `${remoteOrigin}${pathname}${reqUrl.search}`; const upstream = await fetch(remoteUrl).catch((): undefined => undefined); - if (!upstream || !upstream.ok) { + const resolvedUpstream = upstream?.ok + ? upstream + : await fetchRemoteManifestFallbackAsset({ + remoteOrigin, + pathname, + search: reqUrl.search, + }); + + if (!resolvedUpstream || !resolvedUpstream.ok) { await next(); return; } - c.res = new Response(await upstream.arrayBuffer(), { - status: upstream.status, - headers: upstream.headers, + c.res = new Response(await resolvedUpstream.arrayBuffer(), { + status: resolvedUpstream.status, + headers: resolvedUpstream.headers, }); }; diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index 884c0fdbcf58..3d155dfd1ee5 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -247,7 +247,11 @@ describe('rsc-mf host modern.server middleware contracts', () => { await withRemotePort('3999', () => handler(context, next)); - expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + 'http://127.0.0.1:3999/static/mf-manifest.json', + ); expect(next).toHaveBeenCalledTimes(1); expect(context.res).toBeUndefined(); }); @@ -266,8 +270,80 @@ describe('rsc-mf host modern.server middleware contracts', () => { await withRemotePort('3999', () => handler(context, next)); - expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + 'http://127.0.0.1:3999/static/mf-manifest.json', + ); expect(next).toHaveBeenCalledTimes(1); expect(context.res).toBeUndefined(); }); + + it('recovers from stale expose chunk path via manifest-driven fallback', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce(new Response('not-found', { status: 404 })) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + exposes: [ + { + assets: { + js: { + sync: [ + 'static/js/async/__federation_expose_RemoteServerCard.6e997e54ed.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ) + .mockResolvedValueOnce( + new Response('fallback-hit', { + status: 200, + headers: { + 'content-type': 'application/javascript', + }, + }), + ), + ); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteServerCard.js', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(fetchMock).toHaveBeenNthCalledWith( + 1, + 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteServerCard.js', + ); + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + 'http://127.0.0.1:3999/static/mf-manifest.json', + ); + expect(fetchMock).toHaveBeenNthCalledWith( + 3, + 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteServerCard.6e997e54ed.js', + ); + expect(next).not.toHaveBeenCalled(); + await expect(context.res?.text()).resolves.toBe('fallback-hit'); + }); }); From 9dcaef77e950371551afca0a96f097a5485de79f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 09:49:36 +0000 Subject: [PATCH 232/324] test(rsc-mf): cover shared-asset manifest fallback resolution --- .../rsc-mf/tests/modernServerConfig.test.ts | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index 3d155dfd1ee5..43b21aceb23a 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -346,4 +346,72 @@ describe('rsc-mf host modern.server middleware contracts', () => { expect(next).not.toHaveBeenCalled(); await expect(context.res?.text()).resolves.toBe('fallback-hit'); }); + + it('recovers stale expose path when manifest match is under shared assets', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce(new Response('not-found', { status: 404 })) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + shared: [ + { + assets: { + js: { + sync: [ + 'static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ) + .mockResolvedValueOnce( + new Response('shared-fallback-hit', { + status: 200, + headers: { + 'content-type': 'application/javascript', + }, + }), + ), + ); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteClientCounter.js', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(fetchMock).toHaveBeenNthCalledWith( + 1, + 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.js', + ); + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + 'http://127.0.0.1:3999/static/mf-manifest.json', + ); + expect(fetchMock).toHaveBeenNthCalledWith( + 3, + 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', + ); + expect(next).not.toHaveBeenCalled(); + await expect(context.res?.text()).resolves.toBe('shared-fallback-hit'); + }); }); From 40873029f93e2a83d26580579be6b287e952c4ad Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 09:51:27 +0000 Subject: [PATCH 233/324] test(rsc-mf): cover css stale-path manifest fallback --- .../rsc-mf/tests/modernServerConfig.test.ts | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index 43b21aceb23a..020e4de0b4d2 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -414,4 +414,72 @@ describe('rsc-mf host modern.server middleware contracts', () => { expect(next).not.toHaveBeenCalled(); await expect(context.res?.text()).resolves.toBe('shared-fallback-hit'); }); + + it('recovers stale CSS expose path via manifest-driven fallback', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce(new Response('not-found', { status: 404 })) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + exposes: [ + { + assets: { + js: { + sync: [], + async: [], + }, + css: { + sync: [ + 'static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css', + ], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ) + .mockResolvedValueOnce( + new Response('.fallback-style{}', { + status: 200, + headers: { + 'content-type': 'text/css', + }, + }), + ), + ); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/css/async/__federation_expose_RemoteClientCounter.css', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(fetchMock).toHaveBeenNthCalledWith( + 1, + 'http://127.0.0.1:3999/static/css/async/__federation_expose_RemoteClientCounter.css', + ); + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + 'http://127.0.0.1:3999/static/mf-manifest.json', + ); + expect(fetchMock).toHaveBeenNthCalledWith( + 3, + 'http://127.0.0.1:3999/static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css', + ); + expect(next).not.toHaveBeenCalled(); + await expect(context.res?.text()).resolves.toBe('.fallback-style{}'); + }); }); From bc6434c8c52363bea398b6c366097e0cdd8dfd6d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 09:54:03 +0000 Subject: [PATCH 234/324] test(rsc-mf): assert no-match manifest fallback passthrough --- .../rsc-mf/tests/modernServerConfig.test.ts | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index 020e4de0b4d2..d4eaff3b3c26 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -482,4 +482,61 @@ describe('rsc-mf host modern.server middleware contracts', () => { expect(next).not.toHaveBeenCalled(); await expect(context.res?.text()).resolves.toBe('.fallback-style{}'); }); + + it('falls through when manifest lookup has no matching fallback asset', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce(new Response('not-found', { status: 404 })) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + exposes: [ + { + assets: { + js: { + sync: [ + 'static/js/async/__federation_expose_other.abc123.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ), + ); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteServerCard.js', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(fetchMock).toHaveBeenNthCalledWith( + 1, + 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteServerCard.js', + ); + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + 'http://127.0.0.1:3999/static/mf-manifest.json', + ); + expect(next).toHaveBeenCalledTimes(1); + expect(context.res).toBeUndefined(); + }); }); From 800c14c42f1cd43fac4fd23120e13efc122fb45b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 09:55:46 +0000 Subject: [PATCH 235/324] fix(rsc-mf): tolerate invalid remote manifest fallback payloads --- .../rsc-mf/host/server/modern.server.ts | 7 ++++- .../rsc-mf/tests/modernServerConfig.test.ts | 29 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/tests/integration/rsc-mf/host/server/modern.server.ts b/tests/integration/rsc-mf/host/server/modern.server.ts index d84702202c06..e8be793692af 100644 --- a/tests/integration/rsc-mf/host/server/modern.server.ts +++ b/tests/integration/rsc-mf/host/server/modern.server.ts @@ -102,7 +102,12 @@ const fetchRemoteManifestFallbackAsset = async ({ return undefined; } - const manifest = (await manifestResponse.json()) as RemoteManifestShape; + const manifest = (await manifestResponse + .json() + .catch((): undefined => undefined)) as RemoteManifestShape | undefined; + if (!manifest) { + return undefined; + } const fallbackAssetPath = resolveManifestFallbackAssetPath( pathname, manifest, diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index d4eaff3b3c26..cbc236ca4048 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -539,4 +539,33 @@ describe('rsc-mf host modern.server middleware contracts', () => { expect(next).toHaveBeenCalledTimes(1); expect(context.res).toBeUndefined(); }); + + it('falls through when manifest response body is invalid JSON', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce(new Response('not-found', { status: 404 })) + .mockResolvedValueOnce( + new Response('not-json-manifest', { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }), + ), + ); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteServerCard.js', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(next).toHaveBeenCalledTimes(1); + expect(context.res).toBeUndefined(); + }); }); From 9b627099545a2a80bf4c44ea61c10fa2e77788e7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 09:57:22 +0000 Subject: [PATCH 236/324] test(rsc-mf): cover manifest-fetch failure passthrough --- .../rsc-mf/tests/modernServerConfig.test.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index cbc236ca4048..5c5a803ffe8f 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -568,4 +568,26 @@ describe('rsc-mf host modern.server middleware contracts', () => { expect(next).toHaveBeenCalledTimes(1); expect(context.res).toBeUndefined(); }); + + it('falls through when manifest request throws after stale asset miss', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce(new Response('not-found', { status: 404 })) + .mockRejectedValueOnce(new Error('manifest-fetch-failed')), + ); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteServerCard.js', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(next).toHaveBeenCalledTimes(1); + expect(context.res).toBeUndefined(); + }); }); From 8b96753dbdbe16703f4f449cd2a44df1d5679060 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 10:00:45 +0000 Subject: [PATCH 237/324] test(rsc-mf): extend manifest fallback query and async-asset coverage --- .../rsc-mf/tests/modernServerConfig.test.ts | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index 5c5a803ffe8f..5b0623c94fa8 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -590,4 +590,124 @@ describe('rsc-mf host modern.server middleware contracts', () => { expect(next).toHaveBeenCalledTimes(1); expect(context.res).toBeUndefined(); }); + + it('preserves query string when retrying manifest-resolved fallback asset', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce(new Response('not-found', { status: 404 })) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + exposes: [ + { + assets: { + js: { + sync: [ + 'static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ) + .mockResolvedValueOnce( + new Response('query-fallback-hit', { + status: 200, + headers: { + 'content-type': 'application/javascript', + }, + }), + ), + ); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteClientCounter.js?cache=1&v=2', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(fetchMock).toHaveBeenNthCalledWith( + 3, + 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js?cache=1&v=2', + ); + expect(next).not.toHaveBeenCalled(); + await expect(context.res?.text()).resolves.toBe('query-fallback-hit'); + }); + + it('resolves fallback asset paths from manifest async asset arrays', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce(new Response('not-found', { status: 404 })) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + shared: [ + { + assets: { + js: { + sync: [], + async: [ + 'static/js/async/__federation_expose_nestedActions.a8ce95b11a.js', + ], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ) + .mockResolvedValueOnce( + new Response('async-array-fallback-hit', { + status: 200, + headers: { + 'content-type': 'application/javascript', + }, + }), + ), + ); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_nestedActions.js', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(fetchMock).toHaveBeenNthCalledWith( + 3, + 'http://127.0.0.1:3999/static/js/async/__federation_expose_nestedActions.a8ce95b11a.js', + ); + expect(next).not.toHaveBeenCalled(); + await expect(context.res?.text()).resolves.toBe('async-array-fallback-hit'); + }); }); From 3a11926d4ca0dac83692f7e83f79bb01c153fc7f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 10:03:26 +0000 Subject: [PATCH 238/324] refactor(rsc-mf): skip manifest fallback for non-expose proxy paths --- .../rsc-mf/host/server/modern.server.ts | 13 ++++++---- .../rsc-mf/tests/modernServerConfig.test.ts | 24 +++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/tests/integration/rsc-mf/host/server/modern.server.ts b/tests/integration/rsc-mf/host/server/modern.server.ts index e8be793692af..00f59a4d30c2 100644 --- a/tests/integration/rsc-mf/host/server/modern.server.ts +++ b/tests/integration/rsc-mf/host/server/modern.server.ts @@ -24,6 +24,10 @@ interface RemoteManifestShape { exposes?: RemoteManifestAssetEntry[]; } +const isManifestFallbackEligiblePath = (pathname: string) => + pathname.includes('__federation_expose_') && + (pathname.endsWith('.js') || pathname.endsWith('.css')); + const toCanonicalChunkName = (filePath: string) => filePath .replace(/\/+$/, '') @@ -56,10 +60,7 @@ const resolveManifestFallbackAssetPath = ( pathname: string, manifest: RemoteManifestShape, ) => { - if ( - !pathname.includes('__federation_expose_') || - (!pathname.endsWith('.js') && !pathname.endsWith('.css')) - ) { + if (!isManifestFallbackEligiblePath(pathname)) { return undefined; } @@ -89,6 +90,10 @@ const fetchRemoteManifestFallbackAsset = async ({ pathname: string; search: string; }) => { + if (!isManifestFallbackEligiblePath(pathname)) { + return undefined; + } + const manifestResponse = await fetch(`${remoteOrigin}${REMOTE_MANIFEST_PATH}`) .then(response => { if (!response.ok) { diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index 5b0623c94fa8..892883366837 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -710,4 +710,28 @@ describe('rsc-mf host modern.server middleware contracts', () => { expect(next).not.toHaveBeenCalled(); await expect(context.res?.text()).resolves.toBe('async-array-fallback-hit'); }); + + it('does not attempt manifest fallback for non-expose marker chunk paths', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce(new Response('not-found', { status: 404 })), + ); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/503_react-server-components_0f2d4f91.js', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenCalledWith( + 'http://127.0.0.1:3999/static/js/async/503_react-server-components_0f2d4f91.js', + ); + expect(next).toHaveBeenCalledTimes(1); + expect(context.res).toBeUndefined(); + }); }); From 4321302d3759e07e3e978320c72ef83eb5e4a961 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 10:05:32 +0000 Subject: [PATCH 239/324] fix(rsc-mf): support alphanumeric fallback hash suffix matching --- .../rsc-mf/host/server/modern.server.ts | 2 +- .../rsc-mf/tests/modernServerConfig.test.ts | 62 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/tests/integration/rsc-mf/host/server/modern.server.ts b/tests/integration/rsc-mf/host/server/modern.server.ts index 00f59a4d30c2..f198171ee51b 100644 --- a/tests/integration/rsc-mf/host/server/modern.server.ts +++ b/tests/integration/rsc-mf/host/server/modern.server.ts @@ -4,7 +4,7 @@ import { } from '@modern-js/server-runtime'; const REMOTE_MANIFEST_PATH = '/static/mf-manifest.json'; -const EXPOSE_CHUNK_HASH_SUFFIX_PATTERN = /\.[a-f0-9]{6,}$/i; +const EXPOSE_CHUNK_HASH_SUFFIX_PATTERN = /\.[a-z0-9]{6,}$/i; interface RemoteManifestAssetEntry { assets?: { diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index 892883366837..d453823105e0 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -651,6 +651,68 @@ describe('rsc-mf host modern.server middleware contracts', () => { await expect(context.res?.text()).resolves.toBe('query-fallback-hit'); }); + it('matches fallback chunks when manifest hash suffix includes non-hex characters', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce(new Response('not-found', { status: 404 })) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + exposes: [ + { + assets: { + js: { + sync: [ + 'static/js/async/__federation_expose_RemoteServerCard.a1b2c3x9.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ) + .mockResolvedValueOnce( + new Response('non-hex-hash-fallback-hit', { + status: 200, + headers: { + 'content-type': 'application/javascript', + }, + }), + ), + ); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteServerCard.js', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(fetchMock).toHaveBeenNthCalledWith( + 3, + 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteServerCard.a1b2c3x9.js', + ); + expect(next).not.toHaveBeenCalled(); + await expect(context.res?.text()).resolves.toBe( + 'non-hex-hash-fallback-hit', + ); + }); + it('resolves fallback asset paths from manifest async asset arrays', async () => { const handler = getProxyMiddlewareHandler(); const next = jest.fn(async (): Promise => undefined); From 18a81935df3883f5c25df71ae79d06bcbbe4f5cc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 10:20:20 +0000 Subject: [PATCH 240/324] test(rsc-mf): harden manifest fallback URL resolution --- .../rsc-mf/host/server/modern.server.ts | 59 +++++++++- .../rsc-mf/tests/modernServerConfig.test.ts | 111 ++++++++++++++++++ 2 files changed, 167 insertions(+), 3 deletions(-) diff --git a/tests/integration/rsc-mf/host/server/modern.server.ts b/tests/integration/rsc-mf/host/server/modern.server.ts index f198171ee51b..7f8c701edb6e 100644 --- a/tests/integration/rsc-mf/host/server/modern.server.ts +++ b/tests/integration/rsc-mf/host/server/modern.server.ts @@ -36,6 +36,14 @@ const toCanonicalChunkName = (filePath: string) => ?.replace(/\.(js|css)$/i, '') .replace(EXPOSE_CHUNK_HASH_SUFFIX_PATTERN, ''); +const toNormalizedManifestAssetPath = (assetPath: string) => { + try { + return new URL(assetPath).pathname.replace(/^\/+/, ''); + } catch { + return assetPath.replace(/^[./]+/, '').split(/[?#]/, 1)[0]; + } +}; + const collectManifestAssetPaths = (manifest: RemoteManifestShape) => { const entries = [...(manifest.shared || []), ...(manifest.exposes || [])]; const assetPaths = new Set(); @@ -74,13 +82,51 @@ const resolveManifestFallbackAssetPath = ( : 'static/js/async/'; const manifestAssets = collectManifestAssetPaths(manifest); return manifestAssets.find(assetPath => { - if (!assetPath.startsWith(requestedAssetDirectory)) { + const normalizedAssetPath = toNormalizedManifestAssetPath(assetPath); + if (!normalizedAssetPath.startsWith(requestedAssetDirectory)) { return false; } - return toCanonicalChunkName(assetPath) === canonicalRequestedChunkName; + return ( + toCanonicalChunkName(normalizedAssetPath) === canonicalRequestedChunkName + ); }); }; +const createManifestFallbackAssetUrl = ({ + remoteOrigin, + fallbackAssetPath, + requestSearch, +}: { + remoteOrigin: string; + fallbackAssetPath: string; + requestSearch: string; +}) => { + let fallbackAssetUrl: URL; + try { + fallbackAssetUrl = new URL(fallbackAssetPath, `${remoteOrigin}/`); + } catch { + return undefined; + } + + if (fallbackAssetUrl.origin !== new URL(remoteOrigin).origin) { + return undefined; + } + + if (!requestSearch) { + return fallbackAssetUrl.toString(); + } + + const mergedSearchParams = new URLSearchParams(fallbackAssetUrl.search); + const requestSearchParams = new URLSearchParams(requestSearch); + for (const [key, value] of requestSearchParams.entries()) { + mergedSearchParams.set(key, value); + } + const mergedSearch = mergedSearchParams.toString(); + fallbackAssetUrl.search = mergedSearch ? `?${mergedSearch}` : ''; + + return fallbackAssetUrl.toString(); +}; + const fetchRemoteManifestFallbackAsset = async ({ remoteOrigin, pathname, @@ -121,7 +167,14 @@ const fetchRemoteManifestFallbackAsset = async ({ return undefined; } - const fallbackAssetUrl = `${remoteOrigin}/${fallbackAssetPath}${search}`; + const fallbackAssetUrl = createManifestFallbackAssetUrl({ + remoteOrigin, + fallbackAssetPath, + requestSearch: search, + }); + if (!fallbackAssetUrl) { + return undefined; + } const fallbackAssetResponse = await fetch(fallbackAssetUrl).catch( (): undefined => undefined, ); diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index d453823105e0..06045fe889d9 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -651,6 +651,117 @@ describe('rsc-mf host modern.server middleware contracts', () => { await expect(context.res?.text()).resolves.toBe('query-fallback-hit'); }); + it('supports absolute manifest fallback asset URLs and merges request query params', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce(new Response('not-found', { status: 404 })) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + exposes: [ + { + assets: { + js: { + sync: [ + 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js?manifest=1', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ) + .mockResolvedValueOnce( + new Response('absolute-query-fallback-hit', { + status: 200, + headers: { + 'content-type': 'application/javascript', + }, + }), + ), + ); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteClientCounter.js?cache=1', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(fetchMock).toHaveBeenNthCalledWith( + 3, + 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js?manifest=1&cache=1', + ); + expect(next).not.toHaveBeenCalled(); + await expect(context.res?.text()).resolves.toBe( + 'absolute-query-fallback-hit', + ); + }); + + it('falls through when manifest fallback asset URL points to another origin', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce(new Response('not-found', { status: 404 })) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + exposes: [ + { + assets: { + js: { + sync: [ + 'https://cdn.example.com/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ), + ); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteClientCounter.js', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(next).toHaveBeenCalledTimes(1); + expect(context.res).toBeUndefined(); + }); + it('matches fallback chunks when manifest hash suffix includes non-hex characters', async () => { const handler = getProxyMiddlewareHandler(); const next = jest.fn(async (): Promise => undefined); From 260278b5afae4fb26ac3dd19c0bccd63d978642f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 10:26:04 +0000 Subject: [PATCH 241/324] feat(rsc-mf): add remote expose-asset fallback middleware --- .../rsc-mf/remote/server/modern.server.ts | 216 ++++++++++++++++ .../tests/remoteModernServerConfig.test.ts | 244 ++++++++++++++++++ .../tests/types/modern-js-server-runtime.d.ts | 9 +- 3 files changed, 468 insertions(+), 1 deletion(-) create mode 100644 tests/integration/rsc-mf/remote/server/modern.server.ts create mode 100644 tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts diff --git a/tests/integration/rsc-mf/remote/server/modern.server.ts b/tests/integration/rsc-mf/remote/server/modern.server.ts new file mode 100644 index 000000000000..dd8263a7df3a --- /dev/null +++ b/tests/integration/rsc-mf/remote/server/modern.server.ts @@ -0,0 +1,216 @@ +import { + type MiddlewareHandler, + defineServerConfig, +} from '@modern-js/server-runtime'; + +const INTERNAL_FALLBACK_HEADER = 'x-rsc-mf-internal-fallback'; +const REMOTE_MANIFEST_PATH = '/static/mf-manifest.json'; +const EXPOSE_CHUNK_HASH_SUFFIX_PATTERN = /\.[a-z0-9]{6,}$/i; + +interface RemoteManifestAssetEntry { + assets?: { + js?: { + sync?: string[]; + async?: string[]; + }; + css?: { + sync?: string[]; + async?: string[]; + }; + }; +} + +interface RemoteManifestShape { + shared?: RemoteManifestAssetEntry[]; + exposes?: RemoteManifestAssetEntry[]; +} + +const isExposeAssetRequestPath = (pathname: string) => + pathname.includes('__federation_expose_') && + (pathname.endsWith('.js') || pathname.endsWith('.css')); + +const toCanonicalChunkName = (filePath: string) => + filePath + .replace(/\/+$/, '') + .split('/') + .pop() + ?.replace(/\.(js|css)$/i, '') + .replace(EXPOSE_CHUNK_HASH_SUFFIX_PATTERN, ''); + +const toNormalizedManifestAssetPath = (assetPath: string) => { + try { + return new URL(assetPath).pathname.replace(/^\/+/, ''); + } catch { + return assetPath.replace(/^[./]+/, '').split(/[?#]/, 1)[0]; + } +}; + +const collectManifestAssetPaths = (manifest: RemoteManifestShape) => { + const entries = [...(manifest.shared || []), ...(manifest.exposes || [])]; + const assetPaths = new Set(); + for (const entry of entries) { + const jsSyncAssets = entry.assets?.js?.sync || []; + const jsAsyncAssets = entry.assets?.js?.async || []; + const cssSyncAssets = entry.assets?.css?.sync || []; + const cssAsyncAssets = entry.assets?.css?.async || []; + for (const assetPath of [ + ...jsSyncAssets, + ...jsAsyncAssets, + ...cssSyncAssets, + ...cssAsyncAssets, + ]) { + assetPaths.add(assetPath); + } + } + return [...assetPaths]; +}; + +const resolveManifestFallbackAssetPath = ( + pathname: string, + manifest: RemoteManifestShape, +) => { + if (!isExposeAssetRequestPath(pathname)) { + return undefined; + } + + const canonicalRequestedChunkName = toCanonicalChunkName(pathname); + if (!canonicalRequestedChunkName) { + return undefined; + } + + const requestedAssetDirectory = pathname.includes('/static/css/async/') + ? 'static/css/async/' + : 'static/js/async/'; + const manifestAssets = collectManifestAssetPaths(manifest); + return manifestAssets.find(assetPath => { + const normalizedAssetPath = toNormalizedManifestAssetPath(assetPath); + if (!normalizedAssetPath.startsWith(requestedAssetDirectory)) { + return false; + } + return ( + toCanonicalChunkName(normalizedAssetPath) === canonicalRequestedChunkName + ); + }); +}; + +const createManifestFallbackAssetUrl = ({ + remoteOrigin, + fallbackAssetPath, + requestSearch, +}: { + remoteOrigin: string; + fallbackAssetPath: string; + requestSearch: string; +}) => { + let fallbackAssetUrl: URL; + try { + fallbackAssetUrl = new URL(fallbackAssetPath, `${remoteOrigin}/`); + } catch { + return undefined; + } + + if (fallbackAssetUrl.origin !== remoteOrigin) { + return undefined; + } + + if (!requestSearch) { + return fallbackAssetUrl.toString(); + } + + const mergedSearchParams = new URLSearchParams(fallbackAssetUrl.search); + const requestSearchParams = new URLSearchParams(requestSearch); + for (const [key, value] of requestSearchParams.entries()) { + mergedSearchParams.set(key, value); + } + const mergedSearch = mergedSearchParams.toString(); + fallbackAssetUrl.search = mergedSearch ? `?${mergedSearch}` : ''; + + return fallbackAssetUrl.toString(); +}; + +const recoverRemoteExposeAssetMiddleware: MiddlewareHandler = async ( + c, + next, +) => { + const reqUrl = new URL(c.req.url); + const pathname = reqUrl.pathname; + if (!isExposeAssetRequestPath(pathname)) { + await next(); + return; + } + + const requestHeaders = c.req.headers; + const isInternalFallbackFetch = + requestHeaders?.get?.(INTERNAL_FALLBACK_HEADER) === '1'; + if (isInternalFallbackFetch) { + await next(); + return; + } + + const remoteOrigin = reqUrl.origin; + const manifestResponse = await fetch( + `${remoteOrigin}${REMOTE_MANIFEST_PATH}`, + { + headers: { + [INTERNAL_FALLBACK_HEADER]: '1', + }, + }, + ).catch((): undefined => undefined); + if (!manifestResponse?.ok) { + await next(); + return; + } + + const manifest = (await manifestResponse + .json() + .catch((): undefined => undefined)) as RemoteManifestShape | undefined; + if (!manifest) { + await next(); + return; + } + + const fallbackAssetPath = resolveManifestFallbackAssetPath( + pathname, + manifest, + ); + if (!fallbackAssetPath) { + await next(); + return; + } + + const fallbackAssetUrl = createManifestFallbackAssetUrl({ + remoteOrigin, + fallbackAssetPath, + requestSearch: reqUrl.search, + }); + if (!fallbackAssetUrl || fallbackAssetUrl === reqUrl.toString()) { + await next(); + return; + } + + const fallbackAssetResponse = await fetch(fallbackAssetUrl, { + headers: { + [INTERNAL_FALLBACK_HEADER]: '1', + }, + }).catch((): undefined => undefined); + if (!fallbackAssetResponse?.ok) { + await next(); + return; + } + + c.res = new Response(await fallbackAssetResponse.arrayBuffer(), { + status: fallbackAssetResponse.status, + headers: fallbackAssetResponse.headers, + }); +}; + +export default defineServerConfig({ + middlewares: [ + { + name: 'recover-remote-federation-expose-asset', + handler: recoverRemoteExposeAssetMiddleware, + order: 'pre', + before: ['server-static'], + }, + ], +}); diff --git a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts new file mode 100644 index 000000000000..4e15989845f0 --- /dev/null +++ b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts @@ -0,0 +1,244 @@ +const REMOTE_SERVER_CONFIG_MODULE = '../remote/server/modern.server'; +const INTERNAL_FALLBACK_HEADER = 'x-rsc-mf-internal-fallback'; + +const loadRemoteServerConfig = () => { + jest.resetModules(); + jest.doMock('@modern-js/server-runtime', () => ({ + defineServerConfig: (config: unknown) => config, + })); + + let config: any; + jest.isolateModules(() => { + config = require(REMOTE_SERVER_CONFIG_MODULE).default; + }); + + return config; +}; + +const getRecoverMiddlewareHandler = () => { + const config = loadRemoteServerConfig(); + if (!Array.isArray(config.middlewares)) { + throw new Error('Remote server config did not provide a middlewares array'); + } + + const middleware = config.middlewares.find( + (entry: { name?: string }) => + entry.name === 'recover-remote-federation-expose-asset', + ); + if (!middleware) { + throw new Error( + 'recover-remote-federation-expose-asset middleware missing', + ); + } + + expect(middleware.order).toBe('pre'); + expect(middleware.before).toEqual(['server-static']); + expect(typeof middleware.handler).toBe('function'); + + return middleware.handler as ( + c: { + req: { url: string; headers?: { get?: (name: string) => string | null } }; + res?: Response; + }, + next: () => Promise, + ) => Promise; +}; + +describe('rsc-mf remote modern.server middleware contracts', () => { + const originalFetch = global.fetch; + const originalFetchDescriptor = Object.getOwnPropertyDescriptor( + global, + 'fetch', + ); + + const installFetchMock = (implementation: typeof fetch) => { + const fetchMock = jest.fn(implementation); + Object.defineProperty(global, 'fetch', { + value: fetchMock, + configurable: true, + writable: true, + }); + return fetchMock; + }; + + afterAll(() => { + if (originalFetchDescriptor) { + Object.defineProperty(global, 'fetch', originalFetchDescriptor); + return; + } + global.fetch = originalFetch; + }); + + it('recovers stale expose asset path via remote manifest fallback', async () => { + const handler = getRecoverMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + exposes: [ + { + assets: { + js: { + sync: [ + 'static/js/async/__federation_expose_RemoteServerCard.a1b2c3d4.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ) + .mockResolvedValueOnce( + new Response('fallback-asset', { + status: 200, + headers: { + 'content-type': 'application/javascript', + }, + }), + ), + ); + const context: { + req: { url: string; headers?: { get?: (name: string) => string | null } }; + res?: Response; + } = { + req: { + url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteServerCard.js?cache=1', + }, + }; + + await handler(context, next); + + expect(fetchMock).toHaveBeenNthCalledWith( + 1, + 'http://127.0.0.1:3008/static/mf-manifest.json', + { + headers: { + [INTERNAL_FALLBACK_HEADER]: '1', + }, + }, + ); + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteServerCard.a1b2c3d4.js?cache=1', + { + headers: { + [INTERNAL_FALLBACK_HEADER]: '1', + }, + }, + ); + expect(next).not.toHaveBeenCalled(); + await expect(context.res?.text()).resolves.toBe('fallback-asset'); + }); + + it('falls through when request path is not a federated expose asset', async () => { + const handler = getRecoverMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + async () => new Response('ignored', { status: 200 }), + ); + const context: { + req: { url: string; headers?: { get?: (name: string) => string | null } }; + res?: Response; + } = { + req: { + url: 'http://127.0.0.1:3008/static/js/async/743.32436c1247.js', + }, + }; + + await handler(context, next); + + expect(fetchMock).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalledTimes(1); + expect(context.res).toBeUndefined(); + }); + + it('skips fallback when request is marked as internal middleware fetch', async () => { + const handler = getRecoverMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + async () => new Response('ignored', { status: 200 }), + ); + const context: { + req: { url: string; headers?: { get?: (name: string) => string | null } }; + res?: Response; + } = { + req: { + url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteServerCard.js', + headers: { + get: (name: string) => + name === INTERNAL_FALLBACK_HEADER ? '1' : undefined, + }, + }, + }; + + await handler(context, next); + + expect(fetchMock).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalledTimes(1); + expect(context.res).toBeUndefined(); + }); + + it('falls through when manifest fallback asset resolves to another origin', async () => { + const handler = getRecoverMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest.fn().mockResolvedValueOnce( + new Response( + JSON.stringify({ + exposes: [ + { + assets: { + js: { + sync: [ + 'https://cdn.example.com/static/js/async/__federation_expose_RemoteServerCard.a1b2c3d4.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ), + ); + const context: { + req: { url: string; headers?: { get?: (name: string) => string | null } }; + res?: Response; + } = { + req: { + url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteServerCard.js', + }, + }; + + await handler(context, next); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(context.res).toBeUndefined(); + }); +}); diff --git a/tests/integration/rsc-mf/tests/types/modern-js-server-runtime.d.ts b/tests/integration/rsc-mf/tests/types/modern-js-server-runtime.d.ts index cd6fce867d72..0a386ef1c4f7 100644 --- a/tests/integration/rsc-mf/tests/types/modern-js-server-runtime.d.ts +++ b/tests/integration/rsc-mf/tests/types/modern-js-server-runtime.d.ts @@ -1,6 +1,13 @@ declare module '@modern-js/server-runtime' { + interface MiddlewareRequestLike { + url: string; + headers?: { + get?: (name: string) => string | null | undefined; + }; + } + export type MiddlewareHandler = ( - c: { req: { url: string }; res?: Response }, + c: { req: MiddlewareRequestLike; res?: Response }, next: () => Promise, ) => Promise | void; From cc65c7490aee250f678bf38ad7d3a40881c6af9e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 10:39:22 +0000 Subject: [PATCH 242/324] test(rsc-mf): expand remote manifest fallback contracts --- .../tests/remoteModernServerConfig.test.ts | 342 ++++++++++++++++++ 1 file changed, 342 insertions(+) diff --git a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts index 4e15989845f0..961f0208872e 100644 --- a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts @@ -241,4 +241,346 @@ describe('rsc-mf remote modern.server middleware contracts', () => { expect(next).toHaveBeenCalledTimes(1); expect(context.res).toBeUndefined(); }); + + it('recovers stale css expose assets via manifest fallback', async () => { + const handler = getRecoverMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + exposes: [ + { + assets: { + js: { + sync: [], + async: [], + }, + css: { + sync: [ + 'static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css', + ], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ) + .mockResolvedValueOnce( + new Response('.fallback-style{}', { + status: 200, + headers: { + 'content-type': 'text/css', + }, + }), + ), + ); + const context: { + req: { url: string; headers?: { get?: (name: string) => string | null } }; + res?: Response; + } = { + req: { + url: 'http://127.0.0.1:3008/static/css/async/__federation_expose_RemoteClientCounter.css?cache=1', + }, + }; + + await handler(context, next); + + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + 'http://127.0.0.1:3008/static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css?cache=1', + { + headers: { + [INTERNAL_FALLBACK_HEADER]: '1', + }, + }, + ); + expect(next).not.toHaveBeenCalled(); + await expect(context.res?.text()).resolves.toBe('.fallback-style{}'); + }); + + it('falls through when manifest response body is invalid json', async () => { + const handler = getRecoverMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest.fn().mockResolvedValueOnce( + new Response('not-json-manifest', { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }), + ), + ); + const context: { + req: { url: string; headers?: { get?: (name: string) => string | null } }; + res?: Response; + } = { + req: { + url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.js', + }, + }; + + await handler(context, next); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(context.res).toBeUndefined(); + }); + + it('falls through when manifest request throws', async () => { + const handler = getRecoverMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock(async () => { + throw new Error('manifest-fetch-failed'); + }); + const context: { + req: { url: string; headers?: { get?: (name: string) => string | null } }; + res?: Response; + } = { + req: { + url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.js', + }, + }; + + await handler(context, next); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(context.res).toBeUndefined(); + }); + + it('falls through when manifest fallback lookup has no canonical asset match', async () => { + const handler = getRecoverMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest.fn().mockResolvedValueOnce( + new Response( + JSON.stringify({ + exposes: [ + { + assets: { + js: { + sync: [ + 'static/js/async/__federation_expose_other.abc123.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ), + ); + const context: { + req: { url: string; headers?: { get?: (name: string) => string | null } }; + res?: Response; + } = { + req: { + url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.js', + }, + }; + + await handler(context, next); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(context.res).toBeUndefined(); + }); + + it('falls through when fallback asset fetch returns non-ok response', async () => { + const handler = getRecoverMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + exposes: [ + { + assets: { + js: { + sync: [ + 'static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ) + .mockResolvedValueOnce( + new Response('missing-fallback-asset', { + status: 404, + headers: { + 'content-type': 'text/plain', + }, + }), + ), + ); + const context: { + req: { url: string; headers?: { get?: (name: string) => string | null } }; + res?: Response; + } = { + req: { + url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.js', + }, + }; + + await handler(context, next); + + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(next).toHaveBeenCalledTimes(1); + expect(context.res).toBeUndefined(); + }); + + it('merges request query params into absolute same-origin manifest fallback assets', async () => { + const handler = getRecoverMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + shared: [ + { + assets: { + js: { + sync: [ + 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js?manifest=1', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ) + .mockResolvedValueOnce( + new Response('absolute-fallback-asset', { + status: 200, + headers: { + 'content-type': 'application/javascript', + }, + }), + ), + ); + const context: { + req: { url: string; headers?: { get?: (name: string) => string | null } }; + res?: Response; + } = { + req: { + url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.js?cache=1', + }, + }; + + await handler(context, next); + + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js?manifest=1&cache=1', + { + headers: { + [INTERNAL_FALLBACK_HEADER]: '1', + }, + }, + ); + expect(next).not.toHaveBeenCalled(); + await expect(context.res?.text()).resolves.toBe('absolute-fallback-asset'); + }); + + it('falls through when fallback asset resolves to the same request url', async () => { + const handler = getRecoverMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest.fn().mockResolvedValueOnce( + new Response( + JSON.stringify({ + exposes: [ + { + assets: { + js: { + sync: [ + 'static/js/async/__federation_expose_RemoteClientCounter.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ), + ); + const context: { + req: { url: string; headers?: { get?: (name: string) => string | null } }; + res?: Response; + } = { + req: { + url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.js', + }, + }; + + await handler(context, next); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(context.res).toBeUndefined(); + }); }); From 42251e928f781c905343ebd74b172bca29c10aa4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 10:43:00 +0000 Subject: [PATCH 243/324] fix(rsc-mf): avoid host fallback refetching same expose URL --- .../rsc-mf/host/server/modern.server.ts | 6 +++ .../rsc-mf/tests/modernServerConfig.test.ts | 49 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/tests/integration/rsc-mf/host/server/modern.server.ts b/tests/integration/rsc-mf/host/server/modern.server.ts index 7f8c701edb6e..e2d3d6b16b41 100644 --- a/tests/integration/rsc-mf/host/server/modern.server.ts +++ b/tests/integration/rsc-mf/host/server/modern.server.ts @@ -131,10 +131,12 @@ const fetchRemoteManifestFallbackAsset = async ({ remoteOrigin, pathname, search, + requestUrl, }: { remoteOrigin: string; pathname: string; search: string; + requestUrl: string; }) => { if (!isManifestFallbackEligiblePath(pathname)) { return undefined; @@ -175,6 +177,9 @@ const fetchRemoteManifestFallbackAsset = async ({ if (!fallbackAssetUrl) { return undefined; } + if (fallbackAssetUrl === requestUrl) { + return undefined; + } const fallbackAssetResponse = await fetch(fallbackAssetUrl).catch( (): undefined => undefined, ); @@ -225,6 +230,7 @@ const proxyRemoteFederationAsset: MiddlewareHandler = async (c, next) => { remoteOrigin, pathname, search: reqUrl.search, + requestUrl: remoteUrl, }); if (!resolvedUpstream || !resolvedUpstream.ok) { diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index 06045fe889d9..5e4114f68f6f 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -762,6 +762,55 @@ describe('rsc-mf host modern.server middleware contracts', () => { expect(context.res).toBeUndefined(); }); + it('falls through when manifest fallback resolves to original request URL', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce(new Response('not-found', { status: 404 })) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + exposes: [ + { + assets: { + js: { + sync: [ + 'static/js/async/__federation_expose_RemoteClientCounter.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ), + ); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteClientCounter.js', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(next).toHaveBeenCalledTimes(1); + expect(context.res).toBeUndefined(); + }); + it('matches fallback chunks when manifest hash suffix includes non-hex characters', async () => { const handler = getProxyMiddlewareHandler(); const next = jest.fn(async (): Promise => undefined); From cdf5a36e48eb8f5a50416ed2287521f5e9bd12ae Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 10:46:21 +0000 Subject: [PATCH 244/324] fix(rsc-mf): block fallback path traversal escapes --- .../rsc-mf/host/server/modern.server.ts | 19 +++++-- .../rsc-mf/remote/server/modern.server.ts | 19 +++++-- .../rsc-mf/tests/modernServerConfig.test.ts | 49 +++++++++++++++++++ .../tests/remoteModernServerConfig.test.ts | 49 +++++++++++++++++++ 4 files changed, 130 insertions(+), 6 deletions(-) diff --git a/tests/integration/rsc-mf/host/server/modern.server.ts b/tests/integration/rsc-mf/host/server/modern.server.ts index e2d3d6b16b41..66f39aa09edb 100644 --- a/tests/integration/rsc-mf/host/server/modern.server.ts +++ b/tests/integration/rsc-mf/host/server/modern.server.ts @@ -28,6 +28,11 @@ const isManifestFallbackEligiblePath = (pathname: string) => pathname.includes('__federation_expose_') && (pathname.endsWith('.js') || pathname.endsWith('.css')); +const getRequestedAssetDirectory = (pathname: string) => + pathname.includes('/static/css/async/') + ? 'static/css/async/' + : 'static/js/async/'; + const toCanonicalChunkName = (filePath: string) => filePath .replace(/\/+$/, '') @@ -77,9 +82,7 @@ const resolveManifestFallbackAssetPath = ( return undefined; } - const requestedAssetDirectory = pathname.includes('/static/css/async/') - ? 'static/css/async/' - : 'static/js/async/'; + const requestedAssetDirectory = getRequestedAssetDirectory(pathname); const manifestAssets = collectManifestAssetPaths(manifest); return manifestAssets.find(assetPath => { const normalizedAssetPath = toNormalizedManifestAssetPath(assetPath); @@ -96,10 +99,12 @@ const createManifestFallbackAssetUrl = ({ remoteOrigin, fallbackAssetPath, requestSearch, + requestedAssetDirectory, }: { remoteOrigin: string; fallbackAssetPath: string; requestSearch: string; + requestedAssetDirectory: string; }) => { let fallbackAssetUrl: URL; try { @@ -111,6 +116,13 @@ const createManifestFallbackAssetUrl = ({ if (fallbackAssetUrl.origin !== new URL(remoteOrigin).origin) { return undefined; } + const normalizedFallbackPathname = fallbackAssetUrl.pathname.replace( + /^\/+/, + '', + ); + if (!normalizedFallbackPathname.startsWith(requestedAssetDirectory)) { + return undefined; + } if (!requestSearch) { return fallbackAssetUrl.toString(); @@ -173,6 +185,7 @@ const fetchRemoteManifestFallbackAsset = async ({ remoteOrigin, fallbackAssetPath, requestSearch: search, + requestedAssetDirectory: getRequestedAssetDirectory(pathname), }); if (!fallbackAssetUrl) { return undefined; diff --git a/tests/integration/rsc-mf/remote/server/modern.server.ts b/tests/integration/rsc-mf/remote/server/modern.server.ts index dd8263a7df3a..9b1109dd6eb0 100644 --- a/tests/integration/rsc-mf/remote/server/modern.server.ts +++ b/tests/integration/rsc-mf/remote/server/modern.server.ts @@ -29,6 +29,11 @@ const isExposeAssetRequestPath = (pathname: string) => pathname.includes('__federation_expose_') && (pathname.endsWith('.js') || pathname.endsWith('.css')); +const getRequestedAssetDirectory = (pathname: string) => + pathname.includes('/static/css/async/') + ? 'static/css/async/' + : 'static/js/async/'; + const toCanonicalChunkName = (filePath: string) => filePath .replace(/\/+$/, '') @@ -78,9 +83,7 @@ const resolveManifestFallbackAssetPath = ( return undefined; } - const requestedAssetDirectory = pathname.includes('/static/css/async/') - ? 'static/css/async/' - : 'static/js/async/'; + const requestedAssetDirectory = getRequestedAssetDirectory(pathname); const manifestAssets = collectManifestAssetPaths(manifest); return manifestAssets.find(assetPath => { const normalizedAssetPath = toNormalizedManifestAssetPath(assetPath); @@ -97,10 +100,12 @@ const createManifestFallbackAssetUrl = ({ remoteOrigin, fallbackAssetPath, requestSearch, + requestedAssetDirectory, }: { remoteOrigin: string; fallbackAssetPath: string; requestSearch: string; + requestedAssetDirectory: string; }) => { let fallbackAssetUrl: URL; try { @@ -112,6 +117,13 @@ const createManifestFallbackAssetUrl = ({ if (fallbackAssetUrl.origin !== remoteOrigin) { return undefined; } + const normalizedFallbackPathname = fallbackAssetUrl.pathname.replace( + /^\/+/, + '', + ); + if (!normalizedFallbackPathname.startsWith(requestedAssetDirectory)) { + return undefined; + } if (!requestSearch) { return fallbackAssetUrl.toString(); @@ -182,6 +194,7 @@ const recoverRemoteExposeAssetMiddleware: MiddlewareHandler = async ( remoteOrigin, fallbackAssetPath, requestSearch: reqUrl.search, + requestedAssetDirectory: getRequestedAssetDirectory(pathname), }); if (!fallbackAssetUrl || fallbackAssetUrl === reqUrl.toString()) { await next(); diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index 5e4114f68f6f..079122f9bdb4 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -811,6 +811,55 @@ describe('rsc-mf host modern.server middleware contracts', () => { expect(context.res).toBeUndefined(); }); + it('falls through when manifest fallback path escapes async asset directory', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce(new Response('not-found', { status: 404 })) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + shared: [ + { + assets: { + js: { + sync: [ + 'static/js/async/../__federation_expose_RemoteClientCounter.7745fe5f0a.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ), + ); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteClientCounter.js', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(next).toHaveBeenCalledTimes(1); + expect(context.res).toBeUndefined(); + }); + it('matches fallback chunks when manifest hash suffix includes non-hex characters', async () => { const handler = getProxyMiddlewareHandler(); const next = jest.fn(async (): Promise => undefined); diff --git a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts index 961f0208872e..cbb2f3b6e751 100644 --- a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts @@ -583,4 +583,53 @@ describe('rsc-mf remote modern.server middleware contracts', () => { expect(next).toHaveBeenCalledTimes(1); expect(context.res).toBeUndefined(); }); + + it('falls through when manifest fallback path escapes async asset directory', async () => { + const handler = getRecoverMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest.fn().mockResolvedValueOnce( + new Response( + JSON.stringify({ + shared: [ + { + assets: { + js: { + sync: [ + 'static/js/async/../__federation_expose_RemoteClientCounter.7745fe5f0a.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ), + ); + const context: { + req: { url: string; headers?: { get?: (name: string) => string | null } }; + res?: Response; + } = { + req: { + url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.js', + }, + }; + + await handler(context, next); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(context.res).toBeUndefined(); + }); }); From 52e1eeb9d964671981cd49951fa6388115a8fed2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 10:47:58 +0000 Subject: [PATCH 245/324] test(rsc-mf): broaden remote fallback matching coverage --- .../tests/remoteModernServerConfig.test.ts | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts index cbb2f3b6e751..de3eb85de537 100644 --- a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts @@ -632,4 +632,138 @@ describe('rsc-mf remote modern.server middleware contracts', () => { expect(next).toHaveBeenCalledTimes(1); expect(context.res).toBeUndefined(); }); + + it('resolves fallback asset paths from manifest async asset arrays', async () => { + const handler = getRecoverMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + shared: [ + { + assets: { + js: { + sync: [], + async: [ + 'static/js/async/__federation_expose_nestedActions.a8ce95b11a.js', + ], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ) + .mockResolvedValueOnce( + new Response('async-array-fallback-hit', { + status: 200, + headers: { + 'content-type': 'application/javascript', + }, + }), + ), + ); + const context: { + req: { url: string; headers?: { get?: (name: string) => string | null } }; + res?: Response; + } = { + req: { + url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_nestedActions.js', + }, + }; + + await handler(context, next); + + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + 'http://127.0.0.1:3008/static/js/async/__federation_expose_nestedActions.a8ce95b11a.js', + { + headers: { + [INTERNAL_FALLBACK_HEADER]: '1', + }, + }, + ); + expect(next).not.toHaveBeenCalled(); + await expect(context.res?.text()).resolves.toBe('async-array-fallback-hit'); + }); + + it('matches fallback chunks with non-hex hash suffixes', async () => { + const handler = getRecoverMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + exposes: [ + { + assets: { + js: { + sync: [ + 'static/js/async/__federation_expose_RemoteServerCard.a1b2c3x9.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ) + .mockResolvedValueOnce( + new Response('non-hex-fallback-hit', { + status: 200, + headers: { + 'content-type': 'application/javascript', + }, + }), + ), + ); + const context: { + req: { url: string; headers?: { get?: (name: string) => string | null } }; + res?: Response; + } = { + req: { + url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteServerCard.js', + }, + }; + + await handler(context, next); + + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteServerCard.a1b2c3x9.js', + { + headers: { + [INTERNAL_FALLBACK_HEADER]: '1', + }, + }, + ); + expect(next).not.toHaveBeenCalled(); + await expect(context.res?.text()).resolves.toBe('non-hex-fallback-hit'); + }); }); From fb1b47f7fcbcc2c1ebf618d6689c1db621848dc7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 10:51:11 +0000 Subject: [PATCH 246/324] refactor(rsc-mf): share manifest fallback helpers --- .../rsc-mf/host/server/modern.server.ts | 147 ++---------------- .../rsc-mf/remote/server/modern.server.ts | 144 ++--------------- .../rsc-mf/shared/manifestFallback.ts | 139 +++++++++++++++++ 3 files changed, 157 insertions(+), 273 deletions(-) create mode 100644 tests/integration/rsc-mf/shared/manifestFallback.ts diff --git a/tests/integration/rsc-mf/host/server/modern.server.ts b/tests/integration/rsc-mf/host/server/modern.server.ts index 66f39aa09edb..39dbeaa9cb9b 100644 --- a/tests/integration/rsc-mf/host/server/modern.server.ts +++ b/tests/integration/rsc-mf/host/server/modern.server.ts @@ -2,142 +2,15 @@ import { type MiddlewareHandler, defineServerConfig, } from '@modern-js/server-runtime'; +import { + type RemoteManifestShape, + createManifestFallbackAssetUrl, + getRequestedAssetDirectory, + isExposeAssetRequestPath, + resolveManifestFallbackAssetPath, +} from '../../shared/manifestFallback'; const REMOTE_MANIFEST_PATH = '/static/mf-manifest.json'; -const EXPOSE_CHUNK_HASH_SUFFIX_PATTERN = /\.[a-z0-9]{6,}$/i; - -interface RemoteManifestAssetEntry { - assets?: { - js?: { - sync?: string[]; - async?: string[]; - }; - css?: { - sync?: string[]; - async?: string[]; - }; - }; -} - -interface RemoteManifestShape { - shared?: RemoteManifestAssetEntry[]; - exposes?: RemoteManifestAssetEntry[]; -} - -const isManifestFallbackEligiblePath = (pathname: string) => - pathname.includes('__federation_expose_') && - (pathname.endsWith('.js') || pathname.endsWith('.css')); - -const getRequestedAssetDirectory = (pathname: string) => - pathname.includes('/static/css/async/') - ? 'static/css/async/' - : 'static/js/async/'; - -const toCanonicalChunkName = (filePath: string) => - filePath - .replace(/\/+$/, '') - .split('/') - .pop() - ?.replace(/\.(js|css)$/i, '') - .replace(EXPOSE_CHUNK_HASH_SUFFIX_PATTERN, ''); - -const toNormalizedManifestAssetPath = (assetPath: string) => { - try { - return new URL(assetPath).pathname.replace(/^\/+/, ''); - } catch { - return assetPath.replace(/^[./]+/, '').split(/[?#]/, 1)[0]; - } -}; - -const collectManifestAssetPaths = (manifest: RemoteManifestShape) => { - const entries = [...(manifest.shared || []), ...(manifest.exposes || [])]; - const assetPaths = new Set(); - for (const entry of entries) { - const jsSyncAssets = entry.assets?.js?.sync || []; - const jsAsyncAssets = entry.assets?.js?.async || []; - const cssSyncAssets = entry.assets?.css?.sync || []; - const cssAsyncAssets = entry.assets?.css?.async || []; - for (const assetPath of [ - ...jsSyncAssets, - ...jsAsyncAssets, - ...cssSyncAssets, - ...cssAsyncAssets, - ]) { - assetPaths.add(assetPath); - } - } - return [...assetPaths]; -}; - -const resolveManifestFallbackAssetPath = ( - pathname: string, - manifest: RemoteManifestShape, -) => { - if (!isManifestFallbackEligiblePath(pathname)) { - return undefined; - } - - const canonicalRequestedChunkName = toCanonicalChunkName(pathname); - if (!canonicalRequestedChunkName) { - return undefined; - } - - const requestedAssetDirectory = getRequestedAssetDirectory(pathname); - const manifestAssets = collectManifestAssetPaths(manifest); - return manifestAssets.find(assetPath => { - const normalizedAssetPath = toNormalizedManifestAssetPath(assetPath); - if (!normalizedAssetPath.startsWith(requestedAssetDirectory)) { - return false; - } - return ( - toCanonicalChunkName(normalizedAssetPath) === canonicalRequestedChunkName - ); - }); -}; - -const createManifestFallbackAssetUrl = ({ - remoteOrigin, - fallbackAssetPath, - requestSearch, - requestedAssetDirectory, -}: { - remoteOrigin: string; - fallbackAssetPath: string; - requestSearch: string; - requestedAssetDirectory: string; -}) => { - let fallbackAssetUrl: URL; - try { - fallbackAssetUrl = new URL(fallbackAssetPath, `${remoteOrigin}/`); - } catch { - return undefined; - } - - if (fallbackAssetUrl.origin !== new URL(remoteOrigin).origin) { - return undefined; - } - const normalizedFallbackPathname = fallbackAssetUrl.pathname.replace( - /^\/+/, - '', - ); - if (!normalizedFallbackPathname.startsWith(requestedAssetDirectory)) { - return undefined; - } - - if (!requestSearch) { - return fallbackAssetUrl.toString(); - } - - const mergedSearchParams = new URLSearchParams(fallbackAssetUrl.search); - const requestSearchParams = new URLSearchParams(requestSearch); - for (const [key, value] of requestSearchParams.entries()) { - mergedSearchParams.set(key, value); - } - const mergedSearch = mergedSearchParams.toString(); - fallbackAssetUrl.search = mergedSearch ? `?${mergedSearch}` : ''; - - return fallbackAssetUrl.toString(); -}; const fetchRemoteManifestFallbackAsset = async ({ remoteOrigin, @@ -150,7 +23,7 @@ const fetchRemoteManifestFallbackAsset = async ({ search: string; requestUrl: string; }) => { - if (!isManifestFallbackEligiblePath(pathname)) { + if (!isExposeAssetRequestPath(pathname)) { return undefined; } @@ -186,13 +59,11 @@ const fetchRemoteManifestFallbackAsset = async ({ fallbackAssetPath, requestSearch: search, requestedAssetDirectory: getRequestedAssetDirectory(pathname), + requestUrl, }); if (!fallbackAssetUrl) { return undefined; } - if (fallbackAssetUrl === requestUrl) { - return undefined; - } const fallbackAssetResponse = await fetch(fallbackAssetUrl).catch( (): undefined => undefined, ); diff --git a/tests/integration/rsc-mf/remote/server/modern.server.ts b/tests/integration/rsc-mf/remote/server/modern.server.ts index 9b1109dd6eb0..e4a8a1800e7d 100644 --- a/tests/integration/rsc-mf/remote/server/modern.server.ts +++ b/tests/integration/rsc-mf/remote/server/modern.server.ts @@ -2,143 +2,16 @@ import { type MiddlewareHandler, defineServerConfig, } from '@modern-js/server-runtime'; +import { + type RemoteManifestShape, + createManifestFallbackAssetUrl, + getRequestedAssetDirectory, + isExposeAssetRequestPath, + resolveManifestFallbackAssetPath, +} from '../../shared/manifestFallback'; const INTERNAL_FALLBACK_HEADER = 'x-rsc-mf-internal-fallback'; const REMOTE_MANIFEST_PATH = '/static/mf-manifest.json'; -const EXPOSE_CHUNK_HASH_SUFFIX_PATTERN = /\.[a-z0-9]{6,}$/i; - -interface RemoteManifestAssetEntry { - assets?: { - js?: { - sync?: string[]; - async?: string[]; - }; - css?: { - sync?: string[]; - async?: string[]; - }; - }; -} - -interface RemoteManifestShape { - shared?: RemoteManifestAssetEntry[]; - exposes?: RemoteManifestAssetEntry[]; -} - -const isExposeAssetRequestPath = (pathname: string) => - pathname.includes('__federation_expose_') && - (pathname.endsWith('.js') || pathname.endsWith('.css')); - -const getRequestedAssetDirectory = (pathname: string) => - pathname.includes('/static/css/async/') - ? 'static/css/async/' - : 'static/js/async/'; - -const toCanonicalChunkName = (filePath: string) => - filePath - .replace(/\/+$/, '') - .split('/') - .pop() - ?.replace(/\.(js|css)$/i, '') - .replace(EXPOSE_CHUNK_HASH_SUFFIX_PATTERN, ''); - -const toNormalizedManifestAssetPath = (assetPath: string) => { - try { - return new URL(assetPath).pathname.replace(/^\/+/, ''); - } catch { - return assetPath.replace(/^[./]+/, '').split(/[?#]/, 1)[0]; - } -}; - -const collectManifestAssetPaths = (manifest: RemoteManifestShape) => { - const entries = [...(manifest.shared || []), ...(manifest.exposes || [])]; - const assetPaths = new Set(); - for (const entry of entries) { - const jsSyncAssets = entry.assets?.js?.sync || []; - const jsAsyncAssets = entry.assets?.js?.async || []; - const cssSyncAssets = entry.assets?.css?.sync || []; - const cssAsyncAssets = entry.assets?.css?.async || []; - for (const assetPath of [ - ...jsSyncAssets, - ...jsAsyncAssets, - ...cssSyncAssets, - ...cssAsyncAssets, - ]) { - assetPaths.add(assetPath); - } - } - return [...assetPaths]; -}; - -const resolveManifestFallbackAssetPath = ( - pathname: string, - manifest: RemoteManifestShape, -) => { - if (!isExposeAssetRequestPath(pathname)) { - return undefined; - } - - const canonicalRequestedChunkName = toCanonicalChunkName(pathname); - if (!canonicalRequestedChunkName) { - return undefined; - } - - const requestedAssetDirectory = getRequestedAssetDirectory(pathname); - const manifestAssets = collectManifestAssetPaths(manifest); - return manifestAssets.find(assetPath => { - const normalizedAssetPath = toNormalizedManifestAssetPath(assetPath); - if (!normalizedAssetPath.startsWith(requestedAssetDirectory)) { - return false; - } - return ( - toCanonicalChunkName(normalizedAssetPath) === canonicalRequestedChunkName - ); - }); -}; - -const createManifestFallbackAssetUrl = ({ - remoteOrigin, - fallbackAssetPath, - requestSearch, - requestedAssetDirectory, -}: { - remoteOrigin: string; - fallbackAssetPath: string; - requestSearch: string; - requestedAssetDirectory: string; -}) => { - let fallbackAssetUrl: URL; - try { - fallbackAssetUrl = new URL(fallbackAssetPath, `${remoteOrigin}/`); - } catch { - return undefined; - } - - if (fallbackAssetUrl.origin !== remoteOrigin) { - return undefined; - } - const normalizedFallbackPathname = fallbackAssetUrl.pathname.replace( - /^\/+/, - '', - ); - if (!normalizedFallbackPathname.startsWith(requestedAssetDirectory)) { - return undefined; - } - - if (!requestSearch) { - return fallbackAssetUrl.toString(); - } - - const mergedSearchParams = new URLSearchParams(fallbackAssetUrl.search); - const requestSearchParams = new URLSearchParams(requestSearch); - for (const [key, value] of requestSearchParams.entries()) { - mergedSearchParams.set(key, value); - } - const mergedSearch = mergedSearchParams.toString(); - fallbackAssetUrl.search = mergedSearch ? `?${mergedSearch}` : ''; - - return fallbackAssetUrl.toString(); -}; const recoverRemoteExposeAssetMiddleware: MiddlewareHandler = async ( c, @@ -195,8 +68,9 @@ const recoverRemoteExposeAssetMiddleware: MiddlewareHandler = async ( fallbackAssetPath, requestSearch: reqUrl.search, requestedAssetDirectory: getRequestedAssetDirectory(pathname), + requestUrl: reqUrl.toString(), }); - if (!fallbackAssetUrl || fallbackAssetUrl === reqUrl.toString()) { + if (!fallbackAssetUrl) { await next(); return; } diff --git a/tests/integration/rsc-mf/shared/manifestFallback.ts b/tests/integration/rsc-mf/shared/manifestFallback.ts new file mode 100644 index 000000000000..c0bcee580e14 --- /dev/null +++ b/tests/integration/rsc-mf/shared/manifestFallback.ts @@ -0,0 +1,139 @@ +const EXPOSE_CHUNK_HASH_SUFFIX_PATTERN = /\.[a-z0-9]{6,}$/i; + +export interface RemoteManifestAssetEntry { + assets?: { + js?: { + sync?: string[]; + async?: string[]; + }; + css?: { + sync?: string[]; + async?: string[]; + }; + }; +} + +export interface RemoteManifestShape { + shared?: RemoteManifestAssetEntry[]; + exposes?: RemoteManifestAssetEntry[]; +} + +export const isExposeAssetRequestPath = (pathname: string) => + pathname.includes('__federation_expose_') && + (pathname.endsWith('.js') || pathname.endsWith('.css')); + +export const getRequestedAssetDirectory = (pathname: string) => + pathname.includes('/static/css/async/') + ? 'static/css/async/' + : 'static/js/async/'; + +const toCanonicalChunkName = (filePath: string) => + filePath + .replace(/\/+$/, '') + .split('/') + .pop() + ?.replace(/\.(js|css)$/i, '') + .replace(EXPOSE_CHUNK_HASH_SUFFIX_PATTERN, ''); + +const toNormalizedManifestAssetPath = (assetPath: string) => { + try { + return new URL(assetPath).pathname.replace(/^\/+/, ''); + } catch { + return assetPath.replace(/^[./]+/, '').split(/[?#]/, 1)[0]; + } +}; + +const collectManifestAssetPaths = (manifest: RemoteManifestShape) => { + const entries = [...(manifest.shared || []), ...(manifest.exposes || [])]; + const assetPaths = new Set(); + for (const entry of entries) { + const jsSyncAssets = entry.assets?.js?.sync || []; + const jsAsyncAssets = entry.assets?.js?.async || []; + const cssSyncAssets = entry.assets?.css?.sync || []; + const cssAsyncAssets = entry.assets?.css?.async || []; + for (const assetPath of [ + ...jsSyncAssets, + ...jsAsyncAssets, + ...cssSyncAssets, + ...cssAsyncAssets, + ]) { + assetPaths.add(assetPath); + } + } + return [...assetPaths]; +}; + +export const resolveManifestFallbackAssetPath = ( + pathname: string, + manifest: RemoteManifestShape, +) => { + if (!isExposeAssetRequestPath(pathname)) { + return undefined; + } + + const canonicalRequestedChunkName = toCanonicalChunkName(pathname); + if (!canonicalRequestedChunkName) { + return undefined; + } + + const requestedAssetDirectory = getRequestedAssetDirectory(pathname); + const manifestAssets = collectManifestAssetPaths(manifest); + return manifestAssets.find(assetPath => { + const normalizedAssetPath = toNormalizedManifestAssetPath(assetPath); + if (!normalizedAssetPath.startsWith(requestedAssetDirectory)) { + return false; + } + return ( + toCanonicalChunkName(normalizedAssetPath) === canonicalRequestedChunkName + ); + }); +}; + +export const createManifestFallbackAssetUrl = ({ + remoteOrigin, + fallbackAssetPath, + requestSearch, + requestedAssetDirectory, + requestUrl, +}: { + remoteOrigin: string; + fallbackAssetPath: string; + requestSearch: string; + requestedAssetDirectory: string; + requestUrl?: string; +}) => { + let fallbackAssetUrl: URL; + try { + fallbackAssetUrl = new URL(fallbackAssetPath, `${remoteOrigin}/`); + } catch { + return undefined; + } + + if (fallbackAssetUrl.origin !== new URL(remoteOrigin).origin) { + return undefined; + } + const normalizedFallbackPathname = fallbackAssetUrl.pathname.replace( + /^\/+/, + '', + ); + if (!normalizedFallbackPathname.startsWith(requestedAssetDirectory)) { + return undefined; + } + + if (requestSearch) { + const mergedSearchParams = new URLSearchParams(fallbackAssetUrl.search); + const requestSearchParams = new URLSearchParams(requestSearch); + for (const [key, value] of requestSearchParams.entries()) { + mergedSearchParams.set(key, value); + } + const mergedSearch = mergedSearchParams.toString(); + fallbackAssetUrl.search = mergedSearch ? `?${mergedSearch}` : ''; + } + + const resolvedFallbackAssetUrl = fallbackAssetUrl.toString(); + if (requestUrl && resolvedFallbackAssetUrl === requestUrl) { + return undefined; + } + + return resolvedFallbackAssetUrl; +}; From 101d5cfaba894ce0ed430ddbbb6b81ef95cbde42 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 10:53:08 +0000 Subject: [PATCH 247/324] test(rsc-mf): add shared manifest fallback helper coverage --- .../rsc-mf/tests/manifestFallback.test.ts | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 tests/integration/rsc-mf/tests/manifestFallback.test.ts diff --git a/tests/integration/rsc-mf/tests/manifestFallback.test.ts b/tests/integration/rsc-mf/tests/manifestFallback.test.ts new file mode 100644 index 000000000000..f58788eb2b95 --- /dev/null +++ b/tests/integration/rsc-mf/tests/manifestFallback.test.ts @@ -0,0 +1,172 @@ +import { + type RemoteManifestShape, + createManifestFallbackAssetUrl, + getRequestedAssetDirectory, + isExposeAssetRequestPath, + resolveManifestFallbackAssetPath, +} from '../shared/manifestFallback'; + +describe('manifest fallback shared helpers', () => { + it('identifies expose asset request paths', () => { + expect( + isExposeAssetRequestPath( + '/static/js/async/__federation_expose_RemoteClientCounter.js', + ), + ).toBe(true); + expect( + isExposeAssetRequestPath( + '/static/css/async/__federation_expose_RemoteClientCounter.css', + ), + ).toBe(true); + expect(isExposeAssetRequestPath('/static/js/async/743.32436c1247.js')).toBe( + false, + ); + }); + + it('derives requested async asset directory from pathname', () => { + expect( + getRequestedAssetDirectory( + '/static/js/async/__federation_expose_RemoteClientCounter.js', + ), + ).toBe('static/js/async/'); + expect( + getRequestedAssetDirectory( + '/static/css/async/__federation_expose_RemoteClientCounter.css', + ), + ).toBe('static/css/async/'); + }); + + it('resolves fallback assets from shared and exposes manifest entries', () => { + const manifest: RemoteManifestShape = { + shared: [ + { + assets: { + js: { + sync: [ + 'static/js/async/__federation_expose_actions.44d8f1d7ae.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + exposes: [ + { + assets: { + js: { + sync: [ + 'static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }; + + expect( + resolveManifestFallbackAssetPath( + '/static/js/async/__federation_expose_RemoteClientCounter.js', + manifest, + ), + ).toBe( + 'static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', + ); + expect( + resolveManifestFallbackAssetPath( + '/static/js/async/__federation_expose_actions.js', + manifest, + ), + ).toBe('static/js/async/__federation_expose_actions.44d8f1d7ae.js'); + }); + + it('supports absolute manifest assets and rejects non-matching paths', () => { + const manifest: RemoteManifestShape = { + shared: [ + { + assets: { + js: { + sync: [ + 'http://127.0.0.1:3999/static/js/async/__federation_expose_nestedActions.a8ce95b11a.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }; + + expect( + resolveManifestFallbackAssetPath( + '/static/js/async/__federation_expose_nestedActions.js', + manifest, + ), + ).toBe( + 'http://127.0.0.1:3999/static/js/async/__federation_expose_nestedActions.a8ce95b11a.js', + ); + expect( + resolveManifestFallbackAssetPath( + '/static/js/async/not-an-expose.js', + manifest, + ), + ).toBeUndefined(); + }); + + it('builds safe fallback URL and merges request query params', () => { + expect( + createManifestFallbackAssetUrl({ + remoteOrigin: 'http://127.0.0.1:3999', + fallbackAssetPath: + 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js?manifest=1', + requestSearch: '?cache=1', + requestedAssetDirectory: 'static/js/async/', + }), + ).toBe( + 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js?manifest=1&cache=1', + ); + }); + + it('rejects unsafe fallback URLs', () => { + expect( + createManifestFallbackAssetUrl({ + remoteOrigin: 'http://127.0.0.1:3999', + fallbackAssetPath: + 'https://cdn.example.com/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', + requestSearch: '', + requestedAssetDirectory: 'static/js/async/', + }), + ).toBeUndefined(); + expect( + createManifestFallbackAssetUrl({ + remoteOrigin: 'http://127.0.0.1:3999', + fallbackAssetPath: + 'static/js/async/../__federation_expose_RemoteClientCounter.7745fe5f0a.js', + requestSearch: '', + requestedAssetDirectory: 'static/js/async/', + }), + ).toBeUndefined(); + expect( + createManifestFallbackAssetUrl({ + remoteOrigin: 'http://127.0.0.1:3999', + fallbackAssetPath: + 'static/js/async/__federation_expose_RemoteClientCounter.js', + requestSearch: '', + requestedAssetDirectory: 'static/js/async/', + requestUrl: + 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.js', + }), + ).toBeUndefined(); + }); +}); From 80084f97efb678ac147865a96fb2a1f4a4ab4879 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 11:07:32 +0000 Subject: [PATCH 248/324] fix(rsc-mf): mark host fallback fetches as internal --- .../rsc-mf/host/server/modern.server.ts | 18 +++++-- .../rsc-mf/remote/server/modern.server.ts | 2 +- .../rsc-mf/shared/manifestFallback.ts | 1 + .../rsc-mf/tests/modernServerConfig.test.ts | 53 ++++++++++++++----- .../tests/remoteModernServerConfig.test.ts | 3 +- 5 files changed, 58 insertions(+), 19 deletions(-) diff --git a/tests/integration/rsc-mf/host/server/modern.server.ts b/tests/integration/rsc-mf/host/server/modern.server.ts index 39dbeaa9cb9b..080ba37958b3 100644 --- a/tests/integration/rsc-mf/host/server/modern.server.ts +++ b/tests/integration/rsc-mf/host/server/modern.server.ts @@ -3,6 +3,7 @@ import { defineServerConfig, } from '@modern-js/server-runtime'; import { + INTERNAL_FALLBACK_HEADER, type RemoteManifestShape, createManifestFallbackAssetUrl, getRequestedAssetDirectory, @@ -27,7 +28,14 @@ const fetchRemoteManifestFallbackAsset = async ({ return undefined; } - const manifestResponse = await fetch(`${remoteOrigin}${REMOTE_MANIFEST_PATH}`) + const manifestResponse = await fetch( + `${remoteOrigin}${REMOTE_MANIFEST_PATH}`, + { + headers: { + [INTERNAL_FALLBACK_HEADER]: '1', + }, + }, + ) .then(response => { if (!response.ok) { return undefined; @@ -64,9 +72,11 @@ const fetchRemoteManifestFallbackAsset = async ({ if (!fallbackAssetUrl) { return undefined; } - const fallbackAssetResponse = await fetch(fallbackAssetUrl).catch( - (): undefined => undefined, - ); + const fallbackAssetResponse = await fetch(fallbackAssetUrl, { + headers: { + [INTERNAL_FALLBACK_HEADER]: '1', + }, + }).catch((): undefined => undefined); if (!fallbackAssetResponse || !fallbackAssetResponse.ok) { return undefined; } diff --git a/tests/integration/rsc-mf/remote/server/modern.server.ts b/tests/integration/rsc-mf/remote/server/modern.server.ts index e4a8a1800e7d..7de52cba9f2d 100644 --- a/tests/integration/rsc-mf/remote/server/modern.server.ts +++ b/tests/integration/rsc-mf/remote/server/modern.server.ts @@ -3,6 +3,7 @@ import { defineServerConfig, } from '@modern-js/server-runtime'; import { + INTERNAL_FALLBACK_HEADER, type RemoteManifestShape, createManifestFallbackAssetUrl, getRequestedAssetDirectory, @@ -10,7 +11,6 @@ import { resolveManifestFallbackAssetPath, } from '../../shared/manifestFallback'; -const INTERNAL_FALLBACK_HEADER = 'x-rsc-mf-internal-fallback'; const REMOTE_MANIFEST_PATH = '/static/mf-manifest.json'; const recoverRemoteExposeAssetMiddleware: MiddlewareHandler = async ( diff --git a/tests/integration/rsc-mf/shared/manifestFallback.ts b/tests/integration/rsc-mf/shared/manifestFallback.ts index c0bcee580e14..245201981bbb 100644 --- a/tests/integration/rsc-mf/shared/manifestFallback.ts +++ b/tests/integration/rsc-mf/shared/manifestFallback.ts @@ -1,4 +1,5 @@ const EXPOSE_CHUNK_HASH_SUFFIX_PATTERN = /\.[a-z0-9]{6,}$/i; +export const INTERNAL_FALLBACK_HEADER = 'x-rsc-mf-internal-fallback'; export interface RemoteManifestAssetEntry { assets?: { diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index 079122f9bdb4..e9081df0feee 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -1,3 +1,5 @@ +import { INTERNAL_FALLBACK_HEADER } from '../shared/manifestFallback'; + const HOST_SERVER_CONFIG_MODULE = '../host/server/modern.server'; const withRemotePort = (remotePort: string | undefined, run: () => T): T => { @@ -65,6 +67,18 @@ const getProxyMiddlewareHandler = () => { ) => Promise; }; +const expectInternalFallbackFetchCall = ( + fetchMock: jest.Mock, + nthCall: number, + url: string, +) => { + expect(fetchMock).toHaveBeenNthCalledWith(nthCall, url, { + headers: { + [INTERNAL_FALLBACK_HEADER]: '1', + }, + }); +}; + describe('rsc-mf host modern.server middleware contracts', () => { const originalFetch = global.fetch; const originalFetchDescriptor = Object.getOwnPropertyDescriptor( @@ -248,7 +262,8 @@ describe('rsc-mf host modern.server middleware contracts', () => { await withRemotePort('3999', () => handler(context, next)); expect(fetchMock).toHaveBeenCalledTimes(2); - expect(fetchMock).toHaveBeenNthCalledWith( + expectInternalFallbackFetchCall( + fetchMock, 2, 'http://127.0.0.1:3999/static/mf-manifest.json', ); @@ -271,7 +286,8 @@ describe('rsc-mf host modern.server middleware contracts', () => { await withRemotePort('3999', () => handler(context, next)); expect(fetchMock).toHaveBeenCalledTimes(2); - expect(fetchMock).toHaveBeenNthCalledWith( + expectInternalFallbackFetchCall( + fetchMock, 2, 'http://127.0.0.1:3999/static/mf-manifest.json', ); @@ -335,11 +351,13 @@ describe('rsc-mf host modern.server middleware contracts', () => { 1, 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteServerCard.js', ); - expect(fetchMock).toHaveBeenNthCalledWith( + expectInternalFallbackFetchCall( + fetchMock, 2, 'http://127.0.0.1:3999/static/mf-manifest.json', ); - expect(fetchMock).toHaveBeenNthCalledWith( + expectInternalFallbackFetchCall( + fetchMock, 3, 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteServerCard.6e997e54ed.js', ); @@ -403,11 +421,13 @@ describe('rsc-mf host modern.server middleware contracts', () => { 1, 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.js', ); - expect(fetchMock).toHaveBeenNthCalledWith( + expectInternalFallbackFetchCall( + fetchMock, 2, 'http://127.0.0.1:3999/static/mf-manifest.json', ); - expect(fetchMock).toHaveBeenNthCalledWith( + expectInternalFallbackFetchCall( + fetchMock, 3, 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', ); @@ -471,11 +491,13 @@ describe('rsc-mf host modern.server middleware contracts', () => { 1, 'http://127.0.0.1:3999/static/css/async/__federation_expose_RemoteClientCounter.css', ); - expect(fetchMock).toHaveBeenNthCalledWith( + expectInternalFallbackFetchCall( + fetchMock, 2, 'http://127.0.0.1:3999/static/mf-manifest.json', ); - expect(fetchMock).toHaveBeenNthCalledWith( + expectInternalFallbackFetchCall( + fetchMock, 3, 'http://127.0.0.1:3999/static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css', ); @@ -532,7 +554,8 @@ describe('rsc-mf host modern.server middleware contracts', () => { 1, 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteServerCard.js', ); - expect(fetchMock).toHaveBeenNthCalledWith( + expectInternalFallbackFetchCall( + fetchMock, 2, 'http://127.0.0.1:3999/static/mf-manifest.json', ); @@ -643,7 +666,8 @@ describe('rsc-mf host modern.server middleware contracts', () => { await withRemotePort('3999', () => handler(context, next)); - expect(fetchMock).toHaveBeenNthCalledWith( + expectInternalFallbackFetchCall( + fetchMock, 3, 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js?cache=1&v=2', ); @@ -703,7 +727,8 @@ describe('rsc-mf host modern.server middleware contracts', () => { await withRemotePort('3999', () => handler(context, next)); - expect(fetchMock).toHaveBeenNthCalledWith( + expectInternalFallbackFetchCall( + fetchMock, 3, 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js?manifest=1&cache=1', ); @@ -912,7 +937,8 @@ describe('rsc-mf host modern.server middleware contracts', () => { await withRemotePort('3999', () => handler(context, next)); - expect(fetchMock).toHaveBeenNthCalledWith( + expectInternalFallbackFetchCall( + fetchMock, 3, 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteServerCard.a1b2c3x9.js', ); @@ -974,7 +1000,8 @@ describe('rsc-mf host modern.server middleware contracts', () => { await withRemotePort('3999', () => handler(context, next)); - expect(fetchMock).toHaveBeenNthCalledWith( + expectInternalFallbackFetchCall( + fetchMock, 3, 'http://127.0.0.1:3999/static/js/async/__federation_expose_nestedActions.a8ce95b11a.js', ); diff --git a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts index de3eb85de537..0dd55a43312f 100644 --- a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts @@ -1,5 +1,6 @@ +import { INTERNAL_FALLBACK_HEADER } from '../shared/manifestFallback'; + const REMOTE_SERVER_CONFIG_MODULE = '../remote/server/modern.server'; -const INTERNAL_FALLBACK_HEADER = 'x-rsc-mf-internal-fallback'; const loadRemoteServerConfig = () => { jest.resetModules(); From 7fc15bf64f77f025b86129ae1c8e4ca14d32c02d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 11:12:06 +0000 Subject: [PATCH 249/324] fix(rsc-mf): skip host proxy on internal fallback requests --- .../rsc-mf/host/server/modern.server.ts | 8 +++++ .../rsc-mf/tests/modernServerConfig.test.ts | 31 ++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/tests/integration/rsc-mf/host/server/modern.server.ts b/tests/integration/rsc-mf/host/server/modern.server.ts index 080ba37958b3..92cdfbd2355d 100644 --- a/tests/integration/rsc-mf/host/server/modern.server.ts +++ b/tests/integration/rsc-mf/host/server/modern.server.ts @@ -100,6 +100,14 @@ const shouldProxyRemoteAsset = (pathname: string) => { }; const proxyRemoteFederationAsset: MiddlewareHandler = async (c, next) => { + const requestHeaders = c.req.headers; + const isInternalFallbackFetch = + requestHeaders?.get?.(INTERNAL_FALLBACK_HEADER) === '1'; + if (isInternalFallbackFetch) { + await next(); + return; + } + const reqUrl = new URL(c.req.url); const pathname = reqUrl.pathname; diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index e9081df0feee..96b61417c735 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -62,7 +62,10 @@ const getProxyMiddlewareHandler = () => { expect(middleware.before).toEqual(['server-static']); expect(typeof middleware.handler).toBe('function'); return middleware.handler as ( - c: { req: { url: string }; res?: Response }, + c: { + req: { url: string; headers?: { get?: (name: string) => string | null } }; + res?: Response; + }, next: () => Promise, ) => Promise; }; @@ -228,6 +231,32 @@ describe('rsc-mf host modern.server middleware contracts', () => { expect(context.res).toBeUndefined(); }); + it('skips proxying when request is marked as internal fallback fetch', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + async () => new Response('ignored', { status: 200 }), + ); + const context: { + req: { url: string; headers?: { get?: (name: string) => string | null } }; + res?: Response; + } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_actions.44d8f1d7ae.js', + headers: { + get: (name: string) => + name === INTERNAL_FALLBACK_HEADER ? '1' : undefined, + }, + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(fetchMock).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalledTimes(1); + expect(context.res).toBeUndefined(); + }); + it('falls through when remote port is not configured', async () => { const handler = getProxyMiddlewareHandler(); const next = jest.fn(async (): Promise => undefined); From 2d115bfdc5c309648ca2385a20ccd4531a43259c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 11:15:48 +0000 Subject: [PATCH 250/324] fix(rsc-mf): guard fallback helper against invalid origins --- tests/integration/rsc-mf/shared/manifestFallback.ts | 11 +++++++++-- .../integration/rsc-mf/tests/manifestFallback.test.ts | 9 +++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/integration/rsc-mf/shared/manifestFallback.ts b/tests/integration/rsc-mf/shared/manifestFallback.ts index 245201981bbb..7528afa194a2 100644 --- a/tests/integration/rsc-mf/shared/manifestFallback.ts +++ b/tests/integration/rsc-mf/shared/manifestFallback.ts @@ -103,14 +103,21 @@ export const createManifestFallbackAssetUrl = ({ requestedAssetDirectory: string; requestUrl?: string; }) => { + let remoteOriginUrl: URL; + try { + remoteOriginUrl = new URL(remoteOrigin); + } catch { + return undefined; + } + let fallbackAssetUrl: URL; try { - fallbackAssetUrl = new URL(fallbackAssetPath, `${remoteOrigin}/`); + fallbackAssetUrl = new URL(fallbackAssetPath, `${remoteOriginUrl.origin}/`); } catch { return undefined; } - if (fallbackAssetUrl.origin !== new URL(remoteOrigin).origin) { + if (fallbackAssetUrl.origin !== remoteOriginUrl.origin) { return undefined; } const normalizedFallbackPathname = fallbackAssetUrl.pathname.replace( diff --git a/tests/integration/rsc-mf/tests/manifestFallback.test.ts b/tests/integration/rsc-mf/tests/manifestFallback.test.ts index f58788eb2b95..2a328c6d011b 100644 --- a/tests/integration/rsc-mf/tests/manifestFallback.test.ts +++ b/tests/integration/rsc-mf/tests/manifestFallback.test.ts @@ -139,6 +139,15 @@ describe('manifest fallback shared helpers', () => { }); it('rejects unsafe fallback URLs', () => { + expect( + createManifestFallbackAssetUrl({ + remoteOrigin: 'not-a-valid-origin', + fallbackAssetPath: + 'static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', + requestSearch: '', + requestedAssetDirectory: 'static/js/async/', + }), + ).toBeUndefined(); expect( createManifestFallbackAssetUrl({ remoteOrigin: 'http://127.0.0.1:3999', From a9551bcc5d6eebe1a36ca6243fb54ed2d1272c8a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 11:19:23 +0000 Subject: [PATCH 251/324] test(rsc-mf): cover stale hashed expose fallback paths --- .../rsc-mf/tests/manifestFallback.test.ts | 30 ++++++++ .../rsc-mf/tests/modernServerConfig.test.ts | 70 +++++++++++++++++++ .../tests/remoteModernServerConfig.test.ts | 67 ++++++++++++++++++ 3 files changed, 167 insertions(+) diff --git a/tests/integration/rsc-mf/tests/manifestFallback.test.ts b/tests/integration/rsc-mf/tests/manifestFallback.test.ts index 2a328c6d011b..50e33e1ed486 100644 --- a/tests/integration/rsc-mf/tests/manifestFallback.test.ts +++ b/tests/integration/rsc-mf/tests/manifestFallback.test.ts @@ -124,6 +124,36 @@ describe('manifest fallback shared helpers', () => { ).toBeUndefined(); }); + it('resolves stale hashed expose requests to current hashed assets', () => { + const manifest: RemoteManifestShape = { + exposes: [ + { + assets: { + js: { + sync: [ + 'static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }; + + expect( + resolveManifestFallbackAssetPath( + '/static/js/async/__federation_expose_RemoteClientCounter.deadbeef12.js', + manifest, + ), + ).toBe( + 'static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', + ); + }); + it('builds safe fallback URL and merges request query params', () => { expect( createManifestFallbackAssetUrl({ diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index 96b61417c735..5ae95b8157e6 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -394,6 +394,76 @@ describe('rsc-mf host modern.server middleware contracts', () => { await expect(context.res?.text()).resolves.toBe('fallback-hit'); }); + it('recovers stale hashed expose chunk path via manifest-driven fallback', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce(new Response('not-found', { status: 404 })) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + exposes: [ + { + assets: { + js: { + sync: [ + 'static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ) + .mockResolvedValueOnce( + new Response('hashed-fallback-hit', { + status: 200, + headers: { + 'content-type': 'application/javascript', + }, + }), + ), + ); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteClientCounter.deadbeef12.js', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(fetchMock).toHaveBeenNthCalledWith( + 1, + 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.deadbeef12.js', + ); + expectInternalFallbackFetchCall( + fetchMock, + 2, + 'http://127.0.0.1:3999/static/mf-manifest.json', + ); + expectInternalFallbackFetchCall( + fetchMock, + 3, + 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', + ); + expect(next).not.toHaveBeenCalled(); + await expect(context.res?.text()).resolves.toBe('hashed-fallback-hit'); + }); + it('recovers stale expose path when manifest match is under shared assets', async () => { const handler = getProxyMiddlewareHandler(); const next = jest.fn(async (): Promise => undefined); diff --git a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts index 0dd55a43312f..07e837de97b4 100644 --- a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts @@ -146,6 +146,73 @@ describe('rsc-mf remote modern.server middleware contracts', () => { await expect(context.res?.text()).resolves.toBe('fallback-asset'); }); + it('recovers stale hashed expose asset path via remote manifest fallback', async () => { + const handler = getRecoverMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + exposes: [ + { + assets: { + js: { + sync: [ + 'static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ) + .mockResolvedValueOnce( + new Response('hashed-fallback-asset', { + status: 200, + headers: { + 'content-type': 'application/javascript', + }, + }), + ), + ); + const context: { + req: { url: string; headers?: { get?: (name: string) => string | null } }; + res?: Response; + } = { + req: { + url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.deadbeef12.js', + }, + }; + + await handler(context, next); + + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', + { + headers: { + [INTERNAL_FALLBACK_HEADER]: '1', + }, + }, + ); + expect(next).not.toHaveBeenCalled(); + await expect(context.res?.text()).resolves.toBe('hashed-fallback-asset'); + }); + it('falls through when request path is not a federated expose asset', async () => { const handler = getRecoverMiddlewareHandler(); const next = jest.fn(async (): Promise => undefined); From ff31a2f3eadc44a4debb2321dc58316514ff7561 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 11:23:25 +0000 Subject: [PATCH 252/324] fix(rsc-mf): reject encoded fallback path traversal --- .../rsc-mf/shared/manifestFallback.ts | 11 ++++- .../rsc-mf/tests/manifestFallback.test.ts | 9 ++++ .../rsc-mf/tests/modernServerConfig.test.ts | 49 +++++++++++++++++++ .../tests/remoteModernServerConfig.test.ts | 49 +++++++++++++++++++ 4 files changed, 117 insertions(+), 1 deletion(-) diff --git a/tests/integration/rsc-mf/shared/manifestFallback.ts b/tests/integration/rsc-mf/shared/manifestFallback.ts index 7528afa194a2..e2bc67183b6c 100644 --- a/tests/integration/rsc-mf/shared/manifestFallback.ts +++ b/tests/integration/rsc-mf/shared/manifestFallback.ts @@ -124,7 +124,16 @@ export const createManifestFallbackAssetUrl = ({ /^\/+/, '', ); - if (!normalizedFallbackPathname.startsWith(requestedAssetDirectory)) { + let decodedFallbackPathname: string; + try { + decodedFallbackPathname = decodeURIComponent(normalizedFallbackPathname); + } catch { + return undefined; + } + if (!decodedFallbackPathname.startsWith(requestedAssetDirectory)) { + return undefined; + } + if (decodedFallbackPathname.split('/').some(segment => segment === '..')) { return undefined; } diff --git a/tests/integration/rsc-mf/tests/manifestFallback.test.ts b/tests/integration/rsc-mf/tests/manifestFallback.test.ts index 50e33e1ed486..57cc5831ba63 100644 --- a/tests/integration/rsc-mf/tests/manifestFallback.test.ts +++ b/tests/integration/rsc-mf/tests/manifestFallback.test.ts @@ -196,6 +196,15 @@ describe('manifest fallback shared helpers', () => { requestedAssetDirectory: 'static/js/async/', }), ).toBeUndefined(); + expect( + createManifestFallbackAssetUrl({ + remoteOrigin: 'http://127.0.0.1:3999', + fallbackAssetPath: + 'static/js/async/%2e%2e/__federation_expose_RemoteClientCounter.7745fe5f0a.js', + requestSearch: '', + requestedAssetDirectory: 'static/js/async/', + }), + ).toBeUndefined(); expect( createManifestFallbackAssetUrl({ remoteOrigin: 'http://127.0.0.1:3999', diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index 5ae95b8157e6..4a8ca9777e9b 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -984,6 +984,55 @@ describe('rsc-mf host modern.server middleware contracts', () => { expect(context.res).toBeUndefined(); }); + it('falls through when manifest fallback path uses encoded async-directory escape', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce(new Response('not-found', { status: 404 })) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + shared: [ + { + assets: { + js: { + sync: [ + 'static/js/async/%2e%2e/__federation_expose_RemoteClientCounter.7745fe5f0a.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ), + ); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteClientCounter.js', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(next).toHaveBeenCalledTimes(1); + expect(context.res).toBeUndefined(); + }); + it('matches fallback chunks when manifest hash suffix includes non-hex characters', async () => { const handler = getProxyMiddlewareHandler(); const next = jest.fn(async (): Promise => undefined); diff --git a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts index 07e837de97b4..988bbfc4ed6e 100644 --- a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts @@ -701,6 +701,55 @@ describe('rsc-mf remote modern.server middleware contracts', () => { expect(context.res).toBeUndefined(); }); + it('falls through when manifest fallback path uses encoded async-directory escape', async () => { + const handler = getRecoverMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest.fn().mockResolvedValueOnce( + new Response( + JSON.stringify({ + shared: [ + { + assets: { + js: { + sync: [ + 'static/js/async/%2e%2e/__federation_expose_RemoteClientCounter.7745fe5f0a.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ), + ); + const context: { + req: { url: string; headers?: { get?: (name: string) => string | null } }; + res?: Response; + } = { + req: { + url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.js', + }, + }; + + await handler(context, next); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(context.res).toBeUndefined(); + }); + it('resolves fallback asset paths from manifest async asset arrays', async () => { const handler = getRecoverMiddlewareHandler(); const next = jest.fn(async (): Promise => undefined); From 9dd6b4eb0f06c34179fbc5ae5e8870ef2707b783 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 11:27:32 +0000 Subject: [PATCH 253/324] test(rsc-mf): cover stale-hashed css fallback paths --- .../rsc-mf/tests/manifestFallback.test.ts | 39 +++++++++++ .../rsc-mf/tests/modernServerConfig.test.ts | 70 +++++++++++++++++++ .../tests/remoteModernServerConfig.test.ts | 67 ++++++++++++++++++ 3 files changed, 176 insertions(+) diff --git a/tests/integration/rsc-mf/tests/manifestFallback.test.ts b/tests/integration/rsc-mf/tests/manifestFallback.test.ts index 57cc5831ba63..2d3c4f51c1c2 100644 --- a/tests/integration/rsc-mf/tests/manifestFallback.test.ts +++ b/tests/integration/rsc-mf/tests/manifestFallback.test.ts @@ -154,6 +154,36 @@ describe('manifest fallback shared helpers', () => { ); }); + it('resolves stale hashed css expose requests to current hashed css assets', () => { + const manifest: RemoteManifestShape = { + exposes: [ + { + assets: { + js: { + sync: [], + async: [], + }, + css: { + sync: [ + 'static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css', + ], + async: [], + }, + }, + }, + ], + }; + + expect( + resolveManifestFallbackAssetPath( + '/static/css/async/__federation_expose_RemoteClientCounter.deadbeef12.css', + manifest, + ), + ).toBe( + 'static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css', + ); + }); + it('builds safe fallback URL and merges request query params', () => { expect( createManifestFallbackAssetUrl({ @@ -205,6 +235,15 @@ describe('manifest fallback shared helpers', () => { requestedAssetDirectory: 'static/js/async/', }), ).toBeUndefined(); + expect( + createManifestFallbackAssetUrl({ + remoteOrigin: 'http://127.0.0.1:3999', + fallbackAssetPath: + 'static/js/async/%E0%A4%A/__federation_expose_RemoteClientCounter.7745fe5f0a.js', + requestSearch: '', + requestedAssetDirectory: 'static/js/async/', + }), + ).toBeUndefined(); expect( createManifestFallbackAssetUrl({ remoteOrigin: 'http://127.0.0.1:3999', diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index 4a8ca9777e9b..752fa740c623 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -604,6 +604,76 @@ describe('rsc-mf host modern.server middleware contracts', () => { await expect(context.res?.text()).resolves.toBe('.fallback-style{}'); }); + it('recovers stale hashed CSS expose path via manifest-driven fallback', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce(new Response('not-found', { status: 404 })) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + exposes: [ + { + assets: { + js: { + sync: [], + async: [], + }, + css: { + sync: [ + 'static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css', + ], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ) + .mockResolvedValueOnce( + new Response('.hashed-fallback-style{}', { + status: 200, + headers: { + 'content-type': 'text/css', + }, + }), + ), + ); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/css/async/__federation_expose_RemoteClientCounter.deadbeef12.css', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(fetchMock).toHaveBeenNthCalledWith( + 1, + 'http://127.0.0.1:3999/static/css/async/__federation_expose_RemoteClientCounter.deadbeef12.css', + ); + expectInternalFallbackFetchCall( + fetchMock, + 2, + 'http://127.0.0.1:3999/static/mf-manifest.json', + ); + expectInternalFallbackFetchCall( + fetchMock, + 3, + 'http://127.0.0.1:3999/static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css', + ); + expect(next).not.toHaveBeenCalled(); + await expect(context.res?.text()).resolves.toBe('.hashed-fallback-style{}'); + }); + it('falls through when manifest lookup has no matching fallback asset', async () => { const handler = getProxyMiddlewareHandler(); const next = jest.fn(async (): Promise => undefined); diff --git a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts index 988bbfc4ed6e..94000e851e54 100644 --- a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts @@ -377,6 +377,73 @@ describe('rsc-mf remote modern.server middleware contracts', () => { await expect(context.res?.text()).resolves.toBe('.fallback-style{}'); }); + it('recovers stale hashed css expose assets via manifest fallback', async () => { + const handler = getRecoverMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + exposes: [ + { + assets: { + js: { + sync: [], + async: [], + }, + css: { + sync: [ + 'static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css', + ], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ) + .mockResolvedValueOnce( + new Response('.hashed-fallback-style{}', { + status: 200, + headers: { + 'content-type': 'text/css', + }, + }), + ), + ); + const context: { + req: { url: string; headers?: { get?: (name: string) => string | null } }; + res?: Response; + } = { + req: { + url: 'http://127.0.0.1:3008/static/css/async/__federation_expose_RemoteClientCounter.deadbeef12.css?cache=1', + }, + }; + + await handler(context, next); + + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + 'http://127.0.0.1:3008/static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css?cache=1', + { + headers: { + [INTERNAL_FALLBACK_HEADER]: '1', + }, + }, + ); + expect(next).not.toHaveBeenCalled(); + await expect(context.res?.text()).resolves.toBe('.hashed-fallback-style{}'); + }); + it('falls through when manifest response body is invalid json', async () => { const handler = getRecoverMiddlewareHandler(); const next = jest.fn(async (): Promise => undefined); From 34077844143fc198fd2b734fe925722fc8dd3b92 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 11:36:32 +0000 Subject: [PATCH 254/324] test(rsc-mf): cover malformed percent fallback paths --- .../rsc-mf/tests/modernServerConfig.test.ts | 49 +++++++++++++++++++ .../tests/remoteModernServerConfig.test.ts | 49 +++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index 752fa740c623..aceae2eab8d4 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -1103,6 +1103,55 @@ describe('rsc-mf host modern.server middleware contracts', () => { expect(context.res).toBeUndefined(); }); + it('falls through when manifest fallback path has invalid percent encoding', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce(new Response('not-found', { status: 404 })) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + shared: [ + { + assets: { + js: { + sync: [ + 'static/js/async/%E0%A4%A/__federation_expose_RemoteClientCounter.7745fe5f0a.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ), + ); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteClientCounter.js', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(next).toHaveBeenCalledTimes(1); + expect(context.res).toBeUndefined(); + }); + it('matches fallback chunks when manifest hash suffix includes non-hex characters', async () => { const handler = getProxyMiddlewareHandler(); const next = jest.fn(async (): Promise => undefined); diff --git a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts index 94000e851e54..8c76c5fb5c62 100644 --- a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts @@ -817,6 +817,55 @@ describe('rsc-mf remote modern.server middleware contracts', () => { expect(context.res).toBeUndefined(); }); + it('falls through when manifest fallback path has invalid percent encoding', async () => { + const handler = getRecoverMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest.fn().mockResolvedValueOnce( + new Response( + JSON.stringify({ + shared: [ + { + assets: { + js: { + sync: [ + 'static/js/async/%E0%A4%A/__federation_expose_RemoteClientCounter.7745fe5f0a.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ), + ); + const context: { + req: { url: string; headers?: { get?: (name: string) => string | null } }; + res?: Response; + } = { + req: { + url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.js', + }, + }; + + await handler(context, next); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(context.res).toBeUndefined(); + }); + it('resolves fallback asset paths from manifest async asset arrays', async () => { const handler = getRecoverMiddlewareHandler(); const next = jest.fn(async (): Promise => undefined); From 4dcb3f9e300e992751e2952ad396bf0c5e4e28ea Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 11:41:32 +0000 Subject: [PATCH 255/324] test: cover manifest query override precedence --- .../rsc-mf/tests/manifestFallback.test.ts | 15 ++++ .../rsc-mf/tests/modernServerConfig.test.ts | 67 ++++++++++++++++++ .../tests/remoteModernServerConfig.test.ts | 69 +++++++++++++++++++ 3 files changed, 151 insertions(+) diff --git a/tests/integration/rsc-mf/tests/manifestFallback.test.ts b/tests/integration/rsc-mf/tests/manifestFallback.test.ts index 2d3c4f51c1c2..2765ae79557e 100644 --- a/tests/integration/rsc-mf/tests/manifestFallback.test.ts +++ b/tests/integration/rsc-mf/tests/manifestFallback.test.ts @@ -198,6 +198,21 @@ describe('manifest fallback shared helpers', () => { ); }); + it('lets request query params override manifest query params', () => { + const fallbackUrl = createManifestFallbackAssetUrl({ + remoteOrigin: 'http://127.0.0.1:3999', + fallbackAssetPath: + 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js?cache=manifest&v=1', + requestSearch: '?cache=request&x=2', + requestedAssetDirectory: 'static/js/async/', + }); + expect(fallbackUrl).toBeDefined(); + const parsedFallbackUrl = new URL(fallbackUrl!); + expect(parsedFallbackUrl.searchParams.get('cache')).toBe('request'); + expect(parsedFallbackUrl.searchParams.get('v')).toBe('1'); + expect(parsedFallbackUrl.searchParams.get('x')).toBe('2'); + }); + it('rejects unsafe fallback URLs', () => { expect( createManifestFallbackAssetUrl({ diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index aceae2eab8d4..2496631918de 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -907,6 +907,73 @@ describe('rsc-mf host modern.server middleware contracts', () => { ); }); + it('lets request query params override manifest fallback query params', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce(new Response('not-found', { status: 404 })) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + exposes: [ + { + assets: { + js: { + sync: [ + 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js?cache=manifest&v=1', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ) + .mockResolvedValueOnce( + new Response('query-override-fallback-hit', { + status: 200, + headers: { + 'content-type': 'application/javascript', + }, + }), + ), + ); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteClientCounter.js?cache=request&x=2', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + const fallbackCallUrl = fetchMock.mock.calls[2]?.[0] as string; + const parsedFallbackCallUrl = new URL(fallbackCallUrl); + expect(parsedFallbackCallUrl.origin).toBe('http://127.0.0.1:3999'); + expect(parsedFallbackCallUrl.pathname).toBe( + '/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', + ); + expect(parsedFallbackCallUrl.searchParams.get('cache')).toBe('request'); + expect(parsedFallbackCallUrl.searchParams.get('v')).toBe('1'); + expect(parsedFallbackCallUrl.searchParams.get('x')).toBe('2'); + expect(next).not.toHaveBeenCalled(); + await expect(context.res?.text()).resolves.toBe( + 'query-override-fallback-hit', + ); + }); + it('falls through when manifest fallback asset URL points to another origin', async () => { const handler = getProxyMiddlewareHandler(); const next = jest.fn(async (): Promise => undefined); diff --git a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts index 8c76c5fb5c62..56552d47d943 100644 --- a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts @@ -670,6 +670,75 @@ describe('rsc-mf remote modern.server middleware contracts', () => { await expect(context.res?.text()).resolves.toBe('absolute-fallback-asset'); }); + it('lets request query params override manifest fallback query params', async () => { + const handler = getRecoverMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + shared: [ + { + assets: { + js: { + sync: [ + 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js?cache=manifest&v=1', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ) + .mockResolvedValueOnce( + new Response('query-override-fallback-asset', { + status: 200, + headers: { + 'content-type': 'application/javascript', + }, + }), + ), + ); + const context: { + req: { url: string; headers?: { get?: (name: string) => string | null } }; + res?: Response; + } = { + req: { + url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.js?cache=request&x=2', + }, + }; + + await handler(context, next); + + const fallbackCallUrl = fetchMock.mock.calls[1]?.[0] as string; + const parsedFallbackCallUrl = new URL(fallbackCallUrl); + expect(parsedFallbackCallUrl.origin).toBe('http://127.0.0.1:3008'); + expect(parsedFallbackCallUrl.pathname).toBe( + '/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', + ); + expect(parsedFallbackCallUrl.searchParams.get('cache')).toBe('request'); + expect(parsedFallbackCallUrl.searchParams.get('v')).toBe('1'); + expect(parsedFallbackCallUrl.searchParams.get('x')).toBe('2'); + expect(next).not.toHaveBeenCalled(); + await expect(context.res?.text()).resolves.toBe( + 'query-override-fallback-asset', + ); + }); + it('falls through when fallback asset resolves to the same request url', async () => { const handler = getRecoverMiddlewareHandler(); const next = jest.fn(async (): Promise => undefined); From d71fde9e1f1bdc28a5276cbbd36186a7ebbf2d9b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 12:03:21 +0000 Subject: [PATCH 256/324] fix(rsc-mf): alias server-only in remote client bundle --- tests/integration/rsc-mf/remote/modern.config.ts | 2 +- tests/integration/rsc-mf/tests/modernConfig.test.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/integration/rsc-mf/remote/modern.config.ts b/tests/integration/rsc-mf/remote/modern.config.ts index a0fd50dd1597..e99d258402ff 100644 --- a/tests/integration/rsc-mf/remote/modern.config.ts +++ b/tests/integration/rsc-mf/remote/modern.config.ts @@ -47,6 +47,7 @@ export default defineConfig({ 'rsc-mf-react-server-dom-client-browser$', reactServerDomClientBrowserPath, ); + chain.resolve.alias.set('server-only$', serverOnlyEmptyPath); if (targets.some(item => String(item).includes('node'))) { chain.target('async-node'); chain.resolve.conditionNames @@ -57,7 +58,6 @@ export default defineConfig({ chain.resolve.alias .set('react/jsx-runtime$', reactJsxRuntimeServerPath) .set('react/jsx-dev-runtime$', reactJsxDevRuntimeServerPath); - chain.resolve.alias.set('server-only$', serverOnlyEmptyPath); chain.output.publicPath(`http://127.0.0.1:${remotePort}/bundles/`); chain.module .rule('rsc-mf-remote-components-layer') diff --git a/tests/integration/rsc-mf/tests/modernConfig.test.ts b/tests/integration/rsc-mf/tests/modernConfig.test.ts index c79faab47621..6bcb64a46801 100644 --- a/tests/integration/rsc-mf/tests/modernConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernConfig.test.ts @@ -362,5 +362,8 @@ describe('rsc-mf modern config contracts', () => { expect( harness.aliasMap.get('rsc-mf-react-server-dom-client-browser$'), ).toContain('react-server-dom-rspack'); + expect(harness.aliasMap.get('server-only$')).toMatch( + /server-only[\\/]empty\.js$/, + ); }); }); From ee7456fca71be4c539bcf967d2292cdfa806c257 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 12:19:22 +0000 Subject: [PATCH 257/324] test(rsc-mf): guard host rsc streaming mode invariants --- tests/integration/rsc-mf/tests/index.test.ts | 2 ++ tests/integration/rsc-mf/tests/modernConfig.test.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 89151d6d5462..401870d1bd1f 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -115,6 +115,8 @@ async function renderRemoteRscIntoHost({ hostPort, page }: TestContext) { const response = await fetch(`http://127.0.0.1:${hostPort}${HOST_RSC_URL}`); const html = await response.text(); expect(html).toContain('Host RSC Module Federation'); + expect(html).toContain('__FLIGHT_DATA'); + expect(html).not.toContain('window._SSR_DATA'); expect(html).toContain('Remote Federated Tree'); expect(html).toContain('remote-server-only-ok'); expect(html).toContain('remote-server-only-default-ok'); diff --git a/tests/integration/rsc-mf/tests/modernConfig.test.ts b/tests/integration/rsc-mf/tests/modernConfig.test.ts index 6bcb64a46801..2013e410f647 100644 --- a/tests/integration/rsc-mf/tests/modernConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernConfig.test.ts @@ -200,6 +200,7 @@ describe('rsc-mf modern config contracts', () => { port: 3007, }), ); + expect(hostConfig.server).not.toHaveProperty('ssr'); expect(hostConfig.source).toEqual( expect.objectContaining({ enableAsyncEntry: false, From b3ea21404e43f0b278184746ac31d078dc5b21e3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 12:32:47 +0000 Subject: [PATCH 258/324] fix(rsc-mf): sanitize proxied asset transport headers --- .../rsc-mf/host/server/modern.server.ts | 16 +++-- .../rsc-mf/remote/server/modern.server.ts | 16 +++-- .../rsc-mf/tests/modernServerConfig.test.ts | 34 +++++++++ .../tests/remoteModernServerConfig.test.ts | 69 +++++++++++++++++++ 4 files changed, 127 insertions(+), 8 deletions(-) diff --git a/tests/integration/rsc-mf/host/server/modern.server.ts b/tests/integration/rsc-mf/host/server/modern.server.ts index 92cdfbd2355d..8572c48a285c 100644 --- a/tests/integration/rsc-mf/host/server/modern.server.ts +++ b/tests/integration/rsc-mf/host/server/modern.server.ts @@ -99,6 +99,17 @@ const shouldProxyRemoteAsset = (pathname: string) => { return false; }; +const createProxyResponse = (upstream: Response) => { + const headers = new Headers(upstream.headers); + headers.delete('content-length'); + headers.delete('content-encoding'); + headers.delete('transfer-encoding'); + return new Response(upstream.body, { + status: upstream.status, + headers, + }); +}; + const proxyRemoteFederationAsset: MiddlewareHandler = async (c, next) => { const requestHeaders = c.req.headers; const isInternalFallbackFetch = @@ -140,10 +151,7 @@ const proxyRemoteFederationAsset: MiddlewareHandler = async (c, next) => { return; } - c.res = new Response(await resolvedUpstream.arrayBuffer(), { - status: resolvedUpstream.status, - headers: resolvedUpstream.headers, - }); + c.res = createProxyResponse(resolvedUpstream); }; export default defineServerConfig({ diff --git a/tests/integration/rsc-mf/remote/server/modern.server.ts b/tests/integration/rsc-mf/remote/server/modern.server.ts index 7de52cba9f2d..928ff3397c20 100644 --- a/tests/integration/rsc-mf/remote/server/modern.server.ts +++ b/tests/integration/rsc-mf/remote/server/modern.server.ts @@ -13,6 +13,17 @@ import { const REMOTE_MANIFEST_PATH = '/static/mf-manifest.json'; +const createProxyResponse = (upstream: Response) => { + const headers = new Headers(upstream.headers); + headers.delete('content-length'); + headers.delete('content-encoding'); + headers.delete('transfer-encoding'); + return new Response(upstream.body, { + status: upstream.status, + headers, + }); +}; + const recoverRemoteExposeAssetMiddleware: MiddlewareHandler = async ( c, next, @@ -85,10 +96,7 @@ const recoverRemoteExposeAssetMiddleware: MiddlewareHandler = async ( return; } - c.res = new Response(await fallbackAssetResponse.arrayBuffer(), { - status: fallbackAssetResponse.status, - headers: fallbackAssetResponse.headers, - }); + c.res = createProxyResponse(fallbackAssetResponse); }; export default defineServerConfig({ diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index 2496631918de..c48cabea228a 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -134,6 +134,40 @@ describe('rsc-mf host modern.server middleware contracts', () => { await expect(context.res?.text()).resolves.toBe('proxied-js'); }); + it('strips transfer headers from proxied upstream responses', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + installFetchMock(async () => { + return new Response('proxied-with-transport-headers', { + status: 200, + headers: { + 'content-type': 'application/javascript', + 'content-length': '999', + 'content-encoding': 'gzip', + 'transfer-encoding': 'chunked', + }, + }); + }); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_infoBundle.11dea89e81.js', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expect(next).not.toHaveBeenCalled(); + expect(context.res?.headers.get('content-type')).toBe( + 'application/javascript', + ); + expect(context.res?.headers.get('content-length')).toBeNull(); + expect(context.res?.headers.get('content-encoding')).toBeNull(); + expect(context.res?.headers.get('transfer-encoding')).toBeNull(); + await expect(context.res?.text()).resolves.toBe( + 'proxied-with-transport-headers', + ); + }); + it('proxies federated async CSS expose chunks to remote origin', async () => { const handler = getProxyMiddlewareHandler(); const next = jest.fn(async (): Promise => undefined); diff --git a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts index 56552d47d943..babdf3540c20 100644 --- a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts @@ -146,6 +146,75 @@ describe('rsc-mf remote modern.server middleware contracts', () => { await expect(context.res?.text()).resolves.toBe('fallback-asset'); }); + it('strips transfer headers from recovered fallback responses', async () => { + const handler = getRecoverMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + installFetchMock( + jest + .fn() + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + exposes: [ + { + assets: { + js: { + sync: [ + 'static/js/async/__federation_expose_RemoteServerCard.a1b2c3d4.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ) + .mockResolvedValueOnce( + new Response('fallback-with-transport-headers', { + status: 200, + headers: { + 'content-type': 'application/javascript', + 'content-length': '999', + 'content-encoding': 'gzip', + 'transfer-encoding': 'chunked', + }, + }), + ), + ); + const context: { + req: { url: string; headers?: { get?: (name: string) => string | null } }; + res?: Response; + } = { + req: { + url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteServerCard.js', + }, + }; + + await handler(context, next); + + expect(next).not.toHaveBeenCalled(); + expect(context.res?.headers.get('content-type')).toBe( + 'application/javascript', + ); + expect(context.res?.headers.get('content-length')).toBeNull(); + expect(context.res?.headers.get('content-encoding')).toBeNull(); + expect(context.res?.headers.get('transfer-encoding')).toBeNull(); + await expect(context.res?.text()).resolves.toBe( + 'fallback-with-transport-headers', + ); + }); + it('recovers stale hashed expose asset path via remote manifest fallback', async () => { const handler = getRecoverMiddlewareHandler(); const next = jest.fn(async (): Promise => undefined); From 42cffd947d4d4af195957abfe4a83ea25ce7f19d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 12:35:16 +0000 Subject: [PATCH 259/324] refactor(rsc-mf): share safe proxy response helper --- .../rsc-mf/host/server/modern.server.ts | 14 +------ .../rsc-mf/remote/server/modern.server.ts | 14 +------ .../rsc-mf/shared/proxyResponse.ts | 16 ++++++++ .../rsc-mf/tests/proxyResponse.test.ts | 41 +++++++++++++++++++ 4 files changed, 61 insertions(+), 24 deletions(-) create mode 100644 tests/integration/rsc-mf/shared/proxyResponse.ts create mode 100644 tests/integration/rsc-mf/tests/proxyResponse.test.ts diff --git a/tests/integration/rsc-mf/host/server/modern.server.ts b/tests/integration/rsc-mf/host/server/modern.server.ts index 8572c48a285c..4f7c33ed5d58 100644 --- a/tests/integration/rsc-mf/host/server/modern.server.ts +++ b/tests/integration/rsc-mf/host/server/modern.server.ts @@ -10,6 +10,7 @@ import { isExposeAssetRequestPath, resolveManifestFallbackAssetPath, } from '../../shared/manifestFallback'; +import { createSafeProxyResponse } from '../../shared/proxyResponse'; const REMOTE_MANIFEST_PATH = '/static/mf-manifest.json'; @@ -99,17 +100,6 @@ const shouldProxyRemoteAsset = (pathname: string) => { return false; }; -const createProxyResponse = (upstream: Response) => { - const headers = new Headers(upstream.headers); - headers.delete('content-length'); - headers.delete('content-encoding'); - headers.delete('transfer-encoding'); - return new Response(upstream.body, { - status: upstream.status, - headers, - }); -}; - const proxyRemoteFederationAsset: MiddlewareHandler = async (c, next) => { const requestHeaders = c.req.headers; const isInternalFallbackFetch = @@ -151,7 +141,7 @@ const proxyRemoteFederationAsset: MiddlewareHandler = async (c, next) => { return; } - c.res = createProxyResponse(resolvedUpstream); + c.res = createSafeProxyResponse(resolvedUpstream); }; export default defineServerConfig({ diff --git a/tests/integration/rsc-mf/remote/server/modern.server.ts b/tests/integration/rsc-mf/remote/server/modern.server.ts index 928ff3397c20..ac271613c613 100644 --- a/tests/integration/rsc-mf/remote/server/modern.server.ts +++ b/tests/integration/rsc-mf/remote/server/modern.server.ts @@ -10,20 +10,10 @@ import { isExposeAssetRequestPath, resolveManifestFallbackAssetPath, } from '../../shared/manifestFallback'; +import { createSafeProxyResponse } from '../../shared/proxyResponse'; const REMOTE_MANIFEST_PATH = '/static/mf-manifest.json'; -const createProxyResponse = (upstream: Response) => { - const headers = new Headers(upstream.headers); - headers.delete('content-length'); - headers.delete('content-encoding'); - headers.delete('transfer-encoding'); - return new Response(upstream.body, { - status: upstream.status, - headers, - }); -}; - const recoverRemoteExposeAssetMiddleware: MiddlewareHandler = async ( c, next, @@ -96,7 +86,7 @@ const recoverRemoteExposeAssetMiddleware: MiddlewareHandler = async ( return; } - c.res = createProxyResponse(fallbackAssetResponse); + c.res = createSafeProxyResponse(fallbackAssetResponse); }; export default defineServerConfig({ diff --git a/tests/integration/rsc-mf/shared/proxyResponse.ts b/tests/integration/rsc-mf/shared/proxyResponse.ts new file mode 100644 index 000000000000..4c525fd8b23f --- /dev/null +++ b/tests/integration/rsc-mf/shared/proxyResponse.ts @@ -0,0 +1,16 @@ +const HOP_BY_HOP_RESPONSE_HEADERS = [ + 'content-length', + 'content-encoding', + 'transfer-encoding', +]; + +export const createSafeProxyResponse = (upstream: Response) => { + const headers = new Headers(upstream.headers); + for (const headerName of HOP_BY_HOP_RESPONSE_HEADERS) { + headers.delete(headerName); + } + return new Response(upstream.body, { + status: upstream.status, + headers, + }); +}; diff --git a/tests/integration/rsc-mf/tests/proxyResponse.test.ts b/tests/integration/rsc-mf/tests/proxyResponse.test.ts new file mode 100644 index 000000000000..1636546ceb9d --- /dev/null +++ b/tests/integration/rsc-mf/tests/proxyResponse.test.ts @@ -0,0 +1,41 @@ +import { createSafeProxyResponse } from '../shared/proxyResponse'; + +describe('rsc-mf proxy response helper', () => { + it('removes hop-by-hop response headers while preserving payload', async () => { + const upstream = new Response('proxied-body', { + status: 202, + headers: { + 'content-type': 'application/javascript', + 'content-length': '999', + 'content-encoding': 'gzip', + 'transfer-encoding': 'chunked', + }, + }); + + const proxied = createSafeProxyResponse(upstream); + + expect(proxied.status).toBe(202); + expect(proxied.headers.get('content-type')).toBe('application/javascript'); + expect(proxied.headers.get('content-length')).toBeNull(); + expect(proxied.headers.get('content-encoding')).toBeNull(); + expect(proxied.headers.get('transfer-encoding')).toBeNull(); + await expect(proxied.text()).resolves.toBe('proxied-body'); + }); + + it('keeps unrelated response headers intact', () => { + const upstream = new Response('ok', { + status: 200, + headers: { + 'cache-control': 'public,max-age=31536000', + etag: '"abc123"', + }, + }); + + const proxied = createSafeProxyResponse(upstream); + + expect(proxied.headers.get('cache-control')).toBe( + 'public,max-age=31536000', + ); + expect(proxied.headers.get('etag')).toBe('"abc123"'); + }); +}); From e7aa75899e61152fa1d40abb2aaa4a73b52cb9e8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 12:39:45 +0000 Subject: [PATCH 260/324] test(rsc-mf): expand hop-by-hop header sanitization coverage --- .../integration/rsc-mf/shared/proxyResponse.ts | 18 ++++++++++++++++-- .../rsc-mf/tests/modernServerConfig.test.ts | 12 ++++++++++++ .../rsc-mf/tests/proxyResponse.test.ts | 12 ++++++++++++ .../tests/remoteModernServerConfig.test.ts | 12 ++++++++++++ 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/tests/integration/rsc-mf/shared/proxyResponse.ts b/tests/integration/rsc-mf/shared/proxyResponse.ts index 4c525fd8b23f..9acf1f1193d9 100644 --- a/tests/integration/rsc-mf/shared/proxyResponse.ts +++ b/tests/integration/rsc-mf/shared/proxyResponse.ts @@ -1,14 +1,28 @@ -const HOP_BY_HOP_RESPONSE_HEADERS = [ +const PROXY_UNSAFE_RESPONSE_HEADERS = [ 'content-length', 'content-encoding', + 'connection', + 'keep-alive', + 'proxy-authenticate', + 'proxy-authorization', + 'te', + 'trailer', 'transfer-encoding', + 'upgrade', ]; export const createSafeProxyResponse = (upstream: Response) => { const headers = new Headers(upstream.headers); - for (const headerName of HOP_BY_HOP_RESPONSE_HEADERS) { + for (const headerName of PROXY_UNSAFE_RESPONSE_HEADERS) { headers.delete(headerName); } + const connectionHeaderTokens = (upstream.headers.get('connection') || '') + .split(',') + .map(token => token.trim().toLowerCase()) + .filter(Boolean); + for (const token of connectionHeaderTokens) { + headers.delete(token); + } return new Response(upstream.body, { status: upstream.status, headers, diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index c48cabea228a..646bcf2cd7e5 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -144,6 +144,12 @@ describe('rsc-mf host modern.server middleware contracts', () => { 'content-type': 'application/javascript', 'content-length': '999', 'content-encoding': 'gzip', + connection: 'keep-alive, x-proxy-hop-header', + 'keep-alive': 'timeout=5', + 'x-proxy-hop-header': 'remove-me', + te: 'trailers', + trailer: 'x-trailer-a', + upgrade: 'websocket', 'transfer-encoding': 'chunked', }, }); @@ -162,6 +168,12 @@ describe('rsc-mf host modern.server middleware contracts', () => { ); expect(context.res?.headers.get('content-length')).toBeNull(); expect(context.res?.headers.get('content-encoding')).toBeNull(); + expect(context.res?.headers.get('connection')).toBeNull(); + expect(context.res?.headers.get('keep-alive')).toBeNull(); + expect(context.res?.headers.get('x-proxy-hop-header')).toBeNull(); + expect(context.res?.headers.get('te')).toBeNull(); + expect(context.res?.headers.get('trailer')).toBeNull(); + expect(context.res?.headers.get('upgrade')).toBeNull(); expect(context.res?.headers.get('transfer-encoding')).toBeNull(); await expect(context.res?.text()).resolves.toBe( 'proxied-with-transport-headers', diff --git a/tests/integration/rsc-mf/tests/proxyResponse.test.ts b/tests/integration/rsc-mf/tests/proxyResponse.test.ts index 1636546ceb9d..3f23081d79c9 100644 --- a/tests/integration/rsc-mf/tests/proxyResponse.test.ts +++ b/tests/integration/rsc-mf/tests/proxyResponse.test.ts @@ -8,6 +8,12 @@ describe('rsc-mf proxy response helper', () => { 'content-type': 'application/javascript', 'content-length': '999', 'content-encoding': 'gzip', + connection: 'keep-alive, x-custom-hop-header', + 'keep-alive': 'timeout=5', + 'x-custom-hop-header': 'remove-me', + te: 'trailers', + trailer: 'x-trailer-a', + upgrade: 'websocket', 'transfer-encoding': 'chunked', }, }); @@ -18,6 +24,12 @@ describe('rsc-mf proxy response helper', () => { expect(proxied.headers.get('content-type')).toBe('application/javascript'); expect(proxied.headers.get('content-length')).toBeNull(); expect(proxied.headers.get('content-encoding')).toBeNull(); + expect(proxied.headers.get('connection')).toBeNull(); + expect(proxied.headers.get('keep-alive')).toBeNull(); + expect(proxied.headers.get('x-custom-hop-header')).toBeNull(); + expect(proxied.headers.get('te')).toBeNull(); + expect(proxied.headers.get('trailer')).toBeNull(); + expect(proxied.headers.get('upgrade')).toBeNull(); expect(proxied.headers.get('transfer-encoding')).toBeNull(); await expect(proxied.text()).resolves.toBe('proxied-body'); }); diff --git a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts index babdf3540c20..f09e87ea0f78 100644 --- a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts @@ -187,6 +187,12 @@ describe('rsc-mf remote modern.server middleware contracts', () => { 'content-type': 'application/javascript', 'content-length': '999', 'content-encoding': 'gzip', + connection: 'keep-alive, x-proxy-hop-header', + 'keep-alive': 'timeout=5', + 'x-proxy-hop-header': 'remove-me', + te: 'trailers', + trailer: 'x-trailer-a', + upgrade: 'websocket', 'transfer-encoding': 'chunked', }, }), @@ -209,6 +215,12 @@ describe('rsc-mf remote modern.server middleware contracts', () => { ); expect(context.res?.headers.get('content-length')).toBeNull(); expect(context.res?.headers.get('content-encoding')).toBeNull(); + expect(context.res?.headers.get('connection')).toBeNull(); + expect(context.res?.headers.get('keep-alive')).toBeNull(); + expect(context.res?.headers.get('x-proxy-hop-header')).toBeNull(); + expect(context.res?.headers.get('te')).toBeNull(); + expect(context.res?.headers.get('trailer')).toBeNull(); + expect(context.res?.headers.get('upgrade')).toBeNull(); expect(context.res?.headers.get('transfer-encoding')).toBeNull(); await expect(context.res?.text()).resolves.toBe( 'fallback-with-transport-headers', From bf39dff2a90e98c9fff30bfc475a30c349fe13e0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 12:46:56 +0000 Subject: [PATCH 261/324] fix(rsc-mf): strip proxy-connection hop headers --- tests/integration/rsc-mf/shared/proxyResponse.ts | 1 + tests/integration/rsc-mf/tests/modernServerConfig.test.ts | 2 ++ tests/integration/rsc-mf/tests/proxyResponse.test.ts | 2 ++ tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts | 2 ++ 4 files changed, 7 insertions(+) diff --git a/tests/integration/rsc-mf/shared/proxyResponse.ts b/tests/integration/rsc-mf/shared/proxyResponse.ts index 9acf1f1193d9..342825909c26 100644 --- a/tests/integration/rsc-mf/shared/proxyResponse.ts +++ b/tests/integration/rsc-mf/shared/proxyResponse.ts @@ -4,6 +4,7 @@ const PROXY_UNSAFE_RESPONSE_HEADERS = [ 'connection', 'keep-alive', 'proxy-authenticate', + 'proxy-connection', 'proxy-authorization', 'te', 'trailer', diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index 646bcf2cd7e5..6f4adedd12c8 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -146,6 +146,7 @@ describe('rsc-mf host modern.server middleware contracts', () => { 'content-encoding': 'gzip', connection: 'keep-alive, x-proxy-hop-header', 'keep-alive': 'timeout=5', + 'proxy-connection': 'keep-alive', 'x-proxy-hop-header': 'remove-me', te: 'trailers', trailer: 'x-trailer-a', @@ -170,6 +171,7 @@ describe('rsc-mf host modern.server middleware contracts', () => { expect(context.res?.headers.get('content-encoding')).toBeNull(); expect(context.res?.headers.get('connection')).toBeNull(); expect(context.res?.headers.get('keep-alive')).toBeNull(); + expect(context.res?.headers.get('proxy-connection')).toBeNull(); expect(context.res?.headers.get('x-proxy-hop-header')).toBeNull(); expect(context.res?.headers.get('te')).toBeNull(); expect(context.res?.headers.get('trailer')).toBeNull(); diff --git a/tests/integration/rsc-mf/tests/proxyResponse.test.ts b/tests/integration/rsc-mf/tests/proxyResponse.test.ts index 3f23081d79c9..859f6fcb69fb 100644 --- a/tests/integration/rsc-mf/tests/proxyResponse.test.ts +++ b/tests/integration/rsc-mf/tests/proxyResponse.test.ts @@ -11,6 +11,7 @@ describe('rsc-mf proxy response helper', () => { connection: 'keep-alive, x-custom-hop-header', 'keep-alive': 'timeout=5', 'x-custom-hop-header': 'remove-me', + 'proxy-connection': 'keep-alive', te: 'trailers', trailer: 'x-trailer-a', upgrade: 'websocket', @@ -27,6 +28,7 @@ describe('rsc-mf proxy response helper', () => { expect(proxied.headers.get('connection')).toBeNull(); expect(proxied.headers.get('keep-alive')).toBeNull(); expect(proxied.headers.get('x-custom-hop-header')).toBeNull(); + expect(proxied.headers.get('proxy-connection')).toBeNull(); expect(proxied.headers.get('te')).toBeNull(); expect(proxied.headers.get('trailer')).toBeNull(); expect(proxied.headers.get('upgrade')).toBeNull(); diff --git a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts index f09e87ea0f78..db4cd4a19808 100644 --- a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts @@ -189,6 +189,7 @@ describe('rsc-mf remote modern.server middleware contracts', () => { 'content-encoding': 'gzip', connection: 'keep-alive, x-proxy-hop-header', 'keep-alive': 'timeout=5', + 'proxy-connection': 'keep-alive', 'x-proxy-hop-header': 'remove-me', te: 'trailers', trailer: 'x-trailer-a', @@ -217,6 +218,7 @@ describe('rsc-mf remote modern.server middleware contracts', () => { expect(context.res?.headers.get('content-encoding')).toBeNull(); expect(context.res?.headers.get('connection')).toBeNull(); expect(context.res?.headers.get('keep-alive')).toBeNull(); + expect(context.res?.headers.get('proxy-connection')).toBeNull(); expect(context.res?.headers.get('x-proxy-hop-header')).toBeNull(); expect(context.res?.headers.get('te')).toBeNull(); expect(context.res?.headers.get('trailer')).toBeNull(); From 4220d7c9f23c111a5f468c87c405e664a0817b04 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 12:50:21 +0000 Subject: [PATCH 262/324] fix(rsc-mf): sanitize quoted connection hop headers --- tests/integration/rsc-mf/shared/proxyResponse.ts | 9 ++++++++- .../rsc-mf/tests/modernServerConfig.test.ts | 9 ++++++++- tests/integration/rsc-mf/tests/proxyResponse.test.ts | 11 ++++++++++- .../rsc-mf/tests/remoteModernServerConfig.test.ts | 9 ++++++++- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/tests/integration/rsc-mf/shared/proxyResponse.ts b/tests/integration/rsc-mf/shared/proxyResponse.ts index 342825909c26..2a3a2f362b86 100644 --- a/tests/integration/rsc-mf/shared/proxyResponse.ts +++ b/tests/integration/rsc-mf/shared/proxyResponse.ts @@ -17,9 +17,16 @@ export const createSafeProxyResponse = (upstream: Response) => { for (const headerName of PROXY_UNSAFE_RESPONSE_HEADERS) { headers.delete(headerName); } + + const normalizeConnectionHeaderToken = (token: string) => + token + .trim() + .replace(/^"+|"+$/g, '') + .toLowerCase(); + const connectionHeaderTokens = (upstream.headers.get('connection') || '') .split(',') - .map(token => token.trim().toLowerCase()) + .map(normalizeConnectionHeaderToken) .filter(Boolean); for (const token of connectionHeaderTokens) { headers.delete(token); diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index 6f4adedd12c8..ccb5c711f8f2 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -144,10 +144,14 @@ describe('rsc-mf host modern.server middleware contracts', () => { 'content-type': 'application/javascript', 'content-length': '999', 'content-encoding': 'gzip', - connection: 'keep-alive, x-proxy-hop-header', + connection: + 'keep-alive, x-proxy-hop-header, "x-proxy-quoted-hop-header"', 'keep-alive': 'timeout=5', 'proxy-connection': 'keep-alive', + 'proxy-authenticate': 'Basic realm=test', + 'proxy-authorization': 'Basic dGVzdA==', 'x-proxy-hop-header': 'remove-me', + 'x-proxy-quoted-hop-header': 'remove-me', te: 'trailers', trailer: 'x-trailer-a', upgrade: 'websocket', @@ -172,7 +176,10 @@ describe('rsc-mf host modern.server middleware contracts', () => { expect(context.res?.headers.get('connection')).toBeNull(); expect(context.res?.headers.get('keep-alive')).toBeNull(); expect(context.res?.headers.get('proxy-connection')).toBeNull(); + expect(context.res?.headers.get('proxy-authenticate')).toBeNull(); + expect(context.res?.headers.get('proxy-authorization')).toBeNull(); expect(context.res?.headers.get('x-proxy-hop-header')).toBeNull(); + expect(context.res?.headers.get('x-proxy-quoted-hop-header')).toBeNull(); expect(context.res?.headers.get('te')).toBeNull(); expect(context.res?.headers.get('trailer')).toBeNull(); expect(context.res?.headers.get('upgrade')).toBeNull(); diff --git a/tests/integration/rsc-mf/tests/proxyResponse.test.ts b/tests/integration/rsc-mf/tests/proxyResponse.test.ts index 859f6fcb69fb..51ed2289df9d 100644 --- a/tests/integration/rsc-mf/tests/proxyResponse.test.ts +++ b/tests/integration/rsc-mf/tests/proxyResponse.test.ts @@ -8,10 +8,15 @@ describe('rsc-mf proxy response helper', () => { 'content-type': 'application/javascript', 'content-length': '999', 'content-encoding': 'gzip', - connection: 'keep-alive, x-custom-hop-header', + connection: + 'keep-alive, x-custom-hop-header, "x-quoted-hop-header", "x-spaced-hop-header"', 'keep-alive': 'timeout=5', 'x-custom-hop-header': 'remove-me', + 'x-quoted-hop-header': 'remove-me', + 'x-spaced-hop-header': 'remove-me', 'proxy-connection': 'keep-alive', + 'proxy-authenticate': 'Basic realm=test', + 'proxy-authorization': 'Basic dGVzdA==', te: 'trailers', trailer: 'x-trailer-a', upgrade: 'websocket', @@ -28,7 +33,11 @@ describe('rsc-mf proxy response helper', () => { expect(proxied.headers.get('connection')).toBeNull(); expect(proxied.headers.get('keep-alive')).toBeNull(); expect(proxied.headers.get('x-custom-hop-header')).toBeNull(); + expect(proxied.headers.get('x-quoted-hop-header')).toBeNull(); + expect(proxied.headers.get('x-spaced-hop-header')).toBeNull(); expect(proxied.headers.get('proxy-connection')).toBeNull(); + expect(proxied.headers.get('proxy-authenticate')).toBeNull(); + expect(proxied.headers.get('proxy-authorization')).toBeNull(); expect(proxied.headers.get('te')).toBeNull(); expect(proxied.headers.get('trailer')).toBeNull(); expect(proxied.headers.get('upgrade')).toBeNull(); diff --git a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts index db4cd4a19808..18e9d86dd14b 100644 --- a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts @@ -187,10 +187,14 @@ describe('rsc-mf remote modern.server middleware contracts', () => { 'content-type': 'application/javascript', 'content-length': '999', 'content-encoding': 'gzip', - connection: 'keep-alive, x-proxy-hop-header', + connection: + 'keep-alive, x-proxy-hop-header, "x-proxy-quoted-hop-header"', 'keep-alive': 'timeout=5', 'proxy-connection': 'keep-alive', + 'proxy-authenticate': 'Basic realm=test', + 'proxy-authorization': 'Basic dGVzdA==', 'x-proxy-hop-header': 'remove-me', + 'x-proxy-quoted-hop-header': 'remove-me', te: 'trailers', trailer: 'x-trailer-a', upgrade: 'websocket', @@ -219,7 +223,10 @@ describe('rsc-mf remote modern.server middleware contracts', () => { expect(context.res?.headers.get('connection')).toBeNull(); expect(context.res?.headers.get('keep-alive')).toBeNull(); expect(context.res?.headers.get('proxy-connection')).toBeNull(); + expect(context.res?.headers.get('proxy-authenticate')).toBeNull(); + expect(context.res?.headers.get('proxy-authorization')).toBeNull(); expect(context.res?.headers.get('x-proxy-hop-header')).toBeNull(); + expect(context.res?.headers.get('x-proxy-quoted-hop-header')).toBeNull(); expect(context.res?.headers.get('te')).toBeNull(); expect(context.res?.headers.get('trailer')).toBeNull(); expect(context.res?.headers.get('upgrade')).toBeNull(); From b52cb63105ad003b666ecce888ac4c11bf728471 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 12:54:35 +0000 Subject: [PATCH 263/324] fix(rsc-mf): normalize quoted connection tokens --- tests/integration/rsc-mf/shared/proxyResponse.ts | 1 + tests/integration/rsc-mf/tests/proxyResponse.test.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/tests/integration/rsc-mf/shared/proxyResponse.ts b/tests/integration/rsc-mf/shared/proxyResponse.ts index 2a3a2f362b86..2faaab4d96f5 100644 --- a/tests/integration/rsc-mf/shared/proxyResponse.ts +++ b/tests/integration/rsc-mf/shared/proxyResponse.ts @@ -33,6 +33,7 @@ export const createSafeProxyResponse = (upstream: Response) => { } return new Response(upstream.body, { status: upstream.status, + statusText: upstream.statusText, headers, }); }; diff --git a/tests/integration/rsc-mf/tests/proxyResponse.test.ts b/tests/integration/rsc-mf/tests/proxyResponse.test.ts index 51ed2289df9d..daaf9e8e9311 100644 --- a/tests/integration/rsc-mf/tests/proxyResponse.test.ts +++ b/tests/integration/rsc-mf/tests/proxyResponse.test.ts @@ -4,6 +4,7 @@ describe('rsc-mf proxy response helper', () => { it('removes hop-by-hop response headers while preserving payload', async () => { const upstream = new Response('proxied-body', { status: 202, + statusText: 'Accepted-Proxy', headers: { 'content-type': 'application/javascript', 'content-length': '999', @@ -27,6 +28,7 @@ describe('rsc-mf proxy response helper', () => { const proxied = createSafeProxyResponse(upstream); expect(proxied.status).toBe(202); + expect(proxied.statusText).toBe('Accepted-Proxy'); expect(proxied.headers.get('content-type')).toBe('application/javascript'); expect(proxied.headers.get('content-length')).toBeNull(); expect(proxied.headers.get('content-encoding')).toBeNull(); From 91b8d88ec5fb562ad7fb8b2e591c6252a6d7d09c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 13:01:49 +0000 Subject: [PATCH 264/324] test(rsc-mf): cover mixed-case connection token stripping --- .../rsc-mf/tests/proxyResponse.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/integration/rsc-mf/tests/proxyResponse.test.ts b/tests/integration/rsc-mf/tests/proxyResponse.test.ts index daaf9e8e9311..4ceac71c3806 100644 --- a/tests/integration/rsc-mf/tests/proxyResponse.test.ts +++ b/tests/integration/rsc-mf/tests/proxyResponse.test.ts @@ -63,4 +63,22 @@ describe('rsc-mf proxy response helper', () => { ); expect(proxied.headers.get('etag')).toBe('"abc123"'); }); + + it('normalizes mixed-case quoted connection tokens', () => { + const upstream = new Response('ok', { + status: 200, + headers: { + connection: '"X-Mixed-Hop-Header", Keep-Alive', + 'x-mixed-hop-header': 'remove-me', + 'Keep-Alive': 'timeout=5', + 'x-safe-header': 'preserve-me', + }, + }); + + const proxied = createSafeProxyResponse(upstream); + + expect(proxied.headers.get('x-mixed-hop-header')).toBeNull(); + expect(proxied.headers.get('keep-alive')).toBeNull(); + expect(proxied.headers.get('x-safe-header')).toBe('preserve-me'); + }); }); From 4f3cb6a44a139bdd445f5f91cf0d5c371d6a43ce Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 13:07:15 +0000 Subject: [PATCH 265/324] test(rsc-mf): assert proxied response status text --- tests/integration/rsc-mf/tests/modernServerConfig.test.ts | 5 ++++- .../rsc-mf/tests/remoteModernServerConfig.test.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index ccb5c711f8f2..3b7ec06daef7 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -139,7 +139,8 @@ describe('rsc-mf host modern.server middleware contracts', () => { const next = jest.fn(async (): Promise => undefined); installFetchMock(async () => { return new Response('proxied-with-transport-headers', { - status: 200, + status: 206, + statusText: 'Partial Content', headers: { 'content-type': 'application/javascript', 'content-length': '999', @@ -168,6 +169,8 @@ describe('rsc-mf host modern.server middleware contracts', () => { await withRemotePort('3999', () => handler(context, next)); expect(next).not.toHaveBeenCalled(); + expect(context.res?.status).toBe(206); + expect(context.res?.statusText).toBe('Partial Content'); expect(context.res?.headers.get('content-type')).toBe( 'application/javascript', ); diff --git a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts index 18e9d86dd14b..ea4a8b12c390 100644 --- a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts @@ -182,7 +182,8 @@ describe('rsc-mf remote modern.server middleware contracts', () => { ) .mockResolvedValueOnce( new Response('fallback-with-transport-headers', { - status: 200, + status: 201, + statusText: 'Created', headers: { 'content-type': 'application/javascript', 'content-length': '999', @@ -215,6 +216,8 @@ describe('rsc-mf remote modern.server middleware contracts', () => { await handler(context, next); expect(next).not.toHaveBeenCalled(); + expect(context.res?.status).toBe(201); + expect(context.res?.statusText).toBe('Created'); expect(context.res?.headers.get('content-type')).toBe( 'application/javascript', ); From 21b564984781995014deb5795387f15625f7d198 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 13:12:34 +0000 Subject: [PATCH 266/324] fix(rsc-mf): guard no-body statuses in proxy responses --- .../rsc-mf/shared/proxyResponse.ts | 6 ++++- .../rsc-mf/tests/proxyResponse.test.ts | 24 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/integration/rsc-mf/shared/proxyResponse.ts b/tests/integration/rsc-mf/shared/proxyResponse.ts index 2faaab4d96f5..e8feafb97951 100644 --- a/tests/integration/rsc-mf/shared/proxyResponse.ts +++ b/tests/integration/rsc-mf/shared/proxyResponse.ts @@ -11,6 +11,7 @@ const PROXY_UNSAFE_RESPONSE_HEADERS = [ 'transfer-encoding', 'upgrade', ]; +const STATUS_CODES_WITHOUT_BODY = new Set([204, 205, 304]); export const createSafeProxyResponse = (upstream: Response) => { const headers = new Headers(upstream.headers); @@ -31,7 +32,10 @@ export const createSafeProxyResponse = (upstream: Response) => { for (const token of connectionHeaderTokens) { headers.delete(token); } - return new Response(upstream.body, { + const responseBody = STATUS_CODES_WITHOUT_BODY.has(upstream.status) + ? null + : upstream.body; + return new Response(responseBody, { status: upstream.status, statusText: upstream.statusText, headers, diff --git a/tests/integration/rsc-mf/tests/proxyResponse.test.ts b/tests/integration/rsc-mf/tests/proxyResponse.test.ts index 4ceac71c3806..9f0de2db7bee 100644 --- a/tests/integration/rsc-mf/tests/proxyResponse.test.ts +++ b/tests/integration/rsc-mf/tests/proxyResponse.test.ts @@ -81,4 +81,28 @@ describe('rsc-mf proxy response helper', () => { expect(proxied.headers.get('keep-alive')).toBeNull(); expect(proxied.headers.get('x-safe-header')).toBe('preserve-me'); }); + + it('forces empty body for no-content status responses', async () => { + const upstream = { + status: 204, + statusText: 'No Content', + headers: new Headers({ + 'content-type': 'application/javascript', + }), + body: new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode('should-not-be-forwarded'), + ); + controller.close(); + }, + }), + } as unknown as Response; + + const proxied = createSafeProxyResponse(upstream); + + expect(proxied.status).toBe(204); + expect(proxied.statusText).toBe('No Content'); + await expect(proxied.text()).resolves.toBe(''); + }); }); From 637bddda54bba8cc620f1aeffd66f1fc818c9075 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 13:14:31 +0000 Subject: [PATCH 267/324] test(rsc-mf): cover 205 and 304 proxy body stripping --- .../rsc-mf/tests/proxyResponse.test.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/integration/rsc-mf/tests/proxyResponse.test.ts b/tests/integration/rsc-mf/tests/proxyResponse.test.ts index 9f0de2db7bee..a343b119ef32 100644 --- a/tests/integration/rsc-mf/tests/proxyResponse.test.ts +++ b/tests/integration/rsc-mf/tests/proxyResponse.test.ts @@ -105,4 +105,32 @@ describe('rsc-mf proxy response helper', () => { expect(proxied.statusText).toBe('No Content'); await expect(proxied.text()).resolves.toBe(''); }); + + it.each([ + { status: 205, statusText: 'Reset Content' }, + { status: 304, statusText: 'Not Modified' }, + ])( + 'forces empty body for status $status ($statusText)', + async ({ status, statusText }) => { + const upstream = { + status, + statusText, + headers: new Headers({ + 'content-type': 'application/json', + }), + body: new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode('discard-body')); + controller.close(); + }, + }), + } as unknown as Response; + + const proxied = createSafeProxyResponse(upstream); + + expect(proxied.status).toBe(status); + expect(proxied.statusText).toBe(statusText); + await expect(proxied.text()).resolves.toBe(''); + }, + ); }); From 2994076f17942af4bf729e39ccf751fc866a74c8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 13:21:24 +0000 Subject: [PATCH 268/324] fix(rsc-mf): normalize empty quoted connection tokens --- .../rsc-mf/shared/proxyResponse.ts | 19 +++++++++++-------- .../rsc-mf/tests/proxyResponse.test.ts | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/tests/integration/rsc-mf/shared/proxyResponse.ts b/tests/integration/rsc-mf/shared/proxyResponse.ts index e8feafb97951..68a1c61440c3 100644 --- a/tests/integration/rsc-mf/shared/proxyResponse.ts +++ b/tests/integration/rsc-mf/shared/proxyResponse.ts @@ -13,18 +13,21 @@ const PROXY_UNSAFE_RESPONSE_HEADERS = [ ]; const STATUS_CODES_WITHOUT_BODY = new Set([204, 205, 304]); +const isResponseStatusWithoutBody = (status: number) => + STATUS_CODES_WITHOUT_BODY.has(status); + +const normalizeConnectionHeaderToken = (token: string) => + token + .trim() + .replace(/^"+|"+$/g, '') + .trim() + .toLowerCase(); + export const createSafeProxyResponse = (upstream: Response) => { const headers = new Headers(upstream.headers); for (const headerName of PROXY_UNSAFE_RESPONSE_HEADERS) { headers.delete(headerName); } - - const normalizeConnectionHeaderToken = (token: string) => - token - .trim() - .replace(/^"+|"+$/g, '') - .toLowerCase(); - const connectionHeaderTokens = (upstream.headers.get('connection') || '') .split(',') .map(normalizeConnectionHeaderToken) @@ -32,7 +35,7 @@ export const createSafeProxyResponse = (upstream: Response) => { for (const token of connectionHeaderTokens) { headers.delete(token); } - const responseBody = STATUS_CODES_WITHOUT_BODY.has(upstream.status) + const responseBody = isResponseStatusWithoutBody(upstream.status) ? null : upstream.body; return new Response(responseBody, { diff --git a/tests/integration/rsc-mf/tests/proxyResponse.test.ts b/tests/integration/rsc-mf/tests/proxyResponse.test.ts index a343b119ef32..a1879005fad8 100644 --- a/tests/integration/rsc-mf/tests/proxyResponse.test.ts +++ b/tests/integration/rsc-mf/tests/proxyResponse.test.ts @@ -82,6 +82,23 @@ describe('rsc-mf proxy response helper', () => { expect(proxied.headers.get('x-safe-header')).toBe('preserve-me'); }); + it('ignores empty connection tokens after quote normalization', async () => { + const upstream = new Response('ok', { + status: 200, + headers: { + connection: ' , "", " ", "x-trimmed-hop-header" ', + 'x-trimmed-hop-header': 'remove-me', + 'x-safe-header': 'preserve-me', + }, + }); + + const proxied = createSafeProxyResponse(upstream); + + expect(proxied.headers.get('x-trimmed-hop-header')).toBeNull(); + expect(proxied.headers.get('x-safe-header')).toBe('preserve-me'); + await expect(proxied.text()).resolves.toBe('ok'); + }); + it('forces empty body for no-content status responses', async () => { const upstream = { status: 204, From 18bbd03438939fe1cf97603ea1dd8224bfee79d2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 13:50:34 +0000 Subject: [PATCH 269/324] fix(rsc-mf): typecheck and finalize federation asset middleware --- pnpm-lock.yaml | 37 ++++++++++---- tests/integration/rsc-mf/host/package.json | 1 + .../rsc-mf/host/server/modern.server.ts | 48 +++++++++++++++++-- tests/integration/rsc-mf/host/tsconfig.json | 10 +++- tests/integration/rsc-mf/remote/package.json | 1 + .../rsc-mf/remote/server/modern.server.ts | 44 +++++++++++++++-- tests/integration/rsc-mf/remote/tsconfig.json | 10 +++- .../rsc-mf/tests/tsconfigContracts.test.ts | 31 ++++++++++++ .../tests/types/modern-js-server-runtime.d.ts | 3 +- 9 files changed, 165 insertions(+), 20 deletions(-) create mode 100644 tests/integration/rsc-mf/tests/tsconfigContracts.test.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0fecf53dc720..d2d7c642331a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -804,7 +804,7 @@ importers: version: 19.2.4(react@19.2.4) react-server-dom-rspack: specifier: 0.0.1-beta.0 - version: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) typescript: specifier: ^5 version: 5.9.3 @@ -1915,7 +1915,7 @@ importers: devDependencies: '@rsbuild/plugin-react': specifier: 1.4.4 - version: 1.4.4(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0)) + version: 1.4.4(@rsbuild/core@1.7.3) '@rslib/core': specifier: 0.19.5 version: 0.19.5(typescript@5.9.3) @@ -3050,7 +3050,7 @@ importers: version: link:../../../../../packages/runtime/plugin-runtime '@module-federation/modern-js-v3': specifier: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437 - version: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + version: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) i18next: specifier: 25.7.4 version: 25.7.4(typescript@5.9.3) @@ -3608,6 +3608,9 @@ importers: '@modern-js/builder': specifier: workspace:* version: link:../../../../packages/cli/builder + '@modern-js/server-runtime': + specifier: workspace:* + version: link:../../../../packages/server/server-runtime '@types/jest': specifier: ^29.5.14 version: 29.5.14 @@ -3637,7 +3640,7 @@ importers: version: link:../../../../packages/runtime/plugin-runtime '@module-federation/modern-js-v3': specifier: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437 - version: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) + version: https://pkg.pr.new/module-federation/core/@module-federation/modern-js-v3@4437(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(esbuild@0.27.2)) '@module-federation/runtime': specifier: https://pkg.pr.new/module-federation/core/@module-federation/runtime@73bd1ddb2266002f586a872ddae8020052c5457c version: https://pkg.pr.new/module-federation/core/@module-federation/runtime@73bd1ddb2266002f586a872ddae8020052c5457c @@ -3655,7 +3658,7 @@ importers: version: 19.2.4(react@19.2.4) react-server-dom-rspack: specifier: 0.0.1-beta.0 - version: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) server-only: specifier: ^0.0.1 version: 0.0.1 @@ -3666,6 +3669,9 @@ importers: '@modern-js/builder': specifier: workspace:* version: link:../../../../packages/cli/builder + '@modern-js/server-runtime': + specifier: workspace:* + version: link:../../../../packages/server/server-runtime '@types/jest': specifier: ^29.5.14 version: 29.5.14 @@ -6284,7 +6290,6 @@ packages: '@module-federation/node@2.7.32': resolution: {integrity: sha512-hUj5v2GGwpNzl2gaJS4AyzCYRzJBhN8875A+ucKF9tq3jaQb5zpy3izYMISqqbN2q9a7jz3nEUgwAh3pjri+rQ==} - version: 2.7.32 peerDependencies: webpack: ^5.40.0 peerDependenciesMeta: @@ -17561,6 +17566,14 @@ snapshots: deepmerge: 4.3.1 reduce-configs: 1.1.1 + '@rsbuild/plugin-react@1.4.4(@rsbuild/core@1.7.3)': + dependencies: + '@rsbuild/core': 1.7.3 + '@rspack/plugin-react-refresh': 1.6.0(react-refresh@0.18.0) + react-refresh: 0.18.0 + transitivePeerDependencies: + - webpack-hot-middleware + '@rsbuild/plugin-react@1.4.4(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))': dependencies: '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) @@ -25291,15 +25304,21 @@ snapshots: optionalDependencies: react-dom: 19.2.4(react@19.2.4) + react-server-dom-rspack@0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.17) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-server-dom-rspack@0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.18) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - react-server-dom-rspack@0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-server-dom-rspack@0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.17) + '@rspack/core': 2.0.0-beta.2(@module-federation/runtime-tools@https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@73bd1ddb2266002f586a872ddae8020052c5457c)(@swc/helpers@0.5.18) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -25597,7 +25616,7 @@ snapshots: rsbuild-plugin-rsc@0.0.1-beta.0(@rsbuild/core@2.0.0-beta.4(core-js@3.48.0))(react-server-dom-rspack@0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)): dependencies: '@rsbuild/core': 2.0.0-beta.4(core-js@3.48.0) - react-server-dom-rspack: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react-server-dom-rspack: 0.0.1-beta.0(@rspack/core@2.0.0-beta.2(@module-federation/runtime-tools@2.0.1)(@swc/helpers@0.5.17))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) rslog@1.3.2: {} diff --git a/tests/integration/rsc-mf/host/package.json b/tests/integration/rsc-mf/host/package.json index 14376c975537..b89517335cc4 100644 --- a/tests/integration/rsc-mf/host/package.json +++ b/tests/integration/rsc-mf/host/package.json @@ -22,6 +22,7 @@ "devDependencies": { "@modern-js/app-tools": "workspace:*", "@modern-js/builder": "workspace:*", + "@modern-js/server-runtime": "workspace:*", "@types/jest": "^29.5.14", "@types/node": "^20", "@types/react": "^19.2.13", diff --git a/tests/integration/rsc-mf/host/server/modern.server.ts b/tests/integration/rsc-mf/host/server/modern.server.ts index 4f7c33ed5d58..1f04d936e80d 100644 --- a/tests/integration/rsc-mf/host/server/modern.server.ts +++ b/tests/integration/rsc-mf/host/server/modern.server.ts @@ -100,19 +100,57 @@ const shouldProxyRemoteAsset = (pathname: string) => { return false; }; +const getRequestHeader = ( + c: Parameters[0], + name: string, +) => { + const requestWithHeaders = c.req as typeof c.req & { + header?: (headerName: string) => string | undefined; + headers?: { get?: (headerName: string) => string | null | undefined }; + }; + const headerValue = + typeof requestWithHeaders.header === 'function' + ? requestWithHeaders.header(name) + : undefined; + return headerValue ?? requestWithHeaders.headers?.get?.(name); +}; + +const setContextResponse = ( + c: Parameters[0], + response: Response, +) => { + const contextWithBody = c as typeof c & { + body?: ( + body: BodyInit | null, + status?: number, + headers?: HeadersInit, + ) => Response; + }; + if (typeof contextWithBody.body === 'function') { + const finalizedResponse = contextWithBody.body( + response.body, + response.status, + response.headers, + ); + c.res = finalizedResponse; + return finalizedResponse; + } + c.res = response; + return response; +}; + const proxyRemoteFederationAsset: MiddlewareHandler = async (c, next) => { - const requestHeaders = c.req.headers; const isInternalFallbackFetch = - requestHeaders?.get?.(INTERNAL_FALLBACK_HEADER) === '1'; + getRequestHeader(c, INTERNAL_FALLBACK_HEADER) === '1'; if (isInternalFallbackFetch) { await next(); return; } - const reqUrl = new URL(c.req.url); const pathname = reqUrl.pathname; + const shouldProxy = shouldProxyRemoteAsset(pathname); - if (!shouldProxyRemoteAsset(pathname)) { + if (!shouldProxy) { await next(); return; } @@ -141,7 +179,7 @@ const proxyRemoteFederationAsset: MiddlewareHandler = async (c, next) => { return; } - c.res = createSafeProxyResponse(resolvedUpstream); + return setContextResponse(c, createSafeProxyResponse(resolvedUpstream)); }; export default defineServerConfig({ diff --git a/tests/integration/rsc-mf/host/tsconfig.json b/tests/integration/rsc-mf/host/tsconfig.json index 9637e7fc052c..9346bf36cdb6 100644 --- a/tests/integration/rsc-mf/host/tsconfig.json +++ b/tests/integration/rsc-mf/host/tsconfig.json @@ -3,10 +3,18 @@ "compilerOptions": { "declaration": false, "jsx": "react-jsx", + "rootDir": "..", "baseUrl": "./", "paths": { "@/*": ["./src/*"] } }, - "include": ["src", "config"] + "include": [ + "src", + "config", + "server", + "../shared", + "modern.config.ts", + "module-federation.config.ts" + ] } diff --git a/tests/integration/rsc-mf/remote/package.json b/tests/integration/rsc-mf/remote/package.json index 00829cb847fe..441add5ba826 100644 --- a/tests/integration/rsc-mf/remote/package.json +++ b/tests/integration/rsc-mf/remote/package.json @@ -22,6 +22,7 @@ "devDependencies": { "@modern-js/app-tools": "workspace:*", "@modern-js/builder": "workspace:*", + "@modern-js/server-runtime": "workspace:*", "@types/jest": "^29.5.14", "@types/node": "^20", "@types/react": "^19.2.13", diff --git a/tests/integration/rsc-mf/remote/server/modern.server.ts b/tests/integration/rsc-mf/remote/server/modern.server.ts index ac271613c613..f46b1af31d84 100644 --- a/tests/integration/rsc-mf/remote/server/modern.server.ts +++ b/tests/integration/rsc-mf/remote/server/modern.server.ts @@ -14,6 +14,45 @@ import { createSafeProxyResponse } from '../../shared/proxyResponse'; const REMOTE_MANIFEST_PATH = '/static/mf-manifest.json'; +const getRequestHeader = ( + c: Parameters[0], + name: string, +) => { + const requestWithHeaders = c.req as typeof c.req & { + header?: (headerName: string) => string | undefined; + headers?: { get?: (headerName: string) => string | null | undefined }; + }; + const headerValue = + typeof requestWithHeaders.header === 'function' + ? requestWithHeaders.header(name) + : undefined; + return headerValue ?? requestWithHeaders.headers?.get?.(name); +}; + +const setContextResponse = ( + c: Parameters[0], + response: Response, +) => { + const contextWithBody = c as typeof c & { + body?: ( + body: BodyInit | null, + status?: number, + headers?: HeadersInit, + ) => Response; + }; + if (typeof contextWithBody.body === 'function') { + const finalizedResponse = contextWithBody.body( + response.body, + response.status, + response.headers, + ); + c.res = finalizedResponse; + return finalizedResponse; + } + c.res = response; + return response; +}; + const recoverRemoteExposeAssetMiddleware: MiddlewareHandler = async ( c, next, @@ -25,9 +64,8 @@ const recoverRemoteExposeAssetMiddleware: MiddlewareHandler = async ( return; } - const requestHeaders = c.req.headers; const isInternalFallbackFetch = - requestHeaders?.get?.(INTERNAL_FALLBACK_HEADER) === '1'; + getRequestHeader(c, INTERNAL_FALLBACK_HEADER) === '1'; if (isInternalFallbackFetch) { await next(); return; @@ -86,7 +124,7 @@ const recoverRemoteExposeAssetMiddleware: MiddlewareHandler = async ( return; } - c.res = createSafeProxyResponse(fallbackAssetResponse); + return setContextResponse(c, createSafeProxyResponse(fallbackAssetResponse)); }; export default defineServerConfig({ diff --git a/tests/integration/rsc-mf/remote/tsconfig.json b/tests/integration/rsc-mf/remote/tsconfig.json index 9637e7fc052c..9346bf36cdb6 100644 --- a/tests/integration/rsc-mf/remote/tsconfig.json +++ b/tests/integration/rsc-mf/remote/tsconfig.json @@ -3,10 +3,18 @@ "compilerOptions": { "declaration": false, "jsx": "react-jsx", + "rootDir": "..", "baseUrl": "./", "paths": { "@/*": ["./src/*"] } }, - "include": ["src", "config"] + "include": [ + "src", + "config", + "server", + "../shared", + "modern.config.ts", + "module-federation.config.ts" + ] } diff --git a/tests/integration/rsc-mf/tests/tsconfigContracts.test.ts b/tests/integration/rsc-mf/tests/tsconfigContracts.test.ts new file mode 100644 index 000000000000..c6563dcf03a0 --- /dev/null +++ b/tests/integration/rsc-mf/tests/tsconfigContracts.test.ts @@ -0,0 +1,31 @@ +import fs from 'fs'; +import path from 'path'; + +const readTsconfig = (projectDir: 'host' | 'remote') => { + const tsconfigPath = path.resolve( + __dirname, + `../${projectDir}/tsconfig.json`, + ); + return JSON.parse(fs.readFileSync(tsconfigPath, 'utf8')) as { + include?: string[]; + }; +}; + +describe('rsc-mf tsconfig contracts', () => { + it.each(['host', 'remote'] as const)( + 'keeps %s tsconfig include entries aligned with runtime server config files', + projectDir => { + const tsconfig = readTsconfig(projectDir); + expect(Array.isArray(tsconfig.include)).toBe(true); + expect(tsconfig.include).toEqual( + expect.arrayContaining([ + 'src', + 'server', + '../shared', + 'modern.config.ts', + 'module-federation.config.ts', + ]), + ); + }, + ); +}); diff --git a/tests/integration/rsc-mf/tests/types/modern-js-server-runtime.d.ts b/tests/integration/rsc-mf/tests/types/modern-js-server-runtime.d.ts index 0a386ef1c4f7..5d110660f13f 100644 --- a/tests/integration/rsc-mf/tests/types/modern-js-server-runtime.d.ts +++ b/tests/integration/rsc-mf/tests/types/modern-js-server-runtime.d.ts @@ -1,6 +1,7 @@ declare module '@modern-js/server-runtime' { interface MiddlewareRequestLike { url: string; + header?: (name: string) => string | undefined; headers?: { get?: (name: string) => string | null | undefined; }; @@ -9,7 +10,7 @@ declare module '@modern-js/server-runtime' { export type MiddlewareHandler = ( c: { req: MiddlewareRequestLike; res?: Response }, next: () => Promise, - ) => Promise | void; + ) => Promise | void | Response; export function defineServerConfig(config: T): T; } From e1e43b0540b3be6732112339b7a17b5063ce577c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 13:58:19 +0000 Subject: [PATCH 270/324] test(rsc-mf): cover middleware context body response finalization --- .../rsc-mf/tests/modernServerConfig.test.ts | 43 +++++++++++ .../tests/remoteModernServerConfig.test.ts | 75 +++++++++++++++++++ 2 files changed, 118 insertions(+) diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index 3b7ec06daef7..9c8929a5da3f 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -134,6 +134,49 @@ describe('rsc-mf host modern.server middleware contracts', () => { await expect(context.res?.text()).resolves.toBe('proxied-js'); }); + it('finalizes proxied responses through context body API when available', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock(async () => { + return new Response('proxied-via-body', { + status: 203, + headers: { + 'content-type': 'application/javascript', + 'content-length': '111', + }, + }); + }); + const context: { + req: { url: string }; + res?: Response; + body: ( + body: BodyInit | null, + status?: number, + headers?: HeadersInit, + ) => Response; + } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_infoBundle.11dea89e81.js', + }, + body: jest.fn((body, status, headers) => { + return new Response(body, { + status, + headers, + }); + }), + }; + + await withRemotePort('3999', () => handler(context as any, next)); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(context.body).toHaveBeenCalledTimes(1); + expect(context.res).toBeInstanceOf(Response); + expect(context.res?.status).toBe(203); + expect(context.res?.headers.get('content-length')).toBeNull(); + await expect(context.res?.text()).resolves.toBe('proxied-via-body'); + expect(next).not.toHaveBeenCalled(); + }); + it('strips transfer headers from proxied upstream responses', async () => { const handler = getProxyMiddlewareHandler(); const next = jest.fn(async (): Promise => undefined); diff --git a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts index ea4a8b12c390..0f3bd442edad 100644 --- a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts @@ -146,6 +146,81 @@ describe('rsc-mf remote modern.server middleware contracts', () => { await expect(context.res?.text()).resolves.toBe('fallback-asset'); }); + it('finalizes recovered responses through context body API when available', async () => { + const handler = getRecoverMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + exposes: [ + { + assets: { + js: { + sync: [ + 'static/js/async/__federation_expose_RemoteServerCard.a1b2c3d4.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ) + .mockResolvedValueOnce( + new Response('fallback-via-body', { + status: 203, + headers: { + 'content-type': 'application/javascript', + 'content-length': '222', + }, + }), + ), + ); + const context: { + req: { url: string }; + res?: Response; + body: ( + body: BodyInit | null, + status?: number, + headers?: HeadersInit, + ) => Response; + } = { + req: { + url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteServerCard.js', + }, + body: jest.fn((body, status, headers) => { + return new Response(body, { + status, + headers, + }); + }), + }; + + await handler(context as any, next); + + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(context.body).toHaveBeenCalledTimes(1); + expect(context.res).toBeInstanceOf(Response); + expect(context.res?.status).toBe(203); + expect(context.res?.headers.get('content-length')).toBeNull(); + await expect(context.res?.text()).resolves.toBe('fallback-via-body'); + expect(next).not.toHaveBeenCalled(); + }); + it('strips transfer headers from recovered fallback responses', async () => { const handler = getRecoverMiddlewareHandler(); const next = jest.fn(async (): Promise => undefined); From 6db78739faba63b648917ab58b4233f1afb43399 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 14:06:44 +0000 Subject: [PATCH 271/324] fix(rsc-mf): prefer hashed manifest fallback assets --- .../rsc-mf/shared/manifestFallback.ts | 48 ++++++++++--- .../rsc-mf/tests/manifestFallback.test.ts | 29 ++++++++ .../rsc-mf/tests/modernServerConfig.test.ts | 64 +++++++++++++++++ .../tests/remoteModernServerConfig.test.ts | 70 +++++++++++++++++++ 4 files changed, 202 insertions(+), 9 deletions(-) diff --git a/tests/integration/rsc-mf/shared/manifestFallback.ts b/tests/integration/rsc-mf/shared/manifestFallback.ts index e2bc67183b6c..959b7b5ae0bc 100644 --- a/tests/integration/rsc-mf/shared/manifestFallback.ts +++ b/tests/integration/rsc-mf/shared/manifestFallback.ts @@ -1,4 +1,5 @@ const EXPOSE_CHUNK_HASH_SUFFIX_PATTERN = /\.[a-z0-9]{6,}$/i; +const EXPOSE_CHUNK_HASHED_ASSET_PATTERN = /\.[a-z0-9]{6,}\.(js|css)$/i; export const INTERNAL_FALLBACK_HEADER = 'x-rsc-mf-internal-fallback'; export interface RemoteManifestAssetEntry { @@ -44,6 +45,12 @@ const toNormalizedManifestAssetPath = (assetPath: string) => { } }; +const toNormalizedRequestPath = (pathname: string) => + pathname.replace(/^\/+/, '').split(/[?#]/, 1)[0]; + +const hasChunkHashInAssetPath = (assetPath: string) => + EXPOSE_CHUNK_HASHED_ASSET_PATTERN.test(assetPath); + const collectManifestAssetPaths = (manifest: RemoteManifestShape) => { const entries = [...(manifest.shared || []), ...(manifest.exposes || [])]; const assetPaths = new Set(); @@ -78,16 +85,39 @@ export const resolveManifestFallbackAssetPath = ( } const requestedAssetDirectory = getRequestedAssetDirectory(pathname); + const normalizedRequestedPath = toNormalizedRequestPath(pathname); const manifestAssets = collectManifestAssetPaths(manifest); - return manifestAssets.find(assetPath => { - const normalizedAssetPath = toNormalizedManifestAssetPath(assetPath); - if (!normalizedAssetPath.startsWith(requestedAssetDirectory)) { - return false; - } - return ( - toCanonicalChunkName(normalizedAssetPath) === canonicalRequestedChunkName - ); - }); + const candidateAssets = manifestAssets + .map(assetPath => ({ + assetPath, + normalizedAssetPath: toNormalizedManifestAssetPath(assetPath), + })) + .filter(({ normalizedAssetPath }) => { + if (!normalizedAssetPath.startsWith(requestedAssetDirectory)) { + return false; + } + return ( + toCanonicalChunkName(normalizedAssetPath) === + canonicalRequestedChunkName + ); + }); + + const preferredCandidate = + candidateAssets.find( + ({ normalizedAssetPath }) => + normalizedAssetPath !== normalizedRequestedPath && + hasChunkHashInAssetPath(normalizedAssetPath), + ) || + candidateAssets.find( + ({ normalizedAssetPath }) => + normalizedAssetPath !== normalizedRequestedPath, + ) || + candidateAssets.find(({ normalizedAssetPath }) => + hasChunkHashInAssetPath(normalizedAssetPath), + ) || + candidateAssets[0]; + + return preferredCandidate?.assetPath; }; export const createManifestFallbackAssetUrl = ({ diff --git a/tests/integration/rsc-mf/tests/manifestFallback.test.ts b/tests/integration/rsc-mf/tests/manifestFallback.test.ts index 2765ae79557e..ab5dc678e442 100644 --- a/tests/integration/rsc-mf/tests/manifestFallback.test.ts +++ b/tests/integration/rsc-mf/tests/manifestFallback.test.ts @@ -154,6 +154,35 @@ describe('manifest fallback shared helpers', () => { ); }); + it('prefers hashed fallback candidates over identical stale aliases', () => { + const manifest: RemoteManifestShape = { + exposes: [ + { + assets: { + js: { + sync: [ + 'static/js/async/__federation_expose_RemoteServerCard.js', + 'static/js/async/__federation_expose_RemoteServerCard.a1b2c3d4.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }; + + expect( + resolveManifestFallbackAssetPath( + '/static/js/async/__federation_expose_RemoteServerCard.js', + manifest, + ), + ).toBe('static/js/async/__federation_expose_RemoteServerCard.a1b2c3d4.js'); + }); + it('resolves stale hashed css expose requests to current hashed css assets', () => { const manifest: RemoteManifestShape = { exposes: [ diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index 9c8929a5da3f..20f9b6fc7b32 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -495,6 +495,70 @@ describe('rsc-mf host modern.server middleware contracts', () => { await expect(context.res?.text()).resolves.toBe('fallback-hit'); }); + it('prefers hashed manifest fallback when stale alias is also listed', async () => { + const handler = getProxyMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce(new Response('not-found', { status: 404 })) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + exposes: [ + { + assets: { + js: { + sync: [ + 'static/js/async/__federation_expose_RemoteServerCard.js', + 'static/js/async/__federation_expose_RemoteServerCard.a1b2c3d4.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ) + .mockResolvedValueOnce( + new Response('hashed-preferred-fallback-hit', { + status: 200, + headers: { + 'content-type': 'application/javascript', + }, + }), + ), + ); + const context: { req: { url: string }; res?: Response } = { + req: { + url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteServerCard.js', + }, + }; + + await withRemotePort('3999', () => handler(context, next)); + + expectInternalFallbackFetchCall( + fetchMock, + 3, + 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteServerCard.a1b2c3d4.js', + ); + expect(next).not.toHaveBeenCalled(); + await expect(context.res?.text()).resolves.toBe( + 'hashed-preferred-fallback-hit', + ); + }); + it('recovers stale hashed expose chunk path via manifest-driven fallback', async () => { const handler = getProxyMiddlewareHandler(); const next = jest.fn(async (): Promise => undefined); diff --git a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts index 0f3bd442edad..02ac3678efb9 100644 --- a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts @@ -146,6 +146,76 @@ describe('rsc-mf remote modern.server middleware contracts', () => { await expect(context.res?.text()).resolves.toBe('fallback-asset'); }); + it('prefers hashed manifest fallback when stale alias is also listed', async () => { + const handler = getRecoverMiddlewareHandler(); + const next = jest.fn(async (): Promise => undefined); + const fetchMock = installFetchMock( + jest + .fn() + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + exposes: [ + { + assets: { + js: { + sync: [ + 'static/js/async/__federation_expose_RemoteServerCard.js', + 'static/js/async/__federation_expose_RemoteServerCard.a1b2c3d4.js', + ], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ), + ) + .mockResolvedValueOnce( + new Response('hashed-preferred-fallback-asset', { + status: 200, + headers: { + 'content-type': 'application/javascript', + }, + }), + ), + ); + const context: { + req: { url: string; headers?: { get?: (name: string) => string | null } }; + res?: Response; + } = { + req: { + url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteServerCard.js', + }, + }; + + await handler(context, next); + + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteServerCard.a1b2c3d4.js', + { + headers: { + [INTERNAL_FALLBACK_HEADER]: '1', + }, + }, + ); + expect(next).not.toHaveBeenCalled(); + await expect(context.res?.text()).resolves.toBe( + 'hashed-preferred-fallback-asset', + ); + }); + it('finalizes recovered responses through context body API when available', async () => { const handler = getRecoverMiddlewareHandler(); const next = jest.fn(async (): Promise => undefined); From eb3907e36c98327d07a5c778e2c3d340afa7a4ab Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 14:38:53 +0000 Subject: [PATCH 272/324] test(rsc-mf): lock tsconfig rootDir shared-import contract --- .../rsc-mf/tests/tsconfigContracts.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/integration/rsc-mf/tests/tsconfigContracts.test.ts b/tests/integration/rsc-mf/tests/tsconfigContracts.test.ts index c6563dcf03a0..a4c5d4e44f26 100644 --- a/tests/integration/rsc-mf/tests/tsconfigContracts.test.ts +++ b/tests/integration/rsc-mf/tests/tsconfigContracts.test.ts @@ -8,6 +8,9 @@ const readTsconfig = (projectDir: 'host' | 'remote') => { ); return JSON.parse(fs.readFileSync(tsconfigPath, 'utf8')) as { include?: string[]; + compilerOptions?: { + rootDir?: string; + }; }; }; @@ -28,4 +31,12 @@ describe('rsc-mf tsconfig contracts', () => { ); }, ); + + it.each(['host', 'remote'] as const)( + 'keeps %s tsconfig rootDir aligned for shared fixture imports', + projectDir => { + const tsconfig = readTsconfig(projectDir); + expect(tsconfig.compilerOptions?.rootDir).toBe('..'); + }, + ); }); From d3a15f517c8cdb09a1e87596f707ac87e4d744ae Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 15:11:46 +0000 Subject: [PATCH 273/324] refactor(rsc-mf): localize server fallback utilities per fixture app --- .../rsc-mf/host/server/manifestFallback.ts | 186 ++++++++++++++++++ .../rsc-mf/host/server/modern.server.ts | 4 +- .../rsc-mf/host/server/proxyResponse.ts | 46 +++++ tests/integration/rsc-mf/host/tsconfig.json | 2 - .../rsc-mf/remote/server/manifestFallback.ts | 186 ++++++++++++++++++ .../rsc-mf/remote/server/modern.server.ts | 4 +- .../rsc-mf/remote/server/proxyResponse.ts | 46 +++++ tests/integration/rsc-mf/remote/tsconfig.json | 2 - .../rsc-mf/tests/modernServerConfig.test.ts | 2 +- .../tests/remoteModernServerConfig.test.ts | 2 +- .../rsc-mf/tests/tsconfigContracts.test.ts | 5 +- 11 files changed, 472 insertions(+), 13 deletions(-) create mode 100644 tests/integration/rsc-mf/host/server/manifestFallback.ts create mode 100644 tests/integration/rsc-mf/host/server/proxyResponse.ts create mode 100644 tests/integration/rsc-mf/remote/server/manifestFallback.ts create mode 100644 tests/integration/rsc-mf/remote/server/proxyResponse.ts diff --git a/tests/integration/rsc-mf/host/server/manifestFallback.ts b/tests/integration/rsc-mf/host/server/manifestFallback.ts new file mode 100644 index 000000000000..959b7b5ae0bc --- /dev/null +++ b/tests/integration/rsc-mf/host/server/manifestFallback.ts @@ -0,0 +1,186 @@ +const EXPOSE_CHUNK_HASH_SUFFIX_PATTERN = /\.[a-z0-9]{6,}$/i; +const EXPOSE_CHUNK_HASHED_ASSET_PATTERN = /\.[a-z0-9]{6,}\.(js|css)$/i; +export const INTERNAL_FALLBACK_HEADER = 'x-rsc-mf-internal-fallback'; + +export interface RemoteManifestAssetEntry { + assets?: { + js?: { + sync?: string[]; + async?: string[]; + }; + css?: { + sync?: string[]; + async?: string[]; + }; + }; +} + +export interface RemoteManifestShape { + shared?: RemoteManifestAssetEntry[]; + exposes?: RemoteManifestAssetEntry[]; +} + +export const isExposeAssetRequestPath = (pathname: string) => + pathname.includes('__federation_expose_') && + (pathname.endsWith('.js') || pathname.endsWith('.css')); + +export const getRequestedAssetDirectory = (pathname: string) => + pathname.includes('/static/css/async/') + ? 'static/css/async/' + : 'static/js/async/'; + +const toCanonicalChunkName = (filePath: string) => + filePath + .replace(/\/+$/, '') + .split('/') + .pop() + ?.replace(/\.(js|css)$/i, '') + .replace(EXPOSE_CHUNK_HASH_SUFFIX_PATTERN, ''); + +const toNormalizedManifestAssetPath = (assetPath: string) => { + try { + return new URL(assetPath).pathname.replace(/^\/+/, ''); + } catch { + return assetPath.replace(/^[./]+/, '').split(/[?#]/, 1)[0]; + } +}; + +const toNormalizedRequestPath = (pathname: string) => + pathname.replace(/^\/+/, '').split(/[?#]/, 1)[0]; + +const hasChunkHashInAssetPath = (assetPath: string) => + EXPOSE_CHUNK_HASHED_ASSET_PATTERN.test(assetPath); + +const collectManifestAssetPaths = (manifest: RemoteManifestShape) => { + const entries = [...(manifest.shared || []), ...(manifest.exposes || [])]; + const assetPaths = new Set(); + for (const entry of entries) { + const jsSyncAssets = entry.assets?.js?.sync || []; + const jsAsyncAssets = entry.assets?.js?.async || []; + const cssSyncAssets = entry.assets?.css?.sync || []; + const cssAsyncAssets = entry.assets?.css?.async || []; + for (const assetPath of [ + ...jsSyncAssets, + ...jsAsyncAssets, + ...cssSyncAssets, + ...cssAsyncAssets, + ]) { + assetPaths.add(assetPath); + } + } + return [...assetPaths]; +}; + +export const resolveManifestFallbackAssetPath = ( + pathname: string, + manifest: RemoteManifestShape, +) => { + if (!isExposeAssetRequestPath(pathname)) { + return undefined; + } + + const canonicalRequestedChunkName = toCanonicalChunkName(pathname); + if (!canonicalRequestedChunkName) { + return undefined; + } + + const requestedAssetDirectory = getRequestedAssetDirectory(pathname); + const normalizedRequestedPath = toNormalizedRequestPath(pathname); + const manifestAssets = collectManifestAssetPaths(manifest); + const candidateAssets = manifestAssets + .map(assetPath => ({ + assetPath, + normalizedAssetPath: toNormalizedManifestAssetPath(assetPath), + })) + .filter(({ normalizedAssetPath }) => { + if (!normalizedAssetPath.startsWith(requestedAssetDirectory)) { + return false; + } + return ( + toCanonicalChunkName(normalizedAssetPath) === + canonicalRequestedChunkName + ); + }); + + const preferredCandidate = + candidateAssets.find( + ({ normalizedAssetPath }) => + normalizedAssetPath !== normalizedRequestedPath && + hasChunkHashInAssetPath(normalizedAssetPath), + ) || + candidateAssets.find( + ({ normalizedAssetPath }) => + normalizedAssetPath !== normalizedRequestedPath, + ) || + candidateAssets.find(({ normalizedAssetPath }) => + hasChunkHashInAssetPath(normalizedAssetPath), + ) || + candidateAssets[0]; + + return preferredCandidate?.assetPath; +}; + +export const createManifestFallbackAssetUrl = ({ + remoteOrigin, + fallbackAssetPath, + requestSearch, + requestedAssetDirectory, + requestUrl, +}: { + remoteOrigin: string; + fallbackAssetPath: string; + requestSearch: string; + requestedAssetDirectory: string; + requestUrl?: string; +}) => { + let remoteOriginUrl: URL; + try { + remoteOriginUrl = new URL(remoteOrigin); + } catch { + return undefined; + } + + let fallbackAssetUrl: URL; + try { + fallbackAssetUrl = new URL(fallbackAssetPath, `${remoteOriginUrl.origin}/`); + } catch { + return undefined; + } + + if (fallbackAssetUrl.origin !== remoteOriginUrl.origin) { + return undefined; + } + const normalizedFallbackPathname = fallbackAssetUrl.pathname.replace( + /^\/+/, + '', + ); + let decodedFallbackPathname: string; + try { + decodedFallbackPathname = decodeURIComponent(normalizedFallbackPathname); + } catch { + return undefined; + } + if (!decodedFallbackPathname.startsWith(requestedAssetDirectory)) { + return undefined; + } + if (decodedFallbackPathname.split('/').some(segment => segment === '..')) { + return undefined; + } + + if (requestSearch) { + const mergedSearchParams = new URLSearchParams(fallbackAssetUrl.search); + const requestSearchParams = new URLSearchParams(requestSearch); + for (const [key, value] of requestSearchParams.entries()) { + mergedSearchParams.set(key, value); + } + const mergedSearch = mergedSearchParams.toString(); + fallbackAssetUrl.search = mergedSearch ? `?${mergedSearch}` : ''; + } + + const resolvedFallbackAssetUrl = fallbackAssetUrl.toString(); + if (requestUrl && resolvedFallbackAssetUrl === requestUrl) { + return undefined; + } + + return resolvedFallbackAssetUrl; +}; diff --git a/tests/integration/rsc-mf/host/server/modern.server.ts b/tests/integration/rsc-mf/host/server/modern.server.ts index 1f04d936e80d..d5bc6b389b80 100644 --- a/tests/integration/rsc-mf/host/server/modern.server.ts +++ b/tests/integration/rsc-mf/host/server/modern.server.ts @@ -9,8 +9,8 @@ import { getRequestedAssetDirectory, isExposeAssetRequestPath, resolveManifestFallbackAssetPath, -} from '../../shared/manifestFallback'; -import { createSafeProxyResponse } from '../../shared/proxyResponse'; +} from './manifestFallback'; +import { createSafeProxyResponse } from './proxyResponse'; const REMOTE_MANIFEST_PATH = '/static/mf-manifest.json'; diff --git a/tests/integration/rsc-mf/host/server/proxyResponse.ts b/tests/integration/rsc-mf/host/server/proxyResponse.ts new file mode 100644 index 000000000000..68a1c61440c3 --- /dev/null +++ b/tests/integration/rsc-mf/host/server/proxyResponse.ts @@ -0,0 +1,46 @@ +const PROXY_UNSAFE_RESPONSE_HEADERS = [ + 'content-length', + 'content-encoding', + 'connection', + 'keep-alive', + 'proxy-authenticate', + 'proxy-connection', + 'proxy-authorization', + 'te', + 'trailer', + 'transfer-encoding', + 'upgrade', +]; +const STATUS_CODES_WITHOUT_BODY = new Set([204, 205, 304]); + +const isResponseStatusWithoutBody = (status: number) => + STATUS_CODES_WITHOUT_BODY.has(status); + +const normalizeConnectionHeaderToken = (token: string) => + token + .trim() + .replace(/^"+|"+$/g, '') + .trim() + .toLowerCase(); + +export const createSafeProxyResponse = (upstream: Response) => { + const headers = new Headers(upstream.headers); + for (const headerName of PROXY_UNSAFE_RESPONSE_HEADERS) { + headers.delete(headerName); + } + const connectionHeaderTokens = (upstream.headers.get('connection') || '') + .split(',') + .map(normalizeConnectionHeaderToken) + .filter(Boolean); + for (const token of connectionHeaderTokens) { + headers.delete(token); + } + const responseBody = isResponseStatusWithoutBody(upstream.status) + ? null + : upstream.body; + return new Response(responseBody, { + status: upstream.status, + statusText: upstream.statusText, + headers, + }); +}; diff --git a/tests/integration/rsc-mf/host/tsconfig.json b/tests/integration/rsc-mf/host/tsconfig.json index 9346bf36cdb6..be7c601218c7 100644 --- a/tests/integration/rsc-mf/host/tsconfig.json +++ b/tests/integration/rsc-mf/host/tsconfig.json @@ -3,7 +3,6 @@ "compilerOptions": { "declaration": false, "jsx": "react-jsx", - "rootDir": "..", "baseUrl": "./", "paths": { "@/*": ["./src/*"] @@ -13,7 +12,6 @@ "src", "config", "server", - "../shared", "modern.config.ts", "module-federation.config.ts" ] diff --git a/tests/integration/rsc-mf/remote/server/manifestFallback.ts b/tests/integration/rsc-mf/remote/server/manifestFallback.ts new file mode 100644 index 000000000000..959b7b5ae0bc --- /dev/null +++ b/tests/integration/rsc-mf/remote/server/manifestFallback.ts @@ -0,0 +1,186 @@ +const EXPOSE_CHUNK_HASH_SUFFIX_PATTERN = /\.[a-z0-9]{6,}$/i; +const EXPOSE_CHUNK_HASHED_ASSET_PATTERN = /\.[a-z0-9]{6,}\.(js|css)$/i; +export const INTERNAL_FALLBACK_HEADER = 'x-rsc-mf-internal-fallback'; + +export interface RemoteManifestAssetEntry { + assets?: { + js?: { + sync?: string[]; + async?: string[]; + }; + css?: { + sync?: string[]; + async?: string[]; + }; + }; +} + +export interface RemoteManifestShape { + shared?: RemoteManifestAssetEntry[]; + exposes?: RemoteManifestAssetEntry[]; +} + +export const isExposeAssetRequestPath = (pathname: string) => + pathname.includes('__federation_expose_') && + (pathname.endsWith('.js') || pathname.endsWith('.css')); + +export const getRequestedAssetDirectory = (pathname: string) => + pathname.includes('/static/css/async/') + ? 'static/css/async/' + : 'static/js/async/'; + +const toCanonicalChunkName = (filePath: string) => + filePath + .replace(/\/+$/, '') + .split('/') + .pop() + ?.replace(/\.(js|css)$/i, '') + .replace(EXPOSE_CHUNK_HASH_SUFFIX_PATTERN, ''); + +const toNormalizedManifestAssetPath = (assetPath: string) => { + try { + return new URL(assetPath).pathname.replace(/^\/+/, ''); + } catch { + return assetPath.replace(/^[./]+/, '').split(/[?#]/, 1)[0]; + } +}; + +const toNormalizedRequestPath = (pathname: string) => + pathname.replace(/^\/+/, '').split(/[?#]/, 1)[0]; + +const hasChunkHashInAssetPath = (assetPath: string) => + EXPOSE_CHUNK_HASHED_ASSET_PATTERN.test(assetPath); + +const collectManifestAssetPaths = (manifest: RemoteManifestShape) => { + const entries = [...(manifest.shared || []), ...(manifest.exposes || [])]; + const assetPaths = new Set(); + for (const entry of entries) { + const jsSyncAssets = entry.assets?.js?.sync || []; + const jsAsyncAssets = entry.assets?.js?.async || []; + const cssSyncAssets = entry.assets?.css?.sync || []; + const cssAsyncAssets = entry.assets?.css?.async || []; + for (const assetPath of [ + ...jsSyncAssets, + ...jsAsyncAssets, + ...cssSyncAssets, + ...cssAsyncAssets, + ]) { + assetPaths.add(assetPath); + } + } + return [...assetPaths]; +}; + +export const resolveManifestFallbackAssetPath = ( + pathname: string, + manifest: RemoteManifestShape, +) => { + if (!isExposeAssetRequestPath(pathname)) { + return undefined; + } + + const canonicalRequestedChunkName = toCanonicalChunkName(pathname); + if (!canonicalRequestedChunkName) { + return undefined; + } + + const requestedAssetDirectory = getRequestedAssetDirectory(pathname); + const normalizedRequestedPath = toNormalizedRequestPath(pathname); + const manifestAssets = collectManifestAssetPaths(manifest); + const candidateAssets = manifestAssets + .map(assetPath => ({ + assetPath, + normalizedAssetPath: toNormalizedManifestAssetPath(assetPath), + })) + .filter(({ normalizedAssetPath }) => { + if (!normalizedAssetPath.startsWith(requestedAssetDirectory)) { + return false; + } + return ( + toCanonicalChunkName(normalizedAssetPath) === + canonicalRequestedChunkName + ); + }); + + const preferredCandidate = + candidateAssets.find( + ({ normalizedAssetPath }) => + normalizedAssetPath !== normalizedRequestedPath && + hasChunkHashInAssetPath(normalizedAssetPath), + ) || + candidateAssets.find( + ({ normalizedAssetPath }) => + normalizedAssetPath !== normalizedRequestedPath, + ) || + candidateAssets.find(({ normalizedAssetPath }) => + hasChunkHashInAssetPath(normalizedAssetPath), + ) || + candidateAssets[0]; + + return preferredCandidate?.assetPath; +}; + +export const createManifestFallbackAssetUrl = ({ + remoteOrigin, + fallbackAssetPath, + requestSearch, + requestedAssetDirectory, + requestUrl, +}: { + remoteOrigin: string; + fallbackAssetPath: string; + requestSearch: string; + requestedAssetDirectory: string; + requestUrl?: string; +}) => { + let remoteOriginUrl: URL; + try { + remoteOriginUrl = new URL(remoteOrigin); + } catch { + return undefined; + } + + let fallbackAssetUrl: URL; + try { + fallbackAssetUrl = new URL(fallbackAssetPath, `${remoteOriginUrl.origin}/`); + } catch { + return undefined; + } + + if (fallbackAssetUrl.origin !== remoteOriginUrl.origin) { + return undefined; + } + const normalizedFallbackPathname = fallbackAssetUrl.pathname.replace( + /^\/+/, + '', + ); + let decodedFallbackPathname: string; + try { + decodedFallbackPathname = decodeURIComponent(normalizedFallbackPathname); + } catch { + return undefined; + } + if (!decodedFallbackPathname.startsWith(requestedAssetDirectory)) { + return undefined; + } + if (decodedFallbackPathname.split('/').some(segment => segment === '..')) { + return undefined; + } + + if (requestSearch) { + const mergedSearchParams = new URLSearchParams(fallbackAssetUrl.search); + const requestSearchParams = new URLSearchParams(requestSearch); + for (const [key, value] of requestSearchParams.entries()) { + mergedSearchParams.set(key, value); + } + const mergedSearch = mergedSearchParams.toString(); + fallbackAssetUrl.search = mergedSearch ? `?${mergedSearch}` : ''; + } + + const resolvedFallbackAssetUrl = fallbackAssetUrl.toString(); + if (requestUrl && resolvedFallbackAssetUrl === requestUrl) { + return undefined; + } + + return resolvedFallbackAssetUrl; +}; diff --git a/tests/integration/rsc-mf/remote/server/modern.server.ts b/tests/integration/rsc-mf/remote/server/modern.server.ts index f46b1af31d84..5ead78507bbc 100644 --- a/tests/integration/rsc-mf/remote/server/modern.server.ts +++ b/tests/integration/rsc-mf/remote/server/modern.server.ts @@ -9,8 +9,8 @@ import { getRequestedAssetDirectory, isExposeAssetRequestPath, resolveManifestFallbackAssetPath, -} from '../../shared/manifestFallback'; -import { createSafeProxyResponse } from '../../shared/proxyResponse'; +} from './manifestFallback'; +import { createSafeProxyResponse } from './proxyResponse'; const REMOTE_MANIFEST_PATH = '/static/mf-manifest.json'; diff --git a/tests/integration/rsc-mf/remote/server/proxyResponse.ts b/tests/integration/rsc-mf/remote/server/proxyResponse.ts new file mode 100644 index 000000000000..68a1c61440c3 --- /dev/null +++ b/tests/integration/rsc-mf/remote/server/proxyResponse.ts @@ -0,0 +1,46 @@ +const PROXY_UNSAFE_RESPONSE_HEADERS = [ + 'content-length', + 'content-encoding', + 'connection', + 'keep-alive', + 'proxy-authenticate', + 'proxy-connection', + 'proxy-authorization', + 'te', + 'trailer', + 'transfer-encoding', + 'upgrade', +]; +const STATUS_CODES_WITHOUT_BODY = new Set([204, 205, 304]); + +const isResponseStatusWithoutBody = (status: number) => + STATUS_CODES_WITHOUT_BODY.has(status); + +const normalizeConnectionHeaderToken = (token: string) => + token + .trim() + .replace(/^"+|"+$/g, '') + .trim() + .toLowerCase(); + +export const createSafeProxyResponse = (upstream: Response) => { + const headers = new Headers(upstream.headers); + for (const headerName of PROXY_UNSAFE_RESPONSE_HEADERS) { + headers.delete(headerName); + } + const connectionHeaderTokens = (upstream.headers.get('connection') || '') + .split(',') + .map(normalizeConnectionHeaderToken) + .filter(Boolean); + for (const token of connectionHeaderTokens) { + headers.delete(token); + } + const responseBody = isResponseStatusWithoutBody(upstream.status) + ? null + : upstream.body; + return new Response(responseBody, { + status: upstream.status, + statusText: upstream.statusText, + headers, + }); +}; diff --git a/tests/integration/rsc-mf/remote/tsconfig.json b/tests/integration/rsc-mf/remote/tsconfig.json index 9346bf36cdb6..be7c601218c7 100644 --- a/tests/integration/rsc-mf/remote/tsconfig.json +++ b/tests/integration/rsc-mf/remote/tsconfig.json @@ -3,7 +3,6 @@ "compilerOptions": { "declaration": false, "jsx": "react-jsx", - "rootDir": "..", "baseUrl": "./", "paths": { "@/*": ["./src/*"] @@ -13,7 +12,6 @@ "src", "config", "server", - "../shared", "modern.config.ts", "module-federation.config.ts" ] diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index 20f9b6fc7b32..6d9954a52e2a 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -1,4 +1,4 @@ -import { INTERNAL_FALLBACK_HEADER } from '../shared/manifestFallback'; +import { INTERNAL_FALLBACK_HEADER } from '../host/server/manifestFallback'; const HOST_SERVER_CONFIG_MODULE = '../host/server/modern.server'; diff --git a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts index 02ac3678efb9..542454f8564b 100644 --- a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts @@ -1,4 +1,4 @@ -import { INTERNAL_FALLBACK_HEADER } from '../shared/manifestFallback'; +import { INTERNAL_FALLBACK_HEADER } from '../remote/server/manifestFallback'; const REMOTE_SERVER_CONFIG_MODULE = '../remote/server/modern.server'; diff --git a/tests/integration/rsc-mf/tests/tsconfigContracts.test.ts b/tests/integration/rsc-mf/tests/tsconfigContracts.test.ts index a4c5d4e44f26..3c6afc353bff 100644 --- a/tests/integration/rsc-mf/tests/tsconfigContracts.test.ts +++ b/tests/integration/rsc-mf/tests/tsconfigContracts.test.ts @@ -24,7 +24,6 @@ describe('rsc-mf tsconfig contracts', () => { expect.arrayContaining([ 'src', 'server', - '../shared', 'modern.config.ts', 'module-federation.config.ts', ]), @@ -33,10 +32,10 @@ describe('rsc-mf tsconfig contracts', () => { ); it.each(['host', 'remote'] as const)( - 'keeps %s tsconfig rootDir aligned for shared fixture imports', + 'keeps %s tsconfig rootDir unset for server dist runtime path', projectDir => { const tsconfig = readTsconfig(projectDir); - expect(tsconfig.compilerOptions?.rootDir).toBe('..'); + expect(tsconfig.compilerOptions?.rootDir).toBeUndefined(); }, ); }); From f0c05306c4de72483b49f4b7ee0cc59768ad973a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 15:30:56 +0000 Subject: [PATCH 274/324] refactor(rsc-mf): scope callback bootstrap to client/action exposes --- .../src/runtime/createRscExposeDefinitions.ts | 22 ++++++++- .../tests/createRscExposeDefinitions.test.ts | 45 ++++++++++++++----- tests/integration/rsc-mf/tests/index.test.ts | 1 - .../tests/moduleFederationConfig.test.ts | 8 +--- 4 files changed, 57 insertions(+), 19 deletions(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts index 742b483e4086..3507cdf6ec2c 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts @@ -3,6 +3,10 @@ const CALLBACK_BOOTSTRAP_PREFIX = './src/runtime/'; const USERLAND_EXPOSE_PREFIX = './'; const SOURCE_ENTRY_EXTENSION_PATTERN = /\.[cm]?[jt]sx?$/i; const RSC_LAYER = 'react-server-components'; +const CALLBACK_BOOTSTRAP_EXPOSE_KEY_PATTERN = + /^\.\/(?:RemoteClient[\w-]*|actions|nestedActions|defaultAction|actionBundle)$/; +const CALLBACK_BOOTSTRAP_IMPORT_PATH_PATTERN = + /\/(?:RemoteClient[\w-]*|actions|nestedActions|defaultAction|actionBundle)\.[cm]?[jt]sx?$/; type ExposeImportInput = string | string[]; export type ExposeDefinitionInput = @@ -102,13 +106,25 @@ const normalizeExposeImportPaths = ( const createRscExpose = ( importPaths: string[], exposeOverrides: Record, + includeCallbackBootstrap: boolean, ) => ({ ...exposeOverrides, - import: [CALLBACK_BOOTSTRAP_IMPORT, ...importPaths], + import: includeCallbackBootstrap + ? [CALLBACK_BOOTSTRAP_IMPORT, ...importPaths] + : importPaths, layer: RSC_LAYER, }) as const; +const shouldInjectCallbackBootstrap = ( + exposeKey: string, + importPaths: string[], +) => + CALLBACK_BOOTSTRAP_EXPOSE_KEY_PATTERN.test(exposeKey) || + importPaths.some(importPath => + CALLBACK_BOOTSTRAP_IMPORT_PATH_PATTERN.test(importPath), + ); + const assertValidExposeConfig = ( normalizedExposeImportPaths: NormalizedExposeImportPaths, ) => { @@ -231,6 +247,10 @@ export const createRscExposeDefinitions = ( createRscExpose( normalizedDefinition.importPaths, normalizedDefinition.exposeOverrides, + shouldInjectCallbackBootstrap( + exposeKey, + normalizedDefinition.importPaths, + ), ), ]), ); diff --git a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts index 4b1befc4038a..32fe094ef845 100644 --- a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts +++ b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts @@ -102,8 +102,7 @@ describe('createRscExposeDefinitions', () => { }); it('allows cts and mts expose entry extensions', () => { - const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = - loadCreateRscExposeDefinitions(); + const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); expect( createRscExposeDefinitions({ './serverOnlyHelper': './src/lib/serverOnlyHelper.cts', @@ -111,11 +110,11 @@ describe('createRscExposeDefinitions', () => { }), ).toEqual({ './serverOnlyHelper': { - import: [CALLBACK_BOOTSTRAP_MODULE, './src/lib/serverOnlyHelper.cts'], + import: ['./src/lib/serverOnlyHelper.cts'], layer: 'react-server-components', }, './rscBridgeUtil': { - import: [CALLBACK_BOOTSTRAP_MODULE, './src/lib/rscBridgeUtil.mts'], + import: ['./src/lib/rscBridgeUtil.mts'], layer: 'react-server-components', }, }); @@ -162,8 +161,7 @@ describe('createRscExposeDefinitions', () => { }); it('deduplicates repeated entries in object expose import arrays', () => { - const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = - loadCreateRscExposeDefinitions(); + const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); const exposeDefinitions = createRscExposeDefinitions({ './infoBundle': { import: [ @@ -176,15 +174,14 @@ describe('createRscExposeDefinitions', () => { expect(exposeDefinitions).toEqual({ './infoBundle': { - import: [CALLBACK_BOOTSTRAP_MODULE, './src/components/infoBundle.ts'], + import: ['./src/components/infoBundle.ts'], layer: 'react-server-components', }, }); }); it('trims expose import path entries before deduping', () => { - const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = - loadCreateRscExposeDefinitions(); + const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); const exposeDefinitions = createRscExposeDefinitions({ './infoBundle': { import: [ @@ -197,7 +194,7 @@ describe('createRscExposeDefinitions', () => { expect(exposeDefinitions).toEqual({ './infoBundle': { - import: [CALLBACK_BOOTSTRAP_MODULE, './src/components/infoBundle.ts'], + import: ['./src/components/infoBundle.ts'], layer: 'react-server-components', }, }); @@ -322,4 +319,32 @@ describe('createRscExposeDefinitions', () => { }), ).toThrow('must remain internal-only and cannot be exposed'); }); + + it('injects callback bootstrap only for callback-capable expose keys', () => { + const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = + loadCreateRscExposeDefinitions(); + expect( + createRscExposeDefinitions({ + './RemoteClientCounter': './src/components/RemoteClientCounter.tsx', + './actions': './src/components/actions.ts', + './RemoteServerDefault': './src/components/RemoteServerDefault.tsx', + }), + ).toEqual({ + './RemoteClientCounter': { + import: [ + CALLBACK_BOOTSTRAP_MODULE, + './src/components/RemoteClientCounter.tsx', + ], + layer: 'react-server-components', + }, + './actions': { + import: [CALLBACK_BOOTSTRAP_MODULE, './src/components/actions.ts'], + layer: 'react-server-components', + }, + './RemoteServerDefault': { + import: ['./src/components/RemoteServerDefault.tsx'], + layer: 'react-server-components', + }, + }); + }); }); diff --git a/tests/integration/rsc-mf/tests/index.test.ts b/tests/integration/rsc-mf/tests/index.test.ts index 401870d1bd1f..cfb1a21fb42e 100644 --- a/tests/integration/rsc-mf/tests/index.test.ts +++ b/tests/integration/rsc-mf/tests/index.test.ts @@ -26,7 +26,6 @@ const EXPECTED_BROWSER_EXPOSE_CHUNKS = [ '__federation_expose_nestedActions', '__federation_expose_defaultAction', '__federation_expose_actionBundle', - '__federation_expose_infoBundle', ]; const EXPECTED_REMOTE_EXPOSE_PATHS = [ './RemoteClientCounter', diff --git a/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts b/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts index 5f583f3b62a0..3ee35077cb31 100644 --- a/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts +++ b/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts @@ -155,13 +155,7 @@ describe('rsc-mf module federation config contracts', () => { importPath => importPath === './src/components/infoBundle.ts', ), ).toEqual(['./src/components/infoBundle.ts']); - expect( - imports.every( - importPath => - importPath === './src/components/infoBundle.ts' || - importPath === CALLBACK_BOOTSTRAP_IMPORT, - ), - ).toBe(true); + expect(imports).toEqual(['./src/components/infoBundle.ts']); }); it('uses remote port env var in host manifest remote URL', () => { From b52276d9afb16c8f8019cb5e989f0728b2869537 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 15:42:09 +0000 Subject: [PATCH 275/324] refactor(rsc-mf): infer callback bootstrap from expose source directives --- .../src/runtime/createRscExposeDefinitions.ts | 100 +++++++++++++++++- .../tests/createRscExposeDefinitions.test.ts | 29 +++++ 2 files changed, 127 insertions(+), 2 deletions(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts index 3507cdf6ec2c..243c2caae2ee 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts @@ -1,3 +1,6 @@ +import fs from 'node:fs'; +import path from 'node:path'; + const CALLBACK_BOOTSTRAP_IMPORT = './src/runtime/initServerCallback.ts'; const CALLBACK_BOOTSTRAP_PREFIX = './src/runtime/'; const USERLAND_EXPOSE_PREFIX = './'; @@ -7,6 +10,21 @@ const CALLBACK_BOOTSTRAP_EXPOSE_KEY_PATTERN = /^\.\/(?:RemoteClient[\w-]*|actions|nestedActions|defaultAction|actionBundle)$/; const CALLBACK_BOOTSTRAP_IMPORT_PATH_PATTERN = /\/(?:RemoteClient[\w-]*|actions|nestedActions|defaultAction|actionBundle)\.[cm]?[jt]sx?$/; +const LOCAL_MODULE_SPECIFIER_PATTERN = /^\.{1,2}\//; +const SOURCE_DIRECTIVE_PATTERN = /^\s*['"]use (?:client|server)['"]\s*;?/m; +const EXPORT_FROM_SPECIFIER_PATTERN = + /export\s+(?:\*\s+from|\{[^}]*\}\s+from)\s*['"]([^'"]+)['"]/g; +const SOURCE_ENTRY_EXTENSIONS = [ + '.ts', + '.tsx', + '.js', + '.jsx', + '.mts', + '.cts', + '.mjs', + '.cjs', +] as const; +const REMOTE_PROJECT_ROOT = path.resolve(__dirname, '../..'); type ExposeImportInput = string | string[]; export type ExposeDefinitionInput = @@ -121,9 +139,87 @@ const shouldInjectCallbackBootstrap = ( importPaths: string[], ) => CALLBACK_BOOTSTRAP_EXPOSE_KEY_PATTERN.test(exposeKey) || - importPaths.some(importPath => - CALLBACK_BOOTSTRAP_IMPORT_PATH_PATTERN.test(importPath), + importPaths.some( + importPath => + CALLBACK_BOOTSTRAP_IMPORT_PATH_PATTERN.test(importPath) || + referencesCallbackCapableSourceModule(importPath), + ); + +const readSourceFile = (filePath: string) => { + try { + return fs.readFileSync(filePath, 'utf8'); + } catch { + return undefined; + } +}; + +const resolveUserlandImportPathToFile = ( + importPath: string, + fromFilePath?: string, +) => { + const importPathWithoutPrefix = importPath.replace(/^\.\//, ''); + const basePath = fromFilePath + ? path.resolve(path.dirname(fromFilePath), importPath) + : path.resolve(REMOTE_PROJECT_ROOT, importPathWithoutPrefix); + const hasExplicitExtension = SOURCE_ENTRY_EXTENSIONS.some(extension => + basePath.endsWith(extension), ); + if (hasExplicitExtension) { + return fs.existsSync(basePath) ? basePath : undefined; + } + const extensionCandidates = SOURCE_ENTRY_EXTENSIONS.map( + extension => `${basePath}${extension}`, + ); + const indexCandidates = SOURCE_ENTRY_EXTENSIONS.map(extension => + path.join(basePath, `index${extension}`), + ); + return [...extensionCandidates, ...indexCandidates].find(candidatePath => + fs.existsSync(candidatePath), + ); +}; + +const referencesCallbackCapableSourceModule = (importPath: string) => { + const rootSourceFile = resolveUserlandImportPathToFile(importPath); + if (!rootSourceFile) { + return false; + } + + const visitedFiles = new Set(); + const hasCallbackDirective = (filePath: string): boolean => { + if (visitedFiles.has(filePath)) { + return false; + } + visitedFiles.add(filePath); + const sourceText = readSourceFile(filePath); + if (!sourceText) { + return false; + } + if (SOURCE_DIRECTIVE_PATTERN.test(sourceText)) { + return true; + } + + const exportFromMatches = sourceText.matchAll( + EXPORT_FROM_SPECIFIER_PATTERN, + ); + for (const match of exportFromMatches) { + const moduleSpecifier = match[1]; + if (!LOCAL_MODULE_SPECIFIER_PATTERN.test(moduleSpecifier)) { + continue; + } + const childModuleFilePath = resolveUserlandImportPathToFile( + moduleSpecifier, + filePath, + ); + if (childModuleFilePath && hasCallbackDirective(childModuleFilePath)) { + return true; + } + } + + return false; + }; + + return hasCallbackDirective(rootSourceFile); +}; const assertValidExposeConfig = ( normalizedExposeImportPaths: NormalizedExposeImportPaths, diff --git a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts index 32fe094ef845..2ba671c8b693 100644 --- a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts +++ b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts @@ -347,4 +347,33 @@ describe('createRscExposeDefinitions', () => { }, }); }); + + it('infers callback bootstrap from re-exported use server modules', () => { + const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = + loadCreateRscExposeDefinitions(); + expect( + createRscExposeDefinitions({ + './customBundledActions': './src/components/actionBundle.ts', + }), + ).toEqual({ + './customBundledActions': { + import: [CALLBACK_BOOTSTRAP_MODULE, './src/components/actionBundle.ts'], + layer: 'react-server-components', + }, + }); + }); + + it('keeps non-callback source modules free of callback bootstrap import', () => { + const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); + expect( + createRscExposeDefinitions({ + './customInfoBundle': './src/components/infoBundle.ts', + }), + ).toEqual({ + './customInfoBundle': { + import: ['./src/components/infoBundle.ts'], + layer: 'react-server-components', + }, + }); + }); }); From 03b1c978b026bb2b9296c52216170dfba0f807b4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 15:46:37 +0000 Subject: [PATCH 276/324] refactor(rsc-mf): make callback bootstrap injection source-driven --- .../src/runtime/createRscExposeDefinitions.ts | 30 ++++++++----------- .../tests/createRscExposeDefinitions.test.ts | 8 ++--- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts index 243c2caae2ee..6a385d744864 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts @@ -6,10 +6,6 @@ const CALLBACK_BOOTSTRAP_PREFIX = './src/runtime/'; const USERLAND_EXPOSE_PREFIX = './'; const SOURCE_ENTRY_EXTENSION_PATTERN = /\.[cm]?[jt]sx?$/i; const RSC_LAYER = 'react-server-components'; -const CALLBACK_BOOTSTRAP_EXPOSE_KEY_PATTERN = - /^\.\/(?:RemoteClient[\w-]*|actions|nestedActions|defaultAction|actionBundle)$/; -const CALLBACK_BOOTSTRAP_IMPORT_PATH_PATTERN = - /\/(?:RemoteClient[\w-]*|actions|nestedActions|defaultAction|actionBundle)\.[cm]?[jt]sx?$/; const LOCAL_MODULE_SPECIFIER_PATTERN = /^\.{1,2}\//; const SOURCE_DIRECTIVE_PATTERN = /^\s*['"]use (?:client|server)['"]\s*;?/m; const EXPORT_FROM_SPECIFIER_PATTERN = @@ -25,6 +21,7 @@ const SOURCE_ENTRY_EXTENSIONS = [ '.cjs', ] as const; const REMOTE_PROJECT_ROOT = path.resolve(__dirname, '../..'); +const callbackDirectiveBySourceFile = new Map(); type ExposeImportInput = string | string[]; export type ExposeDefinitionInput = @@ -134,15 +131,9 @@ const createRscExpose = ( layer: RSC_LAYER, }) as const; -const shouldInjectCallbackBootstrap = ( - exposeKey: string, - importPaths: string[], -) => - CALLBACK_BOOTSTRAP_EXPOSE_KEY_PATTERN.test(exposeKey) || - importPaths.some( - importPath => - CALLBACK_BOOTSTRAP_IMPORT_PATH_PATTERN.test(importPath) || - referencesCallbackCapableSourceModule(importPath), +const shouldInjectCallbackBootstrap = (importPaths: string[]) => + importPaths.some(importPath => + referencesCallbackCapableSourceModule(importPath), ); const readSourceFile = (filePath: string) => { @@ -186,15 +177,21 @@ const referencesCallbackCapableSourceModule = (importPath: string) => { const visitedFiles = new Set(); const hasCallbackDirective = (filePath: string): boolean => { + const cachedResult = callbackDirectiveBySourceFile.get(filePath); + if (typeof cachedResult !== 'undefined') { + return cachedResult; + } if (visitedFiles.has(filePath)) { return false; } visitedFiles.add(filePath); const sourceText = readSourceFile(filePath); if (!sourceText) { + callbackDirectiveBySourceFile.set(filePath, false); return false; } if (SOURCE_DIRECTIVE_PATTERN.test(sourceText)) { + callbackDirectiveBySourceFile.set(filePath, true); return true; } @@ -211,10 +208,12 @@ const referencesCallbackCapableSourceModule = (importPath: string) => { filePath, ); if (childModuleFilePath && hasCallbackDirective(childModuleFilePath)) { + callbackDirectiveBySourceFile.set(filePath, true); return true; } } + callbackDirectiveBySourceFile.set(filePath, false); return false; }; @@ -343,10 +342,7 @@ export const createRscExposeDefinitions = ( createRscExpose( normalizedDefinition.importPaths, normalizedDefinition.exposeOverrides, - shouldInjectCallbackBootstrap( - exposeKey, - normalizedDefinition.importPaths, - ), + shouldInjectCallbackBootstrap(normalizedDefinition.importPaths), ), ]), ); diff --git a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts index 2ba671c8b693..65a84fa2a217 100644 --- a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts +++ b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts @@ -53,18 +53,14 @@ describe('createRscExposeDefinitions', () => { }); it('allows expose imports outside src root when path is relative', () => { - const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = - loadCreateRscExposeDefinitions(); + const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); expect( createRscExposeDefinitions({ './RemoteClientCounter': './app/components/RemoteClientCounter.tsx', }), ).toEqual({ './RemoteClientCounter': { - import: [ - CALLBACK_BOOTSTRAP_MODULE, - './app/components/RemoteClientCounter.tsx', - ], + import: ['./app/components/RemoteClientCounter.tsx'], layer: 'react-server-components', }, }); From 66f13600dce5590d59775a4edae01fe868a661a1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 15:48:19 +0000 Subject: [PATCH 277/324] refactor(rsc-mf): infer callback bootstrap from local import graph --- .../src/runtime/createRscExposeDefinitions.ts | 23 +++++++++++++------ .../tests/createRscExposeDefinitions.test.ts | 18 +++++++++++++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts index 6a385d744864..a872b7f66810 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts @@ -10,6 +10,8 @@ const LOCAL_MODULE_SPECIFIER_PATTERN = /^\.{1,2}\//; const SOURCE_DIRECTIVE_PATTERN = /^\s*['"]use (?:client|server)['"]\s*;?/m; const EXPORT_FROM_SPECIFIER_PATTERN = /export\s+(?:\*\s+from|\{[^}]*\}\s+from)\s*['"]([^'"]+)['"]/g; +const IMPORT_FROM_SPECIFIER_PATTERN = + /import\s+(?:type\s+)?(?:[^'";]+?\s+from\s+)?['"]([^'"]+)['"]/g; const SOURCE_ENTRY_EXTENSIONS = [ '.ts', '.tsx', @@ -195,19 +197,26 @@ const referencesCallbackCapableSourceModule = (importPath: string) => { return true; } - const exportFromMatches = sourceText.matchAll( - EXPORT_FROM_SPECIFIER_PATTERN, - ); - for (const match of exportFromMatches) { - const moduleSpecifier = match[1]; - if (!LOCAL_MODULE_SPECIFIER_PATTERN.test(moduleSpecifier)) { + const localSpecifierMatches = [ + ...sourceText.matchAll(EXPORT_FROM_SPECIFIER_PATTERN), + ...sourceText.matchAll(IMPORT_FROM_SPECIFIER_PATTERN), + ]; + for (const match of localSpecifierMatches) { + const [moduleSpecifier] = match.slice(1); + if ( + typeof moduleSpecifier !== 'string' || + !LOCAL_MODULE_SPECIFIER_PATTERN.test(moduleSpecifier) + ) { continue; } const childModuleFilePath = resolveUserlandImportPathToFile( moduleSpecifier, filePath, ); - if (childModuleFilePath && hasCallbackDirective(childModuleFilePath)) { + if (!childModuleFilePath) { + continue; + } + if (hasCallbackDirective(childModuleFilePath)) { callbackDirectiveBySourceFile.set(filePath, true); return true; } diff --git a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts index 65a84fa2a217..aaa8c530308e 100644 --- a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts +++ b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts @@ -359,6 +359,24 @@ describe('createRscExposeDefinitions', () => { }); }); + it('infers callback bootstrap from local import graph use client directives', () => { + const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = + loadCreateRscExposeDefinitions(); + expect( + createRscExposeDefinitions({ + './customRemoteServerCard': './src/components/RemoteServerCard.tsx', + }), + ).toEqual({ + './customRemoteServerCard': { + import: [ + CALLBACK_BOOTSTRAP_MODULE, + './src/components/RemoteServerCard.tsx', + ], + layer: 'react-server-components', + }, + }); + }); + it('keeps non-callback source modules free of callback bootstrap import', () => { const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); expect( From 29ec04531e480fab256d0cb226e5546b3b3678ae Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 15:52:55 +0000 Subject: [PATCH 278/324] test(rsc-mf): lock callback bootstrap expose coverage --- .../tests/moduleFederationConfig.test.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts b/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts index 3ee35077cb31..899a73bba5f1 100644 --- a/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts +++ b/tests/integration/rsc-mf/tests/moduleFederationConfig.test.ts @@ -17,6 +17,18 @@ const EXPECTED_REMOTE_EXPOSE_KEYS = [ './actionBundle', './infoBundle', ].sort(); +const EXPECTED_CALLBACK_BOOTSTRAPPED_EXPOSE_KEYS = [ + './RemoteClientCounter', + './RemoteClientBadge', + './RemoteServerCard', + './actions', + './nestedActions', + './defaultAction', + './actionBundle', +].sort(); +const EXPECTED_NON_CALLBACK_EXPOSE_KEYS = EXPECTED_REMOTE_EXPOSE_KEYS.filter( + exposeKey => !EXPECTED_CALLBACK_BOOTSTRAPPED_EXPOSE_KEYS.includes(exposeKey), +).sort(); const CALLBACK_BOOTSTRAP_IMPORT = './src/runtime/initServerCallback.ts'; const EXPECTED_SHARED_SCOPES = ['default', 'ssr', 'rsc']; @@ -119,6 +131,26 @@ describe('rsc-mf module federation config contracts', () => { } }); + it('applies callback bootstrap only to callback-capable expose entries', () => { + const remoteConfig = loadRemoteConfig(); + const exposeDefinitions = remoteConfig.exposes as Record< + string, + { + import?: string[]; + } + >; + + for (const exposeKey of EXPECTED_CALLBACK_BOOTSTRAPPED_EXPOSE_KEYS) { + const exposeImports = exposeDefinitions[exposeKey]?.import || []; + expect(exposeImports).toContain(CALLBACK_BOOTSTRAP_IMPORT); + } + + for (const exposeKey of EXPECTED_NON_CALLBACK_EXPOSE_KEYS) { + const exposeImports = exposeDefinitions[exposeKey]?.import || []; + expect(exposeImports).not.toContain(CALLBACK_BOOTSTRAP_IMPORT); + } + }); + it('keeps remote shared scopes and experiments aligned for rsc', () => { const remoteConfig = loadRemoteConfig(); const sharedScopes = remoteConfig.shared as Array< From 1259a7f13a0818b7c02f8edecdb4f33833017ae4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 15:58:25 +0000 Subject: [PATCH 279/324] refactor(rsc-mf): infer callback bootstrap from dynamic imports --- .../src/components/dynamicActionLoader.ts | 4 ++++ .../src/runtime/createRscExposeDefinitions.ts | 2 ++ .../tests/createRscExposeDefinitions.test.ts | 19 +++++++++++++++++++ 3 files changed, 25 insertions(+) create mode 100644 tests/integration/rsc-mf/remote/src/components/dynamicActionLoader.ts diff --git a/tests/integration/rsc-mf/remote/src/components/dynamicActionLoader.ts b/tests/integration/rsc-mf/remote/src/components/dynamicActionLoader.ts new file mode 100644 index 000000000000..b664bc789ac9 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/dynamicActionLoader.ts @@ -0,0 +1,4 @@ +export async function loadRemoteActionsDynamically() { + const actionModule = await import('./actions'); + return actionModule; +} diff --git a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts index a872b7f66810..a3d306108c87 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts @@ -12,6 +12,7 @@ const EXPORT_FROM_SPECIFIER_PATTERN = /export\s+(?:\*\s+from|\{[^}]*\}\s+from)\s*['"]([^'"]+)['"]/g; const IMPORT_FROM_SPECIFIER_PATTERN = /import\s+(?:type\s+)?(?:[^'";]+?\s+from\s+)?['"]([^'"]+)['"]/g; +const DYNAMIC_IMPORT_SPECIFIER_PATTERN = /import\(\s*['"]([^'"]+)['"]\s*\)/g; const SOURCE_ENTRY_EXTENSIONS = [ '.ts', '.tsx', @@ -200,6 +201,7 @@ const referencesCallbackCapableSourceModule = (importPath: string) => { const localSpecifierMatches = [ ...sourceText.matchAll(EXPORT_FROM_SPECIFIER_PATTERN), ...sourceText.matchAll(IMPORT_FROM_SPECIFIER_PATTERN), + ...sourceText.matchAll(DYNAMIC_IMPORT_SPECIFIER_PATTERN), ]; for (const match of localSpecifierMatches) { const [moduleSpecifier] = match.slice(1); diff --git a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts index aaa8c530308e..a5fbc0681fc4 100644 --- a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts +++ b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts @@ -377,6 +377,25 @@ describe('createRscExposeDefinitions', () => { }); }); + it('infers callback bootstrap from local dynamic import graph', () => { + const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = + loadCreateRscExposeDefinitions(); + expect( + createRscExposeDefinitions({ + './customDynamicActionLoader': + './src/components/dynamicActionLoader.ts', + }), + ).toEqual({ + './customDynamicActionLoader': { + import: [ + CALLBACK_BOOTSTRAP_MODULE, + './src/components/dynamicActionLoader.ts', + ], + layer: 'react-server-components', + }, + }); + }); + it('keeps non-callback source modules free of callback bootstrap import', () => { const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); expect( From aec0a142567b213bc925faecd67bec239c67c9a7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 16:02:22 +0000 Subject: [PATCH 280/324] fix(rsc-mf): ignore type-only imports in callback bootstrap inference --- .../src/components/typeOnlyActionImport.ts | 5 +++ .../src/runtime/createRscExposeDefinitions.ts | 40 +++++++++++++++---- .../tests/createRscExposeDefinitions.test.ts | 15 +++++++ 3 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 tests/integration/rsc-mf/remote/src/components/typeOnlyActionImport.ts diff --git a/tests/integration/rsc-mf/remote/src/components/typeOnlyActionImport.ts b/tests/integration/rsc-mf/remote/src/components/typeOnlyActionImport.ts new file mode 100644 index 000000000000..7221522d534f --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/typeOnlyActionImport.ts @@ -0,0 +1,5 @@ +import type * as RemoteActionsModule from './actions'; + +export type RemoteActionsModuleShape = typeof RemoteActionsModule; + +export const TYPE_ONLY_ACTION_IMPORT_MARKER = 'type-only-action-import'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts index a3d306108c87..614170bddfd7 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts @@ -9,9 +9,9 @@ const RSC_LAYER = 'react-server-components'; const LOCAL_MODULE_SPECIFIER_PATTERN = /^\.{1,2}\//; const SOURCE_DIRECTIVE_PATTERN = /^\s*['"]use (?:client|server)['"]\s*;?/m; const EXPORT_FROM_SPECIFIER_PATTERN = - /export\s+(?:\*\s+from|\{[^}]*\}\s+from)\s*['"]([^'"]+)['"]/g; + /export\s+(type\s+)?(?:\*\s+from|\{[^}]*\}\s+from)\s*['"]([^'"]+)['"]/g; const IMPORT_FROM_SPECIFIER_PATTERN = - /import\s+(?:type\s+)?(?:[^'";]+?\s+from\s+)?['"]([^'"]+)['"]/g; + /import\s+(type\s+)?(?:[^'";]+?\s+from\s+)?['"]([^'"]+)['"]/g; const DYNAMIC_IMPORT_SPECIFIER_PATTERN = /import\(\s*['"]([^'"]+)['"]\s*\)/g; const SOURCE_ENTRY_EXTENSIONS = [ '.ts', @@ -139,6 +139,11 @@ const shouldInjectCallbackBootstrap = (importPaths: string[]) => referencesCallbackCapableSourceModule(importPath), ); +interface SourceModuleSpecifier { + moduleSpecifier: string; + typeOnly: boolean; +} + const readSourceFile = (filePath: string) => { try { return fs.readFileSync(filePath, 'utf8'); @@ -198,14 +203,33 @@ const referencesCallbackCapableSourceModule = (importPath: string) => { return true; } - const localSpecifierMatches = [ - ...sourceText.matchAll(EXPORT_FROM_SPECIFIER_PATTERN), - ...sourceText.matchAll(IMPORT_FROM_SPECIFIER_PATTERN), - ...sourceText.matchAll(DYNAMIC_IMPORT_SPECIFIER_PATTERN), + const localSpecifierMatches: SourceModuleSpecifier[] = [ + ...Array.from( + sourceText.matchAll(EXPORT_FROM_SPECIFIER_PATTERN), + match => ({ + moduleSpecifier: match[2], + typeOnly: Boolean(match[1]), + }), + ), + ...Array.from( + sourceText.matchAll(IMPORT_FROM_SPECIFIER_PATTERN), + match => ({ + moduleSpecifier: match[2], + typeOnly: Boolean(match[1]), + }), + ), + ...Array.from( + sourceText.matchAll(DYNAMIC_IMPORT_SPECIFIER_PATTERN), + match => ({ + moduleSpecifier: match[1], + typeOnly: false, + }), + ), ]; - for (const match of localSpecifierMatches) { - const [moduleSpecifier] = match.slice(1); + + for (const { moduleSpecifier, typeOnly } of localSpecifierMatches) { if ( + typeOnly || typeof moduleSpecifier !== 'string' || !LOCAL_MODULE_SPECIFIER_PATTERN.test(moduleSpecifier) ) { diff --git a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts index a5fbc0681fc4..83edb9e70bdd 100644 --- a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts +++ b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts @@ -396,6 +396,21 @@ describe('createRscExposeDefinitions', () => { }); }); + it('does not infer callback bootstrap from type-only local imports', () => { + const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); + expect( + createRscExposeDefinitions({ + './customTypeOnlyActionImport': + './src/components/typeOnlyActionImport.ts', + }), + ).toEqual({ + './customTypeOnlyActionImport': { + import: ['./src/components/typeOnlyActionImport.ts'], + layer: 'react-server-components', + }, + }); + }); + it('keeps non-callback source modules free of callback bootstrap import', () => { const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); expect( From f8adc5dc6b9892ab8537660411def580cd4d021e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 16:08:33 +0000 Subject: [PATCH 281/324] fix(rsc-mf): distinguish type-only and runtime specifier edges --- .../components/exportTypeOnlyActionBridge.ts | 3 + .../components/mixedTypeValueActionImport.ts | 6 ++ .../components/namedTypeOnlyActionImport.ts | 5 + .../src/runtime/createRscExposeDefinitions.ts | 98 ++++++++++++++++++- .../tests/createRscExposeDefinitions.test.ts | 49 ++++++++++ 5 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 tests/integration/rsc-mf/remote/src/components/exportTypeOnlyActionBridge.ts create mode 100644 tests/integration/rsc-mf/remote/src/components/mixedTypeValueActionImport.ts create mode 100644 tests/integration/rsc-mf/remote/src/components/namedTypeOnlyActionImport.ts diff --git a/tests/integration/rsc-mf/remote/src/components/exportTypeOnlyActionBridge.ts b/tests/integration/rsc-mf/remote/src/components/exportTypeOnlyActionBridge.ts new file mode 100644 index 000000000000..7658e440e485 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/exportTypeOnlyActionBridge.ts @@ -0,0 +1,3 @@ +export type { incrementRemoteCount, remoteActionEcho } from './actions'; + +export const EXPORT_TYPE_ONLY_ACTION_BRIDGE = 'export-type-only-action-bridge'; diff --git a/tests/integration/rsc-mf/remote/src/components/mixedTypeValueActionImport.ts b/tests/integration/rsc-mf/remote/src/components/mixedTypeValueActionImport.ts new file mode 100644 index 000000000000..3ff79b65f56c --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/mixedTypeValueActionImport.ts @@ -0,0 +1,6 @@ +import { type incrementRemoteCount, remoteActionEcho } from './actions'; + +export const runMixedTypeValueActionImport = async () => + remoteActionEcho('mixed-type-value-action-import'); + +export type IncrementRemoteCountActionFromMixed = typeof incrementRemoteCount; diff --git a/tests/integration/rsc-mf/remote/src/components/namedTypeOnlyActionImport.ts b/tests/integration/rsc-mf/remote/src/components/namedTypeOnlyActionImport.ts new file mode 100644 index 000000000000..bc89f83705f2 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/namedTypeOnlyActionImport.ts @@ -0,0 +1,5 @@ +import type { incrementRemoteCount } from './actions'; + +export type IncrementRemoteCountAction = typeof incrementRemoteCount; + +export const NAMED_TYPE_ONLY_ACTION_IMPORT = 'named-type-only-action-import'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts index 614170bddfd7..8496630f94d3 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts @@ -9,9 +9,10 @@ const RSC_LAYER = 'react-server-components'; const LOCAL_MODULE_SPECIFIER_PATTERN = /^\.{1,2}\//; const SOURCE_DIRECTIVE_PATTERN = /^\s*['"]use (?:client|server)['"]\s*;?/m; const EXPORT_FROM_SPECIFIER_PATTERN = - /export\s+(type\s+)?(?:\*\s+from|\{[^}]*\}\s+from)\s*['"]([^'"]+)['"]/g; + /export\s+([^'";]+?)\s+from\s+['"]([^'"]+)['"]/g; const IMPORT_FROM_SPECIFIER_PATTERN = - /import\s+(type\s+)?(?:[^'";]+?\s+from\s+)?['"]([^'"]+)['"]/g; + /import\s+([^'";]+?)\s+from\s+['"]([^'"]+)['"]/g; +const SIDE_EFFECT_IMPORT_SPECIFIER_PATTERN = /import\s+['"]([^'"]+)['"]/g; const DYNAMIC_IMPORT_SPECIFIER_PATTERN = /import\(\s*['"]([^'"]+)['"]\s*\)/g; const SOURCE_ENTRY_EXTENSIONS = [ '.ts', @@ -144,6 +145,88 @@ interface SourceModuleSpecifier { typeOnly: boolean; } +const NAMED_SPECIFIER_LIST_PATTERN = /\{([^}]*)\}/; +const TYPE_ONLY_PREFIX = 'type '; + +const hasRuntimeNamedSpecifiers = ( + clause: string, + forceTypeOnlyNamedSpecifiers: boolean, +) => { + const namedSpecifierMatch = clause.match(NAMED_SPECIFIER_LIST_PATTERN); + if (!namedSpecifierMatch) { + return false; + } + if (forceTypeOnlyNamedSpecifiers) { + return false; + } + const namedSpecifiers = namedSpecifierMatch[1] + .split(',') + .map(specifier => specifier.trim()) + .filter(Boolean); + if (namedSpecifiers.length === 0) { + return true; + } + return namedSpecifiers.some( + specifier => !specifier.startsWith(TYPE_ONLY_PREFIX), + ); +}; + +const hasRuntimeBareSpecifiers = ( + clause: string, + forceTypeOnlyNamedSpecifiers: boolean, +) => { + const bareSpecifiers = clause + .replace(NAMED_SPECIFIER_LIST_PATTERN, '') + .replaceAll(',', ' ') + .split(/\s+/) + .map(specifier => specifier.trim()) + .filter(Boolean); + if (bareSpecifiers.length === 0) { + return false; + } + if (bareSpecifiers.length >= 2 && bareSpecifiers[0] === 'as') { + return true; + } + if ( + forceTypeOnlyNamedSpecifiers && + bareSpecifiers.every(specifier => specifier === 'type') + ) { + return false; + } + return !bareSpecifiers.every(specifier => specifier === 'type'); +}; + +const isTypeOnlyImportClause = (clause: string) => { + const normalizedClause = clause.trim(); + if (!normalizedClause) { + return false; + } + if (normalizedClause.startsWith(TYPE_ONLY_PREFIX)) { + return true; + } + if (hasRuntimeNamedSpecifiers(normalizedClause, false)) { + return false; + } + return !hasRuntimeBareSpecifiers(normalizedClause, false); +}; + +const isTypeOnlyExportClause = (clause: string) => { + const normalizedClause = clause.trim(); + if (!normalizedClause) { + return false; + } + if (normalizedClause.startsWith(TYPE_ONLY_PREFIX)) { + return true; + } + if (normalizedClause.startsWith('*')) { + return false; + } + if (hasRuntimeNamedSpecifiers(normalizedClause, false)) { + return false; + } + return !hasRuntimeBareSpecifiers(normalizedClause, false); +}; + const readSourceFile = (filePath: string) => { try { return fs.readFileSync(filePath, 'utf8'); @@ -208,14 +291,21 @@ const referencesCallbackCapableSourceModule = (importPath: string) => { sourceText.matchAll(EXPORT_FROM_SPECIFIER_PATTERN), match => ({ moduleSpecifier: match[2], - typeOnly: Boolean(match[1]), + typeOnly: isTypeOnlyExportClause(match[1]), }), ), ...Array.from( sourceText.matchAll(IMPORT_FROM_SPECIFIER_PATTERN), match => ({ moduleSpecifier: match[2], - typeOnly: Boolean(match[1]), + typeOnly: isTypeOnlyImportClause(match[1]), + }), + ), + ...Array.from( + sourceText.matchAll(SIDE_EFFECT_IMPORT_SPECIFIER_PATTERN), + match => ({ + moduleSpecifier: match[1], + typeOnly: false, }), ), ...Array.from( diff --git a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts index 83edb9e70bdd..59aebaaec4c4 100644 --- a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts +++ b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts @@ -411,6 +411,55 @@ describe('createRscExposeDefinitions', () => { }); }); + it('does not infer callback bootstrap from named type-only imports', () => { + const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); + expect( + createRscExposeDefinitions({ + './customNamedTypeOnlyActionImport': + './src/components/namedTypeOnlyActionImport.ts', + }), + ).toEqual({ + './customNamedTypeOnlyActionImport': { + import: ['./src/components/namedTypeOnlyActionImport.ts'], + layer: 'react-server-components', + }, + }); + }); + + it('does not infer callback bootstrap from type-only re-export clauses', () => { + const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); + expect( + createRscExposeDefinitions({ + './customExportTypeOnlyActionBridge': + './src/components/exportTypeOnlyActionBridge.ts', + }), + ).toEqual({ + './customExportTypeOnlyActionBridge': { + import: ['./src/components/exportTypeOnlyActionBridge.ts'], + layer: 'react-server-components', + }, + }); + }); + + it('infers callback bootstrap when import clause includes runtime bindings', () => { + const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = + loadCreateRscExposeDefinitions(); + expect( + createRscExposeDefinitions({ + './customMixedTypeValueActionImport': + './src/components/mixedTypeValueActionImport.ts', + }), + ).toEqual({ + './customMixedTypeValueActionImport': { + import: [ + CALLBACK_BOOTSTRAP_MODULE, + './src/components/mixedTypeValueActionImport.ts', + ], + layer: 'react-server-components', + }, + }); + }); + it('keeps non-callback source modules free of callback bootstrap import', () => { const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); expect( From 2b24a635e7f9da5715d6e9f260cc41fb9b82e273 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 16:14:48 +0000 Subject: [PATCH 282/324] test(rsc-mf): cover side-effect import callback inference boundaries --- .../src/components/sideEffectActionImport.ts | 3 ++ .../sideEffectTypeOnlyBridgeImport.ts | 4 +++ .../tests/createRscExposeDefinitions.test.ts | 34 +++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 tests/integration/rsc-mf/remote/src/components/sideEffectActionImport.ts create mode 100644 tests/integration/rsc-mf/remote/src/components/sideEffectTypeOnlyBridgeImport.ts diff --git a/tests/integration/rsc-mf/remote/src/components/sideEffectActionImport.ts b/tests/integration/rsc-mf/remote/src/components/sideEffectActionImport.ts new file mode 100644 index 000000000000..cddaff1876a1 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/sideEffectActionImport.ts @@ -0,0 +1,3 @@ +import './actions'; + +export const SIDE_EFFECT_ACTION_IMPORT_MARKER = 'side-effect-action-import'; diff --git a/tests/integration/rsc-mf/remote/src/components/sideEffectTypeOnlyBridgeImport.ts b/tests/integration/rsc-mf/remote/src/components/sideEffectTypeOnlyBridgeImport.ts new file mode 100644 index 000000000000..5c4979602bfe --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/sideEffectTypeOnlyBridgeImport.ts @@ -0,0 +1,4 @@ +import './typeOnlyActionImport'; + +export const SIDE_EFFECT_TYPE_ONLY_BRIDGE_IMPORT_MARKER = + 'side-effect-type-only-bridge-import'; diff --git a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts index 59aebaaec4c4..ae25240cddee 100644 --- a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts +++ b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts @@ -460,6 +460,40 @@ describe('createRscExposeDefinitions', () => { }); }); + it('infers callback bootstrap from local side-effect imports of action modules', () => { + const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = + loadCreateRscExposeDefinitions(); + expect( + createRscExposeDefinitions({ + './customSideEffectActionImport': + './src/components/sideEffectActionImport.ts', + }), + ).toEqual({ + './customSideEffectActionImport': { + import: [ + CALLBACK_BOOTSTRAP_MODULE, + './src/components/sideEffectActionImport.ts', + ], + layer: 'react-server-components', + }, + }); + }); + + it('does not infer callback bootstrap from side-effect imports without callback directives', () => { + const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); + expect( + createRscExposeDefinitions({ + './customSideEffectTypeOnlyBridgeImport': + './src/components/sideEffectTypeOnlyBridgeImport.ts', + }), + ).toEqual({ + './customSideEffectTypeOnlyBridgeImport': { + import: ['./src/components/sideEffectTypeOnlyBridgeImport.ts'], + layer: 'react-server-components', + }, + }); + }); + it('keeps non-callback source modules free of callback bootstrap import', () => { const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); expect( From 6d2e37d77bf3ef6c56dcd242f8e974a258f6febb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 16:19:24 +0000 Subject: [PATCH 283/324] test(rsc-mf): cover namespace and export-type-all inference edges --- .../components/exportNamespaceActionBridge.ts | 3 ++ .../components/exportTypeAllActionBridge.ts | 3 ++ .../tests/createRscExposeDefinitions.test.ts | 34 +++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 tests/integration/rsc-mf/remote/src/components/exportNamespaceActionBridge.ts create mode 100644 tests/integration/rsc-mf/remote/src/components/exportTypeAllActionBridge.ts diff --git a/tests/integration/rsc-mf/remote/src/components/exportNamespaceActionBridge.ts b/tests/integration/rsc-mf/remote/src/components/exportNamespaceActionBridge.ts new file mode 100644 index 000000000000..78a480b14d74 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/exportNamespaceActionBridge.ts @@ -0,0 +1,3 @@ +export * as remoteActionsNamespace from './actions'; + +export const EXPORT_NAMESPACE_ACTION_BRIDGE = 'export-namespace-action-bridge'; diff --git a/tests/integration/rsc-mf/remote/src/components/exportTypeAllActionBridge.ts b/tests/integration/rsc-mf/remote/src/components/exportTypeAllActionBridge.ts new file mode 100644 index 000000000000..646ab1d3f0f0 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/exportTypeAllActionBridge.ts @@ -0,0 +1,3 @@ +export type * from './actions'; + +export const EXPORT_TYPE_ALL_ACTION_BRIDGE = 'export-type-all-action-bridge'; diff --git a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts index ae25240cddee..55605a9af7d3 100644 --- a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts +++ b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts @@ -441,6 +441,21 @@ describe('createRscExposeDefinitions', () => { }); }); + it('does not infer callback bootstrap from export type * re-export clauses', () => { + const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); + expect( + createRscExposeDefinitions({ + './customExportTypeAllActionBridge': + './src/components/exportTypeAllActionBridge.ts', + }), + ).toEqual({ + './customExportTypeAllActionBridge': { + import: ['./src/components/exportTypeAllActionBridge.ts'], + layer: 'react-server-components', + }, + }); + }); + it('infers callback bootstrap when import clause includes runtime bindings', () => { const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = loadCreateRscExposeDefinitions(); @@ -460,6 +475,25 @@ describe('createRscExposeDefinitions', () => { }); }); + it('infers callback bootstrap from export namespace runtime bindings', () => { + const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = + loadCreateRscExposeDefinitions(); + expect( + createRscExposeDefinitions({ + './customExportNamespaceActionBridge': + './src/components/exportNamespaceActionBridge.ts', + }), + ).toEqual({ + './customExportNamespaceActionBridge': { + import: [ + CALLBACK_BOOTSTRAP_MODULE, + './src/components/exportNamespaceActionBridge.ts', + ], + layer: 'react-server-components', + }, + }); + }); + it('infers callback bootstrap from local side-effect imports of action modules', () => { const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = loadCreateRscExposeDefinitions(); From 11d6e462e97e170e73d090af4cca6016993fd250 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 16:24:31 +0000 Subject: [PATCH 284/324] test(rsc-mf): cover inline export type-only and mixed bindings --- .../exportInlineMixedActionBridge.ts | 4 +++ .../exportInlineTypeOnlyActionBridge.ts | 5 +++ .../tests/createRscExposeDefinitions.test.ts | 34 +++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 tests/integration/rsc-mf/remote/src/components/exportInlineMixedActionBridge.ts create mode 100644 tests/integration/rsc-mf/remote/src/components/exportInlineTypeOnlyActionBridge.ts diff --git a/tests/integration/rsc-mf/remote/src/components/exportInlineMixedActionBridge.ts b/tests/integration/rsc-mf/remote/src/components/exportInlineMixedActionBridge.ts new file mode 100644 index 000000000000..b12cfe957b9f --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/exportInlineMixedActionBridge.ts @@ -0,0 +1,4 @@ +export { type incrementRemoteCount, remoteActionEcho } from './actions'; + +export const EXPORT_INLINE_MIXED_ACTION_BRIDGE = + 'export-inline-mixed-action-bridge'; diff --git a/tests/integration/rsc-mf/remote/src/components/exportInlineTypeOnlyActionBridge.ts b/tests/integration/rsc-mf/remote/src/components/exportInlineTypeOnlyActionBridge.ts new file mode 100644 index 000000000000..14453183a8c5 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/exportInlineTypeOnlyActionBridge.ts @@ -0,0 +1,5 @@ +// biome-ignore lint/style/useExportType: keep inline type-only syntax for parser coverage +export { type incrementRemoteCount } from './actions'; + +export const EXPORT_INLINE_TYPE_ONLY_ACTION_BRIDGE = + 'export-inline-type-only-action-bridge'; diff --git a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts index 55605a9af7d3..d19ddbbbe6a3 100644 --- a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts +++ b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts @@ -456,6 +456,21 @@ describe('createRscExposeDefinitions', () => { }); }); + it('does not infer callback bootstrap from inline type-only named re-export clauses', () => { + const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); + expect( + createRscExposeDefinitions({ + './customExportInlineTypeOnlyActionBridge': + './src/components/exportInlineTypeOnlyActionBridge.ts', + }), + ).toEqual({ + './customExportInlineTypeOnlyActionBridge': { + import: ['./src/components/exportInlineTypeOnlyActionBridge.ts'], + layer: 'react-server-components', + }, + }); + }); + it('infers callback bootstrap when import clause includes runtime bindings', () => { const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = loadCreateRscExposeDefinitions(); @@ -494,6 +509,25 @@ describe('createRscExposeDefinitions', () => { }); }); + it('infers callback bootstrap from mixed inline export clauses with runtime bindings', () => { + const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = + loadCreateRscExposeDefinitions(); + expect( + createRscExposeDefinitions({ + './customExportInlineMixedActionBridge': + './src/components/exportInlineMixedActionBridge.ts', + }), + ).toEqual({ + './customExportInlineMixedActionBridge': { + import: [ + CALLBACK_BOOTSTRAP_MODULE, + './src/components/exportInlineMixedActionBridge.ts', + ], + layer: 'react-server-components', + }, + }); + }); + it('infers callback bootstrap from local side-effect imports of action modules', () => { const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = loadCreateRscExposeDefinitions(); From e40ab15a4fd740c5b4f71b969dcf1a1145fca199 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 16:28:35 +0000 Subject: [PATCH 285/324] fix(rsc-mf): infer callback bootstrap through commonjs require edges --- .../src/components/commonJsActionRequire.cts | 4 +++ .../commonJsTypeOnlyBridgeRequire.cts | 5 +++ .../src/runtime/createRscExposeDefinitions.ts | 9 +++++ .../tests/createRscExposeDefinitions.test.ts | 34 +++++++++++++++++++ 4 files changed, 52 insertions(+) create mode 100644 tests/integration/rsc-mf/remote/src/components/commonJsActionRequire.cts create mode 100644 tests/integration/rsc-mf/remote/src/components/commonJsTypeOnlyBridgeRequire.cts diff --git a/tests/integration/rsc-mf/remote/src/components/commonJsActionRequire.cts b/tests/integration/rsc-mf/remote/src/components/commonJsActionRequire.cts new file mode 100644 index 000000000000..ed28ba1d3c59 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/commonJsActionRequire.cts @@ -0,0 +1,4 @@ +const remoteActions = require('./actions') as typeof import('./actions'); + +export const runCommonJsRemoteActionEcho = async () => + remoteActions.remoteActionEcho('commonjs-action-require'); diff --git a/tests/integration/rsc-mf/remote/src/components/commonJsTypeOnlyBridgeRequire.cts b/tests/integration/rsc-mf/remote/src/components/commonJsTypeOnlyBridgeRequire.cts new file mode 100644 index 000000000000..bf72a20560c3 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/commonJsTypeOnlyBridgeRequire.cts @@ -0,0 +1,5 @@ +const typeOnlyActionImportBridge = + require('./typeOnlyActionImport') as typeof import('./typeOnlyActionImport'); + +export const COMMON_JS_TYPE_ONLY_BRIDGE_REQUIRE_MARKER = + typeOnlyActionImportBridge.TYPE_ONLY_ACTION_IMPORT_MARKER; diff --git a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts index 8496630f94d3..79e71f56f547 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts @@ -14,6 +14,8 @@ const IMPORT_FROM_SPECIFIER_PATTERN = /import\s+([^'";]+?)\s+from\s+['"]([^'"]+)['"]/g; const SIDE_EFFECT_IMPORT_SPECIFIER_PATTERN = /import\s+['"]([^'"]+)['"]/g; const DYNAMIC_IMPORT_SPECIFIER_PATTERN = /import\(\s*['"]([^'"]+)['"]\s*\)/g; +const COMMONJS_REQUIRE_SPECIFIER_PATTERN = + /(?:^|[^.\w$])require\(\s*['"]([^'"]+)['"]\s*\)/gm; const SOURCE_ENTRY_EXTENSIONS = [ '.ts', '.tsx', @@ -315,6 +317,13 @@ const referencesCallbackCapableSourceModule = (importPath: string) => { typeOnly: false, }), ), + ...Array.from( + sourceText.matchAll(COMMONJS_REQUIRE_SPECIFIER_PATTERN), + match => ({ + moduleSpecifier: match[1], + typeOnly: false, + }), + ), ]; for (const { moduleSpecifier, typeOnly } of localSpecifierMatches) { diff --git a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts index d19ddbbbe6a3..05cb2ec08d42 100644 --- a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts +++ b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts @@ -396,6 +396,40 @@ describe('createRscExposeDefinitions', () => { }); }); + it('infers callback bootstrap from commonjs require graph', () => { + const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = + loadCreateRscExposeDefinitions(); + expect( + createRscExposeDefinitions({ + './customCommonJsActionRequire': + './src/components/commonJsActionRequire.cts', + }), + ).toEqual({ + './customCommonJsActionRequire': { + import: [ + CALLBACK_BOOTSTRAP_MODULE, + './src/components/commonJsActionRequire.cts', + ], + layer: 'react-server-components', + }, + }); + }); + + it('does not infer callback bootstrap from commonjs require graph without callback directives', () => { + const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); + expect( + createRscExposeDefinitions({ + './customCommonJsTypeOnlyBridgeRequire': + './src/components/commonJsTypeOnlyBridgeRequire.cts', + }), + ).toEqual({ + './customCommonJsTypeOnlyBridgeRequire': { + import: ['./src/components/commonJsTypeOnlyBridgeRequire.cts'], + layer: 'react-server-components', + }, + }); + }); + it('does not infer callback bootstrap from type-only local imports', () => { const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); expect( From 585a17e07a61d1b93be0417331f92fb7a3128ecc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 16:33:47 +0000 Subject: [PATCH 286/324] refactor(rsc-mf): use TypeScript AST for callback edge detection --- .../src/runtime/createRscExposeDefinitions.ts | 218 +++++++++--------- 1 file changed, 114 insertions(+), 104 deletions(-) diff --git a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts index 79e71f56f547..ffbddbbdca45 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts @@ -1,5 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; +import * as ts from 'typescript'; const CALLBACK_BOOTSTRAP_IMPORT = './src/runtime/initServerCallback.ts'; const CALLBACK_BOOTSTRAP_PREFIX = './src/runtime/'; @@ -8,14 +9,6 @@ const SOURCE_ENTRY_EXTENSION_PATTERN = /\.[cm]?[jt]sx?$/i; const RSC_LAYER = 'react-server-components'; const LOCAL_MODULE_SPECIFIER_PATTERN = /^\.{1,2}\//; const SOURCE_DIRECTIVE_PATTERN = /^\s*['"]use (?:client|server)['"]\s*;?/m; -const EXPORT_FROM_SPECIFIER_PATTERN = - /export\s+([^'";]+?)\s+from\s+['"]([^'"]+)['"]/g; -const IMPORT_FROM_SPECIFIER_PATTERN = - /import\s+([^'";]+?)\s+from\s+['"]([^'"]+)['"]/g; -const SIDE_EFFECT_IMPORT_SPECIFIER_PATTERN = /import\s+['"]([^'"]+)['"]/g; -const DYNAMIC_IMPORT_SPECIFIER_PATTERN = /import\(\s*['"]([^'"]+)['"]\s*\)/g; -const COMMONJS_REQUIRE_SPECIFIER_PATTERN = - /(?:^|[^.\w$])require\(\s*['"]([^'"]+)['"]\s*\)/gm; const SOURCE_ENTRY_EXTENSIONS = [ '.ts', '.tsx', @@ -147,86 +140,136 @@ interface SourceModuleSpecifier { typeOnly: boolean; } -const NAMED_SPECIFIER_LIST_PATTERN = /\{([^}]*)\}/; -const TYPE_ONLY_PREFIX = 'type '; - -const hasRuntimeNamedSpecifiers = ( - clause: string, - forceTypeOnlyNamedSpecifiers: boolean, -) => { - const namedSpecifierMatch = clause.match(NAMED_SPECIFIER_LIST_PATTERN); - if (!namedSpecifierMatch) { - return false; - } - if (forceTypeOnlyNamedSpecifiers) { - return false; - } - const namedSpecifiers = namedSpecifierMatch[1] - .split(',') - .map(specifier => specifier.trim()) - .filter(Boolean); - if (namedSpecifiers.length === 0) { - return true; +const getScriptKindByFilePath = (filePath: string) => { + const extension = path.extname(filePath).toLowerCase(); + switch (extension) { + case '.tsx': + return ts.ScriptKind.TSX; + case '.jsx': + return ts.ScriptKind.JSX; + case '.js': + case '.mjs': + case '.cjs': + return ts.ScriptKind.JS; + default: + return ts.ScriptKind.TS; } - return namedSpecifiers.some( - specifier => !specifier.startsWith(TYPE_ONLY_PREFIX), - ); }; -const hasRuntimeBareSpecifiers = ( - clause: string, - forceTypeOnlyNamedSpecifiers: boolean, -) => { - const bareSpecifiers = clause - .replace(NAMED_SPECIFIER_LIST_PATTERN, '') - .replaceAll(',', ' ') - .split(/\s+/) - .map(specifier => specifier.trim()) - .filter(Boolean); - if (bareSpecifiers.length === 0) { +const getImportDeclarationTypeOnlyState = (node: ts.ImportDeclaration) => { + const importClause = node.importClause; + if (!importClause) { return false; } - if (bareSpecifiers.length >= 2 && bareSpecifiers[0] === 'as') { + if (importClause.isTypeOnly) { return true; } - if ( - forceTypeOnlyNamedSpecifiers && - bareSpecifiers.every(specifier => specifier === 'type') - ) { + if (importClause.name) { return false; } - return !bareSpecifiers.every(specifier => specifier === 'type'); -}; - -const isTypeOnlyImportClause = (clause: string) => { - const normalizedClause = clause.trim(); - if (!normalizedClause) { + const namedBindings = importClause.namedBindings; + if (!namedBindings) { return false; } - if (normalizedClause.startsWith(TYPE_ONLY_PREFIX)) { - return true; - } - if (hasRuntimeNamedSpecifiers(normalizedClause, false)) { + if (ts.isNamespaceImport(namedBindings)) { return false; } - return !hasRuntimeBareSpecifiers(normalizedClause, false); + if (ts.isNamedImports(namedBindings)) { + if (namedBindings.elements.length === 0) { + return false; + } + return namedBindings.elements.every(element => element.isTypeOnly); + } + return false; }; -const isTypeOnlyExportClause = (clause: string) => { - const normalizedClause = clause.trim(); - if (!normalizedClause) { - return false; - } - if (normalizedClause.startsWith(TYPE_ONLY_PREFIX)) { +const getExportDeclarationTypeOnlyState = (node: ts.ExportDeclaration) => { + if (node.isTypeOnly) { return true; } - if (normalizedClause.startsWith('*')) { + if (!node.exportClause) { return false; } - if (hasRuntimeNamedSpecifiers(normalizedClause, false)) { + if (ts.isNamespaceExport(node.exportClause)) { return false; } - return !hasRuntimeBareSpecifiers(normalizedClause, false); + if (ts.isNamedExports(node.exportClause)) { + if (node.exportClause.elements.length === 0) { + return false; + } + return node.exportClause.elements.every(element => element.isTypeOnly); + } + return false; +}; + +const collectLocalModuleSpecifiers = ( + filePath: string, + sourceText: string, +): SourceModuleSpecifier[] => { + const sourceFile = ts.createSourceFile( + filePath, + sourceText, + ts.ScriptTarget.Latest, + true, + getScriptKindByFilePath(filePath), + ); + const moduleSpecifiers: SourceModuleSpecifier[] = []; + + const collectNodeSpecifiers = (node: ts.Node): void => { + if (ts.isImportDeclaration(node)) { + if (ts.isStringLiteralLike(node.moduleSpecifier)) { + moduleSpecifiers.push({ + moduleSpecifier: node.moduleSpecifier.text, + typeOnly: getImportDeclarationTypeOnlyState(node), + }); + } + return; + } + + if (ts.isExportDeclaration(node)) { + if ( + node.moduleSpecifier && + ts.isStringLiteralLike(node.moduleSpecifier) + ) { + moduleSpecifiers.push({ + moduleSpecifier: node.moduleSpecifier.text, + typeOnly: getExportDeclarationTypeOnlyState(node), + }); + } + return; + } + + if (ts.isCallExpression(node)) { + const [firstArgument] = node.arguments; + if (!firstArgument || !ts.isStringLiteralLike(firstArgument)) { + return; + } + if (node.expression.kind === ts.SyntaxKind.ImportKeyword) { + moduleSpecifiers.push({ + moduleSpecifier: firstArgument.text, + typeOnly: false, + }); + return; + } + if ( + ts.isIdentifier(node.expression) && + node.expression.text === 'require' + ) { + moduleSpecifiers.push({ + moduleSpecifier: firstArgument.text, + typeOnly: false, + }); + } + } + }; + + const walkNode = (node: ts.Node): void => { + collectNodeSpecifiers(node); + ts.forEachChild(node, walkNode); + }; + walkNode(sourceFile); + + return moduleSpecifiers; }; const readSourceFile = (filePath: string) => { @@ -288,43 +331,10 @@ const referencesCallbackCapableSourceModule = (importPath: string) => { return true; } - const localSpecifierMatches: SourceModuleSpecifier[] = [ - ...Array.from( - sourceText.matchAll(EXPORT_FROM_SPECIFIER_PATTERN), - match => ({ - moduleSpecifier: match[2], - typeOnly: isTypeOnlyExportClause(match[1]), - }), - ), - ...Array.from( - sourceText.matchAll(IMPORT_FROM_SPECIFIER_PATTERN), - match => ({ - moduleSpecifier: match[2], - typeOnly: isTypeOnlyImportClause(match[1]), - }), - ), - ...Array.from( - sourceText.matchAll(SIDE_EFFECT_IMPORT_SPECIFIER_PATTERN), - match => ({ - moduleSpecifier: match[1], - typeOnly: false, - }), - ), - ...Array.from( - sourceText.matchAll(DYNAMIC_IMPORT_SPECIFIER_PATTERN), - match => ({ - moduleSpecifier: match[1], - typeOnly: false, - }), - ), - ...Array.from( - sourceText.matchAll(COMMONJS_REQUIRE_SPECIFIER_PATTERN), - match => ({ - moduleSpecifier: match[1], - typeOnly: false, - }), - ), - ]; + const localSpecifierMatches = collectLocalModuleSpecifiers( + filePath, + sourceText, + ); for (const { moduleSpecifier, typeOnly } of localSpecifierMatches) { if ( From d7b4139339f4e66c15e9ef411ec0b76763379f4d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 16:36:44 +0000 Subject: [PATCH 287/324] docs(rsc-mf): document AST-based callback edge detection rationale --- .../components/commentedActionReference.ts | 5 ++++ .../stringLiteralActionReference.ts | 9 ++++++ .../src/runtime/createRscExposeDefinitions.ts | 2 ++ .../tests/createRscExposeDefinitions.test.ts | 30 +++++++++++++++++++ 4 files changed, 46 insertions(+) create mode 100644 tests/integration/rsc-mf/remote/src/components/commentedActionReference.ts create mode 100644 tests/integration/rsc-mf/remote/src/components/stringLiteralActionReference.ts diff --git a/tests/integration/rsc-mf/remote/src/components/commentedActionReference.ts b/tests/integration/rsc-mf/remote/src/components/commentedActionReference.ts new file mode 100644 index 000000000000..9979562ebef4 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/commentedActionReference.ts @@ -0,0 +1,5 @@ +// require('./actions'); +// import('./actions'); +// export * from './actions'; + +export const COMMENTED_ACTION_REFERENCE_MARKER = 'commented-action-reference'; diff --git a/tests/integration/rsc-mf/remote/src/components/stringLiteralActionReference.ts b/tests/integration/rsc-mf/remote/src/components/stringLiteralActionReference.ts new file mode 100644 index 000000000000..1ba5cbecd289 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/stringLiteralActionReference.ts @@ -0,0 +1,9 @@ +const actionReferenceText = "require('./actions')"; +const dynamicImportText = "import('./actions')"; +const exportFromText = "export * from './actions'"; + +export const STRING_LITERAL_ACTION_REFERENCE = [ + actionReferenceText, + dynamicImportText, + exportFromText, +].join('|'); diff --git a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts index ffbddbbdca45..d912e2cee11c 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts @@ -206,6 +206,8 @@ const collectLocalModuleSpecifiers = ( filePath: string, sourceText: string, ): SourceModuleSpecifier[] => { + // Parse with TypeScript AST to avoid regex false positives from comments/string + // literals and to preserve accurate type-only vs runtime edge semantics. const sourceFile = ts.createSourceFile( filePath, sourceText, diff --git a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts index 05cb2ec08d42..54caa72bc9e0 100644 --- a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts +++ b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts @@ -596,6 +596,36 @@ describe('createRscExposeDefinitions', () => { }); }); + it('does not infer callback bootstrap from comment-only callback-like syntax', () => { + const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); + expect( + createRscExposeDefinitions({ + './customCommentedActionReference': + './src/components/commentedActionReference.ts', + }), + ).toEqual({ + './customCommentedActionReference': { + import: ['./src/components/commentedActionReference.ts'], + layer: 'react-server-components', + }, + }); + }); + + it('does not infer callback bootstrap from callback-like string literals', () => { + const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); + expect( + createRscExposeDefinitions({ + './customStringLiteralActionReference': + './src/components/stringLiteralActionReference.ts', + }), + ).toEqual({ + './customStringLiteralActionReference': { + import: ['./src/components/stringLiteralActionReference.ts'], + layer: 'react-server-components', + }, + }); + }); + it('keeps non-callback source modules free of callback bootstrap import', () => { const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); expect( From edbbd3fde5d55f1884e8563d486da7d73bd994ee Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 16:41:49 +0000 Subject: [PATCH 288/324] test(rsc-mf): cover import-equals and import-type callback inference --- .../components/importEqualsActionRequire.cts | 4 +++ .../importTypeQueryActionReference.ts | 6 ++++ .../src/runtime/createRscExposeDefinitions.ts | 14 ++++++++ .../tests/createRscExposeDefinitions.test.ts | 34 +++++++++++++++++++ 4 files changed, 58 insertions(+) create mode 100644 tests/integration/rsc-mf/remote/src/components/importEqualsActionRequire.cts create mode 100644 tests/integration/rsc-mf/remote/src/components/importTypeQueryActionReference.ts diff --git a/tests/integration/rsc-mf/remote/src/components/importEqualsActionRequire.cts b/tests/integration/rsc-mf/remote/src/components/importEqualsActionRequire.cts new file mode 100644 index 000000000000..9de6696c7c5e --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/importEqualsActionRequire.cts @@ -0,0 +1,4 @@ +import remoteActions = require('./actions'); + +export const runImportEqualsRemoteActionEcho = async () => + remoteActions.remoteActionEcho('import-equals-action-require'); diff --git a/tests/integration/rsc-mf/remote/src/components/importTypeQueryActionReference.ts b/tests/integration/rsc-mf/remote/src/components/importTypeQueryActionReference.ts new file mode 100644 index 000000000000..b35c8180cdc4 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/importTypeQueryActionReference.ts @@ -0,0 +1,6 @@ +export type IncrementActionFromImportTypeQuery = import( + './actions', +).incrementRemoteCount; + +export const IMPORT_TYPE_QUERY_ACTION_REFERENCE = + 'import-type-query-action-reference'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts index d912e2cee11c..401169327f3f 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts @@ -218,6 +218,20 @@ const collectLocalModuleSpecifiers = ( const moduleSpecifiers: SourceModuleSpecifier[] = []; const collectNodeSpecifiers = (node: ts.Node): void => { + if (ts.isImportEqualsDeclaration(node)) { + if ( + ts.isExternalModuleReference(node.moduleReference) && + node.moduleReference.expression && + ts.isStringLiteralLike(node.moduleReference.expression) + ) { + moduleSpecifiers.push({ + moduleSpecifier: node.moduleReference.expression.text, + typeOnly: false, + }); + } + return; + } + if (ts.isImportDeclaration(node)) { if (ts.isStringLiteralLike(node.moduleSpecifier)) { moduleSpecifiers.push({ diff --git a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts index 54caa72bc9e0..fec090a57777 100644 --- a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts +++ b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts @@ -415,6 +415,25 @@ describe('createRscExposeDefinitions', () => { }); }); + it('infers callback bootstrap from import-equals commonjs require graph', () => { + const { createRscExposeDefinitions, CALLBACK_BOOTSTRAP_MODULE } = + loadCreateRscExposeDefinitions(); + expect( + createRscExposeDefinitions({ + './customImportEqualsActionRequire': + './src/components/importEqualsActionRequire.cts', + }), + ).toEqual({ + './customImportEqualsActionRequire': { + import: [ + CALLBACK_BOOTSTRAP_MODULE, + './src/components/importEqualsActionRequire.cts', + ], + layer: 'react-server-components', + }, + }); + }); + it('does not infer callback bootstrap from commonjs require graph without callback directives', () => { const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); expect( @@ -460,6 +479,21 @@ describe('createRscExposeDefinitions', () => { }); }); + it('does not infer callback bootstrap from import-type query references', () => { + const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); + expect( + createRscExposeDefinitions({ + './customImportTypeQueryActionReference': + './src/components/importTypeQueryActionReference.ts', + }), + ).toEqual({ + './customImportTypeQueryActionReference': { + import: ['./src/components/importTypeQueryActionReference.ts'], + layer: 'react-server-components', + }, + }); + }); + it('does not infer callback bootstrap from type-only re-export clauses', () => { const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); expect( From 438da696b1fc507801cf818952472540236f5fb4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 17:10:20 +0000 Subject: [PATCH 289/324] fix(rsc-mf): ignore type-only import-equals callback edges --- .../importEqualsTypeOnlyActionRequire.cts | 6 ++++++ .../src/runtime/createRscExposeDefinitions.ts | 2 +- .../tests/createRscExposeDefinitions.test.ts | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 tests/integration/rsc-mf/remote/src/components/importEqualsTypeOnlyActionRequire.cts diff --git a/tests/integration/rsc-mf/remote/src/components/importEqualsTypeOnlyActionRequire.cts b/tests/integration/rsc-mf/remote/src/components/importEqualsTypeOnlyActionRequire.cts new file mode 100644 index 000000000000..816c34fda883 --- /dev/null +++ b/tests/integration/rsc-mf/remote/src/components/importEqualsTypeOnlyActionRequire.cts @@ -0,0 +1,6 @@ +import type RemoteActions = require('./actions'); + +export type ImportEqualsTypeOnlyActionRequireShape = typeof RemoteActions; + +export const IMPORT_EQUALS_TYPE_ONLY_ACTION_REQUIRE = + 'import-equals-type-only-action-require'; diff --git a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts index 401169327f3f..bd2476a40950 100644 --- a/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts +++ b/tests/integration/rsc-mf/remote/src/runtime/createRscExposeDefinitions.ts @@ -226,7 +226,7 @@ const collectLocalModuleSpecifiers = ( ) { moduleSpecifiers.push({ moduleSpecifier: node.moduleReference.expression.text, - typeOnly: false, + typeOnly: Boolean(node.isTypeOnly), }); } return; diff --git a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts index fec090a57777..00907e7049e9 100644 --- a/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts +++ b/tests/integration/rsc-mf/tests/createRscExposeDefinitions.test.ts @@ -449,6 +449,21 @@ describe('createRscExposeDefinitions', () => { }); }); + it('does not infer callback bootstrap from type-only import-equals require references', () => { + const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); + expect( + createRscExposeDefinitions({ + './customImportEqualsTypeOnlyActionRequire': + './src/components/importEqualsTypeOnlyActionRequire.cts', + }), + ).toEqual({ + './customImportEqualsTypeOnlyActionRequire': { + import: ['./src/components/importEqualsTypeOnlyActionRequire.cts'], + layer: 'react-server-components', + }, + }); + }); + it('does not infer callback bootstrap from type-only local imports', () => { const { createRscExposeDefinitions } = loadCreateRscExposeDefinitions(); expect( From 7410cb8b1b12ca905b2eb77bb15fc5132fff1b4c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 20:21:50 +0000 Subject: [PATCH 290/324] feat(rsc-mf): auto-resolve remote public paths from federation state --- .../host/runtime/forceRemotePublicPath.ts | 216 +++++++++++++++++- .../tests/forceRemotePublicPath.test.ts | 106 +++++++++ 2 files changed, 313 insertions(+), 9 deletions(-) diff --git a/tests/integration/rsc-mf/host/runtime/forceRemotePublicPath.ts b/tests/integration/rsc-mf/host/runtime/forceRemotePublicPath.ts index 0e074972eb84..1e63cc2b8ed3 100644 --- a/tests/integration/rsc-mf/host/runtime/forceRemotePublicPath.ts +++ b/tests/integration/rsc-mf/host/runtime/forceRemotePublicPath.ts @@ -1,6 +1,35 @@ import type { ModuleFederationRuntimePlugin } from '@module-federation/modern-js-v3'; -const getRemotePublicPath = (entry: string) => { +const TARGET_REMOTE_ALIAS = 'rscRemote'; +const SSR_BUNDLES_SEGMENT = '/bundles/'; + +interface FederationRemoteConfig { + alias?: string; + name?: string; + entry?: string; +} + +interface FederationInstanceLike { + options?: { + remotes?: Record | unknown[]; + }; +} + +interface FederationModuleInfoLike { + alias?: string; + name?: string; + metaData?: { + publicPath?: string; + ssrPublicPath?: string; + }; +} + +interface FederationGlobalLike { + __INSTANCES__?: FederationInstanceLike[]; + moduleInfo?: Record; +} + +const getRemoteOriginPublicPath = (entry: string) => { try { return `${new URL(entry).origin}/`; } catch { @@ -8,19 +37,187 @@ const getRemotePublicPath = (entry: string) => { } }; +const getNormalizedAbsolutePublicPath = (value: string) => { + try { + const publicPathUrl = new URL(value); + publicPathUrl.search = ''; + publicPathUrl.hash = ''; + publicPathUrl.pathname = publicPathUrl.pathname.endsWith('/') + ? publicPathUrl.pathname + : `${publicPathUrl.pathname}/`; + return publicPathUrl.toString(); + } catch { + return undefined; + } +}; + +const getPublicPathFromSsrPublicPath = (ssrPublicPath: string) => { + const normalizedSsrPublicPath = + getNormalizedAbsolutePublicPath(ssrPublicPath); + if (!normalizedSsrPublicPath) { + return undefined; + } + try { + const ssrPublicPathUrl = new URL(normalizedSsrPublicPath); + if (!ssrPublicPathUrl.pathname.endsWith(SSR_BUNDLES_SEGMENT)) { + return undefined; + } + ssrPublicPathUrl.pathname = ssrPublicPathUrl.pathname.replace( + /\/bundles\/$/, + '/', + ); + return ssrPublicPathUrl.toString(); + } catch { + return undefined; + } +}; + +const getGlobalFederationState = () => + (globalThis as typeof globalThis & { __FEDERATION__?: FederationGlobalLike }) + .__FEDERATION__; + +const getRemoteEntryFromRemoteConfig = ( + remoteConfig: string | FederationRemoteConfig | undefined, +) => { + if (!remoteConfig) { + return undefined; + } + if (typeof remoteConfig === 'string') { + const atIndex = remoteConfig.indexOf('@'); + return atIndex >= 0 ? remoteConfig.slice(atIndex + 1) : remoteConfig; + } + if (typeof remoteConfig.entry === 'string') { + return remoteConfig.entry; + } + return undefined; +}; + +const getRemoteEntryFromFederationInstances = (remoteAlias: string) => { + const federationState = getGlobalFederationState(); + if (!federationState?.__INSTANCES__) { + return undefined; + } + for (const instance of federationState.__INSTANCES__) { + const remotes = instance?.options?.remotes; + if (!remotes) { + continue; + } + if (!Array.isArray(remotes) && typeof remotes === 'object') { + const remoteConfig = remotes[remoteAlias] as + | string + | FederationRemoteConfig + | undefined; + const remoteEntry = getRemoteEntryFromRemoteConfig(remoteConfig); + if (remoteEntry) { + return remoteEntry; + } + for (const remotesEntry of Object.values(remotes)) { + if ( + remotesEntry && + typeof remotesEntry === 'object' && + 'alias' in remotesEntry && + remotesEntry.alias === remoteAlias + ) { + return getRemoteEntryFromRemoteConfig( + remotesEntry as FederationRemoteConfig, + ); + } + } + continue; + } + if (!Array.isArray(remotes)) { + continue; + } + for (const remoteConfig of remotes) { + if (!remoteConfig || typeof remoteConfig !== 'object') { + continue; + } + const alias = 'alias' in remoteConfig ? remoteConfig.alias : undefined; + const name = 'name' in remoteConfig ? remoteConfig.name : undefined; + if (alias !== remoteAlias && name !== remoteAlias) { + continue; + } + const remoteEntry = getRemoteEntryFromRemoteConfig( + remoteConfig as FederationRemoteConfig, + ); + if (remoteEntry) { + return remoteEntry; + } + } + } + return undefined; +}; + +const getRemoteModuleInfoFromFederationState = (remoteAlias: string) => { + const federationState = getGlobalFederationState(); + if (!federationState?.moduleInfo) { + return undefined; + } + const directRemoteModuleInfo = federationState.moduleInfo[remoteAlias]; + if (directRemoteModuleInfo) { + return directRemoteModuleInfo; + } + return Object.values(federationState.moduleInfo).find( + moduleInfo => + moduleInfo?.alias === remoteAlias || moduleInfo?.name === remoteAlias, + ); +}; + +const resolveRemotePublicPaths = ({ + remoteAlias, + remoteEntry, +}: { + remoteAlias: string; + remoteEntry?: string; +}) => { + const preferredRemoteEntry = + remoteEntry || getRemoteEntryFromFederationInstances(remoteAlias); + const remotePublicPath = preferredRemoteEntry + ? getRemoteOriginPublicPath(preferredRemoteEntry) + : undefined; + if (remotePublicPath) { + return { + remotePublicPath, + remoteSsrPublicPath: `${remotePublicPath}bundles/`, + }; + } + + const remoteModuleInfo = getRemoteModuleInfoFromFederationState(remoteAlias); + const remoteModuleInfoPublicPath = remoteModuleInfo?.metaData?.publicPath + ? getNormalizedAbsolutePublicPath(remoteModuleInfo.metaData.publicPath) + : undefined; + const remoteModuleInfoSsrPublicPath = remoteModuleInfo?.metaData + ?.ssrPublicPath + ? getNormalizedAbsolutePublicPath(remoteModuleInfo.metaData.ssrPublicPath) + : undefined; + const fallbackRemotePublicPath = + remoteModuleInfoPublicPath || + (remoteModuleInfoSsrPublicPath + ? getPublicPathFromSsrPublicPath(remoteModuleInfoSsrPublicPath) + : undefined); + const fallbackRemoteSsrPublicPath = + remoteModuleInfoSsrPublicPath || + (fallbackRemotePublicPath + ? `${fallbackRemotePublicPath}bundles/` + : undefined); + return { + remotePublicPath: fallbackRemotePublicPath, + remoteSsrPublicPath: fallbackRemoteSsrPublicPath, + }; +}; + const forceRemotePublicPath = (): ModuleFederationRuntimePlugin => ({ name: 'rsc-mf-force-remote-public-path', loadRemoteSnapshot(args: any) { const { remoteInfo, remoteSnapshot } = args; - if (remoteInfo?.alias !== 'rscRemote' || !remoteSnapshot) { - return args; - } - - const entry = remoteInfo?.entry; - if (!entry || typeof entry !== 'string') { + if (remoteInfo?.alias !== TARGET_REMOTE_ALIAS || !remoteSnapshot) { return args; } - const remotePublicPath = getRemotePublicPath(entry); + const { remotePublicPath, remoteSsrPublicPath } = resolveRemotePublicPaths({ + remoteAlias: remoteInfo.alias, + remoteEntry: + typeof remoteInfo?.entry === 'string' ? remoteInfo.entry : undefined, + }); if (!remotePublicPath) { return args; } @@ -32,7 +229,8 @@ const forceRemotePublicPath = (): ModuleFederationRuntimePlugin => ({ remoteSnapshot.metaData.publicPath = remotePublicPath; } if (remoteSnapshot.metaData && 'ssrPublicPath' in remoteSnapshot.metaData) { - remoteSnapshot.metaData.ssrPublicPath = `${remotePublicPath}bundles/`; + remoteSnapshot.metaData.ssrPublicPath = + remoteSsrPublicPath || `${remotePublicPath}bundles/`; } return args; diff --git a/tests/integration/rsc-mf/tests/forceRemotePublicPath.test.ts b/tests/integration/rsc-mf/tests/forceRemotePublicPath.test.ts index af991850185c..8163bd99632f 100644 --- a/tests/integration/rsc-mf/tests/forceRemotePublicPath.test.ts +++ b/tests/integration/rsc-mf/tests/forceRemotePublicPath.test.ts @@ -1,6 +1,24 @@ import forceRemotePublicPath from '../host/runtime/forceRemotePublicPath'; describe('host forceRemotePublicPath runtime plugin', () => { + const originalFederation = ( + globalThis as typeof globalThis & { __FEDERATION__?: unknown } + ).__FEDERATION__; + + afterEach(() => { + if (typeof originalFederation === 'undefined') { + delete (globalThis as typeof globalThis & { __FEDERATION__?: unknown }) + .__FEDERATION__; + return; + } + + ( + globalThis as typeof globalThis & { + __FEDERATION__?: unknown; + } + ).__FEDERATION__ = originalFederation; + }); + it('keeps plugin name stable', () => { const plugin = forceRemotePublicPath(); expect(plugin.name).toBe('rsc-mf-force-remote-public-path'); @@ -55,6 +73,94 @@ describe('host forceRemotePublicPath runtime plugin', () => { ); }); + it('resolves remote public paths from __FEDERATION__ remotes when entry is missing', () => { + ( + globalThis as typeof globalThis & { + __FEDERATION__?: unknown; + } + ).__FEDERATION__ = { + __INSTANCES__: [ + { + options: { + remotes: { + rscRemote: + 'rscRemote@https://federation-runtime.example.com/static/mf-manifest.json', + }, + }, + }, + ], + } as any; + const plugin = forceRemotePublicPath(); + const args = { + remoteInfo: { + alias: 'rscRemote', + }, + remoteSnapshot: { + publicPath: 'http://stale.example.com/', + metaData: { + publicPath: 'http://stale.example.com/', + ssrPublicPath: 'http://stale.example.com/bundles/', + }, + }, + }; + + plugin.loadRemoteSnapshot?.(args as any); + + expect(args.remoteSnapshot.publicPath).toBe( + 'https://federation-runtime.example.com/', + ); + expect(args.remoteSnapshot.metaData.publicPath).toBe( + 'https://federation-runtime.example.com/', + ); + expect(args.remoteSnapshot.metaData.ssrPublicPath).toBe( + 'https://federation-runtime.example.com/bundles/', + ); + }); + + it('resolves remote public paths from __FEDERATION__ module metadata fallback', () => { + ( + globalThis as typeof globalThis & { + __FEDERATION__?: unknown; + } + ).__FEDERATION__ = { + moduleInfo: { + rscRemote: { + metaData: { + publicPath: + 'https://federation-metadata.example.com/assets?cache=1#hash', + ssrPublicPath: + 'https://federation-metadata.example.com/assets/bundles?cache=1#hash', + }, + }, + }, + } as any; + const plugin = forceRemotePublicPath(); + const args = { + remoteInfo: { + alias: 'rscRemote', + }, + remoteSnapshot: { + publicPath: 'http://stale.example.com/', + metaData: { + publicPath: 'http://stale.example.com/', + ssrPublicPath: 'http://stale.example.com/bundles/', + }, + }, + }; + + plugin.loadRemoteSnapshot?.(args as any); + + expect(args.remoteSnapshot.publicPath).toBe( + 'https://federation-metadata.example.com/assets/', + ); + expect(args.remoteSnapshot.metaData.publicPath).toBe( + 'https://federation-metadata.example.com/assets/', + ); + expect(args.remoteSnapshot.metaData.ssrPublicPath).toBe( + 'https://federation-metadata.example.com/assets/bundles/', + ); + }); + it('does not mutate when entry is not a valid URL', () => { const plugin = forceRemotePublicPath(); const args = { From fc5e8513ce3c320bd14782ff65bae2b9f08b4e7b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 20:34:14 +0000 Subject: [PATCH 291/324] refactor(rsc-mf): generalize remote publicPath runtime resolution --- .../host/runtime/forceRemotePublicPath.ts | 11 +++++-- .../tests/forceRemotePublicPath.test.ts | 33 +++++++++++++++++-- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/tests/integration/rsc-mf/host/runtime/forceRemotePublicPath.ts b/tests/integration/rsc-mf/host/runtime/forceRemotePublicPath.ts index 1e63cc2b8ed3..5fa074ff9913 100644 --- a/tests/integration/rsc-mf/host/runtime/forceRemotePublicPath.ts +++ b/tests/integration/rsc-mf/host/runtime/forceRemotePublicPath.ts @@ -1,6 +1,5 @@ import type { ModuleFederationRuntimePlugin } from '@module-federation/modern-js-v3'; -const TARGET_REMOTE_ALIAS = 'rscRemote'; const SSR_BUNDLES_SEGMENT = '/bundles/'; interface FederationRemoteConfig { @@ -210,11 +209,17 @@ const forceRemotePublicPath = (): ModuleFederationRuntimePlugin => ({ name: 'rsc-mf-force-remote-public-path', loadRemoteSnapshot(args: any) { const { remoteInfo, remoteSnapshot } = args; - if (remoteInfo?.alias !== TARGET_REMOTE_ALIAS || !remoteSnapshot) { + const remoteAlias = + typeof remoteInfo?.alias === 'string' && remoteInfo.alias + ? remoteInfo.alias + : typeof remoteInfo?.name === 'string' && remoteInfo.name + ? remoteInfo.name + : undefined; + if (!remoteAlias || !remoteSnapshot) { return args; } const { remotePublicPath, remoteSsrPublicPath } = resolveRemotePublicPaths({ - remoteAlias: remoteInfo.alias, + remoteAlias, remoteEntry: typeof remoteInfo?.entry === 'string' ? remoteInfo.entry : undefined, }); diff --git a/tests/integration/rsc-mf/tests/forceRemotePublicPath.test.ts b/tests/integration/rsc-mf/tests/forceRemotePublicPath.test.ts index 8163bd99632f..f3f73a317e50 100644 --- a/tests/integration/rsc-mf/tests/forceRemotePublicPath.test.ts +++ b/tests/integration/rsc-mf/tests/forceRemotePublicPath.test.ts @@ -25,11 +25,10 @@ describe('host forceRemotePublicPath runtime plugin', () => { expect(typeof plugin.loadRemoteSnapshot).toBe('function'); }); - it('does not mutate non-target remotes', () => { + it('does not mutate when remote alias and name are missing', () => { const plugin = forceRemotePublicPath(); const args = { remoteInfo: { - alias: 'anotherRemote', entry: 'http://127.0.0.1:3008/static/mf-manifest.json', }, remoteSnapshot: { @@ -42,6 +41,36 @@ describe('host forceRemotePublicPath runtime plugin', () => { expect(args.remoteSnapshot.publicPath).toBe('http://example.com/'); }); + it('supports remote names when alias is unavailable', () => { + const plugin = forceRemotePublicPath(); + const args = { + remoteInfo: { + name: 'anotherRemote', + entry: 'https://another-remote.example.com/static/mf-manifest.json', + }, + remoteSnapshot: { + publicPath: 'http://example.com/', + metaData: { + publicPath: 'http://example.com/', + ssrPublicPath: 'http://example.com/bundles/', + }, + }, + }; + + const result = plugin.loadRemoteSnapshot?.(args as any); + + expect(result).toBe(args); + expect(args.remoteSnapshot.publicPath).toBe( + 'https://another-remote.example.com/', + ); + expect(args.remoteSnapshot.metaData.publicPath).toBe( + 'https://another-remote.example.com/', + ); + expect(args.remoteSnapshot.metaData.ssrPublicPath).toBe( + 'https://another-remote.example.com/bundles/', + ); + }); + it('does not mutate when entry is missing or non-string', () => { const plugin = forceRemotePublicPath(); const argsWithoutEntry = { From 1a291c720c2dcf32426261c23f3af0da1b1a85ce Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 20:44:03 +0000 Subject: [PATCH 292/324] test(rsc-mf): cover federation remotes-array and ssr-only path fallbacks --- .../tests/forceRemotePublicPath.test.ts | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/tests/integration/rsc-mf/tests/forceRemotePublicPath.test.ts b/tests/integration/rsc-mf/tests/forceRemotePublicPath.test.ts index f3f73a317e50..177c0d948b07 100644 --- a/tests/integration/rsc-mf/tests/forceRemotePublicPath.test.ts +++ b/tests/integration/rsc-mf/tests/forceRemotePublicPath.test.ts @@ -146,6 +146,53 @@ describe('host forceRemotePublicPath runtime plugin', () => { ); }); + it('resolves remote public paths from __FEDERATION__ remotes array entries', () => { + ( + globalThis as typeof globalThis & { + __FEDERATION__?: unknown; + } + ).__FEDERATION__ = { + __INSTANCES__: [ + { + options: { + remotes: [ + { + alias: 'rscRemote', + entry: + 'https://federation-array.example.com/static/mf-manifest.json', + }, + ], + }, + }, + ], + } as any; + const plugin = forceRemotePublicPath(); + const args = { + remoteInfo: { + alias: 'rscRemote', + }, + remoteSnapshot: { + publicPath: 'http://stale.example.com/', + metaData: { + publicPath: 'http://stale.example.com/', + ssrPublicPath: 'http://stale.example.com/bundles/', + }, + }, + }; + + plugin.loadRemoteSnapshot?.(args as any); + + expect(args.remoteSnapshot.publicPath).toBe( + 'https://federation-array.example.com/', + ); + expect(args.remoteSnapshot.metaData.publicPath).toBe( + 'https://federation-array.example.com/', + ); + expect(args.remoteSnapshot.metaData.ssrPublicPath).toBe( + 'https://federation-array.example.com/bundles/', + ); + }); + it('resolves remote public paths from __FEDERATION__ module metadata fallback', () => { ( globalThis as typeof globalThis & { @@ -190,6 +237,49 @@ describe('host forceRemotePublicPath runtime plugin', () => { ); }); + it('derives publicPath from __FEDERATION__ ssrPublicPath-only metadata', () => { + ( + globalThis as typeof globalThis & { + __FEDERATION__?: unknown; + } + ).__FEDERATION__ = { + moduleInfo: { + customModuleInfoKey: { + alias: 'rscRemote', + metaData: { + ssrPublicPath: + 'https://federation-ssr-only.example.com/assets/bundles/?cache=1#hash', + }, + }, + }, + } as any; + const plugin = forceRemotePublicPath(); + const args = { + remoteInfo: { + alias: 'rscRemote', + }, + remoteSnapshot: { + publicPath: 'http://stale.example.com/', + metaData: { + publicPath: 'http://stale.example.com/', + ssrPublicPath: 'http://stale.example.com/bundles/', + }, + }, + }; + + plugin.loadRemoteSnapshot?.(args as any); + + expect(args.remoteSnapshot.publicPath).toBe( + 'https://federation-ssr-only.example.com/assets/', + ); + expect(args.remoteSnapshot.metaData.publicPath).toBe( + 'https://federation-ssr-only.example.com/assets/', + ); + expect(args.remoteSnapshot.metaData.ssrPublicPath).toBe( + 'https://federation-ssr-only.example.com/assets/bundles/', + ); + }); + it('does not mutate when entry is not a valid URL', () => { const plugin = forceRemotePublicPath(); const args = { From 56679b458bf11eee5b7486576e647bcca0c6680d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 22:26:15 +0000 Subject: [PATCH 293/324] refactor(rsc-mf): remove host fallback middleware layer --- .../rsc-mf/host/server/manifestFallback.ts | 186 -- .../rsc-mf/host/server/modern.server.ts | 194 --- .../rsc-mf/host/server/proxyResponse.ts | 46 - .../rsc-mf/tests/modernConfig.test.ts | 23 +- .../rsc-mf/tests/modernServerConfig.test.ts | 1539 +---------------- 5 files changed, 27 insertions(+), 1961 deletions(-) delete mode 100644 tests/integration/rsc-mf/host/server/manifestFallback.ts delete mode 100644 tests/integration/rsc-mf/host/server/modern.server.ts delete mode 100644 tests/integration/rsc-mf/host/server/proxyResponse.ts diff --git a/tests/integration/rsc-mf/host/server/manifestFallback.ts b/tests/integration/rsc-mf/host/server/manifestFallback.ts deleted file mode 100644 index 959b7b5ae0bc..000000000000 --- a/tests/integration/rsc-mf/host/server/manifestFallback.ts +++ /dev/null @@ -1,186 +0,0 @@ -const EXPOSE_CHUNK_HASH_SUFFIX_PATTERN = /\.[a-z0-9]{6,}$/i; -const EXPOSE_CHUNK_HASHED_ASSET_PATTERN = /\.[a-z0-9]{6,}\.(js|css)$/i; -export const INTERNAL_FALLBACK_HEADER = 'x-rsc-mf-internal-fallback'; - -export interface RemoteManifestAssetEntry { - assets?: { - js?: { - sync?: string[]; - async?: string[]; - }; - css?: { - sync?: string[]; - async?: string[]; - }; - }; -} - -export interface RemoteManifestShape { - shared?: RemoteManifestAssetEntry[]; - exposes?: RemoteManifestAssetEntry[]; -} - -export const isExposeAssetRequestPath = (pathname: string) => - pathname.includes('__federation_expose_') && - (pathname.endsWith('.js') || pathname.endsWith('.css')); - -export const getRequestedAssetDirectory = (pathname: string) => - pathname.includes('/static/css/async/') - ? 'static/css/async/' - : 'static/js/async/'; - -const toCanonicalChunkName = (filePath: string) => - filePath - .replace(/\/+$/, '') - .split('/') - .pop() - ?.replace(/\.(js|css)$/i, '') - .replace(EXPOSE_CHUNK_HASH_SUFFIX_PATTERN, ''); - -const toNormalizedManifestAssetPath = (assetPath: string) => { - try { - return new URL(assetPath).pathname.replace(/^\/+/, ''); - } catch { - return assetPath.replace(/^[./]+/, '').split(/[?#]/, 1)[0]; - } -}; - -const toNormalizedRequestPath = (pathname: string) => - pathname.replace(/^\/+/, '').split(/[?#]/, 1)[0]; - -const hasChunkHashInAssetPath = (assetPath: string) => - EXPOSE_CHUNK_HASHED_ASSET_PATTERN.test(assetPath); - -const collectManifestAssetPaths = (manifest: RemoteManifestShape) => { - const entries = [...(manifest.shared || []), ...(manifest.exposes || [])]; - const assetPaths = new Set(); - for (const entry of entries) { - const jsSyncAssets = entry.assets?.js?.sync || []; - const jsAsyncAssets = entry.assets?.js?.async || []; - const cssSyncAssets = entry.assets?.css?.sync || []; - const cssAsyncAssets = entry.assets?.css?.async || []; - for (const assetPath of [ - ...jsSyncAssets, - ...jsAsyncAssets, - ...cssSyncAssets, - ...cssAsyncAssets, - ]) { - assetPaths.add(assetPath); - } - } - return [...assetPaths]; -}; - -export const resolveManifestFallbackAssetPath = ( - pathname: string, - manifest: RemoteManifestShape, -) => { - if (!isExposeAssetRequestPath(pathname)) { - return undefined; - } - - const canonicalRequestedChunkName = toCanonicalChunkName(pathname); - if (!canonicalRequestedChunkName) { - return undefined; - } - - const requestedAssetDirectory = getRequestedAssetDirectory(pathname); - const normalizedRequestedPath = toNormalizedRequestPath(pathname); - const manifestAssets = collectManifestAssetPaths(manifest); - const candidateAssets = manifestAssets - .map(assetPath => ({ - assetPath, - normalizedAssetPath: toNormalizedManifestAssetPath(assetPath), - })) - .filter(({ normalizedAssetPath }) => { - if (!normalizedAssetPath.startsWith(requestedAssetDirectory)) { - return false; - } - return ( - toCanonicalChunkName(normalizedAssetPath) === - canonicalRequestedChunkName - ); - }); - - const preferredCandidate = - candidateAssets.find( - ({ normalizedAssetPath }) => - normalizedAssetPath !== normalizedRequestedPath && - hasChunkHashInAssetPath(normalizedAssetPath), - ) || - candidateAssets.find( - ({ normalizedAssetPath }) => - normalizedAssetPath !== normalizedRequestedPath, - ) || - candidateAssets.find(({ normalizedAssetPath }) => - hasChunkHashInAssetPath(normalizedAssetPath), - ) || - candidateAssets[0]; - - return preferredCandidate?.assetPath; -}; - -export const createManifestFallbackAssetUrl = ({ - remoteOrigin, - fallbackAssetPath, - requestSearch, - requestedAssetDirectory, - requestUrl, -}: { - remoteOrigin: string; - fallbackAssetPath: string; - requestSearch: string; - requestedAssetDirectory: string; - requestUrl?: string; -}) => { - let remoteOriginUrl: URL; - try { - remoteOriginUrl = new URL(remoteOrigin); - } catch { - return undefined; - } - - let fallbackAssetUrl: URL; - try { - fallbackAssetUrl = new URL(fallbackAssetPath, `${remoteOriginUrl.origin}/`); - } catch { - return undefined; - } - - if (fallbackAssetUrl.origin !== remoteOriginUrl.origin) { - return undefined; - } - const normalizedFallbackPathname = fallbackAssetUrl.pathname.replace( - /^\/+/, - '', - ); - let decodedFallbackPathname: string; - try { - decodedFallbackPathname = decodeURIComponent(normalizedFallbackPathname); - } catch { - return undefined; - } - if (!decodedFallbackPathname.startsWith(requestedAssetDirectory)) { - return undefined; - } - if (decodedFallbackPathname.split('/').some(segment => segment === '..')) { - return undefined; - } - - if (requestSearch) { - const mergedSearchParams = new URLSearchParams(fallbackAssetUrl.search); - const requestSearchParams = new URLSearchParams(requestSearch); - for (const [key, value] of requestSearchParams.entries()) { - mergedSearchParams.set(key, value); - } - const mergedSearch = mergedSearchParams.toString(); - fallbackAssetUrl.search = mergedSearch ? `?${mergedSearch}` : ''; - } - - const resolvedFallbackAssetUrl = fallbackAssetUrl.toString(); - if (requestUrl && resolvedFallbackAssetUrl === requestUrl) { - return undefined; - } - - return resolvedFallbackAssetUrl; -}; diff --git a/tests/integration/rsc-mf/host/server/modern.server.ts b/tests/integration/rsc-mf/host/server/modern.server.ts deleted file mode 100644 index d5bc6b389b80..000000000000 --- a/tests/integration/rsc-mf/host/server/modern.server.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { - type MiddlewareHandler, - defineServerConfig, -} from '@modern-js/server-runtime'; -import { - INTERNAL_FALLBACK_HEADER, - type RemoteManifestShape, - createManifestFallbackAssetUrl, - getRequestedAssetDirectory, - isExposeAssetRequestPath, - resolveManifestFallbackAssetPath, -} from './manifestFallback'; -import { createSafeProxyResponse } from './proxyResponse'; - -const REMOTE_MANIFEST_PATH = '/static/mf-manifest.json'; - -const fetchRemoteManifestFallbackAsset = async ({ - remoteOrigin, - pathname, - search, - requestUrl, -}: { - remoteOrigin: string; - pathname: string; - search: string; - requestUrl: string; -}) => { - if (!isExposeAssetRequestPath(pathname)) { - return undefined; - } - - const manifestResponse = await fetch( - `${remoteOrigin}${REMOTE_MANIFEST_PATH}`, - { - headers: { - [INTERNAL_FALLBACK_HEADER]: '1', - }, - }, - ) - .then(response => { - if (!response.ok) { - return undefined; - } - return response; - }) - .catch((): undefined => undefined); - - if (!manifestResponse) { - return undefined; - } - - const manifest = (await manifestResponse - .json() - .catch((): undefined => undefined)) as RemoteManifestShape | undefined; - if (!manifest) { - return undefined; - } - const fallbackAssetPath = resolveManifestFallbackAssetPath( - pathname, - manifest, - ); - if (!fallbackAssetPath) { - return undefined; - } - - const fallbackAssetUrl = createManifestFallbackAssetUrl({ - remoteOrigin, - fallbackAssetPath, - requestSearch: search, - requestedAssetDirectory: getRequestedAssetDirectory(pathname), - requestUrl, - }); - if (!fallbackAssetUrl) { - return undefined; - } - const fallbackAssetResponse = await fetch(fallbackAssetUrl, { - headers: { - [INTERNAL_FALLBACK_HEADER]: '1', - }, - }).catch((): undefined => undefined); - if (!fallbackAssetResponse || !fallbackAssetResponse.ok) { - return undefined; - } - return fallbackAssetResponse; -}; - -const shouldProxyRemoteAsset = (pathname: string) => { - if (pathname.startsWith('/static/js/async/')) { - return ( - pathname.includes('__federation_expose_') || - pathname.includes('_react-server-components_') || - pathname.includes('node_modules_pnpm_react') - ); - } - - if (pathname.startsWith('/static/css/async/')) { - return pathname.includes('__federation_expose_'); - } - - return false; -}; - -const getRequestHeader = ( - c: Parameters[0], - name: string, -) => { - const requestWithHeaders = c.req as typeof c.req & { - header?: (headerName: string) => string | undefined; - headers?: { get?: (headerName: string) => string | null | undefined }; - }; - const headerValue = - typeof requestWithHeaders.header === 'function' - ? requestWithHeaders.header(name) - : undefined; - return headerValue ?? requestWithHeaders.headers?.get?.(name); -}; - -const setContextResponse = ( - c: Parameters[0], - response: Response, -) => { - const contextWithBody = c as typeof c & { - body?: ( - body: BodyInit | null, - status?: number, - headers?: HeadersInit, - ) => Response; - }; - if (typeof contextWithBody.body === 'function') { - const finalizedResponse = contextWithBody.body( - response.body, - response.status, - response.headers, - ); - c.res = finalizedResponse; - return finalizedResponse; - } - c.res = response; - return response; -}; - -const proxyRemoteFederationAsset: MiddlewareHandler = async (c, next) => { - const isInternalFallbackFetch = - getRequestHeader(c, INTERNAL_FALLBACK_HEADER) === '1'; - if (isInternalFallbackFetch) { - await next(); - return; - } - const reqUrl = new URL(c.req.url); - const pathname = reqUrl.pathname; - const shouldProxy = shouldProxyRemoteAsset(pathname); - - if (!shouldProxy) { - await next(); - return; - } - - const remotePort = process.env.RSC_MF_REMOTE_PORT; - if (!remotePort) { - await next(); - return; - } - - const remoteOrigin = `http://127.0.0.1:${remotePort}`; - const remoteUrl = `${remoteOrigin}${pathname}${reqUrl.search}`; - const upstream = await fetch(remoteUrl).catch((): undefined => undefined); - - const resolvedUpstream = upstream?.ok - ? upstream - : await fetchRemoteManifestFallbackAsset({ - remoteOrigin, - pathname, - search: reqUrl.search, - requestUrl: remoteUrl, - }); - - if (!resolvedUpstream || !resolvedUpstream.ok) { - await next(); - return; - } - - return setContextResponse(c, createSafeProxyResponse(resolvedUpstream)); -}; - -export default defineServerConfig({ - middlewares: [ - { - name: 'proxy-remote-federation-asset', - handler: proxyRemoteFederationAsset, - order: 'pre', - before: ['server-static'], - }, - ], -}); diff --git a/tests/integration/rsc-mf/host/server/proxyResponse.ts b/tests/integration/rsc-mf/host/server/proxyResponse.ts deleted file mode 100644 index 68a1c61440c3..000000000000 --- a/tests/integration/rsc-mf/host/server/proxyResponse.ts +++ /dev/null @@ -1,46 +0,0 @@ -const PROXY_UNSAFE_RESPONSE_HEADERS = [ - 'content-length', - 'content-encoding', - 'connection', - 'keep-alive', - 'proxy-authenticate', - 'proxy-connection', - 'proxy-authorization', - 'te', - 'trailer', - 'transfer-encoding', - 'upgrade', -]; -const STATUS_CODES_WITHOUT_BODY = new Set([204, 205, 304]); - -const isResponseStatusWithoutBody = (status: number) => - STATUS_CODES_WITHOUT_BODY.has(status); - -const normalizeConnectionHeaderToken = (token: string) => - token - .trim() - .replace(/^"+|"+$/g, '') - .trim() - .toLowerCase(); - -export const createSafeProxyResponse = (upstream: Response) => { - const headers = new Headers(upstream.headers); - for (const headerName of PROXY_UNSAFE_RESPONSE_HEADERS) { - headers.delete(headerName); - } - const connectionHeaderTokens = (upstream.headers.get('connection') || '') - .split(',') - .map(normalizeConnectionHeaderToken) - .filter(Boolean); - for (const token of connectionHeaderTokens) { - headers.delete(token); - } - const responseBody = isResponseStatusWithoutBody(upstream.status) - ? null - : upstream.body; - return new Response(responseBody, { - status: upstream.status, - statusText: upstream.statusText, - headers, - }); -}; diff --git a/tests/integration/rsc-mf/tests/modernConfig.test.ts b/tests/integration/rsc-mf/tests/modernConfig.test.ts index 2013e410f647..dc0348814d17 100644 --- a/tests/integration/rsc-mf/tests/modernConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernConfig.test.ts @@ -91,6 +91,19 @@ const loadRemoteConfig = ({ }, ); +const expectModuleFederationPluginEnabled = (plugins: Array) => { + const hasFederationPlugin = plugins.some(plugin => { + if (!plugin || typeof plugin !== 'object') { + return false; + } + if (plugin.name === 'mf-plugin-mock') { + return plugin.options?.ssr === true; + } + return plugin.name === '@modern-js/plugin-module-federation'; + }); + expect(hasFederationPlugin).toBe(true); +}; + const createChainHarness = (target: string | string[]) => { const aliasMap = new Map(); const conditionNames: string[] = []; @@ -211,12 +224,9 @@ describe('rsc-mf modern config contracts', () => { expect(hostConfig.plugins).toEqual( expect.arrayContaining([ expect.objectContaining({ name: 'app-tools-mock' }), - expect.objectContaining({ - name: 'mf-plugin-mock', - options: expect.objectContaining({ ssr: true }), - }), ]), ); + expectModuleFederationPluginEnabled(hostConfig.plugins); }); it('applies host async-node bundler behavior for node targets', () => { @@ -274,12 +284,9 @@ describe('rsc-mf modern config contracts', () => { expect(remoteConfig.plugins).toEqual( expect.arrayContaining([ expect.objectContaining({ name: 'app-tools-mock' }), - expect.objectContaining({ - name: 'mf-plugin-mock', - options: expect.objectContaining({ ssr: true }), - }), ]), ); + expectModuleFederationPluginEnabled(remoteConfig.plugins); }); it('enables remote ssr mode when explicit PORT is set', () => { diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index 6d9954a52e2a..3d715d1cd51c 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -1,1534 +1,19 @@ -import { INTERNAL_FALLBACK_HEADER } from '../host/server/manifestFallback'; +import fs from 'node:fs'; +import path from 'node:path'; -const HOST_SERVER_CONFIG_MODULE = '../host/server/modern.server'; +describe('rsc-mf host modern.server contracts', () => { + const hostServerDir = path.resolve(__dirname, '../host/server'); -const withRemotePort = (remotePort: string | undefined, run: () => T): T => { - const previousRemotePort = process.env.RSC_MF_REMOTE_PORT; - if (typeof remotePort === 'undefined') { - delete process.env.RSC_MF_REMOTE_PORT; - } else { - process.env.RSC_MF_REMOTE_PORT = remotePort; - } - - try { - return run(); - } finally { - if (typeof previousRemotePort === 'undefined') { - delete process.env.RSC_MF_REMOTE_PORT; - } else { - process.env.RSC_MF_REMOTE_PORT = previousRemotePort; - } - } -}; - -const loadHostServerConfig = () => { - jest.resetModules(); - jest.doMock('@modern-js/server-runtime', () => ({ - defineServerConfig: (config: unknown) => config, - })); - - let config: any; - let loadError: unknown; - jest.isolateModules(() => { - try { - config = require(HOST_SERVER_CONFIG_MODULE).default; - } catch (error) { - loadError = error; - } - }); - if (loadError) { - const message = - loadError instanceof Error - ? loadError.stack || loadError.message - : String(loadError); - throw new Error(`Failed to load host server config: ${message}`); - } - return config; -}; - -const getProxyMiddlewareHandler = () => { - const config = loadHostServerConfig(); - if (!Array.isArray(config.middlewares)) { - throw new Error('Host server config did not provide a middlewares array'); - } - const middleware = config.middlewares.find( - (entry: { name?: string }) => - entry.name === 'proxy-remote-federation-asset', - ); - if (!middleware) { - throw new Error('proxy-remote-federation-asset middleware not found'); - } - expect(middleware.order).toBe('pre'); - expect(middleware.before).toEqual(['server-static']); - expect(typeof middleware.handler).toBe('function'); - return middleware.handler as ( - c: { - req: { url: string; headers?: { get?: (name: string) => string | null } }; - res?: Response; - }, - next: () => Promise, - ) => Promise; -}; - -const expectInternalFallbackFetchCall = ( - fetchMock: jest.Mock, - nthCall: number, - url: string, -) => { - expect(fetchMock).toHaveBeenNthCalledWith(nthCall, url, { - headers: { - [INTERNAL_FALLBACK_HEADER]: '1', - }, - }); -}; - -describe('rsc-mf host modern.server middleware contracts', () => { - const originalFetch = global.fetch; - const originalFetchDescriptor = Object.getOwnPropertyDescriptor( - global, - 'fetch', - ); - - const installFetchMock = (implementation: typeof fetch) => { - const fetchMock = jest.fn(implementation); - Object.defineProperty(global, 'fetch', { - value: fetchMock, - configurable: true, - writable: true, - }); - return fetchMock; - }; - - afterAll(() => { - if (originalFetchDescriptor) { - Object.defineProperty(global, 'fetch', originalFetchDescriptor); - return; - } - global.fetch = originalFetch; - }); - - it('proxies federated async JS expose chunks to remote origin', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock(async () => { - return new Response('proxied-js', { - status: 200, - headers: { - 'content-type': 'application/javascript', - }, - }); - }); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_infoBundle.11dea89e81.js?cache=1', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expect(fetchMock).toHaveBeenCalledWith( - 'http://127.0.0.1:3999/static/js/async/__federation_expose_infoBundle.11dea89e81.js?cache=1', - ); - expect(next).not.toHaveBeenCalled(); - expect(context.res).toBeInstanceOf(Response); - await expect(context.res?.text()).resolves.toBe('proxied-js'); - }); - - it('finalizes proxied responses through context body API when available', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock(async () => { - return new Response('proxied-via-body', { - status: 203, - headers: { - 'content-type': 'application/javascript', - 'content-length': '111', - }, - }); - }); - const context: { - req: { url: string }; - res?: Response; - body: ( - body: BodyInit | null, - status?: number, - headers?: HeadersInit, - ) => Response; - } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_infoBundle.11dea89e81.js', - }, - body: jest.fn((body, status, headers) => { - return new Response(body, { - status, - headers, - }); - }), - }; - - await withRemotePort('3999', () => handler(context as any, next)); - - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(context.body).toHaveBeenCalledTimes(1); - expect(context.res).toBeInstanceOf(Response); - expect(context.res?.status).toBe(203); - expect(context.res?.headers.get('content-length')).toBeNull(); - await expect(context.res?.text()).resolves.toBe('proxied-via-body'); - expect(next).not.toHaveBeenCalled(); - }); - - it('strips transfer headers from proxied upstream responses', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - installFetchMock(async () => { - return new Response('proxied-with-transport-headers', { - status: 206, - statusText: 'Partial Content', - headers: { - 'content-type': 'application/javascript', - 'content-length': '999', - 'content-encoding': 'gzip', - connection: - 'keep-alive, x-proxy-hop-header, "x-proxy-quoted-hop-header"', - 'keep-alive': 'timeout=5', - 'proxy-connection': 'keep-alive', - 'proxy-authenticate': 'Basic realm=test', - 'proxy-authorization': 'Basic dGVzdA==', - 'x-proxy-hop-header': 'remove-me', - 'x-proxy-quoted-hop-header': 'remove-me', - te: 'trailers', - trailer: 'x-trailer-a', - upgrade: 'websocket', - 'transfer-encoding': 'chunked', - }, - }); - }); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_infoBundle.11dea89e81.js', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expect(next).not.toHaveBeenCalled(); - expect(context.res?.status).toBe(206); - expect(context.res?.statusText).toBe('Partial Content'); - expect(context.res?.headers.get('content-type')).toBe( - 'application/javascript', - ); - expect(context.res?.headers.get('content-length')).toBeNull(); - expect(context.res?.headers.get('content-encoding')).toBeNull(); - expect(context.res?.headers.get('connection')).toBeNull(); - expect(context.res?.headers.get('keep-alive')).toBeNull(); - expect(context.res?.headers.get('proxy-connection')).toBeNull(); - expect(context.res?.headers.get('proxy-authenticate')).toBeNull(); - expect(context.res?.headers.get('proxy-authorization')).toBeNull(); - expect(context.res?.headers.get('x-proxy-hop-header')).toBeNull(); - expect(context.res?.headers.get('x-proxy-quoted-hop-header')).toBeNull(); - expect(context.res?.headers.get('te')).toBeNull(); - expect(context.res?.headers.get('trailer')).toBeNull(); - expect(context.res?.headers.get('upgrade')).toBeNull(); - expect(context.res?.headers.get('transfer-encoding')).toBeNull(); - await expect(context.res?.text()).resolves.toBe( - 'proxied-with-transport-headers', - ); - }); - - it('proxies federated async CSS expose chunks to remote origin', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock(async () => { - return new Response('.remote-style{}', { - status: 200, - headers: { - 'content-type': 'text/css', - }, - }); - }); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expect(fetchMock).toHaveBeenCalledWith( - 'http://127.0.0.1:3999/static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css', - ); - expect(next).not.toHaveBeenCalled(); - await expect(context.res?.text()).resolves.toBe('.remote-style{}'); - }); - - it('proxies async JS chunks with react server component marker', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock(async () => { - return new Response('proxied-rsc-chunk', { - status: 200, - headers: { - 'content-type': 'application/javascript', - }, - }); - }); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/503_react-server-components_0f2d4f91.js', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expect(fetchMock).toHaveBeenCalledWith( - 'http://127.0.0.1:3999/static/js/async/503_react-server-components_0f2d4f91.js', - ); - expect(next).not.toHaveBeenCalled(); - await expect(context.res?.text()).resolves.toBe('proxied-rsc-chunk'); - }); - - it('proxies async JS chunks containing node_modules react markers', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock(async () => { - return new Response('proxied-react-chunk', { - status: 200, - headers: { - 'content-type': 'application/javascript', - }, - }); - }); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/node_modules_pnpm_react_19.0.0_react-dom_19.0.0.js', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expect(fetchMock).toHaveBeenCalledWith( - 'http://127.0.0.1:3999/static/js/async/node_modules_pnpm_react_19.0.0_react-dom_19.0.0.js', - ); - expect(next).not.toHaveBeenCalled(); - await expect(context.res?.text()).resolves.toBe('proxied-react-chunk'); - }); - - it('falls through when request path is outside federated asset patterns', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - async () => new Response('ignored', { status: 200 }), - ); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/js/server-component-root.abc123.js', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expect(fetchMock).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(context.res).toBeUndefined(); - }); - - it('skips proxying when request is marked as internal fallback fetch', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - async () => new Response('ignored', { status: 200 }), - ); - const context: { - req: { url: string; headers?: { get?: (name: string) => string | null } }; - res?: Response; - } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_actions.44d8f1d7ae.js', - headers: { - get: (name: string) => - name === INTERNAL_FALLBACK_HEADER ? '1' : undefined, - }, - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expect(fetchMock).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(context.res).toBeUndefined(); - }); - - it('falls through when remote port is not configured', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - async () => new Response('ignored', { status: 200 }), - ); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_actions.44d8f1d7ae.js', - }, - }; - - await withRemotePort(undefined, () => handler(context, next)); - - expect(fetchMock).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(context.res).toBeUndefined(); - }); - - it('falls through when upstream returns non-ok response', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - async () => new Response('not-found', { status: 404 }), - ); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_actions.44d8f1d7ae.js', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expect(fetchMock).toHaveBeenCalledTimes(2); - expectInternalFallbackFetchCall( - fetchMock, - 2, - 'http://127.0.0.1:3999/static/mf-manifest.json', - ); - expect(next).toHaveBeenCalledTimes(1); - expect(context.res).toBeUndefined(); - }); - - it('falls through when upstream fetch throws', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock(async () => { - throw new Error('upstream-unreachable'); - }); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_actionBundle.c842b162f4.js', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expect(fetchMock).toHaveBeenCalledTimes(2); - expectInternalFallbackFetchCall( - fetchMock, - 2, - 'http://127.0.0.1:3999/static/mf-manifest.json', - ); - expect(next).toHaveBeenCalledTimes(1); - expect(context.res).toBeUndefined(); - }); - - it('recovers from stale expose chunk path via manifest-driven fallback', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce(new Response('not-found', { status: 404 })) - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - exposes: [ - { - assets: { - js: { - sync: [ - 'static/js/async/__federation_expose_RemoteServerCard.6e997e54ed.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ) - .mockResolvedValueOnce( - new Response('fallback-hit', { - status: 200, - headers: { - 'content-type': 'application/javascript', - }, - }), - ), - ); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteServerCard.js', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expect(fetchMock).toHaveBeenNthCalledWith( - 1, - 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteServerCard.js', - ); - expectInternalFallbackFetchCall( - fetchMock, - 2, - 'http://127.0.0.1:3999/static/mf-manifest.json', - ); - expectInternalFallbackFetchCall( - fetchMock, - 3, - 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteServerCard.6e997e54ed.js', - ); - expect(next).not.toHaveBeenCalled(); - await expect(context.res?.text()).resolves.toBe('fallback-hit'); - }); - - it('prefers hashed manifest fallback when stale alias is also listed', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce(new Response('not-found', { status: 404 })) - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - exposes: [ - { - assets: { - js: { - sync: [ - 'static/js/async/__federation_expose_RemoteServerCard.js', - 'static/js/async/__federation_expose_RemoteServerCard.a1b2c3d4.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ) - .mockResolvedValueOnce( - new Response('hashed-preferred-fallback-hit', { - status: 200, - headers: { - 'content-type': 'application/javascript', - }, - }), - ), - ); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteServerCard.js', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expectInternalFallbackFetchCall( - fetchMock, - 3, - 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteServerCard.a1b2c3d4.js', - ); - expect(next).not.toHaveBeenCalled(); - await expect(context.res?.text()).resolves.toBe( - 'hashed-preferred-fallback-hit', - ); - }); - - it('recovers stale hashed expose chunk path via manifest-driven fallback', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce(new Response('not-found', { status: 404 })) - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - exposes: [ - { - assets: { - js: { - sync: [ - 'static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ) - .mockResolvedValueOnce( - new Response('hashed-fallback-hit', { - status: 200, - headers: { - 'content-type': 'application/javascript', - }, - }), - ), - ); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteClientCounter.deadbeef12.js', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expect(fetchMock).toHaveBeenNthCalledWith( - 1, - 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.deadbeef12.js', - ); - expectInternalFallbackFetchCall( - fetchMock, - 2, - 'http://127.0.0.1:3999/static/mf-manifest.json', - ); - expectInternalFallbackFetchCall( - fetchMock, - 3, - 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', - ); - expect(next).not.toHaveBeenCalled(); - await expect(context.res?.text()).resolves.toBe('hashed-fallback-hit'); + it('does not define a host modern.server middleware override', () => { + const hostModernServerPath = path.join(hostServerDir, 'modern.server.ts'); + expect(fs.existsSync(hostModernServerPath)).toBe(false); }); - it('recovers stale expose path when manifest match is under shared assets', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce(new Response('not-found', { status: 404 })) - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - shared: [ - { - assets: { - js: { - sync: [ - 'static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ) - .mockResolvedValueOnce( - new Response('shared-fallback-hit', { - status: 200, - headers: { - 'content-type': 'application/javascript', - }, - }), - ), - ); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteClientCounter.js', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expect(fetchMock).toHaveBeenNthCalledWith( - 1, - 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.js', - ); - expectInternalFallbackFetchCall( - fetchMock, - 2, - 'http://127.0.0.1:3999/static/mf-manifest.json', - ); - expectInternalFallbackFetchCall( - fetchMock, - 3, - 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', - ); - expect(next).not.toHaveBeenCalled(); - await expect(context.res?.text()).resolves.toBe('shared-fallback-hit'); - }); - - it('recovers stale CSS expose path via manifest-driven fallback', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce(new Response('not-found', { status: 404 })) - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - exposes: [ - { - assets: { - js: { - sync: [], - async: [], - }, - css: { - sync: [ - 'static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css', - ], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ) - .mockResolvedValueOnce( - new Response('.fallback-style{}', { - status: 200, - headers: { - 'content-type': 'text/css', - }, - }), - ), - ); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/css/async/__federation_expose_RemoteClientCounter.css', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expect(fetchMock).toHaveBeenNthCalledWith( - 1, - 'http://127.0.0.1:3999/static/css/async/__federation_expose_RemoteClientCounter.css', - ); - expectInternalFallbackFetchCall( - fetchMock, - 2, - 'http://127.0.0.1:3999/static/mf-manifest.json', - ); - expectInternalFallbackFetchCall( - fetchMock, - 3, - 'http://127.0.0.1:3999/static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css', - ); - expect(next).not.toHaveBeenCalled(); - await expect(context.res?.text()).resolves.toBe('.fallback-style{}'); - }); - - it('recovers stale hashed CSS expose path via manifest-driven fallback', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce(new Response('not-found', { status: 404 })) - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - exposes: [ - { - assets: { - js: { - sync: [], - async: [], - }, - css: { - sync: [ - 'static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css', - ], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ) - .mockResolvedValueOnce( - new Response('.hashed-fallback-style{}', { - status: 200, - headers: { - 'content-type': 'text/css', - }, - }), - ), - ); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/css/async/__federation_expose_RemoteClientCounter.deadbeef12.css', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expect(fetchMock).toHaveBeenNthCalledWith( - 1, - 'http://127.0.0.1:3999/static/css/async/__federation_expose_RemoteClientCounter.deadbeef12.css', - ); - expectInternalFallbackFetchCall( - fetchMock, - 2, - 'http://127.0.0.1:3999/static/mf-manifest.json', - ); - expectInternalFallbackFetchCall( - fetchMock, - 3, - 'http://127.0.0.1:3999/static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css', - ); - expect(next).not.toHaveBeenCalled(); - await expect(context.res?.text()).resolves.toBe('.hashed-fallback-style{}'); - }); - - it('falls through when manifest lookup has no matching fallback asset', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce(new Response('not-found', { status: 404 })) - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - exposes: [ - { - assets: { - js: { - sync: [ - 'static/js/async/__federation_expose_other.abc123.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ), - ); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteServerCard.js', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expect(fetchMock).toHaveBeenCalledTimes(2); - expect(fetchMock).toHaveBeenNthCalledWith( - 1, - 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteServerCard.js', - ); - expectInternalFallbackFetchCall( - fetchMock, - 2, - 'http://127.0.0.1:3999/static/mf-manifest.json', - ); - expect(next).toHaveBeenCalledTimes(1); - expect(context.res).toBeUndefined(); - }); - - it('falls through when manifest response body is invalid JSON', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce(new Response('not-found', { status: 404 })) - .mockResolvedValueOnce( - new Response('not-json-manifest', { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }), - ), - ); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteServerCard.js', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expect(fetchMock).toHaveBeenCalledTimes(2); - expect(next).toHaveBeenCalledTimes(1); - expect(context.res).toBeUndefined(); - }); - - it('falls through when manifest request throws after stale asset miss', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce(new Response('not-found', { status: 404 })) - .mockRejectedValueOnce(new Error('manifest-fetch-failed')), - ); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteServerCard.js', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expect(fetchMock).toHaveBeenCalledTimes(2); - expect(next).toHaveBeenCalledTimes(1); - expect(context.res).toBeUndefined(); - }); - - it('preserves query string when retrying manifest-resolved fallback asset', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce(new Response('not-found', { status: 404 })) - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - exposes: [ - { - assets: { - js: { - sync: [ - 'static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ) - .mockResolvedValueOnce( - new Response('query-fallback-hit', { - status: 200, - headers: { - 'content-type': 'application/javascript', - }, - }), - ), - ); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteClientCounter.js?cache=1&v=2', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expectInternalFallbackFetchCall( - fetchMock, - 3, - 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js?cache=1&v=2', - ); - expect(next).not.toHaveBeenCalled(); - await expect(context.res?.text()).resolves.toBe('query-fallback-hit'); - }); - - it('supports absolute manifest fallback asset URLs and merges request query params', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce(new Response('not-found', { status: 404 })) - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - exposes: [ - { - assets: { - js: { - sync: [ - 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js?manifest=1', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ) - .mockResolvedValueOnce( - new Response('absolute-query-fallback-hit', { - status: 200, - headers: { - 'content-type': 'application/javascript', - }, - }), - ), - ); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteClientCounter.js?cache=1', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expectInternalFallbackFetchCall( - fetchMock, - 3, - 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js?manifest=1&cache=1', - ); - expect(next).not.toHaveBeenCalled(); - await expect(context.res?.text()).resolves.toBe( - 'absolute-query-fallback-hit', - ); - }); - - it('lets request query params override manifest fallback query params', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce(new Response('not-found', { status: 404 })) - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - exposes: [ - { - assets: { - js: { - sync: [ - 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js?cache=manifest&v=1', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ) - .mockResolvedValueOnce( - new Response('query-override-fallback-hit', { - status: 200, - headers: { - 'content-type': 'application/javascript', - }, - }), - ), - ); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteClientCounter.js?cache=request&x=2', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - const fallbackCallUrl = fetchMock.mock.calls[2]?.[0] as string; - const parsedFallbackCallUrl = new URL(fallbackCallUrl); - expect(parsedFallbackCallUrl.origin).toBe('http://127.0.0.1:3999'); - expect(parsedFallbackCallUrl.pathname).toBe( - '/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', - ); - expect(parsedFallbackCallUrl.searchParams.get('cache')).toBe('request'); - expect(parsedFallbackCallUrl.searchParams.get('v')).toBe('1'); - expect(parsedFallbackCallUrl.searchParams.get('x')).toBe('2'); - expect(next).not.toHaveBeenCalled(); - await expect(context.res?.text()).resolves.toBe( - 'query-override-fallback-hit', - ); - }); - - it('falls through when manifest fallback asset URL points to another origin', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce(new Response('not-found', { status: 404 })) - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - exposes: [ - { - assets: { - js: { - sync: [ - 'https://cdn.example.com/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ), - ); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteClientCounter.js', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expect(fetchMock).toHaveBeenCalledTimes(2); - expect(next).toHaveBeenCalledTimes(1); - expect(context.res).toBeUndefined(); - }); - - it('falls through when manifest fallback resolves to original request URL', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce(new Response('not-found', { status: 404 })) - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - exposes: [ - { - assets: { - js: { - sync: [ - 'static/js/async/__federation_expose_RemoteClientCounter.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ), - ); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteClientCounter.js', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expect(fetchMock).toHaveBeenCalledTimes(2); - expect(next).toHaveBeenCalledTimes(1); - expect(context.res).toBeUndefined(); - }); - - it('falls through when manifest fallback path escapes async asset directory', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce(new Response('not-found', { status: 404 })) - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - shared: [ - { - assets: { - js: { - sync: [ - 'static/js/async/../__federation_expose_RemoteClientCounter.7745fe5f0a.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ), - ); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteClientCounter.js', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expect(fetchMock).toHaveBeenCalledTimes(2); - expect(next).toHaveBeenCalledTimes(1); - expect(context.res).toBeUndefined(); - }); - - it('falls through when manifest fallback path uses encoded async-directory escape', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce(new Response('not-found', { status: 404 })) - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - shared: [ - { - assets: { - js: { - sync: [ - 'static/js/async/%2e%2e/__federation_expose_RemoteClientCounter.7745fe5f0a.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ), - ); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteClientCounter.js', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expect(fetchMock).toHaveBeenCalledTimes(2); - expect(next).toHaveBeenCalledTimes(1); - expect(context.res).toBeUndefined(); - }); - - it('falls through when manifest fallback path has invalid percent encoding', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce(new Response('not-found', { status: 404 })) - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - shared: [ - { - assets: { - js: { - sync: [ - 'static/js/async/%E0%A4%A/__federation_expose_RemoteClientCounter.7745fe5f0a.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ), - ); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteClientCounter.js', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expect(fetchMock).toHaveBeenCalledTimes(2); - expect(next).toHaveBeenCalledTimes(1); - expect(context.res).toBeUndefined(); - }); - - it('matches fallback chunks when manifest hash suffix includes non-hex characters', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce(new Response('not-found', { status: 404 })) - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - exposes: [ - { - assets: { - js: { - sync: [ - 'static/js/async/__federation_expose_RemoteServerCard.a1b2c3x9.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ) - .mockResolvedValueOnce( - new Response('non-hex-hash-fallback-hit', { - status: 200, - headers: { - 'content-type': 'application/javascript', - }, - }), - ), - ); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_RemoteServerCard.js', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expectInternalFallbackFetchCall( - fetchMock, - 3, - 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteServerCard.a1b2c3x9.js', - ); - expect(next).not.toHaveBeenCalled(); - await expect(context.res?.text()).resolves.toBe( - 'non-hex-hash-fallback-hit', - ); - }); - - it('resolves fallback asset paths from manifest async asset arrays', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce(new Response('not-found', { status: 404 })) - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - shared: [ - { - assets: { - js: { - sync: [], - async: [ - 'static/js/async/__federation_expose_nestedActions.a8ce95b11a.js', - ], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ) - .mockResolvedValueOnce( - new Response('async-array-fallback-hit', { - status: 200, - headers: { - 'content-type': 'application/javascript', - }, - }), - ), - ); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/__federation_expose_nestedActions.js', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expectInternalFallbackFetchCall( - fetchMock, - 3, - 'http://127.0.0.1:3999/static/js/async/__federation_expose_nestedActions.a8ce95b11a.js', - ); - expect(next).not.toHaveBeenCalled(); - await expect(context.res?.text()).resolves.toBe('async-array-fallback-hit'); - }); - - it('does not attempt manifest fallback for non-expose marker chunk paths', async () => { - const handler = getProxyMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce(new Response('not-found', { status: 404 })), - ); - const context: { req: { url: string }; res?: Response } = { - req: { - url: 'http://127.0.0.1:3007/static/js/async/503_react-server-components_0f2d4f91.js', - }, - }; - - await withRemotePort('3999', () => handler(context, next)); - - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(fetchMock).toHaveBeenCalledWith( - 'http://127.0.0.1:3999/static/js/async/503_react-server-components_0f2d4f91.js', + it('does not define manifest fallback helpers in host server layer', () => { + const hostManifestFallbackPath = path.join( + hostServerDir, + 'manifestFallback.ts', ); - expect(next).toHaveBeenCalledTimes(1); - expect(context.res).toBeUndefined(); + expect(fs.existsSync(hostManifestFallbackPath)).toBe(false); }); }); From 84938fb6c31ecc863c65816921849b95796e830c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 22:32:13 +0000 Subject: [PATCH 294/324] refactor(rsc-mf): delete all fallback and proxy utilities --- .../rsc-mf/remote/server/manifestFallback.ts | 186 --- .../rsc-mf/remote/server/modern.server.ts | 139 -- .../rsc-mf/remote/server/proxyResponse.ts | 46 - .../rsc-mf/shared/manifestFallback.ts | 186 --- .../rsc-mf/shared/proxyResponse.ts | 46 - .../rsc-mf/tests/manifestFallback.test.ts | 303 ---- .../rsc-mf/tests/modernServerConfig.test.ts | 5 + .../rsc-mf/tests/proxyResponse.test.ts | 153 -- .../tests/remoteModernServerConfig.test.ts | 1317 +---------------- 9 files changed, 24 insertions(+), 2357 deletions(-) delete mode 100644 tests/integration/rsc-mf/remote/server/manifestFallback.ts delete mode 100644 tests/integration/rsc-mf/remote/server/modern.server.ts delete mode 100644 tests/integration/rsc-mf/remote/server/proxyResponse.ts delete mode 100644 tests/integration/rsc-mf/shared/manifestFallback.ts delete mode 100644 tests/integration/rsc-mf/shared/proxyResponse.ts delete mode 100644 tests/integration/rsc-mf/tests/manifestFallback.test.ts delete mode 100644 tests/integration/rsc-mf/tests/proxyResponse.test.ts diff --git a/tests/integration/rsc-mf/remote/server/manifestFallback.ts b/tests/integration/rsc-mf/remote/server/manifestFallback.ts deleted file mode 100644 index 959b7b5ae0bc..000000000000 --- a/tests/integration/rsc-mf/remote/server/manifestFallback.ts +++ /dev/null @@ -1,186 +0,0 @@ -const EXPOSE_CHUNK_HASH_SUFFIX_PATTERN = /\.[a-z0-9]{6,}$/i; -const EXPOSE_CHUNK_HASHED_ASSET_PATTERN = /\.[a-z0-9]{6,}\.(js|css)$/i; -export const INTERNAL_FALLBACK_HEADER = 'x-rsc-mf-internal-fallback'; - -export interface RemoteManifestAssetEntry { - assets?: { - js?: { - sync?: string[]; - async?: string[]; - }; - css?: { - sync?: string[]; - async?: string[]; - }; - }; -} - -export interface RemoteManifestShape { - shared?: RemoteManifestAssetEntry[]; - exposes?: RemoteManifestAssetEntry[]; -} - -export const isExposeAssetRequestPath = (pathname: string) => - pathname.includes('__federation_expose_') && - (pathname.endsWith('.js') || pathname.endsWith('.css')); - -export const getRequestedAssetDirectory = (pathname: string) => - pathname.includes('/static/css/async/') - ? 'static/css/async/' - : 'static/js/async/'; - -const toCanonicalChunkName = (filePath: string) => - filePath - .replace(/\/+$/, '') - .split('/') - .pop() - ?.replace(/\.(js|css)$/i, '') - .replace(EXPOSE_CHUNK_HASH_SUFFIX_PATTERN, ''); - -const toNormalizedManifestAssetPath = (assetPath: string) => { - try { - return new URL(assetPath).pathname.replace(/^\/+/, ''); - } catch { - return assetPath.replace(/^[./]+/, '').split(/[?#]/, 1)[0]; - } -}; - -const toNormalizedRequestPath = (pathname: string) => - pathname.replace(/^\/+/, '').split(/[?#]/, 1)[0]; - -const hasChunkHashInAssetPath = (assetPath: string) => - EXPOSE_CHUNK_HASHED_ASSET_PATTERN.test(assetPath); - -const collectManifestAssetPaths = (manifest: RemoteManifestShape) => { - const entries = [...(manifest.shared || []), ...(manifest.exposes || [])]; - const assetPaths = new Set(); - for (const entry of entries) { - const jsSyncAssets = entry.assets?.js?.sync || []; - const jsAsyncAssets = entry.assets?.js?.async || []; - const cssSyncAssets = entry.assets?.css?.sync || []; - const cssAsyncAssets = entry.assets?.css?.async || []; - for (const assetPath of [ - ...jsSyncAssets, - ...jsAsyncAssets, - ...cssSyncAssets, - ...cssAsyncAssets, - ]) { - assetPaths.add(assetPath); - } - } - return [...assetPaths]; -}; - -export const resolveManifestFallbackAssetPath = ( - pathname: string, - manifest: RemoteManifestShape, -) => { - if (!isExposeAssetRequestPath(pathname)) { - return undefined; - } - - const canonicalRequestedChunkName = toCanonicalChunkName(pathname); - if (!canonicalRequestedChunkName) { - return undefined; - } - - const requestedAssetDirectory = getRequestedAssetDirectory(pathname); - const normalizedRequestedPath = toNormalizedRequestPath(pathname); - const manifestAssets = collectManifestAssetPaths(manifest); - const candidateAssets = manifestAssets - .map(assetPath => ({ - assetPath, - normalizedAssetPath: toNormalizedManifestAssetPath(assetPath), - })) - .filter(({ normalizedAssetPath }) => { - if (!normalizedAssetPath.startsWith(requestedAssetDirectory)) { - return false; - } - return ( - toCanonicalChunkName(normalizedAssetPath) === - canonicalRequestedChunkName - ); - }); - - const preferredCandidate = - candidateAssets.find( - ({ normalizedAssetPath }) => - normalizedAssetPath !== normalizedRequestedPath && - hasChunkHashInAssetPath(normalizedAssetPath), - ) || - candidateAssets.find( - ({ normalizedAssetPath }) => - normalizedAssetPath !== normalizedRequestedPath, - ) || - candidateAssets.find(({ normalizedAssetPath }) => - hasChunkHashInAssetPath(normalizedAssetPath), - ) || - candidateAssets[0]; - - return preferredCandidate?.assetPath; -}; - -export const createManifestFallbackAssetUrl = ({ - remoteOrigin, - fallbackAssetPath, - requestSearch, - requestedAssetDirectory, - requestUrl, -}: { - remoteOrigin: string; - fallbackAssetPath: string; - requestSearch: string; - requestedAssetDirectory: string; - requestUrl?: string; -}) => { - let remoteOriginUrl: URL; - try { - remoteOriginUrl = new URL(remoteOrigin); - } catch { - return undefined; - } - - let fallbackAssetUrl: URL; - try { - fallbackAssetUrl = new URL(fallbackAssetPath, `${remoteOriginUrl.origin}/`); - } catch { - return undefined; - } - - if (fallbackAssetUrl.origin !== remoteOriginUrl.origin) { - return undefined; - } - const normalizedFallbackPathname = fallbackAssetUrl.pathname.replace( - /^\/+/, - '', - ); - let decodedFallbackPathname: string; - try { - decodedFallbackPathname = decodeURIComponent(normalizedFallbackPathname); - } catch { - return undefined; - } - if (!decodedFallbackPathname.startsWith(requestedAssetDirectory)) { - return undefined; - } - if (decodedFallbackPathname.split('/').some(segment => segment === '..')) { - return undefined; - } - - if (requestSearch) { - const mergedSearchParams = new URLSearchParams(fallbackAssetUrl.search); - const requestSearchParams = new URLSearchParams(requestSearch); - for (const [key, value] of requestSearchParams.entries()) { - mergedSearchParams.set(key, value); - } - const mergedSearch = mergedSearchParams.toString(); - fallbackAssetUrl.search = mergedSearch ? `?${mergedSearch}` : ''; - } - - const resolvedFallbackAssetUrl = fallbackAssetUrl.toString(); - if (requestUrl && resolvedFallbackAssetUrl === requestUrl) { - return undefined; - } - - return resolvedFallbackAssetUrl; -}; diff --git a/tests/integration/rsc-mf/remote/server/modern.server.ts b/tests/integration/rsc-mf/remote/server/modern.server.ts deleted file mode 100644 index 5ead78507bbc..000000000000 --- a/tests/integration/rsc-mf/remote/server/modern.server.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { - type MiddlewareHandler, - defineServerConfig, -} from '@modern-js/server-runtime'; -import { - INTERNAL_FALLBACK_HEADER, - type RemoteManifestShape, - createManifestFallbackAssetUrl, - getRequestedAssetDirectory, - isExposeAssetRequestPath, - resolveManifestFallbackAssetPath, -} from './manifestFallback'; -import { createSafeProxyResponse } from './proxyResponse'; - -const REMOTE_MANIFEST_PATH = '/static/mf-manifest.json'; - -const getRequestHeader = ( - c: Parameters[0], - name: string, -) => { - const requestWithHeaders = c.req as typeof c.req & { - header?: (headerName: string) => string | undefined; - headers?: { get?: (headerName: string) => string | null | undefined }; - }; - const headerValue = - typeof requestWithHeaders.header === 'function' - ? requestWithHeaders.header(name) - : undefined; - return headerValue ?? requestWithHeaders.headers?.get?.(name); -}; - -const setContextResponse = ( - c: Parameters[0], - response: Response, -) => { - const contextWithBody = c as typeof c & { - body?: ( - body: BodyInit | null, - status?: number, - headers?: HeadersInit, - ) => Response; - }; - if (typeof contextWithBody.body === 'function') { - const finalizedResponse = contextWithBody.body( - response.body, - response.status, - response.headers, - ); - c.res = finalizedResponse; - return finalizedResponse; - } - c.res = response; - return response; -}; - -const recoverRemoteExposeAssetMiddleware: MiddlewareHandler = async ( - c, - next, -) => { - const reqUrl = new URL(c.req.url); - const pathname = reqUrl.pathname; - if (!isExposeAssetRequestPath(pathname)) { - await next(); - return; - } - - const isInternalFallbackFetch = - getRequestHeader(c, INTERNAL_FALLBACK_HEADER) === '1'; - if (isInternalFallbackFetch) { - await next(); - return; - } - - const remoteOrigin = reqUrl.origin; - const manifestResponse = await fetch( - `${remoteOrigin}${REMOTE_MANIFEST_PATH}`, - { - headers: { - [INTERNAL_FALLBACK_HEADER]: '1', - }, - }, - ).catch((): undefined => undefined); - if (!manifestResponse?.ok) { - await next(); - return; - } - - const manifest = (await manifestResponse - .json() - .catch((): undefined => undefined)) as RemoteManifestShape | undefined; - if (!manifest) { - await next(); - return; - } - - const fallbackAssetPath = resolveManifestFallbackAssetPath( - pathname, - manifest, - ); - if (!fallbackAssetPath) { - await next(); - return; - } - - const fallbackAssetUrl = createManifestFallbackAssetUrl({ - remoteOrigin, - fallbackAssetPath, - requestSearch: reqUrl.search, - requestedAssetDirectory: getRequestedAssetDirectory(pathname), - requestUrl: reqUrl.toString(), - }); - if (!fallbackAssetUrl) { - await next(); - return; - } - - const fallbackAssetResponse = await fetch(fallbackAssetUrl, { - headers: { - [INTERNAL_FALLBACK_HEADER]: '1', - }, - }).catch((): undefined => undefined); - if (!fallbackAssetResponse?.ok) { - await next(); - return; - } - - return setContextResponse(c, createSafeProxyResponse(fallbackAssetResponse)); -}; - -export default defineServerConfig({ - middlewares: [ - { - name: 'recover-remote-federation-expose-asset', - handler: recoverRemoteExposeAssetMiddleware, - order: 'pre', - before: ['server-static'], - }, - ], -}); diff --git a/tests/integration/rsc-mf/remote/server/proxyResponse.ts b/tests/integration/rsc-mf/remote/server/proxyResponse.ts deleted file mode 100644 index 68a1c61440c3..000000000000 --- a/tests/integration/rsc-mf/remote/server/proxyResponse.ts +++ /dev/null @@ -1,46 +0,0 @@ -const PROXY_UNSAFE_RESPONSE_HEADERS = [ - 'content-length', - 'content-encoding', - 'connection', - 'keep-alive', - 'proxy-authenticate', - 'proxy-connection', - 'proxy-authorization', - 'te', - 'trailer', - 'transfer-encoding', - 'upgrade', -]; -const STATUS_CODES_WITHOUT_BODY = new Set([204, 205, 304]); - -const isResponseStatusWithoutBody = (status: number) => - STATUS_CODES_WITHOUT_BODY.has(status); - -const normalizeConnectionHeaderToken = (token: string) => - token - .trim() - .replace(/^"+|"+$/g, '') - .trim() - .toLowerCase(); - -export const createSafeProxyResponse = (upstream: Response) => { - const headers = new Headers(upstream.headers); - for (const headerName of PROXY_UNSAFE_RESPONSE_HEADERS) { - headers.delete(headerName); - } - const connectionHeaderTokens = (upstream.headers.get('connection') || '') - .split(',') - .map(normalizeConnectionHeaderToken) - .filter(Boolean); - for (const token of connectionHeaderTokens) { - headers.delete(token); - } - const responseBody = isResponseStatusWithoutBody(upstream.status) - ? null - : upstream.body; - return new Response(responseBody, { - status: upstream.status, - statusText: upstream.statusText, - headers, - }); -}; diff --git a/tests/integration/rsc-mf/shared/manifestFallback.ts b/tests/integration/rsc-mf/shared/manifestFallback.ts deleted file mode 100644 index 959b7b5ae0bc..000000000000 --- a/tests/integration/rsc-mf/shared/manifestFallback.ts +++ /dev/null @@ -1,186 +0,0 @@ -const EXPOSE_CHUNK_HASH_SUFFIX_PATTERN = /\.[a-z0-9]{6,}$/i; -const EXPOSE_CHUNK_HASHED_ASSET_PATTERN = /\.[a-z0-9]{6,}\.(js|css)$/i; -export const INTERNAL_FALLBACK_HEADER = 'x-rsc-mf-internal-fallback'; - -export interface RemoteManifestAssetEntry { - assets?: { - js?: { - sync?: string[]; - async?: string[]; - }; - css?: { - sync?: string[]; - async?: string[]; - }; - }; -} - -export interface RemoteManifestShape { - shared?: RemoteManifestAssetEntry[]; - exposes?: RemoteManifestAssetEntry[]; -} - -export const isExposeAssetRequestPath = (pathname: string) => - pathname.includes('__federation_expose_') && - (pathname.endsWith('.js') || pathname.endsWith('.css')); - -export const getRequestedAssetDirectory = (pathname: string) => - pathname.includes('/static/css/async/') - ? 'static/css/async/' - : 'static/js/async/'; - -const toCanonicalChunkName = (filePath: string) => - filePath - .replace(/\/+$/, '') - .split('/') - .pop() - ?.replace(/\.(js|css)$/i, '') - .replace(EXPOSE_CHUNK_HASH_SUFFIX_PATTERN, ''); - -const toNormalizedManifestAssetPath = (assetPath: string) => { - try { - return new URL(assetPath).pathname.replace(/^\/+/, ''); - } catch { - return assetPath.replace(/^[./]+/, '').split(/[?#]/, 1)[0]; - } -}; - -const toNormalizedRequestPath = (pathname: string) => - pathname.replace(/^\/+/, '').split(/[?#]/, 1)[0]; - -const hasChunkHashInAssetPath = (assetPath: string) => - EXPOSE_CHUNK_HASHED_ASSET_PATTERN.test(assetPath); - -const collectManifestAssetPaths = (manifest: RemoteManifestShape) => { - const entries = [...(manifest.shared || []), ...(manifest.exposes || [])]; - const assetPaths = new Set(); - for (const entry of entries) { - const jsSyncAssets = entry.assets?.js?.sync || []; - const jsAsyncAssets = entry.assets?.js?.async || []; - const cssSyncAssets = entry.assets?.css?.sync || []; - const cssAsyncAssets = entry.assets?.css?.async || []; - for (const assetPath of [ - ...jsSyncAssets, - ...jsAsyncAssets, - ...cssSyncAssets, - ...cssAsyncAssets, - ]) { - assetPaths.add(assetPath); - } - } - return [...assetPaths]; -}; - -export const resolveManifestFallbackAssetPath = ( - pathname: string, - manifest: RemoteManifestShape, -) => { - if (!isExposeAssetRequestPath(pathname)) { - return undefined; - } - - const canonicalRequestedChunkName = toCanonicalChunkName(pathname); - if (!canonicalRequestedChunkName) { - return undefined; - } - - const requestedAssetDirectory = getRequestedAssetDirectory(pathname); - const normalizedRequestedPath = toNormalizedRequestPath(pathname); - const manifestAssets = collectManifestAssetPaths(manifest); - const candidateAssets = manifestAssets - .map(assetPath => ({ - assetPath, - normalizedAssetPath: toNormalizedManifestAssetPath(assetPath), - })) - .filter(({ normalizedAssetPath }) => { - if (!normalizedAssetPath.startsWith(requestedAssetDirectory)) { - return false; - } - return ( - toCanonicalChunkName(normalizedAssetPath) === - canonicalRequestedChunkName - ); - }); - - const preferredCandidate = - candidateAssets.find( - ({ normalizedAssetPath }) => - normalizedAssetPath !== normalizedRequestedPath && - hasChunkHashInAssetPath(normalizedAssetPath), - ) || - candidateAssets.find( - ({ normalizedAssetPath }) => - normalizedAssetPath !== normalizedRequestedPath, - ) || - candidateAssets.find(({ normalizedAssetPath }) => - hasChunkHashInAssetPath(normalizedAssetPath), - ) || - candidateAssets[0]; - - return preferredCandidate?.assetPath; -}; - -export const createManifestFallbackAssetUrl = ({ - remoteOrigin, - fallbackAssetPath, - requestSearch, - requestedAssetDirectory, - requestUrl, -}: { - remoteOrigin: string; - fallbackAssetPath: string; - requestSearch: string; - requestedAssetDirectory: string; - requestUrl?: string; -}) => { - let remoteOriginUrl: URL; - try { - remoteOriginUrl = new URL(remoteOrigin); - } catch { - return undefined; - } - - let fallbackAssetUrl: URL; - try { - fallbackAssetUrl = new URL(fallbackAssetPath, `${remoteOriginUrl.origin}/`); - } catch { - return undefined; - } - - if (fallbackAssetUrl.origin !== remoteOriginUrl.origin) { - return undefined; - } - const normalizedFallbackPathname = fallbackAssetUrl.pathname.replace( - /^\/+/, - '', - ); - let decodedFallbackPathname: string; - try { - decodedFallbackPathname = decodeURIComponent(normalizedFallbackPathname); - } catch { - return undefined; - } - if (!decodedFallbackPathname.startsWith(requestedAssetDirectory)) { - return undefined; - } - if (decodedFallbackPathname.split('/').some(segment => segment === '..')) { - return undefined; - } - - if (requestSearch) { - const mergedSearchParams = new URLSearchParams(fallbackAssetUrl.search); - const requestSearchParams = new URLSearchParams(requestSearch); - for (const [key, value] of requestSearchParams.entries()) { - mergedSearchParams.set(key, value); - } - const mergedSearch = mergedSearchParams.toString(); - fallbackAssetUrl.search = mergedSearch ? `?${mergedSearch}` : ''; - } - - const resolvedFallbackAssetUrl = fallbackAssetUrl.toString(); - if (requestUrl && resolvedFallbackAssetUrl === requestUrl) { - return undefined; - } - - return resolvedFallbackAssetUrl; -}; diff --git a/tests/integration/rsc-mf/shared/proxyResponse.ts b/tests/integration/rsc-mf/shared/proxyResponse.ts deleted file mode 100644 index 68a1c61440c3..000000000000 --- a/tests/integration/rsc-mf/shared/proxyResponse.ts +++ /dev/null @@ -1,46 +0,0 @@ -const PROXY_UNSAFE_RESPONSE_HEADERS = [ - 'content-length', - 'content-encoding', - 'connection', - 'keep-alive', - 'proxy-authenticate', - 'proxy-connection', - 'proxy-authorization', - 'te', - 'trailer', - 'transfer-encoding', - 'upgrade', -]; -const STATUS_CODES_WITHOUT_BODY = new Set([204, 205, 304]); - -const isResponseStatusWithoutBody = (status: number) => - STATUS_CODES_WITHOUT_BODY.has(status); - -const normalizeConnectionHeaderToken = (token: string) => - token - .trim() - .replace(/^"+|"+$/g, '') - .trim() - .toLowerCase(); - -export const createSafeProxyResponse = (upstream: Response) => { - const headers = new Headers(upstream.headers); - for (const headerName of PROXY_UNSAFE_RESPONSE_HEADERS) { - headers.delete(headerName); - } - const connectionHeaderTokens = (upstream.headers.get('connection') || '') - .split(',') - .map(normalizeConnectionHeaderToken) - .filter(Boolean); - for (const token of connectionHeaderTokens) { - headers.delete(token); - } - const responseBody = isResponseStatusWithoutBody(upstream.status) - ? null - : upstream.body; - return new Response(responseBody, { - status: upstream.status, - statusText: upstream.statusText, - headers, - }); -}; diff --git a/tests/integration/rsc-mf/tests/manifestFallback.test.ts b/tests/integration/rsc-mf/tests/manifestFallback.test.ts deleted file mode 100644 index ab5dc678e442..000000000000 --- a/tests/integration/rsc-mf/tests/manifestFallback.test.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { - type RemoteManifestShape, - createManifestFallbackAssetUrl, - getRequestedAssetDirectory, - isExposeAssetRequestPath, - resolveManifestFallbackAssetPath, -} from '../shared/manifestFallback'; - -describe('manifest fallback shared helpers', () => { - it('identifies expose asset request paths', () => { - expect( - isExposeAssetRequestPath( - '/static/js/async/__federation_expose_RemoteClientCounter.js', - ), - ).toBe(true); - expect( - isExposeAssetRequestPath( - '/static/css/async/__federation_expose_RemoteClientCounter.css', - ), - ).toBe(true); - expect(isExposeAssetRequestPath('/static/js/async/743.32436c1247.js')).toBe( - false, - ); - }); - - it('derives requested async asset directory from pathname', () => { - expect( - getRequestedAssetDirectory( - '/static/js/async/__federation_expose_RemoteClientCounter.js', - ), - ).toBe('static/js/async/'); - expect( - getRequestedAssetDirectory( - '/static/css/async/__federation_expose_RemoteClientCounter.css', - ), - ).toBe('static/css/async/'); - }); - - it('resolves fallback assets from shared and exposes manifest entries', () => { - const manifest: RemoteManifestShape = { - shared: [ - { - assets: { - js: { - sync: [ - 'static/js/async/__federation_expose_actions.44d8f1d7ae.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - exposes: [ - { - assets: { - js: { - sync: [ - 'static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }; - - expect( - resolveManifestFallbackAssetPath( - '/static/js/async/__federation_expose_RemoteClientCounter.js', - manifest, - ), - ).toBe( - 'static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', - ); - expect( - resolveManifestFallbackAssetPath( - '/static/js/async/__federation_expose_actions.js', - manifest, - ), - ).toBe('static/js/async/__federation_expose_actions.44d8f1d7ae.js'); - }); - - it('supports absolute manifest assets and rejects non-matching paths', () => { - const manifest: RemoteManifestShape = { - shared: [ - { - assets: { - js: { - sync: [ - 'http://127.0.0.1:3999/static/js/async/__federation_expose_nestedActions.a8ce95b11a.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }; - - expect( - resolveManifestFallbackAssetPath( - '/static/js/async/__federation_expose_nestedActions.js', - manifest, - ), - ).toBe( - 'http://127.0.0.1:3999/static/js/async/__federation_expose_nestedActions.a8ce95b11a.js', - ); - expect( - resolveManifestFallbackAssetPath( - '/static/js/async/not-an-expose.js', - manifest, - ), - ).toBeUndefined(); - }); - - it('resolves stale hashed expose requests to current hashed assets', () => { - const manifest: RemoteManifestShape = { - exposes: [ - { - assets: { - js: { - sync: [ - 'static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }; - - expect( - resolveManifestFallbackAssetPath( - '/static/js/async/__federation_expose_RemoteClientCounter.deadbeef12.js', - manifest, - ), - ).toBe( - 'static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', - ); - }); - - it('prefers hashed fallback candidates over identical stale aliases', () => { - const manifest: RemoteManifestShape = { - exposes: [ - { - assets: { - js: { - sync: [ - 'static/js/async/__federation_expose_RemoteServerCard.js', - 'static/js/async/__federation_expose_RemoteServerCard.a1b2c3d4.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }; - - expect( - resolveManifestFallbackAssetPath( - '/static/js/async/__federation_expose_RemoteServerCard.js', - manifest, - ), - ).toBe('static/js/async/__federation_expose_RemoteServerCard.a1b2c3d4.js'); - }); - - it('resolves stale hashed css expose requests to current hashed css assets', () => { - const manifest: RemoteManifestShape = { - exposes: [ - { - assets: { - js: { - sync: [], - async: [], - }, - css: { - sync: [ - 'static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css', - ], - async: [], - }, - }, - }, - ], - }; - - expect( - resolveManifestFallbackAssetPath( - '/static/css/async/__federation_expose_RemoteClientCounter.deadbeef12.css', - manifest, - ), - ).toBe( - 'static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css', - ); - }); - - it('builds safe fallback URL and merges request query params', () => { - expect( - createManifestFallbackAssetUrl({ - remoteOrigin: 'http://127.0.0.1:3999', - fallbackAssetPath: - 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js?manifest=1', - requestSearch: '?cache=1', - requestedAssetDirectory: 'static/js/async/', - }), - ).toBe( - 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js?manifest=1&cache=1', - ); - }); - - it('lets request query params override manifest query params', () => { - const fallbackUrl = createManifestFallbackAssetUrl({ - remoteOrigin: 'http://127.0.0.1:3999', - fallbackAssetPath: - 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js?cache=manifest&v=1', - requestSearch: '?cache=request&x=2', - requestedAssetDirectory: 'static/js/async/', - }); - expect(fallbackUrl).toBeDefined(); - const parsedFallbackUrl = new URL(fallbackUrl!); - expect(parsedFallbackUrl.searchParams.get('cache')).toBe('request'); - expect(parsedFallbackUrl.searchParams.get('v')).toBe('1'); - expect(parsedFallbackUrl.searchParams.get('x')).toBe('2'); - }); - - it('rejects unsafe fallback URLs', () => { - expect( - createManifestFallbackAssetUrl({ - remoteOrigin: 'not-a-valid-origin', - fallbackAssetPath: - 'static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', - requestSearch: '', - requestedAssetDirectory: 'static/js/async/', - }), - ).toBeUndefined(); - expect( - createManifestFallbackAssetUrl({ - remoteOrigin: 'http://127.0.0.1:3999', - fallbackAssetPath: - 'https://cdn.example.com/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', - requestSearch: '', - requestedAssetDirectory: 'static/js/async/', - }), - ).toBeUndefined(); - expect( - createManifestFallbackAssetUrl({ - remoteOrigin: 'http://127.0.0.1:3999', - fallbackAssetPath: - 'static/js/async/../__federation_expose_RemoteClientCounter.7745fe5f0a.js', - requestSearch: '', - requestedAssetDirectory: 'static/js/async/', - }), - ).toBeUndefined(); - expect( - createManifestFallbackAssetUrl({ - remoteOrigin: 'http://127.0.0.1:3999', - fallbackAssetPath: - 'static/js/async/%2e%2e/__federation_expose_RemoteClientCounter.7745fe5f0a.js', - requestSearch: '', - requestedAssetDirectory: 'static/js/async/', - }), - ).toBeUndefined(); - expect( - createManifestFallbackAssetUrl({ - remoteOrigin: 'http://127.0.0.1:3999', - fallbackAssetPath: - 'static/js/async/%E0%A4%A/__federation_expose_RemoteClientCounter.7745fe5f0a.js', - requestSearch: '', - requestedAssetDirectory: 'static/js/async/', - }), - ).toBeUndefined(); - expect( - createManifestFallbackAssetUrl({ - remoteOrigin: 'http://127.0.0.1:3999', - fallbackAssetPath: - 'static/js/async/__federation_expose_RemoteClientCounter.js', - requestSearch: '', - requestedAssetDirectory: 'static/js/async/', - requestUrl: - 'http://127.0.0.1:3999/static/js/async/__federation_expose_RemoteClientCounter.js', - }), - ).toBeUndefined(); - }); -}); diff --git a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts index 3d715d1cd51c..8134062ec222 100644 --- a/tests/integration/rsc-mf/tests/modernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/modernServerConfig.test.ts @@ -16,4 +16,9 @@ describe('rsc-mf host modern.server contracts', () => { ); expect(fs.existsSync(hostManifestFallbackPath)).toBe(false); }); + + it('does not define proxy response helpers in host server layer', () => { + const hostProxyResponsePath = path.join(hostServerDir, 'proxyResponse.ts'); + expect(fs.existsSync(hostProxyResponsePath)).toBe(false); + }); }); diff --git a/tests/integration/rsc-mf/tests/proxyResponse.test.ts b/tests/integration/rsc-mf/tests/proxyResponse.test.ts deleted file mode 100644 index a1879005fad8..000000000000 --- a/tests/integration/rsc-mf/tests/proxyResponse.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { createSafeProxyResponse } from '../shared/proxyResponse'; - -describe('rsc-mf proxy response helper', () => { - it('removes hop-by-hop response headers while preserving payload', async () => { - const upstream = new Response('proxied-body', { - status: 202, - statusText: 'Accepted-Proxy', - headers: { - 'content-type': 'application/javascript', - 'content-length': '999', - 'content-encoding': 'gzip', - connection: - 'keep-alive, x-custom-hop-header, "x-quoted-hop-header", "x-spaced-hop-header"', - 'keep-alive': 'timeout=5', - 'x-custom-hop-header': 'remove-me', - 'x-quoted-hop-header': 'remove-me', - 'x-spaced-hop-header': 'remove-me', - 'proxy-connection': 'keep-alive', - 'proxy-authenticate': 'Basic realm=test', - 'proxy-authorization': 'Basic dGVzdA==', - te: 'trailers', - trailer: 'x-trailer-a', - upgrade: 'websocket', - 'transfer-encoding': 'chunked', - }, - }); - - const proxied = createSafeProxyResponse(upstream); - - expect(proxied.status).toBe(202); - expect(proxied.statusText).toBe('Accepted-Proxy'); - expect(proxied.headers.get('content-type')).toBe('application/javascript'); - expect(proxied.headers.get('content-length')).toBeNull(); - expect(proxied.headers.get('content-encoding')).toBeNull(); - expect(proxied.headers.get('connection')).toBeNull(); - expect(proxied.headers.get('keep-alive')).toBeNull(); - expect(proxied.headers.get('x-custom-hop-header')).toBeNull(); - expect(proxied.headers.get('x-quoted-hop-header')).toBeNull(); - expect(proxied.headers.get('x-spaced-hop-header')).toBeNull(); - expect(proxied.headers.get('proxy-connection')).toBeNull(); - expect(proxied.headers.get('proxy-authenticate')).toBeNull(); - expect(proxied.headers.get('proxy-authorization')).toBeNull(); - expect(proxied.headers.get('te')).toBeNull(); - expect(proxied.headers.get('trailer')).toBeNull(); - expect(proxied.headers.get('upgrade')).toBeNull(); - expect(proxied.headers.get('transfer-encoding')).toBeNull(); - await expect(proxied.text()).resolves.toBe('proxied-body'); - }); - - it('keeps unrelated response headers intact', () => { - const upstream = new Response('ok', { - status: 200, - headers: { - 'cache-control': 'public,max-age=31536000', - etag: '"abc123"', - }, - }); - - const proxied = createSafeProxyResponse(upstream); - - expect(proxied.headers.get('cache-control')).toBe( - 'public,max-age=31536000', - ); - expect(proxied.headers.get('etag')).toBe('"abc123"'); - }); - - it('normalizes mixed-case quoted connection tokens', () => { - const upstream = new Response('ok', { - status: 200, - headers: { - connection: '"X-Mixed-Hop-Header", Keep-Alive', - 'x-mixed-hop-header': 'remove-me', - 'Keep-Alive': 'timeout=5', - 'x-safe-header': 'preserve-me', - }, - }); - - const proxied = createSafeProxyResponse(upstream); - - expect(proxied.headers.get('x-mixed-hop-header')).toBeNull(); - expect(proxied.headers.get('keep-alive')).toBeNull(); - expect(proxied.headers.get('x-safe-header')).toBe('preserve-me'); - }); - - it('ignores empty connection tokens after quote normalization', async () => { - const upstream = new Response('ok', { - status: 200, - headers: { - connection: ' , "", " ", "x-trimmed-hop-header" ', - 'x-trimmed-hop-header': 'remove-me', - 'x-safe-header': 'preserve-me', - }, - }); - - const proxied = createSafeProxyResponse(upstream); - - expect(proxied.headers.get('x-trimmed-hop-header')).toBeNull(); - expect(proxied.headers.get('x-safe-header')).toBe('preserve-me'); - await expect(proxied.text()).resolves.toBe('ok'); - }); - - it('forces empty body for no-content status responses', async () => { - const upstream = { - status: 204, - statusText: 'No Content', - headers: new Headers({ - 'content-type': 'application/javascript', - }), - body: new ReadableStream({ - start(controller) { - controller.enqueue( - new TextEncoder().encode('should-not-be-forwarded'), - ); - controller.close(); - }, - }), - } as unknown as Response; - - const proxied = createSafeProxyResponse(upstream); - - expect(proxied.status).toBe(204); - expect(proxied.statusText).toBe('No Content'); - await expect(proxied.text()).resolves.toBe(''); - }); - - it.each([ - { status: 205, statusText: 'Reset Content' }, - { status: 304, statusText: 'Not Modified' }, - ])( - 'forces empty body for status $status ($statusText)', - async ({ status, statusText }) => { - const upstream = { - status, - statusText, - headers: new Headers({ - 'content-type': 'application/json', - }), - body: new ReadableStream({ - start(controller) { - controller.enqueue(new TextEncoder().encode('discard-body')); - controller.close(); - }, - }), - } as unknown as Response; - - const proxied = createSafeProxyResponse(upstream); - - expect(proxied.status).toBe(status); - expect(proxied.statusText).toBe(statusText); - await expect(proxied.text()).resolves.toBe(''); - }, - ); -}); diff --git a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts index 542454f8564b..e0bb11419cc2 100644 --- a/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts +++ b/tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts @@ -1,1309 +1,30 @@ -import { INTERNAL_FALLBACK_HEADER } from '../remote/server/manifestFallback'; +import fs from 'node:fs'; +import path from 'node:path'; -const REMOTE_SERVER_CONFIG_MODULE = '../remote/server/modern.server'; +describe('rsc-mf remote modern.server contracts', () => { + const remoteServerDir = path.resolve(__dirname, '../remote/server'); -const loadRemoteServerConfig = () => { - jest.resetModules(); - jest.doMock('@modern-js/server-runtime', () => ({ - defineServerConfig: (config: unknown) => config, - })); - - let config: any; - jest.isolateModules(() => { - config = require(REMOTE_SERVER_CONFIG_MODULE).default; - }); - - return config; -}; - -const getRecoverMiddlewareHandler = () => { - const config = loadRemoteServerConfig(); - if (!Array.isArray(config.middlewares)) { - throw new Error('Remote server config did not provide a middlewares array'); - } - - const middleware = config.middlewares.find( - (entry: { name?: string }) => - entry.name === 'recover-remote-federation-expose-asset', - ); - if (!middleware) { - throw new Error( - 'recover-remote-federation-expose-asset middleware missing', - ); - } - - expect(middleware.order).toBe('pre'); - expect(middleware.before).toEqual(['server-static']); - expect(typeof middleware.handler).toBe('function'); - - return middleware.handler as ( - c: { - req: { url: string; headers?: { get?: (name: string) => string | null } }; - res?: Response; - }, - next: () => Promise, - ) => Promise; -}; - -describe('rsc-mf remote modern.server middleware contracts', () => { - const originalFetch = global.fetch; - const originalFetchDescriptor = Object.getOwnPropertyDescriptor( - global, - 'fetch', - ); - - const installFetchMock = (implementation: typeof fetch) => { - const fetchMock = jest.fn(implementation); - Object.defineProperty(global, 'fetch', { - value: fetchMock, - configurable: true, - writable: true, - }); - return fetchMock; - }; - - afterAll(() => { - if (originalFetchDescriptor) { - Object.defineProperty(global, 'fetch', originalFetchDescriptor); - return; - } - global.fetch = originalFetch; - }); - - it('recovers stale expose asset path via remote manifest fallback', async () => { - const handler = getRecoverMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - exposes: [ - { - assets: { - js: { - sync: [ - 'static/js/async/__federation_expose_RemoteServerCard.a1b2c3d4.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ) - .mockResolvedValueOnce( - new Response('fallback-asset', { - status: 200, - headers: { - 'content-type': 'application/javascript', - }, - }), - ), - ); - const context: { - req: { url: string; headers?: { get?: (name: string) => string | null } }; - res?: Response; - } = { - req: { - url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteServerCard.js?cache=1', - }, - }; - - await handler(context, next); - - expect(fetchMock).toHaveBeenNthCalledWith( - 1, - 'http://127.0.0.1:3008/static/mf-manifest.json', - { - headers: { - [INTERNAL_FALLBACK_HEADER]: '1', - }, - }, - ); - expect(fetchMock).toHaveBeenNthCalledWith( - 2, - 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteServerCard.a1b2c3d4.js?cache=1', - { - headers: { - [INTERNAL_FALLBACK_HEADER]: '1', - }, - }, - ); - expect(next).not.toHaveBeenCalled(); - await expect(context.res?.text()).resolves.toBe('fallback-asset'); - }); - - it('prefers hashed manifest fallback when stale alias is also listed', async () => { - const handler = getRecoverMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - exposes: [ - { - assets: { - js: { - sync: [ - 'static/js/async/__federation_expose_RemoteServerCard.js', - 'static/js/async/__federation_expose_RemoteServerCard.a1b2c3d4.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ) - .mockResolvedValueOnce( - new Response('hashed-preferred-fallback-asset', { - status: 200, - headers: { - 'content-type': 'application/javascript', - }, - }), - ), - ); - const context: { - req: { url: string; headers?: { get?: (name: string) => string | null } }; - res?: Response; - } = { - req: { - url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteServerCard.js', - }, - }; - - await handler(context, next); - - expect(fetchMock).toHaveBeenNthCalledWith( - 2, - 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteServerCard.a1b2c3d4.js', - { - headers: { - [INTERNAL_FALLBACK_HEADER]: '1', - }, - }, - ); - expect(next).not.toHaveBeenCalled(); - await expect(context.res?.text()).resolves.toBe( - 'hashed-preferred-fallback-asset', - ); - }); - - it('finalizes recovered responses through context body API when available', async () => { - const handler = getRecoverMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - exposes: [ - { - assets: { - js: { - sync: [ - 'static/js/async/__federation_expose_RemoteServerCard.a1b2c3d4.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ) - .mockResolvedValueOnce( - new Response('fallback-via-body', { - status: 203, - headers: { - 'content-type': 'application/javascript', - 'content-length': '222', - }, - }), - ), - ); - const context: { - req: { url: string }; - res?: Response; - body: ( - body: BodyInit | null, - status?: number, - headers?: HeadersInit, - ) => Response; - } = { - req: { - url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteServerCard.js', - }, - body: jest.fn((body, status, headers) => { - return new Response(body, { - status, - headers, - }); - }), - }; - - await handler(context as any, next); - - expect(fetchMock).toHaveBeenCalledTimes(2); - expect(context.body).toHaveBeenCalledTimes(1); - expect(context.res).toBeInstanceOf(Response); - expect(context.res?.status).toBe(203); - expect(context.res?.headers.get('content-length')).toBeNull(); - await expect(context.res?.text()).resolves.toBe('fallback-via-body'); - expect(next).not.toHaveBeenCalled(); - }); - - it('strips transfer headers from recovered fallback responses', async () => { - const handler = getRecoverMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - installFetchMock( - jest - .fn() - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - exposes: [ - { - assets: { - js: { - sync: [ - 'static/js/async/__federation_expose_RemoteServerCard.a1b2c3d4.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ) - .mockResolvedValueOnce( - new Response('fallback-with-transport-headers', { - status: 201, - statusText: 'Created', - headers: { - 'content-type': 'application/javascript', - 'content-length': '999', - 'content-encoding': 'gzip', - connection: - 'keep-alive, x-proxy-hop-header, "x-proxy-quoted-hop-header"', - 'keep-alive': 'timeout=5', - 'proxy-connection': 'keep-alive', - 'proxy-authenticate': 'Basic realm=test', - 'proxy-authorization': 'Basic dGVzdA==', - 'x-proxy-hop-header': 'remove-me', - 'x-proxy-quoted-hop-header': 'remove-me', - te: 'trailers', - trailer: 'x-trailer-a', - upgrade: 'websocket', - 'transfer-encoding': 'chunked', - }, - }), - ), + it('does not define a remote modern.server middleware override', () => { + const remoteModernServerPath = path.join( + remoteServerDir, + 'modern.server.ts', ); - const context: { - req: { url: string; headers?: { get?: (name: string) => string | null } }; - res?: Response; - } = { - req: { - url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteServerCard.js', - }, - }; - - await handler(context, next); - - expect(next).not.toHaveBeenCalled(); - expect(context.res?.status).toBe(201); - expect(context.res?.statusText).toBe('Created'); - expect(context.res?.headers.get('content-type')).toBe( - 'application/javascript', - ); - expect(context.res?.headers.get('content-length')).toBeNull(); - expect(context.res?.headers.get('content-encoding')).toBeNull(); - expect(context.res?.headers.get('connection')).toBeNull(); - expect(context.res?.headers.get('keep-alive')).toBeNull(); - expect(context.res?.headers.get('proxy-connection')).toBeNull(); - expect(context.res?.headers.get('proxy-authenticate')).toBeNull(); - expect(context.res?.headers.get('proxy-authorization')).toBeNull(); - expect(context.res?.headers.get('x-proxy-hop-header')).toBeNull(); - expect(context.res?.headers.get('x-proxy-quoted-hop-header')).toBeNull(); - expect(context.res?.headers.get('te')).toBeNull(); - expect(context.res?.headers.get('trailer')).toBeNull(); - expect(context.res?.headers.get('upgrade')).toBeNull(); - expect(context.res?.headers.get('transfer-encoding')).toBeNull(); - await expect(context.res?.text()).resolves.toBe( - 'fallback-with-transport-headers', - ); - }); - - it('recovers stale hashed expose asset path via remote manifest fallback', async () => { - const handler = getRecoverMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - exposes: [ - { - assets: { - js: { - sync: [ - 'static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ) - .mockResolvedValueOnce( - new Response('hashed-fallback-asset', { - status: 200, - headers: { - 'content-type': 'application/javascript', - }, - }), - ), - ); - const context: { - req: { url: string; headers?: { get?: (name: string) => string | null } }; - res?: Response; - } = { - req: { - url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.deadbeef12.js', - }, - }; - - await handler(context, next); - - expect(fetchMock).toHaveBeenNthCalledWith( - 2, - 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', - { - headers: { - [INTERNAL_FALLBACK_HEADER]: '1', - }, - }, - ); - expect(next).not.toHaveBeenCalled(); - await expect(context.res?.text()).resolves.toBe('hashed-fallback-asset'); - }); - - it('falls through when request path is not a federated expose asset', async () => { - const handler = getRecoverMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - async () => new Response('ignored', { status: 200 }), - ); - const context: { - req: { url: string; headers?: { get?: (name: string) => string | null } }; - res?: Response; - } = { - req: { - url: 'http://127.0.0.1:3008/static/js/async/743.32436c1247.js', - }, - }; - - await handler(context, next); - - expect(fetchMock).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(context.res).toBeUndefined(); - }); - - it('skips fallback when request is marked as internal middleware fetch', async () => { - const handler = getRecoverMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - async () => new Response('ignored', { status: 200 }), - ); - const context: { - req: { url: string; headers?: { get?: (name: string) => string | null } }; - res?: Response; - } = { - req: { - url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteServerCard.js', - headers: { - get: (name: string) => - name === INTERNAL_FALLBACK_HEADER ? '1' : undefined, - }, - }, - }; - - await handler(context, next); - - expect(fetchMock).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(context.res).toBeUndefined(); - }); - - it('falls through when manifest fallback asset resolves to another origin', async () => { - const handler = getRecoverMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest.fn().mockResolvedValueOnce( - new Response( - JSON.stringify({ - exposes: [ - { - assets: { - js: { - sync: [ - 'https://cdn.example.com/static/js/async/__federation_expose_RemoteServerCard.a1b2c3d4.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ), - ); - const context: { - req: { url: string; headers?: { get?: (name: string) => string | null } }; - res?: Response; - } = { - req: { - url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteServerCard.js', - }, - }; - - await handler(context, next); - - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(context.res).toBeUndefined(); + expect(fs.existsSync(remoteModernServerPath)).toBe(false); }); - it('recovers stale css expose assets via manifest fallback', async () => { - const handler = getRecoverMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - exposes: [ - { - assets: { - js: { - sync: [], - async: [], - }, - css: { - sync: [ - 'static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css', - ], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ) - .mockResolvedValueOnce( - new Response('.fallback-style{}', { - status: 200, - headers: { - 'content-type': 'text/css', - }, - }), - ), - ); - const context: { - req: { url: string; headers?: { get?: (name: string) => string | null } }; - res?: Response; - } = { - req: { - url: 'http://127.0.0.1:3008/static/css/async/__federation_expose_RemoteClientCounter.css?cache=1', - }, - }; - - await handler(context, next); - - expect(fetchMock).toHaveBeenNthCalledWith( - 2, - 'http://127.0.0.1:3008/static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css?cache=1', - { - headers: { - [INTERNAL_FALLBACK_HEADER]: '1', - }, - }, + it('does not define manifest fallback helpers in remote server layer', () => { + const remoteManifestFallbackPath = path.join( + remoteServerDir, + 'manifestFallback.ts', ); - expect(next).not.toHaveBeenCalled(); - await expect(context.res?.text()).resolves.toBe('.fallback-style{}'); + expect(fs.existsSync(remoteManifestFallbackPath)).toBe(false); }); - it('recovers stale hashed css expose assets via manifest fallback', async () => { - const handler = getRecoverMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - exposes: [ - { - assets: { - js: { - sync: [], - async: [], - }, - css: { - sync: [ - 'static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css', - ], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ) - .mockResolvedValueOnce( - new Response('.hashed-fallback-style{}', { - status: 200, - headers: { - 'content-type': 'text/css', - }, - }), - ), - ); - const context: { - req: { url: string; headers?: { get?: (name: string) => string | null } }; - res?: Response; - } = { - req: { - url: 'http://127.0.0.1:3008/static/css/async/__federation_expose_RemoteClientCounter.deadbeef12.css?cache=1', - }, - }; - - await handler(context, next); - - expect(fetchMock).toHaveBeenNthCalledWith( - 2, - 'http://127.0.0.1:3008/static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css?cache=1', - { - headers: { - [INTERNAL_FALLBACK_HEADER]: '1', - }, - }, - ); - expect(next).not.toHaveBeenCalled(); - await expect(context.res?.text()).resolves.toBe('.hashed-fallback-style{}'); - }); - - it('falls through when manifest response body is invalid json', async () => { - const handler = getRecoverMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest.fn().mockResolvedValueOnce( - new Response('not-json-manifest', { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }), - ), - ); - const context: { - req: { url: string; headers?: { get?: (name: string) => string | null } }; - res?: Response; - } = { - req: { - url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.js', - }, - }; - - await handler(context, next); - - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(context.res).toBeUndefined(); - }); - - it('falls through when manifest request throws', async () => { - const handler = getRecoverMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock(async () => { - throw new Error('manifest-fetch-failed'); - }); - const context: { - req: { url: string; headers?: { get?: (name: string) => string | null } }; - res?: Response; - } = { - req: { - url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.js', - }, - }; - - await handler(context, next); - - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(context.res).toBeUndefined(); - }); - - it('falls through when manifest fallback lookup has no canonical asset match', async () => { - const handler = getRecoverMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest.fn().mockResolvedValueOnce( - new Response( - JSON.stringify({ - exposes: [ - { - assets: { - js: { - sync: [ - 'static/js/async/__federation_expose_other.abc123.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ), - ); - const context: { - req: { url: string; headers?: { get?: (name: string) => string | null } }; - res?: Response; - } = { - req: { - url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.js', - }, - }; - - await handler(context, next); - - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(context.res).toBeUndefined(); - }); - - it('falls through when fallback asset fetch returns non-ok response', async () => { - const handler = getRecoverMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - exposes: [ - { - assets: { - js: { - sync: [ - 'static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ) - .mockResolvedValueOnce( - new Response('missing-fallback-asset', { - status: 404, - headers: { - 'content-type': 'text/plain', - }, - }), - ), - ); - const context: { - req: { url: string; headers?: { get?: (name: string) => string | null } }; - res?: Response; - } = { - req: { - url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.js', - }, - }; - - await handler(context, next); - - expect(fetchMock).toHaveBeenCalledTimes(2); - expect(next).toHaveBeenCalledTimes(1); - expect(context.res).toBeUndefined(); - }); - - it('merges request query params into absolute same-origin manifest fallback assets', async () => { - const handler = getRecoverMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - shared: [ - { - assets: { - js: { - sync: [ - 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js?manifest=1', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ) - .mockResolvedValueOnce( - new Response('absolute-fallback-asset', { - status: 200, - headers: { - 'content-type': 'application/javascript', - }, - }), - ), - ); - const context: { - req: { url: string; headers?: { get?: (name: string) => string | null } }; - res?: Response; - } = { - req: { - url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.js?cache=1', - }, - }; - - await handler(context, next); - - expect(fetchMock).toHaveBeenNthCalledWith( - 2, - 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js?manifest=1&cache=1', - { - headers: { - [INTERNAL_FALLBACK_HEADER]: '1', - }, - }, - ); - expect(next).not.toHaveBeenCalled(); - await expect(context.res?.text()).resolves.toBe('absolute-fallback-asset'); - }); - - it('lets request query params override manifest fallback query params', async () => { - const handler = getRecoverMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - shared: [ - { - assets: { - js: { - sync: [ - 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js?cache=manifest&v=1', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ) - .mockResolvedValueOnce( - new Response('query-override-fallback-asset', { - status: 200, - headers: { - 'content-type': 'application/javascript', - }, - }), - ), - ); - const context: { - req: { url: string; headers?: { get?: (name: string) => string | null } }; - res?: Response; - } = { - req: { - url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.js?cache=request&x=2', - }, - }; - - await handler(context, next); - - const fallbackCallUrl = fetchMock.mock.calls[1]?.[0] as string; - const parsedFallbackCallUrl = new URL(fallbackCallUrl); - expect(parsedFallbackCallUrl.origin).toBe('http://127.0.0.1:3008'); - expect(parsedFallbackCallUrl.pathname).toBe( - '/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js', - ); - expect(parsedFallbackCallUrl.searchParams.get('cache')).toBe('request'); - expect(parsedFallbackCallUrl.searchParams.get('v')).toBe('1'); - expect(parsedFallbackCallUrl.searchParams.get('x')).toBe('2'); - expect(next).not.toHaveBeenCalled(); - await expect(context.res?.text()).resolves.toBe( - 'query-override-fallback-asset', - ); - }); - - it('falls through when fallback asset resolves to the same request url', async () => { - const handler = getRecoverMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest.fn().mockResolvedValueOnce( - new Response( - JSON.stringify({ - exposes: [ - { - assets: { - js: { - sync: [ - 'static/js/async/__federation_expose_RemoteClientCounter.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ), - ); - const context: { - req: { url: string; headers?: { get?: (name: string) => string | null } }; - res?: Response; - } = { - req: { - url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.js', - }, - }; - - await handler(context, next); - - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(context.res).toBeUndefined(); - }); - - it('falls through when manifest fallback path escapes async asset directory', async () => { - const handler = getRecoverMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest.fn().mockResolvedValueOnce( - new Response( - JSON.stringify({ - shared: [ - { - assets: { - js: { - sync: [ - 'static/js/async/../__federation_expose_RemoteClientCounter.7745fe5f0a.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ), - ); - const context: { - req: { url: string; headers?: { get?: (name: string) => string | null } }; - res?: Response; - } = { - req: { - url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.js', - }, - }; - - await handler(context, next); - - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(context.res).toBeUndefined(); - }); - - it('falls through when manifest fallback path uses encoded async-directory escape', async () => { - const handler = getRecoverMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest.fn().mockResolvedValueOnce( - new Response( - JSON.stringify({ - shared: [ - { - assets: { - js: { - sync: [ - 'static/js/async/%2e%2e/__federation_expose_RemoteClientCounter.7745fe5f0a.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ), - ); - const context: { - req: { url: string; headers?: { get?: (name: string) => string | null } }; - res?: Response; - } = { - req: { - url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.js', - }, - }; - - await handler(context, next); - - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(context.res).toBeUndefined(); - }); - - it('falls through when manifest fallback path has invalid percent encoding', async () => { - const handler = getRecoverMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest.fn().mockResolvedValueOnce( - new Response( - JSON.stringify({ - shared: [ - { - assets: { - js: { - sync: [ - 'static/js/async/%E0%A4%A/__federation_expose_RemoteClientCounter.7745fe5f0a.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ), - ); - const context: { - req: { url: string; headers?: { get?: (name: string) => string | null } }; - res?: Response; - } = { - req: { - url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.js', - }, - }; - - await handler(context, next); - - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(context.res).toBeUndefined(); - }); - - it('resolves fallback asset paths from manifest async asset arrays', async () => { - const handler = getRecoverMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - shared: [ - { - assets: { - js: { - sync: [], - async: [ - 'static/js/async/__federation_expose_nestedActions.a8ce95b11a.js', - ], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ) - .mockResolvedValueOnce( - new Response('async-array-fallback-hit', { - status: 200, - headers: { - 'content-type': 'application/javascript', - }, - }), - ), - ); - const context: { - req: { url: string; headers?: { get?: (name: string) => string | null } }; - res?: Response; - } = { - req: { - url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_nestedActions.js', - }, - }; - - await handler(context, next); - - expect(fetchMock).toHaveBeenNthCalledWith( - 2, - 'http://127.0.0.1:3008/static/js/async/__federation_expose_nestedActions.a8ce95b11a.js', - { - headers: { - [INTERNAL_FALLBACK_HEADER]: '1', - }, - }, - ); - expect(next).not.toHaveBeenCalled(); - await expect(context.res?.text()).resolves.toBe('async-array-fallback-hit'); - }); - - it('matches fallback chunks with non-hex hash suffixes', async () => { - const handler = getRecoverMiddlewareHandler(); - const next = jest.fn(async (): Promise => undefined); - const fetchMock = installFetchMock( - jest - .fn() - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - exposes: [ - { - assets: { - js: { - sync: [ - 'static/js/async/__federation_expose_RemoteServerCard.a1b2c3x9.js', - ], - async: [], - }, - css: { - sync: [], - async: [], - }, - }, - }, - ], - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ), - ) - .mockResolvedValueOnce( - new Response('non-hex-fallback-hit', { - status: 200, - headers: { - 'content-type': 'application/javascript', - }, - }), - ), - ); - const context: { - req: { url: string; headers?: { get?: (name: string) => string | null } }; - res?: Response; - } = { - req: { - url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteServerCard.js', - }, - }; - - await handler(context, next); - - expect(fetchMock).toHaveBeenNthCalledWith( - 2, - 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteServerCard.a1b2c3x9.js', - { - headers: { - [INTERNAL_FALLBACK_HEADER]: '1', - }, - }, + it('does not define proxy response helpers in remote server layer', () => { + const remoteProxyResponsePath = path.join( + remoteServerDir, + 'proxyResponse.ts', ); - expect(next).not.toHaveBeenCalled(); - await expect(context.res?.text()).resolves.toBe('non-hex-fallback-hit'); + expect(fs.existsSync(remoteProxyResponsePath)).toBe(false); }); }); From 112bbec365b78913819b2df2895dd045c37eeb95 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 22:44:16 +0000 Subject: [PATCH 295/324] chore(mf): vendor modern-js-v3 plugin into workspace --- packages/modernjs-v3/.eslintrc.json | 41 + packages/modernjs-v3/CHANGELOG.md | 1081 ++++++++++++ packages/modernjs-v3/LICENSE | 21 + packages/modernjs-v3/README.md | 5 + packages/modernjs-v3/bin/mf.js | 4 + packages/modernjs-v3/package.json | 188 +++ packages/modernjs-v3/project.json | 57 + packages/modernjs-v3/rslib.config.ts | 58 + .../modernjs-v3/src/cli/configPlugin.spec.ts | 85 + packages/modernjs-v3/src/cli/configPlugin.ts | 529 ++++++ packages/modernjs-v3/src/cli/index.ts | 78 + .../cli/mfRuntimePlugins/inject-node-fetch.ts | 14 + .../mfRuntimePlugins/resolve-entry-ipv4.ts | 70 + .../cli/mfRuntimePlugins/shared-strategy.ts | 22 + .../cli/server/data-fetch-server-plugin.ts | 19 + packages/modernjs-v3/src/cli/ssrPlugin.ts | 377 +++++ packages/modernjs-v3/src/cli/utils.ts | 57 + packages/modernjs-v3/src/constant.ts | 2 + .../modernjs-v3/src/interfaces/bundler.ts | 29 + packages/modernjs-v3/src/logger.ts | 6 + packages/modernjs-v3/src/react/index.ts | 1 + packages/modernjs-v3/src/react/plugin.ts | 2 + packages/modernjs-v3/src/react/v18.ts | 1 + packages/modernjs-v3/src/react/v19.ts | 1 + packages/modernjs-v3/src/runtime/index.ts | 1 + .../modernjs-v3/src/server/fileCache.spec.ts | 29 + packages/modernjs-v3/src/server/fileCache.ts | 60 + packages/modernjs-v3/src/server/index.ts | 49 + .../src/server/staticMiddleware.spec.ts | 240 +++ .../src/server/staticMiddleware.ts | 78 + .../src/ssr-runtime/SSRLiveReload.tsx | 19 + .../modernjs-v3/src/ssr-runtime/devPlugin.tsx | 34 + .../injectDataFetchFunctionPlugin.tsx | 20 + packages/modernjs-v3/src/types/index.ts | 42 + packages/modernjs-v3/tsconfig.json | 11 + packages/modernjs-v3/tsup.config.ts | 12 + packages/modernjs-v3/typedoc.json | 5 + packages/modernjs-v3/types.d.ts | 6 + packages/modernjs-v3/vite.config.mts | 24 + pnpm-lock.yaml | 1481 ++++++++++++++--- .../i18n/mf/mf-app-provider/package.json | 2 +- .../mf/mf-component-provider/package.json | 2 +- .../i18n/mf/mf-consumer/package.json | 2 +- tests/integration/rsc-mf/host/package.json | 2 +- tests/integration/rsc-mf/remote/package.json | 2 +- 45 files changed, 4600 insertions(+), 269 deletions(-) create mode 100644 packages/modernjs-v3/.eslintrc.json create mode 100644 packages/modernjs-v3/CHANGELOG.md create mode 100644 packages/modernjs-v3/LICENSE create mode 100644 packages/modernjs-v3/README.md create mode 100755 packages/modernjs-v3/bin/mf.js create mode 100644 packages/modernjs-v3/package.json create mode 100644 packages/modernjs-v3/project.json create mode 100644 packages/modernjs-v3/rslib.config.ts create mode 100644 packages/modernjs-v3/src/cli/configPlugin.spec.ts create mode 100644 packages/modernjs-v3/src/cli/configPlugin.ts create mode 100644 packages/modernjs-v3/src/cli/index.ts create mode 100644 packages/modernjs-v3/src/cli/mfRuntimePlugins/inject-node-fetch.ts create mode 100644 packages/modernjs-v3/src/cli/mfRuntimePlugins/resolve-entry-ipv4.ts create mode 100644 packages/modernjs-v3/src/cli/mfRuntimePlugins/shared-strategy.ts create mode 100644 packages/modernjs-v3/src/cli/server/data-fetch-server-plugin.ts create mode 100644 packages/modernjs-v3/src/cli/ssrPlugin.ts create mode 100644 packages/modernjs-v3/src/cli/utils.ts create mode 100644 packages/modernjs-v3/src/constant.ts create mode 100644 packages/modernjs-v3/src/interfaces/bundler.ts create mode 100644 packages/modernjs-v3/src/logger.ts create mode 100644 packages/modernjs-v3/src/react/index.ts create mode 100644 packages/modernjs-v3/src/react/plugin.ts create mode 100644 packages/modernjs-v3/src/react/v18.ts create mode 100644 packages/modernjs-v3/src/react/v19.ts create mode 100644 packages/modernjs-v3/src/runtime/index.ts create mode 100644 packages/modernjs-v3/src/server/fileCache.spec.ts create mode 100644 packages/modernjs-v3/src/server/fileCache.ts create mode 100644 packages/modernjs-v3/src/server/index.ts create mode 100644 packages/modernjs-v3/src/server/staticMiddleware.spec.ts create mode 100644 packages/modernjs-v3/src/server/staticMiddleware.ts create mode 100644 packages/modernjs-v3/src/ssr-runtime/SSRLiveReload.tsx create mode 100644 packages/modernjs-v3/src/ssr-runtime/devPlugin.tsx create mode 100644 packages/modernjs-v3/src/ssr-runtime/injectDataFetchFunctionPlugin.tsx create mode 100644 packages/modernjs-v3/src/types/index.ts create mode 100644 packages/modernjs-v3/tsconfig.json create mode 100644 packages/modernjs-v3/tsup.config.ts create mode 100644 packages/modernjs-v3/typedoc.json create mode 100644 packages/modernjs-v3/types.d.ts create mode 100644 packages/modernjs-v3/vite.config.mts diff --git a/packages/modernjs-v3/.eslintrc.json b/packages/modernjs-v3/.eslintrc.json new file mode 100644 index 000000000000..5c3013868ca3 --- /dev/null +++ b/packages/modernjs-v3/.eslintrc.json @@ -0,0 +1,41 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": [ + "!**/*", + "**/*.d.ts", + "**/vite.config.*.timestamp*", + "**/vitest.config.*.timestamp*" + ], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/ban-ts-comment": "warn", + "@typescript-eslint/no-var-requires": 0, + "@typescript-eslint/no-restricted-imports": [ + "error", + { + "paths": [ + { + "name": "webpack", + "message": "Please use require(normalizeWebpackPath('webpack')) instead.", + "allowTypeImports": true + } + ], + "patterns": [ + { + "group": ["webpack/lib/*"], + "message": "Please use require(normalizeWebpackPath('webpack')) instead.", + "allowTypeImports": true + } + ] + } + ] + } + }, + { + "files": ["*.js", "*.jsx"] + } + ] +} diff --git a/packages/modernjs-v3/CHANGELOG.md b/packages/modernjs-v3/CHANGELOG.md new file mode 100644 index 000000000000..b86780048843 --- /dev/null +++ b/packages/modernjs-v3/CHANGELOG.md @@ -0,0 +1,1081 @@ +# @module-federation/modern-js-v3 + +## 2.0.1 + +### Patch Changes + +- @module-federation/cli@2.0.1 +- @module-federation/enhanced@2.0.1 +- @module-federation/bridge-react@2.0.1 +- @module-federation/runtime@2.0.1 +- @module-federation/node@2.7.32 +- @module-federation/rsbuild-plugin@2.0.1 +- @module-federation/sdk@2.0.1 + +## 2.0.0 + +### Major Changes + +- 2bd445d: feat: upgrade to modern js 3.0 + +### Patch Changes + +- Updated dependencies [2bd445d] + - @module-federation/rsbuild-plugin@2.0.0 + - @module-federation/enhanced@2.0.0 + - @module-federation/node@2.7.31 + - @module-federation/runtime@2.0.0 + - @module-federation/sdk@2.0.0 + - @module-federation/bridge-react@2.0.0 + - @module-federation/cli@2.0.0 + +## 0.24.1 + +### Patch Changes + +- @module-federation/bridge-react@0.24.1 +- @module-federation/runtime@0.24.1 +- @module-federation/node@2.7.30 +- @module-federation/enhanced@0.24.1 +- @module-federation/rsbuild-plugin@0.24.1 +- @module-federation/sdk@0.24.1 +- @module-federation/cli@0.24.1 + +## 0.24.0 + +### Minor Changes + +- f46cdd6: fix(modernjs)!: ship .mjs files for correct ESM output + +### Patch Changes + +- Updated dependencies [c3c068e] + - @module-federation/enhanced@0.24.0 + - @module-federation/node@2.7.29 + - @module-federation/rsbuild-plugin@0.24.0 + - @module-federation/runtime@0.24.0 + - @module-federation/sdk@0.24.0 + - @module-federation/bridge-react@0.24.0 + - @module-federation/cli@0.24.0 + +## 0.23.0 + +### Patch Changes + +- fdce013: fix(modern-js-plugin): use jiti to load config +- Updated dependencies [3aa3cf9] + - @module-federation/rsbuild-plugin@0.23.0 + - @module-federation/cli@0.23.0 + - @module-federation/enhanced@0.23.0 + - @module-federation/bridge-react@0.23.0 + - @module-federation/runtime@0.23.0 + - @module-federation/node@2.7.28 + - @module-federation/sdk@0.23.0 + +## 0.22.1 + +### Patch Changes + +- @module-federation/runtime@0.22.1 +- @module-federation/enhanced@0.22.1 +- @module-federation/sdk@0.22.1 +- @module-federation/bridge-react@0.22.1 +- @module-federation/rsbuild-plugin@0.22.1 +- @module-federation/cli@0.22.1 +- @module-federation/node@2.7.27 + +## 0.22.0 + +### Minor Changes + +- 90c6a40: fix(modernjs)!: ship .mjs files for correct ESM output + +### Patch Changes + +- 6e27880: fix(modern-js-plugin): handle distOutputDir for SSR and web targets differently + - @module-federation/runtime@0.22.0 + - @module-federation/enhanced@0.22.0 + - @module-federation/sdk@0.22.0 + - @module-federation/bridge-react@0.22.0 + - @module-federation/rsbuild-plugin@0.22.0 + - @module-federation/cli@0.22.0 + - @module-federation/node@2.7.26 + +## 0.21.6 + +### Patch Changes + +- Updated dependencies [b307ae0] + - @module-federation/rsbuild-plugin@0.21.6 + - @module-federation/runtime@0.21.6 + - @module-federation/enhanced@0.21.6 + - @module-federation/sdk@0.21.6 + - @module-federation/bridge-react@0.21.6 + - @module-federation/cli@0.21.6 + - @module-federation/node@2.7.25 + +## 0.21.5 + +### Patch Changes + +- Updated dependencies [35c3695] +- Updated dependencies [94d8868] + - @module-federation/cli@0.21.5 + - @module-federation/sdk@0.21.5 + - @module-federation/enhanced@0.21.5 + - @module-federation/bridge-react@0.21.5 + - @module-federation/node@2.7.24 + - @module-federation/rsbuild-plugin@0.21.5 + - @module-federation/runtime@0.21.5 + +## 0.21.4 + +### Patch Changes + +- a50e068: refactor(manifest): collect assets from build hook +- Updated dependencies [444db72] +- Updated dependencies [a50e068] + - @module-federation/bridge-react@0.21.4 + - @module-federation/rsbuild-plugin@0.21.4 + - @module-federation/enhanced@0.21.4 + - @module-federation/sdk@0.21.4 + - @module-federation/cli@0.21.4 + - @module-federation/node@2.7.23 + - @module-federation/runtime@0.21.4 + +## 0.21.3 + +### Patch Changes + +- @module-federation/bridge-react@0.21.3 +- @module-federation/runtime@0.21.3 +- @module-federation/node@2.7.22 +- @module-federation/enhanced@0.21.3 +- @module-federation/rsbuild-plugin@0.21.3 +- @module-federation/sdk@0.21.3 +- @module-federation/cli@0.21.3 + +## 0.21.2 + +### Patch Changes + +- e98133e: feat: Re-export the exports of the v18/v19/plugin from @module-federation/bridge-react in modernjs +- Updated dependencies [e98133e] +- Updated dependencies [dc103ee] + - @module-federation/bridge-react@0.21.2 + - @module-federation/cli@0.21.2 + - @module-federation/enhanced@0.21.2 + - @module-federation/node@2.7.21 + - @module-federation/rsbuild-plugin@0.21.2 + - @module-federation/runtime@0.21.2 + - @module-federation/sdk@0.21.2 + +## 0.21.1 + +### Patch Changes + +- 32600de: fix(modern-js-plugin): add bridge peer deps + - @module-federation/runtime@0.21.1 + - @module-federation/enhanced@0.21.1 + - @module-federation/sdk@0.21.1 + - @module-federation/bridge-react@0.21.1 + - @module-federation/rsbuild-plugin@0.21.1 + - @module-federation/cli@0.21.1 + - @module-federation/node@2.7.20 + +## 0.21.0 + +### Patch Changes + +- aa7daae: feat: delete set disableAlias true in @module-federation/modern-js-v3 +- Updated dependencies [d1e90a4] +- Updated dependencies [d225658] + - @module-federation/sdk@0.21.0 + - @module-federation/bridge-react@0.21.0 + - @module-federation/cli@0.21.0 + - @module-federation/enhanced@0.21.0 + - @module-federation/node@2.7.19 + - @module-federation/rsbuild-plugin@0.21.0 + - @module-federation/runtime@0.21.0 + +## 0.20.0 + +### Patch Changes + +- Updated dependencies [dcc290e] +- Updated dependencies [0008621] +- Updated dependencies [2eea0d0] +- Updated dependencies [b7872a1] +- Updated dependencies [25df940] +- Updated dependencies [22b9ff9] +- Updated dependencies [8a80605] +- Updated dependencies [e89e972] +- Updated dependencies [c66c21e] +- Updated dependencies [37346d4] +- Updated dependencies [8038f61] +- Updated dependencies [639a83b] + - @module-federation/enhanced@0.20.0 + - @module-federation/bridge-react@0.20.0 + - @module-federation/rsbuild-plugin@0.20.0 + - @module-federation/node@2.7.18 + - @module-federation/runtime@0.20.0 + - @module-federation/sdk@0.20.0 + - @module-federation/cli@0.20.0 + +## 0.19.1 + +### Patch Changes + +- Updated dependencies + - @module-federation/sdk@0.19.1 + - @module-federation/bridge-react@0.19.1 + - @module-federation/cli@0.19.1 + - @module-federation/enhanced@0.19.1 + - @module-federation/node@2.7.17 + - @module-federation/rsbuild-plugin@0.19.1 + - @module-federation/runtime@0.19.1 + +## 0.19.0 + +### Patch Changes + +- @module-federation/runtime@0.19.0 +- @module-federation/enhanced@0.19.0 +- @module-federation/sdk@0.19.0 +- @module-federation/bridge-react@0.19.0 +- @module-federation/rsbuild-plugin@0.19.0 +- @module-federation/cli@0.19.0 +- @module-federation/node@2.7.16 + +## 0.18.4 + +### Patch Changes + +- Updated dependencies [8061f8c] + - @module-federation/rsbuild-plugin@0.18.4 + - @module-federation/runtime@0.18.4 + - @module-federation/cli@0.18.4 + - @module-federation/sdk@0.18.4 + - @module-federation/bridge-react@0.18.4 + - @module-federation/enhanced@0.18.4 + - @module-federation/node@2.7.15 + +## 0.18.3 + +### Patch Changes + +- a892d74: feat: support env vars to add cors when use serve command + - @module-federation/runtime@0.18.3 + - @module-federation/enhanced@0.18.3 + - @module-federation/sdk@0.18.3 + - @module-federation/bridge-react@0.18.3 + - @module-federation/rsbuild-plugin@0.18.3 + - @module-federation/cli@0.18.3 + - @module-federation/node@2.7.14 + +## 0.18.2 + +### Patch Changes + +- Updated dependencies [756750e] +- Updated dependencies [756750e] +- Updated dependencies [991f57c] +- Updated dependencies [756750e] +- Updated dependencies [e110593] + - @module-federation/enhanced@0.18.2 + - @module-federation/rsbuild-plugin@0.18.2 + - @module-federation/node@2.7.13 + - @module-federation/bridge-react@0.18.2 + - @module-federation/runtime@0.18.2 + - @module-federation/cli@0.18.2 + - @module-federation/sdk@0.18.2 + +## 0.18.1 + +### Patch Changes + +- fix(modern-js-plugin): set bridge.disableAlias true when installing @module-federation/bridge-react +- 41ee332: chore(modern-js-plugin): re-export all bridge react +- Updated dependencies [8004e95] +- Updated dependencies [0bf3a3a] +- Updated dependencies [0bf3a3a] +- Updated dependencies [0bf3a3a] +- Updated dependencies [0bf3a3a] +- Updated dependencies [765b448] +- Updated dependencies [7dbc25d] + - @module-federation/bridge-react@0.18.1 + - @module-federation/enhanced@0.18.1 + - @module-federation/node@2.7.12 + - @module-federation/sdk@0.18.1 + - @module-federation/rsbuild-plugin@0.18.1 + - @module-federation/runtime@0.18.1 + - @module-federation/cli@0.18.1 + +## 0.18.0 + +### Patch Changes + +- Updated dependencies [609d477] +- Updated dependencies [0ab51b8] +- Updated dependencies [98a29c3] +- Updated dependencies [f6381e6] +- Updated dependencies [38b8d24] + - @module-federation/runtime@0.18.0 + - @module-federation/enhanced@0.18.0 + - @module-federation/sdk@0.18.0 + - @module-federation/rsbuild-plugin@0.18.0 + - @module-federation/bridge-react@0.18.0 + - @module-federation/node@2.7.11 + - @module-federation/cli@0.18.0 + +## 0.17.1 + +### Patch Changes + +- a7cf276: chore: upgrade NX to 21.2.3, Storybook to 9.0.9, and TypeScript to 5.8.3 + + - Upgraded NX from 21.0.3 to 21.2.3 with workspace configuration updates + - Migrated Storybook from 8.3.5 to 9.0.9 with updated configurations and automigrations + - Upgraded TypeScript from 5.7.3 to 5.8.3 with compatibility fixes + - Fixed package exports and type declaration paths across all packages + - Resolved module resolution issues and TypeScript compatibility problems + - Updated build configurations and dependencies to support latest versions + +- d31a326: refactor: sink React packages from root to individual packages + + - Removed React dependencies from root package.json and moved them to packages that actually need them + - Fixed rsbuild-plugin configuration to match workspace patterns + - Updated tests to handle platform-specific files + - This change improves dependency management by ensuring packages only have the dependencies they actually use + +- Updated dependencies [bc3bc10] +- Updated dependencies [7000c1f] +- Updated dependencies [bb953a6] +- Updated dependencies [2428be0] +- Updated dependencies [4ffefbe] +- Updated dependencies [65aa038] +- Updated dependencies [a7cf276] +- Updated dependencies [d31a326] +- Updated dependencies [1825b9d] +- Updated dependencies [8727aa3] + - @module-federation/enhanced@0.17.1 + - @module-federation/rsbuild-plugin@0.17.1 + - @module-federation/runtime@0.17.1 + - @module-federation/cli@0.17.1 + - @module-federation/bridge-react@0.17.1 + - @module-federation/sdk@0.17.1 + - @module-federation/node@2.7.10 + +## 0.17.0 + +### Minor Changes + +- e874c64: refactor(modern-js-plugin): deprecate createRemoteComponent and createRemoteSSRComponent + +### Patch Changes + +- e874c64: refactor(modern-js-plugin): add subpath react to export createLazyCompoent and wrapNoSSR apis +- f9985a8: chore(modern-js-plugin): update source.alias to resolve.alias +- 3f736b6: chore: rename FederationHost to ModuleFederation +- e0ceca6: bump modern.js to fix esbuild vulnerability +- Updated dependencies [e874c64] +- Updated dependencies [3f736b6] +- Updated dependencies [3f736b6] +- Updated dependencies [3f736b6] +- Updated dependencies [e874c64] +- Updated dependencies [3f736b6] +- Updated dependencies [e0ceca6] + - @module-federation/bridge-react@0.17.0 + - @module-federation/runtime@0.17.0 + - @module-federation/node@2.7.9 + - @module-federation/cli@0.17.0 + - @module-federation/enhanced@0.17.0 + - @module-federation/rsbuild-plugin@0.17.0 + - @module-federation/sdk@0.17.0 + +## 0.16.0 + +### Patch Changes + +- 98136ca: fix(modern-js-plugin): use contenthash instead of chunkhash +- de350f3: fix(modern-js-plugin): adjust fetch type +- Updated dependencies [1485fcf] +- Updated dependencies [98136ca] +- Updated dependencies [98136ca] + - @module-federation/sdk@0.16.0 + - @module-federation/node@2.7.8 + - @module-federation/rsbuild-plugin@0.16.0 + - @module-federation/cli@0.16.0 + - @module-federation/enhanced@0.16.0 + - @module-federation/runtime@0.16.0 + +## 0.15.0 + +### Minor Changes + +- f432619: feat(modern-js-plugin): support component-level data fetch + +### Patch Changes + +- c343589: fix(modern-js-plugin): only inject ipv4 str in dev mode +- 2faa3a3: chore(modernjs-js-plugin): keep the version of swc/helpers consistent with rsbuild +- Updated dependencies [ad446af] +- Updated dependencies [f777710] + - @module-federation/enhanced@0.15.0 + - @module-federation/rsbuild-plugin@0.15.0 + - @module-federation/cli@0.15.0 + - @module-federation/node@2.7.7 + - @module-federation/runtime@0.15.0 + - @module-federation/sdk@0.15.0 + +## 0.14.3 + +### Patch Changes + +- fix: empty dist + - @module-federation/enhanced@0.14.3 + - @module-federation/sdk@0.14.3 + - @module-federation/rsbuild-plugin@0.14.3 + - @module-federation/cli@0.14.3 + - @module-federation/node@2.7.6 + +## 0.14.2 + +### Patch Changes + +- e6ac307: fix(modern-js-plugin): downgrade lru-cache + - @module-federation/enhanced@0.14.2 + - @module-federation/sdk@0.14.2 + - @module-federation/rsbuild-plugin@0.14.2 + - @module-federation/cli@0.14.2 + - @module-federation/node@2.7.5 + +## 0.14.1 + +### Patch Changes + +- 0c68c2f: feat(modern-js-plugin): add server plugin to handle remote's SSR assets +- Updated dependencies [0c68c2f] + - @module-federation/cli@0.14.1 + - @module-federation/enhanced@0.14.1 + - @module-federation/node@2.7.4 + - @module-federation/rsbuild-plugin@0.14.1 + - @module-federation/sdk@0.14.1 + +## 0.14.0 + +### Patch Changes + +- Updated dependencies [82b8cac] +- Updated dependencies [82b8cac] +- Updated dependencies [26f8a77] +- Updated dependencies [d237ab9] +- Updated dependencies [0eb6697] + - @module-federation/enhanced@0.14.0 + - @module-federation/sdk@0.14.0 + - @module-federation/rsbuild-plugin@0.14.0 + - @module-federation/node@2.7.3 + - @module-federation/cli@0.14.0 + +## 0.13.1 + +### Patch Changes + +- b99d57c: fix(modern-js-plugin): export kit namespace to prevent import react directly + - @module-federation/enhanced@0.13.1 + - @module-federation/cli@0.13.1 + - @module-federation/node@2.7.2 + - @module-federation/rsbuild-plugin@0.13.1 + - @module-federation/sdk@0.13.1 + +## 0.13.0 + +### Patch Changes + +- 38f324f: Disable live bindings on cjs builds of the runtime packages +- Updated dependencies [e9a0681] +- Updated dependencies [9efb9b9] +- Updated dependencies [122f1b3] +- Updated dependencies [38f324f] + - @module-federation/cli@0.13.0 + - @module-federation/enhanced@0.13.0 + - @module-federation/node@2.7.1 + - @module-federation/rsbuild-plugin@0.13.0 + - @module-federation/sdk@0.13.0 + +## 0.12.0 + +### Patch Changes + +- Updated dependencies [f4fb242] +- Updated dependencies [f4fb242] +- Updated dependencies [f4fb242] +- Updated dependencies [c399b9a] +- Updated dependencies [ef96c4d] +- Updated dependencies [f4fb242] +- Updated dependencies [f4fb242] + - @module-federation/enhanced@0.12.0 + - @module-federation/node@2.7.0 + - @module-federation/sdk@0.12.0 + - @module-federation/rsbuild-plugin@0.12.0 + - @module-federation/cli@0.12.0 + +## 0.11.4 + +### Patch Changes + +- 64a2bc1: fix(modern-js-plugin): correct publicpath in build +- 292f2fd: chore(modern-js-plugin): warn if header origin is not specified +- 21c2fb9: fix(modern-js-plugin): apply ssr.distOutputDir in bundlerChain +- Updated dependencies [64a2bc1] +- Updated dependencies [ed8bda3] +- Updated dependencies [ebe7d89] +- Updated dependencies [c14842f] + - @module-federation/sdk@0.11.4 + - @module-federation/node@2.6.33 + - @module-federation/enhanced@0.11.4 + - @module-federation/cli@0.11.4 + - @module-federation/rsbuild-plugin@0.11.4 + +## 0.11.3 + +### Patch Changes + +- Updated dependencies [e5fae18] + - @module-federation/node@2.6.32 + - @module-federation/cli@0.11.3 + - @module-federation/enhanced@0.11.3 + - @module-federation/rsbuild-plugin@0.11.3 + - @module-federation/sdk@0.11.3 + +## 0.11.2 + +### Patch Changes + +- Updated dependencies [60d1fc1] +- Updated dependencies [047857b] + - @module-federation/rsbuild-plugin@0.11.2 + - @module-federation/sdk@0.11.2 + - @module-federation/cli@0.11.2 + - @module-federation/enhanced@0.11.2 + - @module-federation/node@2.6.31 + +## 0.11.1 + +### Patch Changes + +- Updated dependencies [09d6bc1] + - @module-federation/enhanced@0.11.1 + - @module-federation/node@2.6.30 + - @module-federation/rsbuild-plugin@0.11.1 + - @module-federation/sdk@0.11.1 + +## 0.11.0 + +### Patch Changes + +- Updated dependencies [fce107e] +- Updated dependencies [fce107e] +- Updated dependencies [5c4175e] +- Updated dependencies [f302eeb] + - @module-federation/enhanced@0.11.0 + - @module-federation/sdk@0.11.0 + - @module-federation/node@2.6.29 + - @module-federation/rsbuild-plugin@0.11.0 + +## 0.10.0 + +### Patch Changes + +- 1010f96: chore(modern-js-plugin): use bundlerChain instead of tools.webpack or tools.rspack +- Updated dependencies [0f71cbc] +- Updated dependencies [5b391b5] +- Updated dependencies [1010f96] +- Updated dependencies [22fcccd] +- Updated dependencies [3c8bd83] + - @module-federation/sdk@0.10.0 + - @module-federation/rsbuild-plugin@0.10.0 + - @module-federation/enhanced@0.10.0 + - @module-federation/node@2.6.28 + +## 0.9.1 + +### Patch Changes + +- Updated dependencies [35d925b] +- Updated dependencies [35d925b] +- Updated dependencies [8acd217] + - @module-federation/sdk@0.9.1 + - @module-federation/enhanced@0.9.1 + - @module-federation/node@2.6.27 + - @module-federation/rsbuild-plugin@0.9.1 + +## 0.9.0 + +### Patch Changes + +- @module-federation/enhanced@0.9.0 +- @module-federation/node@2.6.26 +- @module-federation/rsbuild-plugin@0.9.0 +- @module-federation/sdk@0.9.0 + +## 0.8.12 + +### Patch Changes + +- e602d82: fix: enable SSR by utilizing pluginOptions and configuration adjustments for improved accuracy +- Updated dependencies [9062cee] + - @module-federation/enhanced@0.8.12 + - @module-federation/node@2.6.25 + - @module-federation/rsbuild-plugin@0.8.12 + - @module-federation/sdk@0.8.12 + +## 0.8.11 + +### Patch Changes + +- @module-federation/enhanced@0.8.11 +- @module-federation/sdk@0.8.11 +- @module-federation/rsbuild-plugin@0.8.11 +- @module-federation/node@2.6.24 + +## 0.8.10 + +### Patch Changes + +- 21cc62c: chore: use new modern.js plugin for improved functionality + - @module-federation/node@2.6.23 + - @module-federation/enhanced@0.8.10 + - @module-federation/rsbuild-plugin@0.8.10 + - @module-federation/sdk@0.8.10 + +## 0.8.9 + +### Patch Changes + +- Updated dependencies [6e3afc6] + - @module-federation/enhanced@0.8.9 + - @module-federation/node@2.6.22 + - @module-federation/rsbuild-plugin@0.8.9 + - @module-federation/sdk@0.8.9 + +## 0.8.8 + +### Patch Changes + +- Updated dependencies [eda5184] + - @module-federation/enhanced@0.8.8 + - @module-federation/node@2.6.21 + - @module-federation/rsbuild-plugin@0.8.8 + - @module-federation/sdk@0.8.8 + +## 0.8.7 + +### Patch Changes + +- 5f67582: chore(modern-js-plugin): add ssr option +- Updated dependencies [835b09c] +- Updated dependencies [f573ad0] +- Updated dependencies [336f3d8] +- Updated dependencies [4fd33fb] + - @module-federation/sdk@0.8.7 + - @module-federation/enhanced@0.8.7 + - @module-federation/node@2.6.20 + - @module-federation/rsbuild-plugin@0.8.7 + +## 0.8.6 + +### Patch Changes + +- Updated dependencies [a1d46b7] + - @module-federation/rsbuild-plugin@0.8.6 + - @module-federation/enhanced@0.8.6 + - @module-federation/node@2.6.19 + - @module-federation/sdk@0.8.6 + +## 0.8.5 + +### Patch Changes + +- @module-federation/enhanced@0.8.5 +- @module-federation/sdk@0.8.5 +- @module-federation/node@2.6.18 + +## 0.8.4 + +### Patch Changes + +- @module-federation/enhanced@0.8.4 +- @module-federation/node@2.6.17 +- @module-federation/sdk@0.8.4 + +## 0.8.3 + +### Patch Changes + +- Updated dependencies [8e172c8] + - @module-federation/sdk@0.8.3 + - @module-federation/node@2.6.16 + - @module-federation/enhanced@0.8.3 + +## 0.8.2 + +### Patch Changes + +- @module-federation/enhanced@0.8.2 +- @module-federation/node@2.6.15 +- @module-federation/sdk@0.8.2 + +## 0.8.1 + +### Patch Changes + +- @module-federation/enhanced@0.8.1 +- @module-federation/node@2.6.14 +- @module-federation/sdk@0.8.1 + +## 0.8.0 + +### Patch Changes + +- d5c783b: fix: override watchOptions.ignored if the modernjs internal value is regexp +- e10725f: chore: no auto add watchOptions.ignored + - @module-federation/enhanced@0.8.0 + - @module-federation/sdk@0.8.0 + - @module-federation/node@2.6.13 + +## 0.7.7 + +### Patch Changes + +- a960c88: fix(modern-js-plugin): only export esm mfRuntimePlugin + - @module-federation/node@2.6.12 + - @module-federation/enhanced@0.7.7 + - @module-federation/sdk@0.7.7 + +## 0.7.6 + +### Patch Changes + +- Updated dependencies [6d35cf7] + - @module-federation/node@2.6.11 + - @module-federation/enhanced@0.7.6 + - @module-federation/sdk@0.7.6 + +## 0.7.5 + +### Patch Changes + +- a50b000: fix(modern-js-plugin): prevent components render multiple times if props change +- Updated dependencies [5613265] + - @module-federation/enhanced@0.7.5 + - @module-federation/node@2.6.10 + - @module-federation/sdk@0.7.5 + +## 0.7.4 + +### Patch Changes + +- @module-federation/node@2.6.9 +- @module-federation/enhanced@0.7.4 +- @module-federation/sdk@0.7.4 + +## 0.7.3 + +### Patch Changes + +- Updated dependencies [4ab9295] + - @module-federation/sdk@0.7.3 + - @module-federation/enhanced@0.7.3 + - @module-federation/node@2.6.8 + +## 0.7.2 + +### Patch Changes + +- @module-federation/enhanced@0.7.2 +- @module-federation/node@2.6.7 +- @module-federation/sdk@0.7.2 + +## 0.7.1 + +### Patch Changes + +- Updated dependencies [66ba7b1] +- Updated dependencies [6db4c5f] +- Updated dependencies [47fdbc2] + - @module-federation/node@2.6.6 + - @module-federation/sdk@0.7.1 + - @module-federation/enhanced@0.7.1 + +## 0.7.0 + +### Minor Changes + +- Updated dependencies [879ad87] +- Updated dependencies [4eb09e7] +- Updated dependencies [206b56d] + - @module-federation/sdk@0.7.0 + - @module-federation/enhanced@0.7.0 + - @module-federation/node@2.6.5 + +## 0.6.16 + +### Patch Changes + +- Updated dependencies [f779188] +- Updated dependencies [024df60] + - @module-federation/sdk@0.6.16 + - @module-federation/enhanced@0.6.16 + - @module-federation/node@2.6.4 + +## 0.6.15 + +### Patch Changes + +- d1e0f3e: fix(modern-js-plugin): set cors responseHeaders as \* + - @module-federation/node@2.6.3 + - @module-federation/enhanced@0.6.15 + - @module-federation/sdk@0.6.15 + +## 0.6.14 + +### Patch Changes + +- ad605d2: chore: unified logger +- Updated dependencies [87a2862] +- Updated dependencies [ad605d2] + - @module-federation/node@2.6.2 + - @module-federation/enhanced@0.6.14 + - @module-federation/sdk@0.6.14 + +## 0.6.13 + +### Patch Changes + +- Updated dependencies [f1b8848] + - @module-federation/node@2.6.1 + - @module-federation/enhanced@0.6.13 + - @module-federation/sdk@0.6.13 + +## 0.6.12 + +### Patch Changes + +- Updated dependencies [1478f50] +- Updated dependencies [1478f50] + - @module-federation/node@2.6.0 + - @module-federation/enhanced@0.6.12 + - @module-federation/sdk@0.6.12 + +## 0.6.11 + +### Patch Changes + +- Updated dependencies [d5a3072] + - @module-federation/sdk@0.6.11 + - @module-federation/node@2.5.21 + - @module-federation/enhanced@0.6.11 + +## 0.6.10 + +### Patch Changes + +- Updated dependencies [6b02145] +- Updated dependencies [22a3b83] + - @module-federation/enhanced@0.6.10 + - @module-federation/sdk@0.6.10 + - @module-federation/node@2.5.20 + +## 0.6.9 + +### Patch Changes + +- Updated dependencies [70a1708] + - @module-federation/enhanced@0.6.9 + - @module-federation/node@2.5.19 + - @module-federation/sdk@0.6.9 + +## 0.6.8 + +### Patch Changes + +- Updated dependencies [32db0ac] + - @module-federation/sdk@0.6.8 + - @module-federation/enhanced@0.6.8 + - @module-federation/node@2.5.18 + +## 0.6.7 + +### Patch Changes + +- Updated dependencies [1b6bf0e] +- Updated dependencies [9e32644] +- Updated dependencies [9e32644] +- Updated dependencies [9e32644] +- Updated dependencies [9e32644] + - @module-federation/enhanced@0.6.7 + - @module-federation/sdk@0.6.7 + - @module-federation/node@2.5.17 + +## 0.6.6 + +### Patch Changes + +- @module-federation/enhanced@0.6.6 +- @module-federation/node@2.5.16 +- @module-federation/sdk@0.6.6 + +## 0.6.5 + +### Patch Changes + +- @module-federation/enhanced@0.6.5 +- @module-federation/node@2.5.15 +- @module-federation/sdk@0.6.5 + +## 0.6.4 + +### Patch Changes + +- @module-federation/enhanced@0.6.4 +- @module-federation/node@2.5.14 +- @module-federation/sdk@0.6.4 + +## 0.6.3 + +### Patch Changes + +- 81201b8: fix(modernjs): mfConfigPlugin should run after @modern-js/plugin-initialize + - @module-federation/enhanced@0.6.3 + - @module-federation/sdk@0.6.3 + - @module-federation/node@2.5.13 + +## 0.6.2 + +### Patch Changes + +- 541494d: fix(modernjs): correct splitChunks.cacheGroups key which need to be removed +- 2394e38: fix(modernjs): auto set enableAsyncEntry when bundler is rspack + - @module-federation/node@2.5.12 + - @module-federation/enhanced@0.6.2 + - @module-federation/sdk@0.6.2 + +## 0.6.1 + +### Patch Changes + +- Updated dependencies [2855583] +- Updated dependencies [2855583] +- Updated dependencies [2855583] +- Updated dependencies [2855583] +- Updated dependencies [813680f] + - @module-federation/enhanced@0.6.1 + - @module-federation/sdk@0.6.1 + - @module-federation/node@2.5.11 + +## 0.6.0 + +### Patch Changes + +- Updated dependencies [f245bb3] +- Updated dependencies [1d9bb77] + - @module-federation/enhanced@0.6.0 + - @module-federation/sdk@0.6.0 + - @module-federation/node@2.5.10 + +## 0.5.2 + +### Patch Changes + +- Updated dependencies [b90fa7d] + - @module-federation/enhanced@0.5.2 + - @module-federation/sdk@0.5.2 + - @module-federation/node@2.5.9 + +## 0.5.1 + +### Patch Changes + +- @module-federation/enhanced@0.5.1 +- @module-federation/node@2.5.8 +- @module-federation/sdk@0.5.1 + +## 0.5.0 + +### Patch Changes + +- Updated dependencies [8378a77] + - @module-federation/sdk@0.5.0 + - @module-federation/enhanced@0.5.0 + - @module-federation/node@2.5.7 + +## 0.4.0 + +### Patch Changes + +- 88dec4e: fix(modern-js-plugin): require node plugin on demand +- Updated dependencies [a335707] +- Updated dependencies [a6e2bed] +- Updated dependencies [a6e2bed] + - @module-federation/enhanced@0.4.0 + - @module-federation/sdk@0.4.0 + - @module-federation/node@2.5.6 + +## 0.3.5 + +### Patch Changes + +- Updated dependencies [59db2fd] + - @module-federation/enhanced@0.3.5 + - @module-federation/node@2.5.5 + - @module-federation/sdk@0.3.5 + +## 0.3.4 + +### Patch Changes + +- 951d705: chore: upgrade modernjs@2.57.0 + - @module-federation/node@2.5.4 + - @module-federation/enhanced@0.3.4 + - @module-federation/sdk@0.3.4 + +## 0.3.3 + +### Patch Changes + +- Updated dependencies [85c6a12] + - @module-federation/node@2.5.3 + - @module-federation/enhanced@0.3.3 + - @module-federation/sdk@0.3.3 + +## 0.3.2 + +### Patch Changes + +- 85ae159: feat: support rspack ssr +- Updated dependencies [85ae159] + - @module-federation/enhanced@0.3.2 + - @module-federation/node@2.5.2 + - @module-federation/sdk@0.3.2 + +## 0.3.1 + +### Patch Changes + +- @module-federation/enhanced@0.3.1 +- @module-federation/node@2.5.1 +- @module-federation/sdk@0.3.1 + +## 0.2.0 + +### Minor Changes + +- fa37cc4: feat: support modern.js ssr [#2348](https://github.com/module-federation/core/issues/2348) + +### Patch Changes + +- Updated dependencies [fa37cc4] + - @module-federation/enhanced@0.3.0 + - @module-federation/node@2.5.0 + - @module-federation/sdk@0.3.0 diff --git a/packages/modernjs-v3/LICENSE b/packages/modernjs-v3/LICENSE new file mode 100644 index 000000000000..f74c11c43d62 --- /dev/null +++ b/packages/modernjs-v3/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024-present zhanghang(2heal1) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/modernjs-v3/README.md b/packages/modernjs-v3/README.md new file mode 100644 index 000000000000..a085c0e0296a --- /dev/null +++ b/packages/modernjs-v3/README.md @@ -0,0 +1,5 @@ +# @module-federation/modern-js-v3 + +This plugin provides Module Federation supporting functions for Modern.js + +See [documentation](https://module-federation.io/guide/framework/modernjs.html) for more details . diff --git a/packages/modernjs-v3/bin/mf.js b/packages/modernjs-v3/bin/mf.js new file mode 100755 index 000000000000..6ac43ef81057 --- /dev/null +++ b/packages/modernjs-v3/bin/mf.js @@ -0,0 +1,4 @@ +#!/usr/bin/env node +const { runCli } = require('@module-federation/cli'); + +runCli(); diff --git a/packages/modernjs-v3/package.json b/packages/modernjs-v3/package.json new file mode 100644 index 000000000000..169036952d3e --- /dev/null +++ b/packages/modernjs-v3/package.json @@ -0,0 +1,188 @@ +{ + "name": "@module-federation/modern-js-v3", + "version": "2.0.1", + "files": [ + "dist/", + "types.d.ts", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "rslib build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/module-federation/core.git", + "directory": "packages/modernjs-v3" + }, + "exports": { + ".": { + "types": "./dist/types/cli/index.d.ts", + "import": "./dist/esm/cli/index.mjs", + "require": "./dist/cjs/cli/index.js" + }, + "./runtime": { + "types": "./dist/types/runtime/index.d.ts", + "default": "./dist/esm/runtime/index.mjs" + }, + "./react": { + "types": "./dist/types/react/index.d.ts", + "default": "./dist/esm/react/index.mjs" + }, + "./react-v18": { + "types": "./dist/types/react/v18.d.ts", + "default": "./dist/esm/react/v18.mjs" + }, + "./react-v19": { + "types": "./dist/types/react/v19.d.ts", + "default": "./dist/esm/react/v19.mjs" + }, + "./react-plugin": { + "types": "./dist/types/react/plugin.d.ts", + "default": "./dist/esm/react/plugin.mjs" + }, + "./ssr-dev-plugin": { + "types": "./dist/types/ssr-runtime/devPlugin.d.ts", + "default": "./dist/esm/ssr-runtime/devPlugin.mjs" + }, + "./ssr-inject-data-fetch-function-plugin": { + "types": "./dist/types/ssr-runtime/injectDataFetchFunctionPlugin.d.ts", + "default": "./dist/esm/ssr-runtime/injectDataFetchFunctionPlugin.mjs" + }, + "./config-plugin": { + "types": "./dist/types/cli/configPlugin.d.ts", + "import": "./dist/esm/cli/configPlugin.mjs", + "require": "./dist/cjs/cli/configPlugin.js" + }, + "./ssr-plugin": { + "types": "./dist/types/cli/ssrPlugin.d.ts", + "import": "./dist/esm/cli/ssrPlugin.mjs", + "require": "./dist/cjs/cli/ssrPlugin.js" + }, + "./shared-strategy": { + "types": "./dist/types/cli/mfRuntimePlugins/shared-strategy.d.ts", + "import": "./dist/esm/cli/mfRuntimePlugins/shared-strategy.mjs", + "require": "./dist/cjs/cli/mfRuntimePlugins/shared-strategy.js" + }, + "./resolve-entry-ipv4": { + "types": "./dist/types/cli/mfRuntimePlugins/resolve-entry-ipv4.d.ts", + "import": "./dist/esm/cli/mfRuntimePlugins/resolve-entry-ipv4.mjs", + "require": "./dist/cjs/cli/mfRuntimePlugins/resolve-entry-ipv4.js" + }, + "./inject-node-fetch": { + "types": "./dist/types/cli/mfRuntimePlugins/inject-node-fetch.d.ts", + "import": "./dist/esm/cli/mfRuntimePlugins/inject-node-fetch.mjs", + "require": "./dist/cjs/cli/mfRuntimePlugins/inject-node-fetch.js" + }, + "./data-fetch-server-plugin": { + "types": "./dist/types/cli/server/data-fetch-server-plugin.d.ts", + "default": "./dist/cjs/cli/server/data-fetch-server-plugin.js" + }, + "./server": { + "types": "./dist/types/server/index.d.ts", + "default": "./dist/cjs/server/index.js" + } + }, + "typesVersions": { + "*": { + ".": [ + "./dist/types/cli/index.d.ts" + ], + "runtime": [ + "./dist/types/runtime/index.d.ts" + ], + "react": [ + "./dist/types/react/index.d.ts" + ], + "react-v18": [ + "./dist/types/react/v18.d.ts" + ], + "react-v19": [ + "./dist/types/react/v19.d.ts" + ], + "react-plugin": [ + "./dist/types/react/plugin.d.ts" + ], + "config-plugin": [ + "./dist/types/cli/configPlugin.d.ts" + ], + "ssr-plugin": [ + "./dist/types/cli/ssrPlugin.d.ts" + ], + "shared-strategy": [ + "./dist/types/cli/mfRuntimePlugins/shared-strategy.d.ts" + ], + "resolve-entry-ipv4": [ + "./dist/types/cli/mfRuntimePlugins/resolve-entry-ipv4.d.ts" + ], + "inject-node-fetch": [ + "./dist/types/cli/mfRuntimePlugins/inject-node-fetch.d.ts" + ], + "data-fetch-server-plugin": [ + "./dist/types/cli/server/data-fetch-server-plugin.d.ts" + ], + "ssr-inject-data-fetch-function-plugin": [ + "./dist/types/ssr-runtime/injectDataFetchFunctionPlugin.d.ts" + ], + "server": [ + "./dist/types/server/index.d.ts" + ] + } + }, + "main": "./dist/cjs/cli/index.js", + "types": "./dist/types/cli/index.d.ts", + "author": "hanric ", + "license": "MIT", + "dependencies": { + "@module-federation/rsbuild-plugin": "https://pkg.pr.new/module-federation/core/@module-federation/rsbuild-plugin@73bd1ddb2266002f586a872ddae8020052c5457c", + "@module-federation/bridge-react": "https://pkg.pr.new/module-federation/core/@module-federation/bridge-react@73bd1ddb2266002f586a872ddae8020052c5457c", + "fs-extra": "11.3.0", + "lru-cache": "10.4.3", + "@module-federation/enhanced": "https://pkg.pr.new/module-federation/core/@module-federation/enhanced@73bd1ddb2266002f586a872ddae8020052c5457c", + "@module-federation/runtime": "2.0.0", + "@module-federation/sdk": "https://pkg.pr.new/module-federation/core/@module-federation/sdk@73bd1ddb2266002f586a872ddae8020052c5457c", + "@module-federation/cli": "https://pkg.pr.new/module-federation/core/@module-federation/cli@73bd1ddb2266002f586a872ddae8020052c5457c", + "@swc/helpers": "^0.5.17", + "node-fetch": "~3.3.0", + "jiti": "^2.6.1", + "react-error-boundary": "4.1.2", + "@module-federation/node": "2.7.32" + }, + "devDependencies": { + "@rsbuild/plugin-react": "1.4.4", + "@module-federation/manifest": "https://pkg.pr.new/module-federation/core/@module-federation/manifest@73bd1ddb2266002f586a872ddae8020052c5457c", + "@rslib/core": "0.19.5", + "@rsbuild/core": "2.0.0-beta.4", + "@modern-js/app-tools": "workspace:*", + "@modern-js/server-runtime": "workspace:*", + "@modern-js/module-tools": "2.70.4", + "@modern-js/runtime": "workspace:*", + "@modern-js/tsconfig": "workspace:*", + "@types/react": "^19.2.13", + "@types/react-dom": "^19.2.3" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17", + "typescript": "^4.9.0 || ^5.0.0", + "vue-tsc": "^1.0.24", + "react-router-dom": "^4 || ^5 || ^6 || ^7", + "react-router": "^7" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue-tsc": { + "optional": true + }, + "react-router-dom": { + "optional": true + }, + "react-router": { + "optional": true + } + } +} diff --git a/packages/modernjs-v3/project.json b/packages/modernjs-v3/project.json new file mode 100644 index 000000000000..7f1a5bf5e611 --- /dev/null +++ b/packages/modernjs-v3/project.json @@ -0,0 +1,57 @@ +{ + "name": "modern-js-plugin-v3", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/modernjs-v3/src", + "projectType": "library", + "tags": ["type:pkg"], + "implicitDependencies": [], + "targets": { + "build": { + "executor": "nx:run-commands", + "outputs": ["{projectRoot}/packages/modernjs-v3/dist"], + "dependsOn": [ + { + "target": "build", + "dependencies": true + } + ], + "options": { + "parallel": false, + "commands": [ + "cd packages/modernjs-v3; pnpm run build || (sleep 2 && pnpm run build)", + "cp packages/modernjs-v3/LICENSE packages/modernjs-v3/dist" + ] + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["packages/modernjs-v3/**/*.ts"] + } + }, + "test": { + "executor": "@nx/vite:test", + "outputs": ["{workspaceRoot}/coverage/packages/modernjs-v3"] + }, + "pre-release": { + "executor": "nx:run-commands", + "options": { + "parallel": false, + "commands": [ + { + "command": "nx run modern-js-plugin-v3:test", + "forwardAllArgs": false + }, + { + "command": "nx run modern-js-plugin-v3:build", + "forwardAllArgs": false + } + ] + } + }, + "semantic-release": { + "executor": "@goestav/nx-semantic-release:semantic-release" + } + } +} diff --git a/packages/modernjs-v3/rslib.config.ts b/packages/modernjs-v3/rslib.config.ts new file mode 100644 index 000000000000..6de6b3be6bd3 --- /dev/null +++ b/packages/modernjs-v3/rslib.config.ts @@ -0,0 +1,58 @@ +import { pluginReact } from '@rsbuild/plugin-react'; +import { defineConfig } from '@rslib/core'; + +const sharedLibOptions = { + bundle: false, + externalHelpers: true, + outBase: 'src', +} as const; + +export default defineConfig({ + source: { + entry: { + index: ['./src/**/*.{ts,tsx,js,jsx}', '!./src/**/*.spec.*'], + }, + }, + plugins: [ + pluginReact({ + swcReactOptions: { + runtime: 'automatic', + }, + }), + ], + lib: [ + { + ...sharedLibOptions, + format: 'cjs', + syntax: 'es2019', + dts: false, + output: { + distPath: { + root: './dist/cjs', + }, + }, + }, + { + ...sharedLibOptions, + format: 'esm', + syntax: 'es5', + dts: false, + output: { + distPath: { + root: './dist/esm', + }, + }, + }, + { + ...sharedLibOptions, + format: 'esm', + syntax: 'es2019', + dts: false, + output: { + distPath: { + root: './dist/esm-node', + }, + }, + }, + ], +}); diff --git a/packages/modernjs-v3/src/cli/configPlugin.spec.ts b/packages/modernjs-v3/src/cli/configPlugin.spec.ts new file mode 100644 index 000000000000..27a2977e0f9d --- /dev/null +++ b/packages/modernjs-v3/src/cli/configPlugin.spec.ts @@ -0,0 +1,85 @@ +import { describe, expect, it } from 'vitest'; +import { patchMFConfig } from './configPlugin'; +import { getIPV4 } from './utils'; + +const mfConfig = { + name: 'host', + filename: 'remoteEntry.js', + remotes: { + remote: 'http://localhost:3000/remoteEntry.js', + }, + shared: { + react: { singleton: true, eager: true }, + 'react-dom': { singleton: true, eager: true }, + }, +}; +describe('patchMFConfig', async () => { + it('patchMFConfig: server', async () => { + const patchedConfig = JSON.parse(JSON.stringify(mfConfig)); + patchMFConfig(patchedConfig, true); + const ipv4 = getIPV4(); + + expect(patchedConfig).toStrictEqual({ + dev: false, + dts: false, + filename: 'remoteEntry.js', + library: { + name: 'host', + type: 'commonjs-module', + }, + name: 'host', + remotes: { + remote: `http://${ipv4}:3000/remoteEntry.js`, + }, + remoteType: 'script', + runtimePlugins: [ + require.resolve('@module-federation/modern-js-v3/shared-strategy'), + require.resolve('@module-federation/node/runtimePlugin'), + require.resolve('@module-federation/modern-js-v3/inject-node-fetch'), + ], + shared: { + react: { + eager: true, + singleton: true, + }, + 'react-dom': { + eager: true, + singleton: true, + }, + }, + }); + }); + + it('patchMFConfig: client', async () => { + const patchedConfig = JSON.parse(JSON.stringify(mfConfig)); + patchMFConfig(patchedConfig, false); + const ipv4 = getIPV4(); + + expect(patchedConfig).toStrictEqual({ + filename: 'remoteEntry.js', + name: 'host', + remotes: { + remote: `http://${ipv4}:3000/remoteEntry.js`, + }, + remoteType: 'script', + runtimePlugins: [ + require.resolve('@module-federation/modern-js-v3/shared-strategy'), + ], + shared: { + react: { + eager: true, + singleton: true, + }, + 'react-dom': { + eager: true, + singleton: true, + }, + }, + dts: { + consumeTypes: { + runtimePkgs: ['@module-federation/modern-js-v3/runtime'], + }, + }, + }); + }); +}); diff --git a/packages/modernjs-v3/src/cli/configPlugin.ts b/packages/modernjs-v3/src/cli/configPlugin.ts new file mode 100644 index 000000000000..be4990506deb --- /dev/null +++ b/packages/modernjs-v3/src/cli/configPlugin.ts @@ -0,0 +1,529 @@ +import path from 'path'; +import { + addDataFetchExposes, + autoDeleteSplitChunkCacheGroups, +} from '@module-federation/rsbuild-plugin/utils'; +import { + encodeName, + type moduleFederationPlugin, +} from '@module-federation/sdk'; +import { LOCALHOST, PLUGIN_IDENTIFIER } from '../constant'; +import logger from '../logger'; +import type { PluginOptions } from '../types'; +import { getIPV4, isWebTarget, skipByTarget } from './utils'; +import { isDev } from './utils'; + +import type { + AppTools, + AppUserConfig, + CliPlugin, + Rspack, +} from '@modern-js/app-tools'; +import type { BundlerChainConfig } from '../interfaces/bundler'; +import type { InternalModernPluginOptions } from '../types'; + +const defaultPath = path.resolve(process.cwd(), 'module-federation.config.ts'); + +export type ConfigType = Rspack.Configuration; + +type RuntimePluginEntry = NonNullable< + moduleFederationPlugin.ModuleFederationPluginOptions['runtimePlugins'] +>[number]; + +export function setEnv(enableSSR: boolean) { + if (enableSSR) { + process.env.MF_SSR_PRJ = 'true'; + } +} + +export const getMFConfig = async ( + userConfig: PluginOptions, +): Promise => { + const { config, configPath } = userConfig; + if (config) { + return config; + } + const mfConfigPath = configPath ? configPath : defaultPath; + const { createJiti } = require('jiti'); + const jit = createJiti(__filename, { + interopDefault: true, + esmResolve: true, + }); + const configModule = await jit(mfConfigPath); + + const resolvedConfig = ( + configModule && + typeof configModule === 'object' && + 'default' in configModule + ? (configModule as { default: unknown }).default + : configModule + ) as moduleFederationPlugin.ModuleFederationPluginOptions; + + return resolvedConfig; +}; + +const injectRuntimePlugins = ( + runtimePlugin: RuntimePluginEntry, + runtimePlugins: RuntimePluginEntry[], +): void => { + const pluginName = + typeof runtimePlugin === 'string' ? runtimePlugin : runtimePlugin[0]; + + const hasPlugin = runtimePlugins.some(existingPlugin => { + if (typeof existingPlugin === 'string') { + return existingPlugin === pluginName; + } + + return existingPlugin[0] === pluginName; + }); + + if (!hasPlugin) { + runtimePlugins.push(runtimePlugin); + } +}; + +const replaceRemoteUrl = ( + mfConfig: moduleFederationPlugin.ModuleFederationPluginOptions, + remoteIpStrategy?: 'ipv4' | 'inherit', +) => { + if (remoteIpStrategy && remoteIpStrategy === 'inherit') { + return; + } + if (!mfConfig.remotes) { + return; + } + const ipv4 = getIPV4(); + const handleRemoteObject = ( + remoteObject: moduleFederationPlugin.RemotesObject, + ) => { + Object.keys(remoteObject).forEach(remoteKey => { + const remote = remoteObject[remoteKey]; + // no support array items yet + if (Array.isArray(remote)) { + return; + } + if (typeof remote === 'string' && remote.includes(LOCALHOST)) { + remoteObject[remoteKey] = remote.replace(LOCALHOST, ipv4); + } + if ( + typeof remote === 'object' && + !Array.isArray(remote.external) && + remote.external.includes(LOCALHOST) + ) { + remote.external = remote.external.replace(LOCALHOST, ipv4); + } + }); + }; + if (Array.isArray(mfConfig.remotes)) { + mfConfig.remotes.forEach(remoteObject => { + if (typeof remoteObject === 'string') { + return; + } + handleRemoteObject(remoteObject); + }); + } else if (typeof mfConfig.remotes !== 'string') { + handleRemoteObject(mfConfig.remotes); + } +}; + +const patchDTSConfig = ( + mfConfig: moduleFederationPlugin.ModuleFederationPluginOptions, + isServer: boolean, +) => { + if (isServer) { + return; + } + const ModernJSRuntime = '@module-federation/modern-js-v3/runtime'; + if (mfConfig.dts !== false) { + if (typeof mfConfig.dts === 'boolean' || mfConfig.dts === undefined) { + mfConfig.dts = { + consumeTypes: { + runtimePkgs: [ModernJSRuntime], + }, + }; + } else if ( + mfConfig.dts?.consumeTypes || + mfConfig.dts?.consumeTypes === undefined + ) { + if ( + typeof mfConfig.dts.consumeTypes === 'boolean' || + mfConfig.dts?.consumeTypes === undefined + ) { + mfConfig.dts.consumeTypes = { + runtimePkgs: [ModernJSRuntime], + }; + } else { + mfConfig.dts.consumeTypes.runtimePkgs = + mfConfig.dts.consumeTypes.runtimePkgs || []; + if (!mfConfig.dts.consumeTypes.runtimePkgs.includes(ModernJSRuntime)) { + mfConfig.dts.consumeTypes.runtimePkgs.push(ModernJSRuntime); + } + } + } + } +}; + +export const patchMFConfig = ( + mfConfig: moduleFederationPlugin.ModuleFederationPluginOptions, + isServer: boolean, + remoteIpStrategy?: 'ipv4' | 'inherit', + enableSSR?: boolean, +) => { + replaceRemoteUrl(mfConfig, remoteIpStrategy); + addDataFetchExposes(mfConfig.exposes, isServer); + + if (mfConfig.remoteType === undefined) { + mfConfig.remoteType = 'script'; + } + + if (!mfConfig.name) { + throw new Error(`${PLUGIN_IDENTIFIER} mfConfig.name can not be empty!`); + } + + const runtimePlugins = [ + ...(mfConfig.runtimePlugins || []), + ] as RuntimePluginEntry[]; + + patchDTSConfig(mfConfig, isServer); + + injectRuntimePlugins( + require.resolve('@module-federation/modern-js-v3/shared-strategy'), + runtimePlugins, + ); + + if (enableSSR && isDev()) { + injectRuntimePlugins( + require.resolve('@module-federation/modern-js-v3/resolve-entry-ipv4'), + runtimePlugins, + ); + } + + if (isServer) { + injectRuntimePlugins( + require.resolve('@module-federation/node/runtimePlugin'), + runtimePlugins, + ); + if (isDev()) { + injectRuntimePlugins( + require.resolve( + '@module-federation/node/record-dynamic-remote-entry-hash-plugin', + ), + runtimePlugins, + ); + } + + injectRuntimePlugins( + require.resolve('@module-federation/modern-js-v3/inject-node-fetch'), + runtimePlugins, + ); + + if (!mfConfig.library) { + mfConfig.library = { + type: 'commonjs-module', + name: mfConfig.name, + }; + } else { + if (!mfConfig.library.type) { + mfConfig.library.type = 'commonjs-module'; + } + if (!mfConfig.library.name) { + mfConfig.library.name = mfConfig.name; + } + } + } + + mfConfig.runtimePlugins = runtimePlugins; + + if (!isServer) { + if (mfConfig.library?.type === 'commonjs-module') { + mfConfig.library.type = 'global'; + } + return mfConfig; + } + + mfConfig.dts = false; + mfConfig.dev = false; + + return mfConfig; +}; + +function patchIgnoreWarning(chain: BundlerChainConfig) { + const ignoreWarnings = chain.get('ignoreWarnings') || []; + const ignoredMsgs = [ + 'external script', + 'process.env.WS_NO_BUFFER_UTIL', + `Can't resolve 'utf-8-validate`, + ]; + ignoreWarnings.push(warning => { + if (ignoredMsgs.some(msg => warning.message.includes(msg))) { + return true; + } + return false; + }); + chain.ignoreWarnings(ignoreWarnings); +} + +export function addMyTypes2Ignored( + chain: BundlerChainConfig, + mfConfig: moduleFederationPlugin.ModuleFederationPluginOptions, +) { + const watchOptions = chain.get( + 'watchOptions', + ) as Rspack.Configuration['watchOptions']; + if (!watchOptions || !watchOptions.ignored) { + chain.watchOptions({ + ignored: /[\\/](?:\.git|node_modules|@mf-types)[\\/]/, + }); + return; + } + const ignored = watchOptions.ignored; + const DEFAULT_IGNORED_GLOB = '**/@mf-types/**'; + + if (Array.isArray(ignored)) { + if ( + mfConfig.dts !== false && + typeof mfConfig.dts === 'object' && + typeof mfConfig.dts.consumeTypes === 'object' && + mfConfig.dts.consumeTypes.remoteTypesFolder + ) { + chain.watchOptions({ + ...watchOptions, + ignored: ignored.concat( + `**/${mfConfig.dts.consumeTypes.remoteTypesFolder}/**`, + ), + }); + } else { + chain.watchOptions({ + ...watchOptions, + ignored: ignored.concat(DEFAULT_IGNORED_GLOB), + }); + } + + return; + } + + if (typeof ignored !== 'string') { + chain.watchOptions({ + ...watchOptions, + ignored: /[\\/](?:\.git|node_modules|@mf-types)[\\/]/, + }); + return; + } + + chain.watchOptions({ + ...watchOptions, + ignored: ignored.concat(DEFAULT_IGNORED_GLOB), + }); +} +export function patchBundlerConfig(options: { + chain: BundlerChainConfig; + isServer: boolean; + modernjsConfig: AppUserConfig; + mfConfig: moduleFederationPlugin.ModuleFederationPluginOptions; + enableSSR: boolean; +}) { + const { chain, modernjsConfig, isServer, mfConfig, enableSSR } = options; + + chain.optimization.delete('runtimeChunk'); + + patchIgnoreWarning(chain); + + if (!chain.output.get('chunkLoadingGlobal')) { + chain.output.chunkLoadingGlobal(`chunk_${mfConfig.name}`); + } + if (!chain.output.get('uniqueName')) { + chain.output.uniqueName(mfConfig.name!); + } + + const splitChunkConfig = chain.optimization.splitChunks.entries(); + if (!isServer) { + // @ts-ignore type not the same + autoDeleteSplitChunkCacheGroups(mfConfig, splitChunkConfig); + } + + if ( + !isServer && + enableSSR && + splitChunkConfig && + typeof splitChunkConfig === 'object' && + splitChunkConfig.cacheGroups + ) { + splitChunkConfig.chunks = 'async'; + logger.warn( + `splitChunks.chunks = async is not allowed with stream SSR mode, it will auto changed to "async"`, + ); + } + + if (isDev() && chain.output.get('publicPath') === 'auto') { + // TODO: only in dev temp + const port = modernjsConfig.server?.port || 8080; + const publicPath = `http://localhost:${port}/`; + chain.output.publicPath(publicPath); + } + + if (isServer && enableSSR) { + const uniqueName = mfConfig.name || chain.output.get('uniqueName'); + const chunkFileName = chain.output.get('chunkFilename'); + if ( + typeof chunkFileName === 'string' && + uniqueName && + !chunkFileName.includes(uniqueName) + ) { + const suffix = `${encodeName(uniqueName)}-[contenthash].js`; + chain.output.chunkFilename(chunkFileName.replace('.js', suffix)); + } + } + // modernjs project has the same entry for server/client, add polyfill:false to skip compile error in browser target + if (isDev() && enableSSR && !isServer) { + chain.resolve.fallback + .set('crypto', false) + .set('stream', false) + .set('vm', false); + } + + if ( + modernjsConfig.deploy?.microFrontend && + Object.keys(mfConfig.exposes || {}).length + ) { + chain.optimization.usedExports(false); + } +} + +export const moduleFederationConfigPlugin = ( + userConfig: InternalModernPluginOptions, +): CliPlugin => ({ + name: '@modern-js/plugin-module-federation-config', + pre: ['@modern-js/plugin-initialize'], + post: ['@modern-js/plugin-module-federation'], + setup: async api => { + const modernjsConfig = api.getConfig(); + const mfConfig = await getMFConfig(userConfig.originPluginOptions); + const csrConfig = + userConfig.csrConfig || JSON.parse(JSON.stringify(mfConfig)); + const ssrConfig = + userConfig.ssrConfig || JSON.parse(JSON.stringify(mfConfig)); + userConfig.ssrConfig = ssrConfig; + userConfig.csrConfig = csrConfig; + const enableSSR = Boolean( + userConfig.userConfig?.ssr ?? Boolean(modernjsConfig?.server?.ssr), + ); + const enableRsc = Boolean(modernjsConfig?.server?.rsc); + + api.modifyBundlerChain(chain => { + const target = chain.get('target'); + if (skipByTarget(target)) { + return; + } + const isWeb = isWebTarget(target); + addMyTypes2Ignored(chain, !isWeb ? ssrConfig : csrConfig); + + const targetMFConfig = !isWeb ? ssrConfig : csrConfig; + patchMFConfig( + targetMFConfig, + !isWeb, + userConfig.remoteIpStrategy || 'ipv4', + enableSSR, + ); + + patchBundlerConfig({ + chain, + isServer: !isWeb, + modernjsConfig, + mfConfig, + enableSSR, + }); + + if (isWeb) { + userConfig.distOutputDir = + chain.output.get('path') || path.resolve(process.cwd(), 'dist'); + } else if (enableSSR && !enableRsc) { + userConfig.userConfig ||= {}; + userConfig.userConfig.ssr ||= {}; + if (userConfig.userConfig.ssr === true) { + userConfig.userConfig.ssr = {}; + } + userConfig.userConfig.ssr.distOutputDir = + chain.output.get('path') || + path.resolve(process.cwd(), 'dist/bundles'); + } + }); + api.config(() => { + const ipv4 = getIPV4(); + + if (userConfig.remoteIpStrategy === undefined) { + if (!enableSSR) { + userConfig.remoteIpStrategy = 'inherit'; + } else { + userConfig.remoteIpStrategy = 'ipv4'; + } + } + + const devServerConfig = modernjsConfig.tools?.devServer; + const corsWarnMsgs = [ + 'View https://module-federation.io/guide/troubleshooting/other.html#cors-warn for more details.', + ]; + if ( + typeof devServerConfig !== 'object' || + !('headers' in devServerConfig) + ) { + corsWarnMsgs.unshift( + 'Detect devServer.headers is empty, mf modern plugin will add default cors header: devServer.headers["Access-Control-Allow-Headers"] = "*". It is recommended to specify an allowlist of trusted origins instead.', + ); + } + + const exposes = userConfig.csrConfig?.exposes; + const hasExposes = + exposes && Array.isArray(exposes) + ? exposes.length + : Object.keys(exposes ?? {}).length; + + if (corsWarnMsgs.length > 1 && hasExposes) { + logger.warn(corsWarnMsgs.join('\n')); + } + + const corsHeaders = hasExposes + ? { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': + 'GET, POST, PUT, DELETE, PATCH, OPTIONS', + 'Access-Control-Allow-Headers': '*', + } + : undefined; + const defineConfig = { + REMOTE_IP_STRATEGY: JSON.stringify(userConfig.remoteIpStrategy), + }; + if (enableSSR && isDev()) { + defineConfig.FEDERATION_IPV4 = JSON.stringify(ipv4); + } + return { + tools: { + devServer: { + headers: corsHeaders, + }, + }, + resolve: { + alias: { + // TODO: deprecated + '@modern-js/runtime/mf': require.resolve( + '@module-federation/modern-js-v3/runtime', + ), + }, + }, + source: { + define: defineConfig, + enableAsyncEntry: + modernjsConfig.source?.enableAsyncEntry ?? !enableRsc, + }, + dev: { + assetPrefix: modernjsConfig?.dev?.assetPrefix + ? modernjsConfig.dev.assetPrefix + : 'auto', + }, + }; + }); + }, +}); + +export default moduleFederationConfigPlugin; + +export { isWebTarget, skipByTarget }; diff --git a/packages/modernjs-v3/src/cli/index.ts b/packages/modernjs-v3/src/cli/index.ts new file mode 100644 index 000000000000..6158ff3be415 --- /dev/null +++ b/packages/modernjs-v3/src/cli/index.ts @@ -0,0 +1,78 @@ +import type { AppTools, CliPlugin } from '@modern-js/app-tools'; +import { + ModuleFederationPlugin as RspackModuleFederationPlugin, + TreeShakingSharedPlugin as RspackTreeShakingSharedPlugin, +} from '@module-federation/enhanced/rspack'; +import type { moduleFederationPlugin as MFPluginOptions } from '@module-federation/sdk'; +import type { InternalModernPluginOptions, PluginOptions } from '../types'; +import { moduleFederationConfigPlugin } from './configPlugin'; +import { moduleFederationSSRPlugin } from './ssrPlugin'; +import { isWebTarget } from './utils'; + +export const moduleFederationPlugin = ( + userConfig: PluginOptions = {}, +): CliPlugin => { + const internalModernPluginOptions: InternalModernPluginOptions = { + csrConfig: undefined, + ssrConfig: undefined, + browserPlugin: undefined, + nodePlugin: undefined, + assetResources: {}, + distOutputDir: '', + originPluginOptions: { ...userConfig }, + remoteIpStrategy: userConfig?.remoteIpStrategy, + userConfig: userConfig || {}, + assetFileNames: {}, + fetchServerQuery: userConfig.fetchServerQuery ?? undefined, + secondarySharedTreeShaking: userConfig.secondarySharedTreeShaking ?? false, + }; + return { + name: '@modern-js/plugin-module-federation', + setup: async api => { + api.modifyBundlerChain(chain => { + const browserPluginOptions = + internalModernPluginOptions.csrConfig as MFPluginOptions.ModuleFederationPluginOptions; + const { secondarySharedTreeShaking } = internalModernPluginOptions; + if (isWebTarget(chain.get('target'))) { + if (secondarySharedTreeShaking) { + chain + .plugin('plugin-module-federation') + .use(RspackTreeShakingSharedPlugin, [ + { + mfConfig: browserPluginOptions, + secondary: true, + } as any, + ]); + } else { + chain + .plugin('plugin-module-federation') + .use(RspackModuleFederationPlugin, [browserPluginOptions]) + .init((Plugin: typeof RspackModuleFederationPlugin, args) => { + internalModernPluginOptions.browserPlugin = new Plugin(args[0]); + return internalModernPluginOptions.browserPlugin; + }); + } + } + }); + + api._internalServerPlugins(({ plugins }) => { + plugins.push({ + name: '@module-federation/modern-js-v3/server', + }); + return { plugins }; + }); + }, + usePlugins: [ + moduleFederationConfigPlugin(internalModernPluginOptions), + moduleFederationSSRPlugin( + internalModernPluginOptions as Required, + ), + ], + }; +}; + +export default moduleFederationPlugin; + +export { createModuleFederationConfig } from '@module-federation/enhanced'; + +export type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime'; diff --git a/packages/modernjs-v3/src/cli/mfRuntimePlugins/inject-node-fetch.ts b/packages/modernjs-v3/src/cli/mfRuntimePlugins/inject-node-fetch.ts new file mode 100644 index 000000000000..7ea5c64bf326 --- /dev/null +++ b/packages/modernjs-v3/src/cli/mfRuntimePlugins/inject-node-fetch.ts @@ -0,0 +1,14 @@ +import type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime'; +import nodeFetch from 'node-fetch'; + +const injectNodeFetchPlugin: () => ModuleFederationRuntimePlugin = () => ({ + name: 'inject-node-fetch-plugin', + beforeInit(args) { + if (!globalThis.fetch) { + // @ts-expect-error inject node-fetch + globalThis.fetch = nodeFetch; + } + return args; + }, +}); +export default injectNodeFetchPlugin; diff --git a/packages/modernjs-v3/src/cli/mfRuntimePlugins/resolve-entry-ipv4.ts b/packages/modernjs-v3/src/cli/mfRuntimePlugins/resolve-entry-ipv4.ts new file mode 100644 index 000000000000..5e6c9b3087c6 --- /dev/null +++ b/packages/modernjs-v3/src/cli/mfRuntimePlugins/resolve-entry-ipv4.ts @@ -0,0 +1,70 @@ +import type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime'; +import { LOCALHOST } from '../../constant'; + +declare const FEDERATION_IPV4: string | undefined; +declare const REMOTE_IP_STRATEGY: 'ipv4' | 'inherit' | undefined; + +const ipv4 = + typeof FEDERATION_IPV4 !== 'undefined' ? FEDERATION_IPV4 : '127.0.0.1'; + +const remoteIpStrategy = + typeof REMOTE_IP_STRATEGY !== 'undefined' ? REMOTE_IP_STRATEGY : 'inherit'; + +function replaceObjectLocalhost(key: string, obj: Record) { + if (remoteIpStrategy !== 'ipv4') { + return; + } + if (!(key in obj)) { + return; + } + const remote = obj[key]; + if (remote && typeof remote === 'string' && remote.includes(LOCALHOST)) { + obj[key] = replaceLocalhost(remote); + } +} +function replaceLocalhost(url: string): string { + return url.replace(LOCALHOST, ipv4); +} + +const resolveEntryIpv4Plugin: () => ModuleFederationRuntimePlugin = () => ({ + name: 'resolve-entry-ipv4', + + beforeRegisterRemote(args) { + const { remote } = args; + replaceObjectLocalhost('entry', remote); + return args; + }, + async afterResolve(args) { + const { remoteInfo } = args; + replaceObjectLocalhost('entry', remoteInfo); + return args; + }, + beforeLoadRemoteSnapshot(args) { + const { moduleInfo } = args; + if ('entry' in moduleInfo) { + replaceObjectLocalhost('entry', moduleInfo); + return args; + } + if ('version' in moduleInfo) { + replaceObjectLocalhost('version', moduleInfo); + } + return args; + }, + loadRemoteSnapshot(args) { + const { remoteSnapshot } = args; + if ('publicPath' in remoteSnapshot) { + replaceObjectLocalhost('publicPath', remoteSnapshot); + } + if ('getPublicPath' in remoteSnapshot) { + replaceObjectLocalhost('getPublicPath', remoteSnapshot); + } + if (remoteSnapshot.remotesInfo) { + Object.keys(remoteSnapshot.remotesInfo).forEach(key => { + const remoteInfo = remoteSnapshot.remotesInfo[key]; + replaceObjectLocalhost('matchedVersion', remoteInfo); + }); + } + return args; + }, +}); +export default resolveEntryIpv4Plugin; diff --git a/packages/modernjs-v3/src/cli/mfRuntimePlugins/shared-strategy.ts b/packages/modernjs-v3/src/cli/mfRuntimePlugins/shared-strategy.ts new file mode 100644 index 000000000000..295e01d3a856 --- /dev/null +++ b/packages/modernjs-v3/src/cli/mfRuntimePlugins/shared-strategy.ts @@ -0,0 +1,22 @@ +import type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime'; + +const sharedStrategy: () => ModuleFederationRuntimePlugin = () => ({ + name: 'shared-strategy-plugin', + beforeInit(args) { + const { userOptions } = args; + const shared = userOptions.shared; + if (shared) { + Object.keys(shared).forEach(sharedKey => { + const sharedConfigs = shared[sharedKey]; + const arraySharedConfigs = Array.isArray(sharedConfigs) + ? sharedConfigs + : [sharedConfigs]; + arraySharedConfigs.forEach(s => { + s.strategy = 'loaded-first'; + }); + }); + } + return args; + }, +}); +export default sharedStrategy; diff --git a/packages/modernjs-v3/src/cli/server/data-fetch-server-plugin.ts b/packages/modernjs-v3/src/cli/server/data-fetch-server-plugin.ts new file mode 100644 index 000000000000..08a4ae728f04 --- /dev/null +++ b/packages/modernjs-v3/src/cli/server/data-fetch-server-plugin.ts @@ -0,0 +1,19 @@ +import dataFetchMiddleWare from '@module-federation/bridge-react/data-fetch-server-middleware'; + +import type { ServerPlugin } from '@modern-js/server-runtime'; + +const dataFetchServePlugin = (): ServerPlugin => ({ + name: 'mf-data-fetch-server-plugin', + setup: api => { + api.onPrepare(() => { + const { middlewares } = api.getServerContext(); + middlewares.push({ + name: 'module-federation-serve-manifest', + // @ts-ignore type error + handler: dataFetchMiddleWare, + }); + }); + }, +}); + +export default dataFetchServePlugin; diff --git a/packages/modernjs-v3/src/cli/ssrPlugin.ts b/packages/modernjs-v3/src/cli/ssrPlugin.ts new file mode 100644 index 000000000000..c8ac0ebbcf4b --- /dev/null +++ b/packages/modernjs-v3/src/cli/ssrPlugin.ts @@ -0,0 +1,377 @@ +import path from 'path'; +import { + ModuleFederationPlugin as RspackModuleFederationPlugin, + TreeShakingSharedPlugin as RspackTreeShakingSharedPlugin, +} from '@module-federation/enhanced/rspack'; +import UniverseEntryChunkTrackerPlugin from '@module-federation/node/universe-entry-chunk-tracker-plugin'; +import { + type StatsAssetResource, + updateStatsAndManifest, +} from '@module-federation/rsbuild-plugin/utils'; +import { + ManifestFileName, + StatsFileName, + simpleJoinRemoteEntry, +} from '@module-federation/sdk'; +import type { moduleFederationPlugin } from '@module-federation/sdk'; +import fs from 'fs-extra'; +import logger from '../logger'; +import { isDev } from './utils'; +import { isWebTarget, skipByTarget } from './utils'; + +import type { AppTools, CliPlugin } from '@modern-js/app-tools'; +import type { ModifyRspackConfigFn, RsbuildPlugin } from '@rsbuild/core'; +import type { + AssetFileNames, + InternalModernPluginOptions, + PluginOptions, +} from '../types'; + +export function setEnv() { + process.env.MF_SSR_PRJ = 'true'; +} + +export const CHAIN_MF_PLUGIN_ID = 'plugin-module-federation-server'; + +function getManifestAssetFileNames( + manifestOption?: moduleFederationPlugin.ModuleFederationPluginOptions['manifest'], +): AssetFileNames { + if (!manifestOption) { + return { + statsFileName: StatsFileName, + manifestFileName: ManifestFileName, + }; + } + + const JSON_EXT = '.json'; + const filePath = + typeof manifestOption === 'boolean' ? '' : manifestOption.filePath || ''; + const baseFileName = + typeof manifestOption === 'boolean' ? '' : manifestOption.fileName || ''; + const ensureExt = (name: string) => + name.endsWith(JSON_EXT) ? name : `${name}${JSON_EXT}`; + const withSuffix = (name: string, suffix: string) => + name.replace(JSON_EXT, `${suffix}${JSON_EXT}`); + const manifestFileName = baseFileName + ? ensureExt(baseFileName) + : ManifestFileName; + const statsFileName = baseFileName + ? withSuffix(manifestFileName, '-stats') + : StatsFileName; + + return { + statsFileName: simpleJoinRemoteEntry(filePath, statsFileName), + manifestFileName: simpleJoinRemoteEntry(filePath, manifestFileName), + }; +} + +type ModifyBundlerConfiguration = Parameters[0]; +type ModifyBundlerUtils = Parameters[1]; + +const mfSSRRsbuildPlugin = ( + pluginOptions: Required, +): RsbuildPlugin => { + return { + name: '@modern-js/plugin-mf-post-config', + pre: ['@modern-js/builder-plugin-ssr'], + setup(api) { + if (pluginOptions.csrConfig.getPublicPath) { + return; + } + let csrOutputPath = ''; + let ssrOutputPath = ''; + let ssrEnv = ''; + let csrEnv = ''; + + const browserAssetFileNames = + pluginOptions.assetFileNames.browser || + getManifestAssetFileNames(pluginOptions.csrConfig?.manifest); + const nodeAssetFileNames = + pluginOptions.assetFileNames?.node || + getManifestAssetFileNames(pluginOptions.ssrConfig?.manifest); + + const collectAssets = ( + assets: Record string | Buffer }>, + fileNames: { statsFileName: string; manifestFileName: string }, + tag: 'browser' | 'node', + ): StatsAssetResource | undefined => { + const statsAsset = assets[fileNames.statsFileName]; + const manifestAsset = assets[fileNames.manifestFileName]; + + if (!statsAsset || !manifestAsset) { + return undefined; + } + + try { + const statsRaw = statsAsset.source(); + const manifestRaw = manifestAsset.source(); + const statsContent = + typeof statsRaw === 'string' ? statsRaw : statsRaw.toString(); + const manifestContent = + typeof manifestRaw === 'string' + ? manifestRaw + : manifestRaw.toString(); + + return { + stats: { + data: JSON.parse(statsContent), + filename: fileNames.statsFileName, + }, + manifest: { + data: JSON.parse(manifestContent), + filename: fileNames.manifestFileName, + }, + }; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + logger.error(`Failed to parse ${tag} manifest assets: ${message}`); + return undefined; + } + }; + + api.modifyEnvironmentConfig((config, { name }) => { + const target = config.output.target; + if (skipByTarget(target)) { + return config; + } + if (isWebTarget(target)) { + csrOutputPath = config.output.distPath.root; + csrEnv = name; + } else { + ssrOutputPath = config.output.distPath.root; + ssrEnv = name; + } + return config; + }); + + const modifySSRPublicPath = ( + config: ModifyBundlerConfiguration, + utils: ModifyBundlerUtils, + ) => { + if (ssrEnv !== utils.environment.name) { + return config; + } + const userSSRConfig = pluginOptions.userConfig.ssr + ? typeof pluginOptions.userConfig.ssr === 'object' + ? pluginOptions.userConfig.ssr + : {} + : {}; + if (!userSSRConfig.distOutputDir) { + return; + } + config.output!.publicPath = `${config.output!.publicPath}${path.relative(csrOutputPath, ssrOutputPath)}/`; + return config; + }; + api.modifyRspackConfig((config, utils) => { + modifySSRPublicPath(config, utils); + return config; + }); + + api.processAssets( + { stage: 'report' }, + ({ assets, environment: envContext }) => { + const envName = envContext.name; + + if ( + pluginOptions.csrConfig?.manifest !== false && + csrEnv && + envName === csrEnv + ) { + const browserAssets = collectAssets( + assets, + browserAssetFileNames, + 'browser', + ); + if (browserAssets) { + pluginOptions.assetResources.browser = browserAssets; + } + } + + if ( + pluginOptions.ssrConfig?.manifest !== false && + ssrEnv && + envName === ssrEnv + ) { + const nodeAssets = collectAssets( + assets, + nodeAssetFileNames, + 'node', + ); + if (nodeAssets) { + pluginOptions.assetResources.node = nodeAssets; + } + } + }, + ); + }, + }; +}; + +export const moduleFederationSSRPlugin = ( + pluginOptions: Required, +): CliPlugin => ({ + name: '@modern-js/plugin-module-federation-ssr', + pre: [ + '@modern-js/plugin-module-federation-config', + '@modern-js/plugin-module-federation', + ], + setup: async api => { + const modernjsConfig = api.getConfig(); + const enableSSR = + pluginOptions.userConfig?.ssr ?? Boolean(modernjsConfig?.server?.ssr); + const enableRsc = Boolean(modernjsConfig?.server?.rsc); + const { secondarySharedTreeShaking } = pluginOptions; + if (!enableSSR) { + return; + } + + setEnv(); + + api._internalRuntimePlugins(({ entrypoint, plugins }) => { + if (secondarySharedTreeShaking) { + return { entrypoint, plugins }; + } + const { fetchServerQuery } = pluginOptions; + plugins.push({ + name: 'injectDataFetchFunction', + path: '@module-federation/modern-js-v3/ssr-inject-data-fetch-function-plugin', + config: { + fetchServerQuery, + }, + }); + if (!isDev()) { + return { entrypoint, plugins }; + } + plugins.push({ + name: 'mfSSRDev', + path: '@module-federation/modern-js-v3/ssr-dev-plugin', + config: {}, + }); + return { entrypoint, plugins }; + }); + + if (pluginOptions.ssrConfig.remotes) { + api._internalServerPlugins(({ plugins }) => { + plugins.push({ + name: '@module-federation/modern-js-v3/data-fetch-server-plugin', + options: {}, + }); + + return { plugins }; + }); + } + + api.modifyBundlerChain(chain => { + const target = chain.get('target'); + if (skipByTarget(target)) { + return; + } + const isWeb = isWebTarget(target); + + if (!isWeb) { + if (!chain.plugins.has(CHAIN_MF_PLUGIN_ID)) { + if (secondarySharedTreeShaking) { + chain + .plugin(CHAIN_MF_PLUGIN_ID) + .use(RspackTreeShakingSharedPlugin, [ + { + mfConfig: pluginOptions.ssrConfig, + secondary: true, + } as any, + ]); + } else { + chain + .plugin(CHAIN_MF_PLUGIN_ID) + .use(RspackModuleFederationPlugin, [pluginOptions.ssrConfig]) + .init((Plugin: typeof RspackModuleFederationPlugin, args) => { + pluginOptions.nodePlugin = new Plugin(args[0]); + return pluginOptions.nodePlugin; + }); + } + } + } + + if (!isWeb && !secondarySharedTreeShaking) { + chain.target('async-node'); + + if (enableRsc) { + chain.resolve.conditionNames.add('react-server'); + } + + if (isDev()) { + chain + .plugin('UniverseEntryChunkTrackerPlugin') + .use(UniverseEntryChunkTrackerPlugin); + } + } + + if (isDev() && isWeb) { + chain.externals({ + '@module-federation/node/utils': 'NOT_USED_IN_BROWSER', + }); + } + }); + // @ts-ignore + api.config(() => { + return { + builderPlugins: [mfSSRRsbuildPlugin(pluginOptions)], + dev: { + setupMiddlewares: [ + middlewares => + middlewares.unshift((req, res, next) => { + if (!enableSSR) { + next(); + return; + } + try { + if ( + req.url?.includes('.json') && + !req.url?.includes('hot-update') + ) { + const filepath = path.join(process.cwd(), `dist${req.url}`); + fs.statSync(filepath); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader( + 'Access-Control-Allow-Methods', + 'GET, POST, PUT, DELETE, PATCH, OPTIONS', + ); + res.setHeader('Access-Control-Allow-Headers', '*'); + fs.createReadStream(filepath).pipe(res); + } else { + next(); + } + } catch (err) { + logger.debug(err); + next(); + } + }), + ], + }, + }; + }); + const writeMergedManifest = () => { + const { distOutputDir, assetResources } = pluginOptions; + const browserAssets = assetResources.browser; + const nodeAssets = assetResources.node; + + if (!distOutputDir || !browserAssets || !nodeAssets) { + return; + } + try { + updateStatsAndManifest(nodeAssets, browserAssets, distOutputDir); + } catch (err) { + logger.error(err); + } + }; + + api.onAfterBuild(() => { + writeMergedManifest(); + }); + api.onDevCompileDone(() => { + // 热更后修改 manifest + writeMergedManifest(); + }); + }, +}); + +export default moduleFederationSSRPlugin; diff --git a/packages/modernjs-v3/src/cli/utils.ts b/packages/modernjs-v3/src/cli/utils.ts new file mode 100644 index 000000000000..63b26989ef1b --- /dev/null +++ b/packages/modernjs-v3/src/cli/utils.ts @@ -0,0 +1,57 @@ +import os from 'os'; +import type { Rspack } from '@modern-js/app-tools'; + +export type ConfigType = Rspack.Configuration; + +const localIpv4 = '127.0.0.1'; + +const getIpv4Interfaces = (): os.NetworkInterfaceInfo[] => { + try { + const interfaces = os.networkInterfaces(); + const ipv4Interfaces: os.NetworkInterfaceInfo[] = []; + + Object.values(interfaces).forEach(detail => { + detail?.forEach(detail => { + // 'IPv4' is in Node <= 17, from 18 it's a number 4 or 6 + const familyV4Value = typeof detail.family === 'string' ? 'IPv4' : 4; + + if (detail.family === familyV4Value && detail.address !== localIpv4) { + ipv4Interfaces.push(detail); + } + }); + }); + return ipv4Interfaces; + } catch (_err) { + return []; + } +}; + +export const getIPV4 = (): string => { + const ipv4Interfaces = getIpv4Interfaces(); + const ipv4Interface = ipv4Interfaces[0] || { address: localIpv4 }; + return ipv4Interface.address; +}; + +export const isWebTarget = (target: string[] | string) => { + const WEB_TARGET = 'web'; + if (Array.isArray(target)) { + return target.includes(WEB_TARGET); + } else if (typeof target === 'string') { + return target === WEB_TARGET; + } + return false; +}; + +export const skipByTarget = (target: string[] | string) => { + const IGNORE_TARGET = 'webworker'; + if (Array.isArray(target)) { + return target.includes(IGNORE_TARGET); + } else if (typeof target === 'string') { + return target === IGNORE_TARGET; + } + return false; +}; + +export function isDev() { + return process.env.NODE_ENV === 'development'; +} diff --git a/packages/modernjs-v3/src/constant.ts b/packages/modernjs-v3/src/constant.ts new file mode 100644 index 000000000000..8b547453196b --- /dev/null +++ b/packages/modernjs-v3/src/constant.ts @@ -0,0 +1,2 @@ +export const LOCALHOST = 'localhost'; +export const PLUGIN_IDENTIFIER = '[ Modern.js Module Federation ]'; diff --git a/packages/modernjs-v3/src/interfaces/bundler.ts b/packages/modernjs-v3/src/interfaces/bundler.ts new file mode 100644 index 000000000000..5b02ec48e0ee --- /dev/null +++ b/packages/modernjs-v3/src/interfaces/bundler.ts @@ -0,0 +1,29 @@ +import type { AppUserConfig } from '@modern-js/app-tools'; + +type AppToolsUserConfig = AppUserConfig['tools']; + +type ExcludeUndefined = T extends undefined ? never : T; + +type ExtractObjectType = T extends (...args: any[]) => any ? never : T; + +type OmitArrayConfiguration = T extends Array + ? T extends (infer U)[] + ? U + : T + : ExtractObjectType; + +type RspackConfigs = ExcludeUndefined extends { + rspack?: infer U; +} + ? U + : never; + +type ObjectRspack = ExtractObjectType>; + +type BundlerChain = ExcludeUndefined['bundlerChain']; + +type BundlerChainFunc = Extract any>; + +export type BundlerChainConfig = Parameters[0]; + +export type BundlerConfig = ObjectRspack; diff --git a/packages/modernjs-v3/src/logger.ts b/packages/modernjs-v3/src/logger.ts new file mode 100644 index 000000000000..c1be3252163d --- /dev/null +++ b/packages/modernjs-v3/src/logger.ts @@ -0,0 +1,6 @@ +import { createLogger } from '@module-federation/sdk'; +import { PLUGIN_IDENTIFIER } from './constant'; + +const logger = createLogger(PLUGIN_IDENTIFIER); + +export default logger; diff --git a/packages/modernjs-v3/src/react/index.ts b/packages/modernjs-v3/src/react/index.ts new file mode 100644 index 000000000000..7aedf02e47e6 --- /dev/null +++ b/packages/modernjs-v3/src/react/index.ts @@ -0,0 +1 @@ +export * from '@module-federation/bridge-react'; diff --git a/packages/modernjs-v3/src/react/plugin.ts b/packages/modernjs-v3/src/react/plugin.ts new file mode 100644 index 000000000000..5033022a0dc7 --- /dev/null +++ b/packages/modernjs-v3/src/react/plugin.ts @@ -0,0 +1,2 @@ +export * from '@module-federation/bridge-react/plugin'; +export { default } from '@module-federation/bridge-react/plugin'; diff --git a/packages/modernjs-v3/src/react/v18.ts b/packages/modernjs-v3/src/react/v18.ts new file mode 100644 index 000000000000..7985c425f7c6 --- /dev/null +++ b/packages/modernjs-v3/src/react/v18.ts @@ -0,0 +1 @@ +export * from '@module-federation/bridge-react/v18'; diff --git a/packages/modernjs-v3/src/react/v19.ts b/packages/modernjs-v3/src/react/v19.ts new file mode 100644 index 000000000000..dde3fcbda1e8 --- /dev/null +++ b/packages/modernjs-v3/src/react/v19.ts @@ -0,0 +1 @@ +export * from '@module-federation/bridge-react/v19'; diff --git a/packages/modernjs-v3/src/runtime/index.ts b/packages/modernjs-v3/src/runtime/index.ts new file mode 100644 index 000000000000..532254ed0fc5 --- /dev/null +++ b/packages/modernjs-v3/src/runtime/index.ts @@ -0,0 +1 @@ +export * from '@module-federation/enhanced/runtime'; diff --git a/packages/modernjs-v3/src/server/fileCache.spec.ts b/packages/modernjs-v3/src/server/fileCache.spec.ts new file mode 100644 index 000000000000..bd5b4bd0a0bf --- /dev/null +++ b/packages/modernjs-v3/src/server/fileCache.spec.ts @@ -0,0 +1,29 @@ +import { beforeAll, describe, expect, it, vi } from 'vitest'; +import { FileCache } from './fileCache'; + +beforeAll(() => { + vi.mock('fs-extra', () => ({ + default: { + pathExists: () => { + return true; + }, + lstat: () => { + return { + mtimeMs: Date.now(), + size: 4, + }; + }, + readFile: () => { + return 'test'; + }, + }, + })); +}); + +describe('modern serve static file cache', async () => { + it('should cache file', async () => { + const cache = new FileCache(); + const result = await cache.getFile('test.txt'); + expect(result?.content).toBe('test'); + }); +}); diff --git a/packages/modernjs-v3/src/server/fileCache.ts b/packages/modernjs-v3/src/server/fileCache.ts new file mode 100644 index 000000000000..8fbbf2fa6432 --- /dev/null +++ b/packages/modernjs-v3/src/server/fileCache.ts @@ -0,0 +1,60 @@ +import fs from 'fs-extra'; +import { LRUCache } from 'lru-cache'; + +export interface FileResult { + content: string; + lastModified: number; +} + +export class FileCache { + private cache = new LRUCache({ + maxSize: 200 * 1024 * 1024, // 200MB + }); + + /** + * Check if file exists and return file info + * @param filepath Path to the file + * @returns FileResult or null if file doesn't exist + */ + async getFile(filepath: string): Promise { + // Check if file exists + if (!(await fs.pathExists(filepath))) { + return null; + } + + try { + const stat = await fs.lstat(filepath); + const currentModified = stat.mtimeMs; + + // Check if file is in cache and if the cached version is still valid + const cachedEntry = this.cache.get(filepath); + if (cachedEntry && currentModified <= cachedEntry.lastModified) { + return { + content: cachedEntry.content, + lastModified: cachedEntry.lastModified, + }; + } + + // Read file and update cache + const content = await fs.readFile(filepath, 'utf-8'); + const newEntry: FileResult = { + content, + lastModified: currentModified, + }; + + this.cache.set(filepath, newEntry, { + size: stat.size || content.length, + }); + + return { + content, + lastModified: currentModified, + }; + } catch (err) { + return null; + } + } +} + +// Export singleton instance +export const fileCache = new FileCache(); diff --git a/packages/modernjs-v3/src/server/index.ts b/packages/modernjs-v3/src/server/index.ts new file mode 100644 index 000000000000..7ef16356ecf8 --- /dev/null +++ b/packages/modernjs-v3/src/server/index.ts @@ -0,0 +1,49 @@ +import type { ServerPlugin } from '@modern-js/server-runtime'; +import { + createCorsMiddleware, + createStaticMiddleware, +} from './staticMiddleware'; + +const staticServePlugin = (): ServerPlugin => ({ + name: '@modern-js/module-federation/server', + setup: api => { + api.onPrepare(() => { + // In development, we don't need to serve the manifest file, bundler dev server will handle it + if (process.env.NODE_ENV === 'development') { + return; + } + + const { middlewares } = api.getServerContext(); + const config = api.getServerConfig(); + + const assetPrefix = config.output?.assetPrefix || ''; + // When SSR is enabled, we need to serve the files in `bundle/` directory externally + // Modern.js will only serve the files in `static/` directory + if (config.server?.ssr) { + const context = api.getServerContext(); + const pwd = context.distDirectory!; + const serverStaticMiddleware = createStaticMiddleware({ + assetPrefix, + pwd, + }); + middlewares.push({ + name: 'module-federation-serve-manifest', + handler: serverStaticMiddleware, + }); + } + + // When the MODERN_MF_AUTO_CORS environment variable is set, the server will add CORS headers to the response + // This environment variable should only be set when running `serve` command in local test. + if (process.env.MODERN_MF_AUTO_CORS) { + const corsMiddleware = createCorsMiddleware(); + middlewares.push({ + name: 'module-federation-cors', + handler: corsMiddleware, + }); + } + }); + }, +}); + +export default staticServePlugin; +export { staticServePlugin }; diff --git a/packages/modernjs-v3/src/server/staticMiddleware.spec.ts b/packages/modernjs-v3/src/server/staticMiddleware.spec.ts new file mode 100644 index 000000000000..a4798bd52a08 --- /dev/null +++ b/packages/modernjs-v3/src/server/staticMiddleware.spec.ts @@ -0,0 +1,240 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { createStaticMiddleware } from './staticMiddleware'; + +// Mock dependencies +vi.mock('fs-extra', () => ({ + default: { + pathExists: vi.fn(), + }, +})); + +vi.mock('./fileCache', () => ({ + fileCache: { + getFile: vi.fn(), + }, +})); + +import fs from 'fs-extra'; +import { fileCache } from './fileCache'; + +describe('staticMiddleware', () => { + let middleware: any; + let mockContext: any; + let nextSpy: any; + + beforeEach(() => { + // Reset all mocks + vi.clearAllMocks(); + + // Create middleware instance + middleware = createStaticMiddleware({ + assetPrefix: '', + pwd: '/test/path', + }); + + // Setup mock context + nextSpy = vi.fn(); + mockContext = { + req: { + path: '', + }, + header: vi.fn(), + body: vi.fn(), + }; + }); + + describe('file extension filtering', () => { + it('should call next() for non-js files', async () => { + mockContext.req.path = '/bundles/test.css'; + + await middleware(mockContext, nextSpy); + + expect(nextSpy).toHaveBeenCalledOnce(); + expect(mockContext.header).not.toHaveBeenCalled(); + expect(mockContext.body).not.toHaveBeenCalled(); + }); + + it('should call next() for files without extension', async () => { + mockContext.req.path = '/bundles/test'; + + await middleware(mockContext, nextSpy); + + expect(nextSpy).toHaveBeenCalledOnce(); + expect(mockContext.header).not.toHaveBeenCalled(); + expect(mockContext.body).not.toHaveBeenCalled(); + }); + + it('should process .js files', async () => { + mockContext.req.path = '/bundles/test.js'; + (fs.pathExists as any).mockResolvedValue(false); + + await middleware(mockContext, nextSpy); + + // Should not return early due to extension check + expect(fs.pathExists).toHaveBeenCalled(); + }); + }); + + describe('asset prefix filtering', () => { + it('should call next() for paths not starting with /bundles', async () => { + mockContext.req.path = '/assets/test.js'; + + await middleware(mockContext, nextSpy); + + expect(nextSpy).toHaveBeenCalledOnce(); + expect(fs.pathExists).not.toHaveBeenCalled(); + expect(mockContext.header).not.toHaveBeenCalled(); + expect(mockContext.body).not.toHaveBeenCalled(); + }); + + it('should call next() for root path', async () => { + mockContext.req.path = '/test.js'; + + await middleware(mockContext, nextSpy); + + expect(nextSpy).toHaveBeenCalledOnce(); + expect(fs.pathExists).not.toHaveBeenCalled(); + }); + + it('should process paths starting with /bundles', async () => { + mockContext.req.path = '/bundles/test.js'; + (fs.pathExists as any).mockResolvedValue(false); + + await middleware(mockContext, nextSpy); + + // Should proceed to file existence check + expect(fs.pathExists).toHaveBeenCalledWith('/test/path/bundles/test.js'); + }); + }); + + describe('file existence check', () => { + it('should call next() when file does not exist', async () => { + mockContext.req.path = '/bundles/nonexistent.js'; + (fs.pathExists as any).mockResolvedValue(false); + + await middleware(mockContext, nextSpy); + + expect(fs.pathExists).toHaveBeenCalledWith( + '/test/path/bundles/nonexistent.js', + ); + expect(nextSpy).toHaveBeenCalledOnce(); + expect(fileCache.getFile).not.toHaveBeenCalled(); + expect(mockContext.header).not.toHaveBeenCalled(); + expect(mockContext.body).not.toHaveBeenCalled(); + }); + + it('should proceed to file cache when file exists', async () => { + mockContext.req.path = '/bundles/existing.js'; + (fs.pathExists as any).mockResolvedValue(true); + (fileCache.getFile as any).mockResolvedValue(null); + + await middleware(mockContext, nextSpy); + + expect(fs.pathExists).toHaveBeenCalledWith( + '/test/path/bundles/existing.js', + ); + expect(fileCache.getFile).toHaveBeenCalledWith( + '/test/path/bundles/existing.js', + ); + }); + }); + + describe('successful file serving', () => { + it('should serve file content with correct headers', async () => { + const mockFileContent = 'console.log("test");'; + const mockFileResult = { + content: mockFileContent, + lastModified: Date.now(), + }; + + mockContext.req.path = '/bundles/app.js'; + (fs.pathExists as any).mockResolvedValue(true); + (fileCache.getFile as any).mockResolvedValue(mockFileResult); + mockContext.body.mockReturnValue('response'); + + const result = await middleware(mockContext, nextSpy); + + expect(fs.pathExists).toHaveBeenCalledWith('/test/path/bundles/app.js'); + expect(fileCache.getFile).toHaveBeenCalledWith( + '/test/path/bundles/app.js', + ); + expect(nextSpy).not.toHaveBeenCalled(); + + // Check headers + expect(mockContext.header).toHaveBeenCalledWith( + 'Content-Type', + 'application/javascript', + ); + expect(mockContext.header).toHaveBeenCalledWith( + 'Content-Length', + String(mockFileResult.content.length), + ); + + // Check response + expect(mockContext.body).toHaveBeenCalledWith( + mockFileResult.content, + 200, + ); + expect(result).toBe('response'); + }); + + it('should handle empty file content', async () => { + const mockFileResult = { + content: '', + lastModified: Date.now(), + }; + + mockContext.req.path = '/bundles/empty.js'; + (fs.pathExists as any).mockResolvedValue(true); + (fileCache.getFile as any).mockResolvedValue(mockFileResult); + mockContext.body.mockReturnValue('empty-response'); + + const result = await middleware(mockContext, nextSpy); + + expect(mockContext.header).toHaveBeenCalledWith('Content-Length', '0'); + expect(mockContext.body).toHaveBeenCalledWith( + mockFileResult.content, + 200, + ); + expect(result).toBe('empty-response'); + expect(nextSpy).not.toHaveBeenCalled(); + }); + }); + + describe('asset prefix handling', () => { + it('should handle custom asset prefix correctly', async () => { + const customMiddleware = createStaticMiddleware({ + assetPrefix: '/custom-prefix', + pwd: '/test/path', + }); + + mockContext.req.path = '/bundles/test.js'; + await customMiddleware(mockContext, nextSpy); + + expect(nextSpy).toHaveBeenCalledOnce(); + expect(mockContext.header).not.toHaveBeenCalled(); + expect(mockContext.body).not.toHaveBeenCalled(); + }); + + it('should handle asset prefix removal correctly', async () => { + const customMiddleware = createStaticMiddleware({ + assetPrefix: '/prefix', + pwd: '/test/path', + }); + + const mockFileResult = { + content: 'test content', + lastModified: Date.now(), + }; + + mockContext.req.path = '/prefix/bundles/test.js'; + (fs.pathExists as any).mockResolvedValue(true); + (fileCache.getFile as any).mockResolvedValue(mockFileResult); + + await customMiddleware(mockContext, nextSpy); + + // Should remove prefix from path + expect(fs.pathExists).toHaveBeenCalledWith('/test/path/bundles/test.js'); + }); + }); +}); diff --git a/packages/modernjs-v3/src/server/staticMiddleware.ts b/packages/modernjs-v3/src/server/staticMiddleware.ts new file mode 100644 index 000000000000..1b9c0248d8ae --- /dev/null +++ b/packages/modernjs-v3/src/server/staticMiddleware.ts @@ -0,0 +1,78 @@ +import path from 'node:path'; +import type { MiddlewareHandler } from '@modern-js/server-runtime'; +import fs from 'fs-extra'; +import { fileCache } from './fileCache'; + +const bundlesAssetPrefix = '/bundles'; +// Remove domain name from assetPrefix if it exists +// and remove trailing slash if it exists, if the url is a single slash, return it as empty string +const removeHost = (url: string): string => { + try { + // Extract pathname + const hasProtocol = url.includes('://'); + const hasDomain = hasProtocol || url.startsWith('//'); + const pathname = hasDomain + ? new URL(hasProtocol ? url : `http:${url}`).pathname + : url; + + return pathname; + } catch (e) { + return url; + } +}; + +const createStaticMiddleware = (options: { + assetPrefix: string; + pwd: string; +}): MiddlewareHandler => { + const { assetPrefix, pwd } = options; + + return async (c, next) => { + const pathname = c.req.path; + + // We only handle js file for performance + if (path.extname(pathname) !== '.js') { + return next(); + } + + const prefixWithoutHost = removeHost(assetPrefix); + const prefixWithBundle = path.join(prefixWithoutHost, bundlesAssetPrefix); + // Skip if the request is not for asset prefix + `/bundles` + if (!pathname.startsWith(prefixWithBundle)) { + return next(); + } + + const pathnameWithoutPrefix = pathname.replace(prefixWithBundle, ''); + const filepath = path.join(pwd, bundlesAssetPrefix, pathnameWithoutPrefix); + if (!(await fs.pathExists(filepath))) { + return next(); + } + + const fileResult = await fileCache.getFile(filepath); + if (!fileResult) { + return next(); + } + + c.header('Content-Type', 'application/javascript'); + c.header('Content-Length', String(fileResult.content.length)); + return c.body(fileResult.content, 200); + }; +}; + +const createCorsMiddleware = (): MiddlewareHandler => { + return async (c, next) => { + const pathname = c.req.path; + // If the request is only for a static file + if (path.extname(pathname)) { + c.header('Access-Control-Allow-Origin', '*'); + c.header( + 'Access-Control-Allow-Methods', + 'GET, POST, PUT, DELETE, PATCH, OPTIONS', + ); + c.header('Access-Control-Allow-Headers', '*'); + } + return next(); + }; +}; + +export { createStaticMiddleware, createCorsMiddleware }; diff --git a/packages/modernjs-v3/src/ssr-runtime/SSRLiveReload.tsx b/packages/modernjs-v3/src/ssr-runtime/SSRLiveReload.tsx new file mode 100644 index 000000000000..76f65b356554 --- /dev/null +++ b/packages/modernjs-v3/src/ssr-runtime/SSRLiveReload.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +export function SSRLiveReload() { + if (process.env.NODE_ENV !== 'development') { + return null; + } + return ( +