From 79f500c0d136a990124bf4063fc4a44ef0f28edd Mon Sep 17 00:00:00 2001 From: Timeless0911 <1604889533@qq.com> Date: Fri, 12 Dec 2025 17:02:49 +0800 Subject: [PATCH 1/3] refactor!: enhance CLI behaviour and logs --- packages/core/bin/rslib.js | 10 ++---- packages/core/src/cli/commands.ts | 13 ++++++-- packages/core/src/cli/index.ts | 54 +++++++++++++++++++++++++++++++ packages/core/src/cli/prepare.ts | 42 ------------------------ packages/core/src/index.ts | 8 +++-- 5 files changed, 72 insertions(+), 55 deletions(-) create mode 100644 packages/core/src/cli/index.ts delete mode 100644 packages/core/src/cli/prepare.ts diff --git a/packages/core/bin/rslib.js b/packages/core/bin/rslib.js index 665f09b3f..a29c927fa 100755 --- a/packages/core/bin/rslib.js +++ b/packages/core/bin/rslib.js @@ -13,14 +13,8 @@ if (enableCompileCache) { } async function main() { - const { logger, prepareCli, runCli } = await import('../dist/index.js'); - prepareCli(); - - try { - runCli(); - } catch (err) { - logger.error(err); - } + const { runCLI } = await import('../dist/index.js'); + runCLI(); } main(); diff --git a/packages/core/src/cli/commands.ts b/packages/core/src/cli/commands.ts index 613bf1746..62286fa5f 100644 --- a/packages/core/src/cli/commands.ts +++ b/packages/core/src/cli/commands.ts @@ -1,7 +1,7 @@ import type { LogLevel, RsbuildMode, RsbuildPlugin } from '@rsbuild/core'; import cac, { type CAC } from 'cac'; import type { ConfigLoader } from '../config'; -import type { Format, Syntax } from '../types/config'; +import type { Format, Syntax } from '../types'; import { color } from '../utils/color'; import { logger } from '../utils/logger'; import { build } from './build'; @@ -10,6 +10,8 @@ import { inspect } from './inspect'; import { startMFDevServer } from './mf'; import { watchFilesForRestart } from './restart'; +export const RSPACK_BUILD_ERROR = 'Rspack build failed.'; + export type CommonOptions = { root?: string; config?: string; @@ -81,11 +83,12 @@ const applyCommonOptions = (cli: CAC) => { ); }; -export function runCli(): void { +export function setupCommands(): void { const cli = cac('rslib'); cli.version(RSLIB_VERSION); + // Apply common options to all commands applyCommonOptions(cli); const buildDescription = `build the library for production ${color.dim('(default if no command is given)')}`; @@ -162,7 +165,11 @@ export function runCli(): void { await cliBuild(); } catch (err) { - logger.error('Failed to build.'); + const isRspackError = + err instanceof Error && err.message === RSPACK_BUILD_ERROR; + if (!isRspackError) { + logger.error('Failed to build.'); + } if (err instanceof AggregateError) { for (const error of err.errors) { logger.error(error); diff --git a/packages/core/src/cli/index.ts b/packages/core/src/cli/index.ts new file mode 100644 index 000000000..881a706f9 --- /dev/null +++ b/packages/core/src/cli/index.ts @@ -0,0 +1,54 @@ +import type { LogLevel } from '@rsbuild/core'; +import { isDebug, logger } from '../utils/logger'; +import { setupCommands } from './commands'; + +function initNodeEnv() { + if (!process.env.NODE_ENV) { + const command = process.argv[2] ?? ''; + process.env.NODE_ENV = ['build'].includes(command) + ? 'production' + : 'development'; + } +} + +function showGreeting() { + // Ensure consistent spacing before the greeting message. + // Different package managers handle output formatting differently - some automatically + // add a blank line before command output, while others do not. + const { npm_execpath, npm_lifecycle_event, NODE_RUN_SCRIPT_NAME } = + process.env; + const isNpx = npm_lifecycle_event === 'npx'; + const isBun = npm_execpath?.includes('.bun'); + const isNodeRun = Boolean(NODE_RUN_SCRIPT_NAME); + const prefix = isNpx || isBun || isNodeRun ? '\n' : ''; + logger.greet(`${prefix}Rslib v${RSLIB_VERSION}\n`); +} + +// ensure log level is set before any log is printed +function setupLogLevel() { + const logLevelIndex = process.argv.findIndex( + (item) => item === '--log-level' || item === '--logLevel', + ); + if (logLevelIndex !== -1) { + const level = process.argv[logLevelIndex + 1]; + if (level && ['warn', 'error', 'silent'].includes(level) && !isDebug()) { + logger.level = level as LogLevel; + } + } +} + +export function runCLI(): void { + // make it easier to identify the process via activity monitor or other tools + process.title = 'rslib-node'; + + initNodeEnv(); + setupLogLevel(); + showGreeting(); + + try { + setupCommands(); + } catch (err) { + logger.error('Failed to start Rslib CLI.'); + logger.error(err); + } +} diff --git a/packages/core/src/cli/prepare.ts b/packages/core/src/cli/prepare.ts deleted file mode 100644 index 644556eaf..000000000 --- a/packages/core/src/cli/prepare.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { LogLevel } from '@rsbuild/core'; -import { isDebug, logger } from '../utils/logger'; - -function initNodeEnv() { - if (!process.env.NODE_ENV) { - const command = process.argv[2] ?? ''; - process.env.NODE_ENV = ['build'].includes(command) - ? 'production' - : 'development'; - } -} - -// ensure log level is set before any log is printed -function setupLogLevel() { - const logLevelIndex = process.argv.findIndex( - (item) => item === '--log-level' || item === '--logLevel', - ); - if (logLevelIndex !== -1) { - const level = process.argv[logLevelIndex + 1]; - if (level && ['warn', 'error', 'silent'].includes(level) && !isDebug()) { - logger.level = level as LogLevel; - } - } -} - -export function prepareCli(): void { - initNodeEnv(); - setupLogLevel(); - - // Print a blank line to keep the greet log nice. - // Some package managers automatically output a blank line, some do not. - const { npm_execpath } = process.env; - if ( - !npm_execpath || - npm_execpath.includes('npx-cli.js') || - npm_execpath.includes('.bun') - ) { - logger.log(); - } - - logger.greet(` Rslib v${RSLIB_VERSION}\n`); -} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 62d4101f0..4754b9e3c 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,8 +1,12 @@ +/** + * The methods and types exported from this file are considered as + * the public API of @rslib/core. + */ + +export { runCLI } from './cli'; export { build } from './cli/build'; -export { runCli } from './cli/commands'; export { inspect } from './cli/inspect'; export { startMFDevServer } from './cli/mf'; -export { prepareCli } from './cli/prepare'; export { composeCreateRsbuildConfig as unstable_composeCreateRsbuildConfig, defineConfig, From 1c8a0d8b37bacba52390301653ca9ee7f17afb04 Mon Sep 17 00:00:00 2001 From: Timeless0911 <1604889533@qq.com> Date: Mon, 15 Dec 2025 15:22:34 +0800 Subject: [PATCH 2/3] refactor: load config --- packages/core/src/cli/commands.ts | 2 +- packages/core/src/cli/initConfig.ts | 6 +- packages/core/src/config.ts | 83 +----------- packages/core/src/constant.ts | 11 -- packages/core/src/index.ts | 8 +- packages/core/src/loadConfig.ts | 120 ++++++++++++++++++ packages/core/src/types/config.ts | 13 -- packages/core/tests/config.test.ts | 2 +- .../fixtures/config/cjs/rslib.config.cjs | 2 +- .../fixtures/config/esm/rslib.config.mjs | 2 +- 10 files changed, 134 insertions(+), 115 deletions(-) create mode 100644 packages/core/src/loadConfig.ts diff --git a/packages/core/src/cli/commands.ts b/packages/core/src/cli/commands.ts index 62286fa5f..fbc299c16 100644 --- a/packages/core/src/cli/commands.ts +++ b/packages/core/src/cli/commands.ts @@ -1,6 +1,6 @@ import type { LogLevel, RsbuildMode, RsbuildPlugin } from '@rsbuild/core'; import cac, { type CAC } from 'cac'; -import type { ConfigLoader } from '../config'; +import type { ConfigLoader } from '../loadConfig'; import type { Format, Syntax } from '../types'; import { color } from '../utils/color'; import { logger } from '../utils/logger'; diff --git a/packages/core/src/cli/initConfig.ts b/packages/core/src/cli/initConfig.ts index e21b75e0d..57866216c 100644 --- a/packages/core/src/cli/initConfig.ts +++ b/packages/core/src/cli/initConfig.ts @@ -1,7 +1,7 @@ import path from 'node:path'; import util from 'node:util'; import { loadEnv, type RsbuildEntry } from '@rsbuild/core'; -import { loadConfig } from '../config'; +import { loadConfig } from '../loadConfig'; import type { LibConfig, RsbuildConfigOutputTarget, @@ -107,7 +107,7 @@ export const applyCliOptions = ( export async function initConfig(options: CommonOptions): Promise<{ config: RslibConfig; - configFilePath?: string; + configFilePath: string | null; watchFiles: string[]; }> { const cwd = process.cwd(); @@ -126,7 +126,7 @@ export async function initConfig(options: CommonOptions): Promise<{ loader: options.configLoader, }); - if (configFilePath === undefined) { + if (configFilePath === null) { config.lib = [{} satisfies LibConfig]; logger.debug( 'No config file found. Falling back to CLI options for the default library.', diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 27f16d1ad..081742d4f 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -1,9 +1,8 @@ import fs from 'node:fs'; -import path, { dirname, extname, isAbsolute, join } from 'node:path'; +import path, { dirname, extname, join } from 'node:path'; import { defineConfig as defineRsbuildConfig, type EnvironmentConfig, - loadConfig as loadRsbuildConfig, mergeRsbuildConfig, type RsbuildConfig, type RsbuildEntry, @@ -16,8 +15,6 @@ import { import { glob } from 'tinyglobby'; import { composeAssetConfig } from './asset/assetConfig'; import { - DEFAULT_CONFIG_EXTENSIONS, - DEFAULT_CONFIG_NAME, DTS_EXTENSIONS_PATTERN, JS_EXTENSIONS_PATTERN, SWC_HELPERS, @@ -50,9 +47,6 @@ import type { RsbuildConfigOutputTarget, RsbuildConfigWithLibInfo, RslibConfig, - RslibConfigAsyncFn, - RslibConfigExport, - RslibConfigSyncFn, RspackResolver, Shims, Syntax, @@ -81,81 +75,6 @@ import { } from './utils/syntax'; import { loadTsconfig } from './utils/tsconfig'; -/** - * This function helps you to autocomplete configuration types. - * It accepts a Rslib config object, or a function that returns a config. - */ -export function defineConfig(config: RslibConfig): RslibConfig; -export function defineConfig(config: RslibConfigSyncFn): RslibConfigSyncFn; -export function defineConfig(config: RslibConfigAsyncFn): RslibConfigAsyncFn; -export function defineConfig(config: RslibConfigExport): RslibConfigExport; -export function defineConfig(config: RslibConfigExport) { - return config; -} - -const findConfig = (basePath: string): string | undefined => { - return DEFAULT_CONFIG_EXTENSIONS.map((ext) => basePath + ext).find( - fs.existsSync, - ); -}; - -const resolveConfigPath = ( - root: string, - customConfig?: string, -): string | undefined => { - if (customConfig) { - const customConfigPath = isAbsolute(customConfig) - ? customConfig - : join(root, customConfig); - if (fs.existsSync(customConfigPath)) { - return customConfigPath; - } - logger.warn(`Cannot find config file: ${color.dim(customConfigPath)}\n`); - } - - const configFilePath = findConfig(join(root, DEFAULT_CONFIG_NAME)); - - if (configFilePath) { - return configFilePath; - } - return undefined; -}; - -export type ConfigLoader = 'auto' | 'jiti' | 'native'; - -export async function loadConfig({ - cwd = process.cwd(), - path, - envMode, - loader, -}: { - cwd?: string; - path?: string; - envMode?: string; - loader?: ConfigLoader; -}): Promise<{ - content: RslibConfig; - filePath?: string; -}> { - const configFilePath = resolveConfigPath(cwd, path); - if (!configFilePath) { - return { - content: { - lib: [], - }, - filePath: undefined, - }; - } - const { content } = await loadRsbuildConfig({ - cwd: dirname(configFilePath), - path: configFilePath, - envMode, - loader, - }); - - return { content: content as RslibConfig, filePath: configFilePath }; -} - // Match logic is derived from https://github.com/webpack/webpack/blob/94aba382eccf3de1004d235045d4462918dfdbb7/lib/ExternalModuleFactoryPlugin.js#L89-L158 const handleMatchedExternal = ( value: string | string[] | boolean | Record, diff --git a/packages/core/src/constant.ts b/packages/core/src/constant.ts index 3e4bae461..2cd8fdd12 100644 --- a/packages/core/src/constant.ts +++ b/packages/core/src/constant.ts @@ -1,14 +1,3 @@ -export const DEFAULT_CONFIG_NAME = 'rslib.config'; - -export const DEFAULT_CONFIG_EXTENSIONS = [ - '.js', - '.ts', - '.mjs', - '.mts', - '.cjs', - '.cts', -] as const; - export const SWC_HELPERS = '@swc/helpers'; const DTS_EXTENSIONS: string[] = ['d.ts', 'd.mts', 'd.cts']; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 4754b9e3c..d1320e6d2 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -7,11 +7,15 @@ export { runCLI } from './cli'; export { build } from './cli/build'; export { inspect } from './cli/inspect'; export { startMFDevServer } from './cli/mf'; +export { composeCreateRsbuildConfig as unstable_composeCreateRsbuildConfig } from './config'; export { - composeCreateRsbuildConfig as unstable_composeCreateRsbuildConfig, + type ConfigParams, defineConfig, + type LoadConfigOptions, + type LoadConfigResult, loadConfig, -} from './config'; +} from './loadConfig'; + export type * from './types'; export { logger } from './utils/logger'; diff --git a/packages/core/src/loadConfig.ts b/packages/core/src/loadConfig.ts new file mode 100644 index 000000000..cfaa06899 --- /dev/null +++ b/packages/core/src/loadConfig.ts @@ -0,0 +1,120 @@ +import fs from 'node:fs'; +import { dirname, isAbsolute, join } from 'node:path'; +import { + loadConfig as loadRsbuildConfig, + type LoadConfigOptions as RsbuildLoadConfigOptions, +} from '@rsbuild/core'; +import type { RslibConfig } from './types'; +import { color } from './utils/color'; +import { logger } from './utils/logger'; + +export type ConfigParams = { + env: string; + command: string; + envMode?: string; + meta?: Record; +}; + +export type RslibConfigSyncFn = (env: ConfigParams) => RslibConfig; + +export type RslibConfigAsyncFn = (env: ConfigParams) => Promise; + +export type RslibConfigExport = + | RslibConfig + | RslibConfigSyncFn + | RslibConfigAsyncFn; + +export type LoadConfigOptions = Pick< + RsbuildLoadConfigOptions, + 'cwd' | 'path' | 'envMode' | 'meta' | 'loader' +>; + +export type ConfigLoader = RsbuildLoadConfigOptions['loader']; + +export type LoadConfigResult = { + /** + * The loaded configuration object. + */ + content: RslibConfig; + /** + * The path to the loaded configuration file. + * Return `null` if the configuration file is not found. + */ + filePath: string | null; +}; + +/** + * This function helps you to autocomplete configuration types. + * It accepts a Rslib config object, or a function that returns a config. + */ +export function defineConfig(config: RslibConfig): RslibConfig; +export function defineConfig(config: RslibConfigSyncFn): RslibConfigSyncFn; +export function defineConfig(config: RslibConfigAsyncFn): RslibConfigAsyncFn; +export function defineConfig(config: RslibConfigExport): RslibConfigExport; +export function defineConfig(config: RslibConfigExport) { + return config; +} + +const resolveConfigPath = ( + root: string, + customConfig?: string, +): string | null => { + if (customConfig) { + const customConfigPath = isAbsolute(customConfig) + ? customConfig + : join(root, customConfig); + if (fs.existsSync(customConfigPath)) { + return customConfigPath; + } + logger.warn(`Cannot find config file: ${color.dim(customConfigPath)}\n`); + } + + const CONFIG_FILES = [ + // `.mjs` and `.ts` are the most used configuration types, + // so we resolve them first for performance + 'rslib.config.mjs', + 'rslib.config.ts', + 'rslib.config.js', + 'rslib.config.cjs', + 'rslib.config.mts', + 'rslib.config.cts', + ]; + + for (const file of CONFIG_FILES) { + const configFile = join(root, file); + + if (fs.existsSync(configFile)) { + return configFile; + } + } + + return null; +}; + +export async function loadConfig({ + cwd = process.cwd(), + path, + envMode, + meta, + loader, +}: LoadConfigOptions): Promise { + const configFilePath = resolveConfigPath(cwd, path); + + if (!configFilePath) { + logger.debug('no config file found.'); + return { + content: {} as RslibConfig, + filePath: null, + }; + } + + const { content } = await loadRsbuildConfig({ + cwd: dirname(configFilePath), + path: configFilePath, + envMode, + meta, + loader, + }); + + return { content: content as RslibConfig, filePath: configFilePath }; +} diff --git a/packages/core/src/types/config.ts b/packages/core/src/types/config.ts index e84a9bb6e..14270a546 100644 --- a/packages/core/src/types/config.ts +++ b/packages/core/src/types/config.ts @@ -390,16 +390,3 @@ export interface RslibConfig extends RsbuildConfig { */ output?: RslibOutputConfig; } - -export type ConfigParams = { - env: string; - command: string; - envMode?: string; -}; - -export type RslibConfigSyncFn = (env: ConfigParams) => RslibConfig; -export type RslibConfigAsyncFn = (env: ConfigParams) => Promise; -export type RslibConfigExport = - | RslibConfig - | RslibConfigSyncFn - | RslibConfigAsyncFn; diff --git a/packages/core/tests/config.test.ts b/packages/core/tests/config.test.ts index 36d152e07..0fdfd0992 100644 --- a/packages/core/tests/config.test.ts +++ b/packages/core/tests/config.test.ts @@ -7,8 +7,8 @@ import { initConfig } from '../src/cli/initConfig'; import { composeCreateRsbuildConfig, composeRsbuildEnvironments, - loadConfig, } from '../src/config'; +import { loadConfig } from '../src/loadConfig'; import type { RslibConfig } from '../src/types/config'; rs.mock('rslog'); diff --git a/packages/core/tests/fixtures/config/cjs/rslib.config.cjs b/packages/core/tests/fixtures/config/cjs/rslib.config.cjs index 73396182b..af5854b2e 100644 --- a/packages/core/tests/fixtures/config/cjs/rslib.config.cjs +++ b/packages/core/tests/fixtures/config/cjs/rslib.config.cjs @@ -1,4 +1,4 @@ -const { defineConfig } = require('../../../../../core/src/config'); +const { defineConfig } = require('../../../../../core/src/loadConfig'); module.exports = defineConfig((_args) => ({ lib: [], diff --git a/packages/core/tests/fixtures/config/esm/rslib.config.mjs b/packages/core/tests/fixtures/config/esm/rslib.config.mjs index cb516051d..23ab5806b 100644 --- a/packages/core/tests/fixtures/config/esm/rslib.config.mjs +++ b/packages/core/tests/fixtures/config/esm/rslib.config.mjs @@ -1,4 +1,4 @@ -import { defineConfig } from '../../../../../core/src/config'; +import { defineConfig } from '../../../../../core/src/loadConfig'; export default defineConfig((_args) => ({ lib: [], From 7d5883d5b809f0c393a3b1e99ceba7b30ad7886b Mon Sep 17 00:00:00 2001 From: Timeless0911 <1604889533@qq.com> Date: Wed, 17 Dec 2025 20:08:00 +0800 Subject: [PATCH 3/3] feat: JS api --- packages/core/src/cli/build.ts | 37 --- packages/core/src/cli/commands.ts | 61 ++--- .../core/src/cli/{initConfig.ts => init.ts} | 77 +++--- packages/core/src/cli/inspect.ts | 36 --- packages/core/src/cli/mf.ts | 66 ----- packages/core/src/config.ts | 6 +- packages/core/src/createRslib.ts | 256 ++++++++++++++++++ packages/core/src/index.ts | 36 ++- packages/core/src/mergeConfig.ts | 11 + packages/core/src/{cli => }/restart.ts | 12 +- packages/core/src/types/index.ts | 1 + packages/core/src/types/rslib.ts | 100 +++++++ packages/core/src/utils/helper.ts | 10 +- .../tests/__snapshots__/config.test.ts.snap | 24 +- packages/core/tests/cli.test.ts | 2 +- packages/core/tests/config.test.ts | 22 +- pnpm-lock.yaml | 2 + rslint.jsonc | 1 + rstest.config.ts | 28 +- .../integration/cli/mf/dev-error/package.json | 6 + .../mf/dev-error/rslib.config.libNotExist.ts | 10 + .../cli/mf/dev-error/rslib.config.noFormat.ts | 6 + tests/integration/cli/mf/mf.test.ts | 36 +-- tests/scripts/shared.ts | 96 +++++-- website/docs/en/guide/basic/cli.mdx | 1 + website/docs/zh/guide/basic/cli.mdx | 1 + 26 files changed, 640 insertions(+), 304 deletions(-) delete mode 100644 packages/core/src/cli/build.ts rename packages/core/src/cli/{initConfig.ts => init.ts} (71%) delete mode 100644 packages/core/src/cli/inspect.ts delete mode 100644 packages/core/src/cli/mf.ts create mode 100644 packages/core/src/createRslib.ts create mode 100644 packages/core/src/mergeConfig.ts rename packages/core/src/{cli => }/restart.ts (84%) create mode 100644 packages/core/src/types/rslib.ts create mode 100644 tests/integration/cli/mf/dev-error/package.json create mode 100644 tests/integration/cli/mf/dev-error/rslib.config.libNotExist.ts create mode 100644 tests/integration/cli/mf/dev-error/rslib.config.noFormat.ts diff --git a/packages/core/src/cli/build.ts b/packages/core/src/cli/build.ts deleted file mode 100644 index f557016f9..000000000 --- a/packages/core/src/cli/build.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { createRsbuild, type RsbuildInstance } from '@rsbuild/core'; -import { composeRsbuildEnvironments, pruneEnvironments } from '../config'; -import type { RslibConfig } from '../types/config'; -import { isDebug } from '../utils/logger'; -import type { BuildOptions } from './commands'; -import { onBeforeRestart } from './restart'; - -export async function build( - config: RslibConfig, - options: Pick = {}, -): Promise { - const { environments } = await composeRsbuildEnvironments(config); - const rsbuildInstance = await createRsbuild({ - callerName: 'rslib', - config: { - mode: 'production', - root: config.root, - plugins: config.plugins, - dev: config.dev, - server: config.server, - logLevel: isDebug() ? 'info' : config.logLevel, - environments: pruneEnvironments(environments, options.lib), - }, - }); - - const buildInstance = await rsbuildInstance.build({ - watch: options.watch, - }); - - if (options.watch) { - onBeforeRestart(buildInstance.close); - } else { - await buildInstance.close(); - } - - return rsbuildInstance; -} diff --git a/packages/core/src/cli/commands.ts b/packages/core/src/cli/commands.ts index fbc299c16..df6f25309 100644 --- a/packages/core/src/cli/commands.ts +++ b/packages/core/src/cli/commands.ts @@ -1,24 +1,22 @@ -import type { LogLevel, RsbuildMode, RsbuildPlugin } from '@rsbuild/core'; +import type { LogLevel, RsbuildMode } from '@rsbuild/core'; import cac, { type CAC } from 'cac'; import type { ConfigLoader } from '../loadConfig'; +import { watchFilesForRestart } from '../restart'; import type { Format, Syntax } from '../types'; import { color } from '../utils/color'; import { logger } from '../utils/logger'; -import { build } from './build'; -import { initConfig } from './initConfig'; -import { inspect } from './inspect'; -import { startMFDevServer } from './mf'; -import { watchFilesForRestart } from './restart'; +import { init } from './init'; export const RSPACK_BUILD_ERROR = 'Rspack build failed.'; export type CommonOptions = { root?: string; config?: string; + configLoader?: ConfigLoader; + env?: boolean; envDir?: string; envMode?: string; lib?: string[]; - configLoader?: ConfigLoader; logLevel?: LogLevel; }; @@ -53,6 +51,13 @@ const applyCommonOptions = (cli: CAC) => { '-c, --config ', 'specify the configuration file, can be a relative or absolute path', ) + .option( + '--config-loader ', + 'Set the config file loader (auto | jiti | native)', + { + default: 'auto', + }, + ) .option( '-r, --root ', 'specify the project root directory, can be an absolute path or a path relative to cwd', @@ -61,13 +66,6 @@ const applyCommonOptions = (cli: CAC) => { '--env-mode ', 'specify the env mode to load the `.env.[mode]` file', ) - .option( - '--config-loader ', - 'Set the config file loader (auto | jiti | native)', - { - default: 'auto', - }, - ) .option('--env-dir ', 'specify the directory to load `.env` files') .option( '--log-level ', @@ -80,7 +78,8 @@ const applyCommonOptions = (cli: CAC) => { type: [String], default: [], }, - ); + ) + .option('--no-env', 'Disable loading of `.env` files'); }; export function setupCommands(): void { @@ -141,26 +140,15 @@ export function setupCommands(): void { .action(async (options: BuildOptions) => { try { const cliBuild = async () => { - const { config, watchFiles } = await initConfig(options); + const rslib = await init(options); if (options.watch) { - config.plugins = config.plugins || []; - config.plugins.push({ - name: 'rslib:on-after-build', - setup(api) { - api.onAfterBuild(({ isFirstCompile }) => { - if (isFirstCompile) { - logger.success('build complete, watching for changes...'); - } - }); - }, - } satisfies RsbuildPlugin); - - watchFilesForRestart(watchFiles, async () => { + watchFilesForRestart(rslib.context.watchFiles, async () => { await cliBuild(); }); } - await build(config, options); + + await rslib.build(options); }; await cliBuild(); @@ -189,12 +177,13 @@ export function setupCommands(): void { .action(async (options: InspectOptions) => { try { // TODO: inspect should output Rslib's config - const { config } = await initConfig(options); - await inspect(config, { + const rslib = await init(options); + await rslib.inspectConfig({ lib: options.lib, mode: options.mode, - output: options.output, + outputPath: options.output, verbose: options.verbose, + writeToDisk: true, }); } catch (err) { logger.error('Failed to inspect config.'); @@ -206,12 +195,12 @@ export function setupCommands(): void { mfDevCommand.action(async (options: MfDevOptions) => { try { const cliMfDev = async () => { - const { config, watchFiles } = await initConfig(options); - await startMFDevServer(config, { + const rslib = await init(options); + await rslib.startMFDevServer({ lib: options.lib, }); - watchFilesForRestart(watchFiles, async () => { + watchFilesForRestart(rslib.context.watchFiles, async () => { await cliMfDev(); }); }; diff --git a/packages/core/src/cli/initConfig.ts b/packages/core/src/cli/init.ts similarity index 71% rename from packages/core/src/cli/initConfig.ts rename to packages/core/src/cli/init.ts index 57866216c..4f2ad165d 100644 --- a/packages/core/src/cli/initConfig.ts +++ b/packages/core/src/cli/init.ts @@ -1,16 +1,16 @@ import path from 'node:path'; -import util from 'node:util'; -import { loadEnv, type RsbuildEntry } from '@rsbuild/core'; -import { loadConfig } from '../loadConfig'; +import type { RsbuildEntry } from '@rsbuild/core'; +import { createRslib } from '../createRslib'; +import { loadConfig as baseLoadConfig } from '../loadConfig'; import type { LibConfig, RsbuildConfigOutputTarget, RslibConfig, + RslibInstance, } from '../types'; -import { getAbsolutePath } from '../utils/helper'; -import { isDebugKey, logger } from '../utils/logger'; +import { ensureAbsolutePath } from '../utils/helper'; +import { logger } from '../utils/logger'; import type { BuildOptions, CommonOptions } from './commands'; -import { onBeforeRestart } from './restart'; const getEnvDir = (cwd: string, envDir?: string) => { if (envDir) { @@ -67,8 +67,13 @@ export const applyCliOptions = ( options: BuildOptions, root: string, ): void => { - if (options.root) config.root = root; - if (options.logLevel) config.logLevel = options.logLevel; + if (options.root) { + config.root = root; + } + + if (options.logLevel) { + config.logLevel = options.logLevel; + } for (const lib of config.lib) { if (options.format !== undefined) lib.format = options.format; @@ -105,21 +110,8 @@ export const applyCliOptions = ( } }; -export async function initConfig(options: CommonOptions): Promise<{ - config: RslibConfig; - configFilePath: string | null; - watchFiles: string[]; -}> { - const cwd = process.cwd(); - const root = options.root ? getAbsolutePath(cwd, options.root) : cwd; - const envs = loadEnv({ - cwd: getEnvDir(root, options.envDir), - mode: options.envMode, - }); - - onBeforeRestart(envs.cleanup); - - const { content: config, filePath: configFilePath } = await loadConfig({ +const loadConfig = async (options: CommonOptions, root: string) => { + const { content: config, filePath: configFilePath } = await baseLoadConfig({ cwd: root, path: options.config, envMode: options.envMode, @@ -128,28 +120,29 @@ export async function initConfig(options: CommonOptions): Promise<{ if (configFilePath === null) { config.lib = [{} satisfies LibConfig]; - logger.debug( - 'No config file found. Falling back to CLI options for the default library.', - ); + logger.debug('Falling back to CLI options for the default library.'); } - config.source ||= {}; - config.source.define = { - ...envs.publicVars, - ...config.source.define, - }; - applyCliOptions(config, options, root); - // only debug serialized rslib config when DEBUG=rslib - if (isDebugKey(['rslib'])) { - logger.debug('Rslib config used to generate Rsbuild environments:'); - logger.debug(`\n${util.inspect(config, { depth: null, colors: true })}`); - } + return config; +}; + +export async function init(options: CommonOptions): Promise { + const cwd = process.cwd(); + const root = options.root ? ensureAbsolutePath(cwd, options.root) : cwd; + + const rslib = await createRslib({ + cwd: root, + config: () => loadConfig(options, root), + loadEnv: + options.env === false + ? false + : { + cwd: getEnvDir(root, options.envDir), + mode: options.envMode, + }, + }); - return { - config, - configFilePath, - watchFiles: [configFilePath, ...envs.filePaths].filter(Boolean) as string[], - }; + return rslib; } diff --git a/packages/core/src/cli/inspect.ts b/packages/core/src/cli/inspect.ts deleted file mode 100644 index 6a496d911..000000000 --- a/packages/core/src/cli/inspect.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { createRsbuild, type RsbuildInstance } from '@rsbuild/core'; -import { composeRsbuildEnvironments, pruneEnvironments } from '../config'; -import type { RslibConfig } from '../types/config'; -import { isDebug } from '../utils/logger'; -import type { InspectOptions } from './commands'; - -export async function inspect( - config: RslibConfig, - options: Pick = {}, -): Promise { - const { environments } = await composeRsbuildEnvironments(config); - const rsbuildInstance = await createRsbuild({ - callerName: 'rslib', - config: { - mode: 'production', - root: config.root, - plugins: config.plugins, - dev: config.dev, - server: config.server, - logLevel: isDebug() ? 'info' : config.logLevel, - environments: pruneEnvironments(environments, options.lib), - }, - }); - - await rsbuildInstance.inspectConfig({ - mode: options.mode, - verbose: options.verbose, - outputPath: options.output, - writeToDisk: true, - extraConfigs: { - rslib: config, - }, - }); - - return rsbuildInstance; -} diff --git a/packages/core/src/cli/mf.ts b/packages/core/src/cli/mf.ts deleted file mode 100644 index 7f02650d3..000000000 --- a/packages/core/src/cli/mf.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { RsbuildInstance } from '@rsbuild/core'; -import { createRsbuild } from '@rsbuild/core'; -import { composeRsbuildEnvironments, pruneEnvironments } from '../config'; -import type { RslibConfig } from '../types'; -import { isDebug } from '../utils/logger'; -import type { CommonOptions } from './commands'; -import { onBeforeRestart } from './restart'; - -export async function startMFDevServer( - config: RslibConfig, - options: Pick = {}, -): Promise { - const rsbuildInstance = await initMFRsbuild(config, options); - return rsbuildInstance; -} - -async function initMFRsbuild( - config: RslibConfig, - options: Pick = {}, -): Promise { - const { environments, environmentWithInfos } = - await composeRsbuildEnvironments(config); - - const selectedEnvironmentIds = environmentWithInfos - .filter((env) => { - const isMf = env.format === 'mf'; - if (!options?.lib || options.lib.length === 0) { - return isMf; - } - return env.id && options.lib.includes(env.id); - }) - .map((env) => env.id); - - if (!selectedEnvironmentIds.length) { - throw new Error( - `No mf format found in ${ - options.lib - ? `libs ${options.lib.map((lib) => `"${lib}"`).join(', ')}` - : 'your config' - }, please check your config to ensure that the mf format is enabled correctly.`, - ); - } - - const selectedEnvironments = pruneEnvironments( - environments, - selectedEnvironmentIds, - ); - - const rsbuildInstance = await createRsbuild({ - callerName: 'rslib', - config: { - mode: 'development', - root: config.root, - plugins: config.plugins, - dev: config.dev, - server: config.server, - logLevel: isDebug() ? 'info' : config.logLevel, - environments: selectedEnvironments, - }, - }); - - const devServer = await rsbuildInstance.startDevServer(); - - onBeforeRestart(devServer.server.close); - return rsbuildInstance; -} diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 081742d4f..4f2f46e38 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -56,7 +56,7 @@ import { getDefaultExtension } from './utils/extension'; import { calcLongestCommonPath, checkMFPlugin, - getAbsolutePath, + ensureAbsolutePath, isDirectory, isEmptyObject, isIntermediateOutputFormat, @@ -1674,7 +1674,9 @@ async function composeLibRsbuildConfig( checkMFPlugin(config, sharedPlugins); // Get the absolute path of the root directory to align with Rsbuild's default behavior - const rootPath = root ? getAbsolutePath(process.cwd(), root) : process.cwd(); + const rootPath = root + ? ensureAbsolutePath(process.cwd(), root) + : process.cwd(); const pkgJson = readPackageJson(rootPath); const { compilerOptions } = await loadTsconfig( rootPath, diff --git a/packages/core/src/createRslib.ts b/packages/core/src/createRslib.ts new file mode 100644 index 000000000..ba9d13890 --- /dev/null +++ b/packages/core/src/createRslib.ts @@ -0,0 +1,256 @@ +import { join } from 'node:path'; +import util from 'node:util'; +import { + createRsbuild, + type EnvironmentConfig, + type LoadEnvResult, + loadEnv, + type RsbuildInstance, + type RsbuildPlugin, +} from '@rsbuild/core'; +import { composeRsbuildEnvironments, pruneEnvironments } from './config'; +import { onBeforeRestart } from './restart'; +import type { RslibConfig } from './types'; +import type { + BuildOptions, + CreateRslibOptions, + InspectConfigOptions, + RslibContext, + RslibInstance, + StartMFDevServerOptions, +} from './types/rslib'; +import { + ensureAbsolutePath, + getNodeEnv, + isFunction, + setNodeEnv, +} from './utils/helper'; +import { isDebug, isDebugKey, logger } from './utils/logger'; + +export function createContext( + options: CreateRslibOptions, + config: RslibConfig, + envs: LoadEnvResult | null, +): RslibContext { + const cwd = options.cwd || process.cwd(); + const rootPath = config.root ? ensureAbsolutePath(cwd, config.root) : cwd; + const cachePath = join(rootPath, 'node_modules', '.cache'); + const envFilePaths = envs ? envs.filePaths : []; + const watchFiles = [ + config._privateMeta?.configFilePath, + ...envFilePaths, + ].filter(Boolean) as string[]; + + return { + version: RSLIB_VERSION, + rootPath, + cachePath, + config, + watchFiles, + }; +} + +/** + * Create an Rslib instance. + */ +export async function createRslib( + options: CreateRslibOptions = {}, +): Promise { + const envs = options.loadEnv + ? loadEnv({ + cwd: options.cwd, + ...(typeof options.loadEnv === 'boolean' ? {} : options.loadEnv), + }) + : null; + + const configOrFactory = options.config; + const config = isFunction(configOrFactory) + ? await configOrFactory() + : configOrFactory || ({} as RslibConfig); + + if (envs) { + // define the public environment variables + config.source ||= {}; + config.source.define = { + ...envs.publicVars, + ...config.source.define, + }; + } + + // only debug serialized rslib config when DEBUG=rslib + if (isDebugKey(['rslib'])) { + logger.debug('Rslib config used to generate Rsbuild environments:'); + logger.debug( + util.inspect(config, { + depth: null, + colors: true, + compact: true, + breakLength: Number.POSITIVE_INFINITY, + }), + ); + } + + const context = createContext(options, config, envs); + + const createRsbuildInstance = async ( + options: CreateRslibOptions, + mode: 'development' | 'production', + environments: Record, + ): Promise => { + const rsbuildInstance = await createRsbuild({ + cwd: options.cwd, + callerName: 'rslib', + config: { + mode, + root: config.root, + plugins: config.plugins, + dev: config.dev, + server: config.server, + logLevel: isDebug() ? 'info' : config.logLevel, + environments, + }, + }); + + if (envs) { + rsbuildInstance.onCloseBuild(envs.cleanup); + rsbuildInstance.onCloseDevServer(envs.cleanup); + } + + return rsbuildInstance; + }; + + const build = async (buildOptions: BuildOptions = {}) => { + context.action = 'build'; + + if (!getNodeEnv()) { + setNodeEnv('production'); + } + + if (buildOptions.watch) { + config.plugins = config.plugins || []; + config.plugins.push({ + name: 'rslib:on-after-build', + setup(api) { + api.onAfterBuild(({ isFirstCompile }) => { + if (isFirstCompile) { + logger.success('build complete, watching for changes...'); + } + }); + }, + } satisfies RsbuildPlugin); + } + + const { environments } = await composeRsbuildEnvironments(config); + + const rsbuildInstance = await createRsbuildInstance( + options, + 'production', + pruneEnvironments(environments, buildOptions.lib), + ); + + const buildResult = await rsbuildInstance.build({ + watch: buildOptions.watch, + }); + + context.rsbuildConfig = rsbuildInstance.getNormalizedConfig(); + + if (buildOptions.watch) { + onBeforeRestart(buildResult.close); + } else { + await buildResult.close(); + } + + return buildResult; + }; + + const inspectConfig = async (inspectOptions: InspectConfigOptions = {}) => { + context.action = 'inspect'; + + if (inspectOptions.mode) { + setNodeEnv(inspectOptions.mode); + } else if (!getNodeEnv()) { + setNodeEnv('production'); + } + + const { environments } = await composeRsbuildEnvironments(config); + + const rsbuildInstance = await createRsbuildInstance( + options, + 'production', + pruneEnvironments(environments, inspectOptions.lib), + ); + + const inspectConfigResult = await rsbuildInstance.inspectConfig({ + mode: inspectOptions.mode, + verbose: inspectOptions.verbose, + outputPath: inspectOptions.outputPath, + writeToDisk: inspectOptions.writeToDisk, + extraConfigs: { + rslib: config, + }, + }); + + context.rsbuildConfig = rsbuildInstance.getNormalizedConfig(); + + return inspectConfigResult; + }; + + const startMFDevServer = async (mfOptions: StartMFDevServerOptions = {}) => { + context.action = 'mf-dev'; + + if (!getNodeEnv()) { + setNodeEnv('development'); + } + + const { environments, environmentWithInfos } = + await composeRsbuildEnvironments(config); + + const selectedEnvironmentIds = environmentWithInfos + .filter((env) => { + const isMf = env.format === 'mf'; + if (!mfOptions.lib || mfOptions.lib.length === 0) { + return isMf; + } + return env.id && mfOptions.lib.includes(env.id); + }) + .map((env) => env.id); + + if (!selectedEnvironmentIds.length) { + throw new Error( + `No mf format found in ${ + mfOptions.lib && mfOptions.lib.length > 0 + ? `libs ${mfOptions.lib.map((lib) => `"${lib}"`).join(', ')}` + : 'your config' + }, please check your config to ensure that the mf format is enabled correctly.`, + ); + } + + const selectedEnvironments = pruneEnvironments( + environments, + selectedEnvironmentIds, + ); + + const rsbuildInstance = await createRsbuildInstance( + options, + 'development', + selectedEnvironments, + ); + + const startDevServer = await rsbuildInstance.startDevServer(); + + context.rsbuildConfig = rsbuildInstance.getNormalizedConfig(); + + onBeforeRestart(startDevServer.server.close); + + return startDevServer; + }; + + const rslib: RslibInstance = { + context, + build, + inspectConfig, + startMFDevServer, + }; + + return rslib; +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d1320e6d2..1f190ea31 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -4,10 +4,7 @@ */ export { runCLI } from './cli'; -export { build } from './cli/build'; -export { inspect } from './cli/inspect'; -export { startMFDevServer } from './cli/mf'; -export { composeCreateRsbuildConfig as unstable_composeCreateRsbuildConfig } from './config'; +export { createRslib } from './createRslib'; export { type ConfigParams, defineConfig, @@ -15,12 +12,37 @@ export { type LoadConfigResult, loadConfig, } from './loadConfig'; +export { mergeRslibConfig } from './mergeConfig'; -export type * from './types'; -export { logger } from './utils/logger'; +export type { + AutoExternal, + BannerAndFooter, + BuildOptions, + CreateRslibOptions, + Dts, + Format, + InspectConfigOptions, + LibConfig, + LibExperiments, + Redirect, + RslibConfig, + RslibContext, + RslibInstance, + Shims, + StartMFDevServerOptions, + Syntax, +} from './types'; +export { type Logger, logger } from './utils/logger'; export const version: string = RSLIB_VERSION; export type * as Rsbuild from '@rsbuild/core'; export * as rsbuild from '@rsbuild/core'; -export { type RsbuildPlugin, type Rspack, rspack } from '@rsbuild/core'; +export { + type LoadEnvOptions, + type LoadEnvResult, + loadEnv, + type RsbuildPlugin, + type Rspack, + rspack, +} from '@rsbuild/core'; diff --git a/packages/core/src/mergeConfig.ts b/packages/core/src/mergeConfig.ts new file mode 100644 index 000000000..3538dc3fd --- /dev/null +++ b/packages/core/src/mergeConfig.ts @@ -0,0 +1,11 @@ +import { mergeRsbuildConfig } from '@rsbuild/core'; +import type { RslibConfig } from './types'; + +type RslibConfigWithOptionalLib = Omit & { + lib?: RslibConfig['lib']; +}; + +export const mergeRslibConfig: ( + ...originalConfigs: (RslibConfigWithOptionalLib | undefined)[] +) => RslibConfigWithOptionalLib = + mergeRsbuildConfig; diff --git a/packages/core/src/cli/restart.ts b/packages/core/src/restart.ts similarity index 84% rename from packages/core/src/cli/restart.ts rename to packages/core/src/restart.ts index a7a0f39f5..e80398201 100644 --- a/packages/core/src/cli/restart.ts +++ b/packages/core/src/restart.ts @@ -1,13 +1,13 @@ import path from 'node:path'; -import { color } from '../utils/color'; -import { debounce, isTTY } from '../utils/helper'; -import { logger } from '../utils/logger'; +import { color } from './utils/color'; +import { debounce, isTTY } from './utils/helper'; +import { logger } from './utils/logger'; export async function watchFilesForRestart( - files: string[], + files: string[] | undefined, restart: () => Promise, ): Promise { - if (!files.length) { + if (!files || !files.length) { return; } @@ -65,7 +65,7 @@ const beforeRestart = async ({ if (filePath) { const filename = path.basename(filePath); - logger.info(`restart because ${color.yellow(filename)} is changed.\n`); + logger.info(`restarting as ${color.yellow(filename)} is changed\n`); } else { logger.info('restarting...\n'); } diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 235e5fdbc..eb7601220 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -1,2 +1,3 @@ export type * from './config'; +export type * from './rslib'; export type * from './utils'; diff --git a/packages/core/src/types/rslib.ts b/packages/core/src/types/rslib.ts new file mode 100644 index 000000000..225120cce --- /dev/null +++ b/packages/core/src/types/rslib.ts @@ -0,0 +1,100 @@ +import type { + BuildResult, + InspectConfigResult, + LoadEnvOptions, + RsbuildConfig, + RsbuildMode, + StartServerResult, +} from '@rsbuild/core'; +import type { RslibConfig } from './config'; + +export type CommonOptions = { + /** + * Specify the library id to run the action on. + */ + lib?: string[]; +}; + +export type BuildOptions = CommonOptions & { + /** + * Whether to watch for file changes and rebuild. + * @default false + */ + watch?: boolean; +}; + +export type InspectConfigOptions = CommonOptions & { + /** + * Inspect the config in the specified mode. + * Available options: 'development' or 'production'. + * @default 'development' + */ + mode?: RsbuildMode; + /** + * Enables verbose mode to display the complete function + * content in the configuration. + * @default false + */ + verbose?: boolean; + /** + * Specify the output path for inspection results. + * @default 'output.distPath.root' + */ + outputPath?: string; + /** + * Whether to write the inspection results to disk. + * @default false + */ + writeToDisk?: boolean; +}; + +export type StartMFDevServerOptions = CommonOptions; + +export type ActionType = 'build' | 'inspect' | 'mf-dev'; + +export type RslibContext = { + /** The Rslib core version. */ + version: string; + /** The root path of current project. */ + rootPath: string; + /** Absolute path of cache files. */ + cachePath: string; + config: RslibConfig; + /** + * The current action type. + * - build: will be set when running `rslib build` or `rslib.build()` + * - inspect: will be set when running `rslib inspect` or `rslib.inspectConfig()` + * - mf-dev: will be set when running `rslib mf-dev` or `rslib.startMFDevServer()` + */ + action?: ActionType; + watchFiles?: string[]; + rsbuildConfig?: RsbuildConfig; +}; + +export type RslibInstance = { + context: RslibContext; + build(options?: BuildOptions): Promise; + inspectConfig(options?: InspectConfigOptions): Promise; + startMFDevServer( + options?: StartMFDevServerOptions, + ): Promise; +}; + +export type CreateRslibOptions = { + /** + * The root path of current project. + * @default process.cwd() + */ + cwd?: string; + /** + * Rslib configurations. + * Passing a function to load the config asynchronously with custom logic. + */ + config?: RslibConfig | (() => Promise); + /** + * Whether to call `loadEnv` to load environment variables and define them + * as global variables via `source.define`. + * @default false + */ + loadEnv?: boolean | LoadEnvOptions; +}; diff --git a/packages/core/src/utils/helper.ts b/packages/core/src/utils/helper.ts index 952b017e5..6d39088bb 100644 --- a/packages/core/src/utils/helper.ts +++ b/packages/core/src/utils/helper.ts @@ -109,7 +109,7 @@ export async function calcLongestCommonPath( return lca; } -export function getAbsolutePath(base: string, filepath: string): string { +export function ensureAbsolutePath(base: string, filepath: string): string { return isAbsolute(filepath) ? filepath : join(base, filepath); } @@ -255,3 +255,11 @@ export async function isDirectory(filePath: string): Promise { return false; } } + +export const isFunction = (func: unknown): func is (...args: any[]) => any => + typeof func === 'function'; + +export const getNodeEnv = (): string => process.env.NODE_ENV || ''; +export const setNodeEnv = (env: string): void => { + process.env.NODE_ENV = env; +}; diff --git a/packages/core/tests/__snapshots__/config.test.ts.snap b/packages/core/tests/__snapshots__/config.test.ts.snap index b63b025d9..5c8b1cdec 100644 --- a/packages/core/tests/__snapshots__/config.test.ts.snap +++ b/packages/core/tests/__snapshots__/config.test.ts.snap @@ -27,7 +27,7 @@ exports[`Should compose create Rsbuild config correctly > Enable experiment.adva }, cache: { type: 'persistent', - version: 'esm-development', + version: 'esm-production', storage: { type: 'filesystem', directory: '/node_modules/.cache/rspack' @@ -1066,7 +1066,7 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i }, cache: { type: 'persistent', - version: 'esm-development', + version: 'esm-production', storage: { type: 'filesystem', directory: '/node_modules/.cache/rspack' @@ -1690,7 +1690,7 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i use: [ /* config.module.rule('Rslib:js-entry-loader').use('rsbuild:lib-entry-module') */ { - loader: '/dist/entryModuleLoader.js' + loader: '/src/plugins/entryModuleLoader.ts' } ] } @@ -1812,7 +1812,7 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i }, cache: { type: 'persistent', - version: 'cjs-development', + version: 'cjs-production', storage: { type: 'filesystem', directory: '/node_modules/.cache/rspack' @@ -2540,7 +2540,7 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i }, cache: { type: 'persistent', - version: 'umd-development', + version: 'umd-production', storage: { type: 'filesystem', directory: '/node_modules/.cache/rspack' @@ -3094,7 +3094,7 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i ) ], moduleIds: 'named', - nodeEnv: false + nodeEnv: 'production' }, plugins: [ /* config.plugin('RsbuildCorePlugin') */ @@ -3171,7 +3171,7 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i }, cache: { type: 'persistent', - version: 'iife-development', + version: 'iife-production', storage: { type: 'filesystem', directory: '/node_modules/.cache/rspack' @@ -3703,7 +3703,7 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i use: [ /* config.module.rule('Rslib:js-entry-loader').use('rsbuild:lib-entry-module') */ { - loader: '/dist/entryModuleLoader.js' + loader: '/src/plugins/entryModuleLoader.ts' } ] } @@ -3739,7 +3739,7 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i ) ], moduleIds: 'named', - nodeEnv: false + nodeEnv: 'production' }, plugins: [ /* config.plugin('RsbuildCorePlugin') */ @@ -3816,7 +3816,7 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i }, cache: { type: 'persistent', - version: 'mf-development', + version: 'mf-production', storage: { type: 'filesystem', directory: '/node_modules/.cache/rspack' @@ -4323,8 +4323,8 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i } ) ], - moduleIds: 'named', - nodeEnv: 'development' + moduleIds: 'deterministic', + nodeEnv: 'production' }, plugins: [ /* config.plugin('mini-css-extract') */ diff --git a/packages/core/tests/cli.test.ts b/packages/core/tests/cli.test.ts index db4851830..afa848b3e 100644 --- a/packages/core/tests/cli.test.ts +++ b/packages/core/tests/cli.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from '@rstest/core'; import type { CommonOptions } from '../src/cli/commands'; -import { applyCliOptions, parseEntryOption } from '../src/cli/initConfig'; +import { applyCliOptions, parseEntryOption } from '../src/cli/init'; import type { RslibConfig } from '../src/types'; describe('parseEntryOption', () => { diff --git a/packages/core/tests/config.test.ts b/packages/core/tests/config.test.ts index 0fdfd0992..a176e73c6 100644 --- a/packages/core/tests/config.test.ts +++ b/packages/core/tests/config.test.ts @@ -1,13 +1,13 @@ import { join } from 'node:path'; import { pluginModuleFederation } from '@module-federation/rsbuild-plugin'; -import { inspect } from '@rslib/core'; import { describe, expect, rs, test } from '@rstest/core'; import type { BuildOptions } from '../src/cli/commands'; -import { initConfig } from '../src/cli/initConfig'; +import { init } from '../src/cli/init'; import { composeCreateRsbuildConfig, composeRsbuildEnvironments, } from '../src/config'; +import { createRslib } from '../src/createRslib'; import { loadConfig } from '../src/loadConfig'; import type { RslibConfig } from '../src/types/config'; @@ -172,7 +172,8 @@ describe('CLI options', () => { tsconfig: 'tsconfig.build.json', }; - const { config } = await initConfig(options); + const rslib = await init(options); + const { config } = rslib.context; expect(config).toMatchInlineSnapshot(` { "_privateMeta": { @@ -283,9 +284,11 @@ describe('Should compose create Rsbuild config correctly', () => { ); }); - const rsbuildInstance = await inspect(rslibConfig); - const { rsbuildConfig, bundlerConfigs } = - await rsbuildInstance.inspectConfig(); + const rslib = await createRslib({ + config: rslibConfig, + }); + const inspectConfigResult = await rslib.inspectConfig(); + const { rsbuildConfig, bundlerConfigs } = inspectConfigResult; expect(rsbuildConfig).toMatchSnapshot('inspected Rsbuild configs'); @@ -354,8 +357,11 @@ describe('Should compose create Rsbuild config correctly', () => { root: join(__dirname, '..'), }; - const rsbuildInstance = await inspect(rslibConfig); - const { bundlerConfigs } = await rsbuildInstance.inspectConfig(); + const rslib = await createRslib({ + config: rslibConfig, + }); + const inspectConfigResult = await rslib.inspectConfig(); + const { bundlerConfigs } = inspectConfigResult; expect(bundlerConfigs).toMatchSnapshot( 'experiment.advancedEsm Rspack configs', ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8b8bc752..1f1d2c81b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -709,6 +709,8 @@ importers: tests/integration/cli/mf/dev: {} + tests/integration/cli/mf/dev-error: {} + tests/integration/config-check/lib-array: {} tests/integration/copy: {} diff --git a/rslint.jsonc b/rslint.jsonc index 2e5ecc682..fce653f87 100644 --- a/rslint.jsonc +++ b/rslint.jsonc @@ -33,6 +33,7 @@ "@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/consistent-return": "off", "@typescript-eslint/consistent-generic-constructors": "off", + "@typescript-eslint/promise-function-async": "off", }, }, ] diff --git a/rstest.config.ts b/rstest.config.ts index 2644941c5..1392c6b8c 100644 --- a/rstest.config.ts +++ b/rstest.config.ts @@ -1,5 +1,25 @@ // import type { ProjectConfig } from '@rstest/core'; -import { defineConfig, type RstestConfig } from '@rstest/core'; +import { + defineConfig, + type RsbuildPlugin, + type RstestConfig, +} from '@rstest/core'; +import packageJson from './packages/core/package.json' with { type: 'json' }; + +const replaceLoaderUrlPlugin: RsbuildPlugin = { + name: 'replace-loader-url', + setup(api) { + api.transform( + { test: /EntryChunkPlugin.ts$/ }, + async ({ code }: { code: string }) => { + return code.replace( + /.\/entryModuleLoader.js/g, + './entryModuleLoader.ts', + ); + }, + ); + }, +}; export const shared: RstestConfig = { globals: true, @@ -10,6 +30,12 @@ export const shared: RstestConfig = { output: { module: true, }, + source: { + define: { + RSLIB_VERSION: JSON.stringify(packageJson.version), + }, + }, + plugins: [replaceLoaderUrlPlugin], }; export default defineConfig({ diff --git a/tests/integration/cli/mf/dev-error/package.json b/tests/integration/cli/mf/dev-error/package.json new file mode 100644 index 000000000..fa9538fe5 --- /dev/null +++ b/tests/integration/cli/mf/dev-error/package.json @@ -0,0 +1,6 @@ +{ + "name": "cli-mf-dev-error-test", + "version": "1.0.0", + "private": true, + "type": "module" +} diff --git a/tests/integration/cli/mf/dev-error/rslib.config.libNotExist.ts b/tests/integration/cli/mf/dev-error/rslib.config.libNotExist.ts new file mode 100644 index 000000000..1bf475d55 --- /dev/null +++ b/tests/integration/cli/mf/dev-error/rslib.config.libNotExist.ts @@ -0,0 +1,10 @@ +import { defineConfig } from '@rslib/core'; +import { generateBundleMFConfig } from 'test-helper'; + +export default defineConfig({ + lib: [ + generateBundleMFConfig({ + name: 'test-not-exist', + }), + ], +}); diff --git a/tests/integration/cli/mf/dev-error/rslib.config.noFormat.ts b/tests/integration/cli/mf/dev-error/rslib.config.noFormat.ts new file mode 100644 index 000000000..a1b1d8104 --- /dev/null +++ b/tests/integration/cli/mf/dev-error/rslib.config.noFormat.ts @@ -0,0 +1,6 @@ +import { defineConfig } from '@rslib/core'; +import { generateBundleEsmConfig } from 'test-helper'; + +export default defineConfig({ + lib: [generateBundleEsmConfig()], +}); diff --git a/tests/integration/cli/mf/mf.test.ts b/tests/integration/cli/mf/mf.test.ts index cd2e8d376..f3c596d62 100644 --- a/tests/integration/cli/mf/mf.test.ts +++ b/tests/integration/cli/mf/mf.test.ts @@ -1,6 +1,5 @@ import { join } from 'node:path'; -import { pluginModuleFederation } from '@module-federation/rsbuild-plugin'; -import { startMFDevServer } from '@rslib/core'; +import { stripVTControlCharacters as stripAnsi } from 'node:util'; import { describe, expect, test } from '@rstest/core'; import fse from 'fs-extra'; import { expectFile, runCli, runCliSync } from 'test-helper'; @@ -61,39 +60,32 @@ describe('mf-dev', () => { }); test('mf-dev --lib should error when lib not found', async () => { + const fixturePath = join(__dirname, 'dev-error'); try { - await startMFDevServer( + runCliSync( + 'mf-dev --config rslib.config.libNotExist.ts --lib not-exist', { - lib: [ - { - format: 'mf', - plugins: [pluginModuleFederation({ name: 'test-not-exist' }, {})], - }, - ], - }, - { - lib: ['not-exist'], + cwd: fixturePath, + stdio: 'pipe', }, ); } catch (error) { - expect((error as Error).message).toMatchInlineSnapshot( - `"No mf format found in libs "not-exist", please check your config to ensure that the mf format is enabled correctly."`, + expect(stripAnsi((error as Error).message)).toContain( + `No mf format found in libs "not-exist", please check your config to ensure that the mf format is enabled correctly.`, ); } }); test('mf-dev should error when no mf format', async () => { + const fixturePath = join(__dirname, 'dev-error'); try { - await startMFDevServer({ - lib: [ - { - format: 'esm', - }, - ], + runCliSync('mf-dev --config rslib.config.noFormat.ts', { + cwd: fixturePath, + stdio: 'pipe', }); } catch (error) { - expect((error as Error).message).toMatchInlineSnapshot( - `"No mf format found in your config, please check your config to ensure that the mf format is enabled correctly."`, + expect(stripAnsi((error as Error).message)).toContain( + 'No mf format found in your config, please check your config to ensure that the mf format is enabled correctly.', ); } }); diff --git a/tests/scripts/shared.ts b/tests/scripts/shared.ts index af226ce38..31624e64f 100644 --- a/tests/scripts/shared.ts +++ b/tests/scripts/shared.ts @@ -9,6 +9,8 @@ import { import fs from 'node:fs'; import { basename, dirname, join, normalize } from 'node:path'; import { fileURLToPath } from 'node:url'; +import util from 'node:util'; +import vm from 'node:vm'; import { pluginModuleFederation } from '@module-federation/rsbuild-plugin'; import { type InspectConfigResult, @@ -16,7 +18,7 @@ import { mergeRsbuildConfig as mergeConfig, } from '@rsbuild/core'; import type { Format, LibConfig, RslibConfig } from '@rslib/core'; -import { build, loadConfig } from '@rslib/core'; +import { createRslib, loadConfig } from '@rslib/core'; import { globContentJSON } from './helper.ts'; const __filename = fileURLToPath(import.meta.url); @@ -67,32 +69,66 @@ export function getCwdByExample(exampleName: string) { } export function extractRslibConfig(log: string): string { - const markerPattern = /Rslib config used to generate Rsbuild environments/; - const lines = log.split(/\r?\n/); - const markerIndex = lines.findIndex((line) => markerPattern.test(line)); + const marker = /Rslib config used to generate Rsbuild environments/; + const match = marker.exec(log); + if (!match || typeof match.index !== 'number') return ''; - if (markerIndex === -1) { - return ''; - } + const afterMarker = log.slice(match.index + match[0].length); + const firstBrace = afterMarker.indexOf('{'); + if (firstBrace === -1) return ''; - const startIndex = markerIndex + 2; + const text = afterMarker.slice(firstBrace); - if (startIndex >= lines.length || lines[startIndex] !== '{') { - return ''; - } + let depth = 0; + let inString: "'" | '"' | null = null; + let escapeNext = false; - let endIndex = startIndex + 1; + for (let i = 0; i < text.length; i += 1) { + const ch = text[i]; - while (endIndex < lines.length && lines[endIndex] !== '}') { - endIndex += 1; - } + if (escapeNext) { + escapeNext = false; + continue; + } + + if (inString) { + if (ch === '\\') escapeNext = true; + else if (ch === inString) inString = null; + continue; + } + + if (ch === "'" || ch === '"') { + inString = ch; + continue; + } - if (endIndex >= lines.length) { - return ''; + if (ch === '{') { + depth += 1; + continue; + } + + if (ch !== '}') continue; + depth -= 1; + if (depth !== 0) continue; + + const raw = text.slice(0, i + 1).trim(); + const collapsed = raw.replace(/\r?\n\s*/g, '').trim(); + const sanitized = util.stripVTControlCharacters(collapsed); + + try { + const parsed = vm.runInNewContext(`(${sanitized})`, Object.create(null), { + timeout: 50, + }); + return util.inspect(parsed, { + depth: null, + colors: false, + }); + } catch { + return raw; + } } - const configText = lines.slice(startIndex, endIndex + 1).join('\n'); - return configText; + return ''; } export function generateBundleEsmConfig(config: LibConfig = {}): LibConfig { @@ -309,8 +345,14 @@ export async function rslibBuild({ }); modifyConfig?.(rslibConfig); process.chdir(cwd); - const rsbuildInstance = await build(rslibConfig, { lib }); - return { rsbuildInstance, rslibConfig }; + const rslib = await createRslib({ + cwd, + config: rslibConfig, + }); + const buildResult = await rslib.build({ + lib, + }); + return { rslib, buildResult, rslibConfig }; } export async function buildAndGetResults(options: { @@ -344,7 +386,7 @@ export async function buildAndGetResults({ lib?: string[]; logLevel?: LogLevel; }) { - const { rsbuildInstance, rslibConfig } = await rslibBuild({ + const { rslib, buildResult, rslibConfig } = await rslibBuild({ cwd: fixturePath, path: configPath, modifyConfig: updateConfigForTest(logLevel), @@ -352,7 +394,7 @@ export async function buildAndGetResults({ }); const { origin: { bundlerConfigs, rsbuildConfig }, - } = await rsbuildInstance.inspectConfig({ verbose: true }); + } = await rslib.inspectConfig({ verbose: true }); if (type === 'all') { const jsResults = await getResults(rslibConfig, 'js'); const dtsResults = await getResults(rslibConfig, 'dts'); @@ -365,7 +407,7 @@ export async function buildAndGetResults({ entryFiles: jsResults.entryFiles, rspackConfig: bundlerConfigs, rsbuildConfig: rsbuildConfig, - isSuccess: Boolean(rsbuildInstance), + isSuccess: Boolean(buildResult), }, dts: { contents: dtsResults.contents, @@ -374,7 +416,7 @@ export async function buildAndGetResults({ entryFiles: dtsResults.entryFiles, rspackConfig: bundlerConfigs, rsbuildConfig: rsbuildConfig, - isSuccess: Boolean(rsbuildInstance), + isSuccess: Boolean(buildResult), }, css: { contents: cssResults.contents, @@ -383,7 +425,7 @@ export async function buildAndGetResults({ entryFiles: cssResults.entryFiles, rspackConfig: bundlerConfigs, rsbuildConfig: rsbuildConfig, - isSuccess: Boolean(rsbuildInstance), + isSuccess: Boolean(buildResult), }, }; } @@ -396,7 +438,7 @@ export async function buildAndGetResults({ mfExposeEntry: results.mfExposeEntry, rspackConfig: bundlerConfigs, rsbuildConfig: rsbuildConfig, - isSuccess: Boolean(rsbuildInstance), + isSuccess: Boolean(buildResult), }; } diff --git a/website/docs/en/guide/basic/cli.mdx b/website/docs/en/guide/basic/cli.mdx index 11933af78..2eb8be227 100644 --- a/website/docs/en/guide/basic/cli.mdx +++ b/website/docs/en/guide/basic/cli.mdx @@ -35,6 +35,7 @@ Rslib CLI provides several common flags that can be used with all commands: | `-h, --help` | Display help for command | | `--lib ` | Specify the library to run commands (repeatable, e.g. `--lib esm --lib cjs`), see [lib.id](/config/lib/id) to learn how to get or set the ID of the library | | `--log-level ` | Set the log level (`info` \| `warn` \| `error` \| `silent`), see [logLevel](/config/rsbuild/log-level) | +| `--no-env` | Disable loading of `.env` files | | `-r, --root ` | Specify the project root directory, can be an absolute path or a path relative to cwd | ## rslib build diff --git a/website/docs/zh/guide/basic/cli.mdx b/website/docs/zh/guide/basic/cli.mdx index 7e6438cc5..a56426e85 100644 --- a/website/docs/zh/guide/basic/cli.mdx +++ b/website/docs/zh/guide/basic/cli.mdx @@ -35,6 +35,7 @@ Rslib CLI 提供了一些公共选项,可以用于所有命令: | `-h, --help` | 显示命令帮助 | | `--lib ` | 指定运行命令的库(可重复,例如:`--lib esm --lib cjs`),查看 [lib.id](/config/lib/id) 了解如何获取或设置库的 ID | | `--log-level ` | 指定日志级别(`info` \| `warn` \| `error` \| `silent`),详见 [logLevel](/config/rsbuild/log-level) | +| `--no-env` | 禁用 `.env` 文件的加载 | | `-r, --root ` | 指定项目根目录,可以是绝对路径或者相对于 cwd 的路径 | ## rslib build