Skip to content
Draft
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
107 changes: 107 additions & 0 deletions packages/manifest/__tests__/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
describe('getTypesMetaInfo', () => {
const defaultTypesMetaInfo = {
path: '',
name: '',
zip: '',
api: '',
};

beforeEach(() => {
jest.resetModules();
});

it('does not load the DTS core when DTS is disabled', async () => {
const dtsCoreFactory = jest.fn(() => {
throw new Error('DTS core should not be loaded');
});

jest.doMock('@module-federation/dts-plugin/core', dtsCoreFactory, {
virtual: true,
});
jest.doMock(
'@module-federation/sdk',
() => ({
normalizeOptions: jest.fn(() => {
throw new Error('normalizeOptions should not be called');
}),
}),
{ virtual: true },
);
jest.doMock('../src/logger', () => ({
__esModule: true,
default: {
warn: jest.fn(),
},
}));

const { getTypesMetaInfo } = await import('../src/utils');

expect(getTypesMetaInfo({ dts: false }, process.cwd())).toEqual(
defaultTypesMetaInfo,
);
expect(dtsCoreFactory).not.toHaveBeenCalled();
});

it('loads the DTS core when DTS metadata is enabled', async () => {
const normalizeOptions = jest.fn(
(_enable: boolean, defaultOptions: Record<string, unknown> | boolean) =>
(options: unknown) => {
if (options === false) {
return false;
}

if (options && typeof options === 'object') {
return {
...(typeof defaultOptions === 'object' ? defaultOptions : {}),
...options,
};
}

return defaultOptions;
},
);
const isTSProject = jest.fn(() => true);
const retrieveTypesAssetsInfo = jest.fn(() => ({
apiFileName: 'api-types.d.ts',
zipName: 'types.zip',
}));

jest.doMock(
'@module-federation/dts-plugin/core',
() => ({
isTSProject,
retrieveTypesAssetsInfo,
}),
{ virtual: true },
);
jest.doMock(
'@module-federation/sdk',
() => ({
normalizeOptions,
}),
{ virtual: true },
);
jest.doMock('../src/logger', () => ({
__esModule: true,
default: {
warn: jest.fn(),
},
}));

const { getTypesMetaInfo } = await import('../src/utils');

expect(getTypesMetaInfo({ dts: true }, '/workspace/app')).toEqual({
path: '',
name: '',
zip: 'types.zip',
api: 'api-types.d.ts',
});
expect(isTSProject).toHaveBeenCalledWith(true, '/workspace/app');
expect(retrieveTypesAssetsInfo).toHaveBeenCalledWith(
expect.objectContaining({
context: '/workspace/app',
moduleFederationConfig: { dts: true },
}),
);
});
});
30 changes: 26 additions & 4 deletions packages/manifest/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
} from 'webpack/lib/stats/DefaultStatsFactoryPlugin';
import path from 'path';
import fs from 'fs';
import { createRequire } from 'node:module';
import {
StatsAssets,
moduleFederationPlugin,
Expand All @@ -14,13 +15,28 @@ import {
normalizeOptions,
MetaDataTypes,
} from '@module-federation/sdk';
import {
isTSProject,
retrieveTypesAssetsInfo,
} from '@module-federation/dts-plugin/core';
import { HOT_UPDATE_SUFFIX, PLUGIN_IDENTIFIER } from './constants';
import logger from './logger';

// Rslib rewrites import.meta.url for both CJS and ESM outputs.
// @ts-ignore
const nodeRequire = createRequire(import.meta.url);
type DtsCoreModule = {
isTSProject: (
dtsOptions: moduleFederationPlugin.ModuleFederationPluginOptions['dts'],
context: string,
) => boolean;
retrieveTypesAssetsInfo: (
options: moduleFederationPlugin.DtsRemoteOptions & {
context: string;
moduleFederationConfig: moduleFederationPlugin.ModuleFederationPluginOptions;
},
) => {
apiFileName: string;
zipName: string;
};
};

function isHotFile(file: string) {
return file.includes(HOT_UPDATE_SUFFIX);
}
Expand Down Expand Up @@ -238,7 +254,13 @@ export function getTypesMetaInfo(
zip: '',
api: '',
};
if (pluginOptions.dts === false) {
return defaultTypesMetaInfo;
}
try {
const { isTSProject, retrieveTypesAssetsInfo } = nodeRequire(
'@module-federation/dts-plugin/core',
) as DtsCoreModule;
const normalizedDtsOptions =
normalizeOptions<moduleFederationPlugin.PluginDtsOptions>(
isTSProject(pluginOptions.dts, context),
Expand Down
114 changes: 107 additions & 7 deletions packages/rspack/__tests__/ModuleFederationPlugin.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,90 @@
import {
resolveRspackRuntimeAlias,
resolveRspackRuntimeImplementation,
} from '../src/ModuleFederationPlugin';
function mockModuleFederationPluginDependencies(
dtsPluginFactory = () => ({
DtsPlugin: class {
apply = jest.fn();
addRuntimePlugins = jest.fn();
},
}),
) {
const noopLogger = {
debug: jest.fn(),
error: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
};

jest.doMock('@module-federation/dts-plugin', dtsPluginFactory, {
virtual: true,
});
jest.doMock(
'@module-federation/sdk',
() => ({
bindLoggerToCompiler: jest.fn(),
composeKeyWithSeparator: (...parts: string[]) => parts.join(':'),
createInfrastructureLogger: jest.fn(() => noopLogger),
createLogger: jest.fn(() => noopLogger),
moduleFederationPlugin: {},
}),
{ virtual: true },
);
jest.doMock(
'@module-federation/manifest',
() => ({
StatsPlugin: class {
apply = jest.fn();
},
}),
{ virtual: true },
);
jest.doMock(
'@module-federation/managers',
() => ({
ContainerManager: class {
init = jest.fn();

get enable() {
return false;
}

get containerPluginExposesOptions() {
return {};
}
},
utils: {
getBuildVersion: () => 'test-build',
},
}),
{ virtual: true },
);
jest.doMock(
'@module-federation/bridge-react-webpack-plugin',
() => ({
__esModule: true,
default: jest.fn(),
}),
{ virtual: true },
);
jest.doMock('../src/RemoteEntryPlugin', () => ({
RemoteEntryPlugin: class {
apply = jest.fn();
},
}));
}

async function importModuleFederationPlugin() {
mockModuleFederationPluginDependencies();

return import('../src/ModuleFederationPlugin');
}

describe('runtime resolution compatibility', () => {
it('prefers the bundler implementation when available', () => {
beforeEach(() => {
jest.resetModules();
});

it('prefers the bundler implementation when available', async () => {
const { resolveRspackRuntimeImplementation } =
await importModuleFederationPlugin();
const resolve = jest.fn((request: string) => {
if (request === '@module-federation/runtime-tools/bundler') {
return '/workspace/runtime-tools/dist/bundler.js';
Expand All @@ -18,7 +98,8 @@ describe('runtime resolution compatibility', () => {
);
});

it('falls back to legacy esm runtime entries for older implementations', () => {
it('falls back to legacy esm runtime entries for older implementations', async () => {
const { resolveRspackRuntimeAlias } = await importModuleFederationPlugin();
const resolve = jest.fn(
(request: string, options?: { paths?: string[] }) => {
const basedFromLegacy = options?.paths?.[0] === '/legacy/runtime-tools';
Expand All @@ -42,7 +123,8 @@ describe('runtime resolution compatibility', () => {
);
});

it('falls back to legacy cjs runtime entries when esm legacy builds are unavailable', () => {
it('falls back to legacy cjs runtime entries when esm legacy builds are unavailable', async () => {
const { resolveRspackRuntimeAlias } = await importModuleFederationPlugin();
const resolve = jest.fn(
(request: string, options?: { paths?: string[] }) => {
const basedFromLegacy = options?.paths?.[0] === '/legacy/runtime-tools';
Expand All @@ -67,3 +149,21 @@ describe('runtime resolution compatibility', () => {
);
});
});

describe('ModuleFederationPlugin', () => {
beforeEach(() => {
jest.resetModules();
});

it('does not load the DTS plugin when the plugin module is imported', async () => {
const dtsPluginFactory = jest.fn(() => {
throw new Error('DTS plugin should not be loaded');
});
mockModuleFederationPluginDependencies(dtsPluginFactory);

await expect(
import('../src/ModuleFederationPlugin'),
).resolves.toBeDefined();
expect(dtsPluginFactory).not.toHaveBeenCalled();
});
});
16 changes: 15 additions & 1 deletion packages/rspack/src/ModuleFederationPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import {

import { StatsPlugin } from '@module-federation/manifest';
import { ContainerManager, utils } from '@module-federation/managers';
import { DtsPlugin } from '@module-federation/dts-plugin';
import ReactBridgePlugin from '@module-federation/bridge-react-webpack-plugin';
import path from 'node:path';
import fs from 'node:fs';
import { createRequire } from 'node:module';
import { RemoteEntryPlugin } from './RemoteEntryPlugin';
import logger from './logger';

Expand All @@ -26,9 +26,20 @@ type NonUndefined<T = SplitChunks> = ExcludeFalse<T>;
type NonFalseSplitChunks = NonUndefined<SplitChunks>;
type CacheGroups = NonUndefined<NonFalseSplitChunks['cacheGroups']>;
type CacheGroup = CacheGroups[string];
type DtsPluginModule = {
DtsPlugin: new (
options: moduleFederationPlugin.ModuleFederationPluginOptions,
) => {
apply(compiler: Compiler): void;
addRuntimePlugins(): void;
};
};

declare const __VERSION__: string;
export const PLUGIN_NAME = 'RspackModuleFederationPlugin';
// Rslib rewrites import.meta.url for both CJS and ESM outputs.
// @ts-ignore
const nodeRequire = createRequire(import.meta.url);

type ResolveFn = typeof require.resolve;
type RuntimeEntrySpec = {
Expand Down Expand Up @@ -199,6 +210,9 @@ export class ModuleFederationPlugin implements RspackPluginInstance {
let disableDts = options.dts === false;

if (!disableDts) {
const { DtsPlugin } = nodeRequire(
'@module-federation/dts-plugin',
) as DtsPluginModule;
const dtsPlugin = new DtsPlugin(options);
// @ts-ignore
dtsPlugin.apply(compiler);
Expand Down
Loading