diff --git a/crates/node_binding/binding.d.ts b/crates/node_binding/binding.d.ts index d6d261b852b..63dbb845231 100644 --- a/crates/node_binding/binding.d.ts +++ b/crates/node_binding/binding.d.ts @@ -1581,6 +1581,7 @@ export interface RawJavascriptParserOptions { export interface RawJsonParserOptions { exportsDepth?: number + parse?: (source: string) => string } export interface RawLazyCompilationOption { diff --git a/crates/rspack/src/builder/mod.rs b/crates/rspack/src/builder/mod.rs index 63d5220c30c..e0ebff9ed26 100644 --- a/crates/rspack/src/builder/mod.rs +++ b/crates/rspack/src/builder/mod.rs @@ -44,9 +44,9 @@ use rspack_core::{ JavascriptParserOptions, JavascriptParserOrder, JavascriptParserUrl, JsonParserOptions, LibraryName, LibraryNonUmdObject, LibraryOptions, LibraryType, MangleExportsOption, Mode, ModuleNoParseRules, ModuleOptions, ModuleRule, ModuleRuleEffect, Optimization, OutputOptions, - ParserOptions, ParserOptionsMap, PathInfo, PublicPath, Resolve, RspackFuture, RuleSetCondition, - RuleSetLogicalConditions, SideEffectOption, TrustedTypes, UsedExportsOption, WasmLoading, - WasmLoadingType, + ParseOption, ParserOptions, ParserOptionsMap, PathInfo, PublicPath, Resolve, RspackFuture, + RuleSetCondition, RuleSetLogicalConditions, SideEffectOption, TrustedTypes, UsedExportsOption, + WasmLoading, WasmLoadingType, }; use rspack_hash::{HashDigest, HashFunction, HashSalt}; use rspack_paths::{AssertUtf8, Utf8PathBuf}; @@ -744,6 +744,7 @@ impl ModuleOptionsBuilder { } else { Some(u32::MAX) }, + parse: ParseOption::None, }), ); } diff --git a/crates/rspack_binding_values/src/raw_options/raw_module/mod.rs b/crates/rspack_binding_values/src/raw_options/raw_module/mod.rs index 785f12b274a..66f891feefa 100644 --- a/crates/rspack_binding_values/src/raw_options/raw_module/mod.rs +++ b/crates/rspack_binding_values/src/raw_options/raw_module/mod.rs @@ -15,7 +15,7 @@ use rspack_core::{ JavascriptParserOptions, JavascriptParserOrder, JavascriptParserUrl, JsonParserOptions, ModuleNoParseRule, ModuleNoParseRules, ModuleNoParseTestFn, ModuleOptions, ModuleRule, ModuleRuleEffect, ModuleRuleEnforce, ModuleRuleUse, ModuleRuleUseLoader, OverrideStrict, - ParserOptions, ParserOptionsMap, + ParseOption, ParserOptions, ParserOptionsMap, }; use rspack_error::error; use rspack_napi::threadsafe_function::ThreadsafeFunction; @@ -188,7 +188,7 @@ pub struct RawModuleRule { } #[derive(Debug, Default)] -#[napi(object)] +#[napi(object, object_to_js = false)] pub struct RawParserOptions { #[napi( ts_type = r#""asset" | "css" | "css/auto" | "css/module" | "javascript" | "javascript/auto" | "javascript/dynamic" | "javascript/esm" | "json""# @@ -441,15 +441,23 @@ impl From for CssModuleParserOptions { } #[derive(Debug, Default)] -#[napi(object)] +#[napi(object, object_to_js = false)] pub struct RawJsonParserOptions { pub exports_depth: Option, + #[napi(ts_type = "(source: string) => string")] + pub parse: Option>, } impl From for JsonParserOptions { fn from(value: RawJsonParserOptions) -> Self { + let parse = match value.parse { + Some(f) => ParseOption::Func(Arc::new(move |s: String| f.blocking_call_with_sync(s))), + _ => ParseOption::None, + }; + Self { exports_depth: value.exports_depth, + parse, } } } diff --git a/crates/rspack_core/src/normal_module_factory.rs b/crates/rspack_core/src/normal_module_factory.rs index 60ff0f7e479..6ace788e758 100644 --- a/crates/rspack_core/src/normal_module_factory.rs +++ b/crates/rspack_core/src/normal_module_factory.rs @@ -738,6 +738,7 @@ impl NormalModuleFactory { | ParserOptions::JavascriptDynamic(b) | ParserOptions::JavascriptEsm(b), ) => ParserOptions::Javascript(a.merge_from(b)), + (ParserOptions::Json(a), ParserOptions::Json(b)) => ParserOptions::Json(a.merge_from(b)), (global, _) => global, }, ); diff --git a/crates/rspack_core/src/options/module.rs b/crates/rspack_core/src/options/module.rs index e19f7b3d1e0..6ed1fd4bc9f 100644 --- a/crates/rspack_core/src/options/module.rs +++ b/crates/rspack_core/src/options/module.rs @@ -306,10 +306,43 @@ pub struct CssModuleParserOptions { pub named_exports: Option, } +pub type JsonParseFn = Arc Result + Sync + Send>; + +#[cacheable] +pub enum ParseOption { + Func(#[cacheable(with=Unsupported)] JsonParseFn), + None, +} + +impl Debug for ParseOption { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Func(_) => write!(f, "ParseOption::Func(...)"), + _ => write!(f, "ParseOption::None"), + } + } +} + +impl Clone for ParseOption { + fn clone(&self) -> Self { + match self { + Self::Func(f) => Self::Func(f.clone()), + Self::None => Self::None, + } + } +} + +impl MergeFrom for ParseOption { + fn merge_from(self, other: &Self) -> Self { + other.clone() + } +} + #[cacheable] #[derive(Debug, Clone, MergeFrom)] pub struct JsonParserOptions { pub exports_depth: Option, + pub parse: ParseOption, } #[derive(Debug, Default)] diff --git a/crates/rspack_plugin_json/src/lib.rs b/crates/rspack_plugin_json/src/lib.rs index 3b2408b2a4c..8a387f1f676 100644 --- a/crates/rspack_plugin_json/src/lib.rs +++ b/crates/rspack_plugin_json/src/lib.rs @@ -16,8 +16,8 @@ use rspack_core::{ diagnostics::ModuleParseError, rspack_sources::{BoxSource, RawStringSource, Source, SourceExt}, BuildMetaDefaultObject, BuildMetaExportsType, ChunkGraph, CompilerOptions, ExportsInfo, - GenerateContext, Module, ModuleGraph, ParserAndGenerator, Plugin, RuntimeGlobals, RuntimeSpec, - SourceType, UsageState, NAMESPACE_OBJECT_EXPORT, + GenerateContext, Module, ModuleGraph, ParseOption, ParserAndGenerator, Plugin, RuntimeGlobals, + RuntimeSpec, SourceType, UsageState, NAMESPACE_OBJECT_EXPORT, }; use rspack_error::{ miette::diagnostic, DiagnosticExt, DiagnosticKind, IntoTWithDiagnosticArray, Result, @@ -34,6 +34,7 @@ mod utils; #[derive(Debug)] struct JsonParserAndGenerator { pub exports_depth: u32, + pub parse: ParseOption, } #[cacheable_dyn] @@ -55,54 +56,68 @@ impl ParserAndGenerator for JsonParserAndGenerator { build_info, build_meta, loaders, + module_parser_options, .. } = parse_context; let source = box_source.source(); let strip_bom_source = source.strip_prefix('\u{feff}'); let need_strip_bom = strip_bom_source.is_some(); + let strip_bom_source = strip_bom_source.unwrap_or(&source); - let parse_result = json::parse(strip_bom_source.unwrap_or(&source)).map_err(|e| { - match e { - UnexpectedCharacter { ch, line, column } => { - let rope = ropey::Rope::from_str(&source); - let line_offset = rope.try_line_to_byte(line - 1).expect("TODO:"); - let start_offset = source[line_offset..] - .chars() - .take(column) - .fold(line_offset, |acc, cur| acc + cur.len_utf8()); - let start_offset = if need_strip_bom { - start_offset + 1 - } else { - start_offset - }; - TraceableError::from_file( - source.into_owned(), - // one character offset - start_offset, - start_offset + 1, - "Json parsing error".to_string(), - format!("Unexpected character {ch}"), - ) - .with_kind(DiagnosticKind::Json) - .boxed() + // If there is a custom parse, execute it to obtain the returned string. + let parse_result_str = module_parser_options + .and_then(|p| p.get_json()) + .and_then(|p| match &p.parse { + ParseOption::Func(p) => { + let parse_result = p(strip_bom_source.to_string()); + parse_result.ok() } - ExceededDepthLimit | WrongType(_) | FailedUtf8Parsing => diagnostic!("{e}").boxed(), - UnexpectedEndOfJson => { - // End offset of json file - let length = source.len(); - let offset = if length > 0 { length - 1 } else { length }; - TraceableError::from_file( - source.into_owned(), - offset, - offset, - "Json parsing error".to_string(), - format!("{e}"), - ) - .with_kind(DiagnosticKind::Json) - .boxed() + _ => None, + }); + + let parse_result = json::parse(parse_result_str.as_deref().unwrap_or(strip_bom_source)) + .map_err(|e| { + match e { + UnexpectedCharacter { ch, line, column } => { + let rope = ropey::Rope::from_str(&source); + let line_offset = rope.try_line_to_byte(line - 1).expect("TODO:"); + let start_offset = source[line_offset..] + .chars() + .take(column) + .fold(line_offset, |acc, cur| acc + cur.len_utf8()); + let start_offset = if need_strip_bom { + start_offset + 1 + } else { + start_offset + }; + TraceableError::from_file( + source.into_owned(), + // one character offset + start_offset, + start_offset + 1, + "Json parsing error".to_string(), + format!("Unexpected character {ch}"), + ) + .with_kind(DiagnosticKind::Json) + .boxed() + } + ExceededDepthLimit | WrongType(_) | FailedUtf8Parsing => diagnostic!("{e}").boxed(), + UnexpectedEndOfJson => { + // End offset of json file + let length = source.len(); + let offset = if length > 0 { length - 1 } else { length }; + TraceableError::from_file( + source.into_owned(), + offset, + offset, + "Json parsing error".to_string(), + format!("{e}"), + ) + .with_kind(DiagnosticKind::Json) + .boxed() + } } - } - }); + }); let (diagnostics, data) = match parse_result { Ok(data) => (vec![], Some(data)), @@ -236,6 +251,7 @@ impl Plugin for JsonPlugin { Box::new(JsonParserAndGenerator { exports_depth: p.exports_depth.expect("should have exports_depth"), + parse: p.parse.clone(), }) }), ); diff --git a/packages/rspack-test-tools/tests/configCases/module/rspack-issue-8785/data.toml b/packages/rspack-test-tools/tests/configCases/module/rspack-issue-8785/data.toml new file mode 100644 index 00000000000..7d854c9e364 --- /dev/null +++ b/packages/rspack-test-tools/tests/configCases/module/rspack-issue-8785/data.toml @@ -0,0 +1 @@ +foo = 'foo' diff --git a/packages/rspack-test-tools/tests/configCases/module/rspack-issue-8785/index.js b/packages/rspack-test-tools/tests/configCases/module/rspack-issue-8785/index.js new file mode 100644 index 00000000000..2c94e2e3600 --- /dev/null +++ b/packages/rspack-test-tools/tests/configCases/module/rspack-issue-8785/index.js @@ -0,0 +1,5 @@ +import * as json from './data.toml'; + +it('should use custom parse function', () => { + expect(json.foo).toBe('bar'); +}); diff --git a/packages/rspack-test-tools/tests/configCases/module/rspack-issue-8785/rspack.config.js b/packages/rspack-test-tools/tests/configCases/module/rspack-issue-8785/rspack.config.js new file mode 100644 index 00000000000..c5e4ae58101 --- /dev/null +++ b/packages/rspack-test-tools/tests/configCases/module/rspack-issue-8785/rspack.config.js @@ -0,0 +1,14 @@ +/** @type {import("@rspack/core").Configuration} */ +module.exports = { + module: { + rules: [ + { + test: /\.toml$/, + type: 'json', + parser: { + parse: () => ({ foo: 'bar' }) + } + } + ] + } +} diff --git a/packages/rspack/etc/core.api.md b/packages/rspack/etc/core.api.md index ceca67c3eca..7cd4d39066e 100644 --- a/packages/rspack/etc/core.api.md +++ b/packages/rspack/etc/core.api.md @@ -2754,6 +2754,7 @@ type JsonObject_2 = { // @public (undocumented) export type JsonParserOptions = { exportsDepth?: number; + parse?: (source: string) => any; }; // @public (undocumented) diff --git a/packages/rspack/src/config/adapter.ts b/packages/rspack/src/config/adapter.ts index f41be2bbc48..d62f23c663b 100644 --- a/packages/rspack/src/config/adapter.ts +++ b/packages/rspack/src/config/adapter.ts @@ -581,7 +581,11 @@ function getRawJsonParserOptions( parser: JsonParserOptions ): RawJsonParserOptions { return { - exportsDepth: parser.exportsDepth + exportsDepth: parser.exportsDepth, + parse: + typeof parser.parse === "function" + ? str => JSON.stringify(parser.parse!(str)) + : undefined }; } diff --git a/packages/rspack/src/config/types.ts b/packages/rspack/src/config/types.ts index 52e4ae3aec0..3c928c65e00 100644 --- a/packages/rspack/src/config/types.ts +++ b/packages/rspack/src/config/types.ts @@ -1075,6 +1075,10 @@ export type JsonParserOptions = { * The depth of json dependency flagged as `exportInfo`. */ exportsDepth?: number; + /** + * If Rule.type is set to 'json' then Rules.parser.parse option may be a function that implements custom logic to parse module's source and convert it to a json-compatible data. + */ + parse?: (source: string) => any; }; /** Configure all parsers' options in one place with module.parser. */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 738c859e9f4..57d4c8156db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -925,6 +925,9 @@ importers: tapable: specifier: 2.2.1 version: 2.2.1 + toml: + specifier: ^3.0.0 + version: 3.0.0 wast-loader: specifier: ^1.12.1 version: 1.12.1 diff --git a/tests/webpack-test/__snapshots__/ConfigTestCases.basictest.js.snap b/tests/webpack-test/__snapshots__/ConfigTestCases.basictest.js.snap index 1b455833af3..7ddfd3e775a 100644 --- a/tests/webpack-test/__snapshots__/ConfigTestCases.basictest.js.snap +++ b/tests/webpack-test/__snapshots__/ConfigTestCases.basictest.js.snap @@ -415,3 +415,16 @@ Object { "placeholder": "-_6d72b53b84605386-placeholder-gray-700", } `; + +exports[`ConfigTestCases custom-modules json-custom exported tests should transform toml to json 1`] = ` +Object { + "owner": Object { + "bio": "GitHub Cofounder & CEO +Likes tater tots and beer.", + "dob": "1979-05-27T07:32:00.000Z", + "name": "Tom Preston-Werner", + "organization": "GitHub", + }, + "title": "TOML Example", +} +`; diff --git a/tests/webpack-test/configCases/custom-modules/json-custom/test.filter.js b/tests/webpack-test/configCases/custom-modules/json-custom/test.filter.js deleted file mode 100644 index 042493e42a5..00000000000 --- a/tests/webpack-test/configCases/custom-modules/json-custom/test.filter.js +++ /dev/null @@ -1,2 +0,0 @@ -// TODO: Should create a issue for this test -module.exports = () => { return false } diff --git a/tests/webpack-test/package.json b/tests/webpack-test/package.json index bdecff6b43b..ea062284a7e 100644 --- a/tests/webpack-test/package.json +++ b/tests/webpack-test/package.json @@ -64,6 +64,7 @@ "tapable": "2.2.1", "wast-loader": "^1.12.1", "watchpack": "^2.4.0", - "webpack-sources": "3.2.3" + "webpack-sources": "3.2.3", + "toml": "^3.0.0" } -} \ No newline at end of file +}