diff --git a/Cargo.lock b/Cargo.lock index 1e372e9e2b2..455c4dbdec9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1272,6 +1272,7 @@ dependencies = [ "indoc", "path-slash", "pathdiff", + "petgraph", "serde", "serde_bytes", "sha-1", @@ -1386,9 +1387,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "petgraph" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5014253a1331579ce62aa67443b4a658c5e7dd03d4bc6d302b94474888143" +checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" dependencies = [ "fixedbitset", "indexmap", diff --git a/packages/core/types/index.js b/packages/core/types/index.js index 50cb98f4a73..bed47202aec 100644 --- a/packages/core/types/index.js +++ b/packages/core/types/index.js @@ -931,7 +931,10 @@ export type TransformerResult = {| */ +sideEffects?: boolean, /** The symbols that the asset exports. */ - +symbols?: $ReadOnlyMap, + +symbols?: ?$ReadOnlyMap< + Symbol, + {|local: Symbol, loc: ?SourceLocation, meta?: ?Meta|}, + >, /** * When a transformer returns multiple assets, it can give them unique keys to identify them. * This can be used to find assets during packaging, or to create dependencies between multiple diff --git a/packages/packagers/js/src/ScopeHoistingPackager.js b/packages/packagers/js/src/ScopeHoistingPackager.js index 81a70ba9463..bc6ef190e61 100644 --- a/packages/packagers/js/src/ScopeHoistingPackager.js +++ b/packages/packagers/js/src/ScopeHoistingPackager.js @@ -36,7 +36,7 @@ const NON_ID_CONTINUE_RE = /[^$_\u200C\u200D\p{ID_Continue}]/gu; // General regex used to replace imports with the resolved code, references with resolutions, // and count the number of newlines in the file for source maps. const REPLACEMENT_RE = - /\n|import\s+"([0-9a-f]{16}:.+?)";|(?:\$[0-9a-f]{16}\$exports)|(?:\$[0-9a-f]{16}\$(?:import|importAsync|require)\$[0-9a-f]+(?:\$[0-9a-f]+)?)/g; + /\n|import\s+"([0-9a-f]{16,}:.+?)";|(?:\$[0-9a-f]{16,}\$exports)|(?:\$[0-9a-f]{16,}\$(?:import|importAsync|require)\$[0-9a-f]+(?:\$[0-9a-f]+)?)/g; const BUILTINS = Object.keys(globals.builtin); const GLOBALS_BY_CONTEXT = { @@ -443,6 +443,7 @@ export class ScopeHoistingPackager { } let [depMap, replacements] = this.buildReplacements(asset, deps); + // console.log('ASSET', asset.id, asset.meta.id, depMap, '-', code, '-'); let [prepend, prependLines, append] = this.buildAssetPrelude(asset, deps); if (prependLines > 0) { sourceMap?.offsetLines(1, prependLines); @@ -474,6 +475,7 @@ export class ScopeHoistingPackager { // If we matched an import, replace with the source code for the dependency. if (d != null) { let deps = depMap.get(d); + // console.log('match', d, deps); if (!deps) { return m; } diff --git a/packages/transformers/js/core/Cargo.toml b/packages/transformers/js/core/Cargo.toml index 56c1aea55be..40c4218cfe9 100644 --- a/packages/transformers/js/core/Cargo.toml +++ b/packages/transformers/js/core/Cargo.toml @@ -21,3 +21,4 @@ dunce = "1.0.1" pathdiff = "0.2.0" path-slash = "0.1.4" indexmap = "1.9.2" +petgraph = "0.6.3" diff --git a/packages/transformers/js/core/src/collect.rs b/packages/transformers/js/core/src/collect.rs index 163069f6a82..8c20c507613 100644 --- a/packages/transformers/js/core/src/collect.rs +++ b/packages/transformers/js/core/src/collect.rs @@ -24,6 +24,19 @@ macro_rules! collect_visit_fn { }; } +pub fn collect( + module: &Module, + source_map: Lrc, + decls: HashSet, + ignore_mark: Mark, + global_mark: Mark, + trace_bailouts: bool, +) -> Collect { + let mut collect = Collect::new(source_map, decls, ignore_mark, global_mark, trace_bailouts); + module.visit_with(&mut collect); + collect +} + #[derive(Debug, Deserialize, PartialEq, Eq, Clone, Copy, Serialize)] pub enum ImportKind { Require, diff --git a/packages/transformers/js/core/src/decl_collector.rs b/packages/transformers/js/core/src/decl_collector.rs index 8bc321dc249..13338e6b9bf 100644 --- a/packages/transformers/js/core/src/decl_collector.rs +++ b/packages/transformers/js/core/src/decl_collector.rs @@ -1,6 +1,6 @@ use std::collections::HashSet; -use swc_ecmascript::ast::{self, Id}; +use swc_ecmascript::ast::{self, ClassExpr, FnExpr, Id}; use swc_ecmascript::visit::{Visit, VisitWith}; /// This pass collects all declarations in a module into a single HashSet of tuples @@ -41,6 +41,18 @@ impl Visit for DeclCollector { node.visit_children_with(self); } + fn visit_default_decl(&mut self, node: &ast::DefaultDecl) { + match node { + ast::DefaultDecl::Class(ClassExpr { ident, .. }) + | ast::DefaultDecl::Fn(FnExpr { ident, .. }) => { + if let Some(ident) = ident { + self.decls.insert((ident.sym.clone(), ident.span.ctxt())); + } + } + _ => {} + } + } + fn visit_var_declarator(&mut self, node: &ast::VarDeclarator) { self.in_var = true; node.name.visit_with(self); diff --git a/packages/transformers/js/core/src/lib.rs b/packages/transformers/js/core/src/lib.rs index f16d71b3dea..2914147c579 100644 --- a/packages/transformers/js/core/src/lib.rs +++ b/packages/transformers/js/core/src/lib.rs @@ -7,6 +7,7 @@ mod global_replacer; mod hoist; mod modules; mod node_replacer; +mod split; mod typeof_replacer; mod utils; @@ -17,10 +18,11 @@ use std::str::FromStr; use indexmap::IndexMap; use path_slash::PathExt; use serde::{Deserialize, Serialize}; +use split::split; use swc_common::comments::SingleThreadedComments; use swc_common::errors::{DiagnosticBuilder, Emitter, Handler}; use swc_common::{chain, sync::Lrc, FileName, Globals, Mark, SourceMap}; -use swc_ecmascript::ast::{Module, ModuleItem, Program}; +use swc_ecmascript::ast::{Id, Module, ModuleItem, Program}; use swc_ecmascript::codegen::text_writer::JsWriter; use swc_ecmascript::parser::lexer::Lexer; use swc_ecmascript::parser::{EsConfig, PResult, Parser, StringInput, Syntax, TsConfig}; @@ -32,9 +34,9 @@ use swc_ecmascript::transforms::{ pass::Optional, proposals::decorators, react, typescript, }; use swc_ecmascript::transforms::{resolver, Assumptions}; -use swc_ecmascript::visit::{FoldWith, VisitWith}; +use swc_ecmascript::visit::FoldWith; -use collect::{Collect, CollectResult}; +use collect::{collect, CollectResult}; use decl_collector::*; use dependency_collector::*; use env_replacer::*; @@ -56,6 +58,7 @@ pub struct Config { module_id: String, project_root: String, replace_env: bool, + side_effects: bool, env: HashMap, inline_fs: bool, insert_node_globals: bool, @@ -83,6 +86,12 @@ pub struct Config { is_swc_helpers: bool, } +#[derive(Serialize, Debug, Default)] +pub struct TransformResultOuter { + main_module: TransformResult, + modules: Vec<(String, TransformResult)>, +} + #[derive(Serialize, Debug, Default)] pub struct TransformResult { #[serde(with = "serde_bytes")] @@ -137,10 +146,7 @@ impl Emitter for ErrorBuffer { } } -pub fn transform(config: Config) -> Result { - let mut result = TransformResult::default(); - let mut map_buf = vec![]; - +pub fn transform(config: Config) -> Result { let code = unsafe { std::str::from_utf8_unchecked(&config.code) }; let source_map = Lrc::new(SourceMap::default()); let module = parse( @@ -157,12 +163,17 @@ pub fn transform(config: Config) -> Result { let handler = Handler::with_emitter(true, false, Box::new(error_buffer.clone())); err.into_diagnostic(&handler).emit(); - result.diagnostics = Some(error_buffer_to_diagnostics(&error_buffer, &source_map)); - Ok(result) + Ok(TransformResultOuter { + main_module: TransformResult { + diagnostics: Some(error_buffer_to_diagnostics(&error_buffer, &source_map)), + ..Default::default() + }, + modules: vec![], + }) } Ok((module, comments)) => { let mut module = module; - result.shebang = match &mut module { + let shebang = match &mut module { Program::Module(module) => module.shebang.take().map(|s| s.to_string()), Program::Script(script) => script.shebang.take().map(|s| s.to_string()), }; @@ -185,7 +196,7 @@ pub fn transform(config: Config) -> Result { &helpers::Helpers::new( /* external helpers from @swc/helpers */ should_import_swc_helpers, ), - || { + || -> Result { let mut react_options = react::Options::default(); if config.is_jsx { if let Some(jsx_pragma) = &config.jsx_pragma { @@ -293,6 +304,8 @@ pub fn transform(config: Config) -> Result { assumptions.set_public_class_fields |= true; } + let mut used_env = HashSet::new(); + let mut diagnostics = vec![]; let module = { let mut passes = chain!( @@ -307,7 +320,7 @@ pub fn transform(config: Config) -> Result { env: &config.env, is_browser: config.is_browser, decls: &decls, - used_env: &mut result.used_env, + used_env: &mut used_env, source_map: &source_map, diagnostics: &mut diagnostics, unresolved_mark @@ -337,6 +350,8 @@ pub fn transform(config: Config) -> Result { module.fold_with(&mut passes) }; + let mut has_node_replacements = false; + let module = module.fold_with( // Replace __dirname and __filename with placeholders in Node env &mut Optional::new( @@ -349,7 +364,7 @@ pub fn transform(config: Config) -> Result { filename: Path::new(&config.filename), decls: &mut decls, scope_hoist: config.scope_hoist, - has_node_replacements: &mut result.has_node_replacements, + has_node_replacements: &mut has_node_replacements, }, config.node_replacer, ), @@ -406,90 +421,79 @@ pub fn transform(config: Config) -> Result { }; let ignore_mark = Mark::fresh(Mark::root()); - let module = module.fold_with( - // Collect dependencies - &mut dependency_collector( - &source_map, - &mut result.dependencies, - &decls, - ignore_mark, - unresolved_mark, - &config, - &mut diagnostics, - ), - ); - - diagnostics.extend(error_buffer_to_diagnostics(&error_buffer, &source_map)); - - if diagnostics - .iter() - .any(|d| d.severity == DiagnosticSeverity::Error) - { - result.diagnostics = Some(diagnostics); - return Ok(result); - } - - let mut collect = Collect::new( + let collect = collect( + &module, source_map.clone(), decls, ignore_mark, global_mark, config.trace_bailouts, ); - module.visit_with(&mut collect); - if let Some(bailouts) = &collect.bailouts { - diagnostics.extend(bailouts.iter().map(|bailout| bailout.to_diagnostic())); - } - - let module = if config.scope_hoist { - let res = hoist(module, config.module_id.as_str(), unresolved_mark, &collect); - match res { - Ok((module, hoist_result, hoist_diagnostics)) => { - result.hoist_result = Some(hoist_result); - diagnostics.extend(hoist_diagnostics); - module - } - Err(diagnostics) => { - result.diagnostics = Some(diagnostics); - return Ok(result); - } - } + let (main_module, modules) = if config.scope_hoist + && config.side_effects == false + && !collect.should_wrap + && !collect.has_cjs_exports + { + split(&config.module_id, module, &collect) } else { - // Bail if we could not statically analyze. - if collect.static_cjs_exports && !collect.should_wrap { - result.symbol_result = Some(collect.into()); - } - - let (module, needs_helpers) = esm2cjs(module, unresolved_mark, versions); - result.needs_esm_helpers = needs_helpers; - module + (module, vec![]) }; - let module = module.fold_with(&mut chain!( - reserved_words(), - hygiene(), - fixer(Some(&comments)), - )); - - result.dependencies.extend(global_deps); - result.dependencies.extend(fs_deps); - - if !diagnostics.is_empty() { - result.diagnostics = Some(diagnostics); - } - - let (buf, src_map_buf) = - emit(source_map.clone(), comments, &module, config.source_maps)?; - if config.source_maps - && source_map - .build_source_map(&src_map_buf) - .to_writer(&mut map_buf) - .is_ok() - { - result.map = Some(String::from_utf8(map_buf).unwrap()); - } - result.code = buf; - Ok(result) + Ok(TransformResultOuter { + main_module: finish_module( + main_module, + TransformResult { + shebang, + used_env: used_env.clone(), + has_node_replacements, + ..Default::default() + }, + &source_map, + &collect.decls, + ignore_mark, + global_mark, + unresolved_mark, + &config, + &config.module_id, + diagnostics, + &error_buffer, + versions, + comments.clone(), + global_deps.clone(), + fs_deps.clone(), + )?, + modules: modules + .into_iter() + .map( + |(i, module)| -> Result<(String, TransformResult), std::io::Error> { + Ok(( + i.clone(), + finish_module( + module, + TransformResult { + used_env: used_env.clone(), + has_node_replacements, + ..Default::default() + }, + &source_map, + &collect.decls, + ignore_mark, + global_mark, + unresolved_mark, + &config, + &i, + vec![], + &error_buffer, + versions, + comments.clone(), + global_deps.clone(), + fs_deps.clone(), + )?, + )) + }, + ) + .collect::, _>>()?, + }) }, ) }) @@ -498,6 +502,109 @@ pub fn transform(config: Config) -> Result { } } +fn finish_module( + module: Module, + mut result: TransformResult, + source_map: &Lrc, + decls: &HashSet, + ignore_mark: swc_common::Mark, + global_mark: swc_common::Mark, + unresolved_mark: swc_common::Mark, + config: &Config, + module_id: &str, + mut diagnostics: Vec, + error_buffer: &ErrorBuffer, + versions: Option, + comments: SingleThreadedComments, + global_deps: Vec, + fs_deps: Vec, +) -> Result { + let module = module.fold_with( + // Collect dependencies + &mut dependency_collector( + &source_map, + &mut result.dependencies, + &decls, + ignore_mark, + unresolved_mark, + &config, + &mut diagnostics, + ), + ); + + diagnostics.extend(error_buffer_to_diagnostics(&error_buffer, &source_map)); + + if diagnostics + .iter() + .any(|d| d.severity == DiagnosticSeverity::Error) + { + result.diagnostics = Some(diagnostics); + return Ok(result); + } + + let collect = collect( + &module, + source_map.clone(), + decls.clone(), + ignore_mark, + global_mark, + config.trace_bailouts, + ); + if let Some(bailouts) = &collect.bailouts { + diagnostics.extend(bailouts.iter().map(|bailout| bailout.to_diagnostic())); + } + + let module = if config.scope_hoist { + let res = hoist(module, module_id, unresolved_mark, &collect); + match res { + Ok((module, hoist_result, hoist_diagnostics)) => { + result.hoist_result = Some(hoist_result); + diagnostics.extend(hoist_diagnostics); + module + } + Err(diagnostics) => { + result.diagnostics = Some(diagnostics); + return Ok(result); + } + } + } else { + // Bail if we could not statically analyze. + if collect.static_cjs_exports && !collect.should_wrap { + result.symbol_result = Some(collect.into()); + } + + let (module, needs_helpers) = esm2cjs(module, unresolved_mark, versions); + result.needs_esm_helpers = needs_helpers; + module + }; + + let module = module.fold_with(&mut chain!( + reserved_words(), + hygiene(), + fixer(Some(&comments)), + )); + + result.dependencies.extend(global_deps); + result.dependencies.extend(fs_deps); + + if !diagnostics.is_empty() { + result.diagnostics = Some(diagnostics); + } + + let mut map_buf = vec![]; + let (buf, src_map_buf) = emit(source_map.clone(), comments, &module, config.source_maps)?; + if config.source_maps + && source_map + .build_source_map(&src_map_buf) + .to_writer(&mut map_buf) + .is_ok() + { + result.map = Some(String::from_utf8(map_buf).unwrap()); + } + result.code = buf; + Ok(result) +} + fn parse( code: &str, project_root: &str, diff --git a/packages/transformers/js/core/src/split.rs b/packages/transformers/js/core/src/split.rs new file mode 100644 index 00000000000..e6b4834f02f --- /dev/null +++ b/packages/transformers/js/core/src/split.rs @@ -0,0 +1,1385 @@ +use std::{ + collections::{hash_map::Entry, HashMap, HashSet}, + fmt::{Debug, Formatter}, + hash::{Hash, Hasher}, + iter::Peekable, + mem::discriminant, +}; + +use indexmap::IndexMap; +use petgraph::{ + dot::{Config, Dot}, + prelude::{DiGraph, EdgeIndex, NodeIndex}, + visit::{ + DfsPostOrder, EdgeRef, GraphRef, IntoNeighbors, IntoNeighborsDirected, VisitMap, Visitable, + }, +}; +use swc_atoms::JsWord; +use swc_common::{SyntaxContext, DUMMY_SP}; +use swc_ecmascript::{ + ast::{ + AssignExpr, ClassExpr, Decl, DefaultDecl, ExportDecl, ExportNamedSpecifier, + ExportNamespaceSpecifier, ExportSpecifier, FnExpr, Id, Ident, ImportDecl, + ImportDefaultSpecifier, ImportNamedSpecifier, ImportSpecifier, ImportStarAsSpecifier, + MemberProp, Module, ModuleDecl, ModuleExportName, ModuleItem, NamedExport, PropName, Stmt, + UnaryExpr, UnaryOp, UpdateExpr, VarDecl, + }, + visit::{Visit, VisitWith}, +}; + +use crate::{collect::Collect, utils::match_export_name_ident}; + +#[derive(Debug)] +struct ValueGraph { + pub graph: DiGraph, + pub nodes: HashMap, +} + +impl ValueGraph { + fn new() -> Self { + Self { + graph: DiGraph::new(), + nodes: HashMap::new(), + } + } + + fn add_node(&mut self, node: ValueGraphNode) -> NodeIndex { + if let Some(existing) = self.nodes.get(&node) { + *existing + } else { + let n = self.graph.add_node(node.clone()); + self.nodes.insert(node, n); + n + } + } + fn add_update_node_exact(&mut self, node: ValueGraphNode) -> NodeIndex { + if let Some(existing) = self.nodes.get(&node).copied() { + if !self.graph.node_weight(existing).unwrap().exact_eq(&node) { + *self.graph.node_weight_mut(existing).unwrap() = node.clone(); + self.nodes.insert(node, existing); + } + existing + } else { + let n = self.graph.add_node(node.clone()); + self.nodes.insert(node, n); + n + } + } + fn add_edge(&mut self, a: NodeIndex, b: NodeIndex, edge: ValueGraphEdge) -> EdgeIndex { + self.graph.add_edge(a, b, edge) + } + + fn get_node(&self, node: &ValueGraphNode) -> Option { + self.nodes.get(node).copied() + } + + fn add_referenced_locals VisitWith>>( + &mut self, + id_parent: NodeIndex, + name: Option<&Id>, + node: &T, + collect: &Collect, + ) { + let references = find_referenced_locals(node, collect); + for read in references.reads { + if name.as_ref().map_or(false, |ident| &read == *ident) { + continue; + }; + let id_read = self.add_node(ValueGraphNode::Binding(read)); + self.add_edge(id_parent, id_read, ValueGraphEdge::Read); + } + for write in references.writes { + if name.as_ref().map_or(false, |ident| &write == *ident) { + continue; + }; + let id_write = self.add_node(ValueGraphNode::Binding(write)); + self.add_edge(id_parent, id_write, ValueGraphEdge::Write); + } + } + + fn get_exports_of_binding(&self, node: NodeIndex) -> Peekable> { + self + .graph + .neighbors_directed(node, petgraph::Direction::Incoming) + .filter_map(|n| { + let value = self.graph.node_weight(n).unwrap(); + if let ValueGraphNode::Export(name) = value { + Some(name) + } else { + None + } + }) + .peekable() + } + + fn get_single_child_node(&self, node: NodeIndex) -> NodeIndex { + let mut iter = self + .graph + .neighbors_directed(node, petgraph::Direction::Outgoing); + let value = iter.next().unwrap(); + assert!(iter.next().is_none()); + value + } +} + +#[derive(Clone)] +enum ValueGraphNode { + Export(JsWord), + Statement(usize), + Binding(Id), + ImportedBinding((Id, JsWord, ModuleExportName)), +} + +impl Debug for ValueGraphNode { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + match self { + ValueGraphNode::Export(name) => f.debug_tuple("Export").field(&name.to_string()).finish(), + ValueGraphNode::Statement(idx) => f.debug_tuple("Statement").field(idx).finish(), + ValueGraphNode::Binding(id) => f + .debug_tuple("Binding") + .field(&(id.0.to_string(), id.1)) + .finish(), + ValueGraphNode::ImportedBinding((id, source, imported)) => f + .debug_tuple("ImportedBinding") + .field(&(id.0.to_string(), id.1)) + .field(&source.to_string()) + .field(&match imported { + ModuleExportName::Ident(ident) => ident.sym.to_string(), + ModuleExportName::Str(v) => v.value.to_string(), + }) + .finish(), + } + } +} + +impl ValueGraphNode { + fn is_imported_binding(&self) -> bool { + matches!(self, ValueGraphNode::ImportedBinding(_)) + } + #[inline] + fn exact_eq(&self, other: &ValueGraphNode) -> bool { + let self_tag = discriminant(self); + let other_tag = discriminant(other); + self_tag == other_tag + && match (self, other) { + (ValueGraphNode::Export(a), ValueGraphNode::Export(b)) => a == b, + (ValueGraphNode::Binding(a), ValueGraphNode::Binding(b)) => a == b, + (ValueGraphNode::Statement(a), ValueGraphNode::Statement(b)) => a == b, + (ValueGraphNode::ImportedBinding(a), ValueGraphNode::ImportedBinding(b)) => a == b, + _ => unreachable!(), + } + } +} + +impl Hash for ValueGraphNode { + fn hash(&self, state: &mut H) { + match self { + ValueGraphNode::Export(name) => { + 0.hash(state); + name.hash(state); + } + ValueGraphNode::Statement(idx) => { + 1.hash(state); + idx.hash(state); + } + ValueGraphNode::Binding(id) | ValueGraphNode::ImportedBinding((id, _, _)) => { + 2.hash(state); + id.hash(state); + } + } + } +} +impl PartialEq for ValueGraphNode { + #[inline] + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (ValueGraphNode::Export(a), ValueGraphNode::Export(b)) => a == b, + (ValueGraphNode::Statement(a), ValueGraphNode::Statement(b)) => a == b, + (ValueGraphNode::Binding(a), ValueGraphNode::Binding(b)) + | (ValueGraphNode::ImportedBinding((a, _, _)), ValueGraphNode::Binding(b)) + | (ValueGraphNode::Binding(a), ValueGraphNode::ImportedBinding((b, _, _))) + | (ValueGraphNode::ImportedBinding((a, _, _)), ValueGraphNode::ImportedBinding((b, _, _))) => { + a == b + } + _ => false, + } + } +} +impl Eq for ValueGraphNode {} + +#[derive(Debug, PartialEq, Eq)] +enum ValueGraphEdge { + Read, + Write, +} + +const COMMON_ID: usize = 1; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +struct ModuleIndex(usize); +impl ModuleIndex { + pub fn common() -> Self { + ModuleIndex(COMMON_ID) + } + + pub fn index(&self) -> usize { + self.0 + } + + pub fn as_import_specifier(&self, module_id: &str) -> String { + format!("{}{}", module_id, self.index()) + } +} + +// const OFFSET: usize = 2; + +pub fn split( + module_id: &str, + module: Module, + collect: &Collect, +) -> (Module, Vec<(String, Module)>) { + let mut graph = ValueGraph::new(); + + let mut imports: IndexMap = IndexMap::new(); + + for (i, it) in module.body.iter().enumerate() { + match it { + ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(decl)) => { + for (name, decl) in get_decl_names(&decl.decl) { + let id_export = graph.add_node(ValueGraphNode::Export(name.0.clone())); + let id_local = graph.add_node(ValueGraphNode::Binding(name.clone())); + graph.add_edge(id_export, id_local, ValueGraphEdge::Read); + graph.add_referenced_locals(id_local, Some(&name), &decl, collect); + } + } + ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(decl)) => { + let name = match &decl.decl { + DefaultDecl::Class(ClassExpr { + ident: Some(ident), .. + }) + | DefaultDecl::Fn(FnExpr { + ident: Some(ident), .. + }) => Some(ident.to_id()), + _ => None, + }; + + let id_export = graph.add_node(ValueGraphNode::Export("default".into())); + let id_local = graph.add_node(if let Some(name) = &name { + ValueGraphNode::Binding(name.clone()) + } else { + ValueGraphNode::Binding(("default export".into(), SyntaxContext::empty())) + }); + graph.add_edge(id_export, id_local, ValueGraphEdge::Read); + graph.add_referenced_locals(id_local, name.as_ref(), &decl.decl, collect); + } + ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(expr)) => { + let id_export = graph.add_node(ValueGraphNode::Export("default".into())); + let id_local = graph.add_node(ValueGraphNode::Binding(( + "default export".into(), + SyntaxContext::empty(), + ))); + graph.add_edge(id_export, id_local, ValueGraphEdge::Read); + graph.add_referenced_locals(id_local, None, &expr.expr, collect); + } + ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(NamedExport { + specifiers, + src: None, + .. + })) => { + for spec in specifiers { + let (local, exported) = match spec { + ExportSpecifier::Namespace(_) => continue, + ExportSpecifier::Default(v) => (v.exported.to_id(), "default".into()), + ExportSpecifier::Named(spec) => ( + match_export_name_ident(&spec.orig).to_id(), + match spec.exported.as_ref().unwrap_or(&spec.orig) { + ModuleExportName::Ident(v) => v.sym.clone(), + ModuleExportName::Str(v) => v.value.clone(), + }, + ), + }; + let id_export = graph.add_node(ValueGraphNode::Export(exported)); + let id_local = graph.add_node(ValueGraphNode::Binding(local)); + graph.add_edge(id_export, id_local, ValueGraphEdge::Read); + // uses get added below + } + } + ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(_)) + | ModuleItem::ModuleDecl(ModuleDecl::ExportAll(_)) => { + // Ignore, no need to include this in the graph + } + ModuleItem::ModuleDecl(ModuleDecl::Import(decl)) => { + imports.insert( + decl.src.value.clone(), + ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { + span: decl.span, + specifiers: vec![], + src: decl.src.clone(), + type_only: decl.type_only, + asserts: decl.asserts.clone(), + })), + ); + + for spec in &decl.specifiers { + match spec { + ImportSpecifier::Named(ImportNamedSpecifier { + imported, local, .. + }) => { + graph.add_update_node_exact(ValueGraphNode::ImportedBinding(( + local.to_id(), + decl.src.value.clone(), + imported + .clone() + .unwrap_or_else(|| ModuleExportName::Ident(local.clone())), + ))); + } + ImportSpecifier::Default(ImportDefaultSpecifier { local, .. }) => { + graph.add_update_node_exact(ValueGraphNode::ImportedBinding(( + local.to_id(), + decl.src.value.clone(), + ModuleExportName::Ident((JsWord::from("default"), SyntaxContext::empty()).into()), + ))); + } + ImportSpecifier::Namespace(ImportStarAsSpecifier { local, .. }) => { + graph.add_update_node_exact(ValueGraphNode::ImportedBinding(( + local.to_id(), + decl.src.value.clone(), + ModuleExportName::Str(JsWord::from("*").into()), + ))); + } + } + } + } + ModuleItem::Stmt(Stmt::Decl(decl)) => { + for (name, decl) in get_decl_names(decl) { + let id_local = graph.add_node(ValueGraphNode::Binding(name.clone())); + graph.add_referenced_locals(id_local, Some(&name), &decl, collect); + } + } + ModuleItem::Stmt(stmt) => { + let id_stmt = graph.add_node(ValueGraphNode::Statement(i)); + graph.add_referenced_locals(id_stmt, None, stmt, collect); + } + _ => unreachable!(), + } + } + + // node index -> final module index + // let graph_assignment: HashMap = { + // // node index -> export/statement nodes which use it + // let mut users: HashMap> = HashMap::new(); + // for root_idx in graph.graph.node_indices() { + // if let ValueGraphNode::Export(_) | ValueGraphNode::Statement(_) = + // graph.graph.node_weight(root_idx).unwrap() + // { + // let mut bfs = Bfs::new(&graph.graph, root_idx); + // while let Some(nx) = bfs.next(&graph.graph) { + // if nx == root_idx { + // continue; + // } + // users + // .entry(nx.index()) + // .or_default() + // .insert(root_idx.index()); + // } + // } + // } + + // // [export/statements nodes] -> [uses of these nodes] + // let mut users_reverse: HashMap, Vec> = HashMap::new(); + // for (node_idx, export_idx) in &users { + // let mut k: Vec = export_idx.iter().cloned().collect(); + // k.sort(); + // users_reverse.entry(k).or_default().push(*node_idx); + // } + + // println!("{:?}", users); + // println!("{:?}", users_reverse); + + // HashMap::from_iter( + // users_reverse + // .iter() + // .enumerate() + // .flat_map(|(i, (exports, uses))| { + // exports.iter().chain(uses.iter()).map(move |v| (*v, i)) + // }), + // ) + // }; + + // Visit write edges so that source and target are in the same module + // let graph_assignment: HashMap = { + // let mut graph_assignment: HashMap = HashMap::new(); + // let mut next_id = 2; + // for nx in graph.graph.node_indices() { + // for e in graph + // .graph + // .edges_directed(nx, petgraph::Direction::Incoming) + // { + // if e.weight() == &ValueGraphEdge::Write { + // let idx = graph_assignment + // .get(&e.source().index()) + // .copied() + // .or(graph_assignment.get(&e.target().index()).copied()) + // .unwrap_or_else(|| { + // next_id += 1; + // next_id - 1 + // }); + + // graph_assignment.insert(e.source().index(), idx); + // graph_assignment.insert(e.target().index(), idx); + // } + // } + // } + // graph_assignment + // }; + + let graph_assignment: HashMap = { + // graph node -> export index + let mut graph_assignment: HashMap = HashMap::new(); + + // Put every export into a new module, put revisited nodes (so used by multiple exports) into common module + let mut next_id = COMMON_ID + 1; + for root_idx in graph.graph.node_indices() { + if let ValueGraphNode::Export(_) = graph.graph.node_weight(root_idx).unwrap() { + let root_id = next_id; + next_id += 1; + + let mut traversal = Dfs::new(&graph.graph, root_idx); + assert_eq!(root_idx, traversal.next(&graph.graph).unwrap()); // skip root + if let Some(existing) = graph_assignment.get(&traversal.peek(&graph.graph).unwrap()) { + // Binding is exported multiple times + graph_assignment.insert(root_idx, *existing); + } else { + graph_assignment.insert(root_idx, ModuleIndex(root_id)); + while let Some(nx) = traversal.next(&graph.graph) { + if graph.graph.node_weight(nx).unwrap().is_imported_binding() { + continue; + } + match graph_assignment.entry(nx) { + Entry::Vacant(e) => { + e.insert(ModuleIndex(root_id)); + } + Entry::Occupied(mut e) => { + e.insert(ModuleIndex(COMMON_ID)); + // TODO if it's already set to COMMON_ID, skip children + } + }; + } + } + } + } + + // The subgraph of statements also has to be contained in a single module + for root_idx in graph.graph.node_indices() { + if let ValueGraphNode::Statement(_) = graph.graph.node_weight(root_idx).unwrap() { + let mut traversal = DfsPostOrder::new(&graph.graph, root_idx); + let mut in_module = None; + while let Some(nx) = traversal.next(&graph.graph) { + if graph.graph.node_weight(nx).unwrap().is_imported_binding() { + continue; + } + let v = graph_assignment.get(&nx).copied(); + if in_module.is_none() { + in_module = v; + } + if v != in_module { + in_module = Some(ModuleIndex::common()); + break; + } + } + + in_module = in_module.or(Some(ModuleIndex::common())); + let mut traversal = Dfs::new(&graph.graph, root_idx); + while let Some(nx) = traversal.next(&graph.graph) { + if graph.graph.node_weight(nx).unwrap().is_imported_binding() { + continue; + } + graph_assignment.insert(nx, in_module.unwrap()); + } + } + } + + // Ensure that write edges start and end in the same module + for ex in graph.graph.edge_indices() { + let (e_source, e_target) = graph.graph.edge_endpoints(ex).unwrap(); + if graph.graph.edge_weight(ex).unwrap() == &ValueGraphEdge::Write { + let source_module = graph_assignment.get(&e_source); + let target_module = graph_assignment.get(&e_target); + if source_module.is_some() && source_module != target_module { + let mut traversal = Dfs::new(&graph.graph, e_target); + while let Some(nx) = traversal.next(&graph.graph) { + if graph.graph.node_weight(nx).unwrap().is_imported_binding() { + continue; + } + graph_assignment.insert(nx, ModuleIndex::common()); + } + + let mut export_nodes_to_rewrite = vec![]; + let mut traversal = DfsAncestors::new(&graph.graph, e_target); + while let Some(nx) = traversal.next(&graph.graph) { + if let ValueGraphNode::Export(_) = graph.graph.node_weight(nx).unwrap() { + export_nodes_to_rewrite.push(nx); + } + } + + let mut traversal = Dfs::from_parts(export_nodes_to_rewrite, graph.graph.visit_map()); + while let Some(nx) = traversal.next(&graph.graph) { + if graph.graph.node_weight(nx).unwrap().is_imported_binding() { + continue; + } + graph_assignment.insert(nx, ModuleIndex::common()); + } + } + } + } + + graph_assignment + }; + + // println!( + // "{:?}", + // Dot::with_attr_getters( + // &graph.graph, + // &[Config::NodeNoLabel], + // &|_graph, edge| { + // if graph_assignment.get(&edge.source()) == graph_assignment.get(&edge.target()) { + // "".to_owned() + // } else { + // ", color = lightgrey".to_owned() + // } + // }, + // &|_graph, (node_idx, node_weight)| { + // format!( + // "label=\"[{}] {}: {:?}\"", + // node_idx.index(), + // format!("{:?}", node_weight).replace('\"', "\\\""), + // graph_assignment.get(&node_idx).map(|v| v.index()) + // ) + // } + // ) + // ); + + let mut reexports: Vec = vec![]; + let mut modules: IndexMap> = IndexMap::new(); + + for (i, it) in module.body.into_iter().enumerate() { + match it { + ModuleItem::Stmt(Stmt::Decl(decl)) => { + for (name, decl) in split_up_decl(decl) { + let node_local = graph + .get_node(&ValueGraphNode::Binding(name.clone())) + .unwrap(); + let id_local = *graph_assignment + .get(&node_local) + .unwrap_or(&ModuleIndex::common()); + + let mut is_used_externally = false; + let exports = graph + .graph + .neighbors_directed(node_local, petgraph::Direction::Incoming) + .filter_map(|node_parent| { + let value = graph.graph.node_weight(node_parent).unwrap(); + match value { + ValueGraphNode::Export(name) => { + is_used_externally = true; + Some(name) + } + ValueGraphNode::Statement(_) | ValueGraphNode::Binding(_) => { + is_used_externally = is_used_externally + || graph_assignment + .get(&node_parent) + .map_or(false, |i| i != &id_local); + None + } + ValueGraphNode::ImportedBinding(_) => unreachable!(), + } + }); + for exported in exports { + reexports.push(ModuleItem::ModuleDecl(create_export( + ModuleExportName::Ident(name.clone().into()), + Some(word_to_export_name(exported.clone())), + Some(&id_local.as_import_specifier(module_id)), + ))); + } + + let mut body = + generate_imports(module_id, &graph, &graph_assignment, node_local, id_local); + body.push(ModuleItem::Stmt(Stmt::Decl(decl))); + if is_used_externally { + body.push(ModuleItem::ModuleDecl(create_export( + ModuleExportName::Ident(name.clone().into()), + Some(word_to_export_name(name.0.clone())), + None, + ))); + } + modules.entry(id_local).or_default().append(&mut body); + } + } + ModuleItem::Stmt(_) => { + let node = graph.get_node(&ValueGraphNode::Statement(i)).unwrap(); + let id = *graph_assignment.get(&node).unwrap(); + let mut body = generate_imports(module_id, &graph, &graph_assignment, node, id); + body.push(it); + modules.entry(id).or_default().append(&mut body); + continue; + } + ModuleItem::ModuleDecl(decl) => match decl { + ModuleDecl::ExportDefaultDecl(decl) => { + let node_export = graph + .get_node(&ValueGraphNode::Export("default".into())) + .unwrap(); + let id_export = *graph_assignment.get(&node_export).unwrap(); + let node_local = graph.get_single_child_node(node_export); + let id_local = *graph_assignment.get(&node_local).unwrap(); + + for exported_renamed in graph.get_exports_of_binding(node_local) { + reexports.push(ModuleItem::ModuleDecl(create_export( + ModuleExportName::Ident(("default".into(), SyntaxContext::empty()).into()), + Some(word_to_export_name(exported_renamed.clone())), + Some(&id_export.as_import_specifier(module_id)), + ))); + } + + let mut body = + generate_imports(module_id, &graph, &graph_assignment, node_local, id_local); + body.push(ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(decl))); + modules.entry(id_export).or_default().append(&mut body); + } + ModuleDecl::ExportDefaultExpr(expr) => { + let node_export = graph + .get_node(&ValueGraphNode::Export("default".into())) + .unwrap(); + let id_export = *graph_assignment.get(&node_export).unwrap(); + let node_local = graph.get_single_child_node(node_export); + let id_local = *graph_assignment.get(&node_local).unwrap(); + + reexports.push(ModuleItem::ModuleDecl(create_export( + ModuleExportName::Ident(("default".into(), SyntaxContext::empty()).into()), + None, + Some(&id_export.as_import_specifier(module_id)), + ))); + + let mut body = + generate_imports(module_id, &graph, &graph_assignment, node_local, id_local); + body.push(ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(expr))); + modules.entry(id_export).or_default().append(&mut body); + } + ModuleDecl::ExportNamed(NamedExport { src: Some(_), .. }) | ModuleDecl::ExportAll(_) => { + reexports.push(ModuleItem::ModuleDecl(decl)); + } + ModuleDecl::ExportNamed(NamedExport { + specifiers, + src: None, + .. + }) => { + // Handle + // import {foo} from "..."; + // export {foo}; + for spec in specifiers { + let exported = match spec { + ExportSpecifier::Default(_) => "default".into(), + ExportSpecifier::Named(spec) => match spec.exported.as_ref().unwrap_or(&spec.orig) { + ModuleExportName::Ident(v) => v.sym.clone(), + ModuleExportName::Str(v) => v.value.clone(), + }, + ExportSpecifier::Namespace(_) => unreachable!(), + }; + let id_export = graph + .get_node(&ValueGraphNode::Export(exported.clone())) + .unwrap(); + let id_local = graph + .graph + .node_weight(graph.get_single_child_node(id_export)) + .unwrap(); + if let ValueGraphNode::ImportedBinding((_, source, imported)) = id_local { + reexports.push(ModuleItem::ModuleDecl(create_export( + imported.clone(), + Some(ModuleExportName::Str(exported.into())), + Some(source), + ))) + } + // all other (local) exported value Handled when visiting the exported + // declarations themselves + } + } + // Values themselves are handled via locals/ImportedBinding + // Sideeffects are collected in `imports` + ModuleDecl::Import(_) => continue, + ModuleDecl::ExportDecl(decl) => { + for (exported, decl) in split_up_decl(decl.decl) { + // just put the export where the local ended up. Which means that + // graph_assignment(ValueGraphNode:Export) is just ignored + let node_local = graph + .get_node(&ValueGraphNode::Binding(exported.clone())) + .unwrap(); + + let id_local = *graph_assignment.get(&node_local).unwrap(); + + for exported_renamed in graph.get_exports_of_binding(node_local) { + reexports.push(ModuleItem::ModuleDecl(create_export( + ModuleExportName::Ident(exported.clone().into()), + Some(word_to_export_name(exported_renamed.clone())), + Some(&id_local.as_import_specifier(module_id)), + ))); + } + + let mut body = + generate_imports(module_id, &graph, &graph_assignment, node_local, id_local); + body.push(ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { + span: DUMMY_SP, + decl, + }))); + modules.entry(id_local).or_default().append(&mut body); + } + } + _ => unreachable!(), + }, + } + } + + // If there is a locals module, make sure that is also imported. Otherwise Parcel's asset handling is confused if there are two "root" assets that aren't imported via uniqueKey + if modules.contains_key(&ModuleIndex::common()) { + reexports.insert( + 0, + ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { + span: DUMMY_SP, + specifiers: vec![], + src: Box::new(ModuleIndex::common().as_import_specifier(module_id).into()), + type_only: false, + asserts: None, + })), + ); + } + + let mut result = Vec::with_capacity(modules.len()); + for (i, body_rest) in modules.into_iter() { + let mut body: Vec<_> = imports.values().cloned().collect(); + body.extend(body_rest.into_iter()); + result.push(( + i.as_import_specifier(module_id), + Module { + body, + span: DUMMY_SP, + shebang: None, + }, + )) + } + + ( + Module { + body: reexports, + span: DUMMY_SP, + shebang: None, + }, + result, + ) +} + +fn generate_imports( + module_id: &str, + graph: &ValueGraph, + graph_assignment: &HashMap, + node_local: NodeIndex, + id_local: ModuleIndex, +) -> Vec { + graph + .graph + .neighbors_directed(node_local, petgraph::Direction::Outgoing) + .map(|n| { + ( + graph.graph.node_weight(n).unwrap(), + graph_assignment.get(&n).copied(), + ) + }) + .filter_map(|v| match v.0 { + ValueGraphNode::Binding(n) => { + if v.1.unwrap_or(ModuleIndex::common()) != id_local { + Some(ModuleItem::ModuleDecl(create_import( + n, + None, + &v.1.unwrap().as_import_specifier(module_id), + ))) + } else { + None + } + } + ValueGraphNode::ImportedBinding((id, source, imported)) => Some(ModuleItem::ModuleDecl( + create_import(id, Some(imported.clone()), source), + )), + ValueGraphNode::Export(_) | ValueGraphNode::Statement(_) => unreachable!(), + }) + .collect() +} + +fn create_import(local: &Id, imported: Option, src: &str) -> ModuleDecl { + let local: Ident = Ident::from(local.clone()); + + let is_namespace = imported.as_ref().map_or(false, |imported| match imported { + ModuleExportName::Ident(_) => false, + ModuleExportName::Str(v) => v.value == *"*", + }); + ModuleDecl::Import(ImportDecl { + span: DUMMY_SP, + specifiers: vec![if is_namespace { + ImportSpecifier::Namespace(ImportStarAsSpecifier { + span: DUMMY_SP, + local, + }) + } else { + ImportSpecifier::Named(ImportNamedSpecifier { + span: DUMMY_SP, + local, + imported, + is_type_only: false, + }) + }], + src: Box::new(src.into()), + type_only: false, + asserts: None, + }) +} + +fn create_export( + local: ModuleExportName, + exported: Option, + src: Option<&str>, +) -> ModuleDecl { + let is_namespace = match &local { + ModuleExportName::Ident(_) => false, + ModuleExportName::Str(v) => v.value == *"*", + }; + + ModuleDecl::ExportNamed(NamedExport { + span: DUMMY_SP, + specifiers: vec![if is_namespace { + ExportSpecifier::Namespace(ExportNamespaceSpecifier { + name: exported.unwrap(), + span: DUMMY_SP, + }) + } else { + ExportSpecifier::Named(ExportNamedSpecifier { + span: DUMMY_SP, + orig: local, + exported, + is_type_only: false, + }) + }], + src: src.map(|src| Box::new(src.into())), + type_only: false, + asserts: None, + }) +} + +fn word_to_export_name(v: JsWord) -> ModuleExportName { + ModuleExportName::Ident(Ident::new(v, DUMMY_SP)) +} + +fn find_referenced_locals VisitWith>>( + node: &T, + collect: &Collect, +) -> ReferencesResult { + let mut uses = VisitReferences { + collect, + reads: HashSet::new(), + writes: HashSet::new(), + in_write: false, + }; + node.visit_with(&mut uses); + ReferencesResult { + reads: uses.reads, + writes: uses.writes, + } +} +struct VisitReferences<'a> { + collect: &'a Collect, + reads: HashSet, + writes: HashSet, + in_write: bool, +} + +#[derive(Debug)] +struct ReferencesResult { + pub reads: HashSet, + pub writes: HashSet, +} + +impl<'a> VisitReferences<'a> { + fn handle_id(&mut self, id: Id) { + if id.1.has_mark(self.collect.global_mark) + && (self.collect.decls.contains(&id) || self.collect.imports.contains_key(&id)) + { + if self.in_write { + // self.reads.remove(&id); + self.writes.insert(id); + } else { + // if !self.writes.contains(&id) + self.reads.insert(id); + } + } + } +} + +impl Visit for VisitReferences<'_> { + fn visit_ident(&mut self, ident: &Ident) { + self.handle_id(ident.to_id()); + } + // fn visit_expr(&mut self, expr: &Expr) { + // expr.visit_children_with(self); + // if let Expr::Ident(ident) = expr { + // self.handle_id(ident.to_id()); + // } + // } + + // fn visit_assign_pat_prop(&mut self, node: &AssignPatProp) { + // node.value.visit_with(self); + // } + + // fn visit_pat(&mut self, pat: &Pat) { + // pat.visit_children_with(self); + + // if let Pat::Ident(ident) = pat { + // self.handle_id(ident.to_id()); + // } + // } + + fn visit_assign_expr(&mut self, expr: &AssignExpr) { + let old = self.in_write; + self.in_write = true; + expr.left.visit_with(self); + self.in_write = old; + expr.right.visit_with(self); + } + fn visit_update_expr(&mut self, expr: &UpdateExpr) { + let old = self.in_write; + self.in_write = true; + expr.visit_children_with(self); + self.in_write = old; + } + fn visit_unary_expr(&mut self, expr: &UnaryExpr) { + if expr.op == UnaryOp::Delete { + let old = self.in_write; + self.in_write = true; + expr.visit_children_with(self); + self.in_write = old; + } else { + expr.visit_children_with(self); + } + } + + fn visit_member_prop(&mut self, n: &MemberProp) { + if let MemberProp::Computed(..) = n { + n.visit_children_with(self); + } + } + + fn visit_prop_name(&mut self, n: &PropName) { + if let PropName::Computed(..) = n { + n.visit_children_with(self); + } + } +} + +fn get_decl_names(decl: &Decl) -> Vec<(Id, Decl)> { + // TODO get rid of clone which is needed because of the Decl::Var(...) allocation + match decl { + Decl::Class(ref v) => vec![(v.ident.to_id(), decl.clone())], + Decl::Fn(ref v) => vec![(v.ident.to_id(), decl.clone())], + Decl::Var(var) => var + .decls + .iter() + .map(|var_decl| { + ( + var_decl.name.as_ident().unwrap().to_id(), + Decl::Var(Box::new(VarDecl { + span: DUMMY_SP, + kind: var.kind, + declare: var.declare, + decls: vec![var_decl.clone()], + })), + ) + }) + .collect(), + _ => unreachable!(), + } +} + +fn split_up_decl(decl: Decl) -> Vec<(Id, Decl)> { + match decl { + Decl::Class(ref v) => vec![(v.ident.to_id(), decl)], + Decl::Fn(ref v) => vec![(v.ident.to_id(), decl)], + Decl::Var(var) => var + .decls + .into_iter() + .map(|var_decl| { + ( + var_decl.name.as_ident().unwrap().to_id(), + Decl::Var(Box::new(VarDecl { + span: DUMMY_SP, + kind: var.kind, + declare: var.declare, + decls: vec![var_decl], + })), + ) + }) + .collect(), + _ => unreachable!(), + } +} + +#[derive(Clone, Debug)] +pub struct DfsAncestors { + /// The stack of nodes to visit + pub stack: Vec, + /// The map of discovered nodes + pub discovered: VM, +} + +impl Default for DfsAncestors +where + VM: Default, +{ + fn default() -> Self { + DfsAncestors { + stack: Vec::new(), + discovered: VM::default(), + } + } +} + +impl DfsAncestors +where + N: Copy + PartialEq, + VM: VisitMap, +{ + /// Create a new **Dfs**, using the graph's visitor map, and put **start** + /// in the stack of nodes to visit. + pub fn new(graph: G, start: N) -> Self + where + G: GraphRef + Visitable, + { + let mut dfs = DfsAncestors::empty(graph); + dfs.move_to(start); + dfs + } + + /// Create a new **Dfs** using the graph's visitor map, and no stack. + pub fn empty(graph: G) -> Self + where + G: GraphRef + Visitable, + { + DfsAncestors { + stack: Vec::new(), + discovered: graph.visit_map(), + } + } + + /// Keep the discovered map, but clear the visit stack and restart + /// the dfs from a particular node. + pub fn move_to(&mut self, start: N) { + self.stack.clear(); + self.stack.push(start); + } + + /// Return the next node in the dfs, or **None** if the traversal is done. + pub fn next(&mut self, graph: G) -> Option + where + G: IntoNeighborsDirected, + { + while let Some(node) = self.stack.pop() { + if self.discovered.visit(node) { + for succ in graph.neighbors_directed(node, petgraph::Direction::Incoming) { + if !self.discovered.is_visited(&succ) { + self.stack.push(succ); + } + } + return Some(node); + } + } + None + } +} + +#[derive(Clone, Debug)] +pub struct Dfs { + /// The stack of nodes to visit + pub stack: Vec, + /// The map of discovered nodes + pub discovered: VM, + + peeked: Option, +} + +impl Default for Dfs +where + VM: Default, +{ + fn default() -> Self { + Dfs { + stack: Vec::new(), + discovered: VM::default(), + peeked: None, + } + } +} + +impl Dfs +where + N: Copy + PartialEq, + VM: VisitMap, +{ + /// Create a new **Dfs**, using the graph's visitor map, and put **start** + /// in the stack of nodes to visit. + pub fn new(graph: G, start: N) -> Self + where + G: GraphRef + Visitable, + { + let mut dfs = Dfs::empty(graph); + dfs.move_to(start); + dfs + } + + /// Create a `Dfs` from a vector and a visit map + pub fn from_parts(stack: Vec, discovered: VM) -> Self { + Dfs { + stack, + discovered, + peeked: None, + } + } + + /// Create a new **Dfs** using the graph's visitor map, and no stack. + pub fn empty(graph: G) -> Self + where + G: GraphRef + Visitable, + { + Dfs { + stack: Vec::new(), + discovered: graph.visit_map(), + peeked: None, + } + } + + /// Keep the discovered map, but clear the visit stack and restart + /// the dfs from a particular node. + pub fn move_to(&mut self, start: N) { + self.stack.clear(); + self.stack.push(start); + } + + /// Return the next node in the dfs, or **None** if the traversal is done. + pub fn next(&mut self, graph: G) -> Option + where + G: IntoNeighbors, + { + if self.peeked.is_some() { + return self.peeked.take(); + } + + while let Some(node) = self.stack.pop() { + if self.discovered.visit(node) { + for succ in graph.neighbors(node) { + if !self.discovered.is_visited(&succ) { + self.stack.push(succ); + } + } + return Some(node); + } + } + None + } + + /// Peek at the next node + pub fn peek(&mut self, graph: G) -> Option + where + G: IntoNeighbors, + { + if self.peeked.is_some() { + return self.peeked; + } + + self.peeked = self.next(graph); + self.peeked + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::collect_decls; + use swc_common::comments::SingleThreadedComments; + use swc_common::{sync::Lrc, FileName, Globals, Mark, SourceMap}; + use swc_ecmascript::ast::FnDecl; + use swc_ecmascript::parser::lexer::Lexer; + use swc_ecmascript::parser::{Parser, StringInput}; + use swc_ecmascript::transforms::resolver; + use swc_ecmascript::visit::FoldWith; + + fn parse(code: &str, cb: impl FnOnce(Collect, Module)) { + let source_map = Lrc::new(SourceMap::default()); + let source_file = source_map.new_source_file(FileName::Anon, code.into()); + + let comments = SingleThreadedComments::default(); + let lexer = Lexer::new( + Default::default(), + Default::default(), + StringInput::from(&*source_file), + Some(&comments), + ); + + let mut parser = Parser::new_from(lexer); + match parser.parse_module() { + Ok(module) => swc_common::GLOBALS.set(&Globals::new(), || { + let unresolved_mark = Mark::fresh(Mark::root()); + let global_mark = Mark::fresh(Mark::root()); + let module = module.fold_with(&mut resolver(unresolved_mark, global_mark, false)); + + let mut collect = Collect::new( + source_map.clone(), + collect_decls(&module), + Mark::fresh(Mark::root()), + global_mark, + true, + ); + module.visit_with(&mut collect); + + cb(collect, module) + // let module = module.fold_with(&mut chain!(hygiene(), fixer(Some(&comments)))); + // let code = emit(source_map, comments, &module); + // (collect, code, res) + }), + Err(err) => { + panic!("{:?}", err); + } + }; + } + + // fn emit( + // source_map: Lrc, + // comments: SingleThreadedComments, + // module: &Module, + // ) -> String { + // let mut src_map_buf = vec![]; + // let mut buf = vec![]; + // { + // let writer = Box::new(JsWriter::new( + // source_map.clone(), + // "\n", + // &mut buf, + // Some(&mut src_map_buf), + // )); + // let config = swc_ecmascript::codegen::Config { + // minify: false, + // ascii_only: false, + // target: swc_ecmascript::ast::EsVersion::Es5, + // omit_last_semi: false, + // }; + // let mut emitter = swc_ecmascript::codegen::Emitter { + // cfg: config, + // comments: Some(&comments), + // cm: source_map, + // wr: writer, + // }; + // emitter.emit_module(module).unwrap(); + // } + // String::from_utf8(buf).unwrap() + // } + + macro_rules! set ( + { $($key:expr),* } => { + { + #[allow(unused_mut)] + let mut m = HashSet::new(); + $( + m.insert($key); + )* + m + } + }; + ); + + macro_rules! w { + ($s: expr) => {{ + let w: JsWord = $s.into(); + w + }}; + } + + #[test] + fn esm() { + parse( + r#" +let x = 1; +let y = 2; +let z = 2; +function f(){ + console.log(a, x, y); +} +function g(){ + x = 2; + a = 2; + y++; + delete z.x; +} + "#, + |collect, module| { + macro_rules! find_func { + ($n: expr) => {{ + module + .body + .iter() + .find_map(|v| { + if let ModuleItem::Stmt(Stmt::Decl(Decl::Fn(FnDecl { + ident: Ident { sym, .. }, + function, + .. + }))) = v + { + if sym == &JsWord::from($n) { + return Some(&**function); + } + } + None + }) + .unwrap() + }}; + } + let func_f = find_func!("f"); + let func_g = find_func!("g"); + let locals_f = find_referenced_locals(func_f, &collect); + let locals_g = find_referenced_locals(func_g, &collect); + assert_eq!( + locals_f + .reads + .into_iter() + .map(|v| v.0) + .collect::>(), + set! { w!("x"), w!("y") } + ); + assert_eq!( + locals_f + .writes + .into_iter() + .map(|v| v.0) + .collect::>(), + set! {} + ); + assert_eq!( + locals_g + .reads + .into_iter() + .map(|v| v.0) + .collect::>(), + set! {} + ); + assert_eq!( + locals_g + .writes + .into_iter() + .map(|v| v.0) + .collect::>(), + set! { w!("x"), w!("y"), w!("z") } + ); + }, + ); + } +} diff --git a/packages/transformers/js/src/JSTransformer.js b/packages/transformers/js/src/JSTransformer.js index ace82041c55..b07e9a1f6f1 100644 --- a/packages/transformers/js/src/JSTransformer.js +++ b/packages/transformers/js/src/JSTransformer.js @@ -1,7 +1,27 @@ // @flow -import type {JSONObject, EnvMap} from '@parcel/types'; +import type { + // AST, + // ASTGenerator, + // BundleBehavior, + // Dependency, + DependencyOptions, + // Environment, + // EnvironmentOptions, + EnvMap, + // FileCreateInvalidation, + // FilePath, + JSONObject, + Meta, + MutableAsset, + // MutableAssetSymbols, + TransformerResult, + Symbol, + SourceLocation, +} from '@parcel/types'; +// import type {Readable} from 'stream'; import type {SchemaEntity} from '@parcel/utils'; import type {Diagnostic} from '@parcel/diagnostic'; +// import type {FileSystem} from '@parcel/fs'; import SourceMap from '@parcel/source-map'; import {Transformer} from '@parcel/plugin'; import {init, transform} from '../native'; @@ -10,10 +30,33 @@ import browserslist from 'browserslist'; import semver from 'semver'; import nullthrows from 'nullthrows'; import ThrowableDiagnostic, {encodeJSONKeyComponent} from '@parcel/diagnostic'; -import {validateSchema, remapSourceLocation, isGlobMatch} from '@parcel/utils'; +import { + validateSchema, + remapSourceLocation, + isGlobMatch, + objectSortedEntriesDeep, +} from '@parcel/utils'; import WorkerFarm from '@parcel/workers'; import pkg from '../package.json'; +type TransformResultOuter = {| + main_module: TransformResult, + modules: Array<[string, TransformResult]>, +|}; + +type TransformResult = {| + code: Buffer, + map: ?string, + shebang: ?string, + dependencies: Array, + hoist_result: ?any /* HoistResult */, + symbol_result: ?any /* CollectResult */, + diagnostics: ?Array, + needs_esm_helpers: boolean, + used_env: Set, + has_node_replacements: boolean, +|}; + const JSX_EXTENSIONS = { jsx: true, tsx: true, @@ -389,23 +432,13 @@ export default (new Transformer({ } } - let { - dependencies, - code: compiledCode, - map, - shebang, - hoist_result, - symbol_result, - needs_esm_helpers, - diagnostics, - used_env, - has_node_replacements, - } = transform({ + let result: TransformResultOuter = transform({ filename: asset.filePath, code, module_id: asset.id, project_root: options.projectRoot, replace_env: !asset.env.isNode(), + side_effects: asset.sideEffects, inline_fs: Boolean(config?.inlineFS) && !asset.env.isNode(), insert_node_globals: !asset.env.isNode() && asset.env.sourceType !== 'script', @@ -461,497 +494,886 @@ export default (new Transformer({ return location; }; - if (diagnostics) { - let errors = diagnostics.filter( - d => - d.severity === 'Error' || - (d.severity === 'SourceError' && asset.isSource), - ); - let warnings = diagnostics.filter( - d => - d.severity === 'Warning' || - (d.severity === 'SourceError' && !asset.isSource), - ); - let convertDiagnostic = diagnostic => { - let message = diagnostic.message; - if (message === 'SCRIPT_ERROR') { - let err = SCRIPT_ERRORS[(asset.env.context: string)]; - message = err?.message || SCRIPT_ERRORS.browser.message; - } - - let res: Diagnostic = { - message, - codeFrames: [ - { - filePath: asset.filePath, - codeHighlights: diagnostic.code_highlights?.map(highlight => { - let {start, end} = convertLoc(highlight.loc); - return { - message: highlight.message, - start, - end, - }; - }), - }, - ], - hints: diagnostic.hints, - }; + let x = [ + applyResult( + asset, + result.main_module, + asset.id, + logger, + convertLoc, + supportsModuleWorkers, + options, + originalMap, + ), + ...result.modules.map(([id, res]) => + applyResult( + asset, + res, + id, + logger, + convertLoc, + supportsModuleWorkers, + options, + originalMap, + ), + ), + ]; + // console.log( + // asset.filePath, + // // require('util').inspect(result.main_module, {depth: Infinity}), + // require('util').inspect( + // x.map(v => ({...v, content: v.content?.toString()})), + // {depth: Infinity}, + // ), + // ); + return x; + }, +}): Transformer); - if (diagnostic.documentation_url) { - res.documentationURL = diagnostic.documentation_url; - } +// On linux with older versions of glibc (e.g. CentOS 7), we encounter a segmentation fault +// when worker threads exit due to thread local variables used by SWC. A workaround is to +// also load the native module on the main thread, so that it is not unloaded until process exit. +// See https://github.com/rust-lang/rust/issues/91979. +let isLoadedOnMainThread = false; +async function loadOnMainThreadIfNeeded() { + if ( + !isLoadedOnMainThread && + process.platform === 'linux' && + WorkerFarm.isWorker() + ) { + // $FlowFixMe + let {glibcVersionRuntime} = process.report.getReport().header; + if (glibcVersionRuntime && parseFloat(glibcVersionRuntime) <= 2.17) { + let api = WorkerFarm.getWorkerApi(); + await api.callMaster({ + location: __dirname + '/loadNative.js', + args: [], + }); - if (diagnostic.show_environment) { - if (asset.env.loc && asset.env.loc.filePath !== asset.filePath) { - res.codeFrames?.push({ - filePath: asset.env.loc.filePath, - codeHighlights: [ - { - start: asset.env.loc.start, - end: asset.env.loc.end, - message: 'The environment was originally created here', - }, - ], - }); - } + isLoadedOnMainThread = true; + } + } +} - let err = SCRIPT_ERRORS[(asset.env.context: string)]; - if (err) { - if (!res.hints) { - res.hints = [err.hint]; - } else { - res.hints.push(err.hint); - } - } - } +// class AssetWrapper implements MutableAsset { +// value: TransformerResult; +// _id: string; +// constructor(value: TransformerResult, id: string) { +// this.value = value; +// this._id = id; +// } + +// /** The id of the asset. */ +// get id(): string { +// return this._id; +// } +// get fs(): FileSystem { +// throw new Error(); +// } +// get filePath(): FilePath { +// throw new Error(); +// } +// get type(): string { +// throw new Error(); +// } +// get query(): URLSearchParams { +// throw new Error(); +// } +// get env(): Environment { +// throw new Error(); +// } +// get isSource(): boolean { +// throw new Error(); +// } +// get meta(): Meta { +// let meta = +// // $FlowFixMe[cannot-write] +// // $FlowFixMe[incompatible-type] +// this.value.meta ?? (this.value.meta = {}); +// return meta; +// } +// get bundleBehavior(): ?BundleBehavior { +// throw new Error(); +// } +// get isBundleSplittable(): boolean { +// throw new Error(); +// } +// get sideEffects(): boolean { +// throw new Error(); +// } +// get uniqueKey(): ?string { +// throw new Error(); +// } +// get astGenerator(): ?ASTGenerator { +// throw new Error(); +// } +// get pipeline(): ?string { +// throw new Error(); +// } +// getAST(): Promise { +// throw new Error(); +// } +// getCode(): Promise { +// throw new Error(); +// } +// getBuffer(): Promise { +// throw new Error(); +// } +// getStream(): Readable { +// throw new Error(); +// } +// getMap(): Promise { +// throw new Error(); +// } +// getMapBuffer(): Promise { +// throw new Error(); +// } +// getDependencies(): $ReadOnlyArray { +// return this.value.dependencies; +// } + +// set type(v: string): string { +// throw new Error(); +// } +// set bundleBehavior(v: ?BundleBehavior) { +// throw new Error(); +// } +// set isBundleSplittable(v: boolean) { +// throw new Error(); +// } +// set sideEffects(v: boolean) { +// // $FlowFixMe[cannot-write] +// this.value.sideEffects = v; +// } +// get symbols(): MutableAssetSymbols { +// let that = this; +// const EMPTY_ITERATOR = { +// next() { +// return {done: true}; +// }, +// }; + +// return new (class X implements MutableAssetSymbols { +// /*:: +// @@iterator(): Iterator<[Symbol, {|local: Symbol, loc: ?SourceLocation, meta?: ?Meta|}]> { return ({}: any); } +// */ +// get isCleared(): boolean { +// return that.value.symbols == null; +// } +// get(exportSymbol: Symbol): ?{| +// local: Symbol, +// loc: ?SourceLocation, +// meta?: ?Meta, +// |} { +// nullthrows(that.value.symbols).get(exportSymbol); +// } +// hasExportSymbol(exportSymbol: Symbol): boolean { +// return Boolean(that.value.symbols?.has(exportSymbol)); +// } +// hasLocalSymbol(local: Symbol): boolean { +// if (that.value.symbols == null) { +// return false; +// } +// for (let s of that.value.symbols.values()) { +// if (local === s.local) return true; +// } +// return false; +// } +// exportSymbols(): Iterable { +// // $FlowFixMe +// return that.value.symbols.keys(); +// } +// // $FlowFixMe +// [Symbol.iterator]() { +// return that.value.symbols +// ? that.value.symbols[Symbol.iterator]() +// : EMPTY_ITERATOR; +// } + +// ensure(): void { +// if (that.value.symbols == null) { +// // $FlowFixMe[cannot-write] +// that.value.symbols = new Map(); +// } +// } +// set( +// exportSymbol: Symbol, +// local: Symbol, +// loc: ?SourceLocation, +// meta?: ?Meta, +// ): void { +// // $FlowFixMe[incompatible-cast] +// (nullthrows(that.value.symbols): Map< +// Symbol, +// {|loc: ?SourceLocation, local: Symbol, meta?: ?Meta|}, +// >).set(exportSymbol, { +// local, +// loc, +// meta, +// }); +// } +// delete(exportSymbol: Symbol) { +// // $FlowFixMe[incompatible-cast] +// (nullthrows(that.value.symbols): Map< +// Symbol, +// {|loc: ?SourceLocation, local: Symbol, meta?: ?Meta|}, +// >).delete(exportSymbol); +// } +// })(); +// } + +// // eslint-disable-next-line no-unused-vars +// addDependency(v: DependencyOptions): string { +// let deps: Array = +// // $FlowFixMe[cannot-write] +// // $FlowFixMe[incompatible-type] +// this.value.dependencies ?? (this.value.dependencies = []); +// deps.push(v); +// return ''; +// } +// // eslint-disable-next-line no-unused-vars +// addURLDependency(url: string, opts: $Shape): string { +// throw new Error(); +// } +// // eslint-disable-next-line no-unused-vars +// invalidateOnFileChange(v: FilePath): void { +// throw new Error(); +// } +// // eslint-disable-next-line no-unused-vars +// invalidateOnFileCreate(v: FileCreateInvalidation): void { +// throw new Error(); +// } +// // eslint-disable-next-line no-unused-vars +// invalidateOnEnvChange(v: string): void { +// throw new Error(); +// } +// // eslint-disable-next-line no-unused-vars +// setCode(v: string): void { +// throw new Error(); +// } +// // eslint-disable-next-line no-unused-vars +// setBuffer(v: Buffer): void { +// throw new Error(); +// } +// // eslint-disable-next-line no-unused-vars +// setStream(v: Readable): void { +// throw new Error(); +// } +// // eslint-disable-next-line no-unused-vars +// setAST(v: AST): void { +// throw new Error(); +// } +// isASTDirty(): boolean { +// throw new Error(); +// } +// // eslint-disable-next-line no-unused-vars +// setMap(map: ?SourceMap): void { +// throw new Error(); +// } +// // eslint-disable-next-line no-unused-vars +// setEnvironment(opts: EnvironmentOptions): void { +// throw new Error(); +// } +// } + +function applyResult( + baseAsset: MutableAsset, + result: TransformResult, + uniqueKey: ?string, + logger, + convertLoc, + supportsModuleWorkers, + options, + originalMap, +) { + let { + dependencies, + code: compiledCode, + map, + shebang, + hoist_result, + symbol_result, + needs_esm_helpers, + diagnostics, + // used_env, + has_node_replacements, + } = result; + + let asset = { + type: 'js', + content: compiledCode, + uniqueKey, + meta: {}, + dependencies: ([]: Array), + map: (null: ?SourceMap), + symbols: (null: ?Map< + Symbol, + {|local: Symbol, loc: ?SourceLocation, meta?: ?Meta|}, + >), + }; + + if (diagnostics) { + let errors = diagnostics.filter( + d => + d.severity === 'Error' || + (d.severity === 'SourceError' && baseAsset.isSource), + ); + let warnings = diagnostics.filter( + d => + d.severity === 'Warning' || + (d.severity === 'SourceError' && !baseAsset.isSource), + ); + let convertDiagnostic = diagnostic => { + let message = diagnostic.message; + if (message === 'SCRIPT_ERROR') { + let err = SCRIPT_ERRORS[(baseAsset.env.context: string)]; + message = err?.message || SCRIPT_ERRORS.browser.message; + } - return res; + let res: Diagnostic = { + message, + codeFrames: [ + { + filePath: baseAsset.filePath, + codeHighlights: diagnostic.code_highlights?.map(highlight => { + let {start, end} = convertLoc(highlight.loc); + return { + message: highlight.message, + start, + end, + }; + }), + }, + ], + hints: diagnostic.hints, }; - if (errors.length > 0) { - throw new ThrowableDiagnostic({ - diagnostic: errors.map(convertDiagnostic), - }); + if (diagnostic.documentation_url) { + res.documentationURL = diagnostic.documentation_url; } - logger.warn(warnings.map(convertDiagnostic)); - } + if (diagnostic.show_environment) { + if ( + baseAsset.env.loc && + baseAsset.env.loc.filePath !== baseAsset.filePath + ) { + res.codeFrames?.push({ + filePath: baseAsset.env.loc.filePath, + codeHighlights: [ + { + start: baseAsset.env.loc.start, + end: baseAsset.env.loc.end, + message: 'The environment was originally created here', + }, + ], + }); + } - if (shebang) { - asset.meta.interpreter = shebang; - } + let err = SCRIPT_ERRORS[(baseAsset.env.context: string)]; + if (err) { + if (!res.hints) { + res.hints = [err.hint]; + } else { + res.hints.push(err.hint); + } + } + } - if (has_node_replacements) { - asset.meta.has_node_replacements = has_node_replacements; - } + return res; + }; - for (let env of used_env) { - asset.invalidateOnEnvChange(env); + if (errors.length > 0) { + throw new ThrowableDiagnostic({ + diagnostic: errors.map(convertDiagnostic), + }); } - for (let dep of dependencies) { - if (dep.kind === 'WebWorker') { - // Use native ES module output if the worker was created with `type: 'module'` and all targets - // support native module workers. Only do this if parent asset output format is also esmodule so that - // assets can be shared between workers and the main thread in the global output format. - let outputFormat; - if ( - asset.env.outputFormat === 'esmodule' && - dep.source_type === 'Module' && - supportsModuleWorkers - ) { - outputFormat = 'esmodule'; - } else { - outputFormat = - asset.env.outputFormat === 'commonjs' ? 'commonjs' : 'global'; - } + logger.warn(warnings.map(convertDiagnostic)); + } + + if (shebang) { + asset.meta.interpreter = shebang; + } + + if (has_node_replacements) { + asset.meta.has_node_replacements = has_node_replacements; + } - let loc = convertLoc(dep.loc); - asset.addURLDependency(dep.specifier, { + // TODO + // for (let env of used_env) { + // asset.invalidateOnEnvChange(env); + // } + + for (let dep of dependencies) { + if (dep.kind === 'WebWorker') { + // Use native ES module output if the worker was created with `type: 'module'` and all targets + // support native module workers. Only do this if parent asset output format is also esmodule so that + // assets can be shared between workers and the main thread in the global output format. + let outputFormat; + if ( + baseAsset.env.outputFormat === 'esmodule' && + dep.source_type === 'Module' && + supportsModuleWorkers + ) { + outputFormat = 'esmodule'; + } else { + outputFormat = + baseAsset.env.outputFormat === 'commonjs' ? 'commonjs' : 'global'; + } + + let loc = convertLoc(dep.loc); + addURLDependency(asset, dep.specifier, { + loc, + env: { + context: 'web-worker', + sourceType: dep.source_type === 'Module' ? 'module' : 'script', + outputFormat, loc, - env: { - context: 'web-worker', - sourceType: dep.source_type === 'Module' ? 'module' : 'script', - outputFormat, - loc, - }, - meta: { - webworker: true, - placeholder: dep.placeholder, - }, - }); - } else if (dep.kind === 'ServiceWorker') { - let loc = convertLoc(dep.loc); - asset.addURLDependency(dep.specifier, { + }, + meta: { + webworker: true, + placeholder: dep.placeholder, + }, + }); + } else if (dep.kind === 'ServiceWorker') { + let loc = convertLoc(dep.loc); + addURLDependency(asset, dep.specifier, { + loc, + needsStableName: true, + env: { + context: 'service-worker', + sourceType: dep.source_type === 'Module' ? 'module' : 'script', + outputFormat: 'global', // TODO: module service worker support loc, - needsStableName: true, - env: { - context: 'service-worker', - sourceType: dep.source_type === 'Module' ? 'module' : 'script', - outputFormat: 'global', // TODO: module service worker support - loc, - }, - meta: { - placeholder: dep.placeholder, - }, - }); - } else if (dep.kind === 'Worklet') { - let loc = convertLoc(dep.loc); - asset.addURLDependency(dep.specifier, { + }, + meta: { + placeholder: dep.placeholder, + }, + }); + } else if (dep.kind === 'Worklet') { + let loc = convertLoc(dep.loc); + addURLDependency(asset, dep.specifier, { + loc, + env: { + context: 'worklet', + sourceType: 'module', + outputFormat: 'esmodule', // Worklets require ESM loc, - env: { - context: 'worklet', - sourceType: 'module', - outputFormat: 'esmodule', // Worklets require ESM - loc, - }, - meta: { - placeholder: dep.placeholder, - }, - }); - } else if (dep.kind === 'Url') { - asset.addURLDependency(dep.specifier, { - bundleBehavior: 'isolated', - loc: convertLoc(dep.loc), - meta: { - placeholder: dep.placeholder, - }, - }); - } else if (dep.kind === 'File') { - asset.invalidateOnFileChange(dep.specifier); - } else { - let meta: JSONObject = {kind: dep.kind}; - if (dep.attributes) { - meta.importAttributes = dep.attributes; - } - - if (dep.placeholder) { - meta.placeholder = dep.placeholder; - } + }, + meta: { + placeholder: dep.placeholder, + }, + }); + } else if (dep.kind === 'Url') { + addURLDependency(asset, dep.specifier, { + bundleBehavior: 'isolated', + loc: convertLoc(dep.loc), + meta: { + placeholder: dep.placeholder, + }, + }); + // TODO + // } else if (dep.kind === 'File') { + // asset.invalidateOnFileChange(dep.specifier); + } else { + let meta: JSONObject = {kind: dep.kind}; + if (dep.attributes) { + meta.importAttributes = dep.attributes; + } - let env; - if (dep.kind === 'DynamicImport') { - // https://html.spec.whatwg.org/multipage/webappapis.html#hostimportmoduledynamically(referencingscriptormodule,-modulerequest,-promisecapability) - if (asset.env.isWorklet() || asset.env.context === 'service-worker') { - let loc = convertLoc(dep.loc); - let diagnostic = { - message: `import() is not allowed in ${ - asset.env.isWorklet() ? 'worklets' : 'service workers' - }.`, - codeFrames: [ - { - filePath: asset.filePath, - codeHighlights: [ - { - start: loc.start, - end: loc.end, - }, - ], - }, - ], - hints: ['Try using a static `import`.'], - }; + if (dep.placeholder) { + meta.placeholder = dep.placeholder; + } - if (asset.env.loc) { - diagnostic.codeFrames.push({ - filePath: asset.env.loc.filePath, + let env; + if (dep.kind === 'DynamicImport') { + // https://html.spec.whatwg.org/multipage/webappapis.html#hostimportmoduledynamically(referencingscriptormodule,-modulerequest,-promisecapability) + if ( + baseAsset.env.isWorklet() || + baseAsset.env.context === 'service-worker' + ) { + let loc = convertLoc(dep.loc); + let diagnostic = { + message: `import() is not allowed in ${ + baseAsset.env.isWorklet() ? 'worklets' : 'service workers' + }.`, + codeFrames: [ + { + filePath: baseAsset.filePath, codeHighlights: [ { - start: asset.env.loc.start, - end: asset.env.loc.end, - message: 'The environment was originally created here', + start: loc.start, + end: loc.end, }, ], - }); - } + }, + ], + hints: ['Try using a static `import`.'], + }; - throw new ThrowableDiagnostic({ - diagnostic, + if (baseAsset.env.loc) { + diagnostic.codeFrames.push({ + filePath: baseAsset.env.loc.filePath, + codeHighlights: [ + { + start: baseAsset.env.loc.start, + end: baseAsset.env.loc.end, + message: 'The environment was originally created here', + }, + ], }); } - // If all of the target engines support dynamic import natively, - // we can output native ESM if scope hoisting is enabled. - // Only do this for scripts, rather than modules in the global - // output format so that assets can be shared between the bundles. - let outputFormat = asset.env.outputFormat; - if ( - asset.env.sourceType === 'script' && - asset.env.shouldScopeHoist && - asset.env.supports('dynamic-import', true) - ) { - outputFormat = 'esmodule'; - } - - env = { - sourceType: 'module', - outputFormat, - loc: convertLoc(dep.loc), - }; - } - - // Always bundle helpers, even with includeNodeModules: false, except if this is a library. - let isHelper = - dep.is_helper && - !( - dep.specifier.endsWith('/jsx-runtime') || - dep.specifier.endsWith('/jsx-dev-runtime') - ); - if (isHelper && !asset.env.isLibrary) { - env = { - ...env, - includeNodeModules: true, - }; + throw new ThrowableDiagnostic({ + diagnostic, + }); } - // Add required version range for helpers. - let range; - if (isHelper) { - let idx = dep.specifier.indexOf('/'); - if (dep.specifier[0] === '@') { - idx = dep.specifier.indexOf('/', idx + 1); - } - let module = idx >= 0 ? dep.specifier.slice(0, idx) : dep.specifier; - range = pkg.dependencies[module]; + // If all of the target engines support dynamic import natively, + // we can output native ESM if scope hoisting is enabled. + // Only do this for scripts, rather than modules in the global + // output format so that assets can be shared between the bundles. + let outputFormat = baseAsset.env.outputFormat; + if ( + baseAsset.env.sourceType === 'script' && + baseAsset.env.shouldScopeHoist && + baseAsset.env.supports('dynamic-import', true) + ) { + outputFormat = 'esmodule'; } - asset.addDependency({ - specifier: dep.specifier, - specifierType: dep.kind === 'Require' ? 'commonjs' : 'esm', + env = { + sourceType: 'module', + outputFormat, loc: convertLoc(dep.loc), - priority: dep.kind === 'DynamicImport' ? 'lazy' : 'sync', - isOptional: dep.is_optional, - meta, - resolveFrom: isHelper ? __filename : undefined, - range, - env, - }); + }; } - } - asset.meta.id = asset.id; - if (hoist_result) { - asset.symbols.ensure(); - for (let { - exported, - local, - loc, - is_esm, - } of hoist_result.exported_symbols) { - asset.symbols.set(exported, local, convertLoc(loc), {isEsm: is_esm}); + // Always bundle helpers, even with includeNodeModules: false, except if this is a library. + let isHelper = + // TODO + dep.specifier.startsWith('@swc/helpers') && + !dep.specifier.startsWith('@swc/helpers/src') && + dep.is_helper && + !( + dep.specifier.endsWith('/jsx-runtime') || + dep.specifier.endsWith('/jsx-dev-runtime') + ); + if (isHelper && !baseAsset.env.isLibrary) { + env = { + ...env, + includeNodeModules: true, + }; } - // deps is a map of dependencies that are keyed by placeholder or specifier - // If a placeholder is present, that is used first since placeholders are - // hashed with DependencyKind's. - // If not, the specifier is used along with its specifierType appended to - // it to separate dependencies with the same specifier. - let deps = new Map( - asset - .getDependencies() - .map(dep => [dep.meta.placeholder ?? dep.specifier, dep]), - ); - for (let dep of deps.values()) { - dep.symbols.ensure(); + // Add required version range for helpers. + let range; + if (isHelper) { + let idx = dep.specifier.indexOf('/'); + if (dep.specifier[0] === '@') { + idx = dep.specifier.indexOf('/', idx + 1); + } + let module = idx >= 0 ? dep.specifier.slice(0, idx) : dep.specifier; + range = pkg.dependencies[module]; } - for (let { - source, + addDependency(asset, { + specifier: dep.specifier, + specifierType: dep.kind === 'Require' ? 'commonjs' : 'esm', + loc: convertLoc(dep.loc), + priority: dep.kind === 'DynamicImport' ? 'lazy' : 'sync', + isOptional: dep.is_optional, + meta, + resolveFrom: isHelper ? __filename : undefined, + range, + env, + }); + } + } + + asset.meta.id = uniqueKey; + if (hoist_result) { + asset.symbols ??= new Map(); + for (let {exported, local, loc, is_esm} of hoist_result.exported_symbols) { + asset.symbols.set(exported, { local, - imported, - loc, - } of hoist_result.imported_symbols) { - let dep = deps.get(source); - if (!dep) continue; - dep.symbols.set(imported, local, convertLoc(loc)); - } + loc: convertLoc(loc), + meta: {isEsm: is_esm}, + }); + } - for (let {source, local, imported, loc} of hoist_result.re_exports) { - let dep = deps.get(source); - if (!dep) continue; - if (local === '*' && imported === '*') { - dep.symbols.set('*', '*', convertLoc(loc), true); - } else { - let reExportName = - dep.symbols.get(imported)?.local ?? - `$${asset.id}$re_export$${local}`; - asset.symbols.set(local, reExportName); - dep.symbols.set(imported, reExportName, convertLoc(loc), true); - } - } + // deps is a map of dependencies that are keyed by placeholder or specifier + // If a placeholder is present, that is used first since placeholders are + // hashed with DependencyKind's. + // If not, the specifier is used along with its specifierType appended to + // it to separate dependencies with the same specifier. + let deps = new Map( + asset.dependencies.map(dep => [ + nullthrows(dep.meta).placeholder ?? dep.specifier, + dep, + ]), + ); + for (let dep of deps.values()) { + ensureDependencySymbols(dep); + } - for (let specifier of hoist_result.wrapped_requires) { - let dep = deps.get(specifier); - if (!dep) continue; - dep.meta.shouldWrap = true; - } + for (let {source, local, imported, loc} of hoist_result.imported_symbols) { + let dep = deps.get(source); + if (!dep) continue; + setDependencySymbols(dep, imported, local, convertLoc(loc), false); + } - for (let name in hoist_result.dynamic_imports) { - let dep = deps.get(hoist_result.dynamic_imports[name]); - if (!dep) continue; - dep.meta.promiseSymbol = name; + for (let {source, local, imported, loc} of hoist_result.re_exports) { + let dep = deps.get(source); + if (!dep) continue; + if (local === '*' && imported === '*') { + setDependencySymbols(dep, '*', '*', convertLoc(loc), true); + } else { + let reExportName = + getDependencySymbols(dep).get(imported)?.local ?? + `$${baseAsset.id}$re_export$${local}`; + nullthrows(asset.symbols).set(local, {local: reExportName, loc: null}); + setDependencySymbols( + dep, + imported, + reExportName, + convertLoc(loc), + true, + ); } + } - if (hoist_result.self_references.length > 0) { - let symbols = new Map(); - for (let name of hoist_result.self_references) { - // Do not create a self-reference for the `default` symbol unless we have seen an __esModule flag. - if ( - name === 'default' && - !asset.symbols.hasExportSymbol('__esModule') - ) { - continue; - } + for (let specifier of hoist_result.wrapped_requires) { + let dep = deps.get(specifier); + if (!dep) continue; + nullthrows(dep.meta).shouldWrap = true; + } - let local = nullthrows(asset.symbols.get(name)).local; - symbols.set(name, { - local, - isWeak: false, - loc: null, - }); + for (let name in hoist_result.dynamic_imports) { + let dep = deps.get(hoist_result.dynamic_imports[name]); + if (!dep) continue; + nullthrows(dep.meta).promiseSymbol = name; + } + + if (hoist_result.self_references.length > 0) { + let symbols = new Map(); + for (let name of hoist_result.self_references) { + // Do not create a self-reference for the `default` symbol unless we have seen an __esModule flag. + if ( + name === 'default' && + !nullthrows(asset.symbols).has('__esModule') + ) { + continue; } - asset.addDependency({ - specifier: `./${path.basename(asset.filePath)}`, - specifierType: 'esm', - symbols, + let local = nullthrows(nullthrows(asset.symbols).get(name)).local; + symbols.set(name, { + local, + isWeak: false, + loc: null, }); } - // Add * symbol if there are CJS exports, no imports/exports at all, or the asset is wrapped. - // This allows accessing symbols that don't exist without errors in symbol propagation. - if ( - hoist_result.has_cjs_exports || - (!hoist_result.is_esm && - deps.size === 0 && - Object.keys(hoist_result.exported_symbols).length === 0) || - (hoist_result.should_wrap && !asset.symbols.hasExportSymbol('*')) - ) { - asset.symbols.set('*', `$${asset.id}$exports`); - } + addDependency(asset, { + specifier: `./${path.basename(baseAsset.filePath)}`, + specifierType: 'esm', + symbols, + }); + } - asset.meta.hasCJSExports = hoist_result.has_cjs_exports; - asset.meta.staticExports = hoist_result.static_cjs_exports; - asset.meta.shouldWrap = hoist_result.should_wrap; - } else { - if (symbol_result) { - let deps = new Map( - asset - .getDependencies() - .map(dep => [dep.meta.placeholder ?? dep.specifier, dep]), - ); - asset.symbols.ensure(); + // Add * symbol if there are CJS exports, no imports/exports at all, or the asset is wrapped. + // This allows accessing symbols that don't exist without errors in symbol propagation. + if ( + hoist_result.has_cjs_exports || + (!hoist_result.is_esm && + deps.size === 0 && + Object.keys(hoist_result.exported_symbols).length === 0) || + (hoist_result.should_wrap && !nullthrows(asset.symbols).has('*')) + ) { + nullthrows(asset.symbols).set('*', { + local: `$${baseAsset.id}$exports`, + loc: null, + }); + } + + asset.meta.hasCJSExports = hoist_result.has_cjs_exports; + asset.meta.staticExports = hoist_result.static_cjs_exports; + asset.meta.shouldWrap = hoist_result.should_wrap; + } else { + if (symbol_result) { + let deps = new Map( + asset.dependencies.map(dep => [ + nullthrows(dep.meta).placeholder ?? dep.specifier, + dep, + ]), + ); + asset.symbols ??= new Map(); - for (let {exported, local, loc, source} of symbol_result.exports) { - let dep = source ? deps.get(source) : undefined; - asset.symbols.set( - exported, + for (let {exported, local, loc, source} of symbol_result.exports) { + let dep = source ? deps.get(source) : undefined; + nullthrows(asset.symbols).set(exported, { + local: `${dep?.id ?? ''}$${local}`, + loc: convertLoc(loc), + }); + if (dep != null) { + ensureDependencySymbols(dep); + setDependencySymbols( + dep, + local, `${dep?.id ?? ''}$${local}`, convertLoc(loc), + true, ); - if (dep != null) { - dep.symbols.ensure(); - dep.symbols.set( - local, - `${dep?.id ?? ''}$${local}`, - convertLoc(loc), - true, - ); - } - } - - for (let {source, local, imported, loc} of symbol_result.imports) { - let dep = deps.get(source); - if (!dep) continue; - dep.symbols.ensure(); - dep.symbols.set(imported, local, convertLoc(loc)); - } - - for (let {source, loc} of symbol_result.exports_all) { - let dep = deps.get(source); - if (!dep) continue; - dep.symbols.ensure(); - dep.symbols.set('*', '*', convertLoc(loc), true); } + } - // Add * symbol if there are CJS exports, no imports/exports at all, or the asset is wrapped. - // This allows accessing symbols that don't exist without errors in symbol propagation. - if ( - symbol_result.has_cjs_exports || - (!symbol_result.is_esm && - deps.size === 0 && - symbol_result.exports.length === 0) || - (symbol_result.should_wrap && !asset.symbols.hasExportSymbol('*')) - ) { - asset.symbols.ensure(); - asset.symbols.set('*', `$${asset.id}$exports`); - } - } else { - // If the asset is wrapped, add * as a fallback - asset.symbols.ensure(); - asset.symbols.set('*', `$${asset.id}$exports`); + for (let {source, local, imported, loc} of symbol_result.imports) { + let dep = deps.get(source); + if (!dep) continue; + ensureDependencySymbols(dep); + setDependencySymbols(dep, imported, local, convertLoc(loc), false); } - // For all other imports and requires, mark everything as imported (this covers both dynamic - // imports and non-top-level requires.) - for (let dep of asset.getDependencies()) { - if (dep.symbols.isCleared) { - dep.symbols.ensure(); - dep.symbols.set('*', `${dep.id}$`); - } + for (let {source, loc} of symbol_result.exports_all) { + let dep = deps.get(source); + if (!dep) continue; + ensureDependencySymbols(dep); + setDependencySymbols(dep, '*', '*', convertLoc(loc), true); } - if (needs_esm_helpers) { - asset.addDependency({ - specifier: '@parcel/transformer-js/src/esmodule-helpers.js', - specifierType: 'esm', - resolveFrom: __filename, - env: { - includeNodeModules: { - '@parcel/transformer-js': true, - }, - }, - }); + // Add * symbol if there are CJS exports, no imports/exports at all, or the asset is wrapped. + // This allows accessing symbols that don't exist without errors in symbol propagation. + if ( + symbol_result.has_cjs_exports || + (!symbol_result.is_esm && + deps.size === 0 && + symbol_result.exports.length === 0) || + (symbol_result.should_wrap && !nullthrows(asset.symbols).has('*')) + ) { + asset.symbols ??= new Map(); + asset.symbols.set('*', {local: `$${baseAsset.id}$exports`, loc: null}); } + } else { + // If the asset is wrapped, add * as a fallback + asset.symbols ??= new Map(); + nullthrows(asset.symbols).set('*', { + local: `$${baseAsset.id}$exports`, + loc: null, + }); } - asset.type = 'js'; - asset.setBuffer(compiledCode); - - if (map) { - let sourceMap = new SourceMap(options.projectRoot); - sourceMap.addVLQMap(JSON.parse(map)); - if (originalMap) { - sourceMap.extends(originalMap); + // For all other imports and requires, mark everything as imported (this covers both dynamic + // imports and non-top-level requires.) + for (let dep of asset.dependencies) { + if (dep.symbols == null) { + ensureDependencySymbols(dep); + setDependencySymbols(dep, '*', `${dep.id}$`, null, false); } - asset.setMap(sourceMap); } - return [asset]; - }, -}): Transformer); - -// On linux with older versions of glibc (e.g. CentOS 7), we encounter a segmentation fault -// when worker threads exit due to thread local variables used by SWC. A workaround is to -// also load the native module on the main thread, so that it is not unloaded until process exit. -// See https://github.com/rust-lang/rust/issues/91979. -let isLoadedOnMainThread = false; -async function loadOnMainThreadIfNeeded() { - if ( - !isLoadedOnMainThread && - process.platform === 'linux' && - WorkerFarm.isWorker() - ) { - // $FlowFixMe - let {glibcVersionRuntime} = process.report.getReport().header; - if (glibcVersionRuntime && parseFloat(glibcVersionRuntime) <= 2.17) { - let api = WorkerFarm.getWorkerApi(); - await api.callMaster({ - location: __dirname + '/loadNative.js', - args: [], + if (needs_esm_helpers) { + addDependency(asset, { + specifier: '@parcel/transformer-js/src/esmodule-helpers.js', + specifierType: 'esm', + resolveFrom: __filename, + env: { + includeNodeModules: { + '@parcel/transformer-js': true, + }, + }, }); - - isLoadedOnMainThread = true; } } + if (map) { + let sourceMap = new SourceMap(options.projectRoot); + sourceMap.addVLQMap(JSON.parse(map)); + if (originalMap) { + sourceMap.extends(originalMap); + } + asset.map = sourceMap; + } + + return asset; +} + +function getDependencyId(opts: DependencyOptions): string { + let id = + // (opts.sourceAssetId ?? '') + + opts.specifier + + (opts.env ? JSON.stringify(objectSortedEntriesDeep(opts.env)) : '') + + // (opts.target ? JSON.stringify(opts.target) : '') + + (opts.pipeline ?? '') + + opts.specifierType + + (opts.bundleBehavior ?? '') + + (opts.priority ?? 'sync') + + (opts.packageConditions ? JSON.stringify(opts.packageConditions) : ''); + return id; +} + +let assetDependencyIdCache = new WeakMap(); + +function addDependency(asset, {meta = {}, ...opts}: DependencyOptions) { + let dep = {...opts, meta}; + let id = getDependencyId(dep); + let dependencyIds = assetDependencyIdCache.get(asset); + if (!dependencyIds) { + dependencyIds = new Set(asset.dependencies.map(d => getDependencyId(d))); + assetDependencyIdCache.set(asset, dependencyIds); + } + if (dependencyIds.has(id)) { + return; + } + dependencyIds.add(id); + asset.dependencies.push(dep); +} + +function addURLDependency(asset, url: string, opts) { + addDependency(asset, { + specifier: url, + specifierType: 'url', + priority: 'lazy', + ...opts, + }); +} + +function ensureDependencySymbols(dep: DependencyOptions) { + // $FlowFixMe[cannot-write] + dep.symbols ??= new Map(); +} + +function getDependencySymbols(dep: DependencyOptions): Map< + Symbol, + {| + isWeak: boolean, + loc: ?SourceLocation, + local: Symbol, + meta?: Meta, + |}, +> { + // $FlowFixMe[incompatible-return] + return nullthrows(dep.symbols); +} + +function setDependencySymbols( + dep: DependencyOptions, + exportSymbol: Symbol, + local: Symbol, + loc: ?SourceLocation, + isWeak: ?boolean, +) { + let symbols: Map< + Symbol, + {| + isWeak: boolean, + loc: ?SourceLocation, + local: Symbol, + meta?: Meta, + |}, + // $FlowFixMe[incompatible-type] + > = nullthrows(dep.symbols); + symbols.set(exportSymbol, { + local, + loc: loc, + isWeak: (symbols.get(exportSymbol)?.isWeak ?? true) && (isWeak ?? false), + }); }