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
30 changes: 29 additions & 1 deletion packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { type CssLoaderOptionsAuto, isCssGlobalFile } from './css/utils';
import { composeExeConfig } from './exe';
import { composeEntryChunkConfig } from './plugins/EntryChunkPlugin';
import { pluginCjsShims, pluginEsmRequireShim } from './plugins/shims';
import { composeWasmConfig, resolveWasmConfig } from './wasm';
import type {
AutoExternal,
BannerAndFooter,
Expand Down Expand Up @@ -1168,6 +1169,7 @@ const composeEntryConfig = async (
bundle: LibConfig['bundle'],
root: string,
cssModulesAuto: CssLoaderOptionsAuto,
excludeWasmEntry: boolean,
userOutBase?: string,
): Promise<{ entryConfig: EnvironmentConfig; outBase: string | null }> => {
let entries = rawEntry;
Expand Down Expand Up @@ -1291,7 +1293,11 @@ const composeEntryConfig = async (

// Filter the glob resolved entry files based on the allowed extensions
const resolvedEntryFiles = globEntryFiles.filter((i) => {
return !DTS_EXTENSIONS_PATTERN.test(i);
if (DTS_EXTENSIONS_PATTERN.test(i)) return false;
// In preserve mode, `.wasm` files are handled by the wasm externals
// plugin, not as source entries.
if (excludeWasmEntry && i.endsWith('.wasm')) return false;
return true;
});

if (resolvedEntryFiles.length === 0) {
Expand Down Expand Up @@ -1370,6 +1376,7 @@ const composeBundlelessExternalConfig = (
cssModulesAuto: CssLoaderOptionsAuto,
bundle: boolean,
outBase: string | null,
preExternals: Rspack.ExternalItem[],
): {
config: EnvironmentConfig;
resolvedJsRedirect?: DeepRequired<JsRedirect>;
Expand Down Expand Up @@ -1411,6 +1418,7 @@ const composeBundlelessExternalConfig = (
config: {
output: {
externals: [
...preExternals,
async (data, callback) => {
const { request, getResolve, context, contextInfo } = data;
if (!request || !getResolve || !context || !contextInfo) {
Expand Down Expand Up @@ -1828,11 +1836,19 @@ async function composeLibRsbuildConfig(
pkgJson,
);

const resolvedWasm = resolveWasmConfig({
bundle,
format,
wasmConfig: config.wasm,
});
const excludeWasmEntry = resolvedWasm.mode !== 'compile';

const { entryConfig, outBase } = await composeEntryConfig(
config.source?.entry,
config.bundle,
root,
cssModulesAuto,
excludeWasmEntry,
config.outBase,
);
const { config: exeConfig } = composeExeConfig({
Expand All @@ -1844,12 +1860,22 @@ async function composeLibRsbuildConfig(
target,
});

const wasmCompose = composeWasmConfig({
bundle,
format,
mode: resolvedWasm.mode,
hasUserWasmMode: resolvedWasm.hasUserWasmMode,
outBase,
});
const wasmConfig = wasmCompose.config;

const { config: bundlelessExternalConfig } = composeBundlelessExternalConfig(
jsExtension,
redirect,
cssModulesAuto,
bundle,
outBase,
wasmCompose.bundlelessExternal ? [wasmCompose.bundlelessExternal] : [],
);
const syntaxConfig = composeSyntaxConfig(target, config?.syntax);
const autoExternalConfig = composeAutoExternalConfig({
Expand Down Expand Up @@ -1912,6 +1938,7 @@ async function composeLibRsbuildConfig(
entryConfig,
cssConfig,
assetConfig,
wasmConfig,
entryChunkConfig,
minifyConfig,
dtsConfig,
Expand Down Expand Up @@ -2002,6 +2029,7 @@ export async function composeCreateRsbuildConfig(
shims: true,
umdName: true,
outBase: true,
wasm: true,
experiments: true,
}),
),
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type {
StartMFDevServerOptions,
StartDevServerResult,
Syntax,
Wasm,
} from './types';

export const version: string = RSLIB_VERSION;
Expand Down
24 changes: 24 additions & 0 deletions packages/core/src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,25 @@ export type Redirect = {
dts?: DtsRedirect;
};

export type WasmMode = 'compile' | 'preserve';

export type Wasm = {
/**
* How Rslib handles `.wasm` modules in the output.
*
* - `'compile'`: Rspack emits JS glue and the runtime needed to load the wasm.
* - `'preserve'`: Keep `.wasm` as a real ESM import in the output and emit the
* binary. In bundle mode the binary uses a content-hashed filename; in
* bundleless mode it keeps the source-relative path and original filename.
*
* Defaults to `'compile'` when `bundle` is `true`, `'preserve'` when `bundle`
* is `false`.
*
* Only effective for `format: 'esm'` builds.
*/
mode?: WasmMode;
};

export type LibExperiments = {
/**
* Whether to enable Rspack advanced ESM output.
Expand Down Expand Up @@ -473,6 +492,11 @@ export interface LibConfig extends EnvironmentConfig {
* @see {@link https://rslib.rs/config/lib/out-base}
*/
outBase?: string;
/**
* Configure how Rslib handles `.wasm` modules.
* @defaultValue `undefined`
*/
wasm?: Wasm;
/**
* @inheritdoc
*/
Expand Down
46 changes: 46 additions & 0 deletions packages/core/src/wasm/collect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { promises as fs } from 'node:fs';
import { type Rspack, rspack } from '@rsbuild/core';
import { computeEmitDistRelPath } from './path';

/**
* Build the preserved `.wasm` emit list from the module graph.
*
* Walks `compilation.modules` for preserved `ExternalModule` entries whose
* `userRequest` ends with `.wasm`, resolves each request using rspack's
* resolver (so aliases and tsconfig paths work correctly), computes the emit
* path, and returns a map of source path to emit path.
*/
export const collectExternalWasmSources = async (
compiler: Rspack.Compiler,
compilation: Rspack.Compilation,
outBase: string | null,
preserveToSource: boolean,
): Promise<Map<string, string>> => {
const found = new Map<string, string>();
const { moduleGraph } = compilation;
const resolver = compiler.resolverFactory.get('normal', { dependencyType: 'esm' });

for (const module of compilation.modules) {
if (!(module instanceof rspack.ExternalModule)) continue;
const { userRequest } = module;
if (!userRequest || !userRequest.endsWith('.wasm')) continue;
const issuer = moduleGraph.getIssuer(module);
const issuerContext = (issuer as { context?: string } | null)?.context;
if (!issuerContext) continue;

const resolved = resolver.resolveSync({}, issuerContext, userRequest);
if (!resolved) continue;

const emitDistRelPath = computeEmitDistRelPath({
bytes: await fs.readFile(resolved),
compiler,
compilation,
outBase,
preserveToSource,
sourcePath: resolved,
});

found.set(resolved, emitDistRelPath);
}
return found;
};
117 changes: 117 additions & 0 deletions packages/core/src/wasm/compose.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import type { EnvironmentConfig, Rspack } from '@rsbuild/core';
import type { Format, Wasm, WasmMode } from '../types';
import { logger } from '../utils/logger';
import { WasmPreserveEmitPlugin } from './emit';
import { createWasmPreserveExternal } from './external';
import {
PLUGIN_NAME,
type ResolvedWasmConfig,
type WasmCompilerContext,
} from './types';

export const resolveWasmConfig = ({
bundle,
format,
wasmConfig,
}: {
bundle: boolean;
format: Format;
wasmConfig?: Wasm;
}): ResolvedWasmConfig => {
const options = wasmConfig ?? {};
if (format !== 'esm') {
return {
mode: 'compile',
hasUserWasmMode: Boolean(options.mode),
};
}

const defaultMode: WasmMode = bundle ? 'compile' : 'preserve';
return {
mode: options.mode ?? defaultMode,
hasUserWasmMode: Boolean(options.mode),
};
};

export const composeWasmConfig = ({
bundle,
format,
mode,
hasUserWasmMode,
outBase,
}: {
bundle: boolean;
format: Format;
mode: WasmMode;
hasUserWasmMode: boolean;
outBase: string | null;
}): {
config: EnvironmentConfig;
bundlelessExternal?: Rspack.ExternalItem;
} => {
if (format !== 'esm') {
if (hasUserWasmMode) {
logger.warn(
`[${PLUGIN_NAME}] wasm.mode is only effective for "format: 'esm'", but format is "${format}". The option will be ignored.`,
);
}
return { config: {} };
}

if (mode === 'compile') {
return { config: {} };
}

const compilerContext: WasmCompilerContext = {};

// bundle + preserve emits content-hashed assets (the `outBase` is irrelevant
// since everything collapses into the entry chunk). bundleless + preserve
// keeps the original source layout and filename next to the emitted JS.
if (bundle) {
return {
config: {
output: {
externals: [
createWasmPreserveExternal({
compilerContext,
outBase: null,
preserveToSource: false,
}),
],
},
tools: {
rspack: {
plugins: [
new WasmPreserveEmitPlugin({
compilerContext,
outBase: null,
preserveToSource: false,
}),
],
},
},
},
};
}

return {
config: {
tools: {
rspack: {
plugins: [
new WasmPreserveEmitPlugin({
compilerContext,
outBase,
preserveToSource: true,
}),
],
},
},
},
bundlelessExternal: createWasmPreserveExternal({
compilerContext,
outBase,
preserveToSource: true,
}),
};
};
Loading