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
7 changes: 1 addition & 6 deletions crates/node_binding/napi-binding.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -978,7 +978,7 @@ export interface JsLoaderContext {
_module: Module
hot: Readonly<boolean>
/** Content maybe empty in pitching stage */
content: null | Buffer
content: string | Buffer | null
additionalData?: any
__internal__parseMeta: Record<string, string>
sourceMap?: Buffer
Expand All @@ -989,11 +989,6 @@ export interface JsLoaderContext {
loaderState: Readonly<JsLoaderState>
__internal__error?: RspackError
__internal__loaderCache?: JsLoaderCache | undefined
/**
* UTF-8 hint for `content`
* - Some(true): `content` is a `UTF-8` encoded sequence
*/
__internal__utf8Hint?: boolean
}

export interface JsLoaderDependencies {
Expand Down
6 changes: 3 additions & 3 deletions crates/rspack_binding_api/src/plugins/js_loader/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,12 @@ impl JsLoaderCache {
) -> napi::Result<Option<JsLoaderCacheEntry>> {
let loader = self.loader(loader_index)?;
let content = match content {
Either::A(content) => content.into_bytes(),
Either::B(content) => content.to_vec(),
Either::A(content) => Content::String(content),
Either::B(content) => Content::Buffer(content.to_vec()),
};
let existing: LoaderDependencies = existing.into();
let etag = loader_cache_etag(
&Content::Buffer(content),
&content,
&existing,
&loader.options_cache_key,
&loader.loader_version,
Expand Down
15 changes: 5 additions & 10 deletions crates/rspack_binding_api/src/plugins/js_loader/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::{ptr::NonNull, sync::Arc};
use napi::bindgen_prelude::*;
use napi_derive::napi;
use rspack_collections::Identifiable;
use rspack_core::{LoaderContext, LoaderDependencies, Module, RunnerContext};
use rspack_core::{Content, LoaderContext, LoaderDependencies, Module, RunnerContext};
use rspack_error::ToStringResultToRspackResultExt;
use rspack_loader_runner::State as LoaderState;
use rspack_napi::threadsafe_js_value_ref::ThreadsafeJsValueRef;
Expand Down Expand Up @@ -180,7 +180,7 @@ pub struct JsLoaderContext {
pub hot: bool,

/// Content maybe empty in pitching stage
pub content: Either<Null, Buffer>,
pub content: Either3<String, Buffer, Null>,
#[napi(ts_type = "any")]
pub additional_data: Option<ThreadsafeJsValueRef<Unknown<'static>>>,
#[napi(js_name = "__internal__parseMeta")]
Expand All @@ -200,11 +200,6 @@ pub struct JsLoaderContext {
ts_type = "JsLoaderCache | undefined"
)]
pub loader_cache: Option<JsLoaderCacheObject>,

/// UTF-8 hint for `content`
/// - Some(true): `content` is a `UTF-8` encoded sequence
#[napi(js_name = "__internal__utf8Hint")]
pub utf8_hint: Option<bool>,
}

impl TryFrom<&mut LoaderContext<RunnerContext>> for JsLoaderContext {
Expand All @@ -229,8 +224,9 @@ impl TryFrom<&mut LoaderContext<RunnerContext>> for JsLoaderContext {
),
hot: cx.hot,
content: match cx.content() {
Some(c) => Either::B(c.to_owned().into_bytes().into()),
None => Either::A(Null),
Some(Content::String(content)) => Either3::A(content.clone()),
Comment thread
intellild marked this conversation as resolved.
Some(Content::Buffer(content)) => Either3::B(content.clone().into()),
None => Either3::C(Null),
},
// Since js side only set parse meta, and can't read it, so we can use Default here to only bring the
// set values from js side to rust side.
Expand Down Expand Up @@ -265,7 +261,6 @@ impl TryFrom<&mut LoaderContext<RunnerContext>> for JsLoaderContext {
.collect(),
)
}),
utf8_hint: None,
})
}
}
21 changes: 4 additions & 17 deletions crates/rspack_binding_api/src/plugins/js_loader/scheduler.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use napi::{Either, bindgen_prelude::JsValuesTupleIntoVec};
use napi::bindgen_prelude::{Either3, JsValuesTupleIntoVec};
use rspack_core::{
AdditionalData, BUILTIN_LOADER_PREFIX, LoaderContext, NormalModuleLoaderShouldYield,
NormalModuleLoaderStartYielding, RunnerContext,
Expand Down Expand Up @@ -98,22 +98,9 @@ pub(crate) fn merge_loader_context(
}

let content = match from.content {
Either::A(_) => None,
Either::B(c) => {
// perf: Ignore UTF-8 check when JavaScript passed in an UTF-8 encoded value
let content = if let Some(utf8_hint) = from.utf8_hint
&& utf8_hint
{
rspack_core::Content::from(
// SAFETY: UTF-8 passed from JavaScript loader runner should ensure it does not pass non-UTF-8 encoded sequence when `utf_hint` is set to `true`. This invariant should be followed on the JavaScript side.
unsafe { String::from_utf8_unchecked(c.into()) },
)
} else {
rspack_core::Content::from(Into::<Vec<u8>>::into(c))
};

Some(content)
}
Either3::A(content) => Some(rspack_core::Content::String(content)),
Either3::B(content) => Some(rspack_core::Content::Buffer(content.into())),
Either3::C(_) => None,
};
let source_map = from
.source_map
Expand Down
2 changes: 2 additions & 0 deletions crates/rspack_core/src/loader/loader_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ pub fn loader_cache_etag(
// Context and missing dependencies intentionally invalidate the minimal cache: inherited values
// disable lookup, and entries that add either kind are skipped at store time. This trade-off lets
// the etag omit both kinds entirely.
// Equal bytes are not equivalent inputs: non-raw JS loaders strip a BOM only from buffers.
rspack_hash::rspack_hash_object!(&mut hasher, {
"content_is_string" => !content.is_buffer(),
"content" => content,
"file_dependencies" => sorted_dependency_paths(&existing.file),
"build_dependencies" => sorted_dependency_paths(&existing.build),
Expand Down
14 changes: 8 additions & 6 deletions crates/rspack_core/src/loader/rspack_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,14 @@ impl LoaderRunnerPlugin for RspackLoaderRunnerPlugin {
.map(|deps| deps.into_iter().map(Into::into).collect())
.unwrap_or_default();

// Return the content with source map extracted and file dependencies
return Ok(Some((
Content::String(extract_result.source),
extract_result.source_map,
file_deps,
)));
// Preserve the input type: non-raw JS loaders strip a BOM only from buffers.
let content = if content.is_buffer() {
Content::Buffer(extract_result.source.into_bytes())
} else {
Content::String(extract_result.source)
};

return Ok(Some((content, extract_result.source_map, file_deps)));
}
Err(e) => {
// If extraction fails, return original content with empty dependencies
Expand Down
13 changes: 10 additions & 3 deletions packages/rspack/src/loader-runner/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1133,7 +1133,11 @@ export async function runLoaders(

if (hasArg) {
const [content, sourceMap, additionalData] = args;
context.content = isNil(content) ? null : toBuffer(content);
context.content = isNil(content)
? null
: typeof content === 'string'
? content
: toBuffer(content);
context.sourceMap = serializeObject(sourceMap);
context.additionalData = additionalData || undefined;
break;
Expand Down Expand Up @@ -1216,12 +1220,15 @@ export async function runLoaders(
}
}

context.content = isNil(content) ? null : toBuffer(content);
context.content = isNil(content)
? null
: typeof content === 'string'
? content
: toBuffer(content);
context.sourceMap = sourceMapParsed
? JsSourceMap.__to_binding(sourceMap)
: rawSourceMap;
context.additionalData = additionalData || undefined;
context.__internal__utf8Hint = typeof content === 'string';

break;
}
Expand Down
30 changes: 30 additions & 0 deletions tests/rspack-test/configCases/loader/utf8-hint-bom/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
it("should preserve a string BOM across loader boundaries", () => {
expect(require("./input.js?string-normal")).toBe("\ufeffhello");
});

it("should strip a Buffer BOM when converting to a string", () => {
expect(require("./input.js?buffer-normal")).toBe("hello");
});

it("should preserve BOM bytes for raw loaders", () => {
expect(require("./input.js?string-raw")).toBe("efbbbf68656c6c6f");
expect(require("./input.js?buffer-raw")).toBe("efbbbf68656c6c6f");
});

it("should strip a resource BOM for normal loaders without a source map", () => {
expect(require("./resource.txt?resource-plain-normal")).toBe("hello\n");
expect(require("./resource.txt?resource-extract-normal")).toBe("hello\n");
});

it("should preserve resource BOM bytes for raw loaders without a source map", () => {
expect(require("./resource.txt?resource-plain-raw")).toBe("efbbbf68656c6c6f0a");
expect(require("./resource.txt?resource-extract-raw")).toBe("efbbbf68656c6c6f0a");
});

it("should strip a resource BOM for normal loaders after extracting a source map", () => {
expect(require("./resource-with-map.txt?resource-extract-normal")).toBe("hello\n");
});

it("should preserve resource BOM bytes for raw loaders after extracting a source map", () => {
expect(require("./resource-with-map.txt?resource-extract-raw")).toBe("efbbbf68656c6c6f0a");
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// Replaced by producer-loader.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = function (content) {
return `module.exports = ${JSON.stringify(content)};`;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = function () {
throw new Error("A pitch result should skip the producer normal function");
};

module.exports.pitch = require("./producer-loader");
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = function () {
const content = "\ufeffhello";
return this.getOptions().kind === "string" ? content : Buffer.from(content);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = function (content) {
if (!Buffer.isBuffer(content)) throw new Error("Expected a Buffer");
return `module.exports = ${JSON.stringify(content.toString("hex"))};`;
};
module.exports.raw = true;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
hello
//# sourceMappingURL=resource.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hello
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
module.exports = [false, true].flatMap((pitch) =>
[false, true].flatMap((parallel) =>
[false, true].map((mixed) => ({
module: {
rules: [
...['string', 'buffer'].flatMap((kind) =>
[false, true].map((raw) => ({
resourceQuery: new RegExp(
`^\\?${kind}-${raw ? 'raw' : 'normal'}$`,
),
use: [
{
loader: require.resolve(
raw ? './raw-loader' : './normal-loader',
),
options: {},
parallel: parallel ? { maxWorkers: 1 } : false,
},
...(mixed ? ['builtin:test-passthrough-loader'] : []),
{
loader: require.resolve(
pitch ? './pitch-loader' : './producer-loader',
),
options: { kind },
parallel: parallel ? { maxWorkers: 1 } : false,
},
],
})),
),
...[false, true].flatMap((extractSourceMap) =>
[false, true].map((raw) => ({
resourceQuery: new RegExp(
`^\\?resource-${extractSourceMap ? 'extract' : 'plain'}-${raw ? 'raw' : 'normal'}$`,
),
type: 'javascript/auto',
extractSourceMap,
use: [
{
loader: require.resolve(
raw ? './raw-loader' : './normal-loader',
),
options: {},
parallel: parallel ? { maxWorkers: 1 } : false,
},
...(mixed ? ['builtin:test-passthrough-loader'] : []),
],
})),
),
],
},
})),
),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const stringFirst = require("./input.txt?string-first");
const bufferFirst = require("./input.txt?buffer-first");

it("should distinguish equal bytes with different content types in the loader cache", () => {
const step = +WATCH_STEP;
const runs = LOADER_CACHE_ENABLED ? (step < 2 ? 1 : 2) : step + 1;
expect(stringFirst).toEqual({
content: step < 2 ? "\uFEFFhello" : "hello",
runs,
});
expect(bufferFirst).toEqual({
content: step < 2 ? "hello" : "\uFEFFhello",
runs,
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const runs = new Map();

module.exports = function (source) {
const key = `${this.getOptions().name}:${this.resource}`;
const count = (runs.get(key) || 0) + 1;
runs.set(key, count);
return `module.exports = ${JSON.stringify({ content: source, runs: count })};`;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = function (source) {
const firstType = this.resourceQuery === "?string-first";
const isString = Number(source.trim()) < 2 ? firstType : !firstType;
const content = "\uFEFFhello";
return isString ? content : Buffer.from(content);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const { rspack } = require('@rspack/core');

module.exports = [false, true].flatMap((cache) =>
[false, true].flatMap((parallel) =>
[false, true].map((mixed) => ({
mode: 'development',
output: {
filename: `bundle-${cache}-${parallel}-${mixed}.js`,
},
incremental: false,
cache: cache ? { type: 'memory' } : false,
experiments: {
newCache: {
codeGeneration: false,
loader: cache,
minimize: false,
},
},
module: {
rules: [
{
test: /input\.txt$/,
type: 'javascript/auto',
use: [
{
loader: require.resolve('./consumer-loader'),
options: { name: `${cache}-${parallel}-${mixed}` },
cache,
parallel: parallel ? { maxWorkers: 1 } : false,
},
...(mixed
? [{ loader: 'builtin:test-passthrough-loader', cache }]
: []),
{ loader: require.resolve('./producer-loader') },
],
},
],
},
plugins: [new rspack.DefinePlugin({ LOADER_CACHE_ENABLED: cache })],
})),
),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
findBundle(_index, options) {
return options.output.filename;
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = () => !process.env.WASM;
Loading