Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions .changeset/dts-after-generate-types-callback.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
},
},
},
});
Expand Down
31 changes: 31 additions & 0 deletions apps/website-new/docs/en/configure/dts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ interface DtsRemoteOptions {
extractRemoteTypes?: boolean;
abortOnError?: boolean;
deleteTsConfig?: boolean;
afterGenerate?: (
options: DtsGenerateTypesHookOptions,
) => Promise<void> | void;
}

interface DtsGenerateTypesHookOptions {
zipTypesPath: string;
apiTypesPath: string;
zipName: string;
apiFileName: string;
}
```

Expand Down Expand Up @@ -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> | 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`
Expand Down
31 changes: 31 additions & 0 deletions apps/website-new/docs/zh/configure/dts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ interface DtsRemoteOptions {
extractRemoteTypes?: boolean;
abortOnError?: boolean;
deleteTsConfig?: boolean;
afterGenerate?: (
options: DtsGenerateTypesHookOptions,
) => Promise<void> | void;
}

interface DtsGenerateTypesHookOptions {
zipTypesPath: string;
apiTypesPath: string;
zipName: string;
apiFileName: string;
}
```

Expand Down Expand Up @@ -101,6 +111,27 @@ interface DtsRemoteOptions {

是否抛出错误当类型生成过程中碰到问题

#### afterGenerate

- 类型:`(options: DtsGenerateTypesHookOptions) => Promise<void> | 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`
Expand Down
11 changes: 11 additions & 0 deletions arch-doc/sdk-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,10 @@ interface DtsRemoteOptions {
};
extractRemoteTypes?: boolean;
abortOnError?: boolean;
deleteTsConfig?: boolean;
afterGenerate?: (
options: DtsGenerateTypesHookOptions,
) => Promise<void> | void;
}

interface DtsHostOptions {
Expand All @@ -508,6 +512,13 @@ interface RemoteTypeUrls {
zip: string;
};
}

interface DtsGenerateTypesHookOptions {
zipTypesPath: string;
apiTypesPath: string;
zipName: string;
apiFileName: string;
}
```

### PluginDevOptions
Expand Down
4 changes: 2 additions & 2 deletions packages/dts-plugin/src/core/configurations/remotePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,10 +232,10 @@ const resolveExposes = (remoteOptions: Required<RemoteOptions>) => {
export const retrieveRemoteConfig = (options: RemoteOptions) => {
validateOptions(options);

const remoteOptions: Required<RemoteOptions> = {
const remoteOptions = {
...defaultOptions,
...options,
};
} as Required<RemoteOptions>;
const mapComponentsToExpose = resolveExposes(remoteOptions);
const tsConfig = readTsConfig(remoteOptions, mapComponentsToExpose);

Expand Down
86 changes: 69 additions & 17 deletions packages/dts-plugin/src/plugins/GenerateTypesPlugin.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import path from 'path';
import { describe, expect, it, vi, beforeEach } from 'vitest';
import {
callAfterGenerateHook,
isSafeRelativePath,
normalizeGenerateTypesOptions,
resolveEmitAssetName,
Expand Down Expand Up @@ -67,7 +68,7 @@ function createMockCompiler() {
};
}

describe('afterGenerateTypes callback', () => {
describe('afterGenerate callback', () => {
const basePluginOptions = {
name: 'testRemote',
filename: 'remoteEntry.js',
Expand All @@ -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();

Expand All @@ -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(),
);
Expand Down Expand Up @@ -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();
});
});
});
35 changes: 33 additions & 2 deletions packages/dts-plugin/src/plugins/GenerateTypesPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]:[\\/]/;
Expand Down Expand Up @@ -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 (
Expand Down
Loading
Loading