Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/loose-donuts-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@module-federation/vite': minor
---

feat: add target option to configuration for better tree-shaking
5 changes: 1 addition & 4 deletions .github/workflows/e2e-test.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: multi-example - e2e tests
name: multi-example e2e tests

on:
push:
Expand Down Expand Up @@ -34,9 +34,6 @@ jobs:
- name: Build Projects
run: pnpm build

- name: Start Application multi-example
run: nohup pnpm run multi-example & pnpm exec wait-on http://localhost:5173;

- name: Run Playwright Tests
run: pnpm playwright test

Expand Down
434 changes: 434 additions & 0 deletions docs/architecture/entry-injection.md

Large diffs are not rendered by default.

320 changes: 320 additions & 0 deletions docs/architecture/overview.md

Large diffs are not rendered by default.

420 changes: 420 additions & 0 deletions docs/architecture/remote-module-loading.md

Large diffs are not rendered by default.

575 changes: 575 additions & 0 deletions docs/architecture/shared-dependency-resolution.md

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions e2e/vite-vite/tests/host-preview.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { expect, test } from '@playwright/test';

/**
* These tests run against the host preview (port 5175) which loads remote
* modules from the remote preview (port 5176). Both must be running.
*
* This exercises the full Module Federation pipeline in build mode:
* shared deps, default imports, named imports, CJS interop, etc.
*/
test.describe('vite-vite host preview', () => {
test('renders host app with React shared dep', async ({ page }) => {
await page.goto('/');
const heading = page.getByRole('heading', { name: 'MF HOST Demo', exact: true });
await expect(heading).toBeVisible();
});

test('renders Emotion styled component from remote', async ({ page }) => {
await page.goto('/');
// EmotionDemo uses `import styled from '@emotion/styled'` (default import).
// This breaks if the ESM shims plugin doesn't handle default export interop.
const emotionText = page.getByText('Heading with a green background and yellow text.');
await expect(emotionText).toBeVisible();
});

test('renders Styled Components demo from remote', async ({ page }) => {
await page.goto('/');
const heading = page.getByRole('heading', {
name: 'Styled Components Demo',
exact: true,
});
await expect(heading).toBeVisible();
});
});
9 changes: 9 additions & 0 deletions e2e/vite-vite/tests/remote-preview.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { expect, test } from '@playwright/test';

