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..fbc299c16 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 { ConfigLoader } from '../loadConfig'; +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/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/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/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 62d4101f0..d1320e6d2 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,13 +1,21 @@ +/** + * 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 } 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: [],