Skip to content

Commit

Permalink
feat: support custom parser for json type (web-infra-dev#8947)
Browse files Browse the repository at this point in the history
* feat: support custom parser for `json` type

* fix: fixed the code review problems

* feat: fix cr issues

* feat: api-extractor
  • Loading branch information
cbbfcd authored Jan 9, 2025
1 parent c3f2082 commit 79b921f
Show file tree
Hide file tree
Showing 16 changed files with 157 additions and 53 deletions.
1 change: 1 addition & 0 deletions crates/node_binding/binding.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1581,6 +1581,7 @@ export interface RawJavascriptParserOptions {

export interface RawJsonParserOptions {
exportsDepth?: number
parse?: (source: string) => string
}

export interface RawLazyCompilationOption {
Expand Down
7 changes: 4 additions & 3 deletions crates/rspack/src/builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -744,6 +744,7 @@ impl ModuleOptionsBuilder {
} else {
Some(u32::MAX)
},
parse: ParseOption::None,
}),
);
}
Expand Down
14 changes: 11 additions & 3 deletions crates/rspack_binding_values/src/raw_options/raw_module/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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""#
Expand Down Expand Up @@ -441,15 +441,23 @@ impl From<RawCssModuleParserOptions> for CssModuleParserOptions {
}

#[derive(Debug, Default)]
#[napi(object)]
#[napi(object, object_to_js = false)]
pub struct RawJsonParserOptions {
pub exports_depth: Option<u32>,
#[napi(ts_type = "(source: string) => string")]
pub parse: Option<ThreadsafeFunction<String, String>>,
}

impl From<RawJsonParserOptions> 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,
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/rspack_core/src/normal_module_factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
);
Expand Down
33 changes: 33 additions & 0 deletions crates/rspack_core/src/options/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,10 +306,43 @@ pub struct CssModuleParserOptions {
pub named_exports: Option<bool>,
}

pub type JsonParseFn = Arc<dyn Fn(String) -> Result<String> + 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<u32>,
pub parse: ParseOption,
}

#[derive(Debug, Default)]
Expand Down
100 changes: 58 additions & 42 deletions crates/rspack_plugin_json/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -34,6 +34,7 @@ mod utils;
#[derive(Debug)]
struct JsonParserAndGenerator {
pub exports_depth: u32,
pub parse: ParseOption,
}

#[cacheable_dyn]
Expand All @@ -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)),
Expand Down Expand Up @@ -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(),
})
}),
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
foo = 'foo'
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as json from './data.toml';

it('should use custom parse function', () => {
expect(json.foo).toBe('bar');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/** @type {import("@rspack/core").Configuration} */
module.exports = {
module: {
rules: [
{
test: /\.toml$/,
type: 'json',
parser: {
parse: () => ({ foo: 'bar' })
}
}
]
}
}
1 change: 1 addition & 0 deletions packages/rspack/etc/core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2754,6 +2754,7 @@ type JsonObject_2 = {
// @public (undocumented)
export type JsonParserOptions = {
exportsDepth?: number;
parse?: (source: string) => any;
};

// @public (undocumented)
Expand Down
6 changes: 5 additions & 1 deletion packages/rspack/src/config/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
}

Expand Down
4 changes: 4 additions & 0 deletions packages/rspack/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

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

13 changes: 13 additions & 0 deletions tests/webpack-test/__snapshots__/ConfigTestCases.basictest.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
`;

This file was deleted.

5 changes: 3 additions & 2 deletions tests/webpack-test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}

2 comments on commit 79b921f

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⏳ Triggered benchmark: Open

@github-actions
Copy link

@github-actions github-actions bot commented on 79b921f Jan 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Ecosystem CI detail: Open

suite result
modernjs ❌ failure
rspress ✅ success
rslib ✅ success
rsbuild ✅ success
rsdoctor ✅ success
examples ✅ success
devserver ✅ success
nuxt ✅ success

Please sign in to comment.