Skip to content

Commit 38737d4

Browse files
committed
feat: support lib.id
1 parent 5bbe9e9 commit 38737d4

File tree

8 files changed

+257
-24
lines changed

8 files changed

+257
-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: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import type {
3333
AutoExternal,
3434
BannerAndFooter,
3535
DeepRequired,
36+
ExcludesFalse,
3637
Format,
3738
LibConfig,
3839
LibOnlyConfig,
@@ -1179,10 +1180,16 @@ async function composeLibRsbuildConfig(config: LibConfig, configPath: string) {
11791180
);
11801181
}
11811182

1183+
type RsbuildConfigWithLibInfo = {
1184+
id?: string;
1185+
format: Format;
1186+
config: RsbuildConfig;
1187+
};
1188+
11821189
export async function composeCreateRsbuildConfig(
11831190
rslibConfig: RslibConfig,
11841191
path?: string,
1185-
): Promise<{ format: Format; config: RsbuildConfig }[]> {
1192+
): Promise<RsbuildConfigWithLibInfo[]> {
11861193
const constantRsbuildConfig = await createConstantRsbuildConfig();
11871194
const configPath = path ?? rslibConfig._privateMeta?.configFilePath!;
11881195
const { lib: libConfigsArray, ...sharedRsbuildConfig } = rslibConfig;
@@ -1216,7 +1223,7 @@ export async function composeCreateRsbuildConfig(
12161223
userConfig.output ??= {};
12171224
delete userConfig.output.externals;
12181225

1219-
return {
1226+
const config: RsbuildConfigWithLibInfo = {
12201227
format: libConfig.format!,
12211228
// The merge order represents the priority of the configuration
12221229
// The priorities from high to low are as follows:
@@ -1230,6 +1237,7 @@ export async function composeCreateRsbuildConfig(
12301237
constantRsbuildConfig,
12311238
libRsbuildConfig,
12321239
omit<LibConfig, keyof LibOnlyConfig>(userConfig, {
1240+
id: true,
12331241
bundle: true,
12341242
format: true,
12351243
autoExtension: true,
@@ -1245,6 +1253,12 @@ export async function composeCreateRsbuildConfig(
12451253
}),
12461254
),
12471255
};
1256+
1257+
if (typeof libConfig.id === 'string') {
1258+
config.id = libConfig.id;
1259+
}
1260+
1261+
return config;
12481262
});
12491263

12501264
const composedRsbuildConfig = await Promise.all(libConfigPromises);
@@ -1253,31 +1267,52 @@ export async function composeCreateRsbuildConfig(
12531267

12541268
export async function composeRsbuildEnvironments(
12551269
rslibConfig: RslibConfig,
1270+
path?: string,
12561271
): Promise<Record<string, EnvironmentConfig>> {
1257-
const rsbuildConfigObject = await composeCreateRsbuildConfig(rslibConfig);
1272+
const rsbuildConfigWithLibInfo = await composeCreateRsbuildConfig(
1273+
rslibConfig,
1274+
path,
1275+
);
1276+
1277+
// User provided ids should take precedence over generated ids.
1278+
const usedIds = rsbuildConfigWithLibInfo
1279+
.map(({ id }) => id)
1280+
.filter(Boolean as any as ExcludesFalse);
12581281
const environments: RsbuildConfig['environments'] = {};
1259-
const formatCount: Record<Format, number> = rsbuildConfigObject.reduce(
1282+
const formatCount: Record<Format, number> = rsbuildConfigWithLibInfo.reduce(
12601283
(acc, { format }) => {
12611284
acc[format] = (acc[format] ?? 0) + 1;
12621285
return acc;
12631286
},
12641287
{} as Record<Format, number>,
12651288
);
12661289

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

1274-
for (const { format, config } of rsbuildConfigObject) {
1275-
const currentFormatCount = formatCount[format];
1276-
const currentFormatIndex = formatIndex[format]++;
1304+
for (const { format, id, config } of rsbuildConfigWithLibInfo) {
1305+
const libId = typeof id === 'string' ? id : composeDefaultId(format);
1306+
environments[libId] = config;
1307+
}
12771308

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

12831318
return environments;

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,16 @@ export type Redirect = {
7272
};
7373

7474
export interface LibConfig extends RsbuildConfig {
75+
/**
76+
* Each lib configuration has a unique identifier used to distinguish different lib configurations.
77+
* By default, Rslib generates a unique identifier based on the order of lib configurations, in the format `${format}${index}`.
78+
* When there is only one lib of the format, the index is empty, otherwise, it starts from 0 and increments.
79+
* For example:
80+
* - If only ESM and CJS formats are configured, the identifier for ESM is `esm` and for CJS is `cjs`.
81+
* - If two ESM formats and one CJS format are configured, they are represented as `esm0`, `esm1`, and `cjs`.
82+
* @default undefined
83+
*/
84+
id?: string;
7585
/**
7686
* Output format for the generated JavaScript files.
7787
* @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+
## The default ID
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+
## Configuring 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)