Skip to content

feat: support component data fetch in ssr mode #3753

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 52 commits into from
Jun 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
2f2844f
chore: wip
2heal1 Mar 7, 2025
121af12
chore: add runtime plugin
2heal1 Mar 7, 2025
d6dda15
chore: add demo
2heal1 Mar 10, 2025
ec066cd
chore: add demo
2heal1 Mar 10, 2025
b88ad60
chore: comment resources
2heal1 Mar 10, 2025
d3f4639
fix(runtime): preload filter loaded resources
2heal1 Mar 13, 2025
8a072b6
chore: merge main
2heal1 Mar 20, 2025
c8d0f0c
chore: update usage
2heal1 Mar 21, 2025
f3ee19c
chore: merge main
2heal1 May 7, 2025
9f7856d
chore: remove useless files
2heal1 May 7, 2025
acede37
feat: run through basic example
2heal1 May 8, 2025
61e9f70
chore: merge main
2heal1 May 13, 2025
23a29a6
feat: replace data fetch file with temp file in client
2heal1 May 13, 2025
a1f3470
chore: wip
2heal1 May 16, 2025
7eec680
feat: support component data fetch
2heal1 May 20, 2025
abdf1bb
feat: add loading
2heal1 May 21, 2025
1ac4d51
feat: add wrapNoSSR
2heal1 May 21, 2025
ebe8914
feat: support route change and nossr cases
2heal1 May 21, 2025
e1e4971
feat: add e2e
2heal1 May 22, 2025
ea6d2ec
chore: merge main
2heal1 May 22, 2025
4cde779
docs: add doc
2heal1 May 22, 2025
2f28add
feat(modern-js-plugin): support component-level data fetch
2heal1 May 22, 2025
b1938e3
fix: ut
2heal1 May 22, 2025
1046bc0
chore: add changeset
2heal1 May 22, 2025
9e6d7f1
chore: release without cache
2heal1 May 22, 2025
215f80a
chore: remove useless log
2heal1 May 22, 2025
1706285
chore: generate temp js file
2heal1 May 22, 2025
8095955
fix: add data loader key detect
2heal1 May 23, 2025
6883b5e
fix: query may empty
2heal1 May 23, 2025
ee1217b
chore: apply server plugin in ssrPlugin
2heal1 May 23, 2025
6db264e
chore: add server plugin types
2heal1 May 23, 2025
4eb92be
fix: remove useless quote
2heal1 May 23, 2025
8b613a3
fix: server downgrade
2heal1 May 23, 2025
863fe7b
chore: apply e2e
2heal1 May 23, 2025
70b2c38
chore: push id if no exist
2heal1 May 23, 2025
cfd634a
chore: skip cache
2heal1 May 23, 2025
baa4788
fix: return if no create dataFetchMap
2heal1 May 23, 2025
4deff51
chore: remove dev e2e
2heal1 May 23, 2025
db1eff5
docs: typo
2heal1 May 23, 2025
e299c03
chore: add log in server plugin
2heal1 May 23, 2025
f70c76f
chore: log error
2heal1 May 23, 2025
2a2347c
chore: log
2heal1 May 23, 2025
ff8e77c
fix: add isDowngrade:true
2heal1 May 23, 2025
97b1515
chore: log more
2heal1 May 23, 2025
c6d60d1
fix: if set manfiest entry, stop override it
2heal1 May 26, 2025
6055890
fix: correct isDowngrade define
2heal1 May 26, 2025
bcfef8e
feat: add fetchServerQuery
2heal1 May 26, 2025
d00f1de
chore: exclude data loader fallback file in generate dts
2heal1 May 26, 2025
3a7f832
chore: remove useless log
2heal1 May 26, 2025
aeb7ad6
chore: remove uselss changelog
2heal1 May 26, 2025
208a551
chore: merge main
2heal1 Jun 4, 2025
f594663
chore: support rslib
2heal1 Jun 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/rich-bees-applaud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@module-federation/manifest': patch
---

fix(manifest): record all exposes even if the expose value is the same file
5 changes: 5 additions & 0 deletions .changeset/spotty-trains-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@module-federation/modern-js': minor
---

