diff --git a/.changeset/add-metro-plugin-rock.md b/.changeset/add-metro-plugin-rock.md deleted file mode 100644 index 0c442997fe5..00000000000 --- a/.changeset/add-metro-plugin-rock.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@module-federation/metro-plugin-rock': minor ---- - -feat(metro): add metro-plugin-rock for Rock integration diff --git a/.changeset/basic-proxy-core.md b/.changeset/basic-proxy-core.md deleted file mode 100644 index 966106213e6..00000000000 --- a/.changeset/basic-proxy-core.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@module-federation/devtools": patch ---- - -Consolidate the Chrome DevTools proxy logic into a bundled proxy asset. diff --git a/.changeset/deprecate-metro-plugin-rnef.md b/.changeset/deprecate-metro-plugin-rnef.md deleted file mode 100644 index 295921c0526..00000000000 --- a/.changeset/deprecate-metro-plugin-rnef.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@module-federation/metro-plugin-rnef': patch ---- - -chore(metro): deprecate metro-plugin-rnef in favor of metro-plugin-rock diff --git a/.changeset/fix-4576-metro-cache-release.md b/.changeset/fix-4576-metro-cache-release.md deleted file mode 100644 index 18154e888cd..00000000000 --- a/.changeset/fix-4576-metro-cache-release.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@module-federation/metro': minor -'@module-federation/sdk': patch ---- - -feat(metro): add manifest SHA-256 bundle hashes and optional cache layer integration for bundle loading. - -Credit: originally contributed by @zhongwuzw in #4576. diff --git a/.changeset/get-instance-finder.md b/.changeset/get-instance-finder.md deleted file mode 100644 index 62b15060d92..00000000000 --- a/.changeset/get-instance-finder.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@module-federation/runtime': minor -'website-new': patch ---- - -feat(runtime): support finder callbacks in `getInstance` and clarify runtime instance API usage. diff --git a/.changeset/metro-dev-manifest-hashes.md b/.changeset/metro-dev-manifest-hashes.md deleted file mode 100644 index a6a3b74934b..00000000000 --- a/.changeset/metro-dev-manifest-hashes.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@module-federation/metro': patch ---- - -feat(metro): support manifest hashes in dev builds to enable testing federated module caching during development diff --git a/.changeset/read-manifest-type-urls.md b/.changeset/read-manifest-type-urls.md deleted file mode 100644 index 7e6a75ae705..00000000000 --- a/.changeset/read-manifest-type-urls.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@module-federation/dts-plugin": patch ---- - -fix(dts-plugin): read manifest metadata when consuming DTS files from manifest remotes. diff --git a/apps/node-dynamic-remote-new-version/CHANGELOG.md b/apps/node-dynamic-remote-new-version/CHANGELOG.md index d956474e6c0..6a7d7ee2559 100644 --- a/apps/node-dynamic-remote-new-version/CHANGELOG.md +++ b/apps/node-dynamic-remote-new-version/CHANGELOG.md @@ -1,5 +1,11 @@ # node-dynamic-remote-new-version +## 1.0.10 + +### Patch Changes + +- @module-federation/node@2.7.43 + ## 1.0.9 ### Patch Changes diff --git a/apps/node-dynamic-remote-new-version/package.json b/apps/node-dynamic-remote-new-version/package.json index 53bcacfd47d..8d2c4226a46 100644 --- a/apps/node-dynamic-remote-new-version/package.json +++ b/apps/node-dynamic-remote-new-version/package.json @@ -6,7 +6,7 @@ "@module-federation/node": "workspace:*", "lodash": "4.17.23" }, - "version": "1.0.9", + "version": "1.0.10", "scripts": { "build": "NODE_OPTIONS=--max_old_space_size=4096 pnpm exec webpack-cli --config webpack.config.js --output-path dist --mode production", "build:development": "NODE_OPTIONS=--max_old_space_size=4096 pnpm exec webpack-cli --config webpack.config.js --output-path dist --mode development", diff --git a/apps/node-dynamic-remote/CHANGELOG.md b/apps/node-dynamic-remote/CHANGELOG.md index 5d167e7e7c7..1736fc49e3f 100644 --- a/apps/node-dynamic-remote/CHANGELOG.md +++ b/apps/node-dynamic-remote/CHANGELOG.md @@ -1,5 +1,11 @@ # node-dynamic-remote +## 1.0.10 + +### Patch Changes + +- @module-federation/node@2.7.43 + ## 1.0.9 ### Patch Changes diff --git a/apps/node-dynamic-remote/package.json b/apps/node-dynamic-remote/package.json index 693e04a07da..11f6f42b3dd 100644 --- a/apps/node-dynamic-remote/package.json +++ b/apps/node-dynamic-remote/package.json @@ -6,7 +6,7 @@ "@module-federation/node": "workspace:*", "lodash": "4.17.23" }, - "version": "1.0.9", + "version": "1.0.10", "scripts": { "build": "NODE_OPTIONS=--max_old_space_size=4096 pnpm exec webpack-cli --config webpack.config.js --output-path dist --mode production", "build:development": "NODE_OPTIONS=--max_old_space_size=4096 pnpm exec webpack-cli --config webpack.config.js --output-path dist --mode development", diff --git a/apps/router-demo/router-remote5-2005/CHANGELOG.md b/apps/router-demo/router-remote5-2005/CHANGELOG.md index 69b58dd16cd..54626e40c95 100644 --- a/apps/router-demo/router-remote5-2005/CHANGELOG.md +++ b/apps/router-demo/router-remote5-2005/CHANGELOG.md @@ -1,5 +1,13 @@ # remote5 +## 2.0.12 + +### Patch Changes + +- Updated dependencies [180004d] + - @module-federation/bridge-react@2.5.0 + - @module-federation/rsbuild-plugin@2.5.0 + ## 2.0.11 ### Patch Changes diff --git a/apps/router-demo/router-remote5-2005/package.json b/apps/router-demo/router-remote5-2005/package.json index be51602b6c3..2b429659faa 100644 --- a/apps/router-demo/router-remote5-2005/package.json +++ b/apps/router-demo/router-remote5-2005/package.json @@ -1,7 +1,7 @@ { "name": "remote5", "private": true, - "version": "2.0.11", + "version": "2.0.12", "scripts": { "dev": "rsbuild dev", "build": "rsbuild build", diff --git a/apps/router-demo/router-remote6-2006/CHANGELOG.md b/apps/router-demo/router-remote6-2006/CHANGELOG.md index e2f7ab2b788..31f1d6db3d7 100644 --- a/apps/router-demo/router-remote6-2006/CHANGELOG.md +++ b/apps/router-demo/router-remote6-2006/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 2.0.12 + +### Patch Changes + +- Updated dependencies [180004d] + - @module-federation/bridge-react@2.5.0 + - @module-federation/rsbuild-plugin@2.5.0 + ## 2.0.11 ### Patch Changes diff --git a/apps/router-demo/router-remote6-2006/package.json b/apps/router-demo/router-remote6-2006/package.json index ea69ac8150e..1cd756facf5 100644 --- a/apps/router-demo/router-remote6-2006/package.json +++ b/apps/router-demo/router-remote6-2006/package.json @@ -1,7 +1,7 @@ { "name": "remote6", "private": true, - "version": "2.0.11", + "version": "2.0.12", "scripts": { "dev": "rsbuild dev", "build": "rsbuild build", diff --git a/apps/runtime-demo/3005-runtime-host/cypress/e2e/app.cy.ts b/apps/runtime-demo/3005-runtime-host/cypress/e2e/app.cy.ts index 4383ea93ec1..f37b336bb8a 100644 --- a/apps/runtime-demo/3005-runtime-host/cypress/e2e/app.cy.ts +++ b/apps/runtime-demo/3005-runtime-host/cypress/e2e/app.cy.ts @@ -1,5 +1,50 @@ import { getH1, getH3 } from '../support/app.po'; +const getObservabilityReader = (win: Cypress.AUTWindow) => + (win as any).__FEDERATION__?.__OBSERVABILITY__?.runtime_host; + +type ObservabilityTestReport = { + traceId: string; + status?: string; + errorCode?: string; + requestId?: string; + summary: { + componentLoaded?: boolean; + outcome?: string; + flags: { + cached?: boolean; + recovered?: boolean; + fallback?: boolean; + }; + phases: { + remoteEntry?: { + cached?: boolean; + recovered?: boolean; + }; + }; + }; + shared?: { + name?: string; + provider?: string; + reason?: string; + availableVersions?: string[]; + }; + events: Array<{ + eventName?: string; + metadata?: Record; + }>; +}; + +const ignoreExpectedObservabilityException = (expectedMessages: string[]) => { + cy.on('uncaught:exception', (error) => { + if (expectedMessages.some((message) => error.message.includes(message))) { + return false; + } + + return undefined; + }); +}; + describe('3005-runtime-host/', () => { beforeEach(() => cy.visit('/')); @@ -77,4 +122,689 @@ describe('3005-runtime-host/', () => { }); }); }); + + describe('observability demo fixture', () => { + beforeEach(() => { + cy.visit('/observability'); + }); + + it('should emit build observability for the host config', () => { + cy.readFile('.mf/observability/build-info.json').then((buildInfo) => { + expect(buildInfo.source).to.equal('manifest'); + expect(buildInfo.moduleFederation.name).to.equal('runtime_host'); + expect( + buildInfo.moduleFederation.remotes.some( + (remote: { entry?: string; alias?: string }) => + remote.alias === 'remote1' && + remote.entry === 'http://127.0.0.1:3006/mf-manifest.json', + ), + ).to.equal(true); + expect(buildInfo.moduleFederation.exposes).to.deep.include({ + name: 'Button', + }); + expect( + buildInfo.moduleFederation.shared.some( + (shared: { name: string }) => shared.name === 'react', + ), + ).to.equal(true); + const reactShared = buildInfo.moduleFederation.shared.find( + (shared: { name: string }) => shared.name === 'react', + ); + expect(reactShared).to.include({ + name: 'react', + requiredVersion: '^18.2.0', + singleton: true, + }); + expect(JSON.stringify(buildInfo)).not.to.contain('/Users/bytedance'); + expect(JSON.stringify(buildInfo)).not.to.contain('token='); + }); + }); + + it('should expose a successful remote loading scenario', () => { + cy.get('[data-testid="observability-load-success"]').click(); + cy.get('[data-testid="observability-load-status"]').contains('success'); + cy.get('[data-testid="observability-remote-result"]') + .find('button.test-remote2') + .contains('Button from antd@4.24.15'); + cy.window().then((win) => { + const reader = getObservabilityReader(win); + expect(reader).to.exist; + + const latestReport = reader.getLatestReport(); + expect(latestReport.status).to.equal('success'); + expect(latestReport.summary.runtimeLoaded).to.equal(true); + expect(latestReport.summary.loadCompleted).to.equal(true); + expect(latestReport.summary.componentLoaded).to.equal(false); + expect(latestReport.summary.outcome).to.equal('runtime-loaded'); + expect(latestReport.summary.phases.loadRemote.status).to.equal( + 'complete', + ); + expect(latestReport.diagnosis.status).to.equal('success'); + expect(latestReport.diagnosis.outcome).to.equal('runtime-loaded'); + expect(latestReport.diagnosis.facts.runtimeLoaded).to.equal(true); + expect(latestReport.summary.phases.remoteEntryInit.duration).to.be.a( + 'number', + ); + }); + cy.get('[data-testid="observability-business-loaded"]').click(); + cy.get('[data-testid="observability-report"]').contains( + 'component:business-loaded', + ); + cy.get('[data-testid="observability-report"]') + .should('contain', '"route": "/observability?token=demo-secret#hash"') + .should('contain', 'token=demo-secret') + .should('contain', '#hash'); + cy.window().then((win) => { + const reader = getObservabilityReader(win); + expect(reader).to.exist; + + const latestReport = reader.getLatestReport(); + expect(latestReport.status).to.equal('success'); + expect(latestReport.summary.componentLoaded).to.equal(true); + expect(latestReport.summary.outcome).to.equal('component-loaded'); + expect(latestReport.diagnosis.outcome).to.equal('component-loaded'); + expect(latestReport.diagnosis.facts.componentLoaded).to.equal(true); + expect(reader.getReport(latestReport.traceId).traceId).to.equal( + latestReport.traceId, + ); + }); + }); + + it('should expose a preloadRemote observability scenario', () => { + cy.get('[data-testid="observability-preload-success"]').click(); + cy.get('[data-testid="observability-load-status"]').contains('success'); + cy.get('[data-testid="observability-report"]') + .should('contain', '"phase": "preload"') + .should('contain', '"outcome": "preloaded"') + .should('contain', 'preload:assets-ready'); + cy.window().then((win) => { + const reader = getObservabilityReader(win); + expect(reader).to.exist; + + const latestReport = reader.getLatestReport(); + expect(latestReport.status).to.equal('success'); + expect(latestReport.summary.preloaded).to.equal(true); + expect(latestReport.summary.outcome).to.equal('preloaded'); + expect(latestReport.diagnosis.title).to.equal( + 'Remote preloaded successfully', + ); + }); + }); + + it('should expose a failed remote loading scenario', () => { + cy.window().then((win) => { + cy.spy(win.console, 'error').as('observabilityError'); + }); + cy.get('[data-testid="observability-load-missing-expose"]').click(); + cy.get('[data-testid="observability-load-status"]').contains('error'); + cy.get('[data-testid="observability-report"]').contains( + 'dynamic-remote/__missing_expose__', + ); + cy.get('[data-testid="observability-error-message"]').should( + 'not.contain', + 'token=', + ); + cy.get('@observabilityError').should( + 'have.been.calledWithMatch', + /Observability report generated[\s\S]*traceId: mf-/, + ); + cy.window().then((win) => { + const reader = getObservabilityReader(win); + const latestReport = reader.getLatestReport(); + expect(latestReport.status).to.equal('error'); + expect(latestReport.traceId).to.match(/^mf-/); + expect(latestReport.summary.loadCompleted).to.equal(true); + expect(latestReport.summary.outcome).to.equal('failed'); + expect( + latestReport.diagnosis.actions.some( + (action: { id: string }) => action.id === 'check-expose', + ), + ).to.equal(true); + expect(reader.getReport(latestReport.traceId).failedPhase).to.equal( + 'expose', + ); + }); + }); + + it('should expose a manifest failure scenario', () => { + cy.get('[data-testid="observability-load-broken-manifest"]').click(); + cy.get('[data-testid="observability-load-status"]').contains('error'); + cy.get('[data-testid="observability-report"]').contains( + 'observability-broken-remote/Button', + ); + cy.get('[data-testid="observability-report"]') + .should('contain', 'demo-secret') + .should('contain', 'token=') + .should('contain', '#hash') + .should('contain', 'RUNTIME-003') + .should('contain', '"ownerHint": "host"') + .should('contain', '"check-manifest-url"'); + cy.get('[data-testid="observability-error-message"]') + .contains('/observability-missing/mf-manifest.json') + .should('not.contain', 'demo-secret') + .should('not.contain', 'token=') + .should('not.contain', '#hash'); + cy.window().then((win) => { + const latestReport = getObservabilityReader(win).getLatestReport(); + expect(latestReport.diagnosis.facts.url).to.equal( + 'http://127.0.0.1:3005/observability-missing/mf-manifest.json?token=demo-secret#hash', + ); + }); + }); + + it('should expose a remote URL failure scenario', () => { + cy.get('[data-testid="observability-load-remote-url-error"]').click(); + cy.get('[data-testid="observability-load-status"]').contains('error'); + cy.get('[data-testid="observability-report"]') + .should('contain', 'observability-remote-url/Button') + .should( + 'contain', + 'http://127.0.0.1:3999/observability-remote-url/mf-manifest.json', + ) + .should('contain', 'RUNTIME-003') + .should('contain', '"check-manifest-url"'); + cy.window().then((win) => { + const latestReport = getObservabilityReader(win).getLatestReport(); + expect(latestReport.status).to.equal('error'); + expect(latestReport.failedPhase).to.equal('manifest'); + expect(latestReport.diagnosis.facts.url).to.equal( + 'http://127.0.0.1:3999/observability-remote-url/mf-manifest.json', + ); + }); + }); + + it('should expose a retry recovered remote scenario', () => { + cy.get('[data-testid="observability-load-retry-recovered"]').click(); + cy.get('[data-testid="observability-load-status"]').contains('success'); + cy.get('[data-testid="observability-report"]') + .should('contain', 'observability-retry-recovered/Button') + .should('contain', 'remoteEntry:load-recovered') + .should('contain', '"recovered": true'); + cy.window().then((win) => { + const latestReport = getObservabilityReader(win).getLatestReport(); + expect(latestReport.status).to.equal('success'); + expect(latestReport.summary.outcome).to.equal('recovered'); + expect(latestReport.summary.flags.fallback).to.equal(false); + expect(latestReport.summary.phases.remoteEntry.recovered).to.equal( + true, + ); + }); + }); + + it('should expose a fallback recovered remote scenario', () => { + cy.get('[data-testid="observability-load-fallback-success"]').click(); + cy.get('[data-testid="observability-load-status"]').contains('success'); + cy.get('[data-testid="observability-report"]') + .should('contain', 'dynamic-remote/__observability_fallback__') + .should('contain', 'remote:load-recovered') + .should('contain', '"fallback": true') + .should('contain', '"outcome": "recovered"'); + cy.window().then((win) => { + const latestReport = getObservabilityReader(win).getLatestReport(); + expect(latestReport.status).to.equal('success'); + expect(latestReport.failedPhase).to.equal('expose'); + expect(latestReport.summary.outcome).to.equal('recovered'); + expect(latestReport.summary.flags.fallback).to.equal(true); + expect(latestReport.summary.flags.recovered).to.equal(true); + expect(latestReport.summary.error.failedPhase).to.equal('expose'); + expect(latestReport.diagnosis.warnings).to.include( + 'Remote loading completed through fallback recovery', + ); + expect( + latestReport.diagnosis.actions.some( + (action: { id: string }) => action.id === 'check-expose', + ), + ).to.equal(true); + }); + }); + + it('should expose a manifest missing fields scenario', () => { + ignoreExpectedObservabilityException(['RUNTIME-013']); + cy.get( + '[data-testid="observability-load-missing-fields-manifest"]', + ).click(); + cy.get('[data-testid="observability-load-status"]').contains('error'); + cy.get('[data-testid="observability-report"]') + .should('contain', 'observability-missing-fields/Button') + .should('contain', 'RUNTIME-013') + .should('contain', 'Missing required fields') + .should('contain', 'metaData') + .should('contain', 'exposes') + .should('contain', 'shared') + .should('contain', '"check-manifest-url"'); + cy.window().then((win) => { + const latestReport = getObservabilityReader(win).getLatestReport(); + expect(latestReport.status).to.equal('error'); + expect(latestReport.failedPhase).to.equal('manifest'); + expect(latestReport.diagnosis.facts.url).to.equal( + 'http://127.0.0.1:3005/observability-fixtures/missing-fields/mf-manifest.json', + ); + }); + }); + + it('should expose a remoteEntry globalName mismatch scenario', () => { + ignoreExpectedObservabilityException(['RUNTIME-001']); + cy.get('[data-testid="observability-load-wrong-global"]').click(); + cy.get('[data-testid="observability-load-status"]').contains('error'); + cy.get('[data-testid="observability-report"]') + .should('contain', 'observability-wrong-global/Button') + .should('contain', 'RUNTIME-001') + .should('contain', 'observability_wrong_global_expected') + .should('contain', '"check-remote-global"'); + cy.window().then((win) => { + const latestReport = getObservabilityReader(win).getLatestReport(); + expect(latestReport.status).to.equal('error'); + expect(latestReport.failedPhase).to.equal('remoteEntry'); + expect(latestReport.errorCode).to.equal('RUNTIME-001'); + expect( + latestReport.diagnosis.actions.some( + (action: { id: string }) => action.id === 'check-remote-global', + ), + ).to.equal(true); + }); + }); + + it('should expose a remoteEntry execution error scenario', () => { + ignoreExpectedObservabilityException([ + 'observability remoteEntry execution failed', + 'ScriptExecutionError', + ]); + cy.get( + '[data-testid="observability-load-remote-entry-execution-error"]', + ).click(); + cy.get('[data-testid="observability-load-status"]').contains('error'); + cy.get('[data-testid="observability-report"]') + .should('contain', 'observability-execution-error/Button') + .should('contain', 'RUNTIME-008') + .should('contain', 'ScriptExecutionError') + .should('contain', '"check-remote-entry"'); + cy.window().then((win) => { + const latestReport = getObservabilityReader(win).getLatestReport(); + expect(latestReport.status).to.equal('error'); + expect(latestReport.failedPhase).to.equal('remoteEntry'); + expect(latestReport.errorCode).to.equal('RUNTIME-008'); + expect(latestReport.diagnosis.facts.resourceErrorType).to.equal( + 'script-execution', + ); + }); + }); + + it('should expose a snapshot match observability scenario', () => { + ignoreExpectedObservabilityException(['RUNTIME-007']); + cy.get('[data-testid="observability-load-snapshot-match-error"]').click(); + cy.get('[data-testid="observability-load-status"]').contains('error'); + cy.get('[data-testid="observability-report"]') + .should('contain', 'observability-snapshot-miss/Button') + .should('contain', 'RUNTIME-007') + .should('contain', 'remote-snapshot') + .should('contain', 'observability-unrelated-snapshot') + .should('contain', '"check-module-info"'); + cy.window().then((win) => { + const latestReport = getObservabilityReader(win).getLatestReport(); + expect(latestReport.status).to.equal('error'); + expect(latestReport.errorCode).to.equal('RUNTIME-007'); + expect(latestReport.ownerHint).to.equal('host'); + expect(latestReport.moduleInfo.reason).to.equal('remote-snapshot'); + expect(latestReport.moduleInfo.matchedCount).to.equal(0); + expect(latestReport.moduleInfo.availableNames).to.include( + 'remote:observability-unrelated-snapshot', + ); + expect( + latestReport.diagnosis.actions.some( + (action: { id: string }) => action.id === 'check-module-info', + ), + ).to.equal(true); + }); + }); + + it('should expose a shared miss observability scenario', () => { + cy.get('[data-testid="observability-shared-miss"]').click(); + cy.get('[data-testid="observability-load-status"]').contains('error'); + cy.get('[data-testid="observability-report"]') + .should('contain', 'observability-missing-shared') + .should('contain', 'custom-share-info-unmatched'); + cy.window().then((win) => { + const latestReport = getObservabilityReader(win).getLatestReport(); + expect(latestReport.status).to.equal('success'); + expect(latestReport.summary.outcome).to.equal('recovered'); + expect(latestReport.summary.flags.recovered).to.equal(true); + expect(latestReport.shared.reason).to.equal( + 'custom-share-info-unmatched', + ); + }); + }); + + it('should expose a shared version mismatch observability scenario', () => { + cy.get('[data-testid="observability-shared-version-mismatch"]').click(); + cy.get('[data-testid="observability-load-status"]').contains('error'); + cy.get('[data-testid="observability-report"]') + .should('contain', '"name": "react"') + .should('contain', '^99.0.0') + .should('contain', 'custom-share-info-unmatched'); + cy.window().then((win) => { + const latestReport = getObservabilityReader(win).getLatestReport(); + expect(latestReport.status).to.equal('success'); + expect(latestReport.summary.outcome).to.equal('recovered'); + expect(latestReport.summary.flags.recovered).to.equal(true); + expect(latestReport.shared.reason).to.equal( + 'custom-share-info-unmatched', + ); + expect(latestReport.shared.availableVersions).to.include('18.3.1'); + }); + }); + + it('should expose a shared unexpected provider observability scenario', () => { + cy.get( + '[data-testid="observability-shared-unexpected-provider"]', + ).click(); + cy.get('[data-testid="observability-load-status"]').contains('success'); + cy.get('[data-testid="observability-report"]') + .should('contain', 'observability-provider-choice') + .should('contain', '"provider": "runtime_remote2"') + .should('contain', '"selectedVersion": "2.0.0"') + .should('contain', 'shared:resolved'); + cy.window().then((win) => { + const latestReport = getObservabilityReader(win).getLatestReport(); + expect(latestReport.status).to.equal('success'); + expect(latestReport.shared.name).to.equal( + 'observability-provider-choice', + ); + expect(latestReport.shared.provider).to.equal('runtime_remote2'); + expect(latestReport.shared.selectedVersion).to.equal('2.0.0'); + expect(latestReport.summary.shared.provider).to.equal( + 'runtime_remote2', + ); + expect(latestReport.diagnosis.facts.provider).to.equal( + 'runtime_remote2', + ); + }); + }); + + it('should expose a multi-consumer loading chain scenario', () => { + cy.get('[data-testid="observability-multi-consumer-chain"]').click(); + cy.get('[data-testid="observability-load-status"]').contains('success'); + cy.get('[data-testid="observability-multi-consumer-results"]') + .should('contain', 'checkout-page loaded') + .should('contain', 'analytics-page loaded') + .should('contain', 'checkout-page-repeat loaded') + .should( + 'contain', + 'observability-checkout-theme from observability_consumer_checkout@1.4.0', + ) + .should( + 'contain', + 'observability-analytics-sdk from observability_consumer_analytics@1.2.0', + ) + .should('contain', 'cached=true'); + cy.get('[data-testid="observability-report"]') + .should('contain', 'multi-consumer-loading-chain') + .should('contain', 'dynamic-remote/ProfileCard') + .should('contain', 'dynamic-remote/AnalyticsPanel') + .should('contain', '"consumer": "checkout-page"') + .should('contain', '"consumer": "analytics-page"') + .should( + 'contain', + '"sharedProvider": "observability_consumer_checkout"', + ) + .should( + 'contain', + '"sharedProvider": "observability_consumer_analytics"', + ) + .should('contain', '"cached": true'); + cy.window().then((win) => { + const reader = getObservabilityReader(win); + const reports = reader.getReports(); + const profileReports = reader.findReports({ + remote: 'runtime_remote2', + expose: 'ProfileCard', + }); + const analyticsReports = reader.findReports({ + remote: 'runtime_remote2', + expose: 'AnalyticsPanel', + }); + const checkoutSharedReports = reader.findReports({ + shared: 'observability-checkout-theme', + }); + const analyticsSharedReports = reader.findReports({ + shared: 'observability-analytics-sdk', + }); + const cachedRemoteReport = ( + profileReports as ObservabilityTestReport[] + ).find((report) => report.summary.flags.cached === true); + const checkoutComponentReport = ( + profileReports as ObservabilityTestReport[] + ).find((report) => + report.events.some( + (event) => + event.eventName === 'component:business-loaded' && + event.metadata?.consumer === 'checkout-page', + ), + ); + + expect(reports.length).to.be.greaterThan(4); + expect(profileReports.length).to.be.greaterThan(1); + expect(analyticsReports.length).to.equal(1); + expect( + (checkoutSharedReports[0] as ObservabilityTestReport).shared + ?.provider, + ).to.equal('observability_consumer_checkout'); + expect( + (analyticsSharedReports[0] as ObservabilityTestReport).shared + ?.provider, + ).to.equal('observability_consumer_analytics'); + expect(cachedRemoteReport?.summary.flags.cached).to.equal(true); + expect( + checkoutComponentReport?.events.some( + (event) => + event.metadata?.sharedTraceId === + (checkoutSharedReports[0] as ObservabilityTestReport).traceId, + ), + ).to.equal(true); + }); + }); + + it('should expose an eager config observability scenario', () => { + cy.get('[data-testid="observability-eager-config-error"]').click(); + cy.get('[data-testid="observability-load-status"]').contains('error'); + cy.get('[data-testid="observability-report"]') + .should('contain', 'observability-async-shared') + .should('contain', 'sync-async-boundary') + .should('contain', 'RUNTIME-005') + .should('contain', '"ownerHint": "shared"'); + cy.window().then((win) => { + const latestReport = getObservabilityReader(win).getLatestReport(); + expect(latestReport.status).to.equal('error'); + expect(latestReport.shared.reason).to.equal('sync-async-boundary'); + expect( + latestReport.diagnosis.actions.some( + (action: { id: string }) => action.id === 'check-eager-config', + ), + ).to.equal(true); + }); + }); + + it('should expose a runtime eager config observability scenario', () => { + cy.get( + '[data-testid="observability-runtime-eager-config-error"]', + ).click(); + cy.get('[data-testid="observability-load-status"]').contains('error'); + cy.get('[data-testid="observability-report"]') + .should('contain', 'observability-runtime-async-shared') + .should('contain', 'sync-async-boundary') + .should('contain', 'RUNTIME-006') + .should('contain', '"ownerHint": "shared"'); + cy.window().then((win) => { + const latestReport = getObservabilityReader(win).getLatestReport(); + expect(latestReport.status).to.equal('error'); + expect(latestReport.errorCode).to.equal('RUNTIME-006'); + expect(latestReport.shared.reason).to.equal('sync-async-boundary'); + expect( + latestReport.diagnosis.actions.some( + (action: { id: string }) => action.id === 'check-eager-config', + ), + ).to.equal(true); + }); + }); + }); + + describe('observability showcase fixture', () => { + beforeEach(() => { + cy.visit('/observability-showcase'); + }); + + it('should present a product page route flow for AI observability', () => { + cy.contains('Suggested prompt').should('not.exist'); + cy.contains('AI-ready evidence').should('not.exist'); + cy.contains('Show latest observability report').should('not.exist'); + cy.contains( + 'This view is loaded by createInstance when the page opens.', + ).should('not.exist'); + cy.get('[data-testid="observability-showcase-status"]').contains( + 'success', + ); + cy.get('[data-testid="remote2-profile-card"]').should( + 'contain', + 'ProfileCard', + ); + cy.window().should((win) => { + const reports = getObservabilityReader(win).getReports(); + const profileReport = reports.find( + (report: ObservabilityTestReport) => + report.requestId === 'dynamic-remote/ProfileCard' && + report.summary.componentLoaded === true, + ); + + expect(profileReport?.summary.componentLoaded).to.equal(true); + expect(profileReport?.summary.outcome).to.equal('component-loaded'); + expect( + profileReport?.events.some( + (event) => + event.eventName === 'component:business-loaded' && + event.metadata?.producer === 'runtime_remote2', + ), + ).to.equal(true); + }); + cy.get('[data-testid="observability-showcase-handoff"]').click(); + cy.get('[data-testid="observability-showcase-handoff-status"]').contains( + 'success', + ); + cy.get('[data-testid="observability-showcase-handoff-results"]') + .should('contain', 'Account desk') + .should('contain', 'Expansion desk') + .should('contain', 'Success desk') + .should('contain', 'dynamic-remote/ProfileCard') + .should('contain', 'dynamic-remote/AnalyticsPanel') + .should('contain', 'renewal-account-context') + .should('contain', 'renewal-insight-context'); + cy.get('[data-testid="observability-showcase-handoff-report"]') + .should('contain', 'renewal-handoff-chain') + .should('contain', 'observability_showcase_account_desk') + .should('contain', 'observability_showcase_expansion_desk'); + cy.window().then((win) => { + const reader = getObservabilityReader(win); + const profileReports = reader.findReports({ + remote: 'runtime_remote2', + expose: 'ProfileCard', + }); + const analyticsReports = reader.findReports({ + remote: 'runtime_remote2', + expose: 'AnalyticsPanel', + }); + const accountSharedReports = reader.findReports({ + shared: 'renewal-account-context', + }); + const insightSharedReports = reader.findReports({ + shared: 'renewal-insight-context', + }); + const handoffProfileReport = ( + profileReports as ObservabilityTestReport[] + ).find((report) => + report.events.some( + (event) => + event.eventName === 'component:business-loaded' && + event.metadata?.scenario === 'renewal-handoff-chain' && + event.metadata?.owner === 'Account desk', + ), + ); + + expect(profileReports.length).to.be.greaterThan(1); + expect(analyticsReports.length).to.be.greaterThan(0); + expect(accountSharedReports.length).to.be.greaterThan(0); + expect(insightSharedReports.length).to.be.greaterThan(0); + expect( + (accountSharedReports as ObservabilityTestReport[]).some( + (report) => + report.shared?.provider === + 'observability_showcase_account_desk' || + report.shared?.provider === 'observability_showcase_success_desk', + ), + ).to.equal(true); + expect( + (insightSharedReports[0] as ObservabilityTestReport).shared?.provider, + ).to.equal('observability_showcase_expansion_desk'); + expect(handoffProfileReport?.summary.componentLoaded).to.equal(true); + }); + cy.get('[data-testid="observability-showcase-load"]').click(); + cy.location('pathname').should( + 'equal', + '/observability-showcase/analytics', + ); + cy.get('[data-testid="observability-showcase-status"]').contains( + 'degraded', + ); + cy.get('[data-testid="observability-showcase-fallback"]').should( + 'contain', + 'Limited analytics view', + ); + cy.get('[data-testid="observability-showcase-message"]').should( + 'contain', + 'Some analytics details are temporarily unavailable', + ); + cy.get('[data-testid="observability-showcase-shared"]').should( + 'contain', + 'Detailed analytics are temporarily limited', + ); + + cy.window().then((win) => { + const reports = getObservabilityReader(win).getReports(); + const customerSdkReport = reports.find( + (report: ObservabilityTestReport) => + report.shared?.name === 'observability-customer-sdk', + ); + + expect( + reports.some( + (report: ObservabilityTestReport) => + report.requestId === 'dynamic-remote/ProfileCard', + ), + ).to.equal(true); + expect( + reports.some( + (report: ObservabilityTestReport) => + report.requestId === 'dynamic-remote/AnalyticsPanel', + ), + ).to.equal(true); + expect( + reports.some( + (report: ObservabilityTestReport) => + report.shared?.name === 'react', + ), + ).to.equal(true); + expect( + reports.some( + (report: ObservabilityTestReport) => + report.shared?.name === 'observability-customer-sdk', + ), + ).to.equal(true); + expect(customerSdkReport?.status).to.equal('success'); + expect(customerSdkReport?.summary.outcome).to.equal('recovered'); + expect(customerSdkReport?.shared?.reason).to.equal( + 'custom-share-info-unmatched', + ); + expect(customerSdkReport?.shared?.availableVersions).to.include( + '2.1.0', + ); + }); + }); + }); }); diff --git a/apps/runtime-demo/3005-runtime-host/package.json b/apps/runtime-demo/3005-runtime-host/package.json index c4e4f70b641..16b5a8ba8b9 100644 --- a/apps/runtime-demo/3005-runtime-host/package.json +++ b/apps/runtime-demo/3005-runtime-host/package.json @@ -4,6 +4,7 @@ "version": "0.0.0", "devDependencies": { "@module-federation/core": "workspace:*", + "@module-federation/observability-plugin": "workspace:*", "@module-federation/runtime": "workspace:*", "@module-federation/typescript": "workspace:*", "@module-federation/enhanced": "workspace:*", @@ -26,7 +27,7 @@ "serve": "NODE_OPTIONS=--max_old_space_size=4096 pnpm exec webpack-cli serve --config webpack.config.js --mode production --port 3005 --no-hot", "serve:development": "NODE_OPTIONS=--max_old_space_size=4096 pnpm exec webpack-cli serve --config webpack.config.js --mode development --port 3005", "serve:production": "NODE_OPTIONS=--max_old_space_size=4096 pnpm exec webpack-cli serve --config webpack.config.js --mode production --port 3005 --no-hot", - "lint": "ESLINT_USE_FLAT_CONFIG=false pnpm exec eslint --no-error-on-unmatched-pattern --ignore-pattern node_modules **/*.{ts,tsx,js,jsx}", + "lint": "ESLINT_USE_FLAT_CONFIG=false pnpm exec eslint --no-error-on-unmatched-pattern --ignore-pattern node_modules src/**/*.{ts,tsx,js,jsx} cypress/**/*.{ts,tsx,js,jsx} cypress.config.ts webpack.config.js remotes.d.ts @mf-types/**/*.ts", "serve-static": "pnpm exec serve dist -l 3005 --cors", "e2e": "pnpm exec cypress run --project . --e2e --config baseUrl=http://127.0.0.1:3005 --browser chrome", "e2e:development": "pnpm exec cypress open --project . --e2e --config baseUrl=http://127.0.0.1:3005 --browser electron", diff --git a/apps/runtime-demo/3005-runtime-host/src/App.tsx b/apps/runtime-demo/3005-runtime-host/src/App.tsx index 2cf2933f74e..5b3bcd1bc0e 100644 --- a/apps/runtime-demo/3005-runtime-host/src/App.tsx +++ b/apps/runtime-demo/3005-runtime-host/src/App.tsx @@ -1,29 +1,62 @@ -import React, { lazy } from 'react'; -import { loadRemote } from '@module-federation/runtime'; -import { Link, Routes, Route, BrowserRouter } from 'react-router-dom'; +import React from 'react'; +import { + Link, + Routes, + Route, + BrowserRouter, + useLocation, +} from 'react-router-dom'; +import ObservabilityDemo from './ObservabilityDemo'; +import ObservabilityShowcase from './ObservabilityShowcase'; import Root from './Root'; import Remote1 from './Remote1'; import Remote2 from './Remote2'; +const AppRoutes = () => { + const location = useLocation(); + const isShowcase = location.pathname.startsWith('/observability-showcase'); + + return ( + <> + {!isShowcase ? ( + <> +

Runtime Demo

+ + + ) : null} + + } /> + } /> + } /> + } /> + } + /> + + + ); +}; + const App = () => ( -

Runtime Demo

- - - } /> - } /> - } /> - +
); diff --git a/apps/runtime-demo/3005-runtime-host/src/ObservabilityDemo.tsx b/apps/runtime-demo/3005-runtime-host/src/ObservabilityDemo.tsx new file mode 100644 index 00000000000..5fa68746234 --- /dev/null +++ b/apps/runtime-demo/3005-runtime-host/src/ObservabilityDemo.tsx @@ -0,0 +1,1100 @@ +import React, { useCallback, useState } from 'react'; +import type { ModuleFederationRuntimePlugin } from '@module-federation/runtime'; +import { + createInstance, + getInstance, + loadRemote, + loadShare, + loadShareSync, + preloadRemote, + registerPlugins, + registerRemotes, +} from '@module-federation/runtime'; +import { observability } from './observability'; + +type LoadStatus = 'idle' | 'loading' | 'success' | 'error'; + +type RemoteComponent = React.ComponentType>; + +const successRequest = 'dynamic-remote/ButtonOldAnt'; +const preloadRemoteName = 'dynamic-remote'; +const missingExposeRequest = 'dynamic-remote/__missing_expose__'; +const brokenManifestEntry = + 'http://127.0.0.1:3005/observability-missing/mf-manifest.json?token=demo-secret#hash'; +const brokenManifestRequest = 'observability-broken-remote/Button'; +const remoteUrlErrorEntry = + 'http://127.0.0.1:3999/observability-remote-url/mf-manifest.json'; +const remoteUrlErrorRequest = 'observability-remote-url/Button'; +const retryRecoveryRemoteName = 'observability_retry_recovered_remote'; +const retryRecoveryManifestEntry = + 'http://127.0.0.1:3005/observability-fixtures/retry-recovered/mf-manifest.json'; +const retryRecoveryRequest = 'observability-retry-recovered/Button'; +const fallbackRequest = 'dynamic-remote/__observability_fallback__'; +const missingFieldsManifestEntry = + 'http://127.0.0.1:3005/observability-fixtures/missing-fields/mf-manifest.json'; +const missingFieldsManifestRequest = 'observability-missing-fields/Button'; +const wrongGlobalManifestEntry = + 'http://127.0.0.1:3005/observability-fixtures/wrong-global/mf-manifest.json'; +const wrongGlobalRequest = 'observability-wrong-global/Button'; +const executionErrorManifestEntry = + 'http://127.0.0.1:3005/observability-fixtures/execution-error/mf-manifest.json'; +const executionErrorRequest = 'observability-execution-error/Button'; +const multiProducerRemoteName = 'runtime_remote2'; +const multiProducerAlias = 'dynamic-remote'; +const multiProducerManifestEntry = 'http://127.0.0.1:3007/mf-manifest.json'; +const snapshotMissRequest = 'observability-snapshot-miss/Button'; +const unexpectedProviderSharedName = 'observability-provider-choice'; +const unexpectedProviderSharedScope = 'observability-provider-scope'; + +type RuntimeRemote = Parameters[0][number]; +type RuntimeShareScope = Parameters< + NonNullable>['initShareScopeMap'] +>[1]; +type SharedProviderValue = { + provider: string; + version: string; +}; + +interface MultiConsumerScenario { + consumer: string; + request: string; + expose: string; + componentName: string; + sharedName: string; + sharedScope: string; + requiredVersion: string; + hostVersion: string; + expectedProvider: string; +} + +interface MultiConsumerResult { + consumer: string; + request: string; + expose: string; + sharedName: string; + sharedProvider: string; + sharedVersion: string; + sharedTraceId: string; + remoteTraceId: string; + remoteEntryCached: boolean; + manifestCached: boolean; + summaryCached: boolean; +} + +interface RegisteredRemoteFailureScenario { + remote: RuntimeRemote; + request: string; + errorPrefix?: string; +} + +const multiConsumerScenarios: MultiConsumerScenario[] = [ + { + consumer: 'checkout-page', + request: `${multiProducerAlias}/ProfileCard`, + expose: './ProfileCard', + componentName: 'ProfileCard', + sharedName: 'observability-checkout-theme', + sharedScope: 'observability-checkout-scope', + requiredVersion: '^1.0.0', + hostVersion: '1.4.0', + expectedProvider: 'observability_consumer_checkout', + }, + { + consumer: 'analytics-page', + request: `${multiProducerAlias}/AnalyticsPanel`, + expose: './AnalyticsPanel', + componentName: 'AnalyticsPanel', + sharedName: 'observability-analytics-sdk', + sharedScope: 'observability-analytics-scope', + requiredVersion: '^1.0.0', + hostVersion: '1.2.0', + expectedProvider: 'observability_consumer_analytics', + }, + { + consumer: 'checkout-page-repeat', + request: `${multiProducerAlias}/ProfileCard`, + expose: './ProfileCard', + componentName: 'ProfileCard', + sharedName: 'observability-checkout-theme', + sharedScope: 'observability-checkout-scope', + requiredVersion: '^1.0.0', + hostVersion: '1.4.0', + expectedProvider: 'observability_consumer_checkout', + }, +]; + +function sanitizeErrorMessage(error: unknown): string { + const message = error instanceof Error ? error.message : String(error); + + return message + .replace(/https?:\/\/[^\s'"<>]+/g, (url) => { + try { + const parsedUrl = new URL(url); + return `${parsedUrl.origin}${parsedUrl.pathname}`; + } catch { + return '[redacted-url]'; + } + }) + .replace( + /\b(token|authorization|cookie|secret|password)=([^&\s]+)/gi, + '$1=[redacted]', + ); +} + +function resolveRemoteComponent(remoteModule: unknown): RemoteComponent | null { + if (typeof remoteModule === 'function') { + return remoteModule as RemoteComponent; + } + + if (remoteModule && typeof remoteModule === 'object') { + const candidate = (remoteModule as { default?: unknown }).default; + + if (typeof candidate === 'function') { + return candidate as RemoteComponent; + } + } + + return null; +} + +function createUnexpectedProviderShareScope(): RuntimeShareScope { + return { + [unexpectedProviderSharedName]: { + '1.0.0': { + version: '1.0.0', + get: () => () => ({ + provider: 'runtime_host', + version: '1.0.0', + }), + lib: () => ({ + provider: 'runtime_host', + version: '1.0.0', + }), + shareConfig: { + requiredVersion: false, + singleton: false, + eager: false, + strictVersion: false, + }, + scope: [unexpectedProviderSharedScope], + useIn: ['runtime_host'], + from: 'runtime_host', + deps: [], + strategy: 'version-first', + }, + '2.0.0': { + version: '2.0.0', + get: () => () => ({ + provider: 'runtime_remote2', + version: '2.0.0', + }), + lib: () => ({ + provider: 'runtime_remote2', + version: '2.0.0', + }), + shareConfig: { + requiredVersion: false, + singleton: false, + eager: false, + strictVersion: false, + }, + scope: [unexpectedProviderSharedScope], + useIn: ['runtime_remote2'], + from: 'runtime_remote2', + deps: [], + strategy: 'version-first', + }, + }, + }; +} + +function ObservabilityFallbackRemote() { + return null; +} + +const observabilityRetryRecoveryPlugin: ModuleFederationRuntimePlugin = { + name: 'observability-retry-recovery-plugin', + async loadEntryError({ + getRemoteEntry, + origin, + remoteInfo, + remoteEntryExports, + globalLoading, + uniqueKey, + }) { + if (remoteInfo.name !== retryRecoveryRemoteName) { + return undefined; + } + + delete globalLoading[uniqueKey]; + + return getRemoteEntry({ + origin, + remoteInfo, + remoteEntryExports, + getEntryUrl: (url: string) => { + const separator = url.includes('?') ? '&' : '?'; + return `${url}${separator}retryCount=1`; + }, + }); + }, +}; + +const observabilityFallbackPlugin: ModuleFederationRuntimePlugin = { + name: 'observability-fallback-plugin', + async errorLoadRemote({ id, lifecycle }) { + if (id !== fallbackRequest || lifecycle !== 'onLoad') { + return undefined; + } + + return { + default: ObservabilityFallbackRemote, + }; + }, +}; + +export default function ObservabilityDemo() { + const [status, setStatus] = useState('idle'); + const [errorMessage, setErrorMessage] = useState(''); + const [remoteComponent, setRemoteComponent] = + useState(null); + const [multiConsumerResults, setMultiConsumerResults] = useState< + MultiConsumerResult[] + >([]); + const [reportText, setReportText] = useState('null'); + + const refreshReport = useCallback(() => { + setReportText( + JSON.stringify(observability.getLatestReport() ?? null, null, 2), + ); + }, []); + + const loadSuccessRemote = useCallback(async () => { + setStatus('loading'); + setErrorMessage(''); + setRemoteComponent(null); + + try { + const remoteModule = await loadRemote(successRequest); + const component = resolveRemoteComponent(remoteModule); + + if (!component) { + throw new Error(`Remote module ${successRequest} has no component`); + } + + setRemoteComponent(() => component); + setStatus('success'); + } catch (error) { + const message = sanitizeErrorMessage(error); + setStatus('error'); + setErrorMessage(message); + } finally { + refreshReport(); + } + }, [refreshReport]); + + const preloadSuccessRemote = useCallback(async () => { + setStatus('loading'); + setErrorMessage(''); + setRemoteComponent(null); + + try { + await preloadRemote([ + { + nameOrAlias: preloadRemoteName, + exposes: ['ButtonOldAnt'], + resourceCategory: 'all', + share: true, + depsRemote: true, + }, + ]); + await new Promise((resolve) => setTimeout(resolve, 80)); + setStatus('success'); + } catch (error) { + const message = sanitizeErrorMessage(error); + setStatus('error'); + setErrorMessage(message); + } finally { + refreshReport(); + } + }, [refreshReport]); + + const loadMissingExpose = useCallback(async () => { + setStatus('loading'); + setErrorMessage(''); + setRemoteComponent(null); + + try { + const remoteModule = await loadRemote(missingExposeRequest); + + if (!remoteModule) { + throw new Error(`Remote module ${missingExposeRequest} returned empty`); + } + + throw new Error( + `Remote module ${missingExposeRequest} unexpectedly loaded`, + ); + } catch (error) { + const message = sanitizeErrorMessage(error); + setStatus('error'); + setErrorMessage(message); + } finally { + refreshReport(); + } + }, [refreshReport]); + + const loadRegisteredRemoteFailure = useCallback( + async (scenario: RegisteredRemoteFailureScenario) => { + setStatus('loading'); + setErrorMessage(''); + setRemoteComponent(null); + + registerRemotes([scenario.remote], { force: true }); + + try { + await loadRemote(scenario.request); + throw new Error( + `Remote module ${scenario.request} unexpectedly loaded`, + ); + } catch (error) { + const message = sanitizeErrorMessage( + `${scenario.errorPrefix || ''} ${error}`, + ); + setStatus('error'); + setErrorMessage(message); + } finally { + refreshReport(); + } + }, + [refreshReport], + ); + + const loadBrokenManifest = useCallback(async () => { + await loadRegisteredRemoteFailure({ + remote: { + name: 'observability_broken_remote', + alias: 'observability-broken-remote', + entry: brokenManifestEntry, + }, + request: brokenManifestRequest, + errorPrefix: brokenManifestEntry, + }); + }, [loadRegisteredRemoteFailure]); + + const loadRemoteUrlError = useCallback(async () => { + await loadRegisteredRemoteFailure({ + remote: { + name: 'observability_remote_url_remote', + alias: 'observability-remote-url', + entry: remoteUrlErrorEntry, + }, + request: remoteUrlErrorRequest, + errorPrefix: remoteUrlErrorEntry, + }); + }, [loadRegisteredRemoteFailure]); + + const loadRetryRecoveredRemote = useCallback(async () => { + setStatus('loading'); + setErrorMessage(''); + setRemoteComponent(null); + + registerPlugins([observabilityRetryRecoveryPlugin]); + registerRemotes( + [ + { + name: retryRecoveryRemoteName, + alias: 'observability-retry-recovered', + entry: retryRecoveryManifestEntry, + }, + ], + { force: true }, + ); + + try { + const remoteModule = await loadRemote(retryRecoveryRequest); + const component = resolveRemoteComponent(remoteModule); + + if (!component) { + throw new Error( + `Remote module ${retryRecoveryRequest} has no component`, + ); + } + + setRemoteComponent(() => component); + setStatus('success'); + } catch (error) { + const message = sanitizeErrorMessage(error); + setStatus('error'); + setErrorMessage(message); + } finally { + refreshReport(); + } + }, [refreshReport]); + + const loadFallbackRemote = useCallback(async () => { + setStatus('loading'); + setErrorMessage(''); + setRemoteComponent(null); + + registerPlugins([observabilityFallbackPlugin]); + + try { + const remoteModule = await loadRemote(fallbackRequest); + const component = resolveRemoteComponent(remoteModule); + + if (!component) { + throw new Error(`Remote module ${fallbackRequest} has no component`); + } + + setRemoteComponent(() => component); + setStatus('success'); + } catch (error) { + const message = sanitizeErrorMessage(error); + setStatus('error'); + setErrorMessage(message); + } finally { + refreshReport(); + } + }, [refreshReport]); + + const loadMissingFieldsManifest = useCallback(async () => { + await loadRegisteredRemoteFailure({ + remote: { + name: 'observability_missing_fields_remote', + alias: 'observability-missing-fields', + entry: missingFieldsManifestEntry, + }, + request: missingFieldsManifestRequest, + errorPrefix: missingFieldsManifestEntry, + }); + }, [loadRegisteredRemoteFailure]); + + const loadWrongGlobalName = useCallback(async () => { + await loadRegisteredRemoteFailure({ + remote: { + name: 'observability_wrong_global_remote', + alias: 'observability-wrong-global', + entry: wrongGlobalManifestEntry, + }, + request: wrongGlobalRequest, + errorPrefix: wrongGlobalManifestEntry, + }); + }, [loadRegisteredRemoteFailure]); + + const loadRemoteEntryExecutionError = useCallback(async () => { + await loadRegisteredRemoteFailure({ + remote: { + name: 'observability_execution_error_remote', + alias: 'observability-execution-error', + entry: executionErrorManifestEntry, + }, + request: executionErrorRequest, + errorPrefix: executionErrorManifestEntry, + }); + }, [loadRegisteredRemoteFailure]); + + const loadSnapshotMatchError = useCallback(async () => { + const federation = ( + globalThis as { + __FEDERATION__?: { + moduleInfo?: Record; + }; + } + ).__FEDERATION__; + + if (federation) { + federation.moduleInfo = { + ...federation.moduleInfo, + 'remote:observability-unrelated-snapshot': { + publicPath: + 'http://127.0.0.1:3005/observability-fixtures/unrelated-snapshot/', + getPublicPath: + 'function getPublicPath(){return "http://127.0.0.1:3005/observability-fixtures/unrelated-snapshot/";}', + remoteEntry: + 'http://127.0.0.1:3005/observability-fixtures/unrelated-snapshot/remoteEntry.js', + globalName: 'observability_unrelated_snapshot', + modules: [{ moduleName: 'Button' }], + shared: [{ sharedName: 'react' }], + }, + }; + } + + await loadRegisteredRemoteFailure({ + remote: { + name: 'observability_snapshot_miss_remote', + alias: 'observability-snapshot-miss', + version: '1.0.0', + }, + request: snapshotMissRequest, + errorPrefix: 'observability-snapshot-miss@1.0.0', + }); + }, [loadRegisteredRemoteFailure]); + + const loadSharedMiss = useCallback(async () => { + setStatus('loading'); + setErrorMessage(''); + setRemoteComponent(null); + + try { + const result = await loadShare('observability-missing-shared', { + customShareInfo: { + version: '1.0.0', + scope: ['observability-missing-scope'], + shareConfig: { + requiredVersion: '^1.0.0', + singleton: false, + }, + }, + }); + + if (result === false) { + throw new Error( + 'Shared miss: observability-missing-shared was not provided by host', + ); + } + + throw new Error('Shared miss scenario unexpectedly loaded'); + } catch (error) { + const message = sanitizeErrorMessage(error); + setStatus('error'); + setErrorMessage(message); + } finally { + refreshReport(); + } + }, [refreshReport]); + + const loadSharedVersionMismatch = useCallback(async () => { + setStatus('loading'); + setErrorMessage(''); + setRemoteComponent(null); + + try { + const result = await loadShare('react', { + customShareInfo: { + shareConfig: { + requiredVersion: '^99.0.0', + singleton: false, + }, + }, + }); + + if (result === false) { + throw new Error('Shared version mismatch: react needs ^99.0.0'); + } + + throw new Error('Shared version mismatch scenario unexpectedly loaded'); + } catch (error) { + const message = sanitizeErrorMessage(error); + setStatus('error'); + setErrorMessage(message); + } finally { + refreshReport(); + } + }, [refreshReport]); + + const loadSharedUnexpectedProvider = useCallback(async () => { + setStatus('loading'); + setErrorMessage(''); + setRemoteComponent(null); + + try { + const instance = getInstance(); + + if (!instance) { + throw new Error('Runtime instance is not initialized'); + } + + instance.initShareScopeMap( + unexpectedProviderSharedScope, + createUnexpectedProviderShareScope(), + ); + + const result = await loadShare( + unexpectedProviderSharedName, + { + customShareInfo: { + scope: [unexpectedProviderSharedScope], + shareConfig: { + requiredVersion: '^2.0.0', + singleton: false, + eager: false, + strictVersion: false, + }, + }, + }, + ); + + if (result === false) { + throw new Error( + 'Shared provider choice: observability-provider-choice was not resolved', + ); + } + + const sharedValue = result(); + + if (!sharedValue) { + throw new Error( + 'Shared provider choice: observability-provider-choice returned empty', + ); + } + + setStatus('success'); + } catch (error) { + const message = sanitizeErrorMessage(error); + setStatus('error'); + setErrorMessage(message); + } finally { + refreshReport(); + } + }, [refreshReport]); + + const runMultiConsumerScenario = useCallback( + async ( + runtimeInstance: ReturnType, + scenario: MultiConsumerScenario, + ): Promise => { + const sharedFactory = + await runtimeInstance.loadShare( + scenario.sharedName, + { + customShareInfo: { + scope: [scenario.sharedScope], + shareConfig: { + requiredVersion: scenario.requiredVersion, + singleton: false, + eager: false, + strictVersion: false, + }, + }, + }, + ); + + if (sharedFactory === false) { + throw new Error( + `${scenario.consumer} could not resolve ${scenario.sharedName}`, + ); + } + + const sharedValue = sharedFactory(); + const sharedReport = observability.getLatestReport(); + const sharedTraceId = sharedReport?.traceId || ''; + + if (!sharedValue) { + throw new Error( + `${scenario.consumer} resolved an empty ${scenario.sharedName}`, + ); + } + + const remoteModule = await runtimeInstance.loadRemote(scenario.request); + const component = resolveRemoteComponent(remoteModule); + + if (!component) { + throw new Error(`Remote module ${scenario.request} has no component`); + } + + const remoteReportBeforeComponent = observability.getLatestReport(); + const remoteTraceId = remoteReportBeforeComponent?.traceId || ''; + + observability.markComponentLoaded({ + traceId: remoteTraceId, + requestId: scenario.request, + componentName: scenario.componentName, + metadata: { + consumer: scenario.consumer, + producer: multiProducerRemoteName, + expose: scenario.expose, + sharedName: scenario.sharedName, + sharedProvider: sharedValue.provider, + sharedVersion: sharedValue.version, + sharedTraceId, + }, + }); + + const remoteReport = observability.getReport(remoteTraceId); + + return { + consumer: scenario.consumer, + request: scenario.request, + expose: scenario.expose, + sharedName: scenario.sharedName, + sharedProvider: sharedValue.provider, + sharedVersion: sharedValue.version, + sharedTraceId, + remoteTraceId, + remoteEntryCached: + remoteReport?.summary.phases.remoteEntry?.cached === true, + manifestCached: remoteReport?.summary.phases.manifest?.cached === true, + summaryCached: remoteReport?.summary.flags.cached === true, + }; + }, + [], + ); + + const loadMultiConsumerChain = useCallback(async () => { + setStatus('loading'); + setErrorMessage(''); + setRemoteComponent(null); + setMultiConsumerResults([]); + observability.clear(); + + try { + const checkoutConsumer = createInstance({ + name: 'observability_consumer_checkout', + version: '1.0.0', + plugins: [observability.plugin], + remotes: [ + { + name: multiProducerRemoteName, + alias: multiProducerAlias, + entry: multiProducerManifestEntry, + }, + ], + shared: { + 'observability-checkout-theme': { + version: '1.4.0', + scope: ['observability-checkout-scope'], + lib: () => ({ + provider: 'observability_consumer_checkout', + version: '1.4.0', + }), + shareConfig: { + requiredVersion: '^1.0.0', + singleton: false, + eager: false, + strictVersion: false, + }, + }, + }, + }); + const analyticsConsumer = createInstance({ + name: 'observability_consumer_analytics', + version: '1.0.0', + plugins: [observability.plugin], + remotes: [ + { + name: multiProducerRemoteName, + alias: multiProducerAlias, + entry: multiProducerManifestEntry, + }, + ], + shared: { + 'observability-analytics-sdk': { + version: '1.2.0', + scope: ['observability-analytics-scope'], + lib: () => ({ + provider: 'observability_consumer_analytics', + version: '1.2.0', + }), + shareConfig: { + requiredVersion: '^1.0.0', + singleton: false, + eager: false, + strictVersion: false, + }, + }, + }, + }); + const consumerInstances: Record< + string, + ReturnType + > = { + 'checkout-page': checkoutConsumer, + 'analytics-page': analyticsConsumer, + 'checkout-page-repeat': checkoutConsumer, + }; + + const results: MultiConsumerResult[] = []; + + for (const scenario of multiConsumerScenarios) { + const result = await runMultiConsumerScenario( + consumerInstances[scenario.consumer], + scenario, + ); + + if (result.sharedProvider !== scenario.expectedProvider) { + throw new Error( + `${scenario.consumer} expected ${scenario.expectedProvider} but used ${result.sharedProvider}`, + ); + } + + results.push(result); + } + + setMultiConsumerResults(results); + setStatus('success'); + setReportText( + JSON.stringify( + { + scenario: 'multi-consumer-loading-chain', + results, + reports: observability.getReports({ limit: 12 }), + }, + null, + 2, + ), + ); + } catch (error) { + const message = sanitizeErrorMessage(error); + setStatus('error'); + setErrorMessage(message); + refreshReport(); + } + }, [refreshReport, runMultiConsumerScenario]); + + const loadEagerConfigError = useCallback(() => { + setStatus('loading'); + setErrorMessage(''); + setRemoteComponent(null); + + try { + loadShareSync('observability-async-shared', { + from: 'build', + customShareInfo: { + version: '1.0.0', + scope: ['default'], + shareConfig: { + requiredVersion: '^1.0.0', + singleton: false, + eager: false, + strictVersion: false, + }, + get: () => + Promise.resolve(() => ({ + value: 'async shared should not be consumed synchronously', + })), + }, + }); + + throw new Error('Eager config scenario unexpectedly loaded'); + } catch (error) { + const message = sanitizeErrorMessage(error); + setStatus('error'); + setErrorMessage(message); + } finally { + refreshReport(); + } + }, [refreshReport]); + + const loadRuntimeEagerConfigError = useCallback(() => { + setStatus('loading'); + setErrorMessage(''); + setRemoteComponent(null); + + try { + loadShareSync('observability-runtime-async-shared', { + from: 'runtime', + customShareInfo: { + version: '1.0.0', + scope: ['default'], + shareConfig: { + requiredVersion: '^1.0.0', + singleton: false, + eager: false, + strictVersion: false, + }, + get: () => + Promise.resolve(() => ({ + value: + 'runtime async shared should not be consumed synchronously', + })), + }, + }); + + throw new Error('Runtime eager config scenario unexpectedly loaded'); + } catch (error) { + const message = sanitizeErrorMessage(error); + setStatus('error'); + setErrorMessage(message); + } finally { + refreshReport(); + } + }, [refreshReport]); + const markBusinessLoaded = useCallback(() => { + observability.markComponentLoaded({ + requestId: successRequest, + componentName: 'ButtonOldAnt', + metadata: { + route: '/observability?token=demo-secret#hash', + rendered: true, + }, + }); + refreshReport(); + }, [refreshReport]); + + const LoadedRemote = remoteComponent; + + return ( +
+

Observability Demo

+ +
+

Load Remote

+ + + + + + + + + + + + +
+ +
+

Shared / Eager Scenarios

+ + + + + +
+ +
+

Multi Consumer Loading Chain

+ + {multiConsumerResults.length ? ( +
    + {multiConsumerResults.map((result) => ( +
  • + {result.consumer} loaded {result.request} with{' '} + {result.sharedName} from {result.sharedProvider}@ + {result.sharedVersion}; remoteTrace={result.remoteTraceId}; + sharedTrace={result.sharedTraceId}; cached= + {String(result.summaryCached)} +
  • + ))} +
+ ) : null} +
+ +
+

Status

+

{status}

+ {errorMessage ? ( +
{errorMessage}
+ ) : null} + {LoadedRemote ? ( +
+ +
+ ) : null} +
+ +
+

Report Fixture

+
{reportText}
+
+
+ ); +} diff --git a/apps/runtime-demo/3005-runtime-host/src/ObservabilityShowcase.tsx b/apps/runtime-demo/3005-runtime-host/src/ObservabilityShowcase.tsx new file mode 100644 index 00000000000..0c9380d94d7 --- /dev/null +++ b/apps/runtime-demo/3005-runtime-host/src/ObservabilityShowcase.tsx @@ -0,0 +1,658 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { createInstance } from '@module-federation/runtime'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { observability } from './observability'; +import './observability-showcase.css'; + +type ShowcaseStatus = 'loading' | 'success' | 'degraded' | 'error'; +type ShowcaseRoute = 'profile' | 'analytics'; +type RemoteComponent = React.ComponentType>; +type CustomerSdk = { + provider: string; + version: string; + feature: string; +}; +type LoadResult = { + Component: RemoteComponent; + traceId: string; + shared: string[]; + status?: ShowcaseStatus; + message?: string; +}; +type HandoffStatus = 'idle' | 'loading' | 'success' | 'error'; +type HandoffResult = { + owner: string; + step: string; + request: string; + expose: string; + shared: string; + traceId: string; +}; +type HandoffScenario = { + owner: string; + consumerName: string; + request: string; + expose: string; + componentName: string; + sharedName: string; + sharedScope: string; + sharedVersion: string; + requiredVersion: string; +}; + +const producerName = 'runtime_remote2'; +const producerAlias = 'dynamic-remote'; +const producerManifest = 'http://127.0.0.1:3007/mf-manifest.json'; +const profileRequest = `${producerAlias}/ProfileCard`; +const analyticsRequest = `${producerAlias}/AnalyticsPanel`; +const analyticsConsumerName = 'observability_showcase_analytics_consumer'; +const renewalHandoffScenarios: HandoffScenario[] = [ + { + owner: 'Account desk', + consumerName: 'observability_showcase_account_desk', + request: profileRequest, + expose: 'ProfileCard', + componentName: 'ProfileCard', + sharedName: 'renewal-account-context', + sharedScope: 'renewal-account-scope', + sharedVersion: '1.3.0', + requiredVersion: '^1.0.0', + }, + { + owner: 'Expansion desk', + consumerName: 'observability_showcase_expansion_desk', + request: analyticsRequest, + expose: 'AnalyticsPanel', + componentName: 'AnalyticsPanel', + sharedName: 'renewal-insight-context', + sharedScope: 'renewal-insight-scope', + sharedVersion: '2.2.0', + requiredVersion: '^2.0.0', + }, + { + owner: 'Success desk', + consumerName: 'observability_showcase_success_desk', + request: profileRequest, + expose: 'ProfileCard', + componentName: 'ProfileCard', + sharedName: 'renewal-account-context', + sharedScope: 'renewal-account-scope', + sharedVersion: '1.3.0', + requiredVersion: '^1.0.0', + }, +]; + +function getRoute(pathname: string): ShowcaseRoute { + return pathname.endsWith('/analytics') ? 'analytics' : 'profile'; +} + +function resolveRemoteComponent(remoteModule: unknown): RemoteComponent { + if (typeof remoteModule === 'function') { + return remoteModule as RemoteComponent; + } + + if (remoteModule && typeof remoteModule === 'object') { + const candidate = (remoteModule as { default?: unknown }).default; + + if (typeof candidate === 'function') { + return candidate as RemoteComponent; + } + } + + throw new Error('Remote module did not return a React component'); +} + +function createSharedReact() { + return { + version: '18.3.1', + scope: ['default'], + lib: () => React, + shareConfig: { + requiredVersion: '^18.0.0', + singleton: true, + eager: false, + strictVersion: false, + }, + }; +} + +function createConsumer( + name: string, + shared?: Parameters[0]['shared'], +) { + return createInstance({ + name, + version: '1.0.0', + plugins: [observability.plugin], + remotes: [ + { + name: producerName, + alias: producerAlias, + entry: producerManifest, + }, + ], + shared: { + react: createSharedReact(), + ...(shared || {}), + }, + }); +} + +async function loadProfileWidget() { + const consumer = createConsumer('observability_showcase_profile_consumer'); + const remoteModule = await consumer.loadRemote(profileRequest); + const Component = resolveRemoteComponent(remoteModule); + const report = observability.getLatestReport(); + + return { + Component, + traceId: report?.traceId || '', + shared: [] as string[], + } satisfies LoadResult; +} + +async function loadAnalyticsWorkspace() { + const consumer = createConsumer(analyticsConsumerName, { + 'observability-customer-sdk': { + version: '2.1.0', + scope: ['default'], + lib: () => ({ + provider: analyticsConsumerName, + version: '2.1.0', + feature: 'customer-insights', + }), + shareConfig: { + requiredVersion: '^2.0.0', + singleton: false, + eager: false, + strictVersion: false, + }, + }, + }); + const remoteModule = await consumer.loadRemote(analyticsRequest); + resolveRemoteComponent(remoteModule); + + const reactFactory = await consumer.loadShare('react', { + customShareInfo: { + version: '18.3.1', + scope: ['default'], + shareConfig: { + requiredVersion: '^18.0.0', + singleton: true, + eager: false, + strictVersion: false, + }, + }, + }); + const sdkFactory = await consumer.loadShare( + 'observability-customer-sdk', + { + customShareInfo: { + version: '2.1.0', + scope: ['default'], + shareConfig: { + requiredVersion: '^3.0.0', + singleton: false, + eager: false, + strictVersion: false, + }, + }, + }, + ); + + if (reactFactory === false) { + throw new Error('React shared dependency was not resolved'); + } + + if (sdkFactory === false) { + throw new Error('Customer SDK shared dependency was not resolved'); + } + + const sdk = sdkFactory(); + const Component = resolveRemoteComponent(remoteModule); + const report = observability.getLatestReport(); + + return { + Component, + traceId: report?.traceId || '', + shared: [ + `react from ${analyticsConsumerName}@18.3.1`, + `observability-customer-sdk from ${sdk.provider}@${sdk.version}`, + ], + } satisfies LoadResult; +} + +async function loadRenewalHandoffChain() { + const results: HandoffResult[] = []; + + for (const scenario of renewalHandoffScenarios) { + const consumer = createConsumer(scenario.consumerName, { + [scenario.sharedName]: { + version: scenario.sharedVersion, + scope: [scenario.sharedScope], + lib: () => ({ + provider: scenario.consumerName, + version: scenario.sharedVersion, + owner: scenario.owner, + }), + shareConfig: { + requiredVersion: scenario.requiredVersion, + singleton: false, + eager: false, + strictVersion: false, + }, + }, + }); + const sharedFactory = await consumer.loadShare<{ + provider: string; + version: string; + owner: string; + }>(scenario.sharedName, { + customShareInfo: { + version: scenario.sharedVersion, + scope: [scenario.sharedScope], + shareConfig: { + requiredVersion: scenario.requiredVersion, + singleton: false, + eager: false, + strictVersion: false, + }, + }, + }); + + if (sharedFactory === false) { + throw new Error(`${scenario.sharedName} was not resolved`); + } + + const sharedValue = sharedFactory(); + const sharedReport = observability.getLatestReport(); + const remoteModule = await consumer.loadRemote(scenario.request); + resolveRemoteComponent(remoteModule); + const remoteReport = observability.getLatestReport(); + const traceId = remoteReport?.traceId || ''; + + observability.markComponentLoaded({ + traceId, + requestId: scenario.request, + componentName: scenario.componentName, + metadata: { + scenario: 'renewal-handoff-chain', + consumer: scenario.consumerName, + owner: scenario.owner, + producer: producerName, + expose: `./${scenario.expose}`, + sharedName: scenario.sharedName, + sharedTraceId: sharedReport?.traceId || '', + }, + }); + + results.push({ + owner: scenario.owner, + step: + scenario.expose === 'AnalyticsPanel' + ? 'Expansion risk review' + : 'Account owner review', + request: scenario.request, + expose: scenario.expose, + shared: `${scenario.sharedName} from ${sharedValue.provider}@${sharedValue.version}`, + traceId, + }); + } + + return results; +} + +function AnalyticsFallback() { + return ( +
+

Limited analytics view

+

+ Key account metrics are still available while detailed insights are + temporarily limited. +

+
+ summary available + details limited + support reference ready +
+
+ ); +} + +function getLatestTraceId(): string { + return observability.getLatestReport()?.traceId ?? 'pending'; +} + +export default function ObservabilityShowcase() { + const location = useLocation(); + const navigate = useNavigate(); + const route = useMemo(() => getRoute(location.pathname), [location.pathname]); + const [status, setStatus] = useState('loading'); + const [referenceId, setReferenceId] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); + const [remoteComponent, setRemoteComponent] = + useState(null); + const [sharedEvidence, setSharedEvidence] = useState([]); + const [handoffStatus, setHandoffStatus] = useState('idle'); + const [handoffResults, setHandoffResults] = useState([]); + const [handoffError, setHandoffError] = useState(''); + + useEffect(() => { + let disposed = false; + + setStatus('loading'); + setReferenceId(''); + setErrorMessage(''); + setRemoteComponent(null); + setSharedEvidence([]); + + const load = + route === 'analytics' ? loadAnalyticsWorkspace : loadProfileWidget; + + load() + .then((result) => { + if (disposed) { + return; + } + + setRemoteComponent(() => result.Component); + setSharedEvidence(result.shared); + setReferenceId(result.traceId || getLatestTraceId()); + setErrorMessage(result.message || ''); + setStatus(result.status || 'success'); + }) + .catch((error) => { + if (disposed) { + return; + } + + const message = error instanceof Error ? error.message : String(error); + + if (route === 'analytics') { + setRemoteComponent(() => AnalyticsFallback); + setSharedEvidence([ + 'Core account page is available', + 'Detailed analytics are temporarily limited', + ]); + setErrorMessage( + 'Some analytics details are temporarily unavailable.', + ); + setReferenceId(getLatestTraceId()); + setStatus('degraded'); + return; + } + + setErrorMessage(message); + setReferenceId(getLatestTraceId()); + setStatus('error'); + }); + + return () => { + disposed = true; + }; + }, [route]); + + const openAnalyticsWorkspace = useCallback(() => { + navigate('/observability-showcase/analytics'); + }, [navigate]); + const runRenewalHandoff = useCallback(async () => { + setHandoffStatus('loading'); + setHandoffError(''); + setHandoffResults([]); + + try { + const results = await loadRenewalHandoffChain(); + setHandoffResults(results); + setHandoffStatus('success'); + } catch (error) { + setHandoffError(error instanceof Error ? error.message : String(error)); + setHandoffStatus('error'); + } + }, []); + + const isAnalytics = route === 'analytics'; + const RemoteComponent = remoteComponent; + + return ( +
+ + +
+
+
+

Enterprise account

+

Acme Retail Group

+
+ +
+ +
+
+ Contract value + $1.42M +
+
+ Open requests + 18 +
+
+ Health score + 82 +
+
+ +
+
+
+
+

+ {isAnalytics ? 'Route: insights' : 'Route: overview'} +

+

{isAnalytics ? 'Account analytics' : 'User profile'}

+
+ + {status} + +
+ +
+ {RemoteComponent ? ( + + ) : ( + <> +
AR
+
+ + {status === 'loading' + ? 'Loading remote component' + : 'Remote component unavailable'} + + + {isAnalytics + ? 'Loading the analytics expose from the producer.' + : 'Loading the profile expose from the producer.'} + +
+ + )} +
+ + {status === 'error' ? ( +
+ Remote widget is temporarily unavailable. + + {errorMessage || 'Share this reference with support:'} + + {referenceId} + + +
+ ) : status === 'degraded' ? ( +
+ Limited analytics view is active. + + {errorMessage} + + {referenceId} + + +
+ ) : ( +
+ {/* {isAnalytics + ? 'This view loads a second expose and resolves React plus the customer SDK as shared dependencies.' + : 'The owner profile is ready for the renewal workspace.'} + {referenceId ? ( + + {referenceId} + + ) : null} */} +
+ )} + + + + {sharedEvidence.length ? ( +
    + {sharedEvidence.map((item) => ( +
  • {item}
  • + ))} +
+ ) : null} +
+ +
+
+
+
+

Renewal handoff

+

Prepare account review

+
+ + {handoffStatus} + +
+

+ Pull the owner card, expansion insight, and success desk context + before the renewal meeting starts. +

+ + {handoffError ? ( +
+ {handoffError} +
+ ) : null} + {handoffResults.length ? ( +
    + {handoffResults.map((result) => ( +
  • +
    + {result.owner} + {result.step} +
    + {result.request} + {result.shared} +
  • + ))} +
+ ) : null} + + +
+ +
+

Recent activity

+
    +
  • + Profile expose loaded on overview route + +
  • +
  • + Analytics expose waits for route navigation + +
  • +
  • + Shared dependency evidence is kept in reports + +
  • +
+
+
+
+
+
+ ); +} diff --git a/apps/runtime-demo/3005-runtime-host/src/bootstrap.tsx b/apps/runtime-demo/3005-runtime-host/src/bootstrap.tsx index 3f87d871a13..4417a2f9cf6 100644 --- a/apps/runtime-demo/3005-runtime-host/src/bootstrap.tsx +++ b/apps/runtime-demo/3005-runtime-host/src/bootstrap.tsx @@ -1,8 +1,5 @@ import React, { StrictMode } from 'react'; -import { - init, - registerGlobalPlugins, -} from '@module-federation/enhanced/runtime'; +import { init } from '@module-federation/enhanced/runtime'; import * as ReactDOM from 'react-dom/client'; import App from './App'; diff --git a/apps/runtime-demo/3005-runtime-host/src/components/ButtonOldAnt.tsx b/apps/runtime-demo/3005-runtime-host/src/components/ButtonOldAnt.tsx index c1552882ff5..bad5b1a4610 100644 --- a/apps/runtime-demo/3005-runtime-host/src/components/ButtonOldAnt.tsx +++ b/apps/runtime-demo/3005-runtime-host/src/components/ButtonOldAnt.tsx @@ -1,9 +1,7 @@ import Button from 'antd/lib/button'; -import antdPackage from 'antd/package.json'; +import version from 'antd/lib/version'; import stuff from './stuff.module.css'; -const { version } = antdPackage; - export default function ButtonOldAnt() { return ; } diff --git a/apps/runtime-demo/3005-runtime-host/src/index.ts b/apps/runtime-demo/3005-runtime-host/src/index.ts index 51ffb285cfc..8cae17419e4 100644 --- a/apps/runtime-demo/3005-runtime-host/src/index.ts +++ b/apps/runtime-demo/3005-runtime-host/src/index.ts @@ -6,4 +6,4 @@ import customPlugin from './runtimePlugin'; registerGlobalPlugins([customPlugin()]); -require('./bootstrap'); +void import('./bootstrap'); diff --git a/apps/runtime-demo/3005-runtime-host/src/observability-runtime-plugin.ts b/apps/runtime-demo/3005-runtime-host/src/observability-runtime-plugin.ts new file mode 100644 index 00000000000..3e8b68a644e --- /dev/null +++ b/apps/runtime-demo/3005-runtime-host/src/observability-runtime-plugin.ts @@ -0,0 +1,5 @@ +import { observability } from './observability'; + +export default function observabilityRuntimePlugin() { + return observability.plugin; +} diff --git a/apps/runtime-demo/3005-runtime-host/src/observability-showcase.css b/apps/runtime-demo/3005-runtime-host/src/observability-showcase.css new file mode 100644 index 00000000000..05c0cf96f53 --- /dev/null +++ b/apps/runtime-demo/3005-runtime-host/src/observability-showcase.css @@ -0,0 +1,501 @@ +.customer-portal { + display: grid; + grid-template-columns: 248px minmax(0, 1fr); + min-height: 100vh; + color: #1f2933; + background: #f4f6f8; + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; +} + +.customer-portal *, +.customer-portal *::before, +.customer-portal *::after { + box-sizing: border-box; +} + +.customer-portal__sidebar { + padding: 28px 18px; + border-right: 1px solid #dde4eb; + background: #ffffff; +} + +.customer-portal__brand { + margin-bottom: 28px; + color: #111827; + font-size: 18px; + font-weight: 800; +} + +.customer-portal__nav { + display: grid; + gap: 6px; +} + +.customer-portal__nav-item { + padding: 10px 12px; + border-radius: 6px; + color: #536171; + font-size: 14px; + font-weight: 650; + text-decoration: none; +} + +.customer-portal__nav-item--active { + color: #0f4c81; + background: #e8f2fb; +} + +.customer-portal__content { + padding: 32px; +} + +.customer-portal__header { + display: flex; + gap: 18px; + justify-content: space-between; + align-items: flex-start; + margin: 0 0 24px; +} + +.customer-portal__eyebrow { + margin: 0 0 8px; + color: #667789; + font-size: 12px; + font-weight: 800; + letter-spacing: 0; + text-transform: uppercase; +} + +.customer-portal h1, +.customer-portal h2 { + margin: 0; + color: #111827; + letter-spacing: 0; +} + +.customer-portal h1 { + font-size: 34px; + line-height: 1.15; +} + +.customer-portal h2 { + font-size: 22px; + line-height: 1.25; +} + +.customer-portal__metrics { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 16px; + margin-bottom: 20px; +} + +.customer-portal__metrics div, +.customer-portal__card { + border: 1px solid #dde4eb; + border-radius: 8px; + background: #ffffff; + box-shadow: 0 10px 28px rgba(31, 41, 51, 0.06); +} + +.customer-portal__metrics div { + padding: 18px; +} + +.customer-portal__metrics span { + display: block; + margin-bottom: 8px; + color: #667789; + font-size: 13px; + font-weight: 650; +} + +.customer-portal__metrics strong { + color: #111827; + font-size: 26px; +} + +.customer-portal__workspace { + display: grid; + grid-template-columns: minmax(0, 1fr) 360px; + gap: 20px; +} + +.customer-portal__card { + padding: 22px; +} + +.customer-portal__card--profile { + min-height: 430px; +} + +.customer-portal__side-stack { + display: grid; + gap: 20px; + align-content: start; +} + +.customer-portal__card-header { + display: flex; + gap: 16px; + justify-content: space-between; + align-items: flex-start; +} + +.customer-portal__status { + min-width: 82px; + padding: 7px 11px; + border-radius: 999px; + font-size: 12px; + font-weight: 800; + text-align: center; + text-transform: uppercase; +} + +.customer-portal__status--ready { + color: #245275; + background: #e6f2fb; +} + +.customer-portal__status--idle { + color: #526274; + background: #eef2f6; +} + +.customer-portal__status--loading { + color: #7a4f00; + background: #fff3cf; +} + +.customer-portal__status--success { + color: #0f6b45; + background: #ddf8eb; +} + +.customer-portal__status--degraded { + color: #7a4f00; + background: #fff3cf; +} + +.customer-portal__status--error { + color: #9f1d20; + background: #ffe3e3; +} + +.customer-portal__profile-shell { + display: flex; + gap: 16px; + align-items: center; + min-height: 170px; + margin: 28px 0 18px; + padding: 24px; + border: 1px dashed #bac7d3; + border-radius: 8px; + background: #f8fafc; +} + +.customer-portal__avatar { + display: grid; + width: 58px; + height: 58px; + border-radius: 50%; + color: #ffffff; + background: #0f4c81; + font-size: 18px; + font-weight: 800; + place-items: center; + flex: 0 0 auto; +} + +.customer-portal__profile-shell strong { + display: block; + color: #111827; + font-size: 18px; + line-height: 1.35; +} + +.customer-portal__profile-shell span { + display: block; + margin-top: 6px; + color: #667789; + font-size: 14px; + line-height: 1.55; +} + +.customer-portal__remote-widget { + width: 100%; +} + +.customer-portal__remote-widget h3 { + margin: 0 0 8px; + color: #111827; + font-size: 20px; + line-height: 1.25; +} + +.customer-portal__remote-widget p { + margin: 0; + color: #667789; + font-size: 14px; + line-height: 1.55; +} + +.customer-portal__remote-meta { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 16px; +} + +.customer-portal__remote-meta span { + margin: 0; + padding: 6px 9px; + border-radius: 6px; + color: #245275; + background: #e6f2fb; + font-size: 12px; + font-weight: 750; +} + +.customer-portal__hint, +.customer-portal__fallback, +.customer-portal__error { + margin-bottom: 18px; + padding: 14px 16px; + border-radius: 8px; + font-size: 14px; + line-height: 1.6; +} + +.customer-portal__hint { + color: #425466; + background: #f1f5f9; +} + +.customer-portal__error { + color: #7a2222; + background: #fff0f0; +} + +.customer-portal__fallback { + color: #6f4b00; + background: #fff6d9; +} + +.customer-portal__error strong, +.customer-portal__fallback strong, +.customer-portal__fallback span, +.customer-portal__error span { + display: block; +} + +.customer-portal__hint code, +.customer-portal__fallback code, +.customer-portal__error code { + display: inline-block; + margin-left: 6px; + padding: 2px 6px; + border-radius: 4px; + color: #5d1f1f; + background: #ffe1e1; + font-family: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', + monospace; + font-size: 13px; + overflow-wrap: anywhere; +} + +.customer-portal__hint code { + color: #245275; + background: #dceefb; +} + +.customer-portal__fallback code { + color: #6f4b00; + background: #ffedaf; +} + +.customer-portal__shared-list { + display: grid; + gap: 8px; + margin: 16px 0 0; + padding: 0; + list-style: none; +} + +.customer-portal__shared-list li { + padding: 10px 12px; + border: 1px solid #d7e5ef; + border-radius: 6px; + color: #245275; + background: #f4f9fd; + font-size: 13px; + font-weight: 700; +} + +.customer-portal__handoff-copy { + margin: 16px 0; + color: #526274; + font-size: 14px; + line-height: 1.55; +} + +.customer-portal__handoff-button { + width: 100%; +} + +.customer-portal__handoff-error { + margin: 14px 0 0; +} + +.customer-portal__handoff-list { + display: grid; + gap: 10px; + margin: 16px 0 0; + padding: 0; + list-style: none; +} + +.customer-portal__handoff-list li { + display: grid; + gap: 6px; + padding: 12px; + border: 1px solid #dde4eb; + border-radius: 8px; + background: #f8fafc; +} + +.customer-portal__handoff-list strong, +.customer-portal__handoff-list span, +.customer-portal__handoff-list code { + display: block; +} + +.customer-portal__handoff-list strong { + color: #111827; + font-size: 14px; +} + +.customer-portal__handoff-list span { + color: #667789; + font-size: 13px; + line-height: 1.45; +} + +.customer-portal__handoff-list code { + width: fit-content; + max-width: 100%; + padding: 3px 6px; + border-radius: 4px; + color: #245275; + background: #dceefb; + font-family: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', + monospace; + font-size: 12px; + overflow-wrap: anywhere; +} + +.customer-portal__primary-button, +.customer-portal__secondary-button { + min-height: 42px; + border-radius: 6px; + font-size: 14px; + font-weight: 800; + cursor: pointer; +} + +.customer-portal__primary-button { + padding: 0 18px; + border: 0; + color: #ffffff; + background: #0f4c81; +} + +.customer-portal__primary-button:hover { + background: #0b3e69; +} + +.customer-portal__primary-button:disabled { + cursor: wait; + background: #8aa8c1; +} + +.customer-portal__secondary-button { + padding: 0 16px; + border: 1px solid #c8d3de; + color: #334155; + background: #ffffff; +} + +.customer-portal__activity { + display: grid; + gap: 14px; + margin: 8px 0 0; + padding: 0; + list-style: none; +} + +.customer-portal__activity li { + display: flex; + gap: 12px; + justify-content: space-between; + padding-bottom: 14px; + border-bottom: 1px solid #eef2f6; + color: #273547; + font-size: 14px; + line-height: 1.5; +} + +.customer-portal__activity li:last-child { + padding-bottom: 0; + border-bottom: 0; +} + +.customer-portal__activity time { + color: #76879a; + white-space: nowrap; +} + +@media (max-width: 900px) { + .customer-portal { + grid-template-columns: 1fr; + } + + .customer-portal__sidebar { + border-right: 0; + border-bottom: 1px solid #dde4eb; + } + + .customer-portal__nav { + grid-template-columns: repeat(4, max-content); + overflow-x: auto; + } + + .customer-portal__metrics, + .customer-portal__workspace { + grid-template-columns: 1fr; + } +} + +@media (max-width: 560px) { + .customer-portal__content { + padding: 20px; + } + + .customer-portal__header, + .customer-portal__card-header { + display: grid; + } + + .customer-portal h1 { + font-size: 28px; + } +} diff --git a/apps/runtime-demo/3005-runtime-host/src/observability.ts b/apps/runtime-demo/3005-runtime-host/src/observability.ts new file mode 100644 index 00000000000..bfdf94aef2e --- /dev/null +++ b/apps/runtime-demo/3005-runtime-host/src/observability.ts @@ -0,0 +1,21 @@ +import { createObservability } from '@module-federation/observability-plugin'; + +export const observability = createObservability({ + level: 'verbose', + maxEvents: 100, + browser: { + enabled: true, + scope: 'runtime_host', + }, + collector: true, + react: { + injectLoadedCallback: true, + remoteIds: [ + 'dynamic-remote/ProfileCard', + 'dynamic-remote/AnalyticsPanel', + './ProfileCard', + './AnalyticsPanel', + ], + defaultExportMode: 'component', + }, +}); diff --git a/apps/runtime-demo/3005-runtime-host/tsconfig.app.json b/apps/runtime-demo/3005-runtime-host/tsconfig.app.json index 8864b3d724f..af024f1690b 100644 --- a/apps/runtime-demo/3005-runtime-host/tsconfig.app.json +++ b/apps/runtime-demo/3005-runtime-host/tsconfig.app.json @@ -2,16 +2,12 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../../dist/out-tsc", - "types": [ - "node", - "../../../typings/cssmodule.d.ts", - "../../../typings/image.d.ts" - ], + "types": ["node"], "paths": { "*": ["./@mf-types/*"] } }, - "files": ["../../../typings/cssmodule.d.ts", "../../../typings/image.d.ts"], + "files": ["../typings/cssmodule.d.ts", "../typings/image.d.ts"], "exclude": [ "jest.config.ts", "**/*.spec.ts", diff --git a/apps/runtime-demo/3005-runtime-host/webpack.config.js b/apps/runtime-demo/3005-runtime-host/webpack.config.js index dce5c186906..18d57b4adb8 100644 --- a/apps/runtime-demo/3005-runtime-host/webpack.config.js +++ b/apps/runtime-demo/3005-runtime-host/webpack.config.js @@ -1,3 +1,5 @@ +/* eslint-env node */ + const path = require('path'); const reactPath = path.dirname(require.resolve('react/package.json')); const reactDomPath = path.dirname(require.resolve('react-dom/package.json')); @@ -7,11 +9,58 @@ const cssLoader = require.resolve('css-loader'); const { ModuleFederationPlugin, } = require('@module-federation/enhanced/webpack'); +const { + ObservabilityBuildPlugin, +} = require('@module-federation/observability-plugin/build'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = (_env, argv = {}) => { const isProduction = argv.mode === 'production'; const sourcePath = path.resolve(__dirname, 'src'); + const moduleFederationOptions = { + name: 'runtime_host', + experiments: { asyncStartup: true }, + runtimePlugins: [ + path.resolve(__dirname, 'src/observability-runtime-plugin.ts'), + ], + remotes: { + remote1: 'runtime_remote1@http://127.0.0.1:3006/mf-manifest.json', + }, + filename: 'remoteEntry.js', + exposes: { + './Button': './src/Button.tsx', + }, + dts: { + tsConfigPath: path.resolve(__dirname, 'tsconfig.app.json'), + }, + shareStrategy: 'loaded-first', + shared: { + lodash: { + singleton: true, + requiredVersion: '^4.0.0', + }, + antd: { + singleton: true, + requiredVersion: '^4.0.0', + }, + react: { + singleton: true, + requiredVersion: '^18.2.0', + }, + 'react/': { + singleton: true, + requiredVersion: '^18.2.0', + }, + 'react-dom': { + singleton: true, + requiredVersion: '^18.2.0', + }, + 'react-dom/': { + singleton: true, + requiredVersion: '^18.2.0', + }, + }, + }; return { mode: isProduction ? 'production' : 'development', @@ -87,49 +136,13 @@ module.exports = (_env, argv = {}) => { ], }, plugins: [ - new ModuleFederationPlugin({ - name: 'runtime_host', - experiments: { asyncStartup: true }, - remotes: { - remote1: 'runtime_remote1@http://127.0.0.1:3006/mf-manifest.json', - }, - filename: 'remoteEntry.js', - exposes: { - './Button': './src/Button.tsx', - }, - dts: { - tsConfigPath: path.resolve(__dirname, 'tsconfig.app.json'), - }, - shareStrategy: 'loaded-first', - shared: { - lodash: { - singleton: true, - requiredVersion: '^4.0.0', - }, - antd: { - singleton: true, - requiredVersion: '^4.0.0', - }, - react: { - singleton: true, - requiredVersion: '^18.2.0', - }, - 'react/': { - singleton: true, - requiredVersion: '^18.2.0', - }, - 'react-dom': { - singleton: true, - requiredVersion: '^18.2.0', - }, - 'react-dom/': { - singleton: true, - requiredVersion: '^18.2.0', - }, - }, + new ModuleFederationPlugin(moduleFederationOptions), + new ObservabilityBuildPlugin({ + moduleFederation: moduleFederationOptions, }), new HtmlWebpackPlugin({ template: path.resolve(__dirname, 'src/index.html'), + chunks: ['main'], }), ], watchOptions: { @@ -140,10 +153,176 @@ module.exports = (_env, argv = {}) => { historyApiFallback: true, client: { overlay: false, + reconnect: false, }, + hot: false, + liveReload: false, devMiddleware: { writeToDisk: true, }, + setupMiddlewares: (middlewares, devServer) => { + if (!devServer.app) { + return middlewares; + } + + const sendJson = (response, body) => { + response.setHeader('Content-Type', 'application/json'); + response.end(JSON.stringify(body)); + }; + const sendJs = (response, body) => { + response.setHeader('Content-Type', 'application/javascript'); + response.end(body); + }; + const createManifest = ({ name, globalName, publicPath }) => ({ + id: name, + name, + metaData: { + name, + type: 'app', + buildInfo: { + buildVersion: 'observability-fixture', + buildName: name, + }, + remoteEntry: { + name: 'remoteEntry.js', + path: '', + type: 'global', + }, + types: { + path: '', + name: '', + zip: '', + api: '', + }, + globalName, + pluginVersion: 'observability-fixture', + publicPath, + }, + shared: [], + remotes: [], + exposes: [ + { + id: `${name}:Button`, + name: 'Button', + assets: { + js: { + sync: [], + async: [], + }, + css: { + sync: [], + async: [], + }, + }, + path: './Button', + }, + ], + }); + + devServer.app.get( + '/observability-fixtures/missing-fields/mf-manifest.json', + (_request, response) => { + sendJson(response, { + id: 'observability_missing_fields_remote', + name: 'observability_missing_fields_remote', + }); + }, + ); + devServer.app.get( + '/observability-fixtures/retry-recovered/mf-manifest.json', + (_request, response) => { + sendJson( + response, + createManifest({ + name: 'observability_retry_recovered_remote', + globalName: 'observability_retry_recovered_remote', + publicPath: + 'http://127.0.0.1:3005/observability-fixtures/retry-recovered/', + }), + ); + }, + ); + devServer.app.get( + '/observability-fixtures/retry-recovered/remoteEntry.js', + (request, response) => { + if (request.query.retryCount !== '1') { + response.statusCode = 503; + response.end('observability retry fixture failed before retry'); + return; + } + + sendJs( + response, + [ + 'window.observability_retry_recovered_remote = {', + ' init: function() {},', + ' get: function() {', + ' return Promise.resolve(function() {', + ' return { default: function ObservabilityRetryRecovered() { return null; } };', + ' });', + ' }', + '};', + ].join('\n'), + ); + }, + ); + devServer.app.get( + '/observability-fixtures/wrong-global/mf-manifest.json', + (_request, response) => { + sendJson( + response, + createManifest({ + name: 'observability_wrong_global_remote', + globalName: 'observability_wrong_global_expected', + publicPath: + 'http://127.0.0.1:3005/observability-fixtures/wrong-global/', + }), + ); + }, + ); + devServer.app.get( + '/observability-fixtures/wrong-global/remoteEntry.js', + (_request, response) => { + sendJs( + response, + [ + 'window.observability_wrong_global_actual = {', + ' init: function() {},', + ' get: function() {', + ' return Promise.resolve(function() {', + ' return { default: function ObservabilityWrongGlobal() { return null; } };', + ' });', + ' }', + '};', + ].join('\n'), + ); + }, + ); + devServer.app.get( + '/observability-fixtures/execution-error/mf-manifest.json', + (_request, response) => { + sendJson( + response, + createManifest({ + name: 'observability_execution_error_remote', + globalName: 'observability_execution_error_remote', + publicPath: + 'http://127.0.0.1:3005/observability-fixtures/execution-error/', + }), + ); + }, + ); + devServer.app.get( + '/observability-fixtures/execution-error/remoteEntry.js', + (_request, response) => { + sendJs( + response, + "throw new Error('observability remoteEntry execution failed');", + ); + }, + ); + return middlewares; + }, }, optimization: { runtimeChunk: false, diff --git a/apps/runtime-demo/3006-runtime-remote/tsconfig.app.json b/apps/runtime-demo/3006-runtime-remote/tsconfig.app.json index 9f647569f50..196facc7dcf 100644 --- a/apps/runtime-demo/3006-runtime-remote/tsconfig.app.json +++ b/apps/runtime-demo/3006-runtime-remote/tsconfig.app.json @@ -3,12 +3,8 @@ "compilerOptions": { "composite": true, "declaration": true, - "types": [ - "node", - "../../../typings/cssmodule.d.ts", - "../../../typings/image.d.ts" - ] + "types": ["node"] }, - "files": ["../../../typings/cssmodule.d.ts", "../../../typings/image.d.ts"], + "files": ["../typings/cssmodule.d.ts", "../typings/image.d.ts"], "include": ["src/**/*"] } diff --git a/apps/runtime-demo/3006-runtime-remote/webpack.config.js b/apps/runtime-demo/3006-runtime-remote/webpack.config.js index 31248fd4567..b8d5303be1e 100644 --- a/apps/runtime-demo/3006-runtime-remote/webpack.config.js +++ b/apps/runtime-demo/3006-runtime-remote/webpack.config.js @@ -1,3 +1,5 @@ +/* eslint-env node */ + const path = require('path'); const reactPath = path.dirname(require.resolve('react/package.json')); const reactDomPath = path.dirname(require.resolve('react-dom/package.json')); @@ -141,6 +143,8 @@ module.exports = (_env, argv = {}) => { host: '127.0.0.1', port: 3006, allowedHosts: 'all', + hot: false, + liveReload: false, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': @@ -150,6 +154,7 @@ module.exports = (_env, argv = {}) => { }, client: { overlay: false, + reconnect: false, }, devMiddleware: { writeToDisk: true, diff --git a/apps/runtime-demo/3007-runtime-remote/src/components/AnalyticsPanel.tsx b/apps/runtime-demo/3007-runtime-remote/src/components/AnalyticsPanel.tsx new file mode 100644 index 00000000000..bfe76fab0e2 --- /dev/null +++ b/apps/runtime-demo/3007-runtime-remote/src/components/AnalyticsPanel.tsx @@ -0,0 +1,40 @@ +import { useEffect } from 'react'; + +type OnMFRemoteLoaded = (options?: { + metadata?: Record; +}) => void; + +export default function AnalyticsPanel({ + onMFRemoteLoaded, +}: { + onMFRemoteLoaded?: OnMFRemoteLoaded; +}) { + useEffect(() => { + onMFRemoteLoaded?.({ + metadata: { + producer: 'runtime_remote2', + expose: './AnalyticsPanel', + readySource: 'producer', + }, + }); + }, [onMFRemoteLoaded]); + + return ( +
+

Expansion analytics

+

+ Analytics panel loaded from runtime_remote2/AnalyticsPanel after route + navigation. +

+
+ producer: runtime_remote2 + expose: AnalyticsPanel + shared: react + shared: observability-customer-sdk +
+
+ ); +} diff --git a/apps/runtime-demo/3007-runtime-remote/src/components/ProfileCard.tsx b/apps/runtime-demo/3007-runtime-remote/src/components/ProfileCard.tsx new file mode 100644 index 00000000000..a282d84eb42 --- /dev/null +++ b/apps/runtime-demo/3007-runtime-remote/src/components/ProfileCard.tsx @@ -0,0 +1,37 @@ +import { useEffect } from 'react'; + +type OnMFRemoteLoaded = (options?: { + metadata?: Record; +}) => void; + +export default function ProfileCard({ + onMFRemoteLoaded, +}: { + onMFRemoteLoaded?: OnMFRemoteLoaded; +}) { + useEffect(() => { + onMFRemoteLoaded?.({ + metadata: { + producer: 'runtime_remote2', + expose: './ProfileCard', + }, + }); + }, [onMFRemoteLoaded]); + + return ( +
+

Jordan Lee

+

+ Account owner profile loaded from runtime_remote2/ProfileCard when the + page opened. +

+
+ producer: runtime_remote2 + expose: ProfileCard +
+
+ ); +} diff --git a/apps/runtime-demo/3007-runtime-remote/tsconfig.app.json b/apps/runtime-demo/3007-runtime-remote/tsconfig.app.json index e02bdff676a..6d7c66b0018 100644 --- a/apps/runtime-demo/3007-runtime-remote/tsconfig.app.json +++ b/apps/runtime-demo/3007-runtime-remote/tsconfig.app.json @@ -2,13 +2,9 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../../dist/out-tsc", - "types": [ - "node", - "../../../typings/cssmodule.d.ts", - "../../../typings/image.d.ts" - ] + "types": ["node"] }, - "files": ["../../../typings/cssmodule.d.ts", "../../../typings/image.d.ts"], + "files": ["../typings/cssmodule.d.ts", "../typings/image.d.ts"], "exclude": [ "jest.config.ts", "**/*.spec.ts", diff --git a/apps/runtime-demo/3007-runtime-remote/webpack.config.js b/apps/runtime-demo/3007-runtime-remote/webpack.config.js index c7aa170ed70..8d1e44c7b03 100644 --- a/apps/runtime-demo/3007-runtime-remote/webpack.config.js +++ b/apps/runtime-demo/3007-runtime-remote/webpack.config.js @@ -1,3 +1,5 @@ +/* eslint-env node */ + const path = require('path'); const reactPath = path.dirname(require.resolve('react/package.json')); const reactDomPath = path.dirname(require.resolve('react-dom/package.json')); @@ -92,6 +94,8 @@ module.exports = (_env, argv = {}) => { filename: 'remoteEntry.js', exposes: { './ButtonOldAnt': './src/components/ButtonOldAnt', + './ProfileCard': './src/components/ProfileCard', + './AnalyticsPanel': './src/components/AnalyticsPanel', }, shared: { lodash: { @@ -134,6 +138,8 @@ module.exports = (_env, argv = {}) => { devServer: { host: '127.0.0.1', allowedHosts: 'all', + hot: false, + liveReload: false, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': @@ -141,9 +147,7 @@ module.exports = (_env, argv = {}) => { 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization', }, - client: { - overlay: false, - }, + client: false, devMiddleware: { writeToDisk: true, }, diff --git a/apps/runtime-demo/3008-runtime-remote/rsbuild.config.mjs b/apps/runtime-demo/3008-runtime-remote/rsbuild.config.mjs index 912f8ff3e8d..fcadd7a57dd 100644 --- a/apps/runtime-demo/3008-runtime-remote/rsbuild.config.mjs +++ b/apps/runtime-demo/3008-runtime-remote/rsbuild.config.mjs @@ -9,6 +9,8 @@ export default defineConfig({ dev: { // It is necessary to configure assetPrefix, and in the production environment, you need to configure output.assetPrefix assetPrefix: 'http://localhost:3008', + hmr: false, + liveReload: false, }, tools: { rspack: (config, { appendPlugins }) => { diff --git a/apps/runtime-demo/README.md b/apps/runtime-demo/README.md index 81cca8c5617..1070027d4b0 100644 --- a/apps/runtime-demo/README.md +++ b/apps/runtime-demo/README.md @@ -10,8 +10,139 @@ host declare remote2 in webpack.config.js, and use `@module-federation/runtime` # Running Demo -Run `npm run app:runtime:dev` to start host, remote1, remote2 +Run `pnpm run app:runtime:dev` to start host, remote1, remote2. - host: [localhost:3005](http://localhost:3005/) - remote1: [localhost:3006](http://localhost:3006/) - remote2: [localhost:3007](http://localhost:3007/) + +## Observability Demo + +The observability fixture lives in the runtime host. The host explicitly enables +`@module-federation/observability-plugin` for this demo, so the report is +generated by the runtime plugin instead of local UI state. +The host also installs `ObservabilityBuildPlugin`, so each build writes a +build summary to `.mf/observability/build-info.json`. If a host build fails +after the plugin runs, the build-side report is written to +`.mf/observability/build-report.json`. + +- observability fixture page: + [localhost:3005/observability](http://localhost:3005/observability) +- observability showcase: + [localhost:3005/observability-showcase](http://localhost:3005/observability-showcase) + +Start the demo first: + +```bash +pnpm run app:runtime:dev +``` + +Then open the observability fixture page and use these controls: + +- `observability-showcase`: a clean recording page that looks like a normal + product page. Opening the page loads `runtime_remote2/ProfileCard` with + `createInstance`. Click `Open Analytics Workspace` to route to the analytics + view, which loads `runtime_remote2/AnalyticsPanel` with another + `createInstance`, resolves `react`, and then fails to resolve + `observability-customer-sdk` because the route asks for `^3.0.0` while only + `2.1.0` is available. The page renders a limited analytics view instead of + crashing, without exposing the low-level shared error in the UI. Use this page + to record an AI agent discovering which expose was attempted, which shared + dependency failed, why the limited view appeared, and what evidence exists in + the observability report. Use `/observability` for the full fixture matrix. + +- Build observability: after the host compiles, check + `apps/runtime-demo/3005-runtime-host/.mf/observability/build-info.json`. It + should include `runtime_host`, the remote manifest URL, `Button`, and shared + dependencies such as `react`, without local source paths. + A clean build should not leave a stale + `apps/runtime-demo/3005-runtime-host/.mf/observability/build-report.json`. + +- `Load success remote`: loads `dynamic-remote/ButtonOldAnt` from remote2. The + status should become `success`, the remote button should render, and the + report should include a completed `loadRemote` timeline with + `summary.outcome: "runtime-loaded"` and per-phase success facts under + `summary.phases`. The report should also include + `diagnosis.outcome: "runtime-loaded"`. + The same report is available at + `window.__FEDERATION__.__OBSERVABILITY__.runtime_host.getLatestReport()`. +- `Preload remote`: calls `preloadRemote` for the real `dynamic-remote` + producer. The status should become `success`, and the latest report should + include `summary.outcome: "preloaded"` plus a `preload` phase. This verifies + that preloading is not treated as a failed or stuck component load. +- `Load missing expose`: loads a missing expose from remote2. The status should + become `error`, and the report should include + `dynamic-remote/__missing_expose__` with `failedPhase: "expose"`. The + `diagnosis.actions` list should include a concrete expose check. The browser + console should print an observability hint with a `traceId`. +- `Load broken manifest`: loads a remote with a broken manifest URL. The status + should become `error`. The report should include the original manifest URL, + `RUNTIME-003`, `ownerHint: "host"`, `diagnosis.facts.url`, and a manifest + check in `diagnosis.actions`. +- `Load remote URL error`: registers a remote whose manifest URL points to an + unavailable server. The report should fail at `manifest` and include + `RUNTIME-003`, the original URL, and a manifest check. +- `Load retry recovered`: serves a remoteEntry that fails on the first script + load and succeeds on retry. The report should be successful with + `summary.outcome: "recovered"`, `summary.flags.retried: true`, and a retry + recovery warning. +- `Load fallback success`: loads a missing expose and lets the demo fallback + plugin return a fallback module. The report should be successful with + `summary.outcome: "recovered"`, keep the original expose failure under + `summary.error`, and set `summary.flags.fallback: true`. +- `Load manifest missing fields`: serves a JSON manifest that misses required + fields. The report should fail at `manifest`, show the missing fields, and + include a manifest check. +- `Load wrong globalName`: serves a manifest and remoteEntry where the + remoteEntry registers a different global name from the manifest. The report + should fail at `remoteEntry`, include `RUNTIME-001`, and include a remote + global check. +- `Load remoteEntry execution error`: serves a remoteEntry that throws while it + is being executed. The report should fail at `remoteEntry`, include + `RUNTIME-008`, classify the resource error as script execution, and include a + remoteEntry check. +- `Load snapshot match error`: registers a version-only remote that cannot be + matched from deployment `moduleInfo`. The report should include + `RUNTIME-007`, `moduleInfo.reason: "remote-snapshot"`, the clipped + `moduleInfo.availableNames`, and a moduleInfo check. +- `Mark business loaded`: calls the observability plugin's component success API, + and the report should include `component:business-loaded` with + `summary.outcome: "component-loaded"`. The demo also sends sample metadata + to verify that business-provided metadata stays visible in the report. +- `Shared miss`: triggers a missing shared provider report. The report should + include `observability-missing-shared`, `missing-provider`, and shared + provider/version checks in `diagnosis.actions`. +- `Shared version mismatch`: asks for an unsupported React version. The report + should include `react`, `^99.0.0`, the available version, and + `version-mismatch`. +- `Shared unexpected provider`: resolves a shared dependency from a remote-like + provider even though the host provider is present. The report should be + successful and include `observability-provider-choice`, + `provider: "runtime_remote2"`, and `selectedVersion: "2.0.0"`. +- `Load multi-consumer chain`: creates two runtime consumers with + `createInstance`, loads different exposes from the real `runtime_remote2` + remote on port 3007, and gives each consumer its own shared dependency. It + records the remote load report, shared dependency report, business component + success event, and repeated load cache evidence. Use this scenario when + validating whether an agent can answer who loaded which expose, which shared + provider was selected, and whether the producer load reused cached runtime + state. +- `Eager config error`: synchronously consumes an async shared dependency. The + report should include `observability-async-shared`, `RUNTIME-005`, and + `sync-async-boundary`, with `ownerHint: "shared"` and an eager config check + in `diagnosis.actions`. +- `Runtime eager config error`: synchronously consumes an async shared + dependency from the pure runtime path. The report should include + `observability-runtime-async-shared`, `RUNTIME-006`, and + `sync-async-boundary`, with the same eager config check. + +Run the automated verification: + +```bash +pnpm run ci:local --only=e2e-runtime +``` + +This command installs the e2e dependencies if needed, builds the packages, +starts the host and remotes, and runs the Cypress checks. The observability +fixture is covered by the `observability demo fixture` tests in the runtime host +e2e suite. diff --git a/apps/runtime-demo/typings/cssmodule.d.ts b/apps/runtime-demo/typings/cssmodule.d.ts new file mode 100644 index 00000000000..9df325d33dd --- /dev/null +++ b/apps/runtime-demo/typings/cssmodule.d.ts @@ -0,0 +1,6 @@ +declare module '*.module.css' { + const classes: { readonly [key: string]: string }; + export default classes; +} + +declare module '*.css'; diff --git a/apps/runtime-demo/typings/image.d.ts b/apps/runtime-demo/typings/image.d.ts new file mode 100644 index 00000000000..bef5a80f630 --- /dev/null +++ b/apps/runtime-demo/typings/image.d.ts @@ -0,0 +1,29 @@ +declare module '*.png' { + const src: string; + export default src; +} + +declare module '*.jpg' { + const src: string; + export default src; +} + +declare module '*.jpeg' { + const src: string; + export default src; +} + +declare module '*.gif' { + const src: string; + export default src; +} + +declare module '*.webp' { + const src: string; + export default src; +} + +declare module '*.svg' { + const src: string; + export default src; +} diff --git a/apps/shared-tree-shaking/no-server/host/CHANGELOG.md b/apps/shared-tree-shaking/no-server/host/CHANGELOG.md index 19fee0f2fcc..ee44035dc23 100644 --- a/apps/shared-tree-shaking/no-server/host/CHANGELOG.md +++ b/apps/shared-tree-shaking/no-server/host/CHANGELOG.md @@ -1,5 +1,11 @@ # modernjs-ssr-nested-remote +## 1.0.12 + +### Patch Changes + +- @module-federation/enhanced@2.5.0 + ## 1.0.11 ### Patch Changes diff --git a/apps/shared-tree-shaking/no-server/host/package.json b/apps/shared-tree-shaking/no-server/host/package.json index 0df348e0f58..f0d0a8d45d1 100644 --- a/apps/shared-tree-shaking/no-server/host/package.json +++ b/apps/shared-tree-shaking/no-server/host/package.json @@ -1,7 +1,7 @@ { "name": "shared-tree-shaking-no-server-host", "private": true, - "version": "1.0.11", + "version": "1.0.12", "scripts": { "reset": "npx rimraf ./**/node_modules", "dev": "modern dev", diff --git a/apps/shared-tree-shaking/no-server/provider/CHANGELOG.md b/apps/shared-tree-shaking/no-server/provider/CHANGELOG.md index 19fee0f2fcc..ee44035dc23 100644 --- a/apps/shared-tree-shaking/no-server/provider/CHANGELOG.md +++ b/apps/shared-tree-shaking/no-server/provider/CHANGELOG.md @@ -1,5 +1,11 @@ # modernjs-ssr-nested-remote +## 1.0.12 + +### Patch Changes + +- @module-federation/enhanced@2.5.0 + ## 1.0.11 ### Patch Changes diff --git a/apps/shared-tree-shaking/no-server/provider/package.json b/apps/shared-tree-shaking/no-server/provider/package.json index f6dac4f18de..52d7a5ad452 100644 --- a/apps/shared-tree-shaking/no-server/provider/package.json +++ b/apps/shared-tree-shaking/no-server/provider/package.json @@ -1,7 +1,7 @@ { "name": "shared-tree-shaking-no-server-provider", "private": true, - "version": "1.0.11", + "version": "1.0.12", "scripts": { "reset": "npx rimraf ./**/node_modules", "dev": "modern dev", diff --git a/apps/shared-tree-shaking/with-server/host/CHANGELOG.md b/apps/shared-tree-shaking/with-server/host/CHANGELOG.md index 9709a50b506..e64acd85b2b 100644 --- a/apps/shared-tree-shaking/with-server/host/CHANGELOG.md +++ b/apps/shared-tree-shaking/with-server/host/CHANGELOG.md @@ -1,5 +1,12 @@ # modernjs-ssr-nested-remote +## 1.0.12 + +### Patch Changes + +- Updated dependencies [180004d] + - @module-federation/modern-js-v3@2.5.0 + ## 1.0.11 ### Patch Changes diff --git a/apps/shared-tree-shaking/with-server/host/package.json b/apps/shared-tree-shaking/with-server/host/package.json index e553e52a76c..e5660e73d3d 100644 --- a/apps/shared-tree-shaking/with-server/host/package.json +++ b/apps/shared-tree-shaking/with-server/host/package.json @@ -1,7 +1,7 @@ { "name": "shared-tree-shaking-with-server-host", "private": true, - "version": "1.0.11", + "version": "1.0.12", "scripts": { "reset": "npx rimraf ./**/node_modules", "dev": "modern dev", diff --git a/apps/shared-tree-shaking/with-server/provider/CHANGELOG.md b/apps/shared-tree-shaking/with-server/provider/CHANGELOG.md index 9709a50b506..e64acd85b2b 100644 --- a/apps/shared-tree-shaking/with-server/provider/CHANGELOG.md +++ b/apps/shared-tree-shaking/with-server/provider/CHANGELOG.md @@ -1,5 +1,12 @@ # modernjs-ssr-nested-remote +## 1.0.12 + +### Patch Changes + +- Updated dependencies [180004d] + - @module-federation/modern-js-v3@2.5.0 + ## 1.0.11 ### Patch Changes diff --git a/apps/shared-tree-shaking/with-server/provider/package.json b/apps/shared-tree-shaking/with-server/provider/package.json index 039726d7912..650a4aa796d 100644 --- a/apps/shared-tree-shaking/with-server/provider/package.json +++ b/apps/shared-tree-shaking/with-server/provider/package.json @@ -1,7 +1,7 @@ { "name": "shared-tree-shaking-with-server-provider", "private": true, - "version": "1.0.11", + "version": "1.0.12", "scripts": { "reset": "npx rimraf ./**/node_modules", "dev": "modern dev", diff --git a/apps/website-new/CHANGELOG.md b/apps/website-new/CHANGELOG.md index 479597aaaaf..8ac8b785507 100644 --- a/apps/website-new/CHANGELOG.md +++ b/apps/website-new/CHANGELOG.md @@ -1,5 +1,14 @@ # website-new +## 1.3.24 + +### Patch Changes + +- d433ec9: feat(runtime): support finder callbacks in `getInstance` and clarify runtime instance API usage. +- Updated dependencies [41281f4] + - @module-federation/error-codes@2.5.0 + - @module-federation/rspress-plugin@2.5.0 + ## 1.3.23 ### Patch Changes diff --git a/apps/website-new/docs/en/_nav.json b/apps/website-new/docs/en/_nav.json index 664a422d4c1..257a65ce737 100644 --- a/apps/website-new/docs/en/_nav.json +++ b/apps/website-new/docs/en/_nav.json @@ -10,9 +10,63 @@ "activeMatch": "/guide/" }, { - "text": "Practice", - "link": "/practice/overview", - "activeMatch": "/practice/" + "text": "Integrations", + "link": "/integrations/", + "activeMatch": "/integrations/", + "items": [ + { + "text": "Overview", + "link": "/integrations/" + }, + { + "text": "Rsbuild", + "link": "/integrations/build-tool/rsbuild" + }, + { + "text": "Rslib", + "link": "/integrations/build-tool/rslib" + }, + { + "text": "Vite", + "link": "/integrations/build-tool/vite" + }, + { + "text": "Rspack", + "link": "/integrations/bundler/rspack" + }, + { + "text": "Webpack", + "link": "/integrations/bundler/webpack" + }, + { + "text": "Metro", + "link": "/integrations/bundler/metro" + }, + { + "text": "Rspress", + "link": "/integrations/documentation/rspress" + }, + { + "text": "Modern.js", + "link": "/integrations/framework/modernjs" + }, + { + "text": "Next.js", + "link": "/integrations/framework/nextjs" + }, + { + "text": "Angular", + "link": "/integrations/framework/angular" + }, + { + "text": "Monorepos", + "link": "/integrations/framework/monorepos" + }, + { + "text": "Practice", + "link": "/integrations/practice" + } + ] }, { "text": "Configuration", diff --git a/apps/website-new/docs/en/ai/index.mdx b/apps/website-new/docs/en/ai/index.mdx index dbf943ac741..c5237c44837 100644 --- a/apps/website-new/docs/en/ai/index.mdx +++ b/apps/website-new/docs/en/ai/index.mdx @@ -22,7 +22,7 @@ Many models still have incomplete or outdated knowledge of Module Federation 2.0 If you use Claude Code, Cursor, Windsurf, or another editor with Skills support, install: ```bash -npx skills add module-federation/core --skill mf -y +npx skills add module-federation/agent-skills --skill mf -y ``` Then ask the agent to read the docs before answering: @@ -46,9 +46,34 @@ The `mf` skill does more than answer questions. It can also help your agent: - analyze type issues - check shared dependency conflicts - inspect remote module information +- read observability reports and debug MF loading failures 👉 [View Agent Skills documentation](./skill) +## Observe and Debug MF Loading + +If you want an AI coding agent to debug Module Federation loading with concrete +runtime facts, enable the [Observability Plugin](../plugin/plugins/observability-plugin) +first. + +After a loading failure, the browser console will print a `traceId` and, +in development, a `read:` command. Give that console hint to your agent: + +```text +/mf observability +I saw this Module Federation console error: + +[Module Federation] Observability report generated +traceId: mf-... +read: window.__FEDERATION__.__OBSERVABILITY__["runtime_host"].getReport("mf-...") + +Please read the report and fix the issue. +``` + +For Node or SSR, ask the agent to read `.mf/observability/latest.json`. For +production, upload reports with the plugin's `onReport` callback and give the +uploaded report or trace ID to your agent. + ## You Can Still Use This Without Installing Anything If you just want a lightweight version first, send this directly to your agent: @@ -87,4 +112,6 @@ If you want to start using this workflow right away: 1. Install `mf` 2. Ask the question you care about most -3. Open [Skills documentation](./skill) to see which sub-commands it supports +3. Enable the [Observability Plugin](../plugin/plugins/observability-plugin) when + you need MF loading observability +4. Open [Skills documentation](./skill) to see which sub-commands it supports diff --git a/apps/website-new/docs/en/ai/skill.mdx b/apps/website-new/docs/en/ai/skill.mdx index de915d9bc47..4cc1ae37218 100644 --- a/apps/website-new/docs/en/ai/skill.mdx +++ b/apps/website-new/docs/en/ai/skill.mdx @@ -14,7 +14,7 @@ If you have not read it yet, start with [AI Quick Start](./index). ## Installation ```bash -npx skills add module-federation/core --skill mf -y +npx skills add module-federation/agent-skills --skill mf -y ``` After that, the main entry is: @@ -26,7 +26,7 @@ After that, the main entry is: If you cannot use the CLI, you can also copy the directory manually from GitHub: ```text -https://github.com/module-federation/core/tree/main/skills/mf +https://github.com/module-federation/agent-skills/tree/main/skills/mf ``` ## How to use it @@ -40,6 +40,7 @@ With explicit sub-commands: /mf integrate /mf type-check /mf runtime-error RUNTIME-008 +/mf observability ``` With natural language: @@ -48,6 +49,7 @@ With natural language: /mf What's the difference between singleton and requiredVersion in shared? /mf Help me figure out why this project is not pulling remote types /mf Add Module Federation to the current project +/mf I saw a Module Federation console error with traceId mf-... ``` ## What `mf` supports @@ -64,6 +66,7 @@ With natural language: | `config-check` | Troubleshoot config mistakes, missing files, and plugin mismatches | | `bridge-check` | Troubleshoot Bridge integration issues | | `runtime-error` | Troubleshoot explicit runtime error codes, especially `RUNTIME-001` and `RUNTIME-008` | +| `observability` | Read observability reports, trace loading phases, and debug runtime/build loading failures | ## The most common workflows @@ -96,10 +99,35 @@ If you already have a concrete problem: /mf shared-deps /mf config-check /mf runtime-error RUNTIME-008 +/mf observability ``` At that point, the goal is not for you to manually investigate first. The goal is to let the agent start diagnosing immediately. +### 4. Let the agent read MF observability + +For full loading observability, install the [Observability Plugin](../plugin/plugins/observability-plugin) +in your app. When a browser console error prints a `traceId` and `read:` +command, pass it to the skill: + +```text +/mf observability +I saw this Module Federation console error: + +[Module Federation] Observability report generated +traceId: mf-... +read: window.__FEDERATION__.__OBSERVABILITY__["runtime_host"].getReport("mf-...") + +Please read the report and fix the issue. +``` + +For Node, SSR, or production uploads: + +```text +/mf observability +Read .mf/observability/latest.json and explain the likely owner and fix. +``` + ## The one thing you really need to remember You do not need to read the docs first and then tell the agent what to do. diff --git a/apps/website-new/docs/en/configure/index.mdx b/apps/website-new/docs/en/configure/index.mdx index 10c1ea85440..bc75ea3113d 100644 --- a/apps/website-new/docs/en/configure/index.mdx +++ b/apps/website-new/docs/en/configure/index.mdx @@ -1,6 +1,6 @@ # Configuration Overview -The current page lists all the configuration options for `Module Federation`. Please refer to the documentation for [`Build Plugins`](../guide/build-plugins/plugins) to understand how to use them. +This page lists all configuration options for `Module Federation`. For package installation and plugin setup in different project types, see [Integrations](/integrations/). ```ts type ModuleFederationOptions = { diff --git a/apps/website-new/docs/en/guide/_meta.json b/apps/website-new/docs/en/guide/_meta.json index ebb1ec5c376..9ded710dc9d 100644 --- a/apps/website-new/docs/en/guide/_meta.json +++ b/apps/website-new/docs/en/guide/_meta.json @@ -16,23 +16,23 @@ }, { "type": "dir-section-header", - "name": "build-plugins", - "label": "Build Plugins" + "name": "bridge", + "label": "Bridge" }, { "type": "dir-section-header", - "name": "framework", - "label": "Frameworks" + "name": "data", + "label": "Data Management" }, { "type": "dir-section-header", - "name": "performance", - "label": "Performance" + "name": "advanced", + "label": "Advanced" }, { "type": "dir-section-header", - "name": "advanced", - "label": "Advanced" + "name": "debug", + "label": "Debug" }, { "type": "dir-section-header", @@ -41,12 +41,7 @@ }, { "type": "dir-section-header", - "name": "debug", - "label": "Debug" - }, - { - "type":"dir-section-header", - "name":"troubleshooting", - "label":"Troubleshooting" + "name": "troubleshooting", + "label": "Troubleshooting" } ] diff --git a/apps/website-new/docs/en/guide/advanced/_meta.json b/apps/website-new/docs/en/guide/advanced/_meta.json index e4bd4e44c80..fb74ac43c93 100644 --- a/apps/website-new/docs/en/guide/advanced/_meta.json +++ b/apps/website-new/docs/en/guide/advanced/_meta.json @@ -1,8 +1 @@ -[ - { - "type": "file", - "name": "multiple-shared-scope", - "label": "Multiple Share Scopes" - } -] - +["shared-tree-shaking", "multiple-shared-scope"] diff --git a/apps/website-new/docs/en/guide/performance/shared-tree-shaking.mdx b/apps/website-new/docs/en/guide/advanced/shared-tree-shaking.mdx similarity index 100% rename from apps/website-new/docs/en/guide/performance/shared-tree-shaking.mdx rename to apps/website-new/docs/en/guide/advanced/shared-tree-shaking.mdx diff --git a/apps/website-new/docs/en/guide/basic/_meta.json b/apps/website-new/docs/en/guide/basic/_meta.json index 16e101f10a3..c189e5a715e 100644 --- a/apps/website-new/docs/en/guide/basic/_meta.json +++ b/apps/website-new/docs/en/guide/basic/_meta.json @@ -1,8 +1 @@ -[ - "cli", - "css-isolate", - "type-prompt", - "data-fetch", - "data-fetch-cache", - "data-fetch-prefetch" -] +["cli", "css-isolate", "type-prompt", "manifest-snapshot"] diff --git a/apps/website-new/docs/en/guide/basic/manifest-snapshot.mdx b/apps/website-new/docs/en/guide/basic/manifest-snapshot.mdx new file mode 100644 index 00000000000..949a655e3f8 --- /dev/null +++ b/apps/website-new/docs/en/guide/basic/manifest-snapshot.mdx @@ -0,0 +1,120 @@ +# Manifest and Snapshot + +`mf-manifest.json` is a runtime manifest generated by a producer after build. It describes which modules the producer exposes, where the remote entry is, which assets those modules need, which shared dependencies are available, and where the type files are. + +Snapshot is a condensed and pre-resolved result of Manifest information. The runtime can request Manifest and generate Snapshot on demand. If you have a deployment service, it can also generate Snapshot ahead of time and deliver it to the consumer, so the consumer can get the remote entry and preloadable assets directly. + +This document only explains the concepts and where they are used. For configuration, see [manifest configuration](/configure/manifest). For complete fields, see [mf-manifest.json fields](/configure/manifest-fields). + +## Why Manifest + +With only `remoteEntry.js`, the consumer can load remote modules, but it cannot easily know the assets, types, and shared dependency information behind those remote modules ahead of time. + +With `mf-manifest.json`, the consumer can get more information before loading the remote module: + +- Remote entry URL and loading type +- Exposed modules and asset lists +- JavaScript and CSS that can be preloaded +- Shared dependency names, versions, and assets +- Type file URLs + +So Manifest is not just another entry URL. It is closer to a runtime instruction file that the producer provides to the consumer. + +## Manifest and Stats + +After enabling [manifest configuration](/configure/manifest), the build plugin generates both `mf-manifest.json` and `mf-stats.json`. + +| File | Purpose | +| --- | --- | +| `mf-manifest.json` | Read by the consumer runtime. Its fields are more stable and compact | +| `mf-stats.json` | Used for build analysis, diagnostics, and tooling. Its fields are more complete | + +If you only need to consume remote modules, you usually only need `mf-manifest.json`. If you are building analysis tools, diagnostics, or platform integrations, then inspect `mf-stats.json`. + +## What Snapshot Is + +Snapshot can be understood as the module information that the runtime actually consumes. + +Manifest is a build artifact from a single producer and is closer to a source manifest. Snapshot reorganizes that information into a structure that is easier for the runtime to consume, such as: + +- The final `remoteEntry` for the producer +- Assets for exposed modules +- Assets that can be preloaded +- Shared dependency information +- Other remote modules that the producer depends on + +Without a deployment service, the consumer requests `mf-manifest.json` first, and the runtime generates Snapshot from it. + +```txt +Host configures remotes + -> request mf-manifest.json + -> parse Manifest + -> generate Snapshot + -> load remoteEntry, expose, shared, and preload assets +``` + +With a deployment service, the service can consume Manifest ahead of time, generate Snapshot, and deliver it to the consumer. + +```txt +Producer builds Manifest + -> deployment service parses Manifest ahead of time + -> generate Snapshot + -> Host gets remoteEntry and preload assets directly +``` + +This can avoid one extra Manifest request at runtime and move asset resolution to the server side. + +## Where We Use It + +### Remote Loading + +When `remotes` points to `mf-manifest.json`, the runtime reads Manifest, generates Snapshot, and then resolves the final `remoteEntry` URL from Snapshot. + +See [remotes](/configure/remotes) for configuration. + +### Asset Preloading + +Manifest contains asset information for exposes and shared dependencies. After Snapshot is generated, the runtime can use that information to preload the JavaScript and CSS required by remote modules. + +### Chrome DevTools + +[Chrome DevTools](/guide/debug/chrome-devtool) uses Manifest / Snapshot to understand which federated modules exist on the current page, how modules depend on each other, how shared dependencies are reused, and which remote entry is needed for proxying. + +### Local Proxying + +When debugging a production page, proxy tools can use Snapshot to identify the target producer and replace it with a locally running Manifest or remote entry. + +### Runtime Diagnostics + +When remote loading fails, Manifest / Snapshot can help identify where the failure happens: + +- Whether the Manifest URL is reachable +- Whether Manifest fields are complete +- Whether the target producer exists in Snapshot +- Whether Snapshot can resolve `remoteEntry` +- Whether `remoteEntry` loads successfully +- Whether the expose exists + +This helps distinguish Manifest request failures, Snapshot misses, and later remote entry loading failures. + +## What You Can Build With It + +Manifest / Snapshot is suitable as input for runtime plugins, debugging tools, and deployment platforms. + +You can build: + +- **Preload optimization**: get the JS and CSS associated with remote modules earlier and reduce waiting time after user interaction. +- **Module graph visualization**: show which producers, consumers, and shared dependencies are loaded on the current page. +- **Local proxy debugging**: replace a producer on a production page with a locally running producer. +- **Loading diagnostics**: tell whether a failure happens at the Manifest, Snapshot, remoteEntry, or expose stage. +- **Shared analysis**: check whether shared dependencies are reused, duplicated, or matched against the expected version. + +Business code should not modify global Snapshot directly. Prefer consuming this information through Runtime Plugin, DevTools, or a deployment service. + +## Impact of disableSnapshot + +If `optimization.disableSnapshot` is enabled, Snapshot and preload-related runtime capabilities are removed to reduce runtime size. + +This affects capabilities that depend on Manifest / Snapshot, such as remote type hints, preloading, DevTools visualization, and proxying. + +See [optimization.disableSnapshot](/configure/experiments#disablesnapshot) for more details. diff --git a/apps/website-new/docs/en/guide/bridge/_meta.json b/apps/website-new/docs/en/guide/bridge/_meta.json new file mode 100644 index 00000000000..b7f300763b2 --- /dev/null +++ b/apps/website-new/docs/en/guide/bridge/_meta.json @@ -0,0 +1,14 @@ +[ + { + "type": "file", + "name": "overview", + "label": "Overview" + }, + { + "type": "dir", + "name": "react", + "label": "React", + "collapsible": true, + "collapsed": true + } +] diff --git a/apps/website-new/docs/en/practice/bridge/overview.mdx b/apps/website-new/docs/en/guide/bridge/overview.mdx similarity index 96% rename from apps/website-new/docs/en/practice/bridge/overview.mdx rename to apps/website-new/docs/en/guide/bridge/overview.mdx index a0f03c26ad7..4f2cba2807f 100644 --- a/apps/website-new/docs/en/practice/bridge/overview.mdx +++ b/apps/website-new/docs/en/guide/bridge/overview.mdx @@ -46,6 +46,10 @@ A Bridge toolkit designed specifically for React applications, supporting all ve - Advantages: More fine-grained loading control and performance optimization - Scenarios: Suitable for on-demand loading of large component libraries or complex business components +#### Practice Guides +- [React practice](/integrations/practice/react) +- [Vue Bridge practice](/integrations/practice/vue) + ### Vue Bridge (`@module-federation/bridge-vue3`) A Bridge toolkit designed specifically for Vue 3 applications, making full use of Vue 3's modern features. @@ -140,14 +144,20 @@ In addition to application-level modularity, Bridge also supports component-leve Component-level loading and application-level loading work perfectly together: ```tsx +import { getInstance } from '@module-federation/runtime'; +import { lazyLoadComponentPlugin } from '@module-federation/bridge-react/data-fetch'; + // Application-level loading - Load complete remote application const RemoteApp = createRemoteAppComponent({ loader: () => import('remote/app'), fallback: }); +const instance = getInstance(); +instance.registerPlugins([lazyLoadComponentPlugin()]); + // Component-level loading - Load components on demand within remote applications -const LazyComponent = createLazyComponent({ +const LazyComponent = instance.createLazyComponent({ loader: () => import('remote/heavy-component'), loading: , fallback: ({ error }) => @@ -272,4 +282,3 @@ function App() { ``` Through this pattern, Bridge achieves framework-agnostic application-level module loading, providing a solid technical foundation for micro-frontend architectures. - diff --git a/apps/website-new/docs/en/practice/bridge/react-bridge/_meta.json b/apps/website-new/docs/en/guide/bridge/react/_meta.json similarity index 100% rename from apps/website-new/docs/en/practice/bridge/react-bridge/_meta.json rename to apps/website-new/docs/en/guide/bridge/react/_meta.json diff --git a/apps/website-new/docs/en/practice/bridge/react-bridge/export-app.mdx b/apps/website-new/docs/en/guide/bridge/react/export-app.mdx similarity index 100% rename from apps/website-new/docs/en/practice/bridge/react-bridge/export-app.mdx rename to apps/website-new/docs/en/guide/bridge/react/export-app.mdx diff --git a/apps/website-new/docs/en/practice/bridge/react-bridge/getting-started.mdx b/apps/website-new/docs/en/guide/bridge/react/getting-started.mdx similarity index 100% rename from apps/website-new/docs/en/practice/bridge/react-bridge/getting-started.mdx rename to apps/website-new/docs/en/guide/bridge/react/getting-started.mdx diff --git a/apps/website-new/docs/en/practice/bridge/react-bridge/load-app.mdx b/apps/website-new/docs/en/guide/bridge/react/load-app.mdx similarity index 100% rename from apps/website-new/docs/en/practice/bridge/react-bridge/load-app.mdx rename to apps/website-new/docs/en/guide/bridge/react/load-app.mdx diff --git a/apps/website-new/docs/en/practice/bridge/react-bridge/load-component.mdx b/apps/website-new/docs/en/guide/bridge/react/load-component.mdx similarity index 98% rename from apps/website-new/docs/en/practice/bridge/react-bridge/load-component.mdx rename to apps/website-new/docs/en/guide/bridge/react/load-component.mdx index 3179c8138ce..7af4ddb9497 100644 --- a/apps/website-new/docs/en/practice/bridge/react-bridge/load-component.mdx +++ b/apps/website-new/docs/en/guide/bridge/react/load-component.mdx @@ -36,7 +36,7 @@ Register the lazyLoadComponentPlugin plugin at runtime to enable createLazyCompo ```tsx import { getInstance } from '@module-federation/runtime'; -import { lazyLoadComponentPlugin } from '@module-federation/bridge-react'; +import { lazyLoadComponentPlugin } from '@module-federation/bridge-react/data-fetch'; const instance = getInstance(); // Register lazyLoadComponentPlugin plugin @@ -49,7 +49,7 @@ After registering the `lazyLoadComponentPlugin` plugin, you can create lazy-load ```tsx import { getInstance } from '@module-federation/runtime'; -import { lazyLoadComponentPlugin } from '@module-federation/bridge-react'; +import { lazyLoadComponentPlugin } from '@module-federation/bridge-react/data-fetch'; const instance = getInstance(); // After registering lazyLoadComponentPlugin plugin, you can use `createLazyComponent` or `prefetch` APIs @@ -133,7 +133,7 @@ In addition to loading components, this function also supports the following cap ```tsx import React, { FC, memo, useEffect } from 'react'; import { getInstance } from '@module-federation/enhanced/runtime'; -import { ERROR_TYPE } from '@module-federation/bridge-react'; +import { ERROR_TYPE } from '@module-federation/bridge-react/data-fetch'; const instance = getInstance(); const LazyComponent = instance.createLazyComponent({ diff --git a/apps/website-new/docs/en/guide/build-plugins/_meta.json b/apps/website-new/docs/en/guide/build-plugins/_meta.json deleted file mode 100644 index 6e5cdb2df5b..00000000000 --- a/apps/website-new/docs/en/guide/build-plugins/_meta.json +++ /dev/null @@ -1,9 +0,0 @@ -[ - "plugins", - "plugins-rsbuild", - "plugins-rspack", - "plugins-webpack", - "plugins-rspress", - "plugins-vite", - "plugins-metro" -] diff --git a/apps/website-new/docs/en/guide/build-plugins/plugins.mdx b/apps/website-new/docs/en/guide/build-plugins/plugins.mdx deleted file mode 100644 index 883e5a11117..00000000000 --- a/apps/website-new/docs/en/guide/build-plugins/plugins.mdx +++ /dev/null @@ -1,14 +0,0 @@ -# Overview - -{ props.name || 'Module Federation' } provides build plugins for different bundlers. Read the plugin documentation for your bundler to integrate and configure MF in your project. - -Plugins for different bundlers may include a few bundler-specific options, but the core configuration stays consistent. See [Configuration](../../../configure/index). - -## Plugin List - -- [Rsbuild](./plugins-rsbuild) -- [Rspack](./plugins-rspack) -- [Webpack](./plugins-webpack) -- [Vite](./plugins-vite) -- [Metro](./plugins-metro) -- [Rspress](./plugins-rspress) diff --git a/apps/website-new/docs/en/guide/data/_meta.json b/apps/website-new/docs/en/guide/data/_meta.json new file mode 100644 index 00000000000..6730f293a44 --- /dev/null +++ b/apps/website-new/docs/en/guide/data/_meta.json @@ -0,0 +1,17 @@ +[ + { + "type": "file", + "name": "data-fetch", + "label": "Data Fetching" + }, + { + "type": "file", + "name": "data-fetch-cache", + "label": "Data Cache" + }, + { + "type": "file", + "name": "data-prefetch", + "label": "Data Prefetch" + } +] diff --git a/apps/website-new/docs/en/guide/basic/data-fetch-cache.mdx b/apps/website-new/docs/en/guide/data/data-fetch-cache.mdx similarity index 97% rename from apps/website-new/docs/en/guide/basic/data-fetch-cache.mdx rename to apps/website-new/docs/en/guide/data/data-fetch-cache.mdx index 80df73bf19b..f84cb51e414 100644 --- a/apps/website-new/docs/en/guide/basic/data-fetch-cache.mdx +++ b/apps/website-new/docs/en/guide/data/data-fetch-cache.mdx @@ -5,7 +5,7 @@ The `cache` function allows you to cache the results of data fetching or computa ## Basic Usage ```ts -import { cache } from '@module-federation/bridge-react'; +import { cache } from '@module-federation/bridge-react/data-fetch'; export type Data = { data: string; @@ -59,7 +59,7 @@ This function is only supported for use within a DataLoader. After each computation, the framework records the time the data was written to the cache. When the function is called again, it checks if the cache has expired based on the `maxAge` parameter. If it has, the `fn` function is re-executed; otherwise, the cached data is returned. ```ts -import { cache, CacheTime } from '@module-federation/bridge-react'; +import { cache, CacheTime } from '@module-federation/bridge-react/data-fetch'; const getDashboardStats = cache( async () => { @@ -78,7 +78,7 @@ The `revalidate` parameter sets a time window for revalidating the cache after i In the following example, if `getDashboardStats` is called within the 2-minute non-expired window, it returns cached data. If the cache is expired (between 2 and 3 minutes), incoming requests will first receive the old data, and then a new request will be made in the background to update the cache. ```ts -import { cache, CacheTime } from '@module-federation/bridge-react'; +import { cache, CacheTime } from '@module-federation/bridge-react/data-fetch'; const getDashboardStats = cache( async () => { @@ -96,7 +96,7 @@ const getDashboardStats = cache( The `tag` parameter is used to identify a cache with a label, which can be a string or an array of strings. This tag can be used to invalidate the cache, and multiple cache functions can share the same tag. ```ts -import { cache, revalidateTag } from '@module-federation/bridge-react'; +import { cache, revalidateTag } from '@module-federation/bridge-react/data-fetch'; const getDashboardStats = cache( async () => { @@ -124,7 +124,7 @@ revalidateTag('dashboard-stats'); // This will invalidate the caches for both ge The `getKey` parameter allows you to customize how cache keys are generated. For example, you might only need to rely on a subset of the function's parameters to differentiate caches. It is a function that receives the same arguments as the original function and returns a string as the cache key: ```ts -import { cache, CacheTime } from '@module-federation/bridge-react'; +import { cache, CacheTime } from '@module-federation/bridge-react/data-fetch'; import { fetchUserData } from './api'; const getUser = cache( @@ -156,7 +156,7 @@ The `generateKey` function in Modern.js ensures that a consistent, unique key is ::: ```ts -import { cache, CacheTime, generateKey } from '@module-federation/bridge-react'; +import { cache, CacheTime, generateKey } from '@module-federation/bridge-react/data-fetch'; import { fetchUserData } from './api'; const getUser = cache( @@ -193,7 +193,7 @@ Generally, the cache will be invalidated in the following scenarios: This is very useful in certain scenarios, such as when the function reference changes, but you still want to return cached data. ```ts -import { cache } from '@module-federation/bridge-react'; +import { cache } from '@module-federation/bridge-react/data-fetch'; import { fetchUserData } from './api'; // Different function references, but they can share a cache via customKey. @@ -249,7 +249,7 @@ const getUserD = cache( The `onCache` parameter allows you to track cache statistics, such as hit rates. It is a callback function that receives information about each cache operation, including its status, key, parameters, and result. You can return `false` from `onCache` to prevent a cache hit. ```ts -import { cache, CacheTime } from '@module-federation/bridge-react'; +import { cache, CacheTime } from '@module-federation/bridge-react/data-fetch'; // Track cache statistics. const stats = { @@ -317,10 +317,9 @@ Considering that the content cached by the `cache` function is not expected to b You can specify the cache storage limit using the `configureCache` function: ```ts -import { configureCache, CacheSize } from '@module-federation/bridge-react'; +import { configureCache, CacheSize } from '@module-federation/bridge-react/data-fetch'; configureCache({ maxSize: CacheSize.MB * 10, // 10MB }); ``` - diff --git a/apps/website-new/docs/en/guide/basic/data-fetch.mdx b/apps/website-new/docs/en/guide/data/data-fetch.mdx similarity index 98% rename from apps/website-new/docs/en/guide/basic/data-fetch.mdx rename to apps/website-new/docs/en/guide/data/data-fetch.mdx index 3eec1050e8d..88365af52e7 100644 --- a/apps/website-new/docs/en/guide/basic/data-fetch.mdx +++ b/apps/website-new/docs/en/guide/data/data-fetch.mdx @@ -44,7 +44,7 @@ Each exposed module can have a corresponding `.data` file with the same name. Th Here, `List.data.ts` needs to export a function named `fetchData`, which will be executed before the `List` component renders and injects its data. Here is an example: ```ts title="List.data.ts" -import type { DataFetchParams } from '@module-federation/bridge-react'; +import type { DataFetchParams } from '@module-federation/bridge-react/data-fetch'; export type Data = { data: string; }; @@ -130,7 +130,7 @@ import Consumer from '@components/en/data-fetch/consumer'; #### Parameters -By default, parameters are passed to the loader function. The type is [DataFetchParams](/practice/bridge/react-bridge/load-component#datafetchparams), which includes the following field: +By default, parameters are passed to the loader function. The type is [DataFetchParams](/guide/bridge/react/load-component#datafetchparams), which includes the following field: - `isDowngrade` (boolean): Indicates whether the current execution context is in a fallback mode. For example, if Server-Side Rendering (SSR) fails, a new request is sent from the client-side (CSR) to the server to call the loader function. In this case, the value is `true`. diff --git a/apps/website-new/docs/en/guide/basic/data-fetch-prefetch.mdx b/apps/website-new/docs/en/guide/data/data-prefetch.mdx similarity index 98% rename from apps/website-new/docs/en/guide/basic/data-fetch-prefetch.mdx rename to apps/website-new/docs/en/guide/data/data-prefetch.mdx index c1d48ee1d56..7e9ea9dd251 100644 --- a/apps/website-new/docs/en/guide/basic/data-fetch-prefetch.mdx +++ b/apps/website-new/docs/en/guide/data/data-prefetch.mdx @@ -1,4 +1,4 @@ -# Prefetch +# Data Prefetch The `prefetch` function is used to pre-fetch resources and **data** for remote modules, thereby improving application performance and user experience. By pre-loading required content before a user accesses a feature, waiting times can be significantly reduced. @@ -39,7 +39,7 @@ interface ModuleFederation { Suppose we have a remote application `shop` that exposes a `Button` component, and this component is associated with a data fetch function. -import PrefetchDemo from '@components/en/data-fetch/prefetch-demo' +import PrefetchDemo from '@components/en/data-fetch/prefetch-demo'; {props.prefetchDemo || React.createElement(PrefetchDemo)} diff --git a/apps/website-new/docs/en/guide/debug/chrome-devtool.mdx b/apps/website-new/docs/en/guide/debug/chrome-devtool.mdx index dc164d89f76..658f74806ad 100644 --- a/apps/website-new/docs/en/guide/debug/chrome-devtool.mdx +++ b/apps/website-new/docs/en/guide/debug/chrome-devtool.mdx @@ -8,6 +8,7 @@ The `Micro-frontend` architecture differs from the traditional monolithic applic - Switch online page `Module Federation` versions for quick functional verification. - Support viewing module dependency information. - Support filtering specified module dependency information. +- Support Loading Trace for remote, shared, component-ready signals, and failure reports. ::: tip Limitations of Chrome Devtool: @@ -15,6 +16,12 @@ You must use `mf-manifest.json` to use the visualization and proxy capabilities ::: +::: tip Loading Trace availability + +The Chrome extension version that includes the Loading Trace tab is under review and will be available in the Chrome Web Store soon. + +::: + ## Usage Scenarios DevTools provides multiple functional panels suitable for different debugging needs in development and production environments: @@ -32,6 +39,12 @@ DevTools provides multiple functional panels suitable for different debugging ne - Analyze version reuse of shared dependencies (Loaded / Reused). - Check the effective status of configurations such as Singleton and Strict Version. +- **Loading Trace**: Records Module Federation loading on the current page. + - Inspect `loadRemote` and `loadShare` start, success, failure, and recovery states. + - See which consumer loaded which remote and expose, and which shared provider/version was selected. + - If the page already uses the [Observability Plugin](../../plugin/plugins/observability-plugin), the panel reads those reports and shows `CUSTOM`. + - If the page does not use the plugin, click `Observe now` to temporarily inject the Chrome collection plugin for the current tab. + ![](https://module-federation-assest.netlify.app/document/guide/chrome-devtools/shared-overview.png) ## How to Install @@ -57,6 +70,59 @@ As shown below, the proxy page provides options such as `add new proxy module`, - **Support Multi-Tab Isolation**: When opening pages using Module Federation in multiple tabs simultaneously, the proxy rules and module information of each tab are independent. Proxy rules set in Tab A will not affect Tab B, and vice versa. This allows you to debug multiple environments or application states simultaneously. ![](https://module-federation-assest.netlify.app/document/guide/chrome-devtools/proxy.png) +{/* +## Loading Trace + +The `Loading Trace` tab answers what actually happened during Module Federation loading. Use it when the page is blank, a component stays in loading, shared versions are unclear, or a producer fails intermittently. + +![](https://module-federation-assest.netlify.app/document/guide/chrome-devtools/loading-trace-overview-en.png) + +The top status means: + +- `OFF`: no readable loading report has been found on the current page. +- `ON`: Chrome collection is enabled for the current tab. +- `CUSTOM`: the inspected page already registered its own observability plugin, and DevTools is reading those reports. + +Common workflow: + +1. Open the target page. +2. Open Chrome DevTools and choose the `Module Federation` panel. +3. Open `Loading Trace`. +4. If the status is `OFF`, click `Observe now`. The page reloads once. +5. Reproduce the issue or trigger the remote component load you want to inspect. +6. Select a report and inspect current loading, previous loading records, the event timeline, and troubleshooting suggestions. +7. Click `Export` to save the full report. + +Report states: + +- `Success`: the MF chain completed. Remote reports show the remote result; shared reports show the selected provider and version. +- `Failed`: the chain failed. Start from the failed phase, error code, and suggested actions. +- `Pending`: a start event was recorded, but no completion, failure, or recovery event was seen yet. +- `Recovered`: loading hit a problem first, then continued through fallback or recovery. +- `Basic observability`: the runtime version is too low or cannot be detected, so detailed phases may be missing. + +### Analyze an exported report + +The exported JSON contains `config`, `scopes`, and `reports`. For each report, start with: + +![](https://module-federation-assest.netlify.app/document/guide/chrome-devtools/loading-trace-shared-report-en.png) + +- `diagnosis`: the troubleshooting conclusion, facts, and next actions. +- `summary`: the final state, such as `runtime-loaded`, `component-loaded`, `shared-resolved`, `failed`, `recovered`, or `pending`. +- `remote` / `shared`: what was loaded. For shared reports, inspect provider, required version, selected version, and available versions. +- `loadedBefore`: whether the same producer had already been loaded by another consumer. +- `events`: the ordered timeline, useful for finding the phase that got stuck. + +You can give the exported report directly to an AI coding agent: + +```text +/mf observability + +I have an MF Loading Trace report exported from Chrome DevTools. +Please tell me whether the load succeeded, where it failed, who likely owns the issue, and how to fix it. + + +``` */} ## How to Proxy Locally Developed Modules to Online diff --git a/apps/website-new/docs/en/guide/framework/_meta.json b/apps/website-new/docs/en/guide/framework/_meta.json deleted file mode 100644 index 0e5f843e429..00000000000 --- a/apps/website-new/docs/en/guide/framework/_meta.json +++ /dev/null @@ -1 +0,0 @@ -["modernjs","nextjs"] diff --git a/apps/website-new/docs/en/guide/performance/_meta.json b/apps/website-new/docs/en/guide/performance/_meta.json deleted file mode 100644 index 9738874ed56..00000000000 --- a/apps/website-new/docs/en/guide/performance/_meta.json +++ /dev/null @@ -1,3 +0,0 @@ -[ - "shared-tree-shaking" -] diff --git a/apps/website-new/docs/en/guide/runtime/index.mdx b/apps/website-new/docs/en/guide/runtime/index.mdx index 50f2da18263..dc61a3d3222 100644 --- a/apps/website-new/docs/en/guide/runtime/index.mdx +++ b/apps/website-new/docs/en/guide/runtime/index.mdx @@ -38,31 +38,44 @@ import Runtime from '@components/en/runtime/index'; ## Installation -import { PackageManagerTabs } from '@theme'; +Different project types should install and import Runtime from different entries. Most projects should install `@module-federation/enhanced` and import Runtime APIs from `@module-federation/enhanced/runtime`. If your Modern.js project already uses a Module Federation plugin, import Runtime APIs from the plugin's `/runtime` entry so the framework plugin and manual Runtime calls use the same Runtime instance. + +import { PackageManagerTabs, Tab, Tabs } from '@theme'; + +### @module-federation/enhanced -::: tip Note: +```ts +import { createInstance, loadRemote } from '@module-federation/enhanced/runtime'; +``` -- In the following `Federation Runtime` examples, we all show cases that are detached from specific frameworks such as Modern.js, so the API will be exported from the initial `@module-federation/enhanced/runtime` package. +### Modern.js -- If your project is a Modern.js project and uses `@module-federation/modern-js-v3`, the runtime should export the runtime API from `@module-federation/modern-js-v3/runtime`. This ensures that the plugin and the runtime use the same runtime instance, ensuring that modules are loaded normally. + -- If your project is a Modern.js project but does not use `@module-federation/modern-js-v3`, you should export the runtime API from `@module-federation/enhanced/runtime`. However, we recommend that you use `@module-federation/modern-js-v3` for module registration and loading, which will allow you to enjoy more capabilities combined with the framework. +```ts +import { createInstance, loadRemote } from '@module-federation/modern-js-v3/runtime'; +``` -::: +For Modern.js v2 projects, install `@module-federation/modern-js` and import Runtime APIs from `@module-federation/modern-js/runtime`. ## Module Registration -import { Steps, Tab, Tabs } from '@theme'; - ```tsx @@ -324,3 +337,9 @@ import { Steps, Tab, Tabs } from '@theme'; ``` + +## Read Next + +- [Runtime API](./runtime-api): `createInstance`, `loadRemote`, `registerRemotes`, and other runtime APIs. +- [Runtime Plugins](./runtime-plugins): extend the runtime loading flow. +- [Runtime Hooks](./runtime-hooks): lifecycle hooks available to runtime plugins. diff --git a/apps/website-new/docs/en/guide/runtime/runtime-api.mdx b/apps/website-new/docs/en/guide/runtime/runtime-api.mdx index 1f6d0ef5035..ee25ae50e21 100644 --- a/apps/website-new/docs/en/guide/runtime/runtime-api.mdx +++ b/apps/website-new/docs/en/guide/runtime/runtime-api.mdx @@ -126,6 +126,7 @@ type Share = { + ```ts import { init, loadRemote } from '@module-federation/enhanced/runtime'; @@ -142,6 +143,7 @@ init({ ], }); ``` + ### Recommended alternatives @@ -243,6 +245,7 @@ If you are not using the build plugin, you can switch from `init` to [createInst When the build plugin or [init](#init) creates the default instance, you can call `getInstance()` to retrieve it. + ```ts import { getInstance } from '@module-federation/enhanced/runtime'; @@ -254,6 +257,8 @@ if (!mfInstance) { mfInstance.loadRemote('remote/util'); ``` +If the build plugin is not used, calling `getInstance` will throw an exception. In this case, you need to use the [createInstance](#createinstance) to create a new instance. + Instances created by [createInstance](#createinstance) do not replace the default instance, but they are still registered globally. You can pass a finder callback to `getInstance` to retrieve one later even if you did not keep the returned reference. The finder callback works like `Array.prototype.find`: the runtime iterates over the registered instances and returns the first match. If no matching instance exists, `getInstance` returns `null`. @@ -467,11 +472,10 @@ Use this when you want a plugin to be picked up by all future runtime instances, Global plugins are deduplicated by `plugin.name`. + ```ts -import { - registerGlobalPlugins, - createInstance, -} from '@module-federation/enhanced/runtime'; +import { registerGlobalPlugins, createInstance } from '@module-federation/enhanced/runtime'; + import runtimePlugin from './runtime-plugin'; registerGlobalPlugins([runtimePlugin()]); @@ -487,136 +491,8 @@ const mf = createInstance({ }); ``` -For predictable behavior, register global plugins before creating or using runtime instances. - -## getRemoteInfo - -- type - -```typescript -function getRemoteInfo(remote: Remote): RemoteInfo {} -``` - -Normalizes a remote definition into the runtime shape used internally by the loader. - -This helper fills in defaults such as: - -- `type` -- `entryGlobalName` -- `shareScope` - -```ts -import { getRemoteInfo } from '@module-federation/enhanced/runtime'; - -const remoteInfo = getRemoteInfo({ - name: 'sub1', - entry: 'http://localhost:2001/mf-manifest.json', -}); - -console.log(remoteInfo.entryGlobalName); -console.log(remoteInfo.shareScope); -``` - -This is a low-level helper. Most applications should configure remotes through `createInstance`, `registerRemotes`, or the build plugin. - -## getRemoteEntry - -- type - -```typescript -function getRemoteEntry(params: { - origin: ModuleFederation; - remoteInfo: RemoteInfo; - remoteEntryExports?: RemoteEntryExports; - getEntryUrl?: (url: string) => string; -}): Promise {} -``` - -Loads a remote entry and returns its entry exports. - -This is mainly useful for low-level tooling, debugging, custom loaders, or advanced runtime integrations. Most applications should prefer `loadRemote`. - -```ts -import { - createInstance, - getRemoteInfo, - getRemoteEntry, -} from '@module-federation/enhanced/runtime'; - -const mf = createInstance({ - name: 'mf_host', - remotes: [], -}); - -const remoteInfo = getRemoteInfo({ - name: 'sub1', - entry: 'http://localhost:2001/mf-manifest.json', -}); - -const remoteEntryExports = await getRemoteEntry({ - origin: mf, - remoteInfo, -}); -``` - -## loadScript - -- type - -```typescript -function loadScript( - url: string, - info: { - attrs?: Record; - createScriptHook?: CreateScriptHookDom; - }, -): Promise {} -``` - -Low-level browser helper that injects and loads a remote entry script. - -Use it only when you need to work below `loadRemote` / `getRemoteEntry`, for example in custom script loading flows. - -```ts -import { loadScript } from '@module-federation/enhanced/runtime'; - -await loadScript('http://localhost:2001/remoteEntry.js', { - attrs: { - crossorigin: 'anonymous', - }, -}); -``` - -## loadScriptNode - -- type - -```typescript -function loadScriptNode( - url: string, - info: { - attrs?: Record; - loaderHook?: { - createScriptHook?: CreateScriptHookNode; - }; - }, -): Promise {} -``` - -Low-level Node.js helper that loads a remote entry into the current process. - -Use it only in Node-side integrations or custom loaders. In browser code, use `loadScript` instead. - -```ts -import { loadScriptNode } from '@module-federation/enhanced/runtime'; -await loadScriptNode('http://localhost:2001/remoteEntry.js', { - attrs: { - name: 'sub1', - globalName: 'sub1', - }, -}); -``` +For predictable behavior, register global plugins before creating or using runtime instances. ## registerShared @@ -920,6 +796,82 @@ Through `preloadRemote`, you can start preloading module resources at an earlier * `remote`'s `expose` * `remote`'s synchronous or asynchronous resources * `remote`'s dependent `remote` resources + +`preloadRemote` waits for the resources involved in the current preload call to +finish. The Promise resolves when every resource succeeds or is already cached. +If any resource fails or times out, the Promise rejects and the error object +contains the preload resource results. + +If you only need the final result, use `await` or `.then/.catch`: + +```ts +import { preloadRemote } from '@module-federation/enhanced/runtime'; + +try { + await preloadRemote([ + { + nameOrAlias: 'sub1', + exposes: ['add'], + resourceCategory: 'all', + }, + ]); + + console.log('sub1/add preload success'); +} catch (error) { + console.error('sub1/add preload failed', error); +} +``` + +If you need to count which resources succeeded, failed, timed out, or came from +cache, read `results` from the error object: + +```ts +type PreloadRemoteError = Error & { + results?: Array<{ + id: string; + results: Array<{ + url: string; + status: 'success' | 'error' | 'timeout' | 'cached'; + resourceType: 'manifest' | 'remoteEntry' | 'js' | 'css'; + error?: unknown; + }>; + }>; +}; + +preloadRemote([ + { + nameOrAlias: 'sub1', + exposes: ['add'], + resourceCategory: 'all', + }, +]).catch((error: PreloadRemoteError) => { + const failedResources = + error.results + ?.flatMap((remoteResult) => + remoteResult.results.map((resource) => ({ + id: remoteResult.id, + ...resource, + })), + ) + .filter( + (resource) => + resource.status === 'error' || resource.status === 'timeout', + ) ?? []; + + failedResources.forEach((resource) => { + console.error( + `[preloadRemote] ${resource.id} ${resource.resourceType} failed`, + resource.url, + resource.error, + ); + }); +}); +``` + +When `exposes` is not specified, the resource id is `remoteName/*`. When +`exposes` is specified, the runtime generates resources per expose and the +resource id is `remoteName/expose`. + ```tsx diff --git a/apps/website-new/docs/en/guide/runtime/runtime-hooks.mdx b/apps/website-new/docs/en/guide/runtime/runtime-hooks.mdx index abf89363a3b..77d2aae95c6 100644 --- a/apps/website-new/docs/en/guide/runtime/runtime-hooks.mdx +++ b/apps/website-new/docs/en/guide/runtime/runtime-hooks.mdx @@ -1,5 +1,11 @@ # Runtime Hooks +## Hook return values + +For `SyncHook` and `AsyncHook`, returning `undefined` means the plugin only observes the event. It does not clear a value returned by an earlier plugin. A later plugin can still replace that value by returning another non-`undefined` value. + +For `SyncWaterfallHook` and `AsyncWaterfallHook`, return the full updated args object when you need to change the payload. Returning `undefined` keeps the current args and passes them to the next plugin. + ## beforeInit `SyncWaterfallHook` @@ -64,6 +70,28 @@ type BeforeRequestOptions ={ } ``` +## afterMatchRemote + +`AsyncHook` + +Called after the runtime matches a `loadRemote` request to a configured remote. If matching fails, the hook is still called with `error`. It is useful for diagnostics, request tracing, and checking whether a failure happened before manifest or remoteEntry loading. + +* type + +```ts +async function afterMatchRemote(args: AfterMatchRemoteOptions): Promise + +type AfterMatchRemoteOptions ={ + id: string; + options: ModuleFederationRuntimeOptions; + remote?: Remote; + expose?: string; + remoteInfo?: RemoteInfo; + error?: unknown; + origin: ModuleFederation; +} +``` + ## afterResolve `AsyncWaterfallHook` @@ -126,6 +154,33 @@ interface RemoteInfo { } ``` +## afterLoadRemote + +`AsyncHook` + +Called after a `loadRemote` request finishes. It runs for successful loads, failed loads, and loads recovered by `errorLoadRemote`. Use it to record the final remote loading status. + +`onLoad` runs before `afterLoadRemote` on a successful load. If a load fails and `errorLoadRemote` returns a fallback, `afterLoadRemote` receives the original `error` and `recovered: true`. + +* type + +```ts +async function afterLoadRemote(args: AfterLoadRemoteOptions): Promise + +type AfterLoadRemoteOptions ={ + id: string; + expose?: string; + remote?: RemoteInfo; + options?: { + loadFactory?: boolean; + from?: 'build' | 'runtime'; + }; + error?: unknown; + recovered?: boolean; + origin: ModuleFederation; +} +``` + ## beforeInitContainer `AsyncWaterfallHook` @@ -207,6 +262,8 @@ type ErrorLoadRemoteOptions ={ options?: any; from: 'build' | 'runtime'; lifecycle: 'beforeRequest' | 'beforeLoadShare' | 'afterResolve' | 'onLoad'; + remote?: RemoteInfo; + expose?: string; origin: ModuleFederation; } ``` @@ -216,12 +273,16 @@ The `lifecycle` parameter indicates the stage where the error occurred: - `beforeRequest`: Error during initial request processing - `afterResolve`: Error during manifest loading (most common for network failures) - `onLoad`: Error during module loading and execution -- `beforeLoadShare`: Error during shared dependency loading +- `beforeLoadShare`: Error while loading a remoteEntry during shared dependency initialization + +If this hook returns a fallback module, the runtime uses that value to continue. If a diagnostics or logging plugin returns `undefined`, it does not clear a fallback returned by another plugin. * example + ```ts -import { createInstance, loadRemote } from '@module-federation/enhanced/runtime' +import { createInstance, loadRemote } from '@module-federation/enhanced/runtime'; + import type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime'; const fallbackPlugin: () => ModuleFederationRuntimePlugin = @@ -232,7 +293,7 @@ const fallbackPlugin: () => ModuleFederationRuntimePlugin = const { lifecycle, id, error } = args; if (error) { - console.warn(`Failed to load remote ${id} at ${lifecycle}:`, error?.message || error); + console.warn(\`Failed to load remote \${id} at \${lifecycle}:\`, error?.message || error); } switch (lifecycle) { @@ -247,7 +308,7 @@ const fallbackPlugin: () => ModuleFederationRuntimePlugin = }; case 'beforeRequest': - console.warn(`Request processing failed for ${id}`); + console.warn(\`Request processing failed for \${id}\`); return void 0; case 'onLoad': @@ -257,14 +318,14 @@ const fallbackPlugin: () => ModuleFederationRuntimePlugin = }); case 'beforeLoadShare': - console.warn(`Shared dependency loading failed for ${id}`); + console.warn(\`Shared dependency loading failed for \${id}\`); return () => ({ __esModule: true, default: {} }); default: - console.warn(`Unknown lifecycle ${lifecycle} for ${id}`); + console.warn(\`Unknown lifecycle \${lifecycle} for \${id}\`); return void 0; } }, @@ -288,6 +349,7 @@ mf.loadRemote('app1/un-existed-module').then(mod=>{ }); ``` + ## beforeLoadShare `AsyncWaterfallHook` @@ -307,6 +369,51 @@ type BeforeLoadShareOptions ={ } ``` +## afterLoadShare + +`SyncHook` + +Called after a shared dependency is successfully resolved by `loadShare` or `loadShareSync`. Use it to observe which provider and version were selected. + +* type + +```ts +function afterLoadShare(args: AfterLoadShareOptions): void + +type AfterLoadShareOptions ={ + pkgName: string; + shareInfo?: Partial; + selectedShared?: Partial; + shared: Options['shared']; + shareScopeMap: ShareScopeMap; + lifecycle: 'loadShare' | 'loadShareSync'; + origin: ModuleFederation; +} +``` + +## errorLoadShare + +`SyncHook` + +Called when shared dependency resolution fails or cannot select a valid shared dependency. It is useful for diagnostics around missing shared dependencies, version mismatch, and eager configuration errors. + +* type + +```ts +function errorLoadShare(args: ErrorLoadShareOptions): void + +type ErrorLoadShareOptions ={ + pkgName: string; + shareInfo?: Partial; + shared: Options['shared']; + shareScopeMap: ShareScopeMap; + lifecycle: 'loadShare' | 'loadShareSync'; + origin: ModuleFederation; + error?: unknown; + recovered?: boolean; +} +``` + ## initContainerShareScopeMap `SyncWaterfallHook` @@ -356,8 +463,9 @@ type ResolveShareOptions ={ * example + ```ts -import { createInstance, loadRemote } from '@module-federation/enhanced/runtime' +import { createInstance, loadRemote } from '@module-federation/enhanced/runtime'; import type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime'; @@ -410,6 +518,7 @@ mf.loadShare('react').then((reactFactory) => { }); ``` + ## beforePreloadRemote `AsyncHook` @@ -459,6 +568,131 @@ interface PreloadAssets { `loaderHook` is used to intercept resource loading and module-factory resolution. +## beforeInitRemote + +`AsyncHook` + +Called before initializing the remote container with `remoteEntry.init(...)`. + +* type + +```ts +async function beforeInitRemote(args: BeforeInitRemoteOptions): Promise + +type BeforeInitRemoteOptions ={ + id?: string; + remoteInfo: RemoteInfo; + remoteSnapshot?: ModuleInfo; + origin: ModuleFederation; +} +``` + +## afterInitRemote + +`AsyncHook` + +Called after remote container initialization succeeds or fails. When the remote was already initialized, `cached` is set to `true`. + +* type + +```ts +async function afterInitRemote(args: AfterInitRemoteOptions): Promise + +type AfterInitRemoteOptions ={ + id?: string; + remoteInfo: RemoteInfo; + remoteSnapshot?: ModuleInfo; + remoteEntryExports?: RemoteEntryExports; + error?: unknown; + cached?: boolean; + origin: ModuleFederation; +} +``` + +## beforeGetExpose + +`AsyncHook` + +Called before `remoteEntry.get(expose)`. + +* type + +```ts +async function beforeGetExpose(args: BeforeGetExposeOptions): Promise + +type BeforeGetExposeOptions ={ + id: string; + expose: string; + moduleInfo: RemoteInfo; + remoteEntryExports: RemoteEntryExports; + origin: ModuleFederation; +} +``` + +## afterGetExpose + +`AsyncHook` + +Called after `remoteEntry.get(expose)` succeeds or fails. + +* type + +```ts +async function afterGetExpose(args: AfterGetExposeOptions): Promise + +type AfterGetExposeOptions ={ + id: string; + expose: string; + moduleInfo: RemoteInfo; + remoteEntryExports: RemoteEntryExports; + moduleFactory?: () => unknown | Promise; + error?: unknown; + origin: ModuleFederation; +} +``` + +## beforeExecuteFactory + +`AsyncHook` + +Called before executing the exposed module factory. It is skipped when `loadRemote` is called with `loadFactory: false`. + +* type + +```ts +async function beforeExecuteFactory(args: BeforeExecuteFactoryOptions): Promise + +type BeforeExecuteFactoryOptions ={ + id: string; + expose: string; + moduleInfo: RemoteInfo; + loadFactory: boolean; + origin: ModuleFederation; +} +``` + +## afterExecuteFactory + +`AsyncHook` + +Called after the exposed module factory succeeds or fails. It is skipped when `loadRemote` is called with `loadFactory: false`. + +* type + +```ts +async function afterExecuteFactory(args: AfterExecuteFactoryOptions): Promise + +type AfterExecuteFactoryOptions ={ + id: string; + expose: string; + moduleInfo: RemoteInfo; + loadFactory: boolean; + exposeModule?: unknown; + error?: unknown; + origin: ModuleFederation; +} +``` + ## createScript `SyncHook` @@ -473,9 +707,29 @@ function createScript(args: CreateScriptOptions): CreateScriptHookReturn type CreateScriptOptions ={ url: string; attrs?: Record; + remoteInfo?: RemoteInfo; + resourceContext?: ResourceLoadContext; +} + +type CreateScriptHookReturn = + | HTMLScriptElement + | { script?: HTMLScriptElement; timeout?: number } + | void; + +type ResourceLoadContext = { + initiator: 'loadRemote' | 'preloadRemote'; + id: string; + resourceType: 'manifest' | 'remoteEntry' | 'js' | 'css'; + url?: string; } ``` +`timeout` is measured in milliseconds and controls the script loading timeout. The default value is `20000`. +`resourceContext` tells whether this resource load came from `loadRemote` or +`preloadRemote`, plus the resource type and id. +You can combine `timeout` and `resourceContext` to set different timeouts for +remoteEntry loads and preload loads. + * example ```ts @@ -491,13 +745,17 @@ const changeScriptAttributePlugin: () => ModuleFederationRuntimePlugin = script.src = url; script.setAttribute('loader-hooks', 'isTrue'); script.setAttribute('crossorigin', 'anonymous'); - return script; + return { + script, + timeout: 30000, + }; } } }; }; ``` + ## fetch The `fetch` function allows customizing the request that fetches the manifest JSON. A successful `Response` must yield a valid JSON. @@ -506,15 +764,24 @@ The `fetch` function allows customizing the request that fetches the manifest JS - **Type** ```typescript -function fetch(manifestUrl: string, requestInit: RequestInit): Promise | void | false; +function fetch( + manifestUrl: string, + requestInit: RequestInit, + remoteInfo?: RemoteInfo, + resourceContext?: ResourceLoadContext, +): Promise | void | false; ``` +`resourceContext` can tell whether this manifest request came from `loadRemote` +or `preloadRemote`. + - Example for including the credentials when fetching the manifest JSON: -```typescript -// fetch-manifest-with-credentials-plugin.ts + +```ts import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime'; +// fetch-manifest-with-credentials-plugin.ts export default function (): FederationRuntimePlugin { return { name: 'fetch-manifest-with-credentials-plugin', @@ -528,6 +795,7 @@ export default function (): FederationRuntimePlugin { }; ``` + ## createLink `SyncHook` @@ -542,7 +810,47 @@ function createLink(args: CreateLinkOptions): HTMLLinkElement | void type CreateLinkOptions ={ url: string; attrs?: Record; + remoteInfo?: RemoteInfo; + resourceContext?: ResourceLoadContext; } + +type CreateLinkHookReturn = + | HTMLLinkElement + | { link?: HTMLLinkElement; timeout?: number } + | void; +``` + +`resourceContext` has the same shape as in `createScript`. It can distinguish +preloaded JS/CSS, actual remoteEntry loading, and the related resource id. +`timeout` is measured in milliseconds and controls the link loading timeout. The +default value is `20000`. + +Example: use a shorter timeout for low-priority preload resources, while keeping +normal remote loading more tolerant. + +```ts +import type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime'; + +const resourceTimeoutPlugin = (): ModuleFederationRuntimePlugin => ({ + name: 'resource-timeout-plugin', + createScript({ resourceContext }) { + if ( + resourceContext?.initiator === 'loadRemote' && + resourceContext.resourceType === 'remoteEntry' + ) { + return { + timeout: 30000, + }; + } + }, + createLink({ resourceContext }) { + if (resourceContext?.initiator === 'preloadRemote') { + return { + timeout: 5000, + }; + } + }, +}); ``` ## loadEntryError @@ -566,6 +874,26 @@ type LoadEntryErrorOptions ={ } ``` +## afterLoadEntry + +`AsyncHook` + +Called after loading a remoteEntry succeeds, fails, or is recovered by `loadEntryError`. + +* type + +```ts +async function afterLoadEntry(args: AfterLoadEntryOptions): Promise + +type AfterLoadEntryOptions ={ + origin: ModuleFederation; + remoteInfo: RemoteInfo; + remoteEntryExports?: RemoteEntryExports | false | void; + error?: unknown; + recovered?: boolean; +} +``` + ## getModuleFactory `AsyncHook` @@ -620,11 +948,13 @@ export type RemoteEntryExports = { - Example Loading JSON Data -```typescript -// load-json-data-plugin.ts + +```ts import { init } from '@module-federation/enhanced/runtime'; + import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime'; +// load-json-data-plugin.ts const changeScriptAttributePlugin: () => FederationRuntimePlugin = function () { return { name: 'load-json-data-plugin', @@ -645,6 +975,7 @@ const changeScriptAttributePlugin: () => FederationRuntimePlugin = function () { }; }; ``` + ```ts // module-federation-config { @@ -661,11 +992,13 @@ jsonA // {...json data} - Example Delegate Modules -```typescript -// delegate-modules-plugin.ts + +```ts import { init } from '@module-federation/enhanced/runtime'; + import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime'; +// delegate-modules-plugin.ts const changeScriptAttributePlugin: () => FederationRuntimePlugin = function () { return { name: 'delegate-modules-plugin', @@ -685,6 +1018,7 @@ const changeScriptAttributePlugin: () => FederationRuntimePlugin = function () { }; }; ``` + ```ts // ./src/delegateModulesA.js export async function test1() { @@ -720,7 +1054,7 @@ test2 // "test2 value" ## bridgeHook -`bridgeHook` is defined in `runtime-core/src/core.ts` and is used to extend context across bridge render/destroy stages (for example, React/Vue bridge). +`bridgeHook` is used to extend context across bridge render/destroy stages (for example, React/Vue bridge). ## beforeBridgeRender diff --git a/apps/website-new/docs/en/guide/runtime/runtime-plugins.mdx b/apps/website-new/docs/en/guide/runtime/runtime-plugins.mdx index 5f8e28996b2..b0f21466726 100644 --- a/apps/website-new/docs/en/guide/runtime/runtime-plugins.mdx +++ b/apps/website-new/docs/en/guide/runtime/runtime-plugins.mdx @@ -17,7 +17,8 @@ If you only need to **register** a plugin path in build config, see [`runtimePlu A runtime plugin is a function that returns a `ModuleFederationRuntimePlugin`: -```ts title="my-runtime-plugin.ts" + +```ts import type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime'; export default function myRuntimePlugin(): ModuleFederationRuntimePlugin { @@ -27,6 +28,7 @@ export default function myRuntimePlugin(): ModuleFederationRuntimePlugin { } ``` + The function form matters because it lets you: - accept options @@ -69,8 +71,10 @@ export default { Use runtime registration when the plugin depends on user state, environment, feature flags, or data fetched after startup. -```ts title="bootstrap.ts" + +```ts import { createInstance } from '@module-federation/enhanced/runtime'; + import rewriteRemoteEntryPlugin from './plugins/rewrite-remote-entry'; const mf = createInstance({ @@ -91,17 +95,17 @@ mf.registerPlugins([ ]); ``` + ### Global runtime registration Use `registerGlobalPlugins(...)` when you want the plugin to be available to all future runtime instances, instead of only one current instance. This is best suited for shared instrumentation, cross-app policy, or host-wide defaults. -```ts title="bootstrap.ts" -import { - registerGlobalPlugins, - createInstance, -} from '@module-federation/enhanced/runtime'; + +```ts +import { registerGlobalPlugins, createInstance } from '@module-federation/enhanced/runtime'; + import runtimePlugin from './plugins/runtime-plugin'; registerGlobalPlugins([runtimePlugin()]); @@ -117,6 +121,7 @@ const mf = createInstance({ }); ``` + `registerGlobalPlugins(...)` deduplicates plugins by `name`. In practice, call it before creating or using runtime instances so the global plugins are incorporated consistently. ## Choose the right hook @@ -129,6 +134,8 @@ const mf = createInstance({ | Customize injected scripts and links | `createScript`, `createLink` | You need to add attributes like `crossorigin`, timeouts, or custom script/link elements. When available, `remoteInfo` is provided so you can apply per-remote policy. | | Align or rewrite share scopes before remote init | `beforeInitContainer`, `initContainerShareScopeMap` | You need to change which share scope a remote initializes against | | Override the shared winner | `resolveShare` | You want to force a different shared implementation than the runtime would normally pick | +| Observe remote load status | `afterMatchRemote`, `afterLoadRemote` | You need request tracing or final success/failure status for `loadRemote` | +| Observe shared dependency status | `afterLoadShare`, `errorLoadShare` | You need to diagnose missing shared dependencies, version mismatch, or eager configuration errors | | Recover from load failures | `errorLoadRemote` | You need fallback modules, offline behavior, or layered recovery | | Implement a new remote loading strategy | `loadEntry` | You want to fully customize how a remote entry is loaded or support a new remote type | @@ -143,7 +150,8 @@ This pattern is useful for: - registry-backed routing - domain normalization -```ts title="plugins/rewrite-remote-entry.ts" + +```ts import type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime'; interface RewriteRemoteEntryOptions { @@ -181,6 +189,7 @@ export default function rewriteRemoteEntryPlugin( } ``` + Why this hook: - `beforeRequest` is earlier; the remote is not resolved yet @@ -197,7 +206,8 @@ An `afterResolve` rewrite changes runtime loading behavior. It does not automati Use `fetch` when the manifest request itself needs custom behavior. -```ts title="plugins/fetch-manifest-with-credentials.ts" + +```ts import type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime'; export default function fetchManifestWithCredentials(): ModuleFederationRuntimePlugin { @@ -220,6 +230,7 @@ export default function fetchManifestWithCredentials(): ModuleFederationRuntimeP } ``` + Use `fetch` for: - credentials @@ -235,7 +246,8 @@ If you need a ready-made transport policy, see the built-in retry plugin. The important part: changing `args.scope` or `args.version` alone is not enough. To change the actual winner, replace `args.resolver`. -```ts title="plugins/prefer-host-react.ts" + +```ts import type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime'; export default function preferHostReact(): ModuleFederationRuntimePlugin { @@ -268,6 +280,7 @@ export default function preferHostReact(): ModuleFederationRuntimePlugin { } ``` + ## Failure handling and `shareStrategy` `errorLoadRemote` is the right place for runtime fallbacks. diff --git a/apps/website-new/docs/en/guide/start/_meta.json b/apps/website-new/docs/en/guide/start/_meta.json index 29772c43d6a..c38e165702b 100644 --- a/apps/website-new/docs/en/guide/start/_meta.json +++ b/apps/website-new/docs/en/guide/start/_meta.json @@ -1 +1 @@ -["index", "setting-up-env", "quick-start", "features", "glossary", "npm-packages"] +["index", "quick-start", "glossary"] diff --git a/apps/website-new/docs/en/guide/start/features.mdx b/apps/website-new/docs/en/guide/start/features.mdx deleted file mode 100644 index 1d85b16b2da..00000000000 --- a/apps/website-new/docs/en/guide/start/features.mdx +++ /dev/null @@ -1,17 +0,0 @@ -# Feature Navigation - -Here, you will discover the key functionalities offered through Module Federation. This version of Module Federation, distinct from those integrated into Webpack and Rspack, leverages the advanced features of the bundler tool. It offers enhanced capabilities designed to fulfill more demanding requirements of large-scale application development. - -## Basic - -| Feature | Description | -| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -| [Federation Runtime](../runtime/index) | Capable of operating independently of build plugins: registering remote modules, consuming remote modules, registering shared dependencies | -| [Build Plugins](../build-plugins/plugins) | MF offers a variety of build plugins to help consume and generate remote modules | -| [TypeScript Hints](../basic/type-prompt) | Supports dynamic module type hints and remote types | - -## Frameworks - -| Feature | Description | -| ------------------------------ | ------------------------------------------------------ | -| [Next.js](../framework/nextjs) | Provides Module Federation SSR capabilities to Next.js | diff --git a/apps/website-new/docs/en/guide/start/glossary.mdx b/apps/website-new/docs/en/guide/start/glossary.mdx index a213fe7a44d..8f9f7c5fdc7 100644 --- a/apps/website-new/docs/en/guide/start/glossary.mdx +++ b/apps/website-new/docs/en/guide/start/glossary.mdx @@ -27,20 +27,20 @@ It primarily addresses two issues: ## Bundler -Refers to module bundling tools such as [Rspack](https://rspack.dev/), [Webpack](https://webpack.js.org/). +Refers to module bundling tools such as [Webpack](https://webpack.js.org/), [Rspack](https://rspack.dev/), Vite, and Metro. The main goal of a bundler is to package JavaScript, CSS, and other files together. The packaged files can be used in browsers, Node.js, and other environments. When a Bundler processes a web application, it constructs a dependency graph that includes all the modules required by the application and then packages all modules into one or more bundles. -## Rspack +To choose the right tool and plugin for your project, see [Integrations](/integrations). -[Rspack](https://www.rspack.dev/) is a high-performance web construction tool based on Rust, with interoperability with the webpack ecosystem. It can be integrated into webpack projects at a low cost and offers better build performance. +## Manifest -Compared to webpack, Rspack has significantly improved build performance, thanks to the language advantages brought by Rust, as well as its parallel architecture and incremental compilation features. Benchmark tests have shown that Rspack can bring a 5 to 10 times increase in compilation performance. +Manifest is a runtime manifest generated by a producer during build. It describes the remote entry, exposed modules, assets, shared dependencies, and type information. A consumer can read `mf-manifest.json` and use that information to load remote modules. -## Rsbuild +For more details, see [Manifest and Snapshot](/guide/basic/manifest-snapshot), [manifest configuration](/configure/manifest), and [mf-manifest.json fields](/configure/manifest-fields). -[Rsbuild](https://rsbuild.dev/) is a web construction tool based on Rspack, with the following features: +## Snapshot -- Rsbuild is an enhanced version of the Rspack CLI, more user-friendly and ready out of the box. -- Rsbuild represents the Rspack team's exploration and implementation of best practices for web construction. -- Rsbuild is the best solution for migrating Webpack applications to Rspack, reducing configuration by 90% and speeding up builds by 10 times. +Snapshot is a condensed and pre-resolved result of Manifest information. The runtime can generate a Snapshot from Manifest. If you have a deployment service, it can also generate and deliver Snapshot ahead of time so the consumer can get the remote entry and preloadable assets directly. + +Snapshot is used by remote loading, asset preloading, DevTools, proxying, and runtime diagnostics. diff --git a/apps/website-new/docs/en/guide/start/index.mdx b/apps/website-new/docs/en/guide/start/index.mdx index cd590490066..ca970861cdf 100644 --- a/apps/website-new/docs/en/guide/start/index.mdx +++ b/apps/website-new/docs/en/guide/start/index.mdx @@ -23,7 +23,7 @@ Module Federation has the following features: - 🧩 [Runtime Plugins System](../../plugin/dev/index.mdx) - 🚀 [Dynamic type prompt](../basic/type-prompt.mdx) - 🛠️ [Chrome Devtool](../debug/chrome-devtool) -- 🦀 [Rspack](../build-plugins/plugins-rspack) and [Webpack](../build-plugins/plugins-webpack) Support +- 🦀 [Rspack](/integrations/bundler/rspack) and [Webpack](/integrations/bundler/webpack) Support ### 🎯 Use Cases @@ -72,9 +72,9 @@ import Step from '@components/Step'; description="Learn how to use Module Federation" /> = 16. **We recommend using the LTS version of Node.js 20**. - -You can check the current Node.js version in use with the following command: - -```bash -node -v -``` - -If you do not have Node.js installed in your current environment, or if the installed version is too low, you can install the required version through [nvm](https://github.com/nvm-sh/nvm) or [fnm](https://github.com/Schniz/fnm). - -Here is an example of installing the LTS version of Node.js 20 with nvm: - -```bash -# Install the long-term support version of Node.js 20 -nvm install 20 --lts - -# Set the newly installed Node.js 20 as the default version -nvm alias default 20 - -# Switch to the newly installed Node.js 20 -nvm use 20 -``` - -## Using Module Federation - -To use Module Federation, you need to follow these steps: - -- Identify shared modules: Determine which modules you want to share between applications. -- Create a shared package/repository: Add these modules to a shared package or code repository. -- Ensure access rights: Make sure each application can access the shared package or code repository. -- Configure build plugins: Configure the [Webpack](../build-plugins/plugins-webpack), [Rspack](../build-plugins/plugins-rspack) configuration files for each application to use Module Federation. -- Use shared modules: Use the shared modules in your applications as needed. - -For more information and advanced configuration options, please refer to the [Build Configuration](../../configure/index) documentation. diff --git a/apps/website-new/docs/en/guide/troubleshooting/runtime.mdx b/apps/website-new/docs/en/guide/troubleshooting/runtime.mdx index eec6b4ff9bd..68e02ac73b5 100644 --- a/apps/website-new/docs/en/guide/troubleshooting/runtime.mdx +++ b/apps/website-new/docs/en/guide/troubleshooting/runtime.mdx @@ -20,6 +20,9 @@ This page collects runtime-related error codes and common troubleshooting guidan - [RUNTIME-009](#runtime-009) - [RUNTIME-010](#runtime-010) - [RUNTIME-012](#runtime-012) +- [RUNTIME-013](#runtime-013) +- [RUNTIME-014](#runtime-014) +- [RUNTIME-015](#runtime-015) ## RUNTIME-001 @@ -352,6 +355,62 @@ When `loadShare` returns `false` — meaning the remote did not provide the shar Make sure the shared module name and version range are consistent between the host and all remotes, so a match can be found and the fallback is never reached with an empty `getter`. +## RUNTIME-013 + + + +### Reason + +The manifest can be reached and parsed as JSON, but it is not a valid Module Federation manifest. + +The most common case is that required fields such as `metaData`, `exposes`, or `shared` are missing. This usually means the URL returned the wrong JSON, a gateway returned incomplete data, or the producer did not emit a valid MF manifest. + +### Solution + +1. Open the manifestUrl from the error message directly and confirm that it returns an MF manifest, not another business JSON response or an empty object +2. Check that manifest output is enabled in the producer and that the build artifact contains `metaData`, `exposes`, and `shared` +3. Check whether a gateway, deployment platform, or proxy rewrote the manifest response +4. If the observability plugin is enabled, start from `diagnosis.facts.url` and `diagnosis.actions` + +## RUNTIME-014 + + + +### Reason + +The producer entry was loaded and initialized, but the producer did not export the requested expose. + +Common causes include: + +1. The expose name requested by the consumer is wrong +2. The producer build config does not declare this expose +3. The `./` prefix, letter case, or alias does not match +4. The consumer is using an old build artifact and the latest producer expose has not been deployed + +### Solution + +1. Compare the consumer `loadRemote('remote/expose')` request with the producer `exposes` config +2. Check whether the expose needs the `./` prefix, and verify that the case matches exactly +3. If loading through manifest, check whether the manifest `exposes` list contains the module +4. If the observability plugin is enabled, inspect `diagnosis.facts.expose` and the related `check-expose` action + +## RUNTIME-015 + + + +### Reason + +The producer entry was loaded, but container initialization failed. + +This usually happens while the producer runs `init`, for example because shared dependency initialization failed, shareScope data did not match expectations, or the producer entry threw an internal error. + +### Solution + +1. Read the original error in the message; it points to the actual failure thrown during `init` +2. Check shared config between the host and producer, especially shareScope, singleton, strictVersion, requiredVersion, and eager +3. Confirm that the producer remoteEntry type and global name match the consumer config +4. If the observability plugin is enabled, inspect `summary.phases.remoteEntry`, `diagnosis.facts`, and the `check-shared-provider` action + ## Common Issues (No Error Code) ### Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons: diff --git a/apps/website-new/docs/en/index.md b/apps/website-new/docs/en/index.md index 2b7ec0ea161..2d5fac3b338 100644 --- a/apps/website-new/docs/en/index.md +++ b/apps/website-new/docs/en/index.md @@ -11,7 +11,7 @@ hero: link: /blog/v2-stable-version.html - theme: alt text: Quick Start - link: /ai/index.html + link: /guide/start/quick-start.html image: src: /svg.svg alt: module federation Logo diff --git a/apps/website-new/docs/en/integrations/_meta.json b/apps/website-new/docs/en/integrations/_meta.json new file mode 100644 index 00000000000..723c6151cfb --- /dev/null +++ b/apps/website-new/docs/en/integrations/_meta.json @@ -0,0 +1,36 @@ +[ + { + "type": "file", + "name": "index", + "label": "Overview" + }, + { + "type": "dir-section-header", + "name": "build-tool", + "label": "Build Tool" + }, + { + "type": "dir-section-header", + "name": "bundler", + "label": "Bundler" + }, + { + "type": "dir-section-header", + "name": "documentation", + "label": "Documentation Solution" + }, + { + "type": "dir-section-header", + "name": "framework", + "label": "Framework", + "collapsible": true, + "collapsed": true + }, + { + "type": "dir-section-header", + "name": "practice", + "label": "Practice", + "collapsible": true, + "collapsed": true + } +] diff --git a/apps/website-new/docs/en/integrations/build-tool/_meta.json b/apps/website-new/docs/en/integrations/build-tool/_meta.json new file mode 100644 index 00000000000..e0da6374d13 --- /dev/null +++ b/apps/website-new/docs/en/integrations/build-tool/_meta.json @@ -0,0 +1,17 @@ +[ + { + "type": "file", + "name": "rsbuild", + "label": "Rsbuild" + }, + { + "type": "file", + "name": "rslib", + "label": "Rslib" + }, + { + "type": "file", + "name": "vite", + "label": "Vite" + } +] diff --git a/apps/website-new/docs/en/guide/build-plugins/plugins-rsbuild.mdx b/apps/website-new/docs/en/integrations/build-tool/rsbuild.mdx similarity index 76% rename from apps/website-new/docs/en/guide/build-plugins/plugins-rsbuild.mdx rename to apps/website-new/docs/en/integrations/build-tool/rsbuild.mdx index 42df4d6c6fc..2a49ccf76fb 100644 --- a/apps/website-new/docs/en/guide/build-plugins/plugins-rsbuild.mdx +++ b/apps/website-new/docs/en/integrations/build-tool/rsbuild.mdx @@ -1,6 +1,6 @@ # Rsbuild -Help users quickly build Module Federation products in **Rsbuild App** or **Rslib** +Help users quickly build Module Federation products in **Rsbuild App**. ## Quick Start @@ -43,44 +43,6 @@ export default defineConfig({ }); ``` -#### Rslib Module -``` ts title='rslib.config.ts' -import { pluginModuleFederation } from '@module-federation/rsbuild-plugin'; -import { defineConfig } from '@rslib/core'; - -export default defineConfig({ - lib: [ - // ... - { - format: 'mf', - output: { - distPath: { - root: './dist/mf', - }, - assetPrefix: 'xxx', - }, - plugins: [ - // ... - pluginModuleFederation({ - name: 'rslib_provider', - exposes: { - '.': './src/index.tsx', - }, - shared: { - react: { - singleton: true, - }, - 'react-dom': { - singleton: true, - }, - }, - }), - ], - }, - ], -}); -``` - ### Note If you need to use the Module Federation runtime capabilities, please install [@module-federation/enhanced](/en/guide/runtime/index.html) @@ -101,7 +63,7 @@ type RSBUILD_PLUGIN_OPTIONS = { ### moduleFederationOptions -[Module Federation Configuration](../../../configure/index) +[Module Federation Configuration](/configure/index) ### rsbuildOptions @@ -121,7 +83,7 @@ Additional configuration for the Rsbuild plugin. Used to specify the target runtime environment for the output. When set to `dual`, it builds both Web (browser) output and Node.js (SSR) output. -After generating SSR output with `target: 'dual'`, you can refer to [Create a Modern.js Consumer](../../../practice/frameworks/modern/index), create a consumer, and integrate the corresponding Rslib SSR producer for development. +After generating SSR output with `target: 'dual'`, you can refer to [Create a Modern.js Consumer](/integrations/framework/modernjs/quick-start), create a consumer, and integrate the corresponding Rslib SSR producer for development. For Rsbuild app SSR, use `target: 'node'` with `environment` to apply Module Federation to a specific app environment. diff --git a/apps/website-new/docs/en/integrations/build-tool/rslib.mdx b/apps/website-new/docs/en/integrations/build-tool/rslib.mdx new file mode 100644 index 00000000000..e561acb17af --- /dev/null +++ b/apps/website-new/docs/en/integrations/build-tool/rslib.mdx @@ -0,0 +1,75 @@ +# Rslib + +Rslib can use `@module-federation/rsbuild-plugin` to build Module Federation producers. It is a good fit when a component library, business module, or SSR producer needs to be published as an independent artifact for other apps to consume. + +## Quick Start + +### Installation + +import { PackageManagerTabs } from '@theme'; + + + +### Register Plugin + +```ts title='rslib.config.ts' +import { pluginModuleFederation } from '@module-federation/rsbuild-plugin'; +import { defineConfig } from '@rslib/core'; + +export default defineConfig({ + lib: [ + { + format: 'mf', + output: { + distPath: { + root: './dist/mf', + }, + assetPrefix: 'https://example.com/mf/', + }, + plugins: [ + pluginModuleFederation({ + name: 'rslib_provider', + exposes: { + '.': './src/index.tsx', + }, + shared: { + react: { + singleton: true, + }, + 'react-dom': { + singleton: true, + }, + }, + }), + ], + }, + ], +}); +``` + +## SSR + +If you need to build both browser and Node.js artifacts, use `target: 'dual'` in the plugin options. + +```ts title='rslib.config.ts' +pluginModuleFederation( + { + name: 'rslib_provider', + exposes: { + '.': './src/index.tsx', + }, + }, + { + target: 'dual', + }, +); +``` + +For all plugin options, continue with [Rsbuild plugin configuration](/integrations/build-tool/rsbuild#rsbuildoptions). diff --git a/apps/website-new/docs/en/guide/build-plugins/plugins-vite.mdx b/apps/website-new/docs/en/integrations/build-tool/vite.mdx similarity index 94% rename from apps/website-new/docs/en/guide/build-plugins/plugins-vite.mdx rename to apps/website-new/docs/en/integrations/build-tool/vite.mdx index 32061114633..388fc8551ed 100644 --- a/apps/website-new/docs/en/guide/build-plugins/plugins-vite.mdx +++ b/apps/website-new/docs/en/integrations/build-tool/vite.mdx @@ -6,7 +6,7 @@ - When a module has remote types, it automatically downloads and consumes the remote types. :::warning Unsupported Options -Except for the [dev](../../../configure/dev) option, all options are supported. +Except for the [dev](/configure/dev) option, all options are supported. ::: - roadmap 🗓️ - Consuming remote modules will have hot update capabilities. @@ -89,4 +89,4 @@ type ModuleFederationOptions = { }; ``` -You can find detailed explanations of all configuration items on the [Configuration Overview](../../../configure/index) page. +You can find detailed explanations of all configuration items on the [Configuration Overview](/configure/index) page. diff --git a/apps/website-new/docs/en/integrations/bundler/_meta.json b/apps/website-new/docs/en/integrations/bundler/_meta.json new file mode 100644 index 00000000000..90657d07f06 --- /dev/null +++ b/apps/website-new/docs/en/integrations/bundler/_meta.json @@ -0,0 +1,5 @@ +[ + "rspack", + "webpack", + "metro" +] diff --git a/apps/website-new/docs/en/guide/build-plugins/plugins-metro.mdx b/apps/website-new/docs/en/integrations/bundler/metro.mdx similarity index 100% rename from apps/website-new/docs/en/guide/build-plugins/plugins-metro.mdx rename to apps/website-new/docs/en/integrations/bundler/metro.mdx diff --git a/apps/website-new/docs/en/guide/build-plugins/plugins-rspack.mdx b/apps/website-new/docs/en/integrations/bundler/rspack.mdx similarity index 96% rename from apps/website-new/docs/en/guide/build-plugins/plugins-rspack.mdx rename to apps/website-new/docs/en/integrations/bundler/rspack.mdx index ad9eb3049fa..9fb00596c2d 100644 --- a/apps/website-new/docs/en/guide/build-plugins/plugins-rspack.mdx +++ b/apps/website-new/docs/en/integrations/bundler/rspack.mdx @@ -43,4 +43,4 @@ import RegisterPlugin from '@components/common/rspack/register-plugin'; ## Configuration -You can find detailed descriptions of all configuration items on the [Config Overview](../../../configure/index) page. +You can find detailed descriptions of all configuration items on the [Config Overview](/configure/index) page. diff --git a/apps/website-new/docs/en/guide/build-plugins/plugins-webpack.mdx b/apps/website-new/docs/en/integrations/bundler/webpack.mdx similarity index 96% rename from apps/website-new/docs/en/guide/build-plugins/plugins-webpack.mdx rename to apps/website-new/docs/en/integrations/bundler/webpack.mdx index f27b1a47710..89f235ef951 100644 --- a/apps/website-new/docs/en/guide/build-plugins/plugins-webpack.mdx +++ b/apps/website-new/docs/en/integrations/bundler/webpack.mdx @@ -40,4 +40,4 @@ import RegisterPlugin from '@components/common/webpack/register-plugin'; ## Configuration -You can find detailed descriptions of all configuration items on the [Config Overview](../../../configure/index) page. +You can find detailed descriptions of all configuration items on the [Config Overview](/configure/index) page. diff --git a/apps/website-new/docs/en/integrations/documentation/_meta.json b/apps/website-new/docs/en/integrations/documentation/_meta.json new file mode 100644 index 00000000000..049e12afcab --- /dev/null +++ b/apps/website-new/docs/en/integrations/documentation/_meta.json @@ -0,0 +1,7 @@ +[ + { + "type": "file", + "name": "rspress", + "label": "Rspress" + } +] diff --git a/apps/website-new/docs/en/guide/build-plugins/plugins-rspress.mdx b/apps/website-new/docs/en/integrations/documentation/rspress.mdx similarity index 99% rename from apps/website-new/docs/en/guide/build-plugins/plugins-rspress.mdx rename to apps/website-new/docs/en/integrations/documentation/rspress.mdx index b18b1166bfb..2baab40880d 100644 --- a/apps/website-new/docs/en/guide/build-plugins/plugins-rspress.mdx +++ b/apps/website-new/docs/en/integrations/documentation/rspress.mdx @@ -64,7 +64,7 @@ import ConfigType from '@components/common/rspress/config-type'; ### {props.pluginOptionName || 'moduleFederationOptions'} -[{props.brandName || props.name || 'Module Federation'} Configuration](../../../configure/index) +[{props.brandName || props.name || 'Module Federation'} Configuration](/configure/index) ### rspressOptions diff --git a/apps/website-new/docs/zh/practice/frameworks/_meta.json b/apps/website-new/docs/en/integrations/framework/_meta.json similarity index 60% rename from apps/website-new/docs/zh/practice/frameworks/_meta.json rename to apps/website-new/docs/en/integrations/framework/_meta.json index 57b96785840..b722ce651e5 100644 --- a/apps/website-new/docs/zh/practice/frameworks/_meta.json +++ b/apps/website-new/docs/en/integrations/framework/_meta.json @@ -1,31 +1,30 @@ [ - { - "type": "file", - "name": "overview", - "label": "框架概览" - }, - { - "type": "dir", - "name": "react", - "label": "React", - "collapsed": true - }, { "type": "dir", - "name": "modern", + "name": "modernjs", "label": "Modern.js", + "collapsible": true, "collapsed": true }, { "type": "dir", - "name": "next", + "name": "nextjs", "label": "Next.js", + "collapsible": true, "collapsed": true }, { "type": "dir", "name": "angular", "label": "Angular", + "collapsible": true, + "collapsed": true + }, + { + "type": "dir", + "name": "monorepos", + "label": "Monorepos", + "collapsible": true, "collapsed": true } ] diff --git a/apps/website-new/docs/en/practice/frameworks/angular/_meta.json b/apps/website-new/docs/en/integrations/framework/angular/_meta.json similarity index 71% rename from apps/website-new/docs/en/practice/frameworks/angular/_meta.json rename to apps/website-new/docs/en/integrations/framework/angular/_meta.json index aeb52f448a8..45e787a240c 100644 --- a/apps/website-new/docs/en/practice/frameworks/angular/_meta.json +++ b/apps/website-new/docs/en/integrations/framework/angular/_meta.json @@ -1,4 +1,9 @@ [ + { + "type": "file", + "name": "index", + "label": "Overview" + }, "angular-cli", "using-nx-for-angular", "angular-mfe", diff --git a/apps/website-new/docs/en/practice/frameworks/angular/angular-cli.mdx b/apps/website-new/docs/en/integrations/framework/angular/angular-cli.mdx similarity index 100% rename from apps/website-new/docs/en/practice/frameworks/angular/angular-cli.mdx rename to apps/website-new/docs/en/integrations/framework/angular/angular-cli.mdx diff --git a/apps/website-new/docs/en/practice/frameworks/angular/angular-mfe.mdx b/apps/website-new/docs/en/integrations/framework/angular/angular-mfe.mdx similarity index 100% rename from apps/website-new/docs/en/practice/frameworks/angular/angular-mfe.mdx rename to apps/website-new/docs/en/integrations/framework/angular/angular-mfe.mdx diff --git a/apps/website-new/docs/en/practice/frameworks/angular/auth0.mdx b/apps/website-new/docs/en/integrations/framework/angular/auth0.mdx similarity index 100% rename from apps/website-new/docs/en/practice/frameworks/angular/auth0.mdx rename to apps/website-new/docs/en/integrations/framework/angular/auth0.mdx diff --git a/apps/website-new/docs/en/integrations/framework/angular/index.mdx b/apps/website-new/docs/en/integrations/framework/angular/index.mdx new file mode 100644 index 00000000000..33ea451ce46 --- /dev/null +++ b/apps/website-new/docs/en/integrations/framework/angular/index.mdx @@ -0,0 +1,16 @@ +# Angular Integration Overview + +When integrating Module Federation into an Angular app, choose the guide based on the build setup used by your project. + +| Project type | Read next | +| --- | --- | +| Angular CLI project | [Angular CLI Setup](./angular-cli) | +| Nx workspace | [Using Nx CLI for Angular](./using-nx-for-angular) | +| Full micro-frontend example | [Angular MFE](./angular-mfe) | +| SSR scenario | [Angular SSR](./mf-ssr-angular) | +| Service Worker scenario | [Service Workers with Module Federation](./service-workers-mf) | + +Angular integrations commonly depend on Angular CLI, a custom Webpack builder, or Nx. Before choosing a guide, confirm your current build setup and whether SSR is required. + +If you only need the shared Module Federation configuration concepts, see [Configuration](/configure/) for `name`, `remotes`, `exposes`, and `shared`. + diff --git a/apps/website-new/docs/en/practice/frameworks/angular/mf-ssr-angular.mdx b/apps/website-new/docs/en/integrations/framework/angular/mf-ssr-angular.mdx similarity index 100% rename from apps/website-new/docs/en/practice/frameworks/angular/mf-ssr-angular.mdx rename to apps/website-new/docs/en/integrations/framework/angular/mf-ssr-angular.mdx diff --git a/apps/website-new/docs/en/practice/frameworks/angular/okta-auth.mdx b/apps/website-new/docs/en/integrations/framework/angular/okta-auth.mdx similarity index 100% rename from apps/website-new/docs/en/practice/frameworks/angular/okta-auth.mdx rename to apps/website-new/docs/en/integrations/framework/angular/okta-auth.mdx diff --git a/apps/website-new/docs/en/practice/frameworks/angular/service-workers-mf.mdx b/apps/website-new/docs/en/integrations/framework/angular/service-workers-mf.mdx similarity index 100% rename from apps/website-new/docs/en/practice/frameworks/angular/service-workers-mf.mdx rename to apps/website-new/docs/en/integrations/framework/angular/service-workers-mf.mdx diff --git a/apps/website-new/docs/en/practice/frameworks/angular/splitting-to-mf-part1.mdx b/apps/website-new/docs/en/integrations/framework/angular/splitting-to-mf-part1.mdx similarity index 100% rename from apps/website-new/docs/en/practice/frameworks/angular/splitting-to-mf-part1.mdx rename to apps/website-new/docs/en/integrations/framework/angular/splitting-to-mf-part1.mdx diff --git a/apps/website-new/docs/en/practice/frameworks/angular/splitting-to-mf-part2.mdx b/apps/website-new/docs/en/integrations/framework/angular/splitting-to-mf-part2.mdx similarity index 100% rename from apps/website-new/docs/en/practice/frameworks/angular/splitting-to-mf-part2.mdx rename to apps/website-new/docs/en/integrations/framework/angular/splitting-to-mf-part2.mdx diff --git a/apps/website-new/docs/en/practice/frameworks/angular/using-nx-for-angular.mdx b/apps/website-new/docs/en/integrations/framework/angular/using-nx-for-angular.mdx similarity index 100% rename from apps/website-new/docs/en/practice/frameworks/angular/using-nx-for-angular.mdx rename to apps/website-new/docs/en/integrations/framework/angular/using-nx-for-angular.mdx diff --git a/apps/website-new/docs/en/integrations/framework/modernjs/_meta.json b/apps/website-new/docs/en/integrations/framework/modernjs/_meta.json new file mode 100644 index 00000000000..754dff65201 --- /dev/null +++ b/apps/website-new/docs/en/integrations/framework/modernjs/_meta.json @@ -0,0 +1,9 @@ +[ + { + "type": "file", + "name": "index", + "label": "Overview" + }, + "quick-start", + "dynamic-remote" +] diff --git a/apps/website-new/docs/en/practice/frameworks/modern/dynamic-remote.mdx b/apps/website-new/docs/en/integrations/framework/modernjs/dynamic-remote.mdx similarity index 94% rename from apps/website-new/docs/en/practice/frameworks/modern/dynamic-remote.mdx rename to apps/website-new/docs/en/integrations/framework/modernjs/dynamic-remote.mdx index 57fe96e0bb7..cfbe700cc27 100644 --- a/apps/website-new/docs/en/practice/frameworks/modern/dynamic-remote.mdx +++ b/apps/website-new/docs/en/integrations/framework/modernjs/dynamic-remote.mdx @@ -146,15 +146,17 @@ Consume loader data and dynamically load the corresponding producer: ```tsx import { loadRemote, registerRemotes, getInstance } from '@module-federation/modern-js-v3/runtime'; -import { createLazyComponent } from '@module-federation/modern-js-v3/react'; +import { lazyLoadComponentPlugin } from '@module-federation/modern-js-v3/data-fetch'; // Use import type to get data loader types import type { DataLoaderRes } from './page.data'; import { useLoaderData } from '@modern-js/runtime/router'; import './index.css'; -const RemoteSSRComponent = createLazyComponent({ - instance: getInstance(), +const instance = getInstance(); +instance.registerPlugins([lazyLoadComponentPlugin()]); + +const RemoteSSRComponent = instance.createLazyComponent({ loader: () => import('remote/Image'), loading: 'loading...', export: 'default', @@ -174,8 +176,7 @@ const Index = () => { const DynamicRemoteSSRComponents = dataLoader.providerList.map(item => { const { id } = item; - const Com = createLazyComponent({ - instance: getInstance(), + const Com = instance.createLazyComponent({ loader: () => loadRemote(id), loading: 'loading...', fallback: ({ error }) => { diff --git a/apps/website-new/docs/en/guide/framework/modernjs.mdx b/apps/website-new/docs/en/integrations/framework/modernjs/index.mdx similarity index 78% rename from apps/website-new/docs/en/guide/framework/modernjs.mdx rename to apps/website-new/docs/en/integrations/framework/modernjs/index.mdx index dffaa2b4fa9..be036e28dc4 100644 --- a/apps/website-new/docs/en/guide/framework/modernjs.mdx +++ b/apps/website-new/docs/en/integrations/framework/modernjs/index.mdx @@ -1,14 +1,17 @@ -# Modern.js +# Modern.js Integration Overview import { Badge } from '@theme'; [Modern.js](https://modernjs.dev/guides/get-started/introduction.html) is a progressive web development framework based on React. Internally at ByteDance, Modern.js supports the development of thousands of web applications. -The Module Federation team works closely with the Modern.js team and provides the `@module-federation/modern-js-v3` plugin to help users better utilize Module Federation within Modern.js. +The Module Federation team works closely with the Modern.js team and provides both `@module-federation/modern-js-v3` and `@module-federation/modern-js` to help users use Module Federation in Modern.js. + +We recommend upgrading to Modern.js v3 and using `@module-federation/modern-js-v3` first. ## Supports -- modern.js ^2.56.1 +- Modern.js v3: use `@module-federation/modern-js-v3` (recommended) +- Modern.js v2.56.1 and later: use `@module-federation/modern-js` - Includes Server-Side Rendering (SSR) We highly recommend referencing these applications, which showcases the best practices for integrating Modern.js with Module Federation: @@ -20,7 +23,7 @@ We highly recommend referencing these applications, which showcases the best pra ### Installation -You can install the plugin using the following commands: +For Modern.js v3, install `@module-federation/modern-js-v3`: import { PackageManagerTabs } from '@theme'; @@ -33,8 +36,21 @@ import { PackageManagerTabs } from '@theme'; }} /> +If you are still using Modern.js v2, install `@module-federation/modern-js`: + + + ### Apply Plugin +The following example uses Modern.js v3. If you are still using Modern.js v2, replace `@module-federation/modern-js-v3` with `@module-federation/modern-js`. + Apply this plugin in the `plugins` section of `modern.config.ts`: ```ts title="modern.config.ts" @@ -73,9 +89,9 @@ There is no difference in using Module Federation in SSR scenarios compared to C ## Component-Level Data Fetch -See [Data Fetching](../basic/data-fetch). +See [Data Fetching](/guide/data/data-fetch). -The Modern.js plugin re-exports `@module-federation/bridge-react` from `@module-federation/modern-js-v3/react`, so you don't need to install it separately. +The Modern.js plugin provides `lazyLoadComponentPlugin` from the `@module-federation/modern-js-v3/data-fetch` subpath, so you don't need to install `@module-federation/bridge-react` separately. ## API @@ -85,7 +101,7 @@ The Modern.js plugin re-exports `@module-federation/bridge-react` from `@module- ### createRemoteComponent Deprecated ::: danger -This API has been deprecated. Please use [createLazyComponent](/practice/bridge/react-bridge/load-component.html#what-is-createlazycomponent) instead. +This API has been deprecated. Please use [createLazyComponent](/guide/bridge/react/load-component.html#what-is-createlazycomponent) instead. ::: #### Migration Guide @@ -95,7 +111,7 @@ The parameters for `createRemoteComponent` and `createLazyComponent` are identic ```diff - import { createRemoteComponent } from '@module-federation/modern-js-v3/runtime'; + import { getInstance } from '@module-federation/modern-js-v3/runtime'; -+ import { lazyLoadComponentPlugin } from '@module-federation/modern-js-v3/react'; ++ import { lazyLoadComponentPlugin } from '@module-federation/modern-js-v3/data-fetch'; const instance = getInstance(); // After registering the lazyLoadComponentPlugin, the instance will automatically add the createLazyComponent API @@ -122,7 +138,7 @@ export default App; ### createRemoteSSRComponent Deprecated ::: danger -This API has been deprecated. Please use [createLazyComponent](/practice/bridge/react-bridge/load-component.html#createlazycomponent-vs-createremoteappcomponent) instead. +This API has been deprecated. Please use [createLazyComponent](/guide/bridge/react/load-component.html#createlazycomponent-vs-createremoteappcomponent) instead. ::: #### Migration Guide diff --git a/apps/website-new/docs/en/practice/frameworks/modern/index.mdx b/apps/website-new/docs/en/integrations/framework/modernjs/quick-start.mdx similarity index 95% rename from apps/website-new/docs/en/practice/frameworks/modern/index.mdx rename to apps/website-new/docs/en/integrations/framework/modernjs/quick-start.mdx index b90f820b6a0..97a0b65d334 100644 --- a/apps/website-new/docs/en/practice/frameworks/modern/index.mdx +++ b/apps/website-new/docs/en/integrations/framework/modernjs/quick-start.mdx @@ -246,15 +246,17 @@ Start the project and visit `http://localhost:3007/` and find that SSR is workin This is because the producer's style files cannot be injected into the corresponding html. -This issue can be solved by using the [createremotessrcomponent](../../../guide/framework/modernjs#createremotessrcomponent) provided by `@module-federation/modern-js-v3`. +This issue can be solved by using the [createremotessrcomponent](/integrations/framework/modernjs#createremotessrcomponent) provided by `@module-federation/modern-js-v3`. ```tsx title='page.tsx' import { getInstance } from '@module-federation/modern-js-v3/runtime'; -import { createLazyComponent } from '@module-federation/modern-js-v3/react' +import { lazyLoadComponentPlugin } from '@module-federation/modern-js-v3/data-fetch'; import './index.css'; -const RemoteSSRComponent = createLazyComponent({ - instance: getInstance(), +const instance = getInstance(); +instance.registerPlugins([lazyLoadComponentPlugin()]); + +const RemoteSSRComponent = instance.createLazyComponent({ loader: () => import('remote/Image'), loading: 'loading...', export: 'default', diff --git a/apps/website-new/docs/en/integrations/framework/monorepos/_meta.json b/apps/website-new/docs/en/integrations/framework/monorepos/_meta.json new file mode 100644 index 00000000000..2d8c290cbf3 --- /dev/null +++ b/apps/website-new/docs/en/integrations/framework/monorepos/_meta.json @@ -0,0 +1 @@ +["index", "nx-for-module-federation"] diff --git a/apps/website-new/docs/en/practice/monorepos/index.mdx b/apps/website-new/docs/en/integrations/framework/monorepos/index.mdx similarity index 100% rename from apps/website-new/docs/en/practice/monorepos/index.mdx rename to apps/website-new/docs/en/integrations/framework/monorepos/index.mdx diff --git a/apps/website-new/docs/en/practice/monorepos/nx-for-module-federation.mdx b/apps/website-new/docs/en/integrations/framework/monorepos/nx-for-module-federation.mdx similarity index 96% rename from apps/website-new/docs/en/practice/monorepos/nx-for-module-federation.mdx rename to apps/website-new/docs/en/integrations/framework/monorepos/nx-for-module-federation.mdx index 5dde952209f..86d26309ff3 100644 --- a/apps/website-new/docs/en/practice/monorepos/nx-for-module-federation.mdx +++ b/apps/website-new/docs/en/integrations/framework/monorepos/nx-for-module-federation.mdx @@ -35,7 +35,7 @@ nx g @nx/angular:consumer shell --producers=products,cart,checkout nx g @nx/react:consumer shell --producers=products,cart,checkout ``` -You can learn more about setting up Module Federation with Nx in the guides for [Angular](/practice/frameworks/angular/using-nx-for-angular) and [React](/practice/frameworks/react/using-nx-for-react). +You can learn more about setting up Module Federation with Nx in the guides for [Angular](/integrations/framework/angular/using-nx-for-angular) and [React](/integrations/practice/react/using-nx-for-react). ### Executors @@ -98,5 +98,5 @@ Learn more about [Faster Builds](https://nx.dev/concepts/module-federation/faste - [Nx](https://nx.dev) - [Nx Module Federation](https://nx.dev/concepts/module-federation) -- [[GUIDE]: Using Nx CLI for React](../frameworks/react/using-nx-for-react) -- [[GUIDE]: Using Nx CLI for Angular](../frameworks/angular/using-nx-for-angular) +- [[GUIDE]: Using Nx CLI for React](/integrations/practice/react/using-nx-for-react) +- [[GUIDE]: Using Nx CLI for Angular](/integrations/framework/angular/using-nx-for-angular) diff --git a/apps/website-new/docs/en/integrations/framework/nextjs/_meta.json b/apps/website-new/docs/en/integrations/framework/nextjs/_meta.json new file mode 100644 index 00000000000..16c703c7710 --- /dev/null +++ b/apps/website-new/docs/en/integrations/framework/nextjs/_meta.json @@ -0,0 +1,13 @@ +[ + { + "type": "file", + "name": "index", + "label": "Overview" + }, + "basic-example", + "dynamic-remotes", + "importing-components", + "importing-pages", + "express", + "presets" +] diff --git a/apps/website-new/docs/en/practice/frameworks/next/index.mdx b/apps/website-new/docs/en/integrations/framework/nextjs/basic-example.mdx similarity index 100% rename from apps/website-new/docs/en/practice/frameworks/next/index.mdx rename to apps/website-new/docs/en/integrations/framework/nextjs/basic-example.mdx diff --git a/apps/website-new/docs/en/practice/frameworks/next/dynamic-remotes.mdx b/apps/website-new/docs/en/integrations/framework/nextjs/dynamic-remotes.mdx similarity index 100% rename from apps/website-new/docs/en/practice/frameworks/next/dynamic-remotes.mdx rename to apps/website-new/docs/en/integrations/framework/nextjs/dynamic-remotes.mdx diff --git a/apps/website-new/docs/en/practice/frameworks/next/express.mdx b/apps/website-new/docs/en/integrations/framework/nextjs/express.mdx similarity index 95% rename from apps/website-new/docs/en/practice/frameworks/next/express.mdx rename to apps/website-new/docs/en/integrations/framework/nextjs/express.mdx index 293585394a7..0affbe38eb8 100644 --- a/apps/website-new/docs/en/practice/frameworks/next/express.mdx +++ b/apps/website-new/docs/en/integrations/framework/nextjs/express.mdx @@ -2,7 +2,7 @@ import {Steps} from '@theme' # Working with Express.js -If using express, hot server hot module reloading may not work after [these steps](/practice/frameworks/next/index#step-3-implementing-ssr) +If using express, hot server hot module reloading may not work after [these steps](/integrations/framework/nextjs/basic-example#step-3-implementing-ssr) Express has its own route stack, so reloading require cache will not be enough to reload the routes inside express. diff --git a/apps/website-new/docs/en/practice/frameworks/next/importing-components.mdx b/apps/website-new/docs/en/integrations/framework/nextjs/importing-components.mdx similarity index 100% rename from apps/website-new/docs/en/practice/frameworks/next/importing-components.mdx rename to apps/website-new/docs/en/integrations/framework/nextjs/importing-components.mdx diff --git a/apps/website-new/docs/en/practice/frameworks/next/importing-pages.mdx b/apps/website-new/docs/en/integrations/framework/nextjs/importing-pages.mdx similarity index 100% rename from apps/website-new/docs/en/practice/frameworks/next/importing-pages.mdx rename to apps/website-new/docs/en/integrations/framework/nextjs/importing-pages.mdx diff --git a/apps/website-new/docs/en/guide/framework/nextjs.mdx b/apps/website-new/docs/en/integrations/framework/nextjs/index.mdx similarity index 99% rename from apps/website-new/docs/en/guide/framework/nextjs.mdx rename to apps/website-new/docs/en/integrations/framework/nextjs/index.mdx index dccc1ac9afe..848b93b6e03 100644 --- a/apps/website-new/docs/en/guide/framework/nextjs.mdx +++ b/apps/website-new/docs/en/integrations/framework/nextjs/index.mdx @@ -2,7 +2,7 @@ import { Badge } from '@theme';
-# Next.js +# Next.js Integration Overview :::danger Project Deprecation Support for Next.js is ending [read more](https://github.com/module-federation/core/issues/3153) diff --git a/apps/website-new/docs/en/practice/frameworks/next/presets.mdx b/apps/website-new/docs/en/integrations/framework/nextjs/presets.mdx similarity index 100% rename from apps/website-new/docs/en/practice/frameworks/next/presets.mdx rename to apps/website-new/docs/en/integrations/framework/nextjs/presets.mdx diff --git a/apps/website-new/docs/en/integrations/index.mdx b/apps/website-new/docs/en/integrations/index.mdx new file mode 100644 index 00000000000..0ff928d1d47 --- /dev/null +++ b/apps/website-new/docs/en/integrations/index.mdx @@ -0,0 +1,27 @@ +# Integrations + +Module Federation is powered by MF Runtime. Most projects should use a build plugin. It lets you `import` remote modules like npm packages and provides engineering capabilities such as type hints. If your project needs to expose modules for other applications to consume, you must use a build plugin. If you do not want to change your build setup and only need to load remote modules at runtime, you can use Runtime directly. + +If you are not sure which package to install, start with your project type: + +| Your project | Package to install | Read next | +| --- | --- | --- | +| Rsbuild app | `@module-federation/rsbuild-plugin` | [Rsbuild](/integrations/build-tool/rsbuild) | +| Rslib module | `@module-federation/rsbuild-plugin` | [Rslib](/integrations/build-tool/rslib) | +| Vite app | `@module-federation/vite` | [Vite](/integrations/build-tool/vite) | +| Rspack app | `@module-federation/enhanced` | [Rspack](/integrations/bundler/rspack) | +| Webpack app | `@module-federation/enhanced` | [Webpack](/integrations/bundler/webpack) | +| React Native / Metro | `@module-federation/metro` and the matching Metro plugin | [Metro](/integrations/bundler/metro) | +| Rspress site | `@module-federation/rspress-plugin` | [Rspress](/integrations/documentation/rspress) | +| Modern.js app | `@module-federation/modern-js-v3` (recommended for Modern.js v3) or `@module-federation/modern-js` (Modern.js v2) | [Modern.js](/integrations/framework/modernjs) | +| Next.js app | `@module-federation/nextjs-mf` and `webpack` | [Next.js](/integrations/framework/nextjs) | +| Angular app | Choose the integration package for your Angular build setup | [Angular](/integrations/framework/angular) | +| Monorepos | Use the package for your app framework and monorepo tooling | [Monorepos](/integrations/framework/monorepos) | + +You can also use Runtime only to load remote modules. Different project types import Runtime from different entries. Continue with [Runtime Installation](/guide/runtime/#installation) to learn how to choose the correct Runtime entry. + +## Next + +- Choose the integration that matches your project first, then complete package installation and plugin setup. +- If you use React, Vue, or another framework-specific scenario, continue with [Practice](/integrations/practice/). +- Shared Module Federation capabilities are still introduced in the [Guide](/guide/start/index). Use the matching integration page for package installation and plugin setup. diff --git a/apps/website-new/docs/en/integrations/practice/_meta.json b/apps/website-new/docs/en/integrations/practice/_meta.json new file mode 100644 index 00000000000..5fa66babdc5 --- /dev/null +++ b/apps/website-new/docs/en/integrations/practice/_meta.json @@ -0,0 +1,19 @@ +[ + { + "type": "file", + "name": "index", + "label": "Overview" + }, + { + "type": "dir", + "name": "react", + "label": "React", + "collapsible": true, + "collapsed": true + }, + { + "type": "file", + "name": "vue", + "label": "Vue" + } +] diff --git a/apps/website-new/docs/en/integrations/practice/index.mdx b/apps/website-new/docs/en/integrations/practice/index.mdx new file mode 100644 index 00000000000..856ddad6559 --- /dev/null +++ b/apps/website-new/docs/en/integrations/practice/index.mdx @@ -0,0 +1,14 @@ +# Practice + +This section collects framework and scenario-specific practice guides. Choose the integration for your build tool, framework, or Runtime usage in the [Integrations overview](/integrations/) first, then come back here for matching practice guides. + +## React + +- [React practice overview](./react) +- [Rsbuild CRA](./react/rsbuild-cra) +- [React i18n](./react/i18n-react) +- [Using Nx CLI for React](./react/using-nx-for-react) + +## Vue + +- [Vue Bridge](./vue) diff --git a/apps/website-new/docs/en/integrations/practice/react/_meta.json b/apps/website-new/docs/en/integrations/practice/react/_meta.json new file mode 100644 index 00000000000..1ed2bbb92b9 --- /dev/null +++ b/apps/website-new/docs/en/integrations/practice/react/_meta.json @@ -0,0 +1,14 @@ +[ + { + "type": "file", + "name": "index", + "label": "Overview" + }, + { + "type": "file", + "name": "rsbuild-cra", + "label": "Rsbuild CRA" + }, + "i18n-react", + "using-nx-for-react" +] diff --git a/apps/website-new/docs/en/practice/frameworks/react/i18n-react.mdx b/apps/website-new/docs/en/integrations/practice/react/i18n-react.mdx similarity index 100% rename from apps/website-new/docs/en/practice/frameworks/react/i18n-react.mdx rename to apps/website-new/docs/en/integrations/practice/react/i18n-react.mdx diff --git a/apps/website-new/docs/en/integrations/practice/react/index.mdx b/apps/website-new/docs/en/integrations/practice/react/index.mdx new file mode 100644 index 00000000000..7879c42354d --- /dev/null +++ b/apps/website-new/docs/en/integrations/practice/react/index.mdx @@ -0,0 +1,20 @@ +# React Practice + +React apps do not use one fixed React-specific Module Federation plugin. Choose the integration based on the bundler used by your project first, then use these React scenario guides. + +| Project type | Read next | +| --- | --- | +| Rsbuild | [Rsbuild](/integrations/build-tool/rsbuild) | +| Rslib | [Rslib](/integrations/build-tool/rslib) | +| Rspack | [Rspack](/integrations/bundler/rspack) | +| Webpack | [Webpack](/integrations/bundler/webpack) | +| Vite | [Vite](/integrations/build-tool/vite) | +| Runtime remote loading | [Runtime Installation](/guide/runtime/#installation) | + +If you need to load remote React components, continue with [Bridge](/guide/bridge/overview). If remote components need data fetching, data cache, or prefetch, continue with [Data Management](/guide/data/data-fetch). + +## Examples + +- [Rsbuild CRA](./rsbuild-cra) +- [React i18n](./i18n-react) +- [Using Nx CLI for React](./using-nx-for-react) diff --git a/apps/website-new/docs/en/practice/frameworks/react/index.mdx b/apps/website-new/docs/en/integrations/practice/react/rsbuild-cra.mdx similarity index 100% rename from apps/website-new/docs/en/practice/frameworks/react/index.mdx rename to apps/website-new/docs/en/integrations/practice/react/rsbuild-cra.mdx diff --git a/apps/website-new/docs/en/practice/frameworks/react/using-nx-for-react.mdx b/apps/website-new/docs/en/integrations/practice/react/using-nx-for-react.mdx similarity index 100% rename from apps/website-new/docs/en/practice/frameworks/react/using-nx-for-react.mdx rename to apps/website-new/docs/en/integrations/practice/react/using-nx-for-react.mdx diff --git a/apps/website-new/docs/en/practice/bridge/vue-bridge.mdx b/apps/website-new/docs/en/integrations/practice/vue.mdx similarity index 100% rename from apps/website-new/docs/en/practice/bridge/vue-bridge.mdx rename to apps/website-new/docs/en/integrations/practice/vue.mdx diff --git a/apps/website-new/docs/en/plugin/plugins/_meta.json b/apps/website-new/docs/en/plugin/plugins/_meta.json index 124735d30d1..0316025bb9c 100644 --- a/apps/website-new/docs/en/plugin/plugins/_meta.json +++ b/apps/website-new/docs/en/plugin/plugins/_meta.json @@ -1 +1 @@ -["index", "retry-plugin", "building-custom-retry-plugin"] +["index", "retry-plugin", "observability-plugin", "building-custom-retry-plugin"] diff --git a/apps/website-new/docs/en/plugin/plugins/observability-plugin.mdx b/apps/website-new/docs/en/plugin/plugins/observability-plugin.mdx new file mode 100644 index 00000000000..f4c7fd2e279 --- /dev/null +++ b/apps/website-new/docs/en/plugin/plugins/observability-plugin.mdx @@ -0,0 +1,490 @@ +--- +title: Observability Plugin +description: Observe Module Federation loading, collect runtime and build reports, and give humans or AI agents enough facts to debug failures. +--- + +# Observability Plugin + +The Observability Plugin makes Module Federation loading observable. It records +runtime loading events, summarizes the final result, prints a stable +`traceId` when loading fails, and can correlate runtime failures with build +information. + +The plugin is designed for Module Federation `2.5.0` and later. If a project is +on an older MF version, runtime error codes still help with basic +troubleshooting, but the richer report and loading observability workflow +requires upgrading to `2.5.0+` and enabling this plugin. + +Use it when you want to answer questions such as: + +- Did this remote load successfully? +- Which phase failed: manifest, remoteEntry, init, expose, factory, or shared? +- Did the load recover through a runtime fallback or recovery path? +- Which shared dependency provider and version were selected? +- Did `preloadRemote` actually finish loading its resources? +- What report should I give to a human or AI coding agent? + +Shared observability is scoped to the MF instance. It tells you which MF +instance loaded a shared dependency, which registered provider/version was +selected, and the related scope, version, and eager configuration. It does not +guarantee a causal link from that shared dependency back to a specific +remote/expose. When a flow involves multiple shared dependencies, inspect all +`phase: "shared"` events. `summary.shared` is only the last observed shared +summary. + +If the build plugin supplies `customShareInfo` but no registered shared +provider matches it, this is not always a fatal error. The report describes the +handled path as `summary.outcome: "recovered"`, +`summary.phases.shared.status: "complete"`, and +`shared.reason: "custom-share-info-unmatched"`. It means the runtime continued +through a recoverable path. Inspect shared configuration only if you expected a +specific provider/version to be selected. + +Preload observability answers whether `preloadRemote` actually finished loading +its resources. After `preloadRemote` completes, reports include +`phase: "preload"` resource results with the resource URL, resource type, +status, and preload `id`. Status can be `success`, `error`, `timeout`, or +`cached`. When the call does not specify `exposes`, the `id` is `remoteName/*`. +When `exposes` are provided, each expose is recorded separately as +`remoteName/expose`. + +If you want to try the report workflow first, or inspect and export reports +directly from the page, install the latest +[Module Federation Chrome extension](https://chromewebstore.google.com/detail/module-federation/aeoilchhomapofiopejjlecddfldpeom). +The `Loading Trace` tab reads reports from the page's own observability plugin. +If the page has not installed the plugin, the extension can also start temporary +collection for the current tab. See +[Chrome Devtool Loading Trace](../../guide/debug/chrome-devtool#loading-trace) +for the full workflow. + +## Install + +```bash +npm install @module-federation/observability-plugin +``` + +## Browser Runtime + +Use the default entry in browser runtime code. + +```ts title="mf-runtime.ts" +import { createInstance } from '@module-federation/runtime'; +import { ObservabilityPlugin } from '@module-federation/observability-plugin'; + +export const mf = createInstance({ + name: 'runtime_host', + remotes: [ + { + name: 'remote1', + entry: 'https://example.com/mf-manifest.json', + }, + ], + plugins: [ + ObservabilityPlugin({ + level: 'verbose', + browser: { + enabled: true, + scope: 'runtime_host', + }, + }), + ], +}); +``` + +When a Module Federation load fails, the plugin prints a compact +`console.error` hint: + +```text +[Module Federation] Observability report generated +traceId: mf-... +phase: manifest +errorCode: RUNTIME-003 +read: window.__FEDERATION__.__OBSERVABILITY__["runtime_host"].getReport("mf-...") +``` + +Run the `read:` command in the browser console to get the full report. + +You can also read reports directly: + +```ts +window.__FEDERATION__.__OBSERVABILITY__.runtime_host.getLatestReport(); +window.__FEDERATION__.__OBSERVABILITY__.runtime_host.getReport('mf-...'); +window.__FEDERATION__.__OBSERVABILITY__.runtime_host.getReports({ limit: 5 }); +window.__FEDERATION__.__OBSERVABILITY__.runtime_host.findReports({ + remote: 'remote1', +}); +window.__FEDERATION__.__OBSERVABILITY__.runtime_host.exportReport('mf-...'); +``` + +If you want to observe loading chains in development, or if the page stays in a +loading state before any error is printed, enable the browser reader. +Development browser mode prints start logs by default: + +```ts +ObservabilityPlugin({ + level: 'verbose', + browser: { + enabled: true, + scope: 'runtime_host', + }, +}); +``` + +The plugin prints `console.info` only when `loadRemote` or `loadShare` starts. +The line includes the `traceId` and read command. Agents can use that `traceId` +to inspect the current report, including `status: "pending"`, +`summary.phases`, `updatedAt`, and `duration`. Set `trace.printStart: false` to +disable it in development browser mode. In production browser mode, start logs +are disabled by default and require `trace.printStart: true`. + +## Production Runtime + +In production, avoid exposing a public browser reader by default. Keep console +output small and upload reports through your own system. + +```ts title="mf-runtime.ts" +import { ObservabilityPlugin } from '@module-federation/observability-plugin'; + +export const observabilityPlugin = ObservabilityPlugin({ + level: 'summary', + browser: { + mode: 'production', + }, + onReport(report) { + if (report.status === 'error' || report.summary.outcome === 'recovered') { + navigator.sendBeacon( + '/api/mf-observability', + JSON.stringify({ + traceId: report.traceId, + status: report.status, + diagnosis: report.diagnosis, + summary: report.summary, + remote: report.remote, + shared: report.shared, + moduleInfo: report.moduleInfo, + }), + ); + } + }, +}); +``` + +In production browser mode, the console hint only contains `traceId` and known +`errorCode`. The full report should come from `onReport`, `exportReport()`, or +your own telemetry backend. + +## Analyze Reports from `onReport` + +`onReport` is called whenever a report is updated. Production apps usually do +not need to store every successful report, but this callback is the right place +to upload failures, recovered paths, or selected successful loading chains to +your own system. + +Common strategies: + +- Troubleshooting only: upload `report.status === "error"` and + `report.summary.outcome === "recovered"`. +- Shared dependency auditing: also upload + `report.summary.outcome === "shared-resolved"` so you can see which provider + and version were selected. +- Preload result auditing: also upload + `report.summary.outcome === "preloaded"` or failed `phase: "preload"` events + to count successful, failed, timed out, and cached preload resources. +- Full loading observability: sample successful `runtime-loaded`, + `component-loaded`, and `shared-resolved` reports. + +After you have a report, read it in this order: + +1. `diagnosis`: owner hint, key facts, and suggested next actions. +2. `summary`: final result. `runtime-loaded` means the remote loaded, + `component-loaded` means business code reported readiness, `shared-resolved` + means a shared provider/version was selected, `preloaded` means preload + resources completed, `failed` means loading failed, and `recovered` means the + runtime continued through a recoverable path. +3. `remote` / `shared`: the current load target. For shared reports, inspect + `provider`, `requiredVersion`, `selectedVersion`, and `availableVersions`. +4. `moduleInfo`: deployment-provided module information, useful for snapshot + matching issues. +5. `events`: the ordered timeline, useful for finding the phase that got stuck. + +Example: + +```ts +ObservabilityPlugin({ + level: 'summary', + browser: { + mode: 'production', + }, + onReport(report) { + const outcome = report.summary.outcome; + const shouldUpload = + report.status === 'error' || + outcome === 'recovered' || + outcome === 'shared-resolved' || + outcome === 'preloaded'; + + if (!shouldUpload) { + return; + } + + navigator.sendBeacon( + '/api/mf-observability', + JSON.stringify({ + traceId: report.traceId, + status: report.status, + outcome, + diagnosis: report.diagnosis, + summary: report.summary, + remote: report.remote, + shared: report.shared, + moduleInfo: report.moduleInfo, + events: report.events, + }), + ); + }, +}); +``` + +You can give the uploaded report to an AI coding agent: + +```text +/mf observability + +Here is an MF observability report uploaded from production. +Please tell me whether the load succeeded, where it failed, who likely owns the issue, and how to fix it. + + +``` + +## Runtime Options + +`ObservabilityPlugin(options)` supports these runtime options: + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `enabled` | `boolean` | `true` | Enable or disable the plugin. Disabled plugins do not record events or generate reports. | +| `level` | `'summary' \| 'verbose'` | `'summary'` | Report detail level. `verbose` keeps the full event timeline. | +| `maxEvents` | `number` | built-in limit | Maximum retained events for one plugin instance. | +| `console` | `boolean` | `true` | Print a short `console.error` hint when a load fails. | +| `printRawStack` | `boolean` | `false` | Print the raw error stack to console. Keep this off by default in production. | +| `stackTrace.enabled` | `boolean` | `true` | Store a clipped stack in the report. | +| `stackTrace.maxLines` | `number` | built-in limit | Maximum stack lines kept in the report. | +| `stackTrace.maxLength` | `number` | built-in limit | Maximum stack characters kept in the report. | +| `collector` | `boolean \| { enabled?: boolean; port?: number }` | `false` | POST browser runtime events to a local collector. `true` uses `127.0.0.1:17891`; custom config only needs a `port`. This is for local AI debugging, and the runtime plugin does not start the server. | +| `browser.enabled` | `boolean` | `false` | Expose the browser reader on `window.__FEDERATION__.__OBSERVABILITY__`. | +| `browser.scope` | `string` | host name | Browser reader namespace, for example `runtime_host`. | +| `browser.mode` | `'development' \| 'production'` | `'development'` | Browser output mode. Production mode keeps console hints minimal. | +| `trace.printStart` | `boolean` | `true` in development browser mode, `false` in production browser mode | Print `console.info` when `loadRemote` or `loadShare` starts so development agents can get the `traceId`. Production mode requires an explicit `true`. | +| `react.enabled` | `boolean` | `true` | Master switch for React-specific debugging options. Set to `false` to disable all React wrapping. | +| `react.injectLoadedCallback` | `boolean` | `false` | Explicitly wrap matched remote React components and inject `onMFRemoteLoaded`. This changes the component reference; use it as a temporary debugging option and remove it after the issue is fixed. | +| `react.remoteIds` | `string[]` | `[]` | Limit callback injection to specific remote requests, such as `remote/Button` or `./Button`. Empty means no remote filter. | +| `react.defaultExportMode` | `'preserve' \| 'component'` | auto | Controls whether `{ default: Component }` remotes return the wrapped component directly. Usually keep the default. | +| `onEvent` | `(event, report, context) => void` | `undefined` | Called whenever an observability event is recorded. | +| `onReport` | `(report, context) => void` | `undefined` | Called whenever a report is updated. Production apps commonly upload reports here. | +| `onRawError` | `(error, context) => void` | `undefined` | Called with the original error object for integration with your own error system. | + +## Node or SSR Runtime + +Use the Node entry when you want local report files. + +```ts title="mf-node-runtime.ts" +import { createInstance } from '@module-federation/runtime'; +import { ObservabilityPlugin } from '@module-federation/observability-plugin/node'; + +createInstance({ + name: 'node_host', + remotes: [], + plugins: [ + ObservabilityPlugin({ + level: 'verbose', + fileOutput: true, + directory: '.mf/observability', + }), + ], +}); +``` + +The Node entry writes: + +- `.mf/observability/latest.json`: latest complete report +- `.mf/observability/events.jsonl`: event stream for multiple traces + +Read `latest.json` first. Use `events.jsonl` only when you need ordering or +multiple traces. + +## Build Observability + +Add the build plugin next to your Module Federation build plugin when you want +separate build-side evidence. + +```js title="webpack.config.js" +const { + ModuleFederationPlugin, +} = require('@module-federation/enhanced/webpack'); +const { + ObservabilityBuildPlugin, +} = require('@module-federation/observability-plugin/build'); + +const moduleFederationOptions = { + name: 'runtime_host', + remotes: { + remote1: 'remote1@https://example.com/mf-manifest.json', + }, + exposes: { + './Button': './src/Button', + }, + shared: { + react: { singleton: true, requiredVersion: '^18.0.0' }, + }, +}; + +module.exports = { + plugins: [ + new ModuleFederationPlugin(moduleFederationOptions), + new ObservabilityBuildPlugin({ + moduleFederation: moduleFederationOptions, + }), + ], +}; +``` + +Build observability can write: + +- `.mf/observability/build-info.json` +- `.mf/observability/build-report.json` + +Runtime reports do not embed these files. When debugging needs build evidence, +read the build file separately and compare it with the runtime report. + +## Mark Business Success + +Module Federation can know that a remote module loaded. It cannot always know +that your business component finished its own data loading, chart rendering, or +SDK initialization. + +When `react.injectLoadedCallback` is explicitly enabled, the plugin injects an +`onMFRemoteLoaded` prop into matched remote React components. The producer can +call it when its own ready condition is met: + +```tsx +import { useEffect } from 'react'; +import type { OnMFRemoteLoaded } from '@module-federation/observability-plugin'; + +export default function RemotePanel({ + onMFRemoteLoaded, +}: { + onMFRemoteLoaded?: OnMFRemoteLoaded; +}) { + useEffect(() => { + onMFRemoteLoaded?.({ + metadata: { + dataReady: true, + }, + }); + }, [onMFRemoteLoaded]); + + return
Remote panel
; +} +``` + +Consumer-side code can still call the instance method directly when needed: + +```ts +import { getInstance } from '@module-federation/runtime'; +import '@module-federation/observability-plugin'; + +getInstance()?.markComponentLoaded({ + requestId: 'remote1/Button', + componentName: 'Button', + metadata: { + route: '/dashboard', + }, +}); +``` + +The report will include `component:business-loaded` and +`summary.outcome: "component-loaded"`. + +## Inject React Loaded Callback + +For development, AI debugging, or temporary production debugging, you can +explicitly inject a loaded callback into matched remote React components: + +```ts +ObservabilityPlugin({ + level: 'verbose', + react: { + injectLoadedCallback: true, + remoteIds: ['remote/Button'], + }, +}); +``` + +When enabled, the plugin tries to detect remote React function components after +`loadRemote` succeeds and wraps them with a component that does not add DOM +nodes. The wrapper injects only the `onMFRemoteLoaded` prop. It does not observe +React mount, render lifecycle, or timeout. When the producer calls +`props.onMFRemoteLoaded?.()`, the report records `component:business-loaded`. + +If `summary.componentLoaded` is `false` after enabling +`react.injectLoadedCallback`, first check whether the producer source actually +calls `props.onMFRemoteLoaded?.(...)`. If it does not, the report can only prove +that the remote resource loaded; it cannot prove whether the component reached +the producer's business-ready point. If the producer source is unavailable, ask +the producer owner to confirm whether the callback was added. + +This option changes the component reference because `loadRemote` returns a +wrapper component. Use `remoteIds` to keep the matched scope narrow, and remove +this option after the production issue is fixed. + +## Use With the `mf` Skill + +Install the skill: + +```bash +npx skills add module-federation/agent-skills --skill mf -y +``` + +When the console prints an observability hint, ask your agent: + +```text +/mf observability +I saw this Module Federation console error: + +[Module Federation] Observability report generated +traceId: mf-... +read: window.__FEDERATION__.__OBSERVABILITY__["runtime_host"].getReport("mf-...") + +Please read the report and fix the issue. +``` + +If you are in Node or SSR, give the agent the file path instead: + +```text +/mf observability +Read .mf/observability/latest.json and explain the likely owner and fix. +``` + +If your production app uploads reports, give the uploaded report or `traceId` +to the agent: + +```text +/mf observability +Here is the uploaded report for traceId mf-... +Tell me whether this is a host, remote, shared, network, or build issue. +``` + +## What the AI Reads First + +The skill reads fields in this order: + +1. `diagnosis` +2. `summary` +3. `moduleInfo` +4. `events` + +If build-side evidence is needed, read `.mf/observability/build-info.json` or +`.mf/observability/build-report.json` as a separate file. + +Reports omit `undefined` fields. If a field is absent, treat it as not observed +or not relevant for this trace. diff --git a/apps/website-new/docs/en/practice/_meta.json b/apps/website-new/docs/en/practice/_meta.json deleted file mode 100644 index d5bde5bcf8d..00000000000 --- a/apps/website-new/docs/en/practice/_meta.json +++ /dev/null @@ -1,18 +0,0 @@ -[ - { - "type": "file", - "name": "overview", - "label": "Overview" - }, - { - "type": "dir-section-header", - "name": "bridge", - "label": "Bridge" - }, - { - "type": "dir-section-header", - "name": "frameworks", - "label": "Frameworks", - "collapsed": false - } -] diff --git a/apps/website-new/docs/en/practice/bridge/_meta.json b/apps/website-new/docs/en/practice/bridge/_meta.json deleted file mode 100644 index facac18a53f..00000000000 --- a/apps/website-new/docs/en/practice/bridge/_meta.json +++ /dev/null @@ -1,19 +0,0 @@ -[ - { - "type": "file", - "name": "overview", - "label": "Bridge Overview" - }, - { - "type": "dir-section-header", - "name": "react-bridge", - "label": "React Bridge", - "collapsed": false, - "index": "getting-started" - }, - { - "type": "file", - "name": "vue-bridge", - "label": "Vue Bridge" - } -] diff --git a/apps/website-new/docs/en/practice/frameworks/_meta.json b/apps/website-new/docs/en/practice/frameworks/_meta.json deleted file mode 100644 index c8163aedb6e..00000000000 --- a/apps/website-new/docs/en/practice/frameworks/_meta.json +++ /dev/null @@ -1,31 +0,0 @@ -[ - { - "type": "file", - "name": "overview", - "label": "Framework Overview" - }, - { - "type": "dir-section-header", - "name": "react", - "label": "React", - "collapsed": true - }, - { - "type": "dir-section-header", - "name": "modern", - "label": "Modern.js", - "collapsed": true - }, - { - "type": "dir-section-header", - "name": "next", - "label": "Next.js", - "collapsed": true - }, - { - "type": "dir-section-header", - "name": "angular", - "label": "Angular", - "collapsed": true - } -] diff --git a/apps/website-new/docs/en/practice/frameworks/modern/_meta.json b/apps/website-new/docs/en/practice/frameworks/modern/_meta.json deleted file mode 100644 index 1b38edd64ba..00000000000 --- a/apps/website-new/docs/en/practice/frameworks/modern/_meta.json +++ /dev/null @@ -1 +0,0 @@ -["index","dynamic-remote"] diff --git a/apps/website-new/docs/en/practice/frameworks/next/_meta.json b/apps/website-new/docs/en/practice/frameworks/next/_meta.json deleted file mode 100644 index 9d91eff3307..00000000000 --- a/apps/website-new/docs/en/practice/frameworks/next/_meta.json +++ /dev/null @@ -1 +0,0 @@ -["index", "importing-components","importing-pages","express","presets"] diff --git a/apps/website-new/docs/en/practice/frameworks/overview.mdx b/apps/website-new/docs/en/practice/frameworks/overview.mdx deleted file mode 100644 index 41fc3a5f6db..00000000000 --- a/apps/website-new/docs/en/practice/frameworks/overview.mdx +++ /dev/null @@ -1,3 +0,0 @@ ---- -overview: true ---- diff --git a/apps/website-new/docs/en/practice/frameworks/react/_meta.json b/apps/website-new/docs/en/practice/frameworks/react/_meta.json deleted file mode 100644 index 5c817e06ae4..00000000000 --- a/apps/website-new/docs/en/practice/frameworks/react/_meta.json +++ /dev/null @@ -1 +0,0 @@ -["index", "using-nx-for-react", "i18n-react"] diff --git a/apps/website-new/docs/en/practice/monorepos/_meta.json b/apps/website-new/docs/en/practice/monorepos/_meta.json deleted file mode 100644 index 975f087c9fa..00000000000 --- a/apps/website-new/docs/en/practice/monorepos/_meta.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "type": "file", - "name": "index", - "label": "Benefits of Module Federation in a Monorepo", - "collapsed": true - }, - { - "type": "file", - "name": "nx-for-module-federation", - "label": "Nx and Module Federation", - "collapsed": true - } -] diff --git a/apps/website-new/docs/en/practice/overview.md b/apps/website-new/docs/en/practice/overview.md deleted file mode 100644 index e75b040b7b9..00000000000 --- a/apps/website-new/docs/en/practice/overview.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: "Overview" ---- - -# Overview - -Module Federation, as a module sharing solution, aims to solve code reuse issues, optimize the build process, and enhance runtime performance. However, in practical project development, these functionalities alone are insufficient. It often needs to be combined with various frameworks to understand how to use Module Federation under different frameworks and how to integrate multiple functionalities from different frameworks. Additionally, it is necessary to consider the differing needs of various application scenarios, such as backend applications versus mobile application development scenarios. -This "Practical Guide" aims to address the above issues by providing a series of best practices for using Module Federation. The main content includes two parts: - -1. Bridge: For common business development scenarios: how to load application-level modules (with routing) and how to load modules across different frontend frameworks. -2. Framework: Introduces the usage of Module Federation in different frameworks. diff --git a/apps/website-new/docs/en/practice/performance/_meta.json b/apps/website-new/docs/en/practice/performance/_meta.json deleted file mode 100644 index 033162d887f..00000000000 --- a/apps/website-new/docs/en/practice/performance/_meta.json +++ /dev/null @@ -1 +0,0 @@ -["prefetch"] diff --git a/apps/website-new/docs/en/practice/performance/prefetch.mdx b/apps/website-new/docs/en/practice/performance/prefetch.mdx deleted file mode 100644 index 16319ba24ae..00000000000 --- a/apps/website-new/docs/en/practice/performance/prefetch.mdx +++ /dev/null @@ -1,29 +0,0 @@ -# Data Prefetch - -:::danger Deprecated - -Legacy Data Prefetch has been removed. The `.prefetch.ts`, `dataPrefetch`, and `@module-federation/enhanced/prefetch` APIs are no longer available. - -Use [bridge-react - prefetch](../../guide/basic/data-fetch-prefetch) instead. It supports Rspack/Webpack and works in both SSR and CSR scenarios. - -::: - -## How to migrate to bridge-react - prefetch - -### Producer - -1. Rename the `.prefetch.ts` file to `.data.ts`. -2. Change the default export to a named export called `fetchData`. -3. Remove `dataPrefetch` from your config file. -4. If you are using the `defer` API, remove it. -5. If the component uses `usePrefetch` to fetch data, accept the data via `props` instead. - -The producer does not execute `fetchData` by itself. It is only called when the consumer loads the module, and the result is injected into the component. - -If the producer application also needs to render this component with data, call `fetchData` before rendering and pass the data via `props`. - -### Consumer - -1. Remove `dataPrefetch` from your config file. -2. Use `createLazyComponent` to load the producer. See [bridge-react - data fetch](../../guide/basic/data-fetch#consumer). -3. Use [prefetch](../../guide/basic/data-fetch-prefetch) to prefetch data. diff --git a/apps/website-new/docs/zh/_nav.json b/apps/website-new/docs/zh/_nav.json index e0fbd3ef7b8..250125e0291 100644 --- a/apps/website-new/docs/zh/_nav.json +++ b/apps/website-new/docs/zh/_nav.json @@ -10,9 +10,59 @@ "activeMatch": "/guide/" }, { - "text": "实践", - "link": "/practice/overview", - "activeMatch": "/practice/" + "text": "接入方案", + "link": "/integrations/", + "activeMatch": "/integrations/", + "items": [ + { + "text": "总览", + "link": "/integrations/" + }, + { + "text": "Rsbuild", + "link": "/integrations/build-tool/rsbuild" + }, + { + "text": "Rslib", + "link": "/integrations/build-tool/rslib" + }, + { + "text": "Vite", + "link": "/integrations/build-tool/vite" + }, + { + "text": "Rspack", + "link": "/integrations/bundler/rspack" + }, + { + "text": "Webpack", + "link": "/integrations/bundler/webpack" + }, + { + "text": "Metro", + "link": "/integrations/bundler/metro" + }, + { + "text": "Rspress", + "link": "/integrations/documentation/rspress" + }, + { + "text": "Modern.js", + "link": "/integrations/framework/modernjs" + }, + { + "text": "Next.js", + "link": "/integrations/framework/nextjs" + }, + { + "text": "Angular", + "link": "/integrations/framework/angular" + }, + { + "text": "实践", + "link": "/integrations/practice" + } + ] }, { "text": "配置", diff --git a/apps/website-new/docs/zh/ai/index.mdx b/apps/website-new/docs/zh/ai/index.mdx index a517ce323c4..e990752ac32 100644 --- a/apps/website-new/docs/zh/ai/index.mdx +++ b/apps/website-new/docs/zh/ai/index.mdx @@ -22,7 +22,7 @@ sidebar: false 如果你在用 Claude Code、Cursor、Windsurf 这类支持 Skills 的 AI 编程工具,优先安装: ```bash -npx skills add module-federation/core --skill mf -y +npx skills add module-federation/agent-skills --skill mf -y ``` 装好以后,直接让 Agent 去读文档再回答: @@ -46,9 +46,31 @@ npx skills add module-federation/core --skill mf -y - 帮你分析类型问题 - 帮你看共享依赖冲突 - 帮你分析远端模块信息 +- 帮你读取观测报告并排查 MF 加载失败 👉 [查看 Agent Skills 详细说明](./skill) +## 观测和调试 MF 加载 + +如果你希望 AI coding agent 基于真实运行时信息排查 Module Federation +加载问题,需要先接入 [观测插件](../plugin/plugins/observability-plugin)。 +`/mf observability` 这个 skill 负责帮你接入、读取和分析报告;真正的运行时证据由观测插件采集。 + +加载失败后,浏览器控制台会打印 `traceId`,开发环境还会打印 `read:` 命令。把这段提示交给 Agent: + +```text +/mf observability +我看到了这条 Module Federation console error: + +[Module Federation] Observability report generated +traceId: mf-... +read: window.__FEDERATION__.__OBSERVABILITY__["runtime_host"].getReport("mf-...") + +请读取报告并帮我修复问题。 +``` + +如果是 Node 或 SSR,让 Agent 读取 `.mf/observability/latest.json`。如果是生产环境,建议通过插件的 `onReport` 回调把报告上传到自己的系统,然后把上传报告或 traceId 交给 Agent。 + ## 不装 Skill 也能先用 如果你只是临时问一下,也可以直接把下面这句话发给你的 Agent: @@ -87,4 +109,5 @@ https://module-federation.io/guide/basic/runtime.md 1. 先装 `mf` 2. 问一个你当前最关心的问题 -3. 再看 [Skills 说明](./skill),了解它支持哪些子命令 +3. 需要 MF 加载可观测时,先接入 [观测插件](../plugin/plugins/observability-plugin) +4. 再看 [Skills 说明](./skill),了解它支持哪些子命令 diff --git a/apps/website-new/docs/zh/ai/skill.mdx b/apps/website-new/docs/zh/ai/skill.mdx index b67d6c58f2d..46d07a1a526 100644 --- a/apps/website-new/docs/zh/ai/skill.mdx +++ b/apps/website-new/docs/zh/ai/skill.mdx @@ -14,7 +14,7 @@ Module Federation 现在对外提供一个统一的 **`mf` skill**。 ## 安装 ```bash -npx skills add module-federation/core --skill mf -y +npx skills add module-federation/agent-skills --skill mf -y ``` 安装后,主入口就是: @@ -26,7 +26,7 @@ npx skills add module-federation/core --skill mf -y 如果无法使用命令行安装,也可以直接从 GitHub 手动复制目录: ```text -https://github.com/module-federation/core/tree/main/skills/mf +https://github.com/module-federation/agent-skills/tree/main/skills/mf ``` ## 这个 skill 是怎么用的 @@ -40,6 +40,7 @@ https://github.com/module-federation/core/tree/main/skills/mf /mf integrate /mf type-check /mf runtime-error RUNTIME-008 +/mf observability 帮我接入观测插件,记录 MF 加载过程 ``` 直接写自然语言: @@ -48,6 +49,8 @@ https://github.com/module-federation/core/tree/main/skills/mf /mf shared 里的 singleton 和 requiredVersion 有什么区别? /mf 帮我看看这个项目为什么 remote types 没拉下来 /mf 帮我把 Module Federation 接到当前项目里 +/mf 我想让 AI 能看见 MF remote 和 shared 的加载状态,应该怎么接 +/mf 我看到 Module Federation console error,traceId 是 mf-... ``` ## `mf` 目前支持什么 @@ -64,6 +67,7 @@ https://github.com/module-federation/core/tree/main/skills/mf | `config-check` | 排查配置错误、暴露路径错误、插件不匹配 | | `bridge-check` | 排查 Bridge 接入问题 | | `runtime-error` | 排查明确的运行时报错码,尤其是 `RUNTIME-001` / `RUNTIME-008` | +| `observability` | 配合观测插件记录 MF 加载过程,读取观测报告,排查运行时和构建加载问题 | ## 最常用的几类场景 @@ -96,10 +100,44 @@ https://github.com/module-federation/core/tree/main/skills/mf /mf shared-deps /mf config-check /mf runtime-error RUNTIME-008 +/mf observability ``` 这时候重点不是你自己查,而是让 Agent 直接开始定位。 +### 4. 让 Agent 分析 MF 加载过程 + +如果你希望 AI 不只是“看到报错”,而是能知道 MF 的 remote、shared、组件加载状态在哪里成功、在哪里失败,可以用 `observability` 子命令。 + +要读取真实的 MF 加载报告,需要先接入 [观测插件](../plugin/plugins/observability-plugin)。观测插件会把 MF 加载过程整理成报告,包括加载成功、加载失败、错误原因、`traceId` 和读取方式;skill 负责帮你接入、读取和分析这些信息,再判断问题更可能在宿主、远端、共享依赖、构建配置还是部署地址。 + +如果你还没有接入观测插件,可以先这样问: + +```text +/mf observability +帮我看当前项目怎么接入观测插件,记录 MF 加载过程。 +``` + +接入后,如果你看到浏览器控制台打印了 `traceId` 和 `read:` 命令,再把它交给 skill: + +```text +/mf observability +我看到了这条 Module Federation console error: + +[Module Federation] Observability report generated +traceId: mf-... +read: window.__FEDERATION__.__OBSERVABILITY__["runtime_host"].getReport("mf-...") + +请读取报告并帮我修复问题。 +``` + +这里的“报告”不是让你手动整理出来的长日志,而是观测插件生成的结构化结果。浏览器里通常通过控制台里的 `read:` 命令读取;Node、SSR 或生产环境上报场景,可以让 Agent 读取文件或你们自己上报系统里的报告: + +```text +/mf observability +请读取 .mf/observability/latest.json,告诉我可能是谁的问题,以及应该怎么修。 +``` + ## 你真正需要记住的只有一件事 不是你自己先读文档、再告诉 Agent 该怎么办。 diff --git a/apps/website-new/docs/zh/configure/index.mdx b/apps/website-new/docs/zh/configure/index.mdx index 85f1e313516..5be3c0d50a1 100644 --- a/apps/website-new/docs/zh/configure/index.mdx +++ b/apps/website-new/docs/zh/configure/index.mdx @@ -1,6 +1,6 @@ # Config 总览 -当前页面列出了 {props.brandName || props.name || 'Module Federation'} 所有的配置项,请查看 「[Build Plugins](../guide/build-plugins/plugins)」、「[Webpack Plugin](../guide/build-plugins/plugins-webpack)」 了解使用方式。 +当前页面列出了 {props.brandName || props.name || 'Module Federation'} 所有的配置项。不同项目如何安装包和注册插件,请查看[接入方案](/integrations/)。 ```ts type ModuleFederationOptions { diff --git a/apps/website-new/docs/zh/guide/_meta.json b/apps/website-new/docs/zh/guide/_meta.json index 87517f086d1..fed763d072a 100644 --- a/apps/website-new/docs/zh/guide/_meta.json +++ b/apps/website-new/docs/zh/guide/_meta.json @@ -9,30 +9,30 @@ "name": "basic", "label": "基础" }, - { + { "type": "dir-section-header", "name": "runtime", "label": "运行时" }, { "type": "dir-section-header", - "name": "build-plugins", - "label": "构建插件" + "name": "bridge", + "label": "Bridge" }, { "type": "dir-section-header", - "name": "framework", - "label": "框架" + "name": "data", + "label": "数据管理" }, { "type": "dir-section-header", - "name": "performance", - "label": "性能优化" + "name": "advanced", + "label": "高级能力" }, { "type": "dir-section-header", - "name": "advanced", - "label": "高级能力" + "name": "debug", + "label": "调试" }, { "type": "dir-section-header", @@ -40,13 +40,8 @@ "label": "部署" }, { - "type":"dir-section-header", - "name":"debug", - "label":"调试" - }, - { - "type":"dir-section-header", - "name":"troubleshooting", - "label":"Troubleshooting" + "type": "dir-section-header", + "name": "troubleshooting", + "label": "Troubleshooting" } ] diff --git a/apps/website-new/docs/zh/guide/advanced/_meta.json b/apps/website-new/docs/zh/guide/advanced/_meta.json new file mode 100644 index 00000000000..fb74ac43c93 --- /dev/null +++ b/apps/website-new/docs/zh/guide/advanced/_meta.json @@ -0,0 +1 @@ +["shared-tree-shaking", "multiple-shared-scope"] diff --git a/apps/website-new/docs/zh/guide/performance/shared-tree-shaking.mdx b/apps/website-new/docs/zh/guide/advanced/shared-tree-shaking.mdx similarity index 100% rename from apps/website-new/docs/zh/guide/performance/shared-tree-shaking.mdx rename to apps/website-new/docs/zh/guide/advanced/shared-tree-shaking.mdx diff --git a/apps/website-new/docs/zh/guide/basic/_meta.json b/apps/website-new/docs/zh/guide/basic/_meta.json index 16e101f10a3..c189e5a715e 100644 --- a/apps/website-new/docs/zh/guide/basic/_meta.json +++ b/apps/website-new/docs/zh/guide/basic/_meta.json @@ -1,8 +1 @@ -[ - "cli", - "css-isolate", - "type-prompt", - "data-fetch", - "data-fetch-cache", - "data-fetch-prefetch" -] +["cli", "css-isolate", "type-prompt", "manifest-snapshot"] diff --git a/apps/website-new/docs/zh/guide/basic/manifest-snapshot.mdx b/apps/website-new/docs/zh/guide/basic/manifest-snapshot.mdx new file mode 100644 index 00000000000..57ab31bb615 --- /dev/null +++ b/apps/website-new/docs/zh/guide/basic/manifest-snapshot.mdx @@ -0,0 +1,120 @@ +# Manifest 与 Snapshot + +`mf-manifest.json` 是生产者构建后生成的运行时清单。它描述这个生产者暴露了哪些模块、远程入口在哪里、模块依赖哪些资源、有哪些共享依赖,以及类型文件在哪里。 + +Snapshot 是对 Manifest 信息的高度浓缩和预解析结果。运行时可以请求 Manifest 后临时生成 Snapshot;如果你有部署服务,也可以提前生成 Snapshot 并下发给消费者,让消费者直接获得远程入口和可预加载资源。 + +这篇文档只介绍概念和使用场景。具体配置见 [manifest 配置](/configure/manifest),完整字段见 [mf-manifest.json 字段定义](/configure/manifest-fields)。 + +## 为什么需要 Manifest + +只使用 `remoteEntry.js` 时,消费者可以加载远程模块,但很难提前知道远程模块背后的资源、类型和共享依赖信息。 + +使用 `mf-manifest.json` 时,消费者可以在真正加载远程模块前拿到更多信息: + +- 远程入口地址和加载类型 +- 暴露模块与资源列表 +- 可预加载的 JavaScript 与 CSS +- 共享依赖名称、版本和资源 +- 类型文件地址 + +因此,Manifest 不只是一个入口文件地址,它更像生产者给消费者提供的运行时说明书。 + +## Manifest 和 Stats 的区别 + +启用 [manifest 配置](/configure/manifest) 后,构建插件会生成 `mf-manifest.json` 和 `mf-stats.json`。 + +| 文件 | 用途 | +| --- | --- | +| `mf-manifest.json` | 给消费者运行时读取,字段更稳定、更精简 | +| `mf-stats.json` | 给构建分析、诊断和工具使用,字段更完整 | + +如果你只是接入远程模块,通常只需要配置和访问 `mf-manifest.json`。如果你要做构建分析、诊断工具或平台集成,再查看 `mf-stats.json`。 + +## Snapshot 是什么 + +Snapshot 可以理解为运行时真正消费的模块信息。 + +Manifest 来自单个生产者的构建产物,内容更接近“原始清单”。Snapshot 则会把这些信息整理成运行时更容易使用的结构,比如: + +- 当前生产者的真实 `remoteEntry` +- 暴露模块对应的资源 +- 可用于 preload 的资源 +- shared 依赖信息 +- 生产者依赖的其他远程模块 + +在没有部署服务的场景下,消费者会先请求 `mf-manifest.json`,再由运行时生成 Snapshot。 + +```txt +Host 配置 remotes + -> 请求 mf-manifest.json + -> 解析 Manifest + -> 生成 Snapshot + -> 加载 remoteEntry、expose、shared 和 preload assets +``` + +在有部署服务的场景下,部署服务可以提前消费 Manifest,生成 Snapshot 并下发给消费者。 + +```txt +生产者构建 Manifest + -> 部署服务提前解析 Manifest + -> 生成 Snapshot + -> Host 直接拿到 remoteEntry 和 preload assets +``` + +这样可以减少运行时多请求一次 Manifest 的开销,也可以让服务端提前完成资源定位。 + +## 我们在哪里用到它 + +### 远程模块加载 + +当 `remotes` 指向 `mf-manifest.json` 时,运行时会读取 Manifest,生成 Snapshot,再从 Snapshot 中得到最终的 `remoteEntry` 地址。 + +相关配置见 [remotes](/configure/remotes)。 + +### 资源预加载 + +Manifest 中包含 exposes、shared 等资源信息。运行时生成 Snapshot 后,可以基于这些信息提前加载远程模块需要的 JavaScript 和 CSS。 + +### Chrome DevTools + +[Chrome DevTools](/guide/debug/chrome-devtool) 需要基于 Manifest / Snapshot 获取当前页面有哪些联邦模块、模块之间的依赖关系、共享依赖复用情况,以及代理所需的远程入口信息。 + +### 本地代理 + +调试线上页面时,代理工具可以根据 Snapshot 识别目标生产者,并把它替换成本地启动的 Manifest 或远程入口。 + +### 运行时诊断 + +当远程模块加载失败时,可以通过 Manifest / Snapshot 判断问题发生在哪一层: + +- Manifest 地址是否可以访问 +- Manifest 字段是否完整 +- Snapshot 中是否存在目标生产者 +- Snapshot 是否能解析出 `remoteEntry` +- `remoteEntry` 是否加载成功 +- expose 是否存在 + +这能帮助你区分问题是 Manifest 请求失败、Snapshot 缺失,还是后续远程入口加载失败。 + +## 可以基于它做什么 + +Manifest / Snapshot 适合作为运行时插件、调试工具和部署平台的输入。 + +你可以基于它做: + +- **预加载优化**:提前拿到远程模块关联的 JS 和 CSS,减少用户点击后的等待时间。 +- **模块关系可视化**:展示当前页面加载了哪些生产者、消费者和 shared 依赖。 +- **本地代理调试**:把线上页面里的某个生产者替换成本地开发中的生产者。 +- **加载诊断**:判断失败发生在 Manifest、Snapshot、remoteEntry 还是 expose 阶段。 +- **Shared 分析**:判断共享依赖是否被复用、是否重复加载、版本是否符合预期。 + +不建议业务代码直接修改全局 Snapshot。更推荐通过 Runtime Plugin、DevTools 或部署服务来消费这些信息。 + +## disableSnapshot 的影响 + +如果开启 `optimization.disableSnapshot`,运行时会移除 Snapshot 与预加载相关能力,以换取更小的运行时代码体积。 + +这意味着依赖 Manifest / Snapshot 的能力会受到影响,例如动态类型提示、预加载、DevTools 可视化和代理等。 + +更多说明见 [optimization.disableSnapshot](/configure/experiments#disablesnapshot)。 diff --git a/apps/website-new/docs/zh/guide/bridge/_meta.json b/apps/website-new/docs/zh/guide/bridge/_meta.json new file mode 100644 index 00000000000..a13c2a8e6bf --- /dev/null +++ b/apps/website-new/docs/zh/guide/bridge/_meta.json @@ -0,0 +1,14 @@ +[ + { + "type": "file", + "name": "overview", + "label": "概览" + }, + { + "type": "dir", + "name": "react", + "label": "React", + "collapsible": true, + "collapsed": true + } +] diff --git a/apps/website-new/docs/zh/practice/bridge/overview.mdx b/apps/website-new/docs/zh/guide/bridge/overview.mdx similarity index 96% rename from apps/website-new/docs/zh/practice/bridge/overview.mdx rename to apps/website-new/docs/zh/guide/bridge/overview.mdx index 15a3c7afaf6..0ff655b7c5c 100644 --- a/apps/website-new/docs/zh/practice/bridge/overview.mdx +++ b/apps/website-new/docs/zh/guide/bridge/overview.mdx @@ -47,6 +47,8 @@ Bridge 解决的核心问题是:如何让使用不同前端框架开发的完 - 场景:适用于大型组件库或复杂业务组件的按需加载 #### 实践示例 +- [React 实践](/integrations/practice/react) +- [Vue Bridge 实践](/integrations/practice/vue) - [宿主应用示例](https://github.com/module-federation/core/tree/main/apps/router-demo/router-host-2000) - [远程应用示例](https://github.com/module-federation/core/tree/main/apps/router-demo/router-remote2-2002) @@ -144,14 +146,20 @@ Bridge 解决了现代前端开发中的几个关键挑战: 组件级加载和应用级加载可以完美配合使用: ```tsx +import { getInstance } from '@module-federation/runtime'; +import { lazyLoadComponentPlugin } from '@module-federation/bridge-react/data-fetch'; + // 应用级加载 - 加载完整的远程应用 const RemoteApp = createRemoteAppComponent({ loader: () => import('remote/app'), fallback: }); +const instance = getInstance(); +instance.registerPlugins([lazyLoadComponentPlugin()]); + // 组件级加载 - 在远程应用内部按需加载组件 -const LazyComponent = createLazyComponent({ +const LazyComponent = instance.createLazyComponent({ loader: () => import('remote/heavy-component'), loading: , fallback: ({ error }) => @@ -276,4 +284,3 @@ function App() { ``` 通过这种模式,Bridge 实现了框架无关的应用级模块加载,为微前端架构提供了坚实的技术基础。 - diff --git a/apps/website-new/docs/zh/practice/bridge/react-bridge/_meta.json b/apps/website-new/docs/zh/guide/bridge/react/_meta.json similarity index 92% rename from apps/website-new/docs/zh/practice/bridge/react-bridge/_meta.json rename to apps/website-new/docs/zh/guide/bridge/react/_meta.json index af9cbef4f84..d68817e1d35 100644 --- a/apps/website-new/docs/zh/practice/bridge/react-bridge/_meta.json +++ b/apps/website-new/docs/zh/guide/bridge/react/_meta.json @@ -11,7 +11,7 @@ }, { "type": "file", - "name": "load-app", + "name": "load-app", "label": "加载应用" }, { diff --git a/apps/website-new/docs/zh/practice/bridge/react-bridge/export-app.mdx b/apps/website-new/docs/zh/guide/bridge/react/export-app.mdx similarity index 100% rename from apps/website-new/docs/zh/practice/bridge/react-bridge/export-app.mdx rename to apps/website-new/docs/zh/guide/bridge/react/export-app.mdx diff --git a/apps/website-new/docs/zh/practice/bridge/react-bridge/getting-started.mdx b/apps/website-new/docs/zh/guide/bridge/react/getting-started.mdx similarity index 100% rename from apps/website-new/docs/zh/practice/bridge/react-bridge/getting-started.mdx rename to apps/website-new/docs/zh/guide/bridge/react/getting-started.mdx diff --git a/apps/website-new/docs/zh/practice/bridge/react-bridge/load-app.mdx b/apps/website-new/docs/zh/guide/bridge/react/load-app.mdx similarity index 100% rename from apps/website-new/docs/zh/practice/bridge/react-bridge/load-app.mdx rename to apps/website-new/docs/zh/guide/bridge/react/load-app.mdx diff --git a/apps/website-new/docs/zh/practice/bridge/react-bridge/load-component.mdx b/apps/website-new/docs/zh/guide/bridge/react/load-component.mdx similarity index 98% rename from apps/website-new/docs/zh/practice/bridge/react-bridge/load-component.mdx rename to apps/website-new/docs/zh/guide/bridge/react/load-component.mdx index d2f60c60a88..c5e7413d222 100644 --- a/apps/website-new/docs/zh/practice/bridge/react-bridge/load-component.mdx +++ b/apps/website-new/docs/zh/guide/bridge/react/load-component.mdx @@ -36,7 +36,7 @@ import { PackageManagerTabs } from '@theme'; ```tsx import { getInstance } from '@module-federation/runtime'; -import { lazyLoadComponentPlugin } from '@module-federation/bridge-react'; +import { lazyLoadComponentPlugin } from '@module-federation/bridge-react/data-fetch'; const instance = getInstance(); // 注册 lazyLoadComponentPlugin 插件 @@ -50,7 +50,7 @@ instance.registerPlugins([lazyLoadComponentPlugin()]); ```tsx import { getInstance } from '@module-federation/runtime'; -import { lazyLoadComponentPlugin } from '@module-federation/bridge-react'; +import { lazyLoadComponentPlugin } from '@module-federation/bridge-react/data-fetch'; const instance = getInstance(); // 注册 lazyLoadComponentPlugin 插件后, 注册后即可使用 `createLazyComponent` 或 `prefetch` API @@ -134,7 +134,7 @@ type ErrorInfo = { ```tsx import React, { FC, memo, useEffect } from 'react'; import { getInstance } from '@module-federation/enhanced/runtime'; -import { ERROR_TYPE } from '@module-federation/bridge-react'; +import { ERROR_TYPE } from '@module-federation/bridge-react/data-fetch'; const instance = getInstance(); const LazyComponent = instance.createLazyComponent({ diff --git a/apps/website-new/docs/zh/guide/build-plugins/_meta.json b/apps/website-new/docs/zh/guide/build-plugins/_meta.json deleted file mode 100644 index 6e5cdb2df5b..00000000000 --- a/apps/website-new/docs/zh/guide/build-plugins/_meta.json +++ /dev/null @@ -1,9 +0,0 @@ -[ - "plugins", - "plugins-rsbuild", - "plugins-rspack", - "plugins-webpack", - "plugins-rspress", - "plugins-vite", - "plugins-metro" -] diff --git a/apps/website-new/docs/zh/guide/build-plugins/plugins.mdx b/apps/website-new/docs/zh/guide/build-plugins/plugins.mdx deleted file mode 100644 index 10ff8b49709..00000000000 --- a/apps/website-new/docs/zh/guide/build-plugins/plugins.mdx +++ /dev/null @@ -1,14 +0,0 @@ -# Overview - -{props.brandName || props.name || 'Module Federation'} 为不同构建器提供了配套的构建插件。选择与你项目构建器对应的插件文档,即可完成接入与配置。 - -不同构建器的插件可能包含少量专属选项,但核心的配置项保持一致,详见[配置项](../../../configure/index)。 - -## 插件列表 - -- [Rsbuild](./plugins-rsbuild) -- [Rspack](./plugins-rspack) -- [Webpack](./plugins-webpack) -- [Vite](./plugins-vite) -- [Metro](./plugins-metro) -- [Rspress](./plugins-rspress) diff --git a/apps/website-new/docs/zh/guide/data/_meta.json b/apps/website-new/docs/zh/guide/data/_meta.json new file mode 100644 index 00000000000..01c3f0517a9 --- /dev/null +++ b/apps/website-new/docs/zh/guide/data/_meta.json @@ -0,0 +1,17 @@ +[ + { + "type": "file", + "name": "data-fetch", + "label": "数据获取" + }, + { + "type": "file", + "name": "data-fetch-cache", + "label": "数据缓存" + }, + { + "type": "file", + "name": "data-prefetch", + "label": "数据预取" + } +] diff --git a/apps/website-new/docs/zh/guide/basic/data-fetch-cache.mdx b/apps/website-new/docs/zh/guide/data/data-fetch-cache.mdx similarity index 97% rename from apps/website-new/docs/zh/guide/basic/data-fetch-cache.mdx rename to apps/website-new/docs/zh/guide/data/data-fetch-cache.mdx index 2a330fb78ad..f866e281113 100644 --- a/apps/website-new/docs/zh/guide/basic/data-fetch-cache.mdx +++ b/apps/website-new/docs/zh/guide/data/data-fetch-cache.mdx @@ -5,7 +5,7 @@ ## 基本用法 ```ts -import { cache } from '@module-federation/bridge-react'; +import { cache } from '@module-federation/bridge-react/data-fetch'; export type Data = { data: string; @@ -59,7 +59,7 @@ interface CacheOptions { 每次计算完成后,框架会记录写入缓存的时间,当再次调用该函数时,会根据 `maxAge` 参数判断缓存是否过期,如果过期,则重新执行 `fn` 函数,否则返回缓存的数据。 ```ts -import { cache, CacheTime } from '@module-federation/bridge-react'; +import { cache, CacheTime } from '@module-federation/bridge-react/data-fetch'; const getDashboardStats = cache( async () => { @@ -79,7 +79,7 @@ const getDashboardStats = cache( 如以下示例,在缓存未过期的 2分钟内,如果调用 `getDashboardStats` 函数,会返回缓存的数据,如果缓存过期,2分到3分钟内,收到的请求会先返回旧数据,然后后台会重新请求数据,并更新缓存。 ```ts -import { cache, CacheTime } from '@module-federation/bridge-react'; +import { cache, CacheTime } from '@module-federation/bridge-react/data-fetch'; const getDashboardStats = cache( async () => { @@ -97,7 +97,7 @@ const getDashboardStats = cache( `tag` 参数用于标识缓存的标签,可以传入一个字符串或字符串数组,可以基于这个标签使缓存失效,多个缓存函数可以使用一个标签。 ```ts -import { cache, revalidateTag } from '@module-federation/bridge-react'; +import { cache, revalidateTag } from '@module-federation/bridge-react/data-fetch'; const getDashboardStats = cache( async () => { @@ -125,7 +125,7 @@ revalidateTag('dashboard-stats'); // 会使 getDashboardStats 函数和 getCompl `getKey` 参数用于自定义缓存键的生成方式,例如你可能只需要依赖函数参数的一部分来区分缓存。它是一个函数,接收与原始函数相同的参数,返回一个字符串作为缓存键: ```ts -import { cache, CacheTime } from '@module-federation/bridge-react'; +import { cache, CacheTime } from '@module-federation/bridge-react/data-fetch'; import { fetchUserData } from './api'; const getUser = cache( @@ -157,7 +157,7 @@ Modern.js 中的 `generateKey` 函数确保即使对象属性顺序发生变化 ::: ```ts -import { cache, CacheTime, generateKey } from '@module-federation/bridge-react'; +import { cache, CacheTime, generateKey } from '@module-federation/bridge-react/data-fetch'; import { fetchUserData } from './api'; const getUser = cache( @@ -195,7 +195,7 @@ const getUser = cache( 这在某些场景下非常有用,比如当函数引用发生变化时,但你希望仍然返回缓存的数据。 ```ts -import { cache } from '@module-federation/bridge-react'; +import { cache } from '@module-federation/bridge-react/data-fetch'; import { fetchUserData } from './api'; // 不同的函数引用,但是通过 customKey 可以使它们共享一个缓存 @@ -251,7 +251,7 @@ const getUserD = cache( `onCache` 参数允许你跟踪缓存统计信息,例如命中率。这是一个回调函数,接收有关每次缓存操作的信息,包括状态、键、参数和结果。你可以在 onCache 返回 `false` 来阻止命中缓存。 ```ts -import { cache, CacheTime } from '@module-federation/bridge-react'; +import { cache, CacheTime } from '@module-federation/bridge-react/data-fetch'; // 跟踪缓存统计 const stats = { @@ -319,10 +319,9 @@ await getUser(2); // 缓存未命中 可以通过 `configureCache` 函数指定缓存的存储上限: ```ts -import { configureCache, CacheSize } from '@module-federation/bridge-react'; +import { configureCache, CacheSize } from '@module-federation/bridge-react/data-fetch'; configureCache({ maxSize: CacheSize.MB * 10, // 10MB }); ``` - diff --git a/apps/website-new/docs/zh/guide/basic/data-fetch.mdx b/apps/website-new/docs/zh/guide/data/data-fetch.mdx similarity index 98% rename from apps/website-new/docs/zh/guide/basic/data-fetch.mdx rename to apps/website-new/docs/zh/guide/data/data-fetch.mdx index f82cc62caac..0a2f5adf29b 100644 --- a/apps/website-new/docs/zh/guide/basic/data-fetch.mdx +++ b/apps/website-new/docs/zh/guide/data/data-fetch.mdx @@ -45,7 +45,7 @@ Module Federation 使用大体分为两种部分:组件(函数)、应用 其中 `List.data.ts` 需要导出名为 `fetchData` 的函数,该函数将会在 `List` 组件渲染前执行,并将其数据注入,示例如下: ```ts title="List.data.ts" -import type { DataFetchParams } from '@module-federation/bridge-react'; +import type { DataFetchParams } from '@module-federation/bridge-react/data-fetch'; export type Data = { data: string; }; @@ -129,7 +129,7 @@ export default Index; ##### 入参 -默认会往 loader 函数传递参数,其类型为 [DataFetchParams](/practice/bridge/react-bridge/load-component#datafetchparams),包含以下字段: +默认会往 loader 函数传递参数,其类型为 [DataFetchParams](/guide/bridge/react/load-component#datafetchparams),包含以下字段: - `isDowngrade` (boolean): 表示当前执行上下文是否处于降级模式。例如,在服务端渲染 (SSR) 失败,在客户端渲染(CSR)中会重新往服务端发起请求,调用 loader 函数,此时该值为 `true`。 diff --git a/apps/website-new/docs/zh/guide/basic/data-fetch-prefetch.mdx b/apps/website-new/docs/zh/guide/data/data-prefetch.mdx similarity index 99% rename from apps/website-new/docs/zh/guide/basic/data-fetch-prefetch.mdx rename to apps/website-new/docs/zh/guide/data/data-prefetch.mdx index e88af07c6d8..fedbbac3ca1 100644 --- a/apps/website-new/docs/zh/guide/basic/data-fetch-prefetch.mdx +++ b/apps/website-new/docs/zh/guide/data/data-prefetch.mdx @@ -1,4 +1,4 @@ -# Prefetch +# 数据预取 `prefetch` 函数用于预取远程模块的资源和**数据**,从而提升应用的性能和用户体验。通过在用户访问某个功能之前提前加载所需内容,可以显著减少等待时间。 diff --git a/apps/website-new/docs/zh/guide/debug/chrome-devtool.mdx b/apps/website-new/docs/zh/guide/debug/chrome-devtool.mdx index 2d28b5031d6..1644ca7e23c 100644 --- a/apps/website-new/docs/zh/guide/debug/chrome-devtool.mdx +++ b/apps/website-new/docs/zh/guide/debug/chrome-devtool.mdx @@ -8,6 +8,7 @@ - 切换线上页面`Module Federation`版本,来进行快速的功能验证 - 支持查看模块依赖信息 - 支持筛选指定模块依赖信息 +- 支持加载追踪,记录 remote、shared、组件加载信号和失败报告 ::: tip 关于 Chrome Devtool 的限制: @@ -15,6 +16,12 @@ ::: +::: tip 加载追踪能力上线说明 + +包含「加载追踪」Tab 的 Chrome 插件版本还在审核中,即将上线 Chrome 应用商店。 + +::: + ## 使用场景 DevTools 提供了多个功能面板,适用于开发环境以及生产环境的不同调试需求: @@ -32,6 +39,12 @@ DevTools 提供了多个功能面板,适用于开发环境以及生产环境 - 分析共享依赖的版本复用情况(Loaded / Reused)。 - 检查单例(Singleton)、严格版本(Strict Version)等配置的生效状态。 +- **加载追踪**:记录当前页面的 Module Federation 加载过程。 + - 查看 `loadRemote`、`loadShare` 的开始、成功、失败和恢复状态。 + - 查看当前是谁在加载哪个 remote、哪个 expose,以及 shared 使用了哪个 provider 和版本。 + - 如果页面已经接入 [观测插件](../../plugin/plugins/observability-plugin),面板会读取页面已有报告,状态显示为 `CUSTOM`。 + - 如果页面没有接入观测插件,可以在面板中点击「开启采集」,Chrome 插件会为当前 Tab 临时注入采集插件。 + ![](https://module-federation-assest.netlify.app/document/guide/chrome-devtools/shared-overview.png) ## 如何安装 @@ -57,6 +70,59 @@ DevTools 提供了多个功能面板,适用于开发环境以及生产环境 - **支持多 Tab 隔离**:在多个标签页中同时打开使用了 Module Federation 的页面时,每个 Tab 的代理规则和模块信息都是相互独立的。你在 Tab A 中设置的代理规则不会影响 Tab B,反之亦然。这允许你同时调试多个环境或应用状态。 ![](https://module-federation-assest.netlify.app/document/guide/chrome-devtools/proxy.png) +{/* +## 加载追踪 + +「加载追踪」Tab 用于回答“当前页面的 MF 加载到底发生了什么”。它适合在页面空白、组件一直 loading、shared 版本不确定、生产者偶发失败时使用。 + +![](https://module-federation-assest.netlify.app/document/guide/chrome-devtools/loading-trace-overview.png) + +面板顶部的状态含义如下: + +- `OFF`:当前页面还没有可读取的加载报告。 +- `ON`:Chrome 插件已经为当前 Tab 开启采集。 +- `CUSTOM`:当前页面已经自己注册了观测插件,面板正在读取页面已有报告。 + +常用流程: + +1. 打开目标页面。 +2. 打开 Chrome DevTools,进入 `Module Federation` 面板。 +3. 点击「加载追踪」。 +4. 如果状态是 `OFF`,点击「开启采集」,页面会刷新一次。 +5. 复现问题,或者触发需要观察的远程组件加载。 +6. 在报告列表中选择一条记录,查看当前加载、历史加载记录、事件时间线和失败建议。 +7. 点击「导出」保存完整报告。 + +报告状态可以这样理解: + +- `成功`:这条 MF 链路已经完成。remote 加载成功会显示 remote 结果;shared 解析成功会显示 provider 和版本。 +- `失败`:这条链路出现错误,优先看右侧的失败阶段、错误码和建议。 +- `进行中`:只看到了开始事件,还没有看到完成、失败或恢复事件。 +- `兜底成功`:加载过程中出现过问题,但运行时通过 fallback 或恢复路径继续执行。 +- `基础观测`:当前运行时版本较低或无法识别,只能看到基础事件,细阶段可能缺失。 + +### 导出报告后怎么分析 + +导出的 JSON 会包含 `config`、`scopes` 和 `reports`。每条 report 里最重要的是: + +![](https://module-federation-assest.netlify.app/document/guide/chrome-devtools/loading-trace-shared-report.png) + +- `diagnosis`:面向排查的结论、证据和下一步建议。 +- `summary`:最终状态,例如 `runtime-loaded`、`component-loaded`、`shared-resolved`、`failed`、`recovered`、`pending`。 +- `remote` / `shared`:当前加载对象。shared 报告里重点看 provider、要求版本、实际版本和可用版本。 +- `loadedBefore`:同一个生产者之前是否已经被其他消费者加载过。 +- `events`:完整事件顺序,用于确认卡在哪一步。 + +可以把导出的报告直接交给 AI coding agent: + +```text +/mf observability + +我这里有一份 Chrome DevTools 导出的 MF 加载追踪报告。 +请帮我判断这次加载是否成功,失败点在哪里,最可能是谁的问题,以及下一步怎么修。 + +<粘贴导出的 JSON> +``` */} ## 如何将本地开发的模块代理到线上 diff --git a/apps/website-new/docs/zh/guide/framework/_meta.json b/apps/website-new/docs/zh/guide/framework/_meta.json deleted file mode 100644 index 0e5f843e429..00000000000 --- a/apps/website-new/docs/zh/guide/framework/_meta.json +++ /dev/null @@ -1 +0,0 @@ -["modernjs","nextjs"] diff --git a/apps/website-new/docs/zh/guide/performance/_meta.json b/apps/website-new/docs/zh/guide/performance/_meta.json deleted file mode 100644 index 9738874ed56..00000000000 --- a/apps/website-new/docs/zh/guide/performance/_meta.json +++ /dev/null @@ -1,3 +0,0 @@ -[ - "shared-tree-shaking" -] diff --git a/apps/website-new/docs/zh/guide/runtime/index.mdx b/apps/website-new/docs/zh/guide/runtime/index.mdx index 57aa84a9279..06a4d138651 100644 --- a/apps/website-new/docs/zh/guide/runtime/index.mdx +++ b/apps/website-new/docs/zh/guide/runtime/index.mdx @@ -37,31 +37,44 @@ import Runtime from '@components/zh/runtime/index'; ## 安装 -import { PackageManagerTabs } from '@theme'; +不同项目需要安装和引用不同的 Runtime 入口。大多数项目默认安装 `@module-federation/enhanced`,并从 `@module-federation/enhanced/runtime` 引用 Runtime API。Modern.js 项目如果已经使用 Module Federation 插件,应该从对应插件的 `/runtime` 入口引用 Runtime API,保证框架插件和手动调用的 Runtime 使用同一个实例。 + +import { PackageManagerTabs, Tab, Tabs } from '@theme'; + +### @module-federation/enhanced -::: tip 注意: +```ts +import { createInstance, loadRemote } from '@module-federation/enhanced/runtime'; +``` -- 以下 `Federation Runtime` 示例我们均展示脱离特定框架如 Modern.js 的 case, 所以 API 将均从初始 `@module-federation/enhanced/runtime` 包中导出。 +### Modern.js -- 如果你的项目是 Modern.js 项目且使用 `@module-federation/modern-js-v3`,运行时应该从 `@module-federation/modern-js-v3/runtime` 中导出运行时 API。这样能保证插件和运行时使用的是同一个运行时实例,保证模块加载正常 + -- 如果你的项目是 Modern.js 项目但是没有使用 `@module-federation/modern-js-v3`,则应当从 `@module-federation/enhanced/runtime` 导出 runtime API。但是我们推荐你使用 `@module-federation/modern-js-v3` 进行模块注册和加载,这将使你享受到更多和框架结合的能力。 +```ts +import { createInstance, loadRemote } from '@module-federation/modern-js-v3/runtime'; +``` -::: +如果是 Modern.js v2 项目,请安装 `@module-federation/modern-js`,并从 `@module-federation/modern-js/runtime` 引用 Runtime API。 ## 模块注册 -import { Steps, Tab, Tabs } from '@theme'; - ```tsx @@ -323,3 +336,9 @@ import { Steps, Tab, Tabs } from '@theme'; ``` + +## 继续阅读 + +- [Runtime API](./runtime-api):查看 `createInstance`、`loadRemote`、`registerRemotes` 等 API。 +- [Runtime 插件](./runtime-plugins):了解如何扩展运行时加载流程。 +- [Runtime Hooks](./runtime-hooks):查看插件可使用的生命周期。 diff --git a/apps/website-new/docs/zh/guide/runtime/runtime-api.mdx b/apps/website-new/docs/zh/guide/runtime/runtime-api.mdx index 68e935183c3..047f0460a31 100644 --- a/apps/website-new/docs/zh/guide/runtime/runtime-api.mdx +++ b/apps/website-new/docs/zh/guide/runtime/runtime-api.mdx @@ -125,6 +125,7 @@ type Share = { + ```ts import { init, loadRemote } from '@module-federation/enhanced/runtime'; @@ -141,6 +142,7 @@ init({ ], }); ``` + ### 推荐替代方式 @@ -244,6 +246,7 @@ plugins: [mfRuntimePlugin()] 当使用构建插件或 [init](#init) 创建默认实例后,可以调用 `getInstance()` 获取这个默认实例。 + ```ts import { getInstance } from '@module-federation/enhanced/runtime'; @@ -255,6 +258,8 @@ if (!mfInstance) { mfInstance.loadRemote('remote/util'); ``` +如果没有使用构建插件,调用 `getInstance` 会抛出异常,此时你需要使用 [createInstance](#createinstance) 来创建一个新的实例。 + 通过 [createInstance](#createinstance) 创建的实例不会替换默认实例,但仍然会注册到全局实例列表中。所以即使你没有保存它的返回值,也可以通过给 `getInstance` 传入 finder 回调把它找回来。 finder 回调的行为和 `Array.prototype.find` 类似:运行时会遍历当前已注册的实例,并返回第一个匹配项。如果没有找到匹配实例,`getInstance` 会返回 `null`。 @@ -468,11 +473,10 @@ function registerGlobalPlugins(plugins: ModuleFederationRuntimePlugin[]): void { 全局插件会按 `plugin.name` 去重。 + ```ts -import { - registerGlobalPlugins, - createInstance, -} from '@module-federation/enhanced/runtime'; +import { registerGlobalPlugins, createInstance } from '@module-federation/enhanced/runtime'; + import runtimePlugin from './runtime-plugin'; registerGlobalPlugins([runtimePlugin()]); @@ -488,136 +492,8 @@ const mf = createInstance({ }); ``` -为了保证行为可预期,建议在创建或使用 runtime 实例之前注册全局插件。 - -## getRemoteInfo - -- type - -```typescript -function getRemoteInfo(remote: Remote): RemoteInfo {} -``` - -把 remote 配置归一化为 runtime 内部使用的 `RemoteInfo` 结构。 - -这个 helper 会补齐一些默认值,比如: - -- `type` -- `entryGlobalName` -- `shareScope` - -```ts -import { getRemoteInfo } from '@module-federation/enhanced/runtime'; - -const remoteInfo = getRemoteInfo({ - name: 'sub1', - entry: 'http://localhost:2001/mf-manifest.json', -}); - -console.log(remoteInfo.entryGlobalName); -console.log(remoteInfo.shareScope); -``` - -这是一个偏底层的 helper。大多数应用仍然应该优先使用 `createInstance`、`registerRemotes` 或构建插件去管理 remotes。 - -## getRemoteEntry -- type - -```typescript -function getRemoteEntry(params: { - origin: ModuleFederation; - remoteInfo: RemoteInfo; - remoteEntryExports?: RemoteEntryExports; - getEntryUrl?: (url: string) => string; -}): Promise {} -``` - -用于加载 remote entry,并返回对应的 entry exports。 - -这个 API 更适合底层工具、调试、自定义 loader 或高级 runtime 集成。大多数应用应该优先使用 `loadRemote`。 - -```ts -import { - createInstance, - getRemoteInfo, - getRemoteEntry, -} from '@module-federation/enhanced/runtime'; - -const mf = createInstance({ - name: 'mf_host', - remotes: [], -}); - -const remoteInfo = getRemoteInfo({ - name: 'sub1', - entry: 'http://localhost:2001/mf-manifest.json', -}); - -const remoteEntryExports = await getRemoteEntry({ - origin: mf, - remoteInfo, -}); -``` - -## loadScript - -- type - -```typescript -function loadScript( - url: string, - info: { - attrs?: Record; - createScriptHook?: CreateScriptHookDom; - }, -): Promise {} -``` - -浏览器侧的底层 helper,用于插入并加载 remote entry script。 - -只有在你需要绕过 `loadRemote` / `getRemoteEntry`、自己控制 script 加载流程时才需要直接使用它。 - -```ts -import { loadScript } from '@module-federation/enhanced/runtime'; - -await loadScript('http://localhost:2001/remoteEntry.js', { - attrs: { - crossorigin: 'anonymous', - }, -}); -``` - -## loadScriptNode - -- type - -```typescript -function loadScriptNode( - url: string, - info: { - attrs?: Record; - loaderHook?: { - createScriptHook?: CreateScriptHookNode; - }; - }, -): Promise {} -``` - -Node.js 侧的底层 helper,用于把 remote entry 加载到当前进程。 - -适合 Node 端集成或自定义 loader。浏览器环境请使用 `loadScript`。 - -```ts -import { loadScriptNode } from '@module-federation/enhanced/runtime'; - -await loadScriptNode('http://localhost:2001/remoteEntry.js', { - attrs: { - name: 'sub1', - globalName: 'sub1', - }, -}); -``` +为了保证行为可预期,建议在创建或使用 runtime 实例之前注册全局插件。 ## registerShared @@ -922,6 +798,75 @@ type PreloadRemoteArgs = { * `remote` 的同步资源还是异步资源 * `remote` 依赖的 `remote` 资源 +`preloadRemote` 会等待本次预加载涉及的资源完成。如果资源全部成功加载或命中缓存,Promise 会 resolve;如果有资源加载失败或超时,Promise 会 reject,并在错误对象上携带本次预加载的资源结果。 + +如果只关心最终结果,可以直接用 `await` 或 `.then/.catch` 判断: + +```ts +import { preloadRemote } from '@module-federation/enhanced/runtime'; + +try { + await preloadRemote([ + { + nameOrAlias: 'sub1', + exposes: ['add'], + resourceCategory: 'all', + }, + ]); + + console.log('sub1/add preload success'); +} catch (error) { + console.error('sub1/add preload failed', error); +} +``` + +如果需要统计具体哪些资源成功、失败、超时或命中缓存,可以读取错误对象上的 `results`: + +```ts +type PreloadRemoteError = Error & { + results?: Array<{ + id: string; + results: Array<{ + url: string; + status: 'success' | 'error' | 'timeout' | 'cached'; + resourceType: 'manifest' | 'remoteEntry' | 'js' | 'css'; + error?: unknown; + }>; + }>; +}; + +preloadRemote([ + { + nameOrAlias: 'sub1', + exposes: ['add'], + resourceCategory: 'all', + }, +]).catch((error: PreloadRemoteError) => { + const failedResources = + error.results + ?.flatMap((remoteResult) => + remoteResult.results.map((resource) => ({ + id: remoteResult.id, + ...resource, + })), + ) + .filter( + (resource) => + resource.status === 'error' || resource.status === 'timeout', + ) ?? []; + + failedResources.forEach((resource) => { + console.error( + `[preloadRemote] ${resource.id} ${resource.resourceType} failed`, + resource.url, + resource.error, + ); + }); +}); +``` + +如果没有指定 `exposes`,本次预加载的资源 id 是 `remoteName/*`。如果指定了 `exposes`,运行时会按 expose 单独生成资源,资源 id 是 `remoteName/expose`。 + ```tsx diff --git a/apps/website-new/docs/zh/guide/runtime/runtime-hooks.mdx b/apps/website-new/docs/zh/guide/runtime/runtime-hooks.mdx index 7522fddf814..a4260b78bd1 100644 --- a/apps/website-new/docs/zh/guide/runtime/runtime-hooks.mdx +++ b/apps/website-new/docs/zh/guide/runtime/runtime-hooks.mdx @@ -1,5 +1,11 @@ # Runtime Hooks +## Hook 返回值 + +对于 `SyncHook` 和 `AsyncHook`,返回 `undefined` 表示插件只观察事件,不会清空前一个插件已经返回的结果。后续插件如果返回新的非 `undefined` 值,仍然可以替换这个结果。 + +对于 `SyncWaterfallHook` 和 `AsyncWaterfallHook`,需要改写参数时返回完整的新 args 对象;返回 `undefined` 会保留当前 args,并继续传给下一个插件。 + ## beforeInit `SyncWaterfallHook` @@ -64,6 +70,28 @@ type BeforeRequestOptions ={ } ``` +## afterMatchRemote + +`AsyncHook` + +在 runtime 把一次 `loadRemote` 请求匹配到具体 remote 之后触发。如果匹配失败,也会带着 `error` 触发。适合用于诊断、链路追踪,以及判断问题是否发生在 manifest 或 remoteEntry 加载之前。 + +* type + +```ts +async function afterMatchRemote(args: AfterMatchRemoteOptions): Promise + +type AfterMatchRemoteOptions ={ + id: string; + options: ModuleFederationRuntimeOptions; + remote?: Remote; + expose?: string; + remoteInfo?: RemoteInfo; + error?: unknown; + origin: ModuleFederation; +} +``` + ## afterResolve `AsyncWaterfallHook` @@ -128,6 +156,33 @@ interface RemoteInfo { } ``` +## afterLoadRemote + +`AsyncHook` + +在一次 `loadRemote` 请求结束后触发。加载成功、加载失败、以及通过 `errorLoadRemote` 兜底恢复的场景都会触发。适合记录 remote 加载的最终状态。 + +成功加载时,`onLoad` 会先于 `afterLoadRemote` 触发。如果加载失败后由 `errorLoadRemote` 返回兜底结果,`afterLoadRemote` 会带上原始 `error` 和 `recovered: true`。 + +* type + +```ts +async function afterLoadRemote(args: AfterLoadRemoteOptions): Promise + +type AfterLoadRemoteOptions ={ + id: string; + expose?: string; + remote?: RemoteInfo; + options?: { + loadFactory?: boolean; + from?: 'build' | 'runtime'; + }; + error?: unknown; + recovered?: boolean; + origin: ModuleFederation; +} +``` + ## beforeInitContainer `AsyncWaterfallHook` @@ -209,6 +264,8 @@ type ErrorLoadRemoteOptions ={ options?: any; from: 'build' | 'runtime'; lifecycle: 'beforeRequest' | 'beforeLoadShare' | 'afterResolve' | 'onLoad'; + remote?: RemoteInfo; + expose?: string; origin: ModuleFederation; } ``` @@ -218,12 +275,15 @@ type ErrorLoadRemoteOptions ={ - `beforeRequest`: 处理 remote 请求参数阶段出错 - `afterResolve`: 解析/拉取 manifest 阶段出错(常见于网络异常) - `onLoad`: 加载 exposes 模块阶段出错 -- `beforeLoadShare`: 加载 shared 依赖阶段出错 +- `beforeLoadShare`: shared 初始化过程中加载 remoteEntry 出错 + +如果这个 hook 返回兜底模块,runtime 会使用这个值继续执行。如果诊断或日志插件返回 `undefined`,不会清空其他插件已经返回的兜底结果。 * example + ```ts -import { createInstance } from '@module-federation/enhanced/runtime' +import { createInstance } from '@module-federation/enhanced/runtime'; import type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime'; @@ -256,6 +316,7 @@ mf.loadRemote('app1/un-existed-module').then(mod=>{ }) ``` + ## beforeLoadShare `AsyncWaterfallHook` @@ -275,6 +336,51 @@ type BeforeLoadShareOptions ={ } ``` +## afterLoadShare + +`SyncHook` + +在 `loadShare` 或 `loadShareSync` 成功解析 shared 依赖后触发。适合观察最终选中了哪个提供方和版本。 + +* type + +```ts +function afterLoadShare(args: AfterLoadShareOptions): void + +type AfterLoadShareOptions ={ + pkgName: string; + shareInfo?: Partial; + selectedShared?: Partial; + shared: Options['shared']; + shareScopeMap: ShareScopeMap; + lifecycle: 'loadShare' | 'loadShareSync'; + origin: ModuleFederation; +} +``` + +## errorLoadShare + +`SyncHook` + +在 shared 依赖解析失败,或者无法选中可用 shared 依赖时触发。适合诊断 shared 缺失、版本不匹配、eager 配置错误等问题。 + +* type + +```ts +function errorLoadShare(args: ErrorLoadShareOptions): void + +type ErrorLoadShareOptions ={ + pkgName: string; + shareInfo?: Partial; + shared: Options['shared']; + shareScopeMap: ShareScopeMap; + lifecycle: 'loadShare' | 'loadShareSync'; + origin: ModuleFederation; + error?: unknown; + recovered?: boolean; +} +``` + ## initContainerShareScopeMap `SyncWaterfallHook` @@ -324,8 +430,9 @@ type ResolveShareOptions ={ * example + ```ts -import { createInstance, loadRemote } from '@module-federation/enhanced/runtime' +import { createInstance, loadRemote } from '@module-federation/enhanced/runtime'; import type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime'; @@ -378,6 +485,7 @@ mf.loadShare('react').then((reactFactory) => { }); ``` + ## beforePreloadRemote `AsyncHook` @@ -427,6 +535,131 @@ interface PreloadAssets { `loaderHook` 用于拦截资源加载与工厂获取流程。 +## beforeInitRemote + +`AsyncHook` + +在调用 `remoteEntry.init(...)` 初始化 remote 容器之前触发。 + +* type + +```ts +async function beforeInitRemote(args: BeforeInitRemoteOptions): Promise + +type BeforeInitRemoteOptions ={ + id?: string; + remoteInfo: RemoteInfo; + remoteSnapshot?: ModuleInfo; + origin: ModuleFederation; +} +``` + +## afterInitRemote + +`AsyncHook` + +在 remote 容器初始化成功或失败后触发。如果 remote 已经初始化过,会带上 `cached: true`。 + +* type + +```ts +async function afterInitRemote(args: AfterInitRemoteOptions): Promise + +type AfterInitRemoteOptions ={ + id?: string; + remoteInfo: RemoteInfo; + remoteSnapshot?: ModuleInfo; + remoteEntryExports?: RemoteEntryExports; + error?: unknown; + cached?: boolean; + origin: ModuleFederation; +} +``` + +## beforeGetExpose + +`AsyncHook` + +在调用 `remoteEntry.get(expose)` 之前触发。 + +* type + +```ts +async function beforeGetExpose(args: BeforeGetExposeOptions): Promise + +type BeforeGetExposeOptions ={ + id: string; + expose: string; + moduleInfo: RemoteInfo; + remoteEntryExports: RemoteEntryExports; + origin: ModuleFederation; +} +``` + +## afterGetExpose + +`AsyncHook` + +在 `remoteEntry.get(expose)` 成功或失败后触发。 + +* type + +```ts +async function afterGetExpose(args: AfterGetExposeOptions): Promise + +type AfterGetExposeOptions ={ + id: string; + expose: string; + moduleInfo: RemoteInfo; + remoteEntryExports: RemoteEntryExports; + moduleFactory?: () => unknown | Promise; + error?: unknown; + origin: ModuleFederation; +} +``` + +## beforeExecuteFactory + +`AsyncHook` + +在执行 exposed module factory 之前触发。`loadRemote` 使用 `loadFactory: false` 时不会触发。 + +* type + +```ts +async function beforeExecuteFactory(args: BeforeExecuteFactoryOptions): Promise + +type BeforeExecuteFactoryOptions ={ + id: string; + expose: string; + moduleInfo: RemoteInfo; + loadFactory: boolean; + origin: ModuleFederation; +} +``` + +## afterExecuteFactory + +`AsyncHook` + +在 exposed module factory 执行成功或失败后触发。`loadRemote` 使用 `loadFactory: false` 时不会触发。 + +* type + +```ts +async function afterExecuteFactory(args: AfterExecuteFactoryOptions): Promise + +type AfterExecuteFactoryOptions ={ + id: string; + expose: string; + moduleInfo: RemoteInfo; + loadFactory: boolean; + exposeModule?: unknown; + error?: unknown; + origin: ModuleFederation; +} +``` + ## createScript `SyncHook` @@ -441,9 +674,27 @@ function createScript(args: CreateScriptOptions): CreateScriptHookReturn type CreateScriptOptions ={ url: string; attrs?: Record; + remoteInfo?: RemoteInfo; + resourceContext?: ResourceLoadContext; +} + +type CreateScriptHookReturn = + | HTMLScriptElement + | { script?: HTMLScriptElement; timeout?: number } + | void; + +type ResourceLoadContext = { + initiator: 'loadRemote' | 'preloadRemote'; + id: string; + resourceType: 'manifest' | 'remoteEntry' | 'js' | 'css'; + url?: string; } ``` +`timeout` 单位为毫秒,用于设置脚本加载超时时间。默认值为 `20000`。 +`resourceContext` 用于判断这次资源加载来自 `loadRemote` 还是 `preloadRemote`,以及对应的资源类型和 id。 +可以结合 `timeout` 和 `resourceContext`,为实际远程加载和预加载设置不同的超时时间。 + * example ```ts @@ -459,13 +710,17 @@ const changeScriptAttributePlugin: () => ModuleFederationRuntimePlugin = script.src = testRemoteEntry; script.setAttribute('loader-hooks', 'isTrue'); script.setAttribute('crossorigin', 'anonymous'); - return script; + return { + script, + timeout: 30000, + }; } } }; }; ``` + ## fetch `fetch` 函数允许自定义获取清单(manifest)JSON 的请求。成功的 `Response` 必须返回一个有效的 JSON。 @@ -474,15 +729,23 @@ const changeScriptAttributePlugin: () => ModuleFederationRuntimePlugin = - **Type** ```typescript -function fetch(manifestUrl: string, requestInit: RequestInit): Promise | void | false; +function fetch( + manifestUrl: string, + requestInit: RequestInit, + remoteInfo?: RemoteInfo, + resourceContext?: ResourceLoadContext, +): Promise | void | false; ``` +`resourceContext` 可用于判断这次 manifest 请求来自 `loadRemote` 还是 `preloadRemote`。 + - 示例:在获取清单(manifest)JSON 时包含凭证: -```typescript -// fetch-manifest-with-credentials-plugin.ts + +```ts import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime'; +// fetch-manifest-with-credentials-plugin.ts export default function (): FederationRuntimePlugin { return { name: 'fetch-manifest-with-credentials-plugin', @@ -496,6 +759,7 @@ export default function (): FederationRuntimePlugin { }; ``` + ## createLink `SyncHook` @@ -510,7 +774,44 @@ function createLink(args: CreateLinkOptions): HTMLLinkElement | void type CreateLinkOptions ={ url: string; attrs?: Record; + remoteInfo?: RemoteInfo; + resourceContext?: ResourceLoadContext; } + +type CreateLinkHookReturn = + | HTMLLinkElement + | { link?: HTMLLinkElement; timeout?: number } + | void; +``` + +`resourceContext` 的结构和 `createScript` 一致,可用于区分预加载的 JS/CSS、实际加载的 remoteEntry,以及它们对应的资源 id。 +`timeout` 单位为毫秒,用于设置 link 加载超时时间。默认值为 `20000`。 + +示例:让实际加载 remoteEntry 时等待更久,让低优先级预加载资源更快结束。 + +```ts +import type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime'; + +const resourceTimeoutPlugin = (): ModuleFederationRuntimePlugin => ({ + name: 'resource-timeout-plugin', + createScript({ resourceContext }) { + if ( + resourceContext?.initiator === 'loadRemote' && + resourceContext.resourceType === 'remoteEntry' + ) { + return { + timeout: 30000, + }; + } + }, + createLink({ resourceContext }) { + if (resourceContext?.initiator === 'preloadRemote') { + return { + timeout: 5000, + }; + } + }, +}); ``` ## loadEntryError @@ -534,6 +835,26 @@ type LoadEntryErrorOptions ={ } ``` +## afterLoadEntry + +`AsyncHook` + +在 remoteEntry 加载成功、失败,或被 `loadEntryError` 恢复之后触发。 + +* type + +```ts +async function afterLoadEntry(args: AfterLoadEntryOptions): Promise + +type AfterLoadEntryOptions ={ + origin: ModuleFederation; + remoteInfo: RemoteInfo; + remoteEntryExports?: RemoteEntryExports | false | void; + error?: unknown; + recovered?: boolean; +} +``` + ## getModuleFactory `AsyncHook` @@ -588,11 +909,13 @@ export type RemoteEntryExports = { - 示例:加载 JSON 数据 -```typescript -// load-json-data-plugin.ts + +```ts import { init } from '@module-federation/enhanced/runtime'; + import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime'; +// load-json-data-plugin.ts const changeScriptAttributePlugin: () => FederationRuntimePlugin = function () { return { name: 'load-json-data-plugin', @@ -613,6 +936,7 @@ const changeScriptAttributePlugin: () => FederationRuntimePlugin = function () { }; }; ``` + ```ts // module-federation-config { @@ -629,11 +953,13 @@ jsonA // {...json data} - 示例:模块代理(Delegate Modules) -```typescript -// delegate-modules-plugin.ts + +```ts import { init } from '@module-federation/enhanced/runtime'; + import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime'; +// delegate-modules-plugin.ts const changeScriptAttributePlugin: () => FederationRuntimePlugin = function () { return { name: 'delegate-modules-plugin', @@ -653,6 +979,7 @@ const changeScriptAttributePlugin: () => FederationRuntimePlugin = function () { }; }; ``` + ```ts // ./src/delegateModulesA.js export async function test1() { @@ -688,7 +1015,7 @@ test2 // "test2 value" ## bridgeHook -`bridgeHook` 定义在 `runtime-core/src/core.ts`,用于桥接渲染/销毁阶段(如 React/Vue bridge)扩展上下文。 +`bridgeHook` 用于桥接渲染/销毁阶段(如 React/Vue bridge)扩展上下文。 ## beforeBridgeRender diff --git a/apps/website-new/docs/zh/guide/runtime/runtime-plugins.mdx b/apps/website-new/docs/zh/guide/runtime/runtime-plugins.mdx index 4d20c38d747..f97e91a6107 100644 --- a/apps/website-new/docs/zh/guide/runtime/runtime-plugins.mdx +++ b/apps/website-new/docs/zh/guide/runtime/runtime-plugins.mdx @@ -17,7 +17,8 @@ Runtime 插件用于改写 **运行时行为**,而不是改写 runtime 本身 一个 runtime 插件本质上是一个返回 `ModuleFederationRuntimePlugin` 的函数: -```ts title="my-runtime-plugin.ts" + +```ts import type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime'; export default function myRuntimePlugin(): ModuleFederationRuntimePlugin { @@ -27,6 +28,7 @@ export default function myRuntimePlugin(): ModuleFederationRuntimePlugin { } ``` + 用函数返回插件实例,而不是直接导出对象,主要有几个好处: - 可以接收 options @@ -69,8 +71,10 @@ export default { 如果插件依赖用户态、环境变量、特性开关、启动后拿到的数据,适合运行时注册。 -```ts title="bootstrap.ts" + +```ts import { createInstance } from '@module-federation/enhanced/runtime'; + import rewriteRemoteEntryPlugin from './plugins/rewrite-remote-entry'; const mf = createInstance({ @@ -91,6 +95,7 @@ mf.registerPlugins([ ]); ``` + ### 全局运行时注册 如果你希望插件对之后创建的所有 runtime 实例都生效,而不是只绑定到某一个当前实例,可以使用 `registerGlobalPlugins(...)`。 @@ -101,11 +106,10 @@ mf.registerPlugins([ - 跨应用统一策略 - host 级默认插件 -```ts title="bootstrap.ts" -import { - registerGlobalPlugins, - createInstance, -} from '@module-federation/enhanced/runtime'; + +```ts +import { registerGlobalPlugins, createInstance } from '@module-federation/enhanced/runtime'; + import runtimePlugin from './plugins/runtime-plugin'; registerGlobalPlugins([runtimePlugin()]); @@ -121,6 +125,7 @@ const mf = createInstance({ }); ``` + `registerGlobalPlugins(...)` 会按 `name` 去重。实践里建议在创建或使用 runtime 实例之前调用,这样全局插件会更稳定地并入实例插件链。 ## 怎么选 hook @@ -133,6 +138,8 @@ const mf = createInstance({ | 自定义 script / link 注入 | `createScript`、`createLink` | 需要加 `crossorigin`、timeout、特殊 script/link 属性;在具备 remote 上下文时会额外提供 `remoteInfo`,便于按 remote 做策略 | | 在 remote init 前对齐 / 改写共享池 | `beforeInitContainer`、`initContainerShareScopeMap` | 需要控制 remote 初始化时使用哪个 share scope | | 改写 shared 最终命中结果 | `resolveShare` | 需要强制命中某个 shared,而不是用 runtime 默认选择 | +| 观测 remote 加载状态 | `afterMatchRemote`、`afterLoadRemote` | 需要记录 `loadRemote` 的请求链路或最终成功/失败状态 | +| 观测 shared 依赖状态 | `afterLoadShare`、`errorLoadShare` | 需要诊断 shared 缺失、版本不匹配、eager 配置错误等问题 | | 加载失败时兜底 | `errorLoadRemote` | 需要离线兜底、fallback module、分层恢复策略 | | 扩展新的 remote 加载方式 | `loadEntry` | 需要完整接管 remote entry 加载过程,或实现新的 remote 类型 | @@ -147,7 +154,8 @@ const mf = createInstance({ - 基于注册中心改写地址 - 域名归一化 -```ts title="plugins/rewrite-remote-entry.ts" + +```ts import type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime'; interface RewriteRemoteEntryOptions { @@ -185,6 +193,7 @@ export default function rewriteRemoteEntryPlugin( } ``` + 为什么选这个 hook: - `beforeRequest` 太早,此时还拿不到解析后的 entry @@ -202,7 +211,8 @@ export default function rewriteRemoteEntryPlugin( 当你需要改 manifest 请求本身时,使用 `fetch`。 -```ts title="plugins/fetch-manifest-with-credentials.ts" + +```ts import type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime'; export default function fetchManifestWithCredentials(): ModuleFederationRuntimePlugin { @@ -225,6 +235,7 @@ export default function fetchManifestWithCredentials(): ModuleFederationRuntimeP } ``` + `fetch` 常见用途: - 带凭证请求 @@ -240,7 +251,8 @@ export default function fetchManifestWithCredentials(): ModuleFederationRuntimeP 关键点:只改 `args.scope`、`args.version` 这类字段,并不会自动改变最终命中项。要真正改掉结果,必须替换 `args.resolver`。 -```ts title="plugins/prefer-host-react.ts" + +```ts import type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime'; export default function preferHostReact(): ModuleFederationRuntimePlugin { @@ -273,6 +285,7 @@ export default function preferHostReact(): ModuleFederationRuntimePlugin { } ``` + ## 容错与 `shareStrategy` 做运行时容错时,`errorLoadRemote` 很关键。 diff --git a/apps/website-new/docs/zh/guide/start/_meta.json b/apps/website-new/docs/zh/guide/start/_meta.json index 29772c43d6a..c38e165702b 100644 --- a/apps/website-new/docs/zh/guide/start/_meta.json +++ b/apps/website-new/docs/zh/guide/start/_meta.json @@ -1 +1 @@ -["index", "setting-up-env", "quick-start", "features", "glossary", "npm-packages"] +["index", "quick-start", "glossary"] diff --git a/apps/website-new/docs/zh/guide/start/features.mdx b/apps/website-new/docs/zh/guide/start/features.mdx deleted file mode 100644 index 90a59ad8454..00000000000 --- a/apps/website-new/docs/zh/guide/start/features.mdx +++ /dev/null @@ -1,18 +0,0 @@ -# 功能导航 - -在这里,你可以了解到 Module Federation 支持的主要功能。不同于 Webpack、Rspack 内置的 Module federation,这里的 Module Federation 基于 Builder 构建工具的能力基础之上提供了更加丰富的能力支持,以满足大型 Web 应用开发的各种诉求 - -## 基础 - -| 功能 | 描述 | -| ----------------------------------------------------------------- | ---------------------------------------------------------- | -| [Federation Runtime](../runtime/index) | 可以脱离构建插件:注册远程模块、消费远程模块、注册共享依赖 | -| [Build Plugins](../build-plugins/plugins) | MF 提供了多种构建插件来帮助消费和生成远程模块 | -| [类型提示](../basic/type-prompt) | 支持动态模块类型提示能力 | - -## 框架 - -| 功能 | 描述 | -| ------------------------------ | ---------------------------------------------- | -| [Modern.js](../framework/modernjs) | 基于 modern.js 提供了 Module federation SSR 能力 | -| [Next.js](../framework/nextjs) | 基于 Next.js 提供了 Module federation SSR 能力 | diff --git a/apps/website-new/docs/zh/guide/start/glossary.mdx b/apps/website-new/docs/zh/guide/start/glossary.mdx index 373a0b902c0..23ce98104a7 100644 --- a/apps/website-new/docs/zh/guide/start/glossary.mdx +++ b/apps/website-new/docs/zh/guide/start/glossary.mdx @@ -27,20 +27,20 @@ ## Bundler -指 [Rspack](https://rspack.dev/)、[Webpack](https://webpack.js.org/) 等模块打包工具。 +指 [Webpack](https://webpack.js.org/)、[Rspack](https://rspack.dev/)、Vite、Metro 等模块打包工具。 打包工具的主要目标是将 JavaScript、CSS 等文件打包在一起,打包后的文件可以在浏览器、Node.js 等环境中使用。当 Bundler 处理 Web 应用时,它会构建一个依赖关系图,其中包含应用需要的各个模块,然后将所有模块打包成一个或多个 bundle。 -## Rspack +具体项目应该选择哪个工具和插件,可以查看[接入方案](/integrations)。 -[Rspack](https://www.rspack.dev/) 是一个基于 Rust 的高性能 Web 构建工具,具备与 webpack 生态系统的互操作性,可以被 webpack 项目低成本集成,并提供更好的构建性能。 +## Manifest -相较于 webpack,Rspack 的构建性能有明显提升,除了 Rust 带来的语言优势,这也来自于它的并行架构和增量编译等特性。经过 benchmark 验证,Rspack 可以带来 5 ~ 10 倍编译性能的提升。 +Manifest 是生产者在构建时生成的运行时清单,用于描述远程入口、暴露模块、资源、共享依赖和类型信息。消费者可以通过 `mf-manifest.json` 获取这些信息,再加载对应的远程模块。 -## Rsbuild +更多内容可以查看 [Manifest 与 Snapshot](/guide/basic/manifest-snapshot)、[manifest 配置](/configure/manifest) 和 [mf-manifest.json 字段定义](/configure/manifest-fields)。 -[Rsbuild](https://rsbuild.dev/) 是一个基于 Rspack 的 web 构建工具,具备以下特点: +## Snapshot -- Rsbuild 是一个增强版的 Rspack CLI,更易用、更开箱即用。 -- Rsbuild 是 Rspack 团队对于 web 构建最佳实践的探索和实现。 -- Rsbuild 是 Webpack 应用迁移到 Rspack 的最佳方案,减少 90% 配置,构建快 10 倍。 +Snapshot 是对 Manifest 信息的浓缩和预解析结果。运行时可以从 Manifest 生成 Snapshot;有部署服务时,也可以提前生成并下发 Snapshot,让消费者直接获得远程入口和可预加载资源。 + +Snapshot 会被用于远程模块加载、资源预加载、调试工具、代理和运行时诊断等场景。 diff --git a/apps/website-new/docs/zh/guide/start/index.mdx b/apps/website-new/docs/zh/guide/start/index.mdx index 63377690dba..73a705b6b8d 100644 --- a/apps/website-new/docs/zh/guide/start/index.mdx +++ b/apps/website-new/docs/zh/guide/start/index.mdx @@ -30,7 +30,7 @@ Module Federation 2.0 具有以下特性: - 🧩 [运行时插件系统](../../plugin/dev/index) - 🚀 [动态类型提示](../basic/type-prompt) - 🛠️ [Chrome Devtool](../debug/chrome-devtool) -- 🦀 [Rspack](../build-plugins/plugins-rspack) and [Webpack](../build-plugins/plugins-webpack) Support +- 🦀 [Rspack](/integrations/bundler/rspack) and [Webpack](/integrations/bundler/webpack) Support ### 🎯 定位 @@ -75,14 +75,14 @@ import Step from '@components/Step'; = 16,**我们推荐使用 Node.js 20 的 LTS 版本**。 - -你可以通过以下命令检查当前使用的 Node.js 版本: - -```bash -node -v -``` - -如果你当前的环境中尚未安装 Node.js,或是安装的版本过低,可以通过 [nvm](https://github.com/nvm-sh/nvm) 或 [fnm](https://github.com/Schniz/fnm) 安装需要的版本。 - -下面是通过 nvm 安装 Node.js 20 LTS 版本的例子: - -```bash -# 安装 Node.js 20 的长期支持版本 -nvm install 20 --lts - -# 将刚安装的 Node.js 20 设置为默认版本 -nvm alias default 20 - -# 切换到刚安装的 Node.js 20 -nvm use 20 -``` - -## 使用 Module Federation - -要使用 Module Federation,你需要遵循以下步骤: - -- 识别共享模块: 确定要在应用程序之间共享的模块。 -- 创建共享包/仓库: 将这些模块添加到共享包或代码仓库中。 -- 确保访问权限: 确保每个应用程序都可以访问共享包或代码仓库。 -- 配置构建插件: 配置每个应用程序的 [Webpack](../build-plugins/plugins-webpack)、[Rspack](../build-plugins/plugins-rspack) 配置文件以使用 Module Federation。 -- 使用共享模块: 根据需要在应用程序中使用共享模块。 - -有关更多信息和高级配置选项,请参考 [构建配置](../../../configure/index) 文档。 diff --git a/apps/website-new/docs/zh/guide/troubleshooting/runtime.mdx b/apps/website-new/docs/zh/guide/troubleshooting/runtime.mdx index 47f32ef96bc..71aba60ee17 100644 --- a/apps/website-new/docs/zh/guide/troubleshooting/runtime.mdx +++ b/apps/website-new/docs/zh/guide/troubleshooting/runtime.mdx @@ -20,6 +20,9 @@ import { Tab, Tabs } from '@theme'; - [RUNTIME-009](#runtime-009) - [RUNTIME-010](#runtime-010) - [RUNTIME-012](#runtime-012) +- [RUNTIME-013](#runtime-013) +- [RUNTIME-014](#runtime-014) +- [RUNTIME-015](#runtime-015) ## RUNTIME-001 @@ -349,6 +352,62 @@ ScriptExecutionError 表示脚本已成功下载,重试不会解决此类问 确保宿主与子应用配置的共享模块名称、版本范围一致,避免因无法匹配而导致回退到空的 `getter`。 +## RUNTIME-013 + + + +### 原因 + +manifest 可以访问并解析为 JSON,但它不是有效的 Module Federation manifest。 + +最常见的情况是返回内容缺少必要字段,例如 `metaData`、`exposes` 或 `shared`。这通常说明访问到了错误的 JSON、网关返回了不完整数据,或生产者没有产出正确的 MF manifest。 + +### 解决方法 + +1. 直接打开错误信息里的 manifestUrl,确认返回内容是 MF manifest,而不是其他业务 JSON 或空对象 +2. 检查生产者是否开启了 manifest 输出,并确认构建产物里包含 `metaData`、`exposes`、`shared` +3. 检查网关、部署平台或代理规则是否改写了 manifest 响应 +4. 如果使用观测插件,优先查看报告中的 `diagnosis.facts.url` 和 `diagnosis.actions` + +## RUNTIME-014 + + + +### 原因 + +生产者入口已经加载并初始化,但生产者没有导出本次请求的 expose。 + +常见原因包括: + +1. 消费者请求的 expose 名称写错 +2. 生产者构建配置中没有声明这个 expose +3. `./` 前缀、大小写或别名不一致 +4. 消费者使用了旧的构建产物,生产者最新 expose 还没有部署 + +### 解决方法 + +1. 对比消费者的 `loadRemote('remote/expose')` 请求和生产者的 `exposes` 配置 +2. 检查 expose 是否需要带 `./`,并确认大小写完全一致 +3. 如果通过 manifest 加载,查看 manifest 中 `exposes` 列表是否包含该模块 +4. 如果使用观测插件,查看报告里的 `diagnosis.facts.expose` 和相关 `check-expose` 建议 + +## RUNTIME-015 + + + +### 原因 + +生产者入口文件已经加载,但在初始化容器时失败。 + +这类问题通常发生在生产者执行 `init` 时,例如共享依赖初始化异常、shareScope 数据不符合预期,或生产者入口文件内部逻辑抛错。 + +### 解决方法 + +1. 查看错误信息里的原始异常,它会说明 `init` 过程中真正抛出的错误 +2. 检查消费者和生产者的 shared 配置,尤其是 shareScope、singleton、strictVersion、requiredVersion 和 eager +3. 确认生产者的 remoteEntry 类型和全局名与消费者配置一致 +4. 如果使用观测插件,查看 `summary.phases.remoteEntry`、`diagnosis.facts` 和 `check-shared-provider` 建议 + ## 常见问题(无错误码) ### Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons: diff --git a/apps/website-new/docs/zh/index.md b/apps/website-new/docs/zh/index.md index 7e28ef5d97a..ce617481cf5 100644 --- a/apps/website-new/docs/zh/index.md +++ b/apps/website-new/docs/zh/index.md @@ -11,7 +11,7 @@ hero: link: /zh/blog/v2-stable-version.html - theme: alt text: 快速开始 - link: /zh/ai/index.html + link: /zh/guide/start/quick-start.html image: src: /svg.svg alt: module federation Logo diff --git a/apps/website-new/docs/zh/integrations/_meta.json b/apps/website-new/docs/zh/integrations/_meta.json new file mode 100644 index 00000000000..faea7008e5b --- /dev/null +++ b/apps/website-new/docs/zh/integrations/_meta.json @@ -0,0 +1,36 @@ +[ + { + "type": "file", + "name": "index", + "label": "总览" + }, + { + "type": "dir-section-header", + "name": "build-tool", + "label": "Build Tool" + }, + { + "type": "dir-section-header", + "name": "bundler", + "label": "Bundler" + }, + { + "type": "dir-section-header", + "name": "documentation", + "label": "文档解决方案" + }, + { + "type": "dir-section-header", + "name": "framework", + "label": "Framework", + "collapsible": true, + "collapsed": true + }, + { + "type": "dir-section-header", + "name": "practice", + "label": "实践", + "collapsible": true, + "collapsed": true + } +] diff --git a/apps/website-new/docs/zh/integrations/build-tool/_meta.json b/apps/website-new/docs/zh/integrations/build-tool/_meta.json new file mode 100644 index 00000000000..e0da6374d13 --- /dev/null +++ b/apps/website-new/docs/zh/integrations/build-tool/_meta.json @@ -0,0 +1,17 @@ +[ + { + "type": "file", + "name": "rsbuild", + "label": "Rsbuild" + }, + { + "type": "file", + "name": "rslib", + "label": "Rslib" + }, + { + "type": "file", + "name": "vite", + "label": "Vite" + } +] diff --git a/apps/website-new/docs/zh/guide/build-plugins/plugins-rsbuild.mdx b/apps/website-new/docs/zh/integrations/build-tool/rsbuild.mdx similarity index 75% rename from apps/website-new/docs/zh/guide/build-plugins/plugins-rsbuild.mdx rename to apps/website-new/docs/zh/integrations/build-tool/rsbuild.mdx index ed21279363c..e055113ea50 100644 --- a/apps/website-new/docs/zh/guide/build-plugins/plugins-rsbuild.mdx +++ b/apps/website-new/docs/zh/integrations/build-tool/rsbuild.mdx @@ -1,6 +1,6 @@ # Rsbuild -帮助用户快速在 **Rsbuild App** 或 **Rslib** 中构建 Module Federation 产物 +帮助用户快速在 **Rsbuild App** 中构建 Module Federation 产物。 ## 快速开始 @@ -43,44 +43,6 @@ export default defineConfig({ }); ``` -#### Rslib Module -``` ts title='rslib.config.ts' -import { pluginModuleFederation } from '@module-federation/rsbuild-plugin'; -import { defineConfig } from '@rslib/core'; - -export default defineConfig({ - lib: [ - // ... - { - format: 'mf', - output: { - distPath: { - root: './dist/mf', - }, - assetPrefix: 'xxx', - }, - plugins: [ - // ... - pluginModuleFederation({ - name: 'rslib_provider', - exposes: { - '.': './src/index.tsx', - }, - shared: { - react: { - singleton: true, - }, - 'react-dom': { - singleton: true, - }, - }, - }), - ], - }, - ], -}); -``` - ### 注意 如果需要使用 Module Federation 运行时能力,请安装 [@module-federation/enhanced](/zh/guide/runtime/index.html) @@ -101,7 +63,7 @@ type RSBUILD_PLUGIN_OPTIONS = { ### moduleFederationOptions -[Module Federation 配置项](../../../configure/index) +[Module Federation 配置项](/configure/index) ### rsbuildOptions @@ -120,7 +82,7 @@ Rsbuild 插件额外配置。 用于指定产物的运行目标环境。当设置为 `dual` 时,会同时构建 Web(浏览器)产物与 Node.js(SSR)产物。 -使用 `target: 'dual'` 生成 SSR 产物后,可参考 [创建 Modern.js 消费者](../../../practice/frameworks/modern/index) 创建消费者,并接入对应的 Rslib SSR 生产者进行开发。 +使用 `target: 'dual'` 生成 SSR 产物后,可参考 [创建 Modern.js 消费者](/integrations/framework/modernjs/quick-start) 创建消费者,并接入对应的 Rslib SSR 生产者进行开发。 对于 Rsbuild App 的 SSR,可使用 `target: 'node'` + `environment`,将 Module Federation 应用到指定环境。 diff --git a/apps/website-new/docs/zh/integrations/build-tool/rslib.mdx b/apps/website-new/docs/zh/integrations/build-tool/rslib.mdx new file mode 100644 index 00000000000..ec304a6446e --- /dev/null +++ b/apps/website-new/docs/zh/integrations/build-tool/rslib.mdx @@ -0,0 +1,75 @@ +# Rslib + +Rslib 可以用 `@module-federation/rsbuild-plugin` 产出 Module Federation 生产者。它更适合把组件库、业务模块或 SSR 生产者作为独立产物发布给其他应用消费。 + +## 快速开始 + +### 安装 + +import { PackageManagerTabs } from '@theme'; + + + +### 注册插件 + +```ts title='rslib.config.ts' +import { pluginModuleFederation } from '@module-federation/rsbuild-plugin'; +import { defineConfig } from '@rslib/core'; + +export default defineConfig({ + lib: [ + { + format: 'mf', + output: { + distPath: { + root: './dist/mf', + }, + assetPrefix: 'https://example.com/mf/', + }, + plugins: [ + pluginModuleFederation({ + name: 'rslib_provider', + exposes: { + '.': './src/index.tsx', + }, + shared: { + react: { + singleton: true, + }, + 'react-dom': { + singleton: true, + }, + }, + }), + ], + }, + ], +}); +``` + +## SSR + +如果需要同时生成浏览器和 Node.js 产物,可以在插件选项中使用 `target: 'dual'`。 + +```ts title='rslib.config.ts' +pluginModuleFederation( + { + name: 'rslib_provider', + exposes: { + '.': './src/index.tsx', + }, + }, + { + target: 'dual', + }, +); +``` + +完整插件选项可以继续阅读 [Rsbuild 插件配置](/integrations/build-tool/rsbuild#rsbuildoptions)。 diff --git a/apps/website-new/docs/zh/guide/build-plugins/plugins-vite.mdx b/apps/website-new/docs/zh/integrations/build-tool/vite.mdx similarity index 91% rename from apps/website-new/docs/zh/guide/build-plugins/plugins-vite.mdx rename to apps/website-new/docs/zh/integrations/build-tool/vite.mdx index 2edf7165290..13088eba0fc 100644 --- a/apps/website-new/docs/zh/guide/build-plugins/plugins-vite.mdx +++ b/apps/website-new/docs/zh/integrations/build-tool/vite.mdx @@ -6,7 +6,7 @@ - 当模块具备远程类型时会自动下载并消费远程模块的类型 :::warning 不支持的选项 -除了 [dev](../../../configure/dev.html) 选项外,其他选项全部支持(包含 dts)。 +除了 [dev](/configure/dev.html) 选项外,其他选项全部支持(包含 dts)。 ::: - roadmap 🗓️ - 消费远程模块时将具备热更新能力 @@ -89,4 +89,4 @@ type ModuleFederationOptions { }; ``` -你可以在 [Config 总览](../../../configure/index) 页面找到所有配置项的详细说明。 +你可以在 [Config 总览](/configure/index) 页面找到所有配置项的详细说明。 diff --git a/apps/website-new/docs/zh/integrations/bundler/_meta.json b/apps/website-new/docs/zh/integrations/bundler/_meta.json new file mode 100644 index 00000000000..90657d07f06 --- /dev/null +++ b/apps/website-new/docs/zh/integrations/bundler/_meta.json @@ -0,0 +1,5 @@ +[ + "rspack", + "webpack", + "metro" +] diff --git a/apps/website-new/docs/zh/guide/build-plugins/plugins-metro.mdx b/apps/website-new/docs/zh/integrations/bundler/metro.mdx similarity index 100% rename from apps/website-new/docs/zh/guide/build-plugins/plugins-metro.mdx rename to apps/website-new/docs/zh/integrations/bundler/metro.mdx diff --git a/apps/website-new/docs/zh/guide/build-plugins/plugins-rspack.mdx b/apps/website-new/docs/zh/integrations/bundler/rspack.mdx similarity index 92% rename from apps/website-new/docs/zh/guide/build-plugins/plugins-rspack.mdx rename to apps/website-new/docs/zh/integrations/bundler/rspack.mdx index 002f64f8042..5b2d0e546c8 100644 --- a/apps/website-new/docs/zh/guide/build-plugins/plugins-rspack.mdx +++ b/apps/website-new/docs/zh/integrations/bundler/rspack.mdx @@ -40,4 +40,4 @@ import RegisterPlugin from '@components/common/rspack/register-plugin'; ## 配置 -你可以在 [Config 总览](../../../configure/index) 页面找到所有配置项的详细说明。 +你可以在 [Config 总览](/configure/index) 页面找到所有配置项的详细说明。 diff --git a/apps/website-new/docs/zh/guide/build-plugins/plugins-webpack.mdx b/apps/website-new/docs/zh/integrations/bundler/webpack.mdx similarity index 92% rename from apps/website-new/docs/zh/guide/build-plugins/plugins-webpack.mdx rename to apps/website-new/docs/zh/integrations/bundler/webpack.mdx index 10c86ce1002..153d4afc239 100644 --- a/apps/website-new/docs/zh/guide/build-plugins/plugins-webpack.mdx +++ b/apps/website-new/docs/zh/integrations/bundler/webpack.mdx @@ -40,4 +40,4 @@ import RegisterPlugin from '@components/common/webpack/register-plugin'; ## 配置 -你可以在 [Config 总览](../../../configure/index) 页面找到所有配置项的详细说明。 +你可以在 [Config 总览](/configure/index) 页面找到所有配置项的详细说明。 diff --git a/apps/website-new/docs/zh/integrations/documentation/_meta.json b/apps/website-new/docs/zh/integrations/documentation/_meta.json new file mode 100644 index 00000000000..049e12afcab --- /dev/null +++ b/apps/website-new/docs/zh/integrations/documentation/_meta.json @@ -0,0 +1,7 @@ +[ + { + "type": "file", + "name": "rspress", + "label": "Rspress" + } +] diff --git a/apps/website-new/docs/zh/guide/build-plugins/plugins-rspress.mdx b/apps/website-new/docs/zh/integrations/documentation/rspress.mdx similarity index 99% rename from apps/website-new/docs/zh/guide/build-plugins/plugins-rspress.mdx rename to apps/website-new/docs/zh/integrations/documentation/rspress.mdx index 2514750541a..bed19e69b93 100644 --- a/apps/website-new/docs/zh/guide/build-plugins/plugins-rspress.mdx +++ b/apps/website-new/docs/zh/integrations/documentation/rspress.mdx @@ -64,7 +64,7 @@ import ConfigType from '@components/common/rspress/config-type'; ### {props.pluginOptionName || 'moduleFederationOptions'} -[{props.brandName || props.name || 'Module Federation'} 配置项](../../../configure/index) +[{props.brandName || props.name || 'Module Federation'} 配置项](/configure/index) ### rspressOptions diff --git a/apps/website-new/docs/zh/integrations/framework/_meta.json b/apps/website-new/docs/zh/integrations/framework/_meta.json new file mode 100644 index 00000000000..6cc895b6028 --- /dev/null +++ b/apps/website-new/docs/zh/integrations/framework/_meta.json @@ -0,0 +1,23 @@ +[ + { + "type": "dir", + "name": "modernjs", + "label": "Modern.js", + "collapsible": true, + "collapsed": true + }, + { + "type": "dir", + "name": "nextjs", + "label": "Next.js", + "collapsible": true, + "collapsed": true + }, + { + "type": "dir", + "name": "angular", + "label": "Angular", + "collapsible": true, + "collapsed": true + } +] diff --git a/apps/website-new/docs/zh/practice/frameworks/angular/_meta.json b/apps/website-new/docs/zh/integrations/framework/angular/_meta.json similarity index 67% rename from apps/website-new/docs/zh/practice/frameworks/angular/_meta.json rename to apps/website-new/docs/zh/integrations/framework/angular/_meta.json index 6264b4b24a0..ec3909aab95 100644 --- a/apps/website-new/docs/zh/practice/frameworks/angular/_meta.json +++ b/apps/website-new/docs/zh/integrations/framework/angular/_meta.json @@ -1,4 +1,9 @@ [ + { + "type": "file", + "name": "index", + "label": "接入概览" + }, "angular-cli", "angular-mfe", "mf-ssr-angular", diff --git a/apps/website-new/docs/zh/practice/frameworks/angular/angular-cli.mdx b/apps/website-new/docs/zh/integrations/framework/angular/angular-cli.mdx similarity index 100% rename from apps/website-new/docs/zh/practice/frameworks/angular/angular-cli.mdx rename to apps/website-new/docs/zh/integrations/framework/angular/angular-cli.mdx diff --git a/apps/website-new/docs/zh/practice/frameworks/angular/angular-mfe.mdx b/apps/website-new/docs/zh/integrations/framework/angular/angular-mfe.mdx similarity index 100% rename from apps/website-new/docs/zh/practice/frameworks/angular/angular-mfe.mdx rename to apps/website-new/docs/zh/integrations/framework/angular/angular-mfe.mdx diff --git a/apps/website-new/docs/zh/practice/frameworks/angular/auth0.mdx b/apps/website-new/docs/zh/integrations/framework/angular/auth0.mdx similarity index 100% rename from apps/website-new/docs/zh/practice/frameworks/angular/auth0.mdx rename to apps/website-new/docs/zh/integrations/framework/angular/auth0.mdx diff --git a/apps/website-new/docs/zh/integrations/framework/angular/index.mdx b/apps/website-new/docs/zh/integrations/framework/angular/index.mdx new file mode 100644 index 00000000000..08336e13832 --- /dev/null +++ b/apps/website-new/docs/zh/integrations/framework/angular/index.mdx @@ -0,0 +1,15 @@ +# Angular 接入概览 + +Angular 接入 Module Federation 时,先根据项目的构建方式选择文档。 + +| 项目类型 | 推荐阅读 | +| --- | --- | +| Angular CLI 项目 | [Angular CLI 设置](./angular-cli) | +| 需要完整微前端示例 | [Angular MFE](./angular-mfe) | +| SSR 场景 | [Angular SSR](./mf-ssr-angular) | +| Service Worker 场景 | [Service Worker 与 Module Federation](./service-workers-mf) | + +Angular 生态中常见接入方式会依赖 Angular CLI、自定义 Webpack Builder 或 Nx。进入具体文档前,建议先确认当前项目的构建方式和是否需要 SSR。 + +如果你只是想了解 Module Federation 的通用配置,可以回到 [配置](/configure/) 查看 `name`、`remotes`、`exposes` 和 `shared` 等基础配置。 + diff --git a/apps/website-new/docs/zh/practice/frameworks/angular/mf-ssr-angular.mdx b/apps/website-new/docs/zh/integrations/framework/angular/mf-ssr-angular.mdx similarity index 100% rename from apps/website-new/docs/zh/practice/frameworks/angular/mf-ssr-angular.mdx rename to apps/website-new/docs/zh/integrations/framework/angular/mf-ssr-angular.mdx diff --git a/apps/website-new/docs/zh/practice/frameworks/angular/okta-auth.mdx b/apps/website-new/docs/zh/integrations/framework/angular/okta-auth.mdx similarity index 100% rename from apps/website-new/docs/zh/practice/frameworks/angular/okta-auth.mdx rename to apps/website-new/docs/zh/integrations/framework/angular/okta-auth.mdx diff --git a/apps/website-new/docs/zh/practice/frameworks/angular/service-workers-mf.mdx b/apps/website-new/docs/zh/integrations/framework/angular/service-workers-mf.mdx similarity index 100% rename from apps/website-new/docs/zh/practice/frameworks/angular/service-workers-mf.mdx rename to apps/website-new/docs/zh/integrations/framework/angular/service-workers-mf.mdx diff --git a/apps/website-new/docs/zh/practice/frameworks/angular/splitting-to-mf-part1.mdx b/apps/website-new/docs/zh/integrations/framework/angular/splitting-to-mf-part1.mdx similarity index 100% rename from apps/website-new/docs/zh/practice/frameworks/angular/splitting-to-mf-part1.mdx rename to apps/website-new/docs/zh/integrations/framework/angular/splitting-to-mf-part1.mdx diff --git a/apps/website-new/docs/zh/practice/frameworks/angular/splitting-to-mf-part2.mdx b/apps/website-new/docs/zh/integrations/framework/angular/splitting-to-mf-part2.mdx similarity index 100% rename from apps/website-new/docs/zh/practice/frameworks/angular/splitting-to-mf-part2.mdx rename to apps/website-new/docs/zh/integrations/framework/angular/splitting-to-mf-part2.mdx diff --git a/apps/website-new/docs/zh/integrations/framework/modernjs/_meta.json b/apps/website-new/docs/zh/integrations/framework/modernjs/_meta.json new file mode 100644 index 00000000000..0a6ab451798 --- /dev/null +++ b/apps/website-new/docs/zh/integrations/framework/modernjs/_meta.json @@ -0,0 +1,9 @@ +[ + { + "type": "file", + "name": "index", + "label": "接入概览" + }, + "quick-start", + "dynamic-remote" +] diff --git a/apps/website-new/docs/zh/practice/frameworks/modern/dynamic-remote.mdx b/apps/website-new/docs/zh/integrations/framework/modernjs/dynamic-remote.mdx similarity index 94% rename from apps/website-new/docs/zh/practice/frameworks/modern/dynamic-remote.mdx rename to apps/website-new/docs/zh/integrations/framework/modernjs/dynamic-remote.mdx index 354af561bce..6c1948829d6 100644 --- a/apps/website-new/docs/zh/practice/frameworks/modern/dynamic-remote.mdx +++ b/apps/website-new/docs/zh/integrations/framework/modernjs/dynamic-remote.mdx @@ -142,15 +142,17 @@ export const loader = async ({ request }: LoaderFunctionArgs): Promise import('remote/Image'), loading: 'loading...', export: 'default', @@ -170,8 +172,7 @@ const Index = () => { const DynamicRemoteSSRComponents = dataLoader.providerList.map(item => { const { id } = item; - const Com = createLazyComponent({ - instance: getInstance(), + const Com = instance.createLazyComponent({ loader: () => loadRemote(id), loading: 'loading...', fallback: ({ error }) => { diff --git a/apps/website-new/docs/zh/guide/framework/modernjs.mdx b/apps/website-new/docs/zh/integrations/framework/modernjs/index.mdx similarity index 76% rename from apps/website-new/docs/zh/guide/framework/modernjs.mdx rename to apps/website-new/docs/zh/integrations/framework/modernjs/index.mdx index 56b493b1e39..23b4cca2783 100644 --- a/apps/website-new/docs/zh/guide/framework/modernjs.mdx +++ b/apps/website-new/docs/zh/integrations/framework/modernjs/index.mdx @@ -1,14 +1,17 @@ -# Modern.js +# Modern.js 接入概览 import { Badge } from '@theme'; [Modern.js](https://modernjs.dev/zh/guides/get-started/introduction.html) 是一个基于 React 的渐进式 Web 开发框架。在字节跳动内部,Modern.js 支撑了数千个 Web 应用的研发。 -Module Federation 团队与 Modern.js 团队紧密合作,并提供 `@module-federation/modern-js-v3` 插件来帮助用户在 Modern.js 中更好的使用 Module Federation。 +Module Federation 团队与 Modern.js 团队紧密合作,并提供 `@module-federation/modern-js-v3` 和 `@module-federation/modern-js` 两个插件来帮助用户在 Modern.js 中使用 Module Federation。 + +推荐升级到 Modern.js v3,并优先使用 `@module-federation/modern-js-v3`。 ## 支持 -- modern.js ^3.0.0 | ^2.56.1 +- Modern.js v3:推荐使用 `@module-federation/modern-js-v3` +- Modern.js v2.56.1 及以上:使用 `@module-federation/modern-js` - 包含服务器端渲染(SSR) 强烈推荐参考下列应用,它提供了 Modern.js 与 Module Federation 结合的最佳实践: @@ -20,7 +23,7 @@ Module Federation 团队与 Modern.js 团队紧密合作,并提供 `@module-fe ### 安装 -你可以通过如下的命令安装插件: +如果使用 Modern.js v3,安装 `@module-federation/modern-js-v3`: import { PackageManagerTabs } from '@theme'; @@ -33,8 +36,21 @@ import { PackageManagerTabs } from '@theme'; }} /> +如果仍在使用 Modern.js v2,安装 `@module-federation/modern-js`: + + + ### 应用插件 +下面以 Modern.js v3 为例。如果你仍在使用 Modern.js v2,将示例中的 `@module-federation/modern-js-v3` 替换为 `@module-federation/modern-js`。 + 在 `modern.config.ts` 的 `plugins` 中应用此插件: ```ts title="modern.config.ts" @@ -74,9 +90,9 @@ export default createModuleFederationConfig({ ## 组件级别数据获取 -参考[数据获取](../basic/data-fetch)。 +参考[数据获取](/guide/data/data-fetch)。 -其中 Modern.js 插件在 `@module-federation/modern-js-v3/react` 重导出了 `@module-federation/bridge-react` ,因此你不需要额外安装。 +其中 Modern.js 插件在 `@module-federation/modern-js-v3/data-fetch` 子路径提供了 `lazyLoadComponentPlugin`,因此你不需要额外安装 `@module-federation/bridge-react`。 ## API @@ -85,7 +101,7 @@ export default createModuleFederationConfig({ ### createRemoteComponent 废弃 ::: danger -此 API 已被废弃,请使用[createLazyComponent](/practice/bridge/react-bridge/load-component.html#什么是-createlazycomponent) 。 +此 API 已被废弃,请使用[createLazyComponent](/guide/bridge/react/load-component.html#什么是-createlazycomponent) 。 ::: #### 如何迁移 @@ -95,7 +111,7 @@ createRemoteComponent 参数和 createLazyComponent 完全一致,区别在于 ```diff - import { createRemoteComponent } from '@module-federation/modern-js-v3/runtime'; + import { getInstance } from '@module-federation/modern-js-v3/runtime'; -+ import { lazyLoadComponentPlugin } from '@module-federation/modern-js-v3/react'; ++ import { lazyLoadComponentPlugin } from '@module-federation/modern-js-v3/data-fetch'; const instance = getInstance(); // 注册 lazyLoadComponentPlugin 插件后,instance 会自动添加 createLazyComponent API @@ -122,7 +138,7 @@ export default App; ### createRemoteSSRComponent 废弃 ::: danger -此 API 已被废弃,请使用[createLazyComponent](/practice/bridge/react-bridge/load-component.html#什么是-createlazycomponent) 。 +此 API 已被废弃,请使用[createLazyComponent](/guide/bridge/react/load-component.html#什么是-createlazycomponent) 。 ::: #### 如何迁移 diff --git a/apps/website-new/docs/zh/practice/frameworks/modern/index.mdx b/apps/website-new/docs/zh/integrations/framework/modernjs/quick-start.mdx similarity index 95% rename from apps/website-new/docs/zh/practice/frameworks/modern/index.mdx rename to apps/website-new/docs/zh/integrations/framework/modernjs/quick-start.mdx index e8c3f9a0bc9..8b238d67a56 100644 --- a/apps/website-new/docs/zh/practice/frameworks/modern/index.mdx +++ b/apps/website-new/docs/zh/integrations/framework/modernjs/quick-start.mdx @@ -247,17 +247,19 @@ export default Index; 这是因为生产者的样式文件无法注入到对应的 html 中。 -此问题可以通过使用 `@module-federation/modern-js-v3` 提供的 [createremotessrcomponent](../../../guide/framework/modernjs#createremotessrcomponent) 解决。 +此问题可以通过使用 `@module-federation/modern-js-v3` 提供的 [createremotessrcomponent](/integrations/framework/modernjs#createremotessrcomponent) 解决。 修改消费者引用生产者处的代码(`src/routes/page.tsx`): ```tsx title='page.tsx' import { getInstance } from '@module-federation/modern-js-v3/runtime'; -import { createLazyComponent } from '@module-federation/modern-js-v3/react' +import { lazyLoadComponentPlugin } from '@module-federation/modern-js-v3/data-fetch'; import './index.css'; -const RemoteSSRComponent = createLazyComponent({ - instance: getInstance(), +const instance = getInstance(); +instance.registerPlugins([lazyLoadComponentPlugin()]); + +const RemoteSSRComponent = instance.createLazyComponent({ loader: () => import('remote/Image'), loading: 'loading...', export: 'default', diff --git a/apps/website-new/docs/zh/integrations/framework/nextjs/_meta.json b/apps/website-new/docs/zh/integrations/framework/nextjs/_meta.json new file mode 100644 index 00000000000..96959a79c4b --- /dev/null +++ b/apps/website-new/docs/zh/integrations/framework/nextjs/_meta.json @@ -0,0 +1,13 @@ +[ + { + "type": "file", + "name": "index", + "label": "接入概览" + }, + "basic-example", + "dynamic-remotes", + "importing-components", + "importing-pages", + "express", + "presets" +] diff --git a/apps/website-new/docs/zh/practice/frameworks/next/index.mdx b/apps/website-new/docs/zh/integrations/framework/nextjs/basic-example.mdx similarity index 100% rename from apps/website-new/docs/zh/practice/frameworks/next/index.mdx rename to apps/website-new/docs/zh/integrations/framework/nextjs/basic-example.mdx diff --git a/apps/website-new/docs/zh/practice/frameworks/next/dynamic-remotes.mdx b/apps/website-new/docs/zh/integrations/framework/nextjs/dynamic-remotes.mdx similarity index 100% rename from apps/website-new/docs/zh/practice/frameworks/next/dynamic-remotes.mdx rename to apps/website-new/docs/zh/integrations/framework/nextjs/dynamic-remotes.mdx diff --git a/apps/website-new/docs/zh/practice/frameworks/next/express.mdx b/apps/website-new/docs/zh/integrations/framework/nextjs/express.mdx similarity index 100% rename from apps/website-new/docs/zh/practice/frameworks/next/express.mdx rename to apps/website-new/docs/zh/integrations/framework/nextjs/express.mdx diff --git a/apps/website-new/docs/zh/practice/frameworks/next/importing-components.mdx b/apps/website-new/docs/zh/integrations/framework/nextjs/importing-components.mdx similarity index 100% rename from apps/website-new/docs/zh/practice/frameworks/next/importing-components.mdx rename to apps/website-new/docs/zh/integrations/framework/nextjs/importing-components.mdx diff --git a/apps/website-new/docs/zh/practice/frameworks/next/importing-pages.mdx b/apps/website-new/docs/zh/integrations/framework/nextjs/importing-pages.mdx similarity index 100% rename from apps/website-new/docs/zh/practice/frameworks/next/importing-pages.mdx rename to apps/website-new/docs/zh/integrations/framework/nextjs/importing-pages.mdx diff --git a/apps/website-new/docs/zh/guide/framework/nextjs.mdx b/apps/website-new/docs/zh/integrations/framework/nextjs/index.mdx similarity index 99% rename from apps/website-new/docs/zh/guide/framework/nextjs.mdx rename to apps/website-new/docs/zh/integrations/framework/nextjs/index.mdx index e1d4337e3ea..111b3e03a9a 100644 --- a/apps/website-new/docs/zh/guide/framework/nextjs.mdx +++ b/apps/website-new/docs/zh/integrations/framework/nextjs/index.mdx @@ -1,4 +1,4 @@ -# Next.js +# Next.js 接入概览 这个插件为 Next.js 启用 Module Federation diff --git a/apps/website-new/docs/zh/practice/frameworks/next/presets.mdx b/apps/website-new/docs/zh/integrations/framework/nextjs/presets.mdx similarity index 100% rename from apps/website-new/docs/zh/practice/frameworks/next/presets.mdx rename to apps/website-new/docs/zh/integrations/framework/nextjs/presets.mdx diff --git a/apps/website-new/docs/zh/integrations/index.mdx b/apps/website-new/docs/zh/integrations/index.mdx new file mode 100644 index 00000000000..5196fd6b4b5 --- /dev/null +++ b/apps/website-new/docs/zh/integrations/index.mdx @@ -0,0 +1,26 @@ +# 接入方案 + +Module Federation 的核心能力来自 MF Runtime。大多数项目推荐使用构建插件接入,它可以让你像使用 npm 包一样 `import` 远程模块,并获得类型提示等工程能力。如果你的项目需要暴露模块给其他应用消费,必须使用构建插件接入。如果你不想改造构建流程,只需要在运行时加载远程模块,也可以直接使用 Runtime。 + +如果你不知道应该安装哪个包,先按当前项目类型选择: + +| 你的项目 | 安装的包 | 继续阅读 | +| --- | --- | --- | +| Rsbuild 应用 | `@module-federation/rsbuild-plugin` | [Rsbuild](/integrations/build-tool/rsbuild) | +| Rslib 模块 | `@module-federation/rsbuild-plugin` | [Rslib](/integrations/build-tool/rslib) | +| Vite 应用 | `@module-federation/vite` | [Vite](/integrations/build-tool/vite) | +| Rspack 应用 | `@module-federation/enhanced` | [Rspack](/integrations/bundler/rspack) | +| Webpack 应用 | `@module-federation/enhanced` | [Webpack](/integrations/bundler/webpack) | +| React Native / Metro | `@module-federation/metro` 以及对应 Metro 插件 | [Metro](/integrations/bundler/metro) | +| Rspress 站点 | `@module-federation/rspress-plugin` | [Rspress](/integrations/documentation/rspress) | +| Modern.js 应用 | `@module-federation/modern-js-v3`(推荐,Modern.js v3)或 `@module-federation/modern-js`(Modern.js v2) | [Modern.js](/integrations/framework/modernjs) | +| Next.js 应用 | `@module-federation/nextjs-mf` 和 `webpack` | [Next.js](/integrations/framework/nextjs) | +| Angular 应用 | 根据 Angular 构建方式选择对应插件 | [Angular](/integrations/framework/angular) | + +你也可以只用 Runtime 来加载远程模块。不同项目引入 Runtime 的方式不同,你可以继续阅读 [Runtime 安装](/guide/runtime/#安装),了解如何选择正确的 Runtime 入口。 + +## 下一步 + +- 先在本页选择与你项目匹配的接入方案,完成包安装和插件配置。 +- 如果你使用 React、Vue 或其他具体框架场景,可以继续查看[实践](/integrations/practice/)。 +- Module Federation 的通用能力仍然在[指南](/guide/start/index)中介绍,具体项目如何启用插件,请以对应接入方案为准。 diff --git a/apps/website-new/docs/zh/integrations/practice/_meta.json b/apps/website-new/docs/zh/integrations/practice/_meta.json new file mode 100644 index 00000000000..41500faff8a --- /dev/null +++ b/apps/website-new/docs/zh/integrations/practice/_meta.json @@ -0,0 +1,19 @@ +[ + { + "type": "file", + "name": "index", + "label": "实践总览" + }, + { + "type": "dir", + "name": "react", + "label": "React", + "collapsible": true, + "collapsed": true + }, + { + "type": "file", + "name": "vue", + "label": "Vue" + } +] diff --git a/apps/website-new/docs/zh/integrations/practice/index.mdx b/apps/website-new/docs/zh/integrations/practice/index.mdx new file mode 100644 index 00000000000..176cf2a048a --- /dev/null +++ b/apps/website-new/docs/zh/integrations/practice/index.mdx @@ -0,0 +1,13 @@ +# 实践 + +这里放和具体框架、场景相关的实践内容。先在[接入方案总览](/integrations/)里选择适合当前项目的构建工具、框架或 Runtime 方案,再回到这里查看对应实践。 + +## React + +- [React 实践概览](./react) +- [Rsbuild CRA](./react/rsbuild-cra) +- [React i18n](./react/i18n-react) + +## Vue + +- [Vue Bridge](./vue) diff --git a/apps/website-new/docs/zh/integrations/practice/react/_meta.json b/apps/website-new/docs/zh/integrations/practice/react/_meta.json new file mode 100644 index 00000000000..c31455eb5e6 --- /dev/null +++ b/apps/website-new/docs/zh/integrations/practice/react/_meta.json @@ -0,0 +1,13 @@ +[ + { + "type": "file", + "name": "index", + "label": "接入概览" + }, + { + "type": "file", + "name": "rsbuild-cra", + "label": "Rsbuild CRA" + }, + "i18n-react" +] diff --git a/apps/website-new/docs/zh/practice/frameworks/react/i18n-react.mdx b/apps/website-new/docs/zh/integrations/practice/react/i18n-react.mdx similarity index 100% rename from apps/website-new/docs/zh/practice/frameworks/react/i18n-react.mdx rename to apps/website-new/docs/zh/integrations/practice/react/i18n-react.mdx diff --git a/apps/website-new/docs/zh/integrations/practice/react/index.mdx b/apps/website-new/docs/zh/integrations/practice/react/index.mdx new file mode 100644 index 00000000000..67b8312a4bb --- /dev/null +++ b/apps/website-new/docs/zh/integrations/practice/react/index.mdx @@ -0,0 +1,19 @@ +# React 实践 + +React 应用本身不需要一个固定的 React 专用插件。接入 Module Federation 时,先根据项目使用的构建工具选择对应方案,再查看这里的 React 场景实践。 + +| 项目类型 | 推荐阅读 | +| --- | --- | +| Rsbuild | [Rsbuild](/integrations/build-tool/rsbuild) | +| Rslib | [Rslib](/integrations/build-tool/rslib) | +| Rspack | [Rspack](/integrations/bundler/rspack) | +| Webpack | [Webpack](/integrations/bundler/webpack) | +| Vite | [Vite](/integrations/build-tool/vite) | +| 只使用运行时加载远程模块 | [Runtime 安装](/guide/runtime/#安装) | + +如果你要在 React 中加载远程组件,可以继续阅读 [Bridge](/guide/bridge/overview);如果远程组件需要数据获取、数据缓存或预取,可以继续阅读 [数据管理](/guide/data/data-fetch)。 + +## 示例 + +- [Rsbuild CRA](./rsbuild-cra) +- [React i18n](./i18n-react) diff --git a/apps/website-new/docs/zh/practice/frameworks/react/index.mdx b/apps/website-new/docs/zh/integrations/practice/react/rsbuild-cra.mdx similarity index 100% rename from apps/website-new/docs/zh/practice/frameworks/react/index.mdx rename to apps/website-new/docs/zh/integrations/practice/react/rsbuild-cra.mdx diff --git a/apps/website-new/docs/zh/practice/bridge/vue-bridge.mdx b/apps/website-new/docs/zh/integrations/practice/vue.mdx similarity index 100% rename from apps/website-new/docs/zh/practice/bridge/vue-bridge.mdx rename to apps/website-new/docs/zh/integrations/practice/vue.mdx diff --git a/apps/website-new/docs/zh/plugin/plugins/_meta.json b/apps/website-new/docs/zh/plugin/plugins/_meta.json index 1f1c967c866..5d7d71f5ddc 100644 --- a/apps/website-new/docs/zh/plugin/plugins/_meta.json +++ b/apps/website-new/docs/zh/plugin/plugins/_meta.json @@ -1 +1 @@ -["retry-plugin"] +["retry-plugin", "observability-plugin"] diff --git a/apps/website-new/docs/zh/plugin/plugins/observability-plugin.mdx b/apps/website-new/docs/zh/plugin/plugins/observability-plugin.mdx new file mode 100644 index 00000000000..004bb52c60a --- /dev/null +++ b/apps/website-new/docs/zh/plugin/plugins/observability-plugin.mdx @@ -0,0 +1,514 @@ +--- +title: 观测插件 +description: 观测 Module Federation 加载过程,收集运行时和构建侧信息,并给人或 AI 足够的信息定位问题。 +--- + +# 观测插件 + +观测插件用于让 Module Federation 加载过程可观测。它会记录运行时加载事件,整理最终加载结果,并在加载失败时打印稳定的 `traceId`。构建侧信息由构建观测插件单独写出。 + +该插件面向 Module Federation `2.5.0` 及以上版本。如果项目还在更早的 MF +版本,仍然可以先用运行时错误码做基础排查;但更完整的报告和加载观测链路需要升级到 +`2.5.0+` 并启用这个插件。 + +它适合回答这些问题: + +- 这个 remote 是否真的加载成功? +- 失败发生在 manifest、remoteEntry、init、expose、factory 还是 shared? +- 这次加载是否通过运行时 fallback 或恢复路径完成了? +- 这次命中了哪个 shared provider 和版本? +- `preloadRemote` 的资源是否真正预加载成功? +- 应该把哪份报告交给人或 AI coding agent 排查? + +shared 观测的边界是实例级别:它用于说明哪个 MF 实例加载了哪个共享依赖、最终使用了哪个已注册的 provider/version,以及对应的 scope、版本、eager 等基础信息。它不保证还原该 shared 是由哪个 remote/expose 间接触发的。一次链路涉及多个 shared 时,请查看 `events` 中所有 `phase: "shared"` 的事件;`summary.shared` 只是最后一次观测到的 shared 摘要。 + +如果构建插件传入了 `customShareInfo`,但运行时没有匹配到已注册的 shared provider,这不是普通的 fatal error。报告会把它描述为 `summary.outcome: "recovered"`、`summary.phases.shared.status: "complete"`,并在 `shared.reason` 中标记 `"custom-share-info-unmatched"`。它表示运行时走了可继续执行的处理路径;只有当你预期它必须命中某个 provider/version 时,才需要继续检查 shared 配置。 + +预加载观测用于回答 `preloadRemote` 是否真的把资源加载完。`preloadRemote` 完成后,报告会记录 `phase: "preload"` 的资源结果,包含资源地址、资源类型、状态和这次预加载的 `id`。状态可能是 `success`、`error`、`timeout` 或 `cached`。如果调用时没有指定 `exposes`,`id` 会是 `remoteName/*`;如果指定了 `exposes`,每个 expose 会单独记录成 `remoteName/expose`。 + +如果你想先体验报告效果,或者希望在页面里直接查看和导出报告,可以安装最新的 [Module Federation Chrome 插件](https://chromewebstore.google.com/detail/module-federation/aeoilchhomapofiopejjlecddfldpeom?hl=zh-CN&utm_source=ext_sidebar)。插件的「加载追踪」Tab 会读取页面已有的观测插件报告;如果页面还没有接入观测插件,也可以在当前 Tab 临时开启采集。更多使用方式见 [Chrome Devtool 加载追踪](../../guide/debug/chrome-devtool#加载追踪)。 + +## 安装 + +```bash +npm install @module-federation/observability-plugin +``` + +## Browser + +浏览器运行时使用默认入口。开发环境和生产环境的接入方式一样,区别只在于传给 `ObservabilityPlugin` 的参数。 + +import { Tab, Tabs } from '@theme'; + + + + 如果应用已经通过构建插件注册 Module Federation,推荐用 `runtimePlugins` 注入观测插件。 + + 在 Module Federation 构建配置中注入插件入口和可序列化参数: + + ```ts title="module-federation.config.ts" + export default { + name: 'runtime_host', + remotes: { + remote1: 'remote1@https://example.com/mf-manifest.json', + }, + runtimePlugins: [ + [ + require.resolve('@module-federation/observability-plugin'), + { + level: 'verbose', + browser: { + enabled: true, + scope: 'runtime_host', + }, + }, + ], + ], + }; + ``` + + 如果业务代码需要主动标记组件加载成功,可以在注册插件后的默认实例上调用 `markComponentLoaded`。 + + ```ts + import { getInstance } from '@module-federation/runtime'; + import '@module-federation/observability-plugin'; + + getInstance()?.markComponentLoaded({ + requestId: 'remote1/Button', + componentName: 'Button', + }); + ``` + + + + 如果没有使用构建插件,可以在创建 runtime 实例时直接注册观测插件。 + + ```ts title="mf-runtime.ts" + import { createInstance } from '@module-federation/runtime'; + import { ObservabilityPlugin } from '@module-federation/observability-plugin'; + + export const mf = createInstance({ + name: 'runtime_host', + remotes: [ + { + name: 'remote1', + entry: 'https://example.com/mf-manifest.json', + }, + ], + plugins: [ + ObservabilityPlugin({ + level: 'verbose', + browser: { + enabled: true, + scope: 'runtime_host', + }, + }), + ], + }); + ``` + + + +## 开发和生产参数 + +当 Module Federation 加载失败时,插件会打印一条简短的 `console.error`: + +```text +[Module Federation] Observability report generated +traceId: mf-... +phase: manifest +errorCode: RUNTIME-003 +read: window.__FEDERATION__.__OBSERVABILITY__["runtime_host"].getReport("mf-...") +``` + +在浏览器控制台执行 `read:` 后面的命令,就能拿到完整报告。 + +也可以直接读取: + +```ts +window.__FEDERATION__.__OBSERVABILITY__.runtime_host.getLatestReport(); +window.__FEDERATION__.__OBSERVABILITY__.runtime_host.getReport('mf-...'); +window.__FEDERATION__.__OBSERVABILITY__.runtime_host.getReports({ limit: 5 }); +window.__FEDERATION__.__OBSERVABILITY__.runtime_host.findReports({ + remote: 'remote1', +}); +window.__FEDERATION__.__OBSERVABILITY__.runtime_host.exportReport('mf-...'); +``` + +如果只是想在开发环境观察加载链路,或者页面一直停在 loading 状态但还没有报错,开启浏览器读取入口后,开发模式默认会打印开始日志: + +```ts +ObservabilityPlugin({ + level: 'verbose', + browser: { + enabled: true, + scope: 'runtime_host', + }, +}); +``` + +插件只会在 `loadRemote` 和 `loadShare` 开始时打印 `console.info`,其中包含 `traceId` 和读取命令。Agent 可以用这个 `traceId` 读取当前报告,查看 `status: "pending"`、`summary.phases`、`updatedAt` 和 `duration`,判断当前卡在哪一步。浏览器开发模式可通过 `trace.printStart: false` 关闭;浏览器生产模式默认关闭,只有显式设置 `trace.printStart: true` 才会开启。 + +开发环境通常打开浏览器读取入口,方便人或 AI coding agent 直接读取报告: + +```ts title="mf-runtime.ts" +import { ObservabilityPlugin } from '@module-federation/observability-plugin'; + +export const observabilityPlugin = ObservabilityPlugin({ + level: 'verbose', + browser: { + enabled: true, + scope: 'runtime_host', + }, +}); +``` + +生产环境仍然使用同一个插件,只是参数更保守:console 只保留 `traceId` 和已知 `errorCode`,完整报告通过业务自己的系统上报。 + +```ts title="mf-runtime.ts" +import { ObservabilityPlugin } from '@module-federation/observability-plugin'; + +export const observabilityPlugin = ObservabilityPlugin({ + level: 'summary', + browser: { + enabled: true, + scope: 'runtime_host', + mode: 'production', + }, + onReport(report) { + if (report.status === 'error' || report.summary.outcome === 'recovered') { + navigator.sendBeacon( + '/api/mf-observability', + JSON.stringify({ + traceId: report.traceId, + status: report.status, + diagnosis: report.diagnosis, + summary: report.summary, + remote: report.remote, + shared: report.shared, + moduleInfo: report.moduleInfo, + }), + ); + } + }, +}); +``` + +生产环境浏览器模式下,console 只包含 `traceId` 和已知 `errorCode`。完整报告应通过 `onReport`、`exportReport()` 或业务自己的上报系统获取。 + +## 使用 `onReport` 分析报告 + +`onReport` 会在报告更新时触发。生产环境通常不需要保存所有成功报告,但可以在这里把失败、恢复路径,或者你关心的成功链路上报到自己的系统。 + +常见策略: + +- 只排查故障:上报 `report.status === "error"` 和 `report.summary.outcome === "recovered"`。 +- 观测 shared 选择结果:额外上报 `report.summary.outcome === "shared-resolved"`,用于查看 shared 使用了哪个 provider 和版本。 +- 观测预加载结果:额外上报 `report.summary.outcome === "preloaded"` 或 `phase: "preload"` 的失败事件,用于统计哪些资源预加载成功、失败、超时或命中缓存。 +- 观测完整加载链路:按比例采样 `runtime-loaded`、`component-loaded`、`shared-resolved` 等成功报告。 + +拿到报告后,优先按这个顺序分析: + +1. `diagnosis`:直接给出问题归属、关键证据和下一步建议。 +2. `summary`:判断最终结果。`runtime-loaded` 表示 remote 已加载,`component-loaded` 表示业务组件主动确认成功,`shared-resolved` 表示 shared 已选出 provider 和版本,`preloaded` 表示预加载资源已完成,`failed` 表示失败,`recovered` 表示走了可继续执行的恢复路径。 +3. `remote` / `shared`:确认当前加载对象。shared 报告重点看 `provider`、`requiredVersion`、`selectedVersion`、`availableVersions`。 +4. `moduleInfo`:排查依赖部署平台下发的模块信息是否匹配。 +5. `events`:按时间线查看卡在哪个阶段。 + +示例: + +```ts +ObservabilityPlugin({ + level: 'summary', + browser: { + mode: 'production', + }, + onReport(report) { + const outcome = report.summary.outcome; + const shouldUpload = + report.status === 'error' || + outcome === 'recovered' || + outcome === 'shared-resolved' || + outcome === 'preloaded'; + + if (!shouldUpload) { + return; + } + + navigator.sendBeacon( + '/api/mf-observability', + JSON.stringify({ + traceId: report.traceId, + status: report.status, + outcome, + diagnosis: report.diagnosis, + summary: report.summary, + remote: report.remote, + shared: report.shared, + moduleInfo: report.moduleInfo, + events: report.events, + }), + ); + }, +}); +``` + +把上传后的报告交给 AI coding agent 时,可以这样问: + +```text +/mf observability + +这是生产环境上传的 MF observability 报告。 +请帮我判断加载是否成功,失败点在哪里,最可能是谁的问题,以及下一步怎么修。 + +<粘贴报告 JSON> +``` + +## Browser 参数 + +`ObservabilityPlugin(options)` 支持以下参数: + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| `enabled` | `boolean` | `true` | 是否启用观测插件。关闭后不记录事件、不生成报告。 | +| `level` | `'summary' \| 'verbose'` | `'summary'` | 报告详细程度。`summary` 保留摘要和关键事件,`verbose` 保留完整事件时间线。 | +| `maxEvents` | `number` | 内置上限 | 单个插件实例最多保留的事件数量,避免长时间运行时无限增长。 | +| `console` | `boolean` | `true` | 加载失败时是否打印 `console.error` 提示。 | +| `printRawStack` | `boolean` | `false` | 是否把原始错误栈打印到 console。默认关闭,避免生产环境输出过多细节。 | +| `stackTrace.enabled` | `boolean` | `true` | 是否在报告中保存裁剪后的错误栈。 | +| `stackTrace.maxLines` | `number` | 内置上限 | 错误栈最多保留多少行。 | +| `stackTrace.maxLength` | `number` | 内置上限 | 错误栈最多保留多少字符。 | +| `collector` | `boolean \| { enabled?: boolean; port?: number }` | `false` | 是否把浏览器运行时事件 POST 到本地 collector。`true` 使用 `127.0.0.1:17891`,自定义时只需要改 `port`。这个能力用于本地 AI 调试,插件本身不会启动服务。 | +| `browser.enabled` | `boolean` | `false` | 是否把读取入口挂到 `window.__FEDERATION__.__OBSERVABILITY__`。 | +| `browser.scope` | `string` | host 名称 | 浏览器读取入口的命名空间,例如 `runtime_host`。 | +| `browser.mode` | `'development' \| 'production'` | `'development'` | 浏览器输出模式。生产模式下 console 只输出最小提示。 | +| `trace.printStart` | `boolean` | 浏览器开发模式为 `true`,浏览器生产模式为 `false` | 是否在 `loadRemote` 和 `loadShare` 开始时打印 `console.info`,便于开发环境和 Agent 获取 `traceId`。生产模式需要显式设置为 `true` 才会开启。 | +| `react.enabled` | `boolean` | `true` | React 调试能力总开关。设置为 `false` 时关闭所有 React 包装行为。 | +| `react.injectLoadedCallback` | `boolean` | `false` | 显式包装匹配到的远程 React 组件,并注入 `onMFRemoteLoaded`。这个能力会改变组件引用,只建议作为临时调试开关,问题修复后需要及时关闭。 | +| `react.remoteIds` | `string[]` | `[]` | 只对指定远程请求注入回调,例如 `remote/Button` 或 `./Button`。为空时不按远程请求过滤。 | +| `react.defaultExportMode` | `'preserve' \| 'component'` | 自动选择 | 远程模块是 `{ default: Component }` 时,是否直接把包装后的组件作为返回值。通常保持默认即可。 | +| `onEvent` | `(event, report, context) => void` | `undefined` | 每次记录观测事件时触发,适合接入自定义日志系统。 | +| `onReport` | `(report, context) => void` | `undefined` | 报告更新时触发,生产环境常用它上传失败或恢复报告。 | +| `onRawError` | `(error, context) => void` | `undefined` | 捕获原始错误对象时触发,适合接入业务自己的错误系统。 | + +生产环境推荐至少设置: + +```ts +ObservabilityPlugin({ + level: 'summary', + browser: { + mode: 'production', + }, + onReport(report) { + // 上传到业务自己的系统 + }, +}); +``` + +## Node 或 SSR 运行时 + +需要本地观测文件时,使用 Node 专用入口。 + +```ts title="mf-node-runtime.ts" +import { createInstance } from '@module-federation/runtime'; +import { ObservabilityPlugin } from '@module-federation/observability-plugin/node'; + +createInstance({ + name: 'node_host', + remotes: [], + plugins: [ + ObservabilityPlugin({ + level: 'verbose', + fileOutput: true, + directory: '.mf/observability', + }), + ], +}); +``` + +Node 入口会写: + +- `.mf/observability/latest.json`:最近一次完整报告 +- `.mf/observability/events.jsonl`:多次 trace 的事件流水 + +优先读 `latest.json`。只有需要查看事件顺序或多条 trace 时,再读 `events.jsonl`。 + +Node 入口继承运行时参数,并额外支持: + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| `fileOutput` | `boolean` | `false` | 是否写出本地观测文件。 | +| `directory` | `string` | `'.mf/observability'` | 观测文件输出目录。 | +| `latestFile` | `string` | `'latest.json'` | 最近一次完整报告的文件名。 | +| `eventsFile` | `string` | `'events.jsonl'` | 事件流水文件名。 | + +## 构建观测 + +如果希望保留构建侧证据,把构建观测插件放到 Module Federation 构建插件旁边。 + +```js title="webpack.config.js" +const { + ModuleFederationPlugin, +} = require('@module-federation/enhanced/webpack'); +const { + ObservabilityBuildPlugin, +} = require('@module-federation/observability-plugin/build'); + +const moduleFederationOptions = { + name: 'runtime_host', + remotes: { + remote1: 'remote1@https://example.com/mf-manifest.json', + }, + exposes: { + './Button': './src/Button', + }, + shared: { + react: { singleton: true, requiredVersion: '^18.0.0' }, + }, +}; + +module.exports = { + plugins: [ + new ModuleFederationPlugin(moduleFederationOptions), + new ObservabilityBuildPlugin({ + moduleFederation: moduleFederationOptions, + }), + ], +}; +``` + +构建观测可以写出: + +- `.mf/observability/build-info.json` +- `.mf/observability/build-report.json` + +运行时报告不会内嵌这两个文件。排查时如果需要构建侧证据,单独读取构建文件,再和运行时报告对照。 + +`ObservabilityBuildPlugin(options)` 支持以下参数: + +| 参数 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| `enabled` | `boolean` | `true` | 是否启用构建观测插件。 | +| `outputFile` | `string` | `'.mf/observability/build-info.json'` | 成功构建时写出的构建信息文件。 | +| `errorReport` | `false \| object` | `{}` | 构建失败时是否写出构建错误观测报告。设为 `false` 可关闭。 | +| `errorReport.outputFile` | `string` | `'.mf/observability/build-report.json'` | 构建错误观测报告文件路径。 | +| `cwd` | `string` | 编译上下文 | 输出文件的相对目录基准。 | +| `bundler` | `string` | 自动识别 | 手动指定 bundler 名称。 | +| `bundlerVersion` | `string` | 自动识别 | 手动指定 bundler 版本。 | +| `pluginVersion` | `string` | 自动识别 | 手动指定 Module Federation 构建插件版本。 | +| `moduleFederation` | `unknown` | 自动读取 | 显式传入 Module Federation 配置,便于构建观测采集 remotes、exposes、shared 等信息。 | + +## 标记业务组件成功 + +Module Federation 能知道 remote module 是否加载完成,但不一定能知道业务组件自己的请求、图表或 SDK 初始化是否完成。 + +显式开启 `react.injectLoadedCallback` 后,插件会给匹配到的远程 React 组件注入 `onMFRemoteLoaded` prop。生产者组件在自己认为业务可用时调用它即可: + +```tsx +import { useEffect } from 'react'; +import type { OnMFRemoteLoaded } from '@module-federation/observability-plugin'; + +export default function RemotePanel({ + onMFRemoteLoaded, +}: { + onMFRemoteLoaded?: OnMFRemoteLoaded; +}) { + useEffect(() => { + onMFRemoteLoaded?.({ + metadata: { + dataReady: true, + }, + }); + }, [onMFRemoteLoaded]); + + return
Remote panel
; +} +``` + +如果业务需要在消费者侧主动标记,也可以直接调用实例方法: + +```ts +import { getInstance } from '@module-federation/runtime'; +import '@module-federation/observability-plugin'; + +getInstance()?.markComponentLoaded({ + requestId: 'remote1/Button', + componentName: 'Button', + metadata: { + route: '/dashboard', + }, +}); +``` + +报告里会出现 `component:business-loaded`,并且 `summary.outcome` 会变成 `"component-loaded"`。 + +## 注入 React Loaded 回调 + +如果是开发环境、AI 调试,或者线上临时定位“生产者组件没有真正加载”的问题,可以显式开启远程 React 组件回调注入: + +```ts +ObservabilityPlugin({ + level: 'verbose', + react: { + injectLoadedCallback: true, + remoteIds: ['remote/Button'], + }, +}); +``` + +开启后,插件会在 `loadRemote` 成功后尝试识别远程 React 函数组件,并包一层不产生 DOM 的组件。这个包装只注入 `onMFRemoteLoaded` prop,不监听 React mount、render 生命周期或超时。 + +生产者调用 `props.onMFRemoteLoaded?.()` 后,报告里会出现 `component:business-loaded`。 + +如果开启了 `react.injectLoadedCallback`,但 `summary.componentLoaded` 仍然是 `false`,不能只根据这个字段判断组件渲染失败。它只表示没有收到组件级成功信号。需要先看生产者源码里有没有调用 `props.onMFRemoteLoaded?.(...)`;如果没有调用,只能说明远程资源已经加载,组件是否达到业务可用状态还需要生产者补充这个回调。如果拿不到生产者源码,需要询问生产者是否已经接入这个回调。 + +这个能力会改变组件引用。请尽量用 `remoteIds` 缩小范围,并且只作为临时调试开关使用,线上问题修复后需要及时关闭。 + +## 配合 `mf` Skill 使用 + +安装 skill: + +```bash +npx skills add module-federation/agent-skills --skill mf -y +``` + +当控制台打印观测提示时,把这段交给 Agent: + +```text +/mf observability +我看到了这条 Module Federation console error: + +[Module Federation] Observability report generated +traceId: mf-... +read: window.__FEDERATION__.__OBSERVABILITY__["runtime_host"].getReport("mf-...") + +请读取报告并帮我修复问题。 +``` + +如果是 Node 或 SSR,把文件路径交给 Agent: + +```text +/mf observability +请读取 .mf/observability/latest.json,告诉我可能是谁的问题,以及应该怎么修。 +``` + +如果生产环境已经把报告上传到自己的系统,把上传后的报告或 `traceId` 交给 Agent: + +```text +/mf observability +这是 traceId mf-... 对应的上传报告。 +帮我判断这是 host、remote、shared、network 还是 build 的问题。 +``` + +## AI 优先读什么 + +skill 会按这个顺序读报告: + +1. `diagnosis` +2. `summary` +3. `moduleInfo` +4. `events` + +如果需要构建侧证据,再单独读取 `.mf/observability/build-info.json` 或 `.mf/observability/build-report.json`。 + +报告会省略 `undefined` 字段。字段不存在时,表示这次没有观察到,或者这次加载不相关。 diff --git a/apps/website-new/docs/zh/practice/_meta.json b/apps/website-new/docs/zh/practice/_meta.json deleted file mode 100644 index 7c445a9a02d..00000000000 --- a/apps/website-new/docs/zh/practice/_meta.json +++ /dev/null @@ -1,18 +0,0 @@ -[ - { - "type": "file", - "name": "overview", - "label": "概览" - }, - { - "type": "dir", - "name": "bridge", - "label": "Bridge" - }, - { - "type": "dir", - "name": "frameworks", - "label": "框架", - "collapsed": false - } -] diff --git a/apps/website-new/docs/zh/practice/bridge/_meta.json b/apps/website-new/docs/zh/practice/bridge/_meta.json deleted file mode 100644 index 8024191c07d..00000000000 --- a/apps/website-new/docs/zh/practice/bridge/_meta.json +++ /dev/null @@ -1,19 +0,0 @@ -[ - { - "type": "file", - "name": "overview", - "label": "Bridge 介绍" - }, - { - "type": "dir", - "name": "react-bridge", - "label": "React Bridge", - "collapsed": false, - "index": "getting-started" - }, - { - "type": "file", - "name": "vue-bridge", - "label": "Vue Bridge" - } -] diff --git a/apps/website-new/docs/zh/practice/frameworks/modern/_meta.json b/apps/website-new/docs/zh/practice/frameworks/modern/_meta.json deleted file mode 100644 index 1b38edd64ba..00000000000 --- a/apps/website-new/docs/zh/practice/frameworks/modern/_meta.json +++ /dev/null @@ -1 +0,0 @@ -["index","dynamic-remote"] diff --git a/apps/website-new/docs/zh/practice/frameworks/next/_meta.json b/apps/website-new/docs/zh/practice/frameworks/next/_meta.json deleted file mode 100644 index 9d91eff3307..00000000000 --- a/apps/website-new/docs/zh/practice/frameworks/next/_meta.json +++ /dev/null @@ -1 +0,0 @@ -["index", "importing-components","importing-pages","express","presets"] diff --git a/apps/website-new/docs/zh/practice/frameworks/overview.mdx b/apps/website-new/docs/zh/practice/frameworks/overview.mdx deleted file mode 100644 index 41fc3a5f6db..00000000000 --- a/apps/website-new/docs/zh/practice/frameworks/overview.mdx +++ /dev/null @@ -1,3 +0,0 @@ ---- -overview: true ---- diff --git a/apps/website-new/docs/zh/practice/frameworks/react/_meta.json b/apps/website-new/docs/zh/practice/frameworks/react/_meta.json deleted file mode 100644 index 768d1c3ce8f..00000000000 --- a/apps/website-new/docs/zh/practice/frameworks/react/_meta.json +++ /dev/null @@ -1 +0,0 @@ -["index", "i18n-react"] diff --git a/apps/website-new/docs/zh/practice/overview.md b/apps/website-new/docs/zh/practice/overview.md deleted file mode 100644 index eeee7899449..00000000000 --- a/apps/website-new/docs/zh/practice/overview.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: 概览 ---- - -Module Federation 作为一种模块共享方案,其核心目标是解决代码复用问题、优化构建过程和提升运行时性能。然而,在项目开发的实践中,仅具备这些功能是不足的。通常,它需要与各类框架相结合,以便理解在不同框架下如何使用 Module Federation,以及如何整合不同框架中的多个功能。此外,还需考虑不同应用场景的需求差异,例如中后台应用与移动应用开发场景。 - -本篇 “实践篇” 旨在解决上述问题,提供了一系列有关 Module Federation 使用的最佳实践。主要内容包括两个部分: - -1. Bridge:针对常见的业务开发场景:如何加载应用级别模块(带路由)、如何在不同前端框架间加载模块。 -2. 框架:介绍了 Module Federation 在不同框架中的使用方式。 diff --git a/apps/website-new/docs/zh/practice/performance/_meta.json b/apps/website-new/docs/zh/practice/performance/_meta.json deleted file mode 100644 index 033162d887f..00000000000 --- a/apps/website-new/docs/zh/practice/performance/_meta.json +++ /dev/null @@ -1 +0,0 @@ -["prefetch"] diff --git a/apps/website-new/docs/zh/practice/performance/prefetch.mdx b/apps/website-new/docs/zh/practice/performance/prefetch.mdx deleted file mode 100644 index 8b39af928cf..00000000000 --- a/apps/website-new/docs/zh/practice/performance/prefetch.mdx +++ /dev/null @@ -1,29 +0,0 @@ -# Data Prefetch - -:::danger 废弃警告 - -旧版 Data Prefetch 已移除,不再提供 `.prefetch.ts`、`dataPrefetch` 或 `@module-federation/enhanced/prefetch` 用法。 - -推荐使用 [bridge-react - prefetch](../../guide/basic/data-fetch-prefetch),该方案支持 Rspack/Webpack,并且支持 SSR/CSR。 - -::: - -## 如何迁移至 bridge-react - prefetch - -### 生产者 - -1. 将 `.prefetch.ts` 文件重命名为 `.data.ts`。 -2. 将默认导出改为具名导出,函数名为 `fetchData`。 -3. 移除配置文件中的 `dataPrefetch` 配置项。 -4. 如果使用了 `defer` API,需要移除。 -5. 如果组件内部通过 `usePrefetch` 获取数据,需要改为从 `props` 接收数据。 - -生产者侧不会主动执行 `fetchData`;只有在消费者加载该模块时,才会调用 `fetchData` 并将结果注入组件。 - -如果生产者自身项目也需要渲染该组件并传递数据,请在渲染前手动调用 `fetchData`,再将数据通过 `props` 传入。 - -### 消费者 - -1. 移除配置文件中的 `dataPrefetch` 配置项。 -2. 使用 `createLazyComponent` 加载生产者,具体参考 [bridge-react - data fetch](../../guide/basic/data-fetch#消费者)。 -3. 使用 [prefetch](../../guide/basic/data-fetch-prefetch) 函数预取数据。 diff --git a/apps/website-new/module-federation.config.ts b/apps/website-new/module-federation.config.ts index d61a6eb7981..5e18efc9eef 100644 --- a/apps/website-new/module-federation.config.ts +++ b/apps/website-new/module-federation.config.ts @@ -6,25 +6,22 @@ const LANGUAGES = ['zh', 'en']; const exposes = { // basic - [`./plugins-overview-${LANGUAGE}`]: `./docs/${LANGUAGE}/guide/build-plugins/plugins.mdx`, - [`./rspack-${LANGUAGE}`]: `./docs/${LANGUAGE}/guide/build-plugins/plugins-rspack.mdx`, - [`./webpack-${LANGUAGE}`]: `./docs/${LANGUAGE}/guide/build-plugins/plugins-webpack.mdx`, - [`./rspress-${LANGUAGE}`]: `./docs/${LANGUAGE}/guide/build-plugins/plugins-rspress.mdx`, + [`./plugins-overview-${LANGUAGE}`]: `./docs/${LANGUAGE}/integrations/index.mdx`, + [`./rspack-${LANGUAGE}`]: `./docs/${LANGUAGE}/integrations/bundler/rspack.mdx`, + [`./webpack-${LANGUAGE}`]: `./docs/${LANGUAGE}/integrations/bundler/webpack.mdx`, + [`./rspress-${LANGUAGE}`]: `./docs/${LANGUAGE}/integrations/documentation/rspress.mdx`, [`./cli-${LANGUAGE}`]: `./docs/${LANGUAGE}/guide/basic/cli.mdx`, [`./type-prompt-${LANGUAGE}`]: `./docs/${LANGUAGE}/guide/basic/type-prompt.mdx`, [`./css-isolate-${LANGUAGE}`]: `./docs/${LANGUAGE}/guide/basic/css-isolate.mdx`, - [`./data-fetch-index-${LANGUAGE}`]: `./docs/${LANGUAGE}/guide/basic/data-fetch.mdx`, - [`./data-fetch-cache-${LANGUAGE}`]: `./docs/${LANGUAGE}/guide/basic/data-fetch-cache.mdx`, - [`./data-fetch-prefetch-${LANGUAGE}`]: `./docs/${LANGUAGE}/guide/basic/data-fetch-prefetch.mdx`, + [`./data-fetch-index-${LANGUAGE}`]: `./docs/${LANGUAGE}/guide/data/data-fetch.mdx`, + [`./data-fetch-cache-${LANGUAGE}`]: `./docs/${LANGUAGE}/guide/data/data-fetch-cache.mdx`, + [`./data-fetch-prefetch-${LANGUAGE}`]: `./docs/${LANGUAGE}/guide/data/data-prefetch.mdx`, // debug [`./mode-${LANGUAGE}`]: `./docs/${LANGUAGE}/guide/debug/mode.mdx`, [`./variables-${LANGUAGE}`]: `./docs/${LANGUAGE}/guide/debug/variables.mdx`, - // framework - // [`./modernjs-${LANGUAGE}`]: `./docs/${LANGUAGE}/guide/framework/modernjs.mdx`, - // configure [`./configure-overview-${LANGUAGE}`]: `./docs/${LANGUAGE}/configure/index.mdx`, [`./configure-name-${LANGUAGE}`]: `./docs/${LANGUAGE}/configure/name.mdx`, @@ -48,16 +45,17 @@ const exposes = { // [`./configure-experiments-${LANGUAGE}`]: `./docs/${LANGUAGE}/configure/experiments.mdx`, // bridge - [`./bridge-overview-${LANGUAGE}`]: `./docs/${LANGUAGE}/practice/bridge/overview.mdx`, - [`./bridge-getting-started-${LANGUAGE}`]: `./docs/${LANGUAGE}/practice/bridge/react-bridge/getting-started.mdx`, - [`./bridge-export-app-${LANGUAGE}`]: `./docs/${LANGUAGE}/practice/bridge/react-bridge/export-app.mdx`, - [`./bridge-load-app-${LANGUAGE}`]: `./docs/${LANGUAGE}/practice/bridge/react-bridge/load-app.mdx`, - [`./bridge-load-component-${LANGUAGE}`]: `./docs/${LANGUAGE}/practice/bridge/react-bridge/load-component.mdx`, - [`./bridge-vue-${LANGUAGE}`]: `./docs/${LANGUAGE}/practice/bridge/vue-bridge.mdx`, + [`./bridge-overview-${LANGUAGE}`]: `./docs/${LANGUAGE}/guide/bridge/overview.mdx`, + [`./bridge-getting-started-${LANGUAGE}`]: `./docs/${LANGUAGE}/guide/bridge/react/getting-started.mdx`, + [`./bridge-export-app-${LANGUAGE}`]: `./docs/${LANGUAGE}/guide/bridge/react/export-app.mdx`, + [`./bridge-load-app-${LANGUAGE}`]: `./docs/${LANGUAGE}/guide/bridge/react/load-app.mdx`, + [`./bridge-load-component-${LANGUAGE}`]: `./docs/${LANGUAGE}/guide/bridge/react/load-component.mdx`, + [`./bridge-vue-${LANGUAGE}`]: `./docs/${LANGUAGE}/integrations/practice/vue.mdx`, // plugin [`./plugin-introduce-${LANGUAGE}`]: `./docs/${LANGUAGE}/plugin/dev/index.mdx`, [`./plugin-retry-${LANGUAGE}`]: `./docs/${LANGUAGE}/plugin/plugins/retry-plugin.mdx`, + [`./plugin-observability-${LANGUAGE}`]: `./docs/${LANGUAGE}/plugin/plugins/observability-plugin.mdx`, // blog [`./error-load-remote-${LANGUAGE}`]: `./docs/${LANGUAGE}/blog/error-load-remote.mdx`, diff --git a/apps/website-new/package.json b/apps/website-new/package.json index 8c26eb7187b..fad64f2cfc9 100644 --- a/apps/website-new/package.json +++ b/apps/website-new/package.json @@ -1,6 +1,6 @@ { "name": "website-new", - "version": "1.3.23", + "version": "1.3.24", "private": true, "description": "Federation example for website-new", "scripts": { diff --git a/apps/website-new/rspress.config.ts b/apps/website-new/rspress.config.ts index 82cf1b5dd23..7381a344c89 100644 --- a/apps/website-new/rspress.config.ts +++ b/apps/website-new/rspress.config.ts @@ -67,6 +67,7 @@ export default defineConfig({ dev: { assetPrefix: true, writeToDisk: true, + lazyCompilation: false, }, performance: { buildCache: false, diff --git a/apps/website-new/src/components/en/data-fetch/consumer.mdx b/apps/website-new/src/components/en/data-fetch/consumer.mdx index b7812e4cb91..ebef9fef436 100644 --- a/apps/website-new/src/components/en/data-fetch/consumer.mdx +++ b/apps/website-new/src/components/en/data-fetch/consumer.mdx @@ -1,23 +1,19 @@ ::: info Note -In SSR scenarios, this can only be used with [Modern.js](/guide/framework/modernjs). +In SSR scenarios, this can only be used with [Modern.js](/integrations/framework/modernjs). ::: -In the consumer, we need to use the [createLazyComponent](/practice/bridge/react-bridge/load-component.html#什么是-createlazycomponent) API to load the remote component and fetch its data. +In the consumer, we need to use the [createLazyComponent](/guide/bridge/react/load-component.html#what-is-createlazycomponent) API to load the remote component and fetch its data. ```tsx import { getInstance } from '@module-federation/enhanced/runtime'; -import { - ERROR_TYPE, - lazyLoadComponentPlugin, -} from '@module-federation/bridge-react'; +import { ERROR_TYPE } from '@module-federation/bridge-react/data-fetch'; +import { lazyLoadComponentPlugin } from '@module-federation/bridge-react/data-fetch'; const instance = getInstance(); instance.registerPlugins([lazyLoadComponentPlugin()]); const List = instance.createLazyComponent({ - loader: () => { - return import('remote/List'); - }, + loader: () => import('remote/List'), loading: 'loading...', export: 'default', fallback: ({ error, errorType, dataFetchMapKey }) => { @@ -26,24 +22,13 @@ const List = instance.createLazyComponent({ return
load remote failed
; } if (errorType === ERROR_TYPE.DATA_FETCH) { - return ( -
- data fetch failed, the dataFetchMapKey key is: {dataFetchMapKey} -
- ); + return
data fetch failed: {dataFetchMapKey}
; } return
error type is unknown
; }, }); -const Index = (): JSX.Element => { - return ( -
-

Basic usage with data fetch

- -
- ); -}; - -export default Index; +export default function Index() { + return ; +} ``` diff --git a/apps/website-new/src/components/en/data-fetch/prefetch-demo.mdx b/apps/website-new/src/components/en/data-fetch/prefetch-demo.mdx index 0d9a45e8193..ecaca408823 100644 --- a/apps/website-new/src/components/en/data-fetch/prefetch-demo.mdx +++ b/apps/website-new/src/components/en/data-fetch/prefetch-demo.mdx @@ -3,7 +3,7 @@ When a user hovers over a link that will navigate to the shop page, we can prefetch the data needed for that page. ```ts -import { getInstance } from '@module-federation/runtime'; +import { getInstance } from '@module-federation/enhanced/runtime'; const instance = getInstance(); @@ -20,7 +20,7 @@ const handleMouseEnter = () => { For further optimization, we can download the component's JS and CSS files at the same time as prefetching the data. ```ts -import { getInstance } from '@module-federation/runtime'; +import { getInstance } from '@module-federation/enhanced/runtime'; const instance = getInstance(); diff --git a/apps/website-new/src/components/en/data-fetch/prefetch-tip.mdx b/apps/website-new/src/components/en/data-fetch/prefetch-tip.mdx index fbc90a793d3..5f7857c4f08 100644 --- a/apps/website-new/src/components/en/data-fetch/prefetch-tip.mdx +++ b/apps/website-new/src/components/en/data-fetch/prefetch-tip.mdx @@ -1 +1 @@ -> This API requires the [`lazyLoadComponentPlugin` to be registered](/practice/bridge/react-bridge/load-component.html#step-1-register-lazyloadcomponentplugin) before it can be used. +> This API requires the [`lazyLoadComponentPlugin` to be registered](/guide/bridge/react/load-component.html#step-1-register-lazyloadcomponentplugin) before it can be used. diff --git a/apps/website-new/src/components/en/data-fetch/provider-tip.mdx b/apps/website-new/src/components/en/data-fetch/provider-tip.mdx index 8ea77b96b08..d2ab740d037 100644 --- a/apps/website-new/src/components/en/data-fetch/provider-tip.mdx +++ b/apps/website-new/src/components/en/data-fetch/provider-tip.mdx @@ -1,5 +1,5 @@ ::: info Note -Producers can use [Rslib](/guide/build-plugins/plugins-rsbuild#ssr) and [Modern.js](/guide/framework/modernjs) to generate components. +Producers can use [Rslib](/integrations/build-tool/rslib#ssr) and [Modern.js](/integrations/framework/modernjs) to generate components. However, it's important to note that because the data in "Data Fetching" is injected by the consumer, if a component uses "Data Fetching", its exported non-MF components cannot be isomorphic with the MF components. ::: diff --git a/apps/website-new/src/components/zh/data-fetch/consumer.mdx b/apps/website-new/src/components/zh/data-fetch/consumer.mdx index 4274285afa1..65e59038a5e 100644 --- a/apps/website-new/src/components/zh/data-fetch/consumer.mdx +++ b/apps/website-new/src/components/zh/data-fetch/consumer.mdx @@ -1,23 +1,19 @@ ::: info 注意 -SSR 场景中,只能在 [Modern.js](/guide/framework/modernjs) 中使用。 +SSR 场景中,只能在 [Modern.js](/integrations/framework/modernjs) 中使用。 ::: -在消费者中,我们需要通过 [createLazyComponent](/practice/bridge/react-bridge/load-component.html#什么是-createlazycomponent) API 来加载远程组件,并获取数据。 +在消费者中,我们需要通过 [createLazyComponent](/guide/bridge/react/load-component.html#什么是-createlazycomponent) API 来加载远程组件,并获取数据。 ```tsx import { getInstance } from '@module-federation/enhanced/runtime'; -import { - ERROR_TYPE, - lazyLoadComponentPlugin, -} from '@module-federation/bridge-react'; +import { ERROR_TYPE } from '@module-federation/bridge-react/data-fetch'; +import { lazyLoadComponentPlugin } from '@module-federation/bridge-react/data-fetch'; const instance = getInstance(); instance.registerPlugins([lazyLoadComponentPlugin()]); const List = instance.createLazyComponent({ - loader: () => { - return import('remote/List'); - }, + loader: () => import('remote/List'), loading: 'loading...', export: 'default', fallback: ({ error, errorType, dataFetchMapKey }) => { @@ -26,24 +22,13 @@ const List = instance.createLazyComponent({ return
load remote failed
; } if (errorType === ERROR_TYPE.DATA_FETCH) { - return ( -
- data fetch failed, the dataFetchMapKey key is: {dataFetchMapKey} -
- ); + return
data fetch failed: {dataFetchMapKey}
; } return
error type is unknown
; }, }); -const Index = (): JSX.Element => { - return ( -
-

Basic usage with data fetch

- -
- ); -}; - -export default Index; +export default function Index() { + return ; +} ``` diff --git a/apps/website-new/src/components/zh/data-fetch/prefetch-demo.mdx b/apps/website-new/src/components/zh/data-fetch/prefetch-demo.mdx index 5852b991337..59fb3bdfae2 100644 --- a/apps/website-new/src/components/zh/data-fetch/prefetch-demo.mdx +++ b/apps/website-new/src/components/zh/data-fetch/prefetch-demo.mdx @@ -3,7 +3,7 @@ 当用户鼠标悬停在一个将要导航到购物页面的链接上时,我们可以预取该页面所需的数据。 ```ts -import { getInstance } from '@module-federation/runtime'; +import { getInstance } from '@module-federation/enhanced/runtime'; const instance = getInstance(); @@ -20,7 +20,7 @@ const handleMouseEnter = () => { 为了进一步优化,我们可以在预取数据的同时,也把组件的 JS 和 CSS 文件下载下来。 ```ts -import { getInstance } from '@module-federation/runtime'; +import { getInstance } from '@module-federation/enhanced/runtime'; const instance = getInstance(); diff --git a/apps/website-new/src/components/zh/data-fetch/prefetch-tip.mdx b/apps/website-new/src/components/zh/data-fetch/prefetch-tip.mdx index c4449541e16..5af01c082f3 100644 --- a/apps/website-new/src/components/zh/data-fetch/prefetch-tip.mdx +++ b/apps/website-new/src/components/zh/data-fetch/prefetch-tip.mdx @@ -1 +1 @@ -> 该 API 需要先[注册 lazyLoadComponentPlugin 插件](/practice/bridge/react-bridge/load-component.html#步骤-1--注册-lazyloadcomponentplugin),才可以使用。 +> 该 API 需要先[注册 lazyLoadComponentPlugin 插件](/guide/bridge/react/load-component.html#步骤-1--注册-lazyloadcomponentplugin),才可以使用。 diff --git a/apps/website-new/src/components/zh/data-fetch/provider-tip.mdx b/apps/website-new/src/components/zh/data-fetch/provider-tip.mdx index 8e9baec146b..bb1aec1e022 100644 --- a/apps/website-new/src/components/zh/data-fetch/provider-tip.mdx +++ b/apps/website-new/src/components/zh/data-fetch/provider-tip.mdx @@ -1,5 +1,5 @@ ::: info 注意 -生产者可以使用 [Rslib](/guide/build-plugins/plugins-rsbuild#ssr) 和 [Modern.js](/guide/framework/modernjs) 来生成组件。 +生产者可以使用 [Rslib](/integrations/build-tool/rslib#ssr) 和 [Modern.js](/integrations/framework/modernjs) 来生成组件。 不过需要注意的是,因为「数据获取」中的数据由消费者注入。因此如果在组件中使用了「数据获取」,那么其导出的非 MF 组件无法与 MF 组件保持同构。 ::: diff --git a/packages/bridge/bridge-react-webpack-plugin/CHANGELOG.md b/packages/bridge/bridge-react-webpack-plugin/CHANGELOG.md index 5e3f11caee1..199a77e8e29 100644 --- a/packages/bridge/bridge-react-webpack-plugin/CHANGELOG.md +++ b/packages/bridge/bridge-react-webpack-plugin/CHANGELOG.md @@ -1,5 +1,13 @@ # @module-federation/bridge-react-webpack-plugin +## 2.5.0 + +### Patch Changes + +- Updated dependencies [5d4095d] +- Updated dependencies [0716c11] + - @module-federation/sdk@2.5.0 + ## 2.4.0 ### Patch Changes diff --git a/packages/bridge/bridge-react-webpack-plugin/package.json b/packages/bridge/bridge-react-webpack-plugin/package.json index c3b50509865..9d5adb2e6b9 100644 --- a/packages/bridge/bridge-react-webpack-plugin/package.json +++ b/packages/bridge/bridge-react-webpack-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/bridge-react-webpack-plugin", - "version": "2.4.0", + "version": "2.5.0", "publishConfig": { "access": "public" }, diff --git a/packages/bridge/bridge-react/CHANGELOG.md b/packages/bridge/bridge-react/CHANGELOG.md index de9a453c3c1..bf0417d1e56 100644 --- a/packages/bridge/bridge-react/CHANGELOG.md +++ b/packages/bridge/bridge-react/CHANGELOG.md @@ -1,5 +1,14 @@ # @module-federation/bridge-react +## 2.5.0 + +### Patch Changes + +- 180004d: Expose data-fetch subpaths for bridge-react and Modern.js users, and remove hono from bridge-react peer dependencies. +- Updated dependencies [5d4095d] +- Updated dependencies [0716c11] + - @module-federation/sdk@2.5.0 + ## 2.4.0 ### Patch Changes diff --git a/packages/bridge/bridge-react/package.json b/packages/bridge/bridge-react/package.json index 2eced03f0bb..c09f66db9a5 100644 --- a/packages/bridge/bridge-react/package.json +++ b/packages/bridge/bridge-react/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/bridge-react", - "version": "2.4.0", + "version": "2.5.0", "sideEffects": false, "publishConfig": { "access": "public" @@ -75,6 +75,11 @@ "import": "./dist/data-fetch-utils.es.js", "require": "./dist/data-fetch-utils.cjs.js" }, + "./data-fetch": { + "types": "./dist/data-fetch.d.ts", + "import": "./dist/data-fetch.es.js", + "require": "./dist/data-fetch.cjs.js" + }, "./data-fetch-server-middleware": { "types": "./dist/data-fetch-server-middleware.d.ts", "import": "./dist/data-fetch-server-middleware.es.js", @@ -125,6 +130,9 @@ "data-fetch-utils": [ "./dist/data-fetch-utils.d.ts" ], + "data-fetch": [ + "./dist/data-fetch.d.ts" + ], "data-fetch-server-middleware": [ "./dist/data-fetch-server-middleware.d.ts" ] @@ -144,8 +152,7 @@ "react": ">=16.9.0", "react-dom": ">=16.9.0", "react-router-dom": "^4 || ^5 || ^6 || ^7", - "react-router": "^6 || ^7", - "hono": "^4.12.7" + "react-router": "^6 || ^7" }, "peerDependenciesMeta": { "react-router-dom": { @@ -153,9 +160,6 @@ }, "react-router": { "optional": true - }, - "hono": { - "optional": true } }, "devDependencies": { diff --git a/packages/bridge/bridge-react/src/data-fetch.ts b/packages/bridge/bridge-react/src/data-fetch.ts new file mode 100644 index 00000000000..6a102ca0153 --- /dev/null +++ b/packages/bridge/bridge-react/src/data-fetch.ts @@ -0,0 +1,29 @@ +export { + ERROR_TYPE, + createLazyComponent, + collectSSRAssets, + callDataFetch, + setSSREnv, + autoFetchDataPlugin, + CacheSize, + CacheTime, + configureCache, + generateKey, + cache, + revalidateTag, + clearStore, + prefetch, +} from './lazy'; + +export { lazyLoadComponentPlugin } from './plugins/lazy-load-component-plugin'; +export { flushDataFetch } from './lazy/utils'; + +export type { + DataFetchParams, + NoSSRRemoteInfo, + CollectSSRAssetsOptions, + CreateLazyComponentOptions, + CacheStatus, + CacheStatsInfo, + PrefetchOptions, +} from './lazy'; diff --git a/packages/bridge/bridge-react/vite.config.ts b/packages/bridge/bridge-react/vite.config.ts index 2e3b5cfc243..6cbbfc6c4b8 100644 --- a/packages/bridge/bridge-react/vite.config.ts +++ b/packages/bridge/bridge-react/vite.config.ts @@ -41,6 +41,7 @@ export default defineConfig({ __dirname, 'src/lazy/data-fetch/index.ts', ), + 'data-fetch': path.resolve(__dirname, 'src/data-fetch.ts'), }, formats: ['cjs', 'es'], fileName: (format, entryName) => `${entryName}.${format}.js`, diff --git a/packages/bridge/bridge-shared/CHANGELOG.md b/packages/bridge/bridge-shared/CHANGELOG.md index 1276ebc6299..f527f290a06 100644 --- a/packages/bridge/bridge-shared/CHANGELOG.md +++ b/packages/bridge/bridge-shared/CHANGELOG.md @@ -1,5 +1,7 @@ # @module-federation/bridge-shared +## 2.5.0 + ## 2.4.0 ## 2.3.3 diff --git a/packages/bridge/bridge-shared/package.json b/packages/bridge/bridge-shared/package.json index c869d2a26ab..9903d904a6d 100644 --- a/packages/bridge/bridge-shared/package.json +++ b/packages/bridge/bridge-shared/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/bridge-shared", - "version": "2.4.0", + "version": "2.5.0", "publishConfig": { "access": "public" }, diff --git a/packages/bridge/vue3-bridge/CHANGELOG.md b/packages/bridge/vue3-bridge/CHANGELOG.md index ddcafefc6a0..0ae7f1899be 100644 --- a/packages/bridge/vue3-bridge/CHANGELOG.md +++ b/packages/bridge/vue3-bridge/CHANGELOG.md @@ -1,5 +1,17 @@ # @module-federation/bridge-vue3 +## 2.5.0 + +### Patch Changes + +- Updated dependencies [5d4095d] +- Updated dependencies [d433ec9] +- Updated dependencies [0716c11] +- Updated dependencies [41281f4] + - @module-federation/sdk@2.5.0 + - @module-federation/runtime@2.5.0 + - @module-federation/bridge-shared@2.5.0 + ## 2.4.0 ### Patch Changes diff --git a/packages/bridge/vue3-bridge/package.json b/packages/bridge/vue3-bridge/package.json index f3eab2ad870..60802c8f26f 100644 --- a/packages/bridge/vue3-bridge/package.json +++ b/packages/bridge/vue3-bridge/package.json @@ -7,7 +7,7 @@ "url": "git+https://github.com/module-federation/core.git", "directory": "packages/bridge/vue3-bridge" }, - "version": "2.4.0", + "version": "2.5.0", "publishConfig": { "access": "public" }, diff --git a/packages/chrome-devtools/CHANGELOG.md b/packages/chrome-devtools/CHANGELOG.md index 5620a28fb76..7a8543e32c6 100644 --- a/packages/chrome-devtools/CHANGELOG.md +++ b/packages/chrome-devtools/CHANGELOG.md @@ -1,5 +1,22 @@ # @module-federation/devtools +## 2.5.0 + +### Minor Changes + +- 41281f4: Add a Loading Trace panel that can configure and inject the observability plugin, reload the inspected page, stream loading events, and export collected reports. + +### Patch Changes + +- d2e8d79: Consolidate the Chrome DevTools proxy logic into a bundled proxy asset. +- Updated dependencies [41281f4] +- Updated dependencies [5d4095d] +- Updated dependencies [0716c11] +- Updated dependencies [328542c] +- Updated dependencies [41281f4] + - @module-federation/observability-plugin@2.5.0 + - @module-federation/sdk@2.5.0 + ## 2.4.0 ### Minor Changes diff --git a/packages/chrome-devtools/README.md b/packages/chrome-devtools/README.md index 8030d7bf175..a2a19791b8c 100644 --- a/packages/chrome-devtools/README.md +++ b/packages/chrome-devtools/README.md @@ -4,5 +4,8 @@ - Proxy online Module Federation remote module to local - Let proxied remote module get hmr +- Inject the Chrome-specific observability runtime plugin from the Loading Trace + tab, receive page events through `window.postMessage`, and export collected + loading reports https://module-federation.io/ diff --git a/packages/chrome-devtools/__tests__/chrome-tabs.spec.ts b/packages/chrome-devtools/__tests__/chrome-tabs.spec.ts new file mode 100644 index 00000000000..22b81bb4ce6 --- /dev/null +++ b/packages/chrome-devtools/__tests__/chrome-tabs.spec.ts @@ -0,0 +1,40 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { getCurrentTabId, syncActiveTab, TabInfo } from '../src/utils/chrome'; + +describe('chrome tab helpers', () => { + afterEach(() => { + vi.restoreAllMocks(); + Reflect.deleteProperty(globalThis, 'chrome'); + window.targetTab = undefined as any; + TabInfo.currentTabId = 0; + }); + + it('does not warn when active tab query returns no array', async () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + vi.stubGlobal('chrome', { + tabs: { + query: vi.fn().mockResolvedValue(undefined), + }, + }); + + await expect(syncActiveTab()).resolves.toBeUndefined(); + expect(warn).not.toHaveBeenCalled(); + expect(getCurrentTabId()).toBe(0); + }); + + it('syncs the queried active tab when chrome returns a tab array', async () => { + const activeTab = { id: 8080 }; + + vi.stubGlobal('chrome', { + tabs: { + query: vi.fn().mockResolvedValue([activeTab]), + }, + }); + + await expect(syncActiveTab()).resolves.toBe(activeTab); + expect(window.targetTab).toBe(activeTab); + expect(getCurrentTabId()).toBe(8080); + }); +}); diff --git a/packages/chrome-devtools/__tests__/observability-devtools.spec.ts b/packages/chrome-devtools/__tests__/observability-devtools.spec.ts new file mode 100644 index 00000000000..3a978034f45 --- /dev/null +++ b/packages/chrome-devtools/__tests__/observability-devtools.spec.ts @@ -0,0 +1,220 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { + getObservabilityReportScopeLabel, + mergeObservabilityReports, + readObservabilitySnapshot, +} from '../src/utils/chrome/observability'; +import { + createObservabilityPluginOptions, + DEFAULT_OBSERVABILITY_DEVTOOLS_CONFIG, + normalizeObservabilityDevtoolsConfig, +} from '../src/utils/chrome/observability-shared'; + +describe('observability devtools config', () => { + afterEach(() => { + vi.restoreAllMocks(); + Reflect.deleteProperty(globalThis, 'chrome'); + Reflect.deleteProperty(window, '__FEDERATION__'); + Reflect.deleteProperty(window, '__VMOK__'); + window.targetTab = undefined as any; + window.localStorage.clear(); + }); + + it('defaults to development mode with verbose events', () => { + expect(normalizeObservabilityDevtoolsConfig()).toMatchObject({ + enabled: true, + level: 'verbose', + browser: { + enabled: true, + scope: 'chrome_extension', + mode: 'development', + }, + trace: { + printStart: true, + }, + }); + }); + + it('normalizes unsafe values and keeps valid production overrides', () => { + expect( + normalizeObservabilityDevtoolsConfig({ + ...DEFAULT_OBSERVABILITY_DEVTOOLS_CONFIG, + level: 'summary', + maxEvents: 5000, + browser: { + enabled: true, + mode: 'production', + scope: 'runtime host!', + }, + react: { + injectLoadedCallback: true, + remoteIds: 'remote/Button, remote/Card', + }, + }), + ).toMatchObject({ + level: 'summary', + maxEvents: 1000, + browser: { + mode: 'production', + scope: 'runtime-host-', + }, + react: { + injectLoadedCallback: false, + remoteIds: [], + }, + }); + }); + + it('merges reports by trace id and keeps newest first', () => { + const reports = mergeObservabilityReports( + [ + { + traceId: 'a', + status: 'pending', + startedAt: 1, + updatedAt: 1, + duration: 0, + events: [], + }, + ], + [ + { + traceId: 'a', + status: 'success', + startedAt: 1, + updatedAt: 3, + duration: 2, + events: [], + }, + { + traceId: 'b', + status: 'pending', + startedAt: 2, + updatedAt: 2, + duration: 0, + events: [], + }, + ], + ); + + expect(reports.map((report) => report.traceId)).toEqual(['a', 'b']); + expect(reports[0].status).toBe('success'); + }); + + it('only labels application-owned observability scopes', () => { + expect( + getObservabilityReportScopeLabel({ __scope: 'chrome_extension' }), + ).toBeUndefined(); + expect(getObservabilityReportScopeLabel({})).toBeUndefined(); + expect(getObservabilityReportScopeLabel({ __scope: 'runtime_host' })).toBe( + 'custom: runtime_host', + ); + }); + + it('keeps reports without runtime version as basic observability regardless of scope', async () => { + window.targetTab = { id: 8080 } as chrome.tabs.Tab; + window.__FEDERATION__ = { + __OBSERVABILITY__: { + runtime_host: { + getReports: () => [ + { + traceId: 'trace-unknown-version', + status: 'pending', + startedAt: 1, + updatedAt: 2, + duration: 1, + events: [], + }, + ], + }, + }, + } as any; + + const executeScript = vi.fn(async ({ func, args }) => [ + { result: func(...args) }, + ]); + + vi.stubGlobal('chrome', { + scripting: { + executeScript, + }, + }); + + const snapshot = await readObservabilitySnapshot(); + + expect(snapshot.reports[0]).toMatchObject({ + traceId: 'trace-unknown-version', + __scope: 'runtime_host', + }); + expect(snapshot.reports[0]).not.toHaveProperty('runtimeVersion'); + }); + + it('does not pass React callback injection to the injected observability plugin', () => { + const config = normalizeObservabilityDevtoolsConfig({ + ...DEFAULT_OBSERVABILITY_DEVTOOLS_CONFIG, + react: { + injectLoadedCallback: true, + remoteIds: 'remote/Button', + }, + }); + + expect(config.react).toEqual({ + injectLoadedCallback: false, + remoteIds: [], + }); + expect(createObservabilityPluginOptions(config)).not.toHaveProperty( + 'react', + ); + }); + + it('reads application observability reports from the inspected page', async () => { + window.targetTab = { id: 8080 } as chrome.tabs.Tab; + window.__FEDERATION__ = { + __INSTANCES__: [ + { + options: { + plugins: [{ name: 'observability-plugin' }], + }, + }, + ], + __OBSERVABILITY__: { + runtime_host: { + getReports: () => [ + { + traceId: 'trace-1', + status: 'success', + startedAt: 1, + updatedAt: 2, + duration: 1, + events: [], + }, + ], + }, + }, + } as any; + + const executeScript = vi.fn(async ({ func, args }) => [ + { result: func(...args) }, + ]); + + vi.stubGlobal('chrome', { + scripting: { + executeScript, + }, + }); + + const snapshot = await readObservabilitySnapshot(); + + expect(snapshot.hasUserObservabilityPlugin).toBe(true); + expect(snapshot.reports).toHaveLength(1); + expect(snapshot.reports[0]).toMatchObject({ + traceId: 'trace-1', + __scope: 'runtime_host', + }); + expect(executeScript.mock.calls[0]?.[0].args[1]).toMatchObject({ + userPluginName: 'observability-plugin', + chromeScope: 'chrome_extension', + }); + }); +}); diff --git a/packages/chrome-devtools/manifest.json b/packages/chrome-devtools/manifest.json index 6f8d6617974..b1440aa3845 100644 --- a/packages/chrome-devtools/manifest.json +++ b/packages/chrome-devtools/manifest.json @@ -18,7 +18,8 @@ "static/js/post-message-start.js", "static/js/fast-refresh.js", "static/js/override-remote.js", - "static/js/snapshot-plugin.js" + "static/js/snapshot-plugin.js", + "static/js/observability-plugin.js" ], "matches": [""] } @@ -42,7 +43,8 @@ "js": [ "static/js/fast-refresh.js", "static/js/override-remote.js", - "static/js/snapshot-plugin.js" + "static/js/snapshot-plugin.js", + "static/js/observability-plugin.js" ], "world": "MAIN", "run_at": "document_start" diff --git a/packages/chrome-devtools/modern.config.ts b/packages/chrome-devtools/modern.config.ts index 0fe90edc6dd..589871c3bf5 100644 --- a/packages/chrome-devtools/modern.config.ts +++ b/packages/chrome-devtools/modern.config.ts @@ -36,6 +36,8 @@ export default defineConfig({ config.entry['fast-refresh'] = './src/utils/chrome/fast-refresh.ts'; config.entry['override-remote'] = './src/utils/chrome/override-remote.ts'; config.entry['snapshot-plugin'] = './src/utils/chrome/snapshot-plugin.ts'; + config.entry['observability-plugin'] = + './src/utils/chrome/observability-plugin.ts'; config.entry['post-message'] = './src/utils/chrome/post-message.ts'; config.entry['post-message-init'] = './src/utils/chrome/post-message-init.ts'; diff --git a/packages/chrome-devtools/package.json b/packages/chrome-devtools/package.json index b004d3a9939..e6b22b75c29 100644 --- a/packages/chrome-devtools/package.json +++ b/packages/chrome-devtools/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/devtools", - "version": "2.4.0", + "version": "2.5.0", "license": "MIT", "repository": { "type": "git", @@ -60,6 +60,7 @@ "dependencies": { "@modern-js/runtime": "2.70.8", "@arco-design/web-react": "2.66.7", + "@module-federation/observability-plugin": "workspace:*", "@module-federation/sdk": "workspace:*", "ahooks": "3.7.10", "dagre": "0.8.5", diff --git a/packages/chrome-devtools/src/App.module.scss b/packages/chrome-devtools/src/App.module.scss index 0ada841ba8c..e8c3d4cde08 100644 --- a/packages/chrome-devtools/src/App.module.scss +++ b/packages/chrome-devtools/src/App.module.scss @@ -94,14 +94,14 @@ } .activeTab { - border-color: rgba(99, 102, 241, 0.65); + border-color: rgba(24, 24, 27, 0.28); background: linear-gradient( 135deg, - rgba(59, 130, 246, 0.15), - rgba(37, 99, 235, 0.1) + rgba(24, 24, 27, 0.08), + rgba(113, 113, 122, 0.08) ); color: var(--color-text-1, #1e293b); - box-shadow: 0 12px 28px rgba(30, 64, 175, 0.25); + box-shadow: 0 12px 28px rgba(24, 24, 27, 0.14); } .panel { diff --git a/packages/chrome-devtools/src/App.tsx b/packages/chrome-devtools/src/App.tsx index 592dfaf1c11..b01045e0e6d 100644 --- a/packages/chrome-devtools/src/App.tsx +++ b/packages/chrome-devtools/src/App.tsx @@ -11,6 +11,7 @@ import ProxyLayout from './component/Layout'; import Dependency from './component/DependencyGraph'; import ModuleInfo from './component/ModuleInfo'; import SharedDepsExplorer from './component/SharedDepsExplorer'; +import LoadingTrace from './component/LoadingTrace'; import LanguageSwitch from './component/LanguageSwitch'; import ThemeToggle from './component/ThemeToggle'; import { @@ -99,6 +100,7 @@ const NAV_ITEMS = [ { key: 'proxy', i18nKey: 'app.nav.proxy' }, { key: 'dependency', i18nKey: 'app.nav.dependency' }, { key: 'share', i18nKey: 'app.nav.share' }, + { key: 'loadingTrace', i18nKey: 'app.nav.loadingTrace' }, { key: 'performance', i18nKey: 'app.nav.performance' }, ] as const; @@ -133,6 +135,7 @@ const InnerApp = (props: RootComponentProps) => { const [inspectedTab, setInspectedTab] = useState( window.targetTab, ); + const [inspectedTabRefreshKey, setInspectedTabRefreshKey] = useState(0); const [activePanel, setActivePanel] = useState('proxy'); const [selectedModuleId, setSelectedModuleId] = useState(null); const [refreshing, setRefreshing] = useState(false); @@ -253,9 +256,16 @@ const InnerApp = (props: RootComponentProps) => { }, [applyModuleUpdate]); useEffect(() => { - const updateActiveTab = async (tabId?: number) => { + const updateActiveTab = async ( + tabId?: number, + options?: { status?: chrome.tabs.TabChangeInfo['status'] }, + ) => { const tab = await syncActiveTab(tabId); setInspectedTab(tab || undefined); + if (options?.status === 'loading') { + setInspectedTabRefreshKey((key) => key + 1); + return; + } if (window.__FEDERATION__?.moduleInfo) { applyModuleUpdate(cloneModuleInfo(window.__FEDERATION__?.moduleInfo)); } @@ -263,12 +273,16 @@ const InnerApp = (props: RootComponentProps) => { }; const onMessage = ( - message: { type?: string; tabId?: number }, + message: { + type?: string; + tabId?: number; + status?: chrome.tabs.TabChangeInfo['status']; + }, _sender: chrome.runtime.MessageSender, _sendResponse: (response?: any) => void, ) => { if (message?.type === MESSAGE_ACTIVE_TAB_CHANGED) { - updateActiveTab(message.tabId); + updateActiveTab(message.tabId, { status: message.status }); } }; @@ -409,6 +423,13 @@ const InnerApp = (props: RootComponentProps) => { )} /> ); + case 'loadingTrace': + return ( + + ); case 'performance': return (
diff --git a/packages/chrome-devtools/src/component/LoadingTrace/index.module.scss b/packages/chrome-devtools/src/component/LoadingTrace/index.module.scss new file mode 100644 index 00000000000..2b40954aba5 --- /dev/null +++ b/packages/chrome-devtools/src/component/LoadingTrace/index.module.scss @@ -0,0 +1,893 @@ +.wrapper { + display: flex; + flex-direction: column; + gap: 16px; + min-height: 0; + color: var(--color-text-1, #1f2937); +} + +.wrapper :global(.arco-btn-primary) { + border-color: #18181b; + background: #18181b; + color: #fff; +} + +.wrapper :global(.arco-btn-primary:not(.arco-btn-disabled):hover) { + border-color: #27272a; + background: #27272a; + color: #fff; +} + +.wrapper :global(.arco-btn-primary:not(.arco-btn-disabled):active) { + border-color: #09090b; + background: #09090b; +} + +.wrapper :global(.arco-btn-secondary) { + border-color: var(--color-border-2, rgba(203, 213, 225, 0.65)); + background: var(--color-fill-2, rgba(15, 23, 42, 0.04)); + color: var(--color-text-1, #18181b); +} + +.wrapper :global(.arco-btn-secondary:not(.arco-btn-disabled):hover) { + border-color: rgba(24, 24, 27, 0.28); + background: rgba(24, 24, 27, 0.08); + color: var(--color-text-1, #18181b); +} + +.wrapper :global(.arco-switch-checked) { + background-color: #18181b; +} + +.wrapper :global(.arco-switch-checked:hover) { + background-color: #27272a; +} + +.wrapper :global(.arco-input-inner-wrapper-focus), +.wrapper :global(.arco-input-inner-wrapper:focus-within) { + border-color: rgba(24, 24, 27, 0.45) !important; + box-shadow: 0 0 0 2px rgba(24, 24, 27, 0.08) !important; +} + +.wrapper :global(.arco-tag), +.wrapper :global(.arco-tag-color-arcoblue) { + border-color: rgba(24, 24, 27, 0.12); + background: rgba(24, 24, 27, 0.06); + color: var(--color-text-1, #18181b); +} + +.toolbar, +.configGrid, +.stats, +.viewer { + border: 1px solid var(--color-border-2, rgba(203, 213, 225, 0.5)); + background: var(--color-bg-2, rgba(255, 255, 255, 0.92)); + border-radius: 14px; + box-sizing: border-box; +} + +.toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 16px; + flex-wrap: wrap; +} + +.titleGroup { + display: flex; + flex-direction: column; + gap: 4px; +} + +.title { + font-size: 16px; + font-weight: 600; +} + +.subtitle { + font-size: 12px; + color: var(--color-text-2, rgba(75, 85, 99, 0.82)); +} + +.actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + flex-wrap: wrap; +} + +.configButton { + border-color: var(--color-border-2, rgba(203, 213, 225, 0.65)) !important; + background: var(--color-fill-2, rgba(15, 23, 42, 0.04)) !important; + color: var(--color-text-1, #18181b) !important; +} + +.configButton:hover { + border-color: rgba(24, 24, 27, 0.28) !important; + background: rgba(24, 24, 27, 0.08) !important; + color: var(--color-text-1, #18181b) !important; +} + +.configButtonActive, +.configButtonActive:hover, +.primaryAction, +.primaryAction:hover { + border-color: #18181b !important; + background: #18181b !important; + color: #fff !important; +} + +.configButtonActive:active, +.primaryAction:active { + border-color: #09090b !important; + background: #09090b !important; + color: #fff !important; +} + +.primaryAction:disabled { + border-color: rgba(24, 24, 27, 0.14) !important; + background: rgba(24, 24, 27, 0.08) !important; + color: rgba(24, 24, 27, 0.36) !important; +} + +.configGrid { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 14px 24px; + padding: 16px; + overflow: visible; +} + +.field { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; + font-size: 12px; + color: var(--color-text-2, rgba(75, 85, 99, 0.86)); +} + +.labelWithHelp { + display: inline-flex; + align-items: center; + gap: 4px; + flex: 0 0 auto; + position: relative; + white-space: nowrap; +} + +.labelText { + color: var(--color-text-2, rgba(75, 85, 99, 0.86)); +} + +.helpIcon { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + color: var(--color-text-3, rgba(75, 85, 99, 0.62)); + cursor: help; +} + +.helpIcon:hover { + color: var(--color-text-1, #1f2937); +} + +.helpIcon::after { + content: attr(data-tip); + position: absolute; + left: 0; + top: calc(100% + 6px); + z-index: 20; + width: 260px; + max-width: min(260px, calc(100vw - 48px)); + box-sizing: border-box; + padding: 8px 10px; + border-radius: 6px; + background: rgba(15, 23, 42, 0.96); + box-shadow: 0 8px 24px rgba(15, 23, 42, 0.18); + color: #fff; + font-size: 12px; + line-height: 1.5; + white-space: normal; + opacity: 0; + visibility: hidden; + pointer-events: none; + transform: translateY(4px); + transition: + opacity 120ms ease, + transform 120ms ease, + visibility 120ms ease; +} + +.helpIcon::before { + content: ''; + position: absolute; + left: 6px; + top: calc(100% + 1px); + z-index: 21; + border-width: 0 5px 5px; + border-style: solid; + border-color: transparent transparent rgba(15, 23, 42, 0.96); + opacity: 0; + visibility: hidden; + pointer-events: none; + transform: translateY(4px); + transition: + opacity 120ms ease, + transform 120ms ease, + visibility 120ms ease; +} + +.helpIcon:hover::after, +.helpIcon:focus-visible::after, +.helpIcon:hover::before, +.helpIcon:focus-visible::before { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.labelColon { + color: var(--color-text-2, rgba(75, 85, 99, 0.86)); +} + +.segmentedControl { + display: inline-grid; + grid-auto-flow: column; + grid-auto-columns: max-content; + gap: 2px; + min-height: 32px; + padding: 2px; + border: 1px solid var(--color-border-2, rgba(203, 213, 225, 0.65)); + border-radius: 6px; + background: var(--color-fill-2, rgba(15, 23, 42, 0.04)); + box-sizing: border-box; +} + +.segmentButton { + min-width: 48px; + height: 26px; + padding: 0 10px; + border: 0; + border-radius: 4px; + background: transparent; + color: var(--color-text-2, rgba(75, 85, 99, 0.88)); + font-size: 12px; + line-height: 26px; + cursor: pointer; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.segmentButton:hover { + background: rgba(24, 24, 27, 0.08); + color: var(--color-text-1, #1f2937); +} + +.segmentButton:focus-visible { + outline: 2px solid rgba(24, 24, 27, 0.42); + outline-offset: 1px; +} + +.segmentButtonActive, +.segmentButtonActive:hover { + background: #18181b; + color: #fff; +} + +.stats { + display: grid; + grid-template-columns: repeat(4, minmax(120px, 1fr)); + gap: 12px; + padding: 14px 16px; +} + +.stats > div { + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; +} + +.statValueWithHelp { + display: inline-flex; + align-items: center; + gap: 5px; + min-width: 0; + overflow: visible; +} + +.statValue { + font-size: 18px; + font-weight: 600; + color: var(--color-text-1, #1f2937); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.statLabel { + font-size: 11px; + color: var(--color-text-2, rgba(75, 85, 99, 0.78)); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.viewer { + display: grid; + grid-template-columns: minmax(220px, 280px) minmax(0, 1fr); + min-height: 420px; + overflow: hidden; +} + +.viewerEmpty { + grid-template-columns: 1fr; +} + +.reportList { + border-right: 1px solid var(--color-border-2, rgba(203, 213, 225, 0.5)); + overflow: auto; + min-height: 0; +} + +.reportFilter { + position: sticky; + top: 0; + z-index: 2; + padding: 12px; + border-bottom: 1px solid var(--color-border-1, rgba(203, 213, 225, 0.35)); + background: var(--color-bg-2, rgba(255, 255, 255, 0.96)); +} + +.reportSearch { + width: 100%; +} + +.reportItem { + width: 100%; + border: 0; + border-bottom: 1px solid var(--color-border-1, rgba(203, 213, 225, 0.35)); + background: transparent; + color: inherit; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + padding: 12px; + cursor: pointer; + text-align: left; +} + +.reportItem:hover, +.activeReport { + background: rgba(24, 24, 27, 0.08); +} + +.reportTitle { + width: 100%; + font-size: 13px; + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.reportMeta { + width: 100%; + font-size: 11px; + color: var(--color-text-2, rgba(75, 85, 99, 0.78)); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.reportTagRow { + width: 100%; + display: flex; + align-items: center; + gap: 6px; + min-width: 0; +} + +.statusTag { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + height: 20px; + padding: 0 7px; + border: 1px solid rgba(24, 24, 27, 0.12); + border-radius: 999px; + background: rgba(24, 24, 27, 0.05); + color: var(--color-text-2, rgba(75, 85, 99, 0.9)); + font-size: 11px; + font-weight: 600; + line-height: 18px; + text-transform: uppercase; +} + +.statusTagsuccess { + border-color: rgba(22, 163, 74, 0.36); + background: rgba(22, 163, 74, 0.12); + color: #15803d; +} + +.statusTagfailed { + border-color: rgba(220, 38, 38, 0.38); + background: rgba(220, 38, 38, 0.12); + color: #b91c1c; +} + +.statusTagpending { + border-color: rgba(217, 119, 6, 0.42); + background: rgba(245, 158, 11, 0.16); + color: #92400e; +} + +.statusTagrecovered { + border-color: rgba(234, 88, 12, 0.4); + background: rgba(249, 115, 22, 0.14); + color: #c2410c; +} + +.timeline { + min-width: 0; + overflow: auto; + padding: 16px; +} + +.reportHeader { + display: flex; + justify-content: space-between; + gap: 12px; + align-items: flex-start; + padding-bottom: 14px; + border-bottom: 1px solid var(--color-border-1, rgba(203, 213, 225, 0.35)); +} + +.reportHeading { + display: block; + font-size: 15px; + font-weight: 600; + margin-bottom: 4px; +} + +.traceId { + display: block; + font-size: 11px; + color: var(--color-text-2, rgba(75, 85, 99, 0.78)); + word-break: break-all; +} + +.tags { + display: flex; + justify-content: flex-end; + gap: 8px; + flex-wrap: wrap; +} + +.limitedTag { + position: relative; + flex: 0 0 auto; + display: inline-flex; + align-items: center; + gap: 4px; + height: 20px; + padding: 0 7px; + border: 1px solid rgba(24, 24, 27, 0.14); + border-radius: 999px; + background: rgba(24, 24, 27, 0.04); + color: var(--color-text-1, #18181b); + font-size: 11px; + font-weight: 600; + line-height: 18px; + white-space: nowrap; + overflow: visible; +} + +.inlineHelpIcon { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + color: var(--color-text-3, rgba(75, 85, 99, 0.62)); + cursor: help; + line-height: 1; +} + +.inlineHelpIcon svg { + display: block; + width: 13px; + height: 13px; +} + +.inlineHelpIcon::after { + content: attr(data-tip); + position: absolute; + right: -8px; + top: calc(100% + 8px); + z-index: 20; + width: 280px; + max-width: min(280px, calc(100vw - 48px)); + box-sizing: border-box; + padding: 8px 10px; + border-radius: 6px; + background: rgba(15, 23, 42, 0.96); + box-shadow: 0 8px 24px rgba(15, 23, 42, 0.18); + color: #fff; + font-size: 12px; + line-height: 1.5; + white-space: normal; + opacity: 0; + visibility: hidden; + pointer-events: none; + transform: translateY(4px); + transition: + opacity 120ms ease, + transform 120ms ease, + visibility 120ms ease; +} + +.inlineHelpIcon::before { + content: ''; + position: absolute; + right: 2px; + top: calc(100% + 3px); + z-index: 21; + border-width: 0 5px 5px; + border-style: solid; + border-color: transparent transparent rgba(15, 23, 42, 0.96); + opacity: 0; + visibility: hidden; + pointer-events: none; + transform: translateY(4px); + transition: + opacity 120ms ease, + transform 120ms ease, + visibility 120ms ease; +} + +.inlineHelpIcon:hover::after, +.inlineHelpIcon:focus-visible::after, +.inlineHelpIcon:hover::before, +.inlineHelpIcon:focus-visible::before { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.statHelpIcon::after { + right: auto; + left: -8px; +} + +.statHelpIcon::before { + right: auto; + left: 2px; +} + +.diagnosis { + margin-top: 12px; + padding: 10px 12px; + border-radius: 10px; + background: rgba(24, 24, 27, 0.06); + color: var(--color-text-1, #1f2937); + font-size: 13px; +} + +.currentLoad { + margin-top: 12px; + padding: 12px; + border: 1px solid var(--color-border-2, rgba(203, 213, 225, 0.68)); + border-radius: 10px; + background: rgba(255, 255, 255, 0.72); +} + +.currentLoadHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 10px; +} + +.currentLoadTitle { + color: var(--color-text-1, #1f2937); + font-size: 13px; + font-weight: 700; +} + +.currentLoadSummary { + color: var(--color-text-2, rgba(75, 85, 99, 0.72)); + font-size: 11px; +} + +.currentLoadRows { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px 16px; +} + +.currentLoadRow { + display: grid; + grid-template-columns: 96px minmax(0, 1fr); + gap: 10px; + align-items: start; + min-width: 0; +} + +.currentLoadLabel { + color: var(--color-text-3, rgba(75, 85, 99, 0.62)); + font-size: 11px; + line-height: 1.6; +} + +.currentLoadValue { + min-width: 0; + overflow-wrap: anywhere; + color: var(--color-text-1, #1f2937); + font-size: 12px; + font-weight: 600; + line-height: 1.6; +} + +.loadedBefore { + margin-top: 12px; + padding: 12px; + border: 1px solid var(--color-border-2, rgba(203, 213, 225, 0.68)); + border-radius: 10px; + background: rgba(248, 250, 252, 0.72); +} + +.loadedBeforeHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 10px; +} + +.loadedBeforeTitle { + color: var(--color-text-1, #1f2937); + font-size: 13px; + font-weight: 700; +} + +.loadedBeforeSummary { + color: var(--color-text-2, rgba(75, 85, 99, 0.72)); + font-size: 11px; +} + +.loadedBeforeTags { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 10px; +} + +.loadedBeforeTag { + display: inline-flex; + align-items: center; + min-height: 22px; + padding: 0 8px; + border-radius: 999px; + font-size: 11px; + font-weight: 600; + line-height: 20px; +} + +.loadedBeforeTagSuccess { + border: 1px solid rgba(34, 197, 94, 0.38); + background: rgba(34, 197, 94, 0.12); + color: #15803d; +} + +.loadedBeforeTagNeutral { + border: 1px solid rgba(100, 116, 139, 0.34); + background: rgba(100, 116, 139, 0.08); + color: #475569; +} + +.loadedBeforeConsumers { + display: flex; + flex-direction: column; + gap: 8px; +} + +.loadedBeforeConsumer { + display: flex; + flex-direction: column; + gap: 7px; + padding-top: 10px; + border-top: 1px solid rgba(203, 213, 225, 0.45); +} + +.loadedBeforeRow { + display: grid; + grid-template-columns: 96px minmax(0, 1fr); + gap: 10px; + align-items: start; +} + +.loadedBeforeLabel { + color: var(--color-text-3, rgba(75, 85, 99, 0.62)); + font-size: 11px; + line-height: 1.6; +} + +.loadedBeforeName, +.loadedBeforeMeta, +.loadedBeforeExposes { + min-width: 0; + overflow-wrap: anywhere; + line-height: 1.6; +} + +.loadedBeforeName { + color: var(--color-text-1, #1f2937); + font-size: 12px; + font-weight: 650; +} + +.loadedBeforeMeta { + color: var(--color-text-2, rgba(75, 85, 99, 0.78)); + font-size: 12px; +} + +.loadedBeforeExposes { + color: var(--color-text-2, rgba(75, 85, 99, 0.9)); + font-size: 12px; +} + +.eventList { + display: flex; + flex-direction: column; + gap: 10px; + margin-top: 14px; +} + +.eventItem { + display: grid; + grid-template-columns: 76px minmax(0, 1fr); + gap: 12px; + padding: 10px 0; + border-bottom: 1px solid var(--color-border-1, rgba(203, 213, 225, 0.28)); +} + +.eventTime { + font-size: 11px; + color: var(--color-text-2, rgba(75, 85, 99, 0.72)); + padding-top: 2px; +} + +.eventBody { + min-width: 0; + display: flex; + flex-direction: column; + gap: 5px; +} + +.eventMain { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.phase { + font-size: 13px; + font-weight: 600; +} + +.eventStatusTag { + display: inline-flex; + align-items: center; + height: 20px; + padding: 0 7px; + border: 1px solid rgba(24, 24, 27, 0.12); + border-radius: 999px; + background: rgba(24, 24, 27, 0.05); + color: var(--color-text-2, rgba(75, 85, 99, 0.9)); + font-size: 11px; + font-weight: 600; + line-height: 18px; +} + +.eventStatusTagsuccess { + border-color: rgba(22, 163, 74, 0.36); + background: rgba(22, 163, 74, 0.12); + color: #15803d; +} + +.eventStatusTagfailed { + border-color: rgba(220, 38, 38, 0.38); + background: rgba(220, 38, 38, 0.12); + color: #b91c1c; +} + +.eventStatusTagpending { + border-color: rgba(217, 119, 6, 0.42); + background: rgba(245, 158, 11, 0.16); + color: #92400e; +} + +.eventStatusTagneutral { + border-color: rgba(24, 24, 27, 0.12); + background: rgba(24, 24, 27, 0.05); + color: var(--color-text-2, rgba(75, 85, 99, 0.9)); +} + +.eventRecoveredTag { + display: inline-flex; + align-items: center; + height: 20px; + padding: 0 7px; + border: 1px solid rgba(234, 88, 12, 0.32); + border-radius: 999px; + background: rgba(249, 115, 22, 0.12); + color: #c2410c; + font-size: 11px; + font-weight: 600; + line-height: 18px; +} + +.duration, +.eventMeta { + font-size: 12px; + color: var(--color-text-2, rgba(75, 85, 99, 0.76)); +} + +.eventMeta, +.error { + overflow-wrap: anywhere; +} + +.error { + font-size: 12px; + color: var(--color-text-1, #18181b); +} + +.emptyPanel { + min-height: 260px; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; +} + +@media (max-width: 980px) { + .viewer { + grid-template-columns: 1fr; + } + + .reportList { + border-right: 0; + border-bottom: 1px solid var(--color-border-2, rgba(203, 213, 225, 0.5)); + max-height: 220px; + } +} + +@media (max-width: 640px) { + .stats { + grid-template-columns: 1fr; + } + + .currentLoadRows, + .loadedBeforeRow { + grid-template-columns: 1fr; + } + + .currentLoadRow { + grid-template-columns: 1fr; + } + + .eventItem { + grid-template-columns: 1fr; + } +} diff --git a/packages/chrome-devtools/src/component/LoadingTrace/index.tsx b/packages/chrome-devtools/src/component/LoadingTrace/index.tsx new file mode 100644 index 00000000000..8742c1114a6 --- /dev/null +++ b/packages/chrome-devtools/src/component/LoadingTrace/index.tsx @@ -0,0 +1,1164 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Button, Empty, Input, Modal, Switch } from '@arco-design/web-react'; +import { + IconDownload, + IconPlayArrow, + IconQuestionCircle, + IconRefresh, + IconSearch, + IconSettings, +} from '@arco-design/web-react/icon'; +import { useTranslation } from 'react-i18next'; + +import { + applyObservabilityConfig, + disableObservabilityConfig, + getObservabilityReportScopeLabel, + mergeObservabilityReports, + readObservabilityConfig, + readObservabilitySnapshot, + reloadInspectedPage, + type ObservabilityDevtoolsReport, +} from '../../utils/chrome/observability'; +import { + DEFAULT_OBSERVABILITY_DEVTOOLS_CONFIG, + normalizeObservabilityDevtoolsConfig, + type ObservabilityDevtoolsConfig, + type ObservabilityDevtoolsLevel, +} from '../../utils/chrome/observability-shared'; +import { MESSAGE_OBSERVABILITY_DEVTOOLS_EVENT } from '../../utils/chrome/messages'; +import styles from './index.module.scss'; + +interface LoadingTraceProps { + tabId?: number; + resetKey?: number; +} + +interface SegmentedOption { + label: string; + value: T; +} + +interface SegmentedControlProps { + value: T; + options: Array>; + onChange(value: T): void; +} + +interface FieldLabelProps { + label: string; + tip: string; +} + +const FieldLabel = ({ label, tip }: FieldLabelProps) => ( + + {label} + + + + : + +); + +const SegmentedControl = ({ + value, + options, + onChange, +}: SegmentedControlProps) => ( +
+ {options.map((option) => { + const isActive = option.value === value; + return ( + + ); + })} +
+); + +const formatTime = (timestamp?: number) => { + if (!timestamp) { + return '-'; + } + try { + return new Date(timestamp).toLocaleTimeString(); + } catch { + return '-'; + } +}; + +const getReportTitle = (report: ObservabilityDevtoolsReport) => + report.requestId || + report.remote?.name || + report.shared?.name || + report.traceId || + 'unknown'; + +const getReportOutcome = (report: ObservabilityDevtoolsReport) => + report.summary?.outcome || report.status || 'pending'; + +const parseStableVersion = (version?: string) => { + const matched = version?.match(/^(\d+)\.(\d+)\.(\d+)(?:\+[\w.-]+)?$/); + if (!matched) { + return null; + } + + return { + major: Number(matched[1]), + minor: Number(matched[2]), + patch: Number(matched[3]), + }; +}; + +const isVersionLessThan = ( + version: ReturnType, + target: { major: number; minor: number; patch: number }, +) => { + if (!version) { + return false; + } + if (version.major !== target.major) { + return version.major < target.major; + } + if (version.minor !== target.minor) { + return version.minor < target.minor; + } + return version.patch < target.patch; +}; + +const getLimitedObservabilityLabel = (report: ObservabilityDevtoolsReport) => { + const runtimeVersion = parseStableVersion(report.runtimeVersion); + if ( + runtimeVersion && + isVersionLessThan(runtimeVersion, { major: 2, minor: 5, patch: 0 }) + ) { + return 'lowVersion'; + } + + if (!report.runtimeVersion) { + return 'unknownVersion'; + } + + return undefined; +}; + +const getReportState = (report: ObservabilityDevtoolsReport) => { + const outcome = getReportOutcome(report); + const limitedObservability = getLimitedObservabilityLabel(report); + if (outcome === 'recovered' || report.summary?.recovered) { + return 'recovered'; + } + if ( + report.status === 'error' || + outcome === 'failed' || + report.failedPhase || + report.errorMessage + ) { + return 'failed'; + } + if ( + outcome === 'pending' && + report.status === 'success' && + limitedObservability + ) { + return 'success'; + } + if (outcome === 'pending') { + return 'pending'; + } + if (report.status === 'success') { + return 'success'; + } + return 'pending'; +}; + +const getEventStatusState = (status?: string) => { + switch (status) { + case 'success': + case 'complete': + return 'success'; + case 'error': + case 'failed': + return 'failed'; + case 'start': + case 'pending': + return 'pending'; + default: + return 'neutral'; + } +}; + +const getReportSearchText = (report: ObservabilityDevtoolsReport) => + [ + getReportTitle(report), + report.traceId, + report.status, + getReportOutcome(report), + report.hostName, + report.runtimeVersion, + report.remote?.name, + report.remote?.alias, + report.remote?.entry, + report.shared?.name, + report.shared?.provider, + report.shared?.requiredVersion, + report.shared?.selectedVersion, + report.shared?.availableVersions?.join(' '), + report.expose, + report.failedPhase, + report.errorCode, + report.errorName, + report.errorMessage, + report.ownerHint, + report.summary?.recovered ? 'recovered' : undefined, + report.summary?.lastPhase, + report.diagnosis?.title, + report.loadedBefore?.consumers + ?.map((consumer) => + [consumer.name, ...(consumer.exposes || [])].join(' '), + ) + .join(' '), + report.__scope, + ] + .filter(Boolean) + .join(' ') + .toLowerCase(); + +const fuzzyMatchReport = ( + report: ObservabilityDevtoolsReport, + keyword: string, +) => { + const tokens = keyword.trim().toLowerCase().split(/\s+/).filter(Boolean); + if (!tokens.length) { + return true; + } + const haystack = getReportSearchText(report); + return tokens.every((token) => haystack.includes(token)); +}; + +const downloadJson = (name: string, data: unknown) => { + const blob = new Blob([`${JSON.stringify(data, null, 2)}\n`], { + type: 'application/json;charset=utf-8', + }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement('a'); + anchor.href = url; + anchor.download = name; + anchor.click(); + URL.revokeObjectURL(url); +}; + +interface CurrentLoadRow { + labelKey: string; + value?: string | number | boolean | false; +} + +const formatCurrentLoadValue = ( + value: string | number | boolean | false | undefined, +) => { + if (value === false) { + return 'false'; + } + if (value === true) { + return 'true'; + } + if (typeof value === 'number') { + return String(value); + } + return value || undefined; +}; + +const getConfigSignature = (config: ObservabilityDevtoolsConfig) => + JSON.stringify(normalizeObservabilityDevtoolsConfig(config)); + +interface LoadingTraceTabCache { + reports: ObservabilityDevtoolsReport[]; + selectedTraceId?: string; +} + +const getLoadingTraceTabKey = (tabId?: number) => + typeof tabId === 'number' ? String(tabId) : 'unknown'; + +const LoadingTrace = ({ tabId, resetKey = 0 }: LoadingTraceProps) => { + const { t } = useTranslation(); + const [config, setConfig] = useState( + DEFAULT_OBSERVABILITY_DEVTOOLS_CONFIG, + ); + const [savedConfig, setSavedConfig] = useState( + DEFAULT_OBSERVABILITY_DEVTOOLS_CONFIG, + ); + const [stored, setStored] = useState(false); + const [reports, setReports] = useState([]); + const [scopes, setScopes] = useState([]); + const [hasUserObservabilityPlugin, setHasUserObservabilityPlugin] = + useState(false); + const [selectedTraceId, setSelectedTraceId] = useState(); + const [busy, setBusy] = useState(false); + const [statusText, setStatusText] = useState(''); + const [showConfigPanel, setShowConfigPanel] = useState(false); + const [reportKeyword, setReportKeyword] = useState(''); + const refreshTimersRef = useRef>>([]); + const userPluginPollTimerRef = useRef | null>( + null, + ); + const tabCacheRef = useRef>(new Map()); + const activeTabKey = useMemo(() => getLoadingTraceTabKey(tabId), [tabId]); + const activeTabKeyRef = useRef(activeTabKey); + const resetKeyRef = useRef(resetKey); + + const filteredReports = useMemo( + () => reports.filter((report) => fuzzyMatchReport(report, reportKeyword)), + [reports, reportKeyword], + ); + + const selectedReport = useMemo( + () => + filteredReports.find((report) => report.traceId === selectedTraceId) || + filteredReports[0], + [filteredReports, selectedTraceId], + ); + + const latestReport = reports[0]; + const isObservabilityEnabled = stored && config.enabled !== false; + const isConfigDirty = useMemo( + () => getConfigSignature(config) !== getConfigSignature(savedConfig), + [config, savedConfig], + ); + const levelOptions = useMemo< + Array> + >( + () => [ + { + label: t('loadingTrace.config.verbose'), + value: 'verbose', + }, + { + label: t('loadingTrace.config.summary'), + value: 'summary', + }, + { + label: t('loadingTrace.config.error'), + value: 'error', + }, + ], + [t], + ); + const shouldShowApplyButton = !isObservabilityEnabled || isConfigDirty; + const emptyDescription = useMemo(() => { + if (!hasUserObservabilityPlugin && !isObservabilityEnabled) { + return t('loadingTrace.emptyEnableChrome'); + } + + return t('loadingTrace.empty'); + }, [hasUserObservabilityPlugin, isObservabilityEnabled, t]); + const sourceStateLabel = isObservabilityEnabled + ? 'ON' + : hasUserObservabilityPlugin + ? 'CUSTOM' + : 'OFF'; + const eventCount = useMemo( + () => + reports.reduce( + (count, report) => count + (report.events?.length || 0), + 0, + ), + [reports], + ); + + const applyTabCache = useCallback( + ( + tabKey: string, + update: + | LoadingTraceTabCache + | ((current: LoadingTraceTabCache) => LoadingTraceTabCache), + ) => { + const current = tabCacheRef.current.get(tabKey) || { + reports: [], + selectedTraceId: undefined, + }; + const next = typeof update === 'function' ? update(current) : update; + tabCacheRef.current.set(tabKey, next); + if (activeTabKeyRef.current === tabKey) { + setReports(next.reports); + setSelectedTraceId(next.selectedTraceId); + } + return next; + }, + [], + ); + + const clearTabCache = useCallback( + (tabKey: string) => + applyTabCache(tabKey, { + reports: [], + selectedTraceId: undefined, + }), + [applyTabCache], + ); + + const refreshSnapshot = useCallback( + async (targetTabKey = activeTabKeyRef.current) => { + const snapshot = await readObservabilitySnapshot(); + setConfig(snapshot.config); + setSavedConfig(snapshot.config); + setStored(snapshot.stored); + setScopes(snapshot.scopes); + setHasUserObservabilityPlugin(snapshot.hasUserObservabilityPlugin); + const cached = tabCacheRef.current.get(targetTabKey) || { + reports: [], + selectedTraceId: undefined, + }; + const mergedReports = mergeObservabilityReports( + cached.reports, + snapshot.reports, + ); + const nextCache = applyTabCache(targetTabKey, { + reports: mergedReports, + selectedTraceId: cached.selectedTraceId || snapshot.reports[0]?.traceId, + }); + + return { + ...snapshot, + reports: nextCache.reports, + }; + }, + [applyTabCache], + ); + + const clearScheduledRefresh = useCallback(() => { + refreshTimersRef.current.forEach((timer) => clearTimeout(timer)); + refreshTimersRef.current = []; + }, []); + + const scheduleRefreshBurst = useCallback(() => { + clearScheduledRefresh(); + const delays = [200, 600, 1200, 2400]; + const targetTabKey = activeTabKeyRef.current; + + refreshTimersRef.current = delays.map((delay, index) => + setTimeout(async () => { + const snapshot = await refreshSnapshot(targetTabKey); + const isLast = index === delays.length - 1; + if (snapshot.reports.length) { + setStatusText(t('loadingTrace.status.synced')); + clearScheduledRefresh(); + return; + } + if (isLast) { + setStatusText(t('loadingTrace.status.noReports')); + } + }, delay), + ); + }, [clearScheduledRefresh, refreshSnapshot, t]); + + useEffect(() => { + activeTabKeyRef.current = activeTabKey; + const cached = tabCacheRef.current.get(activeTabKey); + setReports(cached?.reports || []); + setSelectedTraceId(cached?.selectedTraceId); + setReportKeyword(''); + }, [activeTabKey]); + + useEffect(() => { + if (resetKey === resetKeyRef.current) { + return; + } + resetKeyRef.current = resetKey; + clearScheduledRefresh(); + clearTabCache(activeTabKeyRef.current); + setStatusText(t('loadingTrace.status.noReports')); + }, [clearScheduledRefresh, clearTabCache, resetKey, t]); + + useEffect(() => { + let cancelled = false; + const load = async () => { + try { + const pageConfig = await readObservabilityConfig(); + if (!cancelled) { + setConfig(pageConfig); + setSavedConfig(pageConfig); + } + const snapshot = await refreshSnapshot(); + if (!cancelled) { + setStatusText( + snapshot.stored + ? t('loadingTrace.status.enabled') + : snapshot.hasUserObservabilityPlugin + ? t('loadingTrace.status.userPlugin') + : t('loadingTrace.status.disabled'), + ); + } + } catch (error) { + if (!cancelled) { + setStatusText(t('loadingTrace.status.unavailable')); + } + } + }; + + void load(); + return () => { + cancelled = true; + clearScheduledRefresh(); + }; + }, [clearScheduledRefresh, refreshSnapshot, t, tabId]); + + useEffect(() => { + if (userPluginPollTimerRef.current) { + clearInterval(userPluginPollTimerRef.current); + userPluginPollTimerRef.current = null; + } + + if (!hasUserObservabilityPlugin) { + return; + } + + let polling = false; + userPluginPollTimerRef.current = setInterval(async () => { + if (polling) { + return; + } + polling = true; + try { + const snapshot = await refreshSnapshot(activeTabKeyRef.current); + if (snapshot.reports.length) { + setStatusText(t('loadingTrace.status.synced')); + } + } finally { + polling = false; + } + }, 1500); + + return () => { + if (userPluginPollTimerRef.current) { + clearInterval(userPluginPollTimerRef.current); + userPluginPollTimerRef.current = null; + } + }; + }, [hasUserObservabilityPlugin, refreshSnapshot, t]); + + useEffect(() => { + const onMessage = ( + message: { type?: string; data?: any }, + sender: chrome.runtime.MessageSender, + ) => { + if (message?.type !== MESSAGE_OBSERVABILITY_DEVTOOLS_EVENT) { + return; + } + const senderTabId = sender?.tab?.id; + const messageTabKey = getLoadingTraceTabKey(senderTabId || tabId); + const isCurrentTabMessage = + !tabId || !senderTabId || senderTabId === tabId; + + const payload = message.data; + if (payload?.config && isCurrentTabMessage) { + const nextConfig = normalizeObservabilityDevtoolsConfig(payload.config); + setConfig(nextConfig); + setSavedConfig(nextConfig); + setStored(payload.config.enabled !== false); + } + if (payload?.kind === 'installed' && isCurrentTabMessage) { + setStatusText(t('loadingTrace.status.enabled')); + scheduleRefreshBurst(); + } + if (payload?.report?.traceId) { + const report = { + ...payload.report, + __scope: payload.scope || payload.report.__scope, + } as ObservabilityDevtoolsReport; + applyTabCache(messageTabKey, (current) => ({ + reports: mergeObservabilityReports(current.reports, [report]), + selectedTraceId: current.selectedTraceId || report.traceId, + })); + if (isCurrentTabMessage) { + clearScheduledRefresh(); + setStatusText(t('loadingTrace.status.synced')); + } + } + }; + + chrome.runtime.onMessage.addListener(onMessage); + return () => chrome.runtime.onMessage.removeListener(onMessage); + }, [applyTabCache, clearScheduledRefresh, scheduleRefreshBurst, tabId, t]); + + const updateConfig = (patch: Partial) => { + setConfig((current) => + normalizeObservabilityDevtoolsConfig({ + ...current, + ...patch, + }), + ); + }; + + const applyAndReload = async () => { + setBusy(true); + try { + const nextConfig = normalizeObservabilityDevtoolsConfig({ + ...config, + enabled: true, + }); + await applyObservabilityConfig(nextConfig); + setConfig(nextConfig); + setSavedConfig(nextConfig); + setStored(true); + clearTabCache(activeTabKeyRef.current); + setStatusText(t('loadingTrace.status.reloading')); + await reloadInspectedPage(); + scheduleRefreshBurst(); + } finally { + setBusy(false); + } + }; + + const confirmApply = () => { + Modal.confirm({ + title: isObservabilityEnabled + ? t('loadingTrace.confirm.updateTitle') + : t('loadingTrace.confirm.observeTitle'), + content: t('loadingTrace.confirm.content'), + okText: t('loadingTrace.actions.confirm'), + cancelText: t('loadingTrace.actions.cancel'), + onOk: () => applyAndReload(), + }); + }; + + const disableAndReload = async () => { + setBusy(true); + try { + await disableObservabilityConfig(); + clearScheduledRefresh(); + setStored(false); + clearTabCache(activeTabKeyRef.current); + setStatusText(t('loadingTrace.status.disabled')); + await reloadInspectedPage(); + } finally { + setBusy(false); + } + }; + + const handleRefresh = async () => { + setBusy(true); + try { + const snapshot = await refreshSnapshot(); + setStatusText( + snapshot.reports.length + ? t('loadingTrace.status.synced') + : t('loadingTrace.status.noReports'), + ); + } finally { + setBusy(false); + } + }; + + const handleExport = () => { + downloadJson(`mf-observability-${Date.now()}.json`, { + exportedAt: new Date().toISOString(), + config, + scopes, + reports, + }); + }; + + const currentLoadRows = useMemo(() => { + if (!selectedReport) { + return []; + } + + const rows: CurrentLoadRow[] = [ + { + labelKey: 'consumer', + value: selectedReport.hostName, + }, + ]; + + if (selectedReport.shared) { + rows.push( + { + labelKey: 'shared', + value: selectedReport.shared.name, + }, + { + labelKey: 'provider', + value: selectedReport.shared.provider, + }, + { + labelKey: 'requiredVersion', + value: selectedReport.shared.requiredVersion, + }, + { + labelKey: 'selectedVersion', + value: selectedReport.shared.selectedVersion, + }, + { + labelKey: 'availableVersions', + value: selectedReport.shared.availableVersions?.join(', '), + }, + ); + } else { + rows.push( + { + labelKey: 'request', + value: selectedReport.requestId, + }, + { + labelKey: 'producer', + value: selectedReport.remote?.alias || selectedReport.remote?.name, + }, + { + labelKey: 'remoteName', + value: selectedReport.remote?.name, + }, + { + labelKey: 'expose', + value: selectedReport.expose, + }, + ); + } + + return rows.filter((row) => formatCurrentLoadValue(row.value)); + }, [selectedReport]); + + return ( +
+
+
+ {t('loadingTrace.title')} + {statusText ? ( + {statusText} + ) : null} +
+
+ + ) : null} + {isObservabilityEnabled ? ( + + ) : null} + + +
+
+ + {showConfigPanel ? ( + <> +
+ + +
+ + ) : null} + + <> +
+
+ + {sourceStateLabel} + {sourceStateLabel === 'CUSTOM' ? ( + + + + ) : null} + + + {t('loadingTrace.stats.state')} + +
+
+ {reports.length} + + {t('loadingTrace.stats.reports')} + +
+
+ {eventCount} + + {t('loadingTrace.stats.events')} + +
+
+ + {latestReport?.summary?.outcome || '-'} + + + {t('loadingTrace.stats.latest')} + +
+
+ +
+ {reports.length ? ( + <> +
+
+ } + allowClear + placeholder={t('loadingTrace.reports.search')} + value={reportKeyword} + onChange={(value) => setReportKeyword(value)} + /> +
+ {filteredReports.length ? ( + filteredReports.map((report) => { + const state = getReportState(report); + return ( + + ); + }) + ) : ( +
+ +
+ )} +
+ +
+ {selectedReport ? ( + <> + {(() => { + const limitedObservability = + getLimitedObservabilityLabel(selectedReport); + const observabilityScopeLabel = + getObservabilityReportScopeLabel(selectedReport); + return ( +
+
+ + {getReportTitle(selectedReport)} + + + {selectedReport.traceId} + +
+
+ + {t( + `loadingTrace.reports.${getReportState(selectedReport)}`, + )} + + {observabilityScopeLabel ? ( + + {observabilityScopeLabel} + + ) : null} + {limitedObservability ? ( + + {t('loadingTrace.reports.limited')} + + + + + ) : null} +
+
+ ); + })()} + {currentLoadRows.length ? ( +
+
+ + {t('loadingTrace.currentLoad.title')} + + + {selectedReport.shared + ? t('loadingTrace.currentLoad.sharedReport') + : t('loadingTrace.currentLoad.remoteReport')} + +
+
+ {currentLoadRows.map((row) => ( +
+ + {t(`loadingTrace.currentLoad.${row.labelKey}`)} + + + {formatCurrentLoadValue(row.value)} + +
+ ))} +
+
+ ) : null} + {selectedReport.diagnosis?.title ? ( +
+ {selectedReport.diagnosis.title} +
+ ) : null} + {selectedReport.loadedBefore ? ( +
+
+ + {t('loadingTrace.loadedBefore.title')} + + + {t('loadingTrace.loadedBefore.consumerCount', { + count: + selectedReport.loadedBefore.consumers.length, + })} + +
+
+ + {t('loadingTrace.loadedBefore.producerLoaded')} + + + {selectedReport.loadedBefore.expose + ? t('loadingTrace.loadedBefore.exposeLoaded') + : t('loadingTrace.loadedBefore.exposeNotLoaded')} + +
+
+ {selectedReport.loadedBefore.consumers.map( + (consumer, index) => ( +
+
+ + {t('loadingTrace.loadedBefore.consumer')} + + + {consumer.name || + t( + 'loadingTrace.loadedBefore.unknownConsumer', + )} + +
+
+ + {t('loadingTrace.loadedBefore.status')} + + + {[ + consumer.remoteEntryExports + ? t( + 'loadingTrace.loadedBefore.entryReady', + ) + : t( + 'loadingTrace.loadedBefore.entryMissing', + ), + consumer.containerInitialized + ? t( + 'loadingTrace.loadedBefore.initReady', + ) + : t( + 'loadingTrace.loadedBefore.initMissing', + ), + ].join(' · ')} + +
+
+ + {t('loadingTrace.loadedBefore.exposes')} + + + {consumer.exposes?.length + ? consumer.exposes.join(', ') + : t( + 'loadingTrace.loadedBefore.noExposes', + )} + +
+
+ ), + )} +
+
+ ) : null} +
+ {(selectedReport.events || []).map((event, index) => ( +
+
+ {formatTime(event.timestamp)} +
+
+
+ + {event.phase} + + + {event.status} + + {event.recovered ? ( + + {t('loadingTrace.reports.eventRecovered')} + + ) : null} + {event.duration !== undefined ? ( + + {event.duration}ms + + ) : null} +
+
+ {event.message || + event.lifecycle || + event.requestId || + '-'} +
+ {event.errorMessage ? ( +
+ {event.errorCode + ? `${event.errorCode}: ${event.errorMessage}` + : event.errorMessage} +
+ ) : null} +
+
+ ))} +
+ + ) : null} +
+ + ) : ( +
+ +
+ )} +
+ +
+ ); +}; + +export default LoadingTrace; diff --git a/packages/chrome-devtools/src/i18n/index.ts b/packages/chrome-devtools/src/i18n/index.ts index ad4f2c96e7e..57ba03d2473 100644 --- a/packages/chrome-devtools/src/i18n/index.ts +++ b/packages/chrome-devtools/src/i18n/index.ts @@ -13,6 +13,7 @@ const resources = { proxy: 'Proxy', dependency: 'Dependency graph', share: 'Shared', + loadingTrace: 'Loading trace', performance: 'Performance', }, header: { @@ -241,6 +242,102 @@ const resources = { entry: 'Entry', version: 'Version', }, + loadingTrace: { + title: 'Loading Trace', + empty: 'No loading report yet', + emptyEnableChrome: + 'No observability plugin was detected on the page. Enable Chrome observability to start collecting loading reports.', + status: { + enabled: 'Observability is enabled for the current page.', + disabled: 'Observability is not enabled for the current page.', + unavailable: 'Current page is unavailable.', + reloading: 'Configuration saved. Reloading current page.', + synced: 'Reports synced.', + noReports: 'No report found on the current page.', + userPlugin: + 'Application observability plugin detected. Syncing existing reports.', + }, + confirm: { + observeTitle: 'Start observability?', + updateTitle: 'Update configuration?', + content: + 'The current tab will reload after the configuration is saved.', + }, + actions: { + observeNow: 'Observe now', + updateConfig: 'Update config', + config: 'Configuration', + disable: 'Disable', + refresh: 'Sync', + export: 'Export', + confirm: 'Confirm', + cancel: 'Cancel', + }, + config: { + level: 'Level', + levelTip: + 'Controls how much event detail is kept. Verbose keeps the full timeline, summary keeps key results, and error focuses on failures.', + verbose: 'Verbose', + summary: 'Summary', + error: 'Error', + console: 'Console hints', + consoleTip: + 'Prints lightweight hints in the inspected page console. Turn it off when the page console should stay clean.', + }, + stats: { + state: 'State', + reports: 'Reports', + events: 'Events', + latest: 'Latest outcome', + customTip: + 'Custom means the inspected page already registered its own observability plugin, so reports are read from the page instead of only from the Chrome collection plugin.', + }, + reports: { + search: 'Filter reports', + noMatch: 'No matching report', + success: 'Success', + failed: 'Failed', + pending: 'Pending', + recovered: 'Recovered', + eventRecovered: 'Recovered', + limited: 'Basic observability', + lowVersionTip: + 'The current MF runtime version is too low, so only basic loading events can be collected. Fine-grained remoteEntry, init, expose, factory, and shared phases may be missing. Upgrade to 2.5.0+ for the full loading trace.', + unknownVersionTip: + 'The current MF runtime version cannot be detected, so Chrome observability may only collect basic loading events. Fine-grained remoteEntry, init, expose, factory, and shared phases may be missing.', + }, + currentLoad: { + title: 'Current loading', + remoteReport: 'Remote', + sharedReport: 'Shared dependency', + consumer: 'Current consumer', + request: 'Request', + producer: 'Producer', + remoteName: 'Remote name', + expose: 'Expose', + shared: 'Shared dependency', + provider: 'Provider', + requiredVersion: 'Required version', + selectedVersion: 'Selected version', + availableVersions: 'Available versions', + }, + loadedBefore: { + title: 'Other consumer loading records', + consumerCount: '{{count}} consumer(s)', + producerLoaded: 'Same producer was loaded before', + exposeLoaded: 'Current expose was loaded before', + exposeNotLoaded: 'Current expose was not loaded before', + consumer: 'Consumer', + status: 'Status', + exposes: 'Loaded exposes', + unknownConsumer: 'Unknown consumer', + entryReady: 'remoteEntry is available', + entryMissing: 'remoteEntry is not available', + initReady: 'container was initialized', + initMissing: 'container was not initialized', + noExposes: 'No expose record', + }, + }, }, }, 'zh-CN': { @@ -251,6 +348,7 @@ const resources = { proxy: '代理配置', dependency: '依赖关系图', share: '共享依赖', + loadingTrace: '加载追踪', performance: '性能', }, header: { @@ -473,6 +571,100 @@ const resources = { entry: '入口', version: '版本', }, + loadingTrace: { + title: '加载追踪', + empty: '暂无加载报告', + emptyEnableChrome: + '当前页面未检测到观测插件。可以开启 Chrome 采集来收集加载报告。', + status: { + enabled: '当前页面已开启加载追踪。', + disabled: '当前页面未开启加载追踪。', + unavailable: '当前页面暂不可用。', + reloading: '配置已保存,正在刷新当前页面。', + synced: '报告已同步。', + noReports: '当前页面还没有报告。', + userPlugin: '已检测到页面自带观测插件,正在同步已有报告。', + }, + confirm: { + observeTitle: '开启加载追踪?', + updateTitle: '更新配置?', + content: '保存配置后会刷新当前标签页。', + }, + actions: { + observeNow: '开启采集', + updateConfig: '更新配置', + config: '配置', + disable: '关闭', + refresh: '同步', + export: '导出', + confirm: '确认', + cancel: '取消', + }, + config: { + level: '记录级别', + levelTip: + '控制保留多少事件细节。完整会保留完整链路,摘要只保留关键结果,错误只关注失败。', + verbose: '完整', + summary: '摘要', + error: '错误', + console: '控制台提示', + consoleTip: + '在被调试页面的控制台打印轻量提示。如果想保持页面控制台干净,可以关闭。', + }, + stats: { + state: '状态', + reports: '报告', + events: '事件', + latest: '最新结果', + customTip: + 'Custom 表示当前页面已经自己注册了观测插件,报告会从页面已有插件中读取,不只依赖 Chrome 插件注入采集。', + }, + reports: { + search: '过滤报告', + noMatch: '没有匹配的报告', + success: '成功', + failed: '失败', + pending: '进行中', + recovered: '兜底成功', + eventRecovered: '兜底', + limited: '基础观测', + lowVersionTip: + '当前 MF 运行时版本较低,只能采集基础加载事件。remoteEntry、init、expose、factory、shared 等细阶段可能缺失。升级到 2.5.0+ 后可获得完整链路。', + unknownVersionTip: + '当前 MF 运行时版本无法识别,Chrome 观测可能只能采集基础加载事件。remoteEntry、init、expose、factory、shared 等细阶段可能缺失。', + }, + currentLoad: { + title: '当前加载', + remoteReport: '远程模块', + sharedReport: '共享依赖', + consumer: '当前消费方', + request: '请求', + producer: '生产者', + remoteName: '生产者名称', + expose: 'Expose', + shared: '共享依赖', + provider: '提供方', + requiredVersion: '要求版本', + selectedVersion: '实际版本', + availableVersions: '可用版本', + }, + loadedBefore: { + title: '其他消费方加载记录', + consumerCount: '{{count}} 个消费方', + producerLoaded: '同一生产者之前已被加载', + exposeLoaded: '当前 expose 之前已被加载', + exposeNotLoaded: '当前 expose 之前未被加载', + consumer: '消费方', + status: '状态', + exposes: '已加载 expose', + unknownConsumer: '未知消费方', + entryReady: 'remoteEntry 已拿到', + entryMissing: 'remoteEntry 未拿到', + initReady: '容器已初始化', + initMissing: '容器未初始化', + noExposes: '暂无导出记录', + }, + }, }, }, } as const; diff --git a/packages/chrome-devtools/src/utils/chrome/index.ts b/packages/chrome-devtools/src/utils/chrome/index.ts index c8d13af0429..25c2d315b84 100644 --- a/packages/chrome-devtools/src/utils/chrome/index.ts +++ b/packages/chrome-devtools/src/utils/chrome/index.ts @@ -38,10 +38,11 @@ export const syncActiveTab = async (tabId?: number) => { setTargetTab(tab); return tab; } - const [activeTab] = await getTabs({ + const tabs = await getTabs({ active: true, lastFocusedWindow: true, }); + const activeTab = Array.isArray(tabs) ? tabs[0] : undefined; setTargetTab(activeTab); return activeTab; } catch (error) { @@ -66,9 +67,9 @@ export function getInspectWindowTabId() { function (info, error) { const { tabId } = chrome.devtools.inspectedWindow; getTabs().then((tabs) => { - const target = tabs.find( - (tab: chrome.tabs.Tab) => tab.id === tabId, - ); + const target = Array.isArray(tabs) + ? tabs.find((tab: chrome.tabs.Tab) => tab.id === tabId) + : undefined; setTargetTab(target as chrome.tabs.Tab); }); console.log( diff --git a/packages/chrome-devtools/src/utils/chrome/messages.ts b/packages/chrome-devtools/src/utils/chrome/messages.ts index aacda8b7b2c..057d48bacf8 100644 --- a/packages/chrome-devtools/src/utils/chrome/messages.ts +++ b/packages/chrome-devtools/src/utils/chrome/messages.ts @@ -1,2 +1,7 @@ export const MESSAGE_OPEN_SIDE_PANEL = 'mf-devtools/open-side-panel'; export const MESSAGE_ACTIVE_TAB_CHANGED = 'mf-devtools/active-tab-changed'; +export const MESSAGE_OBSERVABILITY_DEVTOOLS_EVENT = + 'mf-devtools/observability-event'; +export const OBSERVABILITY_DEVTOOLS_SOURCE = 'module-federation/observability'; +export const OBSERVABILITY_DEVTOOLS_STORAGE_KEY = + '__MF_DEVTOOLS_OBSERVABILITY_CONFIG__'; diff --git a/packages/chrome-devtools/src/utils/chrome/observability-plugin.ts b/packages/chrome-devtools/src/utils/chrome/observability-plugin.ts new file mode 100644 index 00000000000..6de26352aba --- /dev/null +++ b/packages/chrome-devtools/src/utils/chrome/observability-plugin.ts @@ -0,0 +1,127 @@ +import { + ChromeObservabilityPlugin, + type ObservabilityPluginOptions, +} from '@module-federation/observability-plugin/chrome-devtool'; + +import { + OBSERVABILITY_DEVTOOLS_SOURCE, + OBSERVABILITY_DEVTOOLS_STORAGE_KEY, +} from './messages'; +import { + createObservabilityPluginOptions, + normalizeObservabilityDevtoolsConfig, +} from './observability-shared'; + +const DEVTOOLS_PLUGIN_NAME = 'observability-plugin:chrome-extension'; +const LEGACY_DEVTOOLS_PLUGIN_NAME = 'observability-plugin-devtools'; + +type FederationGlobal = { + __GLOBAL_PLUGIN__?: Array<{ name?: string }>; +} & Record; + +type FederationWindow = Window & { + __FEDERATION__?: FederationGlobal; + __VMOK__?: FederationGlobal; +}; + +const getFederationWindow = () => window as unknown as FederationWindow; + +const safeReadStoredConfig = () => { + try { + const raw = window.localStorage?.getItem( + OBSERVABILITY_DEVTOOLS_STORAGE_KEY, + ); + if (!raw) { + return undefined; + } + return normalizeObservabilityDevtoolsConfig(JSON.parse(raw)); + } catch { + return undefined; + } +}; + +const defineWritableGlobal = ( + key: '__FEDERATION__' | '__VMOK__', + value: any, +) => { + const targetWindow = getFederationWindow(); + try { + Object.defineProperty(targetWindow, key, { + value, + configurable: true, + writable: true, + }); + } catch { + targetWindow[key] = value; + } +}; + +const ensureFederationGlobal = () => { + const targetWindow = getFederationWindow(); + const federation = targetWindow.__FEDERATION__ || targetWindow.__VMOK__ || {}; + + if (!targetWindow.__FEDERATION__) { + defineWritableGlobal('__FEDERATION__', federation); + } + if (!targetWindow.__VMOK__) { + defineWritableGlobal('__VMOK__', targetWindow.__FEDERATION__); + } + + if (!targetWindow.__FEDERATION__?.__GLOBAL_PLUGIN__) { + targetWindow.__FEDERATION__ = targetWindow.__FEDERATION__ || federation; + targetWindow.__FEDERATION__.__GLOBAL_PLUGIN__ = []; + } + + return targetWindow.__FEDERATION__; +}; + +const notifyInstalled = ( + status: 'installed' | 'skipped', + reason?: string, + config?: ReturnType, +) => { + try { + window.postMessage( + { + schemaVersion: 1, + source: OBSERVABILITY_DEVTOOLS_SOURCE, + kind: status, + reason, + config, + createdAt: Date.now(), + }, + '*', + ); + } catch { + // Devtools status delivery is best effort only. + } +}; + +const install = () => { + const config = safeReadStoredConfig(); + if (!config?.enabled) { + return; + } + + const federation = ensureFederationGlobal(); + const globalPlugins = federation?.__GLOBAL_PLUGIN__ || []; + if ( + globalPlugins.some( + (plugin) => + plugin?.name === DEVTOOLS_PLUGIN_NAME || + plugin?.name === LEGACY_DEVTOOLS_PLUGIN_NAME, + ) + ) { + notifyInstalled('skipped', 'already-installed', config); + return; + } + + const plugin = ChromeObservabilityPlugin( + createObservabilityPluginOptions(config) as ObservabilityPluginOptions, + ); + globalPlugins.push(plugin); + federation.__GLOBAL_PLUGIN__ = globalPlugins; + notifyInstalled('installed', undefined, config); +}; + +install(); diff --git a/packages/chrome-devtools/src/utils/chrome/observability-shared.ts b/packages/chrome-devtools/src/utils/chrome/observability-shared.ts new file mode 100644 index 00000000000..79b6af0150b --- /dev/null +++ b/packages/chrome-devtools/src/utils/chrome/observability-shared.ts @@ -0,0 +1,139 @@ +import { OBSERVABILITY_DEVTOOLS_SOURCE } from './messages'; + +export type ObservabilityDevtoolsLevel = 'error' | 'summary' | 'verbose'; +export type ObservabilityDevtoolsMode = 'development' | 'production'; + +export interface ObservabilityDevtoolsConfig { + enabled: boolean; + level: ObservabilityDevtoolsLevel; + maxEvents: number; + console: boolean; + browser: { + enabled: boolean; + scope: string; + mode: ObservabilityDevtoolsMode; + }; + trace: { + printStart: boolean; + }; + react: { + injectLoadedCallback: boolean; + remoteIds: string[]; + }; +} + +export const CHROME_OBSERVABILITY_SCOPE = 'chrome_extension'; +const MIN_EVENTS = 10; +const MAX_EVENTS = 1000; + +export const DEFAULT_OBSERVABILITY_DEVTOOLS_CONFIG: ObservabilityDevtoolsConfig = + { + enabled: true, + level: 'verbose', + maxEvents: 300, + console: true, + browser: { + enabled: true, + scope: CHROME_OBSERVABILITY_SCOPE, + mode: 'development', + }, + trace: { + printStart: true, + }, + react: { + injectLoadedCallback: false, + remoteIds: [], + }, + }; + +const isObject = (value: unknown): value is Record => + typeof value === 'object' && value !== null; + +const normalizeBoolean = (value: unknown, fallback: boolean) => + typeof value === 'boolean' ? value : fallback; + +const normalizeLevel = (value: unknown): ObservabilityDevtoolsLevel => + value === 'error' || value === 'summary' || value === 'verbose' + ? value + : DEFAULT_OBSERVABILITY_DEVTOOLS_CONFIG.level; + +const normalizeMode = (value: unknown): ObservabilityDevtoolsMode => + value === 'production' || value === 'development' + ? value + : DEFAULT_OBSERVABILITY_DEVTOOLS_CONFIG.browser.mode; + +const normalizeMaxEvents = (value: unknown) => { + const parsed = Number(value); + if (!Number.isFinite(parsed)) { + return DEFAULT_OBSERVABILITY_DEVTOOLS_CONFIG.maxEvents; + } + return Math.max(MIN_EVENTS, Math.min(MAX_EVENTS, Math.floor(parsed))); +}; + +const normalizeScope = (value: unknown) => { + if (typeof value !== 'string') { + return CHROME_OBSERVABILITY_SCOPE; + } + const trimmed = value.trim().replace(/[^\w:@.-]+/g, '-'); + return trimmed || CHROME_OBSERVABILITY_SCOPE; +}; + +export const normalizeObservabilityDevtoolsConfig = ( + value?: Partial | Record | null, +): ObservabilityDevtoolsConfig => { + const source = isObject(value) ? value : {}; + const browser = isObject(source.browser) ? source.browser : {}; + const trace = isObject(source.trace) ? source.trace : {}; + + return { + enabled: normalizeBoolean( + source.enabled, + DEFAULT_OBSERVABILITY_DEVTOOLS_CONFIG.enabled, + ), + level: normalizeLevel(source.level), + maxEvents: normalizeMaxEvents(source.maxEvents), + console: normalizeBoolean( + source.console, + DEFAULT_OBSERVABILITY_DEVTOOLS_CONFIG.console, + ), + browser: { + enabled: normalizeBoolean( + browser.enabled, + DEFAULT_OBSERVABILITY_DEVTOOLS_CONFIG.browser.enabled, + ), + scope: normalizeScope(browser.scope), + mode: normalizeMode(browser.mode), + }, + trace: { + printStart: normalizeBoolean( + trace.printStart, + DEFAULT_OBSERVABILITY_DEVTOOLS_CONFIG.trace.printStart, + ), + }, + react: { + injectLoadedCallback: false, + remoteIds: [], + }, + }; +}; + +export const createObservabilityPluginOptions = ( + config: ObservabilityDevtoolsConfig, +) => ({ + enabled: config.enabled, + level: config.level, + maxEvents: config.maxEvents, + console: config.console, + browser: { + enabled: config.browser.enabled, + scope: config.browser.scope, + mode: config.browser.mode, + }, + trace: { + printStart: config.trace.printStart, + }, + devtools: { + enabled: true, + source: OBSERVABILITY_DEVTOOLS_SOURCE, + }, +}); diff --git a/packages/chrome-devtools/src/utils/chrome/observability.ts b/packages/chrome-devtools/src/utils/chrome/observability.ts new file mode 100644 index 00000000000..9f5b18c86dc --- /dev/null +++ b/packages/chrome-devtools/src/utils/chrome/observability.ts @@ -0,0 +1,328 @@ +import { injectScript } from './index'; +import { OBSERVABILITY_DEVTOOLS_STORAGE_KEY } from './messages'; +import { + CHROME_OBSERVABILITY_SCOPE, + DEFAULT_OBSERVABILITY_DEVTOOLS_CONFIG, + normalizeObservabilityDevtoolsConfig, + type ObservabilityDevtoolsConfig, +} from './observability-shared'; + +export interface ObservabilityDevtoolsEvent { + traceId: string; + timestamp: number; + phase: string; + status: string; + requestId?: string; + lifecycle?: string; + message?: string; + runtimeVersion?: string; + remote?: { + name?: string; + entry?: string; + alias?: string; + }; + shared?: { + name?: string; + shareScope?: string[]; + requiredVersion?: string | false; + selectedVersion?: string; + availableVersions?: string[]; + provider?: string; + reason?: string; + }; + expose?: string; + duration?: number; + errorCode?: string; + errorName?: string; + errorMessage?: string; + ownerHint?: string; + recovered?: boolean; + cached?: boolean; + loadedBefore?: ObservabilityDevtoolsLoadedBefore; +} + +export interface ObservabilityDevtoolsLoadedBefore { + producer: boolean; + expose: boolean; + consumers: Array<{ + name?: string; + remoteEntryExports?: boolean; + containerInitialized?: boolean; + exposes?: string[]; + }>; +} + +export interface ObservabilityDevtoolsReport { + traceId: string; + status: string; + requestId?: string; + hostName?: string; + runtimeVersion?: string; + remote?: { + name?: string; + entry?: string; + alias?: string; + }; + shared?: { + name?: string; + shareScope?: string[]; + requiredVersion?: string | false; + selectedVersion?: string; + availableVersions?: string[]; + provider?: string; + reason?: string; + }; + expose?: string; + startedAt: number; + updatedAt: number; + duration: number; + failedPhase?: string; + errorCode?: string; + errorName?: string; + errorMessage?: string; + ownerHint?: string; + loadedBefore?: ObservabilityDevtoolsLoadedBefore; + events: ObservabilityDevtoolsEvent[]; + summary?: { + outcome?: string; + runtimeLoaded?: boolean; + sharedResolved?: boolean; + componentLoaded?: boolean; + loadCompleted?: boolean; + recovered?: boolean; + lastPhase?: string; + }; + diagnosis?: { + title?: string; + ownerHint?: string; + errorCode?: string; + actions?: Array<{ title?: string; detail?: string }>; + warnings?: string[]; + }; + __scope?: string; +} + +export interface ObservabilityDevtoolsSnapshot { + config: ObservabilityDevtoolsConfig; + stored: boolean; + scopes: string[]; + reports: ObservabilityDevtoolsReport[]; + hasUserObservabilityPlugin: boolean; +} + +const USER_OBSERVABILITY_PLUGIN_NAME = 'observability-plugin'; +const CHROME_OBSERVABILITY_PLUGIN_NAME = + 'observability-plugin:chrome-extension'; +const LEGACY_CHROME_OBSERVABILITY_PLUGIN_NAME = 'observability-plugin-devtools'; +const OBSERVABILITY_SNAPSHOT_CONTEXT = { + chromeScope: CHROME_OBSERVABILITY_SCOPE, + userPluginName: USER_OBSERVABILITY_PLUGIN_NAME, + chromePluginNames: [ + CHROME_OBSERVABILITY_PLUGIN_NAME, + LEGACY_CHROME_OBSERVABILITY_PLUGIN_NAME, + ], +}; + +const readConfigFromPage = (storageKey: string) => { + try { + const raw = window.localStorage?.getItem(storageKey); + return raw ? JSON.parse(raw) : null; + } catch { + return null; + } +}; + +const writeConfigToPage = (storageKey: string, config: unknown) => { + window.localStorage?.setItem(storageKey, JSON.stringify(config)); + return config; +}; + +const removeConfigFromPage = (storageKey: string) => { + window.localStorage?.removeItem(storageKey); + return true; +}; + +const reloadPage = () => { + globalThis.location?.reload(); +}; + +const readSnapshotFromPage = ( + storageKey: string, + context?: { + chromeScope?: string; + userPluginName?: string; + chromePluginNames?: string[]; + }, +) => { + const chromeScope = + typeof context?.chromeScope === 'string' + ? context.chromeScope + : 'chrome_extension'; + const userPluginName = + typeof context?.userPluginName === 'string' + ? context.userPluginName + : 'observability-plugin'; + const chromePluginNames = Array.isArray(context?.chromePluginNames) + ? context.chromePluginNames + : [ + 'observability-plugin:chrome-extension', + 'observability-plugin-devtools', + ]; + const safeCopy = (value: unknown) => { + try { + return JSON.parse(JSON.stringify(value)); + } catch { + return undefined; + } + }; + + const rawConfig = (() => { + try { + const raw = window.localStorage?.getItem(storageKey); + return raw ? JSON.parse(raw) : null; + } catch { + return null; + } + })(); + const federation = (window as any).__FEDERATION__ || (window as any).__VMOK__; + const readers = federation?.__OBSERVABILITY__ || {}; + const reports: Array = []; + const scopes = Object.keys(readers); + const isChromeObservabilityPluginName = (name: unknown) => + chromePluginNames.includes(String(name)); + const hasUserObservabilityPluginFromInstances = Array.isArray( + federation?.__INSTANCES__, + ) + ? federation.__INSTANCES__.some((instance: any) => { + const plugins = instance?.options?.plugins; + if (!Array.isArray(plugins)) { + return false; + } + + return plugins.some( + (plugin) => + plugin?.name === userPluginName && + !isChromeObservabilityPluginName(plugin?.name), + ); + }) + : false; + const hasUserObservabilityPluginFromReaders = scopes.some( + (scope) => scope !== chromeScope, + ); + + scopes.forEach((scope) => { + const reader = readers[scope]; + if (!reader || typeof reader.getReports !== 'function') { + return; + } + + try { + const scopeReports = reader.getReports({ limit: 100 }); + if (!Array.isArray(scopeReports)) { + return; + } + scopeReports.forEach((report) => { + const copied = safeCopy(report) as ObservabilityDevtoolsReport; + if (copied?.traceId) { + copied.__scope = scope; + reports.push(copied); + } + }); + } catch { + // One broken reader should not block other scopes. + } + }); + + return { + config: rawConfig, + stored: Boolean(rawConfig), + scopes, + reports, + hasUserObservabilityPlugin: + hasUserObservabilityPluginFromInstances || + hasUserObservabilityPluginFromReaders, + }; +}; + +export const readObservabilityConfig = async () => { + const config = await injectScript( + readConfigFromPage, + false, + OBSERVABILITY_DEVTOOLS_STORAGE_KEY, + ); + + return normalizeObservabilityDevtoolsConfig( + config || DEFAULT_OBSERVABILITY_DEVTOOLS_CONFIG, + ); +}; + +export const applyObservabilityConfig = async ( + config: ObservabilityDevtoolsConfig, +) => + injectScript( + writeConfigToPage, + false, + OBSERVABILITY_DEVTOOLS_STORAGE_KEY, + normalizeObservabilityDevtoolsConfig(config), + ); + +export const disableObservabilityConfig = async () => + injectScript(removeConfigFromPage, false, OBSERVABILITY_DEVTOOLS_STORAGE_KEY); + +export const reloadInspectedPage = async () => injectScript(reloadPage, false); + +export const readObservabilitySnapshot = + async (): Promise => { + const snapshot = await injectScript( + readSnapshotFromPage, + true, + OBSERVABILITY_DEVTOOLS_STORAGE_KEY, + OBSERVABILITY_SNAPSHOT_CONTEXT, + ); + + return { + config: normalizeObservabilityDevtoolsConfig( + snapshot?.config || DEFAULT_OBSERVABILITY_DEVTOOLS_CONFIG, + ), + stored: Boolean(snapshot?.stored), + scopes: Array.isArray(snapshot?.scopes) ? snapshot.scopes : [], + reports: Array.isArray(snapshot?.reports) ? snapshot.reports : [], + hasUserObservabilityPlugin: Boolean(snapshot?.hasUserObservabilityPlugin), + }; + }; + +export const mergeObservabilityReports = ( + currentReports: ObservabilityDevtoolsReport[], + incomingReports: ObservabilityDevtoolsReport[], +) => { + const merged = new Map(); + + currentReports.forEach((report) => { + if (report.traceId) { + merged.set(report.traceId, report); + } + }); + incomingReports.forEach((report) => { + if (report.traceId) { + merged.set(report.traceId, report); + } + }); + + return Array.from(merged.values()).sort((left, right) => { + if ((right.updatedAt || 0) !== (left.updatedAt || 0)) { + return (right.updatedAt || 0) - (left.updatedAt || 0); + } + return (right.startedAt || 0) - (left.startedAt || 0); + }); +}; + +export const getObservabilityReportScopeLabel = ( + report: Pick, +) => { + const scope = report.__scope; + if (!scope || scope === CHROME_OBSERVABILITY_SCOPE) { + return undefined; + } + + return `custom: ${scope}`; +}; diff --git a/packages/chrome-devtools/src/utils/chrome/post-message-listener.ts b/packages/chrome-devtools/src/utils/chrome/post-message-listener.ts index 696d43e6f68..7f5c06a4329 100644 --- a/packages/chrome-devtools/src/utils/chrome/post-message-listener.ts +++ b/packages/chrome-devtools/src/utils/chrome/post-message-listener.ts @@ -1,10 +1,27 @@ import { sanitizePostMessagePayload } from './safe-post-message'; +import { + MESSAGE_OBSERVABILITY_DEVTOOLS_EVENT, + OBSERVABILITY_DEVTOOLS_SOURCE, +} from './messages'; if (window.moduleHandler) { window.removeEventListener('message', window.moduleHandler); } else { window.moduleHandler = (event) => { const { origin, data } = event; + if (data?.source === OBSERVABILITY_DEVTOOLS_SOURCE) { + chrome.runtime + .sendMessage({ + type: MESSAGE_OBSERVABILITY_DEVTOOLS_EVENT, + origin, + data: sanitizePostMessagePayload(data), + }) + .catch(() => { + return false; + }); + return; + } + if (!data.moduleInfo) { return; } diff --git a/packages/chrome-devtools/src/worker/index.ts b/packages/chrome-devtools/src/worker/index.ts index bb769d612c7..c4df5f39e21 100644 --- a/packages/chrome-devtools/src/worker/index.ts +++ b/packages/chrome-devtools/src/worker/index.ts @@ -18,11 +18,18 @@ const resolveTabId = async (tabId?: number) => { return activeTab?.id; }; -const broadcastActiveTab = (tabId: number) => { +const broadcastActiveTab = ( + tabId: number, + payload?: { + reason?: 'side-panel' | 'activated' | 'updated'; + status?: chrome.tabs.TabChangeInfo['status']; + }, +) => { try { chrome.runtime.sendMessage({ type: MESSAGE_ACTIVE_TAB_CHANGED, tabId, + ...payload, }); } catch (error) { console.warn( @@ -52,12 +59,12 @@ const openSidePanel = async (tabId?: number) => { if (sidePanel.open) { await sidePanel.open({ tabId: targetTabId }); } - broadcastActiveTab(targetTabId); + broadcastActiveTab(targetTabId, { reason: 'side-panel' }); if (sidePanel.getOptions) { try { const options = await sidePanel.getOptions({ tabId: targetTabId }); - broadcastActiveTab(targetTabId); + broadcastActiveTab(targetTabId, { reason: 'side-panel' }); return options; } catch (error) { console.warn('[Module Federation Devtools] getOptions failed', error); @@ -113,7 +120,7 @@ chrome.tabs.onActivated.addListener(async (activeInfo) => { return; } try { - broadcastActiveTab(tabId); + broadcastActiveTab(tabId, { reason: 'activated' }); } catch (error) { console.warn( '[Module Federation Devtools] Failed to handle tab activation', @@ -123,12 +130,15 @@ chrome.tabs.onActivated.addListener(async (activeInfo) => { }); chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { - if (changeInfo.status !== 'complete') { + if (changeInfo.status !== 'loading' && changeInfo.status !== 'complete') { return; } if (tab?.active) { try { - broadcastActiveTab(tabId); + broadcastActiveTab(tabId, { + reason: 'updated', + status: changeInfo.status, + }); } catch (error) { console.warn( '[Module Federation Devtools] Failed to handle tab update', diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 9819214cd44..62b7e9486b0 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,15 @@ # @module-federation/cli +## 2.5.0 + +### Patch Changes + +- Updated dependencies [5d4095d] +- Updated dependencies [0716c11] +- Updated dependencies [13dce52] + - @module-federation/sdk@2.5.0 + - @module-federation/dts-plugin@2.5.0 + ## 2.4.0 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index bd1d7c047c4..560e0ca395f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/cli", - "version": "2.4.0", + "version": "2.5.0", "type": "commonjs", "description": "Module Federation CLI", "homepage": "https://module-federation.io", diff --git a/packages/create-module-federation/CHANGELOG.md b/packages/create-module-federation/CHANGELOG.md index 17425d28ecf..59f70008c9a 100644 --- a/packages/create-module-federation/CHANGELOG.md +++ b/packages/create-module-federation/CHANGELOG.md @@ -1,5 +1,7 @@ # create-module-federation +## 2.5.0 + ## 2.4.0 ## 2.3.3 diff --git a/packages/create-module-federation/package.json b/packages/create-module-federation/package.json index 8345132451f..47e889b01f4 100644 --- a/packages/create-module-federation/package.json +++ b/packages/create-module-federation/package.json @@ -3,7 +3,7 @@ "description": "Create a new Module Federation project", "public": true, "sideEffects": false, - "version": "2.4.0", + "version": "2.5.0", "license": "MIT", "repository": { "type": "git", diff --git a/packages/dts-plugin/CHANGELOG.md b/packages/dts-plugin/CHANGELOG.md index 1e09f7364bc..ced2e5fb0a4 100644 --- a/packages/dts-plugin/CHANGELOG.md +++ b/packages/dts-plugin/CHANGELOG.md @@ -1,5 +1,18 @@ # @module-federation/dts-plugin +## 2.5.0 + +### Patch Changes + +- 13dce52: fix(dts-plugin): read manifest metadata when consuming DTS files from manifest remotes. +- Updated dependencies [5d4095d] +- Updated dependencies [0716c11] +- Updated dependencies [41281f4] + - @module-federation/sdk@2.5.0 + - @module-federation/error-codes@2.5.0 + - @module-federation/managers@2.5.0 + - @module-federation/third-party-dts-extractor@2.5.0 + ## 2.4.0 ### Patch Changes diff --git a/packages/dts-plugin/package.json b/packages/dts-plugin/package.json index 3b25518ea56..ca3acd9135b 100644 --- a/packages/dts-plugin/package.json +++ b/packages/dts-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/dts-plugin", - "version": "2.4.0", + "version": "2.5.0", "author": "hanric ", "main": "./dist/index.js", "module": "./dist/esm/index.mjs", diff --git a/packages/enhanced/CHANGELOG.md b/packages/enhanced/CHANGELOG.md index fafd418d2d5..eb8bdf2c195 100644 --- a/packages/enhanced/CHANGELOG.md +++ b/packages/enhanced/CHANGELOG.md @@ -1,5 +1,25 @@ # @module-federation/enhanced +## 2.5.0 + +### Patch Changes + +- Updated dependencies [5d4095d] +- Updated dependencies [0716c11] +- Updated dependencies [13dce52] +- Updated dependencies [41281f4] + - @module-federation/sdk@2.5.0 + - @module-federation/dts-plugin@2.5.0 + - @module-federation/error-codes@2.5.0 + - @module-federation/bridge-react-webpack-plugin@2.5.0 + - @module-federation/cli@2.5.0 + - @module-federation/managers@2.5.0 + - @module-federation/manifest@2.5.0 + - @module-federation/rspack@2.5.0 + - @module-federation/webpack-bundler-runtime@2.5.0 + - @module-federation/runtime-tools@2.5.0 + - @module-federation/inject-external-runtime-core-plugin@2.5.0 + ## 2.4.0 ### Minor Changes diff --git a/packages/enhanced/package.json b/packages/enhanced/package.json index 4ae21f6f06a..1c831d0857d 100644 --- a/packages/enhanced/package.json +++ b/packages/enhanced/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/enhanced", - "version": "2.4.0", + "version": "2.5.0", "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "repository": { diff --git a/packages/error-codes/CHANGELOG.md b/packages/error-codes/CHANGELOG.md index 6b5fc1f940b..0f588e19bdc 100644 --- a/packages/error-codes/CHANGELOG.md +++ b/packages/error-codes/CHANGELOG.md @@ -1,5 +1,11 @@ # @module-federation/error-codes +## 2.5.0 + +### Minor Changes + +- 41281f4: Add an opt-in observability plugin, a Chrome-extension-safe observability plugin entry with an independent name and fixed browser scope, a direct runtime plugin API with instance-bound component loaded marks, explicit temporary React `onMFRemoteLoaded` callback injection for matched remotes, opt-in start console traces for `loadRemote` and `loadShare`, a local collector mode for AI-assisted browser debugging, a Node-specific export for file reports, a build-specific export for build summaries and build error reports, remote and shared lifecycle hooks, console trace hints, safe browser/Node report outputs, configurable error stack capture with explicit console raw-stack opt-ins, shared/eager loading evidence gated to stable runtime `2.5.0+` for Chrome-extension compatibility, final loading outcome summaries for Module Federation loading reports including resolved shared dependencies, deterministic fact reports for runtime and build failures, no-op return handling for observer hooks, detailed remote match/init/expose/factory phase events with phase durations, compact phase summaries, cache/fallback markers, loaded-before evidence from existing federation instances when a remote load fails, length-limited business component metadata, clipped moduleInfo evidence with preserved deployment locator fields for snapshot-dependent failures, normalized runtime error summaries with error codes, owner hints, retryability, and safe context, dedicated runtime error codes for invalid manifests, missing exposes, and remote container init failures, plus MF skill guidance for reading and fixing observability reports. + ## 2.4.0 ## 2.3.3 diff --git a/packages/error-codes/package.json b/packages/error-codes/package.json index 6b52dfff700..3938775e6af 100644 --- a/packages/error-codes/package.json +++ b/packages/error-codes/package.json @@ -4,7 +4,7 @@ "author": "zhanghang ", "public": true, "sideEffects": false, - "version": "2.4.0", + "version": "2.5.0", "license": "MIT", "repository": { "type": "git", diff --git a/packages/error-codes/src/MFContext.ts b/packages/error-codes/src/MFContext.ts index 81272178abb..9f6eae8dbb7 100644 --- a/packages/error-codes/src/MFContext.ts +++ b/packages/error-codes/src/MFContext.ts @@ -56,7 +56,7 @@ export interface MFEnvironmentInfo { isCI?: boolean; } -/** Most recent diagnostic event (from .mf/diagnostics/latest.json) */ +/** Most recent observability event (from .mf/observability/latest.json) */ export interface MFLatestErrorEvent { code: string; message: string; diff --git a/packages/error-codes/src/desc.ts b/packages/error-codes/src/desc.ts index 75f9f45830d..b5737b89a5d 100644 --- a/packages/error-codes/src/desc.ts +++ b/packages/error-codes/src/desc.ts @@ -11,6 +11,9 @@ import { RUNTIME_010, RUNTIME_011, RUNTIME_012, + RUNTIME_013, + RUNTIME_014, + RUNTIME_015, TYPE_001, BUILD_001, BUILD_002, @@ -31,6 +34,9 @@ export const runtimeDescMap = { [RUNTIME_011]: 'The remoteEntry URL is missing from the remote snapshot.', [RUNTIME_012]: 'The getter for the shared module is not a function. This may be caused by setting "shared.import: false" without the host providing the corresponding lib.', + [RUNTIME_013]: 'The manifest is not a valid Module Federation manifest.', + [RUNTIME_014]: 'The remote does not expose the requested module.', + [RUNTIME_015]: 'Remote container initialization failed.', }; export const typeDescMap = { diff --git a/packages/error-codes/src/error-codes.ts b/packages/error-codes/src/error-codes.ts index 18ef6a5d5cf..cfc4ac51d00 100644 --- a/packages/error-codes/src/error-codes.ts +++ b/packages/error-codes/src/error-codes.ts @@ -10,6 +10,9 @@ export const RUNTIME_009 = 'RUNTIME-009'; export const RUNTIME_010 = 'RUNTIME-010'; export const RUNTIME_011 = 'RUNTIME-011'; export const RUNTIME_012 = 'RUNTIME-012'; +export const RUNTIME_013 = 'RUNTIME-013'; +export const RUNTIME_014 = 'RUNTIME-014'; +export const RUNTIME_015 = 'RUNTIME-015'; export const TYPE_001 = 'TYPE-001'; export const BUILD_001 = 'BUILD-001'; diff --git a/packages/esbuild/CHANGELOG.md b/packages/esbuild/CHANGELOG.md index 304813b294a..2722b227792 100644 --- a/packages/esbuild/CHANGELOG.md +++ b/packages/esbuild/CHANGELOG.md @@ -1,5 +1,17 @@ # @module-federation/esbuild +## 0.0.107 + +### Patch Changes + +- Updated dependencies [5d4095d] +- Updated dependencies [d433ec9] +- Updated dependencies [0716c11] +- Updated dependencies [41281f4] + - @module-federation/sdk@2.5.0 + - @module-federation/runtime@2.5.0 + - @module-federation/webpack-bundler-runtime@2.5.0 + ## 0.0.106 ### Patch Changes diff --git a/packages/esbuild/package.json b/packages/esbuild/package.json index 6973fd9d6d7..6651a3eaa97 100644 --- a/packages/esbuild/package.json +++ b/packages/esbuild/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/esbuild", - "version": "0.0.106", + "version": "0.0.107", "author": "Zack Jackson (@ScriptedAlchemy)", "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/packages/managers/CHANGELOG.md b/packages/managers/CHANGELOG.md index e8496fc397a..8e93d28a1f7 100644 --- a/packages/managers/CHANGELOG.md +++ b/packages/managers/CHANGELOG.md @@ -1,5 +1,13 @@ # @module-federation/managers +## 2.5.0 + +### Patch Changes + +- Updated dependencies [5d4095d] +- Updated dependencies [0716c11] + - @module-federation/sdk@2.5.0 + ## 2.4.0 ### Patch Changes diff --git a/packages/managers/package.json b/packages/managers/package.json index 685d77b89a5..9552c07a681 100644 --- a/packages/managers/package.json +++ b/packages/managers/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/managers", - "version": "2.4.0", + "version": "2.5.0", "license": "MIT", "description": "Provide managers for helping handle mf data .", "keywords": [ diff --git a/packages/manifest/CHANGELOG.md b/packages/manifest/CHANGELOG.md index 53a72e78bbc..b075fd64c17 100644 --- a/packages/manifest/CHANGELOG.md +++ b/packages/manifest/CHANGELOG.md @@ -1,5 +1,16 @@ # @module-federation/manifest +## 2.5.0 + +### Patch Changes + +- Updated dependencies [5d4095d] +- Updated dependencies [0716c11] +- Updated dependencies [13dce52] + - @module-federation/sdk@2.5.0 + - @module-federation/dts-plugin@2.5.0 + - @module-federation/managers@2.5.0 + ## 2.4.0 ### Minor Changes diff --git a/packages/manifest/package.json b/packages/manifest/package.json index 3af59c62f8a..30c8da38ead 100644 --- a/packages/manifest/package.json +++ b/packages/manifest/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/manifest", - "version": "2.4.0", + "version": "2.5.0", "license": "MIT", "description": "Provide manifest/stats for webpack/rspack MF project .", "keywords": [ diff --git a/packages/metro-core/CHANGELOG.md b/packages/metro-core/CHANGELOG.md index 2c47863baf5..b995cbb424f 100644 --- a/packages/metro-core/CHANGELOG.md +++ b/packages/metro-core/CHANGELOG.md @@ -1,5 +1,25 @@ # @module-federation/metro +## 2.5.0 + +### Minor Changes + +- 5d4095d: feat(metro): add manifest SHA-256 bundle hashes and optional cache layer integration for bundle loading. + + Credit: originally contributed by @zhongwuzw in #4576. + +### Patch Changes + +- 6c9d2ee: feat(metro): support manifest hashes in dev builds to enable testing federated module caching during development +- Updated dependencies [5d4095d] +- Updated dependencies [d433ec9] +- Updated dependencies [0716c11] +- Updated dependencies [13dce52] +- Updated dependencies [41281f4] + - @module-federation/sdk@2.5.0 + - @module-federation/runtime@2.5.0 + - @module-federation/dts-plugin@2.5.0 + ## 2.4.0 ### Patch Changes diff --git a/packages/metro-core/package.json b/packages/metro-core/package.json index 899235fca9f..88be5889baf 100644 --- a/packages/metro-core/package.json +++ b/packages/metro-core/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/metro", - "version": "2.4.0", + "version": "2.5.0", "description": "Module Federation for Metro bundler", "keywords": [ "module-federation", diff --git a/packages/metro-plugin-rnc-cli/CHANGELOG.md b/packages/metro-plugin-rnc-cli/CHANGELOG.md index 7f0e22e1f80..a46547ab6ae 100644 --- a/packages/metro-plugin-rnc-cli/CHANGELOG.md +++ b/packages/metro-plugin-rnc-cli/CHANGELOG.md @@ -1,5 +1,13 @@ # @module-federation/metro-plugin-rnc-cli +## 2.5.0 + +### Patch Changes + +- Updated dependencies [5d4095d] +- Updated dependencies [6c9d2ee] + - @module-federation/metro@2.5.0 + ## 2.4.0 ### Patch Changes diff --git a/packages/metro-plugin-rnc-cli/package.json b/packages/metro-plugin-rnc-cli/package.json index d33d3155a6f..ecfd107e830 100644 --- a/packages/metro-plugin-rnc-cli/package.json +++ b/packages/metro-plugin-rnc-cli/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/metro-plugin-rnc-cli", - "version": "2.4.0", + "version": "2.5.0", "description": "Metro Module Federation plugin for React Native Enterprise Framework (RNEF)", "keywords": [ "rnc", diff --git a/packages/metro-plugin-rnef/CHANGELOG.md b/packages/metro-plugin-rnef/CHANGELOG.md index bb0476673b9..ea222b84cf5 100644 --- a/packages/metro-plugin-rnef/CHANGELOG.md +++ b/packages/metro-plugin-rnef/CHANGELOG.md @@ -1,5 +1,14 @@ # @module-federation/metro-plugin-rnef +## 2.5.0 + +### Patch Changes + +- a1041cc: chore(metro): deprecate metro-plugin-rnef in favor of metro-plugin-rock +- Updated dependencies [5d4095d] +- Updated dependencies [6c9d2ee] + - @module-federation/metro@2.5.0 + ## 2.4.0 ### Patch Changes diff --git a/packages/metro-plugin-rnef/package.json b/packages/metro-plugin-rnef/package.json index 1ac0a7e3276..b722922c9ea 100644 --- a/packages/metro-plugin-rnef/package.json +++ b/packages/metro-plugin-rnef/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/metro-plugin-rnef", - "version": "2.4.0", + "version": "2.5.0", "description": "Metro Module Federation plugin for React Native Enterprise Framework (RNEF). Deprecated: use @module-federation/metro-plugin-rock instead.", "keywords": [ "rnef", diff --git a/packages/metro-plugin-rock/CHANGELOG.md b/packages/metro-plugin-rock/CHANGELOG.md new file mode 100644 index 00000000000..964a1b9a752 --- /dev/null +++ b/packages/metro-plugin-rock/CHANGELOG.md @@ -0,0 +1,13 @@ +# @module-federation/metro-plugin-rock + +## 2.5.0 + +### Minor Changes + +- a1041cc: feat(metro): add metro-plugin-rock for Rock integration + +### Patch Changes + +- Updated dependencies [5d4095d] +- Updated dependencies [6c9d2ee] + - @module-federation/metro@2.5.0 diff --git a/packages/metro-plugin-rock/package.json b/packages/metro-plugin-rock/package.json index b3fc6ca7d39..585e324a7cd 100644 --- a/packages/metro-plugin-rock/package.json +++ b/packages/metro-plugin-rock/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/metro-plugin-rock", - "version": "2.3.1", + "version": "2.5.0", "description": "Metro Module Federation plugin for Rock", "keywords": [ "rock", diff --git a/packages/modernjs-v3/CHANGELOG.md b/packages/modernjs-v3/CHANGELOG.md index 920fc8a2643..ee71a0c430d 100644 --- a/packages/modernjs-v3/CHANGELOG.md +++ b/packages/modernjs-v3/CHANGELOG.md @@ -1,5 +1,23 @@ # @module-federation/modern-js-v3 +## 2.5.0 + +### Patch Changes + +- 180004d: Expose data-fetch subpaths for bridge-react and Modern.js users, and remove hono from bridge-react peer dependencies. +- Updated dependencies [180004d] +- Updated dependencies [5d4095d] +- Updated dependencies [d433ec9] +- Updated dependencies [0716c11] +- Updated dependencies [41281f4] + - @module-federation/bridge-react@2.5.0 + - @module-federation/sdk@2.5.0 + - @module-federation/runtime@2.5.0 + - @module-federation/cli@2.5.0 + - @module-federation/enhanced@2.5.0 + - @module-federation/node@2.7.43 + - @module-federation/rsbuild-plugin@2.5.0 + ## 2.4.0 ### Patch Changes diff --git a/packages/modernjs-v3/package.json b/packages/modernjs-v3/package.json index 03601ebd711..e55282549fb 100644 --- a/packages/modernjs-v3/package.json +++ b/packages/modernjs-v3/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/modern-js-v3", - "version": "2.4.0", + "version": "2.5.0", "files": [ "dist/", "types.d.ts", @@ -30,6 +30,10 @@ "types": "./dist/types/runtime/index.d.ts", "default": "./dist/esm/runtime/index.mjs" }, + "./data-fetch": { + "types": "./dist/types/react/data-fetch.d.ts", + "default": "./dist/esm/react/data-fetch.mjs" + }, "./react": { "types": "./dist/types/react/index.d.ts", "default": "./dist/esm/react/index.mjs" @@ -91,6 +95,9 @@ "runtime": [ "./dist/types/runtime/index.d.ts" ], + "data-fetch": [ + "./dist/types/react/data-fetch.d.ts" + ], "react": [ "./dist/types/react/index.d.ts" ], diff --git a/packages/modernjs-v3/src/react/data-fetch.ts b/packages/modernjs-v3/src/react/data-fetch.ts new file mode 100644 index 00000000000..a603e3c9d39 --- /dev/null +++ b/packages/modernjs-v3/src/react/data-fetch.ts @@ -0,0 +1 @@ +export * from '@module-federation/bridge-react/data-fetch'; diff --git a/packages/modernjs-v3/src/ssr-runtime/devPlugin.tsx b/packages/modernjs-v3/src/ssr-runtime/devPlugin.tsx index d19e5181632..f40042de0a5 100644 --- a/packages/modernjs-v3/src/ssr-runtime/devPlugin.tsx +++ b/packages/modernjs-v3/src/ssr-runtime/devPlugin.tsx @@ -1,6 +1,6 @@ import type { RuntimePlugin } from '@modern-js/runtime'; import { SSRLiveReload } from './SSRLiveReload'; -import { flushDataFetch } from '@module-federation/bridge-react/lazy-utils'; +import { flushDataFetch } from '@module-federation/bridge-react/data-fetch'; export const mfSSRDevPlugin = (): RuntimePlugin => ({ name: '@module-federation/modern-js-v3', diff --git a/packages/modernjs-v3/src/ssr-runtime/injectDataFetchFunctionPlugin.tsx b/packages/modernjs-v3/src/ssr-runtime/injectDataFetchFunctionPlugin.tsx index 91636507443..ee6e6cfd969 100644 --- a/packages/modernjs-v3/src/ssr-runtime/injectDataFetchFunctionPlugin.tsx +++ b/packages/modernjs-v3/src/ssr-runtime/injectDataFetchFunctionPlugin.tsx @@ -1,5 +1,7 @@ -import { callDataFetch } from '@module-federation/bridge-react/data-fetch-utils'; -import { setSSREnv } from '@module-federation/bridge-react/lazy-utils'; +import { + callDataFetch, + setSSREnv, +} from '@module-federation/bridge-react/data-fetch'; import type { RuntimePlugin } from '@modern-js/runtime'; diff --git a/packages/modernjs/CHANGELOG.md b/packages/modernjs/CHANGELOG.md index 138626156dc..9d1c914025f 100644 --- a/packages/modernjs/CHANGELOG.md +++ b/packages/modernjs/CHANGELOG.md @@ -1,5 +1,23 @@ # @module-federation/modern-js +## 2.5.0 + +### Patch Changes + +- 180004d: Expose data-fetch subpaths for bridge-react and Modern.js users, and remove hono from bridge-react peer dependencies. +- Updated dependencies [180004d] +- Updated dependencies [5d4095d] +- Updated dependencies [d433ec9] +- Updated dependencies [0716c11] +- Updated dependencies [41281f4] + - @module-federation/bridge-react@2.5.0 + - @module-federation/sdk@2.5.0 + - @module-federation/runtime@2.5.0 + - @module-federation/cli@2.5.0 + - @module-federation/enhanced@2.5.0 + - @module-federation/node@2.7.43 + - @module-federation/rsbuild-plugin@2.5.0 + ## 2.4.0 ### Patch Changes diff --git a/packages/modernjs/package.json b/packages/modernjs/package.json index 31b8c84cd89..5523dd6273d 100644 --- a/packages/modernjs/package.json +++ b/packages/modernjs/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/modern-js", - "version": "2.4.0", + "version": "2.5.0", "files": [ "dist/", "types.d.ts", @@ -30,6 +30,10 @@ "types": "./dist/types/runtime/index.d.ts", "default": "./dist/esm/runtime/index.mjs" }, + "./data-fetch": { + "types": "./dist/types/react/data-fetch.d.ts", + "default": "./dist/esm/react/data-fetch.mjs" + }, "./react": { "types": "./dist/types/react/index.d.ts", "default": "./dist/esm/react/index.mjs" @@ -91,6 +95,9 @@ "runtime": [ "./dist/types/runtime/index.d.ts" ], + "data-fetch": [ + "./dist/types/react/data-fetch.d.ts" + ], "react": [ "./dist/types/react/index.d.ts" ], diff --git a/packages/modernjs/src/react/data-fetch.ts b/packages/modernjs/src/react/data-fetch.ts new file mode 100644 index 00000000000..a603e3c9d39 --- /dev/null +++ b/packages/modernjs/src/react/data-fetch.ts @@ -0,0 +1 @@ +export * from '@module-federation/bridge-react/data-fetch'; diff --git a/packages/modernjs/src/ssr-runtime/devPlugin.tsx b/packages/modernjs/src/ssr-runtime/devPlugin.tsx index a2c8ec8b086..4e999942602 100644 --- a/packages/modernjs/src/ssr-runtime/devPlugin.tsx +++ b/packages/modernjs/src/ssr-runtime/devPlugin.tsx @@ -1,6 +1,6 @@ import type { RuntimePluginFuture } from '@modern-js/runtime'; import { SSRLiveReload } from './SSRLiveReload'; -import { flushDataFetch } from '@module-federation/bridge-react/lazy-utils'; +import { flushDataFetch } from '@module-federation/bridge-react/data-fetch'; export const mfSSRDevPlugin = (): RuntimePluginFuture => ({ name: '@module-federation/modern-js', diff --git a/packages/modernjs/src/ssr-runtime/injectDataFetchFunctionPlugin.tsx b/packages/modernjs/src/ssr-runtime/injectDataFetchFunctionPlugin.tsx index 0c3c15628aa..1248ce5ef79 100644 --- a/packages/modernjs/src/ssr-runtime/injectDataFetchFunctionPlugin.tsx +++ b/packages/modernjs/src/ssr-runtime/injectDataFetchFunctionPlugin.tsx @@ -1,5 +1,7 @@ -import { callDataFetch } from '@module-federation/bridge-react/data-fetch-utils'; -import { setSSREnv } from '@module-federation/bridge-react/lazy-utils'; +import { + callDataFetch, + setSSREnv, +} from '@module-federation/bridge-react/data-fetch'; import type { RuntimePluginFuture } from '@modern-js/runtime'; diff --git a/packages/nextjs-mf/CHANGELOG.md b/packages/nextjs-mf/CHANGELOG.md index efa426193f3..f1110f457f3 100644 --- a/packages/nextjs-mf/CHANGELOG.md +++ b/packages/nextjs-mf/CHANGELOG.md @@ -1,5 +1,19 @@ # @module-federation/nextjs-mf +## 8.8.67 + +### Patch Changes + +- Updated dependencies [5d4095d] +- Updated dependencies [d433ec9] +- Updated dependencies [0716c11] +- Updated dependencies [41281f4] + - @module-federation/sdk@2.5.0 + - @module-federation/runtime@2.5.0 + - @module-federation/enhanced@2.5.0 + - @module-federation/node@2.7.43 + - @module-federation/webpack-bundler-runtime@2.5.0 + ## 8.8.66 ### Patch Changes diff --git a/packages/nextjs-mf/package.json b/packages/nextjs-mf/package.json index 73aabad2a1b..b195a624202 100644 --- a/packages/nextjs-mf/package.json +++ b/packages/nextjs-mf/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/nextjs-mf", - "version": "8.8.66", + "version": "8.8.67", "license": "MIT", "main": "dist/src/index.js", "module": "dist/src/index.mjs", diff --git a/packages/node/CHANGELOG.md b/packages/node/CHANGELOG.md index b454b324d90..2a20ade349d 100644 --- a/packages/node/CHANGELOG.md +++ b/packages/node/CHANGELOG.md @@ -1,5 +1,17 @@ # @module-federation/node +## 2.7.43 + +### Patch Changes + +- Updated dependencies [5d4095d] +- Updated dependencies [d433ec9] +- Updated dependencies [0716c11] +- Updated dependencies [41281f4] + - @module-federation/sdk@2.5.0 + - @module-federation/runtime@2.5.0 + - @module-federation/enhanced@2.5.0 + ## 2.7.42 ### Patch Changes diff --git a/packages/node/package.json b/packages/node/package.json index ad0993b5c0d..d44b47a8245 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,7 +1,7 @@ { "public": true, "name": "@module-federation/node", - "version": "2.7.42", + "version": "2.7.43", "type": "commonjs", "main": "./dist/src/index.js", "module": "./dist/src/index.mjs", diff --git a/packages/observability-plugin/.eslintrc.json b/packages/observability-plugin/.eslintrc.json new file mode 100644 index 00000000000..2c7d3866f19 --- /dev/null +++ b/packages/observability-plugin/.eslintrc.json @@ -0,0 +1,27 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": [ + "!**/*", + "**/vite.config.*.timestamp*", + "**/vitest.config.*.timestamp*" + ], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.json"], + "parser": "jsonc-eslint-parser", + "rules": {} + } + ] +} diff --git a/packages/observability-plugin/CHANGELOG.md b/packages/observability-plugin/CHANGELOG.md new file mode 100644 index 00000000000..ed1ad41c4b3 --- /dev/null +++ b/packages/observability-plugin/CHANGELOG.md @@ -0,0 +1,16 @@ +# @module-federation/observability-plugin + +## 2.5.0 + +### Minor Changes + +- 41281f4: Add a Loading Trace panel that can configure and inject the observability plugin, reload the inspected page, stream loading events, and export collected reports. +- 41281f4: Add an opt-in observability plugin, a Chrome-extension-safe observability plugin entry with an independent name and fixed browser scope, a direct runtime plugin API with instance-bound component loaded marks, explicit temporary React `onMFRemoteLoaded` callback injection for matched remotes, opt-in start console traces for `loadRemote` and `loadShare`, a local collector mode for AI-assisted browser debugging, a Node-specific export for file reports, a build-specific export for build summaries and build error reports, remote and shared lifecycle hooks, console trace hints, safe browser/Node report outputs, configurable error stack capture with explicit console raw-stack opt-ins, shared/eager loading evidence gated to stable runtime `2.5.0+` for Chrome-extension compatibility, final loading outcome summaries for Module Federation loading reports including resolved shared dependencies, deterministic fact reports for runtime and build failures, no-op return handling for observer hooks, detailed remote match/init/expose/factory phase events with phase durations, compact phase summaries, cache/fallback markers, loaded-before evidence from existing federation instances when a remote load fails, length-limited business component metadata, clipped moduleInfo evidence with preserved deployment locator fields for snapshot-dependent failures, normalized runtime error summaries with error codes, owner hints, retryability, and safe context, dedicated runtime error codes for invalid manifests, missing exposes, and remote container init failures, plus MF skill guidance for reading and fixing observability reports. + +### Patch Changes + +- 0716c11: Track preload resource results and expose resource context to loader hooks. +- 328542c: Send configured local collector events outside debug mode while keeping failures quiet unless debug logging is enabled. +- Updated dependencies [5d4095d] +- Updated dependencies [0716c11] + - @module-federation/sdk@2.5.0 diff --git a/packages/observability-plugin/README.md b/packages/observability-plugin/README.md new file mode 100644 index 00000000000..53424a03426 --- /dev/null +++ b/packages/observability-plugin/README.md @@ -0,0 +1,475 @@ +# @module-federation/observability-plugin + +Runtime observability plugin for Module Federation loading flows. + +This package is designed for Module Federation `2.5.0` and later. Older +projects can still use runtime error codes, but the full observability workflow +requires upgrading the MF runtime and installing this plugin. + +This package is currently the minimal observability foundation. It records +structured in-memory loading events when the plugin is installed and not +disabled. Optional outputs are conservative by default: no browser global, no +Node file output, and no raw error stack in console output. + +```ts +import { createInstance } from '@module-federation/runtime'; +import { ObservabilityPlugin } from '@module-federation/observability-plugin'; + +createInstance({ + name: 'host', + plugins: [ + ObservabilityPlugin({ + level: 'verbose', + browser: { + enabled: true, + scope: 'host', + }, + }), + ], + remotes: [], +}); +``` + +The plugin does not upload data or expose a browser global by default. Reports +are kept in memory. Runtime request URLs, error messages, and stored error +stacks keep their original query/hash and error details because those values are +often needed for debugging. Large deployment locator fields such as +`publicPath` and `remoteEntry` are only length-limited. + +## Safe Observability + +Enable only the output channels that match the environment: + +- Browser dev: use `browser.enabled: true` with a scoped reader such as + `window.__FEDERATION__.__OBSERVABILITY__.host`. +- Chrome DevTools integration: use `devtools: true` together with browser + output so a browser extension can receive loading events through + `window.postMessage`. +- Agent-led browser dev: use `collector: true` to POST reports to the local + skill collector on `127.0.0.1:17891`. +- Browser prod: set `browser.mode: "production"` so console output stays limited + to `traceId` and known `errorCode`; export full reports only through explicit + app-owned flows such as `exportReport()` or `onReport`. +- Node / SSR: use the Node entry + `@module-federation/observability-plugin/node` and set `fileOutput: true` when + local files are needed. +- Build: use `ObservabilityBuildPlugin` when build-side files are needed for + later comparison. + +Reports include the loading timeline, selected host/remote/shared facts, +original runtime URLs, original error message/stack, clipped deployment +`moduleInfo` when it is relevant, and user-provided observability +metadata with count and length limits. Reports do not collect request headers, +cookies, authorization values, remote response bodies, remote source, module +source, React props, full `moduleInfo.modules`, full `moduleInfo.shared`, or +asset lists from deployment `moduleInfo`. +Fields whose value is `undefined` are omitted from returned reports and events, +so missing fields should be read as "not observed or not relevant" instead of a +literal value. + +Failure hints are printed with `console.error` so browser DevTools, CDP-based +agents, Node logs, and log collection systems can detect that a Module +Federation load failed and then use the printed `traceId` to fetch the full +report. Successful or recovered reports are still available through the reader +APIs and callbacks, but they are not promoted to console errors. When the +browser reader is enabled in development mode, the plugin prints a small +`console.info` line by default when a `loadRemote` or `loadShare` trace starts. +The line includes the `traceId` and read command so an agent can inspect pending +loading state before a timeout or error happens. In production browser mode, +start logs are disabled by default; set `trace.printStart: true` only when you +explicitly want them. + +The runtime only exposes the loading lifecycle hooks needed to know whether the +main flow started, succeeded, or failed. This plugin listens to those hooks, +derives detailed reasons like shared version mismatch or eager boundary issues, +and exposes the final loading state through a small `summary` object: + +- `runtime-loaded`: Module Federation finished loading the remote module. +- `component-loaded`: business code called `markComponentLoaded`, or a producer + called the injected `onMFRemoteLoaded` callback. +- `preloaded`: `preloadRemote` finished loading the selected resources. +- `failed`: the load failed and `failedPhase` points to the first specific + failing phase. +- `recovered`: loading hit an error but a fallback/recovery path returned a + result. For shared loading, the `custom-share-info-unmatched` reason means + build-time `customShareInfo` did not match a registered provider, but the + runtime handled it as a non-fatal result instead of a loading failure. + +`summary.componentLoaded: false` only means no component-level ready signal was +observed. If `react.injectLoadedCallback: true` is enabled but this field is +still false, check whether the producer actually calls +`props.onMFRemoteLoaded?.(...)`. Without that producer call, the report can only +confirm that the remote resource loaded; it cannot prove whether the React +component reached the producer's business-ready point. + +For remote loading, the plugin listens to runtime lifecycle hooks such as +`beforeRequest`, `afterMatchRemote`, `onLoad`, `afterLoadRemote`, +`errorLoadRemote`, `loadEntry`, `afterLoadEntry`, `beforeInitRemote`, +`afterInitRemote`, `beforeGetExpose`, `afterGetExpose`, +`beforeExecuteFactory`, `afterExecuteFactory`, and snapshot resolve hooks. It +does not return a value from observer hooks, so it does not change fallback or +retry results from plugins such as `@module-federation/retry-plugin`. + +Successful verbose reports include the key runtime stages: remote match, +manifest, remoteEntry load, remoteEntry init, expose resolution, module factory +execution, and final load completion. When a stage has matching start and end +events, the end event includes a bounded `duration` value. + +Preload reports include resource-level results from `preloadRemote`. Each +result records the resource URL, `resourceType`, `initiator`, preload `id`, and +status: `success`, `error`, `timeout`, or `cached`. Calls without `exposes` use +`remoteName/*` as the preload `id`. Calls with `exposes` are recorded per +expose as `remoteName/expose`. The plugin does not change the existing +`generatePreloadAssets` return shape; preload resources are still plain URL +arrays. + +Runtime resource hooks also receive a `resourceContext` object on manifest, +remoteEntry, preload JS, and preload CSS resource loads. It contains +`initiator`, `id`, `resourceType`, and `url`, so custom loaders can tell whether +the resource was requested by `loadRemote` or `preloadRemote` without parsing +the URL. + +The report also keeps compact loading state under `summary`. It contains the +final outcome, per-phase status and duration under `summary.phases`, safe +cache/recovery markers under `summary.flags`, and the last resolved shared +provider/version under `summary.shared` when a shared dependency was observed. +`level: "summary"` omits start events from the stored timeline but still keeps +the derived durations on the matching success/error events. `level: "verbose"` +keeps the full timeline. + +Each report also includes a deterministic `diagnosis` object. It is generated +by engineering rules, not by an AI model. It keeps the final outcome, likely +owner, completed and pending phases, observability facts, documentation link when +a known runtime error code is present, and a short list of next checks. This is +the field a person or AI coding agent should read first before falling back to +the raw `events` timeline. + +For agent-led debugging, use the repository's single `mf` skill entry with the +`observability` sub-command. The skill is the maintained guide for reading +reports and deciding the next debugging step. + +`errorLoadShare` is used only for observation. Shared dependency miss, version +mismatch, and eager boundary errors are not retried by the retry plugin by +default because they are usually configuration or availability problems instead +of transient network failures. When a build plugin supplies `customShareInfo` +and the runtime reports a handled miss, the observability report uses a +recovered outcome instead of marking the trace as failed. +The Chrome extension entry skips shared events for older or preview runtime +versions because those runtimes do not expose the same shared lifecycle +contract. + +Business code can mark its own success condition with a fixed event. When React +callback injection is explicitly enabled, the wrapper injects an +`onMFRemoteLoaded` prop into the remote component. The producer can call it when +the component's own ready condition is met: + +```tsx +import { useEffect } from 'react'; +import type { OnMFRemoteLoaded } from '@module-federation/observability-plugin'; + +export default function RemotePanel({ onMFRemoteLoaded }: { onMFRemoteLoaded?: OnMFRemoteLoaded }) { + useEffect(() => { + onMFRemoteLoaded?.({ + metadata: { + dataReady: true, + }, + }); + }, [onMFRemoteLoaded]); + + return
Remote panel
; +} +``` + +If the app wants to mark readiness from the consumer side, it can still call the +instance method directly: + +```ts +import { getInstance } from '@module-federation/runtime'; +import '@module-federation/observability-plugin'; + +getInstance()?.markComponentLoaded({ + requestId: 'remote/Button', + componentName: 'Button', + metadata: { + route: '/settings', + }, +}); +``` + +Both paths record `component:business-loaded` on the same trace when possible. +Business metadata is optional. String values keep their original details and are +only length-limited, so user-provided metadata remains trustworthy. The instance +method is attached when the observability plugin is registered. If an +application uses multiple runtime instances, call it on the instance that +registered this plugin. + +React callback injection is available only when explicitly enabled: + +```ts +ObservabilityPlugin({ + level: 'verbose', + react: { + injectLoadedCallback: true, + remoteIds: ['remote/Button'], + }, +}); +``` + +When this option is enabled, the plugin tries to wrap remote function components +returned by `loadRemote`. The wrapper does not add DOM nodes. It injects the +`onMFRemoteLoaded` prop only. It does not observe React mount, render lifecycle, +or timeout. When the producer calls the callback, the report records +`component:business-loaded`. This option changes the component reference because +it returns a wrapper component, so use it as a temporary debugging switch and +remove it after the production issue is fixed. + +If `summary.componentLoaded` is still `false` after enabling this option, inspect +the producer first. If the producer has not called `onMFRemoteLoaded`, the report +only proves remote runtime loading, not component business readiness. If the +producer source is unavailable, ask the producer owner to confirm whether the +callback was added. + +Use `react.remoteIds` to limit this behavior to the remote requests you are +actively debugging. If `remoteIds` is empty, the plugin wraps detected React +function components loaded by the runtime instance that registered it. + +Browser output is available only when the plugin option explicitly enables it. +When browser output is enabled, the report can be read from: + +```ts +window.__FEDERATION__.__OBSERVABILITY__.host.getLatestReport(); +window.__FEDERATION__.__OBSERVABILITY__.host.getReport('mf-trace-id'); +window.__FEDERATION__.__OBSERVABILITY__.host.getReports({ limit: 5 }); +window.__FEDERATION__.__OBSERVABILITY__.host.findReports({ remote: 'remote1' }); +window.__FEDERATION__.__OBSERVABILITY__.host.exportReport('mf-trace-id'); +``` + +Chrome DevTools panels can opt in to event delivery without polling the page: + +```ts +ObservabilityPlugin({ + level: 'verbose', + browser: { + enabled: true, + scope: 'host', + mode: 'development', + }, + trace: { + printStart: true, + }, + devtools: true, +}); +``` + +This posts structured event/report snapshots to the page with +`window.postMessage`. Browser extensions can forward those messages from their +content script to the panel. The channel is disabled by default. + +`getReports({ limit })` returns recent reports newest first. `findReports()` can +filter by `traceId`, `remote`, `expose`, `shared`, `status`, or `outcome`. +`exportReport()` returns a copied report object, using the latest report when no +`traceId` is provided. + +For agent-led development debugging where a page may stay in a loading state, +enable the browser reader. Development browser mode prints start traces by +default: + +```ts +ObservabilityPlugin({ + level: 'verbose', + browser: { + enabled: true, + scope: 'host', + }, +}); +``` + +This prints only `loadRemote` and `loadShare` start lines. It does not print a +line for every internal phase. Set `trace.printStart: false` to disable it in +development browser mode. In production browser mode, set +`trace.printStart: true` to opt in. + +If the agent cannot execute JavaScript in the browser page, enable the local +collector and start the collector from the MF skill: + +```ts +ObservabilityPlugin({ + level: 'verbose', + collector: true, +}); +``` + +`collector: true` posts event/report snapshots to: + +```text +http://127.0.0.1:17891/__mf_observability +``` + +Use a custom local port only when the default port is occupied: + +```ts +ObservabilityPlugin({ + collector: { + enabled: true, + port: 17892, + }, +}); +``` + +The runtime plugin does not create a server. The MF skill starts a temporary +local Node collector, writes reports under `.mf/observability/collector`, and +the agent reads those files. The collector path is local-only and does not +execute code or control the page. Collector delivery is controlled by the +`collector` option; debug mode only decides whether a failed collector request +prints a debug log. + +For browser production use, set `browser.mode: "production"` when the runtime +console must stay minimal: + +```ts +ObservabilityPlugin({ + browser: { + enabled: true, + scope: 'host', + mode: 'production', + }, +}); +``` + +In production browser mode, the `console.error` hint only includes the `traceId` +and known `errorCode`. It does not print the report body, raw stack, request +URL, or `read:` command. Full reports are still available only through explicit +user choices such as `exportReport()` or an application-owned `onReport` upload. +Production applications that want richer observability should prefer +`onReport` / `onEvent` to forward reports to their own telemetry system instead +of exposing a public browser global. + +Node file output is provided by the Node-specific entry: + +```ts +import { createInstance } from '@module-federation/runtime'; +import { ObservabilityPlugin } from '@module-federation/observability-plugin/node'; + +createInstance({ + name: 'host', + plugins: [ + ObservabilityPlugin({ + level: 'verbose', + fileOutput: true, + directory: '.mf/observability', + }), + ], + remotes: [], +}); +``` + +When Node file output is explicitly enabled, the Node entry writes: + +- `.mf/observability/latest.json`: a formatted copy of the latest complete + report, including `traceId`, top-level status/error fields, `diagnosis`, + `summary`, clipped `moduleInfo` when relevant, and the report's own `events`. +- `.mf/observability/events.jsonl`: append-only event stream. Each line is one + JSON object for one runtime event and includes fields such as `traceId`, + `timestamp`, `phase`, `status`, remote/shared/expose context, and error + fields when present. + +Read `latest.json` first. Use `events.jsonl` only when multiple traces must be +compared or when the full event ordering for a `traceId` is needed. + +On errors, the plugin prints a small console hint with the `traceId` and the +available read path. The console hint is intentionally small and does not carry +the full report or the raw stack by default. If a user explicitly needs the +full stack, they can opt in with `printRawStack: true` or capture the original +error through `onRawError`. The default browser/runtime entry does not include +Node file output code. + +Build-time observability is provided by the build-specific entry: + +```js +const { ModuleFederationPlugin } = require('@module-federation/enhanced/webpack'); +const { ObservabilityBuildPlugin } = require('@module-federation/observability-plugin/build'); + +const moduleFederationOptions = { + name: 'host', + remotes: { + remote1: 'remote1@http://localhost:3001/mf-manifest.json', + }, + exposes: { + './Button': './src/Button', + }, + shared: { + react: { singleton: true, requiredVersion: '^18.0.0' }, + }, +}; + +module.exports = { + plugins: [ + new ModuleFederationPlugin(moduleFederationOptions), + new ObservabilityBuildPlugin({ + moduleFederation: moduleFederationOptions, + }), + ], +}; +``` + +When this optional build plugin is installed, it writes +`.mf/observability/build-info.json`. The file is a summary of the Module +Federation build configuration and generated manifest/stats facts: +bundler name/version, Module Federation plugin version when available, build +version when available, remoteEntry file/type/publicPath mode, remotes, +exposes, and shared dependencies. It intentionally omits local expose source +paths, asset lists, source code, and environment variables. Remote URLs and the +`remoteEntry.publicPath` deployment locator keep query/hash data. If build +observability output fails, the build continues and a bundler warning is emitted. + +When the build has compilation errors, or when the observability plugin cannot +write its own build output, the build plugin writes +`.mf/observability/build-report.json`. The report has the same high-level shape +as runtime reports: a `traceId`, `status`, `failedPhase`, `events`, +`summary.error`, `diagnosis`, and the top-level `build` object. Clean builds +remove the stale report file so readers do not mistake an old build error for +the current state. + +Runtime reports do not include build facts. `summary` is only the loading-state +view. If debugging needs build-side evidence, read +`.mf/observability/build-info.json` or `.mf/observability/build-report.json` +separately and compare it with the runtime report. + +For snapshot-dependent failures, such as `RUNTIME-007`, reports can also include +top-level `moduleInfo`. This field is collected only for failures that depend on +`__FEDERATION__.moduleInfo`. It is clipped by default: the plugin keeps matching +entries with only `name`, `publicPath`, `getPublicPath`, `remoteEntry`, and +`globalName`, keeps the deployment locator fields only length-limited, and +removes large fields such as `modules` and `shared`. + +Runtime errors are normalized into stable fields on both events and reports: + +- `errorCode`: for example `RUNTIME-003` or `RUNTIME-008` when the original + error includes one. +- `failedPhase` and `lifecycle`: where the failure happened. +- `ownerHint`: a deterministic hint such as `host`, `remote`, `shared`, or + `network`. +- `retryable`: whether the observed failure looks transient. +- `errorContext`: a context object with values such as manifest URL, + remote name, `entryGlobalName`, request id, expose, or shared package. + +The first batch includes specific diagnosis facts and actions for +`RUNTIME-001`, `RUNTIME-003`, `RUNTIME-004`, `RUNTIME-005`, `RUNTIME-006`, and +`RUNTIME-008`. `RUNTIME-008` is further classified as `network`, `timeout`, +`script-execution`, or `unknown` in `errorContext.resourceErrorType` and +`diagnosis.facts.resourceErrorType`. + +Shared dependency reports include only evidence fields such as package name, +share scope, requested version, available versions, selected provider, and a +reason like `missing-provider`, `version-mismatch`, or `sync-async-boundary`. +They do not include shared factories, module values, source, or business data. + +Shared observability is intentionally scoped to the Module Federation instance +that resolved the shared dependency. It can answer which MF instance loaded a +shared package, which registered provider/version was selected, and the related +scope/version/eager configuration. It does not guarantee a causal link from that +shared dependency back to a specific remote or expose, because shared resolution +can be triggered later by the bundler runtime while chunks and module factories +execute. When multiple shared dependencies are involved, read all +`phase: "shared"` events. `summary.shared` is only a compact last-observed +summary. diff --git a/packages/observability-plugin/__tests__/observability.spec.ts b/packages/observability-plugin/__tests__/observability.spec.ts new file mode 100644 index 00000000000..ea53d15e2d4 --- /dev/null +++ b/packages/observability-plugin/__tests__/observability.spec.ts @@ -0,0 +1,4490 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { describe, expect, it, vi } from 'vitest'; +import { createObservability, ObservabilityPlugin } from '../src'; +import { + ObservabilityBuildPlugin, + createObservabilityBuildInfo, +} from '../src/build'; +import { ChromeObservabilityPlugin } from '../src/chrome-devtool'; +import { createNodeObservability as ObservabilityNode } from '../src/node'; + +const enabledOrigin = { + version: '2.5.0', + options: { + name: 'host', + }, +}; + +const createShared = (overrides: Record = {}) => ({ + version: '18.3.1', + scope: ['default'], + from: 'host', + deps: [], + useIn: [], + strategy: 'loaded-first', + shareConfig: { + requiredVersion: '^18.0.0', + singleton: false, + strictVersion: false, + eager: false, + }, + get: () => () => ({ value: 'shared' }), + ...overrides, +}); + +const hasUndefinedField = (value: unknown): boolean => { + if (Array.isArray(value)) { + return value.some(hasUndefinedField); + } + + if (!value || typeof value !== 'object') { + return false; + } + + return Object.values(value as Record).some( + (item) => item === undefined || hasUndefinedField(item), + ); +}; + +describe('ObservabilityBuildPlugin', () => { + it('creates a build info summary from manifest and config data', () => { + const buildInfo = createObservabilityBuildInfo({ + generatedAt: '2026-01-01T00:00:00.000Z', + bundler: 'webpack', + bundlerVersion: '5.99.0', + compilerOptions: { + mode: 'production', + target: ['web'], + output: { + publicPath: 'https://cdn.example.com/assets/?v=20260101#hash', + }, + }, + moduleFederation: { + name: 'runtime_host', + filename: 'remoteEntry.js', + experiments: { + asyncStartup: true, + }, + shareStrategy: 'loaded-first', + remotes: { + remote1: + 'runtime_remote1@http://localhost:3006/mf-manifest.json?token=secret#hash', + }, + exposes: { + './Button': '/Users/bytedance/project/src/Button.tsx', + }, + shared: { + react: { + singleton: true, + requiredVersion: '^18.2.0', + eager: true, + }, + }, + manifest: { + fileName: 'mf-manifest', + }, + }, + manifest: { + name: 'runtime_host', + metaData: { + pluginVersion: '2.4.0', + globalName: 'runtime_host_global', + publicPath: 'https://cdn.example.com/assets/?v=20260101#hash', + buildInfo: { + buildVersion: '202601010000', + buildName: 'runtime-host', + }, + remoteEntry: { + name: 'remoteEntry.js', + type: 'global', + }, + }, + remotes: [ + { + moduleName: 'remote1', + entry: 'http://localhost:3006/mf-manifest.json?token=secret#hash', + }, + ], + exposes: [ + { + name: './Button', + path: '/Users/bytedance/project/src/Button.tsx', + }, + ], + shared: [ + { + name: 'react', + version: '18.3.1', + singleton: true, + requiredVersion: '^18.2.0', + }, + ], + }, + }); + + expect(buildInfo).toMatchObject({ + schemaVersion: 1, + source: 'manifest', + bundler: { + name: 'webpack', + version: '5.99.0', + mode: 'production', + target: ['web'], + }, + moduleFederation: { + name: 'runtime_host', + pluginVersion: '2.4.0', + buildVersion: '202601010000', + buildName: 'runtime-host', + remoteEntry: { + name: 'remoteEntry.js', + type: 'global', + globalName: 'runtime_host_global', + publicPath: 'https://cdn.example.com/assets/?v=20260101#hash', + publicPathMode: 'static', + }, + options: { + shareStrategy: 'loaded-first', + asyncStartup: true, + manifest: 'mf-manifest', + }, + remotes: [ + { + name: 'remote1', + entry: 'http://localhost:3006/mf-manifest.json?token=secret#hash', + }, + ], + exposes: [ + { + name: './Button', + }, + ], + shared: [ + { + name: 'react', + version: '18.3.1', + singleton: true, + requiredVersion: '^18.2.0', + }, + ], + }, + summary: { + remoteCount: 1, + exposeCount: 1, + sharedCount: 1, + }, + }); + expect(JSON.stringify(buildInfo)).toContain('token=secret#hash'); + expect(JSON.stringify(buildInfo)).not.toContain('/Users/bytedance'); + }); + + it('writes build observability without requiring runtime loading', async () => { + const directory = fs.mkdtempSync( + path.join(os.tmpdir(), 'mf-build-observability-'), + ); + try { + const { processAssetsCallbacks, warn } = createBuildCompilerFixture( + directory, + { + manifest: { + name: 'runtime_host', + metaData: { + pluginVersion: '2.4.0', + globalName: 'runtime_host_global', + publicPath: 'auto', + buildInfo: { + buildVersion: '202601010000', + }, + remoteEntry: { + name: 'remoteEntry.js', + type: 'global', + }, + }, + remotes: [], + exposes: [{ name: './Button' }], + shared: [{ name: 'react', requiredVersion: '^18.2.0' }], + }, + }, + ); + + await processAssetsCallbacks[0](); + + const outputFile = path.join( + directory, + '.mf/observability/build-info.json', + ); + await waitForFile(outputFile); + const buildInfo = JSON.parse(fs.readFileSync(outputFile, 'utf8')); + + expect(buildInfo).toMatchObject({ + source: 'manifest', + bundler: { + name: 'webpack', + version: '5.99.0', + mode: 'development', + target: ['web'], + }, + moduleFederation: { + name: 'runtime_host', + pluginVersion: '2.4.0', + buildVersion: '202601010000', + remoteEntry: { + name: 'remoteEntry.js', + publicPath: 'auto', + publicPathMode: 'auto', + }, + }, + summary: { + exposeCount: 1, + sharedCount: 1, + }, + }); + expect(warn).not.toHaveBeenCalled(); + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } + }); + + it('does not fail the build when observability file output fails', async () => { + const directory = fs.mkdtempSync( + path.join(os.tmpdir(), 'mf-build-observability-'), + ); + try { + const blocker = path.join(directory, 'blocker'); + fs.writeFileSync(blocker, 'not a directory', 'utf8'); + const { processAssetsCallbacks, warn } = createBuildCompilerFixture( + directory, + { + outputFile: path.join(blocker, 'build-info.json'), + }, + ); + + await expect(processAssetsCallbacks[0]()).resolves.toBeUndefined(); + + expect(warn).toHaveBeenCalledWith( + expect.stringContaining('Failed to write build observability'), + ); + const report = JSON.parse( + fs.readFileSync( + path.join(directory, '.mf/observability/build-report.json'), + 'utf8', + ), + ); + expect(report).toMatchObject({ + source: 'build', + status: 'error', + failedPhase: 'observability-output', + summary: { + outcome: 'failed', + error: { + failedPhase: 'observability-output', + ownerHint: 'build', + retryable: false, + }, + }, + diagnosis: { + status: 'error', + outcome: 'failed', + ownerHint: 'build', + failedPhase: 'observability-output', + actions: [ + expect.objectContaining({ + id: 'check-observability-output', + ownerHint: 'build', + }), + ], + facts: expect.objectContaining({ + failedPhase: 'observability-output', + ownerHint: 'build', + mfName: 'runtime_host', + }), + }, + }); + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } + }); + + it('writes a build report for compilation errors', async () => { + const directory = fs.mkdtempSync( + path.join(os.tmpdir(), 'mf-build-observability-'), + ); + try { + const error = new Error( + '[ Federation Build ] remoteEntry failed #BUILD-001 token=demo-secret http://localhost:3001/remoteEntry.js?token=demo-secret#hash', + ); + error.stack = [ + 'Error: token=demo-secret remoteEntry failed', + ' at build (/Users/bytedance/private/webpack.config.js:1:1)', + ' at remote (http://localhost:3001/remoteEntry.js?token=demo-secret#hash:1:1)', + ].join('\n'); + + const { processAssetsCallbacks, warn } = createBuildCompilerFixture( + directory, + { + errors: [error], + }, + ); + + await processAssetsCallbacks[0](); + + const report = JSON.parse( + fs.readFileSync( + path.join(directory, '.mf/observability/build-report.json'), + 'utf8', + ), + ); + expect(report).toMatchObject({ + schemaVersion: 1, + source: 'build', + status: 'error', + failedPhase: 'compilation', + build: { + moduleFederation: { + name: 'runtime_host', + }, + }, + summary: { + outcome: 'failed', + error: { + errorCode: 'BUILD-001', + failedPhase: 'compilation', + ownerHint: 'remote', + retryable: false, + }, + }, + diagnosis: { + title: 'Module Federation build failed', + status: 'error', + outcome: 'failed', + ownerHint: 'remote', + failedPhase: 'compilation', + errorCode: 'BUILD-001', + facts: expect.objectContaining({ + failedPhase: 'compilation', + remoteCount: 1, + exposeCount: 1, + sharedCount: 1, + remotes: 'remote1', + exposes: './Button', + shared: 'react', + }), + actions: [ + expect.objectContaining({ + id: 'inspect-build-errors', + ownerHint: 'remote', + }), + expect.objectContaining({ + id: 'check-remote-config', + ownerHint: 'remote', + }), + ], + }, + }); + expect(JSON.stringify(report)).toContain('demo-secret'); + expect(JSON.stringify(report)).toContain('token='); + expect(JSON.stringify(report)).toContain('#hash'); + expect(JSON.stringify(report)).toContain('/Users/bytedance'); + expect(warn).not.toHaveBeenCalled(); + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } + }); + + it('removes stale build reports after a clean build', async () => { + const directory = fs.mkdtempSync( + path.join(os.tmpdir(), 'mf-build-observability-'), + ); + try { + const reportFile = path.join( + directory, + '.mf/observability/build-report.json', + ); + fs.mkdirSync(path.dirname(reportFile), { recursive: true }); + fs.writeFileSync(reportFile, '{"status":"error"}', 'utf8'); + + const { processAssetsCallbacks } = createBuildCompilerFixture(directory); + + await processAssetsCallbacks[0](); + + expect(fs.existsSync(reportFile)).toBe(false); + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } + }); +}); + +const emitRemoteLoaded = ( + observability: ReturnType, + overrides: Record = {}, +) => + observability.plugin.onLoad?.({ + id: 'remote/Button', + pkgNameOrAlias: 'remote', + expose: './Button', + remote: { + name: 'remote', + entry: 'http://localhost:3001/mf-manifest.json', + }, + options: {}, + origin: enabledOrigin, + exposeModule: {}, + exposeModuleFactory: undefined, + moduleInstance: {}, + ...overrides, + } as any); + +const emitRemoteStart = ( + observability: ReturnType, + overrides: Record = {}, +) => + observability.plugin.beforeRequest?.({ + id: 'remote/Button', + options: {}, + origin: enabledOrigin, + ...overrides, + } as any); + +const emitRemoteComplete = ( + observability: ReturnType, + overrides: Record = {}, +) => + observability.plugin.afterLoadRemote?.({ + id: 'remote/Button', + expose: './Button', + remote: { + name: 'remote', + entry: 'http://localhost:3001/mf-manifest.json', + }, + origin: enabledOrigin, + ...overrides, + } as any); + +const emitRemoteMatch = ( + observability: ReturnType, + overrides: Record = {}, +) => + observability.plugin.afterMatchRemote?.({ + id: 'remote/Button', + expose: './Button', + remoteInfo: { + name: 'remote', + entry: 'http://localhost:3001/mf-manifest.json', + }, + origin: enabledOrigin, + ...overrides, + } as any); + +const emitRemoteInit = ( + observability: ReturnType, + overrides: Record = {}, +) => + observability.plugin.afterInitRemote?.({ + id: 'remote/Button', + remoteInfo: { + name: 'remote', + entry: 'http://localhost:3001/mf-manifest.json', + }, + origin: enabledOrigin, + ...overrides, + } as any); + +const emitExposePhase = ( + observability: ReturnType, + hookName: 'beforeGetExpose' | 'afterGetExpose', + overrides: Record = {}, +) => + observability.plugin[hookName]?.({ + id: 'remote/Button', + expose: './Button', + moduleInfo: { + name: 'remote', + entry: 'http://localhost:3001/mf-manifest.json', + }, + origin: enabledOrigin, + ...overrides, + } as any); + +const emitFactoryPhase = ( + observability: ReturnType, + hookName: 'beforeExecuteFactory' | 'afterExecuteFactory', + overrides: Record = {}, +) => + observability.plugin[hookName]?.({ + id: 'remote/Button', + expose: './Button', + moduleInfo: { + name: 'remote', + entry: 'http://localhost:3001/mf-manifest.json', + }, + loadFactory: true, + origin: enabledOrigin, + ...overrides, + } as any); + +const emitRemoteError = ( + observability: ReturnType, + overrides: Record = {}, +) => + observability.plugin.errorLoadRemote?.({ + id: 'remote/Button', + error: new Error('remote load failed'), + from: 'runtime', + lifecycle: 'onLoad', + expose: './Button', + remote: { + name: 'remote', + entry: 'http://localhost:3001/mf-manifest.json', + }, + origin: enabledOrigin, + ...overrides, + } as any); + +const emitManifestError = ( + observability: ReturnType, + overrides: Record = {}, +) => + observability.plugin.errorLoadRemote?.({ + id: 'http://localhost:3001/mf-manifest.json?token=demo-secret#hash', + error: new Error('token=demo-secret manifest failed'), + from: 'runtime', + lifecycle: 'afterResolve', + remote: { + name: 'remote', + entry: 'http://localhost:3001/mf-manifest.json?token=demo-secret#hash', + }, + origin: enabledOrigin, + ...overrides, + } as any); + +const createLoadedBeforeFederationFixture = () => { + const currentOrigin = { + version: '2.5.0', + options: { + name: 'consumer_b', + }, + moduleCache: new Map([ + [ + 'provider', + { + remoteInfo: { + name: 'provider', + entryGlobalName: 'provider_global', + }, + remoteEntryExports: { get: vi.fn() }, + inited: true, + }, + ], + ]), + }; + const previousConsumer = { + version: '2.5.0', + options: { + name: 'consumer_a', + }, + moduleCache: new Map([ + [ + 'provider', + { + remoteInfo: { + name: 'provider', + entryGlobalName: 'provider_global', + }, + remoteEntryExports: { get: vi.fn() }, + inited: true, + }, + ], + ]), + remoteHandler: { + idToRemoteMap: { + 'provider/Button': { + name: 'provider', + expose: './Button', + }, + 'provider/Card': { + name: 'provider', + expose: './Card', + }, + }, + }, + }; + + return { + currentOrigin, + previousConsumer, + federation: { + __INSTANCES__: [currentOrigin, previousConsumer], + }, + }; +}; + +const waitForFile = async (filePath: string) => { + const startedAt = Date.now(); + + while (Date.now() - startedAt < 1000) { + if (fs.existsSync(filePath)) { + return; + } + + await new Promise((resolve) => setTimeout(resolve, 10)); + } + + throw new Error(`Timed out waiting for ${filePath}`); +}; + +const createBuildCompilerFixture = ( + tempDir: string, + options: { + manifest?: Record; + stats?: Record; + moduleFederation?: Record; + outputFile?: string; + errorReport?: + | false + | { + outputFile?: string; + }; + errors?: unknown[]; + } = {}, +) => { + const processAssetsCallbacks: Array<() => Promise> = []; + const emittedAssets: Record = {}; + const warn = vi.fn(); + const compilation = { + constructor: { + PROCESS_ASSETS_STAGE_REPORT: 5000, + }, + getAsset: (assetName: string) => { + if (assetName === 'mf-manifest.json' && options.manifest) { + return { + source: { + source: () => JSON.stringify(options.manifest), + }, + }; + } + if (assetName === 'mf-stats.json' && options.stats) { + return { + source: { + source: () => JSON.stringify(options.stats), + }, + }; + } + return undefined; + }, + emitAsset: (name: string, source: { source(): string }) => { + emittedAssets[name] = source; + }, + errors: options.errors || [], + hooks: { + processAssets: { + tapPromise: ( + _options: { name: string; stage?: number }, + callback: () => Promise, + ) => { + processAssetsCallbacks.push(callback); + }, + }, + }, + }; + const moduleFederation = options.moduleFederation || { + name: 'runtime_host', + remotes: { + remote1: + 'runtime_remote1@http://localhost:3006/mf-manifest.json?token=secret#hash', + }, + exposes: { + './Button': '/Users/bytedance/project/src/Button.tsx', + }, + shared: { + react: { + singleton: true, + requiredVersion: '^18.2.0', + }, + }, + }; + const compiler = { + context: tempDir, + options: { + context: tempDir, + mode: 'development', + target: 'web', + output: { + publicPath: 'auto', + }, + plugins: [ + { + name: 'ModuleFederationPlugin', + _options: moduleFederation, + }, + ], + }, + webpack: { + version: '5.99.0', + sources: { + RawSource: class RawSource { + private readonly value: string; + + constructor(value: string) { + this.value = value; + } + + source() { + return this.value; + } + }, + }, + Compilation: { + PROCESS_ASSETS_STAGE_REPORT: 5000, + }, + }, + hooks: { + thisCompilation: { + tap: ( + _name: string, + callback: (receivedCompilation: typeof compilation) => void, + ) => { + callback(compilation); + }, + }, + }, + getInfrastructureLogger: () => ({ + warn, + }), + }; + + new ObservabilityBuildPlugin({ + outputFile: options.outputFile, + errorReport: options.errorReport, + }).apply(compiler); + + return { + processAssetsCallbacks, + warn, + }; +}; + +describe('ObservabilityPlugin', () => { + it('returns a runtime plugin and attaches markComponentLoaded to the runtime instance', () => { + const plugin = ObservabilityPlugin({ level: 'verbose' }); + const instance = {} as { + markComponentLoaded?: ReturnType< + typeof createObservability + >['markComponentLoaded']; + }; + + expect(plugin.name).toBe('observability-plugin'); + plugin.apply?.( + instance as unknown as Parameters>[0], + ); + + expect(typeof instance.markComponentLoaded).toBe('function'); + }); + + it('returns a chrome runtime plugin without attaching instance APIs', () => { + const plugin = ChromeObservabilityPlugin({ level: 'verbose' }); + const instance = {} as { + markComponentLoaded?: ReturnType< + typeof createObservability + >['markComponentLoaded']; + }; + + expect(plugin.name).toBe('observability-plugin:chrome-extension'); + plugin.apply?.( + instance as unknown as Parameters>[0], + ); + + expect(instance.markComponentLoaded).toBeUndefined(); + }); + + it('does not register preload hooks for the chrome injected runtime plugin', () => { + const plugin = ChromeObservabilityPlugin({ level: 'verbose' }); + + expect(plugin.generatePreloadAssets).toBeUndefined(); + }); + + it('exposes chrome reports under the fixed browser scope', () => { + const previousFederation = (globalThis as any).__FEDERATION__; + + try { + (globalThis as any).__FEDERATION__ = {}; + const plugin = ChromeObservabilityPlugin({ + level: 'verbose', + console: false, + browser: { + enabled: true, + scope: 'ignored_custom_scope', + }, + }); + + plugin.beforeRequest?.({ + id: 'remote/Button', + options: {}, + origin: enabledOrigin, + } as any); + + expect( + (globalThis as any).__FEDERATION__.__OBSERVABILITY__.chrome_extension, + ).toBeDefined(); + expect( + (globalThis as any).__FEDERATION__.__OBSERVABILITY__ + .ignored_custom_scope, + ).toBeUndefined(); + } finally { + (globalThis as any).__FEDERATION__ = previousFederation; + } + }); + + it('skips paired runtime lifecycle hooks for unsupported runtime versions', () => { + const observability = createObservability( + { + level: 'verbose', + console: false, + }, + { + guardRuntimeHooksByRuntimeVersion: true, + returnHookArgs: true, + }, + ); + const legacyEntryArgs = { + remoteInfo: { + name: 'legacy-remote', + entry: 'http://localhost:3001/remoteEntry.js', + }, + origin: { + version: '2.3.1', + options: { + name: 'legacy-host', + }, + }, + }; + + expect((observability.plugin.loadEntry as any)(legacyEntryArgs)).toBe( + undefined, + ); + expect( + observability.getEvents().some((event) => event.phase === 'remoteEntry'), + ).toBe(false); + + expect( + (observability.plugin.loadEntry as any)({ + ...legacyEntryArgs, + origin: { + ...legacyEntryArgs.origin, + version: '2.5.0', + }, + }), + ).toBeUndefined(); + + expect( + observability.getEvents().some((event) => event.phase === 'remoteEntry'), + ).toBe(true); + }); + + it('keeps waterfall hook return values for the chrome injected runtime plugin', () => { + const plugin = ChromeObservabilityPlugin({ + level: 'verbose', + console: false, + }); + const requestArgs = { + id: 'legacy-remote/Button', + options: { + name: 'legacy-host', + remotes: [ + { + name: 'legacy-remote', + entry: 'http://localhost:3001/remoteEntry.js', + }, + ], + }, + origin: { + version: '2.3.1', + options: { + name: 'legacy-host', + }, + }, + }; + const snapshotArgs = { + options: requestArgs.options, + moduleInfo: { + name: 'legacy-remote', + entry: 'http://localhost:3001/mf-manifest.json', + }, + hostGlobalSnapshot: undefined, + remoteSnapshot: undefined, + globalSnapshot: {}, + }; + const resolveArgs = { + ...requestArgs, + remote: requestArgs.options.remotes[0], + remoteInfo: requestArgs.options.remotes[0], + expose: 'Button', + pkgNameOrAlias: 'legacy-remote', + }; + + expect((plugin.beforeRequest as any)(requestArgs)).toBe(requestArgs); + expect((plugin.loadSnapshot as any)(snapshotArgs)).toBe(snapshotArgs); + expect((plugin.afterResolve as any)(resolveArgs)).toBe(resolveArgs); + }); + + it('does not return lifecycle args from chrome entry hooks', () => { + const plugin = ChromeObservabilityPlugin({ + level: 'verbose', + console: false, + }); + const entryArgs = { + remoteInfo: { + name: 'legacy-remote', + entry: 'http://localhost:3001/remoteEntry.js', + }, + origin: { + version: '2.3.1', + options: { + name: 'legacy-host', + }, + }, + }; + + expect((plugin.loadEntry as any)(entryArgs)).toBeUndefined(); + expect((plugin.afterLoadEntry as any)(entryArgs)).toBeUndefined(); + }); + + it('records paired runtime lifecycle hooks for supported runtime versions', () => { + const observability = createObservability( + { + level: 'verbose', + console: false, + }, + { + guardRuntimeHooksByRuntimeVersion: true, + returnHookArgs: true, + }, + ); + + (observability.plugin.loadEntry as any)({ + remoteInfo: { + name: 'modern-remote', + entry: 'http://localhost:3001/remoteEntry.js', + }, + origin: { + version: '2.5.0', + options: { + name: 'modern-host', + }, + }, + }); + + expect(observability.getEvents()).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + phase: 'remoteEntry', + status: 'start', + remote: expect.objectContaining({ + name: 'modern-remote', + }), + }), + ]), + ); + }); + + it('treats missing runtime version as unsupported for guarded runtime hooks', () => { + const observability = createObservability( + { + level: 'verbose', + console: false, + }, + { + guardRuntimeHooksByRuntimeVersion: true, + returnHookArgs: true, + }, + ); + const entryArgs = { + remoteInfo: { + name: 'unknown-version-remote', + entry: 'http://localhost:3001/remoteEntry.js', + }, + origin: { + options: { + name: 'unknown-version-host', + }, + }, + }; + + expect((observability.plugin.loadEntry as any)(entryArgs)).toBeUndefined(); + expect(observability.getEvents()).toEqual([]); + }); + + it('does not wrap a remote React function component unless callback injection is explicitly enabled', async () => { + const react = { + createElement: vi.fn((type: unknown, props?: unknown) => ({ + type, + props, + })), + }; + const observability = createObservability({ + level: 'verbose', + react: { + enabled: true, + }, + }); + + function RemoteButton() { + return null; + } + + const wrapped = await observability.plugin.onLoad?.({ + id: 'remote/Button', + pkgNameOrAlias: 'remote', + expose: './Button', + remote: { + name: 'remote', + entry: 'http://localhost:3001/mf-manifest.json', + }, + options: {}, + origin: { + ...enabledOrigin, + loadShareSync: () => () => react, + }, + exposeModule: RemoteButton, + exposeModuleFactory: undefined, + moduleInstance: {}, + } as any); + + expect(wrapped).toBeUndefined(); + expect(react.createElement).not.toHaveBeenCalled(); + expect(observability.getEvents()).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + phase: 'component', + }), + ]), + ); + }); + + it('injects the producer loaded callback when explicitly enabled', async () => { + const react = { + createElement: vi.fn((type: unknown, props?: unknown) => ({ + type, + props, + })), + }; + const observability = createObservability({ + level: 'verbose', + react: { + injectLoadedCallback: true, + }, + }); + + function RemoteButton() { + return null; + } + + const wrapped = await observability.plugin.onLoad?.({ + id: 'remote/Button', + pkgNameOrAlias: 'remote', + expose: './Button', + remote: { + name: 'remote', + entry: 'http://localhost:3001/mf-manifest.json', + }, + options: {}, + origin: { + ...enabledOrigin, + loadShareSync: () => () => react, + }, + exposeModule: RemoteButton, + exposeModuleFactory: undefined, + moduleInstance: {}, + } as any); + + expect(typeof wrapped).toBe('function'); + + (wrapped as (props: Record) => unknown)({ + label: 'Save', + }); + + const renderedProps = react.createElement.mock.calls[0]?.[1] as { + label: string; + onMFRemoteLoaded: (options?: { + componentName?: string; + metadata?: Record; + }) => void; + }; + + expect(react.createElement).toHaveBeenCalledWith( + RemoteButton, + expect.objectContaining({ + label: 'Save', + onMFRemoteLoaded: expect.any(Function), + }), + ); + expect(observability.getEvents()).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + eventName: expect.stringContaining('component:react-'), + }), + ]), + ); + + renderedProps.onMFRemoteLoaded({ + metadata: { + readySource: 'producer', + }, + }); + + expect(observability.getEvents()).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + phase: 'component', + status: 'success', + eventName: 'component:business-loaded', + source: 'business', + componentName: 'RemoteButton', + metadata: { + readySource: 'producer', + }, + }), + ]), + ); + + react.createElement.mockClear(); + + function RemoteCard() { + return null; + } + + const remoteModule = { + default: RemoteCard, + named: 'value', + }; + const wrappedModule = await observability.plugin.onLoad?.({ + id: 'remote/Card', + pkgNameOrAlias: 'remote', + expose: './Card', + remote: { + name: 'remote', + entry: 'http://localhost:3001/mf-manifest.json', + }, + options: {}, + origin: { + ...enabledOrigin, + loadShareSync: () => () => react, + }, + exposeModule: remoteModule, + exposeModuleFactory: undefined, + moduleInstance: {}, + } as any); + + expect(wrappedModule).toBeUndefined(); + expect(remoteModule.named).toBe('value'); + expect(remoteModule.default).toEqual(expect.any(Function)); + + const WrappedDefault = remoteModule.default as ( + props: Record, + ) => unknown; + WrappedDefault({}); + + expect(react.createElement).toHaveBeenCalledWith( + RemoteCard, + expect.objectContaining({ + onMFRemoteLoaded: expect.any(Function), + }), + ); + }); + + it('injects the producer loaded callback for build-time remote factories', async () => { + const react = { + createElement: vi.fn((type: unknown, props?: unknown) => ({ + type, + props, + })), + }; + const observability = createObservability({ + level: 'verbose', + react: { + injectLoadedCallback: true, + }, + }); + + function RemoteProvider() { + return null; + } + + const wrappedFactory = await observability.plugin.onLoad?.({ + id: 'provider', + pkgNameOrAlias: 'provider', + expose: '.', + remote: { + name: 'provider', + entry: 'http://localhost:3001/mf-manifest.json', + }, + options: {}, + origin: { + ...enabledOrigin, + loadShareSync: () => () => react, + }, + exposeModule: undefined, + exposeModuleFactory: () => RemoteProvider, + moduleInstance: {}, + } as any); + + expect(typeof wrappedFactory).toBe('function'); + + const WrappedProvider = (wrappedFactory as () => unknown)(); + expect(typeof WrappedProvider).toBe('function'); + + (WrappedProvider as (props: Record) => unknown)({}); + + const renderedProps = react.createElement.mock.calls[0]?.[1] as { + onMFRemoteLoaded: (options?: { + componentName?: string; + metadata?: Record; + }) => void; + }; + + expect(react.createElement).toHaveBeenCalledWith( + RemoteProvider, + expect.objectContaining({ + onMFRemoteLoaded: expect.any(Function), + }), + ); + + renderedProps.onMFRemoteLoaded({ + metadata: { + readySource: 'producer', + }, + }); + + expect(observability.getEvents()).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + phase: 'component', + status: 'success', + eventName: 'component:business-loaded', + source: 'business', + componentName: 'RemoteProvider', + metadata: { + readySource: 'producer', + }, + }), + ]), + ); + + react.createElement.mockClear(); + + function ReadonlyRemoteProvider() { + return null; + } + + const readonlyModule = {}; + Object.defineProperty(readonlyModule, 'default', { + configurable: false, + enumerable: true, + get: () => ReadonlyRemoteProvider, + }); + + const wrappedReadonlyFactory = await observability.plugin.onLoad?.({ + id: 'provider', + pkgNameOrAlias: 'provider', + expose: '.', + remote: { + name: 'provider', + entry: 'http://localhost:3001/mf-manifest.json', + }, + options: {}, + origin: { + ...enabledOrigin, + loadShareSync: () => () => react, + }, + exposeModule: undefined, + exposeModuleFactory: () => readonlyModule, + moduleInstance: {}, + } as any); + + const wrappedReadonlyModule = ( + wrappedReadonlyFactory as () => Record + )(); + + expect(wrappedReadonlyModule).not.toBe(readonlyModule); + expect(wrappedReadonlyModule.default).toEqual(expect.any(Function)); + + ( + wrappedReadonlyModule.default as ( + props: Record, + ) => unknown + )({}); + + expect(react.createElement).toHaveBeenCalledWith( + ReadonlyRemoteProvider, + expect.objectContaining({ + onMFRemoteLoaded: expect.any(Function), + }), + ); + }); + + it('wraps explicitly matched React remotes even when the compiled component name is not capitalized', async () => { + const react = { + createElement: vi.fn((type: unknown, props?: unknown) => ({ + type, + props, + })), + }; + const observability = createObservability({ + level: 'verbose', + react: { + injectLoadedCallback: true, + remoteIds: ['remote/profile'], + }, + }); + + const compiledRemote = function profile_widget() { + return null; + }; + + const unmatched = await observability.plugin.onLoad?.({ + id: 'remote/settings', + pkgNameOrAlias: 'remote', + expose: './settings', + remote: { + name: 'remote', + entry: 'http://localhost:3001/mf-manifest.json', + }, + options: {}, + origin: { + ...enabledOrigin, + loadShareSync: () => () => react, + }, + exposeModule: compiledRemote, + exposeModuleFactory: undefined, + moduleInstance: {}, + } as any); + + expect(unmatched).toBeUndefined(); + + const matched = await observability.plugin.onLoad?.({ + id: 'remote/profile', + pkgNameOrAlias: 'remote', + expose: './profile', + remote: { + name: 'remote', + entry: 'http://localhost:3001/mf-manifest.json', + }, + options: {}, + origin: { + ...enabledOrigin, + loadShareSync: () => () => react, + }, + exposeModule: compiledRemote, + exposeModuleFactory: undefined, + moduleInstance: {}, + } as any); + + expect(typeof matched).toBe('function'); + + (matched as (props: Record) => unknown)({}); + + expect(react.createElement).toHaveBeenCalledWith( + compiledRemote, + expect.objectContaining({ + onMFRemoteLoaded: expect.any(Function), + }), + ); + }); + + it('matches React callback injection by remote alias and expose', async () => { + const react = { + createElement: vi.fn((type: unknown, props?: unknown) => ({ + type, + props, + })), + }; + const observability = createObservability({ + level: 'verbose', + react: { + injectLoadedCallback: true, + remoteIds: ['rslibProvider/Card'], + }, + }); + + function ProviderCard() { + return null; + } + + const wrapped = await observability.plugin.onLoad?.({ + id: '@vmok-demo/rslib-provider/Card', + pkgNameOrAlias: '@vmok-demo/rslib-provider', + expose: './Card', + remote: { + name: '@vmok-demo/rslib-provider', + alias: 'rslibProvider', + entry: 'http://localhost:3001/mf-manifest.json', + }, + options: {}, + origin: { + ...enabledOrigin, + loadShareSync: () => () => react, + }, + exposeModule: ProviderCard, + exposeModuleFactory: undefined, + moduleInstance: {}, + } as any); + + expect(typeof wrapped).toBe('function'); + + (wrapped as (props: Record) => unknown)({}); + + expect(react.createElement).toHaveBeenCalledWith( + ProviderCard, + expect.objectContaining({ + onMFRemoteLoaded: expect.any(Function), + }), + ); + + expect(observability.getLatestReport()).toMatchObject({ + requestId: '@vmok-demo/rslib-provider/Card', + requestAlias: 'rslibProvider/Card', + remote: { + name: '@vmok-demo/rslib-provider', + alias: 'rslibProvider', + }, + diagnosis: { + facts: expect.objectContaining({ + requestId: '@vmok-demo/rslib-provider/Card', + requestAlias: 'rslibProvider/Card', + remoteName: '@vmok-demo/rslib-provider', + remoteAlias: 'rslibProvider', + }), + }, + }); + }); + + it('injects the producer loaded callback even when React cannot be resolved from shared scope', async () => { + const observability = createObservability({ + level: 'verbose', + react: { + injectLoadedCallback: true, + }, + }); + + function RemotePanel(props: { + onMFRemoteLoaded?: (options?: { + metadata?: Record; + }) => void; + }) { + props.onMFRemoteLoaded?.({ + metadata: { + readySource: 'producer', + }, + }); + return 'rendered'; + } + + const wrapped = await observability.plugin.onLoad?.({ + id: 'remote/Panel', + pkgNameOrAlias: 'remote', + expose: './Panel', + remote: { + name: 'remote', + entry: 'http://localhost:3001/mf-manifest.json', + }, + options: {}, + origin: { + ...enabledOrigin, + loadShareSync: () => false, + loadShare: async () => false, + }, + exposeModule: RemotePanel, + exposeModuleFactory: undefined, + moduleInstance: {}, + } as any); + + expect(typeof wrapped).toBe('function'); + expect((wrapped as (props: Record) => unknown)({})).toBe( + 'rendered', + ); + expect(observability.getEvents()).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + phase: 'component', + status: 'success', + eventName: 'component:business-loaded', + source: 'business', + componentName: 'RemotePanel', + metadata: { + readySource: 'producer', + }, + }), + ]), + ); + }); + + it('does not record events when the plugin is disabled', async () => { + const observability = createObservability({ + enabled: false, + level: 'verbose', + }); + + await emitRemoteLoaded(observability); + + expect(observability.getEvents()).toHaveLength(0); + expect(observability.getLatestReport()).toBeUndefined(); + }); + + it('records a successful loadRemote report when enabled', async () => { + const observability = createObservability({ level: 'verbose' }); + + emitRemoteStart(observability); + await emitRemoteLoaded(observability); + + const report = observability.getLatestReport(); + expect(report?.status).toBe('success'); + expect(report?.requestId).toBe('remote/Button'); + expect(report?.remote?.name).toBe('remote'); + expect(report?.summary).toMatchObject({ + runtimeLoaded: true, + loadCompleted: false, + componentLoaded: false, + outcome: 'runtime-loaded', + lastPhase: 'loadRemote', + }); + expect(report?.summary.phases.loadRemote).toMatchObject({ + status: 'success', + duration: expect.any(Number), + }); + expect(report?.diagnosis).toMatchObject({ + title: 'Remote loaded successfully', + status: 'success', + outcome: 'runtime-loaded', + ownerHint: 'remote', + facts: expect.objectContaining({ + requestId: 'remote/Button', + remoteName: 'remote', + runtimeLoaded: true, + componentLoaded: false, + }), + completedPhases: ['loadRemote'], + actions: [], + }); + expect(report?.events).toHaveLength(2); + }); + + it('posts events to the local collector outside debug mode', () => { + const originalFetch = globalThis.fetch; + const originalDebug = process.env['FEDERATION_DEBUG']; + const fetchMock = vi.fn(() => Promise.resolve({ ok: true })); + Object.defineProperty(globalThis, 'fetch', { + value: fetchMock, + configurable: true, + writable: true, + }); + delete process.env['FEDERATION_DEBUG']; + + try { + const observability = createObservability({ + level: 'verbose', + collector: true, + }); + + emitRemoteStart(observability); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock.mock.calls[0]?.[0]).toBe( + 'http://127.0.0.1:17891/__mf_observability', + ); + + const requestInit = fetchMock.mock.calls[0]?.[1] as { + body: string; + credentials: string; + mode: string; + }; + const payload = JSON.parse(requestInit.body); + + expect(requestInit).toMatchObject({ + credentials: 'omit', + mode: 'cors', + }); + expect(payload).toMatchObject({ + schemaVersion: 1, + source: 'browser', + kind: 'event', + event: { + phase: 'loadRemote', + status: 'start', + requestId: 'remote/Button', + }, + report: { + requestId: 'remote/Button', + status: 'pending', + }, + }); + } finally { + if (originalDebug === undefined) { + delete process.env['FEDERATION_DEBUG']; + } else { + process.env['FEDERATION_DEBUG'] = originalDebug; + } + + if (originalFetch) { + Object.defineProperty(globalThis, 'fetch', { + value: originalFetch, + configurable: true, + writable: true, + }); + } else { + Reflect.deleteProperty(globalThis, 'fetch'); + } + } + }); + + it('posts events to the local collector in debug mode', () => { + const originalFetch = globalThis.fetch; + const originalDebug = process.env['FEDERATION_DEBUG']; + const fetchMock = vi.fn(() => Promise.resolve({ ok: true })); + Object.defineProperty(globalThis, 'fetch', { + value: fetchMock, + configurable: true, + writable: true, + }); + process.env['FEDERATION_DEBUG'] = 'true'; + + try { + const observability = createObservability({ + level: 'verbose', + collector: true, + }); + + emitRemoteStart(observability); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock.mock.calls[0]?.[0]).toBe( + 'http://127.0.0.1:17891/__mf_observability', + ); + + const requestInit = fetchMock.mock.calls[0]?.[1] as { + body: string; + credentials: string; + mode: string; + }; + const payload = JSON.parse(requestInit.body); + + expect(requestInit).toMatchObject({ + credentials: 'omit', + mode: 'cors', + }); + expect(payload).toMatchObject({ + schemaVersion: 1, + source: 'browser', + kind: 'event', + event: { + phase: 'loadRemote', + status: 'start', + requestId: 'remote/Button', + }, + report: { + requestId: 'remote/Button', + status: 'pending', + }, + }); + } finally { + if (originalDebug === undefined) { + delete process.env['FEDERATION_DEBUG']; + } else { + process.env['FEDERATION_DEBUG'] = originalDebug; + } + + if (originalFetch) { + Object.defineProperty(globalThis, 'fetch', { + value: originalFetch, + configurable: true, + writable: true, + }); + } else { + Reflect.deleteProperty(globalThis, 'fetch'); + } + } + }); + + it('logs local collector connection failures in debug mode', async () => { + const originalFetch = globalThis.fetch; + const originalDebug = process.env['FEDERATION_DEBUG']; + const consoleDebug = vi + .spyOn(console, 'debug') + .mockImplementation(() => undefined); + const fetchError = new Error('collector unavailable'); + const fetchMock = vi.fn(() => Promise.reject(fetchError)); + Object.defineProperty(globalThis, 'fetch', { + value: fetchMock, + configurable: true, + writable: true, + }); + process.env['FEDERATION_DEBUG'] = 'true'; + + try { + const observability = createObservability({ + level: 'verbose', + collector: true, + }); + + emitRemoteStart(observability); + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(consoleDebug).toHaveBeenCalledWith( + '[ Module Federation Observability Plugin ]', + 'Failed to notify local observability collector.', + fetchError, + expect.stringContaining('Stack trace:'), + ); + } finally { + consoleDebug.mockRestore(); + + if (originalDebug === undefined) { + delete process.env['FEDERATION_DEBUG']; + } else { + process.env['FEDERATION_DEBUG'] = originalDebug; + } + + if (originalFetch) { + Object.defineProperty(globalThis, 'fetch', { + value: originalFetch, + configurable: true, + writable: true, + }); + } else { + Reflect.deleteProperty(globalThis, 'fetch'); + } + } + }); + + it('hides local collector connection failures outside debug mode', async () => { + const originalFetch = globalThis.fetch; + const originalDebug = process.env['FEDERATION_DEBUG']; + const consoleDebug = vi + .spyOn(console, 'debug') + .mockImplementation(() => undefined); + const fetchError = new Error('collector unavailable'); + const fetchMock = vi.fn(() => Promise.reject(fetchError)); + Object.defineProperty(globalThis, 'fetch', { + value: fetchMock, + configurable: true, + writable: true, + }); + delete process.env['FEDERATION_DEBUG']; + + try { + const observability = createObservability({ + level: 'verbose', + collector: true, + }); + + emitRemoteStart(observability); + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(consoleDebug).not.toHaveBeenCalled(); + } finally { + consoleDebug.mockRestore(); + + if (originalDebug === undefined) { + delete process.env['FEDERATION_DEBUG']; + } else { + process.env['FEDERATION_DEBUG'] = originalDebug; + } + + if (originalFetch) { + Object.defineProperty(globalThis, 'fetch', { + value: originalFetch, + configurable: true, + writable: true, + }); + } else { + Reflect.deleteProperty(globalThis, 'fetch'); + } + } + }); + + it('posts events to the browser devtools channel when enabled', () => { + const originalPostMessage = (globalThis as { postMessage?: unknown }) + .postMessage; + const postMessageMock = vi.fn(); + Object.defineProperty(globalThis, 'postMessage', { + value: postMessageMock, + configurable: true, + writable: true, + }); + + try { + const observability = createObservability({ + level: 'verbose', + console: false, + browser: { + enabled: true, + scope: 'runtime_host', + }, + devtools: true, + }); + + emitRemoteStart(observability); + + expect(postMessageMock).toHaveBeenCalledTimes(1); + expect(postMessageMock.mock.calls[0]?.[1]).toBe('*'); + expect(postMessageMock.mock.calls[0]?.[0]).toMatchObject({ + schemaVersion: 1, + source: 'module-federation/observability', + kind: 'event', + scope: 'host', + event: { + phase: 'loadRemote', + status: 'start', + requestId: 'remote/Button', + }, + report: { + requestId: 'remote/Button', + status: 'pending', + }, + }); + } finally { + if (originalPostMessage) { + Object.defineProperty(globalThis, 'postMessage', { + value: originalPostMessage, + configurable: true, + writable: true, + }); + } else { + Reflect.deleteProperty(globalThis, 'postMessage'); + } + } + }); + + it('posts collector events and skips devtools channel in production mode', () => { + const originalFetch = globalThis.fetch; + const originalDebug = process.env['FEDERATION_DEBUG']; + const originalNodeEnv = process.env['NODE_ENV']; + const originalPostMessage = (globalThis as { postMessage?: unknown }) + .postMessage; + const fetchMock = vi.fn(() => Promise.resolve({ ok: true })); + const postMessageMock = vi.fn(); + + Object.defineProperty(globalThis, 'fetch', { + value: fetchMock, + configurable: true, + writable: true, + }); + Object.defineProperty(globalThis, 'postMessage', { + value: postMessageMock, + configurable: true, + writable: true, + }); + process.env['FEDERATION_DEBUG'] = 'true'; + process.env['NODE_ENV'] = 'production'; + + try { + const observability = createObservability({ + level: 'verbose', + console: false, + browser: { + enabled: true, + scope: 'runtime_host', + }, + collector: true, + devtools: true, + }); + + emitRemoteStart(observability); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(postMessageMock).not.toHaveBeenCalled(); + } finally { + if (originalDebug === undefined) { + delete process.env['FEDERATION_DEBUG']; + } else { + process.env['FEDERATION_DEBUG'] = originalDebug; + } + if (originalNodeEnv === undefined) { + delete process.env['NODE_ENV']; + } else { + process.env['NODE_ENV'] = originalNodeEnv; + } + + if (originalFetch) { + Object.defineProperty(globalThis, 'fetch', { + value: originalFetch, + configurable: true, + writable: true, + }); + } else { + Reflect.deleteProperty(globalThis, 'fetch'); + } + if (originalPostMessage) { + Object.defineProperty(globalThis, 'postMessage', { + value: originalPostMessage, + configurable: true, + writable: true, + }); + } else { + Reflect.deleteProperty(globalThis, 'postMessage'); + } + } + }); + + it('omits undefined fields from public snapshots', async () => { + const observability = createObservability({ + level: 'verbose', + }); + + emitRemoteStart(observability); + await emitRemoteLoaded(observability); + + const report = observability.getLatestReport(); + const events = observability.getEvents(); + + expect(report).toBeDefined(); + expect(hasUndefinedField(report)).toBe(false); + expect(hasUndefinedField(events)).toBe(false); + }); + + it('records a complete loadRemote event as the final runtime outcome', async () => { + const observability = createObservability({ level: 'verbose' }); + + emitRemoteStart(observability); + await emitRemoteLoaded(observability); + emitRemoteComplete(observability); + + const report = observability.getLatestReport(); + expect(report?.status).toBe('success'); + expect(report?.summary).toMatchObject({ + runtimeLoaded: true, + loadCompleted: true, + componentLoaded: false, + outcome: 'runtime-loaded', + lastPhase: 'loadRemote', + }); + }); + + it('records manifest and remoteEntry lifecycle hooks without observability events', () => { + const observability = createObservability({ + level: 'verbose', + console: false, + }); + + observability.plugin.beforeLoadRemoteSnapshot?.({ + origin: enabledOrigin, + } as any); + observability.plugin.loadSnapshot?.({ + moduleInfo: { + name: 'remote', + entry: 'http://localhost:3001/mf-manifest.json', + }, + } as any); + observability.plugin.loadRemoteSnapshot?.({ + moduleInfo: { + name: 'remote', + entry: 'http://localhost:3001/mf-manifest.json', + }, + manifestUrl: 'http://localhost:3001/mf-manifest.json', + remoteSnapshot: { + version: 'http://localhost:3001/mf-manifest.json', + remoteEntry: 'http://localhost:3001/remoteEntry.js', + }, + from: 'manifest', + } as any); + observability.plugin.loadEntry?.({ + remoteInfo: { + name: 'remote', + entry: 'http://localhost:3001/remoteEntry.js', + }, + origin: enabledOrigin, + } as any); + observability.plugin.afterLoadEntry?.({ + remoteInfo: { + name: 'remote', + entry: 'http://localhost:3001/remoteEntry.js', + }, + origin: enabledOrigin, + } as any); + + expect(observability.getEvents()).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + phase: 'manifest', + status: 'start', + lifecycle: 'loadSnapshot', + }), + expect.objectContaining({ + phase: 'manifest', + status: 'success', + lifecycle: 'loadRemoteSnapshot', + }), + expect.objectContaining({ + phase: 'remoteEntry', + status: 'start', + lifecycle: 'loadEntry', + }), + expect.objectContaining({ + phase: 'remoteEntry', + status: 'success', + lifecycle: 'afterLoadEntry', + }), + ]), + ); + }); + + it('records repeated manifest snapshot loads as cached success', () => { + const observability = createObservability({ + level: 'verbose', + console: false, + }); + const moduleInfo = { + name: 'remote', + entry: 'http://localhost:3001/mf-manifest.json', + }; + + observability.plugin.beforeLoadRemoteSnapshot?.({ + origin: enabledOrigin, + } as any); + observability.plugin.loadSnapshot?.({ + moduleInfo, + } as any); + observability.plugin.loadRemoteSnapshot?.({ + moduleInfo, + manifestUrl: moduleInfo.entry, + remoteSnapshot: { + version: moduleInfo.entry, + remoteEntry: 'http://localhost:3001/remoteEntry.js', + }, + from: 'manifest', + } as any); + + observability.plugin.loadSnapshot?.({ + moduleInfo, + } as any); + + const manifestEvents = observability + .getEvents() + .filter((event) => event.phase === 'manifest'); + + expect(manifestEvents).toEqual([ + expect.objectContaining({ + status: 'start', + message: 'manifest:load-start', + }), + expect.objectContaining({ + status: 'success', + message: 'manifest:resolved', + }), + expect.objectContaining({ + status: 'success', + message: 'manifest:cached', + cached: true, + }), + ]); + expect( + observability.getLatestReport()?.summary.phases.manifest, + ).toMatchObject({ + status: 'success', + cached: true, + }); + }); + + it('records preloadRemote resource results as a successful preload outcome', async () => { + const observability = createObservability({ + level: 'verbose', + console: false, + }); + + const preloadArgs = { + origin: enabledOrigin, + preloadOptions: { + remote: { + name: 'runtime_remote2', + alias: 'dynamic-remote', + entry: 'http://localhost:3007/mf-manifest.json', + }, + preloadConfig: { + nameOrAlias: 'dynamic-remote', + exposes: ['ButtonOldAnt'], + resourceCategory: 'all', + share: true, + depsRemote: true, + }, + }, + remote: { + name: 'runtime_remote2', + alias: 'dynamic-remote', + entry: 'http://localhost:3007/mf-manifest.json', + }, + remoteInfo: { + name: 'runtime_remote2', + alias: 'dynamic-remote', + entry: 'http://localhost:3007/mf-manifest.json', + }, + } as any; + + await observability.plugin.generatePreloadAssets?.(preloadArgs); + await observability.plugin.afterPreloadRemote?.({ + origin: enabledOrigin, + preloadOps: [preloadArgs.preloadOptions.preloadConfig], + results: [ + { + remote: preloadArgs.remote, + remoteInfo: preloadArgs.remoteInfo, + preloadConfig: preloadArgs.preloadOptions.preloadConfig, + id: 'runtime_remote2/ButtonOldAnt', + results: [ + { + url: 'http://localhost:3007/static/ButtonOldAnt.js', + status: 'success', + resourceType: 'js', + initiator: 'preloadRemote', + id: 'runtime_remote2/ButtonOldAnt', + }, + ], + }, + ], + } as any); + + const report = observability.getLatestReport(); + + expect(report).toMatchObject({ + status: 'success', + remote: { + name: 'runtime_remote2', + alias: 'dynamic-remote', + }, + summary: { + preloaded: true, + outcome: 'preloaded', + }, + diagnosis: { + title: 'Remote preloaded successfully', + outcome: 'preloaded', + }, + }); + expect(report?.events).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + phase: 'preload', + status: 'success', + lifecycle: 'afterPreloadRemote', + message: 'preload:js:success', + }), + ]), + ); + }); + + it('records failed preloadRemote resources with resource details', async () => { + const observability = createObservability({ + level: 'verbose', + console: false, + }); + + await observability.plugin.afterPreloadRemote?.({ + origin: enabledOrigin, + results: [ + { + remote: { + name: 'runtime_remote2', + alias: 'dynamic-remote', + entry: 'http://localhost:3007/mf-manifest.json', + }, + remoteInfo: { + name: 'runtime_remote2', + alias: 'dynamic-remote', + entry: 'http://localhost:3007/mf-manifest.json', + }, + preloadConfig: { + nameOrAlias: 'dynamic-remote', + exposes: ['ButtonOldAnt'], + }, + id: 'runtime_remote2/ButtonOldAnt', + results: [ + { + url: 'http://localhost:3007/static/missing.js', + status: 'error', + resourceType: 'js', + initiator: 'preloadRemote', + id: 'runtime_remote2/ButtonOldAnt', + error: new Error('LinkNetworkError: Failed to load link'), + }, + ], + }, + ], + } as any); + + const report = observability.getLatestReport(); + + expect(report).toMatchObject({ + status: 'error', + requestId: 'runtime_remote2/ButtonOldAnt', + summary: { + outcome: 'failed', + }, + errorContext: { + resourceType: 'js', + initiator: 'preloadRemote', + status: 'error', + id: 'runtime_remote2/ButtonOldAnt', + }, + }); + expect(report?.events[0]).toMatchObject({ + phase: 'preload', + status: 'error', + lifecycle: 'afterPreloadRemote', + message: 'preload:js:error', + sanitizedUrl: 'http://localhost:3007/static/missing.js', + }); + }); + + it('keeps manifest, remoteEntry, and runtime load events in the same remote trace', async () => { + const observability = createObservability({ + level: 'verbose', + console: false, + }); + + emitRemoteStart(observability, { + id: '@cloud-public/ai-assistant/AiAssistant', + options: { + remotes: [ + { + name: '@cloud-public/ai-assistant', + entry: 'https://example.com/vmok-manifest.json', + type: 'global', + }, + ], + }, + }); + observability.plugin.beforeLoadRemoteSnapshot?.({ + origin: enabledOrigin, + } as any); + observability.plugin.loadSnapshot?.({ + moduleInfo: { + name: '@cloud-public/ai-assistant', + entry: 'https://example.com/vmok-manifest.json', + }, + } as any); + observability.plugin.loadRemoteSnapshot?.({ + moduleInfo: { + name: '@cloud-public/ai-assistant', + entry: 'https://example.com/vmok-manifest.json', + }, + manifestUrl: 'https://example.com/vmok-manifest.json', + remoteSnapshot: { + version: 'https://example.com/vmok-manifest.json', + remoteEntry: 'https://example.com/remoteEntry.js', + }, + from: 'manifest', + } as any); + observability.plugin.loadEntry?.({ + remoteInfo: { + name: '@cloud-public/ai-assistant', + entry: 'https://example.com/remoteEntry.js', + }, + origin: enabledOrigin, + } as any); + await emitRemoteLoaded(observability, { + id: '@cloud-public/ai-assistant/AiAssistant', + remote: { + name: '@cloud-public/ai-assistant', + entry: 'https://example.com/vmok-manifest.json', + }, + }); + + const reports = observability.getReports(); + expect(reports).toHaveLength(1); + expect(reports[0].status).toBe('success'); + expect(reports[0].runtimeVersion).toBe('2.5.0'); + expect(reports[0].summary.outcome).toBe('runtime-loaded'); + expect(reports[0].diagnosis.title).toBe('Remote loaded successfully'); + expect(reports[0].events.map((event) => event.phase)).toEqual([ + 'loadRemote', + 'manifest', + 'manifest', + 'remoteEntry', + 'loadRemote', + ]); + }); + + it('uses the applied instance version when hook origin version is missing', () => { + const observability = createObservability({ + level: 'verbose', + console: false, + }); + observability.plugin.apply?.({ + version: '2.5.0', + } as any); + + observability.plugin.beforeRequest?.({ + id: 'remote/Button', + origin: { + options: { + name: 'host', + }, + }, + } as any); + + const report = observability.getLatestReport(); + + expect(report?.runtimeVersion).toBe('2.5.0'); + expect(report?.events[0].runtimeVersion).toBe('2.5.0'); + }); + + it('does not return hook args from the default browser entry', () => { + const observability = createObservability({ + level: 'verbose', + console: false, + }); + const beforeRequestArgs = { + id: 'remote/Button', + options: {}, + origin: enabledOrigin, + }; + const afterResolveArgs = { + id: 'remote/Button', + expose: './Button', + remoteInfo: { + name: 'remote', + entry: 'http://localhost:3001/mf-manifest.json', + }, + origin: enabledOrigin, + }; + const loadRemoteSnapshotArgs = { + options: {}, + moduleInfo: { + name: 'remote', + }, + remoteSnapshot: { + remoteEntry: 'http://localhost:3001/remoteEntry.js', + }, + from: 'global', + }; + + expect(observability.plugin.beforeRequest?.(beforeRequestArgs as any)).toBe( + undefined, + ); + expect(observability.plugin.afterResolve?.(afterResolveArgs as any)).toBe( + undefined, + ); + expect( + observability.plugin.loadRemoteSnapshot?.(loadRemoteSnapshotArgs as any), + ).toBe(undefined); + }); + + it('returns original args from chrome remote waterfall hooks for old runtimes', () => { + const plugin = ChromeObservabilityPlugin({ + level: 'verbose', + console: false, + }); + const beforeRequestArgs = { + id: 'remote/Button', + options: {}, + origin: enabledOrigin, + }; + const afterResolveArgs = { + id: 'remote/Button', + expose: './Button', + remoteInfo: { + name: 'remote', + entry: 'http://localhost:3001/mf-manifest.json', + }, + origin: enabledOrigin, + }; + const loadRemoteSnapshotArgs = { + options: {}, + moduleInfo: { + name: 'remote', + }, + remoteSnapshot: { + remoteEntry: 'http://localhost:3001/remoteEntry.js', + }, + from: 'global', + }; + + expect(plugin.beforeRequest?.(beforeRequestArgs as any)).toBe( + beforeRequestArgs, + ); + expect(plugin.afterResolve?.(afterResolveArgs as any)).toBe( + afterResolveArgs, + ); + expect(plugin.loadRemoteSnapshot?.(loadRemoteSnapshotArgs as any)).toBe( + loadRemoteSnapshotArgs, + ); + }); + + it('does not attach global moduleInfo for unrelated runtime failures', () => { + const originalFederation = (globalThis as any).__FEDERATION__; + + try { + (globalThis as any).__FEDERATION__ = { + moduleInfo: { + 'remote:http://localhost:3001/mf-manifest.json': { + publicPath: 'https://cdn.example.com/remote/', + modules: [{ moduleName: 'Button' }], + shared: [{ sharedName: 'react' }], + }, + }, + }; + + const observability = createObservability({ + level: 'verbose', + console: false, + }); + + emitRemoteStart(observability); + emitManifestError(observability, { + error: new Error( + '[ Federation Runtime ]: Failed to get manifest. #RUNTIME-003', + ), + }); + + expect(observability.getLatestReport()?.moduleInfo).toBeUndefined(); + } finally { + if (originalFederation) { + (globalThis as any).__FEDERATION__ = originalFederation; + } else { + delete (globalThis as any).__FEDERATION__; + } + } + }); + + it('attaches clipped global moduleInfo only for snapshot dependent failures', () => { + const originalFederation = (globalThis as any).__FEDERATION__; + + try { + (globalThis as any).__FEDERATION__ = { + moduleInfo: { + 'remote:http://localhost:3001/mf-manifest.json?token=secret#hash': { + publicPath: 'https://cdn.example.com/remote/?v=20260508#hash', + getPublicPath: + 'return "https://cdn.example.com/remote/?v=20260508#hash";', + remoteEntry: + 'https://cdn.example.com/remote/remoteEntry.js?v=20260508#hash', + globalName: 'remote_global', + modules: [{ moduleName: 'Button', assets: { js: ['large.js'] } }], + shared: [{ sharedName: 'react', assets: { js: ['large.js'] } }], + }, + unrelated: { + publicPath: 'https://cdn.example.com/unrelated/', + modules: [{ moduleName: 'Unused' }], + shared: [{ sharedName: 'react' }], + }, + }, + }; + + const observability = createObservability({ + level: 'verbose', + console: false, + }); + + emitRemoteStart(observability); + emitRemoteError(observability, { + error: new Error( + '[ Federation Runtime ]: Failed to get remote snapshot. #RUNTIME-007', + ), + }); + + const report = observability.getLatestReport(); + + expect(report?.ownerHint).toBe('host'); + expect(report?.diagnosis?.title).toBe( + 'Deployment moduleInfo did not match the requested remote', + ); + expect(report?.moduleInfo).toMatchObject({ + reason: 'remote-snapshot', + clipped: true, + totalCount: 2, + matchedCount: 1, + entries: [ + { + name: 'remote:http://localhost:3001/mf-manifest.json?token=secret#hash', + publicPath: 'https://cdn.example.com/remote/?v=20260508#hash', + getPublicPath: + 'return "https://cdn.example.com/remote/?v=20260508#hash";', + remoteEntry: + 'https://cdn.example.com/remote/remoteEntry.js?v=20260508#hash', + globalName: 'remote_global', + }, + ], + }); + expect(report?.diagnosis?.facts).toMatchObject({ + moduleInfoReason: 'remote-snapshot', + moduleInfoTotalCount: 2, + moduleInfoMatchedCount: 1, + moduleInfoNames: + 'remote:http://localhost:3001/mf-manifest.json?token=secret#hash', + }); + expect(report?.diagnosis?.actions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 'check-module-info', + ownerHint: 'host', + }), + ]), + ); + expect(JSON.stringify(report?.moduleInfo)).not.toContain('modules'); + expect(JSON.stringify(report?.moduleInfo)).not.toContain('shared'); + expect(JSON.stringify(report?.moduleInfo)).toContain('secret'); + expect(JSON.stringify(report?.moduleInfo)).toContain('token='); + } finally { + if (originalFederation) { + (globalThis as any).__FEDERATION__ = originalFederation; + } else { + delete (globalThis as any).__FEDERATION__; + } + } + }); + + it('treats a recovered complete event as a recovered runtime outcome', () => { + const observability = createObservability({ + level: 'verbose', + console: false, + }); + + emitRemoteStart(observability); + expect(emitRemoteError(observability)).toBeUndefined(); + emitRemoteComplete(observability, { + error: new Error('remote load failed'), + recovered: true, + }); + + const report = observability.getLatestReport(); + expect(report?.status).toBe('success'); + expect(report?.failedPhase).toBe('loadRemote'); + expect(report?.summary).toMatchObject({ + recovered: true, + runtimeLoaded: true, + loadCompleted: true, + outcome: 'recovered', + error: { + failedPhase: 'loadRemote', + }, + }); + expect(report?.summary.flags).toMatchObject({ + fallback: true, + recovered: true, + }); + }); + + it('keeps summary level compact while preserving phase durations', () => { + const observability = createObservability({ + level: 'summary', + console: false, + }); + + emitRemoteStart(observability); + emitExposePhase(observability, 'beforeGetExpose'); + emitExposePhase(observability, 'afterGetExpose'); + + const report = observability.getLatestReport(); + + expect(report?.events.map((event) => event.status)).toEqual(['success']); + expect(report?.events[0]).toMatchObject({ + phase: 'expose', + status: 'success', + duration: expect.any(Number), + }); + expect(report?.summary.phases.expose).toMatchObject({ + status: 'success', + duration: expect.any(Number), + }); + }); + + it('preserves urls and error message fields for observability', () => { + const observability = createObservability({ + level: 'verbose', + console: false, + }); + + emitManifestError(observability, { + error: new Error( + 'authorization=demo-secret failed at http://localhost:3001/mf-manifest.json?token=demo-secret#hash', + ), + }); + + const output = JSON.stringify(observability.getLatestReport()); + expect(output).toContain( + 'http://localhost:3001/mf-manifest.json?token=demo-secret#hash', + ); + expect(output).toContain('demo-secret'); + expect(output).toContain('token='); + expect(output).toContain('#hash'); + }); + + it('keeps the first specific failed phase when loadRemote closes the trace', () => { + const observability = createObservability({ + level: 'verbose', + console: false, + }); + + emitRemoteStart(observability, { + remote: { + name: 'remote', + }, + }); + emitManifestError(observability, { + id: 'remote/Button', + remote: { + name: 'remote', + }, + error: new Error('manifest failed'), + }); + expect(emitRemoteError(observability)).toBeUndefined(); + emitRemoteComplete(observability, { + remote: { + name: 'remote', + }, + error: new Error('outer loadRemote failed'), + }); + + const report = observability.getLatestReport(); + expect(report?.status).toBe('error'); + expect(report?.failedPhase).toBe('manifest'); + expect(report?.summary).toMatchObject({ + loadCompleted: true, + runtimeLoaded: false, + componentLoaded: false, + outcome: 'failed', + }); + }); + + it('attaches loaded-before evidence from other consumers on remote failures', () => { + const originalFederation = (globalThis as any).__FEDERATION__; + const { currentOrigin, federation } = createLoadedBeforeFederationFixture(); + + try { + (globalThis as any).__FEDERATION__ = federation; + + const observability = createObservability({ + level: 'verbose', + console: false, + }); + + emitRemoteError(observability, { + origin: currentOrigin, + remote: { + name: 'provider', + entryGlobalName: 'provider_global', + entry: 'http://localhost:3001/mf-manifest.json', + }, + expose: './Button', + }); + + const report = observability.getLatestReport(); + expect(report?.loadedBefore).toEqual({ + producer: true, + expose: true, + consumers: [ + { + name: 'consumer_a', + remoteEntryExports: true, + containerInitialized: true, + exposes: ['./Button', './Card'], + }, + ], + }); + expect(report?.events[0].loadedBefore).toEqual(report?.loadedBefore); + expect( + report?.loadedBefore?.consumers.some( + (consumer) => consumer.name === 'consumer_b', + ), + ).toBe(false); + expect(hasUndefinedField(report)).toBe(false); + } finally { + if (originalFederation) { + (globalThis as any).__FEDERATION__ = originalFederation; + } else { + delete (globalThis as any).__FEDERATION__; + } + } + }); + + it('attaches loaded-before evidence on successful verbose development remote loads', async () => { + const originalFederation = (globalThis as any).__FEDERATION__; + const { currentOrigin, federation } = createLoadedBeforeFederationFixture(); + + try { + (globalThis as any).__FEDERATION__ = federation; + + const observability = createObservability({ + level: 'verbose', + console: false, + browser: { + enabled: true, + mode: 'development', + }, + }); + + await emitRemoteLoaded(observability, { + origin: currentOrigin, + remote: { + name: 'provider', + entryGlobalName: 'provider_global', + entry: 'http://localhost:3001/mf-manifest.json', + }, + expose: './Button', + }); + + const report = observability.getLatestReport(); + expect(report?.status).toBe('success'); + expect(report?.loadedBefore).toEqual({ + producer: true, + expose: true, + consumers: [ + { + name: 'consumer_a', + remoteEntryExports: true, + containerInitialized: true, + exposes: ['./Button', './Card'], + }, + ], + }); + } finally { + if (originalFederation) { + (globalThis as any).__FEDERATION__ = originalFederation; + } else { + delete (globalThis as any).__FEDERATION__; + } + } + }); + + it('does not attach loaded-before evidence on successful production remote loads', async () => { + const originalFederation = (globalThis as any).__FEDERATION__; + const { currentOrigin, federation } = createLoadedBeforeFederationFixture(); + + try { + (globalThis as any).__FEDERATION__ = federation; + + const observability = createObservability({ + level: 'verbose', + console: false, + browser: { + enabled: true, + mode: 'production', + }, + }); + + await emitRemoteLoaded(observability, { + origin: currentOrigin, + remote: { + name: 'provider', + entryGlobalName: 'provider_global', + entry: 'http://localhost:3001/mf-manifest.json', + }, + expose: './Button', + }); + + expect(observability.getLatestReport()?.status).toBe('success'); + expect(observability.getLatestReport()?.loadedBefore).toBeUndefined(); + } finally { + if (originalFederation) { + (globalThis as any).__FEDERATION__ = originalFederation; + } else { + delete (globalThis as any).__FEDERATION__; + } + } + }); + + it('omits loaded-before evidence when no existing producer cache matches', () => { + const originalFederation = (globalThis as any).__FEDERATION__; + + try { + (globalThis as any).__FEDERATION__ = { + __INSTANCES__: [ + { + options: { + name: 'consumer_a', + }, + moduleCache: new Map([ + [ + 'other', + { + remoteInfo: { + name: 'other', + entryGlobalName: 'other_global', + }, + }, + ], + ]), + }, + ], + }; + + const observability = createObservability({ + level: 'verbose', + console: false, + }); + + emitRemoteError(observability, { + remote: { + name: 'provider', + entryGlobalName: 'provider_global', + }, + expose: './Button', + }); + + expect(observability.getLatestReport()?.loadedBefore).toBeUndefined(); + expect( + observability.getLatestReport()?.events[0].loadedBefore, + ).toBeUndefined(); + } finally { + if (originalFederation) { + (globalThis as any).__FEDERATION__ = originalFederation; + } else { + delete (globalThis as any).__FEDERATION__; + } + } + }); + + it('records runtime error code and manifest context for RUNTIME-003', () => { + const observability = createObservability({ + level: 'verbose', + console: false, + }); + const error = new Error( + [ + '[ Federation Runtime ]: Failed to get manifest. #RUNTIME-003', + 'args: {"manifestUrl":"http://localhost:3001/mf-manifest.json?token=demo-secret#hash","moduleName":"remote"}', + 'Original Error Message:', + ' NetworkError: token=demo-secret failed', + ].join('\n'), + ); + + emitRemoteStart(observability); + emitManifestError(observability, { error }); + + const report = observability.getLatestReport(); + const event = report?.events.find((item) => item.phase === 'manifest'); + const output = JSON.stringify(report); + + expect(event).toMatchObject({ + status: 'error', + errorCode: 'RUNTIME-003', + ownerHint: 'host', + retryable: true, + sanitizedUrl: + 'http://localhost:3001/mf-manifest.json?token=demo-secret#hash', + errorContext: expect.objectContaining({ + lifecycle: 'afterResolve', + remoteName: 'remote', + url: 'http://localhost:3001/mf-manifest.json?token=demo-secret#hash', + }), + }); + expect(report).toMatchObject({ + status: 'error', + failedPhase: 'manifest', + errorCode: 'RUNTIME-003', + ownerHint: 'host', + retryable: true, + summary: { + error: expect.objectContaining({ + errorCode: 'RUNTIME-003', + failedPhase: 'manifest', + lifecycle: 'afterResolve', + ownerHint: 'host', + retryable: true, + }), + }, + diagnosis: { + title: 'Manifest could not be loaded', + status: 'error', + outcome: 'failed', + ownerHint: 'host', + failedPhase: 'manifest', + errorCode: 'RUNTIME-003', + docLink: expect.stringContaining('/guide/troubleshooting/runtime'), + facts: expect.objectContaining({ + errorCode: 'RUNTIME-003', + failedPhase: 'manifest', + remoteName: 'remote', + url: 'http://localhost:3001/mf-manifest.json?token=demo-secret#hash', + retryable: true, + }), + actions: [ + expect.objectContaining({ + id: 'check-manifest-url', + ownerHint: 'host', + }), + expect.objectContaining({ + id: 'check-network', + ownerHint: 'network', + }), + ], + }, + }); + expect(output).toContain('demo-secret'); + expect(output).toContain('token='); + expect(output).toContain('#hash'); + }); + + it('records host remote summary for RUNTIME-004 match failures', () => { + const observability = createObservability({ + level: 'verbose', + console: false, + }); + + emitRemoteStart(observability, { + id: 'missing/Button', + }); + observability.plugin.afterMatchRemote?.({ + id: 'missing/Button', + options: { + name: 'host', + remotes: [ + { + name: 'remote', + alias: 'known-remote', + entry: 'http://localhost:3001/mf-manifest.json', + }, + ], + }, + origin: enabledOrigin, + error: new Error( + '[ Federation Runtime ]: Failed to locate remote. #RUNTIME-004', + ), + } as any); + + const report = observability.getLatestReport(); + + expect(report).toMatchObject({ + status: 'error', + failedPhase: 'matchRemote', + requestId: 'missing/Button', + errorCode: 'RUNTIME-004', + ownerHint: 'host', + retryable: false, + errorContext: expect.objectContaining({ + requestId: 'missing/Button', + hostRemotes: 'known-remote', + }), + diagnosis: { + title: 'Remote was not found in host remotes', + ownerHint: 'host', + errorCode: 'RUNTIME-004', + facts: expect.objectContaining({ + requestId: 'missing/Button', + hostRemotes: 'known-remote', + }), + actions: [ + expect.objectContaining({ + id: 'check-host-remotes', + ownerHint: 'host', + }), + ], + }, + }); + }); + + it('records remoteEntry global context for RUNTIME-001', () => { + const observability = createObservability({ + level: 'verbose', + console: false, + }); + + observability.plugin.loadEntry?.({ + remoteInfo: { + name: 'remote', + entry: 'http://localhost:3001/remoteEntry.js?token=demo-secret', + entryGlobalName: 'remote_global', + type: 'global', + }, + origin: enabledOrigin, + } as any); + observability.plugin.afterLoadEntry?.({ + remoteInfo: { + name: 'remote', + entry: 'http://localhost:3001/remoteEntry.js?token=demo-secret', + entryGlobalName: 'remote_global', + type: 'global', + }, + origin: enabledOrigin, + error: new Error( + '[ Federation Runtime ]: Failed to get remoteEntry exports. #RUNTIME-001', + ), + } as any); + + const report = observability.getLatestReport(); + const output = JSON.stringify(report); + + expect(report).toMatchObject({ + status: 'error', + failedPhase: 'remoteEntry', + errorCode: 'RUNTIME-001', + ownerHint: 'remote', + retryable: false, + remote: { + name: 'remote', + entry: 'http://localhost:3001/remoteEntry.js?token=demo-secret', + entryGlobalName: 'remote_global', + type: 'global', + }, + errorContext: expect.objectContaining({ + remoteName: 'remote', + entryGlobalName: 'remote_global', + url: 'http://localhost:3001/remoteEntry.js?token=demo-secret', + }), + diagnosis: { + title: 'Remote entry global was not registered', + ownerHint: 'remote', + errorCode: 'RUNTIME-001', + facts: expect.objectContaining({ + remoteName: 'remote', + remoteEntry: 'http://localhost:3001/remoteEntry.js?token=demo-secret', + entryGlobalName: 'remote_global', + }), + actions: [ + expect.objectContaining({ + id: 'check-remote-global', + ownerHint: 'remote', + }), + expect.objectContaining({ + id: 'check-remote-entry', + ownerHint: 'remote', + }), + ], + }, + }); + expect(output).toContain('demo-secret'); + expect(output).toContain('token='); + }); + + it('classifies RUNTIME-008 remoteEntry resource failures', () => { + const createReportForError = (error: Error) => { + const observability = createObservability({ + level: 'verbose', + console: false, + }); + + observability.plugin.afterLoadEntry?.({ + remoteInfo: { + name: 'remote', + entry: 'http://localhost:3001/remoteEntry.js', + }, + origin: enabledOrigin, + error, + } as any); + + return observability.getLatestReport(); + }; + + const networkReport = createReportForError( + new Error( + '[ Federation Runtime ]: Failed to load script resources. #RUNTIME-008\nOriginal Error Message:\n ScriptNetworkError: 404', + ), + ); + const timeoutReport = createReportForError( + new Error( + '[ Federation Runtime ]: Failed to load script resources. #RUNTIME-008\nOriginal Error Message:\n timeout loading remoteEntry', + ), + ); + const executionReport = createReportForError( + new Error( + '[ Federation Runtime ]: Failed to load script resources. #RUNTIME-008\nOriginal Error Message:\n ScriptExecutionError: boom', + ), + ); + + expect(networkReport).toMatchObject({ + errorCode: 'RUNTIME-008', + ownerHint: 'network', + retryable: true, + errorContext: expect.objectContaining({ + resourceErrorType: 'network', + }), + diagnosis: { + title: 'Remote entry failed because of a network error', + ownerHint: 'network', + facts: expect.objectContaining({ + resourceErrorType: 'network', + }), + actions: [ + expect.objectContaining({ + id: 'check-network', + ownerHint: 'network', + }), + expect.objectContaining({ + id: 'check-remote-entry', + ownerHint: 'network', + }), + ], + }, + }); + expect(timeoutReport).toMatchObject({ + errorCode: 'RUNTIME-008', + ownerHint: 'network', + retryable: true, + errorContext: expect.objectContaining({ + resourceErrorType: 'timeout', + }), + diagnosis: { + title: 'Remote entry request timed out', + facts: expect.objectContaining({ + resourceErrorType: 'timeout', + }), + }, + }); + expect(executionReport).toMatchObject({ + errorCode: 'RUNTIME-008', + ownerHint: 'remote', + retryable: false, + errorContext: expect.objectContaining({ + resourceErrorType: 'script-execution', + }), + diagnosis: { + title: 'Remote entry loaded but failed during execution', + ownerHint: 'remote', + facts: expect.objectContaining({ + resourceErrorType: 'script-execution', + }), + actions: [ + expect.objectContaining({ + id: 'check-remote-entry', + ownerHint: 'remote', + }), + ], + }, + }); + }); + + it('records detailed remote phases and keeps expose as the failed phase', () => { + const observability = createObservability({ + level: 'verbose', + console: false, + }); + const exposeError = new Error('remote expose missing'); + + emitRemoteStart(observability); + emitRemoteMatch(observability); + observability.plugin.beforeInitRemote?.({ + id: 'remote/Button', + remoteInfo: { + name: 'remote', + entry: 'http://localhost:3001/mf-manifest.json', + }, + origin: enabledOrigin, + } as any); + emitRemoteInit(observability); + emitExposePhase(observability, 'beforeGetExpose'); + emitExposePhase(observability, 'afterGetExpose', { + error: exposeError, + }); + expect( + emitRemoteError(observability, { error: exposeError }), + ).toBeUndefined(); + emitRemoteComplete(observability, { + error: exposeError, + }); + + const report = observability.getLatestReport(); + const phases = report?.events.map((event) => event.phase); + const exposeEvent = report?.events.find( + (event) => event.lifecycle === 'afterGetExpose', + ); + + expect(report?.status).toBe('error'); + expect(report?.failedPhase).toBe('expose'); + expect(phases).toEqual( + expect.arrayContaining([ + 'matchRemote', + 'remoteEntryInit', + 'expose', + 'loadRemote', + ]), + ); + expect(exposeEvent).toMatchObject({ + status: 'error', + requestId: 'remote/Button', + expose: './Button', + errorMessage: 'remote expose missing', + duration: expect.any(Number), + }); + }); + + it('records factory execution success with a phase duration', () => { + const observability = createObservability({ + level: 'verbose', + console: false, + }); + + emitRemoteStart(observability); + emitFactoryPhase(observability, 'beforeExecuteFactory'); + emitFactoryPhase(observability, 'afterExecuteFactory'); + + const report = observability.getLatestReport(); + const factoryEvent = report?.events.find( + (event) => event.lifecycle === 'afterExecuteFactory', + ); + + expect(report?.status).toBe('success'); + expect(factoryEvent).toMatchObject({ + phase: 'moduleFactory', + status: 'success', + duration: expect.any(Number), + }); + expect(report?.summary.phases.moduleFactory).toMatchObject({ + status: 'success', + duration: expect.any(Number), + }); + }); + + it('reads recent reports, filters by observability fields, and exports copies', async () => { + const observability = createObservability({ + level: 'verbose', + console: false, + }); + + vi.useFakeTimers(); + try { + vi.setSystemTime(1000); + await emitRemoteLoaded(observability); + const buttonTraceId = observability.getLatestReport()?.traceId || ''; + + vi.setSystemTime(2000); + await emitRemoteLoaded(observability, { + id: 'catalog/Card', + expose: './Card', + remote: { + name: 'catalog', + alias: 'catalogRemote', + entry: 'http://localhost:3002/mf-manifest.json?env=dev', + }, + }); + const cardTraceId = observability.getLatestReport()?.traceId || ''; + + vi.setSystemTime(3000); + observability.plugin.errorLoadShare?.({ + pkgName: 'react', + shareInfo: createShared({ + shareConfig: { + requiredVersion: '^99.0.0', + singleton: true, + strictVersion: true, + eager: false, + }, + }), + shared: { + react: [ + createShared({ + version: '18.3.1', + }), + ], + }, + lifecycle: 'loadShare', + origin: enabledOrigin, + error: new Error('react version mismatch'), + } as any); + + expect(observability.getReports()).toHaveLength(3); + expect( + observability.getReports({ limit: 2 }).map((report) => report.traceId), + ).toEqual([observability.getLatestReport()?.traceId, cardTraceId]); + + expect(observability.findReports({ remote: 'catalog' })).toHaveLength(1); + expect( + observability.findReports({ remote: 'catalogRemote' })[0], + ).toMatchObject({ + traceId: cardTraceId, + expose: './Card', + }); + expect(observability.findReports({ expose: 'Card' })[0]?.traceId).toBe( + cardTraceId, + ); + expect(observability.findReports({ shared: 'react' })[0]).toMatchObject({ + status: 'error', + shared: { + name: 'react', + }, + }); + expect(observability.findReports({ status: 'error' })).toHaveLength(1); + expect(observability.findReports({ outcome: 'failed' })).toHaveLength(1); + + const exported = observability.exportReport(buttonTraceId); + expect(exported?.traceId).toBe(buttonTraceId); + exported?.events.pop(); + expect(observability.getReport(buttonTraceId)?.events).toHaveLength(1); + expect(observability.exportReport()?.traceId).toBe( + observability.getLatestReport()?.traceId, + ); + } finally { + vi.useRealTimers(); + } + }); + + it('summarizes successful manifest, remoteEntry, cache, and runtime recovery facts', () => { + const observability = createObservability({ + level: 'verbose', + console: false, + }); + + observability.plugin.beforeLoadRemoteSnapshot?.({ + origin: enabledOrigin, + } as any); + observability.plugin.loadSnapshot?.({ + moduleInfo: { + name: 'remote', + entry: 'http://localhost:3001/mf-manifest.json', + }, + } as any); + observability.plugin.loadRemoteSnapshot?.({ + moduleInfo: { + name: 'remote', + entry: 'http://localhost:3001/mf-manifest.json', + }, + manifestUrl: 'http://localhost:3001/mf-manifest.json', + remoteSnapshot: { + version: 'http://localhost:3001/mf-manifest.json', + remoteEntry: 'http://localhost:3001/remoteEntry.js', + }, + from: 'manifest', + } as any); + observability.plugin.loadEntry?.({ + remoteInfo: { + name: 'remote', + entry: 'http://localhost:3001/remoteEntry.js', + }, + origin: enabledOrigin, + } as any); + observability.plugin.afterLoadEntry?.({ + remoteInfo: { + name: 'remote', + entry: 'http://localhost:3001/remoteEntry.js', + }, + origin: enabledOrigin, + recovered: true, + } as any); + + const report = observability.getLatestReport(); + + expect(report?.summary.phases.manifest).toMatchObject({ + status: 'success', + duration: expect.any(Number), + }); + expect(report?.summary.phases.remoteEntry).toMatchObject({ + status: 'success', + duration: expect.any(Number), + recovered: true, + }); + expect(report?.summary.flags).toMatchObject({ + recovered: true, + }); + + observability.plugin.beforeLoadRemoteSnapshot?.({ + origin: enabledOrigin, + } as any); + observability.plugin.loadSnapshot?.({ + moduleInfo: { + name: 'remote', + entry: 'http://localhost:3001/mf-manifest.json', + }, + } as any); + observability.plugin.loadRemoteSnapshot?.({ + moduleInfo: { + name: 'remote', + entry: 'http://localhost:3001/mf-manifest.json', + }, + manifestUrl: 'http://localhost:3001/mf-manifest.json', + remoteSnapshot: { + version: 'http://localhost:3001/mf-manifest.json', + remoteEntry: 'http://localhost:3001/remoteEntry.js', + }, + from: 'manifest', + } as any); + observability.plugin.loadEntry?.({ + remoteInfo: { + name: 'remote', + entry: 'http://localhost:3001/remoteEntry.js', + }, + origin: enabledOrigin, + } as any); + observability.plugin.afterLoadEntry?.({ + remoteInfo: { + name: 'remote', + entry: 'http://localhost:3001/remoteEntry.js', + }, + origin: enabledOrigin, + } as any); + + const cachedReport = observability.getLatestReport(); + + expect(cachedReport?.summary.flags.cached).toBe(true); + expect(cachedReport?.summary.phases.manifest.cached).toBe(true); + expect(cachedReport?.summary.phases.remoteEntry.cached).toBe(true); + }); + + it('records shared dependency observability', () => { + const observability = createObservability({ + level: 'verbose', + console: false, + }); + + observability.plugin.errorLoadShare?.({ + pkgName: 'react', + shareInfo: { + version: '18.3.1', + from: 'remote', + scope: ['default'], + strategy: 'loaded-first', + shareConfig: { + requiredVersion: '^99.0.0', + singleton: true, + strictVersion: true, + eager: false, + }, + }, + shared: {}, + shareScopeMap: { + default: { + react: { + '18.3.1': createShared({ + from: 'host?token=demo-secret', + }), + }, + }, + }, + lifecycle: 'loadShare', + origin: enabledOrigin, + error: new Error('token=demo-secret shared failed'), + recovered: true, + } as any); + + const report = observability.getLatestReport(); + const output = JSON.stringify(report); + + expect(report?.status).toBe('error'); + expect(report?.failedPhase).toBe('shared'); + expect(report?.shared).toMatchObject({ + name: 'react', + shareScope: ['default'], + requiredVersion: '^99.0.0', + availableVersions: ['18.3.1'], + reason: 'version-mismatch', + }); + expect(output).toContain('demo-secret'); + expect(output).toContain('token='); + }); + + it('skips shared observability in compatibility mode for unsupported runtime versions and returns hook args', () => { + const observability = createObservability( + { + level: 'verbose', + console: false, + }, + { + guardSharedHooksByRuntimeVersion: true, + returnHookArgs: true, + }, + ); + const sharedArgs = { + pkgName: 'react', + shareInfo: createShared(), + shared: {}, + origin: { + version: '2.4.9', + options: { + name: 'host', + }, + }, + }; + const previewArgs = { + ...sharedArgs, + origin: { + version: '0.0.0-feat-federationdiagnosticerror-20260512025420', + options: { + name: 'host', + }, + }, + }; + const afterArgs = { + ...sharedArgs, + selectedShared: createShared(), + }; + const errorArgs = { + ...sharedArgs, + error: new Error('shared failed'), + }; + + expect(observability.plugin.beforeLoadShare?.(sharedArgs as any)).toBe( + sharedArgs, + ); + expect(observability.plugin.afterLoadShare?.(afterArgs as any)).toBe( + afterArgs, + ); + expect(observability.plugin.errorLoadShare?.(errorArgs as any)).toBe( + errorArgs, + ); + expect(observability.plugin.beforeLoadShare?.(previewArgs as any)).toBe( + previewArgs, + ); + expect(observability.getReports()).toHaveLength(0); + }); + + it('uses the applied instance version for shared compatibility checks', () => { + const observability = createObservability( + { + level: 'verbose', + console: false, + }, + { + guardSharedHooksByRuntimeVersion: true, + returnHookArgs: true, + }, + ); + observability.plugin.apply?.({ + version: '2.5.0', + } as any); + const sharedArgs = { + pkgName: 'react', + shareInfo: createShared(), + shared: {}, + origin: { + options: { + name: 'host', + }, + }, + }; + + expect(observability.plugin.beforeLoadShare?.(sharedArgs as any)).toBe( + sharedArgs, + ); + + const report = observability.getLatestReport(); + + expect(report).toMatchObject({ + runtimeVersion: '2.5.0', + summary: { + phases: { + shared: { + status: 'start', + }, + }, + }, + }); + expect(report?.events[0]).toMatchObject({ + phase: 'shared', + runtimeVersion: '2.5.0', + }); + }); + + it('records shared observability by default without runtime version gating', () => { + const observability = createObservability({ + level: 'verbose', + console: false, + }); + const sharedArgs = { + pkgName: 'react', + shareInfo: createShared(), + shared: {}, + origin: { + version: '2.4.9', + options: { + name: 'host', + }, + }, + }; + + observability.plugin.beforeLoadShare?.(sharedArgs as any); + observability.plugin.afterLoadShare?.({ + ...sharedArgs, + selectedShared: createShared(), + } as any); + + expect(observability.getLatestReport()).toMatchObject({ + status: 'success', + summary: { + shared: { + name: 'react', + }, + }, + }); + }); + + it('derives shared success details from loadShare hooks', () => { + const observability = createObservability({ + level: 'verbose', + console: false, + }); + const shared = createShared(); + + observability.plugin.beforeLoadShare?.({ + pkgName: 'react', + shareInfo: shared, + shared: {}, + origin: enabledOrigin, + }); + observability.plugin.afterLoadShare?.({ + pkgName: 'react', + shareInfo: shared, + selectedShared: shared, + shared: {}, + shareScopeMap: { + default: { + react: { + '18.3.1': shared, + }, + }, + }, + lifecycle: 'loadShare', + origin: enabledOrigin, + }); + + const report = observability.getLatestReport(); + + expect(report?.status).toBe('success'); + expect(report?.summary.outcome).toBe('shared-resolved'); + expect(report?.summary.sharedResolved).toBe(true); + expect(report?.diagnosis?.title).toBe( + 'Shared dependency resolved successfully', + ); + expect(report?.shared).toMatchObject({ + name: 'react', + selectedVersion: '18.3.1', + provider: 'host', + availableVersions: ['18.3.1'], + }); + expect(report?.summary.shared).toMatchObject({ + name: 'react', + selectedVersion: '18.3.1', + provider: 'host', + shareScope: ['default'], + }); + }); + + it('records custom shared info misses as handled recovery', () => { + const observability = createObservability({ + level: 'verbose', + console: false, + }); + const requestedShared = createShared({ + version: '99.0.0', + from: 'remote', + shareConfig: { + requiredVersion: '^99.0.0', + singleton: false, + strictVersion: true, + eager: false, + }, + }); + const hostShared = createShared(); + + observability.plugin.beforeLoadShare?.({ + pkgName: 'react', + shareInfo: requestedShared, + shared: {}, + origin: enabledOrigin, + }); + observability.plugin.errorLoadShare?.({ + pkgName: 'react', + shareInfo: requestedShared, + shared: {}, + shareScopeMap: { + default: { + react: { + '18.3.1': hostShared, + }, + }, + }, + lifecycle: 'loadShare', + origin: enabledOrigin, + recovered: true, + }); + + const report = observability.getLatestReport(); + + expect(report?.status).toBe('success'); + expect(report?.failedPhase).toBeUndefined(); + expect(report?.summary.outcome).toBe('recovered'); + expect(report?.summary.flags.recovered).toBe(true); + expect(report?.summary.phases.shared).toMatchObject({ + status: 'complete', + recovered: true, + }); + expect(report?.shared).toMatchObject({ + name: 'react', + requiredVersion: '^99.0.0', + availableVersions: ['18.3.1'], + reason: 'custom-share-info-unmatched', + }); + expect(report?.events.at(-1)).toMatchObject({ + phase: 'shared', + status: 'complete', + message: 'shared:custom-share-info-unmatched', + recovered: true, + }); + expect(report?.summary.error).toBeUndefined(); + }); + + it('derives eager boundary details from loadShareSync failures', () => { + const observability = createObservability({ + level: 'verbose', + console: false, + }); + const asyncShared = createShared({ + version: '1.0.0', + from: 'remote', + shareConfig: { + requiredVersion: '^1.0.0', + singleton: false, + strictVersion: false, + eager: false, + }, + get: () => Promise.resolve(() => ({ value: 'async' })), + }); + + observability.plugin.errorLoadShare?.({ + pkgName: 'observability-async-shared', + shareInfo: asyncShared, + shared: {}, + shareScopeMap: {}, + lifecycle: 'loadShareSync', + origin: enabledOrigin, + error: new Error('[ Federation Runtime ]: RUNTIME-005 shared failed'), + }); + + const report = observability.getLatestReport(); + + expect(report?.status).toBe('error'); + expect(report?.shared).toMatchObject({ + name: 'observability-async-shared', + requiredVersion: '^1.0.0', + reason: 'sync-async-boundary', + }); + expect(report?.diagnosis).toMatchObject({ + title: 'Shared dependency could not be resolved', + ownerHint: 'shared', + errorCode: 'RUNTIME-005', + facts: expect.objectContaining({ + shareName: 'observability-async-shared', + requiredVersion: '^1.0.0', + eager: false, + sharedReason: 'sync-async-boundary', + }), + actions: [ + expect.objectContaining({ + id: 'check-shared-provider', + ownerHint: 'shared', + }), + expect.objectContaining({ + id: 'check-shared-version', + ownerHint: 'shared', + }), + expect.objectContaining({ + id: 'check-eager-config', + ownerHint: 'shared', + }), + ], + }); + expect(report?.events.at(-1)?.message).toBe('shared:sync-async-boundary'); + }); + + it('reports pure runtime shared failures with available shared versions', () => { + const observability = createObservability({ + level: 'verbose', + console: false, + }); + + observability.plugin.errorLoadShare?.({ + pkgName: 'react', + shareInfo: createShared({ + version: '18.0.0', + shareConfig: { + requiredVersion: '^18.0.0', + singleton: true, + strictVersion: true, + eager: true, + }, + get: undefined, + }), + shared: {}, + shareScopeMap: { + default: { + react: { + '17.0.2': createShared({ + version: '17.0.2', + from: 'host', + }), + }, + }, + }, + lifecycle: 'loadShareSync', + origin: enabledOrigin, + error: new Error('[ Federation Runtime ]: RUNTIME-006 shared failed'), + }); + + const report = observability.getLatestReport(); + + expect(report?.diagnosis).toMatchObject({ + title: 'Shared dependency could not be resolved', + ownerHint: 'shared', + errorCode: 'RUNTIME-006', + facts: expect.objectContaining({ + shareName: 'react', + requiredVersion: '^18.0.0', + availableVersions: '17.0.2', + eager: true, + sharedReason: 'version-mismatch', + }), + actions: [ + expect.objectContaining({ + id: 'check-shared-provider', + ownerHint: 'shared', + }), + expect.objectContaining({ + id: 'check-shared-version', + ownerHint: 'shared', + }), + expect.objectContaining({ + id: 'check-eager-config', + ownerHint: 'shared', + }), + ], + }); + }); + + it('prints a minimal console hint with traceId on error', () => { + const observability = createObservability({ level: 'verbose' }); + const error = vi + .spyOn(console, 'error') + .mockImplementation(() => undefined); + + try { + emitManifestError(observability, { + error: new Error( + '[ Federation Runtime ]: Failed to get manifest. #RUNTIME-003', + ), + }); + + emitManifestError(observability, { + error: new Error('token=demo-secret failed again'), + }); + + expect(error).toHaveBeenCalledTimes(1); + const output = String(error.mock.calls[0]?.[0]); + expect(output).toContain( + '[Module Federation] Observability report generated', + ); + expect(output).toContain('traceId: mf-'); + expect(output).toContain('phase: manifest'); + expect(output).toContain('errorCode:'); + expect(output).toContain('read: enable browser output or use onReport'); + expect(output).toContain('demo-secret'); + expect(output).toContain('token='); + expect(output).toContain('#hash'); + expect(output).not.toContain('rawStack:'); + expect(output).not.toContain('/Users/bytedance/private'); + } finally { + error.mockRestore(); + } + }); + + it('prints only traceId and errorCode for browser production console hints', () => { + const observability = createObservability({ + level: 'verbose', + browser: { + enabled: true, + mode: 'production', + scope: 'runtime_host', + }, + }); + const error = vi + .spyOn(console, 'error') + .mockImplementation(() => undefined); + + try { + emitManifestError(observability, { + error: new Error( + '[ Federation Runtime ]: Failed to get manifest. #RUNTIME-003', + ), + }); + + expect(error).toHaveBeenCalledTimes(1); + const output = String(error.mock.calls[0]?.[0]); + expect(output).toContain( + '[Module Federation] Observability report generated', + ); + expect(output).toContain('traceId: mf-'); + expect(output).toContain('errorCode: RUNTIME-003'); + expect(output).not.toContain('phase:'); + expect(output).not.toContain('requestId:'); + expect(output).not.toContain('read:'); + expect(output).not.toContain('rawStack:'); + expect(output).not.toContain('demo-secret'); + expect(output).not.toContain('mf-manifest.json'); + } finally { + error.mockRestore(); + } + }); + + it('prints start console hints by default in development mode', () => { + const originalFederation = ( + globalThis as { + __FEDERATION__?: Record; + } + ).__FEDERATION__; + const info = vi.spyOn(console, 'info').mockImplementation(() => undefined); + + try { + ( + globalThis as { + __FEDERATION__?: Record; + } + ).__FEDERATION__ = {}; + const observability = createObservability({ + browser: { + enabled: true, + scope: 'runtime_host', + }, + }); + + emitRemoteStart(observability); + observability.plugin.beforeLoadShare?.({ + pkgName: 'react', + shareInfo: createShared(), + shared: {}, + origin: enabledOrigin, + }); + + expect(info).toHaveBeenCalledTimes(2); + const remoteOutput = String(info.mock.calls[0]?.[0]); + const sharedOutput = String(info.mock.calls[1]?.[0]); + + expect(remoteOutput).toContain( + '[Module Federation] Observability trace started', + ); + expect(remoteOutput).toContain('traceId: mf-'); + expect(remoteOutput).toContain('phase: loadRemote'); + expect(remoteOutput).toContain('requestId: remote/Button'); + expect(remoteOutput).toContain( + 'read: window.__FEDERATION__.__OBSERVABILITY__["runtime_host"].getReport', + ); + + expect(sharedOutput).toContain('phase: shared'); + expect(sharedOutput).toContain('requestId: shared:react'); + expect(sharedOutput).toContain('shared: react'); + expect(sharedOutput).toContain('lifecycle: loadShare'); + + const reports = observability.getReports({ limit: 10 }); + expect(reports).toHaveLength(2); + expect(reports.map((report) => report.status)).toEqual([ + 'pending', + 'pending', + ]); + } finally { + ( + globalThis as { + __FEDERATION__?: Record; + } + ).__FEDERATION__ = originalFederation; + info.mockRestore(); + } + }); + + it('requires explicit start console hints in production mode', () => { + const originalFederation = ( + globalThis as { + __FEDERATION__?: Record; + } + ).__FEDERATION__; + const info = vi.spyOn(console, 'info').mockImplementation(() => undefined); + + try { + ( + globalThis as { + __FEDERATION__?: Record; + } + ).__FEDERATION__ = {}; + + const productionDefault = createObservability({ + browser: { + enabled: true, + scope: 'runtime_host', + mode: 'production', + }, + }); + + emitRemoteStart(productionDefault); + productionDefault.plugin.beforeLoadShare?.({ + pkgName: 'react', + shareInfo: createShared(), + shared: {}, + origin: enabledOrigin, + }); + + expect(info).not.toHaveBeenCalled(); + expect(productionDefault.getReports({ limit: 10 })).toHaveLength(0); + + const productionExplicit = createObservability({ + browser: { + enabled: true, + scope: 'runtime_host', + mode: 'production', + }, + trace: { + printStart: true, + }, + }); + + emitRemoteStart(productionExplicit); + productionExplicit.plugin.beforeLoadShare?.({ + pkgName: 'react', + shareInfo: createShared(), + shared: {}, + origin: enabledOrigin, + }); + + expect(info).toHaveBeenCalledTimes(2); + expect(productionExplicit.getReports({ limit: 10 })).toHaveLength(2); + } finally { + ( + globalThis as { + __FEDERATION__?: Record; + } + ).__FEDERATION__ = originalFederation; + info.mockRestore(); + } + }); + + it('stores the original error stack by default', () => { + const observability = createObservability({ + level: 'verbose', + console: false, + }); + const error = new Error('token=demo-secret manifest failed'); + error.stack = [ + 'Error: token=demo-secret manifest failed', + ' at load (/Users/bytedance/private/app.ts:1:2)', + ' at fetch (http://localhost:3001/mf-manifest.json?token=demo-secret#hash:1:2)', + ' at extra (/tmp/secret.ts:1:1)', + ' at more (/private/var/tmp/more.ts:1:1)', + ' at ignored (/home/demo/ignored.ts:1:1)', + ].join('\n'); + + emitManifestError(observability, { error }); + + const report = observability.getLatestReport(); + const stack = report?.errorStack || ''; + + expect(stack).toContain('Error: token=demo-secret manifest failed'); + expect(stack).toContain('/Users/bytedance/private/app.ts'); + expect(stack).toContain( + 'http://localhost:3001/mf-manifest.json?token=demo-secret#hash', + ); + expect(stack.split('\n')).toHaveLength(6); + expect(stack).toContain('demo-secret'); + expect(stack).toContain('token='); + expect(stack).toContain('#hash'); + }); + + it('prints the raw stack only when explicitly allowed', () => { + const observability = createObservability({ + level: 'verbose', + printRawStack: true, + }); + const errorSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => undefined); + const error = new Error('raw stack enabled'); + error.stack = [ + 'Error: raw stack enabled', + ' at raw (/Users/bytedance/raw.ts:1:1)', + ].join('\n'); + + try { + emitManifestError(observability, { error }); + + const output = String(errorSpy.mock.calls[0]?.[0]); + expect(output).toContain('rawStack:'); + expect(output).toContain('/Users/bytedance/raw.ts'); + } finally { + errorSpy.mockRestore(); + } + }); + + it('passes raw errors to explicit callbacks without affecting loading', () => { + const rawError = new Error('raw callback'); + const onRawError = vi.fn(() => { + throw new Error('ignored callback failure'); + }); + const observability = createObservability({ + level: 'verbose', + console: false, + onRawError, + }); + + expect(() => + emitManifestError(observability, { error: rawError }), + ).not.toThrow(); + + expect(onRawError).toHaveBeenCalledTimes(1); + expect(onRawError).toHaveBeenCalledWith( + rawError, + expect.objectContaining({ + origin: enabledOrigin, + event: expect.objectContaining({ + errorMessage: 'raw callback', + errorStack: expect.any(String), + }), + report: expect.objectContaining({ + status: 'error', + errorStack: expect.any(String), + }), + }), + ); + expect(observability.getLatestReport()?.status).toBe('error'); + }); + + it('exposes a browser reader only when plugin options allow it', async () => { + const originalFederation = ( + globalThis as { + __FEDERATION__?: { + __OBSERVABILITY__?: Record; + }; + } + ).__FEDERATION__; + + try { + ( + globalThis as { + __FEDERATION__?: { + __OBSERVABILITY__?: Record; + }; + } + ).__FEDERATION__ = {}; + + const observability = createObservability({ + level: 'verbose', + browser: { + enabled: true, + scope: 'host', + }, + }); + + await emitRemoteLoaded(observability, { + origin: { + options: { + name: 'host', + }, + }, + }); + + const reader = ( + globalThis as { + __FEDERATION__?: { + __OBSERVABILITY__?: Record< + string, + { + getLatestReport(): { traceId: string; events: unknown[] }; + getReport(traceId: string): { events: unknown[] } | undefined; + getReports(options?: { + limit?: number; + }): { events: unknown[] }[]; + findReports(query?: { + remote?: string; + }): { events: unknown[] }[]; + exportReport( + traceId?: string, + ): { events: unknown[] } | undefined; + getTraceIds(): string[]; + clear?: unknown; + } + >; + }; + } + ).__FEDERATION__?.__OBSERVABILITY__?.host; + + expect(reader).toBeDefined(); + expect(reader?.clear).toBeUndefined(); + expect(reader?.getTraceIds()).toHaveLength(1); + + const latestReport = reader?.getLatestReport(); + expect(latestReport?.traceId).toBeDefined(); + latestReport?.events.pop(); + + expect(reader?.getLatestReport().events).toHaveLength(1); + expect( + reader?.getReport(latestReport?.traceId || '')?.events, + ).toHaveLength(1); + expect(reader?.getReports({ limit: 1 })).toHaveLength(1); + expect(reader?.findReports({ remote: 'remote' })).toHaveLength(1); + expect( + reader?.exportReport(latestReport?.traceId || '')?.events, + ).toHaveLength(1); + } finally { + if (originalFederation) { + ( + globalThis as { + __FEDERATION__?: { + __OBSERVABILITY__?: Record; + }; + } + ).__FEDERATION__ = originalFederation; + } else { + delete ( + globalThis as { + __FEDERATION__?: { + __OBSERVABILITY__?: Record; + }; + } + ).__FEDERATION__; + } + } + }); + + it('does not expose the browser reader by default', async () => { + const originalFederation = ( + globalThis as { + __FEDERATION__?: { + __OBSERVABILITY__?: Record; + }; + } + ).__FEDERATION__; + + try { + ( + globalThis as { + __FEDERATION__?: { + __OBSERVABILITY__?: Record; + }; + } + ).__FEDERATION__ = {}; + + const observability = createObservability({ level: 'verbose' }); + + await emitRemoteLoaded(observability); + + expect( + ( + globalThis as { + __FEDERATION__?: { + __OBSERVABILITY__?: Record; + }; + } + ).__FEDERATION__?.__OBSERVABILITY__, + ).toBeUndefined(); + } finally { + if (originalFederation) { + ( + globalThis as { + __FEDERATION__?: { + __OBSERVABILITY__?: Record; + }; + } + ).__FEDERATION__ = originalFederation; + } else { + delete ( + globalThis as { + __FEDERATION__?: { + __OBSERVABILITY__?: Record; + }; + } + ).__FEDERATION__; + } + } + }); + + it('does not write Node observability files by default', async () => { + const directory = fs.mkdtempSync( + path.join(os.tmpdir(), 'mf-observability-'), + ); + const latestFile = path.join(directory, 'latest.json'); + const eventsFile = path.join(directory, 'events.jsonl'); + const observability = ObservabilityNode({ + level: 'verbose', + console: false, + directory, + }); + + try { + emitManifestError(observability); + await new Promise((resolve) => setTimeout(resolve, 20)); + + expect(fs.existsSync(latestFile)).toBe(false); + expect(fs.existsSync(eventsFile)).toBe(false); + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } + }); + + it('prints raw stacks from the Node entry only when explicitly allowed', () => { + const observability = ObservabilityNode({ + level: 'verbose', + printRawStack: true, + }); + const errorSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => undefined); + const error = new Error('node raw stack enabled'); + error.stack = [ + 'Error: node raw stack enabled', + ' at raw (/Users/bytedance/node-raw.ts:1:1)', + ].join('\n'); + + try { + emitManifestError(observability, { error }); + + const output = String(errorSpy.mock.calls[0]?.[0]); + expect(output).toContain('rawStack:'); + expect(output).toContain('/Users/bytedance/node-raw.ts'); + } finally { + errorSpy.mockRestore(); + } + }); + + it('writes Node observability files only when plugin options allow it', async () => { + const directory = fs.mkdtempSync( + path.join(os.tmpdir(), 'mf-observability-'), + ); + const latestFile = path.join(directory, 'latest.json'); + const eventsFile = path.join(directory, 'events.jsonl'); + const originalNonWebpackRequire = ( + globalThis as { + __non_webpack_require__?: (id: string) => unknown; + } + ).__non_webpack_require__; + + try { + ( + globalThis as { + __non_webpack_require__?: (id: string) => unknown; + } + ).__non_webpack_require__ = (id: string) => { + if (id === 'node:fs' || id === 'fs') { + return fs; + } + + if (id === 'node:path' || id === 'path') { + return path; + } + + throw new Error(`Unsupported module: ${id}`); + }; + + const observability = ObservabilityNode({ + level: 'verbose', + console: false, + fileOutput: true, + directory, + }); + + emitManifestError(observability, { + origin: { + options: { + name: 'host', + }, + }, + error: new Error('token=demo-secret manifest failed'), + }); + + await waitForFile(latestFile); + await waitForFile(eventsFile); + + const latest = fs.readFileSync(latestFile, 'utf8'); + const eventsOutput = fs.readFileSync(eventsFile, 'utf8'); + + expect(latest).toContain('"status": "error"'); + expect(eventsOutput).toContain('"phase":"manifest"'); + expect(`${latest}\n${eventsOutput}`).toContain('demo-secret'); + expect(`${latest}\n${eventsOutput}`).toContain('token='); + expect(`${latest}\n${eventsOutput}`).toContain('#hash'); + } finally { + if (originalNonWebpackRequire) { + ( + globalThis as { + __non_webpack_require__?: (id: string) => unknown; + } + ).__non_webpack_require__ = originalNonWebpackRequire; + } else { + delete ( + globalThis as { + __non_webpack_require__?: (id: string) => unknown; + } + ).__non_webpack_require__; + } + fs.rmSync(directory, { recursive: true, force: true }); + } + }); + + it('lets business code mark component loaded after runtime observability starts', async () => { + const observability = createObservability({ level: 'verbose' }); + + await emitRemoteLoaded(observability); + + const componentEvent = observability.markComponentLoaded({ + requestId: 'remote/Button', + componentName: 'RemoteButton', + metadata: { + route: '/account?token=demo-secret', + renderMs: 12, + ready: true, + token: 'demo-secret', + }, + }); + const report = observability.getLatestReport(); + + expect(componentEvent?.phase).toBe('component'); + expect(componentEvent?.eventName).toBe('component:business-loaded'); + expect(componentEvent?.source).toBe('business'); + expect(componentEvent?.componentName).toBe('RemoteButton'); + expect(componentEvent?.metadata).toMatchObject({ + route: '/account?token=demo-secret', + renderMs: 12, + ready: true, + token: 'demo-secret', + }); + expect(report?.summary.componentLoaded).toBe(true); + expect(report?.summary.outcome).toBe('component-loaded'); + expect(report?.diagnosis).toMatchObject({ + title: 'Business component loaded', + status: 'success', + outcome: 'component-loaded', + facts: expect.objectContaining({ + componentName: 'RemoteButton', + componentLoaded: true, + }), + }); + expect(JSON.stringify(report)).toContain('component:business-loaded'); + expect(JSON.stringify(report)).toContain('demo-secret'); + expect(JSON.stringify(report)).toContain('token='); + }); + + it('does not let observability callbacks affect loading', async () => { + const observability = createObservability({ + level: 'verbose', + onEvent: vi.fn(() => { + throw new Error('onEvent failed'); + }), + onReport: vi.fn(() => { + throw new Error('onReport failed'); + }), + }); + + await expect(emitRemoteLoaded(observability)).resolves.toBeUndefined(); + + expect(observability.getLatestReport()?.status).toBe('success'); + }); + + it('does not let observability callbacks affect error collection', () => { + const observability = createObservability({ + level: 'verbose', + console: false, + onEvent: vi.fn(() => { + throw new Error('onEvent failed'); + }), + onReport: vi.fn(() => { + throw new Error('onReport failed'); + }), + onRawError: vi.fn(() => { + throw new Error('onRawError failed'); + }), + }); + + expect(() => + emitRemoteError(observability, { + error: new Error( + '[ Federation Runtime ]: Failed to load script resources. #RUNTIME-008', + ), + }), + ).not.toThrow(); + + expect(observability.getLatestReport()).toMatchObject({ + status: 'error', + errorCode: 'RUNTIME-008', + }); + }); +}); diff --git a/packages/observability-plugin/package.json b/packages/observability-plugin/package.json new file mode 100644 index 00000000000..f552a672720 --- /dev/null +++ b/packages/observability-plugin/package.json @@ -0,0 +1,79 @@ +{ + "name": "@module-federation/observability-plugin", + "version": "2.5.0", + "author": "module-federation", + "main": "./dist/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/index.d.ts", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/module-federation/core.git", + "directory": "packages/observability-plugin" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "dist/", + "README.md" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/esm/index.js", + "require": "./dist/index.js" + }, + "./browser": { + "types": "./dist/browser.d.ts", + "import": "./dist/esm/browser.js", + "require": "./dist/browser.js" + }, + "./chrome-devtool": { + "types": "./dist/chrome-devtool.d.ts", + "import": "./dist/esm/chrome-devtool.js", + "require": "./dist/chrome-devtool.js" + }, + "./node": { + "types": "./dist/node.d.ts", + "import": "./dist/esm/node.js", + "require": "./dist/node.js" + }, + "./build": { + "types": "./dist/build.d.ts", + "import": "./dist/esm/build.js", + "require": "./dist/build.js" + } + }, + "typesVersions": { + "*": { + ".": [ + "./dist/index.d.ts" + ], + "browser": [ + "./dist/browser.d.ts" + ], + "chrome-devtool": [ + "./dist/chrome-devtool.d.ts" + ], + "node": [ + "./dist/node.d.ts" + ], + "build": [ + "./dist/build.d.ts" + ] + } + }, + "devDependencies": { + "@module-federation/runtime": "workspace:*" + }, + "dependencies": { + "@module-federation/sdk": "workspace:*" + }, + "scripts": { + "build": "tsdown --config tsdown.config.ts && cp *.md dist", + "test": "vitest run -u -c vitest.config.ts", + "lint": "ESLINT_USE_FLAT_CONFIG=false pnpm exec eslint --ignore-pattern node_modules \"**/*.ts\" \"package.json\"", + "pre-release": "pnpm run test && pnpm run build" + } +} diff --git a/packages/observability-plugin/src/browser.ts b/packages/observability-plugin/src/browser.ts new file mode 100644 index 00000000000..1ec4268d135 --- /dev/null +++ b/packages/observability-plugin/src/browser.ts @@ -0,0 +1,12 @@ +import type { ObservabilityRuntimePlugin } from './core'; +import { createObservability, type ObservabilityPluginOptions } from './core'; + +export type { ObservabilityPluginOptions } from './core'; + +export function ObservabilityPlugin( + options: ObservabilityPluginOptions = {}, +): ObservabilityRuntimePlugin { + return createObservability(options).plugin; +} + +export default ObservabilityPlugin; diff --git a/packages/observability-plugin/src/build.ts b/packages/observability-plugin/src/build.ts new file mode 100644 index 00000000000..5bb32218eb6 --- /dev/null +++ b/packages/observability-plugin/src/build.ts @@ -0,0 +1,1550 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { simpleJoinRemoteEntry } from '@module-federation/sdk'; + +export type ObservabilityBuildInfoSource = 'config' | 'stats' | 'manifest'; +export type ObservabilityBuildPublicPathMode = + | 'auto' + | 'static' + | 'runtime-getter' + | 'unknown'; +export type ObservabilityBuildReportPhase = + | 'compilation' + | 'observability-output'; +export type ObservabilityBuildOwnerHint = + | 'host' + | 'remote' + | 'shared' + | 'build' + | 'unknown'; +export type ObservabilityBuildMetadata = Record< + string, + string | number | boolean +>; + +export interface ObservabilityBuildRemote { + name?: string; + alias?: string; + entry?: string; + type?: string; + shareScope?: string | string[]; +} + +export interface ObservabilityBuildExpose { + name: string; +} + +export interface ObservabilityBuildShared { + name: string; + shareScope?: string | string[]; + version?: string; + requiredVersion?: string | false; + singleton?: boolean; + strictVersion?: boolean; + eager?: boolean; +} + +export interface ObservabilityBuildInfo { + schemaVersion: 1; + generatedAt: string; + source: ObservabilityBuildInfoSource; + bundler: { + name: string; + version?: string; + mode?: string; + target?: string[]; + }; + moduleFederation: { + name?: string; + pluginVersion?: string; + buildVersion?: string; + buildName?: string; + remoteEntry?: { + name?: string; + type?: string; + globalName?: string; + publicPath?: string; + publicPathMode: ObservabilityBuildPublicPathMode; + }; + options: { + shareStrategy?: string; + shareScope?: string | string[]; + asyncStartup?: boolean; + manifest?: boolean | string; + dts?: boolean; + }; + remotes: ObservabilityBuildRemote[]; + exposes: ObservabilityBuildExpose[]; + shared: ObservabilityBuildShared[]; + }; + summary: { + remoteCount: number; + exposeCount: number; + sharedCount: number; + }; +} + +export interface ObservabilityBuildErrorSummary { + errorCode?: string; + errorName?: string; + errorMessage?: string; + failedPhase: ObservabilityBuildReportPhase; + ownerHint: ObservabilityBuildOwnerHint; + retryable: false; + context?: Record; +} + +export interface ObservabilityBuildAction { + id: string; + ownerHint?: ObservabilityBuildOwnerHint; + title: string; + detail?: string; +} + +export interface ObservabilityBuildFactReport { + title: string; + outcome: 'failed'; + status: 'error'; + ownerHint: ObservabilityBuildOwnerHint; + failedPhase: ObservabilityBuildReportPhase; + errorCode?: string; + errorName?: string; + errorMessage?: string; + facts: ObservabilityBuildMetadata; + completedPhases: string[]; + pendingPhases: string[]; + actions: ObservabilityBuildAction[]; +} + +export interface ObservabilityBuildEvent extends ObservabilityBuildErrorSummary { + traceId: string; + timestamp: number; + phase: 'build'; + status: 'error'; + lifecycle: ObservabilityBuildReportPhase; + errorStack?: string; +} + +export interface ObservabilityBuildReport { + schemaVersion: 1; + traceId: string; + source: 'build'; + status: 'error'; + startedAt: number; + updatedAt: number; + duration: number; + failedPhase: ObservabilityBuildReportPhase; + build: ObservabilityBuildInfo; + events: ObservabilityBuildEvent[]; + summary: { + eventCount: number; + outcome: 'failed'; + error?: ObservabilityBuildErrorSummary; + errors: ObservabilityBuildErrorSummary[]; + }; + diagnosis: ObservabilityBuildFactReport; +} + +export interface CreateObservabilityBuildInfoOptions { + moduleFederation?: unknown; + manifest?: unknown; + stats?: unknown; + compilerOptions?: Record; + bundler?: string; + bundlerVersion?: string; + pluginVersion?: string; + generatedAt?: string; +} + +export interface ObservabilityBuildPluginOptions { + enabled?: boolean; + outputFile?: string; + errorReport?: + | false + | { + outputFile?: string; + }; + cwd?: string; + bundler?: string; + bundlerVersion?: string; + pluginVersion?: string; + moduleFederation?: unknown; +} + +interface CompilerLike { + context?: string; + options?: Record; + webpack?: { + version?: string; + rspackVersion?: string; + sources?: { + RawSource?: new (value: string) => unknown; + }; + Compilation?: { + PROCESS_ASSETS_STAGE_REPORT?: number; + PROCESS_ASSETS_STAGE_SUMMARIZE?: number; + }; + }; + hooks?: { + thisCompilation?: { + tap(name: string, callback: (compilation: CompilationLike) => void): void; + }; + }; + getInfrastructureLogger?: (name: string) => { + warn?: (message: string) => void; + }; +} + +interface CompilationLike { + constructor?: { + PROCESS_ASSETS_STAGE_REPORT?: number; + PROCESS_ASSETS_STAGE_SUMMARIZE?: number; + }; + hooks?: { + processAssets?: { + tapPromise( + options: { name: string; stage?: number }, + callback: () => Promise, + ): void; + }; + }; + getAsset?: (name: string) => + | { + source?: unknown; + } + | undefined; + emitAsset?: (name: string, source: unknown) => void; + assets?: Record; + errors?: unknown[]; +} + +const PLUGIN_NAME = 'ObservabilityBuildPlugin'; +const DEFAULT_OUTPUT_FILE = '.mf/observability/build-info.json'; +const DEFAULT_REPORT_FILE = '.mf/observability/build-report.json'; +const DEFAULT_MANIFEST_FILE = 'mf-manifest.json'; +const DEFAULT_STATS_FILE = 'mf-stats.json'; +const ERROR_CODE_PATTERN = /\b(?:RUNTIME|TYPE|BUILD)-\d{3}\b/; +const MAX_BUILD_FACT_KEYS = 50; +const SENSITIVE_PAIR_PATTERN = + /\b(token|authorization|cookie|secret|password|session|access_token|refresh_token|api_key|apikey|key)\s*[:=]\s*([^&\s'",;<>]+)/gi; +const URL_PATTERN = /https?:\/\/[^\s'"<>]+/g; +const ABSOLUTE_PATH_PATTERN = + /(?:file:\/\/)?(?:\/(?:Users|private|var|tmp|home|workspace|opt|usr)\/[^\s)]+|[A-Za-z]:\\[^\s)]+)/g; + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +function getRecord(value: unknown): Record | undefined { + return isRecord(value) ? value : undefined; +} + +function getString(value: unknown): string | undefined { + return typeof value === 'string' && value ? value : undefined; +} + +function getBoolean(value: unknown): boolean | undefined { + return typeof value === 'boolean' ? value : undefined; +} + +function sanitizeText(value: unknown, maxLength = 240): string | undefined { + if (value === undefined || value === null) { + return undefined; + } + + const sanitized = String(value) + .replace(URL_PATTERN, (url) => sanitizeUrl(url) || '[redacted-url]') + .replace(ABSOLUTE_PATH_PATTERN, '[redacted-path]') + .replace(SENSITIVE_PAIR_PATTERN, '[redacted]'); + + return sanitized.length > maxLength + ? `${sanitized.slice(0, maxLength)}...` + : sanitized; +} + +function getRawText(value: unknown): string | undefined { + if (value === undefined || value === null) { + return undefined; + } + + return String(value); +} + +function clipText(value: unknown, maxLength = 320): string | undefined { + if (value === undefined || value === null) { + return undefined; + } + + const sanitized = String(value); + + return sanitized.length > maxLength + ? `${sanitized.slice(0, maxLength)}...` + : sanitized; +} + +function sanitizeStack(value: unknown): string | undefined { + const stack = getString(value); + if (!stack) { + return undefined; + } + + return stack; +} + +function extractErrorCode(value: unknown): string | undefined { + return sanitizeText(String(value ?? '').match(ERROR_CODE_PATTERN)?.[0], 40); +} + +function sanitizeUrl(value: string | undefined): string | undefined { + if (!value) { + return undefined; + } + + try { + const parsedUrl = new URL(value, 'http://localhost'); + const sanitized = `${parsedUrl.origin}${parsedUrl.pathname}`; + + return /^https?:\/\//i.test(value) ? sanitized : parsedUrl.pathname; + } catch { + const [withoutHash] = value.split('#'); + const [withoutQuery] = withoutHash.split('?'); + return sanitizeText(withoutQuery, 320); + } +} + +function sanitizeRemoteEntry(value: unknown): string | undefined { + const raw = getString(value); + if (!raw) { + return undefined; + } + + const atIndex = raw.lastIndexOf('@'); + if (atIndex > 0) { + const remoteName = sanitizeText(raw.slice(0, atIndex), 120); + const entry = clipText(raw.slice(atIndex + 1), 320); + if (remoteName && entry) { + return `${remoteName}@${entry}`; + } + return entry || remoteName; + } + + return clipText(raw, 320); +} + +function sanitizePublicPath(value: unknown): string | undefined { + const raw = getString(value); + if (!raw) { + return undefined; + } + + return clipText(raw, 320); +} + +function getSanitizedString(value: unknown, maxLength = 160) { + return sanitizeText(value, maxLength); +} + +function normalizeStringArray(value: unknown): string[] | undefined { + if (!Array.isArray(value)) { + const sanitized = getSanitizedString(value); + return sanitized ? [sanitized] : undefined; + } + + const values = value + .map((item) => getSanitizedString(item)) + .filter((item): item is string => Boolean(item)); + + return values.length ? values.slice(0, 20) : undefined; +} + +function normalizeShareScope(value: unknown): string | string[] | undefined { + if (Array.isArray(value)) { + const scopes = normalizeStringArray(value); + return scopes?.length ? scopes : undefined; + } + + return getSanitizedString(value); +} + +function normalizeRequiredVersion(value: unknown): string | false | undefined { + if (value === false) { + return false; + } + + return getSanitizedString(value, 120); +} + +function normalizeManifestOption(value: unknown): boolean | string | undefined { + if (typeof value === 'boolean') { + return value; + } + + const manifestOptions = getRecord(value); + if (!manifestOptions) { + return undefined; + } + + return getSanitizedString(manifestOptions['fileName'], 120) || true; +} + +function getManifestFileName(manifestOption: unknown) { + const manifestOptions = getRecord(manifestOption); + const filePath = getString(manifestOptions?.['filePath']) || ''; + const fileName = getString(manifestOptions?.['fileName']); + const manifestFile = fileName + ? addJsonExtension(fileName) + : DEFAULT_MANIFEST_FILE; + const statsFile = fileName + ? insertSuffix(addJsonExtension(fileName), '-stats') + : DEFAULT_STATS_FILE; + + return { + manifestFileName: simpleJoinRemoteEntry(filePath, manifestFile), + statsFileName: simpleJoinRemoteEntry(filePath, statsFile), + }; +} + +function addJsonExtension(fileName: string) { + return /\.json$/i.test(fileName) ? fileName : `${fileName}.json`; +} + +function insertSuffix(fileName: string, suffix: string) { + return fileName.replace(/\.json$/i, `${suffix}.json`); +} + +function getSourceText(source: unknown): string | undefined { + if (source === undefined || source === null) { + return undefined; + } + + if (typeof source === 'string') { + return source; + } + + if (typeof source === 'function') { + try { + return getSourceText(source()); + } catch { + return undefined; + } + } + + const sourceRecord = getRecord(source); + const sourceFn = sourceRecord?.['source']; + if (typeof sourceFn === 'function') { + try { + return getSourceText(sourceFn.call(source)); + } catch { + return undefined; + } + } + + return String(source); +} + +function readAssetText( + compilation: CompilationLike, + fileName: string, +): string | undefined { + const asset = compilation.getAsset?.(fileName); + const assetSource = asset ? getSourceText(asset.source) : undefined; + if (assetSource !== undefined) { + return assetSource; + } + + const legacyAsset = compilation.assets?.[fileName]; + return getSourceText(legacyAsset); +} + +function readJsonAsset( + compilation: CompilationLike, + fileName: string, +): Record | undefined { + const text = readAssetText(compilation, fileName); + if (!text) { + return undefined; + } + + try { + return getRecord(JSON.parse(text)); + } catch { + return undefined; + } +} + +function getRemoteParts(value: unknown) { + const raw = getString(value); + if (!raw) { + return {}; + } + + const atIndex = raw.lastIndexOf('@'); + if (atIndex > 0) { + return { + name: getSanitizedString(raw.slice(0, atIndex), 120), + entry: sanitizeRemoteEntry(raw.slice(atIndex + 1)), + }; + } + + return { + entry: sanitizeRemoteEntry(raw), + }; +} + +function normalizeRemoteFromConfig( + alias: string | undefined, + value: unknown, +): ObservabilityBuildRemote | undefined { + const remote: ObservabilityBuildRemote = { + alias: getSanitizedString(alias, 120), + }; + + if (typeof value === 'string') { + const parts = getRemoteParts(value); + remote.name = parts.name || remote.alias; + remote.entry = parts.entry; + return remote.name || remote.entry ? remote : undefined; + } + + if (Array.isArray(value)) { + const firstValue = value.find( + (item) => typeof item === 'string' || isRecord(item), + ); + return normalizeRemoteFromConfig(alias, firstValue); + } + + const remoteOptions = getRecord(value); + if (!remoteOptions) { + return remote.alias ? remote : undefined; + } + + const entryValue = + remoteOptions['entry'] || + remoteOptions['external'] || + remoteOptions['version']; + const parts = getRemoteParts(entryValue); + + remote.name = + getSanitizedString(remoteOptions['name'], 120) || + getSanitizedString(remoteOptions['federationContainerName'], 120) || + parts.name || + remote.alias; + remote.alias = + getSanitizedString(remoteOptions['alias'], 120) || remote.alias; + remote.entry = parts.entry || sanitizeRemoteEntry(entryValue); + remote.type = getSanitizedString(remoteOptions['type'], 80); + remote.shareScope = normalizeShareScope(remoteOptions['shareScope']); + + return remote.name || remote.entry ? remote : undefined; +} + +function normalizeRemoteFromManifest( + value: unknown, +): ObservabilityBuildRemote | undefined { + const remoteOptions = getRecord(value); + if (!remoteOptions) { + return undefined; + } + + const entryValue = remoteOptions['entry'] || remoteOptions['version']; + const remote: ObservabilityBuildRemote = { + name: + getSanitizedString(remoteOptions['moduleName'], 120) || + getSanitizedString(remoteOptions['name'], 120) || + getSanitizedString(remoteOptions['federationContainerName'], 120), + alias: getSanitizedString(remoteOptions['alias'], 120), + entry: sanitizeRemoteEntry(entryValue), + type: getSanitizedString(remoteOptions['type'], 80), + shareScope: normalizeShareScope(remoteOptions['shareScope']), + }; + + return remote.name || remote.entry ? remote : undefined; +} + +function normalizeConfigRemotes(value: unknown): ObservabilityBuildRemote[] { + if (Array.isArray(value)) { + return value + .map((item) => normalizeRemoteFromConfig(undefined, item)) + .filter((item): item is ObservabilityBuildRemote => Boolean(item)); + } + + const remotes = getRecord(value); + if (!remotes) { + return []; + } + + return Object.entries(remotes) + .map(([alias, remote]) => normalizeRemoteFromConfig(alias, remote)) + .filter((remote): remote is ObservabilityBuildRemote => Boolean(remote)); +} + +function normalizeManifestRemotes(value: unknown): ObservabilityBuildRemote[] { + return (Array.isArray(value) ? value : []) + .map(normalizeRemoteFromManifest) + .filter((remote): remote is ObservabilityBuildRemote => Boolean(remote)); +} + +function normalizeConfigExposes(value: unknown): ObservabilityBuildExpose[] { + if (Array.isArray(value)) { + return value + .map((item) => + typeof item === 'string' + ? getSanitizedString(item, 160) + : getSanitizedString(getRecord(item)?.['name'], 160), + ) + .filter((name): name is string => Boolean(name)) + .map((name) => ({ name })); + } + + const exposes = getRecord(value); + if (!exposes) { + return []; + } + + return Object.keys(exposes) + .map((name) => getSanitizedString(name, 160)) + .filter((name): name is string => Boolean(name)) + .map((name) => ({ name })); +} + +function normalizeManifestExposes(value: unknown): ObservabilityBuildExpose[] { + return (Array.isArray(value) ? value : []) + .map((item) => { + const expose = getRecord(item); + return ( + getSanitizedString(expose?.['name'], 160) || + getSanitizedString(expose?.['id'], 160) + ); + }) + .filter((name): name is string => Boolean(name)) + .map((name) => ({ name })); +} + +function normalizeSharedFromRecord( + name: string | undefined, + value: unknown, +): ObservabilityBuildShared | undefined { + const sharedOptions = getRecord(value); + if (!sharedOptions) { + const sharedName = + name || + (typeof value === 'string' ? getSanitizedString(value, 160) : undefined); + return sharedName ? { name: sharedName } : undefined; + } + + const sharedName = + getSanitizedString(sharedOptions['name'], 160) || + getSanitizedString(sharedOptions['shareKey'], 160) || + name; + + if (!sharedName) { + return undefined; + } + + return { + name: sharedName, + shareScope: normalizeShareScope(sharedOptions['shareScope']), + version: getSanitizedString(sharedOptions['version'], 120), + requiredVersion: normalizeRequiredVersion(sharedOptions['requiredVersion']), + singleton: getBoolean(sharedOptions['singleton']), + strictVersion: getBoolean(sharedOptions['strictVersion']), + eager: getBoolean(sharedOptions['eager']), + }; +} + +function normalizeConfigShared(value: unknown): ObservabilityBuildShared[] { + if (Array.isArray(value)) { + return value + .map((item) => normalizeSharedFromRecord(undefined, item)) + .filter((shared): shared is ObservabilityBuildShared => Boolean(shared)); + } + + const shared = getRecord(value); + if (!shared) { + return []; + } + + return Object.entries(shared) + .map(([name, sharedOptions]) => + normalizeSharedFromRecord(getSanitizedString(name, 160), sharedOptions), + ) + .filter((item): item is ObservabilityBuildShared => Boolean(item)); +} + +function normalizeManifestShared(value: unknown): ObservabilityBuildShared[] { + return (Array.isArray(value) ? value : []) + .map((item) => { + const sharedOptions = getRecord(item); + return normalizeSharedFromRecord( + getSanitizedString(sharedOptions?.['name'], 160) || + getSanitizedString(sharedOptions?.['id'], 160), + sharedOptions, + ); + }) + .filter((shared): shared is ObservabilityBuildShared => Boolean(shared)); +} + +function getPublicPathInfo( + metaData: Record | undefined, + compilerOptions: Record | undefined, +) { + const outputOptions = getRecord(compilerOptions?.['output']); + const getPublicPath = metaData?.['getPublicPath']; + const publicPath = metaData?.['publicPath'] || outputOptions?.['publicPath']; + + if (getPublicPath) { + return { + mode: 'runtime-getter' as const, + value: undefined, + }; + } + + const sanitizedPublicPath = sanitizePublicPath(publicPath); + if (publicPath === 'auto') { + return { + mode: 'auto' as const, + value: 'auto', + }; + } + + if (sanitizedPublicPath) { + return { + mode: 'static' as const, + value: sanitizedPublicPath, + }; + } + + return { + mode: 'unknown' as const, + value: undefined, + }; +} + +function normalizeTarget(value: unknown): string[] | undefined { + return normalizeStringArray(value); +} + +function getManifestSource( + manifest: Record | undefined, + stats: Record | undefined, +): Record | undefined { + return manifest || stats; +} + +function getMetaData( + manifest: Record | undefined, + stats: Record | undefined, +) { + return ( + getRecord(manifest?.['metaData']) || + getRecord(stats?.['metaData']) || + undefined + ); +} + +function getBuildInfo(metaData: Record | undefined) { + return getRecord(metaData?.['buildInfo']); +} + +function getSource( + manifest: Record | undefined, + stats: Record | undefined, +): ObservabilityBuildInfoSource { + if (manifest) { + return 'manifest'; + } + if (stats) { + return 'stats'; + } + return 'config'; +} + +function getConfigOptions(moduleFederation: unknown) { + return getRecord(moduleFederation) || {}; +} + +function getRemoteEntryType( + moduleFederation: Record, + metaData: Record | undefined, +) { + const remoteEntry = getRecord(metaData?.['remoteEntry']); + const library = getRecord(moduleFederation['library']); + + return ( + getSanitizedString(remoteEntry?.['type'], 80) || + getSanitizedString(library?.['type'], 80) + ); +} + +function getRemoteEntryName( + moduleFederation: Record, + metaData: Record | undefined, +) { + const remoteEntry = getRecord(metaData?.['remoteEntry']); + + return ( + getSanitizedString(remoteEntry?.['name'], 160) || + getSanitizedString(moduleFederation['filename'], 160) + ); +} + +function getGlobalName( + moduleFederation: Record, + metaData: Record | undefined, +) { + const library = getRecord(moduleFederation['library']); + + return ( + getSanitizedString(metaData?.['globalName'], 160) || + getSanitizedString(library?.['name'], 160) + ); +} + +function getBundlerName( + inputBundler: string | undefined, + compilerOptions: Record | undefined, +) { + return ( + getSanitizedString(inputBundler, 80) || + getSanitizedString(compilerOptions?.['name'], 80) || + 'unknown' + ); +} + +function getBundlerVersion( + inputVersion: string | undefined, + compilerOptions: Record | undefined, +) { + return ( + getSanitizedString(inputVersion, 80) || + getSanitizedString(compilerOptions?.['webpackVersion'], 80) + ); +} + +function getDtsOption(value: unknown) { + if (value === false) { + return false; + } + if (value === undefined) { + return undefined; + } + return true; +} + +function getAsyncStartup(moduleFederation: Record) { + const experiments = getRecord(moduleFederation['experiments']); + return getBoolean(experiments?.['asyncStartup']); +} + +function uniqueByName(items: T[]): T[] { + const seen = new Set(); + return items.filter((item) => { + const key = item.name || JSON.stringify(item); + if (seen.has(key)) { + return false; + } + seen.add(key); + return true; + }); +} + +export function createObservabilityBuildInfo( + input: CreateObservabilityBuildInfoOptions, +): ObservabilityBuildInfo { + const moduleFederation = getConfigOptions(input.moduleFederation); + const manifest = getRecord(input.manifest); + const stats = getRecord(input.stats); + const manifestSource = getManifestSource(manifest, stats); + const metaData = getMetaData(manifest, stats); + const buildInfo = getBuildInfo(metaData); + const publicPath = getPublicPathInfo(metaData, input.compilerOptions); + const manifestRemotes = normalizeManifestRemotes(manifestSource?.['remotes']); + const manifestExposes = normalizeManifestExposes(manifestSource?.['exposes']); + const manifestShared = normalizeManifestShared(manifestSource?.['shared']); + const remotes = uniqueByName( + manifestRemotes.length + ? manifestRemotes + : normalizeConfigRemotes(moduleFederation['remotes']), + ); + const exposes = uniqueByName( + manifestExposes.length + ? manifestExposes + : normalizeConfigExposes(moduleFederation['exposes']), + ); + const shared = uniqueByName( + manifestShared.length + ? manifestShared + : normalizeConfigShared(moduleFederation['shared']), + ); + + return { + schemaVersion: 1, + generatedAt: input.generatedAt || new Date().toISOString(), + source: getSource(manifest, stats), + bundler: { + name: getBundlerName(input.bundler, input.compilerOptions), + version: getBundlerVersion(input.bundlerVersion, input.compilerOptions), + mode: getSanitizedString(input.compilerOptions?.['mode'], 80), + target: normalizeTarget(input.compilerOptions?.['target']), + }, + moduleFederation: { + name: + getSanitizedString(manifestSource?.['name'], 160) || + getSanitizedString(metaData?.['name'], 160) || + getSanitizedString(moduleFederation['name'], 160), + pluginVersion: + getSanitizedString(metaData?.['pluginVersion'], 80) || + getSanitizedString(input.pluginVersion, 80), + buildVersion: getSanitizedString(buildInfo?.['buildVersion'], 160), + buildName: getSanitizedString(buildInfo?.['buildName'], 160), + remoteEntry: { + name: getRemoteEntryName(moduleFederation, metaData), + type: getRemoteEntryType(moduleFederation, metaData), + globalName: getGlobalName(moduleFederation, metaData), + publicPath: publicPath.value, + publicPathMode: publicPath.mode, + }, + options: { + shareStrategy: getSanitizedString( + moduleFederation['shareStrategy'], + 80, + ), + shareScope: normalizeShareScope(moduleFederation['shareScope']), + asyncStartup: getAsyncStartup(moduleFederation), + manifest: normalizeManifestOption(moduleFederation['manifest']), + dts: getDtsOption(moduleFederation['dts']), + }, + remotes, + exposes, + shared, + }, + summary: { + remoteCount: remotes.length, + exposeCount: exposes.length, + sharedCount: shared.length, + }, + }; +} + +function getErrorRecord(error: unknown): Record | undefined { + return getRecord(error); +} + +function getErrorName(error: unknown): string | undefined { + if (error instanceof Error) { + return getSanitizedString(error.name, 120); + } + + const errorRecord = getErrorRecord(error); + const constructorName = + typeof error === 'object' && error !== null + ? getSanitizedString( + (error as { constructor?: { name?: unknown } }).constructor?.name, + 120, + ) + : undefined; + return getSanitizedString(errorRecord?.['name'], 120) || constructorName; +} + +function getErrorMessage(error: unknown): string | undefined { + if (error instanceof Error) { + return getRawText(error.message); + } + + const errorRecord = getErrorRecord(error); + return getRawText(errorRecord?.['message']) || getRawText(error); +} + +function getErrorStack(error: unknown): string | undefined { + if (error instanceof Error) { + return sanitizeStack(error.stack); + } + + return sanitizeStack(getErrorRecord(error)?.['stack']); +} + +function getBuildOwnerHint( + error: unknown, + phase: ObservabilityBuildReportPhase, +): ObservabilityBuildOwnerHint { + if (phase === 'observability-output') { + return 'build'; + } + + const text = `${getErrorName(error) || ''}\n${getErrorMessage(error) || ''}\n${ + getErrorStack(error) || '' + }`; + + if (/shared|shareScope|singleton|strictVersion|eager/i.test(text)) { + return 'shared'; + } + if (/remote|manifest|remoteEntry|expose|container/i.test(text)) { + return 'remote'; + } + if (/host|ModuleFederationPlugin|federation/i.test(text)) { + return 'host'; + } + + return 'build'; +} + +function createBuildErrorEvent( + traceId: string, + error: unknown, + phase: ObservabilityBuildReportPhase, +): ObservabilityBuildEvent { + const timestamp = Date.now(); + const errorName = getErrorName(error); + const errorMessage = getErrorMessage(error); + const errorStack = getErrorStack(error); + const errorCode = extractErrorCode( + `${errorName || ''}\n${errorMessage || ''}\n${errorStack || ''}`, + ); + + return { + traceId, + timestamp, + phase: 'build', + status: 'error', + lifecycle: phase, + failedPhase: phase, + errorCode, + errorName, + errorMessage, + errorStack, + ownerHint: getBuildOwnerHint(error, phase), + retryable: false, + context: { + lifecycle: phase, + }, + }; +} + +function copyBuildErrorSummary( + event: ObservabilityBuildEvent, +): ObservabilityBuildErrorSummary { + return { + errorCode: event.errorCode, + errorName: event.errorName, + errorMessage: event.errorMessage, + failedPhase: event.failedPhase, + ownerHint: event.ownerHint, + retryable: event.retryable, + context: event.context ? { ...event.context } : undefined, + }; +} + +function createBuildFacts( + buildInfo: ObservabilityBuildInfo, + phase: ObservabilityBuildReportPhase, + primaryError: ObservabilityBuildErrorSummary | undefined, +): ObservabilityBuildMetadata { + const facts: Record = {}; + const addFact = (key: string, value: unknown) => { + if (value === undefined || value === null || value === '') { + return; + } + + facts[key] = Array.isArray(value) ? value.join(',') : value; + }; + + addFact('source', 'build'); + addFact('status', 'error'); + addFact('outcome', 'failed'); + addFact('failedPhase', phase); + addFact('errorCode', primaryError?.errorCode); + addFact('errorName', primaryError?.errorName); + addFact('ownerHint', primaryError?.ownerHint); + addFact('retryable', primaryError?.retryable); + addFact('bundlerName', buildInfo.bundler.name); + addFact('bundlerVersion', buildInfo.bundler.version); + addFact('buildMode', buildInfo.bundler.mode); + addFact('buildTarget', buildInfo.bundler.target); + addFact('mfName', buildInfo.moduleFederation.name); + addFact('pluginVersion', buildInfo.moduleFederation.pluginVersion); + addFact('buildVersion', buildInfo.moduleFederation.buildVersion); + addFact('buildName', buildInfo.moduleFederation.buildName); + addFact('remoteEntryName', buildInfo.moduleFederation.remoteEntry?.name); + addFact('remoteEntryType', buildInfo.moduleFederation.remoteEntry?.type); + addFact( + 'remoteEntryGlobalName', + buildInfo.moduleFederation.remoteEntry?.globalName, + ); + addFact( + 'publicPathMode', + buildInfo.moduleFederation.remoteEntry?.publicPathMode, + ); + addFact('remoteCount', buildInfo.summary.remoteCount); + addFact('exposeCount', buildInfo.summary.exposeCount); + addFact('sharedCount', buildInfo.summary.sharedCount); + addFact( + 'remotes', + buildInfo.moduleFederation.remotes + .map((remote) => remote.alias || remote.name || remote.entry) + .filter((value): value is string => Boolean(value)), + ); + addFact( + 'exposes', + buildInfo.moduleFederation.exposes.map((expose) => expose.name), + ); + addFact( + 'shared', + buildInfo.moduleFederation.shared.map((shared) => shared.name), + ); + addFact( + 'eagerShared', + buildInfo.moduleFederation.shared + .filter((shared) => shared.eager) + .map((shared) => shared.name), + ); + + return Object.entries(facts) + .slice(0, MAX_BUILD_FACT_KEYS) + .reduce((memo, [key, value]) => { + const sanitizedKey = sanitizeText(key, 80); + if (!sanitizedKey) { + return memo; + } + + if (typeof value === 'boolean') { + memo[sanitizedKey] = value; + return memo; + } + + if (typeof value === 'number') { + if (Number.isFinite(value)) { + memo[sanitizedKey] = value; + } + return memo; + } + + const sanitizedValue = clipText(value, 240); + if (sanitizedValue) { + memo[sanitizedKey] = sanitizedValue; + } + + return memo; + }, {}); +} + +function createBuildActions( + phase: ObservabilityBuildReportPhase, + ownerHint: ObservabilityBuildOwnerHint, +): ObservabilityBuildAction[] { + const actions: ObservabilityBuildAction[] = []; + const pushAction = ( + id: string, + title: string, + hint: ObservabilityBuildOwnerHint = ownerHint, + ) => { + actions.push({ + id, + ownerHint: hint, + title, + }); + }; + + if (phase === 'observability-output') { + pushAction( + 'check-observability-output', + 'Check observability output path permissions and filesystem availability', + 'build', + ); + return actions; + } + + pushAction( + 'inspect-build-errors', + 'Inspect the sanitized build error list for the first failing build phase', + ownerHint, + ); + + if (ownerHint === 'shared') { + pushAction( + 'check-shared-config', + 'Check shared dependency configuration, versions, and eager settings', + 'shared', + ); + } else if (ownerHint === 'remote') { + pushAction( + 'check-remote-config', + 'Check remoteEntry, remotes, exposes, and manifest build output', + 'remote', + ); + } else { + pushAction( + 'check-module-federation-config', + 'Check host Module Federation configuration and build options', + ownerHint === 'host' ? 'host' : 'build', + ); + } + + return actions; +} + +function createBuildFactReport( + buildInfo: ObservabilityBuildInfo, + phase: ObservabilityBuildReportPhase, + primaryError: ObservabilityBuildErrorSummary | undefined, +): ObservabilityBuildFactReport { + const ownerHint = primaryError?.ownerHint || 'build'; + + return { + title: + phase === 'observability-output' + ? 'Build observability output failed' + : 'Module Federation build failed', + outcome: 'failed', + status: 'error', + ownerHint, + failedPhase: phase, + errorCode: primaryError?.errorCode, + errorName: primaryError?.errorName, + errorMessage: primaryError?.errorMessage, + facts: createBuildFacts(buildInfo, phase, primaryError), + completedPhases: + phase === 'compilation' ? ['build-info'] : ['build-report'], + pendingPhases: [], + actions: createBuildActions(phase, ownerHint), + }; +} + +function createBuildReport( + buildInfo: ObservabilityBuildInfo, + errors: unknown[], + phase: ObservabilityBuildReportPhase, + startedAt: number, +): ObservabilityBuildReport | undefined { + const sanitizedErrors = errors.filter((error) => error !== undefined); + if (!sanitizedErrors.length) { + return undefined; + } + + const traceId = `mf-build-${Date.now().toString(36)}`; + const events = sanitizedErrors + .slice(0, 20) + .map((error) => createBuildErrorEvent(traceId, error, phase)); + const updatedAt = events[events.length - 1]?.timestamp || Date.now(); + const errorsSummary = events.map(copyBuildErrorSummary); + const primaryError = errorsSummary[0]; + + return { + schemaVersion: 1, + traceId, + source: 'build', + status: 'error', + startedAt, + updatedAt, + duration: Math.max(0, updatedAt - startedAt), + failedPhase: phase, + build: buildInfo, + events, + summary: { + eventCount: events.length, + outcome: 'failed', + error: primaryError, + errors: errorsSummary, + }, + diagnosis: createBuildFactReport(buildInfo, phase, primaryError), + }; +} + +function getModuleFederationOptions( + options: ObservabilityBuildPluginOptions, + compiler: CompilerLike, +): unknown { + if (options.moduleFederation) { + return options.moduleFederation; + } + + const plugins = compiler.options?.['plugins']; + if (!Array.isArray(plugins)) { + return undefined; + } + + for (const plugin of plugins) { + const pluginRecord = getRecord(plugin); + if (!pluginRecord) { + continue; + } + + const pluginName = getSanitizedString(pluginRecord['name'], 120); + if (pluginName !== 'ModuleFederationPlugin') { + continue; + } + + return pluginRecord['_options'] || pluginRecord['options']; + } + + return undefined; +} + +function getCompilerOptions(compiler: CompilerLike) { + return compiler.options || {}; +} + +function getBundlerFromCompiler( + options: ObservabilityBuildPluginOptions, + compiler: CompilerLike, +) { + if (options.bundler) { + return options.bundler; + } + + if (compiler.webpack?.rspackVersion) { + return 'rspack'; + } + + return 'webpack'; +} + +function getBundlerVersionFromCompiler( + options: ObservabilityBuildPluginOptions, + compiler: CompilerLike, +) { + return options.bundlerVersion || compiler.webpack?.version; +} + +function getProcessAssetsStage( + compiler: CompilerLike, + compilation: CompilationLike, +) { + return ( + compilation.constructor?.PROCESS_ASSETS_STAGE_REPORT || + compiler.webpack?.Compilation?.PROCESS_ASSETS_STAGE_REPORT || + compilation.constructor?.PROCESS_ASSETS_STAGE_SUMMARIZE || + compiler.webpack?.Compilation?.PROCESS_ASSETS_STAGE_SUMMARIZE + ); +} + +function getOutputFile( + options: ObservabilityBuildPluginOptions, + compiler: CompilerLike, +) { + return getResolvedOutputFile(options.outputFile || DEFAULT_OUTPUT_FILE, { + cwd: options.cwd, + compiler, + }); +} + +function getResolvedOutputFile( + outputFile: string, + { + cwd: configuredCwd, + compiler, + }: { + cwd?: string; + compiler: CompilerLike; + }, +) { + const cwd = + configuredCwd || + getString(compiler.options?.['context']) || + compiler.context || + process.cwd(); + + return path.isAbsolute(outputFile) + ? outputFile + : path.resolve(cwd, outputFile); +} + +function getBuildReportOutputFile( + options: ObservabilityBuildPluginOptions, + compiler: CompilerLike, +) { + const reportOptions = options.errorReport; + const outputFile = + reportOptions === false + ? undefined + : reportOptions?.outputFile || DEFAULT_REPORT_FILE; + + return outputFile + ? getResolvedOutputFile(outputFile, { cwd: options.cwd, compiler }) + : undefined; +} + +function writeBuildInfo( + buildInfo: ObservabilityBuildInfo, + options: ObservabilityBuildPluginOptions, + compiler: CompilerLike, +) { + const outputFile = getOutputFile(options, compiler); + fs.mkdirSync(path.dirname(outputFile), { recursive: true }); + fs.writeFileSync( + outputFile, + `${JSON.stringify(buildInfo, null, 2)}\n`, + 'utf8', + ); +} + +function writeBuildReport( + report: ObservabilityBuildReport, + options: ObservabilityBuildPluginOptions, + compiler: CompilerLike, +) { + const outputFile = getBuildReportOutputFile(options, compiler); + if (!outputFile) { + return; + } + + fs.mkdirSync(path.dirname(outputFile), { recursive: true }); + fs.writeFileSync(outputFile, `${JSON.stringify(report, null, 2)}\n`, 'utf8'); +} + +function removeStaleBuildReport( + options: ObservabilityBuildPluginOptions, + compiler: CompilerLike, +) { + const outputFile = getBuildReportOutputFile(options, compiler); + if (!outputFile || !fs.existsSync(outputFile)) { + return; + } + + fs.rmSync(outputFile, { force: true }); +} + +function warn( + compiler: CompilerLike, + error: unknown, + action = 'write build observability', +) { + const message = + getRawText(error instanceof Error ? error.message : String(error)) || + 'unknown error'; + const logger = compiler.getInfrastructureLogger?.(PLUGIN_NAME); + logger?.warn?.(`[${PLUGIN_NAME}] Failed to ${action}: ${message}`); +} + +function writeBuildReportSafely( + report: ObservabilityBuildReport | undefined, + options: ObservabilityBuildPluginOptions, + compiler: CompilerLike, +) { + if (!report) { + return; + } + + try { + writeBuildReport(report, options, compiler); + } catch (error) { + warn(compiler, error, 'write build observability report'); + } +} + +export class ObservabilityBuildPlugin { + readonly name = PLUGIN_NAME; + private readonly options: ObservabilityBuildPluginOptions; + + constructor(options: ObservabilityBuildPluginOptions = {}) { + this.options = options; + } + + apply(compiler: CompilerLike): void { + if (this.options.enabled === false) { + return; + } + + compiler.hooks?.thisCompilation?.tap(PLUGIN_NAME, (compilation) => { + compilation.hooks?.processAssets?.tapPromise( + { + name: PLUGIN_NAME, + stage: getProcessAssetsStage(compiler, compilation), + }, + async () => { + const startedAt = Date.now(); + try { + const moduleFederation = getModuleFederationOptions( + this.options, + compiler, + ); + const fileNames = getManifestFileName( + getRecord(moduleFederation)?.['manifest'], + ); + const manifest = readJsonAsset( + compilation, + fileNames.manifestFileName, + ); + const stats = readJsonAsset(compilation, fileNames.statsFileName); + const buildInfo = createObservabilityBuildInfo({ + moduleFederation, + manifest, + stats, + compilerOptions: getCompilerOptions(compiler), + bundler: getBundlerFromCompiler(this.options, compiler), + bundlerVersion: getBundlerVersionFromCompiler( + this.options, + compiler, + ), + pluginVersion: this.options.pluginVersion, + }); + let observabilityOutputFailed = false; + + try { + writeBuildInfo(buildInfo, this.options, compiler); + } catch (error) { + observabilityOutputFailed = true; + warn(compiler, error); + writeBuildReportSafely( + createBuildReport( + buildInfo, + [error], + 'observability-output', + startedAt, + ), + this.options, + compiler, + ); + } + + const compilationErrors = compilation.errors || []; + if (compilationErrors.length) { + writeBuildReportSafely( + createBuildReport( + buildInfo, + compilationErrors, + 'compilation', + startedAt, + ), + this.options, + compiler, + ); + } else if (!observabilityOutputFailed) { + try { + removeStaleBuildReport(this.options, compiler); + } catch (error) { + warn( + compiler, + error, + 'remove stale build observability report', + ); + } + } + } catch (error) { + const fallbackBuildInfo = createObservabilityBuildInfo({ + moduleFederation: this.options.moduleFederation, + compilerOptions: getCompilerOptions(compiler), + bundler: getBundlerFromCompiler(this.options, compiler), + bundlerVersion: getBundlerVersionFromCompiler( + this.options, + compiler, + ), + pluginVersion: this.options.pluginVersion, + }); + + warn(compiler, error); + writeBuildReportSafely( + createBuildReport( + fallbackBuildInfo, + [error], + 'observability-output', + startedAt, + ), + this.options, + compiler, + ); + } + }, + ); + }); + } +} diff --git a/packages/observability-plugin/src/chrome-devtool.ts b/packages/observability-plugin/src/chrome-devtool.ts new file mode 100644 index 00000000000..3b02a82c812 --- /dev/null +++ b/packages/observability-plugin/src/chrome-devtool.ts @@ -0,0 +1,21 @@ +import type { ObservabilityRuntimePlugin } from './core'; +import { createObservability, type ObservabilityPluginOptions } from './core'; + +export type { ObservabilityPluginOptions } from './core'; + +export function ChromeObservabilityPlugin( + options: ObservabilityPluginOptions = {}, +): ObservabilityRuntimePlugin { + return createObservability(options, { + pluginName: 'observability-plugin:chrome-extension', + fixedBrowserScope: 'chrome_extension', + attachInstanceApi: false, + guardSharedHooksByRuntimeVersion: true, + guardRuntimeHooksByRuntimeVersion: true, + disablePreloadHooks: true, + returnHookArgs: true, + forceDevelopmentChannels: true, + }).plugin; +} + +export default ChromeObservabilityPlugin; diff --git a/packages/observability-plugin/src/core.ts b/packages/observability-plugin/src/core.ts new file mode 100644 index 00000000000..27c6f85ae24 --- /dev/null +++ b/packages/observability-plugin/src/core.ts @@ -0,0 +1,4643 @@ +import type { + ModuleFederation, + ModuleFederationRuntimePlugin, +} from '@module-federation/runtime'; +import { createLogger, isDebugMode } from '@module-federation/sdk'; + +export type ObservabilityLevel = 'error' | 'summary' | 'verbose'; +export type ObservabilityEventStatus = + | 'start' + | 'success' + | 'error' + | 'complete'; +export type ObservabilityReportStatus = 'pending' | 'success' | 'error'; +export type ObservabilityEventSource = 'runtime' | 'business' | 'react'; +export type ObservabilityBrowserMode = 'development' | 'production'; +export type ObservabilityReportOutcome = + | 'pending' + | 'runtime-loaded' + | 'shared-resolved' + | 'preloaded' + | 'component-loaded' + | 'failed' + | 'recovered'; +export type ObservabilityOwnerHint = + | 'host' + | 'remote' + | 'shared' + | 'network' + | 'runtime' + | 'unknown'; +export type ObservabilityMetadataValue = string | number | boolean; +export type ObservabilityMetadata = Record; + +export interface ObservabilityModuleInfoEntry { + name: string; + publicPath?: string; + getPublicPath?: string; + remoteEntry?: string; + globalName?: string; +} + +export interface ObservabilityModuleInfoSummary { + reason: string; + clipped: true; + totalCount: number; + matchedCount: number; + entries: ObservabilityModuleInfoEntry[]; + availableNames?: string[]; +} + +export interface ObservabilityPhaseSummary { + status: ObservabilityEventStatus; + duration?: number; + cached?: boolean; + recovered?: boolean; + lifecycle?: string; +} + +export interface ObservabilitySharedSummary { + name: string; + provider?: string; + selectedVersion?: string; + shareScope?: string[]; +} + +export interface ObservabilityLoadedBeforeConsumer { + name?: string; + remoteEntryExports?: boolean; + containerInitialized?: boolean; + exposes?: string[]; +} + +export interface ObservabilityLoadedBeforeInfo { + producer: boolean; + expose: boolean; + consumers: ObservabilityLoadedBeforeConsumer[]; +} + +export interface ObservabilityReportFlags { + cached: boolean; + fallback: boolean; + recovered: boolean; +} + +export interface ObservabilityPhaseCollection { + phases: Record; + shared?: ObservabilitySharedSummary; + flags: ObservabilityReportFlags; +} + +export interface ObservabilityErrorSummary { + errorCode?: string; + errorName?: string; + errorMessage?: string; + failedPhase?: string; + lifecycle?: string; + ownerHint?: ObservabilityOwnerHint; + retryable?: boolean; + context?: ObservabilityMetadata; +} + +export type ObservabilityActionId = + | 'check-manifest-url' + | 'check-remote-entry' + | 'check-remote-global' + | 'check-host-remotes' + | 'check-shared-provider' + | 'check-shared-version' + | 'check-eager-config' + | 'check-network' + | 'check-expose' + | 'check-module-info' + | 'inspect-runtime-events'; + +export interface ObservabilityAction { + id: ObservabilityActionId | string; + ownerHint?: ObservabilityOwnerHint; + title: string; + detail?: string; +} + +export interface ObservabilityFactReport { + title: string; + outcome: ObservabilityReportOutcome; + status: ObservabilityReportStatus; + ownerHint: ObservabilityOwnerHint; + failedPhase?: string; + errorCode?: string; + errorName?: string; + errorMessage?: string; + docLink?: string; + facts: ObservabilityMetadata; + completedPhases: string[]; + pendingPhases: string[]; + warnings?: string[]; + actions: ObservabilityAction[]; +} + +export interface ObservabilityRemoteInfo { + name: string; + alias?: string; + entry?: string; + entryGlobalName?: string; + type?: string; +} + +export interface ObservabilitySharedInfo { + name: string; + shareScope?: string[]; + requiredVersion?: string | false; + selectedVersion?: string; + availableVersions?: string[]; + provider?: string; + from?: string; + singleton?: boolean; + strictVersion?: boolean; + eager?: boolean; + strategy?: string; + loaded?: boolean; + loading?: boolean; + reason?: string; +} + +export interface ObservabilityEvent { + traceId: string; + timestamp: number; + phase: string; + status: ObservabilityEventStatus; + requestId?: string; + requestAlias?: string; + hostName?: string; + runtimeVersion?: string; + remote?: ObservabilityRemoteInfo; + shared?: ObservabilitySharedInfo; + expose?: string; + sanitizedUrl?: string; + message?: string; + errorCode?: string; + errorName?: string; + errorMessage?: string; + errorStack?: string; + ownerHint?: ObservabilityOwnerHint; + retryable?: boolean; + errorContext?: ObservabilityMetadata; + duration?: number; + lifecycle?: string; + eventName?: string; + source?: ObservabilityEventSource; + recovered?: boolean; + cached?: boolean; + componentName?: string; + metadata?: ObservabilityMetadata; + loadedBefore?: ObservabilityLoadedBeforeInfo; +} + +export interface ObservabilityReport { + traceId: string; + status: ObservabilityReportStatus; + requestId?: string; + requestAlias?: string; + hostName?: string; + runtimeVersion?: string; + remote?: ObservabilityRemoteInfo; + shared?: ObservabilitySharedInfo; + expose?: string; + sanitizedUrl?: string; + startedAt: number; + updatedAt: number; + duration: number; + failedPhase?: string; + errorCode?: string; + errorName?: string; + errorMessage?: string; + errorStack?: string; + ownerHint?: ObservabilityOwnerHint; + retryable?: boolean; + errorContext?: ObservabilityMetadata; + moduleInfo?: ObservabilityModuleInfoSummary; + loadedBefore?: ObservabilityLoadedBeforeInfo; + events: ObservabilityEvent[]; + summary: { + eventCount: number; + recovered: boolean; + loadCompleted: boolean; + runtimeLoaded: boolean; + sharedResolved: boolean; + preloaded: boolean; + componentLoaded: boolean; + outcome: ObservabilityReportOutcome; + lastPhase?: string; + phases: Record; + shared?: ObservabilitySharedSummary; + flags: ObservabilityReportFlags; + error?: ObservabilityErrorSummary; + }; + diagnosis?: ObservabilityFactReport; +} + +export interface ObservabilityPluginOptions { + enabled?: boolean; + level?: ObservabilityLevel; + maxEvents?: number; + console?: boolean; + collector?: + | boolean + | { + enabled?: boolean; + port?: number; + }; + printRawStack?: boolean; + stackTrace?: { + enabled?: boolean; + maxLines?: number; + maxLength?: number; + }; + browser?: { + enabled?: boolean; + scope?: string; + mode?: ObservabilityBrowserMode; + }; + trace?: { + printStart?: boolean; + }; + devtools?: + | boolean + | { + enabled?: boolean; + source?: string; + }; + react?: { + enabled?: boolean; + injectLoadedCallback?: boolean; + remoteIds?: string[]; + defaultExportMode?: 'preserve' | 'component'; + }; + onEvent?: ( + event: ObservabilityEvent, + report: ObservabilityReport, + context?: ObservabilityEventContext, + ) => void; + onReport?: ( + report: ObservabilityReport, + context?: ObservabilityEventContext, + ) => void; + onRawError?: (error: unknown, context: ObservabilityRawErrorContext) => void; +} + +export interface ObservabilityReportListOptions { + limit?: number; +} + +export interface ObservabilityReportQuery extends ObservabilityReportListOptions { + traceId?: string; + remote?: string; + expose?: string; + shared?: string; + status?: ObservabilityReportStatus; + outcome?: ObservabilityReportOutcome; +} + +export interface MarkComponentLoadedOptions { + traceId?: string; + requestId?: string; + componentName?: string; + metadata?: Record; +} + +export interface MFRemoteLoadedOptions { + componentName?: string; + metadata?: Record; +} + +export type OnMFRemoteLoaded = (options?: MFRemoteLoadedOptions) => void; + +export interface ObservabilityController { + plugin: ObservabilityRuntimePlugin; + getEvents(): ObservabilityEvent[]; + getTraceIds(): string[]; + getReports(options?: ObservabilityReportListOptions): ObservabilityReport[]; + findReports(query?: ObservabilityReportQuery): ObservabilityReport[]; + getLatestReport(): ObservabilityReport | undefined; + getReport(traceId: string): ObservabilityReport | undefined; + exportReport(traceId?: string): ObservabilityReport | undefined; + clear(): void; + markComponentLoaded( + options?: MarkComponentLoadedOptions, + ): ObservabilityEvent | undefined; +} + +export interface ObservabilityInstanceAPI { + markComponentLoaded( + options?: MarkComponentLoadedOptions, + ): ObservabilityEvent | undefined; +} + +export interface ObservabilityRuntimeAdapterOptions { + pluginName?: string; + fixedBrowserScope?: string; + disableReact?: boolean; + attachInstanceApi?: boolean; + guardSharedHooksByRuntimeVersion?: boolean; + guardRuntimeHooksByRuntimeVersion?: boolean; + disablePreloadHooks?: boolean; + returnHookArgs?: boolean; + forceDevelopmentChannels?: boolean; +} + +declare module '@module-federation/runtime-core' { + interface ModuleFederation { + markComponentLoaded( + options?: MarkComponentLoadedOptions, + ): ObservabilityEvent | undefined; + } +} + +export interface ObservabilityRuntimeEventInput { + phase: string; + status: ObservabilityEventStatus; + requestId?: string; + requestAlias?: string; + hostName?: string; + remote?: ObservabilityRemoteInfo; + shared?: ObservabilitySharedInfo; + expose?: string; + url?: string; + message?: string; + error?: unknown; + errorContext?: Record; + duration?: number; + lifecycle?: string; + eventName?: string; + source?: ObservabilityEventSource; + recovered?: boolean; + timestamp?: number; + traceId?: string; + cached?: boolean; + componentName?: string; + metadata?: Record; + loadedBefore?: ObservabilityLoadedBeforeInfo; +} + +export interface ObservabilityRuntimeOrigin { + name?: string; + version?: string; + options?: { + id?: string; + name?: string; + }; + loadShare?: (pkgName: string) => Promise unknown)>; + loadShareSync?: (pkgName: string) => false | (() => unknown); +} + +export interface ObservabilityEventContext { + origin?: ObservabilityRuntimeOrigin; +} + +export interface ObservabilityRawErrorContext extends ObservabilityEventContext { + event: ObservabilityEvent; + report: ObservabilityReport; +} + +interface ObservabilityRuntimeSharedConfig { + requiredVersion?: string | false; + singleton?: boolean; + strictVersion?: boolean; + eager?: boolean; +} + +interface ObservabilityRuntimeSharedSource { + version?: string; + scope?: string | string[]; + from?: string; + loaded?: boolean; + loading?: unknown; + strategy?: string; + shareConfig?: ObservabilityRuntimeSharedConfig; + get?: unknown; +} + +interface ObservabilityRuntimeRemoteSource { + name?: string; + alias?: string; + entry?: string; + entryGlobalName?: string; + type?: string; +} + +interface ObservabilityRuntimeOptions { + name?: string; + remotes?: ObservabilityRuntimeRemoteSource[]; +} + +interface ObservabilityRemoteLoadArgs { + id: string; + pkgNameOrAlias?: string; + expose?: string; + remote?: ObservabilityRuntimeRemoteSource; + origin: ObservabilityRuntimeOrigin; + exposeModule?: unknown; + exposeModuleFactory?: unknown; +} + +interface ObservabilityRemoteBeforeRequestArgs { + id: string; + options?: ObservabilityRuntimeOptions; + origin: ObservabilityRuntimeOrigin; +} + +interface ObservabilityRemoteAfterLoadArgs { + id: string; + expose?: string; + remote?: ObservabilityRuntimeRemoteSource; + error?: unknown; + recovered?: boolean; + origin: ObservabilityRuntimeOrigin; +} + +interface ObservabilityRemoteMatchArgs { + id: string; + options?: ObservabilityRuntimeOptions; + expose?: string; + remote?: ObservabilityRuntimeRemoteSource; + remoteInfo?: ObservabilityRuntimeRemoteSource; + error?: unknown; + origin: ObservabilityRuntimeOrigin; +} + +interface ObservabilityRemoteSnapshotArgs { + origin: ObservabilityRuntimeOrigin; +} + +interface ObservabilityPreloadConfig { + nameOrAlias?: string; + exposes?: string[]; + resourceCategory?: 'all' | 'sync'; + share?: boolean; + depsRemote?: boolean | unknown[]; +} + +interface ObservabilityPreloadOption { + remote?: ObservabilityRuntimeRemoteSource; + preloadConfig?: ObservabilityPreloadConfig; +} + +interface ObservabilityPreloadAssetsArgs { + origin: ObservabilityRuntimeOrigin; + preloadOptions?: ObservabilityPreloadOption; + remote?: ObservabilityRuntimeRemoteSource; + remoteInfo?: ObservabilityRuntimeRemoteSource; +} + +interface ObservabilityPreloadAssetResult { + url?: string; + status?: 'success' | 'error' | 'timeout' | 'cached'; + resourceType?: string; + initiator?: string; + id?: string; + error?: unknown; +} + +interface ObservabilityPreloadRemoteResult { + remote?: ObservabilityRuntimeRemoteSource; + remoteInfo?: ObservabilityRuntimeRemoteSource; + preloadConfig?: ObservabilityPreloadConfig; + id?: string; + results?: ObservabilityPreloadAssetResult[]; +} + +interface ObservabilityAfterPreloadRemoteArgs { + origin: ObservabilityRuntimeOrigin; + preloadOps?: ObservabilityPreloadConfig[]; + results?: ObservabilityPreloadRemoteResult[]; + error?: unknown; +} + +type ObservabilitySnapshotRemoteSource = ObservabilityRuntimeRemoteSource & { + remoteEntry?: string; + ssrRemoteEntry?: string; +}; + +interface ObservabilitySnapshotLoadArgs { + moduleInfo?: ObservabilityRuntimeRemoteSource; + remoteSnapshot?: ObservabilitySnapshotRemoteSource; +} + +interface ObservabilityRemoteSnapshotLoadArgs { + moduleInfo?: ObservabilityRuntimeRemoteSource; + manifestJson?: unknown; + manifestUrl?: string; + from?: 'global' | 'manifest'; +} + +interface ObservabilityRemoteResolveArgs { + id: string; + expose?: string; + remote?: ObservabilityRuntimeRemoteSource; + remoteInfo?: ObservabilityRuntimeRemoteSource; + cached?: boolean; + origin: ObservabilityRuntimeOrigin; +} + +interface ObservabilityRemoteErrorArgs { + id: string; + error: unknown; + lifecycle?: string; + remote?: ObservabilityRuntimeRemoteSource; + expose?: string; + origin: ObservabilityRuntimeOrigin; +} + +interface ObservabilityRemoteEntryLoadArgs { + origin: ObservabilityRuntimeOrigin; + remoteInfo: ObservabilityRuntimeRemoteSource; +} + +interface ObservabilityRemoteEntryAfterLoadArgs { + origin: ObservabilityRuntimeOrigin; + remoteInfo: ObservabilityRuntimeRemoteSource; + error?: unknown; + recovered?: boolean; + cached?: boolean; +} + +interface ObservabilityRemoteInitArgs { + id?: string; + remoteInfo: ObservabilityRuntimeRemoteSource; + error?: unknown; + cached?: boolean; + origin: ObservabilityRuntimeOrigin; +} + +interface ObservabilityRemoteExposeArgs { + id: string; + expose: string; + moduleInfo: ObservabilityRuntimeRemoteSource; + error?: unknown; + origin: ObservabilityRuntimeOrigin; +} + +interface ObservabilityRemoteFactoryArgs { + id: string; + expose: string; + moduleInfo: ObservabilityRuntimeRemoteSource; + loadFactory: boolean; + error?: unknown; + origin: ObservabilityRuntimeOrigin; +} + +type ObservabilityRuntimeShareScopeMap = Record< + string, + Record> +>; + +interface ObservabilitySharedLifecycleArgs { + pkgName: string; + shareInfo?: ObservabilityRuntimeSharedSource; + selectedShared?: ObservabilityRuntimeSharedSource; + shared?: Record; + shareScopeMap?: ObservabilityRuntimeShareScopeMap; + lifecycle?: 'loadShare' | 'loadShareSync'; + origin: ObservabilityRuntimeOrigin; + error?: unknown; + recovered?: boolean; +} + +export type ObservabilityRuntimePlugin = ModuleFederationRuntimePlugin; + +export interface ObservabilityBrowserReader { + getEvents(): ObservabilityEvent[]; + getTraceIds(): string[]; + getReports(options?: ObservabilityReportListOptions): ObservabilityReport[]; + findReports(query?: ObservabilityReportQuery): ObservabilityReport[]; + getLatestReport(): ObservabilityReport | undefined; + getReport(traceId: string): ObservabilityReport | undefined; + exportReport(traceId?: string): ObservabilityReport | undefined; +} + +interface FederationObservabilityGlobal { + __OBSERVABILITY__?: Record; + __INSTANCES__?: ObservabilityRuntimeInstanceLike[]; + moduleInfo?: Record; +} + +interface ObservabilityRuntimeModuleLike { + remoteInfo?: ObservabilityRuntimeRemoteSource; + remoteEntryExports?: unknown; + inited?: boolean; +} + +interface ObservabilityRuntimeRemoteHandlerLike { + idToRemoteMap?: Record; +} + +interface ObservabilityRuntimeInstanceLike extends ObservabilityRuntimeOrigin { + moduleCache?: + | Map + | { + entries?: () => IterableIterator<[unknown, unknown]>; + } + | Record; + remoteHandler?: ObservabilityRuntimeRemoteHandlerLike; +} + +interface ObservabilityReactLike { + createElement: ( + type: unknown, + props?: Record | null, + ...children: unknown[] + ) => unknown; +} + +interface ObservabilityCollectorOptions { + enabled: true; + port: number; +} + +interface ObservabilityDevtoolsOptions { + enabled: true; + source: string; +} + +type ObservabilityFetch = ( + input: string, + init?: { + method?: string; + headers?: Record; + body?: string; + keepalive?: boolean; + credentials?: 'omit'; + mode?: 'cors'; + }, +) => Promise; + +const DEFAULT_MAX_EVENTS = 100; +const HARD_MAX_EVENTS = 1000; +const DEFAULT_COLLECTOR_PORT = 17891; +const COLLECTOR_PATH = '/__mf_observability'; +const logger = createLogger('[ Module Federation Observability Plugin ]'); +const DEFAULT_DEVTOOLS_SOURCE = 'module-federation/observability'; +const COMPONENT_BUSINESS_LOADED_EVENT = 'component:business-loaded'; +const ON_MF_REMOTE_LOADED_PROP = 'onMFRemoteLoaded'; +const SENSITIVE_PAIR_PATTERN = + /\b(token|authorization|cookie|secret|password|session|access_token|refresh_token|api_key|apikey|key)\s*[:=]\s*([^&\s'",;<>]+)/gi; +const ERROR_CODE_PATTERN = /\b(?:RUNTIME|TYPE|BUILD)-\d{3}\b/; +const URL_PATTERN = /https?:\/\/[^\s'"<>]+/g; +const DIAGNOSTIC_DOC_LINK_PATTERN = + /https?:\/\/module-federation\.io\/guide\/troubleshooting\/[^\s'"<>]+/i; +const RUNTIME_DOC_LINK = + 'https://module-federation.io/guide/troubleshooting/runtime'; +const ABSOLUTE_PATH_PATTERN = + /(?:file:\/\/)?(?:\/(?:Users|private|var|tmp|home|workspace|opt|usr)\/[^\s)]+|[A-Za-z]:\\[^\s)]+)/g; +const MAX_METADATA_KEYS = 20; +const MAX_FACT_KEYS = 50; +const MAX_BUILD_ITEMS = 50; +const MAX_MODULE_INFO_ENTRIES = 20; +const HARD_MAX_REPORT_QUERY_LIMIT = 1000; + +let traceCounter = 0; + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +function normalizeMaxEvents(value: number | undefined, fallback: number) { + if (typeof value !== 'number' || !Number.isFinite(value)) { + return fallback; + } + + return Math.max(1, Math.min(HARD_MAX_EVENTS, Math.floor(value))); +} + +function normalizeQueryLimit(value: number | undefined): number | undefined { + if (typeof value !== 'number' || !Number.isFinite(value)) { + return undefined; + } + + return Math.max(1, Math.min(HARD_MAX_REPORT_QUERY_LIMIT, Math.floor(value))); +} + +function normalizeCollectorPort(value: number | undefined) { + if (!Number.isFinite(value) || !value) { + return DEFAULT_COLLECTOR_PORT; + } + + const port = Math.floor(value); + return port > 0 && port <= 65535 ? port : DEFAULT_COLLECTOR_PORT; +} + +function normalizeCollectorOptions( + value: ObservabilityPluginOptions['collector'], +): ObservabilityCollectorOptions | undefined { + if (value === true) { + return { + enabled: true, + port: DEFAULT_COLLECTOR_PORT, + }; + } + + if (!value || value === false || value.enabled === false) { + return undefined; + } + + return { + enabled: true, + port: normalizeCollectorPort(value.port), + }; +} + +function normalizeDevtoolsOptions( + value: ObservabilityPluginOptions['devtools'], +): ObservabilityDevtoolsOptions | undefined { + if (value === true) { + return { + enabled: true, + source: DEFAULT_DEVTOOLS_SOURCE, + }; + } + + if (!value || value === false || value.enabled === false) { + return undefined; + } + + return { + enabled: true, + source: sanitizeText(value.source, 160) || DEFAULT_DEVTOOLS_SOURCE, + }; +} + +function getCollectorUrl(port: number) { + return `http://127.0.0.1:${port}${COLLECTOR_PATH}`; +} + +function sanitizeText(value: unknown, maxLength = 800): string | undefined { + if (value === undefined || value === null) { + return undefined; + } + + const sanitized = String(value) + .replace(URL_PATTERN, (url) => sanitizeUrl(url) || '[redacted-url]') + .replace(SENSITIVE_PAIR_PATTERN, '[redacted]'); + + return sanitized.length > maxLength + ? `${sanitized.slice(0, maxLength)}...` + : sanitized; +} + +function getRawText(value: unknown): string | undefined { + if (value === undefined || value === null) { + return undefined; + } + + return String(value); +} + +function clipText(value: unknown, maxLength = 320): string | undefined { + if (value === undefined || value === null) { + return undefined; + } + + const sanitized = String(value); + + return sanitized.length > maxLength + ? `${sanitized.slice(0, maxLength)}...` + : sanitized; +} + +function clipObservabilityMetadata( + metadata: Record | undefined, + maxKeys = MAX_METADATA_KEYS, +): ObservabilityMetadata | undefined { + if (!metadata || typeof metadata !== 'object') { + return undefined; + } + + const clipped: ObservabilityMetadata = {}; + + Object.entries(metadata) + .slice(0, maxKeys) + .forEach(([rawKey, rawValue]) => { + const key = clipText(rawKey, 80); + + if (!key || rawValue === undefined || rawValue === null) { + return; + } + + if (typeof rawValue === 'boolean') { + clipped[key] = rawValue; + return; + } + + if (typeof rawValue === 'number') { + if (Number.isFinite(rawValue)) { + clipped[key] = rawValue; + } + return; + } + + const value = clipText(rawValue, 240); + if (value) { + clipped[key] = value; + } + }); + + return Object.keys(clipped).length ? clipped : undefined; +} + +function clipMetadata( + metadata: Record | undefined, + maxKeys = MAX_METADATA_KEYS, +): ObservabilityMetadata | undefined { + if (!metadata || typeof metadata !== 'object') { + return undefined; + } + + const clipped: ObservabilityMetadata = {}; + + Object.entries(metadata) + .slice(0, maxKeys) + .forEach(([rawKey, rawValue]) => { + const key = sanitizeText(rawKey, 80); + + if (!key || rawValue === undefined || rawValue === null) { + return; + } + + if (typeof rawValue === 'boolean') { + clipped[key] = rawValue; + return; + } + + if (typeof rawValue === 'number') { + if (Number.isFinite(rawValue)) { + clipped[key] = rawValue; + } + return; + } + + const value = clipText(rawValue, 240); + if (value) { + clipped[key] = value; + } + }); + + return Object.keys(clipped).length ? clipped : undefined; +} + +function sanitizeStack( + stack: string | undefined, + options: ObservabilityPluginOptions['stackTrace'], +): string | undefined { + if (!stack || options?.enabled === false) { + return undefined; + } + + return stack; +} + +function getRawStack(error: unknown): string | undefined { + if (error instanceof Error) { + return error.stack || error.message; + } + + return undefined; +} + +function sanitizeRequestId(value: string | undefined): string | undefined { + if (!value) { + return undefined; + } + + return clipText(value, 240); +} + +function sanitizeUrl(value: string | undefined): string | undefined { + if (!value) { + return undefined; + } + + try { + const base = + typeof window !== 'undefined' && window.location + ? window.location.origin + : 'http://localhost'; + const parsedUrl = new URL(value, base); + const sanitized = `${parsedUrl.origin}${parsedUrl.pathname}`; + + return /^https?:\/\//i.test(value) ? sanitized : parsedUrl.pathname; + } catch { + const [withoutHash] = value.split('#'); + const [withoutQuery] = withoutHash.split('?'); + return sanitizeText(withoutQuery, 240); + } +} + +function sanitizeRemote( + remote: ObservabilityRemoteInfo | undefined, +): ObservabilityRemoteInfo | undefined { + if (!remote || !remote.name) { + return undefined; + } + + return { + name: remote.name, + alias: sanitizeText(remote.alias, 120), + entry: clipText(remote.entry, 320), + entryGlobalName: sanitizeText(remote.entryGlobalName, 120), + type: sanitizeText(remote.type, 80), + }; +} + +function createRemoteInfo( + remote: ObservabilityRuntimeRemoteSource | undefined, +): ObservabilityRemoteInfo | undefined { + if (!remote?.name) { + return undefined; + } + + return { + name: remote.name, + alias: remote.alias, + entry: remote.entry, + entryGlobalName: remote.entryGlobalName, + type: remote.type, + }; +} + +function isManifestUrl(value: string | undefined): boolean { + const sanitized = sanitizeUrl(value); + + return Boolean(sanitized && /manifest.*\.json$/i.test(sanitized)); +} + +function normalizeSharedScope(value: string | string[] | undefined): string[] { + if (!value) { + return []; + } + + return (Array.isArray(value) ? value : [value]) + .map((scope) => sanitizeText(scope, 120)) + .filter((scope): scope is string => Boolean(scope)); +} + +function getSharedScopes( + shareInfo: ObservabilityRuntimeSharedSource | undefined, +): string[] { + return normalizeSharedScope(shareInfo?.scope).length + ? normalizeSharedScope(shareInfo?.scope) + : ['default']; +} + +function getAvailableSharedVersions(args: ObservabilitySharedLifecycleArgs) { + const versions = new Set(); + const shareScopeMap = args.shareScopeMap || {}; + + getSharedScopes(args.shareInfo).forEach((scope) => { + Object.keys(shareScopeMap[scope]?.[args.pkgName] || {}).forEach( + (version) => { + versions.add(version); + }, + ); + }); + + return Array.from(versions); +} + +function getSharedMissReason(args: ObservabilitySharedLifecycleArgs) { + if (!args.shareInfo) { + return 'missing-config'; + } + + return getAvailableSharedVersions(args).length + ? 'version-mismatch' + : 'missing-provider'; +} + +function getSharedErrorReason(args: ObservabilitySharedLifecycleArgs) { + if (args.recovered) { + return getSharedMissReason(args); + } + + const errorInfo = getErrorInfo(args.error, { enabled: false }); + const errorMessage = errorInfo.errorMessage || ''; + + if (!args.shareInfo || /Cannot find shared/i.test(errorMessage)) { + return 'missing-config'; + } + + if ( + args.lifecycle === 'loadShareSync' && + typeof args.shareInfo.get === 'function' && + /RUNTIME-00[56]/.test(errorMessage) + ) { + return 'sync-async-boundary'; + } + + if ( + args.lifecycle === 'loadShareSync' && + !args.shareInfo.get && + /RUNTIME-006/.test(errorMessage) + ) { + return getSharedMissReason(args); + } + + if (args.error) { + return 'load-error'; + } + + return undefined; +} + +function parseStableVersion(version?: string) { + const matched = version?.match(/^(\d+)\.(\d+)\.(\d+)(?:\+[\w.-]+)?$/); + + if (!matched) { + return undefined; + } + + return { + major: Number(matched[1]), + minor: Number(matched[2]), + patch: Number(matched[3]), + }; +} + +function isVersionAtLeast( + version: { major: number; minor: number; patch: number }, + target: { major: number; minor: number; patch: number }, +) { + if (version.major !== target.major) { + return version.major > target.major; + } + + if (version.minor !== target.minor) { + return version.minor > target.minor; + } + + return version.patch >= target.patch; +} + +function supportsRuntimeObservability(origin?: ObservabilityRuntimeOrigin) { + const version = parseStableVersion(origin?.version); + + if (!version) { + return false; + } + + return isVersionAtLeast(version, { + major: 2, + minor: 5, + patch: 0, + }); +} + +function createSharedInfo( + args: ObservabilitySharedLifecycleArgs, + reason?: string, +): ObservabilitySharedInfo { + const shareConfig = args.shareInfo?.shareConfig; + + return { + name: args.pkgName, + shareScope: getSharedScopes(args.shareInfo), + requiredVersion: shareConfig?.requiredVersion, + selectedVersion: args.selectedShared?.version, + availableVersions: getAvailableSharedVersions(args), + provider: args.selectedShared?.from, + from: args.shareInfo?.from, + singleton: shareConfig?.singleton, + strictVersion: shareConfig?.strictVersion, + eager: shareConfig?.eager, + strategy: args.shareInfo?.strategy, + loaded: args.selectedShared?.loaded, + loading: Boolean(args.selectedShared?.loading) || undefined, + reason, + }; +} + +function sanitizeShared( + shared: ObservabilitySharedInfo | undefined, +): ObservabilitySharedInfo | undefined { + if (!shared || !shared.name) { + return undefined; + } + + return { + name: sanitizeText(shared.name, 160) || 'unknown', + shareScope: normalizeSharedScope(shared.shareScope), + requiredVersion: + shared.requiredVersion === false + ? false + : sanitizeText(shared.requiredVersion, 120), + selectedVersion: sanitizeText(shared.selectedVersion, 120), + availableVersions: (shared.availableVersions || []) + .map((version) => sanitizeText(version, 120)) + .filter((version): version is string => Boolean(version)) + .slice(0, 20), + provider: sanitizeText(shared.provider, 160), + from: sanitizeText(shared.from, 160), + singleton: shared.singleton, + strictVersion: shared.strictVersion, + eager: shared.eager, + strategy: sanitizeText(shared.strategy, 80), + loaded: shared.loaded, + loading: shared.loading, + reason: sanitizeText(shared.reason, 120), + }; +} + +function getObjectValue(value: Record, key: string) { + return value[key]; +} + +function isReactLike(value: unknown): value is ObservabilityReactLike { + if (!isRecord(value)) { + return false; + } + + return typeof getObjectValue(value, 'createElement') === 'function'; +} + +function resolveReactLike(value: unknown): ObservabilityReactLike | undefined { + if (isReactLike(value)) { + return value; + } + + if (isRecord(value)) { + const defaultExport = getObjectValue(value, 'default'); + if (isReactLike(defaultExport)) { + return defaultExport; + } + } + + return undefined; +} + +function getReactComponentName(component: unknown, fallback: string) { + if (typeof component === 'function') { + const displayName = (component as { displayName?: string }).displayName; + return displayName || component.name || fallback; + } + + if (!isRecord(component)) { + return fallback; + } + + const displayName = getObjectValue(component, 'displayName'); + if (typeof displayName === 'string' && displayName) { + return displayName; + } + + const render = getObjectValue(component, 'render'); + if (typeof render === 'function') { + return render.displayName || render.name || fallback; + } + + return fallback; +} + +function isLikelyReactFunctionComponent( + component: unknown, + allowAnonymousComponent = false, +) { + if (typeof component !== 'function') { + return false; + } + + const name = + (component as { displayName?: string }).displayName || component.name || ''; + if (/^use[A-Z0-9]/.test(name)) { + return false; + } + + if (allowAnonymousComponent) { + return true; + } + + if (!name) { + return false; + } + + return /^[A-Z]/.test(name); +} + +function copyComponentStatics( + target: Record, + source: Record, +) { + const reserved = new Set([ + 'arguments', + 'caller', + 'length', + 'name', + 'prototype', + 'displayName', + ]); + + Object.getOwnPropertyNames(source).forEach((key) => { + if (reserved.has(key)) { + return; + } + + const descriptor = Object.getOwnPropertyDescriptor(source, key); + if (!descriptor || !descriptor.configurable) { + return; + } + + try { + Object.defineProperty(target, key, descriptor); + } catch { + // Static metadata is best effort and must not affect remote rendering. + } + }); +} + +function cloneModuleWithDefaultExport( + moduleExports: Record, + defaultExport: unknown, +) { + const descriptors = Object.getOwnPropertyDescriptors(moduleExports); + const defaultDescriptor = descriptors.default; + + descriptors.default = { + configurable: true, + enumerable: defaultDescriptor?.enumerable ?? true, + writable: true, + value: defaultExport, + }; + + return Object.defineProperties( + Object.create(Object.getPrototypeOf(moduleExports)), + descriptors, + ); +} + +function resolveReactComponentTarget( + component: unknown, + defaultExportMode: 'preserve' | 'component' = 'preserve', + allowAnonymousComponent = false, +): + | { + component: unknown; + createResult: (wrappedComponent: unknown) => unknown; + } + | undefined { + if (isLikelyReactFunctionComponent(component, allowAnonymousComponent)) { + return { + component, + createResult: (wrappedComponent) => wrappedComponent, + }; + } + + if (!isRecord(component)) { + return undefined; + } + + const defaultExport = getObjectValue(component, 'default'); + if (!isLikelyReactFunctionComponent(defaultExport, allowAnonymousComponent)) { + return undefined; + } + + return { + component: defaultExport, + createResult: (wrappedComponent) => { + const descriptor = Object.getOwnPropertyDescriptor(component, 'default'); + let defaultExportReplaced = false; + + try { + if (!descriptor || descriptor.writable || descriptor.set) { + component.default = wrappedComponent; + defaultExportReplaced = true; + } else if (descriptor.configurable) { + Object.defineProperty(component, 'default', { + configurable: true, + enumerable: descriptor.enumerable, + writable: true, + value: wrappedComponent, + }); + defaultExportReplaced = true; + } + } catch { + // If the module namespace is read-only, leave the remote module untouched. + } + + if (defaultExportMode === 'component') { + return wrappedComponent; + } + + return defaultExportReplaced + ? undefined + : cloneModuleWithDefaultExport(component, wrappedComponent); + }, + }; +} + +function normalizeEventSource( + value: ObservabilityEventSource | undefined, +): ObservabilityEventSource | undefined { + return value === 'runtime' || value === 'business' || value === 'react' + ? value + : undefined; +} + +function extractErrorCode(value: unknown): string | undefined { + const matched = String(value ?? '').match(ERROR_CODE_PATTERN)?.[0]; + return matched ? sanitizeText(matched, 40) : undefined; +} + +function getErrorInfo( + error: unknown, + stackTraceOptions?: ObservabilityPluginOptions['stackTrace'], +): { + errorCode?: string; + errorName?: string; + errorMessage?: string; + errorStack?: string; +} { + if (!error) { + return {}; + } + + if (error instanceof Error) { + return { + errorCode: extractErrorCode( + `${error.name}\n${error.message}\n${error.stack || ''}`, + ), + errorName: getRawText(error.name), + errorMessage: getRawText(error.message), + errorStack: sanitizeStack(error.stack, stackTraceOptions), + }; + } + + return { + errorCode: extractErrorCode(error), + errorMessage: getRawText(error), + }; +} + +function omitUndefinedFields(value: T): T { + if (Array.isArray(value)) { + return value.map((item) => omitUndefinedFields(item)) as T; + } + + if (!value || typeof value !== 'object') { + return value; + } + + const cleanValue: Record = {}; + + Object.entries(value as Record).forEach(([key, item]) => { + if (item === undefined) { + return; + } + + cleanValue[key] = omitUndefinedFields(item); + }); + + return cleanValue as T; +} + +function copyEvent(event: ObservabilityEvent): ObservabilityEvent { + return omitUndefinedFields({ + ...event, + remote: event.remote ? { ...event.remote } : undefined, + shared: event.shared + ? { + ...event.shared, + shareScope: event.shared.shareScope + ? [...event.shared.shareScope] + : undefined, + availableVersions: event.shared.availableVersions + ? [...event.shared.availableVersions] + : undefined, + } + : undefined, + errorContext: event.errorContext ? { ...event.errorContext } : undefined, + metadata: event.metadata ? { ...event.metadata } : undefined, + loadedBefore: copyLoadedBeforeInfo(event.loadedBefore), + }); +} + +function copySummary( + summary: ObservabilityReport['summary'], +): ObservabilityReport['summary'] { + return { + ...summary, + phases: Object.entries(summary.phases).reduce< + Record + >((memo, [phase, phaseSummary]) => { + memo[phase] = { ...phaseSummary }; + return memo; + }, {}), + shared: summary.shared + ? { + ...summary.shared, + shareScope: summary.shared.shareScope + ? [...summary.shared.shareScope] + : undefined, + } + : undefined, + flags: { ...summary.flags }, + error: summary.error + ? { + ...summary.error, + context: summary.error.context + ? { ...summary.error.context } + : undefined, + } + : undefined, + }; +} + +function copyFactReport( + diagnosis: ObservabilityFactReport | undefined, +): ObservabilityFactReport | undefined { + if (!diagnosis) { + return undefined; + } + + return { + ...diagnosis, + facts: { ...diagnosis.facts }, + completedPhases: [...diagnosis.completedPhases], + pendingPhases: [...diagnosis.pendingPhases], + warnings: diagnosis.warnings ? [...diagnosis.warnings] : undefined, + actions: diagnosis.actions.map((action) => ({ ...action })), + }; +} + +function copyModuleInfoSummary( + moduleInfo: ObservabilityModuleInfoSummary | undefined, +): ObservabilityModuleInfoSummary | undefined { + if (!moduleInfo) { + return undefined; + } + + return { + ...moduleInfo, + entries: moduleInfo.entries.map((entry) => ({ ...entry })), + availableNames: moduleInfo.availableNames + ? [...moduleInfo.availableNames] + : undefined, + }; +} + +function copyLoadedBeforeInfo( + loadedBefore: ObservabilityLoadedBeforeInfo | undefined, +): ObservabilityLoadedBeforeInfo | undefined { + if (!loadedBefore) { + return undefined; + } + + return { + producer: loadedBefore.producer, + expose: loadedBefore.expose, + consumers: loadedBefore.consumers.map((consumer) => ({ + ...consumer, + exposes: consumer.exposes ? [...consumer.exposes] : undefined, + })), + }; +} + +function copyReport(report: ObservabilityReport): ObservabilityReport { + return omitUndefinedFields({ + ...report, + remote: report.remote ? { ...report.remote } : undefined, + shared: report.shared + ? { + ...report.shared, + shareScope: report.shared.shareScope + ? [...report.shared.shareScope] + : undefined, + availableVersions: report.shared.availableVersions + ? [...report.shared.availableVersions] + : undefined, + } + : undefined, + errorContext: report.errorContext ? { ...report.errorContext } : undefined, + moduleInfo: copyModuleInfoSummary(report.moduleInfo), + loadedBefore: copyLoadedBeforeInfo(report.loadedBefore), + events: report.events.map(copyEvent), + summary: copySummary(report.summary), + diagnosis: copyFactReport(report.diagnosis), + }); +} + +function getFederationGlobal(): FederationObservabilityGlobal | undefined { + return ( + globalThis as { + __FEDERATION__?: FederationObservabilityGlobal; + } + ).__FEDERATION__; +} + +function normalizeExposeName(value: unknown): string | undefined { + const sanitized = sanitizeText(value, 240); + if (!sanitized) { + return undefined; + } + + return sanitized.replace(/^\.\//, ''); +} + +function getModuleCacheEntries( + moduleCache: ObservabilityRuntimeInstanceLike['moduleCache'], +): unknown[] { + if (!moduleCache) { + return []; + } + + if (moduleCache instanceof Map) { + return Array.from(moduleCache.values()); + } + + const entries = + typeof moduleCache.entries === 'function' + ? Array.from(moduleCache.entries()) + : undefined; + + if (entries) { + return entries.map(([, value]) => value); + } + + if (isRecord(moduleCache)) { + return Object.values(moduleCache); + } + + return []; +} + +function getLoadedExposesForRemote( + instance: ObservabilityRuntimeInstanceLike, + remoteName: string | undefined, +) { + if (!remoteName) { + return []; + } + + return Array.from( + new Set( + Object.values(instance.remoteHandler?.idToRemoteMap || {}) + .filter((item) => item?.name === remoteName) + .map((item) => sanitizeText(item.expose, 240)) + .filter((expose): expose is string => Boolean(expose)), + ), + ); +} + +function collectLoadedBeforeInfo( + remote: ObservabilityRemoteInfo | undefined, + expose: string | undefined, + origin?: ObservabilityRuntimeOrigin, +): ObservabilityLoadedBeforeInfo | undefined { + const entryGlobalName = remote?.entryGlobalName; + if (!entryGlobalName) { + return undefined; + } + + const federation = getFederationGlobal(); + const instances = Array.isArray(federation?.__INSTANCES__) + ? federation.__INSTANCES__ + : []; + const targetExpose = normalizeExposeName(expose); + const consumers: ObservabilityLoadedBeforeConsumer[] = []; + + instances.forEach((instance) => { + if (instance === origin) { + return; + } + + const matchedModule = getModuleCacheEntries(instance.moduleCache).find( + (item): item is ObservabilityRuntimeModuleLike => + isRecord(item) && + isRecord(item.remoteInfo) && + item.remoteInfo.entryGlobalName === entryGlobalName, + ); + + if (!matchedModule) { + return; + } + + const exposes = getLoadedExposesForRemote( + instance, + matchedModule.remoteInfo?.name, + ); + const consumer: ObservabilityLoadedBeforeConsumer = { + name: + sanitizeText(instance.options?.name, 120) || + sanitizeText(instance.name, 120), + remoteEntryExports: Boolean(matchedModule.remoteEntryExports), + containerInitialized: matchedModule.inited === true, + exposes: exposes.length ? exposes : undefined, + }; + + consumers.push(omitUndefinedFields(consumer)); + }); + + if (!consumers.length) { + return undefined; + } + + const exposeLoadedBefore = targetExpose + ? consumers.some((consumer) => + (consumer.exposes || []).some( + (loadedExpose) => normalizeExposeName(loadedExpose) === targetExpose, + ), + ) + : false; + + return { + producer: true, + expose: exposeLoadedBefore, + consumers, + }; +} + +function normalizeScope(value: unknown) { + const sanitized = sanitizeText(value, 120); + const normalized = sanitized?.replace(/[^\w:@.-]+/g, '-'); + + return normalized || 'default'; +} + +function shouldRecordEvent( + level: ObservabilityLevel, + event: ObservabilityRuntimeEventInput, +) { + if (level === 'verbose') { + return true; + } + + if (level === 'summary') { + return event.status !== 'start'; + } + + return event.status === 'error' || Boolean(event.error); +} + +function createTraceId(event: ObservabilityRuntimeEventInput) { + traceCounter += 1; + const owner = event.remote?.name || event.phase || 'runtime'; + const normalizedOwner = owner.replace(/[^a-z0-9]+/gi, '-').slice(0, 80); + + return `mf-${normalizedOwner}-${Date.now().toString(36)}-${traceCounter.toString( + 36, + )}`; +} + +function getPhaseDurationKey(event: ObservabilityEvent) { + const exposeKey = + event.phase === 'expose' || event.phase === 'moduleFactory' + ? event.expose || '' + : ''; + + return [ + event.traceId, + event.phase, + event.requestId || event.remote?.name || event.shared?.name || '', + exposeKey, + ].join('|'); +} + +function getRemoteEntryKey( + remote: ObservabilityRemoteInfo | undefined, +): string | undefined { + if (!remote?.name) { + return undefined; + } + + return [remote.name, remote.entryGlobalName || '', remote.entry || ''].join( + '|', + ); +} + +function getHostRemotesSummary( + options: ObservabilityRuntimeOptions | undefined, +): string | undefined { + const remotes = (options?.remotes || []) + .map((remote) => clipText(remote.alias || remote.name || remote.entry, 120)) + .filter((remote): remote is string => Boolean(remote)) + .slice(0, 20); + + return remotes.length ? remotes.join(',') : undefined; +} + +function resolveRemoteFromRequestId( + id: string | undefined, + options: ObservabilityRuntimeOptions | undefined, +): ObservabilityRemoteInfo | undefined { + if (!id) { + return undefined; + } + + const matchedRemote = (options?.remotes || []) + .filter((remote) => { + const keys = [remote.alias, remote.name].filter((key): key is string => + Boolean(key), + ); + + return keys.some((key) => id === key || id.startsWith(`${key}/`)); + }) + .sort((left, right) => { + const leftKey = left.alias || left.name || ''; + const rightKey = right.alias || right.name || ''; + + return rightKey.length - leftKey.length; + })[0]; + + return createRemoteInfo(matchedRemote); +} + +function resolveAliasRequestId( + requestId: string | undefined, + remote: ObservabilityRemoteInfo | undefined, +): string | undefined { + if (!requestId || !remote?.alias || remote.alias === remote.name) { + return undefined; + } + + if (requestId === remote.name) { + return remote.alias; + } + + if (requestId.startsWith(`${remote.name}/`)) { + return `${remote.alias}/${requestId.slice(remote.name.length + 1)}`; + } + + return undefined; +} + +function sanitizeModuleInfoPath(value: unknown): string | undefined { + if (typeof value !== 'string') { + return undefined; + } + + return clipText(value, 320); +} + +function sanitizeModuleInfoGetPublicPath(value: unknown): string | undefined { + if (typeof value !== 'string') { + return undefined; + } + + return clipText(value, 500); +} + +function sanitizeModuleInfoRemoteEntry(value: unknown): string | undefined { + if (typeof value !== 'string') { + return undefined; + } + + return clipText(value, 320); +} + +function createClippedModuleInfoEntry( + rawName: string, + rawValue: unknown, +): ObservabilityModuleInfoEntry | undefined { + const name = clipText(rawName, 240); + if (!name) { + return undefined; + } + + const value = isRecord(rawValue) ? rawValue : {}; + + return { + name, + publicPath: sanitizeModuleInfoPath(value['publicPath']), + getPublicPath: sanitizeModuleInfoGetPublicPath(value['getPublicPath']), + remoteEntry: sanitizeModuleInfoRemoteEntry(value['remoteEntry']), + globalName: sanitizeText(value['globalName'], 160), + }; +} + +function normalizeModuleInfoLookupValue(value: unknown): string | undefined { + if (typeof value !== 'string' || !value) { + return undefined; + } + + const sanitized = + /^https?:\/\//i.test(value) || value.startsWith('/') + ? sanitizeUrl(value) + : sanitizeText(value, 240); + + return sanitized?.toLowerCase(); +} + +function getModuleInfoLookupValues(report: ObservabilityReport): Set { + return new Set( + [ + report.requestId?.split('/')[0], + report.remote?.name, + report.remote?.alias, + report.remote?.entry, + report.remote?.entryGlobalName, + report.sanitizedUrl, + report.errorContext?.['remoteName'], + report.errorContext?.['remoteAlias'], + report.errorContext?.['url'], + report.summary.error?.context?.['remoteName'], + report.summary.error?.context?.['remoteAlias'], + report.summary.error?.context?.['url'], + ] + .map(normalizeModuleInfoLookupValue) + .filter((value): value is string => Boolean(value)), + ); +} + +function matchesModuleInfoLookup( + entry: ObservabilityModuleInfoEntry, + lookupValues: Set, +): boolean { + if (!lookupValues.size) { + return false; + } + + const entryValues = [ + entry.name, + entry.publicPath, + entry.getPublicPath, + entry.remoteEntry, + entry.globalName, + ] + .map(normalizeModuleInfoLookupValue) + .filter((value): value is string => Boolean(value)); + + return entryValues.some((entryValue) => + Array.from(lookupValues).some( + (lookupValue) => + entryValue === lookupValue || + entryValue.startsWith(`${lookupValue}:`) || + entryValue.includes(`:${lookupValue}`) || + (lookupValue.startsWith('http') && entryValue.includes(lookupValue)), + ), + ); +} + +function getModuleInfoCaptureReason( + report: ObservabilityReport, +): string | undefined { + const text = [ + report.errorCode, + report.errorName, + report.errorMessage, + report.summary.error?.errorCode, + report.summary.error?.errorName, + report.summary.error?.errorMessage, + ...report.events.flatMap((event) => [ + event.errorCode, + event.errorName, + event.errorMessage, + event.message, + event.lifecycle, + ]), + ].join('\n'); + + if (/RUNTIME-007/.test(text)) { + return 'remote-snapshot'; + } + if (/RUNTIME-011/.test(text)) { + return 'remote-entry-missing-in-snapshot'; + } + if (/moduleInfo|module info/i.test(text)) { + return 'module-info'; + } + if (/remote snapshot|global snapshot|snapshot/i.test(text)) { + return 'remote-snapshot'; + } + + return undefined; +} + +function createModuleInfoSummary( + report: ObservabilityReport, +): ObservabilityModuleInfoSummary | undefined { + const reason = getModuleInfoCaptureReason(report); + if (!reason) { + return undefined; + } + + const moduleInfo = getFederationGlobal()?.moduleInfo; + const rawEntries = isRecord(moduleInfo) ? Object.entries(moduleInfo) : []; + const clippedEntries = rawEntries + .map(([name, value]) => createClippedModuleInfoEntry(name, value)) + .filter((entry): entry is ObservabilityModuleInfoEntry => Boolean(entry)); + const lookupValues = getModuleInfoLookupValues(report); + const matchedEntries = clippedEntries.filter((entry) => + matchesModuleInfoLookup(entry, lookupValues), + ); + + return { + reason, + clipped: true, + totalCount: rawEntries.length, + matchedCount: matchedEntries.length, + entries: matchedEntries.slice(0, MAX_MODULE_INFO_ENTRIES), + availableNames: matchedEntries.length + ? undefined + : clippedEntries + .map((entry) => entry.name) + .slice(0, MAX_MODULE_INFO_ENTRIES), + }; +} + +function getResourceErrorType( + event: Pick< + ObservabilityEvent, + 'errorCode' | 'errorMessage' | 'message' | 'lifecycle' + >, +): string | undefined { + const text = `${event.errorMessage || ''}\n${event.message || ''}`; + + if (!event.errorCode && !text) { + return undefined; + } + + if (/ScriptExecutionError/i.test(text)) { + return 'script-execution'; + } + + if (/timeout|timed out/i.test(text)) { + return 'timeout'; + } + + if ( + /ScriptNetworkError|NetworkError|Failed to fetch|Request failed|ERR_|404|CORS/i.test( + text, + ) + ) { + return 'network'; + } + + return event.errorCode === 'RUNTIME-008' ? 'unknown' : undefined; +} + +function getOwnerHint( + event: Pick< + ObservabilityEvent, + | 'errorCode' + | 'phase' + | 'shared' + | 'remote' + | 'errorMessage' + | 'message' + | 'lifecycle' + >, +): ObservabilityOwnerHint | undefined { + const resourceErrorType = getResourceErrorType(event); + + switch (event.errorCode) { + case 'RUNTIME-001': + case 'RUNTIME-002': + case 'RUNTIME-011': + case 'RUNTIME-013': + case 'RUNTIME-014': + case 'RUNTIME-015': + return 'remote'; + case 'RUNTIME-003': + case 'RUNTIME-004': + case 'RUNTIME-007': + return 'host'; + case 'RUNTIME-005': + case 'RUNTIME-006': + case 'RUNTIME-012': + return 'shared'; + case 'RUNTIME-008': + return resourceErrorType === 'network' || resourceErrorType === 'timeout' + ? 'network' + : 'remote'; + default: + if (event.shared) { + return 'shared'; + } + if (event.remote) { + return 'remote'; + } + if (event.phase === 'manifest' || event.phase === 'matchRemote') { + return 'host'; + } + return event.errorCode ? 'runtime' : undefined; + } +} + +function getRetryable( + event: Pick< + ObservabilityEvent, + 'errorCode' | 'errorMessage' | 'message' | 'lifecycle' + >, +): boolean | undefined { + const resourceErrorType = getResourceErrorType(event); + + if (event.errorCode === 'RUNTIME-008') { + return resourceErrorType === 'network' || resourceErrorType === 'timeout'; + } + + if (event.errorCode === 'RUNTIME-003') { + const text = `${event.errorMessage || ''}\n${event.message || ''}`; + return /NetworkError|Failed to fetch|Request failed|timeout|timed out/i.test( + text, + ); + } + + if ( + event.errorCode && + [ + 'RUNTIME-001', + 'RUNTIME-002', + 'RUNTIME-004', + 'RUNTIME-005', + 'RUNTIME-006', + 'RUNTIME-011', + 'RUNTIME-012', + 'RUNTIME-013', + 'RUNTIME-014', + 'RUNTIME-015', + ].includes(event.errorCode) + ) { + return false; + } + + return undefined; +} + +function createErrorContext( + event: ObservabilityEvent, + inputContext?: Record, +): ObservabilityMetadata | undefined { + const context: Record = { + ...inputContext, + }; + + if (event.lifecycle) { + context.lifecycle = event.lifecycle; + } + if (event.requestId) { + context.requestId = event.requestId; + } + if (event.requestAlias) { + context.requestAlias = event.requestAlias; + } + if (event.remote?.name) { + context.remoteName = event.remote.name; + } + if (event.remote?.alias) { + context.remoteAlias = event.remote.alias; + } + if (event.remote?.type) { + context.remoteType = event.remote.type; + } + if (event.remote?.entryGlobalName) { + context.entryGlobalName = event.remote.entryGlobalName; + } + if (event.sanitizedUrl) { + context.url = event.sanitizedUrl; + } + if (event.expose) { + context.expose = event.expose; + } + if (event.shared?.name) { + context.shareName = event.shared.name; + } + if (event.shared?.requiredVersion) { + context.requiredVersion = event.shared.requiredVersion; + } + if (event.shared?.selectedVersion) { + context.selectedVersion = event.shared.selectedVersion; + } + if (event.shared?.provider) { + context.provider = event.shared.provider; + } + + const resourceErrorType = getResourceErrorType(event); + if (resourceErrorType) { + context.resourceErrorType = resourceErrorType; + } + + return clipObservabilityMetadata(context); +} + +export function createObservability( + rawOptions: ObservabilityPluginOptions = {}, + adapterOptions: ObservabilityRuntimeAdapterOptions = {}, +): ObservabilityController { + const options: ObservabilityPluginOptions = { + ...rawOptions, + browser: adapterOptions.fixedBrowserScope + ? { + ...rawOptions.browser, + scope: adapterOptions.fixedBrowserScope, + } + : rawOptions.browser, + react: adapterOptions.disableReact + ? { + ...rawOptions.react, + enabled: false, + injectLoadedCallback: false, + } + : rawOptions.react, + }; + const pluginName = adapterOptions.pluginName || 'observability-plugin'; + const shouldAttachInstanceApi = adapterOptions.attachInstanceApi !== false; + const shouldGuardSharedHooksByRuntimeVersion = + adapterOptions.guardSharedHooksByRuntimeVersion === true; + const shouldGuardRuntimeHooksByRuntimeVersion = + adapterOptions.guardRuntimeHooksByRuntimeVersion === true; + const shouldDisablePreloadHooks = adapterOptions.disablePreloadHooks === true; + const shouldReturnHookArgs = adapterOptions.returnHookArgs === true; + const shouldForceDevelopmentChannels = + adapterOptions.forceDevelopmentChannels === true; + const returnHookArgs = (args: T): T | undefined => + shouldReturnHookArgs ? args : undefined; + const level = options.level || 'summary'; + const configuredMaxEvents = normalizeMaxEvents( + options.maxEvents, + DEFAULT_MAX_EVENTS, + ); + const events: ObservabilityEvent[] = []; + const reports = new Map(); + const traceByRequest = new Map(); + const traceByRemote = new Map(); + const phaseStartTimes = new Map(); + const collectorOptions = normalizeCollectorOptions(options.collector); + const devtoolsOptions = normalizeDevtoolsOptions(options.devtools); + const seenManifestUrls = new Set(); + const loadingManifestUrls = new Set(); + const seenRemoteEntryKeys = new Set(); + const consoleReportedTraceIds = new Set(); + const consoleReportedStartKeys = new Set(); + let latestTraceId: string | undefined; + let runtimeObservabilityEnabled = false; + let suppressRuntimeEvents = false; + let effectiveMaxEvents = configuredMaxEvents; + let browserGlobalScope: string | undefined; + let lastRuntimeOrigin: ObservabilityRuntimeOrigin | undefined; + let appliedRuntimeVersion: string | undefined; + + const isEnabled = () => { + if (options.enabled === false) { + return false; + } + + runtimeObservabilityEnabled = true; + return true; + }; + + const resolveTraceId = (event: ObservabilityRuntimeEventInput) => { + const sanitizedRequestId = sanitizeRequestId(event.requestId); + + if (event.traceId && reports.has(event.traceId)) { + return event.traceId; + } + + if (event.status === 'start' && event.phase === 'loadRemote') { + const traceId = event.traceId || createTraceId(event); + if (sanitizedRequestId) { + traceByRequest.set(sanitizedRequestId, traceId); + } + if (event.remote?.name) { + traceByRemote.set(event.remote.name, traceId); + } + return traceId; + } + + if (sanitizedRequestId) { + const traceId = traceByRequest.get(sanitizedRequestId); + if (traceId) { + return traceId; + } + } + + if (event.remote?.name) { + const traceId = traceByRemote.get(event.remote.name); + if (traceId) { + return traceId; + } + } + + return event.traceId || createTraceId(event); + }; + + const normalizeEvent = ( + event: ObservabilityRuntimeEventInput, + traceId: string, + origin?: ObservabilityRuntimeOrigin, + ): ObservabilityEvent => { + const errorInfo = getErrorInfo(event.error, options.stackTrace); + const sanitizedRemote = sanitizeRemote(event.remote); + const sanitizedShared = sanitizeShared(event.shared); + const requestAlias = + sanitizeRequestId(event.requestAlias) || + resolveAliasRequestId(event.requestId, sanitizedRemote); + const hostName = + sanitizeText(event.hostName, 120) || + sanitizeText(origin?.options?.name, 120); + const runtimeVersion = + sanitizeText(origin?.version, 80) || appliedRuntimeVersion; + const message = getRawText(event.message) || errorInfo.errorMessage; + + const normalizedEvent: ObservabilityEvent = { + traceId, + timestamp: event.timestamp || Date.now(), + phase: sanitizeText(event.phase, 120) || 'runtime', + status: event.status, + requestId: sanitizeRequestId(event.requestId), + requestAlias, + hostName, + runtimeVersion, + remote: sanitizedRemote, + shared: sanitizedShared, + expose: sanitizeText(event.expose, 240), + sanitizedUrl: clipText(event.url || event.remote?.entry, 320), + message, + errorCode: errorInfo.errorCode, + errorName: errorInfo.errorName, + errorMessage: errorInfo.errorMessage, + errorStack: errorInfo.errorStack, + duration: + typeof event.duration === 'number' && Number.isFinite(event.duration) + ? Math.max(0, event.duration) + : undefined, + lifecycle: sanitizeText(event.lifecycle, 120), + eventName: sanitizeText(event.eventName, 160), + source: normalizeEventSource(event.source), + recovered: event.recovered === true || undefined, + cached: event.cached === true || undefined, + componentName: sanitizeText(event.componentName, 160), + metadata: clipObservabilityMetadata(event.metadata), + loadedBefore: copyLoadedBeforeInfo(event.loadedBefore), + }; + + if (normalizedEvent.status === 'error' || event.error) { + normalizedEvent.ownerHint = getOwnerHint(normalizedEvent); + normalizedEvent.retryable = getRetryable(normalizedEvent); + normalizedEvent.errorContext = createErrorContext( + normalizedEvent, + event.errorContext, + ); + } + + return normalizedEvent; + }; + + const supportsRuntimeHookObservability = ( + origin?: ObservabilityRuntimeOrigin, + ) => + supportsRuntimeObservability({ + ...origin, + version: + sanitizeText(origin?.version, 80) || + appliedRuntimeVersion || + origin?.version, + } as ObservabilityRuntimeOrigin); + + const shouldSkipRuntimeHook = (origin?: ObservabilityRuntimeOrigin) => + shouldGuardRuntimeHooksByRuntimeVersion && + !supportsRuntimeHookObservability(origin); + + const applyPhaseDuration = (event: ObservabilityEvent) => { + const key = getPhaseDurationKey(event); + + if (event.status === 'start') { + phaseStartTimes.set(key, event.timestamp); + return; + } + + if (event.duration !== undefined) { + return; + } + + const startedAt = phaseStartTimes.get(key); + if (startedAt === undefined) { + return; + } + + event.duration = Math.max(0, event.timestamp - startedAt); + phaseStartTimes.delete(key); + }; + + const updateTraceMaps = (event: ObservabilityEvent) => { + if (event.requestId) { + traceByRequest.set(event.requestId, event.traceId); + } + + if (event.remote?.name) { + traceByRemote.set(event.remote.name, event.traceId); + } + }; + + const trimEvents = (report: ObservabilityReport) => { + while (events.length > effectiveMaxEvents) { + events.shift(); + } + + while (report.events.length > effectiveMaxEvents) { + report.events.shift(); + } + }; + + const getEventOutcome = (event: ObservabilityEvent) => { + if (event.status === 'success') { + return 'success'; + } + + if (event.status === 'error') { + return 'error'; + } + + if (event.status === 'complete') { + if (event.recovered) { + return 'recovered'; + } + + if (event.errorName || event.errorMessage) { + return 'error'; + } + } + + return undefined; + }; + + const isLoadRemoteCompleteEvent = (event: ObservabilityEvent) => + event.phase === 'loadRemote' && event.status === 'complete'; + + const isRuntimeLoadedEvent = (event: ObservabilityEvent) => + event.phase === 'loadRemote' && + (event.status === 'success' || + (event.status === 'complete' && event.recovered)); + + const isSharedResolvedEvent = (event: ObservabilityEvent) => + event.phase === 'shared' && + (event.status === 'success' || + (event.status === 'complete' && event.recovered)); + + const isPreloadedEvent = (event: ObservabilityEvent) => + event.phase === 'preload' && event.status === 'success'; + + const isComponentLoadedEvent = (event: ObservabilityEvent) => + event.status === 'success' && + (event.eventName === COMPONENT_BUSINESS_LOADED_EVENT || + (event.phase === 'component' && + event.message === COMPONENT_BUSINESS_LOADED_EVENT)); + + const shouldReplaceFailedPhase = ( + report: ObservabilityReport, + event: ObservabilityEvent, + ) => { + if (isLoadRemoteCompleteEvent(event) && report.failedPhase) { + return false; + } + + if (!report.failedPhase) { + return true; + } + + return report.failedPhase === 'loadRemote' && event.phase !== 'loadRemote'; + }; + + const createEmptyPhaseCollection = (): ObservabilityPhaseCollection => ({ + phases: {}, + flags: { + cached: false, + fallback: false, + recovered: false, + }, + }); + + const createPhaseCollection = ( + eventsForReport: ObservabilityEvent[], + ): ObservabilityPhaseCollection => { + const collection = createEmptyPhaseCollection(); + + eventsForReport.forEach((event) => { + const phase = event.phase; + const phaseSummary = + collection.phases[phase] || + ({ + status: event.status, + } satisfies ObservabilityPhaseSummary); + + if (event.status !== 'start') { + phaseSummary.status = event.status; + } + if (event.duration !== undefined) { + phaseSummary.duration = event.duration; + } + if (event.cached) { + phaseSummary.cached = true; + collection.flags.cached = true; + } + if (event.recovered) { + phaseSummary.recovered = true; + collection.flags.recovered = true; + } + if (event.lifecycle) { + phaseSummary.lifecycle = event.lifecycle; + } + + collection.phases[phase] = phaseSummary; + + if ( + event.phase === 'loadRemote' && + event.status === 'complete' && + event.recovered + ) { + collection.flags.fallback = true; + } + if (event.shared?.selectedVersion || event.shared?.provider) { + collection.shared = { + name: event.shared.name, + provider: event.shared.provider, + selectedVersion: event.shared.selectedVersion, + shareScope: event.shared.shareScope + ? [...event.shared.shareScope] + : undefined, + }; + } + }); + + return collection; + }; + + const createErrorSummary = ( + eventsForReport: ObservabilityEvent[], + failedPhase?: string, + ): ObservabilityErrorSummary | undefined => { + const errorEvent = + eventsForReport.find( + (event) => event.status === 'error' && event.phase === failedPhase, + ) || + eventsForReport.find((event) => event.status === 'error') || + eventsForReport.find( + (event) => event.status === 'complete' && event.errorMessage, + ); + + if (!errorEvent) { + return undefined; + } + + return { + errorCode: errorEvent.errorCode, + errorName: errorEvent.errorName, + errorMessage: errorEvent.errorMessage, + failedPhase: failedPhase || errorEvent.phase, + lifecycle: errorEvent.lifecycle, + ownerHint: errorEvent.ownerHint, + retryable: errorEvent.retryable, + context: errorEvent.errorContext + ? { ...errorEvent.errorContext } + : undefined, + }; + }; + + const createReportSummary = ( + report: ObservabilityReport, + ): ObservabilityReport['summary'] => { + const loadCompleted = report.events.some(isLoadRemoteCompleteEvent); + const runtimeLoaded = report.events.some(isRuntimeLoadedEvent); + const sharedResolved = report.events.some(isSharedResolvedEvent); + const preloaded = report.events.some(isPreloadedEvent); + const recovered = report.events.some((item) => item.recovered); + const componentLoaded = report.events.some(isComponentLoadedEvent); + const lastEvent = report.events[report.events.length - 1]; + let outcome: ObservabilityReportOutcome = 'pending'; + + if (recovered) { + outcome = 'recovered'; + } else if (componentLoaded) { + outcome = 'component-loaded'; + } else if (report.status === 'error') { + outcome = 'failed'; + } else if (runtimeLoaded) { + outcome = 'runtime-loaded'; + } else if (sharedResolved) { + outcome = 'shared-resolved'; + } else if (preloaded) { + outcome = 'preloaded'; + } + + const phaseCollection = createPhaseCollection(report.events); + + return { + eventCount: report.events.length, + recovered, + loadCompleted, + runtimeLoaded, + sharedResolved, + preloaded, + componentLoaded, + outcome, + lastPhase: lastEvent?.phase, + phases: phaseCollection.phases, + shared: phaseCollection.shared, + flags: phaseCollection.flags, + error: createErrorSummary(report.events, report.failedPhase), + }; + }; + + const refreshModuleInfoSummary = (report: ObservabilityReport) => { + const moduleInfo = createModuleInfoSummary(report); + if (moduleInfo) { + report.moduleInfo = moduleInfo; + } + }; + + const getReportContext = ( + report: ObservabilityReport, + ): ObservabilityMetadata | undefined => + report.summary.error?.context || report.errorContext; + + const getContextText = ( + context: ObservabilityMetadata | undefined, + key: string, + ): string | undefined => { + const value = context?.[key]; + return typeof value === 'string' && value ? value : undefined; + }; + + const getDiagnosisOwnerHint = ( + report: ObservabilityReport, + ): ObservabilityOwnerHint => + report.summary.error?.ownerHint || + report.ownerHint || + (report.shared ? 'shared' : report.remote ? 'remote' : 'unknown'); + + const getDiagnosisResourceErrorType = ( + report: ObservabilityReport, + ): string | undefined => + getContextText(getReportContext(report), 'resourceErrorType') || + getResourceErrorType({ + errorCode: report.errorCode, + errorMessage: report.errorMessage, + message: report.events.at(-1)?.message, + lifecycle: report.summary.error?.lifecycle, + }); + + const getDiagnosisDocLink = ( + report: ObservabilityReport, + ): string | undefined => { + const text = [ + report.errorMessage, + report.errorStack, + ...report.events.flatMap((event) => [ + event.errorMessage, + event.errorStack, + event.message, + ]), + ] + .filter((item): item is string => Boolean(item)) + .join('\n'); + const matched = text.match(DIAGNOSTIC_DOC_LINK_PATTERN)?.[0]; + const docLink = sanitizeText(matched, 240); + + if (docLink) { + return docLink; + } + + return report.errorCode?.startsWith('RUNTIME-') + ? RUNTIME_DOC_LINK + : undefined; + }; + + const getDiagnosisTitle = (report: ObservabilityReport) => { + if (report.status !== 'error') { + if (report.shared) { + if (report.summary.sharedResolved) { + return 'Shared dependency resolved successfully'; + } + return 'Shared dependency loading is pending'; + } + if (report.summary.componentLoaded) { + return 'Business component loaded'; + } + if (report.summary.runtimeLoaded) { + return 'Remote loaded successfully'; + } + if (report.summary.preloaded) { + return 'Remote preloaded successfully'; + } + return 'Remote loading is pending'; + } + + switch (report.errorCode) { + case 'RUNTIME-001': + return 'Remote entry global was not registered'; + case 'RUNTIME-003': + return 'Manifest could not be loaded'; + case 'RUNTIME-004': + return 'Remote was not found in host remotes'; + case 'RUNTIME-007': + return 'Deployment moduleInfo did not match the requested remote'; + case 'RUNTIME-013': + return 'Manifest is not a valid Module Federation manifest'; + case 'RUNTIME-014': + return 'Requested expose was not found in the remote'; + case 'RUNTIME-015': + return 'Remote container initialization failed'; + case 'RUNTIME-005': + case 'RUNTIME-006': + return 'Shared dependency could not be resolved'; + case 'RUNTIME-008': { + const resourceErrorType = getDiagnosisResourceErrorType(report); + if (resourceErrorType === 'network') { + return 'Remote entry failed because of a network error'; + } + if (resourceErrorType === 'timeout') { + return 'Remote entry request timed out'; + } + if (resourceErrorType === 'script-execution') { + return 'Remote entry loaded but failed during execution'; + } + return 'Remote entry resource could not be loaded'; + } + default: + if (report.failedPhase === 'shared' || report.shared) { + return 'Shared dependency could not be resolved'; + } + return report.failedPhase + ? `Module Federation failed at ${report.failedPhase}` + : 'Module Federation loading failed'; + } + }; + + const getCompletedPhases = (report: ObservabilityReport) => + Array.from( + new Set( + report.events + .filter( + (event) => + event.status === 'success' || event.status === 'complete', + ) + .map((event) => event.phase), + ), + ); + + const getPendingPhases = (report: ObservabilityReport) => { + const started = new Set(); + const ended = new Set(); + + report.events.forEach((event) => { + if (event.status === 'start') { + started.add(event.phase); + return; + } + + ended.add(event.phase); + }); + + return Array.from(started).filter((phase) => !ended.has(phase)); + }; + + const createDiagnosisFacts = ( + report: ObservabilityReport, + ownerHint: ObservabilityOwnerHint, + ): ObservabilityMetadata => { + const context = getReportContext(report); + const facts: Record = {}; + const addFact = (key: string, value: unknown) => { + if (value === undefined || value === null || value === '') { + return; + } + + facts[key] = Array.isArray(value) ? value.join(',') : value; + }; + + addFact('traceId', report.traceId); + addFact('status', report.status); + addFact('outcome', report.summary.outcome); + addFact('errorCode', report.errorCode || report.summary.error?.errorCode); + addFact( + 'failedPhase', + report.failedPhase || report.summary.error?.failedPhase, + ); + addFact('lifecycle', report.summary.error?.lifecycle); + addFact('ownerHint', ownerHint); + addFact('retryable', report.retryable ?? report.summary.error?.retryable); + addFact('requestId', report.requestId); + addFact( + 'requestAlias', + report.requestAlias || report.summary.error?.context?.['requestAlias'], + ); + addFact('hostName', report.hostName); + addFact('remoteName', report.remote?.name); + addFact('remoteAlias', report.remote?.alias); + addFact('remoteEntry', report.remote?.entry); + addFact('entryGlobalName', report.remote?.entryGlobalName); + addFact('remoteType', report.remote?.type); + addFact('url', report.sanitizedUrl || getContextText(context, 'url')); + addFact('expose', report.expose); + addFact('hostRemotes', getContextText(context, 'hostRemotes')); + addFact('resourceErrorType', getDiagnosisResourceErrorType(report)); + addFact('shareName', report.shared?.name); + addFact('shareScope', report.shared?.shareScope); + addFact('requiredVersion', report.shared?.requiredVersion); + addFact('selectedVersion', report.shared?.selectedVersion); + addFact('availableVersions', report.shared?.availableVersions); + addFact('provider', report.shared?.provider); + addFact('sharedFrom', report.shared?.from); + addFact('singleton', report.shared?.singleton); + addFact('strictVersion', report.shared?.strictVersion); + addFact('eager', report.shared?.eager); + addFact('sharedReason', report.shared?.reason); + addFact( + 'componentName', + report.events.find(isComponentLoadedEvent)?.componentName, + ); + addFact('moduleInfoReason', report.moduleInfo?.reason); + addFact('moduleInfoTotalCount', report.moduleInfo?.totalCount); + addFact('moduleInfoMatchedCount', report.moduleInfo?.matchedCount); + addFact( + 'moduleInfoNames', + report.moduleInfo?.entries.length + ? report.moduleInfo.entries.map((entry) => entry.name) + : report.moduleInfo?.availableNames, + ); + addFact('cached', report.summary.flags.cached); + addFact('fallback', report.summary.flags.fallback); + addFact('recovered', report.summary.recovered); + addFact('loadCompleted', report.summary.loadCompleted); + addFact('runtimeLoaded', report.summary.runtimeLoaded); + addFact('componentLoaded', report.summary.componentLoaded); + + return clipMetadata(facts, MAX_FACT_KEYS) || {}; + }; + + const createDiagnosisWarnings = (report: ObservabilityReport) => { + const warnings: string[] = []; + + if (report.status === 'error' && !report.errorCode) { + warnings.push('No known Module Federation error code was captured'); + } + if (report.summary.flags.fallback) { + warnings.push('Remote loading completed through fallback recovery'); + } + if (report.summary.runtimeLoaded && !report.summary.componentLoaded) { + warnings.push('Business component readiness signal was not recorded'); + } + if (report.moduleInfo && report.moduleInfo.matchedCount === 0) { + warnings.push( + 'No matching clipped moduleInfo entry was found for the failed remote', + ); + } + + return warnings; + }; + + const createDiagnosisActions = ( + report: ObservabilityReport, + ownerHint: ObservabilityOwnerHint, + ): ObservabilityAction[] => { + const actions: ObservabilityAction[] = []; + const pushAction = ( + id: ObservabilityActionId, + title: string, + hint: ObservabilityOwnerHint = ownerHint, + detail?: string, + ) => { + actions.push({ + id, + ownerHint: hint, + title, + detail, + }); + }; + + if (report.status !== 'error' && !report.summary.error) { + return actions; + } + + switch (report.errorCode) { + case 'RUNTIME-001': + pushAction( + 'check-remote-global', + 'Check the remote global name against the remoteEntry build output', + 'remote', + ); + pushAction( + 'check-remote-entry', + 'Check that remoteEntry registers the expected container', + 'remote', + ); + break; + case 'RUNTIME-003': + pushAction( + 'check-manifest-url', + 'Check the manifest URL and manifest JSON response', + 'host', + ); + pushAction( + 'check-network', + 'Check network availability, CORS, and timeout for the manifest', + 'network', + ); + break; + case 'RUNTIME-013': + pushAction( + 'check-manifest-url', + 'Check that the manifest response is valid Module Federation JSON', + 'remote', + ); + break; + case 'RUNTIME-004': + pushAction( + 'check-host-remotes', + 'Check that the requested remote exists in host remotes', + 'host', + ); + break; + case 'RUNTIME-007': + pushAction( + 'check-module-info', + 'Check deployment-provided __FEDERATION__.moduleInfo for the requested remote', + 'host', + ); + pushAction( + 'check-host-remotes', + 'Check that the runtime remote name or alias matches moduleInfo', + 'host', + ); + break; + case 'RUNTIME-014': + pushAction( + 'check-expose', + 'Check that the requested expose exists in the remote build output', + 'remote', + ); + break; + case 'RUNTIME-015': + pushAction( + 'check-remote-entry', + 'Check the error thrown during remoteEntry init', + 'remote', + ); + pushAction( + 'check-shared-provider', + 'Check share scope initialization data passed to the remote', + 'shared', + ); + break; + case 'RUNTIME-005': + case 'RUNTIME-006': + pushAction( + 'check-shared-provider', + 'Check that a compatible shared provider is available', + 'shared', + ); + pushAction( + 'check-shared-version', + 'Compare requested shared version with available versions', + 'shared', + ); + if ( + report.summary.error?.lifecycle === 'loadShareSync' || + report.shared?.reason === 'sync-async-boundary' || + report.shared?.eager === false + ) { + pushAction( + 'check-eager-config', + 'Check eager configuration or add an async boundary before sync shared consumption', + 'shared', + ); + } + break; + case 'RUNTIME-008': { + const resourceErrorType = getDiagnosisResourceErrorType(report); + if ( + resourceErrorType === 'network' || + resourceErrorType === 'timeout' + ) { + pushAction( + 'check-network', + 'Check remoteEntry URL, CORS, status code, and timeout', + 'network', + ); + } + pushAction( + 'check-remote-entry', + resourceErrorType === 'script-execution' + ? 'Check remoteEntry execution errors in the remote build output' + : 'Check that remoteEntry is reachable and serves JavaScript', + resourceErrorType === 'network' || resourceErrorType === 'timeout' + ? 'network' + : 'remote', + ); + break; + } + default: + if (report.failedPhase === 'manifest') { + pushAction( + 'check-manifest-url', + 'Check manifest loading and parsing', + 'host', + ); + } + if (report.failedPhase === 'remoteEntry') { + pushAction( + 'check-remote-entry', + 'Check remoteEntry loading and initialization', + 'remote', + ); + } + if (report.failedPhase === 'expose') { + pushAction( + 'check-expose', + 'Check that the requested expose exists in the remote', + 'remote', + ); + } + if (report.failedPhase === 'shared') { + pushAction( + 'check-shared-provider', + 'Check shared dependency resolution', + 'shared', + ); + if ( + report.shared?.requiredVersion !== undefined || + report.shared?.availableVersions?.length || + report.shared?.reason === 'version-mismatch' + ) { + pushAction( + 'check-shared-version', + 'Compare requested shared version with available versions', + 'shared', + ); + } + if ( + report.summary.error?.lifecycle === 'loadShareSync' || + report.shared?.reason === 'sync-async-boundary' || + report.shared?.eager === false + ) { + pushAction( + 'check-eager-config', + 'Check eager configuration or add an async boundary before sync shared consumption', + 'shared', + ); + } + } + } + + if ( + report.moduleInfo && + !actions.some((action) => action.id === 'check-module-info') + ) { + pushAction( + 'check-module-info', + 'Check deployment-provided __FEDERATION__.moduleInfo for the requested remote', + 'host', + ); + } + + if (!actions.length) { + pushAction( + 'inspect-runtime-events', + 'Inspect the ordered observability events for the failed phase', + ownerHint, + ); + } + + return actions; + }; + + const createFactReport = ( + report: ObservabilityReport, + ): ObservabilityFactReport => { + const ownerHint = getDiagnosisOwnerHint(report); + const warnings = createDiagnosisWarnings(report); + + return { + title: getDiagnosisTitle(report), + outcome: report.summary.outcome, + status: report.status, + ownerHint, + failedPhase: report.failedPhase || report.summary.error?.failedPhase, + errorCode: report.errorCode || report.summary.error?.errorCode, + errorName: report.errorName || report.summary.error?.errorName, + errorMessage: report.errorMessage || report.summary.error?.errorMessage, + docLink: getDiagnosisDocLink(report), + facts: createDiagnosisFacts(report, ownerHint), + completedPhases: getCompletedPhases(report), + pendingPhases: getPendingPhases(report), + warnings: warnings.length ? warnings : undefined, + actions: createDiagnosisActions(report, ownerHint), + }; + }; + + const refreshReportDerivedFields = (report: ObservabilityReport) => { + report.summary = createReportSummary(report); + refreshModuleInfoSummary(report); + report.diagnosis = createFactReport(report); + }; + + const updateReport = (event: ObservabilityEvent) => { + let report = reports.get(event.traceId); + + if (!report) { + report = { + traceId: event.traceId, + status: event.status === 'error' ? 'error' : 'pending', + requestId: event.requestId, + requestAlias: event.requestAlias, + hostName: event.hostName, + runtimeVersion: event.runtimeVersion, + remote: event.remote ? { ...event.remote } : undefined, + shared: event.shared ? copyEvent(event).shared : undefined, + expose: event.expose, + sanitizedUrl: event.sanitizedUrl, + startedAt: event.timestamp, + updatedAt: event.timestamp, + duration: 0, + failedPhase: event.status === 'error' ? event.phase : undefined, + errorCode: event.errorCode, + errorName: event.errorName, + errorMessage: event.errorMessage, + errorStack: event.errorStack, + ownerHint: event.ownerHint, + retryable: event.retryable, + errorContext: event.errorContext + ? { ...event.errorContext } + : undefined, + loadedBefore: copyLoadedBeforeInfo(event.loadedBefore), + events: [], + summary: { + eventCount: 0, + recovered: false, + loadCompleted: false, + runtimeLoaded: false, + sharedResolved: false, + preloaded: false, + componentLoaded: false, + outcome: 'pending', + lastPhase: undefined, + phases: {}, + shared: undefined, + flags: createEmptyPhaseCollection().flags, + error: undefined, + }, + }; + reports.set(event.traceId, report); + } + + if (event.requestId) { + report.requestId = event.requestId; + } + if (event.requestAlias) { + report.requestAlias = event.requestAlias; + } + if (event.hostName) { + report.hostName = event.hostName; + } + if (event.runtimeVersion) { + report.runtimeVersion = event.runtimeVersion; + } + if (event.remote) { + report.remote = { ...event.remote }; + } + if (event.shared) { + report.shared = copyEvent(event).shared; + } + if (event.expose) { + report.expose = event.expose; + } + if (event.sanitizedUrl) { + report.sanitizedUrl = event.sanitizedUrl; + } + if (event.errorStack) { + report.errorStack = event.errorStack; + } + if (event.errorCode) { + report.errorCode = event.errorCode; + } + if (event.errorName) { + report.errorName = event.errorName; + } + if (event.errorMessage) { + report.errorMessage = event.errorMessage; + } + if (event.ownerHint) { + report.ownerHint = event.ownerHint; + } + if (event.retryable !== undefined) { + report.retryable = event.retryable; + } + if (event.errorContext) { + report.errorContext = { ...event.errorContext }; + } + if (event.loadedBefore) { + report.loadedBefore = copyLoadedBeforeInfo(event.loadedBefore); + } + + report.events.push(event); + report.updatedAt = event.timestamp; + report.duration = Math.max(0, report.updatedAt - report.startedAt); + + const eventOutcome = getEventOutcome(event); + + if (eventOutcome === 'error') { + report.status = 'error'; + if (shouldReplaceFailedPhase(report, event)) { + report.failedPhase = event.phase; + } + } else if (eventOutcome === 'recovered') { + report.status = 'success'; + } else if (eventOutcome === 'success' && report.status !== 'error') { + report.status = 'success'; + } + + refreshReportDerivedFields(report); + + latestTraceId = event.traceId; + trimEvents(report); + return report; + }; + + const notifyEvent = ( + event: ObservabilityEvent, + report: ObservabilityReport, + origin?: ObservabilityRuntimeOrigin, + ) => { + try { + options.onEvent?.(copyEvent(event), copyReport(report), { origin }); + } catch { + // Observability callbacks must not affect Module Federation loading. + } + }; + + const notifyReport = ( + report: ObservabilityReport, + origin?: ObservabilityRuntimeOrigin, + ) => { + if (report.events[report.events.length - 1]?.status === 'start') { + return; + } + + try { + options.onReport?.(copyReport(report), { origin }); + } catch { + // Observability callbacks must not affect Module Federation loading. + } + }; + + const notifyRawError = ( + errorValue: unknown, + event: ObservabilityEvent, + report: ObservabilityReport, + origin?: ObservabilityRuntimeOrigin, + ) => { + if (!errorValue || !options.onRawError) { + return; + } + + try { + options.onRawError(errorValue, { + origin, + event: copyEvent(event), + report: copyReport(report), + }); + } catch { + // Raw error callbacks must not affect Module Federation loading. + } + }; + + const notifyCollector = ( + event: ObservabilityEvent, + report: ObservabilityReport, + ) => { + if (!collectorOptions) { + return; + } + + const fetcher = (globalThis as { fetch?: ObservabilityFetch }).fetch; + if (typeof fetcher !== 'function') { + return; + } + + try { + const body = JSON.stringify({ + schemaVersion: 1, + source: 'browser', + kind: 'event', + createdAt: Date.now(), + event: copyEvent(event), + report: copyReport(report), + }); + + void fetcher(getCollectorUrl(collectorOptions.port), { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + body, + keepalive: body.length <= 64 * 1024, + credentials: 'omit', + mode: 'cors', + }).catch((error) => { + // The local collector is optional and must not affect MF loading. + logger.debug('Failed to notify local observability collector.', error); + }); + } catch (error) { + // The local collector is optional and must not affect MF loading. + logger.debug('Failed to notify local observability collector.', error); + } + }; + + const notifyDevtools = ( + event: ObservabilityEvent, + report: ObservabilityReport, + ) => { + if (!devtoolsOptions) { + return; + } + + const poster = (globalThis as { postMessage?: unknown }).postMessage; + if (typeof poster !== 'function') { + return; + } + + try { + poster.call( + globalThis, + { + schemaVersion: 1, + source: devtoolsOptions.source, + kind: 'event', + createdAt: Date.now(), + scope: browserGlobalScope || report.hostName, + event: copyEvent(event), + report: copyReport(report), + }, + '*', + ); + } catch { + // Browser extension delivery is optional and must not affect MF loading. + } + }; + + const getEventsSnapshot = () => events.map(copyEvent); + + const getTraceIdsSnapshot = () => Array.from(reports.keys()); + + const getReportTimeline = () => + Array.from(reports.values()).sort((left, right) => { + if (right.updatedAt !== left.updatedAt) { + return right.updatedAt - left.updatedAt; + } + + return right.startedAt - left.startedAt; + }); + + const matchesReportValue = ( + value: string | undefined, + expected: string | undefined, + ) => { + if (!value || !expected) { + return false; + } + + const normalizedValue = value.toLowerCase(); + const normalizedExpected = expected.toLowerCase(); + + return ( + normalizedValue === normalizedExpected || + normalizedValue.includes(normalizedExpected) + ); + }; + + const matchesReportQuery = ( + report: ObservabilityReport, + query: ObservabilityReportQuery, + ) => { + if (query.traceId && report.traceId !== query.traceId) { + return false; + } + if (query.status && report.status !== query.status) { + return false; + } + if (query.outcome && report.summary.outcome !== query.outcome) { + return false; + } + if ( + query.remote && + ![ + report.remote?.name, + report.remote?.alias, + report.remote?.entry, + report.requestId, + report.requestAlias, + report.sanitizedUrl, + ].some((value) => matchesReportValue(value, query.remote)) + ) { + return false; + } + if ( + query.expose && + ![report.expose, report.requestId].some((value) => + matchesReportValue(value, query.expose), + ) + ) { + return false; + } + if ( + query.shared && + ![report.shared?.name].some((value) => + matchesReportValue(value, query.shared), + ) + ) { + return false; + } + + return true; + }; + + const getReportsSnapshot = (options: ObservabilityReportListOptions = {}) => { + const limit = normalizeQueryLimit(options.limit); + const timeline = getReportTimeline(); + + return (limit ? timeline.slice(0, limit) : timeline).map(copyReport); + }; + + const findReportsSnapshot = (query: ObservabilityReportQuery = {}) => { + const limit = normalizeQueryLimit(query.limit); + const matchedReports = getReportTimeline().filter((report) => + matchesReportQuery(report, query), + ); + + return (limit ? matchedReports.slice(0, limit) : matchedReports).map( + copyReport, + ); + }; + + const getLatestReportSnapshot = () => { + if (!latestTraceId) { + return undefined; + } + + const report = reports.get(latestTraceId); + return report ? copyReport(report) : undefined; + }; + + const getReportSnapshot = (traceId: string) => { + const report = reports.get(traceId); + return report ? copyReport(report) : undefined; + }; + + const exportReportSnapshot = (traceId?: string) => + traceId ? getReportSnapshot(traceId) : getLatestReportSnapshot(); + + const createBrowserReader = (): ObservabilityBrowserReader => ({ + getEvents: getEventsSnapshot, + getTraceIds: getTraceIdsSnapshot, + getReports: getReportsSnapshot, + findReports: findReportsSnapshot, + getLatestReport: getLatestReportSnapshot, + getReport: getReportSnapshot, + exportReport: exportReportSnapshot, + }); + + const shouldExposeBrowserGlobal = () => options.browser?.enabled === true; + + const ensureBrowserGlobal = (origin?: ObservabilityRuntimeOrigin) => { + if (!shouldExposeBrowserGlobal()) { + return; + } + + const federationGlobal = getFederationGlobal(); + if (!federationGlobal) { + return; + } + + const scope = normalizeScope( + options.browser?.scope || origin?.options?.name || 'default', + ); + const reader = createBrowserReader(); + + const readers = federationGlobal.__OBSERVABILITY__ || {}; + federationGlobal.__OBSERVABILITY__ = readers; + browserGlobalScope = scope; + + try { + Object.defineProperty(readers, scope, { + value: reader, + configurable: true, + enumerable: true, + }); + } catch { + readers[scope] = reader; + } + }; + + const shouldUseConsole = () => options.console !== false; + + const shouldUseDevelopmentChannels = () => { + if (shouldUseMinimalBrowserConsole()) { + return false; + } + + if (shouldForceDevelopmentChannels) { + return true; + } + + if (typeof process === 'undefined' || !process.env) { + return true; + } + + return process.env.NODE_ENV !== 'production'; + }; + + const shouldNotifyCollector = () => Boolean(collectorOptions); + + const shouldNotifyDevtools = () => shouldUseDevelopmentChannels(); + + const shouldUseMinimalBrowserConsole = () => + options.browser?.mode === 'production'; + + const shouldUseStartTrace = () => + options.trace?.printStart ?? + (options.browser?.enabled === true && !shouldUseMinimalBrowserConsole()); + + const shouldPrintStartConsole = (event: ObservabilityEvent) => + shouldUseStartTrace() && + event.status === 'start' && + (event.phase === 'loadRemote' || event.phase === 'shared') && + shouldUseConsole(); + + const shouldRecordStartTrace = (input: ObservabilityRuntimeEventInput) => + shouldUseStartTrace() && + input.status === 'start' && + (input.phase === 'loadRemote' || input.phase === 'shared'); + + const shouldCollectLoadedBefore = (error?: unknown) => + Boolean(error) || + (level === 'verbose' && !shouldUseMinimalBrowserConsole()); + + const getBrowserReadCommand = (traceId: string) => { + if (!browserGlobalScope) { + return undefined; + } + + return `window.__FEDERATION__.__OBSERVABILITY__[${JSON.stringify( + browserGlobalScope, + )}].getReport(${JSON.stringify(traceId)})`; + }; + + const emitConsoleHint = ( + event: ObservabilityEvent, + report: ObservabilityReport, + rawError?: unknown, + ) => { + if ( + getEventOutcome(event) !== 'error' || + !shouldUseConsole() || + consoleReportedTraceIds.has(report.traceId) + ) { + return; + } + + consoleReportedTraceIds.add(report.traceId); + + if (shouldUseMinimalBrowserConsole()) { + const lines = [ + '[Module Federation] Observability report generated', + `traceId: ${report.traceId}`, + ]; + + if (report.errorCode) { + lines.push(`errorCode: ${report.errorCode}`); + } + + try { + console.error(lines.join('\n')); + } catch { + // Console output is best-effort observability only. + } + return; + } + + const lines = [ + '[Module Federation] Observability report generated', + `traceId: ${report.traceId}`, + `phase: ${report.failedPhase || event.phase}`, + ]; + + if (report.requestId) { + lines.push(`requestId: ${report.requestId}`); + } + if (report.requestAlias) { + lines.push(`requestAlias: ${report.requestAlias}`); + } + if (report.errorCode) { + lines.push(`errorCode: ${report.errorCode}`); + } + if (report.shared?.name) { + lines.push(`shared: ${report.shared.name}`); + } + + const browserReadCommand = getBrowserReadCommand(report.traceId); + if (browserReadCommand) { + lines.push(`read: ${browserReadCommand}`); + } else { + lines.push('read: enable browser output or use onReport(report)'); + } + + const rawStack = getRawStack(rawError); + if (options.printRawStack === true && rawStack) { + lines.push('rawStack:', rawStack); + } + + try { + console.error(lines.join('\n')); + } catch { + // Console output is best-effort observability only. + } + }; + + const emitStartConsoleHint = ( + event: ObservabilityEvent, + report: ObservabilityReport, + ) => { + if (!shouldPrintStartConsole(event)) { + return; + } + + const startKey = [ + event.traceId, + event.phase, + event.requestId || event.shared?.name || event.remote?.name || '', + event.lifecycle || '', + ].join('|'); + if (consoleReportedStartKeys.has(startKey)) { + return; + } + consoleReportedStartKeys.add(startKey); + + const lines = [ + '[Module Federation] Observability trace started', + `traceId: ${report.traceId}`, + `phase: ${event.phase}`, + ]; + + if (event.requestId) { + lines.push(`requestId: ${event.requestId}`); + } + if (event.requestAlias) { + lines.push(`requestAlias: ${event.requestAlias}`); + } + if (event.remote?.name) { + lines.push(`remote: ${event.remote.name}`); + } + if (event.shared?.name) { + lines.push(`shared: ${event.shared.name}`); + } + if (event.lifecycle) { + lines.push(`lifecycle: ${event.lifecycle}`); + } + + const browserReadCommand = getBrowserReadCommand(report.traceId); + if (browserReadCommand) { + lines.push(`read: ${browserReadCommand}`); + } else { + lines.push( + 'read: enable browser output or use getReports({ limit: 10 })', + ); + } + + try { + console.info(lines.join('\n')); + } catch { + // Console output is best-effort observability only. + } + }; + + const prepareOutputChannels = (origin: ObservabilityRuntimeOrigin) => { + browserGlobalScope = undefined; + ensureBrowserGlobal(origin); + }; + + const prepareRuntimeOrigin = (origin: ObservabilityRuntimeOrigin) => { + if (!isEnabled()) { + return false; + } + + lastRuntimeOrigin = origin; + prepareOutputChannels(origin); + return true; + }; + + const recordEvent = ( + input: ObservabilityRuntimeEventInput, + origin?: ObservabilityRuntimeOrigin, + ) => { + if (suppressRuntimeEvents) { + return undefined; + } + + const traceId = resolveTraceId(input); + const event = normalizeEvent(input, traceId, origin); + applyPhaseDuration(event); + updateTraceMaps(event); + + if (!shouldRecordEvent(level, input) && !shouldRecordStartTrace(input)) { + return undefined; + } + + events.push(event); + const report = updateReport(event); + emitStartConsoleHint(event, report); + emitConsoleHint(event, report, input.error); + if (shouldNotifyCollector()) { + notifyCollector(event, report); + } + if (shouldNotifyDevtools()) { + notifyDevtools(event, report); + } + notifyRawError(input.error, event, report, origin); + notifyEvent(event, report, origin); + notifyReport(report, origin); + return event; + }; + + const markComponentLoaded = ( + markOptions: MarkComponentLoadedOptions = {}, + ) => { + if (options.enabled === false || !runtimeObservabilityEnabled) { + return undefined; + } + + const traceId = + markOptions.traceId || + (markOptions.requestId + ? traceByRequest.get(sanitizeRequestId(markOptions.requestId) || '') + : undefined) || + latestTraceId || + createTraceId({ + phase: 'component', + status: 'success', + requestId: markOptions.requestId, + }); + + return recordEvent( + { + traceId, + phase: 'component', + status: 'success', + requestId: markOptions.requestId, + componentName: markOptions.componentName, + metadata: markOptions.metadata, + eventName: COMPONENT_BUSINESS_LOADED_EVENT, + message: COMPONENT_BUSINESS_LOADED_EVENT, + source: 'business', + }, + lastRuntimeOrigin, + ); + }; + + const getReactForOrigin = async ( + origin: ObservabilityRuntimeOrigin, + ): Promise => { + const previousSuppressRuntimeEvents = suppressRuntimeEvents; + suppressRuntimeEvents = true; + try { + let reactFactory: false | (() => unknown) | undefined; + try { + reactFactory = origin.loadShareSync?.('react'); + } catch { + reactFactory = undefined; + } + + if (typeof reactFactory !== 'function') { + reactFactory = await origin.loadShare?.('react'); + } + + if (typeof reactFactory !== 'function') { + return undefined; + } + + return resolveReactLike(reactFactory()); + } catch { + return undefined; + } finally { + suppressRuntimeEvents = previousSuppressRuntimeEvents; + } + }; + + const getReactWrapPolicy = (loadArgs: ObservabilityRemoteLoadArgs) => { + if ( + options.react?.enabled === false || + options.react?.injectLoadedCallback !== true + ) { + return undefined; + } + + const remoteIds = options.react.remoteIds || []; + if (!remoteIds.length) { + return { + allowAnonymousComponent: false, + }; + } + + const normalizeRemoteId = (value: string) => + value.replace(/\/\.\//g, '/').replace(/^\.\//, ''); + const expectedRemoteIds = new Set(remoteIds.map(normalizeRemoteId)); + const candidates = new Set(); + const addCandidate = (value: string | undefined) => { + if (!value) { + return; + } + candidates.add(value); + candidates.add(normalizeRemoteId(value)); + }; + const exposeValues = [loadArgs.expose]; + if (loadArgs.expose?.startsWith('./')) { + exposeValues.push(loadArgs.expose.slice(2)); + } + const remoteNames = [ + loadArgs.pkgNameOrAlias, + loadArgs.remote?.alias, + loadArgs.remote?.name, + ]; + + addCandidate(loadArgs.id); + addCandidate(loadArgs.expose); + remoteNames.forEach((remoteName) => { + exposeValues.forEach((expose) => { + addCandidate(remoteName && expose ? `${remoteName}/${expose}` : ''); + }); + }); + + const matched = Array.from(candidates).some((candidate) => + expectedRemoteIds.has(candidate), + ); + + return matched + ? { + allowAnonymousComponent: true, + } + : undefined; + }; + + const createReactComponentWrapper = ( + component: unknown, + loadArgs: ObservabilityRemoteLoadArgs, + wrapPolicy: { allowAnonymousComponent: boolean }, + react: ObservabilityReactLike | undefined, + ) => { + const target = resolveReactComponentTarget( + component, + options.react?.defaultExportMode || + (wrapPolicy.allowAnonymousComponent ? 'component' : 'preserve'), + wrapPolicy.allowAnonymousComponent, + ); + if (!target) { + return undefined; + } + + const componentName = getReactComponentName( + target.component, + loadArgs.expose || loadArgs.id, + ); + const originalComponent = target.component; + + const ObservedRemoteComponent = (props: Record) => { + const incomingProps = isRecord(props) ? props : {}; + const originalLoadedCallback = getObjectValue( + incomingProps, + ON_MF_REMOTE_LOADED_PROP, + ); + const onMFRemoteLoaded: OnMFRemoteLoaded = (loadedOptions = {}) => { + markComponentLoaded({ + requestId: loadArgs.id, + componentName: loadedOptions.componentName || componentName, + metadata: loadedOptions.metadata, + }); + + if (typeof originalLoadedCallback === 'function') { + (originalLoadedCallback as OnMFRemoteLoaded)(loadedOptions); + } + }; + + const nextProps = { + ...incomingProps, + [ON_MF_REMOTE_LOADED_PROP]: onMFRemoteLoaded, + }; + + if (react) { + return react.createElement(originalComponent, nextProps); + } + + return ( + originalComponent as (nextProps: Record) => unknown + )(nextProps); + }; + + ObservedRemoteComponent.displayName = `ObservedRemote(${componentName})`; + copyComponentStatics( + ObservedRemoteComponent as unknown as Record, + originalComponent as unknown as Record, + ); + + return target.createResult(ObservedRemoteComponent); + }; + + const wrapReactComponent = async ( + component: unknown, + loadArgs: ObservabilityRemoteLoadArgs, + ) => { + const wrapPolicy = getReactWrapPolicy(loadArgs); + if (!wrapPolicy) { + return undefined; + } + + return createReactComponentWrapper( + component, + loadArgs, + wrapPolicy, + await getReactForOrigin(loadArgs.origin), + ); + }; + + const wrapReactComponentFactory = async ( + factory: unknown, + loadArgs: ObservabilityRemoteLoadArgs, + ) => { + const wrapPolicy = getReactWrapPolicy(loadArgs); + if (!wrapPolicy || typeof factory !== 'function') { + return undefined; + } + + const react = await getReactForOrigin(loadArgs.origin); + const originalFactory = factory as (...args: unknown[]) => unknown; + + return (...factoryArgs: unknown[]) => { + const moduleOrPromise = originalFactory(...factoryArgs); + if ( + moduleOrPromise && + typeof (moduleOrPromise as Promise).then === 'function' + ) { + return (moduleOrPromise as Promise).then((module) => { + return ( + createReactComponentWrapper(module, loadArgs, wrapPolicy, react) || + module + ); + }); + } + + return ( + createReactComponentWrapper( + moduleOrPromise, + loadArgs, + wrapPolicy, + react, + ) || moduleOrPromise + ); + }; + }; + + const plugin: ObservabilityRuntimePlugin = { + name: pluginName, + apply(instance: ModuleFederation) { + appliedRuntimeVersion = + sanitizeText(instance.version, 80) || appliedRuntimeVersion; + if (shouldAttachInstanceApi) { + instance.markComponentLoaded = markComponentLoaded; + } + }, + beforeRequest(args) { + const requestArgs = args as ObservabilityRemoteBeforeRequestArgs; + if (!prepareRuntimeOrigin(requestArgs.origin)) { + return returnHookArgs(args); + } + + const remote = resolveRemoteFromRequestId( + requestArgs.id, + requestArgs.options, + ); + + recordEvent( + { + phase: 'loadRemote', + status: 'start', + requestId: requestArgs.id, + remote, + lifecycle: 'beforeRequest', + message: 'remote:load-start', + }, + requestArgs.origin, + ); + + return returnHookArgs(args); + }, + afterMatchRemote(args) { + const matchArgs = args as ObservabilityRemoteMatchArgs; + if (!prepareRuntimeOrigin(matchArgs.origin)) { + return; + } + + const remote = createRemoteInfo(matchArgs.remoteInfo || matchArgs.remote); + const hostRemotes = getHostRemotesSummary(matchArgs.options); + recordEvent( + { + phase: 'matchRemote', + status: matchArgs.error ? 'error' : 'success', + requestId: matchArgs.id, + lifecycle: 'afterMatchRemote', + expose: matchArgs.expose, + remote, + message: matchArgs.error ? 'remote:match-failed' : 'remote:matched', + error: matchArgs.error, + errorContext: hostRemotes + ? { + hostRemotes, + } + : undefined, + }, + matchArgs.origin, + ); + }, + beforeLoadRemoteSnapshot(args) { + const snapshotArgs = args as ObservabilityRemoteSnapshotArgs; + prepareRuntimeOrigin(snapshotArgs.origin); + }, + loadSnapshot(args) { + if (!isEnabled()) { + return returnHookArgs(args); + } + + const snapshotArgs = args as ObservabilitySnapshotLoadArgs; + const moduleRemote = createRemoteInfo(snapshotArgs.moduleInfo); + const snapshotRemoteEntry = + snapshotArgs.remoteSnapshot?.remoteEntry || + snapshotArgs.remoteSnapshot?.entry; + const manifestUrl = isManifestUrl(moduleRemote?.entry) + ? moduleRemote?.entry + : isManifestUrl(snapshotRemoteEntry) + ? snapshotRemoteEntry + : undefined; + if (!manifestUrl) { + return returnHookArgs(args); + } + + const remote = createRemoteInfo({ + name: + moduleRemote?.name || + sanitizeText(snapshotArgs.remoteSnapshot?.name, 120), + alias: moduleRemote?.alias, + entry: manifestUrl, + entryGlobalName: + moduleRemote?.entryGlobalName || + sanitizeText(snapshotArgs.remoteSnapshot?.entryGlobalName, 120), + type: + moduleRemote?.type || + sanitizeText(snapshotArgs.remoteSnapshot?.type, 80), + }); + + if (seenManifestUrls.has(manifestUrl)) { + recordEvent( + { + phase: 'manifest', + status: 'success', + requestId: manifestUrl, + remote, + url: manifestUrl, + lifecycle: 'loadSnapshot', + message: 'manifest:cached', + cached: true, + }, + lastRuntimeOrigin, + ); + + return returnHookArgs(args); + } + + if (loadingManifestUrls.has(manifestUrl)) { + return returnHookArgs(args); + } + + loadingManifestUrls.add(manifestUrl); + + recordEvent( + { + phase: 'manifest', + status: 'start', + requestId: manifestUrl, + remote, + url: manifestUrl, + lifecycle: 'loadSnapshot', + message: 'manifest:load-start', + }, + lastRuntimeOrigin, + ); + + return returnHookArgs(args); + }, + loadRemoteSnapshot(args) { + if (options.enabled === false) { + return returnHookArgs(args); + } + + const snapshotArgs = args as ObservabilityRemoteSnapshotLoadArgs; + if (snapshotArgs.from !== 'manifest') { + return returnHookArgs(args); + } + + const manifestUrl = + sanitizeUrl(snapshotArgs.manifestUrl) || + sanitizeUrl(snapshotArgs.moduleInfo?.entry); + const remote = createRemoteInfo({ + ...snapshotArgs.moduleInfo, + entry: manifestUrl || snapshotArgs.moduleInfo?.entry, + }); + const cached = Boolean(manifestUrl && seenManifestUrls.has(manifestUrl)); + + recordEvent( + { + phase: 'manifest', + status: 'success', + requestId: manifestUrl, + remote, + url: manifestUrl, + lifecycle: 'loadRemoteSnapshot', + message: 'manifest:resolved', + cached, + }, + lastRuntimeOrigin, + ); + if (manifestUrl) { + loadingManifestUrls.delete(manifestUrl); + seenManifestUrls.add(manifestUrl); + } + + return returnHookArgs(args); + }, + afterResolve(args) { + const resolveArgs = args as ObservabilityRemoteResolveArgs; + if (!prepareRuntimeOrigin(resolveArgs.origin)) { + return returnHookArgs(args); + } + + const remote = createRemoteInfo( + resolveArgs.remoteInfo || resolveArgs.remote, + ); + if (!isManifestUrl(remote?.entry)) { + return returnHookArgs(args); + } + + return returnHookArgs(args); + }, + async onLoad(args) { + const loadArgs = args as ObservabilityRemoteLoadArgs; + if (!prepareRuntimeOrigin(loadArgs.origin)) { + return; + } + + const wrappedComponent = + typeof loadArgs.exposeModuleFactory === 'function' + ? await wrapReactComponentFactory( + loadArgs.exposeModuleFactory, + loadArgs, + ) + : await wrapReactComponent(loadArgs.exposeModule, loadArgs); + const remote = createRemoteInfo(loadArgs.remote); + recordEvent( + { + phase: 'loadRemote', + status: 'success', + requestId: loadArgs.id, + lifecycle: 'onLoad', + expose: loadArgs.expose, + remote, + message: 'remote:loaded', + loadedBefore: shouldCollectLoadedBefore() + ? collectLoadedBeforeInfo(remote, loadArgs.expose, loadArgs.origin) + : undefined, + }, + loadArgs.origin, + ); + if (wrappedComponent) { + return wrappedComponent; + } + }, + errorLoadRemote(args) { + const errorArgs = args as ObservabilityRemoteErrorArgs; + if ( + !prepareRuntimeOrigin(errorArgs.origin) || + (errorArgs.lifecycle !== 'onLoad' && + errorArgs.lifecycle !== 'beforeRequest' && + errorArgs.lifecycle !== 'afterResolve') + ) { + return undefined; + } + + const isManifestError = errorArgs.lifecycle === 'afterResolve'; + if (isManifestError && errorArgs.id) { + loadingManifestUrls.delete(errorArgs.id); + } + const remote = createRemoteInfo(errorArgs.remote); + recordEvent( + { + phase: isManifestError ? 'manifest' : 'loadRemote', + status: 'error', + requestId: errorArgs.id, + lifecycle: errorArgs.lifecycle, + expose: errorArgs.expose, + remote, + url: isManifestError ? errorArgs.id : undefined, + message: isManifestError + ? 'manifest:failed' + : errorArgs.lifecycle + ? `remote:${errorArgs.lifecycle}:failed` + : 'remote:failed', + error: errorArgs.error, + loadedBefore: collectLoadedBeforeInfo( + remote, + errorArgs.expose, + errorArgs.origin, + ), + }, + errorArgs.origin, + ); + + return undefined; + }, + afterLoadRemote(args) { + const loadArgs = args as ObservabilityRemoteAfterLoadArgs; + if (!prepareRuntimeOrigin(loadArgs.origin)) { + return; + } + + const remote = createRemoteInfo(loadArgs.remote); + recordEvent( + { + phase: 'loadRemote', + status: 'complete', + requestId: loadArgs.id, + lifecycle: 'afterLoadRemote', + expose: loadArgs.expose, + remote, + message: loadArgs.recovered + ? 'remote:load-recovered' + : loadArgs.error + ? 'remote:load-failed' + : 'remote:load-complete', + error: loadArgs.error, + recovered: loadArgs.recovered, + loadedBefore: shouldCollectLoadedBefore(loadArgs.error) + ? collectLoadedBeforeInfo(remote, loadArgs.expose, loadArgs.origin) + : undefined, + }, + loadArgs.origin, + ); + }, + loadEntry(args) { + const entryArgs = args as ObservabilityRemoteEntryLoadArgs; + if ( + shouldSkipRuntimeHook(entryArgs.origin) || + !prepareRuntimeOrigin(entryArgs.origin) + ) { + return; + } + + const remote = createRemoteInfo(entryArgs.remoteInfo); + recordEvent( + { + phase: 'remoteEntry', + status: 'start', + requestId: remote?.name, + remote, + url: remote?.entry, + lifecycle: 'loadEntry', + message: 'remoteEntry:load-start', + }, + entryArgs.origin, + ); + }, + afterLoadEntry(args) { + const entryArgs = args as ObservabilityRemoteEntryAfterLoadArgs; + if ( + shouldSkipRuntimeHook(entryArgs.origin) || + !prepareRuntimeOrigin(entryArgs.origin) + ) { + return; + } + + const remote = createRemoteInfo(entryArgs.remoteInfo); + const remoteEntryKey = getRemoteEntryKey(sanitizeRemote(remote)); + const cached = + entryArgs.cached === true || + Boolean(remoteEntryKey && seenRemoteEntryKeys.has(remoteEntryKey)); + recordEvent( + { + phase: 'remoteEntry', + status: entryArgs.error ? 'error' : 'success', + requestId: remote?.name, + remote, + url: remote?.entry, + lifecycle: 'afterLoadEntry', + message: entryArgs.error + ? 'remoteEntry:load-failed' + : entryArgs.recovered + ? 'remoteEntry:load-recovered' + : 'remoteEntry:loaded', + error: entryArgs.error, + recovered: entryArgs.recovered, + cached, + }, + entryArgs.origin, + ); + if (!entryArgs.error && remoteEntryKey) { + seenRemoteEntryKeys.add(remoteEntryKey); + } + }, + beforeInitRemote(args) { + const initArgs = args as ObservabilityRemoteInitArgs; + if ( + shouldSkipRuntimeHook(initArgs.origin) || + !prepareRuntimeOrigin(initArgs.origin) + ) { + return; + } + + const remote = createRemoteInfo(initArgs.remoteInfo); + recordEvent( + { + phase: 'remoteEntryInit', + status: 'start', + requestId: initArgs.id || remote?.name, + remote, + lifecycle: 'beforeInitRemote', + message: 'remoteEntry:init-start', + }, + initArgs.origin, + ); + }, + afterInitRemote(args) { + const initArgs = args as ObservabilityRemoteInitArgs; + if ( + shouldSkipRuntimeHook(initArgs.origin) || + !prepareRuntimeOrigin(initArgs.origin) + ) { + return; + } + + const remote = createRemoteInfo(initArgs.remoteInfo); + recordEvent( + { + phase: 'remoteEntryInit', + status: initArgs.error ? 'error' : 'success', + requestId: initArgs.id || remote?.name, + remote, + lifecycle: 'afterInitRemote', + message: initArgs.error + ? 'remoteEntry:init-failed' + : initArgs.cached + ? 'remoteEntry:init-reused' + : 'remoteEntry:initialized', + error: initArgs.error, + cached: initArgs.cached, + }, + initArgs.origin, + ); + }, + beforeGetExpose(args) { + const exposeArgs = args as ObservabilityRemoteExposeArgs; + if ( + shouldSkipRuntimeHook(exposeArgs.origin) || + !prepareRuntimeOrigin(exposeArgs.origin) + ) { + return; + } + + recordEvent( + { + phase: 'expose', + status: 'start', + requestId: exposeArgs.id, + expose: exposeArgs.expose, + remote: createRemoteInfo(exposeArgs.moduleInfo), + lifecycle: 'beforeGetExpose', + message: 'expose:get-start', + }, + exposeArgs.origin, + ); + }, + afterGetExpose(args) { + const exposeArgs = args as ObservabilityRemoteExposeArgs; + if ( + shouldSkipRuntimeHook(exposeArgs.origin) || + !prepareRuntimeOrigin(exposeArgs.origin) + ) { + return; + } + + const remote = createRemoteInfo(exposeArgs.moduleInfo); + recordEvent( + { + phase: 'expose', + status: exposeArgs.error ? 'error' : 'success', + requestId: exposeArgs.id, + expose: exposeArgs.expose, + remote, + lifecycle: 'afterGetExpose', + message: exposeArgs.error ? 'expose:get-failed' : 'expose:resolved', + error: exposeArgs.error, + loadedBefore: shouldCollectLoadedBefore(exposeArgs.error) + ? collectLoadedBeforeInfo( + remote, + exposeArgs.expose, + exposeArgs.origin, + ) + : undefined, + }, + exposeArgs.origin, + ); + }, + beforeExecuteFactory(args) { + const factoryArgs = args as ObservabilityRemoteFactoryArgs; + if ( + shouldSkipRuntimeHook(factoryArgs.origin) || + !prepareRuntimeOrigin(factoryArgs.origin) + ) { + return; + } + + recordEvent( + { + phase: 'moduleFactory', + status: 'start', + requestId: factoryArgs.id, + expose: factoryArgs.expose, + remote: createRemoteInfo(factoryArgs.moduleInfo), + lifecycle: 'beforeExecuteFactory', + message: 'moduleFactory:execute-start', + }, + factoryArgs.origin, + ); + }, + afterExecuteFactory(args) { + const factoryArgs = args as ObservabilityRemoteFactoryArgs; + if ( + shouldSkipRuntimeHook(factoryArgs.origin) || + !prepareRuntimeOrigin(factoryArgs.origin) + ) { + return; + } + + const remote = createRemoteInfo(factoryArgs.moduleInfo); + recordEvent( + { + phase: 'moduleFactory', + status: factoryArgs.error ? 'error' : 'success', + requestId: factoryArgs.id, + expose: factoryArgs.expose, + remote, + lifecycle: 'afterExecuteFactory', + message: factoryArgs.error + ? 'moduleFactory:execute-failed' + : 'moduleFactory:executed', + error: factoryArgs.error, + loadedBefore: shouldCollectLoadedBefore(factoryArgs.error) + ? collectLoadedBeforeInfo( + remote, + factoryArgs.expose, + factoryArgs.origin, + ) + : undefined, + }, + factoryArgs.origin, + ); + }, + beforeLoadShare(args) { + if ( + shouldGuardSharedHooksByRuntimeVersion && + !supportsRuntimeHookObservability(args.origin) + ) { + return returnHookArgs(args); + } + + if (!prepareRuntimeOrigin(args.origin)) { + return returnHookArgs(args); + } + + recordEvent( + { + phase: 'shared', + status: 'start', + requestId: `shared:${args.pkgName}`, + lifecycle: 'loadShare', + shared: createSharedInfo(args), + message: 'shared:load-start', + }, + args.origin, + ); + + return returnHookArgs(args); + }, + afterLoadShare(args) { + if ( + shouldGuardSharedHooksByRuntimeVersion && + !supportsRuntimeHookObservability(args.origin) + ) { + return returnHookArgs(args); + } + + if (!prepareRuntimeOrigin(args.origin)) { + return returnHookArgs(args); + } + + recordEvent( + { + phase: 'shared', + status: 'success', + requestId: `shared:${args.pkgName}`, + lifecycle: args.lifecycle, + shared: createSharedInfo(args), + message: + args.lifecycle === 'loadShareSync' + ? 'shared:resolved-sync' + : 'shared:resolved', + }, + args.origin, + ); + + return returnHookArgs(args); + }, + errorLoadShare(args) { + if ( + shouldGuardSharedHooksByRuntimeVersion && + !supportsRuntimeHookObservability(args.origin) + ) { + return returnHookArgs(args); + } + + if (!prepareRuntimeOrigin(args.origin)) { + return returnHookArgs(args); + } + + const handledCustomShareMiss = args.recovered === true && !args.error; + const reason = handledCustomShareMiss + ? 'custom-share-info-unmatched' + : getSharedErrorReason(args); + + recordEvent( + { + phase: 'shared', + status: handledCustomShareMiss ? 'complete' : 'error', + requestId: `shared:${args.pkgName}`, + lifecycle: args.lifecycle, + shared: createSharedInfo(args, reason), + message: reason ? `shared:${reason}` : undefined, + error: handledCustomShareMiss ? undefined : args.error, + recovered: args.recovered, + }, + args.origin, + ); + + return returnHookArgs(args); + }, + } as ObservabilityRuntimePlugin; + + if (!shouldDisablePreloadHooks) { + plugin.generatePreloadAssets = (args) => { + const preloadArgs = args as ObservabilityPreloadAssetsArgs; + if (!prepareRuntimeOrigin(preloadArgs.origin)) { + return returnHookArgs(args); + } + + const remote = createRemoteInfo( + preloadArgs.remoteInfo || preloadArgs.remote, + ); + const preloadConfig = preloadArgs.preloadOptions?.preloadConfig; + recordEvent( + { + phase: 'preload', + status: 'start', + requestId: + remote?.name || sanitizeText(preloadConfig?.nameOrAlias, 160), + remote, + lifecycle: 'generatePreloadAssets', + message: 'preload:assets-ready', + metadata: clipObservabilityMetadata({ + nameOrAlias: preloadConfig?.nameOrAlias, + exposes: preloadConfig?.exposes?.join(','), + resourceCategory: preloadConfig?.resourceCategory, + share: preloadConfig?.share, + depsRemote: Array.isArray(preloadConfig?.depsRemote) + ? 'custom' + : preloadConfig?.depsRemote, + }), + }, + preloadArgs.origin, + ); + + return returnHookArgs(args); + }; + + plugin.afterPreloadRemote = (args) => { + const preloadArgs = args as ObservabilityAfterPreloadRemoteArgs; + if (!prepareRuntimeOrigin(preloadArgs.origin)) { + return returnHookArgs(args); + } + + const results = preloadArgs.results || []; + if (results.length === 0 && preloadArgs.error) { + recordEvent( + { + phase: 'preload', + status: 'error', + requestId: 'preloadRemote', + lifecycle: 'afterPreloadRemote', + message: 'preload:failed', + error: preloadArgs.error, + }, + preloadArgs.origin, + ); + return returnHookArgs(args); + } + + results.forEach((preloadResult) => { + const remote = createRemoteInfo( + preloadResult.remoteInfo || preloadResult.remote, + ); + const requestId = + sanitizeRequestId(preloadResult.id) || + remote?.name || + sanitizeText(preloadResult.preloadConfig?.nameOrAlias, 160); + + preloadResult.results?.forEach((assetResult) => { + const isError = + assetResult.status === 'error' || assetResult.status === 'timeout'; + recordEvent( + { + phase: 'preload', + status: isError ? 'error' : 'success', + requestId, + remote, + url: assetResult.url, + cached: assetResult.status === 'cached', + lifecycle: 'afterPreloadRemote', + message: `preload:${assetResult.resourceType || 'resource'}:${assetResult.status || 'complete'}`, + error: isError ? assetResult.error : undefined, + errorContext: isError + ? { + resourceType: assetResult.resourceType, + initiator: assetResult.initiator, + status: assetResult.status, + id: assetResult.id, + } + : undefined, + metadata: clipObservabilityMetadata({ + resourceType: assetResult.resourceType, + initiator: assetResult.initiator, + status: assetResult.status, + id: assetResult.id, + preloadNameOrAlias: preloadResult.preloadConfig?.nameOrAlias, + }), + }, + preloadArgs.origin, + ); + }); + }); + + return returnHookArgs(args); + }; + } + + return { + plugin, + getEvents() { + return getEventsSnapshot(); + }, + getTraceIds() { + return getTraceIdsSnapshot(); + }, + getReports(options?: ObservabilityReportListOptions) { + return getReportsSnapshot(options); + }, + findReports(query?: ObservabilityReportQuery) { + return findReportsSnapshot(query); + }, + getLatestReport() { + return getLatestReportSnapshot(); + }, + getReport(traceId: string) { + return getReportSnapshot(traceId); + }, + exportReport(traceId?: string) { + return exportReportSnapshot(traceId); + }, + clear() { + events.length = 0; + reports.clear(); + traceByRequest.clear(); + traceByRemote.clear(); + phaseStartTimes.clear(); + seenManifestUrls.clear(); + seenRemoteEntryKeys.clear(); + consoleReportedTraceIds.clear(); + consoleReportedStartKeys.clear(); + latestTraceId = undefined; + runtimeObservabilityEnabled = false; + effectiveMaxEvents = configuredMaxEvents; + browserGlobalScope = undefined; + lastRuntimeOrigin = undefined; + }, + markComponentLoaded, + }; +} diff --git a/packages/observability-plugin/src/index.ts b/packages/observability-plugin/src/index.ts new file mode 100644 index 00000000000..d8abc52b205 --- /dev/null +++ b/packages/observability-plugin/src/index.ts @@ -0,0 +1,4 @@ +export * from './core'; +export { ObservabilityPlugin } from './browser'; +export { ChromeObservabilityPlugin } from './chrome-devtool'; +export { default } from './browser'; diff --git a/packages/observability-plugin/src/node.ts b/packages/observability-plugin/src/node.ts new file mode 100644 index 00000000000..fea4015d57e --- /dev/null +++ b/packages/observability-plugin/src/node.ts @@ -0,0 +1,335 @@ +import { + createObservability as createBaseObservability, + type ObservabilityController, + type ObservabilityEvent, + type ObservabilityEventContext, + type ObservabilityRuntimePlugin, + type ObservabilityPluginOptions, + type ObservabilityReport, +} from './core'; + +export interface ObservabilityNodeOptions extends Omit< + ObservabilityPluginOptions, + 'browser' +> { + fileOutput?: boolean; + directory?: string; + latestFile?: string; + eventsFile?: string; +} + +interface NodeOutputModules { + fs: { + mkdirSync(path: string, options?: { recursive?: boolean }): void; + writeFileSync(path: string, data: string, encoding?: string): void; + appendFileSync(path: string, data: string, encoding?: string): void; + }; + path: { + isAbsolute(path: string): boolean; + join(...paths: string[]): string; + resolve(...paths: string[]): string; + }; +} + +const DEFAULT_NODE_DIRECTORY = '.mf/observability'; +const DEFAULT_LATEST_FILE = 'latest.json'; +const DEFAULT_EVENTS_FILE = 'events.jsonl'; + +let nodeOutputModulesPromise: + | Promise + | undefined; + +declare const __non_webpack_require__: ((id: string) => unknown) | undefined; + +function getNodeProcess(): + | { versions?: { node?: string }; cwd?: () => string } + | undefined { + return ( + globalThis as { + process?: { versions?: { node?: string }; cwd?: () => string }; + } + ).process; +} + +function isNodeEnvironment() { + return Boolean(getNodeProcess()?.versions?.node); +} + +export function getNativeNodeRequire(): ((id: string) => unknown) | undefined { + if (typeof __non_webpack_require__ === 'function') { + return __non_webpack_require__; + } + + const globalRequire = ( + globalThis as { + __non_webpack_require__?: (id: string) => unknown; + } + ).__non_webpack_require__; + if (typeof globalRequire === 'function') { + return globalRequire; + } + + try { + return Function( + 'return typeof require === "function" ? require : undefined', + )() as ((id: string) => unknown) | undefined; + } catch { + return undefined; + } +} + +function getRuntimeImport(): + | ((specifier: string) => Promise>) + | undefined { + try { + return Function('specifier', 'return import(specifier)') as ( + specifier: string, + ) => Promise>; + } catch { + return undefined; + } +} + +function unwrapDefaultExport(moduleValue: T & { default?: T }) { + return moduleValue.default || moduleValue; +} + +async function getNodeOutputModules(): Promise { + if (!isNodeEnvironment()) { + return undefined; + } + + if (nodeOutputModulesPromise) { + return nodeOutputModulesPromise; + } + + nodeOutputModulesPromise = (async () => { + const nativeRequire = getNativeNodeRequire(); + if (nativeRequire) { + try { + const fs = nativeRequire('node:fs'); + const path = nativeRequire('node:path'); + return { + fs, + path, + } as NodeOutputModules; + } catch { + // Fall through to dynamic import for ESM-only Node environments. + } + } + + const runtimeImport = getRuntimeImport(); + if (!runtimeImport) { + return undefined; + } + + try { + const [fsModule, pathModule] = await Promise.all([ + runtimeImport('node:fs'), + runtimeImport('node:path'), + ]); + + return { + fs: unwrapDefaultExport(fsModule), + path: unwrapDefaultExport(pathModule), + } as NodeOutputModules; + } catch { + return undefined; + } + })(); + + return nodeOutputModulesPromise; +} + +function getNodeOutputConfig(options: ObservabilityNodeOptions) { + return { + directory: options.directory || DEFAULT_NODE_DIRECTORY, + latestFile: options.latestFile || DEFAULT_LATEST_FILE, + eventsFile: options.eventsFile || DEFAULT_EVENTS_FILE, + }; +} + +function shouldUseNodeOutput(options: ObservabilityNodeOptions) { + return ( + options.enabled !== false && + options.fileOutput === true && + isNodeEnvironment() + ); +} + +function shouldUseConsole(options: ObservabilityNodeOptions) { + return options.console !== false; +} + +function getNodeLatestPathForConsole(options: ObservabilityNodeOptions) { + const config = getNodeOutputConfig(options); + return `${config.directory}/${config.latestFile}`; +} + +function getNodeEventsPathForConsole(options: ObservabilityNodeOptions) { + const config = getNodeOutputConfig(options); + return `${config.directory}/${config.eventsFile}`; +} + +function isErrorEvent(event: ObservabilityEvent) { + return ( + event.status === 'error' || + (event.status === 'complete' && + Boolean(event.errorName || event.errorMessage)) + ); +} + +function getRawStack(error: unknown): string | undefined { + if (error instanceof Error) { + return error.stack || error.message; + } + + return undefined; +} + +async function writeNodeOutput( + options: ObservabilityNodeOptions, + event: ObservabilityEvent, + report: ObservabilityReport, +) { + const modules = await getNodeOutputModules(); + if (!modules) { + return; + } + + const config = getNodeOutputConfig(options); + const cwd = getNodeProcess()?.cwd?.() || '.'; + const directory = modules.path.isAbsolute(config.directory) + ? config.directory + : modules.path.resolve(cwd, config.directory); + const latestFile = modules.path.join(directory, config.latestFile); + const eventsFile = modules.path.join(directory, config.eventsFile); + + modules.fs.mkdirSync(directory, { recursive: true }); + modules.fs.writeFileSync( + latestFile, + `${JSON.stringify(report, null, 2)}\n`, + 'utf8', + ); + modules.fs.appendFileSync(eventsFile, `${JSON.stringify(event)}\n`, 'utf8'); +} + +function emitNodeConsoleHint( + options: ObservabilityNodeOptions, + event: ObservabilityEvent, + report: ObservabilityReport, + context: ObservabilityEventContext | undefined, + reportedTraceIds: Set, + rawStack?: string, +) { + if ( + !isErrorEvent(event) || + !shouldUseConsole(options) || + reportedTraceIds.has(report.traceId) + ) { + return; + } + + reportedTraceIds.add(report.traceId); + + const lines = [ + '[Module Federation] Observability report generated', + `traceId: ${report.traceId}`, + `phase: ${report.failedPhase || event.phase}`, + ]; + + if (report.requestId) { + lines.push(`requestId: ${report.requestId}`); + } + if (report.errorCode) { + lines.push(`errorCode: ${report.errorCode}`); + } + if (report.shared?.name) { + lines.push(`shared: ${report.shared.name}`); + } + + if (shouldUseNodeOutput(options)) { + lines.push(`latest: ${getNodeLatestPathForConsole(options)}`); + lines.push(`events: ${getNodeEventsPathForConsole(options)}`); + } else { + lines.push('read: enable fileOutput or use onReport(report)'); + } + + if (options.printRawStack === true && rawStack) { + lines.push('rawStack:', rawStack); + } + + try { + console.error(lines.join('\n')); + } catch { + // Console output is best-effort observability only. + } +} + +export function createNodeObservability( + options: ObservabilityNodeOptions = {}, +): ObservabilityController { + let nodeWriteQueue: Promise = Promise.resolve(); + const consoleReportedTraceIds = new Set(); + const rawStackByTraceId = new Map(); + const observability = createBaseObservability({ + ...options, + console: false, + browser: undefined, + onRawError(error, context) { + const rawStack = getRawStack(error); + if (rawStack) { + rawStackByTraceId.set(context.report.traceId, rawStack); + } + + options.onRawError?.(error, context); + }, + onEvent(event, report, context) { + if (shouldUseNodeOutput(options)) { + nodeWriteQueue = nodeWriteQueue + .catch(() => undefined) + .then(() => writeNodeOutput(options, event, report)) + .catch(() => undefined); + } + + emitNodeConsoleHint( + options, + event, + report, + context, + consoleReportedTraceIds, + rawStackByTraceId.get(report.traceId), + ); + try { + options.onEvent?.(event, report, context); + } finally { + if (isErrorEvent(event)) { + rawStackByTraceId.delete(report.traceId); + } + } + }, + onReport(report, context) { + options.onReport?.(report, context); + }, + }); + + observability.plugin.name = 'observability-node-plugin'; + + const clear = observability.clear; + observability.clear = () => { + clear(); + nodeWriteQueue = Promise.resolve(); + consoleReportedTraceIds.clear(); + rawStackByTraceId.clear(); + }; + + return observability; +} + +export function ObservabilityPlugin( + options: ObservabilityNodeOptions = {}, +): ObservabilityRuntimePlugin { + return createNodeObservability(options).plugin; +} + +export default ObservabilityPlugin; diff --git a/packages/observability-plugin/tsconfig.json b/packages/observability-plugin/tsconfig.json new file mode 100644 index 00000000000..90b8d931e39 --- /dev/null +++ b/packages/observability-plugin/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/packages/observability-plugin/tsdown.config.ts b/packages/observability-plugin/tsdown.config.ts new file mode 100644 index 00000000000..2ad0d32cff6 --- /dev/null +++ b/packages/observability-plugin/tsdown.config.ts @@ -0,0 +1,48 @@ +import { defineConfig } from 'tsdown'; + +const entry = { + index: 'src/index.ts', + browser: 'src/browser.ts', + 'chrome-devtool': 'src/chrome-devtool.ts', + node: 'src/node.ts', + build: 'src/build.ts', +}; + +const baseConfig = { + cwd: import.meta.dirname, + tsconfig: 'tsconfig.json', + clean: true, + entry, + external: [ + '@module-federation/runtime', + '@module-federation/sdk', + 'node:fs', + 'node:path', + ], +}; + +export default defineConfig([ + { + ...baseConfig, + name: 'observability-plugin-cjs', + outDir: 'dist', + format: ['cjs'], + dts: { + resolver: 'tsc', + }, + outExtensions: () => ({ + js: '.js', + dts: '.d.ts', + }), + }, + { + ...baseConfig, + name: 'observability-plugin-esm', + outDir: 'dist/esm', + format: ['esm'], + dts: false, + outExtensions: () => ({ + js: '.js', + }), + }, +]); diff --git a/packages/observability-plugin/vitest.config.ts b/packages/observability-plugin/vitest.config.ts new file mode 100644 index 00000000000..9d82a458863 --- /dev/null +++ b/packages/observability-plugin/vitest.config.ts @@ -0,0 +1,19 @@ +import path from 'path'; +import { defineConfig } from 'vitest/config'; +import tsconfigPaths from 'vite-tsconfig-paths'; + +export default defineConfig({ + define: { + __DEV__: true, + __TEST__: true, + __BROWSER__: false, + __VERSION__: '"unknown"', + }, + plugins: [tsconfigPaths()], + test: { + environment: 'node', + include: [path.resolve(__dirname, '__tests__/*.spec.ts')], + globals: true, + testTimeout: 10000, + }, +}); diff --git a/packages/retry-plugin/CHANGELOG.md b/packages/retry-plugin/CHANGELOG.md index f26460777e9..bbe44b74149 100644 --- a/packages/retry-plugin/CHANGELOG.md +++ b/packages/retry-plugin/CHANGELOG.md @@ -1,5 +1,13 @@ # @module-federation/retry-plugin +## 2.5.0 + +### Patch Changes + +- Updated dependencies [5d4095d] +- Updated dependencies [0716c11] + - @module-federation/sdk@2.5.0 + ## 2.4.0 ### Patch Changes diff --git a/packages/retry-plugin/package.json b/packages/retry-plugin/package.json index 02fba0ebded..9683f9f6851 100644 --- a/packages/retry-plugin/package.json +++ b/packages/retry-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/retry-plugin", - "version": "2.4.0", + "version": "2.5.0", "author": "danpeen ", "main": "./dist/index.js", "module": "./dist/esm/index.js", diff --git a/packages/rsbuild-plugin/CHANGELOG.md b/packages/rsbuild-plugin/CHANGELOG.md index 0c6b870d440..84fd13e48d8 100644 --- a/packages/rsbuild-plugin/CHANGELOG.md +++ b/packages/rsbuild-plugin/CHANGELOG.md @@ -1,5 +1,15 @@ # @module-federation/rsbuild-plugin +## 2.5.0 + +### Patch Changes + +- Updated dependencies [5d4095d] +- Updated dependencies [0716c11] + - @module-federation/sdk@2.5.0 + - @module-federation/enhanced@2.5.0 + - @module-federation/node@2.7.43 + ## 2.4.0 ### Patch Changes diff --git a/packages/rsbuild-plugin/package.json b/packages/rsbuild-plugin/package.json index 69ba617225c..4ddbb010536 100644 --- a/packages/rsbuild-plugin/package.json +++ b/packages/rsbuild-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/rsbuild-plugin", - "version": "2.4.0", + "version": "2.5.0", "description": "Module Federation plugin for Rsbuild", "homepage": "https://module-federation.io", "bugs": { diff --git a/packages/rspack/CHANGELOG.md b/packages/rspack/CHANGELOG.md index 73db404ae8e..8a4a730dd28 100644 --- a/packages/rspack/CHANGELOG.md +++ b/packages/rspack/CHANGELOG.md @@ -1,5 +1,20 @@ # @module-federation/rspack +## 2.5.0 + +### Patch Changes + +- Updated dependencies [5d4095d] +- Updated dependencies [0716c11] +- Updated dependencies [13dce52] + - @module-federation/sdk@2.5.0 + - @module-federation/dts-plugin@2.5.0 + - @module-federation/bridge-react-webpack-plugin@2.5.0 + - @module-federation/managers@2.5.0 + - @module-federation/manifest@2.5.0 + - @module-federation/runtime-tools@2.5.0 + - @module-federation/inject-external-runtime-core-plugin@2.5.0 + ## 2.4.0 ### Patch Changes diff --git a/packages/rspack/package.json b/packages/rspack/package.json index 2bbe9f5d606..c88f2600acb 100644 --- a/packages/rspack/package.json +++ b/packages/rspack/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/rspack", - "version": "2.4.0", + "version": "2.5.0", "license": "MIT", "keywords": [ "Module Federation", diff --git a/packages/rspress-plugin/CHANGELOG.md b/packages/rspress-plugin/CHANGELOG.md index db167bd388e..d10949ac383 100644 --- a/packages/rspress-plugin/CHANGELOG.md +++ b/packages/rspress-plugin/CHANGELOG.md @@ -1,5 +1,17 @@ # @module-federation/rspress-plugin +## 2.5.0 + +### Patch Changes + +- Updated dependencies [5d4095d] +- Updated dependencies [0716c11] +- Updated dependencies [41281f4] + - @module-federation/sdk@2.5.0 + - @module-federation/error-codes@2.5.0 + - @module-federation/enhanced@2.5.0 + - @module-federation/rsbuild-plugin@2.5.0 + ## 2.4.0 ### Patch Changes diff --git a/packages/rspress-plugin/package.json b/packages/rspress-plugin/package.json index fc9429ce004..dd235ea297d 100644 --- a/packages/rspress-plugin/package.json +++ b/packages/rspress-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/rspress-plugin", - "version": "2.4.0", + "version": "2.5.0", "type": "module", "description": "Module Federation plugin for Rspress", "keywords": [ diff --git a/packages/runtime-core/CHANGELOG.md b/packages/runtime-core/CHANGELOG.md index 41505698c9f..0d5ba631179 100644 --- a/packages/runtime-core/CHANGELOG.md +++ b/packages/runtime-core/CHANGELOG.md @@ -1,5 +1,20 @@ # @module-federation/runtime +## 2.5.0 + +### Minor Changes + +- 41281f4: Add an opt-in observability plugin, a Chrome-extension-safe observability plugin entry with an independent name and fixed browser scope, a direct runtime plugin API with instance-bound component loaded marks, explicit temporary React `onMFRemoteLoaded` callback injection for matched remotes, opt-in start console traces for `loadRemote` and `loadShare`, a local collector mode for AI-assisted browser debugging, a Node-specific export for file reports, a build-specific export for build summaries and build error reports, remote and shared lifecycle hooks, console trace hints, safe browser/Node report outputs, configurable error stack capture with explicit console raw-stack opt-ins, shared/eager loading evidence gated to stable runtime `2.5.0+` for Chrome-extension compatibility, final loading outcome summaries for Module Federation loading reports including resolved shared dependencies, deterministic fact reports for runtime and build failures, no-op return handling for observer hooks, detailed remote match/init/expose/factory phase events with phase durations, compact phase summaries, cache/fallback markers, loaded-before evidence from existing federation instances when a remote load fails, length-limited business component metadata, clipped moduleInfo evidence with preserved deployment locator fields for snapshot-dependent failures, normalized runtime error summaries with error codes, owner hints, retryability, and safe context, dedicated runtime error codes for invalid manifests, missing exposes, and remote container init failures, plus MF skill guidance for reading and fixing observability reports. + +### Patch Changes + +- 0716c11: Track preload resource results and expose resource context to loader hooks. +- Updated dependencies [5d4095d] +- Updated dependencies [0716c11] +- Updated dependencies [41281f4] + - @module-federation/sdk@2.5.0 + - @module-federation/error-codes@2.5.0 + ## 2.4.0 ### Minor Changes diff --git a/packages/runtime-core/__tests__/hooks.spec.ts b/packages/runtime-core/__tests__/hooks.spec.ts index 57c8b75229c..a8b97e7a52d 100644 --- a/packages/runtime-core/__tests__/hooks.spec.ts +++ b/packages/runtime-core/__tests__/hooks.spec.ts @@ -3,6 +3,12 @@ import { ModuleFederation } from '../src/core'; import { ModuleFederationRuntimePlugin } from '../src/type/plugin'; import { mockStaticServer, removeScriptTags } from './mock/utils'; import { addGlobalSnapshot } from '../src/global'; +import { + AsyncHook, + AsyncWaterfallHook, + SyncHook, + SyncWaterfallHook, +} from '../src/utils/hooks'; // eslint-disable-next-line max-lines-per-function describe('hooks', () => { @@ -164,19 +170,29 @@ describe('hooks', () => { plugins: [ { name: 'change-script-attribute', - createScript({ url, remoteInfo }) { + createScript({ url, remoteInfo, resourceContext }) { // Assert remote context is exposed for remoteEntry loads if (url === testRemoteEntry) { expect(remoteInfo).toMatchObject({ name: '@loader-hooks/app2', entry: testRemoteEntry, }); + expect(resourceContext).toMatchObject({ + initiator: 'loadRemote', + resourceType: 'remoteEntry', + }); + expect(resourceContext?.id).toBe('@loader-hooks/app2/say'); } if (url === preloadRemoteEntry) { expect(remoteInfo).toMatchObject({ name: '@loader-hooks/app3', entry: preloadRemoteEntry, }); + expect(resourceContext).toMatchObject({ + initiator: 'preloadRemote', + id: '@loader-hooks/app3/*', + resourceType: 'remoteEntry', + }); } const script = document.createElement('script'); script.src = url; @@ -255,6 +271,7 @@ describe('hooks', () => { remotes: [ { name: '@loader-hooks/app3', + alias: 'app3-alias', version: '*', }, ], @@ -273,8 +290,12 @@ describe('hooks', () => { }, { name: 'capture-link-remote-info', - createLink({ url, attrs, remoteInfo }) { + createLink({ url, attrs, remoteInfo, resourceContext }) { lastLinkRemoteInfo = remoteInfo; + expect(resourceContext).toMatchObject({ + initiator: 'preloadRemote', + id: '@loader-hooks/app3/*', + }); const link = document.createElement('link'); link.href = url; if (attrs) { @@ -282,13 +303,16 @@ describe('hooks', () => { link.setAttribute(k, String(v)); }); } + setTimeout(() => { + link.onload?.(new Event('load')); + }); return link; }, }, ], }); - await INSTANCE.preloadRemote([{ nameOrAlias: '@loader-hooks/app3' }]); + await INSTANCE.preloadRemote([{ nameOrAlias: 'app3-alias' }]); expect(lastLinkRemoteInfo).toMatchObject({ name: '@loader-hooks/app3', }); @@ -296,6 +320,262 @@ describe('hooks', () => { reset(); }); + it('preloadRemote rejects when a preload resource fails', async () => { + const remotePublicPath = 'http://localhost:1111/'; + const reset = addGlobalSnapshot({ + '@loader-hooks/globalinfo': { + globalName: '', + buildVersion: '', + publicPath: '', + remoteTypes: '', + shared: [], + remoteEntry: '', + remoteEntryType: 'global', + modules: [], + version: '0.0.1', + remotesInfo: { + '@loader-hooks/app3': { + matchedVersion: '0.0.1', + }, + }, + }, + '@loader-hooks/app3:0.0.1': { + globalName: '@loader-hooks/app3', + publicPath: remotePublicPath, + remoteTypes: '', + shared: [], + buildVersion: 'custom', + remotesInfo: {}, + remoteEntryType: 'global', + modules: [], + version: '0.0.1', + remoteEntry: 'resources/hooks/app3/federation-remote-entry.js', + }, + }); + + let afterPreloadArgs: any; + const INSTANCE = new ModuleFederation({ + name: '@loader-hooks/globalinfo', + remotes: [ + { + name: '@loader-hooks/app3', + version: '*', + }, + ], + plugins: [ + { + name: 'force-preload-assets', + async generatePreloadAssets() { + return { + cssAssets: [], + jsAssetsWithoutEntry: [ + 'http://localhost:1111/__virtual__/missing-chunk.js', + ], + entryAssets: [], + } as any; + }, + createLink({ url }) { + const link = document.createElement('link'); + link.href = url; + setTimeout(() => { + link.onerror?.(new Event('error')); + }); + return link; + }, + afterPreloadRemote(args) { + afterPreloadArgs = args; + }, + }, + ], + }); + + await expect( + INSTANCE.preloadRemote([{ nameOrAlias: '@loader-hooks/app3' }]), + ).rejects.toThrow('preloadRemote failed to load 1 resource'); + + expect(afterPreloadArgs.results[0].results[0]).toMatchObject({ + url: 'http://localhost:1111/__virtual__/missing-chunk.js', + status: 'error', + resourceType: 'js', + initiator: 'preloadRemote', + id: '@loader-hooks/app3/*', + }); + + reset(); + }); + + it('preloadRemote reports timeout from createLink hook timeout', async () => { + const remotePublicPath = 'http://localhost:1111/'; + const reset = addGlobalSnapshot({ + '@loader-hooks/globalinfo': { + globalName: '', + buildVersion: '', + publicPath: '', + remoteTypes: '', + shared: [], + remoteEntry: '', + remoteEntryType: 'global', + modules: [], + version: '0.0.1', + remotesInfo: { + '@loader-hooks/app3': { + matchedVersion: '0.0.1', + }, + }, + }, + '@loader-hooks/app3:0.0.1': { + globalName: '@loader-hooks/app3', + publicPath: remotePublicPath, + remoteTypes: '', + shared: [], + buildVersion: 'custom', + remotesInfo: {}, + remoteEntryType: 'global', + modules: [], + version: '0.0.1', + remoteEntry: 'resources/hooks/app3/federation-remote-entry.js', + }, + }); + + let afterPreloadArgs: any; + const INSTANCE = new ModuleFederation({ + name: '@loader-hooks/globalinfo', + remotes: [ + { + name: '@loader-hooks/app3', + version: '*', + }, + ], + plugins: [ + { + name: 'timeout-preload-assets', + async generatePreloadAssets() { + return { + cssAssets: [], + jsAssetsWithoutEntry: [ + 'http://localhost:1111/__virtual__/timeout-chunk.js', + ], + entryAssets: [], + } as any; + }, + createLink({ url, resourceContext }) { + expect(resourceContext).toMatchObject({ + initiator: 'preloadRemote', + resourceType: 'js', + id: '@loader-hooks/app3/*', + }); + const link = document.createElement('link'); + link.href = url; + return { + link, + timeout: 1, + }; + }, + afterPreloadRemote(args) { + afterPreloadArgs = args; + }, + }, + ], + }); + + await expect( + INSTANCE.preloadRemote([{ nameOrAlias: '@loader-hooks/app3' }]), + ).rejects.toThrow('preloadRemote failed to load 1 resource'); + + expect(afterPreloadArgs.results[0].results[0]).toMatchObject({ + url: 'http://localhost:1111/__virtual__/timeout-chunk.js', + status: 'timeout', + resourceType: 'js', + initiator: 'preloadRemote', + id: '@loader-hooks/app3/*', + }); + + reset(); + }); + + it('uses exact expose ids when preloadRemote is configured with exposes', async () => { + const remotePublicPath = 'http://localhost:1111/'; + const reset = addGlobalSnapshot({ + '@loader-hooks/globalinfo': { + globalName: '', + buildVersion: '', + publicPath: '', + remoteTypes: '', + shared: [], + remoteEntry: '', + remoteEntryType: 'global', + modules: [], + version: '0.0.1', + remotesInfo: { + '@loader-hooks/app3': { + matchedVersion: '0.0.1', + }, + }, + }, + '@loader-hooks/app3:0.0.1': { + globalName: '@loader-hooks/app3', + publicPath: remotePublicPath, + remoteTypes: '', + shared: [], + buildVersion: 'custom', + remotesInfo: {}, + remoteEntryType: 'global', + modules: [], + version: '0.0.1', + remoteEntry: 'resources/hooks/app3/federation-remote-entry.js', + }, + }); + + const generatedExposes: Array = []; + const resourceIds: string[] = []; + const INSTANCE = new ModuleFederation({ + name: '@loader-hooks/globalinfo', + remotes: [ + { + name: '@loader-hooks/app3', + version: '*', + }, + ], + plugins: [ + { + name: 'force-expose-preload-assets', + async generatePreloadAssets(args) { + generatedExposes.push(args.preloadOptions.preloadConfig.exposes); + const expose = args.preloadOptions.preloadConfig.exposes?.[0]; + return { + cssAssets: [], + jsAssetsWithoutEntry: [ + `http://localhost:1111/__virtual__/${expose}.js`, + ], + entryAssets: [], + } as any; + }, + createLink({ url, resourceContext }) { + resourceIds.push(resourceContext?.id || ''); + const link = document.createElement('link'); + link.href = url; + setTimeout(() => { + link.onload?.(new Event('load')); + }); + return link; + }, + }, + ], + }); + + await INSTANCE.preloadRemote([ + { nameOrAlias: '@loader-hooks/app3', exposes: ['Button', 'Card'] }, + ]); + + expect(generatedExposes).toEqual([['Button'], ['Card']]); + expect(resourceIds).toEqual([ + '@loader-hooks/app3/Button', + '@loader-hooks/app3/Card', + ]); + + reset(); + }); + it('loader fetch hooks', async () => { const data = { id: '@loader-hooks/app2', @@ -330,8 +610,13 @@ describe('hooks', () => { let lastFetchRemoteInfo: any; const fetchPlugin: () => ModuleFederationRuntimePlugin = () => ({ name: 'fetch-plugin', - fetch(url, options, remoteInfo) { + fetch(url, options, remoteInfo, resourceContext) { lastFetchRemoteInfo = remoteInfo; + expect(resourceContext).toMatchObject({ + initiator: 'loadRemote', + id: '@loader-hooks/app2/say', + resourceType: 'manifest', + }); if (url === 'http://mockxxx.com/loader-fetch-hooks-mf-manifest.json') { return Promise.resolve(responseBody); } @@ -359,6 +644,78 @@ describe('hooks', () => { }); }); + it('emits manifest snapshot lifecycle once when loading a manifest remote', async () => { + const data = { + id: '@loader-hooks/app2', + name: '@loader-hooks/app2', + metaData: { + name: '@loader-hooks/app2', + publicPath: 'http://localhost:1111/', + type: 'app', + buildInfo: { + buildVersion: 'custom', + }, + remoteEntry: { + name: 'federation-remote-entry.js', + path: 'resources/hooks/app2/', + }, + types: { + name: 'index.d.ts', + path: './', + }, + globalName: '@loader-hooks/app2', + }, + remotes: [], + shared: [], + exposes: [], + }; + + const fetchPlugin: () => ModuleFederationRuntimePlugin = () => ({ + name: 'fetch-plugin', + fetch(url) { + if (url === 'http://mockxxx.com/snapshot-hooks-mf-manifest.json') { + return Promise.resolve( + new Response(JSON.stringify(data), { + status: 200, + statusText: 'OK', + headers: { 'Content-Type': 'application/json' }, + }), + ); + } + }, + }); + const snapshotEvents: string[] = []; + const snapshotPlugin: () => ModuleFederationRuntimePlugin = () => ({ + name: 'snapshot-observer-plugin', + loadSnapshot(args) { + snapshotEvents.push('loadSnapshot'); + return args; + }, + loadRemoteSnapshot(args) { + snapshotEvents.push(args.from); + return args; + }, + }); + + const INSTANCE = new ModuleFederation({ + name: '@loader-hooks/snapshot', + remotes: [ + { + name: '@loader-hooks/app2', + entry: 'http://mockxxx.com/snapshot-hooks-mf-manifest.json', + }, + ], + plugins: [fetchPlugin(), snapshotPlugin()], + }); + + const res = await INSTANCE.loadRemote<() => string>( + '@loader-hooks/app2/say', + ); + assert(res); + expect(res()).toBe('hello app2'); + expect(snapshotEvents).toEqual(['loadSnapshot', 'manifest']); + }); + it('loaderEntry hooks', async () => { const data = { id: '@loader-hooks/app2', @@ -447,4 +804,161 @@ describe('hooks', () => { assert(loadEntryTestRes); expect(loadEntryTestRes).toBe('./testtest'); }); + + it('sync hooks preserve previous returned value when later listeners return nothing', () => { + const hook = new SyncHook<[string], string | void>('sync-noop'); + const calls: Array = []; + + hook.on((value) => { + calls.push(`first:${value}`); + return 'first-result'; + }); + hook.on((value) => { + calls.push(`second:${value}`); + }); + + expect(hook.emit('payload')).toBe('first-result'); + expect(calls).toEqual(['first:payload', 'second:payload']); + }); + + it('sync hooks use the latest explicit returned value', () => { + const hook = new SyncHook<[string], string | void>('sync-override'); + + hook.on(() => 'first-result'); + hook.on(() => 'second-result'); + + expect(hook.emit('payload')).toBe('second-result'); + }); + + it('async hooks preserve previous returned value when later listeners return nothing', async () => { + const hook = new AsyncHook<[string], string | void | false>('async-noop'); + const calls: Array = []; + + hook.on(async (value) => { + calls.push(`first:${value}`); + return 'first-result'; + }); + hook.on((value) => { + calls.push(`second:${value}`); + }); + + await expect(hook.emit('payload')).resolves.toBe('first-result'); + expect(calls).toEqual(['first:payload', 'second:payload']); + }); + + it('async hooks use the latest explicit returned value', async () => { + const hook = new AsyncHook<[string], string | void | false>( + 'async-override', + ); + + hook.on(async () => 'first-result'); + hook.on(() => 'second-result'); + + await expect(hook.emit('payload')).resolves.toBe('second-result'); + }); + + it('async hooks treat returning the original payload as no explicit result', async () => { + const hook = new AsyncHook< + [{ id: string }], + { id: string; wrapped?: boolean } | void | false + >('async-passthrough'); + const payload = { id: 'remote/Button' }; + const wrapped = { id: 'remote/Button', wrapped: true }; + + hook.on(() => wrapped); + hook.on((args) => args); + + await expect(hook.emit(payload)).resolves.toBe(wrapped); + }); + + it('async hooks still abort when a listener returns false', async () => { + const hook = new AsyncHook<[string], string | void | false>('async-abort'); + const calls: Array = []; + + hook.on(() => { + calls.push('first'); + return 'first-result'; + }); + hook.on(() => { + calls.push('second'); + return false; + }); + hook.on(() => { + calls.push('third'); + return 'third-result'; + }); + + await expect(hook.emit('payload')).resolves.toBe(false); + expect(calls).toEqual(['first', 'second']); + }); + + it('sync waterfall hooks keep the current payload when observers return nothing', () => { + const hook = new SyncWaterfallHook<{ id: string; changed?: boolean }>( + 'sync-waterfall-noop', + ); + + hook.on((args) => ({ + ...args, + changed: true, + })); + hook.on(() => undefined); + + expect(hook.emit({ id: 'remote/Button' })).toEqual({ + id: 'remote/Button', + changed: true, + }); + }); + + it('async waterfall hooks keep the current payload when observers return nothing', async () => { + const hook = new AsyncWaterfallHook<{ id: string; changed?: boolean }>( + 'async-waterfall-noop', + ); + + hook.on(async (args) => ({ + ...args, + changed: true, + })); + hook.on(() => undefined); + + await expect(hook.emit({ id: 'remote/Button' })).resolves.toEqual({ + id: 'remote/Button', + changed: true, + }); + }); + + it('observer plugins do not clear errorLoadRemote fallback results', async () => { + let observedLifecycle: string | undefined; + const fallbackPlugin: ModuleFederationRuntimePlugin = { + name: 'fallback-plugin', + errorLoadRemote() { + return { + default: () => 'fallback component', + }; + }, + }; + const observerPlugin: ModuleFederationRuntimePlugin = { + name: 'observer-plugin', + errorLoadRemote(args) { + observedLifecycle = args.lifecycle; + }, + }; + const GM = new ModuleFederation({ + name: '@hooks/error-load-remote-fallback', + remotes: [], + plugins: [fallbackPlugin, observerPlugin], + }); + + const result = (await GM.remoteHandler.hooks.lifecycle.errorLoadRemote.emit( + { + id: '@demo/fallback/component', + error: new Error('load failed'), + from: 'runtime', + lifecycle: 'onLoad', + origin: GM, + }, + )) as { default: () => string }; + + expect(result.default()).toBe('fallback component'); + expect(observedLifecycle).toBe('onLoad'); + }); }); diff --git a/packages/runtime-core/__tests__/load-remote-diagnostics.spec.ts b/packages/runtime-core/__tests__/load-remote-diagnostics.spec.ts new file mode 100644 index 00000000000..820f0d08d9e --- /dev/null +++ b/packages/runtime-core/__tests__/load-remote-diagnostics.spec.ts @@ -0,0 +1,201 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { RUNTIME_014 } from '@module-federation/error-codes'; +import { ModuleFederation } from '../src/core'; +import { resetFederationGlobalInfo } from '../src/global'; +import type { ModuleFederationRuntimePlugin } from '../src/type'; +import { mockStaticServer, removeScriptTags } from './mock/utils'; + +const createDiagnosticsRecorder = ( + events: Array>, +): ModuleFederationRuntimePlugin => ({ + name: 'load-remote-diagnostics-test-plugin', + afterMatchRemote(args) { + events.push({ + type: 'afterMatchRemote', + id: args.id, + expose: args.expose, + remote: args.remoteInfo, + error: args.error, + }); + }, + beforeInitRemote(args) { + events.push({ + type: 'beforeInitRemote', + id: args.id, + remote: args.remoteInfo, + }); + }, + afterInitRemote(args) { + events.push({ + type: 'afterInitRemote', + id: args.id, + remote: args.remoteInfo, + error: args.error, + cached: args.cached, + }); + }, + beforeGetExpose(args) { + events.push({ + type: 'beforeGetExpose', + id: args.id, + expose: args.expose, + remote: args.moduleInfo, + }); + }, + afterGetExpose(args) { + events.push({ + type: 'afterGetExpose', + id: args.id, + expose: args.expose, + remote: args.moduleInfo, + error: args.error, + }); + }, + beforeExecuteFactory(args) { + events.push({ + type: 'beforeExecuteFactory', + id: args.id, + expose: args.expose, + remote: args.moduleInfo, + }); + }, + afterExecuteFactory(args) { + events.push({ + type: 'afterExecuteFactory', + id: args.id, + expose: args.expose, + remote: args.moduleInfo, + error: args.error, + }); + }, + afterLoadRemote(args) { + events.push({ + type: 'afterLoadRemote', + ...args, + }); + }, +}); + +describe('loadRemote diagnostics', () => { + mockStaticServer({ + baseDir: __dirname, + filterKeywords: [], + basename: 'http://localhost:1111/', + }); + + beforeEach(() => { + resetFederationGlobalInfo(); + removeScriptTags(); + }); + + it('emits an afterLoadRemote hook after a successful remote load', async () => { + const events: Array> = []; + const mf = new ModuleFederation({ + name: 'load-remote-diagnostics-host', + remotes: [ + { + name: '@demo/main', + entry: + 'http://localhost:1111/resources/main/federation-manifest.json', + }, + ], + plugins: [createDiagnosticsRecorder(events)], + }); + + const say = await mf.loadRemote<() => string>('@demo/main/say'); + + expect(say?.()).toBe('hello world'); + expect(events).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: 'afterLoadRemote', + id: '@demo/main/say', + expose: './say', + }), + ]), + ); + + expect(events.map((event) => event.type)).toEqual([ + 'afterMatchRemote', + 'beforeInitRemote', + 'afterInitRemote', + 'beforeGetExpose', + 'afterGetExpose', + 'beforeExecuteFactory', + 'afterExecuteFactory', + 'afterLoadRemote', + ]); + expect(events).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: 'afterMatchRemote', + id: '@demo/main/say', + expose: './say', + }), + expect.objectContaining({ + type: 'afterInitRemote', + error: undefined, + }), + expect.objectContaining({ + type: 'afterGetExpose', + id: '@demo/main/say', + expose: './say', + error: undefined, + }), + expect.objectContaining({ + type: 'afterExecuteFactory', + id: '@demo/main/say', + expose: './say', + error: undefined, + }), + ]), + ); + }); + + it('emits the expose phase before a missing expose bubbles to loadRemote', async () => { + const events: Array> = []; + const mf = new ModuleFederation({ + name: 'load-remote-diagnostics-host', + remotes: [ + { + name: '@demo/main', + entry: + 'http://localhost:1111/resources/main/federation-manifest.json', + }, + ], + plugins: [createDiagnosticsRecorder(events)], + }); + + await expect( + mf.loadRemote<() => string>('@demo/main/__missing__'), + ).rejects.toThrow(RUNTIME_014); + + expect(events).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: 'afterGetExpose', + id: '@demo/main/__missing__', + expose: './__missing__', + error: expect.any(Error), + }), + expect.objectContaining({ + type: 'afterLoadRemote', + id: '@demo/main/__missing__', + expose: './__missing__', + error: expect.any(Error), + }), + ]), + ); + + const afterGetExposeError = events.find( + (event) => event.type === 'afterGetExpose', + )?.error; + expect(afterGetExposeError).toBeInstanceOf(Error); + expect(String((afterGetExposeError as Error).message)).toContain( + 'availableExposes', + ); + expect(String((afterGetExposeError as Error).message)).toContain( + 'shared-button', + ); + }); +}); diff --git a/packages/runtime-core/__tests__/load.spec.ts b/packages/runtime-core/__tests__/load.spec.ts index 258b0b721c2..39435ab5a5e 100644 --- a/packages/runtime-core/__tests__/load.spec.ts +++ b/packages/runtime-core/__tests__/load.spec.ts @@ -2,7 +2,11 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { getRemoteEntry, getRemoteInfo } from '../src/utils/load'; import { ModuleFederation } from '../src/core'; import { resetFederationGlobalInfo } from '../src/global'; -import { RUNTIME_001, RUNTIME_008 } from '@module-federation/error-codes'; +import { + RUNTIME_001, + RUNTIME_008, + RUNTIME_015, +} from '@module-federation/error-codes'; import { mockStaticServer, removeScriptTags } from './mock/utils'; // All fixture URLs are served via two complementary mechanisms both pointing to __tests__/: @@ -92,4 +96,18 @@ describe('getRemoteEntry - script load error discrimination', () => { }), ); }); + + it('remote container init failure is reported as RUNTIME_015 with the original error', async () => { + const entry = `${BASE}/init-error.js`; + const mf = new ModuleFederation({ + name: 'test-host', + remotes: [{ name: 'remote', entry }], + }); + + const err = await mf.loadRemote('remote/Button').catch((e) => e); + + expect(err.message).toContain(RUNTIME_015); + expect(err.message).toContain('remote init failed'); + expect(err.message).toContain('remoteEntryUrl'); + }); }); diff --git a/packages/runtime-core/__tests__/resources/load/init-error.js b/packages/runtime-core/__tests__/resources/load/init-error.js new file mode 100644 index 00000000000..18616880509 --- /dev/null +++ b/packages/runtime-core/__tests__/resources/load/init-error.js @@ -0,0 +1,11 @@ +// Script that registers a remote whose container init fails. +globalThis['remote'] = { + get: function () { + return function () { + return 'unused'; + }; + }, + init: function () { + throw new Error('remote init failed'); + }, +}; diff --git a/packages/runtime-core/__tests__/shared-diagnostics.spec.ts b/packages/runtime-core/__tests__/shared-diagnostics.spec.ts new file mode 100644 index 00000000000..c6a6088086c --- /dev/null +++ b/packages/runtime-core/__tests__/shared-diagnostics.spec.ts @@ -0,0 +1,166 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { RUNTIME_005 } from '@module-federation/error-codes'; +import { ModuleFederation } from '../src/core'; +import { resetFederationGlobalInfo } from '../src/global'; +import type { ModuleFederationRuntimePlugin } from '../src/type'; + +type SharedLifecycleEvent = + | { type: 'before'; pkgName: string } + | { + type: 'after'; + pkgName: string; + lifecycle: string; + selectedVersion?: string; + provider?: string; + } + | { + type: 'error'; + pkgName: string; + lifecycle: string; + recovered?: boolean; + availableVersions: string[]; + error?: unknown; + }; + +const createSharedLifecyclePlugin = ( + events: SharedLifecycleEvent[], +): ModuleFederationRuntimePlugin => ({ + name: 'shared-lifecycle-test-plugin', + beforeLoadShare(args) { + events.push({ + type: 'before', + pkgName: args.pkgName, + }); + return args; + }, + afterLoadShare(args) { + events.push({ + type: 'after', + pkgName: args.pkgName, + lifecycle: args.lifecycle, + selectedVersion: args.selectedShared?.version, + provider: args.selectedShared?.from, + }); + }, + errorLoadShare(args) { + events.push({ + type: 'error', + pkgName: args.pkgName, + lifecycle: args.lifecycle, + recovered: args.recovered, + availableVersions: Object.keys( + args.shareScopeMap.default?.[args.pkgName] || {}, + ), + error: args.error, + }); + }, +}); + +describe('shared lifecycle hooks', () => { + beforeEach(() => { + resetFederationGlobalInfo(); + }); + + it('emits beforeLoadShare and afterLoadShare for loadShare success', async () => { + const events: SharedLifecycleEvent[] = []; + const mf = new ModuleFederation({ + name: 'shared-lifecycle-host', + remotes: [], + plugins: [createSharedLifecyclePlugin(events)], + shared: { + 'diagnostics-shared': { + version: '1.0.0', + lib: () => ({ value: 'shared' }), + }, + }, + }); + + const factory = await mf.loadShare<{ value: string }>('diagnostics-shared'); + + expect(factory?.()).toEqual({ value: 'shared' }); + expect(events).toEqual([ + { + type: 'before', + pkgName: 'diagnostics-shared', + }, + { + type: 'after', + pkgName: 'diagnostics-shared', + lifecycle: 'loadShare', + selectedVersion: '1.0.0', + provider: 'shared-lifecycle-host', + }, + ]); + }); + + it('emits errorLoadShare when custom shared info cannot be matched', async () => { + const events: SharedLifecycleEvent[] = []; + const mf = new ModuleFederation({ + name: 'shared-lifecycle-version-host', + remotes: [], + plugins: [createSharedLifecyclePlugin(events)], + shared: { + react: { + version: '18.3.1', + lib: () => ({ version: '18.3.1' }), + }, + }, + }); + + const result = await mf.loadShare('react', { + customShareInfo: { + shareConfig: { + requiredVersion: '^99.0.0', + singleton: false, + }, + }, + }); + + expect(result).toBe(false); + expect(events.at(-1)).toEqual({ + type: 'error', + pkgName: 'react', + lifecycle: 'loadShare', + recovered: true, + availableVersions: ['18.3.1'], + error: undefined, + }); + }); + + it('emits errorLoadShare for async shared consumed synchronously', () => { + const events: SharedLifecycleEvent[] = []; + const mf = new ModuleFederation({ + name: 'shared-lifecycle-eager-host', + remotes: [], + plugins: [createSharedLifecyclePlugin(events)], + shared: {}, + }); + + expect(() => + mf.loadShareSync('diagnostics-async-shared', { + from: 'build', + customShareInfo: { + version: '1.0.0', + scope: ['default'], + shareConfig: { + requiredVersion: '^1.0.0', + singleton: false, + eager: false, + strictVersion: false, + }, + get: () => Promise.resolve(() => ({ value: 'async' })), + }, + }), + ).toThrow(RUNTIME_005); + + const errorEvent = events.at(-1); + expect(errorEvent).toMatchObject({ + type: 'error', + pkgName: 'diagnostics-async-shared', + lifecycle: 'loadShareSync', + recovered: undefined, + availableVersions: [], + }); + expect(errorEvent?.error).toBeInstanceOf(Error); + }); +}); diff --git a/packages/runtime-core/package.json b/packages/runtime-core/package.json index 71fce6de631..85dc041d3de 100644 --- a/packages/runtime-core/package.json +++ b/packages/runtime-core/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/runtime-core", - "version": "2.4.0", + "version": "2.5.0", "type": "module", "author": "zhouxiao ", "main": "./dist/index.cjs", diff --git a/packages/runtime-core/src/core.ts b/packages/runtime-core/src/core.ts index 80abc579a80..df09129a9a0 100644 --- a/packages/runtime-core/src/core.ts +++ b/packages/runtime-core/src/core.ts @@ -1,5 +1,6 @@ import { isBrowserEnvValue } from '@module-federation/sdk'; import type { + CreateLinkHookReturnDom, CreateScriptHookReturn, GlobalModuleInfo, ModuleInfo, @@ -17,6 +18,7 @@ import { InitScope, RemoteEntryInitOptions, CallFrom, + ResourceLoadContext, } from './type'; import { getBuilderId, registerPlugins, getRemoteEntry, error } from './utils'; import { @@ -24,7 +26,7 @@ import { RUNTIME_010, runtimeDescMap, } from '@module-federation/error-codes'; -import { Module } from './module'; +import { Module, type RemoteModuleFactory } from './module'; import { AsyncHook, AsyncWaterfallHook, @@ -120,6 +122,7 @@ export class ModuleFederation { * (e.g. preloadRemote / loading remoteEntry). */ remoteInfo?: RemoteInfo; + resourceContext?: ResourceLoadContext; }, ], CreateScriptHookReturn @@ -135,12 +138,13 @@ export class ModuleFederation { * (e.g. preloadRemote / loading remoteEntry). */ remoteInfo?: RemoteInfo; + resourceContext?: ResourceLoadContext; }, ], - HTMLLinkElement | void + CreateLinkHookReturnDom >(), fetch: new AsyncHook< - [string, RequestInit, RemoteInfo?], + [string, RequestInit, RemoteInfo?, ResourceLoadContext?], Promise | void | false >(), loadEntryError: new AsyncHook< @@ -159,6 +163,95 @@ export class ModuleFederation { ], Promise | undefined> >(), + afterLoadEntry: new AsyncHook< + [ + { + origin: ModuleFederation; + remoteInfo: RemoteInfo; + remoteEntryExports?: RemoteEntryExports | false | void; + error?: unknown; + recovered?: boolean; + }, + ], + void + >('afterLoadEntry'), + beforeInitRemote: new AsyncHook< + [ + { + id?: string; + remoteInfo: RemoteInfo; + remoteSnapshot?: ModuleInfo; + origin: ModuleFederation; + }, + ], + void + >('beforeInitRemote'), + afterInitRemote: new AsyncHook< + [ + { + id?: string; + remoteInfo: RemoteInfo; + remoteSnapshot?: ModuleInfo; + remoteEntryExports?: RemoteEntryExports; + error?: unknown; + cached?: boolean; + origin: ModuleFederation; + }, + ], + void + >('afterInitRemote'), + beforeGetExpose: new AsyncHook< + [ + { + id: string; + expose: string; + moduleInfo: RemoteInfo; + remoteEntryExports: RemoteEntryExports; + origin: ModuleFederation; + }, + ], + void + >('beforeGetExpose'), + afterGetExpose: new AsyncHook< + [ + { + id: string; + expose: string; + moduleInfo: RemoteInfo; + remoteEntryExports: RemoteEntryExports; + moduleFactory?: RemoteModuleFactory; + error?: unknown; + origin: ModuleFederation; + }, + ], + void + >('afterGetExpose'), + beforeExecuteFactory: new AsyncHook< + [ + { + id: string; + expose: string; + moduleInfo: RemoteInfo; + loadFactory: boolean; + origin: ModuleFederation; + }, + ], + void + >('beforeExecuteFactory'), + afterExecuteFactory: new AsyncHook< + [ + { + id: string; + expose: string; + moduleInfo: RemoteInfo; + loadFactory: boolean; + exposeModule?: unknown; + error?: unknown; + origin: ModuleFederation; + }, + ], + void + >('afterExecuteFactory'), getModuleFactory: new AsyncHook< [ { @@ -167,7 +260,7 @@ export class ModuleFederation { moduleInfo: RemoteInfo; }, ], - Promise<(() => Promise) | undefined> + RemoteModuleFactory | Promise | undefined >(), }); bridgeHook = new PluginSystem({ diff --git a/packages/runtime-core/src/module/index.ts b/packages/runtime-core/src/module/index.ts index b687761cf6a..d3a42074f12 100644 --- a/packages/runtime-core/src/module/index.ts +++ b/packages/runtime-core/src/module/index.ts @@ -1,14 +1,15 @@ import { - getFMId, assert, error, processModuleAlias, optionsToMFContext, + composeRemoteRequestId, } from '../utils'; import { safeToString, ModuleInfo } from '@module-federation/sdk'; import { RUNTIME_002, - RUNTIME_008, + RUNTIME_014, + RUNTIME_015, runtimeDescMap, } from '@module-federation/error-codes'; import { getRemoteEntry } from '../utils/load'; @@ -21,6 +22,25 @@ import { } from '../type'; export type ModuleOptions = ConstructorParameters[0]; +export type RemoteModuleFactory = () => unknown | Promise; + +function getAvailableExposeNames( + remoteSnapshot?: ModuleInfo, +): string | undefined { + if ( + !remoteSnapshot || + !('modules' in remoteSnapshot) || + !Array.isArray(remoteSnapshot.modules) + ) { + return undefined; + } + + const exposes = remoteSnapshot.modules + .map((module) => module.moduleName) + .filter(Boolean); + + return exposes.length ? exposes.join(',') : undefined; +} export function createRemoteEntryInitOptions( remoteInfo: RemoteInfo, @@ -87,7 +107,7 @@ class Module { this.host = host; } - async getEntry(): Promise { + async getEntry(expose?: string): Promise { if (this.remoteEntryExports) { return this.remoteEntryExports; } @@ -96,6 +116,11 @@ class Module { origin: this.host, remoteInfo: this.remoteInfo, remoteEntryExports: this.remoteEntryExports, + resourceContext: { + initiator: 'loadRemote', + id: composeRemoteRequestId(this.remoteInfo.name, expose), + resourceType: 'remoteEntry', + }, }); assert( @@ -113,21 +138,58 @@ class Module { id?: string, remoteSnapshot?: ModuleInfo, rawInitScope?: InitScope, + expose?: string, ) { // Get remoteEntry.js - const remoteEntryExports = await this.getEntry(); + const remoteEntryExports = await this.getEntry(expose); if (this.inited) { + await this.host.loaderHook.lifecycle.afterInitRemote.emit({ + id, + remoteInfo: this.remoteInfo, + remoteSnapshot, + remoteEntryExports, + cached: true, + origin: this.host, + }); return remoteEntryExports; } if (this.initPromise) { - await this.initPromise; + try { + await this.initPromise; + await this.host.loaderHook.lifecycle.afterInitRemote.emit({ + id, + remoteInfo: this.remoteInfo, + remoteSnapshot, + remoteEntryExports, + cached: true, + origin: this.host, + }); + } catch (initError) { + await this.host.loaderHook.lifecycle.afterInitRemote.emit({ + id, + remoteInfo: this.remoteInfo, + remoteSnapshot, + remoteEntryExports, + error: initError, + cached: true, + origin: this.host, + }); + throw initError; + } return remoteEntryExports; } this.initing = true; this.initPromise = (async () => { + await this.host.loaderHook.lifecycle.beforeInitRemote.emit({ + id, + remoteInfo: this.remoteInfo, + remoteSnapshot, + origin: this.host, + }); + const { remoteEntryInitOptions, shareScope, initScope } = createRemoteEntryInitOptions( this.remoteInfo, @@ -160,11 +222,27 @@ class Module { ); } - await remoteEntryExports.init( - initContainerOptions.shareScope, - initContainerOptions.initScope, - initContainerOptions.remoteEntryInitOptions, - ); + try { + await remoteEntryExports.init( + initContainerOptions.shareScope, + initContainerOptions.initScope, + initContainerOptions.remoteEntryInitOptions, + ); + } catch (initError) { + error( + RUNTIME_015, + runtimeDescMap, + { + hostName: this.host.name, + remoteName: this.remoteInfo.name, + remoteEntryUrl: this.remoteInfo.entry, + remoteEntryKey: this.remoteInfo.entryGlobalName, + shareScope: this.remoteInfo.shareScope, + }, + `${initError}`, + optionsToMFContext(this.host.options), + ); + } await this.host.hooks.lifecycle.initContainer.emit({ ...initContainerOptions, @@ -177,6 +255,23 @@ class Module { try { await this.initPromise; + await this.host.loaderHook.lifecycle.afterInitRemote.emit({ + id, + remoteInfo: this.remoteInfo, + remoteSnapshot, + remoteEntryExports, + origin: this.host, + }); + } catch (initError) { + await this.host.loaderHook.lifecycle.afterInitRemote.emit({ + id, + remoteInfo: this.remoteInfo, + remoteSnapshot, + remoteEntryExports, + error: initError, + origin: this.host, + }); + throw initError; } finally { this.initing = false; this.initPromise = undefined; @@ -194,25 +289,74 @@ class Module { ) { const { loadFactory = true } = options || { loadFactory: true }; - const remoteEntryExports = await this.init(id, remoteSnapshot); + const remoteEntryExports = await this.init( + id, + remoteSnapshot, + undefined, + expose, + ); this.lib = remoteEntryExports; - let moduleFactory; - moduleFactory = await this.host.loaderHook.lifecycle.getModuleFactory.emit({ - remoteEntryExports, + await this.host.loaderHook.lifecycle.beforeGetExpose.emit({ + id, expose, moduleInfo: this.remoteInfo, + remoteEntryExports, + origin: this.host, }); - // get exposeGetter - if (!moduleFactory) { - moduleFactory = await remoteEntryExports.get(expose); - } + let moduleFactory: RemoteModuleFactory | undefined; + try { + const hookModuleFactory = + await this.host.loaderHook.lifecycle.getModuleFactory.emit({ + remoteEntryExports, + expose, + moduleInfo: this.remoteInfo, + }); + moduleFactory = + typeof hookModuleFactory === 'function' ? hookModuleFactory : undefined; - assert( - moduleFactory, - `${getFMId(this.remoteInfo)} remote don't export ${expose}.`, - ); + // get exposeGetter + if (!moduleFactory) { + moduleFactory = await remoteEntryExports.get(expose); + } + + if (!moduleFactory) { + error( + RUNTIME_014, + runtimeDescMap, + { + hostName: this.host.name, + remoteName: this.remoteInfo.name, + remoteEntryUrl: this.remoteInfo.entry, + expose, + requestId: id, + availableExposes: getAvailableExposeNames(remoteSnapshot), + }, + undefined, + optionsToMFContext(this.host.options), + ); + } + + await this.host.loaderHook.lifecycle.afterGetExpose.emit({ + id, + expose, + moduleInfo: this.remoteInfo, + remoteEntryExports, + moduleFactory, + origin: this.host, + }); + } catch (getExposeError) { + await this.host.loaderHook.lifecycle.afterGetExpose.emit({ + id, + expose, + moduleInfo: this.remoteInfo, + remoteEntryExports, + error: getExposeError, + origin: this.host, + }); + throw getExposeError; + } // keep symbol for module name always one format const symbolName = processModuleAlias(this.remoteInfo.name, expose); @@ -221,16 +365,43 @@ class Module { if (!loadFactory) { return wrapModuleFactory; } - const exposeContent = await wrapModuleFactory(); - return exposeContent; + await this.host.loaderHook.lifecycle.beforeExecuteFactory.emit({ + id, + expose, + moduleInfo: this.remoteInfo, + loadFactory, + origin: this.host, + }); + + try { + const exposeContent = await wrapModuleFactory(); + + await this.host.loaderHook.lifecycle.afterExecuteFactory.emit({ + id, + expose, + moduleInfo: this.remoteInfo, + loadFactory, + exposeModule: exposeContent, + origin: this.host, + }); + + return exposeContent; + } catch (executeFactoryError) { + await this.host.loaderHook.lifecycle.afterExecuteFactory.emit({ + id, + expose, + moduleInfo: this.remoteInfo, + loadFactory, + error: executeFactoryError, + origin: this.host, + }); + throw executeFactoryError; + } } - private wraperFactory( - moduleFactory: () => any | (() => Promise), - id: string, - ) { - function defineModuleId(res: any, id: string) { + private wraperFactory(moduleFactory: RemoteModuleFactory, id: string) { + function defineModuleId(res: unknown, id: string) { if ( res && typeof res === 'object' && @@ -244,21 +415,21 @@ class Module { } } - if (moduleFactory instanceof Promise) { - return async () => { - const res = await moduleFactory(); - // This parameter is used for bridge debugging - defineModuleId(res, id); - return res; - }; - } else { - return () => { - const res = moduleFactory(); - // This parameter is used for bridge debugging - defineModuleId(res, id); - return res; - }; - } + return () => { + const res = moduleFactory(); + + if (res instanceof Promise) { + return res.then((asyncRes) => { + // This parameter is used for bridge debugging + defineModuleId(asyncRes, id); + return asyncRes; + }); + } + + // This parameter is used for bridge debugging + defineModuleId(res, id); + return res; + }; } } diff --git a/packages/runtime-core/src/plugins/snapshot/SnapshotHandler.ts b/packages/runtime-core/src/plugins/snapshot/SnapshotHandler.ts index d5a44bd946d..e3661cfdbf7 100644 --- a/packages/runtime-core/src/plugins/snapshot/SnapshotHandler.ts +++ b/packages/runtime-core/src/plugins/snapshot/SnapshotHandler.ts @@ -9,9 +9,10 @@ import { import { RUNTIME_003, RUNTIME_007, + RUNTIME_013, runtimeDescMap, } from '@module-federation/error-codes'; -import { Options, Remote } from '../../type'; +import { Options, Remote, ResourceLoadInitiator } from '../../type'; import { isRemoteInfoWithEntry, error, @@ -28,7 +29,6 @@ import { } from '../../global'; import { PluginSystem, AsyncHook, AsyncWaterfallHook } from '../../utils/hooks'; import { ModuleFederation } from '../../core'; -import { assert } from '../../utils/logger'; export function getGlobalRemoteInfo( moduleInfo: Remote, @@ -81,6 +81,7 @@ export class SnapshotHandler { { options: Options; moduleInfo: Remote; + origin: ModuleFederation; }, ], void @@ -121,11 +122,11 @@ export class SnapshotHandler { async loadRemoteSnapshotInfo({ moduleInfo, id, - expose, + initiator = 'loadRemote', }: { moduleInfo: Remote; id?: string; - expose?: string; + initiator?: ResourceLoadInitiator; }): | Promise<{ remoteSnapshot: ModuleInfo; @@ -137,6 +138,7 @@ export class SnapshotHandler { await this.hooks.lifecycle.beforeLoadRemoteSnapshot.emit({ options, moduleInfo, + origin: this.HostInstance, }); let hostSnapshot = getGlobalSnapshotInfoByModuleInfo({ @@ -196,10 +198,14 @@ export class SnapshotHandler { : globalRemoteSnapshot.ssrRemoteEntry || globalRemoteSnapshot.remoteEntry || ''; - const moduleSnapshot = await this.getManifestJson( + const moduleSnapshot = await this.loadManifestSnapshot( remoteEntry, moduleInfo, {}, + { + initiator, + id: id || moduleInfo.name, + }, ); // eslint-disable-next-line @typescript-eslint/no-shadow const globalSnapshotRes = setGlobalSnapshotInfoByModuleInfo( @@ -227,25 +233,21 @@ export class SnapshotHandler { } else { if (isRemoteInfoWithEntry(moduleInfo)) { // get from manifest.json and merge remote info from remote server - const moduleSnapshot = await this.getManifestJson( + const moduleSnapshot = await this.loadManifestSnapshot( moduleInfo.entry, moduleInfo, {}, + { + initiator, + id: id || moduleInfo.name, + }, ); // eslint-disable-next-line @typescript-eslint/no-shadow const globalSnapshotRes = setGlobalSnapshotInfoByModuleInfo( moduleInfo, moduleSnapshot, ); - const { remoteSnapshot: remoteSnapshotRes } = - await this.hooks.lifecycle.loadRemoteSnapshot.emit({ - options: this.HostInstance.options, - moduleInfo, - remoteSnapshot: moduleSnapshot, - from: 'global', - }); - - mSnapshot = remoteSnapshotRes; + mSnapshot = moduleSnapshot; gSnapshot = globalSnapshotRes; } else { error( @@ -289,8 +291,13 @@ export class SnapshotHandler { manifestUrl: string, moduleInfo: Remote, extraOptions: Record, - ): Promise { + resourceOptions?: { + initiator: ResourceLoadInitiator; + id: string; + }, + ): Promise { const getManifest = async (): Promise => { + const remoteInfo = getRemoteInfo(moduleInfo); let manifestJson: Manifest | undefined = this.manifestCache.get(manifestUrl); if (manifestJson) { @@ -300,7 +307,14 @@ export class SnapshotHandler { let res = await this.loaderHook.lifecycle.fetch.emit( manifestUrl, {}, - getRemoteInfo(moduleInfo), + remoteInfo, + resourceOptions + ? { + ...resourceOptions, + url: manifestUrl, + resourceType: 'manifest', + } + : undefined, ); if (!res || !(res instanceof Response)) { res = await fetch(manifestUrl, {}); @@ -314,6 +328,7 @@ export class SnapshotHandler { error: err, from: 'runtime', lifecycle: 'afterResolve', + remote: remoteInfo, origin: this.HostInstance, }, )) as Manifest | undefined; @@ -334,16 +349,63 @@ export class SnapshotHandler { } } - assert( - manifestJson.metaData && manifestJson.exposes && manifestJson.shared, - `"${manifestUrl}" is not a valid federation manifest for remote "${moduleInfo.name}". Missing required fields: ${[!manifestJson.metaData && 'metaData', !manifestJson.exposes && 'exposes', !manifestJson.shared && 'shared'].filter(Boolean).join(', ')}.`, - ); + const missingRequiredFields = [ + !manifestJson.metaData && 'metaData', + !manifestJson.exposes && 'exposes', + !manifestJson.shared && 'shared', + ].filter(Boolean); + if (missingRequiredFields.length > 0) { + await this.HostInstance.remoteHandler.hooks.lifecycle.errorLoadRemote.emit( + { + id: manifestUrl, + error: new Error( + `"${manifestUrl}" is not a valid federation manifest for remote "${moduleInfo.name}". Missing required fields: ${missingRequiredFields.join(', ')}.`, + ), + from: 'runtime', + lifecycle: 'afterResolve', + remote: remoteInfo, + origin: this.HostInstance, + }, + ); + } + + if (missingRequiredFields.length > 0) { + error( + RUNTIME_013, + runtimeDescMap, + { + manifestUrl, + moduleName: moduleInfo.name, + hostName: this.HostInstance.options.name, + missingFields: missingRequiredFields.join(','), + }, + undefined, + optionsToMFContext(this.HostInstance.options), + ); + } this.manifestCache.set(manifestUrl, manifestJson); return manifestJson; }; + return getManifest(); + } + + private async loadManifestSnapshot( + manifestUrl: string, + moduleInfo: Remote, + extraOptions: Record, + resourceOptions?: { + initiator: ResourceLoadInitiator; + id: string; + }, + ): Promise { const asyncLoadProcess = async () => { - const manifestJson = await getManifest(); + const manifestJson = await this.getManifestJson( + manifestUrl, + moduleInfo, + extraOptions, + resourceOptions, + ); const remoteSnapshot = generateSnapshotFromManifest(manifestJson, { version: manifestUrl, }); diff --git a/packages/runtime-core/src/plugins/snapshot/index.ts b/packages/runtime-core/src/plugins/snapshot/index.ts index fa09668b674..c9d864621ac 100644 --- a/packages/runtime-core/src/plugins/snapshot/index.ts +++ b/packages/runtime-core/src/plugins/snapshot/index.ts @@ -7,6 +7,7 @@ import { ModuleFederationRuntimePlugin } from '../../type/plugin'; import { RUNTIME_011, runtimeDescMap } from '@module-federation/error-codes'; import { error, + composeRemoteRequestId, isPureRemoteEntry, isRemoteInfoWithEntry, getRemoteEntryInfoFromSnapshot, @@ -46,7 +47,7 @@ export function snapshotPlugin(): ModuleFederationRuntimePlugin { const { remoteSnapshot, globalSnapshot } = await origin.snapshotHandler.loadRemoteSnapshotInfo({ moduleInfo: remote, - id, + id: composeRemoteRequestId(remote.name, expose), }); assignRemoteInfo(remoteInfo, remoteSnapshot); @@ -75,7 +76,10 @@ export function snapshotPlugin(): ModuleFederationRuntimePlugin { ); if (assets) { - preloadAssets(remoteInfo, origin, assets, false); + preloadAssets(remoteInfo, origin, assets, false, { + initiator: 'loadRemote', + id, + }).catch(() => undefined); } return { diff --git a/packages/runtime-core/src/remote/index.ts b/packages/runtime-core/src/remote/index.ts index f8eca7ea8c3..37b12ae10c3 100644 --- a/packages/runtime-core/src/remote/index.ts +++ b/packages/runtime-core/src/remote/index.ts @@ -18,6 +18,7 @@ import { PreloadAssets, PreloadOptions, PreloadRemoteArgs, + PreloadRemoteResult, Remote, RemoteInfo, RemoteEntryExports, @@ -36,6 +37,7 @@ import { error, getRemoteInfo, getRemoteEntryUniqueKey, + composeRemoteRequestId, matchRemoteWithNameAndExpose, optionsToMFContext, logger, @@ -75,6 +77,20 @@ export class RemoteHandler { options: Options; origin: ModuleFederation; }>('beforeRequest'), + afterMatchRemote: new AsyncHook< + [ + { + id: string; + options: Options; + remote?: Remote; + expose?: string; + remoteInfo?: RemoteInfo; + error?: unknown; + origin: ModuleFederation; + }, + ], + void + >('afterMatchRemote'), onLoad: new AsyncHook< [ { @@ -89,8 +105,25 @@ export class RemoteHandler { moduleInstance: Module; }, ], - void + unknown >('onLoad'), + afterLoadRemote: new AsyncHook< + [ + { + id: string; + expose?: string; + remote?: RemoteInfo; + options?: { + loadFactory?: boolean; + from?: CallFrom; + }; + error?: unknown; + recovered?: boolean; + origin: ModuleFederation; + }, + ], + void + >('afterLoadRemote'), handlePreloadModule: new SyncHook< [ { @@ -116,6 +149,8 @@ export class RemoteHandler { | 'beforeLoadShare' | 'afterResolve' | 'onLoad'; + remote?: RemoteInfo; + expose?: string; origin: ModuleFederation; }, ], @@ -143,22 +178,28 @@ export class RemoteHandler { ], Promise >('generatePreloadAssets'), - // not used yet - afterPreloadRemote: new AsyncHook<{ - preloadOps: Array; - options: Options; - origin: ModuleFederation; - }>(), + afterPreloadRemote: new AsyncHook< + [ + { + preloadOps: Array; + options: Options; + origin: ModuleFederation; + results: PreloadRemoteResult[]; + error?: unknown; + }, + ] + >('afterPreloadRemote'), // TODO: Move to loaderHook loadEntry: new AsyncHook< [ { + origin: ModuleFederation; loaderHook: ModuleFederation['loaderHook']; remoteInfo: RemoteInfo; remoteEntryExports?: RemoteEntryExports; }, ], - Promise + Promise | RemoteEntryExports | void >(), }); @@ -199,6 +240,21 @@ export class RemoteHandler { options?: { loadFactory?: boolean; from: CallFrom }, ): Promise { const { host } = this; + const startMatchInfo = matchRemoteWithNameAndExpose( + host.options.remotes, + id, + ); + let completeRequestId = id; + let completeExpose = startMatchInfo?.expose; + let completeRemote = startMatchInfo + ? getRemoteInfo(startMatchInfo.remote) + : undefined; + let afterLoadRemoteArgs: + | Parameters< + RemoteHandler['hooks']['lifecycle']['afterLoadRemote']['emit'] + >[0] + | undefined; + try { const { loadFactory = true } = options || { loadFactory: true, @@ -221,6 +277,9 @@ export class RemoteHandler { id: idRes, remoteSnapshot, } = remoteMatchInfo; + completeRequestId = idRes; + completeExpose = expose; + completeRemote = getRemoteInfo(remote); const moduleOrFactory = (await module.get( idRes, @@ -242,6 +301,14 @@ export class RemoteHandler { }); this.setIdToRemoteMap(id, remoteMatchInfo); + afterLoadRemoteArgs = { + id: completeRequestId, + expose: completeExpose, + remote: completeRemote, + options, + origin: host, + }; + if (typeof moduleWrapper === 'function') { return moduleWrapper as T; } @@ -250,25 +317,63 @@ export class RemoteHandler { } catch (error) { const { from = 'runtime' } = options || { from: 'runtime' }; - const failOver = await this.hooks.lifecycle.errorLoadRemote.emit({ - id, - error, - from, - lifecycle: 'onLoad', - origin: host, - }); + let failOver; + try { + failOver = await this.hooks.lifecycle.errorLoadRemote.emit({ + id, + error, + from, + lifecycle: 'onLoad', + expose: completeExpose, + remote: completeRemote, + origin: host, + }); + } catch (hookError) { + afterLoadRemoteArgs = { + id: completeRequestId, + expose: completeExpose, + remote: completeRemote, + options, + error: hookError, + origin: host, + }; + throw hookError; + } if (!failOver) { + afterLoadRemoteArgs = { + id: completeRequestId, + expose: completeExpose, + remote: completeRemote, + options, + error, + origin: host, + }; throw error; } + afterLoadRemoteArgs = { + id: completeRequestId, + expose: completeExpose, + remote: completeRemote, + options, + error, + origin: host, + recovered: true, + }; + return failOver as T; + } finally { + if (afterLoadRemoteArgs) { + await this.hooks.lifecycle.afterLoadRemote.emit(afterLoadRemoteArgs); + } } } // eslint-disable-next-line @typescript-eslint/member-ordering async preloadRemote(preloadOptions: Array): Promise { const { host } = this; + const preloadResults: PreloadRemoteResult[] = []; await this.hooks.lifecycle.beforePreloadRemote.emit({ preloadOps: preloadOptions, @@ -281,29 +386,117 @@ export class RemoteHandler { preloadOptions, ); + const createPreloadAssetOps = (ops: PreloadOptions[number]) => { + const { preloadConfig, remote } = ops; + const exposes = preloadConfig.exposes || []; + + if (!exposes.length) { + return [ + { + ops, + id: `${remote.name}/*`, + }, + ]; + } + + return exposes.map((expose) => ({ + ops: { + ...ops, + preloadConfig: { + ...preloadConfig, + exposes: [expose], + }, + }, + id: composeRemoteRequestId(remote.name, expose), + })); + }; + + let preloadError: Error | undefined; + await Promise.all( - preloadOps.map(async (ops) => { - const { remote } = ops; + preloadOps.flatMap(createPreloadAssetOps).map(async (assetOps) => { + const { ops, id: preloadId } = assetOps; + const { remote, preloadConfig } = ops; const remoteInfo = getRemoteInfo(remote); - const { globalSnapshot, remoteSnapshot } = - await host.snapshotHandler.loadRemoteSnapshotInfo({ - moduleInfo: remote, + try { + const { globalSnapshot, remoteSnapshot } = + await host.snapshotHandler.loadRemoteSnapshotInfo({ + moduleInfo: remote, + id: preloadId, + initiator: 'preloadRemote', + }); + + const assets = await this.hooks.lifecycle.generatePreloadAssets.emit({ + origin: host, + preloadOptions: ops, + remote, + remoteInfo, + globalSnapshot, + remoteSnapshot, + }); + if (!assets) { + return; + } + const results = await preloadAssets(remoteInfo, host, assets, true, { + initiator: 'preloadRemote', + id: preloadId, + }); + preloadResults.push({ + remote, + remoteInfo, + preloadConfig, + id: preloadId, + results, + }); + } catch (error) { + preloadResults.push({ + remote, + remoteInfo, + preloadConfig, + id: preloadId, + results: [ + { + url: remoteInfo.entry, + status: 'error', + resourceType: /\.json(?:$|[?#])/i.test(remoteInfo.entry) + ? 'manifest' + : 'remoteEntry', + initiator: 'preloadRemote', + id: preloadId, + error, + }, + ], }); - - const assets = await this.hooks.lifecycle.generatePreloadAssets.emit({ - origin: host, - preloadOptions: ops, - remote, - remoteInfo, - globalSnapshot, - remoteSnapshot, - }); - if (!assets) { - return; } - preloadAssets(remoteInfo, host, assets); }), ); + + const failedResults = preloadResults.flatMap((preloadResult) => + preloadResult.results.filter( + (result) => result.status === 'error' || result.status === 'timeout', + ), + ); + if (failedResults.length > 0) { + preloadError = new Error( + `preloadRemote failed to load ${failedResults.length} resource(s).`, + ); + Object.assign(preloadError, { + results: preloadResults, + failedResults, + }); + } + + await this.hooks.lifecycle.afterPreloadRemote.emit({ + preloadOps: preloadOptions, + options: host.options, + origin: host, + results: preloadResults, + error: preloadError, + }); + + if (preloadError) { + throw preloadError; + } } registerRemotes(remotes: Remote[], options?: { force?: boolean }): void { @@ -356,20 +549,37 @@ export class RemoteHandler { idRes, ); if (!remoteSplitInfo) { - error( - RUNTIME_004, - runtimeDescMap, - { - hostName: host.options.name, - requestId: idRes, - }, - undefined, - optionsToMFContext(host.options), - ); + try { + error( + RUNTIME_004, + runtimeDescMap, + { + hostName: host.options.name, + requestId: idRes, + }, + undefined, + optionsToMFContext(host.options), + ); + } catch (matchError) { + await this.hooks.lifecycle.afterMatchRemote.emit({ + id: idRes, + options: host.options, + error: matchError, + origin: host, + }); + throw matchError; + } } const { remote: rawRemote } = remoteSplitInfo; const remoteInfo = getRemoteInfo(rawRemote); + await this.hooks.lifecycle.afterMatchRemote.emit({ + id: idRes, + ...remoteSplitInfo, + options: host.options, + remoteInfo, + origin: host, + }); const matchInfo = await host.sharedHandler.hooks.lifecycle.afterResolve.emit({ id: idRes, diff --git a/packages/runtime-core/src/shared/index.ts b/packages/runtime-core/src/shared/index.ts index 50b6b30d12a..07ca4d05942 100644 --- a/packages/runtime-core/src/shared/index.ts +++ b/packages/runtime-core/src/shared/index.ts @@ -23,6 +23,7 @@ import { AsyncHook, AsyncWaterfallHook, SyncWaterfallHook, + SyncHook, } from '../utils/hooks'; import { formatShareConfigs, @@ -33,7 +34,13 @@ import { shouldUseTreeShaking, addUseIn, } from '../utils/share'; -import { assert, error, addUniqueItem, optionsToMFContext } from '../utils'; +import { + assert, + error, + addUniqueItem, + optionsToMFContext, + warn, +} from '../utils'; import { DEFAULT_SCOPE } from '../constant'; import { LoadRemoteMatch } from '../remote'; import { createRemoteEntryInitOptions } from '../module'; @@ -56,6 +63,35 @@ export class SharedHandler { }>('beforeLoadShare'), // not used yet loadShare: new AsyncHook<[ModuleFederation, string, ShareInfos]>(), + afterLoadShare: new SyncHook< + [ + { + pkgName: string; + shareInfo?: Partial; + selectedShared?: Partial; + shared: Options['shared']; + shareScopeMap: ShareScopeMap; + lifecycle: 'loadShare' | 'loadShareSync'; + origin: ModuleFederation; + }, + ], + void + >('afterLoadShare'), + errorLoadShare: new SyncHook< + [ + { + pkgName: string; + shareInfo?: Partial; + shared: Options['shared']; + shareScopeMap: ShareScopeMap; + lifecycle: 'loadShare' | 'loadShareSync'; + origin: ModuleFederation; + error?: unknown; + recovered?: boolean; + }, + ], + void + >('errorLoadShare'), resolveShare: new SyncWaterfallHook<{ shareScopeMap: ShareScopeMap; scope: string; @@ -82,6 +118,61 @@ export class SharedHandler { this._setGlobalShareScopeMap(host.options); } + private emitAfterLoadShare({ + lifecycle, + pkgName, + shareInfo, + selectedShared, + }: { + lifecycle: 'loadShare' | 'loadShareSync'; + pkgName: string; + shareInfo?: Partial; + selectedShared?: Partial; + }): void { + try { + this.hooks.lifecycle.afterLoadShare.emit({ + pkgName, + shareInfo, + selectedShared, + shared: this.host.options.shared, + shareScopeMap: this.shareScopeMap, + lifecycle, + origin: this.host, + }); + } catch (error) { + warn(error); + } + } + + private emitErrorLoadShare({ + lifecycle, + pkgName, + shareInfo, + error, + recovered, + }: { + lifecycle: 'loadShare' | 'loadShareSync'; + pkgName: string; + shareInfo?: Partial; + error?: unknown; + recovered?: boolean; + }): void { + try { + this.hooks.lifecycle.errorLoadShare.emit({ + pkgName, + shareInfo, + shared: this.host.options.shared, + shareScopeMap: this.shareScopeMap, + lifecycle, + origin: this.host, + error, + recovered, + }); + } catch (hookError) { + warn(hookError); + } + } + // register shared in shareScopeMap registerShared(globalOptions: Options, userOptions: UserOptions) { const { newShareInfos, allShareInfos } = formatShareConfigs( @@ -138,117 +229,163 @@ export class SharedHandler { extraOptions, shareInfos: host.options.shared, }); + let shareOptionsRes: Shared | undefined = shareOptions; - if (shareOptions?.scope) { - await Promise.all( - shareOptions.scope.map(async (shareScope) => { - await Promise.all( - this.initializeSharing(shareScope, { - strategy: shareOptions.strategy, - }), - ); - return; - }), - ); - } - const loadShareRes = await this.hooks.lifecycle.beforeLoadShare.emit({ - pkgName, - shareInfo: shareOptions, - shared: host.options.shared, - origin: host, - }); - - const { shareInfo: shareOptionsRes } = loadShareRes; + try { + if (shareOptions?.scope) { + await Promise.all( + shareOptions.scope.map(async (shareScope) => { + await Promise.all( + this.initializeSharing(shareScope, { + strategy: shareOptions.strategy, + }), + ); + return; + }), + ); + } + const loadShareRes = await this.hooks.lifecycle.beforeLoadShare.emit({ + pkgName, + shareInfo: shareOptions, + shared: host.options.shared, + origin: host, + }); - // Assert that shareInfoRes exists, if not, throw an error - assert( - shareOptionsRes, - `Cannot find shared "${pkgName}" in host "${host.options.name}". Ensure the shared config for "${pkgName}" is declared in the federation plugin options and the host has been initialized before loading shares.`, - ); + shareOptionsRes = loadShareRes.shareInfo; - const { shared: registeredShared, useTreesShaking } = - getRegisteredShare( - this.shareScopeMap, - pkgName, + // Assert that shareInfoRes exists, if not, throw an error + assert( shareOptionsRes, - this.hooks.lifecycle.resolveShare, - ) || {}; - - if (registeredShared) { - const targetShared = directShare(registeredShared, useTreesShaking); - if (targetShared.lib) { - addUseIn(targetShared, host.options.name); - return targetShared.lib as () => T; - } else if (targetShared.loading && !targetShared.loaded) { - const factory = await targetShared.loading; - targetShared.loaded = true; - if (!targetShared.lib) { - targetShared.lib = factory; + `Cannot find shared "${pkgName}" in host "${host.options.name}". Ensure the shared config for "${pkgName}" is declared in the federation plugin options and the host has been initialized before loading shares.`, + ); + const resolvedShareOptions = shareOptionsRes; + + const { shared: registeredShared, useTreesShaking } = + getRegisteredShare( + this.shareScopeMap, + pkgName, + shareOptionsRes, + this.hooks.lifecycle.resolveShare, + ) || {}; + + if (registeredShared) { + const targetShared = directShare(registeredShared, useTreesShaking); + if (targetShared.lib) { + addUseIn(targetShared, host.options.name); + this.emitAfterLoadShare({ + lifecycle: 'loadShare', + pkgName, + shareInfo: resolvedShareOptions, + selectedShared: registeredShared, + }); + return targetShared.lib as () => T; + } else if (targetShared.loading && !targetShared.loaded) { + const factory = await targetShared.loading; + targetShared.loaded = true; + if (!targetShared.lib) { + targetShared.lib = factory; + } + addUseIn(targetShared, host.options.name); + this.emitAfterLoadShare({ + lifecycle: 'loadShare', + pkgName, + shareInfo: resolvedShareOptions, + selectedShared: registeredShared, + }); + return factory; + } else { + const asyncLoadProcess = async () => { + const factory = await targetShared.get!(); + addUseIn(targetShared, host.options.name); + targetShared.loaded = true; + targetShared.lib = factory; + return factory as () => T; + }; + const loading = asyncLoadProcess(); + this.setShared({ + pkgName, + loaded: false, + shared: registeredShared, + from: host.options.name, + lib: null, + loading, + treeShaking: useTreesShaking + ? (targetShared as TreeShakingArgs) + : undefined, + }); + const factory = await loading; + this.emitAfterLoadShare({ + lifecycle: 'loadShare', + pkgName, + shareInfo: resolvedShareOptions, + selectedShared: registeredShared, + }); + return factory; } - addUseIn(targetShared, host.options.name); - return factory; } else { + if (extraOptions?.customShareInfo) { + this.emitErrorLoadShare({ + lifecycle: 'loadShare', + pkgName, + shareInfo: resolvedShareOptions, + recovered: true, + }); + return false; + } + const _useTreeShaking = shouldUseTreeShaking( + resolvedShareOptions.treeShaking, + ); + const targetShared = directShare(resolvedShareOptions, _useTreeShaking); + const asyncLoadProcess = async () => { const factory = await targetShared.get!(); - addUseIn(targetShared, host.options.name); - targetShared.loaded = true; targetShared.lib = factory; + targetShared.loaded = true; + addUseIn(targetShared, host.options.name); + const { shared: gShared, useTreesShaking: gUseTreeShaking } = + getRegisteredShare( + this.shareScopeMap, + pkgName, + resolvedShareOptions, + this.hooks.lifecycle.resolveShare, + ) || {}; + if (gShared) { + const targetGShared = directShare(gShared, gUseTreeShaking); + targetGShared.lib = factory; + targetGShared.loaded = true; + gShared.from = resolvedShareOptions.from; + } return factory as () => T; }; const loading = asyncLoadProcess(); this.setShared({ pkgName, loaded: false, - shared: registeredShared, + shared: resolvedShareOptions, from: host.options.name, lib: null, loading, - treeShaking: useTreesShaking + treeShaking: _useTreeShaking ? (targetShared as TreeShakingArgs) : undefined, }); - return loading; - } - } else { - if (extraOptions?.customShareInfo) { - return false; + const factory = await loading; + this.emitAfterLoadShare({ + lifecycle: 'loadShare', + pkgName, + shareInfo: resolvedShareOptions, + selectedShared: resolvedShareOptions, + }); + return factory; } - const _useTreeShaking = shouldUseTreeShaking(shareOptionsRes.treeShaking); - const targetShared = directShare(shareOptionsRes, _useTreeShaking); - - const asyncLoadProcess = async () => { - const factory = await targetShared.get!(); - targetShared.lib = factory; - targetShared.loaded = true; - addUseIn(targetShared, host.options.name); - const { shared: gShared, useTreesShaking: gUseTreeShaking } = - getRegisteredShare( - this.shareScopeMap, - pkgName, - shareOptionsRes, - this.hooks.lifecycle.resolveShare, - ) || {}; - if (gShared) { - const targetGShared = directShare(gShared, gUseTreeShaking); - targetGShared.lib = factory; - targetGShared.loaded = true; - gShared.from = shareOptionsRes.from; - } - return factory as () => T; - }; - const loading = asyncLoadProcess(); - this.setShared({ + } catch (shareError) { + this.emitErrorLoadShare({ + lifecycle: 'loadShare', pkgName, - loaded: false, - shared: shareOptionsRes, - from: host.options.name, - lib: null, - loading, - treeShaking: _useTreeShaking - ? (targetShared as TreeShakingArgs) - : undefined, + shareInfo: shareOptionsRes, + error: shareError, }); - return loading; + throw shareError; } } @@ -326,6 +463,7 @@ export class SharedHandler { error, from: 'runtime', lifecycle: 'beforeLoadShare', + remote: module.remoteInfo, origin: host, })) as RemoteEntryExports; if (!remoteEntryExports) { @@ -381,93 +519,129 @@ export class SharedHandler { shareInfos: host.options.shared, }); - if (shareOptions?.scope) { - shareOptions.scope.forEach((shareScope) => { - this.initializeSharing(shareScope, { strategy: shareOptions.strategy }); - }); - } - const { shared: registeredShared, useTreesShaking } = - getRegisteredShare( - this.shareScopeMap, - pkgName, - shareOptions, - this.hooks.lifecycle.resolveShare, - ) || {}; - - if (registeredShared) { - if (typeof registeredShared.lib === 'function') { - addUseIn(registeredShared, host.options.name); - if (!registeredShared.loaded) { - registeredShared.loaded = true; - if (registeredShared.from === host.options.name) { - shareOptions.loaded = true; - } - } - return registeredShared.lib as () => T; + try { + if (shareOptions?.scope) { + shareOptions.scope.forEach((shareScope) => { + this.initializeSharing(shareScope, { + strategy: shareOptions.strategy, + }); + }); } - if (typeof registeredShared.get === 'function') { - const module = registeredShared.get(); - if (!(module instanceof Promise)) { + const { shared: registeredShared } = + getRegisteredShare( + this.shareScopeMap, + pkgName, + shareOptions, + this.hooks.lifecycle.resolveShare, + ) || {}; + + if (registeredShared) { + if (typeof registeredShared.lib === 'function') { addUseIn(registeredShared, host.options.name); - this.setShared({ + if (!registeredShared.loaded) { + registeredShared.loaded = true; + if (registeredShared.from === host.options.name) { + shareOptions.loaded = true; + } + } + this.emitAfterLoadShare({ + lifecycle: 'loadShareSync', pkgName, - loaded: true, - from: host.options.name, - lib: module, - shared: registeredShared, + shareInfo: shareOptions, + selectedShared: registeredShared, }); - return module; + return registeredShared.lib as () => T; + } + if (typeof registeredShared.get === 'function') { + const module = registeredShared.get(); + if (!(module instanceof Promise)) { + addUseIn(registeredShared, host.options.name); + this.setShared({ + pkgName, + loaded: true, + from: host.options.name, + lib: module, + shared: registeredShared, + }); + this.emitAfterLoadShare({ + lifecycle: 'loadShareSync', + pkgName, + shareInfo: shareOptions, + selectedShared: registeredShared, + }); + return module; + } } } - } - if (shareOptions.lib) { - if (!shareOptions.loaded) { - shareOptions.loaded = true; + if (shareOptions.lib) { + if (!shareOptions.loaded) { + shareOptions.loaded = true; + } + this.emitAfterLoadShare({ + lifecycle: 'loadShareSync', + pkgName, + shareInfo: shareOptions, + selectedShared: shareOptions, + }); + return shareOptions.lib as () => T; } - return shareOptions.lib as () => T; - } - if (shareOptions.get) { - const module = shareOptions.get(); - - if (module instanceof Promise) { - const errorCode = - extraOptions?.from === 'build' ? RUNTIME_005 : RUNTIME_006; - error( - errorCode, - runtimeDescMap, - { - hostName: host.options.name, - sharedPkgName: pkgName, - }, - undefined, - optionsToMFContext(host.options), - ); - } + if (shareOptions.get) { + const module = shareOptions.get(); - shareOptions.lib = module; + if (module instanceof Promise) { + const errorCode = + extraOptions?.from === 'build' ? RUNTIME_005 : RUNTIME_006; + error( + errorCode, + runtimeDescMap, + { + hostName: host.options.name, + sharedPkgName: pkgName, + }, + undefined, + optionsToMFContext(host.options), + ); + } + + shareOptions.lib = module; - this.setShared({ + this.setShared({ + pkgName, + loaded: true, + from: host.options.name, + lib: shareOptions.lib, + shared: shareOptions, + }); + this.emitAfterLoadShare({ + lifecycle: 'loadShareSync', + pkgName, + shareInfo: shareOptions, + selectedShared: shareOptions, + }); + return shareOptions.lib as () => T; + } + + error( + RUNTIME_006, + runtimeDescMap, + { + hostName: host.options.name, + sharedPkgName: pkgName, + }, + undefined, + optionsToMFContext(host.options), + ); + } catch (shareError) { + this.emitErrorLoadShare({ + lifecycle: 'loadShareSync', pkgName, - loaded: true, - from: host.options.name, - lib: shareOptions.lib, - shared: shareOptions, + shareInfo: shareOptions, + error: shareError, }); - return shareOptions.lib as () => T; + throw shareError; } - - error( - RUNTIME_006, - runtimeDescMap, - { - hostName: host.options.name, - sharedPkgName: pkgName, - }, - undefined, - optionsToMFContext(host.options), - ); } initShareScopeMap( diff --git a/packages/runtime-core/src/type/preload.ts b/packages/runtime-core/src/type/preload.ts index 8003c17e212..7b43d030042 100644 --- a/packages/runtime-core/src/type/preload.ts +++ b/packages/runtime-core/src/type/preload.ts @@ -18,6 +18,36 @@ export type PreloadOptions = Array<{ preloadConfig: PreloadConfig; }>; +export type ResourceLoadInitiator = 'loadRemote' | 'preloadRemote'; + +export type ResourceLoadType = 'manifest' | 'remoteEntry' | 'js' | 'css'; + +export interface ResourceLoadContext { + initiator: ResourceLoadInitiator; + id: string; + resourceType: ResourceLoadType; + url?: string; +} + +export type PreloadAssetStatus = 'success' | 'error' | 'timeout' | 'cached'; + +export interface PreloadAssetResult { + url: string; + status: PreloadAssetStatus; + resourceType: ResourceLoadType; + initiator: ResourceLoadInitiator; + id: string; + error?: unknown; +} + +export interface PreloadRemoteResult { + remote: Remote; + remoteInfo: RemoteInfo; + preloadConfig: PreloadConfig; + id: string; + results: PreloadAssetResult[]; +} + export type EntryAssets = { name: string; url: string; diff --git a/packages/runtime-core/src/utils/hooks/asyncHook.ts b/packages/runtime-core/src/utils/hooks/asyncHook.ts index d9692b2d914..f401593e85a 100644 --- a/packages/runtime-core/src/utils/hooks/asyncHook.ts +++ b/packages/runtime-core/src/utils/hooks/asyncHook.ts @@ -13,17 +13,27 @@ export class AsyncHook< const ls = Array.from(this.listeners); if (ls.length > 0) { let i = 0; - const call = (prev?: any): any => { + const call = (prev?: unknown): unknown => { if (prev === false) { return false; // Abort process } else if (i < ls.length) { - return Promise.resolve(ls[i++].apply(null, data)).then(call); + return Promise.resolve(ls[i++].apply(null, data)).then((result) => { + if ( + result === undefined || + (data.length === 1 && result === data[0]) + ) { + return call(prev); + } + return call(result); + }); } else { return prev; } }; result = call(); } - return Promise.resolve(result); + return Promise.resolve(result) as Promise< + void | false | ExternalEmitReturnType + >; } } diff --git a/packages/runtime-core/src/utils/hooks/asyncWaterfallHooks.ts b/packages/runtime-core/src/utils/hooks/asyncWaterfallHooks.ts index 3660d449444..43098a80c91 100644 --- a/packages/runtime-core/src/utils/hooks/asyncWaterfallHooks.ts +++ b/packages/runtime-core/src/utils/hooks/asyncWaterfallHooks.ts @@ -3,9 +3,9 @@ import { isObject } from '../tool'; import { SyncHook } from './syncHook'; import { checkReturnData } from './syncWaterfallHook'; -type CallbackReturnType = T | Promise; +type CallbackReturnType = T | void | Promise; -export class AsyncWaterfallHook> extends SyncHook< +export class AsyncWaterfallHook extends SyncHook< [T], CallbackReturnType > { @@ -23,26 +23,27 @@ export class AsyncWaterfallHook> extends SyncHook< if (ls.length > 0) { let i = 0; - const processError = (e: any) => { + const processError = (e: unknown): T => { warn(e); this.onerror(e); return data; }; - const call = (prevData: T): any => { - if (checkReturnData(data, prevData)) { + const call = (prevData?: T | Awaited | void): T | Promise => { + if (prevData !== undefined && checkReturnData(data, prevData)) { data = prevData as T; - if (i < ls.length) { - try { - return Promise.resolve(ls[i++](data)).then(call, processError); - } catch (e) { - return processError(e); - } - } - } else { + } else if (prevData !== undefined) { this.onerror( `A plugin returned an incorrect value for the "${this.type}" type.`, ); + return data; + } + if (i < ls.length) { + try { + return Promise.resolve(ls[i++](data)).then(call, processError); + } catch (e) { + return processError(e); + } } return data; }; diff --git a/packages/runtime-core/src/utils/hooks/syncHook.ts b/packages/runtime-core/src/utils/hooks/syncHook.ts index 17f6dadb8c9..dda3ab8f512 100644 --- a/packages/runtime-core/src/utils/hooks/syncHook.ts +++ b/packages/runtime-core/src/utils/hooks/syncHook.ts @@ -32,7 +32,10 @@ export class SyncHook { if (this.listeners.size > 0) { // eslint-disable-next-line prefer-spread this.listeners.forEach((fn) => { - result = fn(...data); + const nextResult = fn(...data); + if (nextResult !== undefined) { + result = nextResult; + } }); } return result; diff --git a/packages/runtime-core/src/utils/hooks/syncWaterfallHook.ts b/packages/runtime-core/src/utils/hooks/syncWaterfallHook.ts index f37be00302b..d5daa8a1105 100644 --- a/packages/runtime-core/src/utils/hooks/syncWaterfallHook.ts +++ b/packages/runtime-core/src/utils/hooks/syncWaterfallHook.ts @@ -20,7 +20,7 @@ export function checkReturnData(originalData: any, returnedData: any): boolean { export class SyncWaterfallHook> extends SyncHook< [T], - T + T | void > { onerror: (errMsg: string | Error | unknown) => void = error; @@ -36,6 +36,9 @@ export class SyncWaterfallHook> extends SyncHook< for (const fn of this.listeners) { try { const tempData = fn(data); + if (tempData === undefined) { + continue; + } if (checkReturnData(data, tempData)) { data = tempData; } else { diff --git a/packages/runtime-core/src/utils/load.ts b/packages/runtime-core/src/utils/load.ts index 22be1e71bd3..9452f82736e 100644 --- a/packages/runtime-core/src/utils/load.ts +++ b/packages/runtime-core/src/utils/load.ts @@ -7,7 +7,12 @@ import { import { DEFAULT_REMOTE_TYPE, DEFAULT_SCOPE } from '../constant'; import { ModuleFederation } from '../core'; import { globalLoading, getRemoteEntryExports } from '../global'; -import { Remote, RemoteEntryExports, RemoteInfo } from '../type'; +import { + Remote, + RemoteEntryExports, + RemoteInfo, + ResourceLoadContext, +} from '../type'; import { assert, error } from './logger'; import { RUNTIME_001, @@ -107,6 +112,7 @@ async function loadEntryScript({ remoteInfo, loaderHook, getEntryUrl, + resourceContext, }: { name: string; globalName: string; @@ -114,6 +120,7 @@ async function loadEntryScript({ remoteInfo: RemoteInfo; loaderHook: ModuleFederation['loaderHook']; getEntryUrl?: (url: string) => string; + resourceContext?: ResourceLoadContext; }): Promise { const { entryExports: remoteEntryExports } = getRemoteEntryExports( name, @@ -133,6 +140,12 @@ async function loadEntryScript({ url, attrs, remoteInfo, + resourceContext: resourceContext + ? { + ...resourceContext, + url, + } + : undefined, }); if (!res) return; @@ -178,11 +191,13 @@ async function loadEntryDom({ remoteEntryExports, loaderHook, getEntryUrl, + resourceContext, }: { remoteInfo: RemoteInfo; remoteEntryExports?: RemoteEntryExports; loaderHook: ModuleFederation['loaderHook']; getEntryUrl?: (url: string) => string; + resourceContext?: ResourceLoadContext; }) { const { entry, entryGlobalName: globalName, name, type } = remoteInfo; switch (type) { @@ -199,6 +214,7 @@ async function loadEntryDom({ remoteInfo, loaderHook, getEntryUrl, + resourceContext, }); } } @@ -206,9 +222,11 @@ async function loadEntryDom({ async function loadEntryNode({ remoteInfo, loaderHook, + resourceContext, }: { remoteInfo: RemoteInfo; loaderHook: ModuleFederation['loaderHook']; + resourceContext?: ResourceLoadContext; }) { const { entry, entryGlobalName: globalName, name, type } = remoteInfo; const { entryExports: remoteEntryExports } = getRemoteEntryExports( @@ -228,6 +246,12 @@ async function loadEntryNode({ url, attrs, remoteInfo, + resourceContext: resourceContext + ? { + ...resourceContext, + url, + } + : undefined, }); if (!res) return; @@ -262,12 +286,14 @@ export async function getRemoteEntry(params: { remoteEntryExports?: RemoteEntryExports | undefined; getEntryUrl?: (url: string) => string; _inErrorHandling?: boolean; // Add flag to prevent recursion + resourceContext?: ResourceLoadContext; }): Promise { const { origin, remoteEntryExports, remoteInfo, getEntryUrl, + resourceContext, _inErrorHandling = false, } = params; const uniqueKey = getRemoteEntryUniqueKey(remoteInfo); @@ -281,6 +307,7 @@ export async function getRemoteEntry(params: { globalLoading[uniqueKey] = loadEntryHook .emit({ + origin, loaderHook, remoteInfo, remoteEntryExports, @@ -301,8 +328,17 @@ export async function getRemoteEntry(params: { remoteEntryExports, loaderHook, getEntryUrl, + resourceContext, }) - : loadEntryNode({ remoteInfo, loaderHook }); + : loadEntryNode({ remoteInfo, loaderHook, resourceContext }); + }) + .then(async (res) => { + await origin.loaderHook.lifecycle.afterLoadEntry.emit({ + origin, + remoteInfo, + remoteEntryExports: res, + }); + return res; }) .catch(async (err) => { const uniqueKey = getRemoteEntryUniqueKey(remoteInfo); @@ -333,9 +369,20 @@ export async function getRemoteEntry(params: { }); if (RemoteEntryExports) { + await origin.loaderHook.lifecycle.afterLoadEntry.emit({ + origin, + remoteInfo, + remoteEntryExports: RemoteEntryExports, + recovered: true, + }); return RemoteEntryExports; } } + await origin.loaderHook.lifecycle.afterLoadEntry.emit({ + origin, + remoteInfo, + error: err, + }); throw err; }); } diff --git a/packages/runtime-core/src/utils/manifest.ts b/packages/runtime-core/src/utils/manifest.ts index 0f150aa645b..3d6fb0d9382 100644 --- a/packages/runtime-core/src/utils/manifest.ts +++ b/packages/runtime-core/src/utils/manifest.ts @@ -1,5 +1,16 @@ import { Remote } from '../type'; +export function composeRemoteRequestId( + remoteName: string, + expose?: string, +): string { + if (!expose || expose === '.') { + return remoteName; + } + + return `${remoteName}/${expose.replace(/^\.\//, '')}`; +} + // Function to match a remote with its name and expose // id: pkgName(@federation/app1) + expose(button) = @federation/app1/button // id: alias(app1) + expose(button) = app1/button diff --git a/packages/runtime-core/src/utils/preload.ts b/packages/runtime-core/src/utils/preload.ts index 0a9fddd6165..9deb9810339 100644 --- a/packages/runtime-core/src/utils/preload.ts +++ b/packages/runtime-core/src/utils/preload.ts @@ -1,11 +1,14 @@ import { createLink, createScript, safeToString } from '@module-federation/sdk'; import { PreloadAssets, + PreloadAssetResult, PreloadConfig, PreloadOptions, PreloadRemoteArgs, Remote, RemoteInfo, + ResourceLoadContext, + ResourceLoadType, depsPreloadArg, } from '../type'; import { matchRemote } from './manifest'; @@ -63,32 +66,210 @@ export function normalizePreloadExposes(exposes?: string[]): string[] { }); } +function isTimeoutError(error: unknown): boolean { + if (!(error instanceof Error)) { + return false; + } + return error.message.includes('timed out') || error.name.includes('Timeout'); +} + +function createAssetResult( + context: ResourceLoadContext, + url: string, + status: PreloadAssetResult['status'], + error?: unknown, +): PreloadAssetResult { + return { + url, + status, + resourceType: context.resourceType, + initiator: context.initiator, + id: context.id, + error, + }; +} + +async function waitForRemoteEntryPreload( + host: ModuleFederation, + remoteInfo: RemoteInfo, + entryRemoteInfo: RemoteInfo, + context: ResourceLoadContext, +): Promise { + const cachedRemote = host.moduleCache.get(entryRemoteInfo.name); + const url = entryRemoteInfo.entry; + if (cachedRemote?.remoteEntryExports) { + return createAssetResult(context, url, 'cached'); + } + + try { + const remoteEntryExports = await getRemoteEntry({ + origin: host, + remoteInfo: entryRemoteInfo, + remoteEntryExports: cachedRemote?.remoteEntryExports, + resourceContext: { + ...context, + url, + }, + }); + if (!remoteEntryExports) { + throw new Error(`Failed to load remoteEntry "${url}".`); + } + return createAssetResult(context, url, 'success'); + } catch (error) { + return createAssetResult( + context, + url, + isTimeoutError(error) ? 'timeout' : 'error', + error, + ); + } +} + +function waitForLinkPreload({ + host, + remoteInfo, + url, + attrs, + context, + needDeleteLink, +}: { + host: ModuleFederation; + remoteInfo: RemoteInfo; + url: string; + attrs: Record; + context: ResourceLoadContext; + needDeleteLink?: boolean; +}): Promise { + return new Promise((resolve) => { + const { link, needAttach } = createLink({ + url, + cb: () => { + resolve( + createAssetResult(context, url, needAttach ? 'success' : 'cached'), + ); + }, + onErrorCallback: (error) => { + resolve( + createAssetResult( + context, + url, + isTimeoutError(error) ? 'timeout' : 'error', + error, + ), + ); + }, + attrs, + createLinkHook: (hookUrl, hookAttrs) => { + const res = host.loaderHook.lifecycle.createLink.emit({ + url: hookUrl, + attrs: hookAttrs, + remoteInfo, + resourceContext: { + ...context, + url: hookUrl, + }, + }); + if (res instanceof HTMLLinkElement) { + return res; + } + return res; + }, + needDeleteLink, + }); + + needAttach && document.head.appendChild(link); + }); +} + +function waitForScriptPreload({ + host, + remoteInfo, + url, + attrs, + context, +}: { + host: ModuleFederation; + remoteInfo: RemoteInfo; + url: string; + attrs: Record; + context: ResourceLoadContext; +}): Promise { + return new Promise((resolve) => { + const { script, needAttach } = createScript({ + url, + cb: () => { + resolve( + createAssetResult(context, url, needAttach ? 'success' : 'cached'), + ); + }, + onErrorCallback: (error) => { + resolve( + createAssetResult( + context, + url, + isTimeoutError(error) ? 'timeout' : 'error', + error, + ), + ); + }, + attrs, + createScriptHook: (hookUrl: string, hookAttrs: any) => { + const res = host.loaderHook.lifecycle.createScript.emit({ + url: hookUrl, + attrs: hookAttrs, + remoteInfo, + resourceContext: { + ...context, + url: hookUrl, + }, + }); + if (res instanceof HTMLScriptElement) { + return res; + } + return res; + }, + needDeleteScript: true, + }); + + needAttach && document.head.appendChild(script); + }); +} + +function createResourceContext( + baseContext: Omit, + resourceType: ResourceLoadType, +): ResourceLoadContext { + return { + ...baseContext, + resourceType, + }; +} + export function preloadAssets( remoteInfo: RemoteInfo, host: ModuleFederation, assets: PreloadAssets, // It is used to distinguish preload from load remote parallel loading useLinkPreload = true, -): void { + baseContext: Omit = { + initiator: 'preloadRemote', + id: remoteInfo.name, + }, +): Promise { const { cssAssets, jsAssetsWithoutEntry, entryAssets } = assets; + const results: Array> = []; if (host.options.inBrowser) { entryAssets.forEach((asset) => { - const { moduleInfo } = asset; - const module = host.moduleCache.get(remoteInfo.name); - if (module) { - getRemoteEntry({ - origin: host, - remoteInfo: moduleInfo, - remoteEntryExports: module.remoteEntryExports, - }); - } else { - getRemoteEntry({ - origin: host, - remoteInfo: moduleInfo, - remoteEntryExports: undefined, - }); - } + const { moduleInfo: entryRemoteInfo } = asset; + results.push( + waitForRemoteEntryPreload( + host, + remoteInfo, + entryRemoteInfo, + createResourceContext(baseContext, 'remoteEntry'), + ), + ); }); if (useLinkPreload) { @@ -97,26 +278,15 @@ export function preloadAssets( as: 'style', }; cssAssets.forEach((cssUrl) => { - const { link: cssEl, needAttach } = createLink({ - url: cssUrl, - cb: () => { - // noop - }, - attrs: defaultAttrs, - createLinkHook: (url, attrs) => { - const res = host.loaderHook.lifecycle.createLink.emit({ - url, - attrs, - remoteInfo, - }); - if (res instanceof HTMLLinkElement) { - return res; - } - return; - }, - }); - - needAttach && document.head.appendChild(cssEl); + results.push( + waitForLinkPreload({ + host, + remoteInfo, + url: cssUrl, + attrs: defaultAttrs, + context: createResourceContext(baseContext, 'css'), + }), + ); }); } else { const defaultAttrs = { @@ -124,27 +294,16 @@ export function preloadAssets( type: 'text/css', }; cssAssets.forEach((cssUrl) => { - const { link: cssEl, needAttach } = createLink({ - url: cssUrl, - cb: () => { - // noop - }, - attrs: defaultAttrs, - createLinkHook: (url, attrs) => { - const res = host.loaderHook.lifecycle.createLink.emit({ - url, - attrs, - remoteInfo, - }); - if (res instanceof HTMLLinkElement) { - return res; - } - return; - }, - needDeleteLink: false, - }); - - needAttach && document.head.appendChild(cssEl); + results.push( + waitForLinkPreload({ + host, + remoteInfo, + url: cssUrl, + attrs: defaultAttrs, + needDeleteLink: false, + context: createResourceContext(baseContext, 'css'), + }), + ); }); } @@ -154,25 +313,15 @@ export function preloadAssets( as: 'script', }; jsAssetsWithoutEntry.forEach((jsUrl) => { - const { link: linkEl, needAttach } = createLink({ - url: jsUrl, - cb: () => { - // noop - }, - attrs: defaultAttrs, - createLinkHook: (url: string, attrs) => { - const res = host.loaderHook.lifecycle.createLink.emit({ - url, - attrs, - remoteInfo, - }); - if (res instanceof HTMLLinkElement) { - return res; - } - return; - }, - }); - needAttach && document.head.appendChild(linkEl); + results.push( + waitForLinkPreload({ + host, + remoteInfo, + url: jsUrl, + attrs: defaultAttrs, + context: createResourceContext(baseContext, 'js'), + }), + ); }); } else { const defaultAttrs = { @@ -180,27 +329,18 @@ export function preloadAssets( type: remoteInfo?.type === 'module' ? 'module' : 'text/javascript', }; jsAssetsWithoutEntry.forEach((jsUrl) => { - const { script: scriptEl, needAttach } = createScript({ - url: jsUrl, - cb: () => { - // noop - }, - attrs: defaultAttrs, - createScriptHook: (url: string, attrs: any) => { - const res = host.loaderHook.lifecycle.createScript.emit({ - url, - attrs, - remoteInfo, - }); - if (res instanceof HTMLScriptElement) { - return res; - } - return; - }, - needDeleteScript: true, - }); - needAttach && document.head.appendChild(scriptEl); + results.push( + waitForScriptPreload({ + host, + remoteInfo, + url: jsUrl, + attrs: defaultAttrs, + context: createResourceContext(baseContext, 'js'), + }), + ); }); } } + + return Promise.all(results); } diff --git a/packages/runtime-plugins/inject-external-runtime-core-plugin/CHANGELOG.md b/packages/runtime-plugins/inject-external-runtime-core-plugin/CHANGELOG.md index c80c661ad27..887106b3c67 100644 --- a/packages/runtime-plugins/inject-external-runtime-core-plugin/CHANGELOG.md +++ b/packages/runtime-plugins/inject-external-runtime-core-plugin/CHANGELOG.md @@ -1,5 +1,11 @@ # @module-federation/inject-external-runtime-core-plugin +## 2.5.0 + +### Patch Changes + +- @module-federation/runtime-tools@2.5.0 + ## 2.4.0 ### Patch Changes diff --git a/packages/runtime-plugins/inject-external-runtime-core-plugin/package.json b/packages/runtime-plugins/inject-external-runtime-core-plugin/package.json index a34117dfd66..c6e1982dbe0 100644 --- a/packages/runtime-plugins/inject-external-runtime-core-plugin/package.json +++ b/packages/runtime-plugins/inject-external-runtime-core-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/inject-external-runtime-core-plugin", - "version": "2.4.0", + "version": "2.5.0", "type": "module", "license": "MIT", "description": "A sdk for support module federation", diff --git a/packages/runtime-tools/CHANGELOG.md b/packages/runtime-tools/CHANGELOG.md index 5c9d847ce12..eb2f9eaee1a 100644 --- a/packages/runtime-tools/CHANGELOG.md +++ b/packages/runtime-tools/CHANGELOG.md @@ -1,5 +1,14 @@ # @module-federation/runtime-tools +## 2.5.0 + +### Patch Changes + +- Updated dependencies [d433ec9] +- Updated dependencies [41281f4] + - @module-federation/runtime@2.5.0 + - @module-federation/webpack-bundler-runtime@2.5.0 + ## 2.4.0 ### Patch Changes diff --git a/packages/runtime-tools/package.json b/packages/runtime-tools/package.json index 1106bd3daaf..f3ad8de54d9 100644 --- a/packages/runtime-tools/package.json +++ b/packages/runtime-tools/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/runtime-tools", - "version": "2.4.0", + "version": "2.5.0", "type": "module", "author": "zhanghang ", "main": "./dist/index.cjs", diff --git a/packages/runtime/CHANGELOG.md b/packages/runtime/CHANGELOG.md index 5d1111b3953..312e09822c0 100644 --- a/packages/runtime/CHANGELOG.md +++ b/packages/runtime/CHANGELOG.md @@ -1,5 +1,21 @@ # @module-federation/runtime +## 2.5.0 + +### Minor Changes + +- d433ec9: feat(runtime): support finder callbacks in `getInstance` and clarify runtime instance API usage. +- 41281f4: Add an opt-in observability plugin, a Chrome-extension-safe observability plugin entry with an independent name and fixed browser scope, a direct runtime plugin API with instance-bound component loaded marks, explicit temporary React `onMFRemoteLoaded` callback injection for matched remotes, opt-in start console traces for `loadRemote` and `loadShare`, a local collector mode for AI-assisted browser debugging, a Node-specific export for file reports, a build-specific export for build summaries and build error reports, remote and shared lifecycle hooks, console trace hints, safe browser/Node report outputs, configurable error stack capture with explicit console raw-stack opt-ins, shared/eager loading evidence gated to stable runtime `2.5.0+` for Chrome-extension compatibility, final loading outcome summaries for Module Federation loading reports including resolved shared dependencies, deterministic fact reports for runtime and build failures, no-op return handling for observer hooks, detailed remote match/init/expose/factory phase events with phase durations, compact phase summaries, cache/fallback markers, loaded-before evidence from existing federation instances when a remote load fails, length-limited business component metadata, clipped moduleInfo evidence with preserved deployment locator fields for snapshot-dependent failures, normalized runtime error summaries with error codes, owner hints, retryability, and safe context, dedicated runtime error codes for invalid manifests, missing exposes, and remote container init failures, plus MF skill guidance for reading and fixing observability reports. + +### Patch Changes + +- Updated dependencies [5d4095d] +- Updated dependencies [0716c11] +- Updated dependencies [41281f4] + - @module-federation/sdk@2.5.0 + - @module-federation/runtime-core@2.5.0 + - @module-federation/error-codes@2.5.0 + ## 2.4.0 ### Patch Changes diff --git a/packages/runtime/__tests__/preload-remote.spec.ts b/packages/runtime/__tests__/preload-remote.spec.ts index 32e570f092d..d01c1e44fc6 100644 --- a/packages/runtime/__tests__/preload-remote.spec.ts +++ b/packages/runtime/__tests__/preload-remote.spec.ts @@ -13,20 +13,29 @@ interface ScriptInfo { crossorigin: string; } +const createdLinkInfos: LinkInfo[] = []; +const createdScriptInfos: ScriptInfo[] = []; + function getLinkInfos(): Array { const links = document.querySelectorAll('link'); - return Array.from(links).map((link) => ({ - type: link.getAttribute('as') || '', - href: link.getAttribute('href') || '', - rel: link.getAttribute('rel') || '', - })); + return [ + ...Array.from(links).map((link) => ({ + type: link.getAttribute('as') || '', + href: link.getAttribute('href') || '', + rel: link.getAttribute('rel') || '', + })), + ...createdLinkInfos, + ]; } function getScriptInfos(): Array { const scripts = document.querySelectorAll('script'); - return Array.from(scripts).map((script) => ({ - src: script.getAttribute('src') || '', - crossorigin: script.getAttribute('crossorigin') || '', - })); + return [ + ...Array.from(scripts).map((script) => ({ + src: script.getAttribute('src') || '', + crossorigin: script.getAttribute('crossorigin') || '', + })), + ...createdScriptInfos, + ]; } function getPreloadElInfos() { return { @@ -34,6 +43,37 @@ function getPreloadElInfos() { scripts: getScriptInfos(), }; } + +function applyAttrs( + element: HTMLElement, + attrs?: Record, +): void { + Object.entries(attrs || {}).forEach(([name, value]) => { + element.setAttribute(name, String(value)); + }); +} + +function completeLoadWhenHandlerIsAttached( + element: HTMLElement, + event: Event, +): void { + let onload: GlobalEventHandlers['onload'] | null = null; + Object.defineProperty(element, 'onload', { + configurable: true, + get() { + return onload; + }, + set(handler: GlobalEventHandlers['onload'] | null) { + onload = handler; + if (handler) { + setTimeout(() => { + handler.call(element, event); + }); + } + }, + }); +} + describe('preload-remote inBrowser', () => { mockStaticServer({ baseDir: __dirname, @@ -192,6 +232,8 @@ describe('preload-remote inBrowser', () => { beforeEach(() => { document.head.innerHTML = ''; document.body.innerHTML = ''; + createdLinkInfos.length = 0; + createdScriptInfos.length = 0; Global.__FEDERATION__.__PRELOADED_MAP__.clear(); }); const FMInstance = init({ @@ -208,6 +250,39 @@ describe('preload-remote inBrowser', () => { args.options.inBrowser = true; return args; }, + loadEntry({ remoteInfo }) { + if ( + remoteInfo?.entry && + !createdScriptInfos.some((info) => info.src === remoteInfo.entry) + ) { + createdScriptInfos.push({ + src: remoteInfo.entry, + crossorigin: '', + }); + } + return { + get() { + return () => Promise.resolve({}); + }, + init() { + // noop + }, + }; + }, + createLink({ url, attrs }) { + const link = document.createElement('link'); + link.setAttribute('href', url); + applyAttrs(link, attrs); + if (!createdLinkInfos.some((info) => info.href === url)) { + createdLinkInfos.push({ + type: attrs?.as || '', + href: url, + rel: attrs?.rel || '', + }); + } + completeLoadWhenHandlerIsAttached(link, new Event('load')); + return link; + }, }, ], }); diff --git a/packages/runtime/package.json b/packages/runtime/package.json index e48e32aaffc..3c331e32446 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/runtime", - "version": "2.4.0", + "version": "2.5.0", "type": "module", "author": "zhouxiao ", "main": "./dist/index.cjs", diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index 8f564be3831..f0f7dff1f64 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -1,5 +1,15 @@ # @module-federation/sdk +## 2.5.0 + +### Patch Changes + +- 5d4095d: feat(metro): add manifest SHA-256 bundle hashes and optional cache layer integration for bundle loading. + + Credit: originally contributed by @zhongwuzw in #4576. + +- 0716c11: Track preload resource results and expose resource context to loader hooks. + ## 2.4.0 ### Minor Changes diff --git a/packages/sdk/__tests__/dom.spec.ts b/packages/sdk/__tests__/dom.spec.ts index 970434c6db0..e33ca6c0386 100644 --- a/packages/sdk/__tests__/dom.spec.ts +++ b/packages/sdk/__tests__/dom.spec.ts @@ -348,6 +348,8 @@ describe('createScript - error handling', () => { describe('createLink', () => { afterEach(() => { document.getElementsByTagName('html')[0].innerHTML = ''; + jest.useRealTimers(); + jest.restoreAllMocks(); }); it('should create a new link element if one does not exist', () => { @@ -457,6 +459,50 @@ describe('createLink', () => { expect(onErrorCallback).toHaveBeenCalled(); }); + it('should use the timeout specified in the createLinkHook', () => { + jest.spyOn(global, 'setTimeout'); + const url = 'https://example.com/script.js'; + const cb = jest.fn(); + const customTimeout = 5000; + const { link } = createLink({ + url, + cb, + attrs: { rel: 'preload', as: 'script' }, + createLinkHook: () => ({ timeout: customTimeout }), + }); + + expect(setTimeout).toHaveBeenCalledWith( + expect.any(Function), + customTimeout, + ); + + link?.onload?.(new Event('load')); + }); + + it('timeout calls onErrorCallback with LinkNetworkError', () => { + jest.useFakeTimers(); + const url = 'https://example.com/timeout.css'; + const cb = jest.fn(); + const onErrorCallback = jest.fn(); + + createLink({ + url, + cb, + onErrorCallback, + attrs: { rel: 'preload', as: 'style' }, + createLinkHook: () => ({ timeout: 100 }), + }); + + jest.advanceTimersByTime(100); + + expect(onErrorCallback).toHaveBeenCalledTimes(1); + expect(onErrorCallback.mock.calls[0][0]).toMatchObject({ + name: 'LinkNetworkError', + message: expect.stringContaining('timed out'), + }); + expect(cb).not.toHaveBeenCalled(); + }); + it('should use the link element returned by createLinkHook', () => { const url = 'https://example.com/script.js'; const cb = jest.fn(); diff --git a/packages/sdk/package.json b/packages/sdk/package.json index aff45c3959a..7e3606ebe3e 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/sdk", - "version": "2.4.0", + "version": "2.5.0", "type": "module", "license": "MIT", "description": "A sdk for support module federation", diff --git a/packages/sdk/src/dom.ts b/packages/sdk/src/dom.ts index a88a939fc42..118d9642f2f 100644 --- a/packages/sdk/src/dom.ts +++ b/packages/sdk/src/dom.ts @@ -1,4 +1,9 @@ -import type { CreateScriptHookDom, CreateScriptHookReturnDom } from './types'; +import type { + CreateLinkHookDom, + CreateLinkHookReturnDom, + CreateScriptHookDom, + CreateScriptHookReturnDom, +} from './types'; import { warn } from './utils'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export async function safeWrapper) => any>( @@ -170,16 +175,15 @@ export function createLink(info: { onErrorCallback?: (error: Error) => void; attrs: Record; needDeleteLink?: boolean; - createLinkHook?: ( - url: string, - attrs?: Record, - ) => HTMLLinkElement | void; + createLinkHook?: CreateLinkHookDom; }) { // // Retrieve the existing script element by its src attribute let link: HTMLLinkElement | null = null; let needAttach = true; + let timeout = 20000; + let timeoutId: NodeJS.Timeout | undefined; const links = document.getElementsByTagName('link'); for (let i = 0; i < links.length; i++) { const l = links[i]; @@ -200,17 +204,27 @@ export function createLink(info: { link = document.createElement('link'); link.setAttribute('href', info.url); - let createLinkRes: void | HTMLLinkElement = undefined; + let createLinkRes: CreateLinkHookReturnDom = undefined; + let shouldApplyAttrs = true; const attrs = info.attrs; if (info.createLinkHook) { createLinkRes = info.createLinkHook(info.url, attrs); if (createLinkRes instanceof HTMLLinkElement) { link = createLinkRes; + shouldApplyAttrs = false; + } else if (typeof createLinkRes === 'object') { + if ('link' in createLinkRes && createLinkRes.link) { + link = createLinkRes.link; + shouldApplyAttrs = false; + } + if ('timeout' in createLinkRes && createLinkRes.timeout) { + timeout = createLinkRes.timeout; + } } } - if (attrs && !createLinkRes) { + if (attrs && shouldApplyAttrs) { Object.keys(attrs).forEach((name) => { if (link && !link.getAttribute(name)) { link.setAttribute(name, attrs[name]); @@ -219,14 +233,30 @@ export function createLink(info: { } } + if (!needAttach) { + Promise.resolve().then(() => { + info?.cb && info?.cb(); + }); + return { link, needAttach }; + } + const onLinkComplete = ( prev: OnErrorEventHandler | GlobalEventHandlers['onload'] | null, // eslint-disable-next-line @typescript-eslint/no-explicit-any event: any, ): void => { + if (timeoutId) { + clearTimeout(timeoutId); + } const onLinkCompleteCallback = () => { if (event?.type === 'error') { - info?.onErrorCallback && info?.onErrorCallback(event); + const linkError = new Error( + event?.isTimeout + ? `LinkNetworkError: Link "${info.url}" timed out.` + : `LinkNetworkError: Failed to load link "${info.url}" - the URL is unreachable or the server returned an error.`, + ); + linkError.name = 'LinkNetworkError'; + info?.onErrorCallback && info?.onErrorCallback(linkError); } else { info?.cb && info?.cb(); } @@ -253,6 +283,9 @@ export function createLink(info: { link.onerror = onLinkComplete.bind(null, link.onerror); link.onload = onLinkComplete.bind(null, link.onload); + timeoutId = setTimeout(() => { + onLinkComplete(null, { type: 'error', isTimeout: true }); + }, timeout); return { link, needAttach }; } diff --git a/packages/sdk/src/types/hooks.ts b/packages/sdk/src/types/hooks.ts index 75d4326b820..8362376f3b8 100644 --- a/packages/sdk/src/types/hooks.ts +++ b/packages/sdk/src/types/hooks.ts @@ -5,6 +5,11 @@ export type CreateScriptHookReturnDom = | { script?: HTMLScriptElement; timeout?: number } | void; +export type CreateLinkHookReturnDom = + | HTMLLinkElement + | { link?: HTMLLinkElement; timeout?: number } + | void; + export type CreateScriptHookReturn = | CreateScriptHookReturnNode | CreateScriptHookReturnDom; @@ -19,6 +24,11 @@ export type CreateScriptHookDom = ( attrs?: Record | undefined, ) => CreateScriptHookReturnDom; +export type CreateLinkHookDom = ( + url: string, + attrs?: Record | undefined, +) => CreateLinkHookReturnDom; + export type CreateScriptHook = ( url: string, attrs?: Record | undefined, diff --git a/packages/storybook-addon/CHANGELOG.md b/packages/storybook-addon/CHANGELOG.md index 6226b1be272..58c0cb18bf7 100644 --- a/packages/storybook-addon/CHANGELOG.md +++ b/packages/storybook-addon/CHANGELOG.md @@ -1,5 +1,14 @@ # @module-federation/storybook-addon +## 6.0.12 + +### Patch Changes + +- Updated dependencies [5d4095d] +- Updated dependencies [0716c11] + - @module-federation/sdk@2.5.0 + - @module-federation/enhanced@2.5.0 + ## 6.0.11 ### Patch Changes diff --git a/packages/storybook-addon/package.json b/packages/storybook-addon/package.json index 500e5d83ef2..651f01fabf5 100644 --- a/packages/storybook-addon/package.json +++ b/packages/storybook-addon/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/storybook-addon", - "version": "6.0.11", + "version": "6.0.12", "description": "Storybook addon to consume remote module federated apps/components", "type": "module", "license": "MIT", @@ -64,7 +64,7 @@ }, "peerDependencies": { "@rsbuild/core": "^1.0.1 || ^2.0.0-0", - "@module-federation/sdk": "^2.4.0", + "@module-federation/sdk": "^2.5.0", "@nx/react": ">= 16.0.0", "@nx/webpack": ">= 16.0.0", "@nx/module-federation": ">= 16.0.0", diff --git a/packages/third-party-dts-extractor/CHANGELOG.md b/packages/third-party-dts-extractor/CHANGELOG.md index d244ae563c9..2c281a11155 100644 --- a/packages/third-party-dts-extractor/CHANGELOG.md +++ b/packages/third-party-dts-extractor/CHANGELOG.md @@ -1,5 +1,7 @@ # @module-federation/third-party-dts-extractor +## 2.5.0 + ## 2.4.0 ## 2.3.3 diff --git a/packages/third-party-dts-extractor/package.json b/packages/third-party-dts-extractor/package.json index a2f1c0dfe02..15c6b5e2b6f 100644 --- a/packages/third-party-dts-extractor/package.json +++ b/packages/third-party-dts-extractor/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/third-party-dts-extractor", - "version": "2.4.0", + "version": "2.5.0", "files": [ "dist/", "README.md" diff --git a/packages/treeshake-frontend/CHANGELOG.md b/packages/treeshake-frontend/CHANGELOG.md index 6252222399d..43ef6b8f013 100644 --- a/packages/treeshake-frontend/CHANGELOG.md +++ b/packages/treeshake-frontend/CHANGELOG.md @@ -1,5 +1,7 @@ # @module-federation/treeshake-frontend +## 2.5.0 + ## 2.4.0 ## 2.3.3 diff --git a/packages/treeshake-frontend/package.json b/packages/treeshake-frontend/package.json index 2a636f81c93..87172e84458 100644 --- a/packages/treeshake-frontend/package.json +++ b/packages/treeshake-frontend/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/treeshake-frontend", - "version": "2.4.0", + "version": "2.5.0", "scripts": { "dev": "rsbuild dev", "build": "rsbuild build && rslib build", diff --git a/packages/treeshake-server/CHANGELOG.md b/packages/treeshake-server/CHANGELOG.md index e4e1acb5788..048334cb8b0 100644 --- a/packages/treeshake-server/CHANGELOG.md +++ b/packages/treeshake-server/CHANGELOG.md @@ -1,5 +1,7 @@ # @module-federation/treeshake-server +## 2.5.0 + ## 2.4.0 ## 2.3.3 diff --git a/packages/treeshake-server/package.json b/packages/treeshake-server/package.json index f8cd4ecba2b..1b04f074e81 100644 --- a/packages/treeshake-server/package.json +++ b/packages/treeshake-server/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/treeshake-server", - "version": "2.4.0", + "version": "2.5.0", "description": "Build service powered by Hono that installs dependencies, builds with Rspack, and uploads artifacts.", "main": "dist/index.js", "module": "dist/index.mjs", diff --git a/packages/utilities/CHANGELOG.md b/packages/utilities/CHANGELOG.md index 4d473078778..d29bdbfa9f8 100644 --- a/packages/utilities/CHANGELOG.md +++ b/packages/utilities/CHANGELOG.md @@ -1,5 +1,13 @@ # @module-federation/utilities +## 3.1.95 + +### Patch Changes + +- Updated dependencies [5d4095d] +- Updated dependencies [0716c11] + - @module-federation/sdk@2.5.0 + ## 3.1.94 ### Patch Changes diff --git a/packages/utilities/package.json b/packages/utilities/package.json index 8c65acf8168..9124a7cd8ea 100644 --- a/packages/utilities/package.json +++ b/packages/utilities/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/utilities", - "version": "3.1.94", + "version": "3.1.95", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.mjs", "types": "./dist/types/index.d.ts", diff --git a/packages/webpack-bundler-runtime/CHANGELOG.md b/packages/webpack-bundler-runtime/CHANGELOG.md index 67ff712ee39..d7e829b1173 100644 --- a/packages/webpack-bundler-runtime/CHANGELOG.md +++ b/packages/webpack-bundler-runtime/CHANGELOG.md @@ -1,5 +1,17 @@ # @module-federation/webpack-bundler-runtime +## 2.5.0 + +### Patch Changes + +- Updated dependencies [5d4095d] +- Updated dependencies [d433ec9] +- Updated dependencies [0716c11] +- Updated dependencies [41281f4] + - @module-federation/sdk@2.5.0 + - @module-federation/runtime@2.5.0 + - @module-federation/error-codes@2.5.0 + ## 2.4.0 ### Minor Changes diff --git a/packages/webpack-bundler-runtime/package.json b/packages/webpack-bundler-runtime/package.json index 6849b243def..4ed683c06e3 100644 --- a/packages/webpack-bundler-runtime/package.json +++ b/packages/webpack-bundler-runtime/package.json @@ -1,7 +1,7 @@ { "public": true, "name": "@module-federation/webpack-bundler-runtime", - "version": "2.4.0", + "version": "2.5.0", "type": "module", "license": "MIT", "description": "Module Federation Runtime for webpack", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e13c22a0d9..a1a0bea2ae6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1107,7 +1107,7 @@ importers: version: 7.28.2 '@modern-js/runtime': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(react@18.3.1) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(react@18.3.1) '@module-federation/modern-js-v3': specifier: workspace:* version: link:../../../packages/modernjs-v3 @@ -1126,7 +1126,7 @@ importers: version: 2.59.0(typescript@5.0.4) '@modern-js/app-tools': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1))) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))) '@modern-js/eslint-config': specifier: 2.59.0 version: 2.59.0(typescript@5.0.4) @@ -1165,7 +1165,7 @@ importers: version: 7.28.2 '@modern-js/runtime': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(react@18.3.1) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(react@18.3.1) '@module-federation/modern-js-v3': specifier: workspace:* version: link:../../../packages/modernjs-v3 @@ -1184,7 +1184,7 @@ importers: version: 2.59.0(typescript@5.0.4) '@modern-js/app-tools': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1))) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))) '@modern-js/eslint-config': specifier: 2.59.0 version: 2.59.0(typescript@5.0.4) @@ -1232,7 +1232,7 @@ importers: version: link:../../../packages/storybook-addon '@rsbuild/plugin-react': specifier: ^1.4.5 - version: 1.4.5(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))(webpack-hot-middleware@2.26.1) + version: 1.4.5(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))(webpack-hot-middleware@2.26.1) '@rslib/core': specifier: ^0.9.0 version: 0.9.2(@microsoft/api-extractor@7.57.7(@types/node@25.6.0))(typescript@5.9.3) @@ -1259,7 +1259,7 @@ importers: version: 7.28.2 '@modern-js/runtime': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(react@18.3.1) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(react@18.3.1) '@module-federation/modern-js-v3': specifier: workspace:* version: link:../../../packages/modernjs-v3 @@ -1278,7 +1278,7 @@ importers: version: 2.59.0(typescript@5.0.4) '@modern-js/app-tools': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1))) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))) '@modern-js/eslint-config': specifier: 2.59.0 version: 2.59.0(typescript@5.0.4) @@ -1330,7 +1330,7 @@ importers: version: link:../../../packages/rsbuild-plugin '@rsbuild/plugin-react': specifier: ^1.4.5 - version: 1.4.5(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))(webpack-hot-middleware@2.26.1) + version: 1.4.5(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))(webpack-hot-middleware@2.26.1) '@rslib/core': specifier: ^0.9.0 version: 0.9.2(@microsoft/api-extractor@7.57.7(@types/node@25.6.0))(typescript@5.9.3) @@ -1348,7 +1348,7 @@ importers: version: 7.28.2 '@modern-js/runtime': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(react@18.3.1) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(react@18.3.1) '@module-federation/modern-js-v3': specifier: workspace:* version: link:../../../packages/modernjs-v3 @@ -1367,7 +1367,7 @@ importers: version: 2.59.0(typescript@5.0.4) '@modern-js/app-tools': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1))) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))) '@modern-js/eslint-config': specifier: 2.59.0 version: 2.59.0(typescript@5.0.4) @@ -1406,7 +1406,7 @@ importers: version: 7.28.2 '@modern-js/runtime': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(react@18.3.1) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(react@18.3.1) '@module-federation/modern-js-v3': specifier: workspace:* version: link:../../../packages/modernjs-v3 @@ -1425,7 +1425,7 @@ importers: version: 2.59.0(typescript@5.0.4) '@modern-js/app-tools': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1))) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))) '@modern-js/eslint-config': specifier: 2.59.0 version: 2.59.0(typescript@5.0.4) @@ -1464,7 +1464,7 @@ importers: version: 7.28.2 '@modern-js/runtime': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(react@18.3.1) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(react@18.3.1) '@module-federation/modern-js-v3': specifier: workspace:* version: link:../../../packages/modernjs-v3 @@ -1483,7 +1483,7 @@ importers: version: 2.59.0(typescript@5.0.4) '@modern-js/app-tools': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1))) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))) '@modern-js/eslint-config': specifier: 2.59.0 version: 2.59.0(typescript@5.0.4) @@ -1522,7 +1522,7 @@ importers: version: 7.28.2 '@modern-js/runtime': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(react@18.3.1) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(react@18.3.1) '@module-federation/modern-js-v3': specifier: workspace:* version: link:../../../packages/modernjs-v3 @@ -1541,7 +1541,7 @@ importers: version: 2.59.0(typescript@5.0.4) '@modern-js/app-tools': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1))) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))) '@modern-js/eslint-config': specifier: 2.59.0 version: 2.59.0(typescript@5.0.4) @@ -1580,7 +1580,7 @@ importers: version: 7.28.2 '@modern-js/runtime': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(react@18.3.1) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(react@18.3.1) '@module-federation/modern-js-v3': specifier: workspace:* version: link:../../../packages/modernjs-v3 @@ -1599,7 +1599,7 @@ importers: version: 2.59.0(typescript@5.0.4) '@modern-js/app-tools': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1))) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))) '@modern-js/eslint-config': specifier: 2.59.0 version: 2.59.0(typescript@5.0.4) @@ -2126,7 +2126,7 @@ importers: version: 1.7.3 '@rsbuild/plugin-vue': specifier: ^1.2.6 - version: 1.2.7(@rsbuild/core@1.7.3)(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(vue@3.5.30(typescript@5.9.3)) + version: 1.2.7(@rsbuild/core@1.7.3)(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(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@25.6.0)(typescript@5.9.3)) @@ -2243,7 +2243,7 @@ importers: version: 1.7.3 '@rsbuild/plugin-vue': specifier: ^1.2.6 - version: 1.2.7(@rsbuild/core@1.7.3)(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(vue@3.5.30(typescript@5.9.3)) + version: 1.2.7(@rsbuild/core@1.7.3)(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19))(vue@3.5.30(typescript@5.9.3)) '@vue/tsconfig': specifier: ^0.5.1 version: 0.5.1 @@ -2393,7 +2393,7 @@ importers: version: link:../../packages/storybook-addon '@rsbuild/plugin-react': specifier: ^1.4.5 - version: 1.4.5(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))(webpack-hot-middleware@2.26.1) + version: 1.4.5(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))(webpack-hot-middleware@2.26.1) '@rslib/core': specifier: ^0.9.0 version: 0.9.2(@microsoft/api-extractor@7.57.7(@types/node@25.6.0))(typescript@5.9.3) @@ -2414,10 +2414,10 @@ importers: version: 8.6.17(prettier@3.8.1) storybook-addon-rslib: specifier: ^1.0.1 - version: 1.0.3(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))(@rslib/core@0.9.2(@microsoft/api-extractor@7.57.7(@types/node@25.6.0))(typescript@5.9.3))(storybook-builder-rsbuild@1.0.3(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(@types/react@18.3.28)(storybook@8.6.17(prettier@3.8.1))(tslib@2.8.1)(typescript@5.9.3))(typescript@5.9.3) + version: 1.0.3(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))(@rslib/core@0.9.2(@microsoft/api-extractor@7.57.7(@types/node@25.6.0))(typescript@5.9.3))(storybook-builder-rsbuild@1.0.3(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19))(@types/react@18.3.28)(storybook@8.6.17(prettier@3.8.1))(tslib@2.8.1)(typescript@5.9.3))(typescript@5.9.3) storybook-react-rsbuild: specifier: ^1.0.1 - version: 1.0.3(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.59.0)(storybook@8.6.17(prettier@3.8.1))(tslib@2.8.1)(typescript@5.9.3)(webpack@5.104.1(@swc/core@1.7.26(@swc/helpers@0.5.13))(esbuild@0.25.0)(webpack-cli@5.1.4)) + version: 1.0.3(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.59.0)(storybook@8.6.17(prettier@3.8.1))(tslib@2.8.1)(typescript@5.9.3)(webpack@5.104.1(@swc/core@1.7.26(@swc/helpers@0.5.13))(esbuild@0.25.0)(webpack-cli@5.1.4)) apps/runtime-demo/3005-runtime-host: dependencies: @@ -2443,6 +2443,9 @@ importers: '@module-federation/enhanced': specifier: workspace:* version: link:../../../packages/enhanced + '@module-federation/observability-plugin': + specifier: workspace:* + version: link:../../../packages/observability-plugin '@module-federation/runtime': specifier: workspace:* version: link:../../../packages/runtime @@ -2556,7 +2559,7 @@ importers: version: 7.28.2 '@modern-js/runtime': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(react@18.3.1) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(react@18.3.1) '@module-federation/enhanced': specifier: workspace:* version: link:../../../../packages/enhanced @@ -2575,13 +2578,13 @@ importers: version: 2.59.0(typescript@5.0.4) '@modern-js/app-tools': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.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.8.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@20.19.5)(typescript@5.0.4))(tsconfig-paths@3.14.2)(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(webpack-dev-server@5.2.3)(webpack@5.104.1))) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(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.8.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@20.19.5)(typescript@5.0.4))(tsconfig-paths@3.14.2)(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(webpack-dev-server@5.2.3)(webpack@5.104.1))) '@modern-js/plugin-server': specifier: 2.68.0 - version: 2.68.0(@babel/traverse@7.29.0)(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.68.0(@babel/traverse@7.29.0)(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(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.3.2(node-fetch@3.3.2))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(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/tsconfig': specifier: 3.0.1 version: 3.0.1 @@ -2626,7 +2629,7 @@ importers: version: 7.28.2 '@modern-js/runtime': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(react@18.3.1) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(react@18.3.1) '@module-federation/enhanced': specifier: workspace:* version: link:../../../../packages/enhanced @@ -2645,13 +2648,13 @@ importers: version: 2.59.0(typescript@5.0.4) '@modern-js/app-tools': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1))) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))) '@modern-js/eslint-config': specifier: 2.59.0 version: 2.59.0(typescript@5.0.4) '@modern-js/plugin-server': specifier: 2.68.0 - version: 2.68.0(@babel/traverse@7.29.0)(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.68.0(@babel/traverse@7.29.0)(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(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/tsconfig': specifier: 3.0.1 version: 3.0.1 @@ -2690,7 +2693,7 @@ importers: version: 7.28.2 '@modern-js/runtime': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(react@18.3.1) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(react@18.3.1) '@module-federation/modern-js-v3': specifier: workspace:* version: link:../../../../packages/modernjs-v3 @@ -2709,16 +2712,16 @@ importers: version: 2.59.0(typescript@5.0.4) '@modern-js/app-tools': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.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.8.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@20.19.5)(typescript@5.0.4))(tsconfig-paths@3.14.2)(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(webpack-dev-server@5.2.3)(webpack@5.104.1))) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(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.8.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@20.19.5)(typescript@5.0.4))(tsconfig-paths@3.14.2)(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(webpack-dev-server@5.2.3)(webpack@5.104.1))) '@modern-js/eslint-config': specifier: 2.59.0 version: 2.59.0(typescript@5.0.4) '@modern-js/plugin-server': specifier: 2.68.0 - version: 2.68.0(@babel/traverse@7.29.0)(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.68.0(@babel/traverse@7.29.0)(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(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.3.2(node-fetch@3.3.2))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(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/tsconfig': specifier: 3.0.1 version: 3.0.1 @@ -2766,7 +2769,7 @@ importers: version: 7.28.2 '@modern-js/runtime': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(react@18.3.1) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(react@18.3.1) '@module-federation/modern-js-v3': specifier: workspace:* version: link:../../../../packages/modernjs-v3 @@ -2785,13 +2788,13 @@ importers: version: 2.59.0(typescript@5.0.4) '@modern-js/app-tools': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1))) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))) '@modern-js/eslint-config': specifier: 2.59.0 version: 2.59.0(typescript@5.0.4) '@modern-js/plugin-server': specifier: 2.68.0 - version: 2.68.0(@babel/traverse@7.29.0)(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.68.0(@babel/traverse@7.29.0)(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(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/tsconfig': specifier: 3.0.1 version: 3.0.1 @@ -2830,13 +2833,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.3.2(node-fetch@3.3.2))(core-js@3.49.0)) + version: 1.5.1(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(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.3.2(node-fetch@3.3.2))(@types/react@19.2.14)(core-js@3.49.0)(webpack-hot-middleware@2.26.1) + version: 2.0.3(@module-federation/runtime-tools@2.4.0(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.3.2(node-fetch@3.3.2))(@types/react@19.2.14)(core-js@3.49.0)(webpack-hot-middleware@2.26.1)) + version: 2.0.1(@rspress/core@2.0.3(@module-federation/runtime-tools@2.4.0(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) @@ -3058,6 +3061,9 @@ importers: '@modern-js/runtime': specifier: 2.70.8 version: 2.70.8(react-dom@19.2.4(react@19.2.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)))(react@19.2.4) + '@module-federation/observability-plugin': + specifier: workspace:* + version: link:../observability-plugin '@module-federation/sdk': specifier: workspace:* version: link:../sdk @@ -3479,7 +3485,7 @@ importers: version: 0.80.0(@babel/core@7.29.0)(@react-native-community/cli@19.1.2(typescript@5.9.3))(@types/react@19.2.14)(react@19.1.0) ts-node: specifier: ^10.9.2 - version: 10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@20.19.5)(typescript@5.9.3) + version: 10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.17))(@types/node@20.19.5)(typescript@5.9.3) typescript: specifier: ^5.8.3 version: 5.9.3 @@ -3700,16 +3706,16 @@ importers: devDependencies: '@modern-js/app-tools': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(@swc/helpers@0.5.17))(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.17))(@types/node@25.6.0)(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.17))(esbuild@0.25.5)(webpack-cli@5.1.4)) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(@swc/helpers@0.5.17))(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.17))(@types/node@25.6.0)(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.17))(esbuild@0.25.5)(webpack-cli@5.1.4)) '@modern-js/module-tools': specifier: 2.70.5 version: 2.70.5(@types/node@25.6.0)(typescript@5.9.3) '@modern-js/runtime': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(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.17))(esbuild@0.25.5)(webpack-cli@5.1.4)))(react@18.3.1) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(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.17))(esbuild@0.25.5)(webpack-cli@5.1.4)))(react@18.3.1) '@modern-js/server-runtime': specifier: 3.0.1 - version: 3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@modern-js/tsconfig': specifier: 3.0.1 version: 3.0.1 @@ -3718,10 +3724,10 @@ importers: version: link:../manifest '@rsbuild/core': specifier: 2.0.0-beta.2 - version: 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0) + version: 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0) '@rsbuild/plugin-react': specifier: 1.4.5 - version: 1.4.5(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0))(webpack-hot-middleware@2.26.1) + version: 1.4.5(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0))(webpack-hot-middleware@2.26.1) '@rslib/core': specifier: 0.18.5 version: 0.18.5(@microsoft/api-extractor@7.57.7(@types/node@25.6.0))(typescript@5.9.3) @@ -3879,6 +3885,16 @@ importers: specifier: ^5.40.0 version: 5.104.1(@swc/core@1.7.26(@swc/helpers@0.5.13))(esbuild@0.27.3)(webpack-cli@5.1.4) + packages/observability-plugin: + dependencies: + '@module-federation/sdk': + specifier: workspace:* + version: link:../sdk + devDependencies: + '@module-federation/runtime': + specifier: workspace:* + version: link:../runtime + packages/retry-plugin: dependencies: '@module-federation/sdk': @@ -3903,7 +3919,7 @@ importers: devDependencies: '@rsbuild/core': specifier: 2.0.0-beta.2 - version: 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0) + version: 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0) '@rslib/core': specifier: ^0.12.4 version: 0.12.4(@microsoft/api-extractor@7.57.7(@types/node@25.6.0))(typescript@5.9.3) @@ -3961,7 +3977,7 @@ importers: version: link:../sdk '@rspress/shared': specifier: 2.0.3 - version: 2.0.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0) + version: 2.0.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0) cheerio: specifier: 1.0.0-rc.12 version: 1.0.0-rc.12 @@ -3977,7 +3993,7 @@ importers: version: 0.9.2(@microsoft/api-extractor@7.57.7(@types/node@25.6.0))(typescript@5.9.3) '@rspress/core': specifier: 2.0.3 - version: 2.0.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@types/react@18.3.28)(core-js@3.49.0)(webpack-hot-middleware@2.26.1) + version: 2.0.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@types/react@18.3.28)(core-js@3.49.0)(webpack-hot-middleware@2.26.1) '@types/html-to-text': specifier: ^9.0.4 version: 9.0.4 @@ -8160,8 +8176,8 @@ packages: '@module-federation/error-codes@2.2.2': resolution: {integrity: sha512-e+dxUrqrdRbhfvg8TyG0UQBQAjYZ8pI2b3HWfESLXqWi3xCiivZSnqsPN+zychrQ1hDZAdCheZ9zT91zhMrkxw==} - '@module-federation/error-codes@2.3.2': - resolution: {integrity: sha512-Y8F6EG+shNY5mJ0yKcJh4t6HlMYEtqMvABDDmxWulLz/tSV859SL05I5eDR1EvK0jNhLbq8LFipFEToQLqF0AA==} + '@module-federation/error-codes@2.4.0': + resolution: {integrity: sha512-ktCZtwOoiKR1URJyBt223OsOFAUvc13rICYif55mt7+DomtELlh5FicnEz6mPLBUwmNM9vyBMvkxOdp+fQ5oUg==} '@module-federation/inject-external-runtime-core-plugin@0.21.6': resolution: {integrity: sha512-DJQne7NQ988AVi3QB8byn12FkNb+C2lBeU1NRf8/WbL0gmHsr6kW8hiEJCm8LYaURwtsQqtsEV7i+8+51qjSmQ==} @@ -8241,8 +8257,8 @@ packages: '@module-federation/runtime-core@2.2.2': resolution: {integrity: sha512-JL+W6Yol1+CPJ662H1SEQLwxfBU2kCKhUcYrMr/X6j0qFWB8x5PBSpQbwAIcJiKfflHgBaJZypmCKmq8qRv3Aw==} - '@module-federation/runtime-core@2.3.2': - resolution: {integrity: sha512-o4Pfi21uADHtSl3/BHu/3ph5+069pDOXL8Lck12b+rxsHessbeZkNo+MOxwP+gXf8fFsT+f9spOmx+Y5gtyc2A==} + '@module-federation/runtime-core@2.4.0': + resolution: {integrity: sha512-0S8fDw28DXDW17lTQwq5vfJWe2lG0Lw3+w4vk3DVVImLwXXay+OGxLDxzWUfypWcMznfpnoAnFUMO3PtuXziuA==} '@module-federation/runtime-tools@0.1.6': resolution: {integrity: sha512-7ILVnzMIa0Dlc0Blck5tVZG1tnk1MmLnuZpLOMpbdW+zl+N6wdMjjHMjEZFCUAJh2E5XJ3BREwfX8Ets0nIkLg==} @@ -8274,8 +8290,8 @@ packages: '@module-federation/runtime-tools@2.2.2': resolution: {integrity: sha512-UnlKvy/zrbLTLItrI1tORiS6wdp5pYOCrR2LtDEbjJ2r+avzANSf2vJ7lAIT4SX5Pi9WwY5RhE4Y/BOwhAj4DA==} - '@module-federation/runtime-tools@2.3.2': - resolution: {integrity: sha512-i9B3h2PgEjXMj9slEQdzus58t6UsTRGAaAB+E2fN7cOIKii4t0+bkChFFLtqplUvWMH6/AQ1D4Gj2IfwlbOXDg==} + '@module-federation/runtime-tools@2.4.0': + resolution: {integrity: sha512-BWQsGT4EWscV9bx3bVHEwp6lERBsiYm7rnPiDpwd2fx+hGEpz1IM9Pz35VryHNDXYxw7MzaAuwTMM+L7uN8OYQ==} '@module-federation/runtime@0.1.6': resolution: {integrity: sha512-nj6a+yJ+QxmcE89qmrTl4lphBIoAds0PFPVGnqLRWflwAP88jrCcrrTqRhARegkFDL+wE9AE04+h6jzlbIfMKg==} @@ -8307,8 +8323,8 @@ packages: '@module-federation/runtime@2.2.2': resolution: {integrity: sha512-zV6kbAUU1tQZr4KrZXQhzQP3WTb7oMRlIFw4UBhbh2JhAKGYS5CNc/n7+RV+mDxIs//qVmVzdSpJtTOMBLeFCw==} - '@module-federation/runtime@2.3.2': - resolution: {integrity: sha512-JFSAbr0zBDmNF+wcqHw22CmZGuUZjTsa4qzm1+RUVi3blrnKeBj9Y2FppEOBwPc/FHUr4/MDijA/6GFE2cv7gQ==} + '@module-federation/runtime@2.4.0': + resolution: {integrity: sha512-IrLAMwUuteRgFlEkg9jrn4bk8uC897FnXvfNmkKD8/qIoNtSd+32e5ouQn+PEYbX/RjRUB1TYveY6rYHpTPkyg==} '@module-federation/sdk@0.1.6': resolution: {integrity: sha512-qifXpyYLM7abUeEOIfv0oTkguZgRZuwh89YOAYIZJlkP6QbRG7DJMQvtM8X2yHXm9PTk0IYNnOJH0vNQCo6auQ==} @@ -8345,10 +8361,10 @@ packages: node-fetch: optional: true - '@module-federation/sdk@2.3.2': - resolution: {integrity: sha512-EKiZpLnD2ogy7OcnuU+vkGO5vhh3J25PHwtEzgVGIAsMis3JHYPpY7L1Ue7i0zVrdmI23dVJPBtfGV/Pg7THPA==} + '@module-federation/sdk@2.4.0': + resolution: {integrity: sha512-eZDdF5B69W9npuka0VL24FY7XDM+YAwwfkscSeWOSqv4/8Hm0xmcmSurlP6NIOrwbeogerRCtEcnx/TFXYjoow==} peerDependencies: - node-fetch: ^3.3.2 + node-fetch: ^2.7.0 || ^3.3.2 peerDependenciesMeta: node-fetch: optional: true @@ -8389,8 +8405,8 @@ packages: '@module-federation/webpack-bundler-runtime@2.2.2': resolution: {integrity: sha512-g5UEn5APnNYvajwWQ0oc3Um+HO1z/jBcBkLSKAoSOCI6XxQk5eAV1PNDuDehAgJEtJw5yFD25kZPW3X7m39y7g==} - '@module-federation/webpack-bundler-runtime@2.3.2': - resolution: {integrity: sha512-K9XRr7Jhsmo2JjETwIctLi+R22mUjtSZ5hUEgvwcHeGss8enqdDU+kt2NP/SVG+/dRUXkvzkGppqkjd6sBKg7w==} + '@module-federation/webpack-bundler-runtime@2.4.0': + resolution: {integrity: sha512-Ntx0+QsgcwtXlpGjL/Vf2PMdPjUHl07b3yM4kBc1kbRogW3Ee84QneBRi/X3w4/jlz4JKbHjD+CMXaqi2W6hgw==} '@mswjs/cookies@0.2.2': resolution: {integrity: sha512-mlN83YSrcFgk7Dm1Mys40DLssI1KdJji2CMKN8eOlBqsTADYzj2+jWzsANsUTFbxDMWPD5e9bfA1RGqBpS3O1g==} @@ -12856,22 +12872,6 @@ packages: resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - '@testing-library/react-hooks@8.0.1': - resolution: {integrity: sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==} - engines: {node: '>=12'} - peerDependencies: - '@types/react': ^16.9.0 || ^17.0.0 - react: ^16.9.0 || ^17.0.0 - react-dom: ^16.9.0 || ^17.0.0 - react-test-renderer: ^16.9.0 || ^17.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - react-dom: - optional: true - react-test-renderer: - optional: true - '@testing-library/react@15.0.7': resolution: {integrity: sha512-cg0RvEdD1TIhhkm1IeYMQxrzy0MtUNfa3minv4MjbgcYzJAZ7yD0i0lwoPOTPr+INtiXFezt2o8xMSnyHhEn2Q==} engines: {node: '>=18'} @@ -13361,9 +13361,6 @@ packages: '@types/react-test-renderer@19.1.0': resolution: {integrity: sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ==} - '@types/react@18.0.38': - resolution: {integrity: sha512-ExsidLLSzYj4cvaQjGnQCk4HFfVT9+EZ9XZsQ8Hsrcn8QNgXtpZ3m9vSIC2MWtx7jHictK6wYhQgGh6ic58oOw==} - '@types/react@18.2.79': resolution: {integrity: sha512-RwGAGXPl9kSXwdNTafkOEuFrTBD5SA2B3iEB96xi8+xu5ddUa/cpvyVCSNn+asgLCTHkb5ZxN8gbuibYJi4s1w==} @@ -13385,9 +13382,6 @@ packages: '@types/retry@0.12.2': resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} - '@types/scheduler@0.26.0': - resolution: {integrity: sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==} - '@types/semver@7.5.8': resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} @@ -22869,12 +22863,6 @@ packages: react: ^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0 react-dom: ^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0 - react-error-boundary@3.1.4: - resolution: {integrity: sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==} - engines: {node: '>=10', npm: '>=6'} - peerDependencies: - react: '>=16.13.1' - react-error-boundary@4.1.2: resolution: {integrity: sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==} peerDependencies: @@ -23143,11 +23131,6 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-shallow-renderer@16.15.0: - resolution: {integrity: sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==} - peerDependencies: - react: ^16.0.0 || ^17.0.0 || ^18.0.0 - react-side-effect@2.1.2: resolution: {integrity: sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==} peerDependencies: @@ -23169,11 +23152,6 @@ packages: '@types/react': optional: true - react-test-renderer@18.3.1: - resolution: {integrity: sha512-KkAgygexHUkQqtvvx/otwxtuFu5cVjfzTCtjXLH9boS19/Nbtg84zS7wIQn39G8IlrhThBpQsMKkq5ZHZIYFXA==} - peerDependencies: - react: ^18.3.1 - react-test-renderer@19.1.0: resolution: {integrity: sha512-jXkSl3CpvPYEF+p/eGDLB4sPoDX8pKkYvRl9+rR8HxLY0X04vW7hCm1/0zHoUSjPZ3bDa+wXWNTDVIw/R8aDVw==} peerDependencies: @@ -25154,27 +25132,6 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - ts-jest@29.0.1: - resolution: {integrity: sha512-htQOHshgvhn93QLxrmxpiQPk69+M1g7govO1g6kf6GsjCv4uvRV0znVmDrrvjUrVCnTYeY4FBxTYYYD4airyJA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/types': ^29.0.0 - babel-jest: ^29.0.0 - esbuild: '*' - jest: ^29.0.0 - typescript: '>=4.3' - peerDependenciesMeta: - '@babel/core': - optional: true - '@jest/types': - optional: true - babel-jest: - optional: true - esbuild: - optional: true - ts-jest@29.1.5: resolution: {integrity: sha512-UuClSYxM7byvvYfyWdFI+/2UxMmwNyJb0NPkZPQE2hew3RurV7l7zURgOHAd/1I1ZdPpe3GUsXNXAcN8TFKSIg==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} @@ -29519,41 +29476,6 @@ 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@20.19.5)(typescript@5.9.3))': - 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@20.19.5)(typescript@5.9.3)) - 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/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@25.6.0)(typescript@5.0.4))': dependencies: '@jest/console': 29.7.0 @@ -30382,22 +30304,22 @@ snapshots: - webpack-hot-middleware - webpack-plugin-serve - '@modern-js/app-tools@3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(@swc/helpers@0.5.17))(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.17))(@types/node@25.6.0)(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.17))(esbuild@0.25.5)(webpack-cli@5.1.4))': + '@modern-js/app-tools@3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(@swc/helpers@0.5.17))(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.17))(@types/node@25.6.0)(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.17))(esbuild@0.25.5)(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.3.2(node-fetch@3.3.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(@swc/helpers@0.5.17))(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.17))(esbuild@0.25.5)(webpack-cli@5.1.4)) + '@modern-js/builder': 3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(@swc/helpers@0.5.17))(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.17))(esbuild@0.25.5)(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.3.2(node-fetch@3.3.0))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@modern-js/plugin': 3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(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.3.2(node-fetch@3.3.0))(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.3.2(node-fetch@3.3.0))(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.17))(@types/node@25.6.0)(typescript@5.9.3))(tsconfig-paths@4.2.0) - '@modern-js/server-core': 3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0)(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.4.0(node-fetch@3.3.0))(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.4.0(node-fetch@3.3.0))(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.17))(@types/node@25.6.0)(typescript@5.9.3))(tsconfig-paths@4.2.0) + '@modern-js/server-core': 3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(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.3.2(node-fetch@3.3.0))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0) '@swc/helpers': 0.5.17 es-module-lexer: 1.7.0 esbuild: 0.25.5 @@ -30433,22 +30355,22 @@ snapshots: - webpack - webpack-hot-middleware - '@modern-js/app-tools@3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.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.8.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@20.19.5)(typescript@5.0.4))(tsconfig-paths@3.14.2)(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(webpack-dev-server@5.2.3)(webpack@5.104.1)))': + '@modern-js/app-tools@3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(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.8.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@20.19.5)(typescript@5.0.4))(tsconfig-paths@3.14.2)(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(webpack-dev-server@5.2.3)(webpack@5.104.1)))': 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.3.2(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1))) + '@modern-js/builder': 3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))) '@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.3.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': 3.0.1(@module-federation/runtime-tools@2.4.0(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.3.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.3.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.8.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@20.19.5)(typescript@5.0.4))(tsconfig-paths@3.14.2) - '@modern-js/server-core': 3.0.1(@module-federation/runtime-tools@2.3.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/prod-server': 3.0.1(@module-federation/runtime-tools@2.4.0(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.4.0(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.8.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@20.19.5)(typescript@5.0.4))(tsconfig-paths@3.14.2) + '@modern-js/server-core': 3.0.1(@module-federation/runtime-tools@2.4.0(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.3.2(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0) '@swc/helpers': 0.5.17 es-module-lexer: 1.7.0 esbuild: 0.25.5 @@ -30484,22 +30406,22 @@ snapshots: - webpack - webpack-hot-middleware - '@modern-js/app-tools@3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1)))': + '@modern-js/app-tools@3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1)))': 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.3.2(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1))) + '@modern-js/builder': 3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))) '@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.3.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': 3.0.1(@module-federation/runtime-tools@2.4.0(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.3.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.3.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.3.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/prod-server': 3.0.1(@module-federation/runtime-tools@2.4.0(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.4.0(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.4.0(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.3.2(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0) '@swc/helpers': 0.5.17 es-module-lexer: 1.7.0 esbuild: 0.25.5 @@ -30604,7 +30526,7 @@ snapshots: - '@rsbuild/core' - supports-color - '@modern-js/babel-preset@2.68.0(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))': + '@modern-js/babel-preset@2.68.0(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) @@ -30616,7 +30538,7 @@ snapshots: '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) '@babel/runtime': 7.28.2 '@babel/types': 7.29.0 - '@rsbuild/plugin-babel': 1.0.5(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0)) + '@rsbuild/plugin-babel': 1.0.5(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0)) '@swc/helpers': 0.5.17 '@types/babel__core': 7.20.5 babel-plugin-dynamic-import-node: 2.3.3 @@ -30667,22 +30589,22 @@ snapshots: - '@rsbuild/core' - supports-color - '@modern-js/builder@3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(@swc/helpers@0.5.17))(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.17))(esbuild@0.25.5)(webpack-cli@5.1.4))': + '@modern-js/builder@3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(@swc/helpers@0.5.17))(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.17))(esbuild@0.25.5)(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.3.2(node-fetch@3.3.0))(core-js@3.49.0) - '@rsbuild/plugin-assets-retry': 1.5.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0)) - '@rsbuild/plugin-check-syntax': 1.6.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0)) - '@rsbuild/plugin-css-minimizer': 1.1.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0))(esbuild@0.25.5)(webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4)) - '@rsbuild/plugin-less': 1.6.0(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0)) - '@rsbuild/plugin-react': 1.4.4(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(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.3.2(node-fetch@3.3.0))(core-js@3.49.0)) - '@rsbuild/plugin-sass': 1.5.0(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0)) - '@rsbuild/plugin-source-build': 1.0.4(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0)) - '@rsbuild/plugin-svgr': 1.3.0(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0))(typescript@5.9.3)(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.3.2(node-fetch@3.3.0))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(@swc/helpers@0.5.17))(tslib@2.8.1)(typescript@5.9.3) - '@rsbuild/plugin-typed-css-modules': 1.2.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0)) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0) + '@rsbuild/plugin-assets-retry': 1.5.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0)) + '@rsbuild/plugin-check-syntax': 1.6.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0)) + '@rsbuild/plugin-css-minimizer': 1.1.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0))(esbuild@0.25.5)(webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4)) + '@rsbuild/plugin-less': 1.6.0(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0)) + '@rsbuild/plugin-react': 1.4.4(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(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.4.0(node-fetch@3.3.0))(core-js@3.49.0)) + '@rsbuild/plugin-sass': 1.5.0(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0)) + '@rsbuild/plugin-source-build': 1.0.4(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0)) + '@rsbuild/plugin-svgr': 1.3.0(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0))(typescript@5.9.3)(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.4.0(node-fetch@3.3.0))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(@swc/helpers@0.5.17))(tslib@2.8.1)(typescript@5.9.3) + '@rsbuild/plugin-typed-css-modules': 1.2.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0)) '@swc/core': 1.15.10(@swc/helpers@0.5.17) '@swc/helpers': 0.5.17 autoprefixer: 10.4.24(postcss@8.5.8) @@ -30699,7 +30621,7 @@ snapshots: 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.3.2(node-fetch@3.3.0))(@swc/helpers@0.5.17)) + rspack-manifest-plugin: 5.2.1(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(@swc/helpers@0.5.17)) ts-deepmerge: 7.0.3 transitivePeerDependencies: - '@module-federation/runtime-tools' @@ -30718,22 +30640,22 @@ snapshots: - webpack - webpack-hot-middleware - '@modern-js/builder@3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1)))': + '@modern-js/builder@3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1)))': 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.3.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.3.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.3.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.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1))) - '@rsbuild/plugin-less': 1.6.0(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.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.3.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.3.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.3.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.3.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.3.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.3.2(node-fetch@3.3.2))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.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.3.2(node-fetch@3.3.2))(core-js@3.49.0)) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(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.4.0(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.4.0(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.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))) + '@rsbuild/plugin-less': 1.6.0(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(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.4.0(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.4.0(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.4.0(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.4.0(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.4.0(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.4.0(node-fetch@3.3.2))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(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.4.0(node-fetch@3.3.2))(core-js@3.49.0)) '@swc/core': 1.15.10(@swc/helpers@0.5.17) '@swc/helpers': 0.5.17 autoprefixer: 10.4.24(postcss@8.5.8) @@ -30750,7 +30672,7 @@ snapshots: 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.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19)) + rspack-manifest-plugin: 5.2.1(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19)) ts-deepmerge: 7.0.3 transitivePeerDependencies: - '@module-federation/runtime-tools' @@ -30969,10 +30891,10 @@ snapshots: '@modern-js/utils': 2.70.8 '@swc/helpers': 0.5.17 - '@modern-js/plugin-server@2.68.0(@babel/traverse@7.29.0)(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.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-server@2.68.0(@babel/traverse@7.29.0)(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@modern-js/runtime-utils': 2.68.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@modern-js/server-utils': 2.68.0(@babel/traverse@7.29.0)(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0)) + '@modern-js/server-utils': 2.68.0(@babel/traverse@7.29.0)(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0)) '@modern-js/utils': 2.68.0 '@swc/helpers': 0.5.17 transitivePeerDependencies: @@ -31035,12 +30957,12 @@ snapshots: '@modern-js/utils': 2.70.8 '@swc/helpers': 0.5.17 - '@modern-js/plugin@3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@modern-js/plugin@3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@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) - '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0) '@swc/helpers': 0.5.17 jiti: 2.6.1 transitivePeerDependencies: @@ -31049,12 +30971,12 @@ snapshots: - react - react-dom - '@modern-js/plugin@3.0.1(@module-federation/runtime-tools@2.3.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@3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@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) - '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0) '@swc/helpers': 0.5.17 jiti: 2.6.1 transitivePeerDependencies: @@ -31083,10 +31005,10 @@ snapshots: - react - react-dom - '@modern-js/prod-server@3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0)(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.4.0(node-fetch@3.3.0))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@modern-js/runtime-utils': 3.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@modern-js/server-core': 3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@modern-js/server-core': 3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@modern-js/utils': 3.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@swc/helpers': 0.5.17 transitivePeerDependencies: @@ -31095,10 +31017,10 @@ snapshots: - react - react-dom - '@modern-js/prod-server@3.0.1(@module-federation/runtime-tools@2.3.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/prod-server@3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@modern-js/runtime-utils': 3.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@modern-js/server-core': 3.0.1(@module-federation/runtime-tools@2.3.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-core': 3.0.1(@module-federation/runtime-tools@2.4.0(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/utils': 3.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@swc/helpers': 0.5.17 transitivePeerDependencies: @@ -31278,11 +31200,11 @@ snapshots: - react-server-dom-webpack - supports-color - '@modern-js/runtime@3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(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.17))(esbuild@0.25.5)(webpack-cli@5.1.4)))(react@18.3.1)': + '@modern-js/runtime@3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(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.17))(esbuild@0.25.5)(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.3.2(node-fetch@3.3.0))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@modern-js/plugin': 3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(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.17))(esbuild@0.25.5)(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) @@ -31307,11 +31229,11 @@ snapshots: - core-js - react-server-dom-webpack - '@modern-js/runtime@3.0.1(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(react@18.3.1)': + '@modern-js/runtime@3.0.1(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(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.3.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': 3.0.1(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1))))(react@18.3.1) '@modern-js/runtime-utils': 3.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -31372,9 +31294,9 @@ snapshots: - react - react-dom - '@modern-js/server-core@3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@modern-js/server-core@3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@modern-js/plugin': 3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@modern-js/plugin': 3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(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/utils': 3.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@swc/helpers': 0.5.17 @@ -31391,9 +31313,9 @@ snapshots: - react - react-dom - '@modern-js/server-core@3.0.1(@module-federation/runtime-tools@2.3.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-core@3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@modern-js/plugin': 3.0.1(@module-federation/runtime-tools@2.3.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': 3.0.1(@module-federation/runtime-tools@2.4.0(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/runtime-utils': 3.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@modern-js/utils': 3.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@swc/helpers': 0.5.17 @@ -31419,10 +31341,10 @@ snapshots: - react - react-dom - '@modern-js/server-runtime@3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@modern-js/server-runtime@3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@modern-js/runtime-utils': 3.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@modern-js/server-core': 3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@modern-js/server-core': 3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@modern-js/types': 3.0.1 '@swc/helpers': 0.5.17 transitivePeerDependencies: @@ -31431,10 +31353,10 @@ snapshots: - react - react-dom - '@modern-js/server-runtime@3.0.1(@module-federation/runtime-tools@2.3.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@3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@modern-js/runtime-utils': 3.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@modern-js/server-core': 3.0.1(@module-federation/runtime-tools@2.3.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-core': 3.0.1(@module-federation/runtime-tools@2.4.0(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/types': 3.0.1 '@swc/helpers': 0.5.17 transitivePeerDependencies: @@ -31443,7 +31365,7 @@ snapshots: - react - react-dom - '@modern-js/server-utils@2.68.0(@babel/traverse@7.29.0)(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))': + '@modern-js/server-utils@2.68.0(@babel/traverse@7.29.0)(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) @@ -31452,7 +31374,7 @@ snapshots: '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) '@modern-js/babel-compiler': 2.68.0 '@modern-js/babel-plugin-module-resolver': 2.68.0 - '@modern-js/babel-preset': 2.68.0(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0)) + '@modern-js/babel-preset': 2.68.0(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0)) '@modern-js/utils': 2.68.0 '@swc/helpers': 0.5.17 babel-plugin-transform-typescript-metadata: 0.3.2(@babel/core@7.29.0)(@babel/traverse@7.29.0) @@ -31563,10 +31485,10 @@ snapshots: - supports-color - utf-8-validate - '@modern-js/server@3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(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.17))(@types/node@25.6.0)(typescript@5.9.3))(tsconfig-paths@4.2.0)': + '@modern-js/server@3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(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.17))(@types/node@25.6.0)(typescript@5.9.3))(tsconfig-paths@4.2.0)': dependencies: '@modern-js/runtime-utils': 3.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@modern-js/server-core': 3.0.1(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@modern-js/server-core': 3.0.1(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(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) @@ -31589,10 +31511,10 @@ snapshots: - react-dom - utf-8-validate - '@modern-js/server@3.0.1(@module-federation/runtime-tools@2.3.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.8.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@20.19.5)(typescript@5.0.4))(tsconfig-paths@3.14.2)': + '@modern-js/server@3.0.1(@module-federation/runtime-tools@2.4.0(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.8.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@20.19.5)(typescript@5.0.4))(tsconfig-paths@3.14.2)': dependencies: '@modern-js/runtime-utils': 3.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@modern-js/server-core': 3.0.1(@module-federation/runtime-tools@2.3.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-core': 3.0.1(@module-federation/runtime-tools@2.4.0(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) @@ -31615,10 +31537,10 @@ snapshots: - react-dom - utf-8-validate - '@modern-js/server@3.0.1(@module-federation/runtime-tools@2.3.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@3.0.1(@module-federation/runtime-tools@2.4.0(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)': dependencies: '@modern-js/runtime-utils': 3.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@modern-js/server-core': 3.0.1(@module-federation/runtime-tools@2.3.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-core': 3.0.1(@module-federation/runtime-tools@2.4.0(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) @@ -32261,7 +32183,7 @@ snapshots: '@module-federation/error-codes@2.2.2': {} - '@module-federation/error-codes@2.3.2': + '@module-federation/error-codes@2.4.0': optional: true '@module-federation/inject-external-runtime-core-plugin@0.21.6(@module-federation/runtime-tools@0.21.6)': @@ -32422,18 +32344,18 @@ snapshots: transitivePeerDependencies: - node-fetch - '@module-federation/runtime-core@2.3.2(node-fetch@3.3.0)': + '@module-federation/runtime-core@2.4.0(node-fetch@3.3.0)': dependencies: - '@module-federation/error-codes': 2.3.2 - '@module-federation/sdk': 2.3.2(node-fetch@3.3.0) + '@module-federation/error-codes': 2.4.0 + '@module-federation/sdk': 2.4.0(node-fetch@3.3.0) transitivePeerDependencies: - node-fetch optional: true - '@module-federation/runtime-core@2.3.2(node-fetch@3.3.2)': + '@module-federation/runtime-core@2.4.0(node-fetch@3.3.2)': dependencies: - '@module-federation/error-codes': 2.3.2 - '@module-federation/sdk': 2.3.2(node-fetch@3.3.2) + '@module-federation/error-codes': 2.4.0 + '@module-federation/sdk': 2.4.0(node-fetch@3.3.2) transitivePeerDependencies: - node-fetch optional: true @@ -32490,18 +32412,18 @@ snapshots: transitivePeerDependencies: - node-fetch - '@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0)': + '@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0)': dependencies: - '@module-federation/runtime': 2.3.2(node-fetch@3.3.0) - '@module-federation/webpack-bundler-runtime': 2.3.2(node-fetch@3.3.0) + '@module-federation/runtime': 2.4.0(node-fetch@3.3.0) + '@module-federation/webpack-bundler-runtime': 2.4.0(node-fetch@3.3.0) transitivePeerDependencies: - node-fetch optional: true - '@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2)': + '@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2)': dependencies: - '@module-federation/runtime': 2.3.2(node-fetch@3.3.2) - '@module-federation/webpack-bundler-runtime': 2.3.2(node-fetch@3.3.2) + '@module-federation/runtime': 2.4.0(node-fetch@3.3.2) + '@module-federation/webpack-bundler-runtime': 2.4.0(node-fetch@3.3.2) transitivePeerDependencies: - node-fetch optional: true @@ -32564,20 +32486,20 @@ snapshots: transitivePeerDependencies: - node-fetch - '@module-federation/runtime@2.3.2(node-fetch@3.3.0)': + '@module-federation/runtime@2.4.0(node-fetch@3.3.0)': dependencies: - '@module-federation/error-codes': 2.3.2 - '@module-federation/runtime-core': 2.3.2(node-fetch@3.3.0) - '@module-federation/sdk': 2.3.2(node-fetch@3.3.0) + '@module-federation/error-codes': 2.4.0 + '@module-federation/runtime-core': 2.4.0(node-fetch@3.3.0) + '@module-federation/sdk': 2.4.0(node-fetch@3.3.0) transitivePeerDependencies: - node-fetch optional: true - '@module-federation/runtime@2.3.2(node-fetch@3.3.2)': + '@module-federation/runtime@2.4.0(node-fetch@3.3.2)': dependencies: - '@module-federation/error-codes': 2.3.2 - '@module-federation/runtime-core': 2.3.2(node-fetch@3.3.2) - '@module-federation/sdk': 2.3.2(node-fetch@3.3.2) + '@module-federation/error-codes': 2.4.0 + '@module-federation/runtime-core': 2.4.0(node-fetch@3.3.2) + '@module-federation/sdk': 2.4.0(node-fetch@3.3.2) transitivePeerDependencies: - node-fetch optional: true @@ -32604,12 +32526,12 @@ snapshots: optionalDependencies: node-fetch: 2.7.0(encoding@0.1.13) - '@module-federation/sdk@2.3.2(node-fetch@3.3.0)': + '@module-federation/sdk@2.4.0(node-fetch@3.3.0)': optionalDependencies: node-fetch: 3.3.0 optional: true - '@module-federation/sdk@2.3.2(node-fetch@3.3.2)': + '@module-federation/sdk@2.4.0(node-fetch@3.3.2)': optionalDependencies: node-fetch: 3.3.2 optional: true @@ -32678,20 +32600,20 @@ snapshots: transitivePeerDependencies: - node-fetch - '@module-federation/webpack-bundler-runtime@2.3.2(node-fetch@3.3.0)': + '@module-federation/webpack-bundler-runtime@2.4.0(node-fetch@3.3.0)': dependencies: - '@module-federation/error-codes': 2.3.2 - '@module-federation/runtime': 2.3.2(node-fetch@3.3.0) - '@module-federation/sdk': 2.3.2(node-fetch@3.3.0) + '@module-federation/error-codes': 2.4.0 + '@module-federation/runtime': 2.4.0(node-fetch@3.3.0) + '@module-federation/sdk': 2.4.0(node-fetch@3.3.0) transitivePeerDependencies: - node-fetch optional: true - '@module-federation/webpack-bundler-runtime@2.3.2(node-fetch@3.3.2)': + '@module-federation/webpack-bundler-runtime@2.4.0(node-fetch@3.3.2)': dependencies: - '@module-federation/error-codes': 2.3.2 - '@module-federation/runtime': 2.3.2(node-fetch@3.3.2) - '@module-federation/sdk': 2.3.2(node-fetch@3.3.2) + '@module-federation/error-codes': 2.4.0 + '@module-federation/runtime': 2.4.0(node-fetch@3.3.2) + '@module-federation/sdk': 2.4.0(node-fetch@3.3.2) transitivePeerDependencies: - node-fetch optional: true @@ -36184,9 +36106,9 @@ snapshots: transitivePeerDependencies: - '@module-federation/runtime-tools' - '@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0)': + '@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0)': dependencies: - '@rspack/core': 2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(@swc/helpers@0.5.19) + '@rspack/core': 2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(@swc/helpers@0.5.19) '@swc/helpers': 0.5.19 jiti: 2.6.1 optionalDependencies: @@ -36194,9 +36116,9 @@ snapshots: transitivePeerDependencies: - '@module-federation/runtime-tools' - '@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0)': + '@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0)': dependencies: - '@rspack/core': 2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19) + '@rspack/core': 2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19) '@swc/helpers': 0.5.19 jiti: 2.6.1 optionalDependencies: @@ -36204,9 +36126,9 @@ snapshots: transitivePeerDependencies: - '@module-federation/runtime-tools' - '@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0)': + '@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0)': dependencies: - '@rspack/core': 2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19) + '@rspack/core': 2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19) '@swc/helpers': 0.5.19 jiti: 2.6.1 optionalDependencies: @@ -36214,13 +36136,13 @@ snapshots: transitivePeerDependencies: - '@module-federation/runtime-tools' - '@rsbuild/plugin-assets-retry@1.5.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0))': + '@rsbuild/plugin-assets-retry@1.5.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0))': optionalDependencies: - '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0) - '@rsbuild/plugin-assets-retry@1.5.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.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.4.0(node-fetch@3.3.2))(core-js@3.49.0))': optionalDependencies: - '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0) '@rsbuild/plugin-assets-retry@1.5.2(@rsbuild/core@1.7.3)': optionalDependencies: @@ -36240,13 +36162,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@rsbuild/plugin-babel@1.0.5(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))': + '@rsbuild/plugin-babel@1.0.5(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) - '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0) '@types/babel__core': 7.20.5 deepmerge: 4.3.1 reduce-configs: 1.1.1 @@ -36278,7 +36200,7 @@ snapshots: optionalDependencies: '@rsbuild/core': 1.7.3 - '@rsbuild/plugin-check-syntax@1.6.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0))': + '@rsbuild/plugin-check-syntax@1.6.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0))': dependencies: acorn: 8.16.0 browserslist-to-es-version: 1.4.1 @@ -36286,9 +36208,9 @@ snapshots: picocolors: 1.1.1 source-map: 0.7.6 optionalDependencies: - '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0) - '@rsbuild/plugin-check-syntax@1.6.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.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.4.0(node-fetch@3.3.2))(core-js@3.49.0))': dependencies: acorn: 8.16.0 browserslist-to-es-version: 1.4.1 @@ -36296,7 +36218,7 @@ snapshots: picocolors: 1.1.1 source-map: 0.7.6 optionalDependencies: - '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0) '@rsbuild/plugin-css-minimizer@1.1.1(@rsbuild/core@1.7.3)(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: @@ -36343,12 +36265,12 @@ snapshots: - lightningcss - webpack - '@rsbuild/plugin-css-minimizer@1.1.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0))(esbuild@0.25.5)(webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4))': + '@rsbuild/plugin-css-minimizer@1.1.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0))(esbuild@0.25.5)(webpack@5.104.1(@swc/core@1.15.10(@swc/helpers@0.5.17))(esbuild@0.25.5)(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.17))(esbuild@0.25.5)(webpack-cli@5.1.4)) reduce-configs: 1.1.1 optionalDependencies: - '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0) transitivePeerDependencies: - '@parcel/css' - '@swc/css' @@ -36358,12 +36280,12 @@ snapshots: - lightningcss - webpack - '@rsbuild/plugin-css-minimizer@1.1.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.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(webpack-dev-server@5.2.3)(webpack@5.104.1)))': + '@rsbuild/plugin-css-minimizer@1.1.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(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(webpack-dev-server@5.2.3)(webpack@5.104.1)))': 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(webpack-dev-server@5.2.3)(webpack@5.104.1))) reduce-configs: 1.1.1 optionalDependencies: - '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0) transitivePeerDependencies: - '@parcel/css' - '@swc/css' @@ -36379,15 +36301,15 @@ snapshots: deepmerge: 4.3.1 reduce-configs: 1.1.1 - '@rsbuild/plugin-less@1.6.0(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0))': + '@rsbuild/plugin-less@1.6.0(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0))': dependencies: - '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0) deepmerge: 4.3.1 reduce-configs: 1.1.1 - '@rsbuild/plugin-less@1.6.0(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))': + '@rsbuild/plugin-less@1.6.0(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))': dependencies: - '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0) deepmerge: 4.3.1 reduce-configs: 1.1.1 @@ -36456,17 +36378,17 @@ snapshots: optionalDependencies: '@rsbuild/core': 1.7.3 - '@rsbuild/plugin-react@1.4.4(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0))(webpack-hot-middleware@2.26.1)': + '@rsbuild/plugin-react@1.4.4(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0))(webpack-hot-middleware@2.26.1)': dependencies: - '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0) '@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.4(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))(webpack-hot-middleware@2.26.1)': + '@rsbuild/plugin-react@1.4.4(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))(webpack-hot-middleware@2.26.1)': dependencies: - '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0) '@rspack/plugin-react-refresh': 1.6.1(react-refresh@0.18.0)(webpack-hot-middleware@2.26.1) react-refresh: 0.18.0 transitivePeerDependencies: @@ -36488,25 +36410,25 @@ snapshots: transitivePeerDependencies: - webpack-hot-middleware - '@rsbuild/plugin-react@1.4.5(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0))(webpack-hot-middleware@2.26.1)': + '@rsbuild/plugin-react@1.4.5(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0))(webpack-hot-middleware@2.26.1)': dependencies: - '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0) '@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.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))(webpack-hot-middleware@2.26.1)': + '@rsbuild/plugin-react@1.4.5(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))(webpack-hot-middleware@2.26.1)': dependencies: - '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0) '@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.3.2(node-fetch@3.3.2))(core-js@3.49.0))(webpack-hot-middleware@2.26.1)': + '@rsbuild/plugin-react@1.4.5(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(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.3.2(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0) '@rspack/plugin-react-refresh': 1.6.1(react-refresh@0.18.0)(webpack-hot-middleware@2.26.1) react-refresh: 0.18.0 transitivePeerDependencies: @@ -36519,19 +36441,19 @@ snapshots: optionalDependencies: '@rsbuild/core': 1.7.3 - '@rsbuild/plugin-rem@1.0.5(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0))': + '@rsbuild/plugin-rem@1.0.5(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0))': dependencies: deepmerge: 4.3.1 terser: 5.46.1 optionalDependencies: - '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0) - '@rsbuild/plugin-rem@1.0.5(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))': + '@rsbuild/plugin-rem@1.0.5(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))': dependencies: deepmerge: 4.3.1 terser: 5.46.1 optionalDependencies: - '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0) '@rsbuild/plugin-sass@1.5.0(@rsbuild/core@1.7.3)': dependencies: @@ -36542,25 +36464,25 @@ snapshots: reduce-configs: 1.1.1 sass-embedded: 1.98.0 - '@rsbuild/plugin-sass@1.5.0(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0))': + '@rsbuild/plugin-sass@1.5.0(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0))': dependencies: - '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0) deepmerge: 4.3.1 loader-utils: 2.0.4 postcss: 8.5.8 reduce-configs: 1.1.1 sass-embedded: 1.98.0 - '@rsbuild/plugin-sass@1.5.0(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.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.4.0(node-fetch@3.3.2))(core-js@3.49.0))': dependencies: - '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0) deepmerge: 4.3.1 loader-utils: 2.0.4 postcss: 8.5.8 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.3.2(node-fetch@3.3.2))(core-js@3.49.0))': + '@rsbuild/plugin-sass@1.5.1(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))': dependencies: deepmerge: 4.3.1 loader-utils: 2.0.4 @@ -36568,7 +36490,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.3.2(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0) '@rsbuild/plugin-source-build@1.0.4(@rsbuild/core@1.7.3)': dependencies: @@ -36578,21 +36500,21 @@ snapshots: optionalDependencies: '@rsbuild/core': 1.7.3 - '@rsbuild/plugin-source-build@1.0.4(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0))': + '@rsbuild/plugin-source-build@1.0.4(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0))': dependencies: fast-glob: 3.3.3 json5: 2.2.3 yaml: 2.8.2 optionalDependencies: - '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0) - '@rsbuild/plugin-source-build@1.0.4(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.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.4.0(node-fetch@3.3.2))(core-js@3.49.0))': dependencies: fast-glob: 3.3.3 json5: 2.2.3 yaml: 2.8.2 optionalDependencies: - '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0) '@rsbuild/plugin-styled-components@1.6.0(@rsbuild/core@1.7.3)': dependencies: @@ -36615,10 +36537,10 @@ snapshots: - typescript - webpack-hot-middleware - '@rsbuild/plugin-svgr@1.3.0(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0))(typescript@5.9.3)(webpack-hot-middleware@2.26.1)': + '@rsbuild/plugin-svgr@1.3.0(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0))(typescript@5.9.3)(webpack-hot-middleware@2.26.1)': dependencies: - '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0) - '@rsbuild/plugin-react': 1.4.5(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0))(webpack-hot-middleware@2.26.1) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0) + '@rsbuild/plugin-react': 1.4.5(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0))(webpack-hot-middleware@2.26.1) '@svgr/core': 8.1.0(typescript@5.9.3) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3))(typescript@5.9.3) @@ -36629,10 +36551,10 @@ snapshots: - typescript - webpack-hot-middleware - '@rsbuild/plugin-svgr@1.3.0(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))(typescript@5.0.4)(webpack-hot-middleware@2.26.1)': + '@rsbuild/plugin-svgr@1.3.0(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))(typescript@5.0.4)(webpack-hot-middleware@2.26.1)': dependencies: - '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0) - '@rsbuild/plugin-react': 1.4.5(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))(webpack-hot-middleware@2.26.1) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/plugin-react': 1.4.5(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))(webpack-hot-middleware@2.26.1) '@svgr/core': 8.1.0(typescript@5.0.4) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3))(typescript@5.0.4) @@ -36662,27 +36584,27 @@ snapshots: - tslib - typescript - '@rsbuild/plugin-type-check@1.3.3(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(@swc/helpers@0.5.17))(tslib@2.8.1)(typescript@5.9.3)': + '@rsbuild/plugin-type-check@1.3.3(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(@swc/helpers@0.5.17))(tslib@2.8.1)(typescript@5.9.3)': dependencies: deepmerge: 4.3.1 json5: 2.2.3 reduce-configs: 1.1.1 - ts-checker-rspack-plugin: 1.3.0(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(@swc/helpers@0.5.17))(tslib@2.8.1)(typescript@5.9.3) + ts-checker-rspack-plugin: 1.3.0(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(@swc/helpers@0.5.17))(tslib@2.8.1)(typescript@5.9.3) optionalDependencies: - '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0) transitivePeerDependencies: - '@rspack/core' - tslib - typescript - '@rsbuild/plugin-type-check@1.3.3(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(tslib@2.8.1)(typescript@5.0.4)': + '@rsbuild/plugin-type-check@1.3.3(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19))(tslib@2.8.1)(typescript@5.0.4)': dependencies: deepmerge: 4.3.1 json5: 2.2.3 reduce-configs: 1.1.1 - ts-checker-rspack-plugin: 1.3.0(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(tslib@2.8.1)(typescript@5.0.4) + ts-checker-rspack-plugin: 1.3.0(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19))(tslib@2.8.1)(typescript@5.0.4) optionalDependencies: - '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0) transitivePeerDependencies: - '@rspack/core' - tslib @@ -36701,14 +36623,14 @@ snapshots: - tslib - typescript - '@rsbuild/plugin-type-check@1.3.4(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(tslib@2.8.1)(typescript@5.9.3)': + '@rsbuild/plugin-type-check@1.3.4(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19))(tslib@2.8.1)(typescript@5.9.3)': dependencies: deepmerge: 4.3.1 json5: 2.2.3 reduce-configs: 1.1.1 - ts-checker-rspack-plugin: 1.3.0(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(tslib@2.8.1)(typescript@5.9.3) + ts-checker-rspack-plugin: 1.3.0(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19))(tslib@2.8.1)(typescript@5.9.3) optionalDependencies: - '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0) transitivePeerDependencies: - '@rspack/core' - tslib @@ -36718,17 +36640,17 @@ snapshots: optionalDependencies: '@rsbuild/core': 1.7.3 - '@rsbuild/plugin-typed-css-modules@1.2.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0))': + '@rsbuild/plugin-typed-css-modules@1.2.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0))': optionalDependencies: - '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(core-js@3.49.0) - '@rsbuild/plugin-typed-css-modules@1.2.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))': + '@rsbuild/plugin-typed-css-modules@1.2.1(@rsbuild/core@2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))': optionalDependencies: - '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.2(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0) - '@rsbuild/plugin-vue@1.2.7(@rsbuild/core@1.7.3)(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(vue@3.5.30(typescript@5.9.3))': + '@rsbuild/plugin-vue@1.2.7(@rsbuild/core@1.7.3)(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19))(vue@3.5.30(typescript@5.9.3))': dependencies: - rspack-vue-loader: 17.5.0(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(vue@3.5.30(typescript@5.9.3)) + rspack-vue-loader: 17.5.0(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19))(vue@3.5.30(typescript@5.9.3)) optionalDependencies: '@rsbuild/core': 1.7.3 transitivePeerDependencies: @@ -37444,29 +37366,29 @@ snapshots: '@module-federation/runtime-tools': 0.21.6 '@swc/helpers': 0.5.19 - '@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(@swc/helpers@0.5.17)': + '@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(@swc/helpers@0.5.17)': dependencies: '@rspack/binding': 2.0.0-beta.0 '@rspack/lite-tapable': 1.1.0 optionalDependencies: - '@module-federation/runtime-tools': 2.3.2(node-fetch@3.3.0) + '@module-federation/runtime-tools': 2.4.0(node-fetch@3.3.0) '@swc/helpers': 0.5.17 optional: true - '@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(@swc/helpers@0.5.19)': + '@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(@swc/helpers@0.5.19)': dependencies: '@rspack/binding': 2.0.0-beta.0 '@rspack/lite-tapable': 1.1.0 optionalDependencies: - '@module-federation/runtime-tools': 2.3.2(node-fetch@3.3.0) + '@module-federation/runtime-tools': 2.4.0(node-fetch@3.3.0) '@swc/helpers': 0.5.19 - '@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19)': + '@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19)': dependencies: '@rspack/binding': 2.0.0-beta.0 '@rspack/lite-tapable': 1.1.0 optionalDependencies: - '@module-federation/runtime-tools': 2.3.2(node-fetch@3.3.2) + '@module-federation/runtime-tools': 2.4.0(node-fetch@3.3.2) '@swc/helpers': 0.5.19 '@rspack/dev-server@1.1.1(@rspack/core@1.3.9(@swc/helpers@0.5.13))(@types/express@4.17.21)(webpack-cli@5.1.4)(webpack@5.104.1)': @@ -37505,13 +37427,13 @@ snapshots: optionalDependencies: webpack-hot-middleware: 2.26.1 - '@rspress/core@2.0.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@types/react@18.3.28)(core-js@3.49.0)(webpack-hot-middleware@2.26.1)': + '@rspress/core@2.0.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@types/react@18.3.28)(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@18.3.28)(react@19.2.4) - '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.3.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.3.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.3.2(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(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.4.0(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.4.0(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) @@ -37556,13 +37478,13 @@ snapshots: - supports-color - webpack-hot-middleware - '@rspress/core@2.0.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@types/react@19.2.14)(core-js@3.49.0)(webpack-hot-middleware@2.26.1)': + '@rspress/core@2.0.3(@module-federation/runtime-tools@2.4.0(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.3.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.3.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.3.2(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(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.4.0(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.4.0(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) @@ -37607,9 +37529,9 @@ snapshots: - supports-color - webpack-hot-middleware - '@rspress/plugin-llms@2.0.1(@rspress/core@2.0.3(@module-federation/runtime-tools@2.3.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@2.0.1(@rspress/core@2.0.3(@module-federation/runtime-tools@2.4.0(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.3.2(node-fetch@3.3.2))(@types/react@19.2.14)(core-js@3.49.0)(webpack-hot-middleware@2.26.1) + '@rspress/core': 2.0.3(@module-federation/runtime-tools@2.4.0(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 @@ -37618,9 +37540,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@rspress/shared@2.0.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0)': + '@rspress/shared@2.0.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0)': dependencies: - '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0) '@shikijs/rehype': 3.23.0 gray-matter: 4.0.3 lodash-es: 4.17.23 @@ -38918,16 +38840,6 @@ snapshots: lodash: 4.17.23 redent: 3.0.0 - '@testing-library/react-hooks@8.0.1(@types/react@18.0.38)(react-dom@18.3.1(react@18.3.1))(react-test-renderer@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@babel/runtime': 7.28.2 - react: 18.3.1 - react-error-boundary: 3.1.4(react@18.3.1) - optionalDependencies: - '@types/react': 18.0.38 - react-dom: 18.3.1(react@18.3.1) - react-test-renderer: 18.3.1(react@18.3.1) - '@testing-library/react@15.0.7(@types/react@18.2.79)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.2 @@ -39479,12 +39391,6 @@ snapshots: dependencies: '@types/react': 19.2.14 - '@types/react@18.0.38': - dependencies: - '@types/prop-types': 15.7.15 - '@types/scheduler': 0.26.0 - csstype: 3.2.3 - '@types/react@18.2.79': dependencies: '@types/prop-types': 15.7.15 @@ -39510,8 +39416,6 @@ snapshots: '@types/retry@0.12.2': {} - '@types/scheduler@0.26.0': {} - '@types/semver@7.5.8': {} '@types/send@0.17.6': @@ -43344,21 +43248,6 @@ snapshots: - supports-color - ts-node - create-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.9.3)): - 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@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.9.3)) - jest-util: 29.7.0 - prompts: 2.4.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - create-jest@29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@25.6.0)(typescript@5.0.4)): dependencies: '@jest/types': 29.6.3 @@ -47861,25 +47750,6 @@ snapshots: - supports-color - ts-node - jest-cli@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.9.3)): - 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@20.19.5)(typescript@5.9.3)) - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.19.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.9.3)) - exit: 0.1.2 - import-local: 3.2.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@20.19.5)(typescript@5.9.3)) - 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-cli@29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@25.6.0)(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@25.6.0)(typescript@5.0.4)) @@ -47961,37 +47831,6 @@ 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@20.19.5)(typescript@5.9.3)): - 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@20.19.5)(typescript@5.9.3) - transitivePeerDependencies: - - 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@25.6.0)(typescript@5.0.4)): dependencies: '@babel/core': 7.29.0 @@ -48325,18 +48164,6 @@ snapshots: - supports-color - ts-node - 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.9.3)): - 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@20.19.5)(typescript@5.9.3)) - '@jest/types': 29.6.3 - import-local: 3.2.0 - jest-cli: 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.9.3)) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - jest@29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.19))(@types/node@25.6.0)(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@25.6.0)(typescript@5.0.4)) @@ -53578,11 +53405,6 @@ snapshots: react-dom: 19.2.4(react@19.2.4) react-is: 18.1.0 - react-error-boundary@3.1.4(react@18.3.1): - dependencies: - '@babel/runtime': 7.28.2 - react: 18.3.1 - react-error-boundary@4.1.2(react@18.3.1): dependencies: '@babel/runtime': 7.28.2 @@ -53943,11 +53765,6 @@ snapshots: '@remix-run/router': 1.23.1 react: 19.2.4 - react-router@6.30.3(react@18.3.1): - dependencies: - '@remix-run/router': 1.23.2 - react: 18.3.1 - react-router@6.30.3(react@19.2.4): dependencies: '@remix-run/router': 1.23.2 @@ -54044,12 +53861,6 @@ snapshots: react: 17.0.2 react-dom: 17.0.2(react@17.0.2) - react-shallow-renderer@16.15.0(react@18.3.1): - dependencies: - object-assign: 4.1.1 - react: 18.3.1 - react-is: 18.3.1 - react-side-effect@2.1.2(react@18.3.1): dependencies: react: 18.3.1 @@ -54082,13 +53893,6 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 - react-test-renderer@18.3.1(react@18.3.1): - dependencies: - react: 18.3.1 - react-is: 18.3.1 - react-shallow-renderer: 16.15.0(react@18.3.1) - scheduler: 0.23.2 - react-test-renderer@19.1.0(react@19.1.0): dependencies: react: 19.1.0 @@ -54803,12 +54607,12 @@ snapshots: '@microsoft/api-extractor': 7.57.7(@types/node@25.6.0) typescript: 5.9.3 - rsbuild-plugin-html-minifier-terser@1.1.3(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0)): + rsbuild-plugin-html-minifier-terser@1.1.3(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0)): dependencies: '@types/html-minifier-terser': 7.0.2 html-minifier-terser: 7.2.0 optionalDependencies: - '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0) rsbuild-plugin-publint@0.2.1(@rsbuild/core@1.7.3): dependencies: @@ -54833,24 +54637,24 @@ snapshots: optionalDependencies: '@rspack/core': 1.7.9(@swc/helpers@0.5.19) - rspack-manifest-plugin@5.2.1(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(@swc/helpers@0.5.17)): + rspack-manifest-plugin@5.2.1(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(@swc/helpers@0.5.17)): dependencies: '@rspack/lite-tapable': 1.1.0 optionalDependencies: - '@rspack/core': 2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(@swc/helpers@0.5.17) + '@rspack/core': 2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(@swc/helpers@0.5.17) - rspack-manifest-plugin@5.2.1(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19)): + rspack-manifest-plugin@5.2.1(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19)): dependencies: '@rspack/lite-tapable': 1.1.0 optionalDependencies: - '@rspack/core': 2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19) + '@rspack/core': 2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19) - rspack-vue-loader@17.5.0(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(vue@3.5.30(typescript@5.9.3)): + rspack-vue-loader@17.5.0(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19))(vue@3.5.30(typescript@5.9.3)): dependencies: '@rspack/lite-tapable': 1.1.0 chalk: 4.1.2 optionalDependencies: - '@rspack/core': 2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19) + '@rspack/core': 2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19) vue: 3.5.30(typescript@5.9.3) run-applescript@7.1.0: {} @@ -55619,18 +55423,18 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - storybook-addon-rslib@1.0.3(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))(@rslib/core@0.9.2(@microsoft/api-extractor@7.57.7(@types/node@25.6.0))(typescript@5.9.3))(storybook-builder-rsbuild@1.0.3(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(@types/react@18.3.28)(storybook@8.6.17(prettier@3.8.1))(tslib@2.8.1)(typescript@5.9.3))(typescript@5.9.3): + storybook-addon-rslib@1.0.3(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))(@rslib/core@0.9.2(@microsoft/api-extractor@7.57.7(@types/node@25.6.0))(typescript@5.9.3))(storybook-builder-rsbuild@1.0.3(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19))(@types/react@18.3.28)(storybook@8.6.17(prettier@3.8.1))(tslib@2.8.1)(typescript@5.9.3))(typescript@5.9.3): dependencies: - '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0) '@rslib/core': 0.9.2(@microsoft/api-extractor@7.57.7(@types/node@25.6.0))(typescript@5.9.3) - storybook-builder-rsbuild: 1.0.3(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(@types/react@18.3.28)(storybook@8.6.17(prettier@3.8.1))(tslib@2.8.1)(typescript@5.9.3) + storybook-builder-rsbuild: 1.0.3(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19))(@types/react@18.3.28)(storybook@8.6.17(prettier@3.8.1))(tslib@2.8.1)(typescript@5.9.3) optionalDependencies: typescript: 5.9.3 - storybook-builder-rsbuild@1.0.3(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(@types/react@18.3.28)(storybook@8.6.17(prettier@3.8.1))(tslib@2.8.1)(typescript@5.9.3): + storybook-builder-rsbuild@1.0.3(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19))(@types/react@18.3.28)(storybook@8.6.17(prettier@3.8.1))(tslib@2.8.1)(typescript@5.9.3): dependencies: - '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0) - '@rsbuild/plugin-type-check': 1.3.4(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(tslib@2.8.1)(typescript@5.9.3) + '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/plugin-type-check': 1.3.4(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19))(tslib@2.8.1)(typescript@5.9.3) '@storybook/addon-docs': 8.6.18(@types/react@18.3.28)(storybook@8.6.17(prettier@3.8.1)) '@storybook/core-webpack': 8.6.18(storybook@8.6.17(prettier@3.8.1)) browser-assert: 1.2.1 @@ -55642,7 +55446,7 @@ snapshots: magic-string: 0.30.21 path-browserify: 1.0.1 process: 0.11.10 - rsbuild-plugin-html-minifier-terser: 1.1.3(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0)) + rsbuild-plugin-html-minifier-terser: 1.1.3(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0)) sirv: 2.0.4 storybook: 8.6.17(prettier@3.8.1) ts-dedent: 2.2.0 @@ -55656,10 +55460,10 @@ snapshots: - '@types/react' - tslib - storybook-react-rsbuild@1.0.3(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.59.0)(storybook@8.6.17(prettier@3.8.1))(tslib@2.8.1)(typescript@5.9.3)(webpack@5.104.1(@swc/core@1.7.26(@swc/helpers@0.5.13))(esbuild@0.25.0)(webpack-cli@5.1.4)): + storybook-react-rsbuild@1.0.3(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.59.0)(storybook@8.6.17(prettier@3.8.1))(tslib@2.8.1)(typescript@5.9.3)(webpack@5.104.1(@swc/core@1.7.26(@swc/helpers@0.5.13))(esbuild@0.25.0)(webpack-cli@5.1.4)): dependencies: '@rollup/pluginutils': 5.3.0(rollup@4.59.0) - '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0) + '@rsbuild/core': 2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0) '@storybook/react': 8.6.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.17(prettier@3.8.1))(typescript@5.9.3) '@storybook/react-docgen-typescript-plugin': 1.0.1(typescript@5.9.3)(webpack@5.104.1(@swc/core@1.7.26(@swc/helpers@0.5.13))(esbuild@0.25.0)(webpack-cli@5.1.4)) '@types/node': 18.19.130 @@ -55671,7 +55475,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) resolve: 1.22.11 storybook: 8.6.17(prettier@3.8.1) - storybook-builder-rsbuild: 1.0.3(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(@types/react@18.3.28)(storybook@8.6.17(prettier@3.8.1))(tslib@2.8.1)(typescript@5.9.3) + storybook-builder-rsbuild: 1.0.3(@rsbuild/core@2.0.0-beta.3(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(core-js@3.49.0))(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19))(@types/react@18.3.28)(storybook@8.6.17(prettier@3.8.1))(tslib@2.8.1)(typescript@5.9.3) tsconfig-paths: 4.2.0 optionalDependencies: typescript: 5.9.3 @@ -56723,7 +56527,7 @@ snapshots: transitivePeerDependencies: - tslib - ts-checker-rspack-plugin@1.3.0(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(@swc/helpers@0.5.17))(tslib@2.8.1)(typescript@5.9.3): + ts-checker-rspack-plugin@1.3.0(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(@swc/helpers@0.5.17))(tslib@2.8.1)(typescript@5.9.3): dependencies: '@rspack/lite-tapable': 1.1.0 chokidar: 3.6.0 @@ -56731,11 +56535,11 @@ snapshots: picocolors: 1.1.1 typescript: 5.9.3 optionalDependencies: - '@rspack/core': 2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.0))(@swc/helpers@0.5.17) + '@rspack/core': 2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.0))(@swc/helpers@0.5.17) transitivePeerDependencies: - tslib - ts-checker-rspack-plugin@1.3.0(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(tslib@2.8.1)(typescript@5.0.4): + ts-checker-rspack-plugin@1.3.0(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19))(tslib@2.8.1)(typescript@5.0.4): dependencies: '@rspack/lite-tapable': 1.1.0 chokidar: 3.6.0 @@ -56743,11 +56547,11 @@ snapshots: picocolors: 1.1.1 typescript: 5.0.4 optionalDependencies: - '@rspack/core': 2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19) + '@rspack/core': 2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19) transitivePeerDependencies: - tslib - ts-checker-rspack-plugin@1.3.0(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19))(tslib@2.8.1)(typescript@5.9.3): + ts-checker-rspack-plugin@1.3.0(@rspack/core@2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19))(tslib@2.8.1)(typescript@5.9.3): dependencies: '@rspack/lite-tapable': 1.1.0 chokidar: 3.6.0 @@ -56755,7 +56559,7 @@ snapshots: picocolors: 1.1.1 typescript: 5.9.3 optionalDependencies: - '@rspack/core': 2.0.0-beta.0(@module-federation/runtime-tools@2.3.2(node-fetch@3.3.2))(@swc/helpers@0.5.19) + '@rspack/core': 2.0.0-beta.0(@module-federation/runtime-tools@2.4.0(node-fetch@3.3.2))(@swc/helpers@0.5.19) transitivePeerDependencies: - tslib @@ -56767,24 +56571,6 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.0.1(@babel/core@7.29.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(esbuild@0.27.3)(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.9.3)))(typescript@5.9.3): - dependencies: - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - 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.9.3)) - jest-util: 29.7.0 - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.6.3 - typescript: 5.9.3 - yargs-parser: 21.1.1 - optionalDependencies: - '@babel/core': 7.29.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.29.0) - esbuild: 0.27.3 - ts-jest@29.1.5(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(esbuild@0.27.3)(jest@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)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 @@ -56897,6 +56683,26 @@ snapshots: optionalDependencies: '@swc/core': 1.7.26(@swc/helpers@0.5.13) + ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.17))(@types/node@20.19.5)(typescript@5.9.3): + 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': 20.19.5 + 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.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.15.10(@swc/helpers@0.5.17) + ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.17))(@types/node@22.19.15)(typescript@5.6.3): dependencies: '@cspotcode/source-map-support': 0.8.1 diff --git a/skills/README.md b/skills/README.md index 97b52480e08..a33da462508 100644 --- a/skills/README.md +++ b/skills/README.md @@ -23,6 +23,7 @@ Examples: /mf shared-deps /mf config-check /mf runtime-error RUNTIME-008 +/mf observability ``` Public skill: diff --git a/skills/mf/SKILL.md b/skills/mf/SKILL.md deleted file mode 100644 index 54e9de4afa6..00000000000 --- a/skills/mf/SKILL.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -name: mf -description: "All-in-one Module Federation skill. Use when the user asks anything about MF — concepts, configuration, runtime API, shared dependencies, type errors, runtime error code troubleshooting, slow builds, Bridge integration, or adding MF to an existing project." -argument-hint: [args...] -allowed-tools: Read Glob Bash(node *) Bash(npx tsc*) Bash(npx mf dts*) Bash(curl *) WebFetch Write Edit AskUserQuestion ---- - -# MF — Module Federation All-in-One Skill - -## Step 1: Identify the sub-skill - -Parse `$ARGUMENTS` and map to a reference file in the `reference/` directory (same directory as this file): - -| Sub-command (case-insensitive) | Aliases | Reference file | -|---|---|---| -| `docs` | `doc`, `help`, `?` | `reference/docs.md` | -| `context` | `ctx`, `info`, `status` | `reference/context.md` | -| `module-info` | `module`, `remote`, `manifest` | `reference/module-info.md` | -| `integrate` | `init`, `setup`, `add` | `reference/integrate.md` | -| `type-check` | `types`, `ts`, `dts` | `reference/type-check.md` | -| `shared-deps` | `shared`, `deps`, `singleton` | `reference/shared-deps.md` | -| `perf` | `performance`, `hmr`, `speed` | `reference/perf.md` | -| `config-check` | `config`, `plugin`, `exposes` | `reference/config-check.md` | -| `bridge-check` | `bridge`, `sub-app` | `reference/bridge-check.md` | -| `runtime-error` | `runtime-code`, `runtime-008`, `runtime-001`, `remote-entry` | `reference/runtime-error.md` | - -**If no explicit sub-command is found**, detect intent from the full input: - -| Signal in input | Reference file | -|---|---| -| Question about MF concepts, API, configuration options | `reference/docs.md` | -| "integrate", "add MF", "setup", "scaffold", "new project" | `reference/integrate.md` | -| "type error", "TS error", "@mf-types", "dts", "typescript" | `reference/type-check.md` | -| "shared", "singleton", "duplicate", "antd", "transformImport" | `reference/shared-deps.md` | -| "slow", "HMR", "performance", "build speed", "ts-go" | `reference/perf.md` | -| "plugin", "asyncStartup", "exposes key", "config" | `reference/config-check.md` | -| "bridge", "sub-app", "export-app", "createRemoteAppComponent" | `reference/bridge-check.md` | -| "RUNTIME-001", "RUNTIME-008", "runtime error code", "remote entry load failed", "ScriptNetworkError", "ScriptExecutionError", "container missing", "window[remoteEntryKey]" | `reference/runtime-error.md` | -| "manifest", "remoteEntry URL", "module info", "publicPath" | `reference/module-info.md` | -| "context", "what is configured", "MF role", "bundler" | `reference/context.md` | - -If still ambiguous, show the user the sub-command table above and ask them to pick. - -## Step 2: Load and execute the reference - -Read the matched file from the `reference/` directory (same directory as this SKILL.md). - -Execute all instructions in that file, passing the remaining arguments (everything after the sub-command token, or the full `$ARGUMENTS` if intent-detected) as `ARGS`. diff --git a/skills/mf/reference/bridge-check.md b/skills/mf/reference/bridge-check.md deleted file mode 100644 index 915e683f86f..00000000000 --- a/skills/mf/reference/bridge-check.md +++ /dev/null @@ -1,29 +0,0 @@ -# Sub-skill: bridge-check - -Check Module Federation Bridge usage: verify that producers correctly export `export-app`, and that consumers use the recommended Bridge API. - -## Step 1: Collect MFContext - -Read and follow the instructions in `./context.md`, passing ARGS as the project root. - -## Step 2: Run bridge check script - -Serialize MFContext to JSON and pass it to the check script: - -```bash -node scripts/bridge-check.js --context '' -``` - -Process each item in the output `results` and `context.mfConfig`: - -**BRIDGE-USAGE · info — No export-app export found** -- No key matching the `export-app` pattern found in `exposes` -- If this project is a sub-app that should follow the Bridge spec, guide the user to: - 1. Add `"./export-app": "./src/export-app.tsx"` to `exposes` - 2. The exported module must return an object conforming to the Bridge spec (containing `render` and `destroy` methods) - -**BRIDGE-USAGE · info — Consumer API recommendation** -- Advise consumers to use official Bridge APIs such as `createRemoteAppComponent` -- Avoid directly concatenating remote URLs or manually calling `loadRemote` - -If `context.mfRole` is `host` (no exposes), skip the producer-side check and only provide consumer-side recommendations. diff --git a/skills/mf/reference/browser-debug/long-chain.md b/skills/mf/reference/browser-debug/long-chain.md deleted file mode 100644 index c6a78cae92e..00000000000 --- a/skills/mf/reference/browser-debug/long-chain.md +++ /dev/null @@ -1,72 +0,0 @@ -# Long-Chain Capture - -Keep a tab alive across multiple steps — navigate, click through interactions, then capture. - -## Usage - -```bash -# Step 1 — open tab, keep it alive -TAB=$(node ../scripts/browser-capture.mjs "https://example.com" --keep-tab | jq -r .tabId) - -# Step 2 — click through the interaction chain (faster: domcontentloaded/none) -node ../scripts/browser-capture.mjs --tab-id "$TAB" --click "Profile" --action-wait domcontentloaded -node ../scripts/browser-capture.mjs --tab-id "$TAB" --click "Favorites" --action-wait none - -# Step 3 — final action, capture variables, close tab -node ../scripts/browser-capture.mjs --tab-id "$TAB" --click "Add" --vars __FEDERATION__ --action-wait networkidle --close -``` - -## Flags - -| Flag | Description | -|---|---| -| `--keep-tab` | Don't close tab after capture; outputs `tabId` in result | -| `--tab-id ` | Attach to existing tab instead of navigating | -| `--click ""` | Click an element; matching prefers CSS/interactive elements first | -| `--fill "placeholder::text"` | Type into an input/textarea located by placeholder | -| `--select "placeholder::value"` | Choose an option in a select located by placeholder | -| `--action-wait ` | Wait strategy after click/fill/select (`auto` is default; use `networkidle` on the final step when strict consistency is needed) | -| `--no-entries` | Exclude entries logs to speed up capture and reduce output size | -| `--dump-dom` | Output page DOM structure (for identifying selectors) | -| `--close` | Close the tab after this step | - -## Click matching - -Applied in order: -1. If query starts with `#`, `.`, `[`, or contains `>` → CSS selector -2. Strong interactive elements (`button`, `a`, role/button/tab/menuitem/option, submit/button inputs) -3. Weak interactive elements (`div`/`span`/`li`) only when they look clickable (`cursor:pointer`, `onclick`, or focusable tabindex) -4. Text match priority inside each layer: **exact** → **prefix** → **contains** - -## Fill (input/textarea) - -Locates the field by `placeholder` attribute, injects text using native value setter — compatible with React and Vue controlled inputs. - -```bash -node ../scripts/browser-capture.mjs --tab-id "$TAB" --fill "Enter keyword::Module Federation" -``` - -## Select (dropdown) - -Locates by `placeholder` attribute or default option text, then: -- **Native `, otherwise click the custom dropdown trigger - const r1 = await session.send('Runtime.evaluate', { - expression: `(function(ph, val) { - // native