diff --git a/.changeset/config.json b/.changeset/config.json index 48ecd9e0391..6a62d9a7350 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -26,7 +26,8 @@ "@module-federation/error-codes", "@module-federation/inject-external-runtime-core-plugin", "@module-federation/runtime-core", - "create-module-federation" + "create-module-federation", + "@module-federation/cli" ] ], "ignorePatterns": ["^alpha|^beta"], diff --git a/apps/website-new/docs/en/configure/dts.mdx b/apps/website-new/docs/en/configure/dts.mdx index 4d7e29604a2..a007a500768 100644 --- a/apps/website-new/docs/en/configure/dts.mdx +++ b/apps/website-new/docs/en/configure/dts.mdx @@ -209,6 +209,75 @@ Before loading type files, whether to delete the previously loaded `typesFolder` `typesFolder` corresponding to `remotes` directory configuration +#### remoteTypeUrls + +- Type: `(() => Promise) | RemoteTypeUrls` +- Required: No +- Default value:`undefined` + +Used for getting the address of the `remote` type file. + +Application scenarios: + +* Only the runtime API is used to load the producer, and no build plugin is used. The MF type file address is informed by creating a `module-federation.config.ts` configuration file and setting this configuration. + +```ts title="module-federation.config.ts" +import { createModuleFederationConfig, type moduleFederationPlugin } from '@module-federation/enhanced'; + +export default createModuleFederationConfig({ + // ... + remotes: { + 'remote1-alias': 'remote1@http://localhost:80801/remoteEntry.js' + }, + dts:{ + consumeTypes:{ + remoteTypeUrls: async()=>{ + // Simulate the request interface to obtain the type file address + const data = await new Promise(resolve=>{ + setTimeout(()=>{ + resolve({ + remote1:{ + alias: 'remote1-alias', + api:'http://localhost:8081/custom-dir/@mf-types.d.ts', + zip:'http://localhost:8081/custom-dir/@mf-types.zip' + } + } ) + },1000) + }); + + return data; + } + } + } +}); +``` + + +* When remote is `remoteEntry.js`, the type file address usually directly replaces the js file with the corresponding type file, such as `@mf-types.zip`, but the actual uploaded type file address is not this, so you can tell MF the real type file address by setting this configuration. + +```ts title="module-federation.config.ts" +import { createModuleFederationConfig } from '@module-federation/enhanced'; + +export default createModuleFederationConfig({ + // ... + remotes: { + 'remote1-alias': 'remote1@http://localhost:80801/remoteEntry.js' + }, + dts:{ + consumeTypes:{ + remoteTypeUrls: { + // remote name + remote1:{ + alias: 'remote1-alias', + api:'http://localhost:8081/custom-dir/@mf-types.d.ts', + zip:'http://localhost:8081/custom-dir/@mf-types.zip' + } + } + } + } +}); +``` + ### tsConfigPath - Type: `string` diff --git a/apps/website-new/docs/en/guide/basic/_meta.json b/apps/website-new/docs/en/guide/basic/_meta.json index 0c6e0164841..70b2cb33df2 100644 --- a/apps/website-new/docs/en/guide/basic/_meta.json +++ b/apps/website-new/docs/en/guide/basic/_meta.json @@ -1 +1 @@ -["runtime", "rsbuild", "rspack", "webpack", "vite", "chrome-devtool", "type-prompt"] +["runtime", "rsbuild", "rspack", "webpack", "vite", "chrome-devtool", "type-prompt","cli"] diff --git a/apps/website-new/docs/en/guide/basic/cli.mdx b/apps/website-new/docs/en/guide/basic/cli.mdx new file mode 100644 index 00000000000..fd9ce752a95 --- /dev/null +++ b/apps/website-new/docs/en/guide/basic/cli.mdx @@ -0,0 +1,64 @@ +# CLI + +`@module-federation/enhanced` and `@module-federation/modern-js` provides a lightweight CLI. + +## All commands + +To view all available CLI commands, run the following command in the project directory: + +```bash +npx mf -h +``` + +The output is shown below: + +```text +Usage: mf [options] + +Options: + -V, --version output the version number + -h, --help display help for command + +Commands: + dts [options] generate or fetch the mf types + help [command] display help for command +``` + +## Common flags + +Module Federation CLI provides several common flags that can be used with all commands: + +| Flag | Description | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| `-c, --config ` | Specify the configuration file, can be a relative or absolute path, default value is `module-federation.ts` | +| `-m, --mode ` | Specify the runtime environment. You can choose "dev" or "prod". The default value is "dev". After setting, the `process.env.NODE_ENV` environment variable will be automatically injected with "development" or "production" according to the value. | +| `-h, --help` | Display help for command | + +## mf dts + +The `mf dts` command is used to generate or fetch remote types. + +```bash +Usage: mf dts [options] + +generate or fetch the mf types + +Options: + --root specify the project root directory + --output specify the generated dts output directory + --fetch fetch types from remote, default is true (default: true) + --generate generate types, default is true (default: true) + -c --config specify the configuration file, can be a relative or absolute path + -m --mode Specify the runtime environment. You can choose "dev" or "prod". The default value is "dev". After setting, the process.env.NODE_ENV environment variable will be + automatically injected with "development" or "production" according to the value. (default: "dev") + -h, --help display help for command +``` + +:::info Note + +The `mf dts` command will automatically generate or pull type declaration files based on the configuration in `module-federation.config.ts`. This means you must provide a valid configuration file, otherwise the command will not work properly. + +If you only use the runtime API, you need to create a temporary `module-federation.config.ts` file, write the remote configuration you need to get the type, and then run the `mf dts` command. +If you are only using the runtime API, you need to create a temporary `module-federation.config.ts` file, configure [dts.consumeTypes.remoteTypeUrls](../../configure/dts#remotetypeurls), and then run the `mf dts` command. + +::: \ No newline at end of file diff --git a/apps/website-new/docs/zh/configure/dts.mdx b/apps/website-new/docs/zh/configure/dts.mdx index a3445e08f52..a0253e86325 100644 --- a/apps/website-new/docs/zh/configure/dts.mdx +++ b/apps/website-new/docs/zh/configure/dts.mdx @@ -209,6 +209,74 @@ interface DtsHostOptions { 对应 `remotes` 目录配置的 `typesFolder` +#### remoteTypeUrls + +- 类型:`(() => Promise) | RemoteTypeUrls` +- 是否必填:否 +- 默认值:`undefined` + +用于获取 `remote` 类型文件的地址。 + +应用场景: + +* 只使用了 runtime API 加载生产者,没使用构建插件,通过创建 `module-federation.config.ts` 配置文件,并设置此配置,来告知 MF 类型文件地址。 + +```ts title="module-federation.config.ts" +import { createModuleFederationConfig, type moduleFederationPlugin } from '@module-federation/enhanced'; + +export default createModuleFederationConfig({ + // ... + remotes: { + 'remote1-alias': 'remote1@http://localhost:80801/remoteEntry.js' + }, + dts:{ + consumeTypes:{ + remoteTypeUrls: async()=>{ + // 模拟请求接口获取类型文件地址 + const data = await new Promise(resolve=>{ + setTimeout(()=>{ + resolve({ + remote1:{ + alias: 'remote1-alias', + api:'http://localhost:8081/custom-dir/@mf-types.d.ts', + zip:'http://localhost:8081/custom-dir/@mf-types.zip' + } + } ) + },1000) + }); + + return data; + } + } + } +}); +``` + +* 当 remote 为 `remoteEntry.js` 时,类型文件地址通常会直接替换 js 文件为对应的类型文件,例如 `@mf-types.zip` ,但是实际上传的类型文件地址不是这个,那么可以通过设置此配置来告知 MF 真正的类型文件地址。 + +```ts title="module-federation.config.ts" +import { createModuleFederationConfig } from '@module-federation/enhanced'; + +export default createModuleFederationConfig({ + // ... + remotes: { + 'remote1-alias': 'remote1@http://localhost:80801/remoteEntry.js' + }, + dts:{ + consumeTypes:{ + remoteTypeUrls: { + // remote name + remote1:{ + alias: 'remote1-alias', + api:'http://localhost:8081/custom-dir/@mf-types.d.ts', + zip:'http://localhost:8081/custom-dir/@mf-types.zip' + } + } + } + } +}); +``` + ### tsConfigPath - 类型:`string` diff --git a/apps/website-new/docs/zh/guide/basic/_meta.json b/apps/website-new/docs/zh/guide/basic/_meta.json index 0c6e0164841..70b2cb33df2 100644 --- a/apps/website-new/docs/zh/guide/basic/_meta.json +++ b/apps/website-new/docs/zh/guide/basic/_meta.json @@ -1 +1 @@ -["runtime", "rsbuild", "rspack", "webpack", "vite", "chrome-devtool", "type-prompt"] +["runtime", "rsbuild", "rspack", "webpack", "vite", "chrome-devtool", "type-prompt","cli"] diff --git a/apps/website-new/docs/zh/guide/basic/cli.mdx b/apps/website-new/docs/zh/guide/basic/cli.mdx new file mode 100644 index 00000000000..0889b09abbc --- /dev/null +++ b/apps/website-new/docs/zh/guide/basic/cli.mdx @@ -0,0 +1,63 @@ +# 命令行工具 + +`@module-federation/enhanced` 和 `@module-federation/modern-js` 提供了轻量的命令行工具。 + +## 查看所有命令 + +如果你需要查看所有可用的 CLI 命令,请在项目目录中运行以下命令: + +```bash +npx mf -h +``` + +输出如下: + +```text +Usage: mf [options] + +Options: + -V, --version output the version number + -h, --help display help for command + +Commands: + dts [options] generate or fetch the mf types + help [command] display help for command +``` + +## 公共选项 + +Module Federation CLI 提供了一些公共选项,可以用于所有命令: + +| 选项 | 描述 | +| -------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `-c, --config ` | 指定配置文件路径,可以为相对路径或绝对路径,默认值为 `module-federation.config.ts` | +| `-m, --mode ` | 指定运行环境,可以选择 "dev" 或 "prod" ,默认值为 "dev" ,设置后会按照值自动往 `process.env.NODE_ENV` 环境变量注入 "development" 或 "production" | +| `-h, --help` | 显示命令帮助 | + +## mf dts + +`mf dts` 命令用于拉取或生成 TypeScript 类型声明文件。 + +```bash +Usage: mf dts [options] + +generate or fetch the mf types + +Options: + --root specify the project root directory + --output specify the generated dts output directory + --fetch fetch types from remote, default is true (default: true) + --generate generate types, default is true (default: true) + -c --config specify the configuration file, can be a relative or absolute path + -m --mode Specify the runtime environment. You can choose "dev" or "prod". The default value is "dev". After setting, the process.env.NODE_ENV environment variable will be + automatically injected with "development" or "production" according to the value. (default: "dev") + -h, --help display help for command +``` + +:::info 注意 + +`mf dts` 命令会自动根据 `module-federation.config.ts` 中的配置生成或拉取类型声明文件。 这意味着你必须提供一个有效的配置文件,否则命令将无法正常运行。 + +如果你是只使用了 runtime API,那么你需要创建一份临时的 `module-federation.config.ts` 文件,配置 [dts.consumeTypes.remoteTypeUrls](../../configure/dts#remotetypeurls),然后运行 `mf dts` 命令。 + +::: \ No newline at end of file diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md new file mode 100644 index 00000000000..70a6e7d0a4d --- /dev/null +++ b/packages/cli/CHANGELOG.md @@ -0,0 +1 @@ +# @module-federation/cli diff --git a/packages/cli/LICENSE b/packages/cli/LICENSE new file mode 100644 index 00000000000..ed1b325d093 --- /dev/null +++ b/packages/cli/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025-present hanric(2heal1) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 00000000000..54444c195d1 --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,15 @@ +

