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
45 changes: 45 additions & 0 deletions .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Integration Test

on:
pull_request:
branches:
- main

permissions:
pull-requests: read

jobs:
integration-test:
name: Integration (vite ${{ matrix.vite-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# This plugin does not yet correctly support version above 7.1.7
vite-version: ['5', '6', '7.1.7']
steps:
- name: Checkout
uses: actions/checkout@v5

- name: PNPM Install
uses: pnpm/action-setup@v4
with:
version: 10.28.2

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24
cache: 'pnpm'
registry-url: https://registry.npmjs.org/

- run: corepack enable

- name: Install NPM Dependencies
run: pnpm install --frozen-lockfile

- name: Override vite version
run: pnpm add -Dw vite@${{ matrix.vite-version }}

- name: Run Integration Tests
run: pnpm test:integration
46 changes: 46 additions & 0 deletions integration/build.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { resolve } from 'path';
import { describe, expect, it } from 'vitest';
import { buildFixture, FIXTURES } from './helpers/build';
import { findAsset, findChunk, getAllChunkCode, getChunkNames } from './helpers/matchers';

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

describe('build', () => {
describe('remote', () => {
it('produces a remoteEntry chunk', async () => {
const output = await buildFixture({ mfOptions: BASIC_REMOTE_MF_OPTIONS });
const chunks = getChunkNames(output);
expect(chunks.some((name) => name.includes('remoteEntry'))).toBe(true);
});

it('remoteEntry contains federation runtime init with correct name', async () => {
const output = await buildFixture({ mfOptions: BASIC_REMOTE_MF_OPTIONS });
const remoteEntry = findChunk(output, 'remoteEntry');
expect(remoteEntry).toBeDefined();
expect(remoteEntry!.code).toContain('basicRemote');
expect(remoteEntry!.code).toContain('moduleCache');
});

it('exposed module content is included in output', async () => {
const output = await buildFixture({ mfOptions: BASIC_REMOTE_MF_OPTIONS });
const allCode = getAllChunkCode(output);
expect(allCode).toContain('Hello');
});

it('generates mf-manifest.json when manifest is enabled', async () => {
const manifestOutput = await buildFixture({
mfOptions: { ...BASIC_REMOTE_MF_OPTIONS, manifest: true },
});

const manifest = findAsset(manifestOutput, 'mf-manifest.json');
expect(manifest).toBeDefined();

const parsed = JSON.parse(manifest!.source as string);
expect(parsed).toHaveProperty('exposes');
});
});
});
73 changes: 73 additions & 0 deletions integration/css-manifest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { resolve } from 'path';
import { describe, expect, it } from 'vitest';
import type { ModuleFederationOptions } from '../src/utils/normalizeModuleFederationOptions';
import { buildFixture, FIXTURES } from './helpers/build';
import { parseManifest } from './helpers/matchers';

const CSS_BASE_MF_OPTIONS = {
name: 'cssRemote',
filename: 'remoteEntry.js',
exposes: {
'./widget': resolve(FIXTURES, 'css-remote', 'exposed-module.js'),
},
manifest: true,
dts: false,
} satisfies Partial<ModuleFederationOptions>;

interface ManifestExpose {
id: string;
name: string;
path: string;
assets: {
js: { sync: string[]; async: string[] };
css: { sync: string[]; async: string[] };
};
}

describe('css manifest', () => {
it('tracks CSS and JS assets under the correct expose', async () => {
const output = await buildFixture({
fixture: 'css-remote',
mfOptions: CSS_BASE_MF_OPTIONS,
});
const manifest = parseManifest(output) as Record<string, unknown>;
expect(manifest).toBeDefined();
expect(manifest).toHaveProperty('exposes');

const exposes = manifest.exposes as ManifestExpose[];
const widget = exposes.find((e) => e.name === 'widget');
expect(widget).toBeDefined();

const allCssFiles = [...widget!.assets.css.sync, ...widget!.assets.css.async];
expect(allCssFiles.length).toBeGreaterThanOrEqual(1);
for (const file of allCssFiles) {
expect(file).toMatch(/\.css$/);
}

const allJsFiles = [...widget!.assets.js.sync, ...widget!.assets.js.async];
expect(allJsFiles.length).toBeGreaterThanOrEqual(1);
for (const file of allJsFiles) {
expect(file).toMatch(/\.js$/);
}
});

it('adds CSS to all exposes when bundleAllCSS is enabled', async () => {
const output = await buildFixture({
fixture: 'css-remote',
mfOptions: { ...CSS_BASE_MF_OPTIONS, bundleAllCSS: true },
});
const manifest = parseManifest(output) as Record<string, unknown>;
expect(manifest).toBeDefined();

const exposes = manifest.exposes as ManifestExpose[];
expect(exposes.length).toBeGreaterThanOrEqual(1);

for (const expose of exposes) {
const cssCount = expose.assets.css.sync.length + expose.assets.css.async.length;
expect(
cssCount,
`expose "${expose.name}" should have at least one CSS asset`
).toBeGreaterThanOrEqual(1);
}
});
});
2 changes: 2 additions & 0 deletions integration/fixtures/basic-host/entry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const mod = import('remote1/Module');
mod.then((m) => console.log(m));
8 changes: 8 additions & 0 deletions integration/fixtures/basic-host/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!doctype html>
<html>
<head></head>
<body>
<div id="app"></div>
<script type="module" src="./entry.js"></script>
</body>
</html>
2 changes: 2 additions & 0 deletions integration/fixtures/basic-remote/entry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Minimal app entry — just needs to exist for vite to build
console.log('hello world');
3 changes: 3 additions & 0 deletions integration/fixtures/basic-remote/exposed-module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function greet(name) {
return `Hello, ${name}!`;
}
7 changes: 7 additions & 0 deletions integration/fixtures/basic-remote/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<!doctype html>
<html>
<body>
<div id="app"></div>
<script type="module" src="./entry.js"></script>
</body>
</html>
1 change: 1 addition & 0 deletions integration/fixtures/css-remote/entry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('css remote entry');
5 changes: 5 additions & 0 deletions integration/fixtures/css-remote/exposed-module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import './styles.css';

export function Widget() {
return '<div class="widget">Widget</div>';
}
7 changes: 7 additions & 0 deletions integration/fixtures/css-remote/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<!doctype html>
<html>
<body>
<div id="app"></div>
<script type="module" src="./entry.js"></script>
</body>
</html>
4 changes: 4 additions & 0 deletions integration/fixtures/css-remote/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.widget {
color: red;
padding: 8px;
}
1 change: 1 addition & 0 deletions integration/fixtures/shared-remote/entry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('shared remote entry');
5 changes: 5 additions & 0 deletions integration/fixtures/shared-remote/exposed-module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import defu from 'defu';

export function merge(a, b) {
return defu(a, b);
}
7 changes: 7 additions & 0 deletions integration/fixtures/shared-remote/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<!doctype html>
<html>
<body>
<div id="app"></div>
<script type="module" src="./entry.js"></script>
</body>
</html>
7 changes: 7 additions & 0 deletions integration/helpers/assertions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { OutputAsset, OutputChunk, RollupOutput } from 'rollup';

export const isRollupChunk = (output: RollupOutput['output'][number]): output is OutputChunk =>
output.type === 'chunk';

export const isRollupAsset = (output: RollupOutput['output'][number]): output is OutputAsset =>
output.type === 'asset';
54 changes: 54 additions & 0 deletions integration/helpers/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import defu from 'defu';
import { resolve } from 'path';
import { build, Rollup, UserConfig as ViteUserConfig } from 'vite';
import { expect } from 'vitest';
import { federation } from '../../src/index';
import type { ModuleFederationOptions } from '../../src/utils/normalizeModuleFederationOptions';

export const FIXTURES = resolve(__dirname, '../fixtures');

export interface BuildFixtureOptions {
/**
* @default 'basic-remote'
*/
fixture?: string;
mfOptions?: Partial<ModuleFederationOptions>;
viteConfig?: Partial<ViteUserConfig>;
}

export async function buildFixture(opts?: BuildFixtureOptions): Promise<Rollup.RollupOutput> {
const { fixture = 'basic-remote', mfOptions, viteConfig } = opts ?? {};

const defaultMfOptions = {
name: 'basicRemote',
filename: 'remoteEntry.js',
exposes: {},
shared: {},
dts: false,
} satisfies Parameters<typeof federation>[0];

// defu(overrides, defaults) — first arg wins for any key it provides
const mergedMfOptions = defu(mfOptions, defaultMfOptions);

const defaultViteConfig: ViteUserConfig = {
root: resolve(FIXTURES, fixture),
logLevel: 'silent',
build: {
write: false,
minify: false,
target: 'chrome89',
},
};

const mergedViteConfig = defu(viteConfig, defaultViteConfig);

const result = await build({
...mergedViteConfig,
plugins: [federation(mergedMfOptions)],
});

// Vite returns RollupOutput[] only with multiple rollupOptions.output entries.
// Our test configs should never produce that — fail fast if they do.
expect(Array.isArray(result), 'Expected a single RollupOutput, not an array').toBe(false);
return result as Rollup.RollupOutput;
}
39 changes: 39 additions & 0 deletions integration/helpers/matchers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { Rollup } from 'vite';
import { isRollupAsset, isRollupChunk } from './assertions';

export function getChunkNames(output: Rollup.RollupOutput) {
return output.output.filter(isRollupChunk).map((c) => c.fileName);
}

export function findChunk(
output: Rollup.RollupOutput,
test: string | RegExp
): Rollup.OutputChunk | undefined {
return output.output
.filter(isRollupChunk)
.find((o) => (typeof test === 'string' ? o.fileName.includes(test) : test.test(o.fileName)));
}

export function findAsset(
output: Rollup.RollupOutput,
test: string
): Rollup.OutputAsset | undefined {
return output.output.filter(isRollupAsset).find((o) => o.fileName.includes(test));
}

export function getAllChunkCode(output: Rollup.RollupOutput): string {
return output.output
.filter(isRollupChunk)
.map((c) => c.code)
.join('\n');
}

export function getHtmlAsset(output: Rollup.RollupOutput): Rollup.OutputAsset | undefined {
return output.output.filter(isRollupAsset).find((o) => o.fileName.endsWith('.html'));
}

export function parseManifest(output: Rollup.RollupOutput): object | undefined {
const asset = findAsset(output, 'mf-manifest.json');
if (!asset) return undefined;
return JSON.parse(asset.source as string);
}
Loading