diff --git a/.changeset/tidy-bananas-relax.md b/.changeset/tidy-bananas-relax.md new file mode 100644 index 00000000000..72deb0db02c --- /dev/null +++ b/.changeset/tidy-bananas-relax.md @@ -0,0 +1,6 @@ +--- +'@module-federation/modern-js-v3': patch +'@module-federation/sdk': patch +--- + +Improve Node-side async startup remote loading by stripping browser HMR bootstrap calls, normalizing async startup container exports, and resolving var/global container shapes reliably for RSC federation flows. diff --git a/apps/modernjs-rsc/README.md b/apps/modernjs-rsc/README.md new file mode 100644 index 00000000000..19a2f8fd55a --- /dev/null +++ b/apps/modernjs-rsc/README.md @@ -0,0 +1,16 @@ +# modernjs-rsc + +Minimal ModernJS + Module Federation RSC example in `apps/`. + +## Run + +```bash +pnpm nx run modernjs-rsc:serve +``` + +Open `http://localhost:3060`. + +This app enables `experiments.rsc` + `experiments.asyncStartup` and exposes: + +- `modernjs_rsc/ServerMessage` +- `modernjs_rsc/ClientCounter` diff --git a/apps/modernjs-rsc/modern.config.ts b/apps/modernjs-rsc/modern.config.ts new file mode 100644 index 00000000000..ddee2a9e488 --- /dev/null +++ b/apps/modernjs-rsc/modern.config.ts @@ -0,0 +1,13 @@ +import { appTools, defineConfig } from '@modern-js/app-tools'; +import { moduleFederationPlugin } from '@module-federation/modern-js-v3'; + +export default defineConfig({ + server: { + port: 3060, + ssr: { + mode: 'stream', + }, + rsc: true, + }, + plugins: [appTools(), moduleFederationPlugin()], +}); diff --git a/apps/modernjs-rsc/module-federation.config.ts b/apps/modernjs-rsc/module-federation.config.ts new file mode 100644 index 00000000000..2db6d421faf --- /dev/null +++ b/apps/modernjs-rsc/module-federation.config.ts @@ -0,0 +1,21 @@ +import { createModuleFederationConfig } from '@module-federation/modern-js-v3'; + +export default createModuleFederationConfig({ + name: 'modernjs_rsc', + manifest: { + filePath: 'static', + }, + filename: 'static/remoteEntry.js', + exposes: { + './ServerMessage': './src/server/ServerMessage.tsx', + './ClientCounter': './src/client/ClientCounter.tsx', + }, + shared: { + react: { singleton: true }, + 'react-dom': { singleton: true }, + }, + experiments: { + asyncStartup: true, + rsc: true, + }, +}); diff --git a/apps/modernjs-rsc/package.json b/apps/modernjs-rsc/package.json new file mode 100644 index 00000000000..81995f84530 --- /dev/null +++ b/apps/modernjs-rsc/package.json @@ -0,0 +1,30 @@ +{ + "name": "modernjs-rsc", + "private": true, + "version": "0.1.0", + "scripts": { + "dev": "modern dev", + "build": "modern build", + "start": "modern start", + "serve": "modern serve", + "lint": "modern lint" + }, + "engines": { + "node": ">=16.18.1" + }, + "dependencies": { + "@babel/runtime": "7.28.2", + "@modern-js/runtime": "3.0.1", + "@module-federation/modern-js-v3": "workspace:*", + "react": "~18.3.1", + "react-dom": "~18.3.1" + }, + "devDependencies": { + "@modern-js/app-tools": "3.0.1", + "@modern-js/tsconfig": "3.0.1", + "@types/node": "^20.19.5", + "@types/react": "~18.2.0", + "@types/react-dom": "~18.3.0", + "typescript": "~5.0.4" + } +} diff --git a/apps/modernjs-rsc/project.json b/apps/modernjs-rsc/project.json new file mode 100644 index 00000000000..8277076bc65 --- /dev/null +++ b/apps/modernjs-rsc/project.json @@ -0,0 +1,44 @@ +{ + "name": "modernjs-rsc", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "apps/modernjs-rsc/src", + "projectType": "application", + "tags": [], + "implicitDependencies": ["typescript"], + "targets": { + "build": { + "executor": "nx:run-commands", + "options": { + "dependsOn": [ + { + "target": "build", + "dependencies": true + } + ], + "commands": [ + { + "command": "cd apps/modernjs-rsc; pnpm run build", + "forwardAllArgs": true + } + ] + } + }, + "serve": { + "executor": "nx:run-commands", + "options": { + "dependsOn": [ + { + "target": "build", + "dependencies": true + } + ], + "commands": [ + { + "command": "cd apps/modernjs-rsc; pnpm run dev", + "forwardAllArgs": false + } + ] + } + } + } +} diff --git a/apps/modernjs-rsc/src/client/ClientCounter.tsx b/apps/modernjs-rsc/src/client/ClientCounter.tsx new file mode 100644 index 00000000000..97e4871f519 --- /dev/null +++ b/apps/modernjs-rsc/src/client/ClientCounter.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { useState } from 'react'; + +const ClientCounter = () => { + const [count, setCount] = useState(0); + + return ( +
+

Client Component

+ +
+ ); +}; + +export default ClientCounter; diff --git a/apps/modernjs-rsc/src/modern-app-env.d.ts b/apps/modernjs-rsc/src/modern-app-env.d.ts new file mode 100644 index 00000000000..ca35b059576 --- /dev/null +++ b/apps/modernjs-rsc/src/modern-app-env.d.ts @@ -0,0 +1,4 @@ +/// +/// +/// +/// diff --git a/apps/modernjs-rsc/src/routes/page.tsx b/apps/modernjs-rsc/src/routes/page.tsx new file mode 100644 index 00000000000..92154d7b644 --- /dev/null +++ b/apps/modernjs-rsc/src/routes/page.tsx @@ -0,0 +1,15 @@ +import ClientCounter from '../client/ClientCounter'; +import ServerMessage from '../server/ServerMessage'; + +const Page = async () => { + return ( +
+

ModernJS RSC + Module Federation

+

This example exposes both server and client components.

+ + +
+ ); +}; + +export default Page; diff --git a/apps/modernjs-rsc/src/server/ServerMessage.tsx b/apps/modernjs-rsc/src/server/ServerMessage.tsx new file mode 100644 index 00000000000..7f391cc920a --- /dev/null +++ b/apps/modernjs-rsc/src/server/ServerMessage.tsx @@ -0,0 +1,18 @@ +const wait = (ms: number) => + new Promise((resolve) => { + setTimeout(resolve, ms); + }); + +const ServerMessage = async () => { + await wait(20); + const renderedAt = new Date().toISOString(); + + return ( +
+

Server Component

+

Rendered at: {renderedAt}

+
+ ); +}; + +export default ServerMessage; diff --git a/apps/modernjs-rsc/tsconfig.app.json b/apps/modernjs-rsc/tsconfig.app.json new file mode 100644 index 00000000000..c7c9676cbdd --- /dev/null +++ b/apps/modernjs-rsc/tsconfig.app.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["node", "express"], + "target": "ES2015", + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], + "include": ["src/**/*.ts", "src/**/*.tsx"] +} diff --git a/apps/modernjs-rsc/tsconfig.json b/apps/modernjs-rsc/tsconfig.json new file mode 100644 index 00000000000..8ea6df7ff72 --- /dev/null +++ b/apps/modernjs-rsc/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "@modern-js/tsconfig/base", + "compilerOptions": { + "declaration": false, + "jsx": "preserve", + "baseUrl": "./", + "paths": { + "@/*": ["./src/*"], + "*": ["./@mf-types/*"] + } + }, + "include": ["src", "config", "modern.config.ts", "./@mf-types"], + "exclude": ["**/node_modules"] +} diff --git a/packages/bridge/bridge-react/package.json b/packages/bridge/bridge-react/package.json index afc119f7641..7549e0e3045 100644 --- a/packages/bridge/bridge-react/package.json +++ b/packages/bridge/bridge-react/package.json @@ -19,7 +19,8 @@ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.es.js", - "require": "./dist/index.cjs.js" + "require": "./dist/index.cjs.js", + "default": "./dist/index.es.js" }, "./base": { "types": "./dist/base.d.ts", @@ -64,17 +65,20 @@ "./lazy-utils": { "types": "./dist/lazy-utils.d.ts", "import": "./dist/lazy-utils.es.js", - "require": "./dist/lazy-utils.cjs.js" + "require": "./dist/lazy-utils.cjs.js", + "default": "./dist/lazy-utils.es.js" }, "./data-fetch-utils": { "types": "./dist/data-fetch-utils.d.ts", "import": "./dist/data-fetch-utils.es.js", - "require": "./dist/data-fetch-utils.cjs.js" + "require": "./dist/data-fetch-utils.cjs.js", + "default": "./dist/data-fetch-utils.es.js" }, "./data-fetch-server-middleware": { "types": "./dist/data-fetch-server-middleware.d.ts", "import": "./dist/data-fetch-server-middleware.es.js", - "require": "./dist/data-fetch-server-middleware.cjs.js" + "require": "./dist/data-fetch-server-middleware.cjs.js", + "default": "./dist/data-fetch-server-middleware.es.js" }, "./lazy-load-component-plugin": { "types": "./dist/lazy-load-component-plugin.d.ts", diff --git a/packages/dts-plugin/src/core/lib/typeScriptCompiler.ts b/packages/dts-plugin/src/core/lib/typeScriptCompiler.ts index 6b38a49d8e2..777841c9206 100644 --- a/packages/dts-plugin/src/core/lib/typeScriptCompiler.ts +++ b/packages/dts-plugin/src/core/lib/typeScriptCompiler.ts @@ -1,5 +1,4 @@ -import fse from 'fs-extra'; -const { ensureDirSync, writeFileSync, existsSync } = fse; +import fsExtra from 'fs-extra'; import crypto from 'crypto'; import { stat, readdir, writeFile, rm, readFile } from 'fs/promises'; import { @@ -24,6 +23,7 @@ import { TsConfigJson } from '../interfaces/TsConfigJson'; import { logger } from '../../server'; const STARTS_WITH_SLASH = /^\//; +const { ensureDirSync, writeFileSync, existsSync } = fsExtra; const DEFINITION_FILE_EXTENSION = '.d.ts'; diff --git a/packages/enhanced/src/schemas/container/ContainerPlugin.check.ts b/packages/enhanced/src/schemas/container/ContainerPlugin.check.ts index c812fc30d24..46ec1a6789d 100644 --- a/packages/enhanced/src/schemas/container/ContainerPlugin.check.ts +++ b/packages/enhanced/src/schemas/container/ContainerPlugin.check.ts @@ -44,6 +44,7 @@ const e = { ], }, name: { type: 'string' }, + layer: { type: 'string', minLength: 1 }, }, required: ['import'], }, @@ -233,7 +234,7 @@ function a( { const r = l; for (const e in t) - if ('import' !== e && 'name' !== e) + if ('import' !== e && 'name' !== e && 'layer' !== e) return ((a.errors = [{ params: { additionalProperty: e } }]), !1); if (r === l) { if (void 0 !== t.import) { @@ -241,7 +242,7 @@ function a( const n = l, m = l; let u = !1; - const c = l; + const y = l; if (l == l) if ('string' == typeof r) { if (r.length < 1) { @@ -252,7 +253,7 @@ function a( const t = { params: { type: 'string' } }; (null === i ? (i = [t]) : i.push(t), l++); } - var p = c === l; + var p = y === l; if (((u = u || p), !u)) { const n = l; (s(r, { @@ -278,13 +279,25 @@ function a( ((l = m), null !== i && (m ? (i.length = m) : (i = null))); var f = n === l; } else f = !0; - if (f) + if (f) { if (void 0 !== t.name) { const e = l; if ('string' != typeof t.name) return ((a.errors = [{ params: { type: 'string' } }]), !1); f = e === l; } else f = !0; + if (f) + if (void 0 !== t.layer) { + let e = t.layer; + const r = l; + if (l === r) { + if ('string' != typeof e) + return ((a.errors = [{ params: { type: 'string' } }]), !1); + if (e.length < 1) return ((a.errors = [{ params: {} }]), !1); + } + f = r === l; + } else f = !0; + } } } } @@ -309,16 +322,16 @@ function o( let n = t[r]; const m = p, u = p; - let c = !1; - const y = p; + let y = !1; + const c = p; a(n, { instancePath: e + '/' + r.replace(/~/g, '~0').replace(/\//g, '~1'), parentData: t, parentDataProperty: r, rootData: i, }) || ((l = null === l ? a.errors : l.concat(a.errors)), (p = l.length)); - var f = y === p; - if (((c = c || f), !c)) { + var f = c === p; + if (((y = y || f), !y)) { const a = p; if (p == p) if ('string' == typeof n) { @@ -330,7 +343,7 @@ function o( const t = { params: { type: 'string' } }; (null === l ? (l = [t]) : l.push(t), p++); } - if (((f = a === p), (c = c || f), !c)) { + if (((f = a === p), (y = y || f), !y)) { const a = p; (s(n, { instancePath: e + '/' + r.replace(/~/g, '~0').replace(/\//g, '~1'), @@ -340,10 +353,10 @@ function o( }) || ((l = null === l ? s.errors : l.concat(s.errors)), (p = l.length)), (f = a === p), - (c = c || f)); + (y = y || f)); } } - if (!c) { + if (!y) { const t = { params: {} }; return (null === l ? (l = [t]) : l.push(t), p++, (o.errors = l), !1); } @@ -410,8 +423,8 @@ function i( const t = { params: { type: 'array' } }; (null === a ? (a = [t]) : a.push(t), l++); } - var c = m === l; - if (((f = f || c), !f)) { + var y = m === l; + if (((f = f || y), !f)) { const i = l; (o(t, { instancePath: e, @@ -419,8 +432,8 @@ function i( parentDataProperty: n, rootData: s, }) || ((a = null === a ? o.errors : a.concat(o.errors)), (l = a.length)), - (c = i === l), - (f = f || c)); + (y = i === l), + (f = f || y)); } if (!f) { const t = { params: {} }; @@ -677,8 +690,8 @@ function f( const t = { params: { type: 'array' } }; (null === a ? (a = [t]) : a.push(t), o++); } - var c = i === o; - if (((s = s || c), !s)) { + var y = i === o; + if (((s = s || y), !s)) { const t = o; if (o === t) if ('string' == typeof e) { @@ -690,7 +703,7 @@ function f( const t = { params: { type: 'string' } }; (null === a ? (a = [t]) : a.push(t), o++); } - ((c = t === o), (s = s || c)); + ((y = t === o), (s = s || y)); } if (s) ((o = n), null !== a && (n ? (a.length = n) : (a = null))); @@ -874,14 +887,14 @@ function m( const t = { params: { allowedValues: l.anyOf[0].enum } }; (null === a ? (a = [t]) : a.push(t), o++); } - var c = p === o; - if (((s = s || c), !s)) { + var y = p === o; + if (((s = s || y), !s)) { const t = o; if ('string' != typeof e) { const t = { params: { type: 'string' } }; (null === a ? (a = [t]) : a.push(t), o++); } - ((c = t === o), (s = s || c)); + ((y = t === o), (s = s || y)); } if (!s) { const t = { params: {} }; @@ -926,8 +939,8 @@ function u( } = {}, ) { let f = null, - c = 0; - if (0 === c) { + y = 0; + if (0 === y) { if (!s || 'object' != typeof s || Array.isArray(s)) return ((u.errors = [{ params: { type: 'object' } }]), !1); { @@ -938,13 +951,13 @@ function u( ) return ((u.errors = [{ params: { missingProperty: o } }]), !1); { - const o = c; + const o = y; for (const t in s) if (!n.call(e.properties, t)) return ((u.errors = [{ params: { additionalProperty: t } }]), !1); - if (o === c) { + if (o === y) { if (void 0 !== s.exposes) { - const t = c; + const t = y; i(s.exposes, { instancePath: a + '/exposes', parentData: s, @@ -952,25 +965,25 @@ function u( rootData: p, }) || ((f = null === f ? i.errors : f.concat(i.errors)), - (c = f.length)); - var y = t === c; - } else y = !0; - if (y) { + (y = f.length)); + var c = t === y; + } else c = !0; + if (c) { if (void 0 !== s.filename) { let e = s.filename; - const r = c; - if (c === r) { + const r = y; + if (y === r) { if ('string' != typeof e) return ((u.errors = [{ params: { type: 'string' } }]), !1); if (e.length < 1) return ((u.errors = [{ params: {} }]), !1); if (e.includes('!') || !1 !== t.test(e)) return ((u.errors = [{ params: {} }]), !1); } - y = r === c; - } else y = !0; - if (y) { + c = r === y; + } else c = !0; + if (c) { if (void 0 !== s.library) { - const t = c; + const t = y; (m(s.library, { instancePath: a + '/library', parentData: s, @@ -978,14 +991,14 @@ function u( rootData: p, }) || ((f = null === f ? m.errors : f.concat(m.errors)), - (c = f.length)), - (y = t === c)); - } else y = !0; - if (y) { + (y = f.length)), + (c = t === y)); + } else c = !0; + if (c) { if (void 0 !== s.name) { let t = s.name; - const e = c; - if (c === e) { + const e = y; + if (y === e) { if ('string' != typeof t) return ( (u.errors = [{ params: { type: 'string' } }]), @@ -994,116 +1007,116 @@ function u( if (t.length < 1) return ((u.errors = [{ params: {} }]), !1); } - y = e === c; - } else y = !0; - if (y) { + c = e === y; + } else c = !0; + if (c) { if (void 0 !== s.runtime) { let t = s.runtime; - const e = c, - n = c; + const e = y, + n = y; let a = !1; - const o = c; + const o = y; if (!1 !== t) { const t = { params: { allowedValues: r.anyOf[0].enum } }; - (null === f ? (f = [t]) : f.push(t), c++); + (null === f ? (f = [t]) : f.push(t), y++); } - var g = o === c; + var g = o === y; if (((a = a || g), !a)) { - const e = c; - if (c === e) + const e = y; + if (y === e) if ('string' == typeof t) { if (t.length < 1) { const t = { params: {} }; - (null === f ? (f = [t]) : f.push(t), c++); + (null === f ? (f = [t]) : f.push(t), y++); } } else { const t = { params: { type: 'string' } }; - (null === f ? (f = [t]) : f.push(t), c++); + (null === f ? (f = [t]) : f.push(t), y++); } - ((g = e === c), (a = a || g)); + ((g = e === y), (a = a || g)); } if (!a) { const t = { params: {} }; return ( null === f ? (f = [t]) : f.push(t), - c++, + y++, (u.errors = f), !1 ); } - ((c = n), + ((y = n), null !== f && (n ? (f.length = n) : (f = null)), - (y = e === c)); - } else y = !0; - if (y) { + (c = e === y)); + } else c = !0; + if (c) { if (void 0 !== s.shareScope) { let t = s.shareScope; - const e = c, - r = c; + const e = y, + r = y; let n = !1; - const a = c; - if (c === a) + const a = y; + if (y === a) if ('string' == typeof t) { if (t.length < 1) { const t = { params: {} }; - (null === f ? (f = [t]) : f.push(t), c++); + (null === f ? (f = [t]) : f.push(t), y++); } } else { const t = { params: { type: 'string' } }; - (null === f ? (f = [t]) : f.push(t), c++); + (null === f ? (f = [t]) : f.push(t), y++); } - var h = a === c; + var h = a === y; if (((n = n || h), !n)) { - const e = c; - if (c === e) + const e = y; + if (y === e) if (Array.isArray(t)) { const e = t.length; for (let r = 0; r < e; r++) { let e = t[r]; - const n = c; - if (c === n) + const n = y; + if (y === n) if ('string' == typeof e) { if (e.length < 1) { const t = { params: {} }; - (null === f ? (f = [t]) : f.push(t), c++); + (null === f ? (f = [t]) : f.push(t), y++); } } else { const t = { params: { type: 'string' } }; - (null === f ? (f = [t]) : f.push(t), c++); + (null === f ? (f = [t]) : f.push(t), y++); } - if (n !== c) break; + if (n !== y) break; } } else { const t = { params: { type: 'array' } }; - (null === f ? (f = [t]) : f.push(t), c++); + (null === f ? (f = [t]) : f.push(t), y++); } - ((h = e === c), (n = n || h)); + ((h = e === y), (n = n || h)); } if (!n) { const t = { params: {} }; return ( null === f ? (f = [t]) : f.push(t), - c++, + y++, (u.errors = f), !1 ); } - ((c = r), + ((y = r), null !== f && (r ? (f.length = r) : (f = null)), - (y = e === c)); - } else y = !0; - if (y) { + (c = e === y)); + } else c = !0; + if (c) { if (void 0 !== s.experiments) { let t = s.experiments; - const e = c; - if (c === e) { + const e = y; + if (y === e) { if (!t || 'object' != typeof t || Array.isArray(t)) return ( (u.errors = [{ params: { type: 'object' } }]), !1 ); { - const e = c; + const e = y; for (const e in t) if ( 'asyncStartup' !== e && @@ -1116,9 +1129,9 @@ function u( ]), !1 ); - if (e === c) { + if (e === y) { if (void 0 !== t.asyncStartup) { - const e = c; + const e = y; if ('boolean' != typeof t.asyncStartup) return ( (u.errors = [ @@ -1126,11 +1139,11 @@ function u( ]), !1 ); - var d = e === c; + var d = e === y; } else d = !0; if (d) { if (void 0 !== t.externalRuntime) { - const e = c; + const e = y; if ('boolean' != typeof t.externalRuntime) return ( (u.errors = [ @@ -1138,11 +1151,11 @@ function u( ]), !1 ); - d = e === c; + d = e === y; } else d = !0; if (d) if (void 0 !== t.provideExternalRuntime) { - const e = c; + const e = y; if ( 'boolean' != typeof t.provideExternalRuntime @@ -1153,29 +1166,29 @@ function u( ]), !1 ); - d = e === c; + d = e === y; } else d = !0; } } } } - y = e === c; - } else y = !0; - if (y) { + c = e === y; + } else c = !0; + if (c) { if (void 0 !== s.dataPrefetch) { - const t = c; + const t = y; if ('boolean' != typeof s.dataPrefetch) return ( (u.errors = [{ params: { type: 'boolean' } }]), !1 ); - y = t === c; - } else y = !0; - if (y) + c = t === y; + } else c = !0; + if (c) if (void 0 !== s.runtimePlugins) { let t = s.runtimePlugins; - const e = c; - if (c === e) { + const e = y; + if (y === e) { if (!Array.isArray(t)) return ( (u.errors = [{ params: { type: 'array' } }]), @@ -1185,31 +1198,31 @@ function u( const e = t.length; for (let r = 0; r < e; r++) { let e = t[r]; - const n = c, - s = c; + const n = y, + s = y; let a = !1; - const o = c; + const o = y; if ('string' != typeof e) { const t = { params: { type: 'string' } }; - (null === f ? (f = [t]) : f.push(t), c++); + (null === f ? (f = [t]) : f.push(t), y++); } - var b = o === c; + var b = o === y; if (((a = a || b), !a)) { - const t = c; - if (c === t) + const t = y; + if (y === t) if (Array.isArray(e)) if (e.length > 2) { const t = { params: { limit: 2 } }; (null === f ? (f = [t]) : f.push(t), - c++); + y++); } else if (e.length < 2) { const t = { params: { limit: 2 } }; (null === f ? (f = [t]) : f.push(t), - c++); + y++); } else { const t = e.length; if (t > 0) { - const t = c; + const t = y; if ('string' != typeof e[0]) { const t = { params: { type: 'string' }, @@ -1217,13 +1230,13 @@ function u( (null === f ? (f = [t]) : f.push(t), - c++); + y++); } - var j = t === c; + var j = t === y; } if (j && t > 1) { let t = e[1]; - const r = c; + const r = y; if ( !t || 'object' != typeof t || @@ -1235,39 +1248,39 @@ function u( (null === f ? (f = [t]) : f.push(t), - c++); + y++); } - j = r === c; + j = r === y; } } else { const t = { params: { type: 'array' } }; (null === f ? (f = [t]) : f.push(t), - c++); + y++); } - ((b = t === c), (a = a || b)); + ((b = t === y), (a = a || b)); } if (!a) { const t = { params: {} }; return ( null === f ? (f = [t]) : f.push(t), - c++, + y++, (u.errors = f), !1 ); } if ( - ((c = s), + ((y = s), null !== f && (s ? (f.length = s) : (f = null)), - n !== c) + n !== y) ) break; } } } - y = e === c; - } else y = !0; + c = e === y; + } else c = !0; } } } @@ -1279,5 +1292,5 @@ function u( } } } - return ((u.errors = f), 0 === c); + return ((u.errors = f), 0 === y); } diff --git a/packages/enhanced/src/schemas/container/ContainerPlugin.json b/packages/enhanced/src/schemas/container/ContainerPlugin.json index a5bf16d0d7b..855b73ab2e0 100644 --- a/packages/enhanced/src/schemas/container/ContainerPlugin.json +++ b/packages/enhanced/src/schemas/container/ContainerPlugin.json @@ -70,6 +70,11 @@ "name": { "description": "Custom chunk name for the exposed module.", "type": "string" + }, + "layer": { + "description": "Layer in which the exposed module should be placed.", + "type": "string", + "minLength": 1 } }, "required": ["import"] diff --git a/packages/enhanced/src/schemas/container/ContainerPlugin.ts b/packages/enhanced/src/schemas/container/ContainerPlugin.ts index 6f86e60d6e2..434d3d1ac30 100644 --- a/packages/enhanced/src/schemas/container/ContainerPlugin.ts +++ b/packages/enhanced/src/schemas/container/ContainerPlugin.ts @@ -6,375 +6,370 @@ */ export default { - definitions: { - AmdContainer: { - description: - 'Add a container for define/require functions in the AMD module.', - type: 'string', - minLength: 1, + "definitions": { + "AmdContainer": { + "description": "Add a container for define/require functions in the AMD module.", + "type": "string", + "minLength": 1 }, - AuxiliaryComment: { - description: 'Add a comment in the UMD wrapper.', - anyOf: [ + "AuxiliaryComment": { + "description": "Add a comment in the UMD wrapper.", + "anyOf": [ { - description: 'Append the same comment above each import style.', - type: 'string', + "description": "Append the same comment above each import style.", + "type": "string" }, { - $ref: '#/definitions/LibraryCustomUmdCommentObject', - }, - ], + "$ref": "#/definitions/LibraryCustomUmdCommentObject" + } + ] }, - EntryRuntime: { - description: - 'The name of the runtime chunk. If set a runtime chunk with this name is created or an existing entrypoint is used as runtime.', - anyOf: [ + "EntryRuntime": { + "description": "The name of the runtime chunk. If set a runtime chunk with this name is created or an existing entrypoint is used as runtime.", + "anyOf": [ { - enum: [false], + "enum": [ + false + ] }, { - type: 'string', - minLength: 1, - }, - ], + "type": "string", + "minLength": 1 + } + ] }, - Exposes: { - description: - 'Modules that should be exposed by this container. When provided, property name is used as public name, otherwise public name is automatically inferred from request.', - anyOf: [ + "Exposes": { + "description": "Modules that should be exposed by this container. When provided, property name is used as public name, otherwise public name is automatically inferred from request.", + "anyOf": [ { - type: 'array', - items: { - description: 'Modules that should be exposed by this container.', - anyOf: [ + "type": "array", + "items": { + "description": "Modules that should be exposed by this container.", + "anyOf": [ { - $ref: '#/definitions/ExposesItem', + "$ref": "#/definitions/ExposesItem" }, { - $ref: '#/definitions/ExposesObject', - }, - ], - }, + "$ref": "#/definitions/ExposesObject" + } + ] + } }, { - $ref: '#/definitions/ExposesObject', - }, - ], + "$ref": "#/definitions/ExposesObject" + } + ] }, - ExposesConfig: { - description: - 'Advanced configuration for modules that should be exposed by this container.', - type: 'object', - additionalProperties: false, - properties: { - import: { - description: - 'Request to a module that should be exposed by this container.', - anyOf: [ + "ExposesConfig": { + "description": "Advanced configuration for modules that should be exposed by this container.", + "type": "object", + "additionalProperties": false, + "properties": { + "import": { + "description": "Request to a module that should be exposed by this container.", + "anyOf": [ { - $ref: '#/definitions/ExposesItem', + "$ref": "#/definitions/ExposesItem" }, { - $ref: '#/definitions/ExposesItems', - }, - ], - }, - name: { - description: 'Custom chunk name for the exposed module.', - type: 'string', - }, + "$ref": "#/definitions/ExposesItems" + } + ] + }, + "name": { + "description": "Custom chunk name for the exposed module.", + "type": "string" + }, + "layer": { + "description": "Layer in which the exposed module should be placed.", + "type": "string", + "minLength": 1 + } }, - required: ['import'], + "required": [ + "import" + ] }, - ExposesItem: { - description: 'Module that should be exposed by this container.', - type: 'string', - minLength: 1, + "ExposesItem": { + "description": "Module that should be exposed by this container.", + "type": "string", + "minLength": 1 }, - ExposesItems: { - description: 'Modules that should be exposed by this container.', - type: 'array', - items: { - $ref: '#/definitions/ExposesItem', - }, + "ExposesItems": { + "description": "Modules that should be exposed by this container.", + "type": "array", + "items": { + "$ref": "#/definitions/ExposesItem" + } }, - ExposesObject: { - description: - 'Modules that should be exposed by this container. Property names are used as public paths.', - type: 'object', - additionalProperties: { - description: 'Modules that should be exposed by this container.', - anyOf: [ + "ExposesObject": { + "description": "Modules that should be exposed by this container. Property names are used as public paths.", + "type": "object", + "additionalProperties": { + "description": "Modules that should be exposed by this container.", + "anyOf": [ { - $ref: '#/definitions/ExposesConfig', + "$ref": "#/definitions/ExposesConfig" }, { - $ref: '#/definitions/ExposesItem', + "$ref": "#/definitions/ExposesItem" }, { - $ref: '#/definitions/ExposesItems', - }, - ], - }, + "$ref": "#/definitions/ExposesItems" + } + ] + } }, - LibraryCustomUmdCommentObject: { - description: - 'Set explicit comments for `commonjs`, `commonjs2`, `amd`, and `root`.', - type: 'object', - additionalProperties: false, - properties: { - amd: { - description: 'Set comment for `amd` section in UMD.', - type: 'string', - }, - commonjs: { - description: 'Set comment for `commonjs` (exports) section in UMD.', - type: 'string', - }, - commonjs2: { - description: - 'Set comment for `commonjs2` (module.exports) section in UMD.', - type: 'string', - }, - root: { - description: - 'Set comment for `root` (global variable) section in UMD.', - type: 'string', - }, - }, + "LibraryCustomUmdCommentObject": { + "description": "Set explicit comments for `commonjs`, `commonjs2`, `amd`, and `root`.", + "type": "object", + "additionalProperties": false, + "properties": { + "amd": { + "description": "Set comment for `amd` section in UMD.", + "type": "string" + }, + "commonjs": { + "description": "Set comment for `commonjs` (exports) section in UMD.", + "type": "string" + }, + "commonjs2": { + "description": "Set comment for `commonjs2` (module.exports) section in UMD.", + "type": "string" + }, + "root": { + "description": "Set comment for `root` (global variable) section in UMD.", + "type": "string" + } + } }, - LibraryCustomUmdObject: { - description: - 'Description object for all UMD variants of the library name.', - type: 'object', - additionalProperties: false, - properties: { - amd: { - description: 'Name of the exposed AMD library in the UMD.', - type: 'string', - minLength: 1, - }, - commonjs: { - description: 'Name of the exposed commonjs export in the UMD.', - type: 'string', - minLength: 1, - }, - root: { - description: - 'Name of the property exposed globally by a UMD library.', - anyOf: [ + "LibraryCustomUmdObject": { + "description": "Description object for all UMD variants of the library name.", + "type": "object", + "additionalProperties": false, + "properties": { + "amd": { + "description": "Name of the exposed AMD library in the UMD.", + "type": "string", + "minLength": 1 + }, + "commonjs": { + "description": "Name of the exposed commonjs export in the UMD.", + "type": "string", + "minLength": 1 + }, + "root": { + "description": "Name of the property exposed globally by a UMD library.", + "anyOf": [ { - type: 'array', - items: { - description: - 'Part of the name of the property exposed globally by a UMD library.', - type: 'string', - minLength: 1, - }, + "type": "array", + "items": { + "description": "Part of the name of the property exposed globally by a UMD library.", + "type": "string", + "minLength": 1 + } }, { - type: 'string', - minLength: 1, - }, - ], - }, - }, + "type": "string", + "minLength": 1 + } + ] + } + } }, - LibraryExport: { - description: 'Specify which export should be exposed as library.', - anyOf: [ + "LibraryExport": { + "description": "Specify which export should be exposed as library.", + "anyOf": [ { - type: 'array', - items: { - description: - 'Part of the export that should be exposed as library.', - type: 'string', - minLength: 1, - }, + "type": "array", + "items": { + "description": "Part of the export that should be exposed as library.", + "type": "string", + "minLength": 1 + } }, { - type: 'string', - minLength: 1, - }, - ], + "type": "string", + "minLength": 1 + } + ] }, - LibraryName: { - description: - 'The name of the library (some types allow unnamed libraries too).', - anyOf: [ + "LibraryName": { + "description": "The name of the library (some types allow unnamed libraries too).", + "anyOf": [ { - type: 'array', - items: { - description: 'A part of the library name.', - type: 'string', - minLength: 1, + "type": "array", + "items": { + "description": "A part of the library name.", + "type": "string", + "minLength": 1 }, - minItems: 1, + "minItems": 1 }, { - type: 'string', - minLength: 1, + "type": "string", + "minLength": 1 }, { - $ref: '#/definitions/LibraryCustomUmdObject', - }, - ], + "$ref": "#/definitions/LibraryCustomUmdObject" + } + ] }, - LibraryOptions: { - description: 'Options for library.', - type: 'object', - additionalProperties: false, - properties: { - amdContainer: { - $ref: '#/definitions/AmdContainer', - }, - auxiliaryComment: { - $ref: '#/definitions/AuxiliaryComment', + "LibraryOptions": { + "description": "Options for library.", + "type": "object", + "additionalProperties": false, + "properties": { + "amdContainer": { + "$ref": "#/definitions/AmdContainer" }, - export: { - $ref: '#/definitions/LibraryExport', + "auxiliaryComment": { + "$ref": "#/definitions/AuxiliaryComment" }, - name: { - $ref: '#/definitions/LibraryName', + "export": { + "$ref": "#/definitions/LibraryExport" }, - type: { - $ref: '#/definitions/LibraryType', + "name": { + "$ref": "#/definitions/LibraryName" }, - umdNamedDefine: { - $ref: '#/definitions/UmdNamedDefine', + "type": { + "$ref": "#/definitions/LibraryType" }, + "umdNamedDefine": { + "$ref": "#/definitions/UmdNamedDefine" + } }, - required: ['type'], + "required": [ + "type" + ] }, - LibraryType: { - description: - "Type of library (types included by default are 'var', 'module', 'assign', 'assign-properties', 'this', 'window', 'self', 'global', 'commonjs', 'commonjs2', 'commonjs-module', 'commonjs-static', 'amd', 'amd-require', 'umd', 'umd2', 'jsonp', 'system', but others might be added by plugins).", - anyOf: [ + "LibraryType": { + "description": "Type of library (types included by default are 'var', 'module', 'assign', 'assign-properties', 'this', 'window', 'self', 'global', 'commonjs', 'commonjs2', 'commonjs-module', 'commonjs-static', 'amd', 'amd-require', 'umd', 'umd2', 'jsonp', 'system', but others might be added by plugins).", + "anyOf": [ { - enum: [ - 'var', - 'module', - 'assign', - 'assign-properties', - 'this', - 'window', - 'self', - 'global', - 'commonjs', - 'commonjs2', - 'commonjs-module', - 'commonjs-static', - 'amd', - 'amd-require', - 'umd', - 'umd2', - 'jsonp', - 'system', - ], + "enum": [ + "var", + "module", + "assign", + "assign-properties", + "this", + "window", + "self", + "global", + "commonjs", + "commonjs2", + "commonjs-module", + "commonjs-static", + "amd", + "amd-require", + "umd", + "umd2", + "jsonp", + "system" + ] }, { - type: 'string', - }, - ], - }, - UmdNamedDefine: { - description: - 'If `output.libraryTarget` is set to umd and `output.library` is set, setting this to true will name the AMD module.', - type: 'boolean', + "type": "string" + } + ] }, + "UmdNamedDefine": { + "description": "If `output.libraryTarget` is set to umd and `output.library` is set, setting this to true will name the AMD module.", + "type": "boolean" + } }, - title: 'ContainerPluginOptions', - type: 'object', - additionalProperties: false, - properties: { - exposes: { - $ref: '#/definitions/Exposes', + "title": "ContainerPluginOptions", + "type": "object", + "additionalProperties": false, + "properties": { + "exposes": { + "$ref": "#/definitions/Exposes" }, - filename: { - description: - 'The filename for this container relative path inside the `output.path` directory.', - type: 'string', - absolutePath: false, - minLength: 1, + "filename": { + "description": "The filename for this container relative path inside the `output.path` directory.", + "type": "string", + "absolutePath": false, + "minLength": 1 }, - library: { - $ref: '#/definitions/LibraryOptions', + "library": { + "$ref": "#/definitions/LibraryOptions" }, - name: { - description: 'The name for this container.', - type: 'string', - minLength: 1, + "name": { + "description": "The name for this container.", + "type": "string", + "minLength": 1 }, - runtime: { - $ref: '#/definitions/EntryRuntime', + "runtime": { + "$ref": "#/definitions/EntryRuntime" }, - shareScope: { - description: - "The name of the share scope which is shared with the host (defaults to 'default').", - anyOf: [ + "shareScope": { + "description": "The name of the share scope which is shared with the host (defaults to 'default').", + "anyOf": [ { - type: 'string', - minLength: 1, + "type": "string", + "minLength": 1 }, { - type: 'array', - items: { - type: 'string', - minLength: 1, - }, - }, - ], + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + ] }, - experiments: { - description: 'Experimental features configuration', - type: 'object', - additionalProperties: false, - properties: { - asyncStartup: { - description: 'Enable async startup for the container', - type: 'boolean', - }, - externalRuntime: { - description: - 'After setting true, the external MF runtime will be used and the runtime provided by the consumer will be used. (Please make sure your consumer has provideExternalRuntime: true set, otherwise it will not run properly!)', - type: 'boolean', - default: false, - }, - provideExternalRuntime: { - description: 'Enable providing external runtime', - type: 'boolean', - default: false, - }, - }, + "experiments": { + "description": "Experimental features configuration", + "type": "object", + "additionalProperties": false, + "properties": { + "asyncStartup": { + "description": "Enable async startup for the container", + "type": "boolean" + }, + "externalRuntime": { + "description": "After setting true, the external MF runtime will be used and the runtime provided by the consumer will be used. (Please make sure your consumer has provideExternalRuntime: true set, otherwise it will not run properly!)", + "type": "boolean", + "default": false + }, + "provideExternalRuntime": { + "description": "Enable providing external runtime", + "type": "boolean", + "default": false + } + } }, - dataPrefetch: { - description: 'Enable data prefetching for container modules.', - type: 'boolean', + "dataPrefetch": { + "description": "Enable data prefetching for container modules.", + "type": "boolean" }, - runtimePlugins: { - description: 'Array of runtime plugins to be applied', - type: 'array', - items: { - anyOf: [ + "runtimePlugins": { + "description": "Array of runtime plugins to be applied", + "type": "array", + "items": { + "anyOf": [ { - type: 'string', + "type": "string" }, { - type: 'array', - items: [ + "type": "array", + "items": [ { - type: 'string', + "type": "string" }, { - type: 'object', - }, + "type": "object" + } ], - minItems: 2, - maxItems: 2, - }, - ], - }, - }, + "minItems": 2, + "maxItems": 2 + } + ] + } + } }, - required: ['name', 'exposes'], + "required": [ + "name", + "exposes" + ] } as const; diff --git a/packages/enhanced/src/schemas/container/ContainerReferencePlugin.check.ts b/packages/enhanced/src/schemas/container/ContainerReferencePlugin.check.ts index 1b7e9aee384..47ca05d74a7 100644 --- a/packages/enhanced/src/schemas/container/ContainerReferencePlugin.check.ts +++ b/packages/enhanced/src/schemas/container/ContainerReferencePlugin.check.ts @@ -4,490 +4,4 @@ * This file was automatically generated. * DO NOT MODIFY BY HAND. */ -export const validate = s; -export default s; -const r = { - enum: [ - 'var', - 'module', - 'assign', - 'this', - 'window', - 'self', - 'global', - 'commonjs', - 'commonjs2', - 'commonjs-module', - 'commonjs-static', - 'amd', - 'amd-require', - 'umd', - 'umd2', - 'jsonp', - 'system', - 'promise', - 'import', - 'module-import', - 'script', - 'node-commonjs', - ], -}; -function t( - r, - { - instancePath: e = '', - parentData: n, - parentDataProperty: a, - rootData: s = r, - } = {}, -) { - if (!Array.isArray(r)) - return ((t.errors = [{ params: { type: 'array' } }]), !1); - { - const e = r.length; - for (let n = 0; n < e; n++) { - let e = r[n]; - const a = 0; - if ('string' != typeof e) - return ((t.errors = [{ params: { type: 'string' } }]), !1); - if (e.length < 1) return ((t.errors = [{ params: {} }]), !1); - if (0 !== a) break; - } - } - return ((t.errors = null), !0); -} -function e( - r, - { - instancePath: n = '', - parentData: a, - parentDataProperty: s, - rootData: o = r, - } = {}, -) { - let l = null, - p = 0; - if (0 === p) { - if (!r || 'object' != typeof r || Array.isArray(r)) - return ((e.errors = [{ params: { type: 'object' } }]), !1); - { - let a; - if (void 0 === r.external && (a = 'external')) - return ((e.errors = [{ params: { missingProperty: a } }]), !1); - { - const a = p; - for (const t in r) - if ('external' !== t && 'shareScope' !== t) - return ((e.errors = [{ params: { additionalProperty: t } }]), !1); - if (a === p) { - if (void 0 !== r.external) { - let a = r.external; - const s = p, - u = p; - let f = !1; - const m = p; - if (p == p) - if ('string' == typeof a) { - if (a.length < 1) { - const r = { params: {} }; - (null === l ? (l = [r]) : l.push(r), p++); - } - } else { - const r = { params: { type: 'string' } }; - (null === l ? (l = [r]) : l.push(r), p++); - } - var i = m === p; - if (((f = f || i), !f)) { - const e = p; - (t(a, { - instancePath: n + '/external', - parentData: r, - parentDataProperty: 'external', - rootData: o, - }) || - ((l = null === l ? t.errors : l.concat(t.errors)), - (p = l.length)), - (i = e === p), - (f = f || i)); - } - if (!f) { - const r = { params: {} }; - return ( - null === l ? (l = [r]) : l.push(r), - p++, - (e.errors = l), - !1 - ); - } - ((p = u), null !== l && (u ? (l.length = u) : (l = null))); - var c = s === p; - } else c = !0; - if (c) - if (void 0 !== r.shareScope) { - let t = r.shareScope; - const n = p, - a = p; - let s = !1; - const o = p; - if (p === o) - if ('string' == typeof t) { - if (t.length < 1) { - const r = { params: {} }; - (null === l ? (l = [r]) : l.push(r), p++); - } - } else { - const r = { params: { type: 'string' } }; - (null === l ? (l = [r]) : l.push(r), p++); - } - var u = o === p; - if (((s = s || u), !s)) { - const r = p; - if (p === r) - if (Array.isArray(t)) { - const r = t.length; - for (let e = 0; e < r; e++) { - let r = t[e]; - const n = p; - if (p === n) - if ('string' == typeof r) { - if (r.length < 1) { - const r = { params: {} }; - (null === l ? (l = [r]) : l.push(r), p++); - } - } else { - const r = { params: { type: 'string' } }; - (null === l ? (l = [r]) : l.push(r), p++); - } - if (n !== p) break; - } - } else { - const r = { params: { type: 'array' } }; - (null === l ? (l = [r]) : l.push(r), p++); - } - ((u = r === p), (s = s || u)); - } - if (!s) { - const r = { params: {} }; - return ( - null === l ? (l = [r]) : l.push(r), - p++, - (e.errors = l), - !1 - ); - } - ((p = a), - null !== l && (a ? (l.length = a) : (l = null)), - (c = n === p)); - } else c = !0; - } - } - } - } - return ((e.errors = l), 0 === p); -} -function n( - r, - { - instancePath: a = '', - parentData: s, - parentDataProperty: o, - rootData: l = r, - } = {}, -) { - let p = null, - i = 0; - if (0 === i) { - if (!r || 'object' != typeof r || Array.isArray(r)) - return ((n.errors = [{ params: { type: 'object' } }]), !1); - for (const s in r) { - let o = r[s]; - const u = i, - f = i; - let m = !1; - const y = i; - e(o, { - instancePath: a + '/' + s.replace(/~/g, '~0').replace(/\//g, '~1'), - parentData: r, - parentDataProperty: s, - rootData: l, - }) || ((p = null === p ? e.errors : p.concat(e.errors)), (i = p.length)); - var c = y === i; - if (((m = m || c), !m)) { - const e = i; - if (i == i) - if ('string' == typeof o) { - if (o.length < 1) { - const r = { params: {} }; - (null === p ? (p = [r]) : p.push(r), i++); - } - } else { - const r = { params: { type: 'string' } }; - (null === p ? (p = [r]) : p.push(r), i++); - } - if (((c = e === i), (m = m || c), !m)) { - const e = i; - (t(o, { - instancePath: a + '/' + s.replace(/~/g, '~0').replace(/\//g, '~1'), - parentData: r, - parentDataProperty: s, - rootData: l, - }) || - ((p = null === p ? t.errors : p.concat(t.errors)), (i = p.length)), - (c = e === i), - (m = m || c)); - } - } - if (!m) { - const r = { params: {} }; - return (null === p ? (p = [r]) : p.push(r), i++, (n.errors = p), !1); - } - if (((i = f), null !== p && (f ? (p.length = f) : (p = null)), u !== i)) - break; - } - } - return ((n.errors = p), 0 === i); -} -function a( - r, - { - instancePath: t = '', - parentData: e, - parentDataProperty: s, - rootData: o = r, - } = {}, -) { - let l = null, - p = 0; - const i = p; - let c = !1; - const u = p; - if (p === u) - if (Array.isArray(r)) { - const e = r.length; - for (let a = 0; a < e; a++) { - let e = r[a]; - const s = p, - i = p; - let c = !1; - const u = p; - if (p == p) - if ('string' == typeof e) { - if (e.length < 1) { - const r = { params: {} }; - (null === l ? (l = [r]) : l.push(r), p++); - } - } else { - const r = { params: { type: 'string' } }; - (null === l ? (l = [r]) : l.push(r), p++); - } - var f = u === p; - if (((c = c || f), !c)) { - const s = p; - (n(e, { - instancePath: t + '/' + a, - parentData: r, - parentDataProperty: a, - rootData: o, - }) || - ((l = null === l ? n.errors : l.concat(n.errors)), (p = l.length)), - (f = s === p), - (c = c || f)); - } - if (c) ((p = i), null !== l && (i ? (l.length = i) : (l = null))); - else { - const r = { params: {} }; - (null === l ? (l = [r]) : l.push(r), p++); - } - if (s !== p) break; - } - } else { - const r = { params: { type: 'array' } }; - (null === l ? (l = [r]) : l.push(r), p++); - } - var m = u === p; - if (((c = c || m), !c)) { - const a = p; - (n(r, { - instancePath: t, - parentData: e, - parentDataProperty: s, - rootData: o, - }) || ((l = null === l ? n.errors : l.concat(n.errors)), (p = l.length)), - (m = a === p), - (c = c || m)); - } - if (!c) { - const r = { params: {} }; - return (null === l ? (l = [r]) : l.push(r), p++, (a.errors = l), !1); - } - return ( - (p = i), - null !== l && (i ? (l.length = i) : (l = null)), - (a.errors = l), - 0 === p - ); -} -function s( - t, - { - instancePath: e = '', - parentData: n, - parentDataProperty: o, - rootData: l = t, - } = {}, -) { - let p = null, - i = 0; - if (0 === i) { - if (!t || 'object' != typeof t || Array.isArray(t)) - return ((s.errors = [{ params: { type: 'object' } }]), !1); - { - let n; - if ( - (void 0 === t.remoteType && (n = 'remoteType')) || - (void 0 === t.remotes && (n = 'remotes')) - ) - return ((s.errors = [{ params: { missingProperty: n } }]), !1); - { - const n = i; - for (const r in t) - if ( - 'async' !== r && - 'remoteType' !== r && - 'remotes' !== r && - 'shareScope' !== r - ) - return ((s.errors = [{ params: { additionalProperty: r } }]), !1); - if (n === i) { - if (void 0 !== t.async) { - const r = i; - if ('boolean' != typeof t.async) - return ((s.errors = [{ params: { type: 'boolean' } }]), !1); - var c = r === i; - } else c = !0; - if (c) { - if (void 0 !== t.remoteType) { - let e = t.remoteType; - const n = i, - a = i; - let o = !1, - l = null; - const u = i; - if ( - 'var' !== e && - 'module' !== e && - 'assign' !== e && - 'this' !== e && - 'window' !== e && - 'self' !== e && - 'global' !== e && - 'commonjs' !== e && - 'commonjs2' !== e && - 'commonjs-module' !== e && - 'commonjs-static' !== e && - 'amd' !== e && - 'amd-require' !== e && - 'umd' !== e && - 'umd2' !== e && - 'jsonp' !== e && - 'system' !== e && - 'promise' !== e && - 'import' !== e && - 'module-import' !== e && - 'script' !== e && - 'node-commonjs' !== e - ) { - const t = { params: { allowedValues: r.enum } }; - (null === p ? (p = [t]) : p.push(t), i++); - } - if ((u === i && ((o = !0), (l = 0)), !o)) { - const r = { params: { passingSchemas: l } }; - return ( - null === p ? (p = [r]) : p.push(r), - i++, - (s.errors = p), - !1 - ); - } - ((i = a), - null !== p && (a ? (p.length = a) : (p = null)), - (c = n === i)); - } else c = !0; - if (c) { - if (void 0 !== t.remotes) { - const r = i; - (a(t.remotes, { - instancePath: e + '/remotes', - parentData: t, - parentDataProperty: 'remotes', - rootData: l, - }) || - ((p = null === p ? a.errors : p.concat(a.errors)), - (i = p.length)), - (c = r === i)); - } else c = !0; - if (c) - if (void 0 !== t.shareScope) { - let r = t.shareScope; - const e = i, - n = i; - let a = !1; - const o = i; - if (i === o) - if ('string' == typeof r) { - if (r.length < 1) { - const r = { params: {} }; - (null === p ? (p = [r]) : p.push(r), i++); - } - } else { - const r = { params: { type: 'string' } }; - (null === p ? (p = [r]) : p.push(r), i++); - } - var u = o === i; - if (((a = a || u), !a)) { - const t = i; - if (i === t) - if (Array.isArray(r)) { - const t = r.length; - for (let e = 0; e < t; e++) { - let t = r[e]; - const n = i; - if (i === n) - if ('string' == typeof t) { - if (t.length < 1) { - const r = { params: {} }; - (null === p ? (p = [r]) : p.push(r), i++); - } - } else { - const r = { params: { type: 'string' } }; - (null === p ? (p = [r]) : p.push(r), i++); - } - if (n !== i) break; - } - } else { - const r = { params: { type: 'array' } }; - (null === p ? (p = [r]) : p.push(r), i++); - } - ((u = t === i), (a = a || u)); - } - if (!a) { - const r = { params: {} }; - return ( - null === p ? (p = [r]) : p.push(r), - i++, - (s.errors = p), - !1 - ); - } - ((i = n), - null !== p && (n ? (p.length = n) : (p = null)), - (c = e === i)); - } else c = !0; - } - } - } - } - } - } - return ((s.errors = p), 0 === i); -} +export const validate=s;export default s;const r={enum:["var","module","assign","this","window","self","global","commonjs","commonjs2","commonjs-module","commonjs-static","amd","amd-require","umd","umd2","jsonp","system","promise","import","module-import","script","node-commonjs"]};function t(r,{instancePath:e="",parentData:n,parentDataProperty:a,rootData:s=r}={}){if(!Array.isArray(r))return t.errors=[{params:{type:"array"}}],!1;{const e=r.length;for(let n=0;n { private _manifestModuleInfos?: ManifestModuleInfos; private _parsedOptions?: Array< - [exposeKey: string, { import: string[]; name?: string }] + [exposeKey: string, { import: string[]; name?: string; layer?: string }] >; override get enable(): boolean { @@ -54,10 +54,12 @@ class ContainerManager extends BasicPluginOptionsManager ({ import: Array.isArray(item) ? item : [item], name: generateExposeFilename(key, false), + layer: undefined, }), (item, key) => ({ import: Array.isArray(item.import) ? item.import : [item.import], name: item.name || generateExposeFilename(key, false), + layer: item.layer, }), ); @@ -97,10 +99,12 @@ class ContainerManager extends BasicPluginOptionsManager ({ import: Array.isArray(item) ? item : [item], name: generateExposeFilename(key, false), + layer: undefined, }), (item, key) => ({ import: Array.isArray(item.import) ? item.import : [item.import], name: item.name || generateExposeFilename(key, false), + layer: item.layer, }), ); return parsedOptions.reduce( @@ -185,10 +189,12 @@ class ContainerManager extends BasicPluginOptionsManager ({ import: Array.isArray(item) ? item : [item], name: undefined, + layer: undefined, }), (item) => ({ import: Array.isArray(item.import) ? item.import : [item.import], name: undefined, + layer: item.layer, }), ); diff --git a/packages/modernjs-v3/package.json b/packages/modernjs-v3/package.json index d721e95404a..d428a963fbe 100644 --- a/packages/modernjs-v3/package.json +++ b/packages/modernjs-v3/package.json @@ -74,6 +74,16 @@ "import": "./dist/esm/cli/mfRuntimePlugins/inject-node-fetch.mjs", "require": "./dist/cjs/cli/mfRuntimePlugins/inject-node-fetch.js" }, + "./rsc-bridge-runtime-plugin": { + "types": "./dist/types/cli/mfRuntimePlugins/rsc-bridge-runtime-plugin.d.ts", + "import": "./dist/esm/cli/mfRuntimePlugins/rsc-bridge-runtime-plugin.mjs", + "require": "./dist/cjs/cli/mfRuntimePlugins/rsc-bridge-runtime-plugin.js" + }, + "./rsc-bridge-expose": { + "types": "./dist/types/runtime/rsc-bridge-expose.d.ts", + "import": "./dist/esm/runtime/rsc-bridge-expose.mjs", + "require": "./dist/cjs/runtime/rsc-bridge-expose.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" @@ -115,6 +125,12 @@ "inject-node-fetch": [ "./dist/types/cli/mfRuntimePlugins/inject-node-fetch.d.ts" ], + "rsc-bridge-runtime-plugin": [ + "./dist/types/cli/mfRuntimePlugins/rsc-bridge-runtime-plugin.d.ts" + ], + "rsc-bridge-expose": [ + "./dist/types/runtime/rsc-bridge-expose.d.ts" + ], "data-fetch-server-plugin": [ "./dist/types/cli/server/data-fetch-server-plugin.d.ts" ], @@ -151,6 +167,7 @@ "@rslib/core": "0.18.5", "@rsbuild/core": "2.0.0-beta.2", "@modern-js/app-tools": "3.0.1", + "@modern-js/server-core": "3.0.1", "@modern-js/server-runtime": "3.0.1", "@modern-js/module-tools": "2.70.5", "@modern-js/runtime": "3.0.1", @@ -162,6 +179,7 @@ "vitest": "1.6.0" }, "peerDependencies": { + "@modern-js/server-core": ">=3.0.0", "react": ">=17", "react-dom": ">=17", "typescript": "^4.9.0 || ^5.0.0", diff --git a/packages/modernjs-v3/src/cli/configPlugin.contract.spec.ts b/packages/modernjs-v3/src/cli/configPlugin.contract.spec.ts new file mode 100644 index 00000000000..bf2e3b1e3a6 --- /dev/null +++ b/packages/modernjs-v3/src/cli/configPlugin.contract.spec.ts @@ -0,0 +1,14 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; + +describe('config plugin contracts', () => { + it('passes target MF config to bundler patching', () => { + const configPluginPath = path.resolve(__dirname, './configPlugin.ts'); + const source = fs.readFileSync(configPluginPath, 'utf-8'); + + expect(source).toMatch( + /patchBundlerConfig\(\{\s*chain,\s*isServer:\s*!isWeb,\s*modernjsConfig,\s*mfConfig:\s*targetMFConfig,\s*enableSSR,\s*\}\);/s, + ); + }); +}); diff --git a/packages/modernjs-v3/src/cli/configPlugin.spec.ts b/packages/modernjs-v3/src/cli/configPlugin.spec.ts index a6808b6d8c5..bec54550793 100644 --- a/packages/modernjs-v3/src/cli/configPlugin.spec.ts +++ b/packages/modernjs-v3/src/cli/configPlugin.spec.ts @@ -80,4 +80,169 @@ describe('patchMFConfig', async () => { }, }); }); + + it('patchMFConfig: rsc remotes inject runtime bridge plugin', async () => { + const patchedConfig = { + name: 'rsc-host', + remotes: { + remote: 'http://localhost:3001/remoteEntry.js', + }, + exposes: { + './Widget': './src/widget.ts', + }, + experiments: { + rsc: true, + asyncStartup: true, + }, + }; + + patchMFConfig(patchedConfig as any, true); + + expect( + (patchedConfig as any).runtimePlugins.some((runtimePlugin: any) => + /rsc-bridge-runtime-plugin\.(t|j)s$/.test( + typeof runtimePlugin === 'string' ? runtimePlugin : runtimePlugin[0], + ), + ), + ).toBe(true); + expect(patchedConfig.exposes).toBeDefined(); + const widgetExposeConfig = (patchedConfig.exposes as any)['./Widget']; + expect(widgetExposeConfig.import).toEqual( + expect.arrayContaining([ + expect.stringMatching(/rsc-client-callback-bootstrap\.(mjs|js)$/), + './src/widget.ts', + ]), + ); + expect((patchedConfig.exposes as any)['./Widget']).toMatchObject({ + layer: 'react-server-components', + }); + expect( + (patchedConfig.exposes as any)['./__rspack_rsc_bridge__'], + ).toMatchObject({ + import: expect.stringMatching(/rsc-bridge-expose\.(t|j)s$/), + layer: 'react-server-components', + }); + }); + + it('patchMFConfig: rsc client shared config avoids non-default client.browser providers', async () => { + const patchedConfig = { + name: 'rsc-host', + remotes: { + remote: 'http://localhost:3001/remoteEntry.js', + }, + experiments: { + rsc: true, + asyncStartup: true, + }, + shared: [ + { + 'react-server-dom-rspack/client.browser': { + import: 'react-server-dom-rspack/client.browser', + shareScope: 'default', + }, + }, + { + 'react-server-dom-rspack/client.browser': { + import: 'react-server-dom-rspack/client.browser', + shareScope: 'ssr', + }, + }, + { + 'react-server-dom-rspack/client.browser': { + import: 'react-server-dom-rspack/client.browser', + shareScope: 'rsc', + }, + }, + ], + }; + + patchMFConfig(patchedConfig as any, false); + + const sharedScopes = patchedConfig.shared as Array< + Record + >; + expect( + sharedScopes[0]['react-server-dom-rspack/client.browser'].import, + ).toBe('react-server-dom-rspack/client.browser'); + expect( + sharedScopes[1]['react-server-dom-rspack/client.browser'].import, + ).toBe(false); + expect( + sharedScopes[2]['react-server-dom-rspack/client.browser'].import, + ).toBe(false); + }); + + it('patchMFConfig: rsc requires asyncStartup', async () => { + const patchedConfig = { + name: 'rsc-host', + remotes: { + remote: 'http://localhost:3001/remoteEntry.js', + }, + experiments: { + rsc: true, + }, + }; + + expect(() => patchMFConfig(patchedConfig as any, true)).toThrow( + /experiments\.rsc requires experiments\.asyncStartup = true/, + ); + }); + + it('patchMFConfig: rsc exposes inject bridge expose + bootstrap without runtime bridge plugin', async () => { + const patchedConfig = { + name: 'rsc-remote', + exposes: { + './Widget': './src/widget.ts', + }, + experiments: { + rsc: true, + asyncStartup: true, + }, + }; + + patchMFConfig(patchedConfig as any, true); + + expect( + (patchedConfig as any).runtimePlugins?.some((runtimePlugin: any) => + /rsc-bridge-runtime-plugin\.(t|j)s$/.test( + typeof runtimePlugin === 'string' ? runtimePlugin : runtimePlugin[0], + ), + ), + ).toBe(false); + const widgetExposeConfig = (patchedConfig.exposes as any)['./Widget']; + expect(widgetExposeConfig.import).toEqual( + expect.arrayContaining([ + expect.stringMatching(/rsc-client-callback-bootstrap\.(mjs|js)$/), + './src/widget.ts', + ]), + ); + expect( + (patchedConfig.exposes as any)['./__rspack_rsc_bridge__'], + ).toMatchObject({ + import: expect.stringMatching(/rsc-bridge-expose\.(t|j)s$/), + layer: 'react-server-components', + }); + }); + + it('patchMFConfig: non-rsc does not inject internal bridge expose', async () => { + const patchedConfig = { + name: 'host', + remotes: { + remote: 'http://localhost:3001/remoteEntry.js', + }, + exposes: { + './Widget': './src/widget.ts', + }, + experiments: { + asyncStartup: true, + }, + }; + + patchMFConfig(patchedConfig as any, true); + + expect((patchedConfig.exposes as any)['./Widget']).toBe('./src/widget.ts'); + expect( + (patchedConfig.exposes as any)['./__rspack_rsc_bridge__'], + ).toBeUndefined(); + }); }); diff --git a/packages/modernjs-v3/src/cli/configPlugin.ts b/packages/modernjs-v3/src/cli/configPlugin.ts index f297243f0ff..2f49596bfdd 100644 --- a/packages/modernjs-v3/src/cli/configPlugin.ts +++ b/packages/modernjs-v3/src/cli/configPlugin.ts @@ -1,29 +1,30 @@ import fs from 'fs'; import path from 'path'; -import { isWebTarget, skipByTarget } from './utils'; -import { moduleFederationPlugin, encodeName } from '@module-federation/sdk'; -import { PluginOptions } from '../types'; -import { PLUGIN_IDENTIFIER } from '../constant'; import { - autoDeleteSplitChunkCacheGroups, addDataFetchExposes, + autoDeleteSplitChunkCacheGroups, } from '@module-federation/rsbuild-plugin/utils'; +import { + encodeName, + type moduleFederationPlugin, +} from '@module-federation/sdk'; +import { PLUGIN_IDENTIFIER } from '../constant'; import logger from '../logger'; +import type { PluginOptions } from '../types'; +import { isWebTarget, skipByTarget } from './utils'; import { isDev } from './utils'; -import type { InternalModernPluginOptions } from '../types'; import type { AppTools, - Rspack, 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]; @@ -83,6 +84,56 @@ const resolveNodeRecordRemoteHashPlugin = (): string => 'dist/src/recordDynamicRemoteEntryHashPlugin.js', ); +const RSC_LAYER = 'react-server-components'; +const RSC_BRIDGE_EXPOSE = './__rspack_rsc_bridge__'; +const RSC_CLIENT_BROWSER_SHARED_KEY = 'react-server-dom-rspack/client.browser'; + +const resolveFirstExistingPath = ( + candidatePaths: string[], + fallbackPath: string, +) => + candidatePaths.find((candidatePath) => fs.existsSync(candidatePath)) || + fallbackPath; + +const RSC_BRIDGE_RUNTIME_PLUGIN = resolveFirstExistingPath( + [ + path.resolve(__dirname, './mfRuntimePlugins/rsc-bridge-runtime-plugin.ts'), + path.resolve(__dirname, './mfRuntimePlugins/rsc-bridge-runtime-plugin.js'), + path.resolve( + __dirname, + '../esm/cli/mfRuntimePlugins/rsc-bridge-runtime-plugin.mjs', + ), + ], + require.resolve('@module-federation/modern-js-v3/rsc-bridge-runtime-plugin'), +); + +const RSC_BRIDGE_EXPOSE_MODULE = resolveFirstExistingPath( + [ + path.resolve(__dirname, '../runtime/rsc-bridge-expose.ts'), + path.resolve(__dirname, '../runtime/rsc-bridge-expose.js'), + path.resolve(__dirname, '../esm/runtime/rsc-bridge-expose.mjs'), + ], + require.resolve('@module-federation/modern-js-v3/rsc-bridge-expose'), +); + +const RSC_CLIENT_CALLBACK_BOOTSTRAP_MODULE = resolveFirstExistingPath( + [ + path.resolve(__dirname, '../runtime/rsc-client-callback-bootstrap.js'), + path.resolve(__dirname, '../esm/runtime/rsc-client-callback-bootstrap.mjs'), + path.resolve( + path.dirname(RSC_BRIDGE_EXPOSE_MODULE), + 'rsc-client-callback-bootstrap.js', + ), + path.resolve( + path.dirname(RSC_BRIDGE_EXPOSE_MODULE), + 'rsc-client-callback-bootstrap.mjs', + ), + ], + path.resolve(__dirname, '../runtime/rsc-client-callback-bootstrap.js'), +); + +export type ConfigType = Rspack.Configuration; + export function setEnv(enableSSR: boolean) { if (enableSSR) { process.env['MF_SSR_PRJ'] = 'true'; @@ -172,12 +223,218 @@ const patchDTSConfig = ( } }; +const hasRemotes = ( + remotes: moduleFederationPlugin.ModuleFederationPluginOptions['remotes'], +): boolean => { + if (!remotes) { + return false; + } + if (Array.isArray(remotes)) { + return remotes.length > 0; + } + if (typeof remotes === 'string') { + return (remotes as string).length > 0; + } + return Object.keys(remotes as Record).length > 0; +}; + +const hasExposes = ( + exposes: moduleFederationPlugin.ModuleFederationPluginOptions['exposes'], +) => { + if (!exposes) { + return false; + } + if (Array.isArray(exposes)) { + return exposes.length > 0; + } + return Object.keys(exposes).length > 0; +}; + +const isRscMfEnabled = ( + mfConfig: moduleFederationPlugin.ModuleFederationPluginOptions, +) => Boolean((mfConfig.experiments as { rsc?: boolean } | undefined)?.rsc); + +const normalizeExposeConfig = ( + exposeConfig: moduleFederationPlugin.ExposesObject[string], +) => { + if (typeof exposeConfig === 'string' || Array.isArray(exposeConfig)) { + return { + import: exposeConfig, + }; + } + + if ( + exposeConfig && + typeof exposeConfig === 'object' && + 'import' in exposeConfig + ) { + return { + ...(exposeConfig as unknown as Record), + import: (exposeConfig as { import: string | string[] }).import, + }; + } + + return { + import: exposeConfig as string, + }; +}; + +const setRscExposeConfig = ( + mfConfig: moduleFederationPlugin.ModuleFederationPluginOptions, +) => { + if (!mfConfig.exposes) { + return; + } + + const normalizedExposes: moduleFederationPlugin.ExposesObject = {}; + + const appendExpose = ( + exposeKey: string, + exposeConfig: moduleFederationPlugin.ExposesObject[string], + ) => { + const normalizedConfig = normalizeExposeConfig(exposeConfig); + const importList = Array.isArray(normalizedConfig.import) + ? [...normalizedConfig.import] + : [normalizedConfig.import]; + const normalizedImportList = + exposeKey === RSC_BRIDGE_EXPOSE + ? importList + : [ + RSC_CLIENT_CALLBACK_BOOTSTRAP_MODULE, + ...importList.filter( + (importPath) => + importPath !== RSC_CLIENT_CALLBACK_BOOTSTRAP_MODULE, + ), + ]; + const normalizedImport = + normalizedImportList.length === 1 + ? normalizedImportList[0] + : normalizedImportList; + normalizedExposes[exposeKey] = { + ...normalizedConfig, + import: normalizedImport, + layer: RSC_LAYER, + } as moduleFederationPlugin.ExposesConfig; + }; + + if (Array.isArray(mfConfig.exposes)) { + for (const exposeItem of mfConfig.exposes) { + if (typeof exposeItem === 'string') { + appendExpose(exposeItem, exposeItem); + continue; + } + for (const [exposeKey, exposeConfig] of Object.entries(exposeItem)) { + appendExpose(exposeKey, exposeConfig); + } + } + } else { + for (const [exposeKey, exposeConfig] of Object.entries(mfConfig.exposes)) { + appendExpose(exposeKey, exposeConfig); + } + } + + if ( + !Object.prototype.hasOwnProperty.call(normalizedExposes, RSC_BRIDGE_EXPOSE) + ) { + normalizedExposes[RSC_BRIDGE_EXPOSE] = { + import: RSC_BRIDGE_EXPOSE_MODULE, + layer: RSC_LAYER, + } as moduleFederationPlugin.ExposesConfig; + } + + mfConfig.exposes = normalizedExposes; +}; + +const assertRscMfConfig = ({ + mfConfig, + isServer, + runtimePlugins, +}: { + mfConfig: moduleFederationPlugin.ModuleFederationPluginOptions; + isServer: boolean; + runtimePlugins: RuntimePluginEntry[]; +}) => { + if (!isRscMfEnabled(mfConfig)) { + return; + } + + const asyncStartupEnabled = + (mfConfig.experiments as { asyncStartup?: boolean } | undefined) + ?.asyncStartup === true; + if (!asyncStartupEnabled) { + throw new Error( + `${PLUGIN_IDENTIFIER} experiments.rsc requires experiments.asyncStartup = true`, + ); + } + + if (!isServer) { + return; + } + + const nodeRuntimePluginPath = require.resolve( + '@module-federation/node/runtimePlugin', + ); + const hasNodeRuntimePlugin = runtimePlugins.some((runtimePlugin) => { + const runtimePluginPath = + typeof runtimePlugin === 'string' ? runtimePlugin : runtimePlugin[0]; + return runtimePluginPath === nodeRuntimePluginPath; + }); + + if (!hasNodeRuntimePlugin) { + throw new Error( + `${PLUGIN_IDENTIFIER} experiments.rsc requires @module-federation/node/runtimePlugin in runtimePlugins`, + ); + } +}; + +const patchRscClientBrowserSharedConfig = ( + mfConfig: moduleFederationPlugin.ModuleFederationPluginOptions, + isServer: boolean, +) => { + if (isServer || !mfConfig.shared) { + return; + } + + const patchSharedRecord = (sharedRecord: Record) => { + const clientBrowserShared = sharedRecord[RSC_CLIENT_BROWSER_SHARED_KEY] as + | Record + | undefined; + if (!clientBrowserShared || Array.isArray(clientBrowserShared)) { + return; + } + const shareScope = clientBrowserShared.shareScope; + if (typeof shareScope === 'string' && shareScope !== 'default') { + clientBrowserShared.import = false; + } + }; + + if (Array.isArray(mfConfig.shared)) { + for (const sharedConfig of mfConfig.shared) { + if (!sharedConfig || typeof sharedConfig !== 'object') { + continue; + } + patchSharedRecord(sharedConfig as Record); + } + return; + } + + if (typeof mfConfig.shared === 'object') { + patchSharedRecord(mfConfig.shared as Record); + } +}; + export const patchMFConfig = ( mfConfig: moduleFederationPlugin.ModuleFederationPluginOptions, isServer: boolean, ) => { + const rscEnabled = isRscMfEnabled(mfConfig); addDataFetchExposes(mfConfig.exposes, isServer); + if (rscEnabled) { + setRscExposeConfig(mfConfig); + patchRscClientBrowserSharedConfig(mfConfig, isServer); + } + if (mfConfig.remoteType === undefined) { mfConfig.remoteType = 'script'; } @@ -194,6 +451,9 @@ export const patchMFConfig = ( injectRuntimePlugins(resolveSharedStrategyPlugin(), runtimePlugins); + if (rscEnabled && hasRemotes(mfConfig.remotes)) { + injectRuntimePlugins(RSC_BRIDGE_RUNTIME_PLUGIN, runtimePlugins); + } if (isServer) { injectRuntimePlugins(resolveNodeRuntimePlugin(), runtimePlugins); if (isDev()) { @@ -217,6 +477,12 @@ export const patchMFConfig = ( } } + assertRscMfConfig({ + mfConfig, + isServer, + runtimePlugins, + }); + mfConfig.runtimePlugins = runtimePlugins; if (!isServer) { @@ -248,6 +514,195 @@ function patchIgnoreWarning(chain: BundlerChainConfig) { chain.ignoreWarnings(ignoreWarnings); } +const patchProjectNodeModulesResolution = (chain: BundlerChainConfig) => { + const projectNodeModulesPath = path.resolve(process.cwd(), 'node_modules'); + if (!fs.existsSync(projectNodeModulesPath)) { + return; + } + + const resolveModules = chain.resolve.modules as { + values?: () => string[]; + clear: () => unknown; + add: (value: string) => unknown; + }; + resolveModules.clear(); + resolveModules.add(projectNodeModulesPath); + resolveModules.add('node_modules'); +}; + +const patchServerOnlyAlias = (chain: BundlerChainConfig) => { + const serverOnlyEmptyPath = path.resolve( + process.cwd(), + 'node_modules/server-only/empty.js', + ); + if (!fs.existsSync(serverOnlyEmptyPath)) { + return; + } + + const aliasChain = chain.resolve.alias as { + has?: (key: string) => boolean; + set: (key: string, value: string) => unknown; + }; + const hasServerOnlyAlias = + typeof aliasChain.has === 'function' + ? aliasChain.has('server-only$') + : false; + if (!hasServerOnlyAlias) { + aliasChain.set('server-only$', serverOnlyEmptyPath); + } +}; + +const resolveProjectDependency = (request: string) => { + try { + return require.resolve(request, { paths: [process.cwd()] }); + } catch { + try { + return require.resolve(request); + } catch { + return undefined; + } + } +}; + +const getExposeImports = ( + exposeConfig: moduleFederationPlugin.ExposesObject[string], +): string[] => { + if (typeof exposeConfig === 'string') { + return [exposeConfig]; + } + if (Array.isArray(exposeConfig)) { + return exposeConfig.filter( + (importPath): importPath is string => typeof importPath === 'string', + ); + } + if ( + exposeConfig && + typeof exposeConfig === 'object' && + 'import' in exposeConfig + ) { + const exposeImport = (exposeConfig as { import?: unknown }).import; + if (typeof exposeImport === 'string') { + return [exposeImport]; + } + if (Array.isArray(exposeImport)) { + return exposeImport.filter( + (importPath): importPath is string => typeof importPath === 'string', + ); + } + } + return []; +}; + +const collectExposeImportDirectories = ( + exposes: moduleFederationPlugin.ModuleFederationPluginOptions['exposes'], +) => { + if (!exposes) { + return []; + } + + const exposeEntries = Array.isArray(exposes) + ? exposes.flatMap((exposeItem) => + typeof exposeItem === 'string' + ? [[exposeItem, exposeItem] as const] + : (Object.entries(exposeItem || {}) as Array< + readonly [string, moduleFederationPlugin.ExposesObject[string]] + >), + ) + : (Object.entries(exposes) as Array< + readonly [string, moduleFederationPlugin.ExposesObject[string]] + >); + + const directories = new Set(); + for (const [exposeKey, exposeConfig] of exposeEntries) { + if (exposeKey === RSC_BRIDGE_EXPOSE) { + continue; + } + for (const importPath of getExposeImports(exposeConfig)) { + if (!importPath || !importPath.startsWith('.')) { + continue; + } + directories.add(path.dirname(path.resolve(process.cwd(), importPath))); + } + } + + return Array.from(directories); +}; + +const patchRscRemoteComponentLayer = ( + chain: BundlerChainConfig, + mfConfig: moduleFederationPlugin.ModuleFederationPluginOptions, +) => { + const includeDirectories = collectExposeImportDirectories(mfConfig.exposes); + if (includeDirectories.length === 0) { + return; + } + + const ruleChain = chain.module + .rule('rsc-mf-remote-components-layer') + .test(/\.[cm]?[jt]sx?$/); + + for (const includeDirectory of includeDirectories) { + ruleChain.include.add(includeDirectory); + } + + ruleChain.layer(RSC_LAYER); +}; + +const patchRscServerJsxRuntimeResolution = (chain: BundlerChainConfig) => { + const pluginId = 'rsc-mf-react-server-jsx-runtime'; + if (chain.plugins.has(pluginId)) { + return; + } + + const reactPackagePath = resolveProjectDependency('react/package.json'); + if (!reactPackagePath) { + return; + } + const reactDir = path.dirname(reactPackagePath); + const reactJsxRuntimeServerPath = path.join( + reactDir, + 'jsx-runtime.react-server.js', + ); + const reactJsxDevRuntimeServerPath = path.join( + reactDir, + 'jsx-dev-runtime.react-server.js', + ); + + if ( + !fs.existsSync(reactJsxRuntimeServerPath) || + !fs.existsSync(reactJsxDevRuntimeServerPath) + ) { + return; + } + + const { NormalModuleReplacementPlugin } = require('@rspack/core') as { + NormalModuleReplacementPlugin: new (...args: unknown[]) => unknown; + }; + + chain.plugin(pluginId).use(NormalModuleReplacementPlugin, [ + /^react\/jsx(?:-dev)?-runtime$/, + (resource: { + request?: string; + contextInfo?: { + issuerLayer?: string; + }; + }) => { + if (resource.contextInfo?.issuerLayer !== RSC_LAYER) { + return; + } + + if (resource.request === 'react/jsx-runtime') { + resource.request = reactJsxRuntimeServerPath; + } else if (resource.request === 'react/jsx-dev-runtime') { + resource.request = reactJsxDevRuntimeServerPath; + } + }, + ]); +}; + +const normalizePublicPath = (publicPath: string) => + publicPath.endsWith('/') ? publicPath.slice(0, -1) : publicPath; + export function addMyTypes2Ignored( chain: BundlerChainConfig, mfConfig: moduleFederationPlugin.ModuleFederationPluginOptions, @@ -308,11 +763,37 @@ export function patchBundlerConfig(options: { enableSSR: boolean; }) { const { chain, modernjsConfig, isServer, mfConfig, enableSSR } = options; + const rscMfEnabled = isRscMfEnabled(mfConfig); chain.optimization.delete('runtimeChunk'); patchIgnoreWarning(chain); + if (rscMfEnabled && isServer) { + chain.resolve.conditionNames.add('react-server'); + } + + if (rscMfEnabled && hasExposes(mfConfig.exposes)) { + patchProjectNodeModulesResolution(chain); + patchServerOnlyAlias(chain); + + const assetPrefix = modernjsConfig.output?.assetPrefix; + if (typeof assetPrefix === 'string' && assetPrefix.trim()) { + const normalizedAssetPrefix = normalizePublicPath(assetPrefix.trim()); + chain.output.publicPath( + isServer + ? `${normalizedAssetPrefix}/bundles/` + : `${normalizedAssetPrefix}/`, + ); + } + if (!isServer) { + chain.optimization.splitChunks(false); + } else { + patchRscRemoteComponentLayer(chain, mfConfig); + patchRscServerJsxRuntimeResolution(chain); + } + } + if (!chain.output.get('chunkLoadingGlobal')) { chain.output.chunkLoadingGlobal(`chunk_${mfConfig.name}`); } @@ -391,6 +872,7 @@ export const moduleFederationConfigPlugin = ( const enableSSR = Boolean( userConfig.userConfig?.ssr ?? Boolean(modernjsConfig?.server?.ssr), ); + const enableRsc = Boolean(modernjsConfig?.server?.rsc); api.modifyBundlerChain((chain) => { const target = chain.get('target'); @@ -415,14 +897,14 @@ export const moduleFederationConfigPlugin = ( chain, isServer: !isWeb, modernjsConfig, - mfConfig, + mfConfig: targetMFConfig, enableSSR, }); if (isWeb) { userConfig.distOutputDir = chain.output.get('path') || path.resolve(process.cwd(), 'dist'); - } else if (enableSSR) { + } else if (enableSSR && !enableRsc) { userConfig.userConfig ||= {}; userConfig.userConfig.ssr ||= {}; if (userConfig.userConfig.ssr === true) { @@ -480,7 +962,8 @@ export const moduleFederationConfigPlugin = ( }, }, source: { - enableAsyncEntry: modernjsConfig.source?.enableAsyncEntry ?? true, + enableAsyncEntry: + modernjsConfig.source?.enableAsyncEntry ?? !enableRsc, }, dev: { assetPrefix: modernjsConfig?.dev?.assetPrefix diff --git a/packages/modernjs-v3/src/cli/mfRuntimePlugins/rsc-bridge-runtime-plugin.spec.ts b/packages/modernjs-v3/src/cli/mfRuntimePlugins/rsc-bridge-runtime-plugin.spec.ts new file mode 100644 index 00000000000..e4356dec85f --- /dev/null +++ b/packages/modernjs-v3/src/cli/mfRuntimePlugins/rsc-bridge-runtime-plugin.spec.ts @@ -0,0 +1,490 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import rscBridgeRuntimePlugin from './rsc-bridge-runtime-plugin'; + +type ManifestLike = { + serverManifest?: Record; + clientManifest?: Record; + serverConsumerModuleMap?: Record; +}; + +type WebpackRequireRuntime = { + m?: Record void>; + c?: Record; + rscM?: ManifestLike; +}; + +const ACTION_REMAP_GLOBAL_KEY = '__MODERN_RSC_MF_ACTION_ID_MAP__'; +const PROXY_MODULE_PREFIX = '__modernjs_mf_rsc_action_proxy__:'; +type FederationState = { + [ACTION_REMAP_GLOBAL_KEY]?: Record; +}; + +const createWebpackRequireRuntime = (): WebpackRequireRuntime => ({ + m: {}, + c: {}, + rscM: { + serverManifest: {}, + clientManifest: {}, + serverConsumerModuleMap: {}, + }, +}); +let runtimeRequire: WebpackRequireRuntime; + +const getFederationState = () => + global as typeof global & { + __FEDERATION__?: FederationState; + fetch?: unknown; + }; + +const getActionRemapMap = () => + getFederationState().__FEDERATION__?.[ACTION_REMAP_GLOBAL_KEY] || {}; + +describe('rsc-bridge-runtime-plugin', () => { + beforeEach(() => { + vi.useFakeTimers(); + runtimeRequire = createWebpackRequireRuntime(); + vi.stubGlobal('__webpack_require__', runtimeRequire); + vi.stubGlobal('__FEDERATION__', {}); + vi.stubGlobal('window', { + __MODERN_JS_ENTRY_NAME: 'server-component-root', + }); + delete getFederationState().__FEDERATION__?.[ACTION_REMAP_GLOBAL_KEY]; + }); + + afterEach(() => { + vi.clearAllTimers(); + vi.useRealTimers(); + vi.unstubAllGlobals(); + delete getFederationState().fetch; + delete getFederationState().__FEDERATION__?.[ACTION_REMAP_GLOBAL_KEY]; + }); + + it('merges remote manifest, registers action remap, and installs proxy dispatcher', async () => { + const executeAction = vi.fn(async (id: string, args: unknown[]) => ({ + id, + args, + })); + + const plugin = rscBridgeRuntimePlugin(); + const loadRemote = vi.fn(async () => ({ + getManifest: () => ({ + clientManifest: { + clientRef: { + id: '123', + name: 'default', + chunks: [], + }, + }, + serverConsumerModuleMap: { + '123': { + '*': { + id: '123', + name: 'default', + chunks: [], + }, + }, + }, + serverManifest: { + rawActionId: { + async: true, + }, + }, + }), + executeAction, + })); + + await plugin.onLoad?.({ + remote: { alias: 'rscRemote' }, + options: { name: 'rscHost' }, + origin: { + loadRemote, + }, + }); + + expect(loadRemote).toHaveBeenCalledTimes(1); + expect(loadRemote).toHaveBeenCalledWith('rscRemote/__rspack_rsc_bridge__'); + + const webpackRequire = runtimeRequire; + const hostManifest = webpackRequire.rscM!; + + expect( + hostManifest.clientManifest?.['remote-module:rscRemote:clientRef']?.id, + ).toBe('remote-module:rscRemote:123'); + expect(hostManifest.serverConsumerModuleMap).toHaveProperty( + 'remote-module:rscRemote:123', + ); + expect( + hostManifest.serverConsumerModuleMap?.['remote-module:rscRemote:123']?.[ + '*' + ]?.id, + ).toBe('remote-module:rscRemote:123'); + + const prefixedActionId = 'remote:rscRemote:rawActionId'; + const proxyModuleId = `${PROXY_MODULE_PREFIX}rscRemote`; + expect(hostManifest.serverManifest?.[prefixedActionId]).toMatchObject({ + id: proxyModuleId, + name: prefixedActionId, + }); + expect(getActionRemapMap().rawActionId).toBe(prefixedActionId); + + const proxyFactory = webpackRequire.m?.[proxyModuleId]; + expect(typeof proxyFactory).toBe('function'); + + const proxyModule = { exports: {} as Record }; + proxyFactory?.(proxyModule); + const result = await proxyModule.exports[prefixedActionId]('payload'); + + expect(executeAction).toHaveBeenCalledWith('rawActionId', ['payload']); + expect(result).toEqual({ + id: 'rawActionId', + args: ['payload'], + }); + }); + + it('awaits async bridge manifest responses before merge', async () => { + const plugin = rscBridgeRuntimePlugin(); + + await plugin.onLoad?.({ + remote: { alias: 'rscRemote' }, + options: { name: 'rscHost' }, + origin: { + loadRemote: vi.fn(async () => ({ + getManifest: async () => ({ + serverManifest: { + asyncRawAction: { + async: true, + }, + }, + }), + executeAction: vi.fn(async () => undefined), + })), + }, + } as any); + + const webpackRequire = runtimeRequire; + expect(webpackRequire.rscM?.serverManifest).toHaveProperty( + 'remote:rscRemote:asyncRawAction', + ); + expect(getActionRemapMap().asyncRawAction).toBe( + 'remote:rscRemote:asyncRawAction', + ); + }); + + it('namespaces clientManifest keys per alias for same remote export key', async () => { + const plugin = rscBridgeRuntimePlugin(); + + const loadRemote = vi.fn(async (request: string) => { + if (request.startsWith('rscRemoteA/')) { + return { + getManifest: () => ({ + clientManifest: { + sharedClientRef: { + id: '123', + name: 'default', + chunks: [], + }, + }, + }), + executeAction: vi.fn(async () => undefined), + }; + } + + return { + getManifest: () => ({ + clientManifest: { + sharedClientRef: { + id: '123', + name: 'default', + chunks: [], + }, + }, + }), + executeAction: vi.fn(async () => undefined), + }; + }); + + await plugin.onLoad?.({ + remote: { alias: 'rscRemoteA' }, + options: { name: 'rscHost' }, + origin: { loadRemote }, + } as any); + + await plugin.onLoad?.({ + remote: { alias: 'rscRemoteB' }, + options: { name: 'rscHost' }, + origin: { loadRemote }, + } as any); + + const webpackRequire = runtimeRequire; + + expect(webpackRequire.rscM?.clientManifest).toHaveProperty( + 'remote-module:rscRemoteA:sharedClientRef', + ); + expect(webpackRequire.rscM?.clientManifest).toHaveProperty( + 'remote-module:rscRemoteB:sharedClientRef', + ); + }); + + it('loads and merges each remote alias once', async () => { + const plugin = rscBridgeRuntimePlugin(); + const loadRemote = vi.fn(async () => ({ + getManifest: () => ({ + serverManifest: { + one: { + async: true, + }, + }, + }), + executeAction: vi.fn(async () => undefined), + })); + + const args = { + remote: { alias: 'rscRemote' }, + options: { name: 'rscHost' }, + origin: { + loadRemote, + }, + }; + + await plugin.onLoad?.(args as any); + await plugin.onLoad?.(args as any); + + expect(loadRemote).toHaveBeenCalledTimes(1); + }); + + it('falls back to federation.instance when origin is unavailable', async () => { + const loadRemote = vi.fn(async () => ({ + getManifest: () => ({ + serverManifest: { + rawActionId: { + async: true, + }, + }, + }), + executeAction: vi.fn(async () => undefined), + })); + + const webpackRequire = runtimeRequire; + ( + webpackRequire as WebpackRequireRuntime & { + federation?: { + instance?: { loadRemote?: (request: string) => Promise }; + }; + } + ).federation = { + instance: { + loadRemote, + }, + }; + + const plugin = rscBridgeRuntimePlugin(); + + await plugin.onLoad?.({ + remote: { alias: 'rscRemote' }, + options: { name: 'rscHost' }, + } as any); + + expect(loadRemote).toHaveBeenCalledWith('rscRemote/__rspack_rsc_bridge__'); + expect(getActionRemapMap().rawActionId).toBe( + 'remote:rscRemote:rawActionId', + ); + }); + + it('throws deterministic conflict errors when manifests disagree', async () => { + const webpackRequire = runtimeRequire; + webpackRequire.rscM = { + serverManifest: {}, + clientManifest: { + 'remote-module:rscRemote:sharedKey': { + id: 'remote-module:existingRemote:123', + name: 'default', + chunks: [], + }, + }, + serverConsumerModuleMap: {}, + }; + + const plugin = rscBridgeRuntimePlugin(); + + await expect( + plugin.onLoad?.({ + remote: { alias: 'rscRemote' }, + options: { name: 'rscHost' }, + origin: { + loadRemote: vi.fn(async () => ({ + getManifest: () => ({ + clientManifest: { + sharedKey: { + id: '123', + name: 'default', + chunks: [], + }, + }, + }), + executeAction: vi.fn(async () => undefined), + })), + }, + } as any), + ).rejects.toThrow(/clientManifest conflict/); + }); + + it('marks raw action remaps as false when aliases collide', async () => { + const plugin = rscBridgeRuntimePlugin(); + + const loadRemote = vi.fn(async (request: string) => { + if (request.startsWith('rscRemoteA/')) { + return { + getManifest: () => ({ + serverManifest: { + sameRawId: { + async: true, + }, + }, + }), + executeAction: vi.fn(async () => undefined), + }; + } + + return { + getManifest: () => ({ + serverManifest: { + sameRawId: { + async: true, + }, + }, + }), + executeAction: vi.fn(async () => undefined), + }; + }); + + await plugin.onLoad?.({ + remote: { alias: 'rscRemoteA' }, + options: { name: 'rscHost' }, + origin: { loadRemote }, + } as any); + + await plugin.onLoad?.({ + remote: { alias: 'rscRemoteB' }, + options: { name: 'rscHost' }, + origin: { loadRemote }, + } as any); + + expect(getActionRemapMap().sameRawId).toBe(false); + }); + + it('normalizes missing ssrPublicPath on resolved remote snapshots', async () => { + const plugin = rscBridgeRuntimePlugin(); + const loadRemote = vi.fn(async () => ({ + getManifest: () => ({}), + executeAction: vi.fn(async () => undefined), + })); + const args: any = { + remote: { alias: 'rscRemote' }, + remoteInfo: { + publicPath: 'http://127.0.0.1:3008/bundles/', + remoteEntry: { + name: 'static/remoteEntry.js', + }, + }, + origin: { + loadRemote, + }, + }; + + await plugin.afterResolve?.(args); + + expect(args.remoteInfo.ssrPublicPath).toBe( + 'http://127.0.0.1:3008/bundles/', + ); + expect(args.remoteInfo.remoteEntry.path).toBe(''); + }); + + it('rewrites node bridge loads to bundles/.js when ssr entry is missing', async () => { + const plugin = rscBridgeRuntimePlugin(); + const loadRemote = vi.fn(async () => ({ + getManifest: () => ({}), + executeAction: vi.fn(async () => undefined), + })); + const args: any = { + remote: { alias: 'rscRemote' }, + remoteInfo: { + entry: undefined, + }, + remoteSnapshot: { + name: 'rscRemote', + publicPath: 'http://127.0.0.1:3008/', + remoteEntry: { + name: 'static/remoteEntry.js', + }, + }, + origin: { + loadRemote, + }, + }; + + await plugin.afterResolve?.(args); + + expect(args.remoteInfo.entry).toBe( + 'http://127.0.0.1:3008/bundles/static/remoteEntry.js', + ); + }); + + it('rewrites existing client remoteEntry URLs to node bundles entry', async () => { + const plugin = rscBridgeRuntimePlugin(); + const loadRemote = vi.fn(async () => ({ + getManifest: () => ({}), + executeAction: vi.fn(async () => undefined), + })); + const args: any = { + remote: { alias: 'rscRemote' }, + remoteInfo: { + entry: 'http://127.0.0.1:3008/static/remoteEntry.js', + }, + remoteSnapshot: { + name: 'rscRemote', + publicPath: 'http://127.0.0.1:3008/', + remoteEntry: { + name: 'static/remoteEntry.js', + }, + }, + origin: { + loadRemote, + }, + }; + + await plugin.afterResolve?.(args); + + expect(args.remoteInfo.entry).toBe( + 'http://127.0.0.1:3008/bundles/static/remoteEntry.js', + ); + }); + + it('avoids duplicate bundles segment when ssr publicPath already includes bundles', async () => { + const plugin = rscBridgeRuntimePlugin(); + const loadRemote = vi.fn(async () => ({ + getManifest: () => ({}), + executeAction: vi.fn(async () => undefined), + })); + const args: any = { + remote: { alias: 'rscRemote' }, + remoteInfo: { + entry: undefined, + }, + remoteSnapshot: { + publicPath: 'http://127.0.0.1:3008/bundles/', + remoteEntry: { + name: 'static/remoteEntry.js', + }, + }, + origin: { + loadRemote, + }, + }; + + await plugin.afterResolve?.(args); + + expect(args.remoteInfo.entry).toBe( + 'http://127.0.0.1:3008/bundles/static/remoteEntry.js', + ); + }); +}); diff --git a/packages/modernjs-v3/src/cli/mfRuntimePlugins/rsc-bridge-runtime-plugin.ts b/packages/modernjs-v3/src/cli/mfRuntimePlugins/rsc-bridge-runtime-plugin.ts new file mode 100644 index 00000000000..c35a3a1e337 --- /dev/null +++ b/packages/modernjs-v3/src/cli/mfRuntimePlugins/rsc-bridge-runtime-plugin.ts @@ -0,0 +1,765 @@ +import type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime'; + +const RSC_BRIDGE_EXPOSE = '__rspack_rsc_bridge__'; +const ACTION_PREFIX = 'remote:'; +const MODULE_PREFIX = 'remote-module:'; +const ACTION_REMAP_GLOBAL_KEY = '__MODERN_RSC_MF_ACTION_ID_MAP__'; +const PROXY_MODULE_PREFIX = '__modernjs_mf_rsc_action_proxy__:'; + +type ManifestLike = { + serverManifest?: Record; + clientManifest?: Record; + serverConsumerModuleMap?: Record; +}; + +type BridgeModule = { + getManifest?: () => ManifestLike; + executeAction?: (actionId: string, args: unknown[]) => Promise; +}; + +type ActionMapRecord = Record; +type ActionRemapMap = Record; +type FederationState = { + [ACTION_REMAP_GLOBAL_KEY]?: ActionRemapMap; +}; +type WebpackRequireRuntime = { + m?: Record void>; + c?: Record; + rscM?: ManifestLike; + federation?: { + instance?: { + loadRemote?: (request: string) => Promise; + }; + }; +}; + +declare const __webpack_require__: WebpackRequireRuntime; +declare const __FEDERATION__: FederationState | undefined; + +const isObject = (value: unknown): value is Record => + typeof value === 'object' && value !== null; + +const stableStringify = (value: unknown) => { + try { + return JSON.stringify(value, (_key, current) => { + if (Array.isArray(current)) { + return current; + } + if (!isObject(current)) { + return current; + } + return Object.fromEntries( + Object.keys(current) + .sort() + .map((key) => [key, current[key]]), + ); + }); + } catch { + return String(value); + } +}; + +const assertNoConflict = ( + target: Record, + key: string, + nextValue: unknown, + alias: string, + section: string, +) => { + if (!Object.prototype.hasOwnProperty.call(target, key)) { + return; + } + if (stableStringify(target[key]) !== stableStringify(nextValue)) { + throw new Error( + `[modern-js-v3:rsc-bridge] ${section} conflict for "${key}" while merging remote "${alias}"`, + ); + } +}; + +const getNamespacedModuleId = (alias: string, rawId: string | number) => + `${MODULE_PREFIX}${alias}:${String(rawId)}`; + +const getNamespacedClientManifestKey = (alias: string, key: string | number) => + `${MODULE_PREFIX}${alias}:${String(key)}`; + +const actionRemapMapFallback: ActionRemapMap = {}; + +const getFederationState = (): FederationState | undefined => { + if (typeof __FEDERATION__ !== 'undefined' && isObject(__FEDERATION__)) { + return __FEDERATION__; + } + return undefined; +}; + +const getActionRemapMap = () => { + const federationState = getFederationState(); + if (!federationState) { + return actionRemapMapFallback; + } + if (!isObject(federationState[ACTION_REMAP_GLOBAL_KEY])) { + federationState[ACTION_REMAP_GLOBAL_KEY] = {}; + } + return federationState[ACTION_REMAP_GLOBAL_KEY] as ActionRemapMap; +}; + +const registerActionRemap = (rawActionId: string, prefixedActionId: string) => { + const remapMap = getActionRemapMap(); + const existingValue = remapMap[rawActionId]; + if (typeof existingValue === 'undefined') { + remapMap[rawActionId] = prefixedActionId; + return; + } + if (existingValue === prefixedActionId) { + return; + } + // Ambiguous mapping across remotes; skip unsafe remap. + remapMap[rawActionId] = false; +}; + +const getWebpackRequireIfAvailable = (): WebpackRequireRuntime | undefined => { + if (typeof __webpack_require__ !== 'undefined') { + return __webpack_require__; + } + return undefined; +}; + +const getWebpackRequire = (): WebpackRequireRuntime => { + const runtime = getWebpackRequireIfAvailable(); + if (!runtime) { + throw new Error( + '[modern-js-v3:rsc-bridge] __webpack_require__ is unavailable while installing the RSC bridge runtime plugin', + ); + } + return runtime; +}; + +const ensureHostManifest = () => { + const webpackRequire = getWebpackRequire(); + if (!isObject(webpackRequire.rscM)) { + webpackRequire.rscM = {}; + } + const manifest = webpackRequire.rscM as ManifestLike; + manifest.serverManifest = isObject(manifest.serverManifest) + ? manifest.serverManifest + : {}; + manifest.clientManifest = isObject(manifest.clientManifest) + ? manifest.clientManifest + : {}; + manifest.serverConsumerModuleMap = isObject(manifest.serverConsumerModuleMap) + ? manifest.serverConsumerModuleMap + : {}; + return manifest; +}; + +const remapConsumerNode = ( + alias: string, + value: unknown, + namespacedClientIds: Record, +) => { + if (!isObject(value)) { + return value; + } + return Object.fromEntries( + Object.entries(value).map(([exportName, exportValue]) => { + const nextExportValue = isObject(exportValue) + ? { ...exportValue } + : exportValue; + if (isObject(nextExportValue) && nextExportValue.id != null) { + const rawId = String(nextExportValue.id); + nextExportValue.id = Object.prototype.hasOwnProperty.call( + namespacedClientIds, + rawId, + ) + ? namespacedClientIds[rawId] + : getNamespacedModuleId(alias, rawId); + } + return [exportName, nextExportValue]; + }), + ); +}; + +const getProxyModuleId = (alias: string) => `${PROXY_MODULE_PREFIX}${alias}`; + +const installProxyModule = ({ + proxyModuleId, + actionMap, + ensureBridge, +}: { + proxyModuleId: string; + actionMap: ActionMapRecord; + ensureBridge: (alias: string) => Promise; +}) => { + const webpackRequire = getWebpackRequire(); + if (!isObject(webpackRequire.m)) { + webpackRequire.m = {}; + } + if (webpackRequire.m![proxyModuleId]) { + return; + } + + webpackRequire.m![proxyModuleId] = (module: { exports: any }) => { + module.exports = new Proxy( + {}, + { + get(_target, property) { + if (property === 'then') { + return undefined; + } + if (typeof property !== 'string') { + return undefined; + } + if (!Object.prototype.hasOwnProperty.call(actionMap, property)) { + return undefined; + } + const mapping = actionMap[property]; + return async (...args: unknown[]) => { + const bridge = await ensureBridge(mapping.alias); + if (typeof bridge.executeAction !== 'function') { + throw new Error( + `[modern-js-v3:rsc-bridge] Missing executeAction bridge method for remote "${mapping.alias}"`, + ); + } + return bridge.executeAction( + mapping.rawActionId, + Array.isArray(args) ? args : [], + ); + }; + }, + }, + ); + }; +}; + +const isNodeLikeRuntime = () => + typeof process !== 'undefined' && + Boolean(process.versions?.node) && + typeof document === 'undefined'; + +const rscBridgeRuntimePlugin = (): ModuleFederationRuntimePlugin => { + const bridgePromises: Partial>> = {}; + const aliasMergePromises: Partial>> = {}; + const actionMap: ActionMapRecord = {}; + const mergedRemoteAliases = new Set(); + + const resolveRemoteAlias = (args: any): string | undefined => { + const candidateAliases = [ + args?.remote?.alias, + args?.pkgNameOrAlias, + args?.remote?.name, + args?.remoteInfo?.alias, + args?.remoteInfo?.name, + args?.name, + ]; + for (const candidate of candidateAliases) { + if (typeof candidate === 'string' && candidate.trim()) { + return candidate; + } + } + return undefined; + }; + + const resolveExposeSpecifier = (args: any): string => { + const candidateExposes = [ + args?.expose, + args?.id, + args?.request, + args?.pkgNameOrAlias, + args?.remote?.id, + args?.remote?.expose, + args?.remoteInfo?.id, + args?.remoteInfo?.expose, + args?.name, + ]; + for (const candidate of candidateExposes) { + if ( + (typeof candidate === 'string' || typeof candidate === 'number') && + String(candidate).trim() + ) { + return String(candidate); + } + } + return ''; + }; + + const isBridgeExposeRequest = (args: any) => + resolveExposeSpecifier(args).includes(RSC_BRIDGE_EXPOSE); + + const ensureBridge = async (alias: string, args?: any) => { + const existingBridgePromise = bridgePromises[alias]; + if (existingBridgePromise) { + return existingBridgePromise; + } + const runtimeInstance = + args?.origin && typeof args.origin.loadRemote === 'function' + ? args.origin + : getWebpackRequireIfAvailable()?.federation?.instance; + if (!runtimeInstance || typeof runtimeInstance.loadRemote !== 'function') { + throw new Error( + '[modern-js-v3:rsc-bridge] Module Federation runtime instance is unavailable while loading the RSC bridge', + ); + } + let resolveBridge!: (bridge: BridgeModule) => void; + let rejectBridge!: (error: unknown) => void; + const bridgePromise = new Promise((resolve, reject) => { + resolveBridge = resolve; + rejectBridge = reject; + }); + bridgePromises[alias] = bridgePromise; + + void Promise.resolve() + .then(() => runtimeInstance.loadRemote(`${alias}/${RSC_BRIDGE_EXPOSE}`)) + .then((bridge: BridgeModule) => { + if ( + !bridge || + typeof bridge.getManifest !== 'function' || + typeof bridge.executeAction !== 'function' + ) { + throw new Error( + `[modern-js-v3:rsc-bridge] Remote "${alias}" is missing the internal RSC bridge expose`, + ); + } + resolveBridge(bridge); + }) + .catch((error: unknown) => { + // Allow retry when bridge loading fails transiently. + delete bridgePromises[alias]; + rejectBridge(error); + }); + + return bridgePromise; + }; + + const mergeRemoteManifest = ( + alias: string, + remoteManifest: ManifestLike, + proxyModuleId: string, + ) => { + if (!isObject(remoteManifest)) { + return; + } + + const hostManifest = ensureHostManifest(); + const namespacedClientIds: Record = {}; + + if (isObject(remoteManifest.clientManifest)) { + for (const [key, value] of Object.entries( + remoteManifest.clientManifest, + )) { + const scopedClientManifestKey = getNamespacedClientManifestKey( + alias, + key, + ); + const nextValue = isObject(value) ? { ...value } : value; + if (isObject(nextValue) && nextValue.id != null) { + const namespacedClientId = getNamespacedModuleId(alias, nextValue.id); + namespacedClientIds[String(nextValue.id)] = namespacedClientId; + nextValue.id = namespacedClientId; + } + assertNoConflict( + hostManifest.clientManifest as Record, + scopedClientManifestKey, + nextValue, + alias, + 'clientManifest', + ); + (hostManifest.clientManifest as Record)[ + scopedClientManifestKey + ] = nextValue; + } + } + + if (isObject(remoteManifest.serverConsumerModuleMap)) { + for (const [rawModuleId, value] of Object.entries( + remoteManifest.serverConsumerModuleMap, + )) { + const scopedModuleId = Object.prototype.hasOwnProperty.call( + namespacedClientIds, + String(rawModuleId), + ) + ? namespacedClientIds[String(rawModuleId)] + : getNamespacedModuleId(alias, rawModuleId); + const nextValue = remapConsumerNode(alias, value, namespacedClientIds); + assertNoConflict( + hostManifest.serverConsumerModuleMap as Record, + scopedModuleId, + nextValue, + alias, + 'serverConsumerModuleMap', + ); + (hostManifest.serverConsumerModuleMap as Record)[ + scopedModuleId + ] = nextValue; + } + } + + if (!isObject(remoteManifest.serverManifest)) { + return; + } + + for (const [rawActionId, actionEntry] of Object.entries( + remoteManifest.serverManifest, + )) { + const prefixedActionId = `${ACTION_PREFIX}${alias}:${rawActionId}`; + const hostActionEntry = { + id: proxyModuleId, + name: prefixedActionId, + chunks: [], + async: + isObject(actionEntry) && 'async' in actionEntry + ? (actionEntry as Record).async + : true, + }; + assertNoConflict( + hostManifest.serverManifest as Record, + prefixedActionId, + hostActionEntry, + alias, + 'serverManifest', + ); + (hostManifest.serverManifest as Record)[prefixedActionId] = + hostActionEntry; + actionMap[prefixedActionId] = { alias, rawActionId }; + registerActionRemap(rawActionId, prefixedActionId); + } + }; + + const ensureRemoteAliasMerged = async (alias: string, args: any) => { + const bridgeExposeRequest = isBridgeExposeRequest(args); + const existingMergePromise = aliasMergePromises[alias]; + if (existingMergePromise) { + if (bridgeExposeRequest) { + return; + } + await existingMergePromise; + return; + } + if (mergedRemoteAliases.has(alias) || bridgeExposeRequest) { + return; + } + + const mergePromise = (async () => { + const proxyModuleId = getProxyModuleId(alias); + installProxyModule({ + proxyModuleId, + actionMap, + ensureBridge: async (resolvedAlias) => + ensureBridge(resolvedAlias, args), + }); + + const bridge = await ensureBridge(alias, args); + const remoteManifest = + typeof bridge.getManifest === 'function' + ? await Promise.resolve(bridge.getManifest()) + : {}; + mergeRemoteManifest(alias, remoteManifest || {}, proxyModuleId); + mergedRemoteAliases.add(alias); + })(); + + aliasMergePromises[alias] = mergePromise; + try { + await mergePromise; + } finally { + delete aliasMergePromises[alias]; + } + }; + + const ensureTrailingSlash = (value: string) => + value.endsWith('/') ? value : `${value}/`; + + const isAbsoluteHttpUrl = (value: string) => + value.startsWith('http://') || value.startsWith('https://'); + + const isBrokenRemoteEntryUrl = (value: unknown) => + typeof value !== 'string' || + !value.trim() || + value.includes('undefined') || + value.startsWith('https:undefined') || + value.startsWith('http:undefined'); + + const isLikelyClientRemoteEntryPath = (entryPath: string) => { + const normalizedPath = entryPath.split('?')[0]; + return ( + normalizedPath.endsWith('/static/remoteEntry.js') || + normalizedPath.endsWith('static/remoteEntry.js') || + normalizedPath === 'remoteEntry.js' || + normalizedPath.endsWith('/remoteEntry.js') + ); + }; + + const shouldRewriteNodeClientRemoteEntryUrl = (entry: unknown) => + isNodeLikeRuntime() && + typeof entry === 'string' && + isLikelyClientRemoteEntryPath(entry); + + const resolveNodeSsrRemoteEntryPath = (entryPath: string) => { + if (!isNodeLikeRuntime()) { + return entryPath; + } + + const normalizedEntryPath = entryPath.startsWith('/') + ? entryPath.slice(1) + : entryPath; + if (normalizedEntryPath.startsWith('bundles/')) { + return normalizedEntryPath; + } + + if (!isLikelyClientRemoteEntryPath(normalizedEntryPath)) { + return entryPath; + } + + return `bundles/${normalizedEntryPath}`; + }; + + const resolveSnapshotPublicPath = (snapshot: Record) => { + const metaData = isObject(snapshot.metaData) + ? (snapshot.metaData as Record) + : undefined; + const candidates = [ + snapshot.ssrPublicPath, + snapshot.publicPath, + metaData?.ssrPublicPath, + metaData?.publicPath, + ]; + const publicPath = candidates.find( + (candidate) => typeof candidate === 'string' && candidate.trim(), + ) as string | undefined; + if (!publicPath) { + return undefined; + } + return ensureTrailingSlash(publicPath.trim()); + }; + + const resolveSnapshotRemoteEntryRequest = (snapshot: Record) => { + const remoteEntryCandidate = isObject(snapshot.ssrRemoteEntry) + ? (snapshot.ssrRemoteEntry as Record) + : (snapshot.ssrRemoteEntry ?? snapshot.remoteEntry); + if (!remoteEntryCandidate) { + return undefined; + } + + let entryPath = + typeof remoteEntryCandidate === 'string' + ? remoteEntryCandidate + : (() => { + const pathPart = + typeof remoteEntryCandidate.path === 'string' + ? remoteEntryCandidate.path + : ''; + const namePart = + typeof remoteEntryCandidate.name === 'string' + ? remoteEntryCandidate.name + : ''; + return `${pathPart}${namePart}`; + })(); + if (!entryPath) { + return undefined; + } + + if (isAbsoluteHttpUrl(entryPath)) { + if (isNodeLikeRuntime() && isLikelyClientRemoteEntryPath(entryPath)) { + try { + const url = new URL(entryPath); + const resolvedNodeEntryPath = resolveNodeSsrRemoteEntryPath( + url.pathname, + ); + const normalizedPathname = resolvedNodeEntryPath.startsWith('/') + ? resolvedNodeEntryPath + : `/${resolvedNodeEntryPath}`; + url.pathname = normalizedPathname; + return url.href; + } catch { + return entryPath; + } + } + return entryPath; + } + + if (isNodeLikeRuntime()) { + const resolvedNodeEntryPath = resolveNodeSsrRemoteEntryPath(entryPath); + if (resolvedNodeEntryPath !== entryPath) { + entryPath = resolvedNodeEntryPath; + } else if (!snapshot.ssrRemoteEntry) { + const normalizedEntryPath = entryPath.startsWith('/') + ? entryPath.slice(1) + : entryPath; + if (!normalizedEntryPath.startsWith('bundles/')) { + entryPath = `bundles/${normalizedEntryPath}`; + } + } + } + + const publicPath = resolveSnapshotPublicPath(snapshot); + if (!publicPath) { + return undefined; + } + + const publicPathHasBundlesSegment = publicPath.includes('/bundles/'); + if (isNodeLikeRuntime() && publicPathHasBundlesSegment) { + const normalizedEntryPath = entryPath.startsWith('/') + ? entryPath.slice(1) + : entryPath; + if (normalizedEntryPath.startsWith('bundles/')) { + entryPath = normalizedEntryPath.slice('bundles/'.length); + } + } + + try { + return new URL(entryPath, publicPath).href; + } catch { + const normalizedEntry = entryPath.startsWith('/') + ? entryPath.slice(1) + : entryPath; + return `${publicPath}${normalizedEntry}`; + } + }; + + const patchRemoteInfoEntry = ( + remoteInfo: Record | undefined, + snapshot: Record | undefined, + ) => { + if (!isObject(remoteInfo) || !isObject(snapshot)) { + return; + } + const resolvedEntry = resolveSnapshotRemoteEntryRequest(snapshot); + if (!resolvedEntry) { + return; + } + + if ( + shouldRewriteNodeClientRemoteEntryUrl(remoteInfo.entry) || + isBrokenRemoteEntryUrl(remoteInfo.entry) || + (typeof remoteInfo.entry === 'string' && + remoteInfo.entry.includes('undefined')) + ) { + remoteInfo.entry = resolvedEntry; + } + }; + + const normalizeRemoteEntryPath = (remoteEntry: Record) => { + if ( + remoteEntry.path === undefined || + remoteEntry.path === null || + remoteEntry.path === 'undefined' + ) { + remoteEntry.path = ''; + } + }; + + const patchSnapshotSsrPublicPath = (snapshot: any) => { + if (!isObject(snapshot)) { + return; + } + + const metaData = isObject(snapshot.metaData) + ? (snapshot.metaData as Record) + : undefined; + + const rawPublicPathCandidates = [ + snapshot.ssrPublicPath, + snapshot.publicPath, + metaData?.ssrPublicPath, + metaData?.publicPath, + ]; + const publicPath = rawPublicPathCandidates.find( + (candidate) => typeof candidate === 'string' && candidate.trim(), + ) as string | undefined; + if (publicPath) { + const normalizedPublicPath = ensureTrailingSlash(publicPath.trim()); + snapshot.ssrPublicPath = normalizedPublicPath; + if (metaData) { + metaData.ssrPublicPath = normalizedPublicPath; + } + } + + const remoteEntry = isObject(snapshot.remoteEntry) + ? (snapshot.remoteEntry as Record) + : isObject(metaData?.remoteEntry) + ? (metaData?.remoteEntry as Record) + : undefined; + if (remoteEntry) { + normalizeRemoteEntryPath(remoteEntry); + if (!isObject(snapshot.remoteEntry)) { + snapshot.remoteEntry = remoteEntry; + } + if (metaData && !isObject(metaData.remoteEntry)) { + metaData.remoteEntry = remoteEntry; + } + } + + if ( + !isObject(snapshot.ssrRemoteEntry) && + (isObject(metaData?.ssrRemoteEntry) || remoteEntry) + ) { + const source = isObject(metaData?.ssrRemoteEntry) + ? (metaData?.ssrRemoteEntry as Record) + : (remoteEntry as Record); + const normalizedSsrRemoteEntry = { ...source }; + normalizeRemoteEntryPath(normalizedSsrRemoteEntry); + snapshot.ssrRemoteEntry = normalizedSsrRemoteEntry; + if (metaData) { + metaData.ssrRemoteEntry = { ...normalizedSsrRemoteEntry }; + } + } else if (isObject(snapshot.ssrRemoteEntry)) { + normalizeRemoteEntryPath(snapshot.ssrRemoteEntry as Record); + if (metaData && !isObject(metaData.ssrRemoteEntry)) { + metaData.ssrRemoteEntry = snapshot.ssrRemoteEntry; + } + } + }; + + const patchRuntimeArgsSnapshots = (args: any) => { + patchSnapshotSsrPublicPath(args?.remoteSnapshot); + patchSnapshotSsrPublicPath(args?.remoteInfo); + patchSnapshotSsrPublicPath(args?.remote?.remoteSnapshot); + patchSnapshotSsrPublicPath(args?.remote?.remoteInfo); + patchSnapshotSsrPublicPath(args?.resolvedRemote?.remoteSnapshot); + patchSnapshotSsrPublicPath(args?.resolvedRemote?.remoteInfo); + patchRemoteInfoEntry(args?.remoteInfo, args?.remoteSnapshot); + patchRemoteInfoEntry( + args?.remote?.remoteInfo, + args?.remote?.remoteSnapshot, + ); + patchRemoteInfoEntry( + args?.resolvedRemote?.remoteInfo, + args?.resolvedRemote?.remoteSnapshot, + ); + }; + + return { + name: 'modernjs-rsc-bridge-runtime-plugin', + async afterResolve(args: any) { + patchRuntimeArgsSnapshots(args); + const alias = resolveRemoteAlias(args); + if (!alias || isBridgeExposeRequest(args)) { + return args; + } + await ensureRemoteAliasMerged(alias, args); + return args; + }, + async onLoad(args: any) { + patchRuntimeArgsSnapshots(args); + const alias = resolveRemoteAlias(args); + if (!alias) { + return args; + } + + const hasExposeContext = + typeof args?.expose === 'string' || + typeof args?.id === 'string' || + typeof args?.id === 'number'; + if (!hasExposeContext) { + await ensureRemoteAliasMerged(alias, args); + return args; + } + + if (mergedRemoteAliases.has(alias) || isBridgeExposeRequest(args)) { + return args; + } + await ensureRemoteAliasMerged(alias, args); + + return args; + }, + }; +}; + +export default rscBridgeRuntimePlugin; diff --git a/packages/modernjs-v3/src/cli/ssrPlugin.ts b/packages/modernjs-v3/src/cli/ssrPlugin.ts index ee3467e2224..e76b644c19f 100644 --- a/packages/modernjs-v3/src/cli/ssrPlugin.ts +++ b/packages/modernjs-v3/src/cli/ssrPlugin.ts @@ -28,6 +28,8 @@ export function setEnv() { } export const CHAIN_MF_PLUGIN_ID = 'plugin-module-federation-server'; +const isBuildCommand = () => + process.argv.includes('build') || process.argv.includes('deploy'); function getManifestAssetFileNames( manifestOption?: moduleFederationPlugin.ModuleFederationPluginOptions['manifest'], @@ -79,52 +81,6 @@ const mfSSRRsbuildPlugin = ( 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)) { @@ -162,43 +118,6 @@ const mfSSRRsbuildPlugin = ( 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; - } - } - }, - ); }, }; }; @@ -288,6 +207,7 @@ export const moduleFederationSSRPlugin = ( if (!isWeb && !secondarySharedTreeShaking) { chain.target('async-node'); + if (isDev()) { chain .plugin('UniverseEntryChunkTrackerPlugin') @@ -313,11 +233,33 @@ export const moduleFederationSSRPlugin = ( return; } try { + const requestPath = req.url?.split('?')[0] || ''; + const extension = path.extname(requestPath); + const isJsonRequest = + extension === '.json' && + !requestPath.includes('hot-update'); + const isBundleJsRequest = + extension === '.js' && requestPath.startsWith('/bundles/'); if ( - req.url?.includes('.json') && - !req.url?.includes('hot-update') + (isJsonRequest || isBundleJsRequest) && + requestPath.includes('/') ) { - const filepath = path.join(process.cwd(), `dist${req.url}`); + if (!requestPath.startsWith('/')) { + next(); + return; + } + + const distRoot = path.resolve(process.cwd(), 'dist'); + const filepath = path.resolve(distRoot, `.${requestPath}`); + const allowedPrefix = `${distRoot}${path.sep}`; + if ( + filepath !== distRoot && + !filepath.startsWith(allowedPrefix) + ) { + next(); + return; + } + fs.statSync(filepath); res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader( @@ -338,14 +280,72 @@ export const moduleFederationSSRPlugin = ( }, }; }); + const readAssetResourceFromDisk = ( + outputDir: string, + fileNames: AssetFileNames, + tag: 'browser' | 'node', + ): StatsAssetResource | undefined => { + const statsFilePath = path.resolve(outputDir, fileNames.statsFileName); + const manifestFilePath = path.resolve( + outputDir, + fileNames.manifestFileName, + ); + if (!fs.existsSync(statsFilePath) || !fs.existsSync(manifestFilePath)) { + return undefined; + } + + try { + return { + stats: { + data: fs.readJSONSync(statsFilePath), + filename: fileNames.statsFileName, + }, + manifest: { + data: fs.readJSONSync(manifestFilePath), + filename: fileNames.manifestFileName, + }, + }; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + logger.error( + `[module-federation-ssr] Failed to read ${tag} manifest assets from disk: ${message}`, + ); + return undefined; + } + }; + const writeMergedManifest = () => { - const { distOutputDir, assetResources } = pluginOptions; - const browserAssets = assetResources.browser; - const nodeAssets = assetResources.node; + if (!isBuildCommand()) { + return; + } + const distOutputDir = + pluginOptions.distOutputDir || path.resolve(process.cwd(), 'dist'); + const userSSRConfig = pluginOptions.userConfig.ssr; + const ssrDistOutputDir = + typeof userSSRConfig === 'object' && userSSRConfig.distOutputDir + ? userSSRConfig.distOutputDir + : path.resolve(distOutputDir, 'bundles'); + const browserFileNames = + pluginOptions.assetFileNames.browser || + getManifestAssetFileNames(pluginOptions.csrConfig?.manifest); + const nodeFileNames = + pluginOptions.assetFileNames.node || + getManifestAssetFileNames(pluginOptions.ssrConfig?.manifest); + const browserAssets = readAssetResourceFromDisk( + distOutputDir, + browserFileNames, + 'browser', + ); + const nodeAssets = readAssetResourceFromDisk( + ssrDistOutputDir, + nodeFileNames, + 'node', + ); - if (!distOutputDir || !browserAssets || !nodeAssets) { + if (!browserAssets || !nodeAssets) { return; } + try { updateStatsAndManifest(nodeAssets, browserAssets, distOutputDir); } catch (err) { @@ -356,10 +356,6 @@ export const moduleFederationSSRPlugin = ( api.onAfterBuild(() => { writeMergedManifest(); }); - api.onDevCompileDone(() => { - // 热更后修改 manifest - writeMergedManifest(); - }); }, }); diff --git a/packages/modernjs-v3/src/runtime/rsc-bridge-expose.spec.ts b/packages/modernjs-v3/src/runtime/rsc-bridge-expose.spec.ts new file mode 100644 index 00000000000..c43cabd0be4 --- /dev/null +++ b/packages/modernjs-v3/src/runtime/rsc-bridge-expose.spec.ts @@ -0,0 +1,160 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +type ManifestLike = { + serverManifest?: Record; + clientManifest?: Record; + serverConsumerModuleMap?: Record; +}; + +type ExposeModuleMap = Record Promise | unknown>; + +type WebpackRequireRuntime = { + initializeExposesData?: { + moduleMap?: ExposeModuleMap; + }; + rscM?: ManifestLike; +}; + +const setWebpackRequireRuntime = (runtime: WebpackRequireRuntime) => { + vi.stubGlobal('__webpack_require__', runtime); +}; + +const loadBridgeExposeModule = async () => { + vi.resetModules(); + return import('./rsc-bridge-expose'); +}; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe('rsc-bridge-expose', () => { + it('returns remote manifest through getManifest', async () => { + const manifest: ManifestLike = { + serverManifest: { + action: { + id: 'remote:raw', + }, + }, + }; + setWebpackRequireRuntime({ + rscM: manifest, + initializeExposesData: { + moduleMap: {}, + }, + }); + + const { getManifest } = await loadBridgeExposeModule(); + + expect(getManifest()).toBe(manifest); + }); + + it('scans exposes and executes action references by raw id', async () => { + const action = vi.fn(async (value: string) => `ok:${value}`) as { + (...args: unknown[]): Promise; + $$id?: string; + }; + action.$$id = 'raw-action-id'; + + const getFactory = vi.fn(async () => () => ({ + nested: { + action, + }, + })); + + setWebpackRequireRuntime({ + initializeExposesData: { + moduleMap: { + './actions': getFactory, + './__rspack_rsc_bridge__': vi.fn(async () => () => ({ + ignored: true, + })), + }, + }, + rscM: {}, + }); + + const bridgeExpose = await loadBridgeExposeModule(); + + await expect( + bridgeExpose.executeAction('raw-action-id', ['value-a']), + ).resolves.toBe('ok:value-a'); + await expect( + bridgeExpose.executeAction('raw-action-id', ['value-b']), + ).resolves.toBe('ok:value-b'); + + expect(getFactory).toHaveBeenCalledTimes(1); + expect(action).toHaveBeenNthCalledWith(1, 'value-a'); + expect(action).toHaveBeenNthCalledWith(2, 'value-b'); + }); + + it('normalizes non-array args to empty array before dispatch', async () => { + const action = vi.fn(async (...args: unknown[]) => args.length) as { + (...args: unknown[]): Promise; + $$id?: string; + }; + action.$$id = 'raw-action-id'; + + setWebpackRequireRuntime({ + initializeExposesData: { + moduleMap: { + './actions': async () => () => ({ action }), + }, + }, + rscM: {}, + }); + + const bridgeExpose = await loadBridgeExposeModule(); + + await expect( + bridgeExpose.executeAction('raw-action-id', 'not-array' as any), + ).resolves.toBe(0); + expect(action).toHaveBeenCalledWith(); + }); + + it('throws explicit errors when action id is unresolved', async () => { + setWebpackRequireRuntime({ + initializeExposesData: { + moduleMap: { + './actions': async () => () => ({ value: 'noop' }), + }, + }, + rscM: {}, + }); + + const bridgeExpose = await loadBridgeExposeModule(); + + await expect( + bridgeExpose.executeAction('missing-action-id', []), + ).rejects.toThrow(/Missing remote action for id "missing-action-id"/); + }); + + it('discovers action references from module cache exports', async () => { + const cachedAction = vi.fn(async () => 'cached') as { + (...args: unknown[]): Promise; + $$id?: string; + }; + cachedAction.$$id = 'raw-action-from-cache'; + + setWebpackRequireRuntime({ + initializeExposesData: { + moduleMap: {}, + }, + c: { + cachedModule: { + exports: { + nested: { + cachedAction, + }, + }, + }, + }, + rscM: {}, + }); + + const bridgeExpose = await loadBridgeExposeModule(); + await expect( + bridgeExpose.executeAction('raw-action-from-cache', []), + ).resolves.toBe('cached'); + }); +}); diff --git a/packages/modernjs-v3/src/runtime/rsc-bridge-expose.ts b/packages/modernjs-v3/src/runtime/rsc-bridge-expose.ts new file mode 100644 index 00000000000..48d155eb95c --- /dev/null +++ b/packages/modernjs-v3/src/runtime/rsc-bridge-expose.ts @@ -0,0 +1,166 @@ +type ManifestLike = { + serverManifest?: Record; + clientManifest?: Record; + serverConsumerModuleMap?: Record; +}; + +type ExposeModuleMap = Record< + string, + () => Promise<(() => unknown) | unknown> | (() => unknown) | unknown +>; + +type WebpackRequireRuntime = { + initializeExposesData?: { + moduleMap?: ExposeModuleMap; + }; + m?: Record; + c?: Record; + rscM?: ManifestLike; +}; + +declare const __webpack_require__: WebpackRequireRuntime; + +const BRIDGE_EXPOSE_NAME = './__rspack_rsc_bridge__'; +const actionReferenceCache: Record unknown> = + Object.create(null); +let scanPromise: Promise | null = null; +let scanHadErrors = false; + +const isObject = (value: unknown): value is Record => + typeof value === 'object' && value !== null; +const isFunction = (value: unknown): value is (...args: unknown[]) => unknown => + typeof value === 'function'; +const isWebpackRequireRuntime = ( + value: unknown, +): value is WebpackRequireRuntime => isObject(value) || isFunction(value); +const runtimeRequireFromWrapper: WebpackRequireRuntime | undefined = (() => { + try { + // Access the module-wrapper require function when available. + // biome-ignore lint/security/noGlobalEval: The bundler module wrapper exposes `require` via arguments only. + const wrapperRequire = eval('arguments[2]'); + return isWebpackRequireRuntime(wrapperRequire) + ? (wrapperRequire as WebpackRequireRuntime) + : undefined; + } catch { + return undefined; + } +})(); + +const getWebpackRequire = (): WebpackRequireRuntime => { + if (runtimeRequireFromWrapper) { + return runtimeRequireFromWrapper; + } + + if ( + typeof __webpack_require__ !== 'undefined' && + isWebpackRequireRuntime(__webpack_require__) + ) { + return __webpack_require__; + } + throw new Error( + '[modern-js-v3:rsc-bridge] __webpack_require__ is unavailable while evaluating the internal bridge expose', + ); +}; + +const cacheActionReferencesFromExports = ( + exposedValue: unknown, + visited: WeakSet, +) => { + if ( + typeof exposedValue === 'function' && + typeof (exposedValue as { $$id?: unknown }).$$id === 'string' + ) { + const id = (exposedValue as unknown as { $$id?: string }).$$id; + if (typeof id === 'string') { + actionReferenceCache[id] = exposedValue as ( + ...args: unknown[] + ) => unknown; + } + } + + if (!isObject(exposedValue)) { + return; + } + if (visited.has(exposedValue)) { + return; + } + visited.add(exposedValue); + + for (const value of Object.values(exposedValue)) { + cacheActionReferencesFromExports(value, visited); + } +}; + +const scanExposedModulesForActions = async () => { + if (scanPromise) { + await scanPromise; + if (scanHadErrors || Object.keys(actionReferenceCache).length === 0) { + scanPromise = null; + scanHadErrors = false; + } + return; + } + + scanHadErrors = false; + scanPromise = (async () => { + let hadExposeScanError = false; + const webpackRequire = getWebpackRequire(); + const moduleMap = webpackRequire.initializeExposesData?.moduleMap; + if (isObject(moduleMap)) { + for (const [exposeName, getFactory] of Object.entries(moduleMap)) { + if ( + exposeName === BRIDGE_EXPOSE_NAME || + typeof getFactory !== 'function' + ) { + continue; + } + + try { + const maybeFactory = await getFactory(); + const exportsValue = + typeof maybeFactory === 'function' ? maybeFactory() : maybeFactory; + cacheActionReferencesFromExports(exportsValue, new WeakSet()); + if (isObject(exportsValue) && isObject(exportsValue.default)) { + cacheActionReferencesFromExports( + exportsValue.default, + new WeakSet(), + ); + } + } catch { + hadExposeScanError = true; + // Ignore expose modules that fail to execute during scan. + } + } + } + + if (isObject(webpackRequire.c)) { + for (const moduleRecord of Object.values(webpackRequire.c)) { + const moduleExports = isObject(moduleRecord) + ? (moduleRecord as { exports?: unknown }).exports + : undefined; + cacheActionReferencesFromExports(moduleExports, new WeakSet()); + } + } + + scanHadErrors = hadExposeScanError; + })(); + + await scanPromise; + if (scanHadErrors || Object.keys(actionReferenceCache).length === 0) { + scanPromise = null; + scanHadErrors = false; + } +}; + +export const getManifest = () => getWebpackRequire().rscM; + +export const executeAction = async (actionId: string, args: unknown[]) => { + await scanExposedModulesForActions(); + const action = actionReferenceCache[actionId]; + if (typeof action !== 'function') { + throw new Error( + `[modern-js-v3:rsc-bridge] Missing remote action for id "${actionId}". Ensure it is reachable from a federated expose.`, + ); + } + return action(...(Array.isArray(args) ? args : [])); +}; diff --git a/packages/modernjs-v3/src/runtime/rsc-client-callback-bootstrap.js b/packages/modernjs-v3/src/runtime/rsc-client-callback-bootstrap.js new file mode 100644 index 00000000000..740cf460d2c --- /dev/null +++ b/packages/modernjs-v3/src/runtime/rsc-client-callback-bootstrap.js @@ -0,0 +1,506 @@ +import { setResolveActionId } from '@modern-js/runtime/rsc/client'; +import { + createFromFetch, + createTemporaryReferenceSet, + encodeReply, + setServerCallback, +} from 'react-server-dom-rspack/client.browser'; + +const ACTION_PREFIX = 'remote:'; +const ACTION_REMAP_GLOBAL_KEY = '__MODERN_RSC_MF_ACTION_ID_MAP__'; +const CALLBACK_INSTALL_RETRY_DELAY_MS = 50; +const MAX_CALLBACK_INSTALL_ATTEMPTS = 120; +const CALLBACK_CHUNK_LOADER_HOOK_FLAG = + '__MODERN_RSC_MF_CALLBACK_CHUNK_LOADER_HOOKED__'; +const CALLBACK_CHUNK_LOADER_WRAPPED_FLAG = + '__MODERN_RSC_MF_CALLBACK_CHUNK_LOADER_WRAPPED__'; +const CALLBACK_CHUNK_HANDLER_KEY = '__MODERN_RSC_MF_CALLBACK_CHUNK_HANDLER__'; +const CALLBACK_CHUNK_HANDLER_WRAPPED_FLAG = + '__MODERN_RSC_MF_CALLBACK_CHUNK_HANDLER_WRAPPED__'; +let hasResolvedFallbackAlias = false; +let fallbackRemoteAlias; +let callbackInstallAttempts = 0; +const actionRemapMapFallback = Object.create(null); +const installedClientBrowserRuntimes = new WeakSet(); +const wrappedChunkLoaders = new WeakMap(); +const wrappedChunkHandlers = new WeakMap(); +const hookedChunkHandlers = new WeakSet(); + +function isObject(value) { + return typeof value === 'object' && value !== null; +} + +function isFunction(value) { + return typeof value === 'function'; +} + +const runtimeRequireFromWrapper = (() => { + try { + // Access the module-wrapper require function when available. + // biome-ignore lint/security/noGlobalEval: The bundler module wrapper exposes `require` via arguments only. + const wrapperRequire = eval('arguments[2]'); + if (isFunction(wrapperRequire) || isObject(wrapperRequire)) { + return wrapperRequire; + } + } catch {} + return undefined; +})(); + +function isClientBrowserRuntime(value) { + return ( + isObject(value) && + isFunction(value.setServerCallback) && + isFunction(value.createTemporaryReferenceSet) && + isFunction(value.encodeReply) && + isFunction(value.createFromFetch) + ); +} + +function getWebpackRequire() { + if (runtimeRequireFromWrapper) { + return runtimeRequireFromWrapper; + } + if (typeof __webpack_require__ !== 'undefined') { + return __webpack_require__; + } + return undefined; +} + +function getActionRemapMap() { + const federationState = + typeof __FEDERATION__ !== 'undefined' && isObject(__FEDERATION__) + ? __FEDERATION__ + : undefined; + if (!federationState) { + return actionRemapMapFallback; + } + const map = federationState[ACTION_REMAP_GLOBAL_KEY]; + if (!isObject(map)) { + federationState[ACTION_REMAP_GLOBAL_KEY] = Object.create(null); + return federationState[ACTION_REMAP_GLOBAL_KEY]; + } + return map; +} + +function getHostServerManifest() { + const webpackRequire = getWebpackRequire(); + if ( + webpackRequire && + isObject(webpackRequire.rscM) && + isObject(webpackRequire.rscM.serverManifest) + ) { + return webpackRequire.rscM.serverManifest; + } + return undefined; +} + +function hasHostServerAction(rawId) { + const serverManifest = getHostServerManifest(); + if (!isObject(serverManifest)) { + return false; + } + return Object.prototype.hasOwnProperty.call(serverManifest, rawId); +} + +function resolveFallbackRemoteAlias() { + if (hasResolvedFallbackAlias) { + return fallbackRemoteAlias; + } + + const webpackRequire = getWebpackRequire(); + const runtimeInstance = + webpackRequire && + isObject(webpackRequire.federation) && + isObject(webpackRequire.federation.instance) + ? webpackRequire.federation.instance + : undefined; + if (!runtimeInstance) { + return undefined; + } + + const aliasSet = new Set(); + const remotes = Array.isArray(runtimeInstance.options?.remotes) + ? runtimeInstance.options.remotes + : []; + for (const remote of remotes) { + if (isObject(remote)) { + const alias = + typeof remote.alias === 'string' && remote.alias + ? remote.alias + : typeof remote.name === 'string' && remote.name + ? remote.name + : undefined; + if (alias) { + aliasSet.add(alias); + } + } + } + + const idToRemoteMap = runtimeInstance.remoteHandler?.idToRemoteMap; + if (isObject(idToRemoteMap)) { + for (const remoteInfo of Object.values(idToRemoteMap)) { + if (!isObject(remoteInfo)) { + continue; + } + const name = + typeof remoteInfo.name === 'string' && remoteInfo.name + ? remoteInfo.name + : undefined; + if (name) { + aliasSet.add(name); + } + } + } + + if (aliasSet.size === 1) { + fallbackRemoteAlias = Array.from(aliasSet)[0]; + hasResolvedFallbackAlias = true; + return fallbackRemoteAlias; + } + + if (aliasSet.size === 0 && typeof window === 'undefined') { + return undefined; + } + + if (typeof window !== 'undefined') { + const containerAliases = Object.keys(window).filter((alias) => { + const candidate = window[alias]; + return ( + isObject(candidate) && + isFunction(candidate.get) && + isFunction(candidate.init) + ); + }); + if (aliasSet.size === 0 && containerAliases.length === 1) { + fallbackRemoteAlias = containerAliases[0]; + hasResolvedFallbackAlias = true; + return fallbackRemoteAlias; + } + if (aliasSet.size === 0 && containerAliases.length === 0) { + return undefined; + } + } + + fallbackRemoteAlias = undefined; + hasResolvedFallbackAlias = true; + return undefined; +} + +function resolveActionEndpoint() { + if (typeof window === 'undefined') { + return '/'; + } + + const entryName = window.__MODERN_JS_ENTRY_NAME; + if (!entryName || entryName === 'main' || entryName === 'index') { + return '/'; + } + return `/${entryName}`; +} + +async function resolveActionId(id) { + const rawId = String(id); + if (rawId.startsWith(ACTION_PREFIX)) { + return rawId; + } + + const remapMap = getActionRemapMap(); + const remappedId = remapMap[rawId]; + if (typeof remappedId === 'string') { + return remappedId; + } + if (hasHostServerAction(rawId)) { + return rawId; + } + if (remappedId === false) { + throw new Error( + `[modern-js-v3:rsc-bridge] Ambiguous remote action id "${rawId}" cannot be resolved safely.`, + ); + } + + const fallbackAlias = resolveFallbackRemoteAlias(); + if (typeof fallbackAlias === 'string' && fallbackAlias) { + const prefixedId = `${ACTION_PREFIX}${fallbackAlias}:${rawId}`; + remapMap[rawId] = prefixedId; + return prefixedId; + } + + throw new Error( + `[modern-js-v3:rsc-bridge] Unable to resolve raw action id "${rawId}". Use a prefixed action id or ensure the remote alias manifest is merged before invoking the action.`, + ); +} + +function createServerCallback(runtime) { + return async (id, args) => { + const actionId = await resolveActionId(id); + const temporaryReferences = runtime.createTemporaryReferenceSet(); + const body = await runtime.encodeReply(Array.isArray(args) ? args : [], { + temporaryReferences, + }); + const endpoint = resolveActionEndpoint(); + const response = await fetch(endpoint, { + method: 'POST', + headers: { + Accept: 'text/x-component', + 'x-rsc-action': actionId, + }, + body, + }); + + if (!response.ok) { + throw new Error( + `[modern-js-v3:rsc-bridge] callback request failed (${response.status}) for "${actionId}"`, + ); + } + + return runtime.createFromFetch(Promise.resolve(response), { + temporaryReferences, + }); + }; +} + +function collectClientBrowserRuntimes() { + const runtimes = []; + const maybePushRuntime = (candidate) => { + if (isClientBrowserRuntime(candidate)) { + runtimes.push(candidate); + } else if ( + isObject(candidate) && + isClientBrowserRuntime(candidate.default) + ) { + runtimes.push(candidate.default); + } + }; + + maybePushRuntime({ + createFromFetch, + createTemporaryReferenceSet, + encodeReply, + setServerCallback, + }); + + const webpackRequire = getWebpackRequire(); + const moduleCache = webpackRequire?.c; + if (isObject(moduleCache)) { + for (const moduleRecord of Object.values(moduleCache)) { + if (isObject(moduleRecord) && 'exports' in moduleRecord) { + maybePushRuntime(moduleRecord.exports); + } + } + } + + return runtimes; +} + +function installServerCallbacks() { + let installedCount = 0; + for (const runtime of collectClientBrowserRuntimes()) { + if (installedClientBrowserRuntimes.has(runtime)) { + continue; + } + installedClientBrowserRuntimes.add(runtime); + runtime.setServerCallback(createServerCallback(runtime)); + installedCount += 1; + } + return installedCount; +} + +function getWrappedChunkLoader(chunkLoader) { + if (!isFunction(chunkLoader)) { + return chunkLoader; + } + if (chunkLoader[CALLBACK_CHUNK_LOADER_WRAPPED_FLAG]) { + return chunkLoader; + } + + const cachedWrappedLoader = wrappedChunkLoaders.get(chunkLoader); + if (cachedWrappedLoader) { + return cachedWrappedLoader; + } + + const wrappedChunkLoader = function (...args) { + const chunkLoadResult = chunkLoader.apply(this, args); + Promise.resolve(chunkLoadResult) + .catch(() => undefined) + .then(() => { + installServerCallbacks(); + }); + return chunkLoadResult; + }; + + wrappedChunkLoader[CALLBACK_CHUNK_LOADER_WRAPPED_FLAG] = true; + wrappedChunkLoaders.set(chunkLoader, wrappedChunkLoader); + return wrappedChunkLoader; +} + +function queueCallbackInstallAfterChunkLoad(promises) { + if (!Array.isArray(promises)) { + installServerCallbacks(); + return; + } + + Promise.resolve().then(() => { + Promise.allSettled(promises.slice()) + .catch(() => undefined) + .then(() => { + installServerCallbacks(); + }); + }); +} + +function getWrappedChunkHandler(chunkHandler) { + if (!isFunction(chunkHandler)) { + return chunkHandler; + } + if (chunkHandler[CALLBACK_CHUNK_HANDLER_WRAPPED_FLAG]) { + return chunkHandler; + } + + const cachedWrappedChunkHandler = wrappedChunkHandlers.get(chunkHandler); + if (cachedWrappedChunkHandler) { + return cachedWrappedChunkHandler; + } + + const wrappedChunkHandler = function (...args) { + chunkHandler.apply(this, args); + queueCallbackInstallAfterChunkLoad(args[1]); + }; + wrappedChunkHandler[CALLBACK_CHUNK_HANDLER_WRAPPED_FLAG] = true; + wrappedChunkHandlers.set(chunkHandler, wrappedChunkHandler); + return wrappedChunkHandler; +} + +function hookEnsureChunkHandlersInstall() { + const webpackRequire = getWebpackRequire(); + if (!webpackRequire || !isObject(webpackRequire.f)) { + return false; + } + + const chunkHandlers = webpackRequire.f; + if (hookedChunkHandlers.has(chunkHandlers)) { + return true; + } + + const existingChunkHandler = chunkHandlers[CALLBACK_CHUNK_HANDLER_KEY]; + if (isFunction(existingChunkHandler)) { + chunkHandlers[CALLBACK_CHUNK_HANDLER_KEY] = + getWrappedChunkHandler(existingChunkHandler); + } else { + const callbackInstallChunkHandler = function (_chunkId, promises) { + queueCallbackInstallAfterChunkLoad(promises); + }; + callbackInstallChunkHandler[CALLBACK_CHUNK_HANDLER_WRAPPED_FLAG] = true; + chunkHandlers[CALLBACK_CHUNK_HANDLER_KEY] = callbackInstallChunkHandler; + } + + hookedChunkHandlers.add(chunkHandlers); + return true; +} + +function hookChunkLoaderInstall() { + const webpackRequire = getWebpackRequire(); + if (!webpackRequire) { + return; + } + + const chunkLoaderDescriptor = Object.getOwnPropertyDescriptor( + webpackRequire, + 'e', + ); + if ( + webpackRequire[CALLBACK_CHUNK_LOADER_HOOK_FLAG] && + (!isFunction(chunkLoaderDescriptor?.get) || + chunkLoaderDescriptor.get[CALLBACK_CHUNK_LOADER_HOOK_FLAG]) + ) { + return; + } + + const hookState = { + chunkLoader: isFunction(chunkLoaderDescriptor?.get) + ? chunkLoaderDescriptor.get.call(webpackRequire) + : webpackRequire.e, + }; + + if (!isFunction(hookState.chunkLoader)) { + return; + } + + if (chunkLoaderDescriptor?.configurable === false) { + const wrappedChunkLoader = getWrappedChunkLoader(hookState.chunkLoader); + if (isFunction(chunkLoaderDescriptor?.set)) { + chunkLoaderDescriptor.set.call(webpackRequire, wrappedChunkLoader); + webpackRequire[CALLBACK_CHUNK_LOADER_HOOK_FLAG] = true; + return; + } + if (chunkLoaderDescriptor?.writable !== false) { + const didSetChunkLoader = Reflect.set( + webpackRequire, + 'e', + wrappedChunkLoader, + ); + if (didSetChunkLoader && webpackRequire.e === wrappedChunkLoader) { + webpackRequire[CALLBACK_CHUNK_LOADER_HOOK_FLAG] = true; + } + } + return; + } + + const readChunkLoader = () => { + if (isFunction(chunkLoaderDescriptor?.get)) { + const nextChunkLoader = chunkLoaderDescriptor.get.call(webpackRequire); + if (isFunction(nextChunkLoader)) { + hookState.chunkLoader = nextChunkLoader; + } + } + return hookState.chunkLoader; + }; + + const wrappedChunkLoaderGetter = function () { + const chunkLoader = readChunkLoader(); + return isFunction(chunkLoader) + ? getWrappedChunkLoader(chunkLoader) + : chunkLoader; + }; + wrappedChunkLoaderGetter[CALLBACK_CHUNK_LOADER_HOOK_FLAG] = true; + + Object.defineProperty(webpackRequire, 'e', { + configurable: true, + enumerable: chunkLoaderDescriptor?.enumerable ?? true, + get: wrappedChunkLoaderGetter, + set(nextChunkLoader) { + if (isFunction(chunkLoaderDescriptor?.set)) { + chunkLoaderDescriptor.set.call(webpackRequire, nextChunkLoader); + } + hookState.chunkLoader = nextChunkLoader; + }, + }); + + webpackRequire[CALLBACK_CHUNK_LOADER_HOOK_FLAG] = true; +} + +function runInstallAttempt() { + const hasEnsureChunkHandlersHook = hookEnsureChunkHandlersInstall(); + if (!hasEnsureChunkHandlersHook) { + hookChunkLoaderInstall(); + } + installServerCallbacks(); + callbackInstallAttempts += 1; + + if ( + callbackInstallAttempts >= MAX_CALLBACK_INSTALL_ATTEMPTS || + typeof setTimeout !== 'function' + ) { + return; + } + + setTimeout(runInstallAttempt, CALLBACK_INSTALL_RETRY_DELAY_MS); +} + +setResolveActionId(resolveActionId); + +runInstallAttempt(); +if (typeof queueMicrotask === 'function') { + queueMicrotask(() => { + installServerCallbacks(); + }); +} diff --git a/packages/modernjs-v3/src/runtime/rsc-client-callback-bootstrap.spec.ts b/packages/modernjs-v3/src/runtime/rsc-client-callback-bootstrap.spec.ts new file mode 100644 index 00000000000..cb0e57b78d8 --- /dev/null +++ b/packages/modernjs-v3/src/runtime/rsc-client-callback-bootstrap.spec.ts @@ -0,0 +1,385 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import vm from 'node:vm'; +import { describe, expect, it, vi } from 'vitest'; + +const BOOTSTRAP_FILE_PATH = path.resolve( + __dirname, + './rsc-client-callback-bootstrap.js', +); +const CALLBACK_CHUNK_HANDLER_KEY = '__MODERN_RSC_MF_CALLBACK_CHUNK_HANDLER__'; + +type RuntimeModule = { + setServerCallback: ReturnType; + createTemporaryReferenceSet: ReturnType; + encodeReply: ReturnType; + createFromFetch: ReturnType; +}; + +function createClientRuntimeModule(): RuntimeModule { + return { + setServerCallback: vi.fn(), + createTemporaryReferenceSet: vi.fn(() => ({ __temp: true })), + encodeReply: vi.fn(async () => 'encoded-body'), + createFromFetch: vi.fn(async () => ({ ok: true })), + }; +} + +function stripImports(source: string) { + return source.replace(/^import[\s\S]*?;\n/gm, ''); +} + +function flushMicrotasks() { + return new Promise((resolve) => queueMicrotask(resolve)); +} + +function evaluateBootstrap({ + runtimeRequire, + windowObject, +}: { + runtimeRequire: Record; + windowObject?: Record; +}) { + const source = stripImports(fs.readFileSync(BOOTSTRAP_FILE_PATH, 'utf-8')); + const resolvedActionIds: Array<(id: string) => Promise> = []; + const importedRuntime = createClientRuntimeModule(); + const fetchMock = vi.fn(async () => new Response('ok', { status: 200 })); + + const context = { + __webpack_require__: runtimeRequire, + setResolveActionId: vi.fn((resolver: (id: string) => Promise) => { + resolvedActionIds.push(resolver); + }), + createFromFetch: importedRuntime.createFromFetch, + createTemporaryReferenceSet: importedRuntime.createTemporaryReferenceSet, + encodeReply: importedRuntime.encodeReply, + setServerCallback: importedRuntime.setServerCallback, + fetch: fetchMock, + clearTimeout: vi.fn(), + setTimeout: vi.fn(() => 1), + queueMicrotask: (cb: () => void) => cb(), + window: windowObject, + __FEDERATION__: {}, + Response, + Promise, + Object, + Array, + WeakMap, + WeakSet, + Set, + Map, + JSON, + Reflect, + URL, + console, + process: { env: {} }, + }; + + vm.runInNewContext(source, context, { + filename: BOOTSTRAP_FILE_PATH, + }); + + return { + context, + importedRuntime, + fetchMock, + resolveActionId: resolvedActionIds[0] as ( + id: string, + ) => Promise | undefined, + }; +} + +describe('rsc-client-callback-bootstrap', () => { + it('registers resolver and installs callback on imported client runtime', () => { + const runtimeRequire = { + e: vi.fn(async () => undefined), + c: {}, + }; + const { importedRuntime, resolveActionId } = evaluateBootstrap({ + runtimeRequire, + }); + + expect(resolveActionId).toBeTypeOf('function'); + expect(importedRuntime.setServerCallback).toHaveBeenCalled(); + }); + + it('prefers ensureChunkHandlers hook and leaves chunk loader unpatched', async () => { + const runtimeRequire: Record = { + c: {}, + f: {}, + }; + const originalChunkLoader = vi.fn(async (chunkId: string) => { + const promises: Promise[] = []; + for (const handler of Object.values(runtimeRequire.f)) { + if (typeof handler === 'function') { + handler(chunkId, promises); + } + } + await Promise.all(promises); + return 'chunk-loaded'; + }); + runtimeRequire.e = originalChunkLoader; + + evaluateBootstrap({ runtimeRequire }); + + expect(runtimeRequire.e).toBe(originalChunkLoader); + + const lateRuntime = createClientRuntimeModule(); + runtimeRequire.c['late-runtime-module'] = { exports: lateRuntime }; + + await runtimeRequire.e('late-runtime'); + await flushMicrotasks(); + + expect(lateRuntime.setServerCallback).toHaveBeenCalledTimes(1); + }); + + it('waits for handlers added after the hook to settle before installing callbacks', async () => { + const runtimeRequire: Record = { + c: {}, + f: {}, + }; + const originalChunkLoader = vi.fn(async (chunkId: string) => { + const promises: Promise[] = []; + for (const handler of Object.values(runtimeRequire.f)) { + if (typeof handler === 'function') { + handler(chunkId, promises); + } + } + await Promise.all(promises); + return 'chunk-loaded'; + }); + runtimeRequire.e = originalChunkLoader; + + evaluateBootstrap({ runtimeRequire }); + + const lateRuntime = createClientRuntimeModule(); + runtimeRequire.f.zzzLateChunkHandler = vi.fn( + (_chunkId: string, promises: Promise[]) => { + promises.push( + new Promise((resolve) => { + setTimeout(() => { + runtimeRequire.c['late-runtime-module'] = { + exports: lateRuntime, + }; + resolve(); + }, 0); + }), + ); + }, + ); + + await runtimeRequire.e('late-runtime'); + await new Promise((resolve) => setTimeout(resolve, 0)); + await flushMicrotasks(); + + expect(lateRuntime.setServerCallback).toHaveBeenCalledTimes(1); + }); + + it('wraps existing callback chunk handler and still installs late callbacks', async () => { + const existingCallbackChunkHandler = vi.fn( + (_chunkId: string, promises: Promise[]) => { + promises.push(Promise.resolve('existing-handler')); + }, + ); + const runtimeRequire: Record = { + c: {}, + f: { + [CALLBACK_CHUNK_HANDLER_KEY]: existingCallbackChunkHandler, + }, + }; + const originalChunkLoader = vi.fn(async (chunkId: string) => { + const promises: Promise[] = []; + for (const handler of Object.values(runtimeRequire.f)) { + if (typeof handler === 'function') { + handler(chunkId, promises); + } + } + await Promise.all(promises); + return 'chunk-loaded'; + }); + runtimeRequire.e = originalChunkLoader; + + evaluateBootstrap({ runtimeRequire }); + + expect(runtimeRequire.e).toBe(originalChunkLoader); + + const lateRuntime = createClientRuntimeModule(); + runtimeRequire.c['late-runtime-module'] = { exports: lateRuntime }; + + await runtimeRequire.e('late-runtime'); + await flushMicrotasks(); + + expect(existingCallbackChunkHandler).toHaveBeenCalledWith( + 'late-runtime', + expect.any(Array), + ); + expect(lateRuntime.setServerCallback).toHaveBeenCalledTimes(1); + }); + + it('wraps configurable chunk loader and installs callback for late client runtimes', async () => { + const originalChunkLoader = vi.fn(async () => 'chunk-loaded'); + const runtimeRequire = { + e: originalChunkLoader, + c: {}, + }; + + evaluateBootstrap({ runtimeRequire }); + expect(runtimeRequire.e).not.toBe(originalChunkLoader); + + const lateRuntime = createClientRuntimeModule(); + runtimeRequire.c['late-runtime-module'] = { exports: lateRuntime }; + + await runtimeRequire.e('late-runtime'); + await flushMicrotasks(); + + expect(lateRuntime.setServerCallback).toHaveBeenCalledTimes(1); + }); + + it('wraps non-configurable chunk loader when writable and installs late callbacks', async () => { + const originalChunkLoader = vi.fn(async () => 'chunk-loaded'); + const runtimeRequire: Record = { c: {} }; + Object.defineProperty(runtimeRequire, 'e', { + configurable: false, + writable: true, + enumerable: true, + value: originalChunkLoader, + }); + + evaluateBootstrap({ runtimeRequire }); + expect(runtimeRequire.e).not.toBe(originalChunkLoader); + + const lateRuntime = createClientRuntimeModule(); + runtimeRequire.c['late-runtime-module'] = { exports: lateRuntime }; + + await runtimeRequire.e('late-runtime'); + await flushMicrotasks(); + + expect(lateRuntime.setServerCallback).toHaveBeenCalledTimes(1); + }); + + it('uses root action endpoint when entry name is missing/main/index', async () => { + const runtimeRequire = { + e: vi.fn(async () => undefined), + c: {}, + }; + const { importedRuntime, fetchMock } = evaluateBootstrap({ + runtimeRequire, + windowObject: { __MODERN_JS_ENTRY_NAME: 'main' }, + }); + + const serverCallback = importedRuntime.setServerCallback.mock.calls[0]?.[0]; + expect(serverCallback).toBeTypeOf('function'); + + await serverCallback('remote:demo:action', ['arg']); + const [endpoint, requestInit] = fetchMock.mock.calls[0]; + expect(endpoint).toBe('/'); + expect(requestInit?.method).toBe('POST'); + expect(requestInit?.headers?.['x-rsc-action']).toBe('remote:demo:action'); + }); + + it('fails closed when remap map marks raw action id as ambiguous', async () => { + const runtimeRequire = { + e: vi.fn(async () => undefined), + c: {}, + }; + const { context, resolveActionId } = evaluateBootstrap({ runtimeRequire }); + + expect(resolveActionId).toBeTypeOf('function'); + context.__FEDERATION__.__MODERN_RSC_MF_ACTION_ID_MAP__ = { + rawAction: false, + }; + + await expect(resolveActionId!('rawAction')).rejects.toThrow( + /Ambiguous remote action id "rawAction"/, + ); + }); + + it('keeps host-local action ids unprefixed when a single remote alias exists', async () => { + const runtimeRequire = { + e: vi.fn(async () => undefined), + c: {}, + federation: { + instance: { + options: { + remotes: [{ alias: 'rscRemote' }], + }, + }, + }, + rscM: { + serverManifest: { + hostAction: { id: 'hostAction' }, + }, + }, + }; + const { resolveActionId } = evaluateBootstrap({ + runtimeRequire, + }); + + await expect(resolveActionId!('hostAction')).resolves.toBe('hostAction'); + }); + + it('prefers host-local actions over ambiguous remap markers', async () => { + const runtimeRequire = { + e: vi.fn(async () => undefined), + c: {}, + federation: { + instance: { + options: { + remotes: [{ alias: 'remoteA' }, { alias: 'remoteB' }], + }, + }, + }, + rscM: { + serverManifest: { + hostAction: { id: 'hostAction' }, + }, + }, + }; + const { context, resolveActionId } = evaluateBootstrap({ runtimeRequire }); + context.__FEDERATION__.__MODERN_RSC_MF_ACTION_ID_MAP__ = { + hostAction: false, + }; + + await expect(resolveActionId!('hostAction')).resolves.toBe('hostAction'); + }); + + it('throws when a raw action id cannot be resolved deterministically', async () => { + const runtimeRequire = { + e: vi.fn(async () => undefined), + c: {}, + federation: { + instance: { + options: { + remotes: [{ alias: 'remoteA' }, { alias: 'remoteB' }], + }, + }, + }, + rscM: { + serverManifest: {}, + }, + }; + const { resolveActionId } = evaluateBootstrap({ runtimeRequire }); + + await expect(resolveActionId!('rawAction')).rejects.toThrow( + /Unable to resolve raw action id "rawAction"/, + ); + }); + + it('does not mark hook install when getter-only chunk loader cannot be replaced', () => { + const originalChunkLoader = vi.fn(async () => 'chunk-loaded'); + const runtimeRequire: Record = { c: {} }; + Object.defineProperty(runtimeRequire, 'e', { + configurable: false, + enumerable: true, + get() { + return originalChunkLoader; + }, + }); + + evaluateBootstrap({ runtimeRequire }); + + expect( + runtimeRequire.__MODERN_RSC_MF_CALLBACK_CHUNK_LOADER_HOOKED__, + ).not.toBe(true); + }); +}); diff --git a/packages/modernjs-v3/src/server/asyncStartupLoader.ts b/packages/modernjs-v3/src/server/asyncStartupLoader.ts new file mode 100644 index 00000000000..2e809e4e291 --- /dev/null +++ b/packages/modernjs-v3/src/server/asyncStartupLoader.ts @@ -0,0 +1,80 @@ +import path from 'path'; +import module from 'module'; +import vm from 'vm'; +import type { BundleLoaderStrategy } from '@modern-js/server-core/node'; +import fs from 'fs-extra'; + +const ASYNC_NODE_STARTUP_CALL_PATTERN = + /var\s+__webpack_exports__\s*=\s*__webpack_require__\.x\(\s*\)\s*;/; +const ENCODED_HMR_CLIENT_BOOTSTRAP_CALL = + /__webpack_require__\("data:text\/javascript,[^"]*"\);\s*/g; + +const isPromiseLike = (value: unknown): value is Promise => + Boolean(value) && + (typeof value === 'object' || typeof value === 'function') && + 'then' in (value as Promise) && + typeof (value as Promise).then === 'function'; + +/** + * MF async-node startup loader strategy. + * Handles bundles built with Module Federation experiments.asyncStartup + * that use __webpack_require__.mfAsyncStartup for deferred initialization. + * Patches the async startup call to run synchronously when the default + * require path returns a resolved promise with undefined. + */ +export const mfAsyncStartupLoaderStrategy: BundleLoaderStrategy = async ( + filepath, + context, +): Promise => { + try { + const bundleCode = await fs.readFile(filepath, 'utf-8'); + // Server-side VM fallback should not execute browser-only HMR bootstraps. + const sanitizedBundleCode = bundleCode.replace( + ENCODED_HMR_CLIENT_BOOTSTRAP_CALL, + '', + ); + + if ( + !ASYNC_NODE_STARTUP_CALL_PATTERN.test(sanitizedBundleCode) || + !sanitizedBundleCode.includes('__webpack_require__.mfAsyncStartup') + ) { + return undefined; + } + + const patchedCode = sanitizedBundleCode.replace( + ASYNC_NODE_STARTUP_CALL_PATTERN, + 'var __webpack_exports__ = __webpack_require__.x({}, []);', + ); + + const localModule: { exports: unknown } = { exports: {} }; + const wrapped = `(function(exports, require, module, __filename, __dirname){${patchedCode}\n})`; + const runBundle = vm.runInThisContext(wrapped, { filename: filepath }) as ( + exports: unknown, + require: NodeJS.Require, + module: { exports: unknown }, + __filename: string, + __dirname: string, + ) => void; + const bundleRequire = module.createRequire(filepath); + + runBundle( + localModule.exports, + bundleRequire, + localModule, + filepath, + path.dirname(filepath), + ); + + if (isPromiseLike(localModule.exports)) { + return await localModule.exports; + } + + return localModule.exports; + } catch (error) { + context?.monitors?.error?.( + '[MF] Load async startup bundle strategy failed, error = %s', + error instanceof Error ? error.stack || error.message : error, + ); + return undefined; + } +}; diff --git a/packages/modernjs-v3/src/server/index.ts b/packages/modernjs-v3/src/server/index.ts index 84ab23928cc..8b41249857a 100644 --- a/packages/modernjs-v3/src/server/index.ts +++ b/packages/modernjs-v3/src/server/index.ts @@ -1,12 +1,84 @@ +import fs from 'node:fs'; +import path from 'node:path'; import type { ServerPlugin } from '@modern-js/server-runtime'; +import { mfAsyncStartupLoaderStrategy } from './asyncStartupLoader'; import { createCorsMiddleware, createStaticMiddleware, } from './staticMiddleware'; +const findPackageRootFromResolvedEntry = ( + resolvedEntryPath: string, + packageName: string, +): string | undefined => { + let currentDir = path.dirname(resolvedEntryPath); + while (currentDir !== path.dirname(currentDir)) { + const packageJsonPath = path.resolve(currentDir, 'package.json'); + if (fs.existsSync(packageJsonPath)) { + try { + const { name } = JSON.parse( + fs.readFileSync(packageJsonPath, 'utf-8'), + ) as { name?: string }; + if (name === packageName) { + return currentDir; + } + } catch { + // Ignore malformed package.json and continue searching upwards. + } + } + currentDir = path.dirname(currentDir); + } + return undefined; +}; + +const resolveServerCoreNodeEntry = (): string | undefined => { + const candidateBasePaths: string[] = []; + + try { + const appToolsEntry = require.resolve('@modern-js/app-tools', { + paths: [process.cwd(), __dirname], + }); + const appToolsRoot = findPackageRootFromResolvedEntry( + appToolsEntry, + '@modern-js/app-tools', + ); + if (appToolsRoot) { + candidateBasePaths.push(appToolsRoot); + } + } catch { + // app-tools may not be installed in all environments. + } + + candidateBasePaths.push(process.cwd(), __dirname); + + for (const basePath of candidateBasePaths) { + try { + return require.resolve('@modern-js/server-core/node', { + paths: [basePath], + }); + } catch { + // Try next base path. + } + } + + return undefined; +}; + const staticServePlugin = (): ServerPlugin => ({ name: '@modern-js/module-federation/server', setup: (api) => { + try { + const serverCoreNodeEntry = resolveServerCoreNodeEntry(); + if (serverCoreNodeEntry) { + const { registerBundleLoaderStrategy } = require(serverCoreNodeEntry); + if (typeof registerBundleLoaderStrategy === 'function') { + registerBundleLoaderStrategy(mfAsyncStartupLoaderStrategy); + } + } + } catch { + // registerBundleLoaderStrategy may not exist in all @modern-js/server-core versions + } + 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') { diff --git a/packages/modernjs-v3/src/server/server-core-node.d.ts b/packages/modernjs-v3/src/server/server-core-node.d.ts new file mode 100644 index 00000000000..3adf89a7b5a --- /dev/null +++ b/packages/modernjs-v3/src/server/server-core-node.d.ts @@ -0,0 +1,17 @@ +/** + * Type declarations for @modern-js/server-core/node when the published + * package may not yet export registerBundleLoaderStrategy / BundleLoaderStrategy. + * Used for RSC + async startup loader strategy registration. + */ +declare module '@modern-js/server-core/node' { + export type BundleLoaderStrategy = ( + filepath: string, + context?: { + monitors?: { error?: (msg: string, ...args: unknown[]) => void }; + }, + ) => Promise; + + export function registerBundleLoaderStrategy( + strategy: BundleLoaderStrategy, + ): void; +} diff --git a/packages/modernjs-v3/src/server/staticMiddleware.spec.ts b/packages/modernjs-v3/src/server/staticMiddleware.spec.ts index 5908c694daf..4949c995fc0 100644 --- a/packages/modernjs-v3/src/server/staticMiddleware.spec.ts +++ b/packages/modernjs-v3/src/server/staticMiddleware.spec.ts @@ -87,6 +87,17 @@ describe('staticMiddleware', () => { expect(mockContext.body).not.toHaveBeenCalled(); }); + it('should call next() for paths that only share /bundles prefix', async () => { + mockContext.req.path = '/bundlesevil/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'; @@ -167,7 +178,7 @@ describe('staticMiddleware', () => { ); expect(mockContext.header).toHaveBeenCalledWith( 'Content-Length', - String(mockFileResult.content.length), + String(Buffer.byteLength(mockFileResult.content)), ); // Check response @@ -178,6 +189,26 @@ describe('staticMiddleware', () => { expect(result).toBe('response'); }); + it('should set content-length in bytes for unicode content', async () => { + const mockFileResult = { + content: '你', + lastModified: Date.now(), + }; + + mockContext.req.path = '/bundles/unicode.js'; + (fs.pathExists as any).mockResolvedValue(true); + (fileCache.getFile as any).mockResolvedValue(mockFileResult); + mockContext.body.mockReturnValue('unicode-response'); + + await middleware(mockContext, nextSpy); + + expect(mockContext.header).toHaveBeenCalledWith( + 'Content-Length', + String(Buffer.byteLength(mockFileResult.content)), + ); + expect(nextSpy).not.toHaveBeenCalled(); + }); + it('should handle empty file content', async () => { const mockFileResult = { content: '', diff --git a/packages/modernjs-v3/src/server/staticMiddleware.ts b/packages/modernjs-v3/src/server/staticMiddleware.ts index ca0ee4e0b30..335ce736f67 100644 --- a/packages/modernjs-v3/src/server/staticMiddleware.ts +++ b/packages/modernjs-v3/src/server/staticMiddleware.ts @@ -26,24 +26,37 @@ const createStaticMiddleware = (options: { pwd: string; }): MiddlewareHandler => { const { assetPrefix, pwd } = options; + const bundlesRootDir = path.resolve(pwd, `.${bundlesAssetPrefix}`); return async (c, next) => { const pathname = c.req.path; + const extension = path.extname(pathname); - // We only handle js file for performance - if (path.extname(pathname) !== '.js') { + // Only serve assets required by server-side federation runtime. + if (extension !== '.js' && extension !== '.json') { return next(); } const prefixWithoutHost = removeHost(assetPrefix); - const prefixWithBundle = path.join(prefixWithoutHost, bundlesAssetPrefix); + const prefixWithBundle = path.posix.join( + prefixWithoutHost || '/', + bundlesAssetPrefix, + ); + const isBundleRequest = + pathname === prefixWithBundle || + pathname.startsWith(`${prefixWithBundle}/`); // Skip if the request is not for asset prefix + `/bundles` - if (!pathname.startsWith(prefixWithBundle)) { + if (!isBundleRequest) { return next(); } - const pathnameWithoutPrefix = pathname.replace(prefixWithBundle, ''); - const filepath = path.join(pwd, bundlesAssetPrefix, pathnameWithoutPrefix); + const pathnameWithoutPrefix = pathname.slice(prefixWithBundle.length); + const relativeBundlePath = pathnameWithoutPrefix.replace(/^\/+/, ''); + const filepath = path.resolve(bundlesRootDir, relativeBundlePath); + const allowedPrefix = `${bundlesRootDir}${path.sep}`; + if (filepath !== bundlesRootDir && !filepath.startsWith(allowedPrefix)) { + return next(); + } if (!(await fs.pathExists(filepath))) { return next(); } @@ -53,8 +66,11 @@ const createStaticMiddleware = (options: { return next(); } - c.header('Content-Type', 'application/javascript'); - c.header('Content-Length', String(fileResult.content.length)); + c.header( + 'Content-Type', + extension === '.json' ? 'application/json' : 'application/javascript', + ); + c.header('Content-Length', String(Buffer.byteLength(fileResult.content))); return c.body(fileResult.content, 200); }; }; diff --git a/packages/modernjs-v3/tsconfig.json b/packages/modernjs-v3/tsconfig.json index 9c6a5722240..844190a8043 100644 --- a/packages/modernjs-v3/tsconfig.json +++ b/packages/modernjs-v3/tsconfig.json @@ -7,5 +7,6 @@ "paths": {}, "noImplicitAny": false }, - "include": ["src", "types", "global.d.ts"] + "include": ["src", "types", "global.d.ts"], + "exclude": ["**/*.spec.*", "**/*.test.*"] } diff --git a/packages/rspack/src/ModuleFederationPlugin.ts b/packages/rspack/src/ModuleFederationPlugin.ts index f19e036b5f7..5b3d42f99d9 100644 --- a/packages/rspack/src/ModuleFederationPlugin.ts +++ b/packages/rspack/src/ModuleFederationPlugin.ts @@ -13,10 +13,10 @@ import { import { StatsPlugin } from '@module-federation/manifest'; import { ContainerManager, utils } from '@module-federation/managers'; -import { DtsPlugin } from '@module-federation/dts-plugin'; import ReactBridgePlugin from '@module-federation/bridge-react-webpack-plugin'; import path from 'node:path'; import fs from 'node:fs'; +import { createRequire } from 'node:module'; import { RemoteEntryPlugin } from './RemoteEntryPlugin'; import logger from './logger'; @@ -37,6 +37,21 @@ type RuntimeEntrySpec = { cjs: string; }; +const RUNTIME_TOOLS_REQUEST = '@module-federation/runtime-tools'; +const NODE_RUNTIME_PLUGIN = '@module-federation/node/runtimePlugin'; +const runtimeRequire: NodeJS.Require = createRequire( + path.join(process.cwd(), '__mf_rspack_runtime__.cjs'), +); + +type DtsPluginRuntimeInstance = { + apply(compiler: Compiler): void; + addRuntimePlugins(): void; +}; + +type DtsPluginConstructor = new ( + options: moduleFederationPlugin.ModuleFederationPluginOptions, +) => DtsPluginRuntimeInstance; + function resolveRuntimeEntry( spec: RuntimeEntrySpec, implementation: string | undefined, @@ -89,6 +104,102 @@ export function resolveRspackRuntimeAlias( ); } +function getTargetValues(target: unknown): string[] { + if (Array.isArray(target)) { + return target.filter((item): item is string => typeof item === 'string'); + } + return typeof target === 'string' ? [target] : []; +} + +function isNodeLikeTarget(target: unknown): boolean { + if (target === false) { + return false; + } + const targets = getTargetValues(target); + return targets.some((value) => value.includes('node')); +} + +function hasAsyncNodeTarget(target: unknown): boolean { + if (target === false) { + return false; + } + const targets = getTargetValues(target); + return targets.some((value) => value.includes('async-node')); +} + +function hasNodeRuntimePlugin( + runtimePlugins: moduleFederationPlugin.ModuleFederationPluginOptions['runtimePlugins'], +): boolean { + if (!Array.isArray(runtimePlugins)) { + return false; + } + + return runtimePlugins.some((plugin) => { + const entry = Array.isArray(plugin) ? plugin[0] : plugin; + if (typeof entry !== 'string') { + return false; + } + return ( + entry === NODE_RUNTIME_PLUGIN || + entry.includes('@module-federation/node/runtimePlugin') || + entry.includes('@module-federation/node/dist/src/runtimePlugin') + ); + }); +} + +function validateRscNodeRuntimePlugin( + options: moduleFederationPlugin.ModuleFederationPluginOptions, + target: unknown, +): void { + const isRscEnabled = + (options.experiments as { rsc?: boolean } | undefined)?.rsc === true; + + if (!isRscEnabled || !isNodeLikeTarget(target)) { + return; + } + + if (!hasAsyncNodeTarget(target)) { + throw new Error( + '[ModuleFederationPlugin.rsc] Invalid configuration:\n`target` must include `"async-node"`.', + ); + } + + if ( + (options.experiments as { asyncStartup?: boolean } | undefined) + ?.asyncStartup !== true + ) { + throw new Error( + '[ModuleFederationPlugin.rsc] Invalid configuration:\n`experiments.asyncStartup` must be `true`.', + ); + } + + if (!hasNodeRuntimePlugin(options.runtimePlugins)) { + options.runtimePlugins = [ + ...(options.runtimePlugins || []), + NODE_RUNTIME_PLUGIN, + ]; + } +} + +function resolveFromCompilerContext( + request: string, + compilerContext: string, +): string { + try { + return runtimeRequire.resolve(request, { + paths: [compilerContext], + }); + } catch {} + + return runtimeRequire.resolve(request); +} + +function loadDtsPlugin(): DtsPluginConstructor { + const request = ['@module-federation', 'dts-plugin'].join('/'); + const load = (id: string) => runtimeRequire(id); + return (load(request) as { DtsPlugin: DtsPluginConstructor }).DtsPlugin; +} + export class ModuleFederationPlugin implements RspackPluginInstance { readonly name = PLUGIN_NAME; private _options: moduleFederationPlugin.ModuleFederationPluginOptions; @@ -155,6 +266,7 @@ export class ModuleFederationPlugin implements RspackPluginInstance { apply(compiler: Compiler): void { bindLoggerToCompiler(logger, compiler, PLUGIN_NAME); const { _options: options } = this; + validateRscNodeRuntimePlugin(options, compiler.options.target); if (!options.name) { throw new Error('[ ModuleFederationPlugin ]: name is required'); @@ -180,7 +292,9 @@ export class ModuleFederationPlugin implements RspackPluginInstance { const runtimePlugins = options.runtimePlugins || []; options.runtimePlugins = runtimePlugins.concat( - require.resolve('@module-federation/inject-external-runtime-core-plugin'), + runtimeRequire.resolve( + '@module-federation/inject-external-runtime-core-plugin', + ), ); } @@ -192,13 +306,25 @@ export class ModuleFederationPlugin implements RspackPluginInstance { } const implementationPath = options.implementation - ? options.implementation - : resolveRspackRuntimeImplementation(); - options.implementation = implementationPath; + ? resolveFromCompilerContext(options.implementation, compiler.context) + : resolveFromCompilerContext(RUNTIME_TOOLS_REQUEST, compiler.context); + const implementationResolveBase = path.dirname(implementationPath); + const resolvedImplementationPath = (() => { + try { + return resolveRspackRuntimeImplementation( + implementationResolveBase, + runtimeRequire.resolve.bind(runtimeRequire) as ResolveFn, + ); + } catch { + return implementationPath; + } + })(); + options.implementation = resolvedImplementationPath; let disableManifest = options.manifest === false; let disableDts = options.dts === false; if (!disableDts) { + const DtsPlugin = loadDtsPlugin(); const dtsPlugin = new DtsPlugin(options); // @ts-ignore dtsPlugin.apply(compiler); @@ -220,14 +346,41 @@ export class ModuleFederationPlugin implements RspackPluginInstance { options as unknown as ModuleFederationPluginOptions, ).apply(compiler); + const resolveRuntimePath = (candidates: string[]) => { + const resolvePaths = [ + compiler.context, + path.dirname(resolvedImplementationPath), + ]; + for (const candidate of candidates) { + for (const resolvePath of resolvePaths) { + try { + return runtimeRequire.resolve(candidate, { + paths: [resolvePath], + }); + } catch {} + } + } + throw new Error( + `[ ModuleFederationPlugin ]: Unable to resolve runtime entry from ${candidates.join( + ', ', + )}`, + ); + }; let runtimePath: string; try { - runtimePath = resolveRspackRuntimeAlias(implementationPath); - } catch (err) { - const detail = err instanceof Error ? err.message : String(err); - throw new Error( - `[ ModuleFederationPlugin ]: Unable to resolve runtime entry (paths: [${implementationPath}]): ${detail}`, + runtimePath = resolveRspackRuntimeAlias( + path.dirname(resolvedImplementationPath), + runtimeRequire.resolve.bind(runtimeRequire) as ResolveFn, ); + } catch { + runtimePath = resolveRuntimePath([ + '@module-federation/runtime/bundler', + '@module-federation/runtime/dist/index.js', + '@module-federation/runtime/dist/index.esm.js', + '@module-federation/runtime/dist/index.cjs', + '@module-federation/runtime/dist/index.cjs.cjs', + '@module-federation/runtime', + ]); } compiler.hooks.afterPlugins.tap('PatchAliasWebpackPlugin', () => { diff --git a/packages/runtime-core/package.json b/packages/runtime-core/package.json index ae4cf82dd86..e3b20e3ae4a 100644 --- a/packages/runtime-core/package.json +++ b/packages/runtime-core/package.json @@ -21,24 +21,24 @@ ], "exports": { ".": { + "types": "./dist/index.d.ts", "import": { - "types": "./dist/index.d.ts", "default": "./dist/index.js" }, "require": { - "types": "./dist/index.d.ts", "default": "./dist/index.cjs" - } + }, + "default": "./dist/index.js" }, "./types": { + "types": "./dist/types.d.ts", "import": { - "types": "./dist/types.d.ts", "default": "./dist/types.js" }, "require": { - "types": "./dist/types.d.ts", "default": "./dist/types.cjs" - } + }, + "default": "./dist/types.js" } }, "typesVersions": { diff --git a/packages/runtime-core/src/plugins/snapshot/index.ts b/packages/runtime-core/src/plugins/snapshot/index.ts index fa09668b674..b1fa93fd3f2 100644 --- a/packages/runtime-core/src/plugins/snapshot/index.ts +++ b/packages/runtime-core/src/plugins/snapshot/index.ts @@ -25,8 +25,21 @@ export function assignRemoteInfo( let entryUrl = getResourceUrl(remoteSnapshot, remoteEntryInfo.url); - if (!isBrowserEnvValue && !entryUrl.startsWith('http')) { - entryUrl = `https:${entryUrl}`; + if (!isBrowserEnvValue) { + if (entryUrl.startsWith('//')) { + entryUrl = `https:${entryUrl}`; + } else if ( + !entryUrl.startsWith('http://') && + !entryUrl.startsWith('https://') + ) { + try { + entryUrl = new URL(entryUrl, remoteInfo.entry).href; + } catch { + error( + `Unable to resolve remote entry URL for ${remoteInfo.name}. entry="${entryUrl}", manifest="${remoteInfo.entry}"`, + ); + } + } } remoteInfo.type = remoteEntryInfo.type; diff --git a/packages/runtime/package.json b/packages/runtime/package.json index f7bd7b560e9..b213eed71bc 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -24,44 +24,44 @@ ], "exports": { ".": { + "types": "./dist/index.d.ts", "import": { - "types": "./dist/index.d.ts", "default": "./dist/index.js" }, "require": { - "types": "./dist/index.d.ts", "default": "./dist/index.cjs" - } + }, + "default": "./dist/index.js" }, "./helpers": { + "types": "./dist/helpers.d.ts", "import": { - "types": "./dist/helpers.d.ts", "default": "./dist/helpers.js" }, "require": { - "types": "./dist/helpers.d.ts", "default": "./dist/helpers.cjs" - } + }, + "default": "./dist/helpers.js" }, "./types": { + "types": "./dist/types.d.ts", "import": { - "types": "./dist/types.d.ts", "default": "./dist/types.js" }, "require": { - "types": "./dist/types.d.ts", "default": "./dist/types.cjs" - } + }, + "default": "./dist/types.js" }, "./core": { + "types": "./dist/core.d.ts", "import": { - "types": "./dist/core.d.ts", "default": "./dist/core.js" }, "require": { - "types": "./dist/core.d.ts", "default": "./dist/core.cjs" - } + }, + "default": "./dist/core.js" }, "./bundler": "./dist/bundler.js", "./*": "./*" diff --git a/packages/sdk/__tests__/nodeScriptUtils.spec.ts b/packages/sdk/__tests__/nodeScriptUtils.spec.ts new file mode 100644 index 00000000000..7a74531662e --- /dev/null +++ b/packages/sdk/__tests__/nodeScriptUtils.spec.ts @@ -0,0 +1,73 @@ +import { + patchNodeRemoteEntryCode, + resolveNodeScriptExports, +} from '../src/nodeScriptUtils'; + +describe('nodeScriptUtils', () => { + it('patches async startup entries and strips encoded hmr bootstrap calls', () => { + const source = ` + var rscRemote; + var __webpack_require__ = function(){}; + __webpack_require__.mfAsyncStartup = function () {}; + __webpack_require__("data:text/javascript,import%20%7B%20init%20%7D%20from%20'hmr.js'%3B"); + var __webpack_exports__ = __webpack_require__.x(); + rscRemote = __webpack_exports__; + `; + + const patched = patchNodeRemoteEntryCode(source, { + globalName: 'rscRemote', + }); + + expect(patched).toContain('__webpack_require__.x({}, []);'); + expect(patched).not.toContain('data:text/javascript'); + expect(patched).toContain('module.exports = rscRemote'); + }); + + it('does not append module export shim for invalid global identifiers', () => { + const source = ` + var __webpack_require__ = function(){}; + var __webpack_exports__ = __webpack_require__.x(); + `; + + const patched = patchNodeRemoteEntryCode(source, { + globalName: '__FEDERATION_remote:key__', + }); + + expect(patched).not.toContain('module.exports = __FEDERATION_remote:key__'); + }); + + it('resolves promise-like exports and falls back to global container', async () => { + const key = '__FEDERATION_remote:key__'; + const globalContainer = { init: () => undefined, get: () => undefined }; + (global as Record)[key] = globalContainer; + + const resolvedFromPromise = await resolveNodeScriptExports( + { + exports: {}, + module: { + exports: Promise.resolve({ + init: () => undefined, + get: () => undefined, + }), + }, + }, + { globalName: key }, + ); + + expect(typeof (resolvedFromPromise as Record).init).toBe( + 'function', + ); + + const resolvedFromGlobal = await resolveNodeScriptExports( + { + exports: {}, + module: { exports: {} }, + }, + { globalName: key }, + ); + + expect(resolvedFromGlobal).toBe(globalContainer); + + delete (global as Record)[key]; + }); +}); diff --git a/packages/sdk/package.json b/packages/sdk/package.json index d6c0a140549..f429ba5a507 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -31,24 +31,24 @@ }, "exports": { ".": { + "types": "./dist/index.d.ts", "import": { - "types": "./dist/index.d.ts", "default": "./dist/index.js" }, "require": { - "types": "./dist/index.d.ts", "default": "./dist/index.cjs" - } + }, + "default": "./dist/index.js" }, "./normalize-webpack-path": { + "types": "./dist/normalize-webpack-path.d.ts", "import": { - "types": "./dist/normalize-webpack-path.d.ts", "default": "./dist/normalize-webpack-path.js" }, "require": { - "types": "./dist/normalize-webpack-path.d.ts", "default": "./dist/normalize-webpack-path.cjs" - } + }, + "default": "./dist/normalize-webpack-path.js" } }, "typesVersions": { diff --git a/packages/sdk/src/node.ts b/packages/sdk/src/node.ts index c51fc91ebab..11d31028d38 100644 --- a/packages/sdk/src/node.ts +++ b/packages/sdk/src/node.ts @@ -1,4 +1,8 @@ import { CreateScriptHookNode, FetchHook } from './types'; +import { + patchNodeRemoteEntryCode, + resolveNodeScriptExports, +} from './nodeScriptUtils'; // Declare the ENV_TARGET constant that will be defined by DefinePlugin declare const ENV_TARGET: 'web' | 'node'; @@ -99,6 +103,7 @@ export const createScriptNode = try { const res = await f(urlObj.href); const data = await res.text(); + const patchedData = patchNodeRemoteEntryCode(data, attrs); const [path, vm] = await Promise.all([ importNodeModule('path'), importNodeModule('vm'), @@ -112,7 +117,7 @@ export const createScriptNode = const filename = path.basename(urlObj.pathname); const script = new vm.Script( - `(function(exports, module, require, __dirname, __filename) {${data}\n})`, + `(function(exports, module, require, __dirname, __filename) {${patchedData}\n})`, { filename, importModuleDynamically: @@ -144,12 +149,16 @@ export const createScriptNode = urlDirname, filename, ); - const exportedInterface: Record = - scriptContext.module.exports || scriptContext.exports; + const exportedInterface = await resolveNodeScriptExports( + scriptContext, + attrs, + ); if (attrs && exportedInterface && attrs['globalName']) { const container = - exportedInterface[attrs['globalName']] || exportedInterface; + (exportedInterface as Record)[ + attrs['globalName'] + ] || exportedInterface; cb( undefined, container as keyof typeof scriptContext.module.exports, diff --git a/packages/sdk/src/nodeScriptUtils.ts b/packages/sdk/src/nodeScriptUtils.ts new file mode 100644 index 00000000000..d4491761e69 --- /dev/null +++ b/packages/sdk/src/nodeScriptUtils.ts @@ -0,0 +1,84 @@ +const ASYNC_NODE_STARTUP_CALL_PATTERN = + /var\s+__webpack_exports__\s*=\s*__webpack_require__\.x\(\s*\)\s*;/; +const ENCODED_HMR_CLIENT_BOOTSTRAP_CALL = + /__webpack_require__\("data:text\/javascript,[^"]*"\);\s*/g; + +const isPromiseLike = (value: unknown): value is Promise => + Boolean(value) && + (typeof value === 'object' || typeof value === 'function') && + 'then' in (value as Promise) && + typeof (value as Promise).then === 'function'; + +const isValidIdentifier = (value: string): boolean => + /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value); + +export const patchNodeRemoteEntryCode = ( + code: string, + attrs?: Record, +): string => { + let patchedCode = code.replace(ENCODED_HMR_CLIENT_BOOTSTRAP_CALL, ''); + + if ( + ASYNC_NODE_STARTUP_CALL_PATTERN.test(patchedCode) && + patchedCode.includes('__webpack_require__.mfAsyncStartup') + ) { + patchedCode = patchedCode.replace( + ASYNC_NODE_STARTUP_CALL_PATTERN, + 'var __webpack_exports__ = __webpack_require__.x({}, []);', + ); + } + + const globalName = attrs?.['globalName']; + if (typeof globalName === 'string' && isValidIdentifier(globalName)) { + patchedCode = `${patchedCode} +;if (typeof ${globalName} !== "undefined") { module.exports = ${globalName}; }`; + } + + return patchedCode; +}; + +export const resolveNodeScriptExports = async ( + scriptContext: { exports: unknown; module: { exports: unknown } }, + attrs?: Record, +): Promise => { + const globalName = attrs?.['globalName']; + let exportedInterface = scriptContext.module.exports || scriptContext.exports; + + if (isPromiseLike(exportedInterface)) { + exportedInterface = await exportedInterface; + } + + if ( + typeof globalName === 'string' && + exportedInterface && + typeof exportedInterface === 'object' && + !Array.isArray(exportedInterface) && + globalName in (exportedInterface as Record) + ) { + exportedInterface = (exportedInterface as Record)[ + globalName + ]; + } + + const globalContainer = + typeof globalName === 'string' + ? (global as Record)[globalName] + : undefined; + + if ( + globalContainer !== undefined && + scriptContext.module.exports === exportedInterface && + exportedInterface && + typeof exportedInterface === 'object' && + !Array.isArray(exportedInterface) && + Object.keys(exportedInterface as Record).length === 0 + ) { + exportedInterface = globalContainer; + } + + if (isPromiseLike(exportedInterface)) { + exportedInterface = await exportedInterface; + } + + return exportedInterface; +}; diff --git a/packages/sdk/src/types/plugins/ContainerPlugin.ts b/packages/sdk/src/types/plugins/ContainerPlugin.ts index 041295c4ad8..ca4441599c5 100644 --- a/packages/sdk/src/types/plugins/ContainerPlugin.ts +++ b/packages/sdk/src/types/plugins/ContainerPlugin.ts @@ -4,11 +4,7 @@ * Run `pnpm generate:schema -w` to update. */ -import type { - Exposes, - EntryRuntime, - LibraryOptions, -} from './ModuleFederationPlugin'; +import type { Exposes, EntryRuntime, LibraryOptions } from './ModuleFederationPlugin'; export interface ContainerPluginOptions { exposes: Exposes; diff --git a/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts b/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts index 1b5bfc08a5b..5fba7d4dad1 100644 --- a/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts +++ b/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts @@ -31,6 +31,10 @@ export interface ExposesConfig { * Custom chunk name for the exposed module. */ name?: string; + /** + * Layer in which the exposed module should be placed. + */ + layer?: string; } /** diff --git a/packages/webpack-bundler-runtime/__tests__/resolveRemoteModuleId.spec.ts b/packages/webpack-bundler-runtime/__tests__/resolveRemoteModuleId.spec.ts new file mode 100644 index 00000000000..3a0c03c0393 --- /dev/null +++ b/packages/webpack-bundler-runtime/__tests__/resolveRemoteModuleId.spec.ts @@ -0,0 +1,127 @@ +import { resolveRemoteModuleId } from '../src/resolveRemoteModuleId'; + +describe('resolveRemoteModuleId', () => { + test('resolves wrapper module id by alias and expose', () => { + const webpackRequire = { + remotesLoadingData: { + moduleIdToRemoteDataMapping: { + 'webpack/container/remote/rscRemote/./Dialog': { + name: './Dialog', + remoteName: 'rscRemote', + }, + }, + }, + federation: { + bundlerRuntimeOptions: { + remotes: { + remoteInfos: { + rscRemote: [{ name: 'rscRemote' }], + }, + }, + }, + }, + } as any; + + expect( + resolveRemoteModuleId({ + webpackRequire, + alias: 'rscRemote', + expose: './Dialog', + }), + ).toBe('webpack/container/remote/rscRemote/./Dialog'); + }); + + test('matches expose with and without ./ prefix', () => { + const webpackRequire = { + remotesLoadingData: { + moduleIdToRemoteDataMapping: { + 'webpack/container/remote/rscRemote/./Dialog': { + name: './Dialog', + remoteName: 'rscRemote', + }, + }, + }, + federation: { + bundlerRuntimeOptions: { + remotes: { + remoteInfos: { + rscRemote: [{ name: 'rscRemote' }], + }, + }, + }, + }, + } as any; + + expect( + resolveRemoteModuleId({ + webpackRequire, + alias: 'rscRemote', + expose: 'Dialog', + }), + ).toBe('webpack/container/remote/rscRemote/./Dialog'); + }); + + test('supports alias->remoteName mapping from remoteInfos', () => { + const webpackRequire = { + remotesLoadingData: { + moduleIdToRemoteDataMapping: { + 'webpack/container/remote/app2/./Dialog': { + name: './Dialog', + remoteName: 'app2', + }, + }, + }, + federation: { + bundlerRuntimeOptions: { + remotes: { + remoteInfos: { + rscRemote: [{ name: 'app2' }], + }, + }, + }, + }, + } as any; + + expect( + resolveRemoteModuleId({ + webpackRequire, + alias: 'rscRemote', + expose: './Dialog', + }), + ).toBe('webpack/container/remote/app2/./Dialog'); + }); + + test('returns undefined when mapping is ambiguous', () => { + const webpackRequire = { + remotesLoadingData: { + moduleIdToRemoteDataMapping: { + 'webpack/container/remote/rscRemote/./DialogA': { + name: './Dialog', + remoteName: 'rscRemote', + }, + 'webpack/container/remote/rscRemote/./DialogB': { + name: './Dialog', + remoteName: 'rscRemote', + }, + }, + }, + federation: { + bundlerRuntimeOptions: { + remotes: { + remoteInfos: { + rscRemote: [{ name: 'rscRemote' }], + }, + }, + }, + }, + } as any; + + expect( + resolveRemoteModuleId({ + webpackRequire, + alias: 'rscRemote', + expose: './Dialog', + }), + ).toBeUndefined(); + }); +}); diff --git a/packages/webpack-bundler-runtime/package.json b/packages/webpack-bundler-runtime/package.json index 1ce34c55780..00073cfa44e 100644 --- a/packages/webpack-bundler-runtime/package.json +++ b/packages/webpack-bundler-runtime/package.json @@ -31,24 +31,44 @@ }, "exports": { ".": { + "types": "./dist/index.d.ts", "import": { - "types": "./dist/index.d.ts", "default": "./dist/index.js" }, "require": { - "types": "./dist/index.d.ts", "default": "./dist/index.cjs" - } + }, + "default": "./dist/index.js" }, "./constant": { + "types": "./dist/constant.d.ts", "import": { - "types": "./dist/constant.d.ts", "default": "./dist/constant.js" }, "require": { - "types": "./dist/constant.d.ts", "default": "./dist/constant.cjs" - } + }, + "default": "./dist/constant.js" + }, + "./rsc-bridge-runtime-plugin": { + "types": "./dist/rsc-bridge-runtime-plugin.d.ts", + "import": { + "default": "./dist/rsc-bridge-runtime-plugin.js" + }, + "require": { + "default": "./dist/rsc-bridge-runtime-plugin.cjs" + }, + "default": "./dist/rsc-bridge-runtime-plugin.js" + }, + "./rsc-bridge-expose": { + "types": "./dist/rsc-bridge-expose.d.ts", + "import": { + "default": "./dist/rsc-bridge-expose.js" + }, + "require": { + "default": "./dist/rsc-bridge-expose.cjs" + }, + "default": "./dist/rsc-bridge-expose.js" }, "./bundler": "./dist/bundler.js", "./*": "./*" @@ -61,6 +81,12 @@ "constant": [ "./dist/constant.d.ts" ], + "rsc-bridge-runtime-plugin": [ + "./dist/rsc-bridge-runtime-plugin.d.ts" + ], + "rsc-bridge-expose": [ + "./dist/rsc-bridge-expose.d.ts" + ], "bundler": [ "./dist/bundler.d.ts" ] diff --git a/packages/webpack-bundler-runtime/src/index.ts b/packages/webpack-bundler-runtime/src/index.ts index dc9b130f0eb..97bc908702b 100644 --- a/packages/webpack-bundler-runtime/src/index.ts +++ b/packages/webpack-bundler-runtime/src/index.ts @@ -8,8 +8,10 @@ import { attachShareScopeMap } from './attachShareScopeMap'; import { initContainerEntry } from './initContainerEntry'; import { init } from './init'; import { getSharedFallbackGetter } from './getSharedFallbackGetter'; +import { resolveRemoteModuleId } from './resolveRemoteModuleId'; export * from './types'; +export * from './rscManifest'; const federation: Federation = { runtime, @@ -23,6 +25,7 @@ const federation: Federation = { installInitialConsumes, initContainerEntry, init, + resolveRemoteModuleId, getSharedFallbackGetter, }, attachShareScopeMap, diff --git a/packages/webpack-bundler-runtime/src/initContainerEntry.ts b/packages/webpack-bundler-runtime/src/initContainerEntry.ts index f92d38df3d3..ca786810d9c 100644 --- a/packages/webpack-bundler-runtime/src/initContainerEntry.ts +++ b/packages/webpack-bundler-runtime/src/initContainerEntry.ts @@ -28,6 +28,12 @@ export function initContainerEntry( const hostShareScopeKeys = remoteEntryInitOptions?.shareScopeKeys; const hostShareScopeMap = remoteEntryInitOptions?.shareScopeMap; + const hasHostShareScopeArray = Array.isArray(hostShareScopeKeys); + const normalizedHostShareScopeKeys = Array.isArray(hostShareScopeKeys) + ? hostShareScopeKeys + : typeof hostShareScopeKeys === 'string' && hostShareScopeKeys + ? [hostShareScopeKeys] + : []; // host: 'default' remote: 'default' remote['default'] = hostShareScopeMap['default'] // host: ['default', 'scope1'] remote: 'default' remote['default'] = hostShareScopeMap['default']; remote['scope1'] = hostShareScopeMap['scop1'] @@ -35,7 +41,7 @@ export function initContainerEntry( // host: ['scope1','default'] remote: ['scope1','scope2'] => remote['scope1'] = hostShareScopeMap['scope1']; remote['scope2'] = hostShareScopeMap['scope2'] = {}; if (!shareScopeKey || typeof shareScopeKey === 'string') { const key = shareScopeKey || 'default'; - if (Array.isArray(hostShareScopeKeys)) { + if (normalizedHostShareScopeKeys.length > 0) { // const sc = hostShareScopeMap![key]; // if (!sc) { // throw new Error('shareScopeKey is not exist in hostShareScopeMap'); @@ -43,12 +49,29 @@ export function initContainerEntry( // federationInstance.initShareScopeMap(key, sc, { // hostShareScopeMap: remoteEntryInitOptions?.shareScopeMap || {}, // }); + const shareScopeKeysToInit = Array.from( + new Set( + hasHostShareScopeArray + ? normalizedHostShareScopeKeys + : normalizedHostShareScopeKeys.includes(key) + ? normalizedHostShareScopeKeys + : [key, ...normalizedHostShareScopeKeys], + ), + ); - hostShareScopeKeys.forEach((hostKey) => { - if (!hostShareScopeMap![hostKey]) { - hostShareScopeMap![hostKey] = {}; + shareScopeKeysToInit.forEach((hostKey, index) => { + if (!hostShareScopeMap?.[hostKey]) { + if (hostShareScopeMap) { + hostShareScopeMap[hostKey] = hasHostShareScopeArray + ? {} + : hostKey === key || index === 0 + ? shareScope + : {}; + } } - const sc = hostShareScopeMap![hostKey]; + const sc = + hostShareScopeMap?.[hostKey] || + (hasHostShareScopeArray ? {} : shareScope); federationInstance.initShareScopeMap(hostKey, sc, { hostShareScopeMap: remoteEntryInitOptions?.shareScopeMap || {}, }); @@ -85,8 +108,35 @@ export function initContainerEntry( } if (!Array.isArray(shareScopeKey)) { + const key = shareScopeKey || 'default'; + if (normalizedHostShareScopeKeys.length > 0) { + const uniqueHostKeys = Array.from(new Set(normalizedHostShareScopeKeys)); + const additionalHostKeys = uniqueHostKeys.filter( + (hostKey) => hostKey !== key, + ); + if (additionalHostKeys.length > 0) { + // Initialize host-provided scopes as well so layered shares (ssr/rsc) + // are available even when the remote container's declared scope is "default". + // @ts-ignore + const primaryResult = webpackRequire.I(key, initScope); + // @ts-ignore + const additionalResults = additionalHostKeys.map((hostKey) => + webpackRequire.I(hostKey, initScope), + ); + const asyncResults = [primaryResult, ...additionalResults].filter( + (result): result is Promise => + Boolean( + result && typeof (result as Promise).then === 'function', + ), + ); + if (asyncResults.length > 0) { + return Promise.all(asyncResults).then(() => primaryResult); + } + return primaryResult; + } + } // @ts-ignore - return webpackRequire.I(shareScopeKey || 'default', initScope); + return webpackRequire.I(key, initScope); } var proxyInitializeSharing = Boolean( diff --git a/packages/webpack-bundler-runtime/src/remotes.ts b/packages/webpack-bundler-runtime/src/remotes.ts index 8682d25de31..9f3a6421224 100644 --- a/packages/webpack-bundler-runtime/src/remotes.ts +++ b/packages/webpack-bundler-runtime/src/remotes.ts @@ -118,10 +118,11 @@ export function remotes(options: RemotesOptions) { if (instance.options.shareStrategy === 'version-first') { const shareScopes = Array.isArray(data[0]) ? data[0] : [data[0]]; - return Promise.all( - shareScopes.map((shareScope) => - instance.sharedHandler.initializeSharing(shareScope), - ), + const shareScopeInput = + shareScopes.length === 1 ? shareScopes[0] : shareScopes; + + return Promise.resolve( + instance.sharedHandler.initializeSharing(shareScopeInput), ).then(() => { return loadRemote(); }); diff --git a/packages/webpack-bundler-runtime/src/resolveRemoteModuleId.ts b/packages/webpack-bundler-runtime/src/resolveRemoteModuleId.ts new file mode 100644 index 00000000000..a82ee737bec --- /dev/null +++ b/packages/webpack-bundler-runtime/src/resolveRemoteModuleId.ts @@ -0,0 +1,77 @@ +import type { ResolveRemoteModuleIdOptions } from './types'; + +const normalizeExpose = (expose: string) => { + if (!expose || expose === '.') { + return '.'; + } + return expose.startsWith('./') ? expose : `./${expose}`; +}; + +const normalizeExposeCandidates = (expose: string): string[] => { + const normalized = normalizeExpose(expose); + if (normalized === '.') { + return ['.']; + } + if (normalized.startsWith('./')) { + return [normalized, normalized.slice(2)]; + } + return [normalized, `./${normalized}`]; +}; + +const getRemoteNameCandidates = ( + options: ResolveRemoteModuleIdOptions, +): Set => { + const names = new Set([options.alias]); + const remoteInfos = + options.webpackRequire.federation?.bundlerRuntimeOptions?.remotes + ?.remoteInfos; + const aliasRemoteInfos = remoteInfos?.[options.alias]; + if (Array.isArray(aliasRemoteInfos)) { + for (const info of aliasRemoteInfos) { + if (typeof info?.name === 'string' && info.name) { + names.add(info.name); + } + } + } + return names; +}; + +export function resolveRemoteModuleId( + options: ResolveRemoteModuleIdOptions, +): string | undefined { + const mapping = + options.webpackRequire.remotesLoadingData?.moduleIdToRemoteDataMapping; + if (!mapping) { + return undefined; + } + + const exposeCandidates = new Set(normalizeExposeCandidates(options.expose)); + const remoteNameCandidates = getRemoteNameCandidates(options); + let resolvedModuleId: string | undefined; + + for (const [moduleId, remoteData] of Object.entries(mapping)) { + if ( + !remoteData || + typeof remoteData.name !== 'string' || + !remoteData.name || + typeof remoteData.remoteName !== 'string' || + !remoteNameCandidates.has(remoteData.remoteName) + ) { + continue; + } + if (!exposeCandidates.has(remoteData.name)) { + continue; + } + + if (!resolvedModuleId) { + resolvedModuleId = moduleId; + continue; + } + + if (resolvedModuleId !== moduleId) { + return undefined; + } + } + + return resolvedModuleId; +} diff --git a/packages/webpack-bundler-runtime/src/rscBridgeExpose.ts b/packages/webpack-bundler-runtime/src/rscBridgeExpose.ts new file mode 100644 index 00000000000..ce7b5190468 --- /dev/null +++ b/packages/webpack-bundler-runtime/src/rscBridgeExpose.ts @@ -0,0 +1,877 @@ +import { + type ManifestExport, + type ManifestLike, + type ManifestNode, + getClientManifestKeyWithoutHash, + resolveActionReference, + resolveServerModuleReference, +} from './rscManifest'; + +type WebpackRequireRuntime = { + (moduleId: string): unknown; + initializeExposesData?: { + moduleMap?: Record Promise<() => unknown> | (() => unknown)>; + }; + initializeSharingData?: { + scopeToSharingDataMapping?: Record; + }; + e?: (chunkId: string | number) => unknown; + I?: (shareScope: string, initScope?: unknown) => unknown; + rscM?: ManifestLike; +}; + +declare const __webpack_require__: WebpackRequireRuntime; + +const shouldDebug = + typeof process !== 'undefined' && Boolean(process.env?.RSC_MF_DEBUG); + +const debugLog = (message: string, data?: Record) => { + if (!shouldDebug) { + return; + } + if (data) { + // eslint-disable-next-line no-console + console.error('[mf:rsc-bridge-expose]', message, data); + return; + } + // eslint-disable-next-line no-console + console.error('[mf:rsc-bridge-expose]', message); +}; + +const CLIENT_REFERENCE_SYMBOL = Symbol.for('react.client.reference'); +const BRIDGE_EXPOSE_KEY = './__rspack_rsc_bridge__'; +const RSC_SSR_EXPOSE_PREFIX = './__rspack_rsc_ssr__/'; +const SSR_LAYER_PREFIX = '(server-side-rendering)/'; +const RSC_LAYER_PREFIX = '(react-server-components)/'; + +const actionReferenceCache: Record unknown> = + Object.create(null); +const clientExposeMap: Record = Object.create(null); +const ssrModuleCache: Record = Object.create(null); +const ssrExposeByServerModuleId: Record = Object.create(null); +const shareScopeInitPromises: Partial>> = + Object.create(null); +let scannedExposes = false; +let scannedSsrExposeMap = false; + +const isObject = (value: unknown): value is Record => + typeof value === 'object' && value !== null; + +const normalizeExpose = (expose: string) => + expose.startsWith('./') ? expose : `./${expose}`; + +const getClientReferenceIdentity = ( + value: unknown, +): { key: string; baseKey: string } | undefined => { + if (!isObject(value) && typeof value !== 'function') { + return undefined; + } + const candidate = value as { + $$typeof?: unknown; + $$id?: unknown; + }; + if ( + candidate.$$typeof !== CLIENT_REFERENCE_SYMBOL || + typeof candidate.$$id !== 'string' || + !candidate.$$id + ) { + return undefined; + } + const key = candidate.$$id; + const hashIndex = key.indexOf('#'); + const baseKey = hashIndex >= 0 ? key.slice(0, hashIndex) : key; + return { key, baseKey }; +}; + +const cacheActionReference = (value: unknown) => { + if (typeof value !== 'function') { + return; + } + const id = (value as { $$id?: unknown }).$$id; + if (typeof id === 'string' && id) { + actionReferenceCache[id] = value as (...args: unknown[]) => unknown; + } +}; + +const cacheClientExpose = (value: unknown, exposeName: string) => { + const identity = getClientReferenceIdentity(value); + if (!identity) { + return; + } + const normalizedExpose = normalizeExpose(exposeName); + if (!Object.prototype.hasOwnProperty.call(clientExposeMap, identity.key)) { + clientExposeMap[identity.key] = normalizedExpose; + } + if ( + !Object.prototype.hasOwnProperty.call(clientExposeMap, identity.baseKey) + ) { + clientExposeMap[identity.baseKey] = normalizedExpose; + } +}; + +const inspectExportValue = (value: unknown, exposeName: string) => { + cacheActionReference(value); + cacheClientExpose(value, exposeName); + if (!isObject(value)) { + return; + } + for (const nestedValue of Object.values(value)) { + cacheActionReference(nestedValue); + cacheClientExpose(nestedValue, exposeName); + } +}; + +const toSsrExposeKey = (exposeName: string) => { + if (!exposeName) { + return ''; + } + const normalized = exposeName.startsWith('./') + ? exposeName.slice(2) + : exposeName; + return `${RSC_SSR_EXPOSE_PREFIX}${normalized}`; +}; + +const stripSsrLayerPrefix = (moduleId: string) => + moduleId.startsWith(SSR_LAYER_PREFIX) + ? moduleId.slice(SSR_LAYER_PREFIX.length) + : moduleId; + +const resolveHiddenSsrExposeFromClientExposeMap = ( + moduleId: string, + moduleMap?: Record Promise<() => unknown> | (() => unknown)>, +) => { + if (!isObject(moduleMap)) { + return ''; + } + const moduleIdCandidates = [moduleId, stripSsrLayerPrefix(moduleId)]; + for (const moduleIdCandidate of moduleIdCandidates) { + const exposeName = clientExposeMap[moduleIdCandidate]; + if (typeof exposeName !== 'string' || !exposeName) { + continue; + } + const hiddenExpose = toSsrExposeKey(exposeName); + if (hiddenExpose && typeof moduleMap[hiddenExpose] === 'function') { + return hiddenExpose; + } + } + return ''; +}; + +const resolveHiddenSsrExposeFromHint = ( + exposeHint: string | undefined, + moduleMap?: Record Promise<() => unknown> | (() => unknown)>, +) => { + if (!exposeHint || !isObject(moduleMap)) { + return ''; + } + const normalizedExpose = normalizeExpose(exposeHint); + const hiddenExpose = normalizedExpose.startsWith(RSC_SSR_EXPOSE_PREFIX) + ? normalizedExpose + : toSsrExposeKey(normalizedExpose); + if ( + hiddenExpose && + typeof hiddenExpose === 'string' && + typeof moduleMap[hiddenExpose] === 'function' + ) { + return hiddenExpose; + } + return ''; +}; + +const resolveClientManifestEntry = ( + clientManifest: Record, + clientReferenceId: string, +): ManifestExport | null => { + if (Object.prototype.hasOwnProperty.call(clientManifest, clientReferenceId)) { + return clientManifest[clientReferenceId] as ManifestExport; + } + const hashIndex = clientReferenceId.lastIndexOf('#'); + if (hashIndex >= 0) { + const withoutHash = clientReferenceId.slice(0, hashIndex); + if (Object.prototype.hasOwnProperty.call(clientManifest, withoutHash)) { + return clientManifest[withoutHash] as ManifestExport; + } + } + return null; +}; + +const extractChunkIds = (chunks: Array) => { + const chunkIds: Array = []; + const pushChunkId = (chunkId: string | number) => { + if (!chunkIds.includes(chunkId)) { + chunkIds.push(chunkId); + } + }; + + for (let i = 0; i < chunks.length; i += 1) { + const chunkValue = chunks[i]; + if (typeof chunkValue !== 'string' && typeof chunkValue !== 'number') { + continue; + } + + const nextValue = chunks[i + 1]; + if ( + typeof nextValue === 'string' && + (nextValue.endsWith('.js') || + nextValue.endsWith('.cjs') || + nextValue.endsWith('.mjs')) + ) { + pushChunkId(chunkValue); + i += 1; + continue; + } + + pushChunkId(chunkValue); + } + + return chunkIds; +}; + +const collectServerModuleChunkIds = ( + moduleId: string, + manifest?: ManifestLike, +) => { + const serverConsumerModuleMap = isObject(manifest?.serverConsumerModuleMap) + ? (manifest!.serverConsumerModuleMap as Record) + : undefined; + if (!serverConsumerModuleMap) { + return []; + } + + const chunkIds: Array = []; + const pushChunkIds = (nextChunkIds: Array) => { + for (const chunkId of nextChunkIds) { + if (!chunkIds.includes(chunkId)) { + chunkIds.push(chunkId); + } + } + }; + + for (const node of Object.values(serverConsumerModuleMap)) { + if (!isObject(node)) { + continue; + } + for (const exportValue of Object.values(node)) { + if (!isObject(exportValue) || exportValue.id == null) { + continue; + } + if (String(exportValue.id) !== moduleId) { + continue; + } + const chunks = Array.isArray(exportValue.chunks) + ? (exportValue.chunks as Array) + : []; + pushChunkIds(extractChunkIds(chunks)); + } + } + + return chunkIds; +}; + +const stripLayerPrefix = (moduleId: string) => { + if (moduleId.startsWith(SSR_LAYER_PREFIX)) { + return moduleId.slice(SSR_LAYER_PREFIX.length); + } + if (moduleId.startsWith(RSC_LAYER_PREFIX)) { + return moduleId.slice(RSC_LAYER_PREFIX.length); + } + return moduleId; +}; + +const getClientManifestKeysByModuleId = ( + clientManifest: Record, + clientModuleId: string, +) => + Object.entries(clientManifest) + .filter(([, value]) => String(value.id) === clientModuleId) + .map(([key]) => key); + +const selectClientReferenceKey = ( + candidateKeys: string[], + exportName: string, + exportValue?: ManifestExport, +) => { + if (candidateKeys.length === 0) { + return ''; + } + if (typeof exportValue?.name === 'string' && exportValue.name !== '*') { + const byManifestName = candidateKeys.find((key) => + key.endsWith(`#${exportValue.name}`), + ); + if (byManifestName) { + return byManifestName; + } + } + if (exportName !== '*' && exportName !== '__esModule') { + const byExportName = candidateKeys.find((key) => + key.endsWith(`#${exportName}`), + ); + if (byExportName) { + return byExportName; + } + } + const withoutHash = candidateKeys.find((key) => !key.includes('#')); + if (withoutHash) { + return withoutHash; + } + return candidateKeys[0]; +}; + +const createSyntheticClientReference = ( + referenceId: string, + asyncValue: boolean, +) => { + const referenceFn = function syntheticClientReference() { + throw new Error( + `Attempted to call client reference "${referenceId}" from the server`, + ); + } as ((...args: unknown[]) => never) & { + $$typeof?: symbol; + $$id?: string; + $$async?: boolean; + }; + referenceFn.$$typeof = CLIENT_REFERENCE_SYMBOL; + referenceFn.$$id = referenceId; + referenceFn.$$async = asyncValue; + return referenceFn; +}; + +const buildSyntheticSsrModuleFromManifest = ( + moduleId: string, + manifest?: ManifestLike, +) => { + const clientManifest = isObject(manifest?.clientManifest) + ? (manifest!.clientManifest as Record) + : undefined; + const serverConsumerModuleMap = isObject(manifest?.serverConsumerModuleMap) + ? (manifest!.serverConsumerModuleMap as Record) + : undefined; + if (!clientManifest || !serverConsumerModuleMap) { + return null; + } + + for (const [rawClientModuleId, node] of Object.entries( + serverConsumerModuleMap, + )) { + if (!isObject(node)) { + continue; + } + const normalizedClientModuleId = stripLayerPrefix(rawClientModuleId); + const clientManifestKeys = getClientManifestKeysByModuleId( + clientManifest, + normalizedClientModuleId, + ); + if (clientManifestKeys.length === 0) { + continue; + } + + const syntheticExports: Record = Object.create(null); + for (const [exportName, exportValue] of Object.entries(node)) { + if (!isObject(exportValue) || exportValue.id == null) { + continue; + } + if (String(exportValue.id) !== moduleId) { + continue; + } + + const manifestExport = exportValue as ManifestExport; + const referenceKey = selectClientReferenceKey( + clientManifestKeys, + exportName, + manifestExport, + ); + if (!referenceKey) { + continue; + } + const syntheticRef = createSyntheticClientReference( + referenceKey, + Boolean(manifestExport.async), + ); + if (exportName === '*') { + syntheticExports.default = syntheticRef; + continue; + } + if (exportName === '__esModule') { + continue; + } + syntheticExports[exportName] = syntheticRef; + } + + if (Object.keys(syntheticExports).length > 0) { + return syntheticExports; + } + } + + return null; +}; + +const preloadServerModuleChunks = async ( + moduleId: string, + manifest?: ManifestLike, +) => { + const chunkIds = collectServerModuleChunkIds(moduleId, manifest); + if (chunkIds.length === 0) { + return false; + } + if (typeof __webpack_require__.e !== 'function') { + throw new Error( + `[rsc-bridge-expose] Chunk loader "__webpack_require__.e" is unavailable while preloading server module "${moduleId}"`, + ); + } + await Promise.all( + chunkIds.map((chunkId) => { + const loadResult = __webpack_require__.e!(chunkId); + if ( + loadResult && + typeof (loadResult as Promise).then === 'function' + ) { + return loadResult as Promise; + } + return Promise.resolve(loadResult); + }), + ); + return true; +}; + +const collectClientReferenceIdsFromExports = ( + exportsValue: unknown, + ids: string[], +) => { + const maybeRef = exportsValue as { $$id?: unknown } | undefined; + if ( + typeof exportsValue === 'function' && + typeof maybeRef?.$$id === 'string' + ) { + ids.push(maybeRef.$$id); + } + if (!isObject(exportsValue)) { + return; + } + for (const nestedValue of Object.values(exportsValue)) { + const maybeNestedRef = nestedValue as { $$id?: unknown } | undefined; + if ( + typeof nestedValue === 'function' && + typeof maybeNestedRef?.$$id === 'string' + ) { + ids.push(maybeNestedRef.$$id); + } + } +}; + +const resolveFactoryExports = async (getFactory: () => unknown) => { + const factory = await Promise.resolve(getFactory()); + if (typeof factory === 'function') { + return (factory as () => unknown)(); + } + return factory; +}; + +const getShareScopesForServerModuleId = ( + moduleId: string, + manifest?: ManifestLike, +) => { + const scopes = new Set(['default']); + const declaredShareScopes = + __webpack_require__.initializeSharingData?.scopeToSharingDataMapping; + if (isObject(declaredShareScopes)) { + for (const shareScope of Object.keys(declaredShareScopes)) { + if (shareScope) { + scopes.add(shareScope); + } + } + } + if (moduleId.startsWith('(server-side-rendering)/')) { + scopes.add('ssr'); + } + if (moduleId.startsWith('(react-server-components)/')) { + scopes.add('rsc'); + } + const serverConsumerModuleMap = isObject(manifest?.serverConsumerModuleMap) + ? (manifest!.serverConsumerModuleMap as Record) + : undefined; + if (serverConsumerModuleMap) { + const unprefixedModuleId = moduleId.startsWith(SSR_LAYER_PREFIX) + ? moduleId.slice(SSR_LAYER_PREFIX.length) + : moduleId.startsWith(RSC_LAYER_PREFIX) + ? moduleId.slice(RSC_LAYER_PREFIX.length) + : moduleId; + + if ( + Object.prototype.hasOwnProperty.call( + serverConsumerModuleMap, + `${SSR_LAYER_PREFIX}${unprefixedModuleId}`, + ) + ) { + scopes.add('ssr'); + } + if ( + Object.prototype.hasOwnProperty.call( + serverConsumerModuleMap, + `${RSC_LAYER_PREFIX}${unprefixedModuleId}`, + ) + ) { + scopes.add('rsc'); + } + } + return Array.from(scopes); +}; + +const ensureShareScopeInitialized = async (shareScope: string) => { + if (!shareScope || typeof __webpack_require__.I !== 'function') { + return; + } + const existing = shareScopeInitPromises[shareScope]; + if (existing) { + await existing; + return; + } + + const initPromise = (async () => { + const initResult = __webpack_require__.I!(shareScope); + if ( + initResult && + typeof (initResult as Promise).then === 'function' + ) { + await initResult; + } + })().catch((error: unknown) => { + delete shareScopeInitPromises[shareScope]; + throw error; + }); + + shareScopeInitPromises[shareScope] = initPromise; + await initPromise; +}; + +const ensureShareScopesForModuleId = async ( + moduleId: string, + manifest?: ManifestLike, +) => { + const shareScopes = getShareScopesForServerModuleId(moduleId, manifest); + await Promise.all( + shareScopes.map((shareScope) => ensureShareScopeInitialized(shareScope)), + ); +}; + +const scanExposedModules = async () => { + if (scannedExposes) { + return; + } + scannedExposes = true; + + const moduleMap = __webpack_require__.initializeExposesData?.moduleMap; + if (!isObject(moduleMap)) { + return; + } + + for (const [exposeName, getFactory] of Object.entries(moduleMap)) { + if (exposeName === BRIDGE_EXPOSE_KEY || typeof getFactory !== 'function') { + continue; + } + try { + const exportsValue = await resolveFactoryExports(getFactory); + inspectExportValue(exportsValue, exposeName); + if (isObject(exportsValue) && isObject(exportsValue['default'])) { + inspectExportValue(exportsValue['default'], exposeName); + } + } catch { + // Ignore expose loading errors while scanning bridge metadata. + } + } +}; + +const buildSsrExposeMap = async (force = false) => { + if (scannedSsrExposeMap && !force) { + return; + } + scannedSsrExposeMap = true; + + const moduleMap = __webpack_require__.initializeExposesData?.moduleMap; + const manifest = isObject(__webpack_require__.rscM) + ? (__webpack_require__.rscM as ManifestLike) + : undefined; + const clientManifest = isObject(manifest?.clientManifest) + ? (manifest!.clientManifest as Record) + : undefined; + const serverConsumerModuleMap = isObject(manifest?.serverConsumerModuleMap) + ? (manifest!.serverConsumerModuleMap as Record) + : undefined; + + if (!isObject(moduleMap) || !clientManifest || !serverConsumerModuleMap) { + return; + } + + for (const [exposeName, getFactory] of Object.entries(moduleMap)) { + if ( + exposeName === BRIDGE_EXPOSE_KEY || + exposeName.startsWith(RSC_SSR_EXPOSE_PREFIX) || + typeof getFactory !== 'function' + ) { + continue; + } + const hiddenSsrExpose = toSsrExposeKey(exposeName); + if ( + !hiddenSsrExpose || + !Object.prototype.hasOwnProperty.call(moduleMap, hiddenSsrExpose) + ) { + continue; + } + + try { + const exportsValue = await resolveFactoryExports(getFactory); + const referenceIds: string[] = []; + collectClientReferenceIdsFromExports(exportsValue, referenceIds); + if (isObject(exportsValue) && isObject(exportsValue['default'])) { + collectClientReferenceIdsFromExports( + exportsValue['default'], + referenceIds, + ); + } + debugLog('buildSsrExposeMap:refs', { + exposeName, + hiddenSsrExpose, + referenceIds, + }); + + for (const referenceId of referenceIds) { + const clientEntry = resolveClientManifestEntry( + clientManifest, + referenceId, + ); + if (!clientEntry || clientEntry.id == null) { + debugLog('buildSsrExposeMap:missingClientEntry', { + exposeName, + referenceId, + }); + continue; + } + const consumerNode = serverConsumerModuleMap[String(clientEntry.id)]; + if (!isObject(consumerNode)) { + debugLog('buildSsrExposeMap:missingConsumerNode', { + exposeName, + referenceId, + clientEntryId: String(clientEntry.id), + }); + continue; + } + const consumerTarget = (consumerNode as ManifestNode)['*']; + if (!isObject(consumerTarget) || consumerTarget.id == null) { + debugLog('buildSsrExposeMap:missingConsumerTarget', { + exposeName, + referenceId, + clientEntryId: String(clientEntry.id), + consumerNodeKeys: Object.keys(consumerNode as ManifestNode), + }); + continue; + } + const serverModuleId = String(consumerTarget.id); + if ( + !Object.prototype.hasOwnProperty.call( + ssrExposeByServerModuleId, + serverModuleId, + ) + ) { + ssrExposeByServerModuleId[serverModuleId] = hiddenSsrExpose; + debugLog('buildSsrExposeMap:set', { + exposeName, + serverModuleId, + hiddenSsrExpose, + }); + } + } + } catch { + debugLog('buildSsrExposeMap:exposeLoadError', { + exposeName, + hiddenSsrExpose, + }); + } + } +}; + +export async function getManifest(): Promise { + await scanExposedModules(); + const manifest = isObject(__webpack_require__.rscM) + ? (__webpack_require__.rscM as ManifestLike) + : {}; + return { + ...manifest, + clientExposes: { + ...(manifest.clientExposes || {}), + ...clientExposeMap, + }, + }; +} + +export async function executeAction(actionId: string, args: unknown[]) { + const manifest = isObject(__webpack_require__.rscM) + ? (__webpack_require__.rscM as ManifestLike) + : undefined; + const manifestAction = resolveActionReference(manifest, actionId); + const localActionId = + manifestAction && typeof manifestAction.localActionId === 'string' + ? manifestAction.localActionId + : actionId; + await scanExposedModules(); + const action = actionReferenceCache[localActionId]; + if (typeof action !== 'function') { + throw new Error( + `[rsc-bridge-expose] Missing remote action for "${actionId}". Ensure the action is reachable from a federated expose.`, + ); + } + return action(...(Array.isArray(args) ? args : [])); +} + +export async function preloadSSRModule(moduleId: string, exposeHint?: string) { + const normalizedModuleId = String(moduleId); + if ( + Object.prototype.hasOwnProperty.call(ssrModuleCache, normalizedModuleId) + ) { + return ssrModuleCache[normalizedModuleId]; + } + + await scanExposedModules(); + const manifest = isObject(__webpack_require__.rscM) + ? (__webpack_require__.rscM as ManifestLike) + : undefined; + await ensureShareScopesForModuleId(normalizedModuleId, manifest); + await buildSsrExposeMap(); + + const didPreloadChunksFromManifest = await preloadServerModuleChunks( + normalizedModuleId, + manifest, + ); + if (didPreloadChunksFromManifest) { + try { + const required = __webpack_require__(normalizedModuleId); + const resolvedModuleExports = + required && typeof (required as Promise).then === 'function' + ? await (required as Promise) + : required; + ssrModuleCache[normalizedModuleId] = resolvedModuleExports; + return resolvedModuleExports; + } catch (error) { + debugLog('preloadSSRModule:manifestChunkRequireFailed', { + moduleId: normalizedModuleId, + error: String(error), + }); + } + } + + const shareScopeMap = (__webpack_require__ as unknown as { S?: any }).S; + const ssrReact = shareScopeMap?.ssr?.react; + debugLog('preloadSSRModule:shareState', { + moduleId: normalizedModuleId, + shareScopeKeys: shareScopeMap ? Object.keys(shareScopeMap) : [], + ssrReactVersions: isObject(ssrReact) ? Object.keys(ssrReact) : [], + }); + + const moduleMap = __webpack_require__.initializeExposesData?.moduleMap; + const hiddenExposeFromHint = resolveHiddenSsrExposeFromHint( + typeof exposeHint === 'string' ? exposeHint : undefined, + moduleMap, + ); + let hiddenSsrExpose = + hiddenExposeFromHint || + resolveServerModuleReference(manifest, normalizedModuleId)?.ssrExpose || + ssrExposeByServerModuleId[normalizedModuleId] || + resolveHiddenSsrExposeFromClientExposeMap(normalizedModuleId, moduleMap); + if (!hiddenSsrExpose) { + await ensureShareScopeInitialized('ssr'); + await ensureShareScopeInitialized('rsc'); + await buildSsrExposeMap(true); + hiddenSsrExpose = + hiddenExposeFromHint || + resolveServerModuleReference(manifest, normalizedModuleId)?.ssrExpose || + ssrExposeByServerModuleId[normalizedModuleId] || + resolveHiddenSsrExposeFromClientExposeMap(normalizedModuleId, moduleMap); + } + debugLog('preloadSSRModule:hiddenExpose', { + moduleId: normalizedModuleId, + exposeHint: exposeHint || '', + hiddenExposeFromHint: hiddenExposeFromHint || '', + hiddenSsrExpose: hiddenSsrExpose || '', + hasHiddenExposeFactory: + isObject(moduleMap) && + typeof hiddenSsrExpose === 'string' && + typeof moduleMap[hiddenSsrExpose] === 'function', + }); + if ( + isObject(moduleMap) && + typeof hiddenSsrExpose === 'string' && + typeof moduleMap[hiddenSsrExpose] === 'function' + ) { + // Prime remote chunks through the hidden expose, then read the requested + // server module id from the runtime graph. + await resolveFactoryExports(moduleMap[hiddenSsrExpose]!); + let resolvedModuleExports: unknown; + try { + const required = __webpack_require__(normalizedModuleId); + resolvedModuleExports = + required && typeof (required as Promise).then === 'function' + ? await (required as Promise) + : required; + } catch (error) { + throw new Error( + `[rsc-bridge-expose] Hidden SSR expose "${hiddenSsrExpose}" loaded but server module "${normalizedModuleId}" is unavailable: ${String(error)}`, + ); + } + if (shouldDebug) { + const exportKeys = isObject(resolvedModuleExports) + ? Object.keys(resolvedModuleExports as Record) + : []; + const sampleExportValue = + isObject(resolvedModuleExports) && exportKeys.length > 0 + ? (resolvedModuleExports as Record)[exportKeys[0]] + : resolvedModuleExports; + const sampleClientReferenceType = + (typeof sampleExportValue === 'function' || + isObject(sampleExportValue)) && + sampleExportValue != null && + '$$typeof' in (sampleExportValue as Record) + ? String((sampleExportValue as Record)['$$typeof']) + : ''; + const sampleClientReferenceId = + (typeof sampleExportValue === 'function' || + isObject(sampleExportValue)) && + sampleExportValue != null && + '$$id' in (sampleExportValue as Record) + ? String((sampleExportValue as Record)['$$id']) + : ''; + debugLog('preloadSSRModule:resolvedExports', { + moduleId: normalizedModuleId, + hiddenSsrExpose, + exportKeys, + sampleType: typeof sampleExportValue, + sampleClientReferenceType, + sampleClientReferenceId, + }); + } + ssrModuleCache[normalizedModuleId] = resolvedModuleExports; + return resolvedModuleExports; + } + + const syntheticModuleFromManifest = buildSyntheticSsrModuleFromManifest( + normalizedModuleId, + manifest, + ); + if (syntheticModuleFromManifest) { + ssrModuleCache[normalizedModuleId] = syntheticModuleFromManifest; + return syntheticModuleFromManifest; + } + + throw new Error( + `[rsc-bridge-expose] Missing hidden SSR expose for "${normalizedModuleId}". Bridge mapping failed; no raw module-id fallback is allowed.`, + ); +} + +export function getSSRModule(moduleId: string) { + const normalizedModuleId = String(moduleId); + if ( + Object.prototype.hasOwnProperty.call(ssrModuleCache, normalizedModuleId) + ) { + return ssrModuleCache[normalizedModuleId]; + } + throw new Error( + `[rsc-bridge-expose] SSR module "${normalizedModuleId}" was requested before preload.`, + ); +} diff --git a/packages/webpack-bundler-runtime/src/rscBridgeRuntimePlugin.ts b/packages/webpack-bundler-runtime/src/rscBridgeRuntimePlugin.ts new file mode 100644 index 00000000000..0c05460401e --- /dev/null +++ b/packages/webpack-bundler-runtime/src/rscBridgeRuntimePlugin.ts @@ -0,0 +1,1970 @@ +import { + type ManifestExport, + type ManifestLike, + type ManifestNode, + type RscBridgeModuleV1 as BridgeModule, + getClientManifestKeyWithoutHash, + resolveClientReferenceEntry, +} from './rscManifest'; + +const RSC_BRIDGE_EXPOSE = '__rspack_rsc_bridge__'; +const ACTION_PREFIX = 'remote:'; +const MODULE_PREFIX = 'remote-module:'; +const SERVER_MODULE_PREFIX = 'remote-server-module:'; +const SSR_LAYER_PREFIX = '(server-side-rendering)/'; +const RSC_LAYER_PREFIX = '(react-server-components)/'; +const ACTION_REMAP_GLOBAL_KEY = '__RSPACK_RSC_MF_ACTION_ID_MAP__'; +const ACTION_PROXY_MODULE_ID = '__rspack_mf_rsc_action_proxy__'; + +type ActionMapRecord = Record; +type ActionRemapMap = Record; +type FederationState = { + [ACTION_REMAP_GLOBAL_KEY]?: ActionRemapMap; +}; + +type RemoteDataItem = { + name: string; + remoteName: string; +}; + +type ConsumeDataItem = { + shareKey?: string; +}; + +type ConsumeHandlerItem = { + shareKey?: string; +}; + +type ClientModuleResolutionKind = 'shared' | 'remote'; + +type WebpackRequireRuntime = { + m?: Record void>; + c?: Record; + rscM?: ManifestLike; + remotesLoadingData?: { + chunkMapping?: Record>; + moduleIdToRemoteDataMapping?: Record; + }; + consumesLoadingData?: { + moduleIdToConsumeDataMapping?: Record; + }; + u?: (chunkId: string | number) => string; + federation?: { + instance?: { + loadRemote?: (request: string) => Promise; + loadShareSync?: (shareKey: string) => (() => unknown) | false; + options?: { + remotes?: Array<{ + name: string; + alias?: string; + }>; + }; + }; + bundlerRuntime?: { + resolveRemoteModuleId?: (options: { + webpackRequire: WebpackRequireRuntime; + alias: string; + expose: string; + }) => string | undefined; + }; + bundlerRuntimeOptions?: { + remotes?: { + remoteInfos?: Record< + string, + Array<{ + name?: string; + }> + >; + }; + }; + consumesLoadingModuleToHandlerMapping?: Record; + }; +}; + +declare const __webpack_require__: WebpackRequireRuntime; +declare const __FEDERATION__: FederationState | undefined; + +const isObject = (value: unknown): value is Record => + typeof value === 'object' && value !== null; + +const stableStringify = (value: unknown) => { + try { + return JSON.stringify(value, (_key, current) => { + if (Array.isArray(current)) { + return current; + } + if (!isObject(current)) { + return current; + } + return Object.fromEntries( + Object.keys(current) + .sort() + .map((key) => [key, current[key]]), + ); + }); + } catch { + return String(value); + } +}; + +const assertNoConflict = ( + target: Record, + key: string, + nextValue: unknown, + alias: string, + section: string, +) => { + if (!Object.prototype.hasOwnProperty.call(target, key)) { + return; + } + if (stableStringify(target[key]) !== stableStringify(nextValue)) { + throw new Error( + `[mf:rsc-bridge] ${section} conflict for "${key}" while merging remote "${alias}"`, + ); + } +}; + +const ensureHostManifest = () => { + if (!isObject(__webpack_require__.rscM)) { + __webpack_require__.rscM = {}; + } + const manifest = __webpack_require__.rscM as ManifestLike; + manifest.serverManifest = isObject(manifest.serverManifest) + ? manifest.serverManifest + : {}; + manifest.clientManifest = isObject(manifest.clientManifest) + ? manifest.clientManifest + : {}; + manifest.serverConsumerModuleMap = isObject(manifest.serverConsumerModuleMap) + ? manifest.serverConsumerModuleMap + : {}; + return manifest; +}; + +const getNamespacedModuleId = (alias: string, rawId: string | number) => + `${MODULE_PREFIX}${alias}:${String(rawId)}`; + +const getNamespacedServerModuleId = (alias: string, rawId: string | number) => + `${SERVER_MODULE_PREFIX}${alias}:${String(rawId)}`; + +const getNamespacedClientManifestKey = (alias: string, key: string | number) => + `${MODULE_PREFIX}${alias}:${String(key)}`; + +const namespaceModuleIdDeterministically = ( + alias: string, + rawId: string | number, +) => { + const normalizedRawId = String(rawId); + const namespaceLayerId = (layerPrefix: string) => { + const unprefixedId = normalizedRawId.slice(layerPrefix.length); + return unprefixedId.startsWith(MODULE_PREFIX) + ? normalizedRawId + : `${layerPrefix}${getNamespacedModuleId(alias, unprefixedId)}`; + }; + if (normalizedRawId.startsWith(SSR_LAYER_PREFIX)) { + return namespaceLayerId(SSR_LAYER_PREFIX); + } + if (normalizedRawId.startsWith(RSC_LAYER_PREFIX)) { + return namespaceLayerId(RSC_LAYER_PREFIX); + } + return normalizedRawId.startsWith(MODULE_PREFIX) + ? normalizedRawId + : getNamespacedModuleId(alias, normalizedRawId); +}; + +const namespaceServerModuleIdDeterministically = ( + alias: string, + rawId: string | number, +) => { + const normalizedRawId = String(rawId); + const namespaceLayerId = (layerPrefix: string) => { + const unprefixedId = normalizedRawId.slice(layerPrefix.length); + return unprefixedId.startsWith(SERVER_MODULE_PREFIX) + ? normalizedRawId + : `${layerPrefix}${getNamespacedServerModuleId(alias, unprefixedId)}`; + }; + if (normalizedRawId.startsWith(SSR_LAYER_PREFIX)) { + return namespaceLayerId(SSR_LAYER_PREFIX); + } + if (normalizedRawId.startsWith(RSC_LAYER_PREFIX)) { + return namespaceLayerId(RSC_LAYER_PREFIX); + } + return normalizedRawId.startsWith(SERVER_MODULE_PREFIX) + ? normalizedRawId + : getNamespacedServerModuleId(alias, normalizedRawId); +}; + +const isNamespacedRemoteIdForAlias = (alias: string, moduleId: string) => { + const aliasPrefix = `${MODULE_PREFIX}${alias}:`; + return ( + moduleId.startsWith(aliasPrefix) || + moduleId.startsWith(`${SSR_LAYER_PREFIX}${aliasPrefix}`) || + moduleId.startsWith(`${RSC_LAYER_PREFIX}${aliasPrefix}`) + ); +}; + +const stripLayerPrefix = (moduleId: string | number) => { + const normalizedModuleId = String(moduleId); + if (normalizedModuleId.startsWith(SSR_LAYER_PREFIX)) { + return normalizedModuleId.slice(SSR_LAYER_PREFIX.length); + } + if (normalizedModuleId.startsWith(RSC_LAYER_PREFIX)) { + return normalizedModuleId.slice(RSC_LAYER_PREFIX.length); + } + return normalizedModuleId; +}; + +const applyLayerPrefixFromRawId = ( + rawModuleId: string, + resolvedModuleId: string, +) => { + const normalizedResolvedModuleId = String(resolvedModuleId); + const unprefixedResolvedModuleId = stripLayerPrefix( + normalizedResolvedModuleId, + ); + if (rawModuleId.startsWith(SSR_LAYER_PREFIX)) { + return normalizedResolvedModuleId.startsWith(SSR_LAYER_PREFIX) + ? normalizedResolvedModuleId + : `${SSR_LAYER_PREFIX}${unprefixedResolvedModuleId}`; + } + if (rawModuleId.startsWith(RSC_LAYER_PREFIX)) { + return normalizedResolvedModuleId.startsWith(RSC_LAYER_PREFIX) + ? normalizedResolvedModuleId + : `${RSC_LAYER_PREFIX}${unprefixedResolvedModuleId}`; + } + return normalizedResolvedModuleId; +}; + +const resolveClientModuleIdFromMap = ( + resolvedClientIdsByRawId: Record, + rawId: string | number, +) => { + const normalizedRawId = String(rawId); + if ( + Object.prototype.hasOwnProperty.call( + resolvedClientIdsByRawId, + normalizedRawId, + ) + ) { + return applyLayerPrefixFromRawId( + normalizedRawId, + resolvedClientIdsByRawId[normalizedRawId], + ); + } + const unprefixedRawId = stripLayerPrefix(normalizedRawId); + if ( + unprefixedRawId !== normalizedRawId && + Object.prototype.hasOwnProperty.call( + resolvedClientIdsByRawId, + unprefixedRawId, + ) + ) { + return applyLayerPrefixFromRawId( + normalizedRawId, + resolvedClientIdsByRawId[unprefixedRawId], + ); + } + return undefined; +}; + +const createSharedConsumeModuleIdIndex = () => { + const shareKeyToModuleId: Record = Object.create(null); + const assignShareKey = (moduleId: string, shareKey: string) => { + if (!shareKey) { + return; + } + if (!Object.prototype.hasOwnProperty.call(shareKeyToModuleId, shareKey)) { + shareKeyToModuleId[shareKey] = String(moduleId); + } + }; + const handlerMapping = + __webpack_require__.federation?.consumesLoadingModuleToHandlerMapping; + if (isObject(handlerMapping)) { + for (const [moduleId, handlerValue] of Object.entries(handlerMapping)) { + if ( + !isObject(handlerValue) || + typeof handlerValue.shareKey !== 'string' + ) { + continue; + } + assignShareKey(String(moduleId), handlerValue.shareKey); + } + } + + const consumeDataMapping = + __webpack_require__.consumesLoadingData?.moduleIdToConsumeDataMapping; + if (isObject(consumeDataMapping)) { + for (const [moduleId, consumeData] of Object.entries(consumeDataMapping)) { + if (!isObject(consumeData) || typeof consumeData.shareKey !== 'string') { + continue; + } + assignShareKey(String(moduleId), consumeData.shareKey); + } + } + + return shareKeyToModuleId; +}; + +const resolveSharedConsumeModuleId = ( + rawClientManifestKey: string, + shareKeyToModuleId: Record, + explicitShareKey?: string, +): string | undefined => { + const candidateShareKeys = new Set([ + explicitShareKey || getClientManifestKeyWithoutHash(rawClientManifestKey), + ]); + + for (const shareKey of candidateShareKeys) { + if (!shareKey) { + continue; + } + if (Object.prototype.hasOwnProperty.call(shareKeyToModuleId, shareKey)) { + return shareKeyToModuleId[shareKey]; + } + } + + return undefined; +}; + +const resolveShareKeyFromClientManifestKey = ( + rawClientManifestKey: string, +): string | undefined => { + const keyWithoutHash = getClientManifestKeyWithoutHash(rawClientManifestKey); + if (!keyWithoutHash || keyWithoutHash.startsWith(MODULE_PREFIX)) { + return undefined; + } + if (keyWithoutHash.startsWith('.') || keyWithoutHash.startsWith('/')) { + return undefined; + } + if (/^[A-Za-z]:[\\/]/.test(keyWithoutHash) || keyWithoutHash.includes('\\')) { + return undefined; + } + return keyWithoutHash; +}; + +const getClientModuleResolutionPriority = ( + resolutionKind: ClientModuleResolutionKind | undefined, +) => { + switch (resolutionKind) { + case 'shared': + return 2; + case 'remote': + return 1; + default: + return 0; + } +}; + +const normalizeExpose = (expose: string) => { + if (!expose || expose === '.') { + return '.'; + } + return expose.startsWith('./') ? expose : `./${expose}`; +}; + +const getRemoteClientReferenceModuleId = (alias: string, expose: string) => { + const normalizedExpose = normalizeExpose(expose); + const exposeToken = normalizedExpose.startsWith('./') + ? normalizedExpose.slice(2) + : normalizedExpose; + return getNamespacedModuleId(alias, exposeToken || '.'); +}; + +const parseRemoteClientReferenceModuleId = ( + moduleId: string, +): { alias: string; expose: string } | undefined => { + const normalizedModuleId = stripLayerPrefix(moduleId); + if (!normalizedModuleId.startsWith(MODULE_PREFIX)) { + return undefined; + } + const namespacedToken = normalizedModuleId.slice(MODULE_PREFIX.length); + const aliasSeparatorIndex = namespacedToken.indexOf(':'); + if (aliasSeparatorIndex <= 0) { + return undefined; + } + const alias = namespacedToken.slice(0, aliasSeparatorIndex).trim(); + const exposeToken = namespacedToken.slice(aliasSeparatorIndex + 1).trim(); + if (!alias || !exposeToken) { + return undefined; + } + return { + alias, + expose: normalizeExpose(exposeToken), + }; +}; + +const resolveClientEntryExpose = ( + rawClientManifestKey: string, + entryName: string | undefined, + clientExposes: Record, +): string | undefined => { + const rawClientManifestKeyWithoutHash = + getClientManifestKeyWithoutHash(rawClientManifestKey); + + const manifestKeys = Array.from( + new Set( + rawClientManifestKey === rawClientManifestKeyWithoutHash + ? [rawClientManifestKey] + : [rawClientManifestKey, rawClientManifestKeyWithoutHash], + ), + ); + + if (entryName && entryName !== '*' && entryName !== '__esModule') { + for (const rawClientManifestKeyCandidate of manifestKeys) { + const exposedByFullKey = + clientExposes[`${rawClientManifestKeyCandidate}#${entryName}`]; + if (typeof exposedByFullKey === 'string' && exposedByFullKey) { + return normalizeExpose(exposedByFullKey); + } + } + } + + for (const rawClientManifestKeyCandidate of manifestKeys) { + const exposedByKey = clientExposes[rawClientManifestKeyCandidate]; + if (typeof exposedByKey === 'string' && exposedByKey) { + return normalizeExpose(exposedByKey); + } + } + + if (entryName && entryName !== '*' && entryName !== '__esModule') { + return normalizeExpose(entryName); + } + + return undefined; +}; + +const resolveExplicitClientModuleId = ( + remoteManifest: ManifestLike, + rawClientManifestKey: string, +) => { + const clientReference = resolveClientReferenceEntry( + remoteManifest, + rawClientManifestKey, + ); + if (!clientReference || !isObject(clientReference.resolution)) { + return undefined; + } + const resolution = clientReference.resolution; + if ( + resolution.kind === 'shared' && + typeof resolution.shareKey === 'string' && + resolution.shareKey + ) { + return { + kind: 'shared' as const, + shareKey: String(resolution.shareKey), + }; + } + if ( + resolution.kind === 'remote' && + typeof resolution.request === 'string' && + resolution.request + ) { + return { + kind: 'remote' as const, + expose: normalizeExpose(resolution.request), + }; + } + return undefined; +}; + +const getFederationState = (): FederationState | undefined => { + if (typeof __FEDERATION__ !== 'undefined' && isObject(__FEDERATION__)) { + return __FEDERATION__; + } + return undefined; +}; + +const getActionRemapMap = () => { + const federationState = getFederationState(); + if (!federationState) { + return undefined; + } + if (!isObject(federationState[ACTION_REMAP_GLOBAL_KEY])) { + federationState[ACTION_REMAP_GLOBAL_KEY] = {}; + } + return federationState[ACTION_REMAP_GLOBAL_KEY] as ActionRemapMap; +}; + +const registerActionRemap = (rawActionId: string, prefixedActionId: string) => { + const remapMap = getActionRemapMap(); + if (!remapMap) { + return; + } + const existingValue = remapMap[rawActionId]; + if (!existingValue) { + remapMap[rawActionId] = prefixedActionId; + return; + } + if (existingValue === prefixedActionId) { + return; + } + throw new Error( + `[mf:rsc-bridge] Conflicting raw action id "${rawActionId}" mapped to both "${existingValue}" and "${prefixedActionId}"`, + ); +}; + +const registerActionProxyExport = ( + actionMap: ActionMapRecord, + actionExportName: string, + actionRef: { alias: string; rawActionId: string }, +) => { + const existing = actionMap[actionExportName]; + if (!existing) { + actionMap[actionExportName] = actionRef; + return; + } + if ( + existing.alias === actionRef.alias && + existing.rawActionId === actionRef.rawActionId + ) { + return; + } + throw new Error( + `[mf:rsc-bridge] Conflicting action export "${actionExportName}" mapped to multiple remotes`, + ); +}; + +const remapConsumerNode = ( + value: unknown, + mapServerModuleId: (rawServerModuleId: string, exportName: string) => string, +) => { + if (!isObject(value)) { + return value; + } + return Object.fromEntries( + Object.entries(value).map(([exportName, exportValue]) => { + const nextExportValue = isObject(exportValue) + ? { ...exportValue } + : exportValue; + if (isObject(nextExportValue) && nextExportValue['id'] != null) { + const rawId = String(nextExportValue['id']); + nextExportValue['id'] = mapServerModuleId(rawId, exportName); + } + return [exportName, nextExportValue]; + }), + ); +}; + +const inferExposeHintFromConsumerNode = (node: unknown): string | undefined => { + if (!isObject(node)) { + return undefined; + } + + for (const [exportName, exportValue] of Object.entries(node)) { + if (exportName !== '*' && exportName !== '__esModule') { + return normalizeExpose(exportName); + } + if ( + isObject(exportValue) && + typeof exportValue['name'] === 'string' && + exportValue['name'] !== '*' && + exportValue['name'] !== '__esModule' + ) { + return normalizeExpose(exportValue['name']); + } + } + + const starValue = node['*']; + if ( + isObject(starValue) && + typeof starValue['name'] === 'string' && + starValue['name'] !== '*' && + starValue['name'] !== '__esModule' + ) { + return normalizeExpose(starValue['name']); + } + + return undefined; +}; + +const resolveRemoteAlias = (args: any): string | undefined => { + const candidateAliases = [ + args?.remote?.alias, + args?.pkgNameOrAlias, + args?.remote?.name, + args?.remoteInfo?.alias, + args?.remoteInfo?.name, + args?.name, + ]; + for (const candidate of candidateAliases) { + if (typeof candidate === 'string' && candidate.trim()) { + return candidate; + } + } + return undefined; +}; + +const resolveExposeSpecifier = (args: any): string => { + const candidateExposes = [ + args?.expose, + args?.id, + args?.request, + args?.pkgNameOrAlias, + args?.remote?.id, + args?.remote?.expose, + args?.remoteInfo?.id, + args?.remoteInfo?.expose, + args?.name, + ]; + for (const candidate of candidateExposes) { + if ( + (typeof candidate === 'string' || typeof candidate === 'number') && + String(candidate).trim() + ) { + return String(candidate); + } + } + return ''; +}; + +const isBridgeExposeRequest = (args: any) => + resolveExposeSpecifier(args).includes(RSC_BRIDGE_EXPOSE); + +const resolveBridgeFromLoadArgs = async ( + args: any, +): Promise => { + if ( + isObject(args?.exposeModule) && + typeof args.exposeModule.getManifest === 'function' && + typeof args.exposeModule.executeAction === 'function' + ) { + return args.exposeModule as BridgeModule; + } + + if (typeof args?.exposeModuleFactory === 'function') { + const bridgeCandidate = await Promise.resolve(args.exposeModuleFactory()); + if ( + isObject(bridgeCandidate) && + typeof bridgeCandidate.getManifest === 'function' && + typeof bridgeCandidate.executeAction === 'function' + ) { + return bridgeCandidate as BridgeModule; + } + } +}; + +const installActionProxyModule = ({ + actionMap, + ensureBridge, +}: { + actionMap: ActionMapRecord; + ensureBridge: (alias: string, args?: any) => Promise; +}) => { + if (!isObject(__webpack_require__.m)) { + __webpack_require__.m = {}; + } + if (__webpack_require__.m?.[ACTION_PROXY_MODULE_ID]) { + return; + } + + __webpack_require__.m![ACTION_PROXY_MODULE_ID] = (module: { + exports: any; + }) => { + const getActionFn = (property: string) => { + const actionRef = actionMap[property]; + if (!actionRef) { + return undefined; + } + return async (...args: unknown[]) => { + const bridge = await ensureBridge(actionRef.alias); + if (typeof bridge.executeAction !== 'function') { + throw new Error( + `[mf:rsc-bridge] Missing executeAction bridge method for remote "${actionRef.alias}"`, + ); + } + return bridge.executeAction( + actionRef.rawActionId, + Array.isArray(args) ? args : [], + ); + }; + }; + + module.exports = new Proxy( + {}, + { + get(_target, property) { + if (property === 'then') { + return undefined; + } + if (typeof property !== 'string') { + return undefined; + } + return getActionFn(property); + }, + has(_target, property) { + return ( + typeof property === 'string' && + Object.prototype.hasOwnProperty.call(actionMap, property) + ); + }, + getOwnPropertyDescriptor(_target, property) { + if ( + typeof property !== 'string' || + !Object.prototype.hasOwnProperty.call(actionMap, property) + ) { + return undefined; + } + return { + configurable: true, + enumerable: true, + get() { + return getActionFn(property); + }, + }; + }, + ownKeys() { + return Object.keys(actionMap); + }, + }, + ); + }; +}; + +const rscBridgeRuntimePlugin = () => { + const bridgePromises: Partial>> = {}; + const aliasMergePromises: Partial>> = {}; + const shareScopeInitPromises: Partial>> = {}; + const mergedRemoteAliases = new Set(); + const actionMap: ActionMapRecord = Object.create(null); + const ssrModuleExportsByNamespacedId: Record = + Object.create(null); + const ssrModulePreloadPromises: Partial>> = {}; + const syntheticSharedShareKeyByModuleId: Record = + Object.create(null); + const syntheticSharedExportsByModuleId: Record = + Object.create(null); + const syntheticRemoteRequestByModuleId: Record = + Object.create(null); + const resolvedRemoteRequestByModuleId: Record = + Object.create(null); + const syntheticRemoteExportsByModuleId: Record = + Object.create(null); + const syntheticRemotePromiseByModuleId: Record< + string, + Promise + > = Object.create(null); + const exposeTokenByServerModuleIdByAlias: Record< + string, + Record + > = Object.create(null); + const exposeTokenMapPromiseByAlias: Partial>> = + {}; + const seededRemoteClientModuleIds = new Set(); + let lazyRemoteFactoryPrototypeInstalled = false; + + const ensureSsrModuleFactory = ( + namespacedModuleId: string, + rawServerModuleId: string, + ) => { + if (!isObject(__webpack_require__.m)) { + __webpack_require__.m = {}; + } + if (__webpack_require__.m?.[namespacedModuleId]) { + return; + } + __webpack_require__.m![namespacedModuleId] = (module: { + exports: unknown; + }) => { + if ( + !Object.prototype.hasOwnProperty.call( + ssrModuleExportsByNamespacedId, + namespacedModuleId, + ) + ) { + throw new Error( + `[mf:rsc-bridge] SSR module "${namespacedModuleId}" (source "${rawServerModuleId}") was requested before preload`, + ); + } + module.exports = ssrModuleExportsByNamespacedId[namespacedModuleId]; + }; + }; + + const installSyntheticSharedClientModuleFactory = ( + moduleId: string, + shareKey: string, + ) => { + const existingShareKey = syntheticSharedShareKeyByModuleId[moduleId]; + if (existingShareKey && existingShareKey !== shareKey) { + throw new Error( + `[mf:rsc-bridge] Conflicting shared client reference for "${moduleId}" between "${existingShareKey}" and "${shareKey}"`, + ); + } + syntheticSharedShareKeyByModuleId[moduleId] = shareKey; + + if (!isObject(__webpack_require__.m)) { + __webpack_require__.m = {}; + } + if (__webpack_require__.m?.[moduleId]) { + return; + } + + __webpack_require__.m![moduleId] = (module: { exports: unknown }) => { + if ( + Object.prototype.hasOwnProperty.call( + syntheticSharedExportsByModuleId, + moduleId, + ) + ) { + module.exports = syntheticSharedExportsByModuleId[moduleId]; + return; + } + const federationInstance = __webpack_require__.federation?.instance; + if ( + !federationInstance || + typeof federationInstance.loadShareSync !== 'function' + ) { + throw new Error( + `[mf:rsc-bridge] Missing loadShareSync for shared client reference "${shareKey}"`, + ); + } + const sharedFactory = federationInstance.loadShareSync(shareKey); + if (typeof sharedFactory !== 'function') { + throw new Error( + `[mf:rsc-bridge] Shared client reference "${shareKey}" is unavailable synchronously`, + ); + } + const sharedExports = sharedFactory(); + syntheticSharedExportsByModuleId[moduleId] = sharedExports; + module.exports = sharedExports; + }; + }; + + const installSyntheticRemoteClientModuleFactory = ( + moduleId: string, + alias: string, + expose: string, + ) => { + const normalizedExpose = normalizeExpose(expose); + const remoteRequest = `${alias}/${normalizedExpose.startsWith('./') ? normalizedExpose.slice(2) : normalizedExpose}`; + const existingRequest = syntheticRemoteRequestByModuleId[moduleId]; + if (existingRequest && existingRequest !== remoteRequest) { + throw new Error( + `[mf:rsc-bridge] Conflicting remote client reference for "${moduleId}" between "${existingRequest}" and "${remoteRequest}"`, + ); + } + syntheticRemoteRequestByModuleId[moduleId] = remoteRequest; + + const exposeTokenFromModuleId = + parseRemoteClientReferenceModuleId(moduleId)?.expose; + const normalizedExposeToken = + exposeTokenFromModuleId && exposeTokenFromModuleId.startsWith('./') + ? exposeTokenFromModuleId.slice(2) + : exposeTokenFromModuleId || ''; + + const ensureExposeTokenMapForAlias = async () => { + if ( + isObject(exposeTokenByServerModuleIdByAlias[alias]) && + Object.keys(exposeTokenByServerModuleIdByAlias[alias]).length > 0 + ) { + return; + } + const existingPromise = exposeTokenMapPromiseByAlias[alias]; + if (existingPromise) { + await existingPromise; + return; + } + + const buildPromise = (async () => { + const bridge = await ensureBridge(alias); + if (typeof bridge.getManifest !== 'function') { + return; + } + const bridgeManifest = await Promise.resolve(bridge.getManifest()); + if (!isObject(bridgeManifest)) { + return; + } + + const clientManifest = isObject(bridgeManifest.clientManifest) + ? (bridgeManifest.clientManifest as Record) + : undefined; + const serverConsumerModuleMap = isObject( + bridgeManifest.serverConsumerModuleMap, + ) + ? (bridgeManifest.serverConsumerModuleMap as Record< + string, + ManifestNode + >) + : undefined; + if (!clientManifest || !serverConsumerModuleMap) { + return; + } + + const clientExposes = isObject(bridgeManifest.clientExposes) + ? (bridgeManifest.clientExposes as Record) + : {}; + const exposeTokenByClientModuleId: Record = + Object.create(null); + + for (const [rawClientManifestKey, clientEntry] of Object.entries( + clientManifest, + )) { + if (!isObject(clientEntry) || clientEntry.id == null) { + continue; + } + const entryName = + typeof clientEntry.name === 'string' ? clientEntry.name : undefined; + const entryExpose = resolveClientEntryExpose( + rawClientManifestKey, + entryName, + clientExposes, + ); + if (!entryExpose) { + continue; + } + const exposeToken = entryExpose.startsWith('./') + ? entryExpose.slice(2) + : entryExpose; + if (!exposeToken) { + continue; + } + const clientModuleId = stripLayerPrefix(String(clientEntry.id)); + if ( + !Object.prototype.hasOwnProperty.call( + exposeTokenByClientModuleId, + clientModuleId, + ) + ) { + exposeTokenByClientModuleId[clientModuleId] = exposeToken; + } + } + + const nextExposeTokenMap: Record = Object.create(null); + for (const [rawServerConsumerModuleId, node] of Object.entries( + serverConsumerModuleMap, + )) { + if (!isObject(node)) { + continue; + } + const rawClientModuleId = stripLayerPrefix(rawServerConsumerModuleId); + const exposeToken = exposeTokenByClientModuleId[rawClientModuleId]; + if (!exposeToken) { + continue; + } + if ( + !Object.prototype.hasOwnProperty.call( + nextExposeTokenMap, + rawClientModuleId, + ) + ) { + nextExposeTokenMap[rawClientModuleId] = exposeToken; + } + + for (const nodeEntry of Object.values(node)) { + if (!isObject(nodeEntry) || nodeEntry.id == null) { + continue; + } + const rawServerModuleId = stripLayerPrefix(String(nodeEntry.id)); + if ( + !Object.prototype.hasOwnProperty.call( + nextExposeTokenMap, + rawServerModuleId, + ) + ) { + nextExposeTokenMap[rawServerModuleId] = exposeToken; + } + } + } + + exposeTokenByServerModuleIdByAlias[alias] = nextExposeTokenMap; + })() + .catch((error: unknown) => { + delete exposeTokenMapPromiseByAlias[alias]; + throw error; + }) + .finally(() => { + delete exposeTokenMapPromiseByAlias[alias]; + }); + + exposeTokenMapPromiseByAlias[alias] = buildPromise; + await buildPromise; + }; + + const isLikelyOpaqueExposeToken = + !normalizedExposeToken || /^\d+$/.test(normalizedExposeToken); + + const resolveRemoteRequest = async () => { + if ( + Object.prototype.hasOwnProperty.call( + resolvedRemoteRequestByModuleId, + moduleId, + ) + ) { + return resolvedRemoteRequestByModuleId[moduleId]; + } + + let resolvedRemoteRequest = remoteRequest; + const exposeTokenByServerModuleId = + exposeTokenByServerModuleIdByAlias[alias]; + const remappedExposeToken = + (isObject(exposeTokenByServerModuleId) && + typeof exposeTokenByServerModuleId[normalizedExposeToken] === + 'string' && + exposeTokenByServerModuleId[normalizedExposeToken]) || + ''; + if (remappedExposeToken) { + resolvedRemoteRequest = `${alias}/${remappedExposeToken}`; + } else if (isLikelyOpaqueExposeToken) { + await ensureExposeTokenMapForAlias(); + const nextExposeTokenMap = + exposeTokenByServerModuleIdByAlias[alias] || Object.create(null); + const fallbackExposeToken = + typeof nextExposeTokenMap[normalizedExposeToken] === 'string' + ? nextExposeTokenMap[normalizedExposeToken] + : ''; + if (fallbackExposeToken) { + resolvedRemoteRequest = `${alias}/${fallbackExposeToken}`; + } + } + + resolvedRemoteRequestByModuleId[moduleId] = resolvedRemoteRequest; + return resolvedRemoteRequest; + }; + + if (!isObject(__webpack_require__.m)) { + __webpack_require__.m = {}; + } + + __webpack_require__.m![moduleId] = (module: { exports: unknown }) => { + if ( + Object.prototype.hasOwnProperty.call( + syntheticRemoteExportsByModuleId, + moduleId, + ) + ) { + module.exports = syntheticRemoteExportsByModuleId[moduleId]; + return; + } + + let pending = syntheticRemotePromiseByModuleId[moduleId]; + if (!pending) { + const runtimeInstance = __webpack_require__.federation?.instance; + if ( + !runtimeInstance || + typeof runtimeInstance.loadRemote !== 'function' + ) { + throw new Error( + `[mf:rsc-bridge] Missing loadRemote for remote client reference "${remoteRequest}"`, + ); + } + pending = resolveRemoteRequest() + .then((resolvedRemoteRequest) => + Promise.resolve(runtimeInstance.loadRemote(resolvedRemoteRequest)), + ) + .catch(async (error: unknown) => { + await ensureExposeTokenMapForAlias(); + const exposeTokenMap = + exposeTokenByServerModuleIdByAlias[alias] || Object.create(null); + const fallbackExposeToken = + typeof exposeTokenMap[normalizedExposeToken] === 'string' + ? exposeTokenMap[normalizedExposeToken] + : ''; + if (!fallbackExposeToken) { + throw error; + } + const fallbackRequest = `${alias}/${fallbackExposeToken}`; + if (fallbackRequest === remoteRequest) { + throw error; + } + resolvedRemoteRequestByModuleId[moduleId] = fallbackRequest; + return Promise.resolve(runtimeInstance.loadRemote(fallbackRequest)); + }) + .then((exportsValue) => { + syntheticRemoteExportsByModuleId[moduleId] = exportsValue; + return exportsValue; + }); + syntheticRemotePromiseByModuleId[moduleId] = pending; + } + + module.exports = pending; + }; + }; + + const ensureLazyRemoteClientFactoryPrototype = () => { + if (lazyRemoteFactoryPrototypeInstalled) { + return; + } + if (!isObject(__webpack_require__.m)) { + __webpack_require__.m = {}; + } + const moduleFactories = __webpack_require__.m as Record; + const previousPrototype = Object.getPrototypeOf(moduleFactories); + const fallbackPrototype = new Proxy(previousPrototype ?? Object.prototype, { + get(target, property, receiver) { + if ( + typeof property === 'string' && + !Object.prototype.hasOwnProperty.call(moduleFactories, property) + ) { + const parsedModuleId = parseRemoteClientReferenceModuleId(property); + if (parsedModuleId) { + installSyntheticRemoteClientModuleFactory( + property, + parsedModuleId.alias, + parsedModuleId.expose, + ); + if ( + Object.prototype.hasOwnProperty.call(moduleFactories, property) + ) { + return moduleFactories[property]; + } + } + } + return Reflect.get(target, property, receiver); + }, + has(target, property) { + if ( + typeof property === 'string' && + !Object.prototype.hasOwnProperty.call(moduleFactories, property) + ) { + const parsedModuleId = parseRemoteClientReferenceModuleId(property); + if (parsedModuleId) { + installSyntheticRemoteClientModuleFactory( + property, + parsedModuleId.alias, + parsedModuleId.expose, + ); + } + } + return ( + Object.prototype.hasOwnProperty.call(moduleFactories, property) || + Reflect.has(target, property) + ); + }, + getOwnPropertyDescriptor(target, property) { + if ( + typeof property === 'string' && + !Object.prototype.hasOwnProperty.call(moduleFactories, property) + ) { + const parsedModuleId = parseRemoteClientReferenceModuleId(property); + if (parsedModuleId) { + installSyntheticRemoteClientModuleFactory( + property, + parsedModuleId.alias, + parsedModuleId.expose, + ); + } + } + return ( + Object.getOwnPropertyDescriptor(moduleFactories, property) || + Reflect.getOwnPropertyDescriptor(target, property) + ); + }, + }); + Object.setPrototypeOf(moduleFactories, fallbackPrototype); + lazyRemoteFactoryPrototypeInstalled = true; + }; + + const seedRemoteClientModuleFactoriesFromRuntimeMapping = () => { + ensureLazyRemoteClientFactoryPrototype(); + const moduleIdToRemoteDataMapping = + __webpack_require__.remotesLoadingData?.moduleIdToRemoteDataMapping; + if (!isObject(moduleIdToRemoteDataMapping)) { + return; + } + for (const remoteData of Object.values(moduleIdToRemoteDataMapping)) { + if (!isObject(remoteData)) { + continue; + } + const remoteAlias = + typeof remoteData.remoteName === 'string' ? remoteData.remoteName : ''; + const expose = typeof remoteData.name === 'string' ? remoteData.name : ''; + if (!remoteAlias || !expose) { + continue; + } + const clientModuleId = getRemoteClientReferenceModuleId( + remoteAlias, + expose, + ); + if (seededRemoteClientModuleIds.has(clientModuleId)) { + continue; + } + installSyntheticRemoteClientModuleFactory( + clientModuleId, + remoteAlias, + expose, + ); + seededRemoteClientModuleIds.add(clientModuleId); + } + }; + + const ensureBridge = async (alias: string, args?: any) => { + const existingBridgePromise = bridgePromises[alias]; + if (existingBridgePromise) { + return existingBridgePromise; + } + + const runtimeInstance = + args?.origin && typeof args.origin.loadRemote === 'function' + ? args.origin + : __webpack_require__.federation?.instance; + + if (!runtimeInstance || typeof runtimeInstance.loadRemote !== 'function') { + throw new Error( + '[mf:rsc-bridge] Module Federation runtime instance is unavailable while loading the RSC bridge', + ); + } + + const ensureShareScopesInitialized = async () => { + const existingShareScopeInit = shareScopeInitPromises[alias]; + if (existingShareScopeInit) { + await existingShareScopeInit; + return; + } + + const getScopesFromValue = (value: unknown): string[] => { + if (Array.isArray(value)) { + return value.filter( + (item): item is string => typeof item === 'string', + ); + } + if (typeof value === 'string') { + return [value]; + } + return []; + }; + + const scopeSet = new Set(); + + const aliasRemoteInfos = + __webpack_require__.federation?.bundlerRuntimeOptions?.remotes + ?.remoteInfos?.[alias]; + if (Array.isArray(aliasRemoteInfos)) { + for (const remoteInfo of aliasRemoteInfos) { + for (const scope of getScopesFromValue(remoteInfo?.shareScope)) { + if (scope) { + scopeSet.add(scope); + } + } + } + } + + const configuredRemotes = runtimeInstance?.options?.remotes; + if (Array.isArray(configuredRemotes)) { + for (const remote of configuredRemotes) { + if (!remote || typeof remote !== 'object') { + continue; + } + if (remote.alias !== alias && remote.name !== alias) { + continue; + } + for (const scope of getScopesFromValue(remote.shareScope)) { + if (scope) { + scopeSet.add(scope); + } + } + } + } + + if (scopeSet.size === 0) { + scopeSet.add('default'); + } + + const initPromise = (async () => { + for (const scope of scopeSet) { + const initResult = + typeof runtimeInstance.initializeSharing === 'function' + ? runtimeInstance.initializeSharing(scope) + : typeof __webpack_require__.I === 'function' + ? __webpack_require__.I(scope) + : undefined; + if ( + initResult && + typeof (initResult as Promise).then === 'function' + ) { + await initResult; + } + } + })().catch((error: unknown) => { + delete shareScopeInitPromises[alias]; + throw error; + }); + + shareScopeInitPromises[alias] = initPromise; + await initPromise; + }; + + await ensureShareScopesInitialized(); + + const bridgePromise = Promise.resolve( + runtimeInstance.loadRemote(`${alias}/${RSC_BRIDGE_EXPOSE}`), + ) + .then((bridge: BridgeModule) => { + if ( + !bridge || + typeof bridge.getManifest !== 'function' || + typeof bridge.executeAction !== 'function' + ) { + throw new Error( + `[mf:rsc-bridge] Remote "${alias}" is missing the internal RSC bridge expose`, + ); + } + return bridge; + }) + .catch((error: unknown) => { + delete bridgePromises[alias]; + throw error; + }); + + bridgePromises[alias] = bridgePromise; + return bridgePromise; + }; + + const preloadSsrModule = async ( + alias: string, + rawServerModuleId: string, + exposeHint?: string, + args?: any, + ) => { + const namespacedModuleId = namespaceServerModuleIdDeterministically( + alias, + rawServerModuleId, + ); + ensureSsrModuleFactory(namespacedModuleId, rawServerModuleId); + + if ( + Object.prototype.hasOwnProperty.call( + ssrModuleExportsByNamespacedId, + namespacedModuleId, + ) + ) { + return namespacedModuleId; + } + + const existingPreload = ssrModulePreloadPromises[namespacedModuleId]; + if (existingPreload) { + await existingPreload; + return namespacedModuleId; + } + + const preloadPromise = (async () => { + const bridge = await ensureBridge(alias, args); + if (typeof bridge.preloadSSRModule === 'function') { + ssrModuleExportsByNamespacedId[namespacedModuleId] = + await bridge.preloadSSRModule(rawServerModuleId, exposeHint); + return; + } + if (typeof bridge.getSSRModule === 'function') { + ssrModuleExportsByNamespacedId[namespacedModuleId] = + bridge.getSSRModule(rawServerModuleId); + return; + } + throw new Error( + `[mf:rsc-bridge] Remote "${alias}" does not expose preloadSSRModule/getSSRModule for "${rawServerModuleId}"${exposeHint ? ` (hint: ${exposeHint})` : ''}`, + ); + })() + .catch((error: unknown) => { + delete ssrModuleExportsByNamespacedId[namespacedModuleId]; + throw error; + }) + .finally(() => { + delete ssrModulePreloadPromises[namespacedModuleId]; + }); + + ssrModulePreloadPromises[namespacedModuleId] = preloadPromise; + await preloadPromise; + return namespacedModuleId; + }; + + const mergeRemoteManifest = async ( + alias: string, + remoteManifest: ManifestLike, + proxyModuleId: string, + args?: any, + ) => { + if (!isObject(remoteManifest)) { + return; + } + + const hostManifest = ensureHostManifest(); + const clientExposes = isObject(remoteManifest.clientExposes) + ? (remoteManifest.clientExposes as Record) + : {}; + const resolvedClientIdsByRawId: Record = + Object.create(null); + const resolvedClientIdKindByRawId: Record< + string, + ClientModuleResolutionKind + > = Object.create(null); + const shareKeyToModuleId = createSharedConsumeModuleIdIndex(); + const clientManifestEntriesByRawId: Record< + string, + Array + > = Object.create(null); + + if (isObject(remoteManifest.clientManifest)) { + for (const [rawClientManifestKey, value] of Object.entries( + remoteManifest.clientManifest, + )) { + const scopedClientManifestKey = getNamespacedClientManifestKey( + alias, + rawClientManifestKey, + ); + const nextValue = isObject(value) ? { ...value } : value; + const rawRemoteClientModuleId = + isObject(nextValue) && nextValue.id != null + ? String(nextValue.id) + : undefined; + const entryName = + isObject(nextValue) && typeof nextValue.name === 'string' + ? nextValue.name + : undefined; + const explicitClientModule = resolveExplicitClientModuleId( + remoteManifest, + rawClientManifestKey, + ); + const entryExpose = + explicitClientModule?.kind === 'remote' + ? explicitClientModule.expose + : resolveClientEntryExpose( + rawClientManifestKey, + entryName, + clientExposes, + ); + const resolvedLocalSharedClientModuleId = + explicitClientModule?.kind === 'shared' + ? resolveSharedConsumeModuleId( + rawClientManifestKey, + shareKeyToModuleId, + explicitClientModule.shareKey, + ) + : resolveSharedConsumeModuleId( + rawClientManifestKey, + shareKeyToModuleId, + ); + const resolvedShareKey = + explicitClientModule?.kind === 'shared' + ? explicitClientModule.shareKey + : resolvedLocalSharedClientModuleId + ? undefined + : resolveShareKeyFromClientManifestKey(rawClientManifestKey); + const resolvedRemoteExposeClientModuleId = + entryExpose && !resolvedLocalSharedClientModuleId + ? getRemoteClientReferenceModuleId(alias, entryExpose) + : undefined; + + const fallbackClientModuleId = rawRemoteClientModuleId + ? namespaceModuleIdDeterministically(alias, rawRemoteClientModuleId) + : namespaceModuleIdDeterministically(alias, rawClientManifestKey); + const resolvedSyntheticSharedClientModuleId = + !resolvedLocalSharedClientModuleId && + resolvedShareKey && + rawRemoteClientModuleId + ? namespaceModuleIdDeterministically(alias, rawRemoteClientModuleId) + : undefined; + if (resolvedSyntheticSharedClientModuleId && resolvedShareKey) { + installSyntheticSharedClientModuleFactory( + resolvedSyntheticSharedClientModuleId, + resolvedShareKey, + ); + } + + const nextClientIdKind: ClientModuleResolutionKind = + resolvedLocalSharedClientModuleId || + resolvedSyntheticSharedClientModuleId + ? 'shared' + : 'remote'; + let finalClientModuleId = + resolvedLocalSharedClientModuleId || + resolvedSyntheticSharedClientModuleId || + resolvedRemoteExposeClientModuleId || + fallbackClientModuleId; + let finalClientIdKind: ClientModuleResolutionKind = nextClientIdKind; + + if (isObject(nextValue)) { + nextValue.id = finalClientModuleId; + if (finalClientIdKind === 'shared') { + nextValue.chunks = []; + } else if (entryExpose) { + installSyntheticRemoteClientModuleFactory( + finalClientModuleId, + alias, + entryExpose, + ); + nextValue.chunks = []; + nextValue.async = true; + } + } + const clientManifest = hostManifest.clientManifest as Record< + string, + any + >; + const upsertClientManifestEntry = ( + key: string, + entry: unknown, + preferIncomingNonNamespaced: boolean, + ) => { + if (!Object.prototype.hasOwnProperty.call(clientManifest, key)) { + clientManifest[key] = entry; + return entry; + } + const existingEntry = clientManifest[key]; + if (stableStringify(existingEntry) === stableStringify(entry)) { + return existingEntry; + } + if ( + isObject(existingEntry) && + isObject(entry) && + existingEntry.id != null && + entry.id != null + ) { + const existingId = String(existingEntry.id); + const nextId = String(entry.id); + const existingIsNamespaced = isNamespacedRemoteIdForAlias( + alias, + existingId, + ); + const nextIsNamespaced = isNamespacedRemoteIdForAlias( + alias, + nextId, + ); + if (!existingIsNamespaced && !nextIsNamespaced) { + if (preferIncomingNonNamespaced && existingId !== nextId) { + clientManifest[key] = entry; + return entry; + } + return existingEntry; + } + if (!existingIsNamespaced && nextIsNamespaced) { + return existingEntry; + } + if (existingIsNamespaced && !nextIsNamespaced) { + clientManifest[key] = entry; + return entry; + } + } + throw new Error( + `[mf:rsc-bridge] clientManifest conflict for "${key}" while merging remote "${alias}"`, + ); + }; + + const scopedAssignedValue = upsertClientManifestEntry( + scopedClientManifestKey, + nextValue, + false, + ); + const rawAssignedValue = upsertClientManifestEntry( + rawClientManifestKey, + nextValue, + false, + ); + const preferredAssignedValue = isObject(rawAssignedValue) + ? rawAssignedValue + : scopedAssignedValue; + + if ( + isObject(preferredAssignedValue) && + preferredAssignedValue.id != null + ) { + finalClientModuleId = String(preferredAssignedValue.id); + if (!isNamespacedRemoteIdForAlias(alias, finalClientModuleId)) { + finalClientIdKind = 'shared'; + } + if (isObject(nextValue)) { + nextValue.id = finalClientModuleId; + if (finalClientIdKind === 'shared') { + nextValue.chunks = []; + } else if (entryExpose) { + installSyntheticRemoteClientModuleFactory( + finalClientModuleId, + alias, + entryExpose, + ); + nextValue.chunks = []; + nextValue.async = true; + } + } + } + + if (isObject(nextValue) && rawRemoteClientModuleId) { + if ( + !Array.isArray( + clientManifestEntriesByRawId[rawRemoteClientModuleId], + ) + ) { + clientManifestEntriesByRawId[rawRemoteClientModuleId] = []; + } + clientManifestEntriesByRawId[rawRemoteClientModuleId].push( + nextValue as ManifestExport, + ); + } + + if (!rawRemoteClientModuleId) { + continue; + } + const existingResolvedId = + resolvedClientIdsByRawId[rawRemoteClientModuleId]; + const existingResolvedKind = + resolvedClientIdKindByRawId[rawRemoteClientModuleId]; + if (existingResolvedId && existingResolvedId !== finalClientModuleId) { + if ( + getClientModuleResolutionPriority(existingResolvedKind) >= + getClientModuleResolutionPriority(finalClientIdKind) + ) { + finalClientModuleId = existingResolvedId; + finalClientIdKind = existingResolvedKind || 'remote'; + if (isObject(nextValue)) { + nextValue.id = finalClientModuleId; + if (finalClientIdKind === 'shared') { + nextValue.chunks = []; + } else if (entryExpose) { + installSyntheticRemoteClientModuleFactory( + finalClientModuleId, + alias, + entryExpose, + ); + nextValue.chunks = []; + nextValue.async = true; + } + } + } + } + resolvedClientIdsByRawId[rawRemoteClientModuleId] = finalClientModuleId; + resolvedClientIdKindByRawId[rawRemoteClientModuleId] = + finalClientIdKind; + const prefixedRawRemoteClientModuleId = + rawRemoteClientModuleId.startsWith(SSR_LAYER_PREFIX) + ? rawRemoteClientModuleId + : `${SSR_LAYER_PREFIX}${rawRemoteClientModuleId}`; + const existingPrefixedResolvedId = + resolvedClientIdsByRawId[prefixedRawRemoteClientModuleId]; + const existingPrefixedResolvedKind = + resolvedClientIdKindByRawId[prefixedRawRemoteClientModuleId]; + if ( + existingPrefixedResolvedId && + existingPrefixedResolvedId !== finalClientModuleId + ) { + if ( + getClientModuleResolutionPriority(existingPrefixedResolvedKind) >= + getClientModuleResolutionPriority(finalClientIdKind) + ) { + finalClientModuleId = existingPrefixedResolvedId; + finalClientIdKind = existingPrefixedResolvedKind || 'remote'; + if (isObject(nextValue)) { + nextValue.id = finalClientModuleId; + if (finalClientIdKind === 'shared') { + nextValue.chunks = []; + } else if (entryExpose) { + installSyntheticRemoteClientModuleFactory( + finalClientModuleId, + alias, + entryExpose, + ); + nextValue.chunks = []; + nextValue.async = true; + } + } + } + } + resolvedClientIdsByRawId[prefixedRawRemoteClientModuleId] = + finalClientModuleId; + resolvedClientIdKindByRawId[prefixedRawRemoteClientModuleId] = + finalClientIdKind; + const rawIdEntries = + clientManifestEntriesByRawId[rawRemoteClientModuleId]; + if (Array.isArray(rawIdEntries)) { + for (const entryValue of rawIdEntries) { + entryValue.id = finalClientModuleId; + if (finalClientIdKind === 'shared') { + entryValue.chunks = []; + } else if (entryExpose) { + installSyntheticRemoteClientModuleFactory( + finalClientModuleId, + alias, + entryExpose, + ); + entryValue.chunks = []; + entryValue.async = true; + } + } + } + } + } + + if (isObject(remoteManifest.serverConsumerModuleMap)) { + const remoteServerConsumerModuleMap = + remoteManifest.serverConsumerModuleMap as Record; + const ssrPreloadPromises: Array> = []; + for (const [rawModuleId, value] of Object.entries( + remoteServerConsumerModuleMap, + )) { + const normalizedRawModuleId = String(rawModuleId); + const rscLayerFallbackNode = normalizedRawModuleId.startsWith( + RSC_LAYER_PREFIX, + ) + ? remoteServerConsumerModuleMap[ + `${SSR_LAYER_PREFIX}${stripLayerPrefix(normalizedRawModuleId)}` + ] || + remoteServerConsumerModuleMap[ + stripLayerPrefix(normalizedRawModuleId) + ] + : undefined; + const sourceNode = rscLayerFallbackNode || value; + const inferredExposeHint = inferExposeHintFromConsumerNode(sourceNode); + const resolvedModuleId = + resolveClientModuleIdFromMap(resolvedClientIdsByRawId, rawModuleId) || + namespaceModuleIdDeterministically(alias, rawModuleId); + + const nextValue = remapConsumerNode( + sourceNode, + (rawServerModuleId, exportName) => { + const exportScopedHint = + exportName !== '*' && exportName !== '__esModule' + ? normalizeExpose(exportName) + : inferredExposeHint; + const namespacedServerModuleId = + namespaceServerModuleIdDeterministically( + alias, + rawServerModuleId, + ); + ssrPreloadPromises.push( + preloadSsrModule( + alias, + rawServerModuleId, + exportScopedHint, + args, + ), + ); + return namespacedServerModuleId; + }, + ); + + const serverConsumerModuleMap = + hostManifest.serverConsumerModuleMap as Record; + const upsertServerConsumerNode = ( + key: string, + node: unknown, + preferIncoming = false, + ) => { + if ( + !Object.prototype.hasOwnProperty.call(serverConsumerModuleMap, key) + ) { + serverConsumerModuleMap[key] = node; + return; + } + const existingNode = serverConsumerModuleMap[key]; + if (stableStringify(existingNode) === stableStringify(node)) { + return; + } + if (preferIncoming) { + serverConsumerModuleMap[key] = node; + return; + } + if (!isNamespacedRemoteIdForAlias(alias, key)) { + return; + } + throw new Error( + `[mf:rsc-bridge] serverConsumerModuleMap conflict for "${key}" while merging remote "${alias}"`, + ); + }; + const isLayerSpecificRawModuleId = + rawModuleId.startsWith(SSR_LAYER_PREFIX) || + rawModuleId.startsWith(RSC_LAYER_PREFIX); + const shouldPreferIncomingLayerSpecificNode = + isLayerSpecificRawModuleId && + isNamespacedRemoteIdForAlias(alias, resolvedModuleId); + upsertServerConsumerNode( + resolvedModuleId, + nextValue, + shouldPreferIncomingLayerSpecificNode, + ); + + const prefixedResolvedModuleId = + resolvedModuleId.startsWith(SSR_LAYER_PREFIX) || + resolvedModuleId.startsWith(RSC_LAYER_PREFIX) + ? resolvedModuleId + : `${SSR_LAYER_PREFIX}${resolvedModuleId}`; + upsertServerConsumerNode( + prefixedResolvedModuleId, + nextValue, + shouldPreferIncomingLayerSpecificNode && + isNamespacedRemoteIdForAlias(alias, prefixedResolvedModuleId), + ); + } + if (ssrPreloadPromises.length > 0) { + await Promise.all(ssrPreloadPromises); + } + } + + const remoteScopedManifest = { + moduleLoading: remoteManifest.moduleLoading, + entryCssFiles: remoteManifest.entryCssFiles, + entryJsFiles: remoteManifest.entryJsFiles, + }; + + if (!isObject(hostManifest.remoteManifests)) { + hostManifest.remoteManifests = Object.create(null); + } + (hostManifest.remoteManifests as Record)[alias] = + remoteScopedManifest; + + if (remoteManifest.moduleLoading != null) { + if (isObject(hostManifest.moduleLoading)) { + (hostManifest.moduleLoading as Record)[alias] = + remoteManifest.moduleLoading; + } else { + hostManifest.moduleLoading = { + [alias]: remoteManifest.moduleLoading, + }; + } + } + + if (remoteManifest.entryJsFiles != null) { + if (!Array.isArray(hostManifest.entryJsFiles)) { + const nextEntryJsFiles: unknown[] = []; + if (isObject(hostManifest.entryJsFiles)) { + Object.assign(nextEntryJsFiles, hostManifest.entryJsFiles); + } + hostManifest.entryJsFiles = nextEntryJsFiles; + } + (hostManifest.entryJsFiles as Record)[alias] = + remoteManifest.entryJsFiles; + } + + if (remoteManifest.entryCssFiles != null) { + if ( + isObject(hostManifest.entryCssFiles) || + Array.isArray(hostManifest.entryCssFiles) + ) { + (hostManifest.entryCssFiles as Record)[alias] = + remoteManifest.entryCssFiles; + } else { + hostManifest.entryCssFiles = { + [alias]: remoteManifest.entryCssFiles, + }; + } + } + + if (!isObject(remoteManifest.serverManifest)) { + return; + } + + for (const [rawActionId, actionEntry] of Object.entries( + remoteManifest.serverManifest, + )) { + const prefixedActionId = `${ACTION_PREFIX}${alias}:${rawActionId}`; + const prefixedHostActionEntry = { + id: proxyModuleId, + name: prefixedActionId, + chunks: [], + async: + isObject(actionEntry) && 'async' in actionEntry + ? Boolean((actionEntry as Record)['async']) + : true, + }; + assertNoConflict( + hostManifest.serverManifest as Record, + prefixedActionId, + prefixedHostActionEntry, + alias, + 'serverManifest', + ); + (hostManifest.serverManifest as Record)[prefixedActionId] = + prefixedHostActionEntry; + + const rawHostActionEntry = { + ...prefixedHostActionEntry, + name: rawActionId, + }; + + assertNoConflict( + hostManifest.serverManifest as Record, + rawActionId, + rawHostActionEntry, + alias, + 'serverManifest', + ); + (hostManifest.serverManifest as Record)[rawActionId] = + rawHostActionEntry; + + registerActionProxyExport(actionMap, prefixedActionId, { + alias, + rawActionId, + }); + registerActionProxyExport(actionMap, rawActionId, { + alias, + rawActionId, + }); + registerActionRemap(rawActionId, prefixedActionId); + } + }; + + const ensureRemoteAliasMerged = async (alias: string, args: any) => { + const existingMergePromise = aliasMergePromises[alias]; + if (existingMergePromise) { + await existingMergePromise; + return; + } + if (mergedRemoteAliases.has(alias)) { + return; + } + + const mergePromise = (async () => { + installActionProxyModule({ + actionMap, + ensureBridge, + }); + + let bridge: BridgeModule | undefined; + if (isBridgeExposeRequest(args)) { + bridge = await resolveBridgeFromLoadArgs(args); + if (!bridge) { + return; + } + bridgePromises[alias] = Promise.resolve(bridge); + } else { + bridge = await ensureBridge(alias, args); + } + const remoteManifest = + typeof bridge.getManifest === 'function' + ? await Promise.resolve(bridge.getManifest()) + : {}; + + await mergeRemoteManifest( + alias, + remoteManifest || {}, + ACTION_PROXY_MODULE_ID, + args, + ); + mergedRemoteAliases.add(alias); + })(); + + aliasMergePromises[alias] = mergePromise; + try { + await mergePromise; + } finally { + delete aliasMergePromises[alias]; + } + }; + + seedRemoteClientModuleFactoriesFromRuntimeMapping(); + + return { + name: 'rspack-rsc-bridge-runtime-plugin', + async afterResolve(args: any) { + seedRemoteClientModuleFactoriesFromRuntimeMapping(); + const alias = resolveRemoteAlias(args); + if (!alias) { + return args; + } + if (isBridgeExposeRequest(args)) { + return args; + } + await ensureRemoteAliasMerged(alias, args); + return args; + }, + async onLoad(args: any) { + seedRemoteClientModuleFactoriesFromRuntimeMapping(); + const alias = resolveRemoteAlias(args); + if (!alias) { + return args; + } + if (isBridgeExposeRequest(args) && aliasMergePromises[alias]) { + return args; + } + await ensureRemoteAliasMerged(alias, args); + return args; + }, + }; +}; + +export default rscBridgeRuntimePlugin; diff --git a/packages/webpack-bundler-runtime/src/rscManifest.ts b/packages/webpack-bundler-runtime/src/rscManifest.ts new file mode 100644 index 00000000000..39a546993f9 --- /dev/null +++ b/packages/webpack-bundler-runtime/src/rscManifest.ts @@ -0,0 +1,130 @@ +export type ManifestExport = { + id: string; + name: string; + chunks?: Array; + async?: boolean; +}; + +export type ManifestNode = Record; + +export type RscClientReferenceResolution = + | { + kind: 'local'; + } + | { + kind: 'shared'; + shareKey: string; + shareScope?: string[]; + } + | { + kind: 'remote'; + remoteAlias: string; + request: string; + shareScope?: string[]; + }; + +export type RscClientReference = { + exportName: string; + moduleId: string; + chunks?: Array; + async?: boolean; + resolution: RscClientReferenceResolution; +}; + +export type RscActionReference = { + localActionId: string; + exportName?: string; + moduleResource?: string; +}; + +export type RscServerModuleReference = { + localModuleId?: string; + ssrExpose?: string; + expose?: string; +}; + +export type ManifestLike = { + version?: number; + serverManifest?: Record; + clientManifest?: Record; + serverConsumerModuleMap?: Record; + moduleLoading?: unknown; + entryCssFiles?: Record | Array; + entryJsFiles?: Array | Record; + remoteManifests?: Record< + string, + { + moduleLoading?: unknown; + entryCssFiles?: unknown; + entryJsFiles?: unknown; + } + >; + clientExposes?: Record; + clientReferences?: Record; + actions?: Record; + serverModules?: Record; +}; + +export type RscBridgeModuleV1 = { + getManifest?: () => ManifestLike | Promise; + executeAction?: (actionId: string, args: unknown[]) => Promise; + preloadSSRModule?: ( + moduleId: string, + exposeHint?: string, + ) => Promise; + getSSRModule?: (moduleId: string) => unknown; +}; + +export const getClientManifestKeyWithoutHash = ( + rawClientManifestKey: string, +) => { + const hashIndex = rawClientManifestKey.indexOf('#'); + return hashIndex >= 0 + ? rawClientManifestKey.slice(0, hashIndex) + : rawClientManifestKey; +}; + +export const resolveClientReferenceEntry = ( + manifest: ManifestLike | undefined, + rawClientManifestKey: string, +) => { + const clientReferences = manifest?.clientReferences; + if (!clientReferences) { + return undefined; + } + if ( + Object.prototype.hasOwnProperty.call(clientReferences, rawClientManifestKey) + ) { + return clientReferences[rawClientManifestKey]; + } + const keyWithoutHash = getClientManifestKeyWithoutHash(rawClientManifestKey); + if ( + keyWithoutHash !== rawClientManifestKey && + Object.prototype.hasOwnProperty.call(clientReferences, keyWithoutHash) + ) { + return clientReferences[keyWithoutHash]; + } + return undefined; +}; + +export const resolveActionReference = ( + manifest: ManifestLike | undefined, + actionId: string, +) => { + const actions = manifest?.actions; + if (!actions) { + return undefined; + } + return actions[actionId]; +}; + +export const resolveServerModuleReference = ( + manifest: ManifestLike | undefined, + moduleId: string, +) => { + const serverModules = manifest?.serverModules; + if (!serverModules) { + return undefined; + } + return serverModules[moduleId]; +}; diff --git a/packages/webpack-bundler-runtime/src/types.ts b/packages/webpack-bundler-runtime/src/types.ts index e197f605c9c..d5b98156da8 100644 --- a/packages/webpack-bundler-runtime/src/types.ts +++ b/packages/webpack-bundler-runtime/src/types.ts @@ -215,6 +215,12 @@ export interface GetSharedFallbackGetterOptions { libraryType?: string; } +export interface ResolveRemoteModuleIdOptions { + alias: string; + expose: string; + webpackRequire: WebpackRequire; +} + export interface Federation { runtime?: typeof runtime; instance?: runtime.ModuleFederation; @@ -234,6 +240,9 @@ export interface Federation { webpackRequire: WebpackRequire; libraryType: string; }) => void; + resolveRemoteModuleId: ( + options: ResolveRemoteModuleIdOptions, + ) => string | undefined; getSharedFallbackGetter: ( options: GetSharedFallbackGetterOptions, ) => SharedGetter; diff --git a/packages/webpack-bundler-runtime/tsdown.config.ts b/packages/webpack-bundler-runtime/tsdown.config.ts index 04ea06f2b8b..4f8c8e73b0c 100644 --- a/packages/webpack-bundler-runtime/tsdown.config.ts +++ b/packages/webpack-bundler-runtime/tsdown.config.ts @@ -14,6 +14,8 @@ export default defineConfig([ entry: { index: 'src/index.ts', constant: 'src/constant.ts', + 'rsc-bridge-runtime-plugin': 'src/rscBridgeRuntimePlugin.ts', + 'rsc-bridge-expose': 'src/rscBridgeExpose.ts', bundler: 'src/bundler.ts', }, external: ['@module-federation/*', 'webpack'], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ba1c0777444..1ccb0403938 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -950,7 +950,7 @@ importers: version: 0.80.0(@babel/core@7.29.0) '@react-native/eslint-config': specifier: 0.80.0 - version: 0.80.0(eslint@9.39.3(jiti@2.6.1))(jest@29.7.0(@types/node@20.19.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@20.19.5)(typescript@5.0.4)))(prettier@2.8.8)(typescript@5.0.4) + version: 0.80.0(eslint@9.39.3(jiti@2.6.1))(jest@29.7.0(@types/node@22.19.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.0.4)))(prettier@2.8.8)(typescript@5.0.4) '@react-native/metro-config': specifier: 0.80.0 version: 0.80.0(@babel/core@7.29.0) @@ -986,7 +986,7 @@ importers: version: 9.39.3(jiti@2.6.1) jest: specifier: ^29.6.3 - version: 29.7.0(@types/node@20.19.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@20.19.5)(typescript@5.0.4)) + version: 29.7.0(@types/node@22.19.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.0.4)) nodemon: specifier: ^3.1.9 version: 3.1.14 @@ -1038,7 +1038,7 @@ importers: version: 0.80.0(@babel/core@7.29.0) '@react-native/eslint-config': specifier: 0.80.0 - version: 0.80.0(eslint@9.39.3(jiti@2.6.1))(jest@29.7.0(@types/node@20.19.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@20.19.5)(typescript@5.0.4)))(prettier@2.8.8)(typescript@5.0.4) + version: 0.80.0(eslint@9.39.3(jiti@2.6.1))(jest@29.7.0(@types/node@22.19.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.0.4)))(prettier@2.8.8)(typescript@5.0.4) '@react-native/metro-config': specifier: 0.80.0 version: 0.80.0(@babel/core@7.29.0) @@ -1074,7 +1074,7 @@ importers: version: 9.39.3(jiti@2.6.1) jest: specifier: ^29.6.3 - version: 29.7.0(@types/node@20.19.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@20.19.5)(typescript@5.0.4)) + version: 29.7.0(@types/node@22.19.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.0.4)) nodemon: specifier: ^3.1.9 version: 3.1.14 @@ -1243,6 +1243,43 @@ importers: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + apps/modernjs-rsc: + dependencies: + '@babel/runtime': + specifier: 7.28.2 + version: 7.28.2 + '@modern-js/runtime': + specifier: 3.0.1 + version: 3.0.1(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react-server-dom-webpack@19.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.27.3)(webpack-cli@5.1.4)))(react@18.3.1) + '@module-federation/modern-js-v3': + specifier: workspace:* + version: link:../../packages/modernjs-v3 + react: + specifier: ~18.3.1 + version: 18.3.1 + react-dom: + specifier: ~18.3.1 + version: 18.3.1(react@18.3.1) + devDependencies: + '@modern-js/app-tools': + specifier: 3.0.1 + version: 3.0.1(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(core-js@3.49.0)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.59.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@20.19.5)(typescript@5.0.4))(tsconfig-paths@4.2.0)(tslib@2.8.1)(typescript@5.0.4)(webpack-hot-middleware@2.26.1)(webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.27.3)(webpack-cli@5.1.4)) + '@modern-js/tsconfig': + specifier: 3.0.1 + version: 3.0.1 + '@types/node': + specifier: ^20.19.5 + version: 20.19.5 + '@types/react': + specifier: ~18.2.0 + version: 18.2.79 + '@types/react-dom': + specifier: ~18.3.0 + version: 18.3.7(@types/react@18.2.79) + typescript: + specifier: ~5.0.4 + version: 5.0.4 + apps/modernjs-ssr/dynamic-nested-remote: dependencies: '@babel/runtime': @@ -2040,7 +2077,7 @@ importers: version: 18.3.7(@types/react@18.3.28) tailwindcss: specifier: ^3.4.3 - version: 3.4.13(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@20.19.5)(typescript@5.9.3)) + version: 3.4.13(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.9.3)) typescript: specifier: ^5.4.5 version: 5.9.3 @@ -2089,7 +2126,7 @@ importers: version: 18.3.7(@types/react@18.3.28) tailwindcss: specifier: ^3.4.3 - version: 3.4.13(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@20.19.5)(typescript@5.9.3)) + version: 3.4.13(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.9.3)) typescript: specifier: ^5.4.5 version: 5.9.3 @@ -2120,7 +2157,7 @@ importers: version: 1.2.7(@rsbuild/core@1.7.3)(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(vue@3.5.30(typescript@5.9.3)) tailwindcss: specifier: ^3.4.3 - version: 3.4.13(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@20.19.5)(typescript@5.9.3)) + version: 3.4.13(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.9.3)) typescript: specifier: ^5.4.2 version: 5.9.3 @@ -2240,7 +2277,7 @@ importers: version: 0.5.1 tailwindcss: specifier: ^3.4.3 - version: 3.4.13(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@20.19.5)(typescript@5.9.3)) + version: 3.4.13(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.9.3)) typescript: specifier: ^5.4.2 version: 5.9.3 @@ -2821,13 +2858,13 @@ importers: version: link:../../packages/rspress-plugin '@rsbuild/plugin-sass': specifier: ^1.5.0 - version: 1.5.1(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.36.1)) + version: 1.5.1(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0)) '@rspress/core': specifier: 2.0.3 - version: 2.0.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@types/react@19.2.14)(core-js@3.36.1)(webpack-hot-middleware@2.26.1) + version: 2.0.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@types/react@19.2.14)(core-js@3.49.0)(webpack-hot-middleware@2.26.1) '@rspress/plugin-llms': specifier: 2.0.1 - version: 2.0.1(@rspress/core@2.0.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@types/react@19.2.14)(core-js@3.36.1)(webpack-hot-middleware@2.26.1)) + version: 2.0.1(@rspress/core@2.0.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@types/react@19.2.14)(core-js@3.49.0)(webpack-hot-middleware@2.26.1)) framer-motion: specifier: ^10.0.0 version: 10.18.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -2845,7 +2882,7 @@ importers: version: 3.4.13(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@20.19.5)(typescript@5.9.3)) xgplayer: specifier: ^3.0.16 - version: 3.0.23(core-js@3.36.1) + version: 3.0.23(core-js@3.49.0) devDependencies: '@types/node': specifier: ^20.19.5 @@ -3771,6 +3808,9 @@ importers: '@modern-js/runtime': specifier: 3.0.1 version: 3.0.1(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react-server-dom-webpack@19.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.25.5)(webpack-cli@5.1.4)))(react@18.3.1) + '@modern-js/server-core': + specifier: 3.0.1 + version: 3.0.1(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@modern-js/server-runtime': specifier: 3.0.1 version: 3.0.1(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -29358,6 +29398,41 @@ snapshots: - supports-color - ts-node + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.0.4))': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.5 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@20.19.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.0.4)) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + '@jest/create-cache-key-function@29.7.0': dependencies: '@jest/types': 29.6.3 @@ -30237,6 +30312,57 @@ snapshots: - webpack - webpack-hot-middleware + '@modern-js/app-tools@3.0.1(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(core-js@3.49.0)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.59.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@20.19.5)(typescript@5.0.4))(tsconfig-paths@4.2.0)(tslib@2.8.1)(typescript@5.0.4)(webpack-hot-middleware@2.26.1)(webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.27.3)(webpack-cli@5.1.4))': + dependencies: + '@babel/parser': 7.29.2 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@modern-js/builder': 3.0.1(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(esbuild@0.25.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tslib@2.8.1)(typescript@5.0.4)(webpack-hot-middleware@2.26.1)(webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.27.3)(webpack-cli@5.1.4)) + '@modern-js/i18n-utils': 3.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@modern-js/plugin': 3.0.1(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@modern-js/plugin-data-loader': 3.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@modern-js/prod-server': 3.0.1(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@modern-js/server': 3.0.1(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@20.19.5)(typescript@5.0.4))(tsconfig-paths@4.2.0) + '@modern-js/server-core': 3.0.1(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@modern-js/server-utils': 3.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@modern-js/types': 3.0.1 + '@modern-js/utils': 3.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0) + '@swc/helpers': 0.5.19 + es-module-lexer: 1.7.0 + esbuild: 0.25.5 + esbuild-register: 3.6.0(esbuild@0.25.5) + flatted: 3.4.1 + mlly: 1.8.1 + ndepe: 0.1.13(encoding@0.1.13)(rollup@4.59.0) + pkg-types: 1.3.1 + std-env: 3.10.0 + optionalDependencies: + ts-node: 10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@20.19.5)(typescript@5.0.4) + tsconfig-paths: 4.2.0 + transitivePeerDependencies: + - '@module-federation/runtime-tools' + - '@parcel/css' + - '@rspack/core' + - '@swc/css' + - bufferutil + - clean-css + - core-js + - csso + - debug + - devcert + - encoding + - lightningcss + - react + - react-dom + - rollup + - supports-color + - tslib + - typescript + - utf-8-validate + - webpack + - webpack-hot-middleware + '@modern-js/app-tools@3.0.1(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(core-js@3.49.0)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.59.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.9.3))(tsconfig-paths@4.2.0)(tslib@2.8.1)(typescript@5.9.3)(webpack-hot-middleware@2.26.1)(webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.25.5)(webpack-cli@5.1.4))': dependencies: '@babel/parser': 7.29.2 @@ -30471,6 +30597,57 @@ snapshots: - webpack - webpack-hot-middleware + '@modern-js/builder@3.0.1(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(esbuild@0.25.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tslib@2.8.1)(typescript@5.0.4)(webpack-hot-middleware@2.26.1)(webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.27.3)(webpack-cli@5.1.4))': + dependencies: + '@modern-js/flight-server-transform-plugin': 3.0.1 + '@modern-js/utils': 3.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/plugin-assets-retry': 1.5.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0)) + '@rsbuild/plugin-check-syntax': 1.6.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0)) + '@rsbuild/plugin-css-minimizer': 1.1.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0))(esbuild@0.25.5)(webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.27.3)(webpack-cli@5.1.4)) + '@rsbuild/plugin-less': 1.6.0(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0)) + '@rsbuild/plugin-react': 1.4.4(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0))(webpack-hot-middleware@2.26.1) + '@rsbuild/plugin-rem': 1.0.5(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0)) + '@rsbuild/plugin-sass': 1.5.0(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0)) + '@rsbuild/plugin-source-build': 1.0.4(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0)) + '@rsbuild/plugin-svgr': 1.3.0(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0))(typescript@5.0.4)(webpack-hot-middleware@2.26.1) + '@rsbuild/plugin-type-check': 1.3.3(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(tslib@2.8.1)(typescript@5.0.4) + '@rsbuild/plugin-typed-css-modules': 1.2.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0)) + '@swc/core': 1.15.10(@swc/helpers@0.5.19) + '@swc/helpers': 0.5.19 + autoprefixer: 10.4.24(postcss@8.5.8) + browserslist: 4.28.1 + core-js: 3.49.0 + cssnano: 6.1.2(postcss@8.5.8) + html-minifier-terser: 7.2.0 + lodash: 4.17.23 + postcss: 8.5.8 + postcss-custom-properties: 13.3.12(postcss@8.5.8) + postcss-flexbugs-fixes: 5.0.2(postcss@8.5.8) + postcss-font-variant: 5.0.0(postcss@8.5.8) + postcss-initial: 4.0.1(postcss@8.5.8) + postcss-media-minmax: 5.0.0(postcss@8.5.8) + postcss-nesting: 12.1.5(postcss@8.5.8) + postcss-page-break: 3.0.4(postcss@8.5.8) + rspack-manifest-plugin: 5.2.1(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@swc/helpers@0.5.19)) + ts-deepmerge: 7.0.3 + transitivePeerDependencies: + - '@module-federation/runtime-tools' + - '@parcel/css' + - '@rspack/core' + - '@swc/css' + - clean-css + - csso + - esbuild + - lightningcss + - react + - react-dom + - supports-color + - tslib + - typescript + - webpack + - webpack-hot-middleware + '@modern-js/builder@3.0.1(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(esbuild@0.25.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tslib@2.8.1)(typescript@5.9.3)(webpack-hot-middleware@2.26.1)(webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.25.5)(webpack-cli@5.1.4))': dependencies: '@modern-js/flight-server-transform-plugin': 3.0.1 @@ -30870,6 +31047,15 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-server-dom-webpack: 19.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.27.3)(webpack-cli@5.1.4(webpack-dev-server@5.2.3)(webpack@5.104.1))) + '@modern-js/render@3.0.1(react-dom@18.3.1(react@18.3.1))(react-server-dom-webpack@19.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.27.3)(webpack-cli@5.1.4)))(react@18.3.1)': + dependencies: + '@modern-js/types': 3.0.1 + '@modern-js/utils': 3.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@swc/helpers': 0.5.19 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-server-dom-webpack: 19.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.27.3)(webpack-cli@5.1.4)) + '@modern-js/rsbuild-plugin-esbuild@2.70.5(@swc/core@1.15.10(@swc/helpers@0.5.19))(webpack-cli@5.1.4)': dependencies: '@swc/helpers': 0.5.19 @@ -31063,6 +31249,35 @@ snapshots: - core-js - react-server-dom-webpack + '@modern-js/runtime@3.0.1(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react-server-dom-webpack@19.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.27.3)(webpack-cli@5.1.4)))(react@18.3.1)': + dependencies: + '@loadable/component': 5.16.7(react@18.3.1) + '@loadable/server': 5.16.7(@loadable/component@5.16.7(react@18.3.1))(react@18.3.1) + '@modern-js/plugin': 3.0.1(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@modern-js/plugin-data-loader': 3.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@modern-js/render': 3.0.1(react-dom@18.3.1(react@18.3.1))(react-server-dom-webpack@19.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.27.3)(webpack-cli@5.1.4)))(react@18.3.1) + '@modern-js/runtime-utils': 3.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@modern-js/types': 3.0.1 + '@modern-js/utils': 3.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@swc/helpers': 0.5.19 + '@swc/plugin-loadable-components': 11.7.0 + '@types/loadable__component': 5.13.10 + '@types/react-helmet': 6.1.11 + cookie: 0.7.2 + entities: 7.0.1 + es-module-lexer: 1.7.0 + esbuild: 0.25.5 + invariant: 2.2.4 + isbot: 3.8.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-helmet: 6.1.0(react@18.3.1) + react-is: 18.3.1 + transitivePeerDependencies: + - '@module-federation/runtime-tools' + - core-js + - react-server-dom-webpack + '@modern-js/server-core@2.70.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@modern-js/plugin': 2.70.5 @@ -35304,6 +35519,27 @@ snapshots: - supports-color - typescript + '@react-native/eslint-config@0.80.0(eslint@9.39.3(jiti@2.6.1))(jest@29.7.0(@types/node@22.19.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.0.4)))(prettier@2.8.8)(typescript@5.0.4)': + dependencies: + '@babel/core': 7.29.0 + '@babel/eslint-parser': 7.28.6(@babel/core@7.29.0)(eslint@9.39.3(jiti@2.6.1)) + '@react-native/eslint-plugin': 0.80.0 + '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.0.4))(eslint@9.39.3(jiti@2.6.1))(typescript@5.0.4) + '@typescript-eslint/parser': 7.18.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.0.4) + eslint: 9.39.3(jiti@2.6.1) + eslint-config-prettier: 8.10.2(eslint@9.39.3(jiti@2.6.1)) + eslint-plugin-eslint-comments: 3.2.0(eslint@9.39.3(jiti@2.6.1)) + eslint-plugin-ft-flow: 2.0.3(@babel/eslint-parser@7.28.6(@babel/core@7.29.0)(eslint@9.39.3(jiti@2.6.1)))(eslint@9.39.3(jiti@2.6.1)) + eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.0.4))(eslint@9.39.3(jiti@2.6.1))(typescript@5.0.4))(eslint@9.39.3(jiti@2.6.1))(jest@29.7.0(@types/node@22.19.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.0.4)))(typescript@5.0.4) + eslint-plugin-react: 7.37.2(eslint@9.39.3(jiti@2.6.1)) + eslint-plugin-react-hooks: 5.2.0(eslint@9.39.3(jiti@2.6.1)) + eslint-plugin-react-native: 4.1.0(eslint@9.39.3(jiti@2.6.1)) + prettier: 2.8.8 + transitivePeerDependencies: + - jest + - supports-color + - typescript + '@react-native/eslint-plugin@0.80.0': {} '@react-native/gradle-plugin@0.80.0': {} @@ -35864,16 +36100,6 @@ snapshots: transitivePeerDependencies: - '@module-federation/runtime-tools' - '@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.36.1)': - dependencies: - '@rspack/core': 2.0.0-beta.0(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@swc/helpers@0.5.19) - '@swc/helpers': 0.5.19 - jiti: 2.6.1 - optionalDependencies: - core-js: 3.36.1 - transitivePeerDependencies: - - '@module-federation/runtime-tools' - '@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0)': dependencies: '@rspack/core': 2.0.0-beta.0(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@swc/helpers@0.5.19) @@ -36029,6 +36255,21 @@ snapshots: - lightningcss - webpack + '@rsbuild/plugin-css-minimizer@1.1.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0))(esbuild@0.25.5)(webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.27.3)(webpack-cli@5.1.4))': + dependencies: + css-minimizer-webpack-plugin: 7.0.2(esbuild@0.25.5)(webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.27.3)(webpack-cli@5.1.4)) + reduce-configs: 1.1.1 + optionalDependencies: + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0) + transitivePeerDependencies: + - '@parcel/css' + - '@swc/css' + - clean-css + - csso + - esbuild + - lightningcss + - webpack + '@rsbuild/plugin-less@1.6.0(@rsbuild/core@1.7.3)': dependencies: '@rsbuild/core': 1.7.3 @@ -36138,14 +36379,6 @@ snapshots: transitivePeerDependencies: - webpack-hot-middleware - '@rsbuild/plugin-react@1.4.5(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.36.1))(webpack-hot-middleware@2.26.1)': - dependencies: - '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.36.1) - '@rspack/plugin-react-refresh': 1.6.1(react-refresh@0.18.0)(webpack-hot-middleware@2.26.1) - react-refresh: 0.18.0 - transitivePeerDependencies: - - webpack-hot-middleware - '@rsbuild/plugin-react@1.4.5(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0))(webpack-hot-middleware@2.26.1)': dependencies: '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0) @@ -36186,7 +36419,7 @@ snapshots: reduce-configs: 1.1.1 sass-embedded: 1.98.0 - '@rsbuild/plugin-sass@1.5.1(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.36.1))': + '@rsbuild/plugin-sass@1.5.1(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0))': dependencies: deepmerge: 4.3.1 loader-utils: 2.0.4 @@ -36194,7 +36427,7 @@ snapshots: reduce-configs: 1.1.1 sass-embedded: 1.98.0 optionalDependencies: - '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.36.1) + '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0) '@rsbuild/plugin-source-build@1.0.4(@rsbuild/core@1.7.3)': dependencies: @@ -37127,13 +37360,13 @@ snapshots: - supports-color - webpack-hot-middleware - '@rspress/core@2.0.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@types/react@19.2.14)(core-js@3.36.1)(webpack-hot-middleware@2.26.1)': + '@rspress/core@2.0.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@types/react@19.2.14)(core-js@3.49.0)(webpack-hot-middleware@2.26.1)': dependencies: '@mdx-js/mdx': 3.1.1 '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.4) - '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.36.1) - '@rsbuild/plugin-react': 1.4.5(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.36.1))(webpack-hot-middleware@2.26.1) - '@rspress/shared': 2.0.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.36.1) + '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/plugin-react': 1.4.5(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0))(webpack-hot-middleware@2.26.1) + '@rspress/shared': 2.0.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0) '@shikijs/rehype': 3.23.0 '@types/unist': 3.0.3 '@unhead/react': 2.1.12(react@19.2.4) @@ -37178,9 +37411,9 @@ snapshots: - supports-color - webpack-hot-middleware - '@rspress/plugin-llms@2.0.1(@rspress/core@2.0.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@types/react@19.2.14)(core-js@3.36.1)(webpack-hot-middleware@2.26.1))': + '@rspress/plugin-llms@2.0.1(@rspress/core@2.0.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@types/react@19.2.14)(core-js@3.49.0)(webpack-hot-middleware@2.26.1))': dependencies: - '@rspress/core': 2.0.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@types/react@19.2.14)(core-js@3.36.1)(webpack-hot-middleware@2.26.1) + '@rspress/core': 2.0.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(@types/react@19.2.14)(core-js@3.49.0)(webpack-hot-middleware@2.26.1) remark-mdx: 3.1.1 remark-parse: 11.0.0 remark-stringify: 11.0.0 @@ -37189,17 +37422,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@rspress/shared@2.0.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.36.1)': - dependencies: - '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.36.1) - '@shikijs/rehype': 3.23.0 - gray-matter: 4.0.3 - lodash-es: 4.17.23 - unified: 11.0.5 - transitivePeerDependencies: - - '@module-federation/runtime-tools' - - core-js - '@rspress/shared@2.0.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0)': dependencies: '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.2.2(node-fetch@3.3.2))(core-js@3.49.0) @@ -38981,18 +39203,18 @@ snapshots: '@types/react-helmet@6.1.11': dependencies: - '@types/react': 18.3.28 + '@types/react': 19.2.14 '@types/react-router-dom@5.3.3': dependencies: '@types/history': 4.7.11 - '@types/react': 18.3.28 + '@types/react': 19.2.14 '@types/react-router': 5.1.20 '@types/react-router@5.1.20': dependencies: '@types/history': 4.7.11 - '@types/react': 18.3.28 + '@types/react': 19.2.14 '@types/react-test-renderer@19.1.0': dependencies: @@ -42889,6 +43111,21 @@ snapshots: - supports-color - ts-node + create-jest@29.7.0(@types/node@22.19.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.0.4)): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@22.19.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.0.4)) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + create-require@1.1.1: {} cron-parser@4.9.0: @@ -43028,6 +43265,18 @@ snapshots: optionalDependencies: esbuild: 0.25.5 + css-minimizer-webpack-plugin@7.0.2(esbuild@0.25.5)(webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.27.3)(webpack-cli@5.1.4)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + cssnano: 7.1.3(postcss@8.4.49) + jest-worker: 29.7.0 + postcss: 8.4.49 + schema-utils: 4.3.3 + serialize-javascript: 6.0.2 + webpack: 5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.27.3)(webpack-cli@5.1.4) + optionalDependencies: + esbuild: 0.25.5 + css-select@4.3.0: dependencies: boolbase: 1.0.0 @@ -43425,7 +43674,7 @@ snapshots: debug@4.1.1: dependencies: - ms: 2.1.1 + ms: 2.1.3 debug@4.3.4: dependencies: @@ -44337,7 +44586,7 @@ snapshots: eslint: 9.26.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.26.0(jiti@2.6.1)))(eslint@9.26.0(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.26.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.26.0(jiti@2.6.1)))(eslint@9.26.0(jiti@2.6.1)))(eslint@9.26.0(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.1(eslint@9.26.0(jiti@2.6.1)) eslint-plugin-react: 7.37.2(eslint@9.26.0(jiti@2.6.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.26.0(jiti@2.6.1)) @@ -44382,7 +44631,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.26.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.26.0(jiti@2.6.1)))(eslint@9.26.0(jiti@2.6.1)))(eslint@9.26.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -44552,7 +44801,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.26.0(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.26.0(jiti@2.6.1)))(eslint@9.26.0(jiti@2.6.1)))(eslint@9.26.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -44592,6 +44841,17 @@ snapshots: - supports-color - typescript + eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.0.4))(eslint@9.39.3(jiti@2.6.1))(typescript@5.0.4))(eslint@9.39.3(jiti@2.6.1))(jest@29.7.0(@types/node@22.19.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.0.4)))(typescript@5.0.4): + dependencies: + '@typescript-eslint/utils': 5.62.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.0.4) + eslint: 9.39.3(jiti@2.6.1) + optionalDependencies: + '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.0.4))(eslint@9.39.3(jiti@2.6.1))(typescript@5.0.4) + jest: 29.7.0(@types/node@22.19.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.0.4)) + transitivePeerDependencies: + - supports-color + - typescript + eslint-plugin-jsx-a11y@6.10.1(eslint@9.26.0(jiti@2.6.1)): dependencies: aria-query: 5.3.2 @@ -47357,6 +47617,25 @@ snapshots: - supports-color - ts-node + jest-cli@29.7.0(@types/node@22.19.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.0.4)): + dependencies: + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.0.4)) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@22.19.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.0.4)) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@22.19.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.0.4)) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest-config@29.7.0(@types/node@20.19.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.7.26(@swc/helpers@0.5.13))(@types/node@20.19.5)(typescript@5.9.3)): dependencies: '@babel/core': 7.29.0 @@ -47450,6 +47729,68 @@ snapshots: - babel-plugin-macros - supports-color + jest-config@29.7.0(@types/node@20.19.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.0.4)): + dependencies: + '@babel/core': 7.29.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.29.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.0 + graceful-fs: 4.2.11 + jest-circus: 29.7.0(babel-plugin-macros@3.1.0) + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.19.5 + ts-node: 10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.0.4) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-config@29.7.0(@types/node@22.19.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.0.4)): + dependencies: + '@babel/core': 7.29.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.29.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.0 + graceful-fs: 4.2.11 + jest-circus: 29.7.0(babel-plugin-macros@3.1.0) + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.19.15 + ts-node: 10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.0.4) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-diff@29.7.0: dependencies: chalk: 4.1.2 @@ -47733,6 +48074,18 @@ snapshots: - supports-color - ts-node + jest@29.7.0(@types/node@22.19.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.0.4)): + dependencies: + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.0.4)) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@22.19.15)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.0.4)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jiti@1.21.7: {} jiti@2.4.2: {} @@ -50544,6 +50897,14 @@ snapshots: postcss: 8.4.49 ts-node: 10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.6.3) + postcss-load-config@4.0.2(postcss@8.4.49)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.9.3)): + dependencies: + lilconfig: 3.1.3 + yaml: 2.8.2 + optionalDependencies: + postcss: 8.4.49 + ts-node: 10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.9.3) + postcss-load-config@4.0.2(postcss@8.5.8)(ts-node@10.9.1(@swc/core@1.7.26(@swc/helpers@0.5.13))(@types/node@20.19.5)(typescript@5.9.3)): dependencies: lilconfig: 3.1.3 @@ -53189,6 +53550,15 @@ snapshots: webpack: 5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.27.3)(webpack-cli@5.1.4(webpack-dev-server@5.2.3)(webpack@5.104.1)) webpack-sources: 3.3.4 + react-server-dom-webpack@19.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.27.3)(webpack-cli@5.1.4)): + dependencies: + acorn-loose: 8.5.2 + neo-async: 2.6.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + webpack: 5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.27.3)(webpack-cli@5.1.4) + webpack-sources: 3.3.4 + react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.18.20)(webpack-cli@5.1.4)): dependencies: acorn-loose: 8.5.2 @@ -55355,6 +55725,33 @@ snapshots: transitivePeerDependencies: - ts-node + tailwindcss@3.4.13(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.9.3)): + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 2.1.0 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.4.49 + postcss-import: 15.1.0(postcss@8.4.49) + postcss-js: 4.1.0(postcss@8.4.49) + postcss-load-config: 4.0.2(postcss@8.4.49)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.9.3)) + postcss-nested: 6.2.0(postcss@8.4.49) + postcss-selector-parser: 6.1.2 + resolve: 1.22.8 + sucrase: 3.35.1 + transitivePeerDependencies: + - ts-node + tailwindcss@3.4.16(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.6.3)): dependencies: '@alloc/quick-lru': 5.2.0 @@ -55573,6 +55970,17 @@ snapshots: '@swc/core': 1.15.10(@swc/helpers@0.5.19) esbuild: 0.27.3 + terser-webpack-plugin@5.4.0(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.27.3)(webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.27.3)(webpack-cli@5.1.4)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.46.1 + webpack: 5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.27.3)(webpack-cli@5.1.4) + optionalDependencies: + '@swc/core': 1.15.10(@swc/helpers@0.5.19) + esbuild: 0.27.3 + terser-webpack-plugin@5.4.0(@swc/core@1.15.8(@swc/helpers@0.5.19))(esbuild@0.18.20)(webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.18.20)(webpack-cli@5.1.4)): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -56053,6 +56461,27 @@ snapshots: optionalDependencies: '@swc/core': 1.15.10(@swc/helpers@0.5.19) + ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.0.4): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.19.15 + acorn: 8.16.0 + acorn-walk: 8.3.5 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.4 + make-error: 1.3.6 + typescript: 5.0.4 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.15.10(@swc/helpers@0.5.19) + optional: true + ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@22.19.15)(typescript@5.6.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -57744,6 +58173,40 @@ snapshots: - esbuild - uglify-js + webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.27.3)(webpack-cli@5.1.4): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.16.0 + acorn-import-phases: 1.0.4(acorn@8.16.0) + browserslist: 4.28.1 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.20.1 + es-module-lexer: 2.0.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.1 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.0 + terser-webpack-plugin: 5.4.0(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.27.3)(webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.19))(esbuild@0.27.3)(webpack-cli@5.1.4)) + watchpack: 2.5.1 + webpack-sources: 3.3.4 + optionalDependencies: + webpack-cli: 5.1.4(webpack-dev-server@5.2.3)(webpack@5.104.1) + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + webpack@5.104.1(@swc/core@1.15.8(@swc/helpers@0.5.19))(esbuild@0.18.20)(webpack-cli@5.1.4): dependencies: '@types/eslint-scope': 3.7.7 @@ -58053,19 +58516,19 @@ snapshots: dependencies: os-paths: 4.4.0 - xgplayer-subtitles@3.0.23(core-js@3.36.1): + xgplayer-subtitles@3.0.23(core-js@3.49.0): dependencies: - core-js: 3.36.1 + core-js: 3.49.0 eventemitter3: 4.0.7 - xgplayer@3.0.23(core-js@3.36.1): + xgplayer@3.0.23(core-js@3.49.0): dependencies: - core-js: 3.36.1 + core-js: 3.49.0 danmu.js: 1.2.1 delegate: 3.2.0 downloadjs: 1.4.7 eventemitter3: 4.0.7 - xgplayer-subtitles: 3.0.23(core-js@3.36.1) + xgplayer-subtitles: 3.0.23(core-js@3.49.0) xml-name-validator@4.0.0: {}