Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
6 changes: 6 additions & 0 deletions .changeset/sweet-cats-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@module-federation/metro': patch
---

Add optional dts-plugin support for Metro remotes. When `dts` is enabled in `withModuleFederation` config, `bundle-mf-remote` can generate `@mf-types.zip` / `@mf-types.d.ts` and will populate `mf-manifest.json` `metaData.types` when those files are produced.

Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ module.exports = withModuleFederation(
mergeConfig(getDefaultConfig(__dirname), config),
{
name: 'MiniApp',
// Optional: enable federated type generation for TS projects
// Generates `@mf-types.zip` (and `@mf-types.d.ts` when enabled) during `bundle-mf-remote`
// and writes filenames into `mf-manifest.json` under `metaData.types`.
dts: true,
exposes: {
'./Screen': './src/Screen.tsx',
'./Component': './src/Component.tsx',
Expand Down Expand Up @@ -244,6 +248,7 @@ export interface ModuleFederationConfig {
* Scheduled for removal in the next major version.
*/
plugins?: string[];
dts?: boolean | PluginDtsOptions;
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ module.exports = withModuleFederation(
mergeConfig(getDefaultConfig(__dirname), config),
{
name: 'MiniApp',
// 可选:为 TS 项目启用联邦类型生成
// 执行 `bundle-mf-remote` 时生成 `@mf-types.zip`(以及启用时的 `@mf-types.d.ts`)
// 并将文件名写入 `mf-manifest.json` 的 `metaData.types`。
dts: true,
exposes: {
'./Screen': './src/Screen.tsx',
'./Component': './src/Component.tsx',
Expand Down Expand Up @@ -246,6 +250,7 @@ export interface ModuleFederationConfig {
* 计划在下一个 major 版本移除。
*/
plugins?: string[];
dts?: boolean | PluginDtsOptions;
}
```

Expand Down
37 changes: 37 additions & 0 deletions packages/metro-core/__tests__/plugin/normalize-options.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ vi.mock('node:fs', () => {
import { normalizeOptions } from '../../src/plugin/normalize-options';

let projectCount = 0;
const DYNAMIC_REMOTE_TYPE_HINTS_PLUGIN =
'@module-federation/dts-plugin/dynamic-remote-type-hints-plugin';

function createProjectRoot() {
projectCount += 1;
Expand Down Expand Up @@ -48,6 +50,23 @@ describe('normalizeOptions', () => {
vi.restoreAllMocks();
});

it('defaults dts to false and does not inject type-hints plugin', () => {
const projectRoot = createProjectRoot();
const tmpDirPath = path.join(projectRoot, 'node_modules', '.mf');
vol.mkdirSync(tmpDirPath, { recursive: true });

const normalized = normalizeOptions(
{
name: 'MetroHost',
shared: getShared(),
} as any,
{ projectRoot, tmpDirPath },
);

expect(normalized.dts).toBe(false);
expect(normalized.plugins).not.toContain(DYNAMIC_REMOTE_TYPE_HINTS_PLUGIN);
});

it('supports runtimePlugins as the primary config field', () => {
const projectRoot = createProjectRoot();
const tmpDirPath = path.join(projectRoot, 'node_modules', '.mf');
Expand Down Expand Up @@ -109,4 +128,22 @@ describe('normalizeOptions', () => {
path.relative(tmpDirPath, runtimePluginTwoPath),
]);
});

it('keeps dts enabled without injecting extra runtime plugins', () => {
const projectRoot = createProjectRoot();
const tmpDirPath = path.join(projectRoot, 'node_modules', '.mf');
vol.mkdirSync(tmpDirPath, { recursive: true });

const normalized = normalizeOptions(
{
name: 'MetroHost',
shared: getShared(),
dts: true,
} as any,
{ projectRoot, tmpDirPath },
);

expect(normalized.dts).toBe(true);
expect(normalized.plugins).not.toContain(DYNAMIC_REMOTE_TYPE_HINTS_PLUGIN);
});
});
1 change: 1 addition & 0 deletions packages/metro-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
},
"dependencies": {
"@expo/metro-runtime": "^5.0.4",
"@module-federation/dts-plugin": "workspace:*",
"@module-federation/runtime": "workspace:*",
"@module-federation/sdk": "workspace:*"
},
Expand Down
160 changes: 158 additions & 2 deletions packages/metro-core/src/commands/bundle-remote/index.ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm if this is inside bundle-remote then DTS works only when running bundle remote, is this the desired behavior?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, intentional for now.

DTS generation here targets remote container build artifacts (@mf-types.zip, @mf-types.d.ts, and mf-manifest.json type metadata), so it is bound to bundle-mf-remote. I made that explicit in 9c9e3c644 by renaming the helper to maybeGenerateFederatedRemoteTypes and adding an intent comment at the call site.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from 'node:path';
import { pathToFileURL } from 'node:url';
import util from 'node:util';
import { mergeConfig } from 'metro';
import type { moduleFederationPlugin } from '@module-federation/sdk';
import type { ModuleFederationConfigNormalized } from '../../types';
import { CLIError } from '../../utils/errors';
import type { OutputOptions, RequestOptions } from '../../utils/metro-compat';
Expand All @@ -14,6 +15,15 @@ import loadMetroConfig from '../utils/load-metro-config';
import { saveBundleAndMap } from '../utils/save-bundle-and-map';

import type { BundleFederatedRemoteArgs } from './types';
import {
generateTypesAPI,
normalizeDtsOptions,
normalizeGenerateTypesOptions,
} from '@module-federation/dts-plugin';
import {
consumeTypes,
retrieveTypesAssetsInfo,
} from '@module-federation/dts-plugin/core';

const DEFAULT_OUTPUT = 'dist';

Expand Down Expand Up @@ -43,6 +53,130 @@ interface BundleRequestOptions extends RequestOptions {
sourceUrl: string;
}

async function maybeGenerateFederatedTypes(opts: {
federationConfig: ModuleFederationConfigNormalized;
projectRoot: string;
outputDir: string;
logger: Pick<Console, 'info' | 'warn'>;
}): Promise<{ zipName: string; apiFileName: string } | undefined> {
const { federationConfig, projectRoot, outputDir, logger } = opts;

if (federationConfig.dts === false) {
return;
}

// Safer default for Metro: enable generateTypes, but do not fetch remote types unless
// explicitly configured via `dts: { consumeTypes: ... }`.
const dtsConfig =
federationConfig.dts === true
? { consumeTypes: false }
: federationConfig.dts;

const mfOptions: moduleFederationPlugin.ModuleFederationPluginOptions = {
name: federationConfig.name,
filename: federationConfig.filename,
remotes: federationConfig.remotes,
exposes: federationConfig.exposes,
shared: federationConfig.shared as any,
dts: dtsConfig as any,
};

const normalizedDtsOptions = normalizeDtsOptions(mfOptions, projectRoot, {
defaultGenerateOptions: {
generateAPITypes: true,
compileInChildProcess: false,
abortOnError: false,
extractThirdParty: false,
extractRemoteTypes: false,
},
defaultConsumeOptions: {
abortOnError: true,
consumeAPITypes: true,
},
});

if (!normalizedDtsOptions) {
return;
}

const dtsManagerOptions = normalizeGenerateTypesOptions({
context: projectRoot,
outputDir,
dtsOptions: normalizedDtsOptions,
pluginOptions: mfOptions,
});

if (!dtsManagerOptions) {
return;
}

// If the remote consumes types from other remotes, fetch first so generateTypes can succeed.
if (dtsManagerOptions.host) {
let remoteTypeUrls = dtsManagerOptions.host.remoteTypeUrls as any;
if (typeof remoteTypeUrls === 'function') {
remoteTypeUrls = await remoteTypeUrls();
}
await consumeTypes({
...dtsManagerOptions,
host: {
...dtsManagerOptions.host,
remoteTypeUrls,
},
});
}

logger.info(`${util.styleText('blue', 'Generating federated types (d.ts)')}`);
await generateTypesAPI({ dtsManagerOptions });

const { zipTypesPath, apiTypesPath, zipName, apiFileName } =
retrieveTypesAssetsInfo(dtsManagerOptions.remote);

const produced: { zipName?: string; apiFileName?: string } = {};
const fileExists = async (p: string) => {
try {
await fs.stat(p);
return true;
} catch {
return false;
}
};

if (zipTypesPath && zipName && (await fileExists(zipTypesPath))) {
produced.zipName = zipName;
}
if (apiTypesPath && apiFileName && (await fileExists(apiTypesPath))) {
produced.apiFileName = apiFileName;
}
if (process.env['FEDERATION_DEBUG']) {
logger.info(
`dts debug: zipTypesPath=${zipTypesPath} zipExists=${String(
Boolean(produced.zipName),
)} apiTypesPath=${apiTypesPath} apiExists=${String(
Boolean(produced.apiFileName),
)}`,
);
}

if (!produced.zipName && !produced.apiFileName) {
logger.warn(
`${util.styleText('yellow', 'Federated types enabled, but no types files were produced.')}`,
);
return;
}

logger.info(
`Done writing federated types:\n${util.styleText(
'dim',
[produced.zipName, produced.apiFileName].filter(Boolean).join('\n'),
)}`,
);

return {
zipName: produced.zipName ?? '',
apiFileName: produced.apiFileName ?? '',
};
}

async function buildBundle(server: Server, requestOpts: BundleRequestOptions) {
const bundle = await server.build({
...Server.DEFAULT_BUNDLE_OPTIONS,
Expand Down Expand Up @@ -338,9 +472,31 @@ async function bundleFederatedRemote(
// );
}

logger.info(`${util.styleText('blue', 'Processing manifest')}`);
const manifestOutputFilepath = path.resolve(outputDir, 'mf-manifest.json');
await fs.copyFile(manifestFilepath, manifestOutputFilepath);

const typesMeta = await maybeGenerateFederatedTypes({
federationConfig,
projectRoot: config.projectRoot,
outputDir,
logger,
});

logger.info(`${util.styleText('blue', 'Processing manifest')}`);
const rawManifest = JSON.parse(
await fs.readFile(manifestFilepath, 'utf-8'),
);
if (typesMeta?.zipName || typesMeta?.apiFileName) {
rawManifest.metaData = rawManifest.metaData || {};
rawManifest.metaData.types = rawManifest.metaData.types || {};
if (typesMeta.zipName) rawManifest.metaData.types.zip = typesMeta.zipName;
if (typesMeta.apiFileName)
rawManifest.metaData.types.api = typesMeta.apiFileName;
}
await fs.writeFile(
manifestOutputFilepath,
JSON.stringify(rawManifest, undefined, 2),
'utf-8',
);
logger.info(
`Done writing MF Manifest to:\n${util.styleText('dim', manifestOutputFilepath)}`,
);
Expand Down
13 changes: 8 additions & 5 deletions packages/metro-core/src/plugin/normalize-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export function normalizeOptions(
shared,
shareStrategy,
plugins,
dts: options.dts ?? false,
};
}

Expand Down Expand Up @@ -115,11 +116,13 @@ function getNormalizedPlugins(
];

const deduplicatedPlugins = Array.from(new Set(allPlugins));

// make paths relative to the tmp dir
return deduplicatedPlugins.map((pluginPath) =>
path.relative(tmpDirPath, pluginPath),
);
// Make file paths relative to the tmp dir; keep bare package specifiers as-is.
return deduplicatedPlugins.map((pluginPath) => {
if (path.isAbsolute(pluginPath) || pluginPath.startsWith('.')) {
return path.relative(tmpDirPath, pluginPath);
}
return pluginPath;
});
}

function getNormalizedRuntimePlugins(
Expand Down
1 change: 0 additions & 1 deletion packages/metro-core/src/plugin/validate-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const unsupportedTopLevelOptions: (keyof ModuleFederationConfig)[] = [
'implementation',
'manifest',
'dev',
'dts',
'dataPrefetch',
'virtualRuntimeEntry',
'experiments',
Expand Down
6 changes: 6 additions & 0 deletions packages/metro-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ export interface ModuleFederationConfig
* @deprecated Use runtimePlugins instead. Scheduled for removal in the next major version.
*/
plugins?: string[];
/**
* Federated types (d.ts) support. When enabled, Metro bundle commands can
* generate `@mf-types.zip` and `@mf-types.d.ts` for consumption by hosts.
*/
dts?: boolean | moduleFederationPlugin.PluginDtsOptions;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to redeclare it here, it should be inherited from the SDK type

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, removed in 9c9e3c644.

ModuleFederationConfig now only inherits dts from moduleFederationPlugin.ModuleFederationPluginOptions (no local redeclaration).

}

export type ShareObject = Record<string, moduleFederationPlugin.SharedConfig>;
Expand All @@ -18,6 +23,7 @@ export interface ModuleFederationConfigNormalized {
shared: ShareObject;
shareStrategy: moduleFederationPlugin.SharedStrategy;
plugins: string[];
dts: boolean | moduleFederationPlugin.PluginDtsOptions;
}

export type ModuleFederationExtraOptions = {
Expand Down
Loading
Loading