diff --git a/Cargo.lock b/Cargo.lock index 482cc17a0d7d..ff68c06d03ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2570,6 +2570,7 @@ dependencies = [ "hashlink", "indexmap 1.9.3", "itertools", + "json", "mime_guess", "nodejs-resolver", "once_cell", diff --git a/crates/rspack_core/Cargo.toml b/crates/rspack_core/Cargo.toml index 922bdab3baa0..2d1e3d511316 100644 --- a/crates/rspack_core/Cargo.toml +++ b/crates/rspack_core/Cargo.toml @@ -20,6 +20,7 @@ glob-match = "0.2.1" hashlink = { workspace = true } indexmap = { workspace = true } itertools = { workspace = true } +json = { workspace = true } mime_guess = { workspace = true } nodejs-resolver = { version = "0.1.1" } once_cell = { workspace = true } diff --git a/crates/rspack_core/src/exports_info.rs b/crates/rspack_core/src/exports_info.rs index 0e3b4c3c2cb9..42a2428ea57f 100644 --- a/crates/rspack_core/src/exports_info.rs +++ b/crates/rspack_core/src/exports_info.rs @@ -182,6 +182,8 @@ impl ExportsInfoId { changed } + /// # Panic + /// this function would panic if name doesn't exists in current exportsInfo pub fn get_read_only_export_info<'a>( &self, name: &JsWord, @@ -1364,7 +1366,6 @@ impl ExportInfo { ); let new_exports_info = ExportsInfo::new(other_exports_info.id, side_effects_only_info.id); let new_exports_info_id = new_exports_info.id; - mg.exports_info_map .insert(new_exports_info_id, new_exports_info); mg.export_info_map diff --git a/crates/rspack_core/src/module.rs b/crates/rspack_core/src/module.rs index 88bbbfb7623c..7bda5018f729 100644 --- a/crates/rspack_core/src/module.rs +++ b/crates/rspack_core/src/module.rs @@ -4,6 +4,7 @@ use std::path::PathBuf; use std::{any::Any, borrow::Cow, fmt::Debug}; use async_trait::async_trait; +use json::JsonValue; use rspack_error::{IntoTWithDiagnosticArray, Result, TWithDiagnosticArray}; use rspack_hash::{RspackHash, RspackHashDigest}; use rspack_identifier::{Identifiable, Identifier}; @@ -47,6 +48,7 @@ pub struct BuildInfo { pub harmony_named_exports: HashSet, pub all_star_exports: Vec, pub need_create_require: bool, + pub json_data: Option, } #[derive(Debug, Default, Clone, Hash, PartialEq, Eq)] diff --git a/crates/rspack_core/src/utils/visitor.rs b/crates/rspack_core/src/utils/visitor.rs index c1af8e246fde..2ef29ddccfd7 100644 --- a/crates/rspack_core/src/utils/visitor.rs +++ b/crates/rspack_core/src/utils/visitor.rs @@ -194,6 +194,12 @@ pub fn extract_member_expression_chain<'e, T: Into>>( }) = expr.prop { members.push_front((val.value.clone(), val.span.ctxt)); + } else if let MemberProp::Computed(ComputedPropName { + expr: box Expr::Lit(Lit::Num(ref val)), + .. + }) = expr.prop + { + members.push_front((val.value.to_string().into(), val.span.ctxt)); } else if let MemberProp::Ident(ref ident) = expr.prop { members.push_front((ident.sym.clone(), ident.span.ctxt)); members_spans.push_front(ident.span); diff --git a/crates/rspack_plugin_javascript/src/dependency/esm/harmony_import_specifier_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/esm/harmony_import_specifier_dependency.rs index 5432810fd003..fcbe6c179769 100644 --- a/crates/rspack_plugin_javascript/src/dependency/esm/harmony_import_specifier_dependency.rs +++ b/crates/rspack_plugin_javascript/src/dependency/esm/harmony_import_specifier_dependency.rs @@ -262,15 +262,7 @@ impl ModuleDependency for HarmonyImportSpecifierDependency { } fn get_condition(&self) -> Option { - // dbg!( - // &self.ids, - // &self.specifier, - // self.request(), - // self.used_by_exports.as_ref() - // ); - let ret = get_dependency_used_by_exports_condition(self.id, self.used_by_exports.as_ref()); - // dbg!(&ret); - ret + get_dependency_used_by_exports_condition(self.id, self.used_by_exports.as_ref()) } fn get_referenced_exports( diff --git a/crates/rspack_plugin_javascript/src/visitors/dependency/harmony_import_dependency_scanner.rs b/crates/rspack_plugin_javascript/src/visitors/dependency/harmony_import_dependency_scanner.rs index 6de5af431ac1..72965c6d5e71 100644 --- a/crates/rspack_plugin_javascript/src/visitors/dependency/harmony_import_dependency_scanner.rs +++ b/crates/rspack_plugin_javascript/src/visitors/dependency/harmony_import_dependency_scanner.rs @@ -510,7 +510,6 @@ impl Visit for HarmonyImportRefDependencyScanner<'_> { .map(|item| item.0.clone()) .collect::>(), ); - // dbg!(&ids); self .rewrite_usage_span .insert(member_expr.span, ExtraSpanInfo::ReWriteUsedByExports); @@ -596,6 +595,7 @@ mod test { harmony_named_exports: Default::default(), all_star_exports: Default::default(), need_create_require: false, + json_data: None, }; let mut import_map = Default::default(); let mut deps = vec![]; diff --git a/crates/rspack_plugin_json/src/json_exports_dependency.rs b/crates/rspack_plugin_json/src/json_exports_dependency.rs new file mode 100644 index 000000000000..416280ec4fa6 --- /dev/null +++ b/crates/rspack_plugin_json/src/json_exports_dependency.rs @@ -0,0 +1,101 @@ +use json::JsonValue; +use rspack_core::{ + AsContextDependency, AsModuleDependency, Dependency, DependencyId, DependencyTemplate, + ExportNameOrSpec, ExportSpec, ExportsOfExportsSpec, ExportsSpec, ModuleGraph, TemplateContext, + TemplateReplaceSource, +}; +#[derive(Debug, Clone)] +pub struct JsonExportsDependency { + id: DependencyId, + data: JsonValue, +} + +impl JsonExportsDependency { + pub fn new(data: JsonValue) -> Self { + Self { + data, + id: DependencyId::new(), + } + } +} + +impl Dependency for JsonExportsDependency { + fn id(&self) -> &rspack_core::DependencyId { + &self.id + } + + fn dependency_debug_name(&self) -> &'static str { + "JsonExportsDependency" + } + + fn get_exports(&self, _mg: &ModuleGraph) -> Option { + Some(ExportsSpec { + exports: get_exports_from_data(&self.data).unwrap_or(ExportsOfExportsSpec::Null), + ..Default::default() + }) + } +} + +impl AsModuleDependency for JsonExportsDependency {} +impl AsContextDependency for JsonExportsDependency {} + +impl DependencyTemplate for JsonExportsDependency { + fn apply( + &self, + _source: &mut TemplateReplaceSource, + _code_generatable_context: &mut TemplateContext, + ) { + } +} + +fn get_exports_from_data(data: &JsonValue) -> Option { + let ret = match data { + JsonValue::Null + | JsonValue::Short(_) + | JsonValue::String(_) + | JsonValue::Number(_) + | JsonValue::Boolean(_) => { + return None; + } + JsonValue::Object(obj) => ExportsOfExportsSpec::Array( + obj + .iter() + .map(|(k, v)| { + ExportNameOrSpec::ExportSpec(ExportSpec { + name: k.into(), + can_mangle: Some(true), + exports: get_exports_from_data(v).map(|item| match item { + ExportsOfExportsSpec::True => unreachable!(), + ExportsOfExportsSpec::Null => unreachable!(), + ExportsOfExportsSpec::Array(arr) => arr, + }), + ..Default::default() + }) + }) + .collect::>(), + ), + JsonValue::Array(arr) => { + if arr.len() > 100 { + return None; + } + ExportsOfExportsSpec::Array( + arr + .iter() + .enumerate() + .map(|(i, item)| { + ExportNameOrSpec::ExportSpec(ExportSpec { + name: format!("{i}").into(), + can_mangle: Some(true), + exports: get_exports_from_data(item).map(|item| match item { + ExportsOfExportsSpec::True | ExportsOfExportsSpec::Null => unreachable!(), + ExportsOfExportsSpec::Array(arr) => arr, + }), + ..Default::default() + }) + }) + .collect::>(), + ) + } + }; + Some(ret) +} diff --git a/crates/rspack_plugin_json/src/lib.rs b/crates/rspack_plugin_json/src/lib.rs index 3c7995d04ee4..2e4cdfc5222f 100644 --- a/crates/rspack_plugin_json/src/lib.rs +++ b/crates/rspack_plugin_json/src/lib.rs @@ -1,16 +1,28 @@ -use json::Error::{ - ExceededDepthLimit, FailedUtf8Parsing, UnexpectedCharacter, UnexpectedEndOfJson, WrongType, +#![feature(let_chains)] +use std::borrow::Cow; + +use json::{ + number::Number, + object::Object, + Error::{ + ExceededDepthLimit, FailedUtf8Parsing, UnexpectedCharacter, UnexpectedEndOfJson, WrongType, + }, + JsonValue, }; use rspack_core::{ rspack_sources::{BoxSource, RawSource, Source, SourceExt}, - BuildMetaDefaultObject, BuildMetaExportsType, CompilerOptions, GenerateContext, Module, - ParserAndGenerator, Plugin, RuntimeGlobals, SourceType, + BuildMetaDefaultObject, BuildMetaExportsType, CompilerOptions, ExportsInfo, GenerateContext, + Module, ModuleGraph, ParserAndGenerator, Plugin, RuntimeGlobals, RuntimeSpec, SourceType, + UsageState, }; use rspack_error::{ internal_error, DiagnosticKind, IntoTWithDiagnosticArray, Result, TWithDiagnosticArray, TraceableError, }; +use crate::json_exports_dependency::JsonExportsDependency; + +mod json_exports_dependency; mod utils; #[derive(Debug)] @@ -90,16 +102,20 @@ impl ParserAndGenerator for JsonParserAndGenerator { } }); - let diagnostics = if let Err(err) = parse_result { - vec![err.into()] - } else { - vec![] + let (diagnostics, data) = match parse_result { + Ok(data) => (vec![], Some(data)), + Err(err) => (vec![err.into()], None), }; + build_info.json_data = data.clone(); Ok( rspack_core::ParseResult { presentational_dependencies: vec![], - dependencies: vec![], + dependencies: if let Some(data) = data { + vec![Box::new(JsonExportsDependency::new(data))] + } else { + vec![] + }, blocks: vec![], source: box_source, analyze_result: Default::default(), @@ -112,22 +128,59 @@ impl ParserAndGenerator for JsonParserAndGenerator { #[allow(clippy::unwrap_in_result)] fn generate( &self, - source: &BoxSource, - _module: &dyn rspack_core::Module, + _source: &BoxSource, + module: &dyn rspack_core::Module, generate_context: &mut GenerateContext, ) -> Result { + let GenerateContext { + compilation, + runtime, + .. + } = generate_context; match generate_context.requested_source_type { SourceType::JavaScript => { generate_context .runtime_requirements .insert(RuntimeGlobals::MODULE); - Ok( - RawSource::from(format!( - r#"module.exports = {};"#, - utils::escape_json(&source.source()) - )) - .boxed(), - ) + let mgm = compilation + .module_graph + .module_graph_module_by_identifier(&module.identifier()) + .expect("should have module identifier"); + let json_data = mgm + .build_info + .as_ref() + .and_then(|info| info.json_data.as_ref()) + .expect("should have json data"); + let exports_info = compilation + .module_graph + .get_exports_info(&module.identifier()); + + let final_json = match json_data { + json::JsonValue::Object(_) | json::JsonValue::Array(_) + if exports_info + .other_exports_info + .get_export_info(&compilation.module_graph) + .get_used(*runtime) + == UsageState::Unused => + { + create_object_for_exports_info( + json_data.clone(), + exports_info, + *runtime, + &compilation.module_graph, + ) + } + _ => json_data.clone(), + }; + let is_js_object = final_json.is_object() || final_json.is_array(); + let final_json_string = final_json.to_string(); + let json_str = utils::escape_json(&final_json_string); + let json_expr = if is_js_object && json_str.len() > 20 { + Cow::Owned(format!("JSON.parse('{}')", json_str.replace('\'', r#"\'"#))) + } else { + json_str + }; + Ok(RawSource::from(format!(r#"module.exports = {}"#, json_expr)).boxed()) } _ => panic!( "Unsupported source type: {:?}", @@ -158,3 +211,101 @@ impl Plugin for JsonPlugin { Ok(()) } } + +fn create_object_for_exports_info( + data: JsonValue, + exports_info: &ExportsInfo, + runtime: Option<&RuntimeSpec>, + mg: &ModuleGraph, +) -> JsonValue { + if exports_info.other_exports_info.get_used(mg, runtime) != UsageState::Unused { + return data; + } + + match data { + JsonValue::Null + | JsonValue::Short(_) + | JsonValue::String(_) + | JsonValue::Number(_) + | JsonValue::Boolean(_) => unreachable!(), + JsonValue::Object(mut obj) => { + let mut used_pair = vec![]; + for (key, value) in obj.iter_mut() { + let export_info = exports_info.id.get_read_only_export_info(&key.into(), mg); + let used = export_info.get_used(runtime); + if used == UsageState::Unused { + continue; + } + let new_value = if used == UsageState::OnlyPropertiesUsed + && let Some(exports_info_id) = export_info.exports_info + { + let exports_info = mg.get_exports_info_by_id(&exports_info_id); + // avoid clone + let temp = std::mem::replace(value, JsonValue::Null); + create_object_for_exports_info(temp, exports_info, runtime, mg) + } else { + std::mem::replace(value, JsonValue::Null) + }; + let used_name = export_info + .get_used_name(&key.into(), runtime) + .expect("should have used name"); + used_pair.push((used_name, new_value)); + } + let mut new_obj = Object::new(); + for (k, v) in used_pair { + new_obj.insert(&k, v); + } + JsonValue::Object(new_obj) + } + JsonValue::Array(arr) => { + let original_len = arr.len(); + let mut max_used_index = 0; + let mut ret = arr + .into_iter() + .enumerate() + .map(|(i, item)| { + let export_info = exports_info + .id + .get_read_only_export_info(&format!("{i}").into(), mg); + let used = export_info.get_used(runtime); + if used == UsageState::Unused { + return None; + } + max_used_index = max_used_index.max(i); + if used == UsageState::OnlyPropertiesUsed + && let Some(exports_info_id) = export_info.exports_info + { + let exports_info = mg.get_exports_info_by_id(&exports_info_id); + Some(create_object_for_exports_info( + item, + exports_info, + runtime, + mg, + )) + } else { + Some(item) + } + }) + .collect::>(); + let arr_length_used = exports_info + .id + .get_read_only_export_info(&"length".into(), mg) + .get_used(runtime); + let array_length_when_used = match arr_length_used { + UsageState::Unused => None, + _ => Some(original_len), + }; + let used_length = if let Some(array_length_when_used) = array_length_when_used { + array_length_when_used + } else { + max_used_index + 1 + }; + ret.drain(used_length..); + let normalized_ret = ret + .into_iter() + .map(|item| item.unwrap_or(JsonValue::Number(Number::from(0)))) + .collect::>(); + JsonValue::Array(normalized_ret) + } + } +} diff --git a/crates/rspack_plugin_json/tests/fixtures/simple/json.json b/crates/rspack_plugin_json/tests/fixtures/simple/json.json index f2a886f39de7..92fae2e714b8 100644 --- a/crates/rspack_plugin_json/tests/fixtures/simple/json.json +++ b/crates/rspack_plugin_json/tests/fixtures/simple/json.json @@ -1,3 +1,3 @@ { - "hello": "world" + "hello": "world is better '" } diff --git a/crates/rspack_plugin_json/tests/fixtures/simple/snapshot/output.snap b/crates/rspack_plugin_json/tests/fixtures/simple/snapshot/output.snap index 434fed74ee57..417107eee567 100644 --- a/crates/rspack_plugin_json/tests/fixtures/simple/snapshot/output.snap +++ b/crates/rspack_plugin_json/tests/fixtures/simple/snapshot/output.snap @@ -12,10 +12,7 @@ console.log(_json_json__WEBPACK_IMPORTED_MODULE_0__); }), "./json.json": (function (module, exports, __webpack_require__) { "use strict"; -module.exports = { - "hello": "world" -} -;}), +module.exports = JSON.parse('{"hello":"world is better \'"}')}), },function(__webpack_require__) { var __webpack_exec__ = function(moduleId) { return __webpack_require__(__webpack_require__.s = moduleId) } diff --git a/packages/rspack/tests/configCases/tree-shaking/import-by-name-json/data/c.json b/packages/rspack/tests/configCases/tree-shaking/import-by-name-json/data/c.json new file mode 100644 index 000000000000..12bae17cf72d --- /dev/null +++ b/packages/rspack/tests/configCases/tree-shaking/import-by-name-json/data/c.json @@ -0,0 +1 @@ +[1, 2, 3, 4] diff --git a/packages/rspack/tests/configCases/tree-shaking/import-by-name-json/data/d.json b/packages/rspack/tests/configCases/tree-shaking/import-by-name-json/data/d.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/rspack/tests/configCases/tree-shaking/import-by-name-json/data/d.json @@ -0,0 +1 @@ +{} diff --git a/packages/rspack/tests/configCases/tree-shaking/import-by-name-json/data/e.json b/packages/rspack/tests/configCases/tree-shaking/import-by-name-json/data/e.json new file mode 100644 index 000000000000..3cebd09f51b3 --- /dev/null +++ b/packages/rspack/tests/configCases/tree-shaking/import-by-name-json/data/e.json @@ -0,0 +1,5 @@ +{ + "1": "x", + "bb": 2, + "aa": 1 +} diff --git a/packages/rspack/tests/configCases/tree-shaking/import-by-name-json/data/f.json b/packages/rspack/tests/configCases/tree-shaking/import-by-name-json/data/f.json new file mode 100644 index 000000000000..3cc1d0220710 --- /dev/null +++ b/packages/rspack/tests/configCases/tree-shaking/import-by-name-json/data/f.json @@ -0,0 +1,5 @@ +{ + "named": "named", + "default": "default", + "__esModule": true +} diff --git a/packages/rspack/tests/configCases/tree-shaking/import-by-name-json/data/g.json b/packages/rspack/tests/configCases/tree-shaking/import-by-name-json/data/g.json new file mode 100644 index 000000000000..15e8d0ff8369 --- /dev/null +++ b/packages/rspack/tests/configCases/tree-shaking/import-by-name-json/data/g.json @@ -0,0 +1,3 @@ +{ + "named": {} +} diff --git a/packages/rspack/tests/configCases/tree-shaking/import-by-name-json/index.js b/packages/rspack/tests/configCases/tree-shaking/import-by-name-json/index.js new file mode 100644 index 000000000000..aa40d278210d --- /dev/null +++ b/packages/rspack/tests/configCases/tree-shaking/import-by-name-json/index.js @@ -0,0 +1,21 @@ +import * as c from "./data/c.json"; +import * as d from "./data/d.json"; +import { bb, aa } from "./data/e.json"; +import f, { named } from "./data/f.json"; +import g, { named as gnamed } from "./data/g.json"; + +it("should be possible to import json data", function () { + expect(c[2]).toBe(3); + expect(Object.keys(d)).toEqual(["default"]); + expect(aa).toBe(1); + expect(bb).toBe(2); + expect(named).toBe("named"); + expect({ f }).toEqual({ + f: { + __esModule: true, + default: "default", + named: "named" + } + }); + expect(g.named).toBe(gnamed); +}); diff --git a/packages/rspack/tests/configCases/tree-shaking/import-by-name-json/webpack.config.js b/packages/rspack/tests/configCases/tree-shaking/import-by-name-json/webpack.config.js new file mode 100644 index 000000000000..8f15cf4f94b3 --- /dev/null +++ b/packages/rspack/tests/configCases/tree-shaking/import-by-name-json/webpack.config.js @@ -0,0 +1,20 @@ +/**@type {import('@rspack/cli').Configuration}*/ +module.exports = { + mode: "production", + context: __dirname, + module: { + rules: [] + }, + experiments: { + rspackFuture: { + newTreeshaking: true + } + }, + optimization: { + moduleIds: "named", + minimize: false + }, + externalsPresets: { + node: true + } +}; diff --git a/webpack-test/configCases/sharing/share-multiple-versions/webpack.config.js b/webpack-test/configCases/sharing/share-multiple-versions/webpack.config.js index ae9fd16859da..53ab36ce28c8 100644 --- a/webpack-test/configCases/sharing/share-multiple-versions/webpack.config.js +++ b/webpack-test/configCases/sharing/share-multiple-versions/webpack.config.js @@ -3,6 +3,9 @@ const { SharePlugin } = require("../../../../").sharing; /** @type {import("@rspack/core").Configuration} */ module.exports = { + optimization: { + mangleExports: false + }, plugins: [ new SharePlugin({ shared: ["shared"]