feat(modern-js-plugin): support component-level data fetch
4 changes: 4 additions & 0 deletions apps/manifest-demo/webpack-host/runtimePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ export default function (): FederationRuntimePlugin {
return {
name: 'custom-plugin-build',
beforeInit(args) {
const { userOptions, origin } = args;
if (origin.options.name && origin.options.name !== userOptions.name) {
userOptions.name = origin.options.name;
}
console.log('[build time inject] beforeInit: ', args);
return args;
},
Expand Down
20 changes: 20 additions & 0 deletions apps/modern-component-data-fetch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# modern-component-data-fetch

## Running Demo

- host: [localhost:5001](http://localhost:5001/)
- provider: [localhost:5002](http://localhost:5002/)
- provider-csr: [localhost:5003](http://localhost:5003/)

## How to start the demos ?

```bash
# Root directory
pnpm i

nx build modern-js-plugin

pnpm run app:component-data-fetch:dev

open http://localhost:5001/
```
1 change: 1 addition & 0 deletions apps/modern-component-data-fetch/host/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
strict-peer-dependencies=false
1 change: 1 addition & 0 deletions apps/modern-component-data-fetch/host/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# modernjs-ssr-nested-remote
12 changes: 12 additions & 0 deletions apps/modern-component-data-fetch/host/cypress.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
import { defineConfig } from 'cypress';

export default defineConfig({
projectId: 'sa6wfn',
e2e: nxE2EPreset(__filename, { cypressDir: 'cypress' }),
defaultCommandTimeout: 20000,
retries: {
runMode: 2,
openMode: 1,
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { getH2 } from '../support/app.po';

describe('csr with fetch data', () => {
it('[ /csr ] - should render in client side and fetch data from server', () => {
cy.visit('/csr');
cy.wait(3000);
cy.url().should('include', '/csr');
getH2().contains('[ csr provider - server ] fetched data');

const stub = cy.stub();
cy.on('window:alert', stub);

cy.get('#provider-csr-btn')
.should('be.visible')
.click()
.then(() => {
expect(stub.getCall(0)).to.be.calledWith(
'[provider-csr-btn] Client side Javascript works!',
);
});

// it only render in client side, so it not have downgrade identifier
cy.window().then((win) => {
expect(win.globalThis._mfSSRDowngrade).to.not.exist;
});
});
});
63 changes: 63 additions & 0 deletions apps/modern-component-data-fetch/host/cypress/e2e/downgrade.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { getH1, getH2, getH3 } from '../support/app.po';

declare global {
interface Window {
globalThis: {
_mfSSRDowngrade?: boolean | string[];
};
}
}

describe('downgrade', () => {
beforeEach(() => {
// the page will hydrate failed and downgrade, so catch the error, keep on testing
cy.on('uncaught:exception', (err, runnable) => {
return false;
});
});

it('[ /client-downgrade ] - should downgrade load data.client.js to fetch data', () => {
cy.visit('/client-downgrade');
cy.wait(3000);
cy.url().should('include', '/client-downgrade');
getH2().contains('fetch data from provider client');

const stub = cy.stub();
cy.on('window:alert', stub);

cy.get('#client-downgrade-btn')
.should('be.visible')
.click()
.then(() => {
expect(stub.getCall(0)).to.be.calledWith(
'[client-downgrade] Client side Javascript works!',
);
});
cy.window().then((win) => {
expect(win.globalThis._mfSSRDowngrade).to.exist;
});
});

it('[ /server-downgrade ] - should downgrade and fetch data from server', () => {
cy.visit('/server-downgrade');
cy.wait(3000);
cy.url().should('include', '/server-downgrade');
getH2().contains('[ provider - server - ServerDowngrade]');

const stub = cy.stub();
cy.on('window:alert', stub);

cy.get('#server-downgrade-btn')
.should('be.visible')
.click()
.then(() => {
expect(stub.getCall(0)).to.be.calledWith(
'[server-downgrade] Client side Javascript works!',
);
});
cy.window().then((win) => {
console.log(win);
expect(win.globalThis._mfSSRDowngrade).to.exist;
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { getH1, getH2, getH3 } from '../support/app.po';

declare global {
interface Window {
globalThis: {
_mfSSRDowngrade?: boolean | string[];
};
}
}

describe('/', () => {
beforeEach(() => cy.visit('/'));

describe('Welcome message', () => {
it('should display welcome message', () => {
getH1().contains('Welcome message from host');
});
});

// check that clicking back and forwards in client side routeing still renders the content correctly
describe('Routing checks', () => {
it('[ /basic ] - should render in client side and fetch data from server side', () => {
// wait nav hydration
cy.wait(3000);
cy.get('.basic').parent().click();
cy.url().should('include', '/basic');
getH2().contains('[ provider - client ] fetched data');

const stub = cy.stub();
cy.on('window:alert', stub);

cy.get('#provider-btn')
.should('be.visible')
.click()
.then(() => {
expect(stub.getCall(0)).to.be.calledWith(
'[provider] Client side Javascript works!',
);
});

cy.window().then((win) => {
expect(win.globalThis._mfSSRDowngrade).to.not.exist;
});
});

it('[ /client-downgrade ] - should render in client side and load data.client.js to fetch data', () => {
cy.wait(3000);
cy.get('.client-downgrade').parent().click();
cy.url().should('include', '/client-downgrade');
getH2().contains('fetch data from provider client');

const stub = cy.stub();
cy.on('window:alert', stub);

cy.get('#client-downgrade-btn')
.should('be.visible')
.click()
.then(() => {
expect(stub.getCall(0)).to.be.calledWith(
'[client-downgrade] Client side Javascript works!',
);
});
cy.window().then((win) => {
expect(win.globalThis._mfSSRDowngrade).to.not.exist;
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { getH1, getH2, getH3 } from '../support/app.po';

describe('ssr with fetch data', () => {
it('[ /basic ] - should render in server side and fetch data from server', () => {
cy.visit('/basic');
cy.wait(3000);
cy.url().should('include', '/basic');
getH2().contains('[ provider - server ] fetched data');

const stub = cy.stub();
cy.on('window:alert', stub);

cy.get('#provider-btn')
.should('be.visible')
.click()
.then(() => {
expect(stub.getCall(0)).to.be.calledWith(
'[provider] Client side Javascript works!',
);
});

cy.window().then((win) => {
expect(win.globalThis._mfSSRDowngrade).to.not.exist;
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const getH1 = () => cy.get('h1');
export const getH2 = () => cy.get('h2');
export const getH3 = () => cy.get('h3');
32 changes: 32 additions & 0 deletions apps/modern-component-data-fetch/host/cypress/support/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/// <reference types="cypress" />

// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************

// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
login(email: string, password: string): void;
}
}

// -- This is a parent command --
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
17 changes: 17 additions & 0 deletions apps/modern-component-data-fetch/host/cypress/support/e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************

// Import commands.ts using ES2015 syntax:
import './commands';
20 changes: 20 additions & 0 deletions apps/modern-component-data-fetch/host/cypress/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"allowJs": true,
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["cypress", "node"],
"sourceMap": false
},
"include": [
"**/*.ts",
"**/*.js",
"../cypress.config.ts",
"../**/*.cy.ts",
"../**/*.cy.tsx",
"../**/*.cy.js",
"../**/*.cy.jsx",
"../**/*.d.ts"
]
}
23 changes: 23 additions & 0 deletions apps/modern-component-data-fetch/host/modern.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { appTools, defineConfig } from '@modern-js/app-tools';
import { moduleFederationPlugin } from '@module-federation/modern-js';

// https://modernjs.dev/en/configure/app/usage
export default defineConfig({
runtime: {
router: true,
},
server: {
ssr: {
mode: 'stream',
},
port: 5001,
},
plugins: [
appTools({
bundler: 'rspack',
}),
moduleFederationPlugin({
fetchServerQuery: { extraQuery: true },
}),
],
});
12 changes: 12 additions & 0 deletions apps/modern-component-data-fetch/host/module-federation.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createModuleFederationConfig } from '@module-federation/modern-js';
export default createModuleFederationConfig({
name: 'host',
remotes: {
remote: 'provider@http://localhost:5002/mf-manifest.json',
'provider-csr': 'provider_csr@http://localhost:5003/mf-manifest.json',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
});
Loading