Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/reject-webpack-asset-requests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@callstack/repack": patch
---

Reject pending webpack asset requests when compilation fails instead of leaving requests hanging.
6 changes: 6 additions & 0 deletions .changeset/unify-bundler-commands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@callstack/repack": minor
"@callstack/repack-init": patch
---

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.
7 changes: 2 additions & 5 deletions apps/tester-app/__tests__/bundle.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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');

Expand Down
7 changes: 2 additions & 5 deletions apps/tester-app/__tests__/start.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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');

Expand Down
6 changes: 1 addition & 5 deletions apps/tester-app/react-native.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
const { configureProjects } = require('react-native-test-app');

const useWebpack = Boolean(process.env.USE_WEBPACK);

module.exports = {
project: configureProjects({
android: {
Expand All @@ -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'),
};
6 changes: 1 addition & 5 deletions apps/tester-federation-v2/react-native.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
const { configureProjects } = require('react-native-test-app');

const useWebpack = Boolean(process.env.USE_WEBPACK);

module.exports = {
project: configureProjects({
android: {
Expand All @@ -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'),
};
6 changes: 1 addition & 5 deletions apps/tester-federation/react-native.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
const { configureProjects } = require('react-native-test-app');

const useWebpack = Boolean(process.env.USE_WEBPACK);

module.exports = {
project: configureProjects({
android: {
Expand All @@ -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'),
};
2 changes: 1 addition & 1 deletion packages/init/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export default async function run(options: Options) {
options.entry
);

modifyReactNativeConfig(bundler, projectRootDir);
modifyReactNativeConfig(projectRootDir);

modifyIOS(projectRootDir);

Expand Down
17 changes: 8 additions & 9 deletions packages/init/src/tasks/modifyReactNativeConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,23 @@ 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},
};`;

/**
* Checks whether react-native.config.js exists and adds the commands to it
*
* @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;
}
Expand All @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions packages/repack/commands/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import commands from '../dist/commands/index.js';
export = commands;
2 changes: 2 additions & 0 deletions packages/repack/commands/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const commands = require('../dist/commands/index.js');
module.exports = commands.default;
2 changes: 1 addition & 1 deletion packages/repack/commands/rspack.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
import commands from '../dist/commands/rspack/index.js';
import commands from '../dist/commands/index.js';
export = commands;
7 changes: 5 additions & 2 deletions packages/repack/commands/rspack.js
Original file line number Diff line number Diff line change
@@ -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');
2 changes: 1 addition & 1 deletion packages/repack/commands/webpack.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
import commands from '../dist/commands/webpack/index.js';
import commands from '../dist/commands/index.js';
export = commands;
7 changes: 5 additions & 2 deletions packages/repack/commands/webpack.js
Original file line number Diff line number Diff line change
@@ -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');
1 change: 1 addition & 0 deletions packages/repack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 20 additions & 0 deletions packages/repack/src/commands/__tests__/options.test.ts
Original file line number Diff line number Diff line change
@@ -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 <string>'
);

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".'
);
});
});
167 changes: 167 additions & 0 deletions packages/repack/src/commands/bundle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
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,
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<string, unknown>,
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<string, unknown>;
}

/**
* 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<ConfigurationObject>({
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 <path>' 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: BundleCompiler;
if (bundler === 'rspack') {
const { rspack } = await import('@rspack/core');
compiler = rspack(config) as BundleCompiler;
} else {
const webpack = (await import('webpack')).default;
compiler = webpack(config) as BundleCompiler;
}

return new Promise<void>((resolve) => {
const errorHandler = async (error: Error | null, stats?: BundleStats) => {
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));
}
}
};

if (args.watch) {
compiler.hooks.watchClose.tap('bundle', resolve);
compiler.watch(config.watchOptions ?? {}, errorHandler);
} else {
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);
await errorHandler(error, stats);
resolve();
});
});
}
});
}
Loading
Loading