Skip to content

Commit

Permalink
feat: support lib.id (#436)
Browse files Browse the repository at this point in the history
Co-authored-by: Timeless0911 <50201324+Timeless0911@users.noreply.github.com>
  • Loading branch information
fi3ework and Timeless0911 authored Nov 18, 2024
1 parent 190ca7c commit fe69381
Show file tree
Hide file tree
Showing 8 changed files with 253 additions and 24 deletions.
4 changes: 2 additions & 2 deletions packages/core/src/cli/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export function runCli(): void {

buildCommand
.option(
'--lib <name>',
'--lib <id>',
'build the specified library (may be repeated)',
repeatableOption,
)
Expand All @@ -75,7 +75,7 @@ export function runCli(): void {
inspectCommand
.description('inspect the Rsbuild / Rspack configs of Rslib projects')
.option(
'--lib <name>',
'--lib <id>',
'inspect the specified library (may be repeated)',
repeatableOption,
)
Expand Down
60 changes: 45 additions & 15 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,14 @@ import type {
AutoExternal,
BannerAndFooter,
DeepRequired,
ExcludesFalse,
Format,
LibConfig,
LibOnlyConfig,
PkgJson,
Redirect,
RsbuildConfigOutputTarget,
RsbuildConfigWithLibInfo,
RslibConfig,
RslibConfigAsyncFn,
RslibConfigExport,
Expand Down Expand Up @@ -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<RsbuildConfigWithLibInfo[]> {
const constantRsbuildConfig = await createConstantRsbuildConfig();
const configPath = path ?? rslibConfig._privateMeta?.configFilePath!;
const { lib: libConfigsArray, ...sharedRsbuildConfig } = rslibConfig;
Expand Down Expand Up @@ -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:
Expand All @@ -1230,6 +1232,7 @@ export async function composeCreateRsbuildConfig(
constantRsbuildConfig,
libRsbuildConfig,
omit<LibConfig, keyof LibOnlyConfig>(userConfig, {
id: true,
bundle: true,
format: true,
autoExtension: true,
Expand All @@ -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);
Expand All @@ -1253,31 +1262,52 @@ export async function composeCreateRsbuildConfig(

export async function composeRsbuildEnvironments(
rslibConfig: RslibConfig,
path?: string,
): Promise<Record<string, EnvironmentConfig>> {
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<Format, number> = rsbuildConfigObject.reduce(
const formatCount: Record<Format, number> = rsbuildConfigWithLibInfo.reduce(
(acc, { format }) => {
acc[format] = (acc[format] ?? 0) + 1;
return acc;
},
{} as Record<Format, number>,
);

const formatIndex: Record<Format, number> = {
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;
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/types/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/types/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ export type PkgJson = {
export type DeepRequired<T> = Required<{
[K in keyof T]: T[K] extends Required<T[K]> ? T[K] : DeepRequired<T[K]>;
}>;

export type ExcludesFalse = <T>(x: T | false | undefined | null) => x is T;
120 changes: 119 additions & 1 deletion packages/core/tests/config.test.ts
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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.',
);
});
});
1 change: 1 addition & 0 deletions website/docs/en/config/lib/_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
"footer",
"dts",
"shims",
"id",
"umd-name"
]
60 changes: 60 additions & 0 deletions website/docs/en/config/lib/id.mdx
Original file line number Diff line number Diff line change
@@ -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.
:::
Loading

0 comments on commit fe69381

Please sign in to comment.