Skip to content

Commit 79b921f

Browse files
authored
feat: support custom parser for json type (#8947)
* feat: support custom parser for `json` type * fix: fixed the code review problems * feat: fix cr issues * feat: api-extractor
1 parent c3f2082 commit 79b921f

File tree

16 files changed

+157
-53
lines changed

16 files changed

+157
-53
lines changed

crates/node_binding/binding.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1581,6 +1581,7 @@ export interface RawJavascriptParserOptions {
15811581

15821582
export interface RawJsonParserOptions {
15831583
exportsDepth?: number
1584+
parse?: (source: string) => string
15841585
}
15851586

15861587
export interface RawLazyCompilationOption {

crates/rspack/src/builder/mod.rs

+4-3
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ use rspack_core::{
4444
JavascriptParserOptions, JavascriptParserOrder, JavascriptParserUrl, JsonParserOptions,
4545
LibraryName, LibraryNonUmdObject, LibraryOptions, LibraryType, MangleExportsOption, Mode,
4646
ModuleNoParseRules, ModuleOptions, ModuleRule, ModuleRuleEffect, Optimization, OutputOptions,
47-
ParserOptions, ParserOptionsMap, PathInfo, PublicPath, Resolve, RspackFuture, RuleSetCondition,
48-
RuleSetLogicalConditions, SideEffectOption, TrustedTypes, UsedExportsOption, WasmLoading,
49-
WasmLoadingType,
47+
ParseOption, ParserOptions, ParserOptionsMap, PathInfo, PublicPath, Resolve, RspackFuture,
48+
RuleSetCondition, RuleSetLogicalConditions, SideEffectOption, TrustedTypes, UsedExportsOption,
49+
WasmLoading, WasmLoadingType,
5050
};
5151
use rspack_hash::{HashDigest, HashFunction, HashSalt};
5252
use rspack_paths::{AssertUtf8, Utf8PathBuf};
@@ -744,6 +744,7 @@ impl ModuleOptionsBuilder {
744744
} else {
745745
Some(u32::MAX)
746746
},
747+
parse: ParseOption::None,
747748
}),
748749
);
749750
}

crates/rspack_binding_values/src/raw_options/raw_module/mod.rs

+11-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use rspack_core::{
1515
JavascriptParserOptions, JavascriptParserOrder, JavascriptParserUrl, JsonParserOptions,
1616
ModuleNoParseRule, ModuleNoParseRules, ModuleNoParseTestFn, ModuleOptions, ModuleRule,
1717
ModuleRuleEffect, ModuleRuleEnforce, ModuleRuleUse, ModuleRuleUseLoader, OverrideStrict,
18-
ParserOptions, ParserOptionsMap,
18+
ParseOption, ParserOptions, ParserOptionsMap,
1919
};
2020
use rspack_error::error;
2121
use rspack_napi::threadsafe_function::ThreadsafeFunction;
@@ -188,7 +188,7 @@ pub struct RawModuleRule {
188188
}
189189

190190
#[derive(Debug, Default)]
191-
#[napi(object)]
191+
#[napi(object, object_to_js = false)]
192192
pub struct RawParserOptions {
193193
#[napi(
194194
ts_type = r#""asset" | "css" | "css/auto" | "css/module" | "javascript" | "javascript/auto" | "javascript/dynamic" | "javascript/esm" | "json""#
@@ -441,15 +441,23 @@ impl From<RawCssModuleParserOptions> for CssModuleParserOptions {
441441
}
442442

443443
#[derive(Debug, Default)]
444-
#[napi(object)]
444+
#[napi(object, object_to_js = false)]
445445
pub struct RawJsonParserOptions {
446446
pub exports_depth: Option<u32>,
447+
#[napi(ts_type = "(source: string) => string")]
448+
pub parse: Option<ThreadsafeFunction<String, String>>,
447449
}
448450

449451
impl From<RawJsonParserOptions> for JsonParserOptions {
450452
fn from(value: RawJsonParserOptions) -> Self {
453+
let parse = match value.parse {
454+
Some(f) => ParseOption::Func(Arc::new(move |s: String| f.blocking_call_with_sync(s))),
455+
_ => ParseOption::None,
456+
};
457+
451458
Self {
452459
exports_depth: value.exports_depth,
460+
parse,
453461
}
454462
}
455463
}

crates/rspack_core/src/normal_module_factory.rs

+1
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,7 @@ impl NormalModuleFactory {
738738
| ParserOptions::JavascriptDynamic(b)
739739
| ParserOptions::JavascriptEsm(b),
740740
) => ParserOptions::Javascript(a.merge_from(b)),
741+
(ParserOptions::Json(a), ParserOptions::Json(b)) => ParserOptions::Json(a.merge_from(b)),
741742
(global, _) => global,
742743
},
743744
);

crates/rspack_core/src/options/module.rs

+33
Original file line numberDiff line numberDiff line change
@@ -306,10 +306,43 @@ pub struct CssModuleParserOptions {
306306
pub named_exports: Option<bool>,
307307
}
308308

