Skip to content

Commit 0819f3b

Browse files
committed
feat: support more param for build command
1 parent 8a762f2 commit 0819f3b

File tree

19 files changed

+572
-66
lines changed

19 files changed

+572
-66
lines changed

packages/core/__mocks__/rslog.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ module.exports = {
22
logger: {
33
warn: () => {},
44
override: () => {},
5+
debug: () => {},
56
},
67
};

packages/core/src/cli/commands.ts

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import type { LogLevel, RsbuildMode } from '@rsbuild/core';
22
import cac, { type CAC } from 'cac';
33
import type { ConfigLoader } from '../config';
4+
import type { Format } from '../types/config';
45
import { logger } from '../utils/logger';
56
import { build } from './build';
6-
import { init } from './init';
7+
import { initConfig } from './initConfig';
78
import { inspect } from './inspect';
89
import { startMFDevServer } from './mf';
910
import { watchFilesForRestart } from './restart';
@@ -16,6 +17,19 @@ export type CommonOptions = {
1617
lib?: string[];
1718
configLoader?: ConfigLoader;
1819
logLevel?: LogLevel;
20+
format?: Format;
21+
entry?: string[];
22+
distPath?: string;
23+
bundle?: boolean;
24+
syntax?: string;
25+
target?: string;
26+
dts?: boolean;
27+
external?: string[];
28+
minify?: boolean;
29+
clean?: boolean;
30+
autoExtension?: boolean;
31+
autoExternal?: boolean;
32+
tsconfig?: string;
1933
};
2034

