Skip to content

Commit fe69381

Browse files
feat: support lib.id (#436)
Co-authored-by: Timeless0911 <[email protected]>
1 parent 190ca7c commit fe69381

File tree

8 files changed

+253
-24
lines changed

8 files changed

+253
-24
lines changed

packages/core/src/cli/commands.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export function runCli(): void {
5252

5353
buildCommand
5454
.option(
55-
'--lib <name>',
55+
'--lib <id>',
5656
'build the specified library (may be repeated)',
5757
repeatableOption,
5858
)
@@ -75,7 +75,7 @@ export function runCli(): void {
7575
inspectCommand
7676
.description('inspect the Rsbuild / Rspack configs of Rslib projects')
7777
.option(
78-
'--lib <name>',
78+
'--lib <id>',
7979
'inspect the specified library (may be repeated)',
8080
repeatableOption,
8181
)

packages/core/src/config.ts

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,14 @@ import type {
3333
AutoExternal,
3434
BannerAndFooter,
3535
DeepRequired,
36+
ExcludesFalse,
3637
Format,
3738
LibConfig,
3839
LibOnlyConfig,
3940
PkgJson,
4041
Redirect,
4142
RsbuildConfigOutputTarget,
43+
RsbuildConfigWithLibInfo,
4244
RslibConfig,
4345
RslibConfigAsyncFn,
4446
RslibConfigExport,
@@ -1182,7 +1184,7 @@ async function composeLibRsbuildConfig(config: LibConfig, configPath: string) {
11821184
export async function composeCreateRsbuildConfig(
11831185
rslibConfig: RslibConfig,
11841186
path?: string,
1185-
): Promise<{ format: Format; config: RsbuildConfig }[]> {
1187+
): Promise<RsbuildConfigWithLibInfo[]> {
11861188
const constantRsbuildConfig = await createConstantRsbuildConfig();
11871189
const configPath = path ?? rslibConfig._privateMeta?.configFilePath!;
11881190
const { lib: libConfigsArray, ...sharedRsbuildConfig } = rslibConfig;
@@ -1216,7 +1218,7 @@ export async function composeCreateRsbuildConfig(
12161218
userConfig.output ??= {};
12171219
delete userConfig.output.externals;
12181220

1219-
return {
1221+
const config: RsbuildConfigWithLibInfo = {
12201222
format: libConfig.format!,
12211223
// The merge order represents the priority of the configuration
12221224
// The priorities from high to low are as follows:
@@ -1230,6 +1232,7 @@ export async function composeCreateRsbuildConfig(
12301232
constantRsbuildConfig,
12311233
libRsbuildConfig,
12321234
omit<LibConfig, keyof LibOnlyConfig>(userConfig, {
1235+
id: true,
12331236
bundle: true,
12341237
format: true,
12351238
autoExtension: true,
@@ -1245,6 +1248,12 @@ export async function composeCreateRsbuildConfig(
12451248
}),
12461249
),
12471250
};
1251+
1252+
if (typeof libConfig.id === 'string') {
1253+
config.id = libConfig.id;
1254+
}
1255+
1256+
return config;
12481257
});
12491258

12501259
const composedRsbuildConfig = await Promise.all(libConfigPromises);
@@ -1253,31 +1262,52 @@ export async function composeCreateRsbuildConfig(
12531262

12541263
export async function composeRsbuildEnvironments(
12551264
rslibConfig: RslibConfig,
1265+
path?: string,
12561266
): Promise<Record<string, EnvironmentConfig>> {
1257-
const rsbuildConfigObject = await composeCreateRsbuildConfig(rslibConfig);
1267+
const rsbuildConfigWithLibInfo = await composeCreateRsbuildConfig(
1268+
rslibConfig,
1269+
path,
1270+
);
1271+
1272+
// User provided ids should take precedence over generated ids.
1273+
const usedIds = rsbuildConfigWithLibInfo
1274+
.map(({ id }) => id)
1275+
.filter(Boolean as any as ExcludesFalse);
12581276
const environments: RsbuildConfig['environments'] = {};
1259-
const formatCount: Record<Format, number> = rsbuildConfigObject.reduce(
1277+
const formatCount: Record<Format, number> = rsbuildConfigWithLibInfo.reduce(
12601278
(acc, { format }) => {
12611279
acc[format] = (acc[format] ?? 0) + 1;
12621280
return acc;
12631281
},
12641282
{} as Record<Format, number>,
12651283
);
12661284

1267-
const formatIndex: Record<Format, number> = {
1268-
esm: 0,
1269-
cjs: 0,
1270-
umd: 0,
1271-
mf: 0,
1285+
const composeDefaultId = (format: Format): string => {
1286+
const nextDefaultId = (format: Format, index: number) => {
1287+
return `${format}${formatCount[format] === 1 && index === 0 ? '' : index}`;
1288+
};
1289+
1290+
let index = 0;
1291+
let candidateId = nextDefaultId(format, index);
1292+
while (usedIds.indexOf(candidateId) !== -1) {
1293+
candidateId = nextDefaultId(format, ++index);
1294+
}
1295+
usedIds.push(candidateId);
1296+
return candidateId;
12721297
};
12731298

1274-
for (const { format, config } of rsbuildConfigObject) {
1275-
const currentFormatCount = formatCount[format];
1276-
const currentFormatIndex = formatIndex[format]++;
1299+
for (const { format, id, config } of rsbuildConfigWithLibInfo) {
1300+
const libId = typeof id === 'string' ? id : composeDefaultId(format);
1301+
environments[libId] = config;
1302+
}
12771303

1278-
environments[
1279-
currentFormatCount === 1 ? format : `${format}${currentFormatIndex}`
1280-
] = config;
1304+
const conflictIds = usedIds.filter(
1305+
(id, index) => usedIds.indexOf(id) !== index,
1306+
);
1307+
if (conflictIds.length) {
1308+
throw new Error(
1309+
`The following ids are duplicated: ${conflictIds.map((id) => `"${id}"`).join(', ')}. Please change the "lib.id" to be unique.`,
1310+
);
12811311
}
12821312

12831313
return environments;

packages/core/src/types/config/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ export type FixedEcmaVersions =
1818
export type LatestEcmaVersions = 'es2024' | 'esnext';
1919
export type EcmaScriptVersion = FixedEcmaVersions | LatestEcmaVersions;
2020

21+
export type RsbuildConfigWithLibInfo = {
22+
id?: string;
23+
format: Format;
24+
config: RsbuildConfig;
25+
};
26+
2127
export type RsbuildConfigOutputTarget = NonNullable<
2228
RsbuildConfig['output']
2329
>['target'];
@@ -72,6 +78,11 @@ export type Redirect = {
7278
};
7379

7480
export interface LibConfig extends RsbuildConfig {
81+
/**
82+
* The unique identifier of the library.
83+
* @default undefined
84+
*/
85+
id?: string;
7586
/**
7687
* Output format for the generated JavaScript files.
7788
* @default undefined

packages/core/src/types/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ export type PkgJson = {
1010
export type DeepRequired<T> = Required<{
1111
[K in keyof T]: T[K] extends Required<T[K]> ? T[K] : DeepRequired<T[K]>;
1212
}>;
13+
14+
export type ExcludesFalse = <T>(x: T | false | undefined | null) => x is T;

packages/core/tests/config.test.ts

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { join } from 'node:path';
22
import { describe, expect, test, vi } from 'vitest';
3-
import { composeCreateRsbuildConfig, loadConfig } from '../src/config';
3+
import {
4+
composeCreateRsbuildConfig,
5+
composeRsbuildEnvironments,
6+
loadConfig,
7+
} from '../src/config';
48
import type { RslibConfig } from '../src/types/config';
59

610
vi.mock('rslog');
@@ -402,3 +406,117 @@ describe('minify', () => {
402406
`);
403407
});
404408
});
409+
410+
describe('id', () => {
411+
test('default id logic', async () => {
412+
const rslibConfig: RslibConfig = {
413+
lib: [
414+
{
415+
format: 'esm',
416+
},
417+
{
418+
format: 'cjs',
419+
},
420+
{
421+
format: 'esm',
422+
},
423+
{
424+
format: 'umd',
425+
},
426+
{
427+
format: 'esm',
428+
},
429+
],
430+
};
431+
432+
const composedRsbuildConfig = await composeRsbuildEnvironments(
433+
rslibConfig,
434+
process.cwd(),
435+
);
436+
437+
expect(Object.keys(composedRsbuildConfig)).toMatchInlineSnapshot(`
438+
[
439+
"esm0",
440+
"cjs",
441+
"esm1",
442+
"umd",
443+
"esm2",
444+
]
445+
`);
446+
});
447+
448+
test('with user specified id', async () => {
449+
const rslibConfig: RslibConfig = {
450+
lib: [
451+
{
452+
id: 'esm1',
453+
format: 'esm',
454+
},
455+
{
456+
format: 'cjs',
457+
},
458+
{
459+
format: 'esm',
460+
},
461+
{
462+
id: 'cjs',
463+
format: 'umd',
464+
},
465+
{
466+
id: 'esm0',
467+
format: 'esm',
468+
},
469+
],
470+
};
471+
472+
const composedRsbuildConfig = await composeRsbuildEnvironments(
473+
rslibConfig,
474+
process.cwd(),
475+
);
476+
expect(Object.keys(composedRsbuildConfig)).toMatchInlineSnapshot(`
477+
[
478+
"esm1",
479+
"cjs1",
480+
"esm2",
481+
"cjs",
482+
"esm0",
483+
]
484+
`);
485+
});
486+
487+
test('do not allow conflicted id', async () => {
488+
const rslibConfig: RslibConfig = {
489+
lib: [
490+
{
491+
id: 'a',
492+
format: 'esm',
493+
},
494+
{
495+
format: 'cjs',
496+
},
497+
{
498+
format: 'esm',
499+
},
500+
{
501+
id: 'a',
502+
format: 'umd',
503+
},
504+
{
505+
id: 'b',
506+
format: 'esm',
507+
},
508+
{
509+
id: 'b',
510+
format: 'esm',
511+
},
512+
],
513+
};
514+
515+
// await composeRsbuildEnvironments(rslibConfig, process.cwd());
516+
await expect(() =>
517+
composeRsbuildEnvironments(rslibConfig, process.cwd()),
518+
).rejects.toThrowError(
519+
'The following ids are duplicated: "a", "b". Please change the "lib.id" to be unique.',
520+
);
521+
});
522+
});

website/docs/en/config/lib/_meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@
1010
"footer",
1111
"dts",
1212
"shims",
13+
"id",
1314
"umd-name"
1415
]

website/docs/en/config/lib/id.mdx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# lib.id
2+
3+
- **Type:** `string`
4+
- **Default:** `undefined`
5+
6+
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.
7+
8+
:::tip
9+
10+
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.
11+
12+
:::
13+
14+
## Default Value
15+
16+
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.
17+
18+
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.
19+
20+
```ts title="rslib.config.ts"
21+
export default {
22+
lib: [
23+
{ format: 'esm' }, // id is `esm0`
24+
{ format: 'cjs' }, // id is `cjs`
25+
{ format: 'esm' }, // id is `esm1`
26+
{ format: 'umd' }, // id is `umd`
27+
{ format: 'esm' }, // id is `esm2`
28+
],
29+
};
30+
```
31+
32+
## Customize ID
33+
34+
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.
35+
36+
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.
37+
38+
{/* prettier-ignore-start */}
39+
```ts title="rslib.config.ts"
40+
export default {
41+
lib: [
42+
{ format: 'esm', id: 'my-lib-a' }, // ID is `my-lib-a`
43+
{ format: 'cjs', id: 'my-lib-b' }, // ID is `my-lib-b`
44+
{ format: 'esm' }, // ID is `esm0`
45+
{ format: 'umd', id: 'my-lib-c' }, // ID is `my-lib-c`
46+
{ format: 'esm' }, // ID is `esm1`
47+
],
48+
};
49+
```
50+
{/* prettier-ignore-end */}
51+
52+
Then you could only build `my-lib-a` and `my-lib-b` by running the following command:
53+
54+
```bash
55+
npx rslib build --lib my-lib-a --lib my-lib-b
56+
```
57+
58+
:::note
59+
The id of each library must be unique, otherwise it will cause an error.
60+
:::

0 commit comments

Comments
 (0)