diff --git a/.changeset/six-yaks-teach.md b/.changeset/six-yaks-teach.md new file mode 100644 index 0000000000..9faee34875 --- /dev/null +++ b/.changeset/six-yaks-teach.md @@ -0,0 +1,5 @@ +--- +'@module-federation/rspack': patch +--- + +fix(rspack): import plugin from sub path diff --git a/.changeset/twelve-dingos-ring.md b/.changeset/twelve-dingos-ring.md new file mode 100644 index 0000000000..4202f800a2 --- /dev/null +++ b/.changeset/twelve-dingos-ring.md @@ -0,0 +1,5 @@ +--- +'@module-federation/dts-plugin': patch +--- + +Lazy emit DTS files on hmr rebuilds, do not block compiler pipeline diff --git a/.cursorignore b/.cursorignore index df6eeb993f..4a2b9586d0 100644 --- a/.cursorignore +++ b/.cursorignore @@ -2,12 +2,15 @@ ./tmp ./scripts ./.git -./packages/storybook-addon -./packages/core -./packages/utilities -./packages/typescript -./packages/native-* -./apps +packages/storybook-addon +packages/core +packages/utilities +packages/typescript +packages/native-* +apps **/configCases +**/dist apps/** *.snap +*.js + diff --git a/apps/manifest-demo/3010-rspack-provider/rspack.config.js b/apps/manifest-demo/3010-rspack-provider/rspack.config.js index 00828a45fb..a6aa815f2d 100644 --- a/apps/manifest-demo/3010-rspack-provider/rspack.config.js +++ b/apps/manifest-demo/3010-rspack-provider/rspack.config.js @@ -42,6 +42,7 @@ module.exports = composePlugins( transform: { react: { runtime: 'automatic', + refresh: true, }, }, }, @@ -60,6 +61,18 @@ module.exports = composePlugins( // publicPath must be specific url config.output.publicPath = 'http://localhost:3010/'; + const rspackPlugin = config.plugins.find((plugin) => { + return plugin.name === 'HtmlRspackPlugin'; + }); + + if (rspackPlugin && rspackPlugin._args && rspackPlugin._args[0]) { + rspackPlugin._args[0].excludeChunks = ['rspack_provider']; + } else { + console.warn( + 'HtmlRspackPlugin not found or has unexpected structure. Skipping excludeChunks configuration.', + ); + } + config.plugins.push( new ModuleFederationPlugin({ name: 'rspack_provider', @@ -70,10 +83,10 @@ module.exports = composePlugins( shared: { lodash: {}, antd: {}, - 'react/': { - singleton: true, - requiredVersion: '^18.3.1', - }, + // 'react/': { + // singleton: true, + // requiredVersion: '^18.3.1', + // }, react: { singleton: true, requiredVersion: '^18.3.1', diff --git a/apps/react-ts-host/webpack.config.js b/apps/react-ts-host/webpack.config.js index bea8c49833..d94979294d 100644 --- a/apps/react-ts-host/webpack.config.js +++ b/apps/react-ts-host/webpack.config.js @@ -12,6 +12,8 @@ module.exports = composePlugins( withNx(), withReact(), async (config, context) => { + config.devServer = config.devServer || {}; + config.devServer.host = '127.0.0.1'; // prevent cyclic updates config.watchOptions = { ignored: ['**/node_modules/**', '**/@mf-types/**'], diff --git a/apps/react-ts-nested-remote/webpack.config.js b/apps/react-ts-nested-remote/webpack.config.js index 9602c9ddeb..111c7cf458 100644 --- a/apps/react-ts-nested-remote/webpack.config.js +++ b/apps/react-ts-nested-remote/webpack.config.js @@ -11,6 +11,10 @@ module.exports = composePlugins( withNx(), withReact(), async (config, context) => { + if (!config.devServer) { + config.devServer = {}; + } + config.devServer.host = '127.0.0.1'; config.output.publicPath = 'http://localhost:3005/'; // prevent cyclic updates config.watchOptions = { diff --git a/apps/react-ts-remote/rspack.config.js b/apps/react-ts-remote/rspack.config.js index 3045d124ec..446733ae96 100644 --- a/apps/react-ts-remote/rspack.config.js +++ b/apps/react-ts-remote/rspack.config.js @@ -88,6 +88,7 @@ module.exports = composePlugins( client: { overlay: false, }, + host: '127.0.0.1', port: 3004, devMiddleware: { writeToDisk: true, diff --git a/apps/react-ts-remote/webpack.config.js b/apps/react-ts-remote/webpack.config.js index 929a76487e..d0c8977209 100644 --- a/apps/react-ts-remote/webpack.config.js +++ b/apps/react-ts-remote/webpack.config.js @@ -12,6 +12,10 @@ module.exports = composePlugins( withNx(), withReact(), async (config, context) => { + if (!config.devServer) { + config.devServer = {}; + } + config.devServer.host = '127.0.0.1'; const baseConfig = { name: 'react_ts_remote', filename: 'remoteEntry.js', diff --git a/package.json b/package.json index e7a4eab325..d3d66e1c2e 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "prepare": "husky install", "changeset": "changeset", "build:packages": "npx nx affected -t build --parallel=10 --exclude='*,!tag:type:pkg'", - "changegen": "./changeset-gen.js --path ./packages/enhanced --staged &&./changeset-gen.js --path ./packages/node --staged && ./changeset-gen.js --path ./packages/runtime --staged && ./changeset-gen.js --path ./packages/data-prefetch --staged && ./changeset-gen.js --path ./packages/nextjs-mf --staged", + "changegen": "./changeset-gen.js --path ./packages/enhanced --staged && ./changeset-gen.js --path ./packages/node --staged && ./changeset-gen.js --path ./packages/runtime --staged && ./changeset-gen.js --path ./packages/data-prefetch --staged && ./changeset-gen.js --path ./packages/nextjs-mf --staged && ./changeset-gen.js --path ./packages/dts-plugin --staged", "commitgen:staged": "./commit-gen.js --path ./packages --staged", "commitgen:main": "./commit-gen.js --path ./packages", "changeset:status": "changeset status" diff --git a/packages/dts-plugin/src/core/lib/DTSManager.general.spec.ts b/packages/dts-plugin/src/core/lib/DTSManager.general.spec.ts new file mode 100644 index 0000000000..b74e51ae76 --- /dev/null +++ b/packages/dts-plugin/src/core/lib/DTSManager.general.spec.ts @@ -0,0 +1,457 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest'; +import { DTSManager } from './DTSManager'; +import axios from 'axios'; +import fs from 'fs'; +import path from 'path'; +import { UpdateMode } from '../../server/constant'; +import { ThirdPartyExtractor } from '@module-federation/third-party-dts-extractor'; +import { HostOptions, RemoteInfo } from '../interfaces/HostOptions'; +import { RemoteOptions } from '../interfaces/RemoteOptions'; +import { downloadTypesArchive } from './archiveHandler'; +import { + retrieveHostConfig, + retrieveRemoteInfo, +} from '../configurations/hostPlugin'; + +vi.mock('axios'); +vi.mock('fs/promises'); +vi.mock('fs'); +vi.mock('./archiveHandler'); +vi.mock('@module-federation/third-party-dts-extractor', () => ({ + ThirdPartyExtractor: vi.fn().mockImplementation(() => ({ + collectTypeImports: vi.fn().mockReturnValue([]), + })), +})); + +const projectRoot = path.join(__dirname, '../../..'); + +vi.mock('../configurations/hostPlugin', () => ({ + retrieveHostConfig: vi.fn().mockImplementation((options) => ({ + hostOptions: { + ...options, + context: projectRoot, + typesFolder: '@mf-types', + remoteTypesFolder: '@mf-types/remotes', + deleteTypesFolder: false, + implementation: 'webpack', + abortOnError: false, + consumeAPITypes: true, + maxRetries: 3, + runtimePkgs: [], + }, + mapRemotesToDownload: { + remote1: { + name: 'remote1', + url: 'http://example.com/remote1', + alias: 'remote1', + zipUrl: 'http://example.com/types.zip', + apiTypeUrl: 'http://example.com/api.d.ts', + }, + }, + })), + retrieveRemoteInfo: vi.fn().mockImplementation(({ remoteAlias, remote }) => ({ + name: remoteAlias, + url: remote, + alias: remoteAlias, + })), +})); + +describe('DTSManager General Tests', () => { + let dtsManager: DTSManager; + + beforeEach(() => { + vi.clearAllMocks(); + const remoteOptions: RemoteOptions = { + moduleFederationConfig: { + name: 'testRemote', + filename: 'remoteEntry.js', + exposes: { + './Component': './src/Component.tsx', + './utils': './src/utils.ts', + }, + shared: { + react: { singleton: true, eager: true }, + 'react-dom': { singleton: true, eager: true }, + }, + }, + typesFolder: '@mf-types', + context: projectRoot, + tsConfigPath: path.join(projectRoot, 'tsconfig.json'), + compiledTypesFolder: 'compiled-types', + deleteTypesFolder: false, + additionalFilesToCompile: [], + generateAPITypes: true, + extractRemoteTypes: true, + extractThirdParty: true, + implementation: 'webpack', + abortOnError: false, + }; + dtsManager = new DTSManager({ remote: remoteOptions }); + + // Add mock implementations + vi.spyOn(dtsManager, 'consumeArchiveTypes').mockResolvedValue(undefined); + vi.spyOn(dtsManager, 'consumeAPITypes').mockResolvedValue(undefined); + + // Add mock for fs.writeFileSync + vi.spyOn(fs, 'writeFileSync').mockImplementation(() => undefined); + vi.spyOn(fs, 'readFileSync').mockReturnValue(` + import type { PackageType as PackageType_0, RemoteKeys as RemoteKeys_0 } from './existing/apis.d.ts'; + `); + }); + + describe('generateAPITypes', () => { + it('should generate correct API types for multiple exposed components', () => { + const exposeMap = { + './Component': './src/Component.tsx', + './utils': './src/utils.ts', + }; + + const result = dtsManager.generateAPITypes(exposeMap); + expect(result).toContain("'REMOTE_ALIAS_IDENTIFIER/Component'"); + expect(result).toContain("'REMOTE_ALIAS_IDENTIFIER/utils'"); + expect(result).toContain('type PackageType'); + }); + + it('should handle empty expose map', () => { + const result = dtsManager.generateAPITypes({}); + expect(result).toContain('export type RemoteKeys ='); + expect(result).toContain('type PackageType'); + }); + }); + + describe('requestRemoteManifest', () => { + it('should handle non-manifest URLs correctly', async () => { + const remoteInfo: RemoteInfo = { + name: 'test', + url: 'http://example.com/remote', + alias: 'test-alias', + }; + + const result = await dtsManager.requestRemoteManifest(remoteInfo); + expect(result).toEqual(remoteInfo); + }); + + it('should handle manifest URLs with auto publicPath', async () => { + const manifestResponse = { + data: { + metaData: { + types: { + zip: 'types.zip', + api: 'api.d.ts', + }, + publicPath: 'auto', + }, + }, + }; + + vi.mocked(axios.get).mockResolvedValueOnce(manifestResponse); + + const remoteInfo: RemoteInfo = { + name: 'test', + url: 'http://example.com/remote.manifest.json', + alias: 'test-alias', + }; + + const result = await dtsManager.requestRemoteManifest(remoteInfo); + expect(result.zipUrl).toBeDefined(); + expect(result.apiTypeUrl).toBeDefined(); + expect(result.zipUrl).toContain('http://example.com/types.zip'); + }); + + it('should handle manifest URLs without API types', async () => { + const manifestResponse = { + data: { + metaData: { + types: { + zip: 'types.zip', + }, + publicPath: 'http://example.com', + }, + }, + }; + + vi.mocked(axios.get).mockResolvedValueOnce(manifestResponse); + + const remoteInfo: RemoteInfo = { + name: 'test', + url: 'http://example.com/remote.manifest.json', + alias: 'test-alias', + }; + + const result = await dtsManager.requestRemoteManifest(remoteInfo); + expect(result.zipUrl).toBeDefined(); + expect(result.apiTypeUrl).toBe(''); + }); + + it('should handle manifest fetch errors', async () => { + vi.mocked(axios.get).mockRejectedValueOnce(new Error('Network error')); + + const remoteInfo: RemoteInfo = { + name: 'test', + url: 'http://example.com/remote.manifest.json', + alias: 'test-alias', + }; + + const result = await dtsManager.requestRemoteManifest(remoteInfo); + expect(result).toEqual(remoteInfo); + }); + + it('should handle manifest with getPublicPath function', async () => { + const manifestResponse = { + data: { + metaData: { + types: { + zip: 'types.zip', + api: 'api.d.ts', + }, + publicPath: 'http://example.com/custom/', + }, + }, + }; + + vi.mocked(axios.get).mockResolvedValueOnce(manifestResponse); + + const remoteInfo: RemoteInfo = { + name: 'test', + url: 'http://example.com/remote.manifest.json', + alias: 'test-alias', + }; + + const result = await dtsManager.requestRemoteManifest(remoteInfo); + expect(result.zipUrl).toContain('http://example.com/custom/types.zip'); + expect(result.apiTypeUrl).toContain('http://example.com/custom/api.d.ts'); + }); + }); + + describe('consumeTargetRemotes', () => { + const baseHostOptions: Required = { + context: projectRoot, + typesFolder: '@mf-types', + runtimePkgs: [], + moduleFederationConfig: { + name: 'host', + filename: 'remoteEntry.js', + remotes: {}, + }, + remoteTypesFolder: '@mf-types/remotes', + deleteTypesFolder: false, + implementation: 'webpack', + abortOnError: false, + consumeAPITypes: true, + maxRetries: 3, + }; + + it('should successfully download types archive', async () => { + const remoteInfo: Required = { + name: 'test', + url: 'http://example.com/remote', + alias: 'test-alias', + zipUrl: 'http://example.com/types.zip', + apiTypeUrl: 'http://example.com/api.d.ts', + }; + + const mockDownloader = vi + .fn() + .mockResolvedValue(['test-alias', '/tmp/types']); + vi.mocked(downloadTypesArchive).mockReturnValue(mockDownloader); + + const result = await dtsManager.consumeTargetRemotes( + baseHostOptions, + remoteInfo, + ); + + expect(result).toEqual(['test-alias', '/tmp/types']); + expect(mockDownloader).toHaveBeenCalledWith([ + 'test-alias', + 'http://example.com/types.zip', + ]); + }); + + it('should throw error when zipUrl is missing', async () => { + const remoteInfo: Required = { + name: 'test', + url: 'http://example.com/remote', + alias: 'test-alias', + zipUrl: '', + apiTypeUrl: '', + }; + + await expect( + dtsManager.consumeTargetRemotes(baseHostOptions, remoteInfo), + ).rejects.toThrow("Can not get test's types archive url!"); + }); + }); + + describe('updateTypes', () => { + it('should handle positive update mode for host', async () => { + const generateTypesSpy = vi.spyOn(dtsManager, 'generateTypes'); + dtsManager.options.host = { + moduleFederationConfig: { + name: 'testRemote', + }, + }; + dtsManager.options.remote = { + moduleFederationConfig: { + name: 'testRemote', + filename: 'remoteEntry.js', + exposes: { + './Component': './src/Component.tsx', + }, + }, + typesFolder: '@mf-types', + context: projectRoot, + tsConfigPath: path.join(projectRoot, 'tsconfig.json'), + }; + + await dtsManager.updateTypes({ + remoteName: 'testRemote', + updateMode: UpdateMode.POSITIVE, + once: true, + }); + + expect(generateTypesSpy).toHaveBeenCalled(); + }); + + it('should handle missing remote options', async () => { + dtsManager = new DTSManager({}); + + await dtsManager.updateTypes({ + remoteName: 'testRemote', + updateMode: UpdateMode.POSITIVE, + once: true, + }); + + // Should not throw and handle gracefully + expect(true).toBe(true); + }); + }); + + describe('downloadAPITypes', () => { + it('should download and save API types with correct alias replacement', async () => { + const remoteInfo: Required = { + name: 'test', + url: 'http://example.com/remote', + alias: 'test-alias', + zipUrl: 'http://example.com/types.zip', + apiTypeUrl: 'http://example.com/api.d.ts', + }; + + const apiTypeContent = ` + export type RemoteKeys = 'REMOTE_ALIAS_IDENTIFIER/Component'; + type PackageType = T extends 'REMOTE_ALIAS_IDENTIFIER/Component' ? typeof import('REMOTE_ALIAS_IDENTIFIER/Component') : any; + `; + + vi.mocked(axios.get).mockResolvedValueOnce({ data: apiTypeContent }); + vi.spyOn(fs, 'writeFileSync'); + + await dtsManager.downloadAPITypes(remoteInfo, '/tmp/types'); + + expect(fs.writeFileSync).toHaveBeenCalled(); + const writeCall = vi.mocked(fs.writeFileSync).mock.calls[0]; + const content = writeCall[1] as string; + + expect(content).toContain('test-alias/Component'); + expect(content).not.toContain('REMOTE_ALIAS_IDENTIFIER'); + expect(content).toContain( + "type PackageType = T extends 'test-alias/Component'", + ); + expect(dtsManager.loadedRemoteAPIAlias.has('test-alias')).toBe(true); + }); + + it('should handle missing apiTypeUrl', async () => { + const remoteInfo: Required = { + name: 'test', + url: 'http://example.com/remote', + alias: 'test-alias', + zipUrl: 'http://example.com/types.zip', + apiTypeUrl: '', + }; + + vi.spyOn(fs, 'writeFileSync'); + + await dtsManager.downloadAPITypes(remoteInfo, '/tmp/types'); + + expect(fs.writeFileSync).not.toHaveBeenCalled(); + expect(dtsManager.loadedRemoteAPIAlias.has('test-alias')).toBe(false); + }); + + it('should handle download errors', async () => { + const remoteInfo: Required = { + name: 'test', + url: 'http://example.com/remote', + alias: 'test-alias', + zipUrl: 'http://example.com/types.zip', + apiTypeUrl: 'http://example.com/api.d.ts', + }; + + vi.mocked(axios.get).mockRejectedValueOnce(new Error('Network error')); + vi.spyOn(fs, 'writeFileSync'); + + await dtsManager.downloadAPITypes(remoteInfo, '/tmp/types'); + + expect(fs.writeFileSync).not.toHaveBeenCalled(); + expect(dtsManager.loadedRemoteAPIAlias.has('test-alias')).toBe(false); + }); + }); + + describe('consumeAPITypes', () => { + const baseHostOptions: Required = { + context: projectRoot, + typesFolder: '@mf-types', + runtimePkgs: ['@custom/runtime'], + moduleFederationConfig: { + name: 'host', + filename: 'remoteEntry.js', + remotes: { + remote1: 'remote1@http://example.com/remote1', + remote2: 'remote2@http://example.com/remote2', + }, + }, + remoteTypesFolder: '@mf-types/remotes', + deleteTypesFolder: false, + implementation: 'webpack', + abortOnError: false, + consumeAPITypes: true, + maxRetries: 3, + }; + + it('should handle no loaded remote API aliases', () => { + vi.spyOn(fs, 'writeFileSync'); + + dtsManager.consumeAPITypes(baseHostOptions); + + expect(fs.writeFileSync).not.toHaveBeenCalled(); + }); + + it('should handle existing API types file', () => { + const existingContent = ` + import type { PackageType as PackageType_0, RemoteKeys as RemoteKeys_0 } from './existing/apis.d.ts'; + `; + vi.spyOn(fs, 'readFileSync').mockReturnValue(existingContent); + vi.spyOn(fs, 'writeFileSync'); + + // Mock the ThirdPartyExtractor to return the existing import + vi.mocked(ThirdPartyExtractor).mockImplementation(() => ({ + collectTypeImports: vi.fn().mockReturnValue(['./existing/apis.d.ts']), + pkgs: {} as Record, + pattern: /.*/, + context: '', + destDir: '', + tsConfigPath: '', + typesFolder: '', + implementation: 'webpack', + addPkgs: vi.fn(), + inferPkgDir: vi.fn(), + collectPkgs: vi.fn(), + copyDts: vi.fn().mockResolvedValue(undefined), + })); + + // Add the existing alias to the loadedRemoteAPIAlias set + dtsManager.loadedRemoteAPIAlias.add('existing'); + + dtsManager.consumeAPITypes(baseHostOptions); + + expect(dtsManager.loadedRemoteAPIAlias.has('existing')).toBe(true); + }); + }); +}); diff --git a/packages/dts-plugin/src/core/lib/DtsWorker.spec.ts b/packages/dts-plugin/src/core/lib/DtsWorker.spec.ts index f77ed4fd8f..82830aeb99 100644 --- a/packages/dts-plugin/src/core/lib/DtsWorker.spec.ts +++ b/packages/dts-plugin/src/core/lib/DtsWorker.spec.ts @@ -1,7 +1,10 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; import { join } from 'path'; import dirTree from 'directory-tree'; import { execSync } from 'child_process'; +import { isDebugMode } from './utils'; +import type { DTSManagerOptions } from '../interfaces/DTSManagerOptions'; + const TEST_DIT_DIR = 'dist-test'; describe('generateTypesInChildProcess', () => { @@ -210,3 +213,236 @@ describe('generateTypesInChildProcess', () => { expect(checkProcess()).toEqual(false); }); }); + +describe('DtsWorker Unit Tests', () => { + let dtsWorker: any; + let originalKill: typeof process.kill; + let originalDebugMode: typeof isDebugMode; + let DtsWorkerClass: any; + + const projectRoot = join(__dirname, '../../..'); + const typesFolder = '@mf-types-dts-test'; + + const mockRemoteOptions = { + moduleFederationConfig: { + name: 'dtsWorkerSpecRemote', + filename: 'remoteEntry.js', + exposes: { + './index': join(__dirname, '..', './index.ts'), + }, + shared: { + react: { singleton: true, eager: true }, + 'react-dom': { singleton: true, eager: true }, + }, + manifest: true, + }, + tsConfigPath: join(projectRoot, './tsconfig.spec.json'), + typesFolder: typesFolder, + compiledTypesFolder: 'compiled-types', + deleteTypesFolder: false, + additionalFilesToCompile: [], + context: projectRoot, + extractRemoteTypes: true, + }; + + const mockHostOptions = { + moduleFederationConfig: { + name: 'dtsWorkerSpecHost', + filename: 'remoteEntry.js', + remotes: { + remotes: 'remote@https://foo.it', + }, + shared: { + react: { singleton: true, eager: true }, + 'react-dom': { singleton: true, eager: true }, + }, + manifest: true, + }, + typesFolder: `dist-test/@mf-types-dts-test-consume-types`, + context: projectRoot, + deleteTypesFolder: true, + remoteTypesFolder: 'remote-types', + }; + + const mockOptions: DTSManagerOptions = { + host: mockHostOptions, + remote: mockRemoteOptions, + }; + + beforeEach(() => { + vi.spyOn(console, 'error').mockImplementation(() => {}); + originalKill = process.kill; + originalDebugMode = isDebugMode; + DtsWorkerClass = require('../../../dist/core').DtsWorker; + // Reset isDebugMode for each test + vi.mock('./utils', () => ({ + isDebugMode: () => false, + cloneDeepOptions: (options: any) => JSON.parse(JSON.stringify(options)), + })); + // Mock logger + vi.mock('../../server', () => ({ + logger: { + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + debug: vi.fn(), + }, + fileLog: vi.fn(), + })); + }); + + afterEach(async () => { + if (dtsWorker) { + try { + dtsWorker.exit(); + } catch (err) { + // Ignore exit errors + } + dtsWorker = null; + } + process.kill = originalKill; + vi.restoreAllMocks(); + vi.resetModules(); + }); + + describe('initialization', () => { + it('should create a new instance with valid options', () => { + dtsWorker = new DtsWorkerClass(mockOptions); + + expect(dtsWorker).toBeDefined(); + expect(dtsWorker.rpcWorker).toBeDefined(); + expect(dtsWorker._options).toBeDefined(); + }); + + it('should remove unserializable manifest data from options', () => { + const optionsWithManifest = { + ...mockOptions, + remote: { + ...mockOptions.remote, + moduleFederationConfig: { + ...mockOptions.remote.moduleFederationConfig, + manifest: { some: 'data' }, + }, + }, + }; + + vi.mock('./utils', () => ({ + isDebugMode: () => false, + cloneDeepOptions: (options: any) => { + const cloned = JSON.parse(JSON.stringify(options)); + if (cloned.remote?.moduleFederationConfig?.manifest) { + delete cloned.remote.moduleFederationConfig.manifest; + } + if (cloned.host?.moduleFederationConfig?.manifest) { + delete cloned.host.moduleFederationConfig.manifest; + } + return cloned; + }, + })); + + dtsWorker = new DtsWorkerClass(optionsWithManifest); + expect( + dtsWorker._options.remote.moduleFederationConfig.manifest, + ).toBeFalsy(); + }); + }); + + describe('process management', () => { + it('should handle exit gracefully when worker termination fails', async () => { + dtsWorker = new DtsWorkerClass(mockOptions); + + dtsWorker.rpcWorker.terminate = () => { + throw new Error('Termination failed'); + }; + + expect(() => dtsWorker.exit()).not.toThrow(); + }); + + it('should ensure child process exits even when promise rejects', async () => { + vi.mock('../rpc/index', () => ({ + createRpcWorker: () => ({ + connect: () => Promise.resolve(), + terminate: vi.fn(), + process: { + pid: process.pid, + connected: true, + send: (message: any, callback?: (error: Error | null) => void) => { + if (callback) callback(null); + }, + }, + }), + })); + + dtsWorker = new DtsWorkerClass(mockOptions); + dtsWorker._res = Promise.reject(new Error('Test error')); + + await expect(dtsWorker.controlledPromise).resolves.toBeUndefined(); + }); + }); + + describe('debug mode handling', () => { + it('should log errors in debug mode', async () => { + vi.mock('./utils', () => ({ + isDebugMode: () => true, + cloneDeepOptions: (options: any) => JSON.parse(JSON.stringify(options)), + })); + + vi.mock('../rpc/index', () => ({ + createRpcWorker: () => ({ + connect: () => Promise.resolve(), + terminate: vi.fn(), + process: { + pid: process.pid, + connected: true, + send: (message: any, callback?: (error: Error | null) => void) => { + if (callback) callback(null); + }, + }, + }), + })); + + const consoleSpy = vi.spyOn(console, 'error'); + dtsWorker = new DtsWorkerClass(mockOptions); + dtsWorker._res = Promise.reject(new Error('Test error')); + + await dtsWorker.controlledPromise; + expect(consoleSpy).toHaveBeenCalled(); + }); + + it('should not log errors when not in debug mode', async () => { + vi.mock('./utils', () => ({ + isDebugMode: () => false, + cloneDeepOptions: (options: any) => JSON.parse(JSON.stringify(options)), + })); + + vi.mock('../rpc/index', () => ({ + createRpcWorker: () => ({ + connect: () => Promise.resolve(), + terminate: vi.fn(), + process: { + pid: process.pid, + connected: true, + send: (message: any, callback?: (error: Error | null) => void) => { + if (callback) callback(null); + }, + }, + }), + })); + + const consoleSpy = vi.spyOn(console, 'error'); + dtsWorker = new DtsWorkerClass(mockOptions); + + // Mock process.kill to not throw + process.kill = vi.fn(); + + // Mock the promise to resolve normally + dtsWorker._res = Promise.resolve(); + + // Clear any previous calls to console.error + consoleSpy.mockClear(); + + await dtsWorker.controlledPromise; + expect(consoleSpy).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/dts-plugin/src/core/lib/archiveHandler.test.ts b/packages/dts-plugin/src/core/lib/archiveHandler.test.ts index fa66a0a06d..a48a340509 100644 --- a/packages/dts-plugin/src/core/lib/archiveHandler.test.ts +++ b/packages/dts-plugin/src/core/lib/archiveHandler.test.ts @@ -1,65 +1,246 @@ +import type { HostOptions } from '../interfaces/HostOptions'; +import type { RemoteOptions } from '../interfaces/RemoteOptions'; +import type { TsConfigJson } from '../interfaces/TsConfigJson'; + import AdmZip from 'adm-zip'; -import axios from 'axios'; -import { existsSync, mkdirSync, mkdtempSync, rmSync } from 'fs'; +import axios, { AxiosResponse, InternalAxiosRequestConfig } from 'axios'; +import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs'; import { readJSONSync } from 'fs-extra'; import os from 'os'; import { join } from 'path'; -import { afterAll, describe, expect, it, vi } from 'vitest'; +import { + afterAll, + beforeEach, + describe, + expect, + it, + vi, + MockInstance, +} from 'vitest'; -import { RemoteOptions } from '../interfaces/RemoteOptions'; -import { createTypesArchive, downloadTypesArchive } from './archiveHandler'; -import { HostOptions } from '../interfaces/HostOptions'; +import { + createTypesArchive, + downloadTypesArchive, + retrieveTypesArchiveDestinationPath, + retrieveTypesZipPath, +} from './archiveHandler'; +import { fileLog } from '../../server'; describe('archiveHandler', () => { const tmpDir = mkdtempSync(join(os.tmpdir(), 'archive-handler')); const basicConfig = readJSONSync( join(__dirname, '../../..', './tsconfig.spec.json'), - ); - const tsConfig = { + ) as TsConfigJson; + const tsConfig: TsConfigJson = { ...basicConfig, + compilerOptions: { + ...basicConfig.compilerOptions, + outDir: join(tmpDir, 'typesRemoteFolder', 'compiledTypesFolder'), + }, }; - tsConfig.compilerOptions.outDir = join( - tmpDir, - 'typesRemoteFolder', - 'compiledTypesFolder', - ); + beforeEach(() => { + vi.clearAllMocks(); + // Clean up and recreate the output directory + rmSync(tsConfig.compilerOptions.outDir, { recursive: true, force: true }); + mkdirSync(tsConfig.compilerOptions.outDir, { recursive: true }); + }); + + afterAll(() => { + rmSync(tmpDir, { recursive: true, force: true }); + }); + + describe('retrieveTypesZipPath', () => { + it('should correctly construct zip path', () => { + const mfTypesPath = '/path/to/types/folder'; + const remoteOptions: Required = { + typesFolder: 'folder', + moduleFederationConfig: {}, + context: process.cwd(), + implementation: '', + hostRemoteTypesFolder: 'remoteTypes', + compileInChildProcess: false, + compilerInstance: null, + generateAPITypes: false, + extractThirdParty: false, + extractRemoteTypes: false, + abortOnError: true, + additionalFilesToCompile: [], + compiledTypesFolder: 'compiledTypesFolder', + tsConfigPath: './tsconfig.spec.json', + deleteTypesFolder: false, + }; + + const zipPath = retrieveTypesZipPath(mfTypesPath, remoteOptions); + expect(zipPath).toBe('/path/to/types/folder.zip'); + }); + + it('should handle paths with trailing slashes', () => { + const mfTypesPath = '/path/to/types/folder/'; + const remoteOptions: Required = { + typesFolder: 'folder', + moduleFederationConfig: {}, + context: process.cwd(), + implementation: '', + hostRemoteTypesFolder: 'remoteTypes', + compileInChildProcess: false, + compilerInstance: null, + generateAPITypes: false, + extractThirdParty: false, + extractRemoteTypes: false, + abortOnError: true, + additionalFilesToCompile: [], + compiledTypesFolder: 'compiledTypesFolder', + tsConfigPath: './tsconfig.spec.json', + deleteTypesFolder: false, + }; + + const zipPath = retrieveTypesZipPath(mfTypesPath, remoteOptions); + expect(zipPath).toBe('/path/to/types/folder.zip'); + }); + + it('should handle paths with multiple levels', () => { + const hostOptions: Required = { + context: '/base/path', + typesFolder: 'types/nested', + moduleFederationConfig: {}, + implementation: '', + runtimePkgs: [], + abortOnError: true, + remoteTypesFolder: 'remoteTypes', + deleteTypesFolder: false, + maxRetries: 3, + consumeAPITypes: false, + }; - mkdirSync(tsConfig.compilerOptions.outDir, { recursive: true }); + const path = retrieveTypesArchiveDestinationPath( + hostOptions, + 'remote1/v1', + ); + expect(path).toBe('/base/path/types/nested/remote1/v1'); + }); + }); + + describe('retrieveTypesArchiveDestinationPath', () => { + it('should correctly construct destination path', () => { + const hostOptions: Required = { + context: '/base', + typesFolder: 'types', + moduleFederationConfig: {}, + implementation: '', + runtimePkgs: [], + abortOnError: true, + remoteTypesFolder: 'remoteTypes', + deleteTypesFolder: false, + maxRetries: 3, + consumeAPITypes: false, + }; + + const path = retrieveTypesArchiveDestinationPath(hostOptions, 'remote1'); + expect(path).toBe('/base/types/remote1'); + }); + }); describe('createTypesArchive', () => { - const remoteOptions = { + const remoteOptions: Required = { additionalFilesToCompile: [], compiledTypesFolder: 'compiledTypesFolder', typesFolder: 'typesRemoteFolder', moduleFederationConfig: {}, tsConfigPath: './tsconfig.spec.json', deleteTypesFolder: false, - } as unknown as Required; + context: process.cwd(), + implementation: '', + hostRemoteTypesFolder: 'remoteTypes', + compileInChildProcess: false, + compilerInstance: null, + generateAPITypes: false, + extractThirdParty: false, + extractRemoteTypes: false, + abortOnError: true, + }; - it('correctly creates archive', async () => { - const archivePath = join(tmpDir, `${remoteOptions.typesFolder}.zip`); + it('should correctly create archive with type definitions', async () => { + // Create a sample type definition file + const typePath = join(tsConfig.compilerOptions.outDir, 'sample.d.ts'); + writeFileSync(typePath, 'export declare const foo: string;'); + const archivePath = join(tmpDir, `${remoteOptions.typesFolder}.zip`); const archiveCreated = await createTypesArchive(tsConfig, remoteOptions); expect(archiveCreated).toBeTruthy(); expect(existsSync(archivePath)).toBeTruthy(); + + // Verify archive contents - only check .d.ts files + const zip = new AdmZip(archivePath); + const dtsEntries = zip + .getEntries() + .filter((entry) => entry.entryName.endsWith('.d.ts')); + expect(dtsEntries).toHaveLength(1); + // The entry name includes the compiledTypesFolder since that's how it's stored in the archive + expect(dtsEntries[0].entryName).toBe('compiledTypesFolder/sample.d.ts'); + expect(dtsEntries[0].getData().toString()).toBe( + 'export declare const foo: string;', + ); }); - it('throws for unexisting outDir', async () => { - expect( - createTypesArchive( - { - ...tsConfig, - compilerOptions: { - ...tsConfig.compilerOptions, - outDir: '/foo', - }, - }, - remoteOptions, - ), + it('should throw error for non-existent outDir', async () => { + const invalidConfig: TsConfigJson = { + ...tsConfig, + compilerOptions: { + ...tsConfig.compilerOptions, + outDir: '/foo', + }, + }; + + await expect( + createTypesArchive(invalidConfig, remoteOptions), ).rejects.toThrowError(); }); + + it('should handle empty type definitions directory', async () => { + const archivePath = join(tmpDir, `${remoteOptions.typesFolder}.zip`); + const archiveCreated = await createTypesArchive(tsConfig, remoteOptions); + + expect(archiveCreated).toBeTruthy(); + expect(existsSync(archivePath)).toBeTruthy(); + + // Only check for .d.ts files + const zip = new AdmZip(archivePath); + const dtsEntries = zip + .getEntries() + .filter((entry) => entry.entryName.endsWith('.d.ts')); + expect(dtsEntries).toHaveLength(0); + }); + + it('should handle archive with nested directory structure', async () => { + // Create nested directories with type definitions + const nestedPath = join(tsConfig.compilerOptions.outDir, 'nested/deep'); + mkdirSync(nestedPath, { recursive: true }); + writeFileSync( + join(nestedPath, 'nested.d.ts'), + 'export declare const nested: boolean;', + ); + + const archivePath = join(tmpDir, `${remoteOptions.typesFolder}.zip`); + const archiveCreated = await createTypesArchive(tsConfig, remoteOptions); + + expect(archiveCreated).toBeTruthy(); + expect(existsSync(archivePath)).toBeTruthy(); + + // Verify archive contents including nested structure + const zip = new AdmZip(archivePath); + const dtsEntries = zip + .getEntries() + .filter((entry) => entry.entryName.endsWith('.d.ts')); + expect(dtsEntries).toHaveLength(1); + expect(dtsEntries[0].entryName).toBe( + 'compiledTypesFolder/nested/deep/nested.d.ts', + ); + expect(dtsEntries[0].getData().toString()).toBe( + 'export declare const nested: boolean;', + ); + }); }); describe('downloadTypesArchive', () => { @@ -79,54 +260,161 @@ describe('archiveHandler', () => { const destinationFolder = 'typesHostFolder'; const fileToDownload = 'https://foo.it'; - it('correctly extracts downloaded archive', async () => { + beforeEach(() => { + // Clean up and recreate the destination folder + const archivePath = join(tmpDir, destinationFolder); + rmSync(archivePath, { recursive: true, force: true }); + mkdirSync(archivePath, { recursive: true }); + }); + + it('should correctly extract downloaded archive with type definitions', async () => { const archivePath = join(tmpDir, destinationFolder); const zip = new AdmZip(); - await zip.addLocalFolderPromise(tmpDir, {}); - axios.get = vi.fn().mockResolvedValueOnce({ data: zip.toBuffer() }); + // Add sample type definition to the archive + zip.addFile( + 'sample.d.ts', + Buffer.from('export declare const bar: number;'), + ); - await downloadTypesArchive(hostOptions)([ + const mockResponse: AxiosResponse = { + data: zip.toBuffer(), + status: 200, + statusText: 'OK', + headers: {}, + config: {} as InternalAxiosRequestConfig, + request: {} as XMLHttpRequest, + }; + vi.spyOn(axios, 'get').mockResolvedValueOnce(mockResponse); + + const result = await downloadTypesArchive(hostOptions)([ destinationFolder, fileToDownload, ]); - expect(existsSync(archivePath)).toBeTruthy(); + + expect(result).toEqual([destinationFolder, archivePath]); + expect(existsSync(join(archivePath, 'sample.d.ts'))).toBeTruthy(); expect(axios.get).toHaveBeenCalledTimes(1); + // Only verify the URL and responseType + const axiosGetMock = vi.mocked(axios.get); + const [[url, options]] = axiosGetMock.mock.calls; + expect(url).toBe(fileToDownload); + expect(options.responseType).toBe('arraybuffer'); }); - it('correctly handles exception', async () => { - const message = 'Rejected value'; - - axios.get = vi.fn().mockRejectedValue(new Error(message)); + it('should retry on network failure up to maxRetries', async () => { + const error = new Error('Network error'); + vi.spyOn(axios, 'get').mockRejectedValue(error); await expect(() => downloadTypesArchive(hostOptions)([destinationFolder, fileToDownload]), - ).rejects.toThrowError( - `Network error: Unable to download federated mocks for '${destinationFolder}' from '${fileToDownload}' because '${message}'`, - ); + ).rejects.toThrowError(/Network error/); + expect(axios.get).toHaveBeenCalledTimes(hostOptions.maxRetries); }); - it('not throw error while set abortOnError: false ', async () => { - const message = 'Rejected value'; - const hostOptions: Required = { - moduleFederationConfig: {}, - typesFolder: tmpDir, - remoteTypesFolder: tmpDir, - deleteTypesFolder: true, - maxRetries: 3, - implementation: '', - context: process.cwd(), + it('should handle empty archives gracefully', async () => { + const archivePath = join(tmpDir, destinationFolder); + const zip = new AdmZip(); + // Add an empty directory to the archive + zip.addFile('.keep', Buffer.from('')); + vi.spyOn(axios, 'get').mockResolvedValueOnce({ + data: zip.toBuffer(), + status: 200, + statusText: 'OK', + headers: {}, + config: {} as InternalAxiosRequestConfig, + request: {} as XMLHttpRequest, + } as AxiosResponse); + + const result = await downloadTypesArchive(hostOptions)([ + destinationFolder, + fileToDownload, + ]); + + expect(result).toEqual([destinationFolder, archivePath]); + expect(existsSync(archivePath)).toBeTruthy(); + expect(existsSync(join(archivePath, '.keep'))).toBeTruthy(); + }); + + it('should clean up existing folder when deleteTypesFolder is true', async () => { + const archivePath = join(tmpDir, destinationFolder); + writeFileSync(join(archivePath, 'old.d.ts'), 'old content'); + + const zip = new AdmZip(); + zip.addFile('new.d.ts', Buffer.from('new content')); + vi.spyOn(axios, 'get').mockResolvedValueOnce({ + data: zip.toBuffer(), + status: 200, + statusText: 'OK', + headers: {}, + config: {} as InternalAxiosRequestConfig, + request: {} as XMLHttpRequest, + } as AxiosResponse); + + await downloadTypesArchive(hostOptions)([ + destinationFolder, + fileToDownload, + ]); + + expect(existsSync(join(archivePath, 'old.d.ts'))).toBeFalsy(); + expect(existsSync(join(archivePath, 'new.d.ts'))).toBeTruthy(); + }); + + it('should preserve existing folder when deleteTypesFolder is false', async () => { + const options: Required = { + ...hostOptions, + deleteTypesFolder: false, + }; + const archivePath = join(tmpDir, destinationFolder); + writeFileSync(join(archivePath, 'old.d.ts'), 'old content'); + + const zip = new AdmZip(); + zip.addFile('new.d.ts', Buffer.from('new content')); + vi.spyOn(axios, 'get').mockResolvedValueOnce({ + data: zip.toBuffer(), + status: 200, + statusText: 'OK', + headers: {}, + config: {} as InternalAxiosRequestConfig, + request: {} as XMLHttpRequest, + } as AxiosResponse); + + await downloadTypesArchive(options)([destinationFolder, fileToDownload]); + + expect(existsSync(join(archivePath, 'old.d.ts'))).toBeTruthy(); + expect(existsSync(join(archivePath, 'new.d.ts'))).toBeTruthy(); + }); + + it('should continue without error when abortOnError is false', async () => { + const options: Required = { + ...hostOptions, abortOnError: false, - consumeAPITypes: false, - runtimePkgs: [], }; - axios.get = vi.fn().mockRejectedValue(new Error(message)); - const res = await downloadTypesArchive(hostOptions)([ + vi.spyOn(axios, 'get').mockRejectedValue(new Error('Network error')); + + const result = await downloadTypesArchive(options)([ destinationFolder, fileToDownload, ]); - expect(res).toEqual(undefined); + + expect(result).toBeUndefined(); + expect(axios.get).toHaveBeenCalledTimes(options.maxRetries); + }); + + it('should handle malformed zip data', async () => { + vi.spyOn(axios, 'get').mockResolvedValueOnce({ + data: Buffer.from('not a valid zip file'), + status: 200, + statusText: 'OK', + headers: {}, + config: {} as InternalAxiosRequestConfig, + request: {} as XMLHttpRequest, + } as AxiosResponse); + + await expect(() => + downloadTypesArchive(hostOptions)([destinationFolder, fileToDownload]), + ).rejects.toThrow(/Network error: Unable to download federated mocks/); }); }); }); diff --git a/packages/dts-plugin/src/core/lib/consumeTypes.spec.ts b/packages/dts-plugin/src/core/lib/consumeTypes.spec.ts new file mode 100644 index 0000000000..55da8251ee --- /dev/null +++ b/packages/dts-plugin/src/core/lib/consumeTypes.spec.ts @@ -0,0 +1,62 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { consumeTypes } from './consumeTypes'; +import { getDTSManagerConstructor } from './utils'; +import { DTSManagerOptions } from '../interfaces/DTSManagerOptions'; + +// Mock the utils module +vi.mock('./utils'); + +describe('consumeTypes', () => { + const mockConsumeTypes = vi.fn().mockResolvedValue(undefined); + + // Mock implementation of DTSManager + class MockDTSManager { + constructor(public options: DTSManagerOptions) {} + async consumeTypes() { + return mockConsumeTypes(); + } + } + + beforeEach(() => { + vi.clearAllMocks(); + mockConsumeTypes.mockClear(); + (getDTSManagerConstructor as any).mockReturnValue(MockDTSManager); + }); + + it('should create DTSManager with provided options', async () => { + const options: DTSManagerOptions = { + host: { + implementation: 'test-implementation', + moduleFederationConfig: { + name: 'test-host', + remotes: {}, + }, + }, + }; + + await consumeTypes(options); + + expect(getDTSManagerConstructor).toHaveBeenCalledWith( + 'test-implementation', + ); + expect(mockConsumeTypes).toHaveBeenCalled(); + }); + + it('should work with minimal options', async () => { + const options: DTSManagerOptions = {}; + + await consumeTypes(options); + + expect(getDTSManagerConstructor).toHaveBeenCalledWith(undefined); + expect(mockConsumeTypes).toHaveBeenCalled(); + }); + + it('should propagate errors from consumeTypes', async () => { + const error = new Error('Test error'); + const options: DTSManagerOptions = {}; + + mockConsumeTypes.mockRejectedValueOnce(error); + + await expect(consumeTypes(options)).rejects.toThrow(error); + }); +}); diff --git a/packages/dts-plugin/src/plugins/GenerateTypesPlugin.ts b/packages/dts-plugin/src/plugins/GenerateTypesPlugin.ts index 41c7079bd6..6cb0a94137 100644 --- a/packages/dts-plugin/src/plugins/GenerateTypesPlugin.ts +++ b/packages/dts-plugin/src/plugins/GenerateTypesPlugin.ts @@ -73,6 +73,84 @@ export class GenerateTypesPlugin implements WebpackPluginInstance { }; const generateTypesFn = getGenerateTypesFn(); let compiledOnce = false; + + const emitTypesFiles = async () => { + try { + const { zipTypesPath, apiTypesPath, zipName, apiFileName } = + retrieveTypesAssetsInfo(finalOptions.remote); + + await generateTypesFn(finalOptions); + const config = finalOptions.remote.moduleFederationConfig; + let zipPrefix = ''; + if (typeof config.manifest === 'object' && config.manifest.filePath) { + zipPrefix = config.manifest.filePath; + } else if ( + typeof config.manifest === 'object' && + config.manifest.fileName + ) { + zipPrefix = path.dirname(config.manifest.fileName); + } else if (config.filename) { + zipPrefix = path.dirname(config.filename); + } + + if (zipTypesPath) { + const zipContent = fs.readFileSync(zipTypesPath); + const zipOutputPath = path.join( + compiler.outputPath, + zipPrefix, + zipName, + ); + await new Promise((resolve, reject) => { + compiler.outputFileSystem.mkdir( + path.dirname(zipOutputPath), + (err) => { + if (err) reject(err); + else { + compiler.outputFileSystem.writeFile( + zipOutputPath, + zipContent, + (writeErr) => { + if (writeErr) reject(writeErr); + else resolve(); + }, + ); + } + }, + ); + }); + } + + if (apiTypesPath) { + const apiContent = fs.readFileSync(apiTypesPath); + const apiOutputPath = path.join( + compiler.outputPath, + zipPrefix, + apiFileName, + ); + await new Promise((resolve, reject) => { + compiler.outputFileSystem.mkdir( + path.dirname(apiOutputPath), + (err) => { + if (err) reject(err); + else { + compiler.outputFileSystem.writeFile( + apiOutputPath, + apiContent, + (writeErr) => { + if (writeErr) reject(writeErr); + else resolve(); + }, + ); + } + }, + ); + }); + } + } catch (err) { + console.error(err); + } + }; + compiler.hooks.thisCompilation.tap('mf:generateTypes', (compilation) => { compilation.hooks.processAssets.tapPromise( { @@ -82,15 +160,22 @@ export class GenerateTypesPlugin implements WebpackPluginInstance { compilation.constructor.PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER, }, async () => { - if (pluginOptions.dev === false && compiledOnce) { - return; - } try { + if (pluginOptions.dev === false && compiledOnce) { + return; + } + + if (compiledOnce) { + emitTypesFiles(); + return; + } + const { zipTypesPath, apiTypesPath, zipName, apiFileName } = retrieveTypesAssetsInfo(finalOptions.remote); if (zipName && compilation.getAsset(zipName)) { return; } + await generateTypesFn(finalOptions); const config = finalOptions.remote.moduleFederationConfig; let zipPrefix = ''; @@ -129,7 +214,7 @@ export class GenerateTypesPlugin implements WebpackPluginInstance { } compiledOnce = true; } catch (err) { - console.error(err); + console.error('Error in mf:generateTypes processAssets hook:', err); } }, ); diff --git a/packages/enhanced/src/rspack.ts b/packages/enhanced/src/rspack.ts index 7e5f44c7e6..98e64b3906 100644 --- a/packages/enhanced/src/rspack.ts +++ b/packages/enhanced/src/rspack.ts @@ -1 +1 @@ -export { ModuleFederationPlugin } from '@module-federation/rspack'; +export { ModuleFederationPlugin } from '@module-federation/rspack/plugin'; diff --git a/packages/rspack/package.json b/packages/rspack/package.json index 1d4657fe60..09bd7cb0f4 100644 --- a/packages/rspack/package.json +++ b/packages/rspack/package.json @@ -39,12 +39,20 @@ "import": "./dist/index.esm.js", "require": "./dist/index.cjs.js", "types": "./dist/index.cjs.d.ts" + }, + "./plugin": { + "types": "./dist/plugin.cjs.d.ts", + "import": "./dist/plugin.esm.mjs", + "require": "./dist/plugin.cjs.js" } }, "typesVersions": { "*": { ".": [ "./dist/index.cjs.d.ts" + ], + "plugin": [ + "./dist/plugin.cjs.d.ts" ] } }, diff --git a/packages/rspack/rollup.config.js b/packages/rspack/rollup.config.js index 529f337f44..bb37e988aa 100644 --- a/packages/rspack/rollup.config.js +++ b/packages/rspack/rollup.config.js @@ -1,9 +1,14 @@ const copy = require('rollup-plugin-copy'); const replace = require('@rollup/plugin-replace'); +const path = require('path'); module.exports = (rollupConfig, projectOptions) => { const pkg = require('./package.json'); + rollupConfig.input['plugin'] = path.resolve( + process.cwd(), + './packages/rspack/src/ModuleFederationPlugin.ts', + ); rollupConfig.plugins.push( replace({ __VERSION__: JSON.stringify(pkg.version), diff --git a/packages/rspack/src/ModuleFederationPlugin.ts b/packages/rspack/src/ModuleFederationPlugin.ts index e0f87411bb..b8344efd6c 100644 --- a/packages/rspack/src/ModuleFederationPlugin.ts +++ b/packages/rspack/src/ModuleFederationPlugin.ts @@ -1,4 +1,4 @@ -import { +import type { Compiler, ModuleFederationPluginOptions, RspackPluginInstance,