2135
export type BuildOptions = CommonOptions & {
@@ -84,13 +98,52 @@ export function runCli(): void {
8498

8599
buildCommand
86100
.option('-w, --watch', 'turn on watch mode, watch for changes and rebuild')
101+
.option(
102+
'--entry <entry>',
103+
'set entry file or pattern (repeatable) (e.g. --entry="src/*" or --entry="main=src/main.ts")',
104+
{
105+
type: [String],
106+
default: [],
107+
},
108+
)
109+
.option('--dist-path <dir>', 'set output directory')
110+
.option('--bundle', 'enable bundle mode (use --no-bundle to disable)')
111+
.option(
112+
'--format <format>',
113+
'specify the output format (esm | cjs | umd | mf | iife)',
114+
)
115+
.option(
116+
'--syntax <syntax>',
117+
'set build syntax target (e.g. --syntax=es2018 or --syntax=["node 14", "Chrome 103"])',
118+
)
119+
.option('--target <target>', 'set runtime target (web | node)')
120+
.option('--dts', 'emit declaration files (use --no-dts to disable)')
121+
.option('--external <pkg>', 'add package to externals (repeatable)', {
122+
type: [String],
123+
default: [],
124+
})
125+
.option('--minify', 'minify output (use --no-minify to disable)')
126+
.option(
127+
'--clean',
128+
'clean dist directory before build (use --no-clean to disable)',
129+
)
130+
.option(
131+
'--auto-extension',
132+
'control automatic extension redirect (use --no-auto-extension to disable)',
133+
)
134+
.option(
135+
'--auto-external',
136+
'control automatic dependency externalization (use --no-auto-external to disable)',
137+
)
138+
.option(
139+
'--tsconfig <path>',
140+
'use specific tsconfig (relative to project root)',
141+
)
87142
.action(async (options: BuildOptions) => {
88143
try {
89144
const cliBuild = async () => {
90-
const { config, watchFiles } = await init(options);
91-
145+
const { config, watchFiles } = await initConfig(options);
92146
await build(config, options);
93-
94147
if (options.watch) {
95148
watchFilesForRestart(watchFiles, async () => {
96149
await cliBuild();
@@ -120,7 +173,7 @@ export function runCli(): void {
120173
.action(async (options: InspectOptions) => {
121174
try {
122175
// TODO: inspect should output Rslib's config
123-
const { config } = await init(options);
176+
const { config } = await initConfig(options);
124177
await inspect(config, {
125178
lib: options.lib,
126179
mode: options.mode,
@@ -137,7 +190,7 @@ export function runCli(): void {
137190
mfDevCommand.action(async (options: CommonOptions) => {
138191
try {
139192
const cliMfDev = async () => {
140-
const { config, watchFiles } = await init(options);
193+
const { config, watchFiles } = await initConfig(options);
141194
await startMFDevServer(config, {
142195
lib: options.lib,
143196
});

packages/core/src/cli/init.ts

Lines changed: 0 additions & 56 deletions
This file was deleted.
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import path from 'node:path';
2+
import util from 'node:util';
3+
import { loadEnv, type RsbuildEntry } from '@rsbuild/core';
4+
import { loadConfig } from '../config';
5+
import type {
6+
EcmaScriptVersion,
7+
RsbuildConfigOutputTarget,
8+
RslibConfig,
9+
Syntax,
10+
} from '../types';
11+
import { getAbsolutePath } from '../utils/helper';
12+
import { logger } from '../utils/logger';
13+
import type { CommonOptions } from './commands';
14+
import { onBeforeRestart } from './restart';
15+
16+
const getEnvDir = (cwd: string, envDir?: string) => {
17+
if (envDir) {
18+
return path.isAbsolute(envDir) ? envDir : path.resolve(cwd, envDir);
19+
}
20+
return cwd;
21+
};
22+
23+
export const parseEntryOption = (
24+
entries?: string[],
25+
): Record<string, string> | undefined => {
26+
if (!entries || entries.length === 0) {
27+
return undefined;
28+
}
29+
30+
const parsed: Record<string, string> = {};
31+
let unnamedIndex = 0;
32+
33+
for (const rawEntry of entries) {
34+
const value = rawEntry?.trim();
35+
if (!value) {
36+
continue;
37+
}
38+
39+
const equalIndex = value.indexOf('=');
40+
if (equalIndex > -1) {
41+
const name = value.slice(0, equalIndex).trim();
42+
const entryPath = value.slice(equalIndex + 1).trim();
43+
if (name && entryPath) {
44+
parsed[name] = entryPath;
45+
continue;
46+
}
47+
}
48+
49+
unnamedIndex += 1;
50+
const key = unnamedIndex === 1 ? 'index' : `entry${unnamedIndex}`;
51+
parsed[key] = value;
52+
}
53+
54+
return Object.keys(parsed).length === 0 ? undefined : parsed;
55+
};
56+
57+
export const parseSyntaxOption = (syntax?: string): Syntax | undefined => {
58+
if (!syntax) {
59+
return undefined;
60+
}
61+
62+
const trimmed = syntax.trim();
63+
if (!trimmed) {
64+
return undefined;
65+
}
66+
67+
if (trimmed.startsWith('[')) {
68+
try {
69+
const parsed = JSON.parse(trimmed);
70+
if (Array.isArray(parsed)) {
71+
return parsed;
72+
}
73+
} catch (e) {
74+
const reason = e instanceof Error ? e.message : String(e);
75+
throw new Error(
76+
`Failed to parse --syntax option "${trimmed}" as JSON array: ${reason}`,
77+
);
78+
}
79+
}
80+
81+
return trimmed as EcmaScriptVersion;
82+
};
83+
84+
const applyCliOptions = (
85+
config: RslibConfig,
86+
options: CommonOptions,
87+
root: string,
88+
): void => {
89+
if (options.root) config.root = root;
90+
if (options.logLevel) config.logLevel = options.logLevel;
91+
92+
for (const lib of config.lib) {
93+
if (options.format !== undefined) lib.format = options.format;
94+
if (options.bundle !== undefined) lib.bundle = options.bundle;
95+
if (options.dts !== undefined) lib.dts = options.dts;
96+
if (options.autoExtension !== undefined)
97+
lib.autoExtension = options.autoExtension;
98+
if (options.autoExternal !== undefined)
99+
lib.autoExternal = options.autoExternal;
100+
if (options.tsconfig !== undefined) {
101+
lib.source ||= {};
102+
lib.source.tsconfigPath = options.tsconfig;
103+
}
104+
const entry = parseEntryOption(options.entry);
105+
if (entry !== undefined) {
106+
lib.source ||= {};
107+
lib.source.entry = entry as RsbuildEntry;
108+
}
109+
const syntax = parseSyntaxOption(options.syntax);
110+
if (syntax !== undefined) lib.syntax = syntax;
111+
const output = lib.output ?? {};
112+
if (options.target !== undefined)
113+
output.target = options.target as RsbuildConfigOutputTarget;
114+
if (options.minify !== undefined) output.minify = options.minify;
115+
if (options.clean !== undefined) output.cleanDistPath = options.clean;
116+
const externals = options.external?.filter(Boolean) ?? [];
117+
if (externals.length > 0) output.externals = externals;
118+
if (options.distPath) {
119+
output.distPath ??= {};
120+
output.distPath.root = options.distPath;
121+
}
122+
}
123+
};
124+
125+
export async function initConfig(options: CommonOptions): Promise<{
126+
config: RslibConfig;
127+
configFilePath: string;
128+
watchFiles: string[];
129+
}> {
130+
const cwd = process.cwd();
131+
const root = options.root ? getAbsolutePath(cwd, options.root) : cwd;
132+
const envs = loadEnv({
133+
cwd: getEnvDir(root, options.envDir),
134+
mode: options.envMode,
135+
});
136+
137+
onBeforeRestart(envs.cleanup);
138+
139+
const { content: config, filePath: configFilePath } = await loadConfig({
140+
cwd: root,
141+
path: options.config,
142+
envMode: options.envMode,
143+
loader: options.configLoader,
144+
});
145+
146+
config.source ||= {};
147+
config.source.define = {
148+
...envs.publicVars,
149+
...config.source.define,
150+
};
151+
152+
applyCliOptions(config, options, root);
153+
154+
logger.debug('Rslib config used to generate Rsbuild environments:');
155+
logger.debug(`\n${util.inspect(config, { depth: null, colors: true })}`);
156+
157+
return {
158+
config,
159+
configFilePath,
160+
watchFiles: [configFilePath, ...envs.filePaths],
161+
};
162+
}

packages/core/tests/cli.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { describe, expect, test } from '@rstest/core';
2+
import { parseEntryOption, parseSyntaxOption } from '../src/cli/initConfig';
3+
4+
describe('parseEntryOption', () => {
5+
test('returns undefined when entries are missing or empty', () => {
6+
expect(parseEntryOption()).toBeUndefined();
7+
expect(parseEntryOption([])).toBeUndefined();
8+
expect(parseEntryOption(['', ' '])).toBeUndefined();
9+
});
10+
11+
test('parses named and positional entries with trimming', () => {
12+
const result = parseEntryOption([
13+
' main = ./src/main.ts ',
14+
' ./src/utils.ts ',
15+
'entry=./src/entry.ts',
16+
'./src/extra.ts',
17+
]);
18+
19+
expect(result).toEqual({
20+
main: './src/main.ts',
21+
index: './src/utils.ts',
22+
entry: './src/entry.ts',
23+
entry2: './src/extra.ts',
24+
});
25+
});
26+
});
27+
28+
describe('parseSyntaxOption', () => {
29+
test('returns undefined for missing or whitespace values', () => {
30+
expect(parseSyntaxOption()).toBeUndefined();
31+
expect(parseSyntaxOption('')).toBeUndefined();
32+
expect(parseSyntaxOption(' ')).toBeUndefined();
33+
});
34+
35+
test('returns the trimmed ECMAScript version when not a JSON array', () => {
36+
expect(parseSyntaxOption(' es2020 ')).toBe('es2020');
37+
});
38+
39+
test('parses JSON array syntax', () => {
40+
expect(parseSyntaxOption('["chrome 120", "firefox 115"]')).toEqual([
41+
'chrome 120',
42+
'firefox 115',
43+
]);
44+
});
45+
46+
test('throws descriptive error when JSON parsing fails', () => {
47+
const parseInvalidSyntax = () => parseSyntaxOption('[invalid');
48+
expect(parseInvalidSyntax).toThrowError(
49+
/Failed to parse --syntax option "\[inv.*JSON array/,
50+
);
51+
});
52+
});

0 commit comments

Comments
 (0)