diff --git a/Cargo.lock b/Cargo.lock index e00f98a..ba40dc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1471,12 +1471,19 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "dashmap", "either", + "fxhash", "indexmap 1.9.3", + "lazy_static", "once_cell", + "regex", + "rspack_ast", "rspack_core", "rspack_error", "rspack_loader_runner", + "rspack_plugin_javascript", + "rspack_regex", "rspack_testing", "serde", "serde_json", diff --git a/crates/binding_options/src/options/mod.rs b/crates/binding_options/src/options/mod.rs index 9fe4d0e..2d8a912 100644 --- a/crates/binding_options/src/options/mod.rs +++ b/crates/binding_options/src/options/mod.rs @@ -10,8 +10,8 @@ use serde::Deserialize; use rspack_binding_options::{ RawBuiltins, RawCacheOptions, RawContext, RawDevServer, RawDevtool, RawExperiments, - RawMode, RawNodeOption, RawOutputOptions, RawResolveOptions, RawOptimizationOptions, - RawSnapshotOptions, RawStatsOptions, RawTarget, RawModuleOptions, RawOptionsApply, + RawMode, RawNodeOption, RawOutputOptions, RawResolveOptions, RawOptionsApply, + RawSnapshotOptions, RawStatsOptions, RawTarget, RawModuleOptions, }; mod raw_module; diff --git a/crates/binding_options/src/options/raw_module.rs b/crates/binding_options/src/options/raw_module.rs index 77b6c21..b8010cd 100644 --- a/crates/binding_options/src/options/raw_module.rs +++ b/crates/binding_options/src/options/raw_module.rs @@ -26,7 +26,11 @@ pub fn get_builtin_loader(builtin: &str, options: Option<&str>) -> BoxLoader { if builtin.starts_with(COMPILATION_LOADER_IDENTIFIER) { return Arc::new( - loader_compilation::CompilationLoader::default().with_identifier(builtin.into()), + loader_compilation::CompilationLoader::new( + serde_json::from_str(options.unwrap_or("{}")).unwrap_or_else(|e| { + panic!("Could not parse builtin:compilation-loader options:{options:?},error: {e:?}") + }), + ).with_identifier(builtin.into()), ); } diff --git a/crates/loader_compilation/Cargo.toml b/crates/loader_compilation/Cargo.toml index 94e1198..46e8ced 100644 --- a/crates/loader_compilation/Cargo.toml +++ b/crates/loader_compilation/Cargo.toml @@ -8,11 +8,18 @@ edition = "2021" [dependencies] anyhow = { workspace = true } async-trait = { workspace = true } +dashmap = { workspace = true } either = "1" +fxhash = "0.2.1" +lazy_static = "1.4.0" once_cell = { workspace = true } +rspack_ast = { path = "../.rspack_crates/rspack_ast" } rspack_core = { path = "../.rspack_crates/rspack_core" } rspack_error = { path = "../.rspack_crates/rspack_error" } rspack_loader_runner = { path = "../.rspack_crates/rspack_loader_runner" } +rspack_plugin_javascript = { path = "../.rspack_crates/rspack_plugin_javascript" } +rspack_regex = { path = "../.rspack_crates/rspack_regex" } +regex = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = "1.0.100" swc_config = { workspace = true } diff --git a/crates/loader_compilation/src/compiler.rs b/crates/loader_compilation/src/compiler.rs new file mode 100644 index 0000000..040d40a --- /dev/null +++ b/crates/loader_compilation/src/compiler.rs @@ -0,0 +1,250 @@ +use std::env; +use std::{path::PathBuf, sync::Arc}; +use anyhow::{Context, Error}; +use dashmap::DashMap; +use rspack_ast::javascript::{Ast as JsAst, Context as JsAstContext, Program as JsProgram}; +use swc_core:: { + base::{ + config::{Options, JsMinifyCommentOption, BuiltInput, IsModule}, + try_with_handler, SwcComments + }, + common::{ + Globals, SourceFile, SourceMap, GLOBALS, Mark, FileName, FilePathMapping, BytePos, + comments::{SingleThreadedComments, Comments, Comment, CommentKind}, + errors::{Handler, HANDLER}, + }, + ecma::{transforms::base::helpers::{self, Helpers}, ast::{Program, EsVersion}, visit::{Fold, FoldWith}, + parser::{parse_file_as_module, Syntax, parse_file_as_script, parse_file_as_program}}, +}; +use swc_config::config_types::BoolOr; + +fn minify_file_comments( + comments: &SingleThreadedComments, + preserve_comments: BoolOr, +) { + match preserve_comments { + BoolOr::Bool(true) | BoolOr::Data(JsMinifyCommentOption::PreserveAllComments) => {} + + BoolOr::Data(JsMinifyCommentOption::PreserveSomeComments) => { + let preserve_excl = |_: &BytePos, vc: &mut Vec| -> bool { + // Preserve license comments. + // + // See https://github.com/terser/terser/blob/798135e04baddd94fea403cfaab4ba8b22b1b524/lib/output.js#L175-L181 + vc.retain(|c: &Comment| { + c.text.contains("@lic") + || c.text.contains("@preserve") + || c.text.contains("@copyright") + || c.text.contains("@cc_on") + || (c.kind == CommentKind::Block && c.text.starts_with('!')) + }); + !vc.is_empty() + }; + let (mut l, mut t) = comments.borrow_all_mut(); + + l.retain(preserve_excl); + t.retain(preserve_excl); + } + + BoolOr::Bool(false) => { + let (mut l, mut t) = comments.borrow_all_mut(); + l.clear(); + t.clear(); + } + } +} + +pub(crate) struct SwcCompiler { + cm: Arc, + fm: Arc, + comments: SingleThreadedComments, + options: Options, + globals: Globals, + helpers: Helpers, +} + +impl SwcCompiler { + pub fn new(resource_path: PathBuf, source: String, mut options: Options) -> Result { + let cm = Arc::new(SourceMap::new(FilePathMapping::empty())); + let globals = Globals::default(); + GLOBALS.set(&globals, || { + let top_level_mark = Mark::new(); + let unresolved_mark = Mark::new(); + options.top_level_mark = Some(top_level_mark); + options.unresolved_mark = Some(unresolved_mark); + }); + // TODO: support read config of .swcrc. + let fm = cm.new_source_file(FileName::Real(resource_path), source); + let comments = SingleThreadedComments::default(); + let helpers = GLOBALS.set(&globals, || { + let external_helpers = options.config.jsc.external_helpers; + Helpers::new(external_helpers.into()) + }); + + Ok(Self { + cm, + fm, + comments, + options, + globals, + helpers, + }) + } + + pub fn run(&self, op: impl FnOnce() -> R) -> R { + GLOBALS.set(&self.globals, op) + } + + fn parse_js( + &self, + fm: Arc, + handler: &Handler, + target: EsVersion, + syntax: Syntax, + is_module: IsModule, + comments: Option<&dyn Comments>, + ) -> Result { + let mut error = false; + let mut errors = vec![]; + + let program_result = match is_module { + IsModule::Bool(true) => { + parse_file_as_module(&fm, syntax, target, comments, &mut errors).map(Program::Module) + } + IsModule::Bool(false) => { + parse_file_as_script(&fm, syntax, target, comments, &mut errors).map(Program::Script) + } + IsModule::Unknown => parse_file_as_program(&fm, syntax, target, comments, &mut errors), + }; + + for e in errors { + e.into_diagnostic(handler).emit(); + error = true; + } + + let mut res = program_result.map_err(|e| { + e.into_diagnostic(handler).emit(); + Error::msg("Syntax Error") + }); + + if error { + return Err(anyhow::anyhow!("Syntax Error")); + } + + if env::var("SWC_DEBUG").unwrap_or_default() == "1" { + res = res.with_context(|| format!("Parser config: {:?}", syntax)); + } + + res + + } + + pub fn parse<'a, P>( + &'a self, + program: Option, + before_pass: impl FnOnce(&Program) -> P + 'a, + ) -> Result, Error> + where + P: Fold + 'a, + { + let built = self.run(|| { + try_with_handler(self.cm.clone(), Default::default(), |handler| { + let built = self.options.build_as_input( + &self.cm, + &self.fm.name, + move |syntax, target, is_module| match program { + Some(v) => Ok(v), + _ => self.parse_js( + self.fm.clone(), + handler, + target, + syntax, + is_module, + Some(&self.comments), + ), + }, + self.options.output_path.as_deref(), + self.options.source_file_name.clone(), + handler, + // TODO: support config file. + Some(self.options.config.clone()), + Some(&self.comments), + before_pass, + )?; + + Ok(Some(built)) + }) + })?; + + match built { + Some(v) => Ok(v), + None => { + anyhow::bail!("cannot process file because it's ignored by .swcrc") + } + } + } + + pub fn transform(&self, config: BuiltInput) -> Result { + let program = config.program; + let mut pass = config.pass; + + let program = self.run(|| { + helpers::HELPERS.set(&self.helpers, || { + try_with_handler(self.cm.clone(), Default::default(), |handler| { + HANDLER.set(handler, || { + // Fold module + Ok(program.fold_with(&mut pass)) + }) + }) + }) + }); + if let Some(comments) = &config.comments { + minify_file_comments(comments, config.preserve_comments); + }; + + program + } +} + +pub(crate) trait IntoJsAst { + fn into_js_ast(self, program: Program) -> JsAst; +} + +impl IntoJsAst for SwcCompiler { + fn into_js_ast(self, program: Program) -> JsAst { + JsAst::default() + .with_program(JsProgram::new( + program, + Some(self.comments.into_swc_comments()), + )) + .with_context(JsAstContext { + globals: self.globals, + helpers: self.helpers, + source_map: self.cm, + top_level_mark: self + .options + .top_level_mark + .expect("`top_level_mark` should be initialized"), + unresolved_mark: self + .options + .unresolved_mark + .expect("`unresolved_mark` should be initialized"), + }) + } +} + +trait IntoSwcComments { + fn into_swc_comments(self) -> SwcComments; +} + +impl IntoSwcComments for SingleThreadedComments { + fn into_swc_comments(self) -> SwcComments { + let (l, t) = { + let (l, t) = self.take_all(); + (l.take(), t.take()) + }; + SwcComments { + leading: Arc::new(DashMap::from_iter(l.into_iter())), + trailing: Arc::new(DashMap::from_iter(t.into_iter())), + } + } +} \ No newline at end of file diff --git a/crates/loader_compilation/src/lib.rs b/crates/loader_compilation/src/lib.rs index 34570bc..5bcb3c9 100644 --- a/crates/loader_compilation/src/lib.rs +++ b/crates/loader_compilation/src/lib.rs @@ -1,23 +1,80 @@ +use std::{path::Path, collections::HashMap, sync::Mutex}; +use lazy_static::lazy_static; +use rspack_ast::RspackAst; use rspack_core::{rspack_sources::SourceMap, LoaderRunnerContext, Mode}; use rspack_loader_runner::{Identifiable, Identifier, Loader, LoaderContext}; use rspack_error::{ - internal_error, Diagnostic, DiagnosticKind, Error, InternalError, Result, Severity, - TraceableError, + internal_error, Result, Diagnostic, }; +use swc_core::{ + base::config::{InputSourceMap, Options, OutputCharset, Config, TransformConfig}, + ecma::parser::{Syntax, TsConfig}, +}; + +use swc_config::{config_types::MergingOption, merge::Merge}; +use rspack_plugin_javascript::{ + ast::{self, SourceMapConfig}, + TransformOutput, +}; +use serde::Deserialize; +use rspack_regex::RspackRegex; + +mod compiler; +mod transform; + +use transform::*; +use compiler::{SwcCompiler, IntoJsAst}; +#[derive(Debug, Default, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct CompileRules { + // Built-in rules to exclude files from compilation, such as react, react-dom, etc. + exclude: Option>, +} + +#[derive(Debug, Default, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct LoaderOptions { + pub swc_options: Config, + pub transform_features: TransformFeatureOptions, + pub compile_rules: CompileRules, +} + +pub struct CompilationOptions { + swc_options: Options, + transform_features: TransformFeatureOptions, + compile_rules: CompileRules, +} pub struct CompilationLoader { identifier: Identifier, + loader_options: CompilationOptions, } -impl Default for CompilationLoader { - fn default() -> Self { +pub const COMPILATION_LOADER_IDENTIFIER: &str = "builtin:compilation-loader"; + +impl From for CompilationOptions { + fn from(value: LoaderOptions) -> Self { + let transform_features = value.transform_features; + let compile_rules = value.compile_rules; + CompilationOptions { + swc_options: Options { + config: value.swc_options, + ..Default::default() + }, + transform_features, + compile_rules, + } + } +} + +impl CompilationLoader { + pub fn new(options: LoaderOptions) -> Self { Self { identifier: COMPILATION_LOADER_IDENTIFIER.into(), + loader_options: options.into(), } } -} -impl CompilationLoader { pub fn with_identifier(mut self, identifier: Identifier) -> Self { assert!(identifier.starts_with(COMPILATION_LOADER_IDENTIFIER)); self.identifier = identifier; @@ -25,23 +82,147 @@ impl CompilationLoader { } } +lazy_static! { + static ref GLOBAL_FILE_ACCESS: Mutex> = Mutex::new(HashMap::new()); + static ref GLOBAL_ROUTES_CONFIG: Mutex>> = Mutex::new(None); +} + #[async_trait::async_trait] impl Loader for CompilationLoader { async fn run(&self, loader_context: &mut LoaderContext<'_, LoaderRunnerContext>) -> Result<()> { + let resource_path = loader_context.resource_path.to_path_buf(); + + if self.loader_options.compile_rules.exclude.is_some() { + let exclude = self.loader_options.compile_rules.exclude.as_ref().unwrap(); + for pattern in exclude { + let pattern = RspackRegex::new(pattern).unwrap(); + if pattern.test(&resource_path.to_string_lossy()) { + return Ok(()); + } + } + } + let Some(content) = std::mem::take(&mut loader_context.content) else { return Err(internal_error!("No content found")); }; - let mut source = content.try_into_string()?; - source += r#" -window.__custom_code__ = true; -"#; - loader_context.content = Some(source.into()); + + let swc_options = { + let mut swc_options = self.loader_options.swc_options.clone(); + if swc_options.config.jsc.transform.as_ref().is_some() { + let mut transform = TransformConfig::default(); + let default_development = matches!(loader_context.context.options.mode, Mode::Development); + transform.react.development = Some(default_development); + swc_options + .config + .jsc + .transform + .merge(MergingOption::from(Some(transform))); + } + + let file_extension = resource_path.extension().unwrap(); + let ts_extensions = vec!["tsx", "ts", "mts"]; + if ts_extensions.iter().any(|ext| ext == &file_extension) { + swc_options.config.jsc.syntax = Some(Syntax::Typescript(TsConfig { tsx: true, decorators: true, ..Default::default() })); + } + + if let Some(pre_source_map) = std::mem::take(&mut loader_context.source_map) { + if let Ok(source_map) = pre_source_map.to_json() { + swc_options.config.input_source_map = Some(InputSourceMap:: Str(source_map)) + } + } + + if swc_options.config.jsc.experimental.plugins.is_some() { + loader_context.emit_diagnostic(Diagnostic::warn( + COMPILATION_LOADER_IDENTIFIER.to_string(), + "Experimental plugins are not currently supported.".to_string(), + 0, + 0, + )); + } + + if swc_options.config.jsc.target.is_some() && swc_options.config.env.is_some() { + loader_context.emit_diagnostic(Diagnostic::warn( + COMPILATION_LOADER_IDENTIFIER.to_string(), + "`env` and `jsc.target` cannot be used together".to_string(), + 0, + 0, + )); + } + swc_options + }; + let devtool = &loader_context.context.options.devtool; + let source = content.try_into_string()?; + let compiler = SwcCompiler::new(resource_path.clone(), source.clone(), swc_options)?; + + let transform_options = &self.loader_options.transform_features; + let compiler_context:&str = loader_context.context.options.context.as_ref(); + let mut file_access = GLOBAL_FILE_ACCESS.lock().unwrap(); + let mut routes_config = GLOBAL_ROUTES_CONFIG.lock().unwrap(); + let file_accessed = file_access.contains_key(&resource_path.to_string_lossy().to_string()); + + if routes_config.is_none() || file_accessed { + // Load routes config for transform. + let routes_config_path: std::path::PathBuf = Path::new(compiler_context).join(".ice/route-manifest.json"); + *routes_config = Some(load_routes_config(&routes_config_path).unwrap()); + + if file_accessed { + // If file accessed, then we need to clear the map for the current compilation. + file_access.clear(); + } + } + file_access.insert(resource_path.to_string_lossy().to_string(), true); + + let built = compiler.parse(None, |_| { + transform( + &resource_path, + routes_config.as_ref().unwrap(), + transform_options + ) + })?; + + let codegen_options = ast::CodegenOptions { + target: Some(built.target), + minify: Some(built.minify), + ascii_only: built + .output + .charset + .as_ref() + .map(|v| matches!(v, OutputCharset::Ascii)), + source_map_config: SourceMapConfig { + enable: devtool.source_map(), + inline_sources_content: true, + emit_columns: !devtool.cheap(), + names: Default::default(), + }, + keep_comments: Some(true), + }; + let program = compiler.transform(built)?; + let ast = compiler.into_js_ast(program); + + // If swc-loader is the latest loader available, + // then loader produces AST, which could be used as an optimization. + if loader_context.loader_index() == 0 + && (loader_context + .current_loader() + .composed_index_by_identifier(&self.identifier) + .map(|idx| idx == 0) + .unwrap_or(true)) + { + loader_context + .additional_data + .insert(RspackAst::JavaScript(ast)); + loader_context.additional_data.insert(codegen_options); + loader_context.content = Some("".to_owned().into()) + } else { + let TransformOutput { code, map } = ast::stringify(&ast, codegen_options)?; + loader_context.content = Some(code.into()); + loader_context.source_map = map.map(|m| SourceMap::from_json(&m)).transpose()?; + } + Ok(()) } } -pub const COMPILATION_LOADER_IDENTIFIER: &str = "builtin:compilation-loader"; - impl Identifiable for CompilationLoader { fn identifier(&self) -> Identifier { self.identifier diff --git a/crates/loader_compilation/src/transform/keep_export.rs b/crates/loader_compilation/src/transform/keep_export.rs new file mode 100644 index 0000000..1d23124 --- /dev/null +++ b/crates/loader_compilation/src/transform/keep_export.rs @@ -0,0 +1,589 @@ +// transform code is modified based on swc plugin of keep_export: +// https://github.com/ice-lab/swc-plugins/tree/main/packages/keep-export +use fxhash::FxHashSet; +use std::mem::take; +use swc_core::ecma::{ + ast::*, + visit::{Fold, FoldWith, noop_fold_type}, +}; +use swc_core::common::{ + DUMMY_SP, pass::{Repeat, Repeated} +}; + +/// State of the transforms. Shared by the analyzer and the transform. +#[derive(Debug, Default)] +struct State { + /// Identifiers referenced by other functions. + /// + /// Cleared before running each pass, because we drop ast nodes between the + /// passes. + refs_from_other: FxHashSet, + + /// Identifiers referenced by kept functions or derivatives. + /// + /// Preserved between runs, because we should remember derivatives of data + /// functions as the data function itself is already removed. + refs_used: FxHashSet, + + should_run_again: bool, + keep_exports: Vec, +} + +impl State { + fn should_keep_identifier(&mut self, i: &Ident) -> bool { + self.keep_exports.contains(&String::from(&*i.sym)) + } + + fn should_keep_default(&mut self) -> bool { + self.keep_exports.contains(&String::from("default")) + } +} + +struct KeepExport { + pub state: State, + in_lhs_of_var: bool, +} + +impl KeepExport { + fn should_remove(&self, id: Id) -> bool { + !self.state.refs_used.contains(&id) && !self.state.refs_from_other.contains(&id) + } + + /// Mark identifiers in `n` as a candidate for removal. + fn mark_as_candidate(&mut self, n: N) -> N + where + N: for<'a> FoldWith>, + { + // Analyzer never change `in_kept_fn` to false, so all identifiers in `n` will + // be marked as referenced from a data function. + let mut v = Analyzer { + state: &mut self.state, + in_lhs_of_var: false, + in_kept_fn: false, + }; + + let n = n.fold_with(&mut v); + self.state.should_run_again = true; + n + } +} + +impl Repeated for KeepExport { + fn changed(&self) -> bool { + self.state.should_run_again + } + + fn reset(&mut self) { + self.state.refs_from_other.clear(); + self.state.should_run_again = false; + } +} + +impl Fold for KeepExport { + // This is important for reducing binary sizes. + noop_fold_type!(); + + // Remove import expression + fn fold_import_decl(&mut self, mut i: ImportDecl) -> ImportDecl { + // Imports for side effects. + if i.specifiers.is_empty() { + return i; + } + + i.specifiers.retain(|s| match s { + ImportSpecifier::Named(ImportNamedSpecifier { local, .. }) + | ImportSpecifier::Default(ImportDefaultSpecifier { local, .. }) + | ImportSpecifier::Namespace(ImportStarAsSpecifier { local, .. }) => { + if self.should_remove(local.to_id()) { + self.state.should_run_again = true; + false + } else { + true + } + } + }); + + i + } + + fn fold_module(&mut self, mut m: Module) -> Module { + { + // Fill the state. + let mut v = Analyzer { + state: &mut self.state, + in_lhs_of_var: false, + in_kept_fn: false, + }; + m = m.fold_with(&mut v); + } + + m.fold_children_with(self) + } + + fn fold_module_items(&mut self, mut items: Vec) -> Vec { + items = items.fold_children_with(self); + + // Drop nodes. + items.retain(|s| !matches!(s, ModuleItem::Stmt(Stmt::Empty(..)))); + + // If all exports are deleted, return the empty named export. + if items.len() == 0 { + items.push(ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(NamedExport{ + span: DUMMY_SP, + specifiers: Vec::new(), + src: None, + type_only: false, + with: Default::default(), + }))); + } + + items + } + + fn fold_module_item(&mut self, i: ModuleItem) -> ModuleItem { + if let ModuleItem::ModuleDecl(ModuleDecl::Import(i)) = i { + let i = i.fold_with(self); + + if i.specifiers.is_empty() { + return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })); + } + + return ModuleItem::ModuleDecl(ModuleDecl::Import(i)); + } + + let i = i.fold_children_with(self); + + match &i { + ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)) if e.specifiers.is_empty() => { + return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })) + } + _ => {} + } + + i + } + + fn fold_named_export(&mut self, mut n: NamedExport) -> NamedExport { + n.specifiers = n.specifiers.fold_with(self); + + n.specifiers.retain(|s| { + let preserve = match s { + ExportSpecifier::Namespace(ExportNamespaceSpecifier { + name: ModuleExportName::Ident(exported), + .. + }) + | ExportSpecifier::Default(ExportDefaultSpecifier { exported, .. }) + | ExportSpecifier::Named(ExportNamedSpecifier { + exported: Some(ModuleExportName::Ident(exported)), + .. + }) => self + .state + .should_keep_identifier(exported), + ExportSpecifier::Named(ExportNamedSpecifier { + orig: ModuleExportName::Ident(orig), + .. + }) => self + .state + .should_keep_identifier(orig), + _ => false, + }; + + match preserve { + false => { + if let ExportSpecifier::Named(ExportNamedSpecifier { + orig: ModuleExportName::Ident(_orig), + .. + }) = s + { + self.state.should_run_again = true; + } + + false + } + true => true, + } + }); + + n + } + + /// This methods returns [Pat::Invalid] if the pattern should be removed. + fn fold_pat(&mut self, mut p: Pat) -> Pat { + p = p.fold_children_with(self); + + if self.in_lhs_of_var { + match &mut p { + Pat::Ident(name) => { + if self.should_remove(name.id.to_id()) { + self.state.should_run_again = true; + return Pat::Invalid(Invalid { span: DUMMY_SP }); + } + } + Pat::Array(arr) => { + if !arr.elems.is_empty() { + arr.elems.retain(|e| !matches!(e, Some(Pat::Invalid(..)))); + + if arr.elems.is_empty() { + return Pat::Invalid(Invalid { span: DUMMY_SP }); + } + } + } + Pat::Object(obj) => { + if !obj.props.is_empty() { + obj.props = take(&mut obj.props) + .into_iter() + .filter_map(|prop| match prop { + ObjectPatProp::KeyValue(prop) => { + if prop.value.is_invalid() { + None + } else { + Some(ObjectPatProp::KeyValue(prop)) + } + } + ObjectPatProp::Assign(prop) => { + if self.should_remove(prop.key.to_id()) { + self.mark_as_candidate(prop.value); + + None + } else { + Some(ObjectPatProp::Assign(prop)) + } + } + ObjectPatProp::Rest(prop) => { + if prop.arg.is_invalid() { + None + } else { + Some(ObjectPatProp::Rest(prop)) + } + } + }) + .collect(); + + if obj.props.is_empty() { + return Pat::Invalid(Invalid { span: DUMMY_SP }); + } + } + } + Pat::Rest(rest) => { + if rest.arg.is_invalid() { + return Pat::Invalid(Invalid { span: DUMMY_SP }); + } + } + _ => {} + } + } + + p + } + + #[allow(clippy::single_match)] + fn fold_stmt(&mut self, mut s: Stmt) -> Stmt { + match s { + Stmt::Decl(Decl::Fn(f)) => { + if self.should_remove(f.ident.to_id()) { + self.mark_as_candidate(f.function); + return Stmt::Empty(EmptyStmt { span: DUMMY_SP }); + } + + s = Stmt::Decl(Decl::Fn(f)); + } + Stmt::Decl(Decl::Class(c)) => { + if self.should_remove(c.ident.to_id()) { + self.mark_as_candidate(c.class); + return Stmt::Empty(EmptyStmt { span: DUMMY_SP }); + } + + s = Stmt::Decl(Decl::Class(c)); + } + _ => {} + } + + let s = s.fold_children_with(self); + match s { + Stmt::Decl(Decl::Var(v)) if v.decls.is_empty() => { + return Stmt::Empty(EmptyStmt { span: DUMMY_SP }); + } + _ => {} + } + + s + } + + /// This method make `name` of [VarDeclarator] to [Pat::Invalid] if it + /// should be removed. + fn fold_var_declarator(&mut self, mut d: VarDeclarator) -> VarDeclarator { + let old = self.in_lhs_of_var; + self.in_lhs_of_var = true; + let name = d.name.fold_with(self); + + self.in_lhs_of_var = false; + if name.is_invalid() { + d.init = self.mark_as_candidate(d.init); + } + let init = d.init.fold_with(self); + self.in_lhs_of_var = old; + + VarDeclarator { name, init, ..d } + } + + fn fold_var_declarators(&mut self, mut decls: Vec) -> Vec { + decls = decls.fold_children_with(self); + decls.retain(|d| !d.name.is_invalid()); + + decls + } +} + +struct Analyzer<'a> { + state: &'a mut State, + in_lhs_of_var: bool, + in_kept_fn: bool, +} + +impl Analyzer<'_> { + fn add_ref(&mut self, id: Id) { + if self.in_kept_fn { + self.state.refs_used.insert(id); + } else { + self.state.refs_from_other.insert(id); + } + } + + fn check_default>(&mut self, e: T) -> T { + if self.state.should_keep_default() { + let old_in_kept = self.in_kept_fn; + self.in_kept_fn = true; + let e = e.fold_children_with(self); + self.in_kept_fn = old_in_kept; + return e + } + + return e; + } +} + +impl Fold for Analyzer<'_> { + // This is important for reducing binary sizes. + noop_fold_type!(); + + fn fold_binding_ident(&mut self, i: BindingIdent) -> BindingIdent { + if !self.in_lhs_of_var || self.in_kept_fn { + self.add_ref(i.id.to_id()); + } + i + } + + fn fold_export_named_specifier(&mut self, s: ExportNamedSpecifier) -> ExportNamedSpecifier { + if let ModuleExportName::Ident(i) = &s.orig { + match &s.exported { + Some(exported) => { + if let ModuleExportName::Ident(e) = exported { + if self.state.should_keep_identifier(e) { + self.add_ref(i.to_id()); + } + } + } + None => { + if self.state.should_keep_identifier(i) { + self.add_ref(i.to_id()); + } + } + } + } + s + } + + fn fold_export_decl(&mut self, s: ExportDecl) -> ExportDecl { + let old_in_kept = self.in_kept_fn; + + match &s.decl { + Decl::Fn(f) => { + if self.state.should_keep_identifier(&f.ident) { + self.in_kept_fn = true; + self.add_ref(f.ident.to_id()); + } + } + + Decl::Var(d) => { + if d.decls.is_empty() { + return s; + } + if let Pat::Ident(id) = &d.decls[0].name { + if self.state.should_keep_identifier(&id.id) { + self.in_kept_fn = true; + self.add_ref(id.to_id()); + } + } + } + _ => {} + } + let e = s.fold_children_with(self); + self.in_kept_fn = old_in_kept; + e + } + + fn fold_expr(&mut self, e: Expr) -> Expr { + let e = e.fold_children_with(self); + + if let Expr::Ident(i) = &e { + self.add_ref(i.to_id()); + } + e + } + + fn fold_jsx_element(&mut self, jsx: JSXElement) -> JSXElement { + fn get_leftmost_id_member_expr(e: &JSXMemberExpr) -> Id { + match &e.obj { + JSXObject::Ident(i) => i.to_id(), + JSXObject::JSXMemberExpr(e) => get_leftmost_id_member_expr(e), + } + } + + match &jsx.opening.name { + JSXElementName::Ident(i) => { + self.add_ref(i.to_id()); + } + JSXElementName::JSXMemberExpr(e) => { + self.add_ref(get_leftmost_id_member_expr(e)); + } + _ => {} + } + + jsx.fold_children_with(self) + } + + fn fold_fn_decl(&mut self, f: FnDecl) -> FnDecl { + let f = f.fold_children_with(self); + if self.in_kept_fn { + self.add_ref(f.ident.to_id()); + } + f + } + + fn fold_fn_expr(&mut self, f: FnExpr) -> FnExpr { + let f = f.fold_children_with(self); + if let Some(id) = &f.ident { + self.add_ref(id.to_id()); + } + f + } + + /// Drops [ExportDecl] if all specifiers are removed. + fn fold_module_item(&mut self, s: ModuleItem) -> ModuleItem { + match s { + ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)) if !e.specifiers.is_empty() => { + let e = e.fold_with(self); + + if e.specifiers.is_empty() { + return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })); + } + + return ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)); + } + + ModuleItem::Stmt(Stmt::Expr(_e)) => { + // remove top expression + return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })) + } + + ModuleItem::Stmt(Stmt::If(_e)) => { + return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })) + } + + ModuleItem::Stmt(Stmt::DoWhile(_e)) => { + return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })) + } + + ModuleItem::Stmt(Stmt::Try(_e)) => { + return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })) + } + _ => {} + }; + + if let ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(e)) = &s { + match &e.decl { + Decl::Fn(f) => { + if self.state.should_keep_identifier(&f.ident) { + let s = s.fold_children_with(self); + return s; + } else { + return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })); + } + } + + Decl::Var(d) => { + if d.decls.is_empty() { + return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })); + } + + if let Pat::Ident(id) = &d.decls[0].name { + if self.state.should_keep_identifier(&id.id) { + let s = s.fold_children_with(self); + return s; + } else { + return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })); + } + } + } + _ => {} + } + } + + if let ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(_e)) = &s { + if !self.state.should_keep_default() { + return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })); + } + } + + if let ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(_e)) = &s { + if !self.state.should_keep_default() { + return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })); + } + } + + // Visit children to ensure that all references is added to the scope. + let s = s.fold_children_with(self); + s + } + + fn fold_default_decl(&mut self, d: DefaultDecl) -> DefaultDecl { + return self.check_default(d); + } + + fn fold_export_default_expr(&mut self, e: ExportDefaultExpr) -> ExportDefaultExpr { + return self.check_default(e); + } + + fn fold_prop(&mut self, p: Prop) -> Prop { + let p = p.fold_children_with(self); + + if let Prop::Shorthand(i) = &p { + self.add_ref(i.to_id()); + } + + p + } + + fn fold_var_declarator(&mut self, mut v: VarDeclarator) -> VarDeclarator { + let old_in_lhs_of_var = self.in_lhs_of_var; + + self.in_lhs_of_var = true; + v.name = v.name.fold_with(self); + + self.in_lhs_of_var = false; + v.init = v.init.fold_with(self); + + self.in_lhs_of_var = old_in_lhs_of_var; + v + } +} + +pub fn keep_export(exports: Vec) -> impl Fold { + Repeat::new(KeepExport { + state: State { + keep_exports: exports, + ..Default::default() + }, + in_lhs_of_var: false, + }) +} \ No newline at end of file diff --git a/crates/loader_compilation/src/transform/mod.rs b/crates/loader_compilation/src/transform/mod.rs new file mode 100644 index 0000000..9bc09dd --- /dev/null +++ b/crates/loader_compilation/src/transform/mod.rs @@ -0,0 +1,114 @@ +use std::path::Path; +use anyhow::{Error, Context}; +use either::Either; +use serde::Deserialize; +use swc_core::common::chain; +use swc_core::ecma::{ + transforms::base::pass::noop, visit::Fold, +}; + +mod keep_export; +mod remove_export; + +use keep_export::keep_export; +use remove_export::remove_export; + +macro_rules! either { + ($config:expr, $f:expr) => { + if let Some(config) = &$config { + Either::Left($f(config)) + } else { + Either::Right(noop()) + } + }; + ($config:expr, $f:expr, $enabled:expr) => { + if $enabled() { + either!($config, $f) + } else { + Either::Right(noop()) + } + }; +} + +// Only define the stuct which is used in the following function. +#[derive(Deserialize, Debug)] +struct NestedRoutesManifest { + file: String, + children: Option>, +} + +fn get_routes_file(routes: Vec) -> Vec { + let mut result: Vec = vec![]; + for route in routes { + // Add default prefix of src/pages/ to the route file. + let mut path_str = String::from("src/pages/"); + path_str.push_str(&route.file); + + result.push(path_str.to_string()); + + if let Some(children) = route.children { + result.append(&mut get_routes_file(children)); + } + } + result +} + +fn parse_routes_config(c: String) -> Result, Error> { + let routes = serde_json::from_str(&c)?; + Ok(get_routes_file(routes)) +} + +pub(crate) fn load_routes_config(path: &Path) -> Result, Error> { + let content = std::fs::read_to_string(path).context("failed to read routes config")?; + parse_routes_config(content) +} + +fn match_route_entry(resource_path: &Path, routes: &Vec) -> bool { + let resource_path_str = resource_path.to_str().unwrap(); + for route in routes { + if resource_path_str.ends_with(&route.to_string()) { + return true; + } + } + false +} + +fn match_app_entry(resource_path: &Path) -> bool { + let resource_path_str = resource_path.to_str().unwrap(); + // File path ends with src/app.(ts|tsx|js|jsx) + let regex_for_app = regex::Regex::new(r"src/app\.(ts|tsx|js|jsx)$").unwrap(); + regex_for_app.is_match(resource_path_str) +} + +#[derive(Debug, Default, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct TransformFeatureOptions { + pub keep_export: Option>, + pub remove_export: Option>, +} + +pub(crate) fn transform<'a>( + resource_path: &'a Path, + routes_config: &Vec, + feature_options: &TransformFeatureOptions, +) -> impl Fold + 'a { + chain!( + either!(feature_options.keep_export, |options: &Vec| { + let mut exports_name = options.clone(); + // Special case for app entry. + // When keep pageConfig, we should also keep the default export of app entry. + if match_app_entry(resource_path) && exports_name.contains(&String::from("pageConfig")) { + exports_name.push(String::from("default")); + } + keep_export(exports_name) + }, || { + match_app_entry(resource_path) || match_route_entry(resource_path, routes_config) + }), + either!(feature_options.remove_export, |options: &Vec| { + remove_export(options.clone()) + }, || { + // Remove export only work for app entry and route entry. + match_app_entry(resource_path) || match_route_entry(resource_path, routes_config) + }), + ) +} \ No newline at end of file diff --git a/crates/loader_compilation/src/transform/remove_export.rs b/crates/loader_compilation/src/transform/remove_export.rs new file mode 100644 index 0000000..f58b3c1 --- /dev/null +++ b/crates/loader_compilation/src/transform/remove_export.rs @@ -0,0 +1,586 @@ +// transform code is modified based on swc plugin of remove_export: +// https://github.com/ice-lab/swc-plugins/tree/main/packages/remove-export +use fxhash::FxHashSet; +use std::mem::take; +use swc_core::ecma::{ + ast::*, + visit::{Fold, FoldWith, noop_fold_type}, +}; +use rspack_error::Error; +use swc_core::common::{ + DUMMY_SP, pass::{Repeat, Repeated} +}; + +/// State of the transforms. Shared by the analyzer and the transform. +#[derive(Debug, Default)] +struct State { + /// Identifiers referenced by non-data function codes. + /// + /// Cleared before running each pass, because we drop ast nodes between the + /// passes. + refs_from_other: FxHashSet, + + /// Identifiers referenced by data functions or derivatives. + /// + /// Preserved between runs, because we should remember derivatives of data + /// functions as the data function itself is already removed. + refs_from_data_fn: FxHashSet, + + cur_declaring: FxHashSet, + + should_run_again: bool, + remove_exports: Vec, +} + +impl State { + fn should_remove_identifier(&mut self, i: &Ident) -> Result { + Ok(self.remove_exports.contains(&String::from(&*i.sym))) + } + fn should_remove_default(&mut self) -> bool { + self.remove_exports.contains(&String::from("default")) + } +} + +struct Analyzer<'a> { + state: &'a mut State, + in_lhs_of_var: bool, + in_data_fn: bool, +} + +impl Analyzer<'_> { + fn add_ref(&mut self, id: Id) { + if self.in_data_fn { + self.state.refs_from_data_fn.insert(id); + } else { + if self.state.cur_declaring.contains(&id) { + return; + } + + self.state.refs_from_other.insert(id); + } + } + + fn check_default>(&mut self, e: T) -> T { + if self.state.should_remove_default() { + let old_in_data = self.in_data_fn; + self.in_data_fn = true; + let e = e.fold_children_with(self); + self.in_data_fn = old_in_data; + return e + } + + return e.fold_children_with(self); + } +} + +impl Fold for Analyzer<'_> { + // This is important for reducing binary sizes. + noop_fold_type!(); + + fn fold_binding_ident(&mut self, i: BindingIdent) -> BindingIdent { + if !self.in_lhs_of_var || self.in_data_fn { + self.add_ref(i.id.to_id()); + } + + i + } + + fn fold_export_named_specifier(&mut self, s: ExportNamedSpecifier) -> ExportNamedSpecifier { + if let ModuleExportName::Ident(id) = &s.orig { + if !self.state.remove_exports.contains(&String::from(&*id.sym)) { + self.add_ref(id.to_id()); + } + } + + s + } + + fn fold_export_decl(&mut self, s: ExportDecl) -> ExportDecl { + let old_in_data = self.in_data_fn; + + match &s.decl { + Decl::Fn(f) => { + if let Ok(should_remove_identifier) = self.state.should_remove_identifier(&f.ident) { + if should_remove_identifier { + self.in_data_fn = true; + self.add_ref(f.ident.to_id()); + } + } + } + + Decl::Var(d) => { + if d.decls.is_empty() { + return s; + } + if let Pat::Ident(id) = &d.decls[0].name { + if self.state.remove_exports.contains(&String::from(&*id.id.sym)) { + self.in_data_fn = true; + self.add_ref(id.to_id()); + } + } + } + _ => {} + } + + let e = s.fold_children_with(self); + + self.in_data_fn = old_in_data; + + return e; + } + + fn fold_expr(&mut self, e: Expr) -> Expr { + let e = e.fold_children_with(self); + + if let Expr::Ident(i) = &e { + self.add_ref(i.to_id()); + } + + e + } + + fn fold_jsx_element(&mut self, jsx: JSXElement) -> JSXElement { + fn get_leftmost_id_member_expr(e: &JSXMemberExpr) -> Id { + match &e.obj { + JSXObject::Ident(i) => i.to_id(), + JSXObject::JSXMemberExpr(e) => get_leftmost_id_member_expr(e), + } + } + + match &jsx.opening.name { + JSXElementName::Ident(i) => { + self.add_ref(i.to_id()); + } + JSXElementName::JSXMemberExpr(e) => { + self.add_ref(get_leftmost_id_member_expr(e)); + } + _ => {} + } + + jsx.fold_children_with(self) + } + + fn fold_fn_decl(&mut self, f: FnDecl) -> FnDecl { + let f = f.fold_children_with(self); + if self.in_data_fn { + self.add_ref(f.ident.to_id()); + } + + f + } + + fn fold_fn_expr(&mut self, f: FnExpr) -> FnExpr { + let f = f.fold_children_with(self); + if let Some(id) = &f.ident { + self.add_ref(id.to_id()); + } + + f + } + + /// Drops [ExportDecl] if all specifiers are removed. + fn fold_module_item(&mut self, s: ModuleItem) -> ModuleItem { + match s { + ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)) if !e.specifiers.is_empty() => { + let e = e.fold_with(self); + + if e.specifiers.is_empty() { + return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })); + } + + return ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)); + } + _ => {} + }; + + // Visit children to ensure that all references is added to the scope. + let s = s.fold_children_with(self); + + if let ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(e)) = &s { + match &e.decl { + Decl::Fn(f) => { + if let Ok(should_remove_identifier) = self.state.should_remove_identifier(&f.ident) { + if should_remove_identifier { + return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })); + } + } else { + return s; + } + } + + Decl::Var(d) => { + if d.decls.is_empty() { + return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })); + } + } + _ => {} + } + } + + s + } + + fn fold_named_export(&mut self, mut n: NamedExport) -> NamedExport { + if n.src.is_some() { + n.specifiers = n.specifiers.fold_with(self); + } + + n + } + + fn fold_default_decl(&mut self, d: DefaultDecl) -> DefaultDecl { + return self.check_default(d); + } + + fn fold_export_default_expr(&mut self, e: ExportDefaultExpr) -> ExportDefaultExpr { + return self.check_default(e); + } + + fn fold_prop(&mut self, p: Prop) -> Prop { + let p = p.fold_children_with(self); + if let Prop::Shorthand(i) = &p { + self.add_ref(i.to_id()); + } + p + } + + fn fold_var_declarator(&mut self, mut v: VarDeclarator) -> VarDeclarator { + let old_in_lhs_of_var = self.in_lhs_of_var; + + self.in_lhs_of_var = true; + v.name = v.name.fold_with(self); + + self.in_lhs_of_var = false; + v.init = v.init.fold_with(self); + + self.in_lhs_of_var = old_in_lhs_of_var; + v + } +} + +struct RemoveExport { + pub state: State, + in_lhs_of_var: bool, +} + +impl RemoveExport { + fn should_remove(&self, id: Id) -> bool { + self.state.refs_from_data_fn.contains(&id) && !self.state.refs_from_other.contains(&id) + } + + /// Mark identifiers in `n` as a candidate for removal. + fn mark_as_candidate(&mut self, n: N) -> N + where + N: for<'a> FoldWith>, + { + // Analyzer never change `in_data_fn` to false, so all identifiers in `n` will + // be marked as referenced from a data function. + let mut v = Analyzer { + state: &mut self.state, + in_lhs_of_var: false, + in_data_fn: true, + }; + + let n = n.fold_with(&mut v); + self.state.should_run_again = true; + n + } + + fn create_empty_fn(&mut self) -> FnExpr { + return FnExpr { + ident: None, + function: Box::new(Function { + params: vec![], + body: Some(BlockStmt { + span: DUMMY_SP, + stmts: vec![] + }), + span: DUMMY_SP, + is_generator: false, + is_async: false, + decorators: vec![], + return_type: None, + type_params: None, + }) + }; + } +} + +impl Repeated for RemoveExport { + fn changed(&self) -> bool { + self.state.should_run_again + } + + fn reset(&mut self) { + self.state.refs_from_other.clear(); + self.state.cur_declaring.clear(); + self.state.should_run_again = false; + } +} + +impl Fold for RemoveExport { + // This is important for reducing binary sizes. + noop_fold_type!(); + + // Remove import expression + fn fold_import_decl(&mut self, mut i: ImportDecl) -> ImportDecl { + // Imports for side effects. + if i.specifiers.is_empty() { + return i; + } + + i.specifiers.retain(|s| match s { + ImportSpecifier::Named(ImportNamedSpecifier { local, .. }) + | ImportSpecifier::Default(ImportDefaultSpecifier { local, .. }) + | ImportSpecifier::Namespace(ImportStarAsSpecifier { local, .. }) => { + if self.should_remove(local.to_id()) { + self.state.should_run_again = true; + false + } else { + true + } + } + }); + + i + } + + fn fold_module(&mut self, mut m: Module) -> Module { + { + // Fill the state. + let mut v = Analyzer { + state: &mut self.state, + in_lhs_of_var: false, + in_data_fn: false, + }; + m = m.fold_with(&mut v); + } + + m.fold_children_with(self) + } + + fn fold_module_items(&mut self, mut items: Vec) -> Vec { + items = items.fold_children_with(self); + // Drop nodes. + items.retain(|s| !matches!(s, ModuleItem::Stmt(Stmt::Empty(..)))); + items + } + + fn fold_module_item(&mut self, i: ModuleItem) -> ModuleItem { + if let ModuleItem::ModuleDecl(ModuleDecl::Import(i)) = i { + let is_for_side_effect = i.specifiers.is_empty(); + let i = i.fold_with(self); + + if !is_for_side_effect && i.specifiers.is_empty() { + return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })); + } + return ModuleItem::ModuleDecl(ModuleDecl::Import(i)); + } + + let i = i.fold_children_with(self); + + match &i { + ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)) if e.specifiers.is_empty() => { + return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })) + } + _ => {} + } + + i + } + + fn fold_named_export(&mut self, mut n: NamedExport) -> NamedExport { + n.specifiers = n.specifiers.fold_with(self); + + n.specifiers.retain(|s| { + let preserve = match s { + ExportSpecifier::Namespace(ExportNamespaceSpecifier { + name: ModuleExportName::Ident(exported), + .. + }) + | ExportSpecifier::Default(ExportDefaultSpecifier { exported, .. }) + | ExportSpecifier::Named(ExportNamedSpecifier { + exported: Some(ModuleExportName::Ident(exported)), + .. + }) => self + .state + .should_remove_identifier(exported) + .map(|should_remove_identifier| !should_remove_identifier), + ExportSpecifier::Named(ExportNamedSpecifier { + orig: ModuleExportName::Ident(orig), + .. + }) => self + .state + .should_remove_identifier(orig) + .map(|should_remove_identifier| !should_remove_identifier), + _ => Ok(true), + }; + + match preserve { + Ok(false) => { + if let ExportSpecifier::Named(ExportNamedSpecifier { + orig: ModuleExportName::Ident(orig), + .. + }) = s + { + self.state.should_run_again = true; + self.state.refs_from_data_fn.insert(orig.to_id()); + } + + false + } + Ok(true) => true, + Err(_) => false, + } + }); + + n + } + + fn fold_default_decl(&mut self, d: DefaultDecl) -> DefaultDecl { + if self.state.should_remove_default() { + // Replace with an empty function + return DefaultDecl::Fn(self.create_empty_fn()) + } + d + } + + fn fold_export_default_expr(&mut self, n: ExportDefaultExpr) -> ExportDefaultExpr { + if self.state.should_remove_default() { + // Replace with an empty function + return ExportDefaultExpr { + span: DUMMY_SP, + expr: Box::new(Expr::Fn(self.create_empty_fn())) + }; + } + n + } + + /// This methods returns [Pat::Invalid] if the pattern should be removed. + fn fold_pat(&mut self, mut p: Pat) -> Pat { + p = p.fold_children_with(self); + + if self.in_lhs_of_var { + match &mut p { + Pat::Ident(name) => { + if self.should_remove(name.id.to_id()) { + self.state.should_run_again = true; + return Pat::Invalid(Invalid { span: DUMMY_SP }); + } + } + Pat::Array(arr) => { + if !arr.elems.is_empty() { + arr.elems.retain(|e| !matches!(e, Some(Pat::Invalid(..)))); + + if arr.elems.is_empty() { + return Pat::Invalid(Invalid { span: DUMMY_SP }); + } + } + } + Pat::Object(obj) => { + if !obj.props.is_empty() { + obj.props = take(&mut obj.props) + .into_iter() + .filter_map(|prop| match prop { + ObjectPatProp::KeyValue(prop) => { + if prop.value.is_invalid() { + None + } else { + Some(ObjectPatProp::KeyValue(prop)) + } + } + ObjectPatProp::Assign(prop) => { + if self.should_remove(prop.key.to_id()) { + self.mark_as_candidate(prop.value); + + None + } else { + Some(ObjectPatProp::Assign(prop)) + } + } + ObjectPatProp::Rest(prop) => { + if prop.arg.is_invalid() { + None + } else { + Some(ObjectPatProp::Rest(prop)) + } + } + }) + .collect(); + + if obj.props.is_empty() { + return Pat::Invalid(Invalid { span: DUMMY_SP }); + } + } + } + Pat::Rest(rest) => { + if rest.arg.is_invalid() { + return Pat::Invalid(Invalid { span: DUMMY_SP }); + } + } + _ => {} + } + } + + p + } + + #[allow(clippy::single_match)] + fn fold_stmt(&mut self, mut s: Stmt) -> Stmt { + match s { + Stmt::Decl(Decl::Fn(f)) => { + if self.should_remove(f.ident.to_id()) { + self.mark_as_candidate(f.function); + return Stmt::Empty(EmptyStmt { span: DUMMY_SP }); + } + + s = Stmt::Decl(Decl::Fn(f)); + } + _ => {} + } + + let s = s.fold_children_with(self); + match s { + Stmt::Decl(Decl::Var(v)) if v.decls.is_empty() => { + return Stmt::Empty(EmptyStmt { span: DUMMY_SP }); + } + _ => {} + } + + s + } + + /// This method make `name` of [VarDeclarator] to [Pat::Invalid] if it + /// should be removed. + fn fold_var_declarator(&mut self, mut d: VarDeclarator) -> VarDeclarator { + let old = self.in_lhs_of_var; + self.in_lhs_of_var = true; + let name = d.name.fold_with(self); + + self.in_lhs_of_var = false; + if name.is_invalid() { + d.init = self.mark_as_candidate(d.init); + } + let init = d.init.fold_with(self); + self.in_lhs_of_var = old; + + VarDeclarator { name, init, ..d } + } + + fn fold_var_declarators(&mut self, mut decls: Vec) -> Vec { + decls = decls.fold_children_with(self); + decls.retain(|d| !d.name.is_invalid()); + + decls + } +} + +pub fn remove_export(exports: Vec) -> impl Fold { + Repeat::new(RemoveExport { + state: State { + remove_exports: exports, + ..Default::default() + }, + in_lhs_of_var: false, + }) +} \ No newline at end of file diff --git a/crates/loader_compilation/tests/fixtures.rs b/crates/loader_compilation/tests/fixtures.rs index d99f90b..ae7386a 100644 --- a/crates/loader_compilation/tests/fixtures.rs +++ b/crates/loader_compilation/tests/fixtures.rs @@ -1,19 +1,30 @@ use std::{str::FromStr,env, fs,path::{Path, PathBuf}, sync::Arc}; -use loader_compilation::CompilationLoader; -use rspack_core::{run_loaders, CompilerContext, CompilerOptions, SideEffectOption}; -use rspack_loader_runner::ResourceData; +use loader_compilation::{CompilationLoader, LoaderOptions}; +use rspack_core::{ + run_loaders, CompilerContext, CompilerOptions, Loader, LoaderRunnerContext, ResourceData, SideEffectOption, +}; +use swc_core::base::config::Config; async fn loader_test(actual: impl AsRef, expected: impl AsRef) { let tests_path = PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"))).join("tests"); - let actual_path = tests_path.join(actual); let expected_path = tests_path.join(expected); + let actual_path = tests_path.join(actual); + let parent_path = actual_path.parent().unwrap().to_path_buf(); + + let mut options = Config::default(); let (result, _) = run_loaders( - &[Arc::new(CompilationLoader::default())], + &[Arc::new(CompilationLoader::new(LoaderOptions { + swc_options: options, + transform_features: Default::default(), + compile_rules: Default::default(), + })) as Arc>], &ResourceData::new(actual_path.to_string_lossy().to_string(), actual_path), &[], CompilerContext { + module: None, + module_context: None, options: std::sync::Arc::new(CompilerOptions { - context: rspack_core::Context::default(), + context: rspack_core::Context::new(parent_path.to_string_lossy().to_string()), dev_server: rspack_core::DevServerOptions::default(), devtool: rspack_core::Devtool::from("source-map".to_string()), mode: rspack_core::Mode::None, @@ -71,11 +82,12 @@ async fn loader_test(actual: impl AsRef, expected: impl AsRef) { side_effects: SideEffectOption::False, provided_exports: Default::default(), used_exports: Default::default(), + inner_graph: Default::default(), }, profile: false, }), resolver_factory: Default::default(), - } + }, ) .await .expect("TODO:") diff --git a/crates/loader_compilation/tests/fixtures/basic/.ice/route-manifest.json b/crates/loader_compilation/tests/fixtures/basic/.ice/route-manifest.json new file mode 100644 index 0000000..cfcf422 --- /dev/null +++ b/crates/loader_compilation/tests/fixtures/basic/.ice/route-manifest.json @@ -0,0 +1,22 @@ +[ + { + "path": "error", + "id": "error", + "file": "error.tsx", + "componentName": "error", + "layout": false, + "exports": [ + "default" + ] + }, + { + "index": true, + "id": "/", + "file": "index.tsx", + "componentName": "index", + "layout": false, + "exports": [ + "default" + ] + } +] \ No newline at end of file diff --git a/crates/loader_compilation/tests/fixtures/basic/input.js b/crates/loader_compilation/tests/fixtures/basic/input.js index 54b82a0..f11cfe5 100644 --- a/crates/loader_compilation/tests/fixtures/basic/input.js +++ b/crates/loader_compilation/tests/fixtures/basic/input.js @@ -1 +1,8 @@ const a = 1; +const b = 2; + +export const dataLoader = { + b, +}; + +export default a; \ No newline at end of file diff --git a/scripts/clone-rspack.mjs b/scripts/clone-rspack.mjs index 428c9df..58c7cad 100644 --- a/scripts/clone-rspack.mjs +++ b/scripts/clone-rspack.mjs @@ -1,3 +1,3 @@ import { getRspackCrates } from './github.mjs'; -getRspackCrates(); \ No newline at end of file +getRspackCrates();