From fe693818e261ebbc05343ef17f19b752716422c2 Mon Sep 17 00:00:00 2001 From: Wei Date: Mon, 18 Nov 2024 14:37:40 +0800 Subject: [PATCH] feat: support lib.id (#436) Co-authored-by: Timeless0911 <50201324+Timeless0911@users.noreply.github.com> --- packages/core/src/cli/commands.ts | 4 +- packages/core/src/config.ts | 60 +++++++++--- packages/core/src/types/config/index.ts | 11 +++ packages/core/src/types/utils.ts | 2 + packages/core/tests/config.test.ts | 120 +++++++++++++++++++++++- website/docs/en/config/lib/_meta.json | 1 + website/docs/en/config/lib/id.mdx | 60 ++++++++++++ website/docs/en/guide/basic/cli.mdx | 19 ++-- 8 files changed, 253 insertions(+), 24 deletions(-) create mode 100644 website/docs/en/config/lib/id.mdx diff --git a/packages/core/src/cli/commands.ts b/packages/core/src/cli/commands.ts index b738f3f6d..07af6cc2d 100644 --- a/packages/core/src/cli/commands.ts +++ b/packages/core/src/cli/commands.ts @@ -52,7 +52,7 @@ export function runCli(): void { buildCommand .option( - '--lib ', + '--lib ', 'build the specified library (may be repeated)', repeatableOption, ) @@ -75,7 +75,7 @@ export function runCli(): void { inspectCommand .description('inspect the Rsbuild / Rspack configs of Rslib projects') .option( - '--lib ', + '--lib ', 'inspect the specified library (may be repeated)', repeatableOption, ) diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index abe2f76a4..d194a64d5 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -33,12 +33,14 @@ import type { AutoExternal, BannerAndFooter, DeepRequired, + ExcludesFalse, Format, LibConfig, LibOnlyConfig, PkgJson, Redirect, RsbuildConfigOutputTarget, + RsbuildConfigWithLibInfo, RslibConfig, RslibConfigAsyncFn, RslibConfigExport, @@ -1182,7 +1184,7 @@ async function composeLibRsbuildConfig(config: LibConfig, configPath: string) { export async function composeCreateRsbuildConfig( rslibConfig: RslibConfig, path?: string, -): Promise<{ format: Format; config: RsbuildConfig }[]> { +): Promise { const constantRsbuildConfig = await createConstantRsbuildConfig(); const configPath = path ?? rslibConfig._privateMeta?.configFilePath!; const { lib: libConfigsArray, ...sharedRsbuildConfig } = rslibConfig; @@ -1216,7 +1218,7 @@ export async function composeCreateRsbuildConfig( userConfig.output ??= {}; delete userConfig.output.externals; - return { + const config: RsbuildConfigWithLibInfo = { format: libConfig.format!, // The merge order represents the priority of the configuration // The priorities from high to low are as follows: @@ -1230,6 +1232,7 @@ export async function composeCreateRsbuildConfig( constantRsbuildConfig, libRsbuildConfig, omit(userConfig, { + id: true, bundle: true, format: true, autoExtension: true, @@ -1245,6 +1248,12 @@ export async function composeCreateRsbuildConfig( }), ), }; + + if (typeof libConfig.id === 'string') { + config.id = libConfig.id; + } + + return config; }); const composedRsbuildConfig = await Promise.all(libConfigPromises); @@ -1253,10 +1262,19 @@ export async function composeCreateRsbuildConfig( export async function composeRsbuildEnvironments( rslibConfig: RslibConfig, + path?: string, ): Promise> { - const rsbuildConfigObject = await composeCreateRsbuildConfig(rslibConfig); + const rsbuildConfigWithLibInfo = await composeCreateRsbuildConfig( + rslibConfig, + path, + ); + + // User provided ids should take precedence over generated ids. + const usedIds = rsbuildConfigWithLibInfo + .map(({ id }) => id) + .filter(Boolean as any as ExcludesFalse); const environments: RsbuildConfig['environments'] = {}; - const formatCount: Record = rsbuildConfigObject.reduce( + const formatCount: Record = rsbuildConfigWithLibInfo.reduce( (acc, { format }) => { acc[format] = (acc[format] ?? 0) + 1; return acc; @@ -1264,20 +1282,32 @@ export async function composeRsbuildEnvironments( {} as Record, ); - const formatIndex: Record = { - esm: 0, - cjs: 0, - umd: 0, - mf: 0, + const composeDefaultId = (format: Format): string => { + const nextDefaultId = (format: Format, index: number) => { + return `${format}${formatCount[format] === 1 && index === 0 ? '' : index}`; + }; + + let index = 0; + let candidateId = nextDefaultId(format, index); + while (usedIds.indexOf(candidateId) !== -1) { + candidateId = nextDefaultId(format, ++index); + } + usedIds.push(candidateId); + return candidateId; }; - for (const { format, config } of rsbuildConfigObject) { - const currentFormatCount = formatCount[format]; - const currentFormatIndex = formatIndex[format]++; + for (const { format, id, config } of rsbuildConfigWithLibInfo) { + const libId = typeof id === 'string' ? id : composeDefaultId(format); + environments[libId] = config; + } - environments[ - currentFormatCount === 1 ? format : `${format}${currentFormatIndex}` - ] = config; + const conflictIds = usedIds.filter( + (id, index) => usedIds.indexOf(id) !== index, + ); + if (conflictIds.length) { + throw new Error( + `The following ids are duplicated: ${conflictIds.map((id) => `"${id}"`).join(', ')}. Please change the "lib.id" to be unique.`, + ); } return environments; diff --git a/packages/core/src/types/config/index.ts b/packages/core/src/types/config/index.ts index 8aafb14ae..c312e365f 100644 --- a/packages/core/src/types/config/index.ts +++ b/packages/core/src/types/config/index.ts @@ -18,6 +18,12 @@ export type FixedEcmaVersions = export type LatestEcmaVersions = 'es2024' | 'esnext'; export type EcmaScriptVersion = FixedEcmaVersions | LatestEcmaVersions; +export type RsbuildConfigWithLibInfo = { + id?: string; + format: Format; + config: RsbuildConfig; +}; + export type RsbuildConfigOutputTarget = NonNullable< RsbuildConfig['output'] >['target']; @@ -72,6 +78,11 @@ export type Redirect = { }; export interface LibConfig extends RsbuildConfig { + /** + * The unique identifier of the library. + * @default undefined + */ + id?: string; /** * Output format for the generated JavaScript files. * @default undefined diff --git a/packages/core/src/types/utils.ts b/packages/core/src/types/utils.ts index f97988819..77e0b962c 100644 --- a/packages/core/src/types/utils.ts +++ b/packages/core/src/types/utils.ts @@ -10,3 +10,5 @@ export type PkgJson = { export type DeepRequired = Required<{ [K in keyof T]: T[K] extends Required ? T[K] : DeepRequired; }>; + +export type ExcludesFalse = (x: T | false | undefined | null) => x is T; diff --git a/packages/core/tests/config.test.ts b/packages/core/tests/config.test.ts index d766030e9..aeb2f42ab 100644 --- a/packages/core/tests/config.test.ts +++ b/packages/core/tests/config.test.ts @@ -1,6 +1,10 @@ import { join } from 'node:path'; import { describe, expect, test, vi } from 'vitest'; -import { composeCreateRsbuildConfig, loadConfig } from '../src/config'; +import { + composeCreateRsbuildConfig, + composeRsbuildEnvironments, + loadConfig, +} from '../src/config'; import type { RslibConfig } from '../src/types/config'; vi.mock('rslog'); @@ -402,3 +406,117 @@ describe('minify', () => { `); }); }); + +describe('id', () => { + test('default id logic', async () => { + const rslibConfig: RslibConfig = { + lib: [ + { + format: 'esm', + }, + { + format: 'cjs', + }, + { + format: 'esm', + }, + { + format: 'umd', + }, + { + format: 'esm', + }, + ], + }; + + const composedRsbuildConfig = await composeRsbuildEnvironments( + rslibConfig, + process.cwd(), + ); + + expect(Object.keys(composedRsbuildConfig)).toMatchInlineSnapshot(` + [ + "esm0", + "cjs", + "esm1", + "umd", + "esm2", + ] + `); + }); + + test('with user specified id', async () => { + const rslibConfig: RslibConfig = { + lib: [ + { + id: 'esm1', + format: 'esm', + }, + { + format: 'cjs', + }, + { + format: 'esm', + }, + { + id: 'cjs', + format: 'umd', + }, + { + id: 'esm0', + format: 'esm', + }, + ], + }; + + const composedRsbuildConfig = await composeRsbuildEnvironments( + rslibConfig, + process.cwd(), + ); + expect(Object.keys(composedRsbuildConfig)).toMatchInlineSnapshot(` + [ + "esm1", + "cjs1", + "esm2", + "cjs", + "esm0", + ] + `); + }); + + test('do not allow conflicted id', async () => { + const rslibConfig: RslibConfig = { + lib: [ + { + id: 'a', + format: 'esm', + }, + { + format: 'cjs', + }, + { + format: 'esm', + }, + { + id: 'a', + format: 'umd', + }, + { + id: 'b', + format: 'esm', + }, + { + id: 'b', + format: 'esm', + }, + ], + }; + + // await composeRsbuildEnvironments(rslibConfig, process.cwd()); + await expect(() => + composeRsbuildEnvironments(rslibConfig, process.cwd()), + ).rejects.toThrowError( + 'The following ids are duplicated: "a", "b". Please change the "lib.id" to be unique.', + ); + }); +}); diff --git a/website/docs/en/config/lib/_meta.json b/website/docs/en/config/lib/_meta.json index 5dce7050e..dfd5c8550 100644 --- a/website/docs/en/config/lib/_meta.json +++ b/website/docs/en/config/lib/_meta.json @@ -10,5 +10,6 @@ "footer", "dts", "shims", + "id", "umd-name" ] diff --git a/website/docs/en/config/lib/id.mdx b/website/docs/en/config/lib/id.mdx new file mode 100644 index 000000000..9b0033674 --- /dev/null +++ b/website/docs/en/config/lib/id.mdx @@ -0,0 +1,60 @@ +# lib.id + +- **Type:** `string` +- **Default:** `undefined` + +Specify the library ID. The ID identifies the library and is useful when using the `--lib` flag to build specific libraries with a meaningful `id` in the CLI. + +:::tip + +Rslib uses Rsbuild's [environments](https://rsbuild.dev/guide/advanced/environments) feature to build multiple libraries in a single project under the hood. `lib.id` will be used as the key for the generated Rsbuild environment. + +::: + +## Default Value + +By default, Rslib automatically generates an ID for each library in the format `${format}${index}`. Here, `format` refers to the value specified in the current lib's [format](/config/lib/format), and `index` indicates the order of the library within all libraries of the same format. If there is only one library with the current format, the `index` will be empty. Otherwise, it will start from `0` and increment. + +For example, the libraries in the `esm` format will start from `esm0`, followed by `esm1`, `esm2`, and so on. In contrast, `cjs` and `umd` formats do not include the `index` part since there is only one library for each format. + +```ts title="rslib.config.ts" +export default { + lib: [ + { format: 'esm' }, // id is `esm0` + { format: 'cjs' }, // id is `cjs` + { format: 'esm' }, // id is `esm1` + { format: 'umd' }, // id is `umd` + { format: 'esm' }, // id is `esm2` + ], +}; +``` + +## Customize ID + +You can also specify a readable or meaningful ID of the library by setting the `id` field in the library configuration. The user-specified ID will take priority, while the rest will be used together to generate the default ID. + +For example, `my-lib-a`, `my-lib-b`, and `my-lib-c` will be the IDs of the specified libraries, while the rest will be used to generate and apply the default ID. + +{/* prettier-ignore-start */} +```ts title="rslib.config.ts" +export default { + lib: [ + { format: 'esm', id: 'my-lib-a' }, // ID is `my-lib-a` + { format: 'cjs', id: 'my-lib-b' }, // ID is `my-lib-b` + { format: 'esm' }, // ID is `esm0` + { format: 'umd', id: 'my-lib-c' }, // ID is `my-lib-c` + { format: 'esm' }, // ID is `esm1` + ], +}; +``` +{/* prettier-ignore-end */} + +Then you could only build `my-lib-a` and `my-lib-b` by running the following command: + +```bash +npx rslib build --lib my-lib-a --lib my-lib-b +``` + +:::note +The id of each library must be unique, otherwise it will cause an error. +::: diff --git a/website/docs/en/guide/basic/cli.mdx b/website/docs/en/guide/basic/cli.mdx index 4d6f2ef01..ec170bdd3 100644 --- a/website/docs/en/guide/basic/cli.mdx +++ b/website/docs/en/guide/basic/cli.mdx @@ -38,15 +38,18 @@ build the library for production Options: -c --config specify the configuration file, can be a relative or absolute path --env-mode specify the env mode to load the `.env.[mode]` file - --lib build the specified library (may be repeated) + --lib build the specified library (may be repeated) -w --watch turn on watch mode, watch for changes and rebuild -h, --help display help for command ``` -:::tip +### Watch Mode You can use `rslib build --watch` to turn on watch mode for watching for changes and rebuild. -::: + +### Filter Libraries + +You can use the `--lib` option to build specific libraries. The `--lib` option can be repeated to build multiple libraries. Check out the [lib.id](/config/lib/id) to learn how to get or set the library ID. ## rslib inspect @@ -60,7 +63,7 @@ inspect the Rsbuild / Rspack configs of Rslib projects Options: -c --config specify the configuration file, can be a relative or absolute path --env-mode specify the env mode to load the `.env.[mode]` file - --lib inspect the specified library (may be repeated) + --lib inspect the specified library (may be repeated) --output specify inspect content output path (default: ".rsbuild") --verbose show full function definitions in output -h, --help display help for command @@ -80,7 +83,7 @@ Inspect config succeed, open following files to view the content: - Rspack Config (esm): /project/dist/.rsbuild/rspack.config.esm.mjs ``` -### Verbose content +### Verbose Content By default, the inspect command omits the content of functions in the configuration object. You can add the `--verbose` option to output the complete content of functions: @@ -88,7 +91,7 @@ By default, the inspect command omits the content of functions in the configurat rslib inspect --verbose ``` -### Multiple output formats +### Multiple Output Formats If the current project has multiple output formats, such as ESM artifact and CJS artifact simultaneously, multiple Rspack configuration files will be generated in the `dist/.rsbuild` directory. @@ -103,6 +106,10 @@ Inspect config succeed, open following files to view the content: - Rspack Config (cjs): /project/dist/.rsbuild/rspack.config.cjs.mjs ``` +### Filter Libraries + +You can use the `--lib` option to inspect specific libraries. The `--lib` option can be repeated to inspect multiple libraries. Check out the [lib.id](/config/lib/id) to learn how to get or set the library ID. + ## rslib mf dev The `rslib mf dev` command is utilized to start Rsbuild dev server of Module Federation format.