test.describe('vite-vite remote preview', () => {
test('renders the remote app', async ({ page }) => {
await page.goto('/');
const heading = page.getByRole('heading', { name: 'Vite + React', exact: true });
await expect(heading).toBeVisible();
});
});
2 changes: 1 addition & 1 deletion examples/vite-vite/vite-host/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"scripts": {
"dev": "vite --force",
"build": "vite build",
"preview": "vite build && vite preview"
"preview": "vite build && vite serve"
},
"dependencies": {
"@emotion/react": "^11.13.0",
Expand Down
2 changes: 1 addition & 1 deletion examples/vite-vite/vite-host/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import topLevelAwait from 'vite-plugin-top-level-await';
// https://vitejs.dev/config/
export default defineConfig({
server: {
open: true,
open: false,
port: 5175,
},
preview: {
Expand Down
2 changes: 1 addition & 1 deletion examples/vite-vite/vite-remote/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite build&&vite preview"
"preview": "vite build && vite serve"
},
"dependencies": {
"@emotion/react": "^11.13.0",
Expand Down
2 changes: 1 addition & 1 deletion examples/vite-vite/vite-remote/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import topLevelAwait from 'vite-plugin-top-level-await';
// https://vitejs.dev/config/
export default defineConfig({
server: {
open: true,
open: false,
port: 5176,
origin: 'http://localhost:5176',
},
Expand Down
52 changes: 52 additions & 0 deletions integration/esm-virtual-modules.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { resolve } from 'path';
import { describe, expect, it } from 'vitest';
import { buildFixture, FIXTURES } from './helpers/build';
import { getAllChunkCode } from './helpers/matchers';

const SHARED_REMOTE_MF_OPTIONS = {
shared: { defu: {} },
exposes: {
'./exposed': resolve(FIXTURES, 'shared-remote', 'exposed-module.js'),
},
};

const CJS_SHARED_MF_OPTIONS = {
shared: { 'cjs-dep': {} },
exposes: {
'./exposed': resolve(FIXTURES, 'shared-remote', 'exposed-cjs-module.js'),
},
};

describe('ESM virtual modules', () => {
it('resolves named imports from shared modules in build mode', async () => {
const output = await buildFixture({
fixture: 'shared-remote',
mfOptions: SHARED_REMOTE_MF_OPTIONS,
});
const allCode = getAllChunkCode(output);
// createDefu is a named export from defu — it should be present in the output
expect(allCode).toContain('createDefu');
});

it('emits ESM import/export in build output for shared modules', async () => {
const output = await buildFixture({
fixture: 'shared-remote',
mfOptions: SHARED_REMOTE_MF_OPTIONS,
});
const allCode = getAllChunkCode(output);
// Build output should not contain CJS require() for the runtime init module
expect(allCode).not.toMatch(/require\s*\(\s*["'].*runtimeInit/);
});

it('builds successfully when a shared dependency is CJS', async () => {
const output = await buildFixture({
fixture: 'shared-remote',
mfOptions: CJS_SHARED_MF_OPTIONS,
viteConfig: { resolve: { preserveSymlinks: true } },
});
const allCode = getAllChunkCode(output);
// The CJS dep is replaced by a loadShare shim — verify the build succeeded
// and the shared module was properly resolved via the runtime
expect(allCode).toContain('loadShare("cjs-dep"');
});
});
6 changes: 6 additions & 0 deletions integration/fixtures/shared-remote/exposed-cjs-module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Imports from the CJS sub-path — mirrors `import { createRoot } from 'react-dom/client'`
// cjs-dep/client.js internally require()s 'cjs-dep', which is a shared dep.
import { greeting, add } from 'cjs-dep/client';

export const message = greeting;
export const sum = add(1, 2);
11 changes: 7 additions & 4 deletions integration/fixtures/shared-remote/exposed-module.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import defu from 'defu';
import { createDefu } from 'defu';

export function merge(a, b) {
return defu(a, b);
}
export const merge = createDefu((obj, key, value) => {
if (typeof obj[key] === 'number' && typeof value === 'number') {
obj[key] += value;
return true;
}
});
6 changes: 6 additions & 0 deletions integration/fixtures/shared-remote/vendor/cjs-dep/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// CJS sub-path that require()s the main package — mirrors react-dom/client.js
// When 'cjs-dep' is shared, MF resolves require('cjs-dep') to an ESM virtual
// module. The commonjs plugin then creates a \0-prefixed proxy, triggering the bug.
var m = require('cjs-dep');
module.exports.greeting = m.greeting;
module.exports.add = m.add;
5 changes: 5 additions & 0 deletions integration/fixtures/shared-remote/vendor/cjs-dep/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// CJS module — triggers @rollup/plugin-commonjs proxy during build
module.exports.greeting = 'hello from cjs';
module.exports.add = function add(a, b) {
return a + b;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "cjs-dep",
"version": "1.0.0",
"main": "index.js"
}
31 changes: 31 additions & 0 deletions integration/target-code-elimination.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { resolve } from 'path';
import { describe, expect, it } from 'vitest';
import { buildFixture, FIXTURES } from './helpers/build';
import { getAllChunkCode } from './helpers/matchers';

const BASIC_REMOTE_MF_OPTIONS = {
exposes: {
'./exposed': resolve(FIXTURES, 'basic-remote', 'exposed-module.js'),
},
};

describe('target-specific code elimination', () => {
describe('target: web (default)', () => {
it('excludes eval() from browser builds', async () => {
const output = await buildFixture({ mfOptions: BASIC_REMOTE_MF_OPTIONS });
const allCode = getAllChunkCode(output);
expect(allCode).not.toMatch(/\beval\s*\(/);
});
});

describe('target: node', () => {
it('preserves Node.js script loading implementation', async () => {
const output = await buildFixture({
mfOptions: { ...BASIC_REMOTE_MF_OPTIONS, target: 'node' },
});
const allCode = getAllChunkCode(output);
// Node builds keep eval() for script loading
expect(allCode).toMatch(/\beval\s*\(/);
});
});
});
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"preview-rv": "pnpm -filter 'examples-rust-vite*' run preview",
"dev-vv": "pnpm -filter 'examples-vite-vite*' run dev",
"dev-nv": "pnpm -filter 'examples-nuxt-vite-host' -filter 'examples-vite-vite-remote' run dev",
"preview-vv": "pnpm -filter 'examples-vite-vite*' run preview",
"preview-vv": "pnpm -filter 'examples-vite-vite*' --parallel run preview",
"multi-example": "pnpm --filter \"multi-example-*\" --parallel run start",
"test": "vitest run src",
"test:integration": "vitest run integration",
Expand Down Expand Up @@ -83,6 +83,6 @@
"tsdown": "^0.20.3",
"vite": "^5.4.3",
"vitest": "^2.1.1",
"wait-on": "^8.0.1"
"cjs-dep": "workspace:*"
}
}
32 changes: 32 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ export default defineConfig({
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
forbidOnly: Boolean(process.env.CI),
webServer: [
{
command: 'pnpm run multi-example',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
{
command: 'pnpm run preview-vv',
url: 'http://localhost:5176/testbase',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
],
use: {
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
Expand All @@ -20,6 +34,24 @@ export default defineConfig({
browserName: 'chromium',
},
},
{
name: 'vite-vite-remote',
testDir: 'e2e/vite-vite/tests',
testMatch: 'remote-preview.spec.ts',
use: {
baseURL: 'http://localhost:5176/testbase',
browserName: 'chromium',
},
},
{
name: 'vite-vite-host',
testDir: 'e2e/vite-vite/tests',
testMatch: 'host-preview.spec.ts',
use: {
baseURL: 'http://localhost:5175',
browserName: 'chromium',
},
},
],
outputDir: 'reports/e2e/output',
reporter: [
Expand Down
Loading