+ Module Federation 2.0 +

+ +# @module-federation/cli + +Module Federation CLI + +

+ + npm version + + license + downloads +

diff --git a/packages/cli/bin/mf.js b/packages/cli/bin/mf.js new file mode 100755 index 00000000000..62f70a3dd05 --- /dev/null +++ b/packages/cli/bin/mf.js @@ -0,0 +1,4 @@ +#!/usr/bin/env node +const { runCli } = require('../dist/index.cjs.js'); + +runCli(); diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 00000000000..8b9b0d05315 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,42 @@ +{ + "name": "@module-federation/cli", + "version": "0.10.0", + "type": "commonjs", + "description": "Module Federation CLI", + "homepage": "https://module-federation.io", + "bugs": { + "url": "https://github.com/module-federation/core/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/module-federation/core", + "directory": "packages/cli" + }, + "bin": { + "mf": "./bin/mf.js" + }, + "license": "MIT", + "main": "./dist/index.cjs.js", + "files": [ + "dist", + "bin" + ], + "dependencies": { + "@module-federation/sdk": "workspace:*", + "@module-federation/dts-plugin": "workspace:*", + "commander": "11.1.0", + "chalk": "3.0.0", + "@modern-js/node-bundle-require": "2.65.1" + }, + "devDependencies": { + "@types/node": "~16.11.7" + }, + "engines": { + "node": ">=16.0.0" + }, + "publishConfig": { + "access": "public", + "provenance": true, + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/cli/project.json b/packages/cli/project.json new file mode 100644 index 00000000000..879859f75ab --- /dev/null +++ b/packages/cli/project.json @@ -0,0 +1,62 @@ +{ + "name": "cli", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/cli/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nx/rollup:rollup", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "packages/cli/dist", + "main": "packages/cli/src/index.ts", + "tsConfig": "packages/cli/tsconfig.json", + "assets": [], + "project": "packages/cli/package.json", + "rollupConfig": "packages/cli/rollup.config.js", + "compiler": "swc", + "format": ["cjs"], + "generatePackageJson": false + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": [ + "packages/cli/**/*.ts", + "packages/cli/package.json" + ] + } + }, + "test": { + "executor": "nx:run-commands", + "options": { + "parallel": false, + "commands": [ + { + "command": "echo 'waiting for adding test case...'", + "forwardAllArgs": false + } + ] + } + }, + "pre-release": { + "executor": "nx:run-commands", + "options": { + "parallel": false, + "commands": [ + { + "command": "nx run cli:test", + "forwardAllArgs": false + }, + { + "command": "nx run cli:build", + "forwardAllArgs": false + } + ] + } + } + }, + "tags": ["type:pkg"] +} diff --git a/packages/cli/rollup.config.js b/packages/cli/rollup.config.js new file mode 100644 index 00000000000..56fc1d396bf --- /dev/null +++ b/packages/cli/rollup.config.js @@ -0,0 +1,21 @@ +const copy = require('rollup-plugin-copy'); +const replace = require('@rollup/plugin-replace'); +const pkg = require('./package.json'); + +module.exports = (rollupConfig, _projectOptions) => { + rollupConfig.plugins.push( + replace({ + __VERSION__: JSON.stringify(pkg.version), + }), + copy({ + targets: [ + { + src: 'packages/cli/LICENSE', + dest: 'packages/cli/dist', + }, + ], + }), + ); + + return rollupConfig; +}; diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts new file mode 100644 index 00000000000..6385bafec29 --- /dev/null +++ b/packages/cli/src/cli.ts @@ -0,0 +1,72 @@ +import { program } from 'commander'; +import { applyCommonOptions } from './utils/apply-common-options'; +import { dts } from './commands/dts'; +import { prepareCli } from './utils/prepare'; +import logger, { PREFIX } from './utils/logger'; +import { readConfig } from './utils/readConfig'; + +import type { DtsOptions, CliOptions } from './types'; + +function cli(cliOptions: Required): void { + const { name, version, applyCommands } = cliOptions; + program.name(name).usage(' [options]').version(version); + + const dtsCommand = program.command('dts'); + + dtsCommand + .option('--root ', 'specify the project root directory') + .option('--output ', 'specify the generated dts output directory') + .option( + '--fetch ', + 'fetch types from remote, default is true', + (value) => value === 'true', + true, + ) + .option( + '--generate ', + 'generate types, default is true', + (value) => value === 'true', + true, + ); + + applyCommonOptions(dtsCommand); + + dtsCommand + .description('generate or fetch the mf types') + .action(async (options: DtsOptions) => { + try { + await dts(options, cliOptions); + } catch (err) { + console.error(err); + process.exit(1); + } + }); + + if (typeof applyCommands === 'function') { + applyCommands(program, applyCommonOptions); + } + + program.parse(); +} + +export function runCli(options: CliOptions) { + const normalizedOptions: Required = { + loggerPrefix: PREFIX, + welcomeMsg: `${`Module Federation v${__VERSION__}`}\n`, + name: 'mf', + readConfig, + version: __VERSION__, + applyCommands: () => {}, + ...options, + }; + + logger.setPrefix(normalizedOptions.loggerPrefix); + + prepareCli(normalizedOptions); + + try { + cli(normalizedOptions); + } catch (err) { + console.error(err); + } +} diff --git a/packages/cli/src/commands/dts.ts b/packages/cli/src/commands/dts.ts new file mode 100644 index 00000000000..009f3770ea7 --- /dev/null +++ b/packages/cli/src/commands/dts.ts @@ -0,0 +1,86 @@ +import { + isTSProject, + normalizeDtsOptions, + generateTypesAPI, + consumeTypesAPI, + normalizeGenerateTypesOptions, + normalizeConsumeTypesOptions, +} from '@module-federation/dts-plugin'; +import logger from '../utils/logger'; + +import type { CliOptions, DtsOptions } from '../types'; + +export async function dts( + options: DtsOptions, + { readConfig }: Required, +) { + const { + fetch = true, + generate = true, + root = process.cwd(), + output, + config, + } = options; + + const mfConfig = await readConfig(config); + + if (!isTSProject(mfConfig.dts, root)) { + logger.error('dts is only supported for TypeScript projects'); + return; + } + + const normalizedDtsOptions = normalizeDtsOptions(mfConfig, root, { + defaultGenerateOptions: { + generateAPITypes: true, + compileInChildProcess: false, + abortOnError: true, + extractThirdParty: false, + extractRemoteTypes: false, + }, + defaultConsumeOptions: { + abortOnError: true, + consumeAPITypes: true, + }, + }); + + if (!normalizedDtsOptions) { + logger.error('dts is not enabled in module-federation.config.ts'); + return; + } + + if (fetch) { + const dtsManagerOptions = normalizeConsumeTypesOptions({ + context: root, + dtsOptions: normalizedDtsOptions, + pluginOptions: mfConfig, + }); + if (!dtsManagerOptions) { + logger.warn( + 'dts.consumeTypes is not enabled in module-federation.config.ts, skip fetching remote types', + ); + } else { + logger.debug('start fetching remote types...'); + await consumeTypesAPI(dtsManagerOptions); + logger.debug('fetch remote types success!'); + } + } + + if (generate) { + const dtsManagerOptions = normalizeGenerateTypesOptions({ + context: root, + outputDir: output, + dtsOptions: normalizedDtsOptions, + pluginOptions: mfConfig, + }); + if (!dtsManagerOptions) { + logger.warn( + 'dts.generateTypes is not enabled in module-federation.config.ts, skip generating types', + ); + return; + } + + logger.debug('start generating types...'); + await generateTypesAPI({ dtsManagerOptions }); + logger.debug('generate types success!'); + } +} diff --git a/packages/cli/src/env.d.ts b/packages/cli/src/env.d.ts new file mode 100644 index 00000000000..415c2c8274f --- /dev/null +++ b/packages/cli/src/env.d.ts @@ -0,0 +1 @@ +declare const __VERSION__: string; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 00000000000..87937f10c30 --- /dev/null +++ b/packages/cli/src/index.ts @@ -0,0 +1 @@ +export { runCli } from './cli'; diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts new file mode 100644 index 00000000000..e187770d4c4 --- /dev/null +++ b/packages/cli/src/types.ts @@ -0,0 +1,28 @@ +import type { Command } from 'commander'; +import type { moduleFederationPlugin } from '@module-federation/sdk'; + +export type CommonOptions = { + config?: string; + mode?: string; +}; + +export type DtsOptions = { + fetch?: boolean; + generate?: boolean; + root?: string; + output?: string; +} & CommonOptions; + +export type CliOptions = { + welcomeMsg?: string; + loggerPrefix?: string; + name?: string; + version?: string; + readConfig?: ( + userConfigPath?: string, + ) => Promise; + applyCommands?: ( + command: Command, + applyCommonOptions: (command: Command) => void, + ) => void; +}; diff --git a/packages/cli/src/utils/apply-common-options.ts b/packages/cli/src/utils/apply-common-options.ts new file mode 100644 index 00000000000..0e8be2490f1 --- /dev/null +++ b/packages/cli/src/utils/apply-common-options.ts @@ -0,0 +1,30 @@ +import type { Command } from 'commander'; + +export const applyCommonOptions = (command: Command) => { + command.option( + '-c --config ', + 'specify the configuration file, can be a relative or absolute path', + ); + command.option( + '-m --mode ', + 'Specify the runtime environment. You can choose "dev" or "prod". The default value is "dev". After setting, the process.env.NODE_ENV environment variable will be automatically injected with "development" or "production" according to the value.', + (value) => { + const validChoices = { + dev: 'development', + prod: 'production', + }; + if (!Object.keys(validChoices).includes(value)) { + throw new Error( + `Invalid mode: ${value}. Valid choices are: ${Object.keys(validChoices).join(', ')}`, + ); + } + const targetEnv = validChoices[value as keyof typeof validChoices]; + if (process.env.NODE_ENV !== targetEnv) { + console.log(`process.env.NODE_ENV is set to ${targetEnv}`); + } + process.env.NODE_ENV = targetEnv; + return value; + }, + 'dev', + ); +}; diff --git a/packages/cli/src/utils/logger.ts b/packages/cli/src/utils/logger.ts new file mode 100644 index 00000000000..eba6e5c7b5e --- /dev/null +++ b/packages/cli/src/utils/logger.ts @@ -0,0 +1,8 @@ +import { type Logger, createLogger } from '@module-federation/sdk'; +import chalk from 'chalk'; + +export const PREFIX = '[ Module Federation CLI ]'; + +const logger: Logger = createLogger(chalk`{bold {cyan ${PREFIX}}}`); + +export default logger; diff --git a/packages/cli/src/utils/prepare.ts b/packages/cli/src/utils/prepare.ts new file mode 100644 index 00000000000..6c3791a33bc --- /dev/null +++ b/packages/cli/src/utils/prepare.ts @@ -0,0 +1,20 @@ +import logger from './logger'; +import type { CliOptions } from '../types'; + +export function prepareCli({ welcomeMsg }: Required): void { + if (!process.env.NODE_ENV) { + process.env.NODE_ENV = 'development'; + } + // Print a blank line to keep the greet log nice. + // Some package managers automatically output a blank line, some do not. + const { npm_execpath } = process.env; + if ( + !npm_execpath || + npm_execpath.includes('npx-cli.js') || + npm_execpath.includes('.bun') + ) { + console.log(); + } + + logger.info(welcomeMsg); +} diff --git a/packages/cli/src/utils/readConfig.ts b/packages/cli/src/utils/readConfig.ts new file mode 100644 index 00000000000..ebbcb78e4a8 --- /dev/null +++ b/packages/cli/src/utils/readConfig.ts @@ -0,0 +1,21 @@ +import path from 'path'; +import { bundle } from '@modern-js/node-bundle-require'; +import type { moduleFederationPlugin } from '@module-federation/sdk'; + +const DEFAULT_CONFIG_PATH = 'module-federation.config.ts'; + +export const getConfigPath = (userConfigPath?: string) => { + const defaultPath = path.resolve(process.cwd(), DEFAULT_CONFIG_PATH); + const filepath = userConfigPath ?? defaultPath; + return path.isAbsolute(filepath) + ? filepath + : path.resolve(process.cwd(), filepath); +}; + +export async function readConfig(userConfigPath?: string) { + const configPath = getConfigPath(userConfigPath); + const preBundlePath = await bundle(configPath); + const mfConfig = (await import(preBundlePath)).default + .default as unknown as moduleFederationPlugin.ModuleFederationPluginOptions; + return mfConfig; +} diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 00000000000..fcf1b213e09 --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "rootDir": "./", + "outDir": "dist", + "sourceMap": false, + "module": "commonjs", + "target": "ES2015", + "skipLibCheck": true, + "moduleResolution": "node", + "allowJs": false, + "strict": true, + "types": ["jest", "node"], + "experimentalDecorators": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "removeComments": true, + "declaration": true, + "paths": { + "@/*": ["./*"], + "@src/*": ["./src/*"] + } + }, + "include": ["src", "../../global.d.ts", "__tests__/**/*"], + "exclude": ["node_modules/**/*", "../node_modules"] +} diff --git a/packages/dts-plugin/src/core/configurations/hostPlugin.test.ts b/packages/dts-plugin/src/core/configurations/hostPlugin.test.ts index 8027e8bb7ee..c7aa232927f 100644 --- a/packages/dts-plugin/src/core/configurations/hostPlugin.test.ts +++ b/packages/dts-plugin/src/core/configurations/hostPlugin.test.ts @@ -147,5 +147,62 @@ describe('hostPlugin', () => { }, }); }); + + it('correctly resolve remoteTypeUrls', () => { + const mfConfigWithRemoteTypeUrls = { + ...moduleFederationConfig, + remotes: { + 'remote1-alias': 'remote1@http://localhost:3001/remoteEntry.js', + 'remote2-alias': 'remote2@http://localhost:3002/remoteEntry.js', + }, + }; + + const { mapRemotesToDownload } = retrieveHostConfig({ + moduleFederationConfig: mfConfigWithRemoteTypeUrls, + remoteTypeUrls: { + remote1: { + alias: 'remote1-alias', + zip: 'http://localhost:3001/custom-dir/@mf-types.zip', + api: 'http://localhost:3001/custom-dir/@mf-types.d.ts', + }, + }, + }); + + expect(mapRemotesToDownload).toStrictEqual({ + 'remote1-alias': { + name: 'remote1', + alias: 'remote1-alias', + url: 'http://localhost:3001/remoteEntry.js', + zipUrl: 'http://localhost:3001/custom-dir/@mf-types.zip', + apiTypeUrl: 'http://localhost:3001/custom-dir/@mf-types.d.ts', + }, + 'remote2-alias': { + name: 'remote2', + alias: 'remote2-alias', + url: 'http://localhost:3002/remoteEntry.js', + zipUrl: 'http://localhost:3002/@mf-types.zip', + apiTypeUrl: 'http://localhost:3002/@mf-types.d.ts', + }, + }); + }); + + it('throw error if typeof remoteTypeUrls is not object', () => { + const options = { + moduleFederationConfig, + remoteTypeUrls: () => + Promise.resolve({ + remote1: { + alias: 'remote1-alias', + zip: 'http://localhost:3001/custom-dir/@mf-types.zip', + api: 'http://localhost:3001/custom-dir/@mf-types.d.ts', + }, + }), + }; + + const invokeRetrieve = () => retrieveHostConfig(options); + expect(invokeRetrieve).toThrowError( + 'remoteTypeUrls must be consumed before resolveRemotes', + ); + }); }); }); diff --git a/packages/dts-plugin/src/core/configurations/hostPlugin.ts b/packages/dts-plugin/src/core/configurations/hostPlugin.ts index e10406d416f..19510040d87 100644 --- a/packages/dts-plugin/src/core/configurations/hostPlugin.ts +++ b/packages/dts-plugin/src/core/configurations/hostPlugin.ts @@ -64,10 +64,13 @@ export const retrieveRemoteInfo = (options: { let zipUrl = ''; let apiTypeUrl = ''; const name = parsedInfo.name || remoteAlias; + + // for updated remote if (typeof remoteTypeUrls === 'object' && remoteTypeUrls[name]) { zipUrl = remoteTypeUrls[name].zip; apiTypeUrl = remoteTypeUrls[name].api; } + if (!zipUrl && url) { zipUrl = buildZipUrl(hostOptions, url); } @@ -98,18 +101,45 @@ const resolveRemotes = (hostOptions: Required) => { }), ); - return parsedOptions.reduce( - (accumulator, item) => { - const { key, remote } = item[1]; - accumulator[key] = retrieveRemoteInfo({ - hostOptions, - remoteAlias: key, - remote, - }); - return accumulator; + const remoteTypeUrls = hostOptions.remoteTypeUrls ?? {}; + if (typeof remoteTypeUrls !== 'object') { + throw new Error('remoteTypeUrls must be consumed before resolveRemotes'); + } + const remoteInfos = Object.keys(remoteTypeUrls).reduce( + (sum, remoteName) => { + const { zip, api, alias } = remoteTypeUrls[remoteName]; + sum[alias] = { + name: remoteName, + url: '', + zipUrl: zip, + apiTypeUrl: api, + alias: alias || remoteName, + }; + return sum; }, {} as Record, ); + + return parsedOptions.reduce((accumulator, item) => { + const { key, remote } = item[1]; + const res = retrieveRemoteInfo({ + hostOptions, + remoteAlias: key, + remote, + }); + + if (accumulator[key]) { + accumulator[key] = { + ...accumulator[key], + url: res.url, + apiTypeUrl: accumulator[key].apiTypeUrl || res.apiTypeUrl, + }; + return accumulator; + } + + accumulator[key] = res; + return accumulator; + }, remoteInfos); }; export const retrieveHostConfig = (options: HostOptions) => { diff --git a/packages/dts-plugin/src/core/lib/archiveHandler.ts b/packages/dts-plugin/src/core/lib/archiveHandler.ts index 9975bd902c0..0f47d7defb5 100644 --- a/packages/dts-plugin/src/core/lib/archiveHandler.ts +++ b/packages/dts-plugin/src/core/lib/archiveHandler.ts @@ -8,6 +8,7 @@ import { retrieveMfTypesPath } from './typeScriptCompiler'; import { fileLog } from '../../server'; import { axiosGet } from './utils'; import { TsConfigJson } from '../interfaces/TsConfigJson'; +import { logger } from '../../server'; export const retrieveTypesZipPath = ( mfTypesPath: string, @@ -105,6 +106,9 @@ export const downloadTypesArchive = (hostOptions: Required) => { 'error', ); if (retries >= hostOptions.maxRetries) { + logger.error( + `Failed to download ${fileToDownload}, you can set FEDERATION_DEBUG=true to see detail message.`, + ); if (hostOptions.abortOnError !== false) { throw error; } diff --git a/packages/dts-plugin/src/index.ts b/packages/dts-plugin/src/index.ts index 5c35e25af36..1310ecace5b 100644 --- a/packages/dts-plugin/src/index.ts +++ b/packages/dts-plugin/src/index.ts @@ -1 +1,10 @@ -export { DtsPlugin } from './plugins/DtsPlugin'; +export { DtsPlugin, normalizeDtsOptions } from './plugins/DtsPlugin'; +export { + consumeTypesAPI, + normalizeConsumeTypesOptions, +} from './plugins/ConsumeTypesPlugin'; +export { + generateTypesAPI, + normalizeGenerateTypesOptions, +} from './plugins/GenerateTypesPlugin'; +export { isTSProject } from './core'; diff --git a/packages/dts-plugin/src/plugins/ConsumeTypesPlugin.ts b/packages/dts-plugin/src/plugins/ConsumeTypesPlugin.ts index d36744bbcd0..4b84aecf437 100644 --- a/packages/dts-plugin/src/plugins/ConsumeTypesPlugin.ts +++ b/packages/dts-plugin/src/plugins/ConsumeTypesPlugin.ts @@ -3,15 +3,82 @@ import { normalizeOptions, type moduleFederationPlugin, } from '@module-federation/sdk'; -import { validateOptions, consumeTypes } from '../core/index'; +import { + validateOptions, + consumeTypes, + DTSManagerOptions, +} from '../core/index'; import { isPrd } from './utils'; import type { Compiler, WebpackPluginInstance } from 'webpack'; +export const DEFAULT_CONSUME_TYPES = { + abortOnError: false, + consumeAPITypes: true, +}; + +export const normalizeConsumeTypesOptions = ({ + context, + dtsOptions, + pluginOptions, +}: { + context?: string; + dtsOptions: moduleFederationPlugin.PluginDtsOptions; + pluginOptions: moduleFederationPlugin.ModuleFederationPluginOptions; +}) => { + const normalizedConsumeTypes = + normalizeOptions( + true, + DEFAULT_CONSUME_TYPES, + 'mfOptions.dts.consumeTypes', + )(dtsOptions.consumeTypes); + + if (!normalizedConsumeTypes) { + return; + } + const dtsManagerOptions = { + host: { + implementation: dtsOptions.implementation, + context, + moduleFederationConfig: pluginOptions, + ...normalizedConsumeTypes, + }, + extraOptions: dtsOptions.extraOptions || {}, + displayErrorInTerminal: dtsOptions.displayErrorInTerminal, + }; + validateOptions(dtsManagerOptions.host); + + return dtsManagerOptions; +}; + +export const consumeTypesAPI = async ( + dtsManagerOptions: DTSManagerOptions, + cb?: (options: moduleFederationPlugin.RemoteTypeUrls) => void, +) => { + const fetchRemoteTypeUrlsPromise = + typeof dtsManagerOptions.host.remoteTypeUrls === 'function' + ? dtsManagerOptions.host.remoteTypeUrls() + : Promise.resolve(dtsManagerOptions.host.remoteTypeUrls); + return fetchRemoteTypeUrlsPromise.then((remoteTypeUrls) => { + consumeTypes({ + ...dtsManagerOptions, + host: { + ...dtsManagerOptions.host, + remoteTypeUrls, + }, + }) + .then(() => { + typeof cb === 'function' && cb(remoteTypeUrls); + }) + .catch(() => { + typeof cb === 'function' && cb(remoteTypeUrls); + }); + }); +}; + export class ConsumeTypesPlugin implements WebpackPluginInstance { pluginOptions: moduleFederationPlugin.ModuleFederationPluginOptions; dtsOptions: moduleFederationPlugin.PluginDtsOptions; - defaultOptions: moduleFederationPlugin.DtsHostOptions; callback: () => void; fetchRemoteTypeUrlsResolve: ( options: moduleFederationPlugin.RemoteTypeUrls, @@ -20,74 +87,39 @@ export class ConsumeTypesPlugin implements WebpackPluginInstance { constructor( pluginOptions: moduleFederationPlugin.ModuleFederationPluginOptions, dtsOptions: moduleFederationPlugin.PluginDtsOptions, - defaultOptions: moduleFederationPlugin.DtsHostOptions, fetchRemoteTypeUrlsResolve: ( options: moduleFederationPlugin.RemoteTypeUrls, ) => void, ) { this.pluginOptions = pluginOptions; this.dtsOptions = dtsOptions; - this.defaultOptions = defaultOptions; this.fetchRemoteTypeUrlsResolve = fetchRemoteTypeUrlsResolve; } apply(compiler: Compiler) { - const { - dtsOptions, - defaultOptions, - pluginOptions, - fetchRemoteTypeUrlsResolve, - } = this; + const { dtsOptions, pluginOptions, fetchRemoteTypeUrlsResolve } = this; if (isPrd()) { fetchRemoteTypeUrlsResolve(undefined); return; } - const normalizedConsumeTypes = - normalizeOptions( - true, - defaultOptions, - 'mfOptions.dts.consumeTypes', - )(dtsOptions.consumeTypes); + const dtsManagerOptions = normalizeConsumeTypesOptions({ + context: compiler.context, + dtsOptions, + pluginOptions, + }); - if (!normalizedConsumeTypes) { + if (!dtsManagerOptions) { fetchRemoteTypeUrlsResolve(undefined); return; } - const finalOptions = { - host: { - implementation: dtsOptions.implementation, - context: compiler.context, - moduleFederationConfig: pluginOptions, - ...normalizedConsumeTypes, - }, - extraOptions: dtsOptions.extraOptions || {}, - displayErrorInTerminal: dtsOptions.displayErrorInTerminal, - }; - - validateOptions(finalOptions.host); - const fetchRemoteTypeUrlsPromise = - typeof normalizedConsumeTypes.remoteTypeUrls === 'function' - ? normalizedConsumeTypes.remoteTypeUrls() - : Promise.resolve(normalizedConsumeTypes.remoteTypeUrls); logger.debug('start fetching remote types...'); - const promise = fetchRemoteTypeUrlsPromise.then((remoteTypeUrls) => { - consumeTypes({ - ...finalOptions, - host: { - ...finalOptions.host, - remoteTypeUrls, - }, - }) - .then(() => { - fetchRemoteTypeUrlsResolve(remoteTypeUrls); - }) - .catch(() => { - fetchRemoteTypeUrlsResolve(remoteTypeUrls); - }); - }); + const promise = consumeTypesAPI( + dtsManagerOptions, + fetchRemoteTypeUrlsResolve, + ); compiler.hooks.thisCompilation.tap('mf:generateTypes', (compilation) => { compilation.hooks.processAssets.tapPromise( diff --git a/packages/dts-plugin/src/plugins/DtsPlugin.ts b/packages/dts-plugin/src/plugins/DtsPlugin.ts index 1289b8252d1..9edcefb0a92 100644 --- a/packages/dts-plugin/src/plugins/DtsPlugin.ts +++ b/packages/dts-plugin/src/plugins/DtsPlugin.ts @@ -1,12 +1,40 @@ import { DevPlugin } from './DevPlugin'; import { normalizeOptions } from '@module-federation/sdk'; -import { ConsumeTypesPlugin } from './ConsumeTypesPlugin'; -import { GenerateTypesPlugin } from './GenerateTypesPlugin'; +import { + ConsumeTypesPlugin, + DEFAULT_CONSUME_TYPES, +} from './ConsumeTypesPlugin'; +import { + GenerateTypesPlugin, + DEFAULT_GENERATE_TYPES, +} from './GenerateTypesPlugin'; import { isTSProject } from '../core'; import { type moduleFederationPlugin } from '@module-federation/sdk'; import type { Compiler, WebpackPluginInstance } from 'webpack'; +export const normalizeDtsOptions = ( + options: moduleFederationPlugin.ModuleFederationPluginOptions, + context: string, + defaultOptions?: { + defaultGenerateOptions?: moduleFederationPlugin.DtsRemoteOptions; + defaultConsumeOptions?: moduleFederationPlugin.DtsHostOptions; + }, +) => { + return normalizeOptions( + isTSProject(options.dts, context), + { + generateTypes: + defaultOptions?.defaultGenerateOptions || DEFAULT_GENERATE_TYPES, + consumeTypes: + defaultOptions?.defaultConsumeOptions || DEFAULT_CONSUME_TYPES, + extraOptions: {}, + displayErrorInTerminal: true, + }, + 'mfOptions.dts', + )(options.dts); +}; + export class DtsPlugin implements WebpackPluginInstance { options: moduleFederationPlugin.ModuleFederationPluginOptions; constructor(options: moduleFederationPlugin.ModuleFederationPluginOptions) { @@ -16,25 +44,7 @@ export class DtsPlugin implements WebpackPluginInstance { apply(compiler: Compiler) { const { options } = this; - const defaultGenerateTypes = { - generateAPITypes: true, - compileInChildProcess: true, - abortOnError: false, - extractThirdParty: false, - extractRemoteTypes: false, - }; - const defaultConsumeTypes = { abortOnError: false, consumeAPITypes: true }; - const normalizedDtsOptions = - normalizeOptions( - isTSProject(options.dts, compiler.context), - { - generateTypes: defaultGenerateTypes, - consumeTypes: defaultConsumeTypes, - extraOptions: {}, - displayErrorInTerminal: true, - }, - 'mfOptions.dts', - )(options.dts); + const normalizedDtsOptions = normalizeDtsOptions(options, compiler.context); if (typeof normalizedDtsOptions !== 'object') { return; @@ -67,7 +77,6 @@ export class DtsPlugin implements WebpackPluginInstance { new GenerateTypesPlugin( options, normalizedDtsOptions, - defaultGenerateTypes, fetchRemoteTypeUrlsPromise, generateTypesPromiseResolve, ).apply(compiler); @@ -75,7 +84,6 @@ export class DtsPlugin implements WebpackPluginInstance { new ConsumeTypesPlugin( options, normalizedDtsOptions, - defaultConsumeTypes, fetchRemoteTypeUrlsResolve, ).apply(compiler); } diff --git a/packages/dts-plugin/src/plugins/GenerateTypesPlugin.ts b/packages/dts-plugin/src/plugins/GenerateTypesPlugin.ts index db95db25ece..c014fc29cda 100644 --- a/packages/dts-plugin/src/plugins/GenerateTypesPlugin.ts +++ b/packages/dts-plugin/src/plugins/GenerateTypesPlugin.ts @@ -16,10 +16,93 @@ import { import type { Compilation, Compiler, WebpackPluginInstance } from 'webpack'; +export const DEFAULT_GENERATE_TYPES = { + generateAPITypes: true, + compileInChildProcess: true, + abortOnError: false, + extractThirdParty: false, + extractRemoteTypes: false, +}; + +export const normalizeGenerateTypesOptions = ({ + context, + outputDir, + dtsOptions, + pluginOptions, +}: { + context?: string; + outputDir?: string; + dtsOptions: moduleFederationPlugin.PluginDtsOptions; + pluginOptions: moduleFederationPlugin.ModuleFederationPluginOptions; +}) => { + const normalizedGenerateTypes = + normalizeOptions( + true, + DEFAULT_GENERATE_TYPES, + 'mfOptions.dts.generateTypes', + )(dtsOptions.generateTypes); + + if (!normalizedGenerateTypes) { + return; + } + + const normalizedConsumeTypes = + normalizeOptions( + true, + {}, + 'mfOptions.dts.consumeTypes', + )(dtsOptions.consumeTypes); + + const finalOptions: DTSManagerOptions = { + remote: { + implementation: dtsOptions.implementation, + context, + outputDir, + moduleFederationConfig: pluginOptions, + ...normalizedGenerateTypes, + }, + host: + normalizedConsumeTypes === false + ? undefined + : { + context, + moduleFederationConfig: pluginOptions, + ...normalizedGenerateTypes, + }, + extraOptions: dtsOptions.extraOptions || {}, + displayErrorInTerminal: dtsOptions.displayErrorInTerminal, + }; + + if (dtsOptions.tsConfigPath && !finalOptions.remote.tsConfigPath) { + finalOptions.remote.tsConfigPath = dtsOptions.tsConfigPath; + } + + validateOptions(finalOptions.remote); + + return finalOptions; +}; + +const getGenerateTypesFn = (dtsManagerOptions: DTSManagerOptions) => { + let fn: typeof generateTypes | typeof generateTypesInChildProcess = + generateTypes; + if (dtsManagerOptions.remote.compileInChildProcess) { + fn = generateTypesInChildProcess; + } + return fn; +}; + +export const generateTypesAPI = ({ + dtsManagerOptions, +}: { + dtsManagerOptions: DTSManagerOptions; +}) => { + const fn = getGenerateTypesFn(dtsManagerOptions); + return fn(dtsManagerOptions); +}; + export class GenerateTypesPlugin implements WebpackPluginInstance { pluginOptions: moduleFederationPlugin.ModuleFederationPluginOptions; dtsOptions: moduleFederationPlugin.PluginDtsOptions; - defaultOptions: moduleFederationPlugin.DtsRemoteOptions; fetchRemoteTypeUrlsPromise: Promise< moduleFederationPlugin.DtsHostOptions['remoteTypeUrls'] | undefined >; @@ -28,7 +111,6 @@ export class GenerateTypesPlugin implements WebpackPluginInstance { constructor( pluginOptions: moduleFederationPlugin.ModuleFederationPluginOptions, dtsOptions: moduleFederationPlugin.PluginDtsOptions, - defaultOptions: moduleFederationPlugin.DtsRemoteOptions, fetchRemoteTypeUrlsPromise: Promise< moduleFederationPlugin.DtsHostOptions['remoteTypeUrls'] | undefined >, @@ -36,85 +118,36 @@ export class GenerateTypesPlugin implements WebpackPluginInstance { ) { this.pluginOptions = pluginOptions; this.dtsOptions = dtsOptions; - this.defaultOptions = defaultOptions; this.fetchRemoteTypeUrlsPromise = fetchRemoteTypeUrlsPromise; this.callback = callback; } apply(compiler: Compiler) { - const { + const { dtsOptions, pluginOptions, fetchRemoteTypeUrlsPromise, callback } = + this; + + const outputDir = getCompilerOutputDir(compiler); + const context = compiler.context; + + const dtsManagerOptions = normalizeGenerateTypesOptions({ + context, + outputDir, dtsOptions, - defaultOptions, pluginOptions, - fetchRemoteTypeUrlsPromise, - callback, - } = this; - - const normalizedGenerateTypes = - normalizeOptions( - true, - defaultOptions, - 'mfOptions.dts.generateTypes', - )(dtsOptions.generateTypes); + }); - if (!normalizedGenerateTypes) { + if (!dtsManagerOptions) { callback(); return; } - const normalizedConsumeTypes = - normalizeOptions( - true, - defaultOptions, - 'mfOptions.dts.consumeTypes', - )(dtsOptions.consumeTypes); - - const finalOptions: DTSManagerOptions = { - remote: { - implementation: dtsOptions.implementation, - context: compiler.context, - outputDir: getCompilerOutputDir(compiler), - moduleFederationConfig: pluginOptions, - ...normalizedGenerateTypes, - }, - host: - normalizedConsumeTypes === false - ? undefined - : { - context: compiler.context, - moduleFederationConfig: pluginOptions, - ...normalizedGenerateTypes, - }, - extraOptions: dtsOptions.extraOptions || {}, - displayErrorInTerminal: dtsOptions.displayErrorInTerminal, - }; - - if (dtsOptions.tsConfigPath && !finalOptions.remote.tsConfigPath) { - finalOptions.remote.tsConfigPath = dtsOptions.tsConfigPath; - } - - validateOptions(finalOptions.remote); const isProd = !isDev(); - const getGenerateTypesFn = () => { - let fn: typeof generateTypes | typeof generateTypesInChildProcess = - generateTypes; - let res: ReturnType; - if (finalOptions.remote.compileInChildProcess) { - fn = generateTypesInChildProcess; - } - if (isProd) { - res = fn(finalOptions); - return () => res; - } - return fn; - }; - const generateTypesFn = getGenerateTypesFn(); const emitTypesFiles = async (compilation: Compilation) => { // Dev types will be generated by DevPlugin, the archive filename usually is dist/.dev-server.zip try { const { zipTypesPath, apiTypesPath, zipName, apiFileName } = - retrieveTypesAssetsInfo(finalOptions.remote); + retrieveTypesAssetsInfo(dtsManagerOptions.remote); if (isProd && zipName && compilation.getAsset(zipName)) { callback(); @@ -122,9 +155,9 @@ export class GenerateTypesPlugin implements WebpackPluginInstance { } logger.debug('start generating types...'); - await generateTypesFn(finalOptions); + await generateTypesAPI({ dtsManagerOptions }); logger.debug('generate types success!'); - const config = finalOptions.remote.moduleFederationConfig; + const config = dtsManagerOptions.remote.moduleFederationConfig; let zipPrefix = ''; if (typeof config.manifest === 'object' && config.manifest.filePath) { zipPrefix = config.manifest.filePath; @@ -180,6 +213,7 @@ export class GenerateTypesPlugin implements WebpackPluginInstance { } else { compiler.outputFileSystem.writeFile( zipOutputPath, + // @ts-ignore zipContent, (writeErr) => { if (writeErr && !isEEXIST(writeErr)) { @@ -211,6 +245,7 @@ export class GenerateTypesPlugin implements WebpackPluginInstance { } else { compiler.outputFileSystem.writeFile( apiOutputPath, + // @ts-ignore apiContent, (writeErr) => { if (writeErr && !isEEXIST(writeErr)) { @@ -230,7 +265,7 @@ export class GenerateTypesPlugin implements WebpackPluginInstance { } } catch (err) { callback(); - if (finalOptions.displayErrorInTerminal) { + if (dtsManagerOptions.displayErrorInTerminal) { console.error('Error in mf:generateTypes processAssets hook:', err); } logger.debug('generate types fail!'); diff --git a/packages/dts-plugin/src/server/constant.ts b/packages/dts-plugin/src/server/constant.ts index 1b22c3a4444..92954009aba 100644 --- a/packages/dts-plugin/src/server/constant.ts +++ b/packages/dts-plugin/src/server/constant.ts @@ -1,6 +1,6 @@ export const DEFAULT_WEB_SOCKET_PORT = 16322; export const WEB_SOCKET_CONNECT_MAGIC_ID = '1hpzW-zo2z-o8io-gfmV1-2cb1d82'; -export const MF_SERVER_IDENTIFIER = 'Module Federation Dev Server'; +export const MF_SERVER_IDENTIFIER = 'Module Federation DTS'; export const WEB_CLIENT_OPTIONS_IDENTIFIER = '__WEB_CLIENT_OPTIONS__'; export const DEFAULT_TAR_NAME = '@mf-types.zip'; export const enum UpdateMode { diff --git a/packages/enhanced/README.md b/packages/enhanced/README.md index 9129c2060de..78874298f26 100644 --- a/packages/enhanced/README.md +++ b/packages/enhanced/README.md @@ -171,3 +171,43 @@ Once set, the runtime plugin is automatically injected and used at build time. - Default: `undefined` Used to modify the actual bundler runtime version. Path with value `@module-federation/runtime-tools`. + +## CLI + +To view all available CLI commands, run the following command in the project directory: + +```bash +npx mf -h +``` + +The output is shown below: + +```bash +Usage: mf [options] + +Options: + -V, --version output the version number + -h, --help display help for command + +Commands: + dts [options] generate or fetch the mf types + help [command] display help for command +``` + +### mf dts + +The mf dts command is used to generate or fetch remote types. + +```bash +Usage: mf dts [options] + +generate or fetch the mf types + +Options: + --root specify the project root directory + --output specify the generated dts output directory + --fetch fetch types from remote, default is true (default: true) + --generate generate types, default is true (default: true) + -c --config specify the configuration file, can be a relative or absolute path + -h, --help display help for command +``` diff --git a/packages/enhanced/bin/mf.js b/packages/enhanced/bin/mf.js new file mode 100755 index 00000000000..6ac43ef8105 --- /dev/null +++ b/packages/enhanced/bin/mf.js @@ -0,0 +1,4 @@ +#!/usr/bin/env node +const { runCli } = require('@module-federation/cli'); + +runCli(); diff --git a/packages/enhanced/package.json b/packages/enhanced/package.json index c6bec2cd9e1..eadd24eb0e3 100644 --- a/packages/enhanced/package.json +++ b/packages/enhanced/package.json @@ -9,9 +9,13 @@ "directory": "packages/enhanced" }, "files": [ + "bin", "dist/", "README.md" ], + "bin": { + "mf": "./bin/mf.js" + }, "license": "MIT", "publishConfig": { "access": "public" @@ -96,6 +100,7 @@ "@module-federation/rspack": "workspace:*", "@module-federation/runtime-tools": "workspace:*", "@module-federation/sdk": "workspace:*", + "@module-federation/cli": "workspace:*", "btoa": "^1.2.1", "upath": "2.0.1" } diff --git a/packages/enhanced/src/index.ts b/packages/enhanced/src/index.ts index b383f748dc0..be7892e39fd 100644 --- a/packages/enhanced/src/index.ts +++ b/packages/enhanced/src/index.ts @@ -32,3 +32,5 @@ export const createModuleFederationConfig = ( ): moduleFederationPlugin.ModuleFederationPluginOptions => { return options; }; + +export type { moduleFederationPlugin }; diff --git a/packages/modernjs/bin/mf.js b/packages/modernjs/bin/mf.js new file mode 100755 index 00000000000..6ac43ef8105 --- /dev/null +++ b/packages/modernjs/bin/mf.js @@ -0,0 +1,4 @@ +#!/usr/bin/env node +const { runCli } = require('@module-federation/cli'); + +runCli(); diff --git a/packages/modernjs/package.json b/packages/modernjs/package.json index b073d4a06a7..20ac9d83462 100644 --- a/packages/modernjs/package.json +++ b/packages/modernjs/package.json @@ -93,6 +93,7 @@ "@module-federation/enhanced": "workspace:*", "@module-federation/node": "workspace:*", "@module-federation/sdk": "workspace:*", + "@module-federation/cli": "workspace:*", "@swc/helpers": "0.5.13", "node-fetch": "~3.3.0", "react-error-boundary": "4.1.2" diff --git a/packages/sdk/src/logger.ts b/packages/sdk/src/logger.ts index 35db772bdc3..5368eb0f3d1 100644 --- a/packages/sdk/src/logger.ts +++ b/packages/sdk/src/logger.ts @@ -7,6 +7,11 @@ class Logger { constructor(prefix: string) { this.prefix = prefix; } + + setPrefix(prefix: string) { + this.prefix = prefix; + } + log(...args: any[]) { console.log(this.prefix, ...args); } diff --git a/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts b/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts index d8de9623556..6abf771277f 100644 --- a/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts +++ b/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts @@ -137,6 +137,7 @@ export interface PluginDevOptions { } interface RemoteTypeUrl { + alias?: string; api: string; zip: string; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c65443f5212..320c85c2643 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2037,6 +2037,28 @@ importers: specifier: 1.2.2 version: 1.2.2(@types/node@20.12.14)(@vitest/ui@1.6.0)(less@4.2.2)(stylus@0.64.0) + packages/cli: + dependencies: + '@modern-js/node-bundle-require': + specifier: 2.65.1 + version: 2.65.1 + '@module-federation/dts-plugin': + specifier: workspace:* + version: link:../dts-plugin + '@module-federation/sdk': + specifier: workspace:* + version: link:../sdk + chalk: + specifier: 3.0.0 + version: 3.0.0 + commander: + specifier: 11.1.0 + version: 11.1.0 + devDependencies: + '@types/node': + specifier: ~16.11.7 + version: 16.11.68 + packages/core: dependencies: webpack: @@ -2222,6 +2244,9 @@ importers: '@module-federation/bridge-react-webpack-plugin': specifier: workspace:* version: link:../bridge/bridge-react-webpack-plugin + '@module-federation/cli': + specifier: workspace:* + version: link:../cli '@module-federation/data-prefetch': specifier: workspace:* version: link:../data-prefetch @@ -2370,6 +2395,9 @@ importers: '@modern-js/utils': specifier: 2.65.1 version: 2.65.1 + '@module-federation/cli': + specifier: workspace:* + version: link:../cli '@module-federation/enhanced': specifier: workspace:* version: link:../enhanced @@ -3113,7 +3141,7 @@ packages: '@babel/traverse': 7.25.7 '@babel/types': 7.25.7 convert-source-map: 2.0.0 - debug: 4.3.7(supports-color@9.3.1) + debug: 4.3.7(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -14999,7 +15027,7 @@ packages: nx: 20.1.1(@swc-node/register@1.10.9)(@swc/core@1.7.26) semver: 7.6.3 tmp: 0.2.3 - tslib: 2.6.3 + tslib: 2.8.1 yargs-parser: 21.1.1 dev: false @@ -15015,7 +15043,7 @@ packages: nx: 20.1.4(@swc-node/register@1.10.9)(@swc/core@1.7.26) semver: 7.6.3 tmp: 0.2.3 - tslib: 2.6.3 + tslib: 2.8.1 yargs-parser: 21.1.1 dev: false @@ -15111,7 +15139,7 @@ packages: '@nx/js': 20.1.1(@swc-node/register@1.10.9)(@swc/core@1.7.26)(@types/node@20.12.14)(nx@20.1.4)(typescript@5.4.5)(verdaccio@5.29.2) eslint: 8.57.1 semver: 7.6.3 - tslib: 2.6.3 + tslib: 2.8.1 typescript: 5.4.5 transitivePeerDependencies: - '@babel/traverse' @@ -15254,7 +15282,7 @@ packages: source-map-support: 0.5.19 ts-node: 10.9.1(@swc/core@1.7.26)(@types/node@20.12.14)(typescript@5.4.5) tsconfig-paths: 4.2.0 - tslib: 2.6.3 + tslib: 2.8.1 verdaccio: 5.29.2(encoding@0.1.13)(typanion@3.14.0) transitivePeerDependencies: - '@babel/traverse' @@ -15305,7 +15333,7 @@ packages: source-map-support: 0.5.19 ts-node: 10.9.1(@swc/core@1.7.26)(@types/node@20.12.14)(typescript@5.5.2) tsconfig-paths: 4.2.0 - tslib: 2.6.3 + tslib: 2.8.1 verdaccio: 5.29.2(encoding@0.1.13)(typanion@3.14.0) transitivePeerDependencies: - '@babel/traverse' @@ -15903,7 +15931,7 @@ packages: detect-port: 1.6.1 http-server: 14.1.1 picocolors: 1.1.1 - tslib: 2.6.3 + tslib: 2.8.1 transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -16113,7 +16141,7 @@ packages: chalk: 4.1.2 enquirer: 2.3.6 nx: 20.1.4(@swc-node/register@1.10.9)(@swc/core@1.7.26) - tslib: 2.6.3 + tslib: 2.8.1 yargs-parser: 21.1.1 transitivePeerDependencies: - '@swc-node/register' @@ -20297,7 +20325,7 @@ packages: dependencies: '@semantic-release/error': 3.0.0 aggregate-error: 3.1.0 - debug: 4.3.7(supports-color@9.3.1) + debug: 4.3.7(supports-color@8.1.1) execa: 5.1.1 lodash: 4.17.21 parse-json: 5.2.0 @@ -20314,7 +20342,7 @@ packages: dependencies: '@semantic-release/error': 3.0.0 aggregate-error: 3.1.0 - debug: 4.3.7(supports-color@9.3.1) + debug: 4.3.7(supports-color@8.1.1) dir-glob: 3.0.1 execa: 5.1.1 lodash: 4.17.21 @@ -22265,7 +22293,7 @@ packages: '@swc-node/sourcemap-support': 0.5.1 '@swc/core': 1.7.26(@swc/helpers@0.5.13) colorette: 2.0.20 - debug: 4.3.7(supports-color@9.3.1) + debug: 4.3.7(supports-color@8.1.1) oxc-resolver: 1.12.0 pirates: 4.0.6 tslib: 2.6.3 @@ -22278,7 +22306,7 @@ packages: resolution: {integrity: sha512-JxIvIo/Hrpv0JCHSyRpetAdQ6lB27oFYhv0PKCNf1g2gUXOjpeR1exrXccRxLMuAV5WAmGFBwRnNOJqN38+qtg==} dependencies: source-map-support: 0.5.21 - tslib: 2.6.3 + tslib: 2.8.1 /@swc/cli@0.5.0(@swc/core@1.7.26): resolution: {integrity: sha512-eFsrNt85SbHTeX6svpBNcA5DQLP/wrSyCs3KVZjbuEHWD7JGpajZOIwH74lVhyrmrXOcGxgbnxXEbDIfRlLcSw==} @@ -22546,7 +22574,7 @@ packages: /@swc/helpers@0.5.3: resolution: {integrity: sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==} dependencies: - tslib: 2.6.3 + tslib: 2.8.1 dev: true /@swc/helpers@0.5.5: @@ -23738,7 +23766,7 @@ packages: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.5.2) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.3.7(supports-color@9.3.1) + debug: 4.3.7(supports-color@8.1.1) eslint: 8.57.1 typescript: 5.5.2 transitivePeerDependencies: @@ -24344,7 +24372,7 @@ packages: peerDependencies: vitest: 1.6.0 dependencies: - debug: 4.3.7(supports-color@9.3.1) + debug: 4.3.7(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 istanbul-lib-instrument: 6.0.3 istanbul-lib-report: 3.0.1 @@ -24365,7 +24393,7 @@ packages: dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 - debug: 4.3.7(supports-color@9.3.1) + debug: 4.3.7(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 @@ -27739,6 +27767,11 @@ packages: engines: {node: '>=14'} dev: true + /commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + dev: false + /commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -29292,7 +29325,6 @@ packages: dependencies: ms: 2.1.3 supports-color: 8.1.1 - dev: true /debug@4.3.7(supports-color@9.3.1): resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} @@ -29305,6 +29337,7 @@ packages: dependencies: ms: 2.1.3 supports-color: 9.3.1 + dev: true /debug@4.4.0(supports-color@5.5.0): resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} @@ -35894,7 +35927,7 @@ packages: dependencies: copy-anything: 2.0.6 parse-node-version: 1.0.1 - tslib: 2.6.3 + tslib: 2.8.1 optionalDependencies: errno: 0.1.8 graceful-fs: 4.2.11 @@ -36251,7 +36284,7 @@ packages: engines: {node: '>=8.0'} dependencies: date-format: 4.0.14 - debug: 4.3.7(supports-color@9.3.1) + debug: 4.3.7(supports-color@8.1.1) flatted: 3.3.1 rfdc: 1.4.1 streamroller: 3.1.5 @@ -44033,7 +44066,7 @@ packages: fs-extra: 10.1.0 rollup: 4.24.0 semver: 7.6.3 - tslib: 2.6.3 + tslib: 2.8.1 typescript: 5.5.2 dev: true @@ -46335,6 +46368,7 @@ packages: /supports-color@9.3.1: resolution: {integrity: sha512-knBY82pjmnIzK3NifMo3RxEIRD9E0kIzV4BKcyTZ9+9kWgLMxd4PrsTSMoFQUabgRBbF8KOLRDCyKgNV+iK44Q==} engines: {node: '>=12'} + dev: true /supports-hyperlinks@2.3.0: resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==} @@ -46426,7 +46460,7 @@ packages: engines: {node: ^14.18.0 || >=16.0.0} dependencies: '@pkgr/core': 0.1.1 - tslib: 2.6.3 + tslib: 2.8.1 dev: true /table-layout@1.0.2: @@ -47600,7 +47634,7 @@ packages: bundle-require: 4.2.1(esbuild@0.18.20) cac: 6.7.14 chokidar: 3.6.0 - debug: 4.3.7(supports-color@9.3.1) + debug: 4.3.7(supports-color@8.1.1) esbuild: 0.18.20 execa: 5.1.1 globby: 11.1.0 @@ -47642,7 +47676,7 @@ packages: cac: 6.7.14 chokidar: 3.6.0 consola: 3.2.3 - debug: 4.3.7(supports-color@9.3.1) + debug: 4.3.7(supports-color@8.1.1) esbuild: 0.23.0 execa: 5.1.1 joycon: 3.1.1 @@ -48456,7 +48490,7 @@ packages: compression: 1.7.4 cookies: 0.9.1 cors: 2.8.5 - debug: 4.3.7(supports-color@9.3.1) + debug: 4.3.7(supports-color@8.1.1) envinfo: 7.11.0 express: 4.18.2 express-rate-limit: 5.5.1 @@ -48624,7 +48658,7 @@ packages: '@volar/typescript': 2.4.5 '@vue/language-core': 2.1.6(typescript@5.5.2) compare-versions: 6.1.1 - debug: 4.3.7(supports-color@9.3.1) + debug: 4.4.0(supports-color@8.1.1) kolorist: 1.8.0 local-pkg: 0.5.0 magic-string: 0.30.17 @@ -48651,7 +48685,7 @@ packages: '@volar/typescript': 2.4.5 '@vue/language-core': 2.1.6(typescript@5.5.2) compare-versions: 6.1.1 - debug: 4.3.7(supports-color@9.3.1) + debug: 4.3.7(supports-color@8.1.1) kolorist: 1.8.0 local-pkg: 0.5.0 magic-string: 0.30.17 @@ -48671,7 +48705,7 @@ packages: vite: optional: true dependencies: - debug: 4.3.7(supports-color@9.3.1) + debug: 4.3.7(supports-color@8.1.1) globrex: 0.1.2 tsconfck: 2.1.2(typescript@5.5.2) vite: 5.4.12(@types/node@18.16.9)(less@4.2.2)(stylus@0.64.0) @@ -48850,7 +48884,7 @@ packages: acorn-walk: 8.3.4 cac: 6.7.14 chai: 4.5.0 - debug: 4.3.7(supports-color@9.3.1) + debug: 4.3.7(supports-color@8.1.1) execa: 8.0.1 local-pkg: 0.5.0 magic-string: 0.30.11 @@ -48908,7 +48942,7 @@ packages: '@vitest/utils': 1.6.0 acorn-walk: 8.3.4 chai: 4.5.0 - debug: 4.3.7(supports-color@9.3.1) + debug: 4.3.7(supports-color@8.1.1) execa: 8.0.1 local-pkg: 0.5.0 magic-string: 0.30.11