309+
pub type JsonParseFn = Arc<dyn Fn(String) -> Result<String> + Sync + Send>;
310+
311+
#[cacheable]
312+
pub enum ParseOption {
313+
Func(#[cacheable(with=Unsupported)] JsonParseFn),
314+
None,
315+
}
316+
317+
impl Debug for ParseOption {
318+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
319+
match self {
320+
Self::Func(_) => write!(f, "ParseOption::Func(...)"),
321+
_ => write!(f, "ParseOption::None"),
322+
}
323+
}
324+
}
325+
326+
impl Clone for ParseOption {
327+
fn clone(&self) -> Self {
328+
match self {
329+
Self::Func(f) => Self::Func(f.clone()),
330+
Self::None => Self::None,
331+
}
332+
}
333+
}
334+
335+
impl MergeFrom for ParseOption {
336+
fn merge_from(self, other: &Self) -> Self {
337+
other.clone()
338+
}
339+
}
340+
309341
#[cacheable]
310342
#[derive(Debug, Clone, MergeFrom)]
311343
pub struct JsonParserOptions {
312344
pub exports_depth: Option<u32>,
345+
pub parse: ParseOption,
313346
}
314347

315348
#[derive(Debug, Default)]

crates/rspack_plugin_json/src/lib.rs

+58-42
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ use rspack_core::{
1616
diagnostics::ModuleParseError,
1717
rspack_sources::{BoxSource, RawStringSource, Source, SourceExt},
1818
BuildMetaDefaultObject, BuildMetaExportsType, ChunkGraph, CompilerOptions, ExportsInfo,
19-
GenerateContext, Module, ModuleGraph, ParserAndGenerator, Plugin, RuntimeGlobals, RuntimeSpec,
20-
SourceType, UsageState, NAMESPACE_OBJECT_EXPORT,
19+
GenerateContext, Module, ModuleGraph, ParseOption, ParserAndGenerator, Plugin, RuntimeGlobals,
20+
RuntimeSpec, SourceType, UsageState, NAMESPACE_OBJECT_EXPORT,
2121
};
2222
use rspack_error::{
2323
miette::diagnostic, DiagnosticExt, DiagnosticKind, IntoTWithDiagnosticArray, Result,
@@ -34,6 +34,7 @@ mod utils;
3434
#[derive(Debug)]
3535
struct JsonParserAndGenerator {
3636
pub exports_depth: u32,
37+
pub parse: ParseOption,
3738
}
3839

3940
#[cacheable_dyn]
@@ -55,54 +56,68 @@ impl ParserAndGenerator for JsonParserAndGenerator {
5556
build_info,
5657
build_meta,
5758
loaders,
59+
module_parser_options,
5860
..
5961
} = parse_context;
6062
let source = box_source.source();
6163
let strip_bom_source = source.strip_prefix('\u{feff}');
6264
let need_strip_bom = strip_bom_source.is_some();
65+
let strip_bom_source = strip_bom_source.unwrap_or(&source);
6366

64-
let parse_result = json::parse(strip_bom_source.unwrap_or(&source)).map_err(|e| {
65-
match e {
66-
UnexpectedCharacter { ch, line, column } => {
67-
let rope = ropey::Rope::from_str(&source);
68-
let line_offset = rope.try_line_to_byte(line - 1).expect("TODO:");
69-
let start_offset = source[line_offset..]
70-
.chars()
71-
.take(column)
72-
.fold(line_offset, |acc, cur| acc + cur.len_utf8());
73-
let start_offset = if need_strip_bom {
74-
start_offset + 1
75-
} else {
76-
start_offset
77-
};
78-
TraceableError::from_file(
79-
source.into_owned(),
80-
// one character offset
81-
start_offset,
82-
start_offset + 1,
83-
"Json parsing error".to_string(),
84-
format!("Unexpected character {ch}"),
85-
)
86-
.with_kind(DiagnosticKind::Json)
87-
.boxed()
67+
// If there is a custom parse, execute it to obtain the returned string.
68+
let parse_result_str = module_parser_options
69+
.and_then(|p| p.get_json())
70+
.and_then(|p| match &p.parse {
71+
ParseOption::Func(p) => {
72+
let parse_result = p(strip_bom_source.to_string());
73+
parse_result.ok()
8874
}
89-
ExceededDepthLimit | WrongType(_) | FailedUtf8Parsing => diagnostic!("{e}").boxed(),
90-
UnexpectedEndOfJson => {
91-
// End offset of json file
92-
let length = source.len();
93-
let offset = if length > 0 { length - 1 } else { length };
94-
TraceableError::from_file(
95-
source.into_owned(),
96-
offset,
97-
offset,
98-
"Json parsing error".to_string(),
99-
format!("{e}"),
100-
)
101-
.with_kind(DiagnosticKind::Json)
102-
.boxed()
75+
_ => None,
76+
});
77+
78+
let parse_result = json::parse(parse_result_str.as_deref().unwrap_or(strip_bom_source))
79+
.map_err(|e| {
80+
match e {
81+
UnexpectedCharacter { ch, line, column } => {
82+
let rope = ropey::Rope::from_str(&source);
83+
let line_offset = rope.try_line_to_byte(line - 1).expect("TODO:");
84+
let start_offset = source[line_offset..]
85+
.chars()
86+
.take(column)
87+
.fold(line_offset, |acc, cur| acc + cur.len_utf8());
88+
let start_offset = if need_strip_bom {
89+
start_offset + 1
90+
} else {
91+
start_offset
92+
};
93+
TraceableError::from_file(
94+
source.into_owned(),
95+
// one character offset
96+
start_offset,
97+
start_offset + 1,
98+
"Json parsing error".to_string(),
99+
format!("Unexpected character {ch}"),
100+
)
101+
.with_kind(DiagnosticKind::Json)
102+
.boxed()
103+
}
104+
ExceededDepthLimit | WrongType(_) | FailedUtf8Parsing => diagnostic!("{e}").boxed(),
105+
UnexpectedEndOfJson => {
106+
// End offset of json file
107+
let length = source.len();
108+
let offset = if length > 0 { length - 1 } else { length };
109+
TraceableError::from_file(
110+
source.into_owned(),
111+
offset,
112+
offset,
113+
"Json parsing error".to_string(),
114+
format!("{e}"),
115+
)
116+
.with_kind(DiagnosticKind::Json)
117+
.boxed()
118+
}
103119
}
104-
}
105-
});
120+
});
106121

107122
let (diagnostics, data) = match parse_result {
108123
Ok(data) => (vec![], Some(data)),
@@ -236,6 +251,7 @@ impl Plugin for JsonPlugin {
236251

237252
Box::new(JsonParserAndGenerator {
238253
exports_depth: p.exports_depth.expect("should have exports_depth"),
254+
parse: p.parse.clone(),
239255
})
240256
}),
241257
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
foo = 'foo'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import * as json from './data.toml';
2+
3+
it('should use custom parse function', () => {
4+
expect(json.foo).toBe('bar');
5+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/** @type {import("@rspack/core").Configuration} */
2+
module.exports = {
3+
module: {
4+
rules: [
5+
{
6+
test: /\.toml$/,
7+
type: 'json',
8+
parser: {
9+
parse: () => ({ foo: 'bar' })
10+
}
11+
}
12+
]
13+
}
14+
}

packages/rspack/etc/core.api.md

+1
Original file line numberDiff line numberDiff line change
@@ -2754,6 +2754,7 @@ type JsonObject_2 = {
27542754
// @public (undocumented)
27552755
export type JsonParserOptions = {
27562756
exportsDepth?: number;
2757+
parse?: (source: string) => any;
27572758
};
27582759

27592760
// @public (undocumented)

packages/rspack/src/config/adapter.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,11 @@ function getRawJsonParserOptions(
581581
parser: JsonParserOptions
582582
): RawJsonParserOptions {
583583
return {
584-
exportsDepth: parser.exportsDepth
584+
exportsDepth: parser.exportsDepth,
585+
parse:
586+
typeof parser.parse === "function"
587+
? str => JSON.stringify(parser.parse!(str))
588+
: undefined
585589
};
586590
}
587591

packages/rspack/src/config/types.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1075,6 +1075,10 @@ export type JsonParserOptions = {
10751075
* The depth of json dependency flagged as `exportInfo`.
10761076
*/
10771077
exportsDepth?: number;
1078+
/**
1079+
* 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.
1080+
*/
1081+
parse?: (source: string) => any;
10781082
};
10791083

10801084
/** Configure all parsers' options in one place with module.parser. */

pnpm-lock.yaml

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/webpack-test/__snapshots__/ConfigTestCases.basictest.js.snap

+13
Original file line numberDiff line numberDiff line change
@@ -415,3 +415,16 @@ Object {
415415
"placeholder": "-_6d72b53b84605386-placeholder-gray-700",
416416
}
417417
`;
418+
419+
exports[`ConfigTestCases custom-modules json-custom exported tests should transform toml to json 1`] = `
420+
Object {
421+
"owner": Object {
422+
"bio": "GitHub Cofounder & CEO
423+
Likes tater tots and beer.",
424+
"dob": "1979-05-27T07:32:00.000Z",
425+
"name": "Tom Preston-Werner",
426+
"organization": "GitHub",
427+
},
428+
"title": "TOML Example",
429+
}
430+
`;

tests/webpack-test/configCases/custom-modules/json-custom/test.filter.js

-2
This file was deleted.

tests/webpack-test/package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"tapable": "2.2.1",
6565
"wast-loader": "^1.12.1",
6666
"watchpack": "^2.4.0",
67-
"webpack-sources": "3.2.3"
67+
"webpack-sources": "3.2.3",
68+
"toml": "^3.0.0"
6869
}
69-
}
70+
}

0 commit comments

Comments
 (0)