From 4556a78ee0309679f0c1a523589c556a712a47e8 Mon Sep 17 00:00:00 2001 From: "zhanghang.heal" Date: Thu, 26 Mar 2026 11:52:02 +0800 Subject: [PATCH] feat(dts-plugin): move afterGenerate hook under generateTypes --- .../dts-after-generate-types-callback.md | 18 ++-- apps/website-new/docs/en/configure/dts.mdx | 31 +++++++ apps/website-new/docs/zh/configure/dts.mdx | 31 +++++++ arch-doc/sdk-reference.md | 11 +++ .../src/core/configurations/remotePlugin.ts | 4 +- .../src/plugins/GenerateTypesPlugin.test.ts | 86 +++++++++++++++---- .../src/plugins/GenerateTypesPlugin.ts | 35 +++++++- .../container/ModuleFederationPlugin.check.ts | 65 ++++++++------ .../container/ModuleFederationPlugin.json | 8 +- .../container/ModuleFederationPlugin.ts | 10 +-- .../types/plugins/ModuleFederationPlugin.ts | 11 ++- 11 files changed, 247 insertions(+), 63 deletions(-) diff --git a/.changeset/dts-after-generate-types-callback.md b/.changeset/dts-after-generate-types-callback.md index 181af4e6a42..f0a765102b4 100644 --- a/.changeset/dts-after-generate-types-callback.md +++ b/.changeset/dts-after-generate-types-callback.md @@ -3,17 +3,23 @@ '@module-federation/sdk': minor --- -feat(dts-plugin): add `afterGenerateTypes` callback to `PluginDtsOptions` +feat(dts-plugin): add `dts.generateTypes.afterGenerate` callback -Allows users to hook into the type generation lifecycle. The callback is invoked after each successful `generateTypesAPI` call, in both dev and prod modes, making it possible to perform post-processing (e.g. analyzing generated `.d.ts` files to produce additional artifacts). +Allows users to hook into the type generation lifecycle from +`dts.generateTypes`. The callback runs after each successful type generation in +both dev and prod modes, making it possible to do follow-up work with the +generated files. ```ts new ModuleFederationPlugin({ dts: { - generateTypes: true, - afterGenerateTypes: async () => { - // dts files are on disk here - await analyzeTypesAndGenerateJson(); + generateTypes: { + afterGenerate: async ({ zipTypesPath, apiTypesPath }) => { + await analyzeTypesAndGenerateJson({ + zipTypesPath, + apiTypesPath, + }); + }, }, }, }); diff --git a/apps/website-new/docs/en/configure/dts.mdx b/apps/website-new/docs/en/configure/dts.mdx index 0c99cb79bb7..23d5e68b230 100644 --- a/apps/website-new/docs/en/configure/dts.mdx +++ b/apps/website-new/docs/en/configure/dts.mdx @@ -41,6 +41,16 @@ interface DtsRemoteOptions { extractRemoteTypes?: boolean; abortOnError?: boolean; deleteTsConfig?: boolean; + afterGenerate?: ( + options: DtsGenerateTypesHookOptions, + ) => Promise | void; +} + +interface DtsGenerateTypesHookOptions { + zipTypesPath: string; + apiTypesPath: string; + zipName: string; + apiFileName: string; } ``` @@ -102,6 +112,27 @@ Whether generate types in child process Whether to throw an error when a problem is encountered during type generation +#### afterGenerate + +- Type: `(options: DtsGenerateTypesHookOptions) => Promise | void` +- Required: No +- Default value: `undefined` + +Runs after federated types are generated and before the generated files are emitted. + +```ts title="module-federation.config.ts" +new ModuleFederationPlugin({ + dts: { + generateTypes: { + afterGenerate({ zipTypesPath, apiTypesPath }) { + console.log('zip types:', zipTypesPath); + console.log('api types:', apiTypesPath); + }, + }, + }, +}); +``` + #### tsConfigPath - Type: `string` diff --git a/apps/website-new/docs/zh/configure/dts.mdx b/apps/website-new/docs/zh/configure/dts.mdx index fb4044aaa70..007de8d8299 100644 --- a/apps/website-new/docs/zh/configure/dts.mdx +++ b/apps/website-new/docs/zh/configure/dts.mdx @@ -40,6 +40,16 @@ interface DtsRemoteOptions { extractRemoteTypes?: boolean; abortOnError?: boolean; deleteTsConfig?: boolean; + afterGenerate?: ( + options: DtsGenerateTypesHookOptions, + ) => Promise | void; +} + +interface DtsGenerateTypesHookOptions { + zipTypesPath: string; + apiTypesPath: string; + zipName: string; + apiFileName: string; } ``` @@ -101,6 +111,27 @@ interface DtsRemoteOptions { 是否抛出错误当类型生成过程中碰到问题 +#### afterGenerate + +- 类型:`(options: DtsGenerateTypesHookOptions) => Promise | void` +- 是否必填:否 +- 默认值:`undefined` + +在 federated types 生成完成后、输出文件被提交到文件系统前执行。 + +```ts title="module-federation.config.ts" +new ModuleFederationPlugin({ + dts: { + generateTypes: { + afterGenerate({ zipTypesPath, apiTypesPath }) { + console.log('zip types:', zipTypesPath); + console.log('api types:', apiTypesPath); + }, + }, + }, +}); +``` + #### tsConfigPath - 类型:`string` diff --git a/arch-doc/sdk-reference.md b/arch-doc/sdk-reference.md index ecba49c6060..f8766e95482 100644 --- a/arch-doc/sdk-reference.md +++ b/arch-doc/sdk-reference.md @@ -486,6 +486,10 @@ interface DtsRemoteOptions { }; extractRemoteTypes?: boolean; abortOnError?: boolean; + deleteTsConfig?: boolean; + afterGenerate?: ( + options: DtsGenerateTypesHookOptions, + ) => Promise | void; } interface DtsHostOptions { @@ -508,6 +512,13 @@ interface RemoteTypeUrls { zip: string; }; } + +interface DtsGenerateTypesHookOptions { + zipTypesPath: string; + apiTypesPath: string; + zipName: string; + apiFileName: string; +} ``` ### PluginDevOptions diff --git a/packages/dts-plugin/src/core/configurations/remotePlugin.ts b/packages/dts-plugin/src/core/configurations/remotePlugin.ts index 6878f0feeb0..e3a3f267f16 100644 --- a/packages/dts-plugin/src/core/configurations/remotePlugin.ts +++ b/packages/dts-plugin/src/core/configurations/remotePlugin.ts @@ -232,10 +232,10 @@ const resolveExposes = (remoteOptions: Required) => { export const retrieveRemoteConfig = (options: RemoteOptions) => { validateOptions(options); - const remoteOptions: Required = { + const remoteOptions = { ...defaultOptions, ...options, - }; + } as Required; const mapComponentsToExpose = resolveExposes(remoteOptions); const tsConfig = readTsConfig(remoteOptions, mapComponentsToExpose); diff --git a/packages/dts-plugin/src/plugins/GenerateTypesPlugin.test.ts b/packages/dts-plugin/src/plugins/GenerateTypesPlugin.test.ts index 92d27780c09..b4e76b3ada9 100644 --- a/packages/dts-plugin/src/plugins/GenerateTypesPlugin.test.ts +++ b/packages/dts-plugin/src/plugins/GenerateTypesPlugin.test.ts @@ -1,6 +1,7 @@ import path from 'path'; import { describe, expect, it, vi, beforeEach } from 'vitest'; import { + callAfterGenerateHook, isSafeRelativePath, normalizeGenerateTypesOptions, resolveEmitAssetName, @@ -67,7 +68,7 @@ function createMockCompiler() { }; } -describe('afterGenerateTypes callback', () => { +describe('afterGenerate callback', () => { const basePluginOptions = { name: 'testRemote', filename: 'remoteEntry.js', @@ -79,41 +80,41 @@ describe('afterGenerateTypes callback', () => { vi.clearAllMocks(); }); - it('should call afterGenerateTypes after type generation in prod', async () => { + it('should call afterGenerate after type generation in prod', async () => { mockIsDev.mockReturnValue(false); - const afterGenerateTypes = vi.fn().mockResolvedValue(undefined); + const afterGenerate = vi.fn().mockResolvedValue(undefined); const { compiler, triggerProcessAssets } = createMockCompiler(); const plugin = new GenerateTypesPlugin( basePluginOptions, - { generateTypes: true, afterGenerateTypes }, + { generateTypes: { afterGenerate } }, Promise.resolve(undefined), vi.fn(), ); plugin.apply(compiler as any); await triggerProcessAssets(); - expect(afterGenerateTypes).toHaveBeenCalledOnce(); + expect(afterGenerate).toHaveBeenCalledOnce(); }); - it('should call afterGenerateTypes after type generation in dev', async () => { + it('should call afterGenerate after type generation in dev', async () => { mockIsDev.mockReturnValue(true); - const afterGenerateTypes = vi.fn().mockResolvedValue(undefined); + const afterGenerate = vi.fn().mockResolvedValue(undefined); const { compiler, triggerProcessAssets } = createMockCompiler(); const plugin = new GenerateTypesPlugin( basePluginOptions, - { generateTypes: true, afterGenerateTypes }, + { generateTypes: { afterGenerate } }, Promise.resolve(undefined), vi.fn(), ); plugin.apply(compiler as any); await triggerProcessAssets(); // In dev mode emitTypesFilesPromise is not awaited, flush microtasks - await vi.waitFor(() => expect(afterGenerateTypes).toHaveBeenCalledOnce()); + await vi.waitFor(() => expect(afterGenerate).toHaveBeenCalledOnce()); }); - it('should not throw when afterGenerateTypes is not provided', async () => { + it('should not throw when afterGenerate is not provided', async () => { mockIsDev.mockReturnValue(false); const { compiler, triggerProcessAssets } = createMockCompiler(); @@ -128,36 +129,36 @@ describe('afterGenerateTypes callback', () => { await expect(triggerProcessAssets()).resolves.not.toThrow(); }); - it('should not call afterGenerateTypes when asset already exists (early return)', async () => { + it('should not call afterGenerate when asset already exists (early return)', async () => { mockIsDev.mockReturnValue(false); - const afterGenerateTypes = vi.fn(); + const afterGenerate = vi.fn(); const { compiler, compilation, triggerProcessAssets } = createMockCompiler(); compilation.getAsset.mockReturnValue({}); const plugin = new GenerateTypesPlugin( basePluginOptions, - { generateTypes: true, afterGenerateTypes }, + { generateTypes: { afterGenerate } }, Promise.resolve(undefined), vi.fn(), ); plugin.apply(compiler as any); await triggerProcessAssets(); - expect(afterGenerateTypes).not.toHaveBeenCalled(); + expect(afterGenerate).not.toHaveBeenCalled(); }); - it('should await async afterGenerateTypes before continuing', async () => { + it('should await async afterGenerate before continuing', async () => { mockIsDev.mockReturnValue(false); const order: string[] = []; - const afterGenerateTypes = vi.fn(async () => { + const afterGenerate = vi.fn(async () => { order.push('callback'); }); const { compiler, triggerProcessAssets } = createMockCompiler(); const plugin = new GenerateTypesPlugin( basePluginOptions, - { generateTypes: true, afterGenerateTypes }, + { generateTypes: { afterGenerate } }, Promise.resolve(undefined), vi.fn(), ); @@ -340,4 +341,55 @@ describe('GenerateTypesPlugin', () => { expect(emitZipName).toBe(path.join('production', '@mf-types.zip')); }); }); + + describe('afterGenerate', () => { + it('should call afterGenerate with generated asset info', async () => { + const afterGenerate = vi.fn(); + + await callAfterGenerateHook({ + dtsManagerOptions: { + remote: { + moduleFederationConfig: basePluginOptions, + afterGenerate, + }, + }, + generatedTypes: { + zipTypesPath: '/project/dist/@mf-types.zip', + apiTypesPath: '/project/dist/@mf-types.d.ts', + zipName: '@mf-types.zip', + apiFileName: '@mf-types.d.ts', + }, + }); + + expect(afterGenerate).toHaveBeenCalledWith({ + zipTypesPath: '/project/dist/@mf-types.zip', + apiTypesPath: '/project/dist/@mf-types.d.ts', + zipName: '@mf-types.zip', + apiFileName: '@mf-types.d.ts', + }); + }); + + it('should not throw when afterGenerate fails and abortOnError is false', async () => { + await expect( + callAfterGenerateHook({ + dtsManagerOptions: { + remote: { + moduleFederationConfig: basePluginOptions, + abortOnError: false, + afterGenerate: vi + .fn() + .mockRejectedValue(new Error('hook failed')), + }, + displayErrorInTerminal: false, + }, + generatedTypes: { + zipTypesPath: '', + apiTypesPath: '', + zipName: '', + apiFileName: '', + }, + }), + ).resolves.toBeUndefined(); + }); + }); }); diff --git a/packages/dts-plugin/src/plugins/GenerateTypesPlugin.ts b/packages/dts-plugin/src/plugins/GenerateTypesPlugin.ts index 09a07be7859..ce54ebb0e1a 100644 --- a/packages/dts-plugin/src/plugins/GenerateTypesPlugin.ts +++ b/packages/dts-plugin/src/plugins/GenerateTypesPlugin.ts @@ -102,7 +102,39 @@ export const generateTypesAPI = ({ dtsManagerOptions: DTSManagerOptions; }) => { const fn = getGenerateTypesFn(dtsManagerOptions); - return fn(dtsManagerOptions); + return fn(dtsManagerOptions).then(async () => { + await callAfterGenerateHook({ + dtsManagerOptions, + generatedTypes: retrieveTypesAssetsInfo(dtsManagerOptions.remote), + }); + }); +}; + +export const callAfterGenerateHook = async ({ + dtsManagerOptions, + generatedTypes, +}: { + dtsManagerOptions: DTSManagerOptions; + generatedTypes: moduleFederationPlugin.DtsGenerateTypesHookOptions; +}) => { + const afterGenerate = dtsManagerOptions.remote.afterGenerate; + + if (!afterGenerate) { + return; + } + + try { + await afterGenerate(generatedTypes); + } catch (error) { + if (dtsManagerOptions.remote.abortOnError === false) { + if (dtsManagerOptions.displayErrorInTerminal) { + logger.error(error); + } + return; + } + + throw error; + } }; const WINDOWS_ABSOLUTE_PATH_REGEXP = /^[a-zA-Z]:[\\/]/; @@ -207,7 +239,6 @@ export class GenerateTypesPlugin implements WebpackPluginInstance { logger.debug('start generating types...'); await generateTypesAPI({ dtsManagerOptions }); logger.debug('generate types success!'); - await dtsOptions.afterGenerateTypes?.(); if (isProd) { if ( diff --git a/packages/enhanced/src/schemas/container/ModuleFederationPlugin.check.ts b/packages/enhanced/src/schemas/container/ModuleFederationPlugin.check.ts index 2cacdd93863..4881c76c975 100644 --- a/packages/enhanced/src/schemas/container/ModuleFederationPlugin.check.ts +++ b/packages/enhanced/src/schemas/container/ModuleFederationPlugin.check.ts @@ -407,6 +407,7 @@ const t = { }, extractRemoteTypes: { type: 'boolean' }, abortOnError: { type: 'boolean' }, + afterGenerate: { instanceof: 'Function' }, }, }, ], @@ -454,7 +455,6 @@ const t = { implementation: { type: 'string' }, cwd: { type: 'string' }, displayErrorInTerminal: { type: 'boolean' }, - afterGenerateTypes: { instanceof: 'Function' }, }, }, ], @@ -3623,7 +3623,7 @@ function D( } else O = !0; - if (O) + if (O) { if ( void 0 !== t.abortOnError @@ -3658,6 +3658,42 @@ function D( } else O = !0; + if (O) + if ( + void 0 !== + t.afterGenerate + ) { + const e = + c; + if ( + !( + t.afterGenerate instanceof + Function + ) + ) { + const e = + { + params: + {}, + }; + (null === + u + ? (u = + [ + e, + ]) + : u.push( + e, + ), + c++); + } + O = + e === + c; + } else + O = + !0; + } } } } @@ -4523,7 +4559,7 @@ function D( } T = t === c; } else T = !0; - if (T) { + if (T) if ( void 0 !== e.displayErrorInTerminal @@ -4545,29 +4581,6 @@ function D( } T = t === c; } else T = !0; - if (T) - if ( - void 0 !== - e.afterGenerateTypes - ) { - const t = c; - if ( - !( - e.afterGenerateTypes instanceof - Function - ) - ) { - const e = { - params: {}, - }; - (null === u - ? (u = [e]) - : u.push(e), - c++); - } - T = t === c; - } else T = !0; - } } } } diff --git a/packages/enhanced/src/schemas/container/ModuleFederationPlugin.json b/packages/enhanced/src/schemas/container/ModuleFederationPlugin.json index be512774a84..066e205da0e 100644 --- a/packages/enhanced/src/schemas/container/ModuleFederationPlugin.json +++ b/packages/enhanced/src/schemas/container/ModuleFederationPlugin.json @@ -780,6 +780,10 @@ }, "abortOnError": { "type": "boolean" + }, + "afterGenerate": { + "description": "Hook called after federated types are generated and before assets are emitted", + "instanceof": "Function" } } } @@ -857,10 +861,6 @@ }, "displayErrorInTerminal": { "type": "boolean" - }, - "afterGenerateTypes": { - "description": "Callback invoked after each type generation completes", - "instanceof": "Function" } } } diff --git a/packages/enhanced/src/schemas/container/ModuleFederationPlugin.ts b/packages/enhanced/src/schemas/container/ModuleFederationPlugin.ts index 90178c7cf42..0d33acb00cb 100644 --- a/packages/enhanced/src/schemas/container/ModuleFederationPlugin.ts +++ b/packages/enhanced/src/schemas/container/ModuleFederationPlugin.ts @@ -864,6 +864,11 @@ export default { abortOnError: { type: 'boolean', }, + afterGenerate: { + description: + 'Hook called after federated types are generated and before assets are emitted', + instanceof: 'Function', + }, }, }, ], @@ -955,11 +960,6 @@ export default { displayErrorInTerminal: { type: 'boolean', }, - afterGenerateTypes: { - description: - 'Callback invoked after each type generation completes', - instanceof: 'Function', - }, }, }, ], diff --git a/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts b/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts index d60f1f88b8f..a9340b21711 100644 --- a/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts +++ b/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts @@ -364,6 +364,13 @@ export interface RemoteTypeUrls { [remoteName: string]: RemoteTypeUrl; } +export interface DtsGenerateTypesHookOptions { + zipTypesPath: string; + apiTypesPath: string; + zipName: string; + apiFileName: string; +} + export interface DtsHostOptions { typesFolder?: string; abortOnError?: boolean; @@ -398,6 +405,9 @@ export interface DtsRemoteOptions { extractRemoteTypes?: boolean; abortOnError?: boolean; deleteTsConfig?: boolean; + afterGenerate?: ( + options: DtsGenerateTypesHookOptions, + ) => Promise | void; } export interface PluginDtsOptions { @@ -408,7 +418,6 @@ export interface PluginDtsOptions { implementation?: string; cwd?: string; displayErrorInTerminal?: boolean; - afterGenerateTypes?: () => void | Promise; } export type AsyncBoundaryOptions = {