diff --git a/packages/.eslintrc.json b/packages/.eslintrc.json new file mode 100644 index 00000000000..f15b34b64f0 --- /dev/null +++ b/packages/.eslintrc.json @@ -0,0 +1,23 @@ +{ + "extends": ["../.eslintrc.json"], + "ignorePatterns": ["!**/*", "dist/**/*", "**/*.d.ts"], + "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/assemble-release-plan/.eslintrc.json b/packages/assemble-release-plan/.eslintrc.json new file mode 100644 index 00000000000..8b89f2d127e --- /dev/null +++ b/packages/assemble-release-plan/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["../.eslintrc.json"] +} diff --git a/packages/cli/.eslintrc.json b/packages/cli/.eslintrc.json new file mode 100644 index 00000000000..8b89f2d127e --- /dev/null +++ b/packages/cli/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["../.eslintrc.json"] +} diff --git a/packages/cli/src/utils/readConfig.ts b/packages/cli/src/utils/readConfig.ts index d352e2a124a..af189544aea 100644 --- a/packages/cli/src/utils/readConfig.ts +++ b/packages/cli/src/utils/readConfig.ts @@ -1,7 +1,6 @@ import path from 'path'; import type { moduleFederationPlugin } from '@module-federation/sdk'; - -const { createJiti } = require('jiti'); +import { createJiti } from 'jiti'; const DEFAULT_CONFIG_PATH = 'module-federation.config.ts'; export const getConfigPath = (userConfigPath?: string) => { @@ -16,7 +15,6 @@ export async function readConfig(userConfigPath?: string) { const configPath = getConfigPath(userConfigPath); const jit = createJiti(__filename, { interopDefault: true, - esmResolve: true, }); const configModule = await jit(configPath); const resolvedConfig = ( diff --git a/packages/create-module-federation/.eslintrc.json b/packages/create-module-federation/.eslintrc.json new file mode 100644 index 00000000000..8b89f2d127e --- /dev/null +++ b/packages/create-module-federation/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["../.eslintrc.json"] +} diff --git a/packages/data-prefetch/.eslintrc.json b/packages/data-prefetch/.eslintrc.json new file mode 100644 index 00000000000..8b89f2d127e --- /dev/null +++ b/packages/data-prefetch/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["../.eslintrc.json"] +} diff --git a/packages/data-prefetch/src/cli/index.ts b/packages/data-prefetch/src/cli/index.ts index b16f2fcee59..30b8f89ed42 100644 --- a/packages/data-prefetch/src/cli/index.ts +++ b/packages/data-prefetch/src/cli/index.ts @@ -17,6 +17,7 @@ import { fileExistsWithCaseSync, fixPrefetchPath } from '../common/node-utils'; import { getPrefetchId } from '../common/runtime-utils'; import { SHARED_STRATEGY } from '../constant'; +// eslint-disable-next-line @typescript-eslint/no-require-imports const { RuntimeGlobals, Template } = require( normalizeWebpackPath('webpack'), ) as typeof import('webpack'); diff --git a/packages/dts-plugin/.eslintrc.json b/packages/dts-plugin/.eslintrc.json new file mode 100644 index 00000000000..8b89f2d127e --- /dev/null +++ b/packages/dts-plugin/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["../.eslintrc.json"] +} diff --git a/packages/dts-plugin/src/core/lib/DtsWorker.spec.ts b/packages/dts-plugin/src/core/lib/DtsWorker.spec.ts index 82830aeb994..1b1f99edad5 100644 --- a/packages/dts-plugin/src/core/lib/DtsWorker.spec.ts +++ b/packages/dts-plugin/src/core/lib/DtsWorker.spec.ts @@ -49,6 +49,7 @@ describe('generateTypesInChildProcess', () => { it('generateTypesInChildProcess', async () => { // createRpcWorker will use dist assets , so it need to test dist const { DtsWorker } = + // eslint-disable-next-line @typescript-eslint/no-require-imports require('../../../dist/core') as typeof import('../index'); const dtsWorker = new DtsWorker({ host: hostOptions, @@ -273,6 +274,7 @@ describe('DtsWorker Unit Tests', () => { vi.spyOn(console, 'error').mockImplementation(() => {}); originalKill = process.kill; originalDebugMode = isDebugMode; + // eslint-disable-next-line @typescript-eslint/no-require-imports DtsWorkerClass = require('../../../dist/core').DtsWorker; // Reset isDebugMode for each test vi.mock('./utils', () => ({ diff --git a/packages/dts-plugin/src/core/lib/utils.ts b/packages/dts-plugin/src/core/lib/utils.ts index 5eb517e66b9..176d2cd3970 100644 --- a/packages/dts-plugin/src/core/lib/utils.ts +++ b/packages/dts-plugin/src/core/lib/utils.ts @@ -21,6 +21,7 @@ export function getDTSManagerConstructor( implementation?: string, ): typeof DTSManager { if (implementation) { + // eslint-disable-next-line @typescript-eslint/no-require-imports const NewConstructor = require(implementation); return NewConstructor.default ? NewConstructor.default : NewConstructor; } diff --git a/packages/dts-plugin/src/server/broker/Broker.ts b/packages/dts-plugin/src/server/broker/Broker.ts index a9de2cd9a8c..4fdca8ec15b 100644 --- a/packages/dts-plugin/src/server/broker/Broker.ts +++ b/packages/dts-plugin/src/server/broker/Broker.ts @@ -391,7 +391,6 @@ export class Broker { client, ); fileLog( - // eslint-disable-next-line @ies/eden/max-calls-in-template `[${ ActionKind.ADD_SUBSCRIBER }]: ${identifier} has been started, Adding Subscriber ${subscriberName} Succeed, this.__publisherMap are: ${JSON.stringify( @@ -414,7 +413,6 @@ export class Broker { }, ); fileLog( - // eslint-disable-next-line @ies/eden/max-calls-in-template `[${ActionKind.ADD_SUBSCRIBER}]: notifySubscriber Subscriber ${subscriberName}, updateMode: "PASSIVE", updateSourcePaths: ${registeredPublisher.name}`, 'Broker', 'info', diff --git a/packages/enhanced/src/lib/container/runtime/FederationRuntimePlugin.ts b/packages/enhanced/src/lib/container/runtime/FederationRuntimePlugin.ts index f83ca81f55b..3e6c498f573 100644 --- a/packages/enhanced/src/lib/container/runtime/FederationRuntimePlugin.ts +++ b/packages/enhanced/src/lib/container/runtime/FederationRuntimePlugin.ts @@ -34,23 +34,90 @@ const { mkdirpSync } = require( normalizeWebpackPath('webpack/lib/util/fs'), ) as typeof import('webpack/lib/util/fs'); -function resolveRuntimePaths(implementation?: string) { - const ext = process.env.IS_ESM_BUILD === 'true' ? '.js' : '.cjs'; - const runtimeToolsSpec = `@module-federation/runtime-tools/dist/index${ext}`; - const bundlerRuntimeSpec = `@module-federation/webpack-bundler-runtime/dist/index${ext}`; - const runtimeSpec = `@module-federation/runtime/dist/index${ext}`; +type ResolveFn = typeof require.resolve; +type RuntimeEntrySpec = { + bundler: string; + esm: string; + cjs: string; +}; + +function resolveRuntimeEntry( + spec: RuntimeEntrySpec, + implementation: string | undefined, + resolve: ResolveFn = require.resolve, +) { + const candidates = [spec.bundler, spec.esm, spec.cjs]; + const modulePaths = implementation ? [implementation] : undefined; + let lastError: unknown; + + for (const candidate of candidates) { + try { + return modulePaths + ? resolve(candidate, { paths: modulePaths }) + : resolve(candidate); + } catch (error) { + lastError = error; + } + } - const runtimeToolsPath = require.resolve(runtimeToolsSpec); - const modulePaths = implementation ? [implementation] : [runtimeToolsPath]; + throw lastError; +} + +function resolveRuntimeEntryWithFallback( + spec: RuntimeEntrySpec, + implementation: string | undefined, + resolve: ResolveFn = require.resolve, +) { + if (implementation) { + try { + return resolveRuntimeEntry(spec, implementation, resolve); + } catch { + // Fall back to the workspace runtime packages when a custom + // implementation hasn't published the newer subpath yet. + } + } + + return resolveRuntimeEntry(spec, undefined, resolve); +} + +export function resolveRuntimePaths( + implementation?: string, + resolve: ResolveFn = require.resolve, +) { + // Prefer the dedicated bundler subpath so webpack can tree-shake across the + // runtime package boundary. Fall back to the legacy dist contract for older + // custom implementations that have not published /bundler yet. + const runtimeToolsPath = resolveRuntimeEntryWithFallback( + { + bundler: '@module-federation/runtime-tools/bundler', + esm: '@module-federation/runtime-tools/dist/index.js', + cjs: '@module-federation/runtime-tools/dist/index.cjs', + }, + implementation, + resolve, + ); + const moduleBase = implementation || runtimeToolsPath; return { runtimeToolsPath, - bundlerRuntimePath: require.resolve(bundlerRuntimeSpec, { - paths: modulePaths, - }), - runtimePath: require.resolve(runtimeSpec, { - paths: modulePaths, - }), + bundlerRuntimePath: resolveRuntimeEntry( + { + bundler: '@module-federation/webpack-bundler-runtime/bundler', + esm: '@module-federation/webpack-bundler-runtime/dist/index.js', + cjs: '@module-federation/webpack-bundler-runtime/dist/index.cjs', + }, + moduleBase, + resolve, + ), + runtimePath: resolveRuntimeEntry( + { + bundler: '@module-federation/runtime/bundler', + esm: '@module-federation/runtime/dist/index.js', + cjs: '@module-federation/runtime/dist/index.cjs', + }, + moduleBase, + resolve, + ), }; } diff --git a/packages/enhanced/test/unit/container/FederationRuntimePlugin.test.ts b/packages/enhanced/test/unit/container/FederationRuntimePlugin.test.ts index 74069c11b74..17b4efb5a33 100644 --- a/packages/enhanced/test/unit/container/FederationRuntimePlugin.test.ts +++ b/packages/enhanced/test/unit/container/FederationRuntimePlugin.test.ts @@ -1,4 +1,6 @@ -import FederationRuntimePlugin from '../../../src/lib/container/runtime/FederationRuntimePlugin'; +import FederationRuntimePlugin, { + resolveRuntimePaths, +} from '../../../src/lib/container/runtime/FederationRuntimePlugin'; import type { Compiler } from 'webpack'; import { rs, Mock } from '@rstest/core'; @@ -216,7 +218,7 @@ describe('FederationRuntimePlugin runtimePluginCalls', () => { } }); - it('prefers cjs runtime entry when IS_ESM_BUILD is false', () => { + it('prefers the bundler runtime entry when IS_ESM_BUILD is false', () => { process.env.IS_ESM_BUILD = 'false'; const plugin = new FederationRuntimePlugin({ implementation: '/legacy/runtime-tools', @@ -226,11 +228,131 @@ describe('FederationRuntimePlugin runtimePluginCalls', () => { } as unknown as Compiler); expect(normalizePath(runtimePath)).toMatch( - /\/runtime\/dist\/index\.cjs(?:\.cjs)?$/, + /\/runtime\/dist\/bundler\.js$/, ); }); - it('prefers esm runtime entry when IS_ESM_BUILD is true', () => { + it('falls back to legacy esm runtime entries for older implementations', () => { + const resolve = rs.fn( + (request: string, options?: { paths?: string[] }) => { + const basedFromLegacy = + options?.paths?.[0] === '/legacy/runtime-tools'; + + if (request === '@module-federation/runtime-tools/bundler') { + return '/workspace/runtime-tools/dist/bundler.js'; + } + if (basedFromLegacy && request.endsWith('/bundler')) { + throw new Error(`Cannot find module '${request}'`); + } + if (request === '@module-federation/runtime/dist/index.js') { + return '/legacy/runtime/dist/index.js'; + } + if ( + request === + '@module-federation/webpack-bundler-runtime/dist/index.js' + ) { + return '/legacy/webpack-bundler-runtime/dist/index.js'; + } + + throw new Error(`Unexpected request: ${request}`); + }, + ); + + const resolved = resolveRuntimePaths('/legacy/runtime-tools', resolve); + + expect(normalizePath(resolved.runtimeToolsPath)).toBe( + '/workspace/runtime-tools/dist/bundler.js', + ); + expect(normalizePath(resolved.runtimePath)).toBe( + '/legacy/runtime/dist/index.js', + ); + expect(normalizePath(resolved.bundlerRuntimePath)).toBe( + '/legacy/webpack-bundler-runtime/dist/index.js', + ); + }); + + it('prefers the provided runtime-tools implementation when available', () => { + const resolve = rs.fn( + (request: string, options?: { paths?: string[] }) => { + const basedFromLegacy = + options?.paths?.[0] === '/legacy/runtime-tools'; + + if ( + basedFromLegacy && + request === '@module-federation/runtime-tools/bundler' + ) { + return '/legacy/runtime-tools/dist/bundler.js'; + } + if ( + basedFromLegacy && + request === '@module-federation/runtime/bundler' + ) { + return '/legacy/runtime/dist/bundler.js'; + } + if ( + basedFromLegacy && + request === '@module-federation/webpack-bundler-runtime/bundler' + ) { + return '/legacy/webpack-bundler-runtime/dist/bundler.js'; + } + + throw new Error(`Unexpected request: ${request}`); + }, + ); + + const resolved = resolveRuntimePaths('/legacy/runtime-tools', resolve); + + expect(normalizePath(resolved.runtimeToolsPath)).toBe( + '/legacy/runtime-tools/dist/bundler.js', + ); + expect(normalizePath(resolved.runtimePath)).toBe( + '/legacy/runtime/dist/bundler.js', + ); + expect(normalizePath(resolved.bundlerRuntimePath)).toBe( + '/legacy/webpack-bundler-runtime/dist/bundler.js', + ); + }); + + it('falls back to legacy cjs runtime entries when esm legacy builds are unavailable', () => { + const resolve = rs.fn( + (request: string, options?: { paths?: string[] }) => { + const basedFromLegacy = + options?.paths?.[0] === '/legacy/runtime-tools'; + + if (request === '@module-federation/runtime-tools/bundler') { + return '/workspace/runtime-tools/dist/bundler.js'; + } + if ( + basedFromLegacy && + (request.endsWith('/bundler') || request.endsWith('/dist/index.js')) + ) { + throw new Error(`Cannot find module '${request}'`); + } + if (request === '@module-federation/runtime/dist/index.cjs') { + return '/legacy/runtime/dist/index.cjs'; + } + if ( + request === + '@module-federation/webpack-bundler-runtime/dist/index.cjs' + ) { + return '/legacy/webpack-bundler-runtime/dist/index.cjs'; + } + + throw new Error(`Unexpected request: ${request}`); + }, + ); + + const resolved = resolveRuntimePaths('/legacy/runtime-tools', resolve); + + expect(normalizePath(resolved.runtimePath)).toBe( + '/legacy/runtime/dist/index.cjs', + ); + expect(normalizePath(resolved.bundlerRuntimePath)).toBe( + '/legacy/webpack-bundler-runtime/dist/index.cjs', + ); + }); + + it('prefers the bundler runtime entry when IS_ESM_BUILD is true', () => { process.env.IS_ESM_BUILD = 'true'; const plugin = new FederationRuntimePlugin({ implementation: '/legacy/runtime-tools', @@ -243,7 +365,7 @@ describe('FederationRuntimePlugin runtimePluginCalls', () => { } as unknown as Compiler); expect(normalizePath(runtimePath)).toMatch( - /\/runtime\/dist\/index\.(?:js|esm\.js)$/, + /\/runtime\/dist\/bundler\.js$/, ); }); @@ -267,7 +389,7 @@ describe('FederationRuntimePlugin runtimePluginCalls', () => { '@module-federation/runtime-tools$' ], ), - ).toMatch(/\/runtime-tools\/dist\/index\.cjs(?:\.cjs)?$/); + ).toMatch(/\/runtime-tools\/dist\/bundler\.js$/); }); it('resolves runtime-tools alias for ESM mode when runtime alias is preset', () => { @@ -290,7 +412,7 @@ describe('FederationRuntimePlugin runtimePluginCalls', () => { '@module-federation/runtime-tools$' ], ), - ).toMatch(/\/runtime-tools\/dist\/index\.(?:js|esm\.js)$/); + ).toMatch(/\/runtime-tools\/dist\/bundler\.js$/); }); }); }); diff --git a/packages/error-codes/.eslintrc.json b/packages/error-codes/.eslintrc.json new file mode 100644 index 00000000000..8b89f2d127e --- /dev/null +++ b/packages/error-codes/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["../.eslintrc.json"] +} diff --git a/packages/managers/.eslintrc.json b/packages/managers/.eslintrc.json new file mode 100644 index 00000000000..8b89f2d127e --- /dev/null +++ b/packages/managers/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["../.eslintrc.json"] +} diff --git a/packages/manifest/.eslintrc.json b/packages/manifest/.eslintrc.json new file mode 100644 index 00000000000..8b89f2d127e --- /dev/null +++ b/packages/manifest/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["../.eslintrc.json"] +} diff --git a/packages/manifest/__tests__/ModuleHandler.spec.ts b/packages/manifest/__tests__/ModuleHandler.spec.ts index 2a24c96d141..97568381ad9 100644 --- a/packages/manifest/__tests__/ModuleHandler.spec.ts +++ b/packages/manifest/__tests__/ModuleHandler.spec.ts @@ -95,7 +95,6 @@ jest.mock( ); import type { moduleFederationPlugin } from '@module-federation/sdk'; -// eslint-disable-next-line import/first import { ModuleHandler } from '../src/ModuleHandler'; describe('ModuleHandler', () => { diff --git a/packages/manifest/src/StatsManager.ts b/packages/manifest/src/StatsManager.ts index 7c8556068cf..8fc742ba12f 100644 --- a/packages/manifest/src/StatsManager.ts +++ b/packages/manifest/src/StatsManager.ts @@ -115,7 +115,7 @@ class StatsManager { private _getMetaData( compiler: Compiler, compilation: Compilation, - extraOptions?: {}, + extraOptions?: object, ): StatsMetaData { const { context } = compiler.options; const { @@ -363,7 +363,7 @@ class StatsManager { private async _generateStats( compiler: Compiler, compilation: Compilation, - extraOptions?: {}, + extraOptions?: object, ): Promise { try { const { diff --git a/packages/modernjs-v3/src/cli/configPlugin.ts b/packages/modernjs-v3/src/cli/configPlugin.ts index 0583cdbfadc..c90e483c5dc 100644 --- a/packages/modernjs-v3/src/cli/configPlugin.ts +++ b/packages/modernjs-v3/src/cli/configPlugin.ts @@ -106,7 +106,6 @@ export const getMFConfig = async ( const { createJiti } = require('jiti'); const jit = createJiti(__filename, { interopDefault: true, - esmResolve: true, }); const configModule = await jit(mfConfigPath); @@ -474,9 +473,8 @@ export const moduleFederationConfigPlugin = ( resolve: { alias: { // TODO: deprecated - '@modern-js/runtime/mf': require.resolve( - '@module-federation/modern-js-v3/runtime', - ), + '@modern-js/runtime/mf': + require.resolve('@module-federation/modern-js-v3/runtime'), }, }, source: { diff --git a/packages/modernjs/src/cli/configPlugin.ts b/packages/modernjs/src/cli/configPlugin.ts index 0d509072d08..446f53952bc 100644 --- a/packages/modernjs/src/cli/configPlugin.ts +++ b/packages/modernjs/src/cli/configPlugin.ts @@ -112,7 +112,6 @@ export const getMFConfig = async ( const { createJiti } = require('jiti'); const jit = createJiti(__filename, { interopDefault: true, - esmResolve: true, }); const configModule = await jit(mfConfigPath); @@ -484,9 +483,8 @@ export const moduleFederationConfigPlugin = ( resolve: { alias: { // TODO: deprecated - '@modern-js/runtime/mf': require.resolve( - '@module-federation/modern-js/runtime', - ), + '@modern-js/runtime/mf': + require.resolve('@module-federation/modern-js/runtime'), }, }, source: { diff --git a/packages/rsbuild-plugin/.eslintrc.json b/packages/rsbuild-plugin/.eslintrc.json new file mode 100644 index 00000000000..8b89f2d127e --- /dev/null +++ b/packages/rsbuild-plugin/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["../.eslintrc.json"] +} diff --git a/packages/rsbuild-plugin/src/cli/index.ts b/packages/rsbuild-plugin/src/cli/index.ts index 5b6184fa2da..e4cc7943e33 100644 --- a/packages/rsbuild-plugin/src/cli/index.ts +++ b/packages/rsbuild-plugin/src/cli/index.ts @@ -274,8 +274,14 @@ export const pluginModuleFederation = ( `'${SSR_ENV_NAME}' environment is already defined.Please use another name.`, ); } + const currentEnvironment = config.environments?.[environment]; + if (!currentEnvironment) { + throw new Error( + `Can not find environment '${environment}' when enabling SSR.`, + ); + } config.environments![SSR_ENV_NAME] = createSSRREnvConfig( - config.environments?.[environment]!, + currentEnvironment, moduleFederationOptions, ssrDir, config, diff --git a/packages/rspack/.eslintrc.json b/packages/rspack/.eslintrc.json new file mode 100644 index 00000000000..d31a730359d --- /dev/null +++ b/packages/rspack/.eslintrc.json @@ -0,0 +1,23 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*", "dist/**/*", "**/*.d.ts"], + "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/rspack/__tests__/ModuleFederationPlugin.spec.ts b/packages/rspack/__tests__/ModuleFederationPlugin.spec.ts new file mode 100644 index 00000000000..f8e9cfa355e --- /dev/null +++ b/packages/rspack/__tests__/ModuleFederationPlugin.spec.ts @@ -0,0 +1,69 @@ +import { + resolveRspackRuntimeAlias, + resolveRspackRuntimeImplementation, +} from '../src/ModuleFederationPlugin'; + +describe('runtime resolution compatibility', () => { + it('prefers the bundler implementation when available', () => { + const resolve = jest.fn((request: string) => { + if (request === '@module-federation/runtime-tools/bundler') { + return '/workspace/runtime-tools/dist/bundler.js'; + } + + throw new Error(`Unexpected request: ${request}`); + }) as typeof require.resolve; + + expect(resolveRspackRuntimeImplementation(undefined, resolve)).toBe( + '/workspace/runtime-tools/dist/bundler.js', + ); + }); + + it('falls back to legacy esm runtime entries for older implementations', () => { + const resolve = jest.fn( + (request: string, options?: { paths?: string[] }) => { + const basedFromLegacy = options?.paths?.[0] === '/legacy/runtime-tools'; + + if ( + basedFromLegacy && + request === '@module-federation/runtime/bundler' + ) { + throw new Error(`Cannot find module '${request}'`); + } + if (request === '@module-federation/runtime/dist/index.js') { + return '/legacy/runtime/dist/index.js'; + } + + throw new Error(`Unexpected request: ${request}`); + }, + ) as typeof require.resolve; + + expect(resolveRspackRuntimeAlias('/legacy/runtime-tools', resolve)).toBe( + '/legacy/runtime/dist/index.js', + ); + }); + + it('falls back to legacy cjs runtime entries when esm legacy builds are unavailable', () => { + const resolve = jest.fn( + (request: string, options?: { paths?: string[] }) => { + const basedFromLegacy = options?.paths?.[0] === '/legacy/runtime-tools'; + + if ( + basedFromLegacy && + (request === '@module-federation/runtime/bundler' || + request === '@module-federation/runtime/dist/index.js') + ) { + throw new Error(`Cannot find module '${request}'`); + } + if (request === '@module-federation/runtime/dist/index.cjs') { + return '/legacy/runtime/dist/index.cjs'; + } + + throw new Error(`Unexpected request: ${request}`); + }, + ) as typeof require.resolve; + + expect(resolveRspackRuntimeAlias('/legacy/runtime-tools', resolve)).toBe( + '/legacy/runtime/dist/index.cjs', + ); + }); +}); diff --git a/packages/rspack/src/ModuleFederationPlugin.ts b/packages/rspack/src/ModuleFederationPlugin.ts index 37f12dac7fc..f19e036b5f7 100644 --- a/packages/rspack/src/ModuleFederationPlugin.ts +++ b/packages/rspack/src/ModuleFederationPlugin.ts @@ -28,15 +28,67 @@ type CacheGroups = NonUndefined; type CacheGroup = CacheGroups[string]; declare const __VERSION__: string; -declare global { - namespace NodeJS { - interface ProcessEnv { - IS_ESM_BUILD?: string; +export const PLUGIN_NAME = 'RspackModuleFederationPlugin'; + +type ResolveFn = typeof require.resolve; +type RuntimeEntrySpec = { + bundler: string; + esm: string; + cjs: string; +}; + +function resolveRuntimeEntry( + spec: RuntimeEntrySpec, + implementation: string | undefined, + resolve: ResolveFn = require.resolve, +) { + const candidates = [spec.bundler, spec.esm, spec.cjs]; + const modulePaths = implementation ? [implementation] : undefined; + let lastError: unknown; + + for (const candidate of candidates) { + try { + return modulePaths + ? resolve(candidate, { paths: modulePaths }) + : resolve(candidate); + } catch (error) { + lastError = error; } } + + throw lastError; +} + +export function resolveRspackRuntimeImplementation( + implementation?: string, + resolve: ResolveFn = require.resolve, +) { + return resolveRuntimeEntry( + { + bundler: '@module-federation/runtime-tools/bundler', + esm: '@module-federation/runtime-tools/dist/index.js', + cjs: '@module-federation/runtime-tools/dist/index.cjs', + }, + implementation, + resolve, + ); +} + +export function resolveRspackRuntimeAlias( + implementation: string, + resolve: ResolveFn = require.resolve, +) { + return resolveRuntimeEntry( + { + bundler: '@module-federation/runtime/bundler', + esm: '@module-federation/runtime/dist/index.js', + cjs: '@module-federation/runtime/dist/index.cjs', + }, + implementation, + resolve, + ); } -export const PLUGIN_NAME = 'RspackModuleFederationPlugin'; export class ModuleFederationPlugin implements RspackPluginInstance { readonly name = PLUGIN_NAME; private _options: moduleFederationPlugin.ModuleFederationPluginOptions; @@ -128,9 +180,7 @@ export class ModuleFederationPlugin implements RspackPluginInstance { const runtimePlugins = options.runtimePlugins || []; options.runtimePlugins = runtimePlugins.concat( - require.resolve( - '@module-federation/inject-external-runtime-core-plugin', - ), + require.resolve('@module-federation/inject-external-runtime-core-plugin'), ); } @@ -141,12 +191,9 @@ export class ModuleFederationPlugin implements RspackPluginInstance { }).apply(compiler); } - const runtimeToolsSpecifier = - process.env.IS_ESM_BUILD === 'true' - ? '@module-federation/runtime-tools/dist/index.js' - : '@module-federation/runtime-tools/dist/index.cjs'; - const implementationPath = - options.implementation || require.resolve(runtimeToolsSpecifier); + const implementationPath = options.implementation + ? options.implementation + : resolveRspackRuntimeImplementation(); options.implementation = implementationPath; let disableManifest = options.manifest === false; let disableDts = options.dts === false; @@ -173,19 +220,13 @@ export class ModuleFederationPlugin implements RspackPluginInstance { options as unknown as ModuleFederationPluginOptions, ).apply(compiler); - const runtimeEntrySpecifier = - process.env.IS_ESM_BUILD === 'true' - ? '@module-federation/runtime/dist/index.js' - : '@module-federation/runtime/dist/index.cjs'; let runtimePath: string; try { - runtimePath = require.resolve(runtimeEntrySpecifier, { - paths: [implementationPath], - }); + runtimePath = resolveRspackRuntimeAlias(implementationPath); } catch (err) { const detail = err instanceof Error ? err.message : String(err); throw new Error( - `[ ModuleFederationPlugin ]: Unable to resolve runtime entry at ${runtimeEntrySpecifier} (paths: [${implementationPath}]): ${detail}`, + `[ ModuleFederationPlugin ]: Unable to resolve runtime entry (paths: [${implementationPath}]): ${detail}`, ); } diff --git a/packages/runtime-tools/package.json b/packages/runtime-tools/package.json index 6fed77ac936..9fefed09662 100644 --- a/packages/runtime-tools/package.json +++ b/packages/runtime-tools/package.json @@ -60,6 +60,7 @@ "default": "./dist/webpack-bundler-runtime.cjs" } }, + "./bundler": "./dist/bundler.js", "./*": "./*" }, "typesVersions": { @@ -75,6 +76,9 @@ ], "runtime-core": [ "./dist/runtime-core.d.ts" + ], + "bundler": [ + "./dist/bundler.d.ts" ] } }, diff --git a/packages/runtime-tools/src/bundler.ts b/packages/runtime-tools/src/bundler.ts new file mode 100644 index 00000000000..c218ac14272 --- /dev/null +++ b/packages/runtime-tools/src/bundler.ts @@ -0,0 +1,2 @@ +export { default } from './index'; +export * from './index'; diff --git a/packages/runtime-tools/tsdown.config.ts b/packages/runtime-tools/tsdown.config.ts index 9d6afbce829..f5311b7ee3e 100644 --- a/packages/runtime-tools/tsdown.config.ts +++ b/packages/runtime-tools/tsdown.config.ts @@ -15,6 +15,7 @@ export default defineConfig([ runtime: 'src/runtime.ts', 'runtime-core': 'src/runtime-core.ts', 'webpack-bundler-runtime': 'src/webpack-bundler-runtime.ts', + bundler: 'src/bundler.ts', }, external: ['@module-federation/*'], dts: { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 641a7c6361d..e72daba102f 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -63,6 +63,7 @@ "default": "./dist/core.cjs" } }, + "./bundler": "./dist/bundler.js", "./*": "./*" }, "typesVersions": { @@ -78,6 +79,9 @@ ], "core": [ "./dist/core.d.ts" + ], + "bundler": [ + "./dist/bundler.d.ts" ] } }, diff --git a/packages/runtime/src/bundler.ts b/packages/runtime/src/bundler.ts new file mode 100644 index 00000000000..ea465c2a34a --- /dev/null +++ b/packages/runtime/src/bundler.ts @@ -0,0 +1 @@ +export * from './index'; diff --git a/packages/runtime/tsdown.config.ts b/packages/runtime/tsdown.config.ts index 535b5740f91..1e5fea6ec61 100644 --- a/packages/runtime/tsdown.config.ts +++ b/packages/runtime/tsdown.config.ts @@ -30,6 +30,7 @@ const buildConfig = { helpers: 'src/helpers.ts', types: 'src/types.ts', core: 'src/core.ts', + bundler: 'src/bundler.ts', }, external: ['@module-federation/*'], dts: { diff --git a/packages/sdk/.eslintrc.json b/packages/sdk/.eslintrc.json new file mode 100644 index 00000000000..8b89f2d127e --- /dev/null +++ b/packages/sdk/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["../.eslintrc.json"] +} diff --git a/packages/treeshake-server/.eslintrc.json b/packages/treeshake-server/.eslintrc.json new file mode 100644 index 00000000000..8b89f2d127e --- /dev/null +++ b/packages/treeshake-server/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["../.eslintrc.json"] +} diff --git a/packages/webpack-bundler-runtime/.eslintrc.json b/packages/webpack-bundler-runtime/.eslintrc.json new file mode 100644 index 00000000000..4e4f19169ed --- /dev/null +++ b/packages/webpack-bundler-runtime/.eslintrc.json @@ -0,0 +1,17 @@ +{ + "extends": ["../.eslintrc.json"], + "overrides": [ + { + "files": ["src/initContainerEntry.ts", "src/initializeSharing.ts"], + "rules": { + "no-var": "off" + } + }, + { + "files": ["__tests__/**/*.ts"], + "rules": { + "@typescript-eslint/no-require-imports": "off" + } + } + ] +} diff --git a/packages/webpack-bundler-runtime/package.json b/packages/webpack-bundler-runtime/package.json index 8f8ebba2a66..dd693105901 100644 --- a/packages/webpack-bundler-runtime/package.json +++ b/packages/webpack-bundler-runtime/package.json @@ -50,6 +50,7 @@ "default": "./dist/constant.cjs" } }, + "./bundler": "./dist/bundler.js", "./*": "./*" }, "typesVersions": { @@ -59,6 +60,9 @@ ], "constant": [ "./dist/constant.d.ts" + ], + "bundler": [ + "./dist/bundler.d.ts" ] } }, diff --git a/packages/webpack-bundler-runtime/src/bundler.ts b/packages/webpack-bundler-runtime/src/bundler.ts new file mode 100644 index 00000000000..c218ac14272 --- /dev/null +++ b/packages/webpack-bundler-runtime/src/bundler.ts @@ -0,0 +1,2 @@ +export { default } from './index'; +export * from './index'; diff --git a/packages/webpack-bundler-runtime/tsdown.config.ts b/packages/webpack-bundler-runtime/tsdown.config.ts index fb9b36f5f1b..04ea06f2b8b 100644 --- a/packages/webpack-bundler-runtime/tsdown.config.ts +++ b/packages/webpack-bundler-runtime/tsdown.config.ts @@ -14,6 +14,7 @@ export default defineConfig([ entry: { index: 'src/index.ts', constant: 'src/constant.ts', + bundler: 'src/bundler.ts', }, external: ['@module-federation/*', 'webpack'], dts: { diff --git a/scripts/bundle-size-report.mjs b/scripts/bundle-size-report.mjs index dc75de32537..068a6790ceb 100755 --- a/scripts/bundle-size-report.mjs +++ b/scripts/bundle-size-report.mjs @@ -15,8 +15,9 @@ import { statSync, existsSync, mkdirSync, + rmSync, } from 'fs'; -import { join, resolve, relative, extname } from 'path'; +import { dirname, join, resolve, relative, extname } from 'path'; import { tmpdir } from 'os'; import { gzipSync } from 'zlib'; @@ -81,6 +82,7 @@ const ASSET_RULES = [ ]; const JS_EXTENSIONS = new Set(['.js', '.mjs', '.cjs']); +const EXPORT_CONDITION_SKIP_KEYS = new Set(['types', 'require', 'node']); async function loadRslib() { if (!rslibPromise) { @@ -119,39 +121,90 @@ function readPackageJson(pkgDir) { } } -/** Detect the main ESM entry file from package.json */ -function findEsmEntry(pkgDir, pkg) { - if (!pkg) return null; +function resolvePackageEntry(pkgDir, entry) { + if (typeof entry !== 'string') return null; + const resolved = join(pkgDir, entry); + if (!existsSync(resolved)) return null; + if (!JS_EXTENSIONS.has(extname(resolved))) return null; + return resolved; +} + +function resolveExportImportPath(target) { + if (!target) return null; + if (typeof target === 'string') return target; + + if (Array.isArray(target)) { + for (const item of target) { + const resolved = resolveExportImportPath(item); + if (resolved) return resolved; + } + return null; + } + + if (typeof target !== 'object') { + return null; + } + + for (const key of ['module', 'import', 'browser', 'default']) { + if (!(key in target)) continue; + const resolved = resolveExportImportPath(target[key]); + if (resolved) return resolved; + } + + for (const [key, value] of Object.entries(target)) { + if (EXPORT_CONDITION_SKIP_KEYS.has(key)) continue; + const resolved = resolveExportImportPath(value); + if (resolved) return resolved; + } + + return null; +} + +function addEsmEntry(entries, pkgDir, entry) { + const resolved = resolvePackageEntry(pkgDir, entry); + if (resolved) { + entries.add(resolved); + } +} + +/** Detect explicit public ESM entry files from package.json */ +function findEsmEntries(pkgDir, pkg) { + if (!pkg) return []; + + const entries = new Set(); - // Try module field first if (pkg.module) { - const resolved = join(pkgDir, pkg.module); - if (existsSync(resolved)) return resolved; + addEsmEntry(entries, pkgDir, pkg.module); } - // Try exports["."].import - if (pkg.exports && pkg.exports['.']) { - const dot = pkg.exports['.']; - const importPath = typeof dot === 'string' ? dot : dot.import; - if (importPath) { - const entry = - typeof importPath === 'string' - ? importPath - : importPath.default || importPath; - if (typeof entry === 'string') { - const resolved = join(pkgDir, entry); - if (existsSync(resolved)) return resolved; + if (pkg.exports) { + if (typeof pkg.exports === 'string' || Array.isArray(pkg.exports)) { + addEsmEntry(entries, pkgDir, resolveExportImportPath(pkg.exports)); + } else if (typeof pkg.exports === 'object') { + const exportEntries = Object.entries(pkg.exports); + const hasSubpaths = exportEntries.some(([key]) => key.startsWith('.')); + + if (!hasSubpaths) { + addEsmEntry(entries, pkgDir, resolveExportImportPath(pkg.exports)); + } else { + for (const [subpath, target] of exportEntries) { + if ( + subpath !== '.' && + (!subpath.startsWith('./') || subpath.includes('*')) + ) { + continue; + } + addEsmEntry(entries, pkgDir, resolveExportImportPath(target)); + } } } } - // Fall back to main - if (pkg.main) { - const resolved = join(pkgDir, pkg.main); - if (existsSync(resolved)) return resolved; + if (!entries.size && pkg.main) { + addEsmEntry(entries, pkgDir, pkg.main); } - return null; + return [...entries]; } function createTempDir(pkgName, target) { @@ -205,6 +258,45 @@ function gzipSize(filePath) { return gzipSync(content, { level: 9 }).length; } +function sumGzipSize(filePaths) { + return filePaths.reduce((sum, filePath) => sum + gzipSize(filePath), 0); +} + +function toImportSpecifier(fromDir, targetPath) { + let specifier = relative(fromDir, targetPath).replace(/\\/g, '/'); + if (!specifier.startsWith('.')) { + specifier = `./${specifier}`; + } + return specifier; +} + +function createAggregateEntry(entryPaths, options) { + const stamp = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + const safePackageName = (options.packageName || 'pkg').replace( + /[\\/]/g, + '__', + ); + const dir = join( + ROOT, + '.bundle-size-tmp', + safePackageName, + options.target, + stamp, + ); + mkdirSync(dir, { recursive: true }); + const entryPath = join(dir, 'aggregate-entry.mjs'); + const lines = entryPaths.flatMap((targetPath, index) => { + const importPath = toImportSpecifier(dir, targetPath); + return [ + `import * as entry${index} from ${JSON.stringify(importPath)};`, + `export const __bundle_size_entry_${index} = entry${index};`, + ]; + }); + + writeFileSync(entryPath, `${lines.join('\n')}\n`, 'utf8'); + return entryPath; +} + async function bundleEntry(entryPath, options) { if (!entryPath || !existsSync(entryPath)) { return { bytes: null, gzip: null, error: 'entry not found' }; @@ -278,6 +370,26 @@ async function bundleEntry(entryPath, options) { } } +async function bundleEntries(entryPaths, options) { + if (!entryPaths.length) { + return { bytes: null, gzip: null, error: 'entry not found' }; + } + + if (entryPaths.length === 1) { + return bundleEntry(entryPaths[0], options); + } + + const aggregateEntry = createAggregateEntry(entryPaths, options); + try { + return await bundleEntry(aggregateEntry, options); + } finally { + const aggregateDir = dirname(aggregateEntry); + if (existsSync(aggregateDir)) { + rmSync(aggregateDir, { recursive: true, force: true }); + } + } +} + // ── Discovery ──────────────────────────────────────────────────────────────── /** Find package directories from workspace package manifests */ @@ -332,21 +444,21 @@ async function measure(packagesDir) { const pkgJson = readPackageJson(pkg.dir); const distDir = join(pkg.dir, 'dist'); const totalSize = dirSize(distDir); - const esmEntry = findEsmEntry(pkg.dir, pkgJson); - const esmGzip = gzipSize(esmEntry); + const esmEntries = findEsmEntries(pkg.dir, pkgJson); + const esmGzip = sumGzipSize(esmEntries); let webBundle = { bytes: null, gzip: null }; let nodeBundle = { bytes: null, gzip: null }; const bundleErrors = {}; - if (esmEntry) { + if (esmEntries.length) { const [webResult, nodeResult] = await Promise.all([ - bundleEntry(esmEntry, { + bundleEntries(esmEntries, { target: 'web', packageName: pkg.name, entryName: 'bundle', define: { ENV_TARGET: JSON.stringify('web') }, }), - bundleEntry(esmEntry, { + bundleEntries(esmEntries, { target: 'node', packageName: pkg.name, entryName: 'bundle', @@ -364,12 +476,16 @@ async function measure(packagesDir) { results[pkg.name] = { totalDist: totalSize, esmGzip, - esmEntry: esmEntry ? relative(pkg.dir, esmEntry) : null, + esmEntry: esmEntries[0] ? relative(pkg.dir, esmEntries[0]) : null, + esmEntries: esmEntries.map((entryPath) => relative(pkg.dir, entryPath)), webBundleBytes: webBundle.bytes, webBundleGzip: webBundle.gzip, nodeBundleBytes: nodeBundle.bytes, nodeBundleGzip: nodeBundle.gzip, - bundleEntry: esmEntry ? relative(pkg.dir, esmEntry) : null, + bundleEntry: esmEntries[0] ? relative(pkg.dir, esmEntries[0]) : null, + bundleEntries: esmEntries.map((entryPath) => + relative(pkg.dir, entryPath), + ), bundleErrors: Object.keys(bundleErrors).length ? bundleErrors : null, }; } @@ -395,7 +511,7 @@ function compare(baseData, currentData) { const distMetrics = [ { key: 'totalDist', label: 'Total dist (raw)' }, - { key: 'esmGzip', label: 'ESM gzip' }, + { key: 'esmGzip', label: 'Public ESM gzip' }, ]; const bundleMetrics = [ @@ -485,7 +601,7 @@ function compare(baseData, currentData) { lines.push(''); } - lines.push(...buildTable('Package dist + ESM entry', distMetrics)); + lines.push(...buildTable('Package dist + public ESM exports', distMetrics)); lines.push(...buildTable('Bundle targets', bundleMetrics)); lines.push( @@ -502,7 +618,7 @@ function compare(baseData, currentData) { ); lines.push(''); lines.push( - '_Bundle sizes are generated with rslib (Rspack). Web/node bundles set ENV_TARGET and enable tree-shaking. Bare imports are externalized to keep sizes consistent with prior reporting, and assets are emitted as resources._', + '_Bundle sizes are generated with rslib (Rspack). Public ESM gzip aggregates explicit package export entry files (wildcard exports are ignored). Web/node bundles synthesize a single entry that imports those public ESM exports, set ENV_TARGET, and enable tree-shaking. Bare imports are externalized to keep sizes consistent with prior reporting, and assets are emitted as resources._', ); lines.push(''); @@ -577,8 +693,11 @@ async function main() { const bundleErrorNote = data.bundleErrors ? ` (bundle errors: ${Object.keys(data.bundleErrors).join(', ')})` : ''; + const entryNote = data.esmEntries?.length + ? `, esm-entries=${data.esmEntries.length}` + : ''; console.log( - ` ${name}: dist=${formatBytes(data.totalDist)}, esm-gzip=${formatBytes(data.esmGzip)}, web-gzip=${formatMaybe(data.webBundleGzip)}, node-gzip=${formatMaybe(data.nodeBundleGzip)}${bundleErrorNote}`, + ` ${name}: dist=${formatBytes(data.totalDist)}, esm-gzip=${formatBytes(data.esmGzip)}, web-gzip=${formatMaybe(data.webBundleGzip)}, node-gzip=${formatMaybe(data.nodeBundleGzip)}${entryNote}${bundleErrorNote}`, ); } console.log(