From d6a4ece400d01180d4817d508e3234e244bfef86 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Sat, 7 Feb 2026 15:27:05 +0100 Subject: [PATCH 01/10] refactor(commands): add shared CompilerInterface, CompilerAsset types and --bundler option Add Bundler type alias, CompilerInterface and CompilerAsset to the shared types.ts. Remove duplicate CompilerAsset definitions from rspack/types.ts and webpack/types.ts. Add --bundler CLI option to both startCommandOptions and bundleCommandOptions. Co-Authored-By: Claude Opus 4.6 --- packages/repack/src/commands/options.ts | 10 +++++ packages/repack/src/commands/rspack/types.ts | 11 +----- packages/repack/src/commands/types.ts | 38 +++++++++++++++++++ packages/repack/src/commands/webpack/types.ts | 10 ++--- 4 files changed, 52 insertions(+), 17 deletions(-) diff --git a/packages/repack/src/commands/options.ts b/packages/repack/src/commands/options.ts index b8948725e..98f0153e2 100644 --- a/packages/repack/src/commands/options.ts +++ b/packages/repack/src/commands/options.ts @@ -76,6 +76,11 @@ export const startCommandOptions = [ '[DEPRECATED] Path to a bundler config file, e.g webpack.config.js. Please use --config instead.', parse: (val: string) => path.resolve(val), }, + { + name: '--bundler ', + description: + 'Bundler engine to use: "rspack" or "webpack". If not specified, auto-detected from config filename.', + }, ]; export const bundleCommandOptions = [ @@ -172,4 +177,9 @@ export const bundleCommandOptions = [ '[DEPRECATED] Path to a bundler config file, e.g webpack.config.js. Please use --config instead.', parse: (val: string) => path.resolve(val), }, + { + name: '--bundler ', + description: + 'Bundler engine to use: "rspack" or "webpack". If not specified, auto-detected from config filename.', + }, ]; diff --git a/packages/repack/src/commands/rspack/types.ts b/packages/repack/src/commands/rspack/types.ts index 7d8485e4d..9207803ac 100644 --- a/packages/repack/src/commands/rspack/types.ts +++ b/packages/repack/src/commands/rspack/types.ts @@ -1,12 +1,3 @@ -import type { MultiCompiler, StatsAsset } from '@rspack/core'; -import type { RemoveRecord } from '../types.js'; - -type RspackStatsAsset = RemoveRecord; - -export interface CompilerAsset { - data: Buffer; - info: RspackStatsAsset['info']; - size: number; -} +import type { MultiCompiler } from '@rspack/core'; export type MultiWatching = ReturnType; diff --git a/packages/repack/src/commands/types.ts b/packages/repack/src/commands/types.ts index 73c5cd683..3fa271b37 100644 --- a/packages/repack/src/commands/types.ts +++ b/packages/repack/src/commands/types.ts @@ -1,5 +1,8 @@ +import type { SendProgress, Server } from '@callstack/repack-dev-server'; import type { EnvOptions } from '../types.js'; +export type Bundler = 'rspack' | 'webpack'; + export interface BundleArguments { entryFile: string; platform: string; @@ -16,6 +19,7 @@ export interface BundleArguments { maxWorkers?: number; config?: string; webpackConfig?: string; + bundler?: Bundler; } export interface StartArguments { @@ -35,6 +39,7 @@ export interface StartArguments { maxWorkers?: number; config?: string; webpackConfig?: string; + bundler?: Bundler; } export interface CliConfig { @@ -76,3 +81,36 @@ export type ConfigurationObject = Partial>; export type Configuration = | T | ((env: EnvOptions, argv: Record) => T | Promise); + +export interface CompilerAsset { + data: Buffer; + info: { + hotModuleReplacement?: boolean; + related?: { sourceMap?: string | string[] }; + size?: number; + [key: string]: any; + }; + size: number; +} + +export interface CompilerInterface { + platforms: string[]; + assetsCache: Record | undefined>; + statsCache: Record | undefined>; + setDevServerContext(ctx: Server.DelegateContext): void; + start(): void; + getAsset( + filename: string, + platform: string, + sendProgress?: SendProgress + ): Promise; + getSource( + filename: string, + platform: string | undefined, + sendProgress?: SendProgress + ): Promise; + getSourceMap( + filename: string, + platform: string | undefined + ): Promise; +} diff --git a/packages/repack/src/commands/webpack/types.ts b/packages/repack/src/commands/webpack/types.ts index 2acb8221c..e96aebb4b 100644 --- a/packages/repack/src/commands/webpack/types.ts +++ b/packages/repack/src/commands/webpack/types.ts @@ -1,5 +1,7 @@ import type { StatsAsset, StatsCompilation } from 'webpack'; -import type { RemoveRecord, StartArguments } from '../types.js'; +import type { CompilerAsset, RemoveRecord, StartArguments } from '../types.js'; + +export type { CompilerAsset }; export interface WebpackWorkerOptions { platform: string; @@ -10,12 +12,6 @@ export interface WebpackWorkerOptions { type WebpackStatsAsset = RemoveRecord; -export interface CompilerAsset { - data: Buffer; - info: WebpackStatsAsset['info']; - size: number; -} - export interface WorkerAsset { data: Uint8Array; info: WebpackStatsAsset['info']; From 831f3686c379b34b889d866f77036c05394a653d Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Sat, 7 Feb 2026 15:35:19 +0100 Subject: [PATCH 02/10] feat(commands): add bundler auto-detection from config filename Add detectBundler() utility that determines the bundler engine with the following priority: 1. Explicit --bundler flag 2. Custom config path filename inference (rspack.* vs webpack.*) 3. Config file discovery (rspack configs checked first) 4. Default: rspack Co-Authored-By: Claude Opus 4.6 --- .../commands/common/config/detectBundler.ts | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 packages/repack/src/commands/common/config/detectBundler.ts diff --git a/packages/repack/src/commands/common/config/detectBundler.ts b/packages/repack/src/commands/common/config/detectBundler.ts new file mode 100644 index 000000000..976e13621 --- /dev/null +++ b/packages/repack/src/commands/common/config/detectBundler.ts @@ -0,0 +1,56 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { + DEFAULT_RSPACK_CONFIG_LOCATIONS, + DEFAULT_WEBPACK_CONFIG_LOCATIONS, +} from '../../consts.js'; +import type { Bundler } from '../../types.js'; + +/** + * Detects the bundler engine to use based on config file presence. + * + * Detection priority: + * 1. Explicit `--bundler` flag (passed as `explicitBundler`) + * 2. Custom config path filename inference (`rspack.*` vs `webpack.*`) + * 3. Config file discovery (rspack configs checked first) + * 4. Default: rspack + */ +export function detectBundler( + rootDir: string, + customConfigPath?: string, + explicitBundler?: Bundler +): Bundler { + if (explicitBundler) { + return explicitBundler; + } + + if (customConfigPath) { + const basename = path.basename(customConfigPath); + if (basename.startsWith('rspack')) { + return 'rspack'; + } + if (basename.startsWith('webpack')) { + return 'webpack'; + } + } + + for (const candidate of DEFAULT_RSPACK_CONFIG_LOCATIONS) { + const filename = path.isAbsolute(candidate) + ? candidate + : path.join(rootDir, candidate); + if (fs.existsSync(filename)) { + return 'rspack'; + } + } + + for (const candidate of DEFAULT_WEBPACK_CONFIG_LOCATIONS) { + const filename = path.isAbsolute(candidate) + ? candidate + : path.join(rootDir, candidate); + if (fs.existsSync(filename)) { + return 'webpack'; + } + } + + return 'rspack'; +} From 79b235e1b8a6900e8035da20b160b8ad3a602ff4 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Sat, 7 Feb 2026 15:38:22 +0100 Subject: [PATCH 03/10] refactor(commands): implement CompilerInterface on both Compiler classes Webpack Compiler: - Remove EventEmitter inheritance - Add platforms property and devServerContext with late-init - Add setDevServerContext() and start() (no-op) methods - Internalize event handling: worker messages now call devServerContext.notifyBuildStart/End and broadcastToHmrClients directly instead of emitting events - Constructor now takes platforms as first argument Rspack Compiler: - Add 'implements CompilerInterface' declaration - Import CompilerAsset from shared types Co-Authored-By: Claude Opus 4.6 --- .../repack/src/commands/rspack/Compiler.ts | 4 +- .../repack/src/commands/webpack/Compiler.ts | 73 +++++++++++++++---- 2 files changed, 59 insertions(+), 18 deletions(-) diff --git a/packages/repack/src/commands/rspack/Compiler.ts b/packages/repack/src/commands/rspack/Compiler.ts index 3dc6ce2cb..983fa7e96 100644 --- a/packages/repack/src/commands/rspack/Compiler.ts +++ b/packages/repack/src/commands/rspack/Compiler.ts @@ -13,9 +13,9 @@ import type { Reporter } from '../../logging/types.js'; import type { HMRMessage } from '../../types.js'; import { runAdbReverse } from '../common/index.js'; import { DEV_SERVER_ASSET_TYPES } from '../consts.js'; -import type { CompilerAsset } from './types.js'; +import type { CompilerAsset, CompilerInterface } from '../types.js'; -export class Compiler { +export class Compiler implements CompilerInterface { compiler: MultiCompiler; filesystem: memfs.IFs; platforms: string[]; diff --git a/packages/repack/src/commands/webpack/Compiler.ts b/packages/repack/src/commands/webpack/Compiler.ts index 990506e5f..41fa75b62 100644 --- a/packages/repack/src/commands/webpack/Compiler.ts +++ b/packages/repack/src/commands/webpack/Compiler.ts @@ -1,37 +1,50 @@ -import EventEmitter from 'node:events'; import fs from 'node:fs'; import path from 'node:path'; import { Worker } from 'node:worker_threads'; -import type { SendProgress } from '@callstack/repack-dev-server'; -import type webpack from 'webpack'; +import type { SendProgress, Server } from '@callstack/repack-dev-server'; +import type { StatsCompilation } from 'webpack'; import { WORKER_ENV_KEY } from '../../env.js'; import { CLIError } from '../../helpers/index.js'; import type { LogType, Reporter } from '../../logging/types.js'; +import type { HMRMessage } from '../../types.js'; +import { runAdbReverse } from '../common/index.js'; import { DEV_SERVER_ASSET_TYPES } from '../consts.js'; -import type { StartArguments } from '../types.js'; import type { CompilerAsset, - WebpackWorkerOptions, - WorkerMessages, -} from './types.js'; + CompilerInterface, + StartArguments, +} from '../types.js'; +import type { WebpackWorkerOptions, WorkerMessages } from './types.js'; type Platform = string; -export class Compiler extends EventEmitter { +export class Compiler implements CompilerInterface { workers: Record = {}; - assetsCache: Record> = {}; - statsCache: Record = {}; + platforms: string[]; + assetsCache: Record | undefined> = {}; + statsCache: Record = {}; resolvers: Record void>> = {}; progressSenders: Record = {}; isCompilationInProgress: Record = {}; + // late-init + devServerContext!: Server.DelegateContext; constructor( + platforms: string[], private args: StartArguments, private reporter: Reporter, private rootDir: string, private reactNativePath: string ) { - super(); + this.platforms = platforms; + } + + setDevServerContext(ctx: Server.DelegateContext) { + this.devServerContext = ctx; + } + + start() { + // no-op: webpack workers spawn lazily on first getAsset call } private spawnWorker(platform: string) { @@ -80,7 +93,7 @@ export class Compiler extends EventEmitter { }); const callPendingResolvers = (error?: Error) => { - this.resolvers[platform].forEach((resolver) => resolver(error)); + this.resolvers[platform]?.forEach((resolver) => resolver(error)); this.resolvers[platform] = []; }; @@ -99,7 +112,18 @@ export class Compiler extends EventEmitter { }) ), }; - this.emit(value.event, { platform, stats: value.stats }); + + // notify dev server of build completion + this.devServerContext.notifyBuildEnd(platform); + this.devServerContext.broadcastToHmrClients({ + action: 'hash', + body: { name: platform, hash: value.stats.hash }, + }); + this.devServerContext.broadcastToHmrClients({ + action: 'ok', + body: { name: platform }, + }); + // Emit final progress with timing for this platform this.reporter.process({ issuer: 'DevServer', @@ -109,9 +133,15 @@ export class Compiler extends EventEmitter { }); callPendingResolvers(); } else if (value.event === 'error') { - this.emit(value.event, value.error); + // errors are logged but not fatal to the compiler lifecycle + this.reporter.process({ + type: 'error', + issuer: 'WebpackCompilerWorker', + timestamp: Date.now(), + message: [String(value.error)], + }); } else if (value.event === 'progress') { - this.progressSenders[platform].forEach((sendProgress) => { + this.progressSenders[platform]?.forEach((sendProgress) => { const percentage = Math.floor(value.percentage * 100); sendProgress({ completed: percentage, total: 100 }); }); @@ -126,8 +156,19 @@ export class Compiler extends EventEmitter { }); } } else { + // watchRun / invalid this.isCompilationInProgress[platform] = true; - this.emit(value.event, { platform }); + this.devServerContext.notifyBuildStart(platform); + this.devServerContext.broadcastToHmrClients({ + action: 'compiling', + body: { name: platform }, + }); + if (value.event === 'watchRun' && platform === 'android') { + void runAdbReverse({ + port: this.devServerContext.options.port, + logger: this.devServerContext.log, + }); + } } }); From 0c8939fed26b647ecc6f2eebf6685973d7839329 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Sat, 7 Feb 2026 15:40:29 +0100 Subject: [PATCH 04/10] refactor(commands): create unified command entry point with auto-detection Add unified bundle.ts and start.ts that merge the rspack and webpack implementations, using detectBundler() and dynamic imports to select the correct engine at runtime. Add commands/index.ts exporting the unified command definitions and a createBoundCommands() helper for deprecated entry points. Add commands/index.js and commands/index.d.ts as the new primary CJS entry point. Update commands/rspack.js and commands/webpack.js to be deprecation wrappers that delegate via createBoundCommands(). Add './commands' export to package.json (alongside existing wildcard). Delete old bundler-specific files: - rspack/bundle.ts, rspack/start.ts, rspack/index.ts - webpack/bundle.ts, webpack/start.ts, webpack/index.ts Co-Authored-By: Claude Opus 4.6 --- packages/repack/commands/index.d.ts | 2 + packages/repack/commands/index.js | 2 + packages/repack/commands/rspack.d.ts | 2 +- packages/repack/commands/rspack.js | 7 +- packages/repack/commands/webpack.d.ts | 2 +- packages/repack/commands/webpack.js | 7 +- packages/repack/package.json | 1 + packages/repack/src/commands/bundle.ts | 135 ++++++++++ .../src/commands/{webpack => }/index.ts | 16 +- packages/repack/src/commands/rspack/bundle.ts | 106 -------- packages/repack/src/commands/rspack/index.ts | 32 --- .../repack/src/commands/{rspack => }/start.ts | 95 +++++-- .../repack/src/commands/webpack/bundle.ts | 98 ------- packages/repack/src/commands/webpack/start.ts | 250 ------------------ 14 files changed, 235 insertions(+), 520 deletions(-) create mode 100644 packages/repack/commands/index.d.ts create mode 100644 packages/repack/commands/index.js create mode 100644 packages/repack/src/commands/bundle.ts rename packages/repack/src/commands/{webpack => }/index.ts (59%) delete mode 100644 packages/repack/src/commands/rspack/bundle.ts delete mode 100644 packages/repack/src/commands/rspack/index.ts rename packages/repack/src/commands/{rspack => }/start.ts (69%) delete mode 100644 packages/repack/src/commands/webpack/bundle.ts delete mode 100644 packages/repack/src/commands/webpack/start.ts diff --git a/packages/repack/commands/index.d.ts b/packages/repack/commands/index.d.ts new file mode 100644 index 000000000..5802c7661 --- /dev/null +++ b/packages/repack/commands/index.d.ts @@ -0,0 +1,2 @@ +import commands from '../dist/commands/index.js'; +export = commands; diff --git a/packages/repack/commands/index.js b/packages/repack/commands/index.js new file mode 100644 index 000000000..f24114ce3 --- /dev/null +++ b/packages/repack/commands/index.js @@ -0,0 +1,2 @@ +const commands = require('../dist/commands/index.js'); +module.exports = commands.default; diff --git a/packages/repack/commands/rspack.d.ts b/packages/repack/commands/rspack.d.ts index 05ed7a90b..5802c7661 100644 --- a/packages/repack/commands/rspack.d.ts +++ b/packages/repack/commands/rspack.d.ts @@ -1,2 +1,2 @@ -import commands from '../dist/commands/rspack/index.js'; +import commands from '../dist/commands/index.js'; export = commands; diff --git a/packages/repack/commands/rspack.js b/packages/repack/commands/rspack.js index 92918564c..e504cce68 100644 --- a/packages/repack/commands/rspack.js +++ b/packages/repack/commands/rspack.js @@ -1,2 +1,5 @@ -const commands = require('../dist/commands/rspack/index.js'); -module.exports = commands.default; +console.warn( + '[Re.Pack] Importing "@callstack/repack/commands/rspack" is deprecated. Use "@callstack/repack/commands" instead.' +); +const { createBoundCommands } = require('../dist/commands/index.js'); +module.exports = createBoundCommands('rspack'); diff --git a/packages/repack/commands/webpack.d.ts b/packages/repack/commands/webpack.d.ts index 9fc136815..5802c7661 100644 --- a/packages/repack/commands/webpack.d.ts +++ b/packages/repack/commands/webpack.d.ts @@ -1,2 +1,2 @@ -import commands from '../dist/commands/webpack/index.js'; +import commands from '../dist/commands/index.js'; export = commands; diff --git a/packages/repack/commands/webpack.js b/packages/repack/commands/webpack.js index 067b227bc..0ed55418f 100644 --- a/packages/repack/commands/webpack.js +++ b/packages/repack/commands/webpack.js @@ -1,2 +1,5 @@ -const commands = require('../dist/commands/webpack/index.js'); -module.exports = commands.default; +console.warn( + '[Re.Pack] Importing "@callstack/repack/commands/webpack" is deprecated. Use "@callstack/repack/commands" instead.' +); +const { createBoundCommands } = require('../dist/commands/index.js'); +module.exports = createBoundCommands('webpack'); diff --git a/packages/repack/package.json b/packages/repack/package.json index a5a56c266..3b02ed0d0 100644 --- a/packages/repack/package.json +++ b/packages/repack/package.json @@ -8,6 +8,7 @@ "exports": { ".": "./dist/index.js", "./client": "./client/index.js", + "./commands": "./commands/index.js", "./commands/*": "./commands/*.js", "./assets-loader": "./dist/loaders/assetsLoader/index.js", "./babel-loader": "./dist/loaders/babelLoader/index.js", diff --git a/packages/repack/src/commands/bundle.ts b/packages/repack/src/commands/bundle.ts new file mode 100644 index 000000000..d9011a899 --- /dev/null +++ b/packages/repack/src/commands/bundle.ts @@ -0,0 +1,135 @@ +import { CLIError } from '../helpers/index.js'; +import { detectBundler } from './common/config/detectBundler.js'; +import { makeCompilerConfig } from './common/config/makeCompilerConfig.js'; +import { + getMaxWorkers, + normalizeStatsOptions, + resetPersistentCache, + setupEnvironment, + setupRspackEnvironment, + writeStats, +} from './common/index.js'; +import type { BundleArguments, Bundler, CliConfig } from './types.js'; + +/** + * Unified bundle command that builds and saves the bundle + * alongside any other assets to filesystem. + * + * Auto-detects the bundler engine (rspack or webpack) unless explicitly specified. + * + * @param _ Original, non-parsed arguments that were provided when running this command. + * @param cliConfig Configuration object containing platform and project settings. + * @param args Parsed command line arguments. + * @param forcedBundler Optional bundler override from deprecated entry points. + */ +export async function bundle( + _: string[], + cliConfig: CliConfig, + args: BundleArguments, + forcedBundler?: Bundler +) { + const bundler = + forcedBundler ?? + detectBundler( + cliConfig.root, + args.config ?? args.webpackConfig, + args.bundler + ); + + const [config] = await makeCompilerConfig>({ + args: args, + bundler, + command: 'bundle', + rootDir: cliConfig.root, + platforms: [args.platform], + reactNativePath: cliConfig.reactNativePath, + }); + + // remove devServer configuration to avoid schema validation errors + delete config.devServer; + + // expose selected args as environment variables + setupEnvironment(args); + + if (bundler === 'rspack') { + const maxWorkers = args.maxWorkers ?? getMaxWorkers(); + setupRspackEnvironment(maxWorkers.toString()); + } + + if (!args.entryFile && !config.entry) { + throw new CLIError("Option '--entry-file ' argument is missing"); + } + + if (args.resetCache) { + if (bundler === 'rspack') { + resetPersistentCache({ + bundler: 'rspack', + rootDir: cliConfig.root, + cacheConfigs: [config.experiments?.cache], + }); + } else { + resetPersistentCache({ + bundler: 'webpack', + rootDir: cliConfig.root, + cacheConfigs: [config.cache], + }); + } + } + + // Dynamic import of bundler engine — both are optional peer dependencies + let compiler: any; + if (bundler === 'rspack') { + const { rspack } = await import('@rspack/core'); + compiler = rspack(config); + } else { + const webpack = (await import('webpack')).default; + compiler = webpack(config); + } + + return new Promise((resolve) => { + const errorHandler = async (error: Error | null, stats?: any) => { + if (error) { + throw new CLIError(error.message); + } + + if (stats?.hasErrors()) { + stats.compilation?.errors?.forEach((e: any) => { + console.error(e); + }); + process.exit(2); + } + + if (args.json && stats !== undefined) { + const statsOptions = normalizeStatsOptions( + compiler.options.stats, + args.stats + ); + + const statsJson = stats.toJson(statsOptions); + + try { + await writeStats(statsJson, { + filepath: args.json, + rootDir: compiler.context, + }); + } catch (e) { + throw new CLIError(String(e)); + } + } + }; + + if (args.watch) { + compiler.hooks.watchClose.tap('bundle', resolve); + compiler.watch(config.watchOptions ?? {}, errorHandler); + } else { + compiler.run((error: Error | null, stats: any) => { + // make cache work: https://webpack.js.org/api/node/#run + compiler.close(async (closeErr: Error | null) => { + if (closeErr) console.error(closeErr); + await errorHandler(error, stats); + resolve(); + }); + }); + } + }); +} diff --git a/packages/repack/src/commands/webpack/index.ts b/packages/repack/src/commands/index.ts similarity index 59% rename from packages/repack/src/commands/webpack/index.ts rename to packages/repack/src/commands/index.ts index 7db45a291..bffe6d294 100644 --- a/packages/repack/src/commands/webpack/index.ts +++ b/packages/repack/src/commands/index.ts @@ -1,6 +1,7 @@ -import { bundleCommandOptions, startCommandOptions } from '../options.js'; import { bundle } from './bundle.js'; +import { bundleCommandOptions, startCommandOptions } from './options.js'; import { start } from './start.js'; +import type { Bundler } from './types.js'; const commands = [ { @@ -30,3 +31,16 @@ const commands = [ ] as const; export default commands; + +/** + * Creates command definitions with a forced bundler engine. + * Used by deprecated entry points (`commands/rspack`, `commands/webpack`) + * to maintain backwards compatibility. + */ +export function createBoundCommands(bundler: Bundler) { + return commands.map((cmd) => ({ + ...cmd, + func: (_: string[], cliConfig: any, args: any) => + cmd.func(_, cliConfig, args, bundler), + })); +} diff --git a/packages/repack/src/commands/rspack/bundle.ts b/packages/repack/src/commands/rspack/bundle.ts deleted file mode 100644 index 6c0975144..000000000 --- a/packages/repack/src/commands/rspack/bundle.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { type Configuration, rspack } from '@rspack/core'; -import type { Stats } from '@rspack/core'; -import { CLIError } from '../../helpers/index.js'; -import { makeCompilerConfig } from '../common/config/makeCompilerConfig.js'; -import { - getMaxWorkers, - normalizeStatsOptions, - resetPersistentCache, - setupEnvironment, - setupRspackEnvironment, - writeStats, -} from '../common/index.js'; -import type { BundleArguments, CliConfig } from '../types.js'; - -/** - * Bundle command that builds and saves the bundle - * alongside any other assets to filesystem using Webpack. - * - * @param _ Original, non-parsed arguments that were provided when running this command. - * @param cliConfig Configuration object containing platform and project settings. - * @param args Parsed command line arguments. - */ -export async function bundle( - _: string[], - cliConfig: CliConfig, - args: BundleArguments -) { - const [config] = await makeCompilerConfig({ - args: args, - bundler: 'rspack', - command: 'bundle', - rootDir: cliConfig.root, - platforms: [args.platform], - reactNativePath: cliConfig.reactNativePath, - }); - - // remove devServer configuration to avoid schema validation errors - delete config.devServer; - - // expose selected args as environment variables - setupEnvironment(args); - - const maxWorkers = args.maxWorkers ?? getMaxWorkers(); - setupRspackEnvironment(maxWorkers.toString()); - - if (!args.entryFile && !config.entry) { - throw new CLIError("Option '--entry-file ' argument is missing"); - } - - if (args.resetCache) { - resetPersistentCache({ - bundler: 'rspack', - rootDir: cliConfig.root, - cacheConfigs: [config.experiments?.cache], - }); - } - - const errorHandler = async (error: Error | null, stats?: Stats) => { - if (error) { - throw new CLIError(error.message); - } - - if (stats?.hasErrors()) { - stats.compilation?.errors?.forEach((e) => { - console.error(e); - }); - process.exit(2); - } - - if (args.json && stats !== undefined) { - const statsOptions = normalizeStatsOptions( - compiler.options.stats, - args.stats - ); - - const statsJson = stats.toJson(statsOptions); - - try { - await writeStats(statsJson, { - filepath: args.json, - rootDir: compiler.context, - }); - } catch (e) { - throw new CLIError(String(e)); - } - } - }; - - const compiler = rspack(config); - - return new Promise((resolve) => { - if (args.watch) { - compiler.hooks.watchClose.tap('bundle', resolve); - compiler.watch(config.watchOptions ?? {}, errorHandler); - } else { - compiler.run((error, stats) => { - // make cache work: https://webpack.js.org/api/node/#run - compiler.close(async (closeErr) => { - if (closeErr) console.error(closeErr); - await errorHandler(error, stats); - resolve(); - }); - }); - } - }); -} diff --git a/packages/repack/src/commands/rspack/index.ts b/packages/repack/src/commands/rspack/index.ts deleted file mode 100644 index 7db45a291..000000000 --- a/packages/repack/src/commands/rspack/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { bundleCommandOptions, startCommandOptions } from '../options.js'; -import { bundle } from './bundle.js'; -import { start } from './start.js'; - -const commands = [ - { - name: 'bundle', - description: 'Build the bundle for the provided JavaScript entry file.', - options: bundleCommandOptions, - func: bundle, - }, - { - name: 'webpack-bundle', - description: 'Build the bundle for the provided JavaScript entry file.', - options: bundleCommandOptions, - func: bundle, - }, - { - name: 'start', - description: 'Start the React Native development server.', - options: startCommandOptions, - func: start, - }, - { - name: 'webpack-start', - description: 'Start the React Native development server.', - options: startCommandOptions, - func: start, - }, -] as const; - -export default commands; diff --git a/packages/repack/src/commands/rspack/start.ts b/packages/repack/src/commands/start.ts similarity index 69% rename from packages/repack/src/commands/rspack/start.ts rename to packages/repack/src/commands/start.ts index dc915ab4f..f746c5f3c 100644 --- a/packages/repack/src/commands/rspack/start.ts +++ b/packages/repack/src/commands/start.ts @@ -1,15 +1,15 @@ -import type { Configuration } from '@rspack/core'; -import packageJson from '../../../package.json'; -import { VERBOSE_ENV_KEY } from '../../env.js'; -import { CLIError, isTruthyEnv } from '../../helpers/index.js'; +import packageJson from '../../package.json'; +import { VERBOSE_ENV_KEY } from '../env.js'; +import { CLIError, isTruthyEnv } from '../helpers/index.js'; import { ConsoleReporter, FileReporter, type Reporter, composeReporters, makeLogEntryFromFastifyLog, -} from '../../logging/index.js'; -import { makeCompilerConfig } from '../common/config/makeCompilerConfig.js'; +} from '../logging/index.js'; +import { detectBundler } from './common/config/detectBundler.js'; +import { makeCompilerConfig } from './common/config/makeCompilerConfig.js'; import { getDevMiddleware, getMaxWorkers, @@ -21,25 +21,41 @@ import { setupEnvironment, setupInteractions, setupRspackEnvironment, -} from '../common/index.js'; -import logo from '../common/logo.js'; -import type { CliConfig, StartArguments } from '../types.js'; -import { Compiler } from './Compiler.js'; +} from './common/index.js'; +import logo from './common/logo.js'; +import type { + Bundler, + CliConfig, + CompilerInterface, + StartArguments, +} from './types.js'; /** - * Start command that runs a development server. + * Unified start command that runs a development server. * It runs `@callstack/repack-dev-server` to provide Development Server functionality * in development mode. * + * Auto-detects the bundler engine (rspack or webpack) unless explicitly specified. + * * @param _ Original, non-parsed arguments that were provided when running this command. * @param cliConfig Configuration object containing platform and project settings. * @param args Parsed command line arguments. + * @param forcedBundler Optional bundler override from deprecated entry points. */ export async function start( _: string[], cliConfig: CliConfig, - args: StartArguments + args: StartArguments, + forcedBundler?: Bundler ) { + const bundler = + forcedBundler ?? + detectBundler( + cliConfig.root, + args.config ?? args.webpackConfig, + args.bundler + ); + const detectedPlatforms = Object.keys(cliConfig.platforms); if (args.platform && !detectedPlatforms.includes(args.platform)) { @@ -48,9 +64,9 @@ export async function start( const platforms = args.platform ? [args.platform] : detectedPlatforms; - const configs = await makeCompilerConfig({ + const configs = await makeCompilerConfig>({ args: args, - bundler: 'rspack', + bundler, command: 'start', rootDir: cliConfig.root, platforms: platforms, @@ -60,8 +76,10 @@ export async function start( // expose selected args as environment variables setupEnvironment(args); - const maxWorkers = args.maxWorkers ?? getMaxWorkers(); - setupRspackEnvironment(maxWorkers.toString()); + if (bundler === 'rspack') { + const maxWorkers = args.maxWorkers ?? getMaxWorkers(); + setupRspackEnvironment(maxWorkers.toString()); + } const isVerbose = isTruthyEnv(process.env[VERBOSE_ENV_KEY]); const devServerOptions = configs[0].devServer ?? {}; @@ -77,18 +95,27 @@ export async function start( ].filter(Boolean) as Reporter[] ); - process.stdout.write(logo(packageJson.version, 'Rspack')); + const bundlerLabel = bundler === 'rspack' ? 'Rspack' : 'webpack'; + process.stdout.write(logo(packageJson.version, bundlerLabel)); if (args.resetCache) { - resetPersistentCache({ - bundler: 'rspack', - rootDir: cliConfig.root, - cacheConfigs: configs.map((config) => config.experiments?.cache), - }); + if (bundler === 'rspack') { + resetPersistentCache({ + bundler: 'rspack', + rootDir: cliConfig.root, + cacheConfigs: configs.map((config: any) => config.experiments?.cache), + }); + } else { + resetPersistentCache({ + bundler: 'webpack', + rootDir: cliConfig.root, + cacheConfigs: configs.map((config: any) => config.cache), + }); + } } - if (process.env.RSPACK_PROFILE) { - const { applyProfile } = await import('./profile/index.js'); + if (bundler === 'rspack' && process.env.RSPACK_PROFILE) { + const { applyProfile } = await import('./rspack/profile/index.js'); await applyProfile( process.env.RSPACK_PROFILE, process.env.RSPACK_TRACE_LAYER, @@ -96,10 +123,24 @@ export async function start( ); } - const compiler = new Compiler(configs, reporter, cliConfig.root); + // Create compiler via dynamic import — both engines are optional peer dependencies + let compiler: CompilerInterface; + if (bundler === 'rspack') { + const { Compiler } = await import('./rspack/Compiler.js'); + compiler = new Compiler(configs, reporter, cliConfig.root); + } else { + const { Compiler } = await import('./webpack/Compiler.js'); + compiler = new Compiler( + platforms, + args, + reporter, + cliConfig.root, + cliConfig.reactNativePath + ); + } const { createServer } = await import('@callstack/repack-dev-server'); - const { start, stop } = await createServer({ + const { start: serverStart, stop } = await createServer({ options: { ...devServerOptions, rootDir: cliConfig.root, @@ -205,7 +246,7 @@ export async function start( }, }); - await start(); + await serverStart(); compiler.start(); return { diff --git a/packages/repack/src/commands/webpack/bundle.ts b/packages/repack/src/commands/webpack/bundle.ts deleted file mode 100644 index 78fe82706..000000000 --- a/packages/repack/src/commands/webpack/bundle.ts +++ /dev/null @@ -1,98 +0,0 @@ -import webpack, { type Configuration } from 'webpack'; -import { CLIError } from '../../helpers/index.js'; -import { makeCompilerConfig } from '../common/config/makeCompilerConfig.js'; -import { - normalizeStatsOptions, - resetPersistentCache, - writeStats, -} from '../common/index.js'; -import { setupEnvironment } from '../common/setupEnvironment.js'; -import type { BundleArguments, CliConfig } from '../types.js'; -/** - * Bundle command that builds and saves the bundle - * alongside any other assets to filesystem using Webpack. - * - * @param _ Original, non-parsed arguments that were provided when running this command. - * @param cliConfig Configuration object containing platform and project settings. - * @param args Parsed command line arguments. - */ -export async function bundle( - _: string[], - cliConfig: CliConfig, - args: BundleArguments -) { - const [config] = await makeCompilerConfig({ - args: args, - bundler: 'webpack', - command: 'bundle', - rootDir: cliConfig.root, - platforms: [args.platform], - reactNativePath: cliConfig.reactNativePath, - }); - - // remove devServer configuration to avoid schema validation errors - delete config.devServer; - - // expose selected args as environment variables - setupEnvironment(args); - - if (!args.entryFile && !config.entry) { - throw new CLIError("Option '--entry-file ' argument is missing"); - } - - if (args.resetCache) { - resetPersistentCache({ - bundler: 'webpack', - rootDir: cliConfig.root, - cacheConfigs: [config.cache], - }); - } - - const errorHandler = async (error: Error | null, stats?: webpack.Stats) => { - if (error) { - throw new CLIError(error.message); - } - - if (stats?.hasErrors()) { - stats.compilation?.errors?.forEach((e) => { - console.error(e); - }); - process.exit(2); - } - - if (args.json && stats !== undefined) { - const statsOptions = normalizeStatsOptions( - compiler.options.stats, - args.stats - ); - - const statsJson = stats.toJson(statsOptions); - try { - await writeStats(statsJson, { - filepath: args.json, - rootDir: compiler.context, - }); - } catch (e) { - throw new CLIError(String(e)); - } - } - }; - - const compiler = webpack(config); - - return new Promise((resolve) => { - if (args.watch) { - compiler.hooks.watchClose.tap('bundle', resolve); - compiler.watch(config.watchOptions ?? {}, errorHandler); - } else { - compiler.run((error, stats) => { - // make cache work: https://webpack.js.org/api/node/#run - compiler.close(async (closeErr) => { - if (closeErr) console.error(closeErr); - await errorHandler(error, stats); - resolve(); - }); - }); - } - }); -} diff --git a/packages/repack/src/commands/webpack/start.ts b/packages/repack/src/commands/webpack/start.ts deleted file mode 100644 index 9cb236def..000000000 --- a/packages/repack/src/commands/webpack/start.ts +++ /dev/null @@ -1,250 +0,0 @@ -import type { Server } from '@callstack/repack-dev-server'; -import type { Configuration, StatsCompilation } from 'webpack'; -import packageJson from '../../../package.json'; -import { VERBOSE_ENV_KEY } from '../../env.js'; -import { CLIError, isTruthyEnv } from '../../helpers/index.js'; -import { - ConsoleReporter, - FileReporter, - type Reporter, - composeReporters, - makeLogEntryFromFastifyLog, -} from '../../logging/index.js'; -import type { HMRMessage } from '../../types.js'; -import { makeCompilerConfig } from '../common/config/makeCompilerConfig.js'; -import { - getDevMiddleware, - getMimeType, - parseUrl, - resetPersistentCache, - resolveProjectPath, - runAdbReverse, - setupInteractions, -} from '../common/index.js'; -import logo from '../common/logo.js'; -import { setupEnvironment } from '../common/setupEnvironment.js'; -import type { CliConfig, StartArguments } from '../types.js'; -import { Compiler } from './Compiler.js'; - -/** - * Start command that runs a development server. - * It runs `@callstack/repack-dev-server` to provide Development Server functionality - * in development mode. - * - * @param _ Original, non-parsed arguments that were provided when running this command. - * @param cliConfig Configuration object containing platform and project settings. - * @param args Parsed command line arguments. - */ -export async function start( - _: string[], - cliConfig: CliConfig, - args: StartArguments -) { - const detectedPlatforms = Object.keys(cliConfig.platforms); - - if (args.platform && !detectedPlatforms.includes(args.platform)) { - throw new CLIError(`Unrecognized platform: ${args.platform}`); - } - - const platforms = args.platform ? [args.platform] : detectedPlatforms; - - const configs = await makeCompilerConfig({ - args: args, - bundler: 'webpack', - command: 'start', - rootDir: cliConfig.root, - platforms: platforms, - reactNativePath: cliConfig.reactNativePath, - }); - - // expose selected args as environment variables - setupEnvironment(args); - - const isVerbose = isTruthyEnv(process.env[VERBOSE_ENV_KEY]); - const devServerOptions = configs[0].devServer ?? {}; - const showHttpRequests = isVerbose || args.logRequests; - - // dynamically import dev middleware to match version of react-native - const devMiddleware = await getDevMiddleware(cliConfig.reactNativePath); - - const reporter = composeReporters( - [ - new ConsoleReporter({ asJson: args.json, isVerbose: isVerbose }), - args.logFile ? new FileReporter({ filename: args.logFile }) : undefined, - ].filter(Boolean) as Reporter[] - ); - - process.stdout.write(logo(packageJson.version, 'webpack')); - - if (args.resetCache) { - resetPersistentCache({ - bundler: 'webpack', - rootDir: cliConfig.root, - cacheConfigs: configs.map((config) => config.cache), - }); - } - - const compiler = new Compiler( - args, - reporter, - cliConfig.root, - cliConfig.reactNativePath - ); - - const { createServer } = await import('@callstack/repack-dev-server'); - const { start, stop } = await createServer({ - options: { - ...devServerOptions, - rootDir: cliConfig.root, - logRequests: showHttpRequests, - devMiddleware, - }, - delegate: (ctx): Server.Delegate => { - if (args.interactive) { - setupInteractions( - { - onReload() { - ctx.broadcastToMessageClients({ method: 'reload' }); - }, - onOpenDevMenu() { - ctx.broadcastToMessageClients({ method: 'devMenu' }); - }, - onOpenDevTools() { - fetch(`${ctx.options.url}/open-debugger`, { - method: 'POST', - }).catch(() => { - ctx.log.warn('Failed to open React Native DevTools'); - }); - }, - onAdbReverse() { - void runAdbReverse({ - port: ctx.options.port, - logger: ctx.log, - verbose: true, - }); - }, - }, - { logger: ctx.log } - ); - } - - if (args.reversePort) { - void runAdbReverse({ - logger: ctx.log, - port: ctx.options.port, - wait: true, - }); - } - - compiler.on('watchRun', ({ platform }) => { - ctx.notifyBuildStart(platform); - ctx.broadcastToHmrClients({ - action: 'compiling', - body: { name: platform }, - }); - if (platform === 'android') { - void runAdbReverse({ - port: ctx.options.port, - logger: ctx.log, - }); - } - }); - - compiler.on('invalid', ({ platform }) => { - ctx.notifyBuildStart(platform); - ctx.broadcastToHmrClients({ - action: 'compiling', - body: { name: platform }, - }); - }); - - compiler.on( - 'done', - ({ - platform, - stats, - }: { - platform: string; - stats: StatsCompilation; - }) => { - ctx.notifyBuildEnd(platform); - ctx.broadcastToHmrClients({ - action: 'hash', - body: { name: platform, hash: stats.hash }, - }); - ctx.broadcastToHmrClients({ - action: 'ok', - body: { name: platform }, - }); - } - ); - - return { - compiler: { - getAsset: (url, platform, sendProgress) => { - const { resourcePath } = parseUrl(url, platforms); - return compiler.getSource(resourcePath, platform, sendProgress); - }, - getMimeType: (filename) => { - return getMimeType(filename); - }, - inferPlatform: (url) => { - const { platform } = parseUrl(url, platforms); - return platform; - }, - }, - devTools: { - resolveProjectPath: (filepath) => { - return resolveProjectPath(filepath, cliConfig.root); - }, - }, - symbolicator: { - getSource: (url) => { - let { resourcePath, platform } = parseUrl(url, platforms); - resourcePath = resolveProjectPath(resourcePath, cliConfig.root); - return compiler.getSource(resourcePath, platform); - }, - getSourceMap: (url) => { - const { resourcePath, platform } = parseUrl(url, platforms); - return compiler.getSourceMap(resourcePath, platform); - }, - shouldIncludeFrame: (frame) => { - // If the frame points to internal bootstrap/module system logic, skip the code frame. - return !/webpack[/\\]runtime[/\\].+\s/.test(frame.file); - }, - }, - messages: { - getHello: () => 'React Native packager is running', - getStatus: () => 'packager-status:running', - }, - logger: { - onMessage: (log) => { - const logEntry = makeLogEntryFromFastifyLog(log); - logEntry.issuer = 'DevServer'; - reporter.process(logEntry); - }, - }, - api: { - getPlatforms: () => Promise.resolve(Object.keys(compiler.workers)), - getAssets: (platform) => - Promise.resolve( - Object.entries(compiler.assetsCache[platform] ?? {}).map( - ([name, asset]) => ({ name, size: asset.size }) - ) - ), - getCompilationStats: (platform) => - Promise.resolve(compiler.statsCache[platform] ?? null), - }, - }; - }, - }); - - await start(); - - return { - stop: async () => { - reporter.stop(); - await stop(); - }, - }; -} From 60f05f399720d5877d08f2cb74a80ff3536388a0 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Sat, 7 Feb 2026 15:41:08 +0100 Subject: [PATCH 05/10] refactor: update consumers to use unified @callstack/repack/commands Update init package: - Remove bundler parameter from modifyReactNativeConfig() - Use '@callstack/repack/commands' instead of bundler-specific paths Simplify react-native.config.js in all test apps to use the unified import instead of conditional require based on USE_WEBPACK env var. Update integration tests to import from '@callstack/repack/commands' instead of separate rspack/webpack command sets. Co-Authored-By: Claude Opus 4.6 --- apps/tester-app/__tests__/bundle.test.ts | 7 ++----- apps/tester-app/__tests__/start.test.ts | 7 ++----- apps/tester-app/react-native.config.js | 6 +----- .../tester-federation-v2/react-native.config.js | 6 +----- apps/tester-federation/react-native.config.js | 6 +----- packages/init/src/index.ts | 2 +- .../init/src/tasks/modifyReactNativeConfig.ts | 17 ++++++++--------- .../repack/src/commands/webpack/Compiler.ts | 6 +++++- 8 files changed, 21 insertions(+), 36 deletions(-) diff --git a/apps/tester-app/__tests__/bundle.test.ts b/apps/tester-app/__tests__/bundle.test.ts index 1e77bd671..2a75734a6 100644 --- a/apps/tester-app/__tests__/bundle.test.ts +++ b/apps/tester-app/__tests__/bundle.test.ts @@ -1,7 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; -import rspackCommands from '@callstack/repack/commands/rspack'; -import webpackCommands from '@callstack/repack/commands/webpack'; +import commands from '@callstack/repack/commands'; import { globby } from 'globby'; import { afterEach, @@ -17,15 +16,13 @@ describe('bundle command', () => { describe.each([ { bundler: 'webpack', - commands: webpackCommands, configFile: './webpack.config.mjs', }, { bundler: 'rspack', - commands: rspackCommands, configFile: './rspack.config.mjs', }, - ])('using $bundler', ({ bundler, commands, configFile }) => { + ])('using $bundler', ({ bundler, configFile }) => { const bundleCommand = commands.find((command) => command.name === 'bundle'); if (!bundleCommand) throw new Error('bundle command not found'); diff --git a/apps/tester-app/__tests__/start.test.ts b/apps/tester-app/__tests__/start.test.ts index cc45941f9..03a1419ea 100644 --- a/apps/tester-app/__tests__/start.test.ts +++ b/apps/tester-app/__tests__/start.test.ts @@ -1,7 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; -import rspackCommands from '@callstack/repack/commands/rspack'; -import webpackCommands from '@callstack/repack/commands/webpack'; +import commands from '@callstack/repack/commands'; import getPort from 'get-port'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; @@ -12,15 +11,13 @@ describe('start command', () => { describe.each([ { bundler: 'webpack', - commands: webpackCommands, configFile: './webpack.config.mjs', }, { bundler: 'rspack', - commands: rspackCommands, configFile: './rspack.config.mjs', }, - ])('using $bundler', ({ bundler, commands, configFile }) => { + ])('using $bundler', ({ bundler, configFile }) => { const startCommand = commands.find((command) => command.name === 'start'); if (!startCommand) throw new Error('start command not found'); diff --git a/apps/tester-app/react-native.config.js b/apps/tester-app/react-native.config.js index ceb40d38d..2f7653e16 100644 --- a/apps/tester-app/react-native.config.js +++ b/apps/tester-app/react-native.config.js @@ -1,7 +1,5 @@ const { configureProjects } = require('react-native-test-app'); -const useWebpack = Boolean(process.env.USE_WEBPACK); - module.exports = { project: configureProjects({ android: { @@ -11,7 +9,5 @@ module.exports = { sourceDir: 'ios', }, }), - commands: useWebpack - ? require('@callstack/repack/commands/webpack') - : require('@callstack/repack/commands/rspack'), + commands: require('@callstack/repack/commands'), }; diff --git a/apps/tester-federation-v2/react-native.config.js b/apps/tester-federation-v2/react-native.config.js index ceb40d38d..2f7653e16 100644 --- a/apps/tester-federation-v2/react-native.config.js +++ b/apps/tester-federation-v2/react-native.config.js @@ -1,7 +1,5 @@ const { configureProjects } = require('react-native-test-app'); -const useWebpack = Boolean(process.env.USE_WEBPACK); - module.exports = { project: configureProjects({ android: { @@ -11,7 +9,5 @@ module.exports = { sourceDir: 'ios', }, }), - commands: useWebpack - ? require('@callstack/repack/commands/webpack') - : require('@callstack/repack/commands/rspack'), + commands: require('@callstack/repack/commands'), }; diff --git a/apps/tester-federation/react-native.config.js b/apps/tester-federation/react-native.config.js index ceb40d38d..2f7653e16 100644 --- a/apps/tester-federation/react-native.config.js +++ b/apps/tester-federation/react-native.config.js @@ -1,7 +1,5 @@ const { configureProjects } = require('react-native-test-app'); -const useWebpack = Boolean(process.env.USE_WEBPACK); - module.exports = { project: configureProjects({ android: { @@ -11,7 +9,5 @@ module.exports = { sourceDir: 'ios', }, }), - commands: useWebpack - ? require('@callstack/repack/commands/webpack') - : require('@callstack/repack/commands/rspack'), + commands: require('@callstack/repack/commands'), }; diff --git a/packages/init/src/index.ts b/packages/init/src/index.ts index 5fdd07edf..f4efe4b96 100644 --- a/packages/init/src/index.ts +++ b/packages/init/src/index.ts @@ -65,7 +65,7 @@ export default async function run(options: Options) { options.entry ); - modifyReactNativeConfig(bundler, projectRootDir); + modifyReactNativeConfig(projectRootDir); modifyIOS(projectRootDir); diff --git a/packages/init/src/tasks/modifyReactNativeConfig.ts b/packages/init/src/tasks/modifyReactNativeConfig.ts index e3c6c0513..539f1d129 100644 --- a/packages/init/src/tasks/modifyReactNativeConfig.ts +++ b/packages/init/src/tasks/modifyReactNativeConfig.ts @@ -3,9 +3,11 @@ import path from 'node:path'; import dedent from 'dedent'; import logger from '../utils/logger.js'; -const createDefaultConfig = (bundler: 'rspack' | 'webpack') => dedent` +const COMMANDS_REQUIRE = `require('@callstack/repack/commands')`; + +const createDefaultConfig = () => dedent` module.exports = { - commands: require('@callstack/repack/commands/${bundler}'), + commands: ${COMMANDS_REQUIRE}, };`; /** @@ -13,14 +15,11 @@ const createDefaultConfig = (bundler: 'rspack' | 'webpack') => dedent` * * @param cwd current working directory */ -export default function modifyReactNativeConfig( - bundler: 'rspack' | 'webpack', - cwd: string -): void { +export default function modifyReactNativeConfig(cwd: string): void { const configPath = path.join(cwd, 'react-native.config.js'); if (!fs.existsSync(configPath)) { - fs.writeFileSync(configPath, createDefaultConfig(bundler)); + fs.writeFileSync(configPath, createDefaultConfig()); logger.info('Created react-native.config.js'); return; } @@ -31,14 +30,14 @@ export default function modifyReactNativeConfig( if (!configContent.includes('commands:')) { updatedConfigContent = configContent.replace( 'module.exports = {', - `module.exports = {\n commands: require('@callstack/repack/commands/${bundler}'),` + `module.exports = {\n commands: ${COMMANDS_REQUIRE},` ); } else { const commandsIndex = configContent.indexOf('commands:'); const commandsEndIndex = configContent.indexOf(',', commandsIndex); const commandsString = configContent.slice(commandsIndex, commandsEndIndex); - const newCommandsString = `commands: require('@callstack/repack/commands/${bundler}')`; + const newCommandsString = `commands: ${COMMANDS_REQUIRE}`; if (commandsString === newCommandsString) { logger.info('File react-native.config.js is already up to date'); return; diff --git a/packages/repack/src/commands/webpack/Compiler.ts b/packages/repack/src/commands/webpack/Compiler.ts index 41fa75b62..b880a1098 100644 --- a/packages/repack/src/commands/webpack/Compiler.ts +++ b/packages/repack/src/commands/webpack/Compiler.ts @@ -138,7 +138,11 @@ export class Compiler implements CompilerInterface { type: 'error', issuer: 'WebpackCompilerWorker', timestamp: Date.now(), - message: [String(value.error)], + message: [ + value.error instanceof Error + ? (value.error.stack ?? String(value.error)) + : String(value.error), + ], }); } else if (value.event === 'progress') { this.progressSenders[platform]?.forEach((sendProgress) => { From 35c831ba28f0a7633c05b83032fb8b44a05df929 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Sun, 8 Feb 2026 18:31:24 +0100 Subject: [PATCH 06/10] fix: types --- packages/repack/src/commands/bundle.ts | 48 +++++++++++++++++++++----- packages/repack/src/commands/index.ts | 14 ++++++-- packages/repack/src/commands/start.ts | 7 ++-- packages/repack/src/commands/types.ts | 18 ++++++++-- 4 files changed, 70 insertions(+), 17 deletions(-) diff --git a/packages/repack/src/commands/bundle.ts b/packages/repack/src/commands/bundle.ts index d9011a899..894da4639 100644 --- a/packages/repack/src/commands/bundle.ts +++ b/packages/repack/src/commands/bundle.ts @@ -9,7 +9,39 @@ import { setupRspackEnvironment, writeStats, } from './common/index.js'; -import type { BundleArguments, Bundler, CliConfig } from './types.js'; +import type { + BundleArguments, + Bundler, + CliConfig, + ConfigurationObject, +} from './types.js'; + +/** + * Minimal compiler interface for the bundle command. + * Both rspack() and webpack() return objects satisfying this shape. + * Defined here because both bundlers are optional peer dependencies. + */ +interface BundleCompiler { + run(callback: (error: Error | null, stats?: BundleStats) => void): void; + watch( + options: Record, + callback: (error: Error | null, stats?: BundleStats) => void + ): unknown; + hooks: { watchClose: { tap(name: string, fn: () => void): void } }; + close(callback: (error: Error | null) => void): void; + options: { stats: unknown }; + context: string; +} + +/** + * Minimal stats interface for the bundle command. + * Both rspack and webpack Stats objects satisfy this shape. + */ +interface BundleStats { + hasErrors(): boolean; + compilation?: { errors?: unknown[] }; + toJson(options: unknown): Record; +} /** * Unified bundle command that builds and saves the bundle @@ -36,7 +68,7 @@ export async function bundle( args.bundler ); - const [config] = await makeCompilerConfig>({ + const [config] = await makeCompilerConfig({ args: args, bundler, command: 'bundle', @@ -77,23 +109,23 @@ export async function bundle( } // Dynamic import of bundler engine — both are optional peer dependencies - let compiler: any; + let compiler: BundleCompiler; if (bundler === 'rspack') { const { rspack } = await import('@rspack/core'); - compiler = rspack(config); + compiler = rspack(config) as BundleCompiler; } else { const webpack = (await import('webpack')).default; - compiler = webpack(config); + compiler = webpack(config) as BundleCompiler; } return new Promise((resolve) => { - const errorHandler = async (error: Error | null, stats?: any) => { + const errorHandler = async (error: Error | null, stats?: BundleStats) => { if (error) { throw new CLIError(error.message); } if (stats?.hasErrors()) { - stats.compilation?.errors?.forEach((e: any) => { + stats.compilation?.errors?.forEach((e) => { console.error(e); }); process.exit(2); @@ -122,7 +154,7 @@ export async function bundle( compiler.hooks.watchClose.tap('bundle', resolve); compiler.watch(config.watchOptions ?? {}, errorHandler); } else { - compiler.run((error: Error | null, stats: any) => { + compiler.run((error: Error | null, stats?: BundleStats) => { // make cache work: https://webpack.js.org/api/node/#run compiler.close(async (closeErr: Error | null) => { if (closeErr) console.error(closeErr); diff --git a/packages/repack/src/commands/index.ts b/packages/repack/src/commands/index.ts index bffe6d294..6e278081b 100644 --- a/packages/repack/src/commands/index.ts +++ b/packages/repack/src/commands/index.ts @@ -1,7 +1,12 @@ import { bundle } from './bundle.js'; import { bundleCommandOptions, startCommandOptions } from './options.js'; import { start } from './start.js'; -import type { Bundler } from './types.js'; +import type { + BundleArguments, + Bundler, + CliConfig, + StartArguments, +} from './types.js'; const commands = [ { @@ -40,7 +45,10 @@ export default commands; export function createBoundCommands(bundler: Bundler) { return commands.map((cmd) => ({ ...cmd, - func: (_: string[], cliConfig: any, args: any) => - cmd.func(_, cliConfig, args, bundler), + func: ( + _: string[], + cliConfig: CliConfig, + args: BundleArguments & StartArguments + ) => cmd.func(_, cliConfig, args, bundler), })); } diff --git a/packages/repack/src/commands/start.ts b/packages/repack/src/commands/start.ts index f746c5f3c..22536b47e 100644 --- a/packages/repack/src/commands/start.ts +++ b/packages/repack/src/commands/start.ts @@ -27,6 +27,7 @@ import type { Bundler, CliConfig, CompilerInterface, + ConfigurationObject, StartArguments, } from './types.js'; @@ -64,7 +65,7 @@ export async function start( const platforms = args.platform ? [args.platform] : detectedPlatforms; - const configs = await makeCompilerConfig>({ + const configs = await makeCompilerConfig({ args: args, bundler, command: 'start', @@ -103,13 +104,13 @@ export async function start( resetPersistentCache({ bundler: 'rspack', rootDir: cliConfig.root, - cacheConfigs: configs.map((config: any) => config.experiments?.cache), + cacheConfigs: configs.map((config) => config.experiments?.cache), }); } else { resetPersistentCache({ bundler: 'webpack', rootDir: cliConfig.root, - cacheConfigs: configs.map((config: any) => config.cache), + cacheConfigs: configs.map((config) => config.cache), }); } } diff --git a/packages/repack/src/commands/types.ts b/packages/repack/src/commands/types.ts index 3fa271b37..2bad684dd 100644 --- a/packages/repack/src/commands/types.ts +++ b/packages/repack/src/commands/types.ts @@ -74,8 +74,20 @@ type ConfigKeys = | 'entry' | 'optimization' | 'output' - | 'resolve'; + | 'resolve' + | 'experiments' + | 'cache' + | 'watchOptions' + | 'stats' + | 'plugins'; +/** + * Loose configuration shape used as a generic constraint for `makeCompilerConfig`. + * Values are intentionally `any` — both webpack and rspack `Configuration` types + * must satisfy `C extends ConfigurationObject` in generic helper functions that + * assign to `Partial` properties (e.g. `getCliOverrides`, `normalizeConfig`). + * Using `unknown` values would break these generic assignments. + */ export type ConfigurationObject = Partial>; export type Configuration = @@ -88,7 +100,7 @@ export interface CompilerAsset { hotModuleReplacement?: boolean; related?: { sourceMap?: string | string[] }; size?: number; - [key: string]: any; + [key: string]: unknown; }; size: number; } @@ -96,7 +108,7 @@ export interface CompilerAsset { export interface CompilerInterface { platforms: string[]; assetsCache: Record | undefined>; - statsCache: Record | undefined>; + statsCache: Record; setDevServerContext(ctx: Server.DelegateContext): void; start(): void; getAsset( From 32236c872e45d036828f111984171a0aabda1efa Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 29 Jul 2026 17:30:00 +0200 Subject: [PATCH 07/10] fix(commands): reject pending assets on webpack errors --- .../repack/src/commands/webpack/Compiler.ts | 3 +- .../webpack/__tests__/Compiler.test.ts | 42 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 packages/repack/src/commands/webpack/__tests__/Compiler.test.ts diff --git a/packages/repack/src/commands/webpack/Compiler.ts b/packages/repack/src/commands/webpack/Compiler.ts index b880a1098..291a38747 100644 --- a/packages/repack/src/commands/webpack/Compiler.ts +++ b/packages/repack/src/commands/webpack/Compiler.ts @@ -133,7 +133,7 @@ export class Compiler implements CompilerInterface { }); callPendingResolvers(); } else if (value.event === 'error') { - // errors are logged but not fatal to the compiler lifecycle + this.isCompilationInProgress[platform] = false; this.reporter.process({ type: 'error', issuer: 'WebpackCompilerWorker', @@ -144,6 +144,7 @@ export class Compiler implements CompilerInterface { : String(value.error), ], }); + callPendingResolvers(value.error); } else if (value.event === 'progress') { this.progressSenders[platform]?.forEach((sendProgress) => { const percentage = Math.floor(value.percentage * 100); diff --git a/packages/repack/src/commands/webpack/__tests__/Compiler.test.ts b/packages/repack/src/commands/webpack/__tests__/Compiler.test.ts new file mode 100644 index 000000000..e75b794d1 --- /dev/null +++ b/packages/repack/src/commands/webpack/__tests__/Compiler.test.ts @@ -0,0 +1,42 @@ +import type { EventEmitter } from 'node:events'; +import { Worker } from 'node:worker_threads'; +import type { Reporter } from '../../../logging/types.js'; +import { Compiler } from '../Compiler.js'; + +jest.mock('node:worker_threads', () => { + const { EventEmitter } = + jest.requireActual('node:events'); + + return { + Worker: jest.fn(() => + Object.assign(new EventEmitter(), { + stdout: new EventEmitter(), + stderr: new EventEmitter(), + }) + ), + }; +}); + +test('rejects a pending asset request when the worker reports an error', async () => { + const reporter: Reporter = { + process: jest.fn(), + flush: jest.fn(), + stop: jest.fn(), + }; + const compiler = new Compiler( + ['ios'], + { host: '' }, + reporter, + '/project', + '/react-native' + ); + + const request = compiler.getAsset('index.bundle', 'ios'); + const worker = jest.mocked(Worker).mock.results[0].value as EventEmitter; + const error = new Error('Compilation failed'); + + worker.emit('message', { event: 'error', error }); + + expect(compiler.resolvers.ios).toHaveLength(0); + await expect(request).rejects.toBe(error); +}); From c5254f205a8679cb8ded6a17108a03e832a7489c Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 29 Jul 2026 19:31:20 +0200 Subject: [PATCH 08/10] fix(commands): validate bundler option --- .../src/commands/__tests__/options.test.ts | 20 +++++++++++++++++++ packages/repack/src/commands/options.ts | 14 +++++++++++++ website/src/latest/api/cli/bundle.mdx | 6 ++++++ website/src/latest/api/cli/start.mdx | 6 ++++++ 4 files changed, 46 insertions(+) create mode 100644 packages/repack/src/commands/__tests__/options.test.ts diff --git a/packages/repack/src/commands/__tests__/options.test.ts b/packages/repack/src/commands/__tests__/options.test.ts new file mode 100644 index 000000000..8e90c60ba --- /dev/null +++ b/packages/repack/src/commands/__tests__/options.test.ts @@ -0,0 +1,20 @@ +import { bundleCommandOptions, startCommandOptions } from '../options.js'; + +describe.each([ + ['start', startCommandOptions], + ['bundle', bundleCommandOptions], +])('%s command options', (_, options) => { + const bundlerOption = options.find( + (option) => option.name === '--bundler ' + ); + + test.each(['rspack', 'webpack'])('accepts the %s bundler', (bundler) => { + expect(bundlerOption?.parse?.(bundler)).toBe(bundler); + }); + + test('rejects an unsupported bundler', () => { + expect(() => bundlerOption?.parse?.('metro')).toThrow( + 'Invalid bundler "metro". Expected "rspack" or "webpack".' + ); + }); +}); diff --git a/packages/repack/src/commands/options.ts b/packages/repack/src/commands/options.ts index 98f0153e2..fa7e18819 100644 --- a/packages/repack/src/commands/options.ts +++ b/packages/repack/src/commands/options.ts @@ -1,4 +1,16 @@ import path from 'node:path'; +import { CLIError } from '../helpers/index.js'; +import type { Bundler } from './types.js'; + +function parseBundler(value: string): Bundler { + if (value === 'rspack' || value === 'webpack') { + return value; + } + + throw new CLIError( + `Invalid bundler "${value}". Expected "rspack" or "webpack".` + ); +} export const startCommandOptions = [ { @@ -80,6 +92,7 @@ export const startCommandOptions = [ name: '--bundler ', description: 'Bundler engine to use: "rspack" or "webpack". If not specified, auto-detected from config filename.', + parse: parseBundler, }, ]; @@ -181,5 +194,6 @@ export const bundleCommandOptions = [ name: '--bundler ', description: 'Bundler engine to use: "rspack" or "webpack". If not specified, auto-detected from config filename.', + parse: parseBundler, }, ]; diff --git a/website/src/latest/api/cli/bundle.mdx b/website/src/latest/api/cli/bundle.mdx index 0a4caba30..6e3e47299 100644 --- a/website/src/latest/api/cli/bundle.mdx +++ b/website/src/latest/api/cli/bundle.mdx @@ -132,6 +132,12 @@ Watch for file changes. Path to a bundler config file, e.g rspack.config.js. +### `--bundler` + +- Type: `"rspack" | "webpack"` + +Select the bundler explicitly. Without this option, Re.Pack infers the bundler from the `--config` filename, then checks the standard Rspack and webpack config filenames, and defaults to Rspack. + ### `--webpackConfig` - Type: `string` diff --git a/website/src/latest/api/cli/start.mdx b/website/src/latest/api/cli/start.mdx index 81b280cb3..95aa0d258 100644 --- a/website/src/latest/api/cli/start.mdx +++ b/website/src/latest/api/cli/start.mdx @@ -110,6 +110,12 @@ Enables verbose logging. Path to a bundler config file, e.g webpack.config.js. +### `--bundler` + +- Type: `"rspack" | "webpack"` + +Select the bundler explicitly. Without this option, Re.Pack infers the bundler from the `--config` filename, then checks the standard Rspack and webpack config filenames, and defaults to Rspack. + ### `--webpackConfig` - Type: `path` From ec1d5b3c537daf951c58245ae5f788466cfa1374 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 29 Jul 2026 19:41:22 +0200 Subject: [PATCH 09/10] chore: add unified commands changeset --- .changeset/unify-bundler-commands.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/unify-bundler-commands.md diff --git a/.changeset/unify-bundler-commands.md b/.changeset/unify-bundler-commands.md new file mode 100644 index 000000000..0fca6f261 --- /dev/null +++ b/.changeset/unify-bundler-commands.md @@ -0,0 +1,8 @@ +--- +"@callstack/repack": minor +"@callstack/repack-init": patch +--- + +Add the unified `@callstack/repack/commands` entry point, which automatically detects Rspack or webpack from the project configuration and supports an explicit `--bundler` override. The bundler-specific command entry points remain available with deprecation warnings, and Re.Pack Init now generates the unified command configuration. + +Reject pending webpack asset requests when compilation fails instead of leaving the requests hanging. From 39d67efd761f75c5d4d8764216797e4d6c52cede Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 29 Jul 2026 20:22:24 +0200 Subject: [PATCH 10/10] chore: split unified command release notes --- .changeset/reject-webpack-asset-requests.md | 5 +++++ .changeset/unify-bundler-commands.md | 4 +--- 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 .changeset/reject-webpack-asset-requests.md diff --git a/.changeset/reject-webpack-asset-requests.md b/.changeset/reject-webpack-asset-requests.md new file mode 100644 index 000000000..18966657d --- /dev/null +++ b/.changeset/reject-webpack-asset-requests.md @@ -0,0 +1,5 @@ +--- +"@callstack/repack": patch +--- + +Reject pending webpack asset requests when compilation fails instead of leaving requests hanging. diff --git a/.changeset/unify-bundler-commands.md b/.changeset/unify-bundler-commands.md index 0fca6f261..9f045ab5c 100644 --- a/.changeset/unify-bundler-commands.md +++ b/.changeset/unify-bundler-commands.md @@ -3,6 +3,4 @@ "@callstack/repack-init": patch --- -Add the unified `@callstack/repack/commands` entry point, which automatically detects Rspack or webpack from the project configuration and supports an explicit `--bundler` override. The bundler-specific command entry points remain available with deprecation warnings, and Re.Pack Init now generates the unified command configuration. - -Reject pending webpack asset requests when compilation fails instead of leaving the requests hanging. +Add the unified `@callstack/repack/commands` entry point with automatic bundler detection and a `--bundler` override. Re.Pack Init now uses it, while bundler-specific entry points remain available with deprecation warnings.