Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ interface BrowserRequirePluginOptions {
/**
* This function defines how to execute CommonJS code.
*/
execute: (code: string, runtime: CommonJsRuntime) => void;
execute?: (code: string, runtime: CommonJsRuntime) => void;
/**
* This option provides a direct mapping from the module specifier to the module content, similar to the mechanism of a virtual module.
* If this option is not provided or the mapping result is undefined, it will fallback to resolving from memfs and run `execute`.
*/
modules?: Record<string, unknown> | ((id: string) => unknown);
}

const unsafeExecute: BrowserRequirePluginOptions["execute"] = (
Expand All @@ -30,8 +35,9 @@ const unsafeExecute: BrowserRequirePluginOptions["execute"] = (

/**
* This plugin inject browser-compatible `require` function to the `Compiler`.
* 1. It resolves the JavaScript in the memfs with Node.js resolution algorithm rather than in the host filesystem.
* 2. It transform ESM to CommonJS which will be executed with a user-defined `execute` function.
* 1. This plugin makes it possible to use custom loaders in browser by providing a virtual module mechanism.
* 2. This plugin resolves the JavaScript in the memfs with Node.js resolution algorithm rather than in the host filesystem.
* 3. This plugin transform ESM to CommonJS which will be executed with a user-defined `execute` function.
*/
export class BrowserRequirePlugin {
/**
Expand All @@ -43,8 +49,28 @@ export class BrowserRequirePlugin {
constructor(private options: BrowserRequirePluginOptions) {}

apply(compiler: Compiler) {
const execute = this.options.execute;
const { execute, modules } = this.options;
compiler.__internal_browser_require = function browserRequire(id: string) {
// Try to map id to module
if (typeof modules === "function") {
const module = modules(id);
if (module) {
return module;
}
} else if (typeof modules === "object") {
const module = modules[id];
if (module) {
return module;
}
}

// Fallback: resolve in memfs and execute
if (!execute) {
throw Error(
`You should provide 'execute' option if there's no mapping for module '${id}'`
);
}

const { path: loaderPath } = resolveSync("", id);
if (!loaderPath) {
throw new Error(`Cannot find loader of ${id}`);
Expand Down
2 changes: 1 addition & 1 deletion packages/rspack/src/browser/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export * from "../index";
export { BrowserHttpImportEsmPlugin } from "./BrowserHttpImportEsmPlugin";
export { BrowserRequirePlugin } from "./BrowserRequire";
export { BrowserRequirePlugin } from "./BrowserRequirePlugin";

import { fs, volume } from "./fs";
export const builtinMemFs = {
Expand Down
6 changes: 3 additions & 3 deletions packages/rspack/src/loader-runner/loadLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ export default function loadLoader(
callback: (err: unknown) => void
): void {
if (IS_BROWSER) {
let module: any;
let module: LoaderModule;
try {
module = compiler.__internal_browser_require(loader.path);
module = compiler.__internal_browser_require(loader.path) as LoaderModule;
} catch (e) {
return callback(e);
}
Expand All @@ -52,7 +52,7 @@ export default function loadLoader(
callback(e);
}
} else {
let module: any;
let module: LoaderModule;
try {
module = require(loader.path);
} catch (e) {
Expand Down
85 changes: 57 additions & 28 deletions website/docs/en/api/javascript-api/browser.mdx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ApiMeta } from '../../../../components/ApiMeta';

# Browser API

`@rspack/browser` is a version of Rspack specifically designed for browser environments, without relying on WebContainers or any particular platform. Its API is consistent with the [JavaScript API](/api/javascript-api/) of `@rspack/core`, while additionally providing features and interfaces tailored for the browser environment.
Expand Down Expand Up @@ -145,12 +147,65 @@ interface BrowserHttpImportPluginOptions {

In Rspack, certain scenarios require dynamically loading and executing JavaScript code, such as [Loaders](/guide/features/loader) or the template functions of [HtmlRspackPlugin](/plugins/rspack/html-rspack-plugin#use-template-function). Since this code may come from untrusted users, executing it directly in the browser environment poses potential security risks. To ensure safety, `@rspack/browser` throws errors by default in such cases to prevent unsafe code execution.

The `BrowserRequirePlugin` plugin provides two ways to address this requirement.

Options for `BrowserRequirePlugin` are as follows:

```ts
interface BrowserRequirePluginOptions {
/**
* This function defines how to execute CommonJS code.
*/
execute?: (code: string, runtime: CommonJsRuntime) => void;
/**
* This option provides a direct mapping from the module specifier to the module content, similar to the mechanism of a virtual module.
* If this option is not provided or the mapping result is undefined, it will fallback to resolving from memfs and run `execute`.
*/
modules?: Record<string, any> | ((id: string) => any);
}
```

#### `modules`

<ApiMeta addedVersion="1.5.9" />

This option allows you to directly map a module request id to any JavaScript object within the project. Note that you need to create a corresponding empty file in `memfs`:

```js title="rspack.config.mjs"
import { BrowserRequirePlugin, builtinMemfs } from '@rspack/browser';
import CustomLoader from './custom-loader';

builtinMemFs.volume.fromJSON({
'/LOADER/custom-loader.js': '',
});

export default {
module: {
rules: [
{
test: /a\.js$/,
loader: '/LOADER/custom-loader.js',
},
],
},
plugins: [
new BrowserRequirePlugin({
modules: {
'/LOADER/custom-loader.js': CustomLoader,
},
}),
],
};
```

#### `execute`

This option is used to simulate the `require` process in Node.js: it resolves modules based on memfs, reads file contents, and executes them. When modules is not provided, or the corresponding result is not found in modules, this path will be attempted.

:::warning
Rspack does not execute user code during bundling. For security, it is recommended to run the final bundled output inside an iframe.
:::

The `BrowserRequirePlugin` plugin enables this capability:

```js title="rspack.config.mjs"
import { BrowserRequirePlugin } from '@rspack/browser';

Expand All @@ -174,32 +229,6 @@ function uselessExecute(_code: string, runtime: CommonJsRuntime) {
}
```

Options for `BrowserRequirePlugin` are as follows:

```ts
/**
* Runtime context for loading CommonJS modules
*/
interface CommonJsRuntime {
module: any;
exports: any;
require: BrowserRequire;
}

interface BrowserRequirePluginOptions {
/**
* Function to execute dynamic code
*/
execute: (code: string, runtime: CommonJsRuntime) => void;
}
```

:::tip How to decide whether to use this plugin?
If your project does not require dynamic loading and execution of JavaScript code, you do not need this plugin.

If your project does not distribute untrusted code or distributing such code does not cause security issues, you can directly use `BrowserRequirePlugin.unsafeExecute`. For example, the [Rspack Playground](https://playground.rspack.rs/) does not involve user privacy or account security.
:::

## Using Module Federation

`@rspack/browser` supports Rspack's [ModuleFederationPlugin](/plugins/webpack/module-federation-plugin). With this feature, you can pre-bundle complex dependency modules as provider projects deployed to CDN, and then use them in consumer projects that are bundled online in the browser.
Expand Down
94 changes: 66 additions & 28 deletions website/docs/zh/api/javascript-api/browser.mdx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ApiMeta } from '../../../../components/ApiMeta';

# Browser API

`@rspack/browser` 是专为浏览器环境打造的 Rspack 版本,无需依赖 WebContainers 或特定平台。其 API 与 `@rspack/core` 的 [JavaScript API](/api/javascript-api/) 保持一致,并在此基础上,额外提供了适配浏览器环境的特性和接口。
Expand Down Expand Up @@ -143,12 +145,74 @@ interface BrowserHttpImportPluginOptions {

在 Rspack 中,某些场景需要动态加载和执行 JavaScript 代码,如 [Loader](/guide/features/loader) 或 [HtmlRspackPlugin 的模板函数](/plugins/rspack/html-rspack-plugin#use-template-function)。由于这些代码可能来自不可信的第三方用户,直接在浏览器环境中执行会带来潜在的安全风险。为了保障安全性,`@rspack/browser` 在遇到此类场景时会默认抛出错误,阻止不安全代码的执行。

`BrowserRequirePlugin` 插件提供了两种方式解决这个需求。

`BrowserRequirePlugin` 的选项如下:

```ts
/**
* 加载 CommonJS 模块的运行时上下文
*/
interface CommonJsRuntime {
module: any;
exports: any;
require: BrowserRequire;
}

interface BrowserRequirePluginOptions {
/**
* 执行动态代码的函数
*/
execute?: (code: string, runtime: CommonJsRuntime) => void;
/**
* 这个选项提供了直接从 id 到模块内容的映射,类似于 virtual module 机制。
* 如果没有提供该选项或映射的结果为 undefined,那么会 fallback 到从 memfs 中解析,并执行 `execute`。
*/
modules?: Record<string, any> | ((id: string) => any);
}
```

#### `modules`

<ApiMeta addedVersion="1.5.9" />

该选项能够直接将模块请求 id 映射到项目内的任意 JavaScript 对象,注意,这里需要在 memfs 中对应地写入一个空的文件:

```js title="rspack.config.mjs"
import { BrowserRequirePlugin, builtinMemfs } from '@rspack/browser';
import CustomLoader from './custom-loader';

builtinMemFs.volume.fromJSON({
'/LOADER/custom-loader.js': '',
});

export default {
module: {
rules: [
{
test: /a\.js$/,
loader: '/LOADER/custom-loader.js',
},
],
},
plugins: [
new BrowserRequirePlugin({
modules: {
'/LOADER/custom-loader.js': CustomLoader,
},
}),
],
};
```

#### `execute`

该选项用于模拟 Node.js 中的 Require 过程:基于 memfs 解析模块、读取文件内容并执行。当未提供 `modules`,或在 `modules` 中未找到对应结果时,将尝试使用此路径。

:::warning
Rspack 在打包过程中不会执行项目的用户代码。为了安全起见,建议在 iframe 中运行最终的打包产物。
:::

`BrowserRequirePlugin` 插件开放了此类能力:

```js title="rspack.config.mjs"
import { BrowserRequirePlugin } from '@rspack/browser';

Expand All @@ -172,32 +236,6 @@ function uselessExecute(_code: string, runtime: CommonJsRuntime) {
}
```

`BrowserRequirePlugin` 的选项如下:

```ts
/**
* 加载 CommonJS 模块的运行时上下文
*/
interface CommonJsRuntime {
module: any;
exports: any;
require: BrowserRequire;
}

interface BrowserRequirePluginOptions {
/**
* 执行动态代码的函数
*/
execute: (code: string, runtime: CommonJsRuntime) => void;
}
```

:::tip 如何选择是否使用此插件?
如果你的项目无需动态加载和执行 JavaScript 代码,则无需使用此插件。

若项目不会分发不可信代码,或即使分发也不会造成安全问题,可直接使用 `BrowserRequirePlugin.unsafeExecute`。例如 [Rspack Playground](https://playground.rspack.rs/) 就不涉及用户隐私或账户安全。
:::

## 使用模块联邦

`@rspack/browser` 支持 Rspack 的 [ModuleFederationPlugin](/plugins/webpack/module-federation-plugin) 插件。你可以通过这个功能,将一些复杂的依赖模块提前打包为生产者项目部署到 CDN 上,然后在浏览器中在线打包的消费者项目中使用。
Expand Down
Loading