From 34d363eb67ab25b723397181769661334e9cdedb Mon Sep 17 00:00:00 2001 From: magic-akari Date: Tue, 14 Jul 2026 18:34:27 +0800 Subject: [PATCH] refactor(swc): make the primary compilation pipeline linear Move the primary compiler path to an explicit resolve-config, parse, transform, minify, and finalize pipeline, while freezing build_as_input under config::legacy for bundler and custom-pass compatibility. Expose Compiler::compile as a lazy request with codegen and AST terminals. Keep emit-only preparation on codegen, retain comments and helper requirements for AST consumers, and provide paired inspect/mutate hooks after parsing, resolution, and the post-syntax checkpoint. Route process_js_file, process_js, and the default Node and wasm transforms through the direct pipeline. Keep Flow source classification on source input without reclassifying caller-supplied Program variants. --- bindings/binding_core_node/src/transform.rs | 29 +- .../binding_core_wasm/__tests__/simple.js | 98 +- crates/binding_macros/src/wasm.rs | 63 +- crates/swc/src/codegen.rs | 97 ++ crates/swc/src/config/legacy/README.md | 52 + crates/swc/src/config/legacy/build.rs | 1019 ++++++++++++ .../{builder.rs => config/legacy/minifier.rs} | 0 crates/swc/src/config/legacy/mod.rs | 12 + crates/swc/src/config/loader.rs | 201 +++ crates/swc/src/config/mod.rs | 1013 +----------- crates/swc/src/config/tests.rs | 6 +- crates/swc/src/flow.rs | 253 +++ crates/swc/src/input_source_map.rs | 192 +++ crates/swc/src/legacy.rs | 319 ++++ crates/swc/src/lib.rs | 1369 ++--------------- crates/swc/src/minify.rs | 242 +++ crates/swc/src/pipeline.rs | 105 ++ crates/swc/src/pipeline/README.md | 46 + crates/swc/src/pipeline/api.rs | 446 ++++++ crates/swc/src/pipeline/finalize.rs | 72 + crates/swc/src/pipeline/hooks.rs | 107 ++ crates/swc/src/pipeline/lint.rs | 58 + crates/swc/src/pipeline/minify.rs | 89 ++ crates/swc/src/pipeline/options.rs | 528 +++++++ crates/swc/src/pipeline/parse.rs | 39 + crates/swc/src/pipeline/plugin.rs | 157 ++ crates/swc/src/pipeline/preparation.rs | 113 ++ crates/swc/src/pipeline/resolve.rs | 86 ++ crates/swc/src/pipeline/state.rs | 32 + crates/swc/src/pipeline/terminal.rs | 127 ++ crates/swc/src/pipeline/transform.rs | 402 +++++ crates/swc/src/resolver.rs | 39 + crates/swc/tests/legacy.rs | 233 +++ crates/swc/tests/pipeline.rs | 570 +++++++ crates/swc/tests/projects.rs | 81 +- .../pipeline/flow-module-program/input.js | 1 + .../swc/tests/rust-api/pipeline/flow/input.js | 4 + .../tests/rust-api/pipeline/helpers/input.js | 1 + .../tests/rust-api/pipeline/stages/input.tsx | 4 + crates/swc/tests/rust_api.rs | 120 +- .../tests/fixture/stub_wasm/src/lib.rs | 7 +- 41 files changed, 5955 insertions(+), 2477 deletions(-) create mode 100644 crates/swc/src/codegen.rs create mode 100644 crates/swc/src/config/legacy/README.md create mode 100644 crates/swc/src/config/legacy/build.rs rename crates/swc/src/{builder.rs => config/legacy/minifier.rs} (100%) create mode 100644 crates/swc/src/config/legacy/mod.rs create mode 100644 crates/swc/src/config/loader.rs create mode 100644 crates/swc/src/flow.rs create mode 100644 crates/swc/src/input_source_map.rs create mode 100644 crates/swc/src/legacy.rs create mode 100644 crates/swc/src/minify.rs create mode 100644 crates/swc/src/pipeline.rs create mode 100644 crates/swc/src/pipeline/README.md create mode 100644 crates/swc/src/pipeline/api.rs create mode 100644 crates/swc/src/pipeline/finalize.rs create mode 100644 crates/swc/src/pipeline/hooks.rs create mode 100644 crates/swc/src/pipeline/lint.rs create mode 100644 crates/swc/src/pipeline/minify.rs create mode 100644 crates/swc/src/pipeline/options.rs create mode 100644 crates/swc/src/pipeline/parse.rs create mode 100644 crates/swc/src/pipeline/plugin.rs create mode 100644 crates/swc/src/pipeline/preparation.rs create mode 100644 crates/swc/src/pipeline/resolve.rs create mode 100644 crates/swc/src/pipeline/state.rs create mode 100644 crates/swc/src/pipeline/terminal.rs create mode 100644 crates/swc/src/pipeline/transform.rs create mode 100644 crates/swc/src/resolver.rs create mode 100644 crates/swc/tests/legacy.rs create mode 100644 crates/swc/tests/pipeline.rs create mode 100644 crates/swc/tests/rust-api/pipeline/flow-module-program/input.js create mode 100644 crates/swc/tests/rust-api/pipeline/flow/input.js create mode 100644 crates/swc/tests/rust-api/pipeline/helpers/input.js create mode 100644 crates/swc/tests/rust-api/pipeline/stages/input.tsx diff --git a/bindings/binding_core_node/src/transform.rs b/bindings/binding_core_node/src/transform.rs index c0ac1367af02..1f4a3301f357 100644 --- a/bindings/binding_core_node/src/transform.rs +++ b/bindings/binding_core_node/src/transform.rs @@ -10,9 +10,8 @@ use napi::{ }; use path_clean::clean; use swc_core::{ - base::{config::Options, Compiler, TransformOutput}, - common::{comments::SingleThreadedComments, errors::Handler, FileName}, - ecma::ast::noop_pass, + base::{config::Options, CompileInput, Compiler, TransformOutput}, + common::{errors::Handler, FileName, Spanned}, node::{get_deserialized, MapErr}, }; @@ -55,25 +54,19 @@ fn process_program_input( program_input: ProgramInput, options: &Options, ) -> Result { - match program_input { + let (fm, program) = match program_input { ProgramInput::WithContext { program, source_context, - } => { - let (fm, program) = prepare_program_with_context(c, program, source_context)?; - - c.process_js_with_custom_pass( - fm, - Some(program), - handler, - options, - SingleThreadedComments::default(), - |_| noop_pass(), - |_| noop_pass(), - ) + } => prepare_program_with_context(c, program, source_context)?, + ProgramInput::Raw(program) => { + let fm = c.cm.lookup_char_pos(program.span().lo()).file; + (fm, program) } - ProgramInput::Raw(program) => c.process_js(handler, program, options), - } + }; + + c.compile(handler, CompileInput::program(fm, program), options) + .codegen() } #[napi] diff --git a/bindings/binding_core_wasm/__tests__/simple.js b/bindings/binding_core_wasm/__tests__/simple.js index 1e5fb119757c..f00c20e33651 100644 --- a/bindings/binding_core_wasm/__tests__/simple.js +++ b/bindings/binding_core_wasm/__tests__/simple.js @@ -88,6 +88,26 @@ describe("transform", () => { }); }); +function expectParsedClass(output) { + expect(output.type).toBe("Module"); + expect(output.body).toHaveLength(1); + + const declaration = output.body[0]; + expect(declaration).toMatchObject({ + type: "ClassDeclaration", + body: [], + identifier: { + type: "Identifier", + value: "Foo", + }, + }); + expect(declaration.span.end - declaration.span.start).toBe(12); + expect( + declaration.identifier.span.end - declaration.identifier.span.start + ).toBe(3); + expect(output.span).toEqual(declaration.span); +} + describe("parse", () => { it("should work", () => { const output = swc.parseSync("class Foo {}", { @@ -95,44 +115,7 @@ describe("parse", () => { target: "es2021", }); - expect(output).toMatchInlineSnapshot(` - { - "body": [ - { - "body": [], - "ctxt": 0, - "declare": false, - "decorators": [], - "identifier": { - "ctxt": 2, - "optional": false, - "span": { - "end": 254, - "start": 251, - }, - "type": "Identifier", - "value": "Foo", - }, - "implements": [], - "isAbstract": false, - "span": { - "end": 257, - "start": 245, - }, - "superClass": null, - "superTypeParams": null, - "type": "ClassDeclaration", - "typeParams": null, - }, - ], - "interpreter": null, - "span": { - "end": 257, - "start": 245, - }, - "type": "Module", - } - `); + expectParsedClass(output); }); it("should work with async facade", async () => { @@ -141,44 +124,7 @@ describe("parse", () => { target: "es2021", }); - expect(output).toMatchInlineSnapshot(` - { - "body": [ - { - "body": [], - "ctxt": 0, - "declare": false, - "decorators": [], - "identifier": { - "ctxt": 2, - "optional": false, - "span": { - "end": 267, - "start": 264, - }, - "type": "Identifier", - "value": "Foo", - }, - "implements": [], - "isAbstract": false, - "span": { - "end": 270, - "start": 258, - }, - "superClass": null, - "superTypeParams": null, - "type": "ClassDeclaration", - "typeParams": null, - }, - ], - "interpreter": null, - "span": { - "end": 270, - "start": 258, - }, - "type": "Module", - } - `); + expectParsedClass(output); }); }); diff --git a/crates/binding_macros/src/wasm.rs b/crates/binding_macros/src/wasm.rs index e16baf8adfc1..49c06737d47a 100644 --- a/crates/binding_macros/src/wasm.rs +++ b/crates/binding_macros/src/wasm.rs @@ -9,8 +9,6 @@ use once_cell::sync::Lazy; #[doc(hidden)] pub use serde_wasm_bindgen; use serde_wasm_bindgen::Serializer; -#[doc(hidden)] -pub use swc::PrintArgs; use swc::{config::ErrorFormat, Compiler, HandlerOpts}; #[doc(hidden)] pub use swc::{ @@ -18,10 +16,12 @@ pub use swc::{ try_with_handler, }; #[doc(hidden)] +pub use swc::{CompileInput, PrintArgs}; +#[doc(hidden)] pub use swc_common::{ comments::{self, SingleThreadedComments}, errors::Handler, - FileName, Mark, GLOBALS, + FileName, Mark, Spanned, GLOBALS, }; use swc_common::{sync::Lrc, FilePathMapping, SourceMap}; #[doc(hidden)] @@ -296,13 +296,37 @@ macro_rules! build_print { #[macro_export] macro_rules! build_transform_sync { + // Source input uses the direct pipeline by default and the frozen legacy + // path for custom-pass overloads. Program input remains direct in both. ($(#[$m:meta])*) => { - build_transform_sync!($(#[$m])*, |_| $crate::wasm::noop_pass(), |_| $crate::wasm::noop_pass(), Default::default()); + $crate::build_transform_sync!(@impl [$(#[$m])*] pipeline, |_| $crate::wasm::noop_pass(), |_| $crate::wasm::noop_pass(), Default::default()); }; ($(#[$m:meta])*, $before_pass: expr, $after_pass: expr) => { - build_transform_sync!($(#[$m])*, $before_pass, $after_pass, Default::default()); + $crate::build_transform_sync!(@impl [$(#[$m])*] custom, $before_pass, $after_pass, Default::default()); }; ($(#[$m:meta])*, $before_pass: expr, $after_pass: expr, $opt: expr) => { + $crate::build_transform_sync!(@impl [$(#[$m])*] custom, $before_pass, $after_pass, $opt); + }; + (@transform_source pipeline, $c:ident, $handler:ident, $fm:ident, $comments:ident, $opts:ident, $before_pass:expr, $after_pass:expr) => { + $c.compile( + $handler, + $crate::wasm::CompileInput::source($fm).with_comments($comments), + &$opts, + ) + .codegen() + }; + (@transform_source custom, $c:ident, $handler:ident, $fm:ident, $comments:ident, $opts:ident, $before_pass:expr, $after_pass:expr) => { + $c.process_js_with_custom_pass( + $fm, + None, + $handler, + &$opts, + $comments, + $before_pass, + $after_pass, + ) + }; + (@impl [$(#[$m:meta])*] $mode:ident, $before_pass:expr, $after_pass:expr, $opt:expr) => { $(#[$m])* #[allow(unused_variables)] pub fn transform_sync( @@ -385,18 +409,33 @@ macro_rules! build_transform_sync { let file = fm.clone(); let comments = $crate::wasm::SingleThreadedComments::default(); $crate::wasm::anyhow::Context::context( - c.process_js_with_custom_pass( - fm, - None, + $crate::build_transform_sync!( + @transform_source $mode, + c, handler, - &opts, + fm, comments, + opts, $before_pass, - $after_pass, - ), "failed to process js file" + $after_pass + ), + "failed to process js file" )? } - Err(v) => unsafe { c.process_js(handler, $crate::wasm::serde_wasm_bindgen::from_value(v).expect(""), &opts)? }, + Err(v) => { + let program: $crate::wasm::Program = + $crate::wasm::serde_wasm_bindgen::from_value(v).expect(""); + let fm = c + .cm + .lookup_char_pos($crate::wasm::Spanned::span(&program).lo()) + .file; + c.compile( + handler, + $crate::wasm::CompileInput::program(fm, program), + &opts, + ) + .codegen()? + } }; out diff --git a/crates/swc/src/codegen.rs b/crates/swc/src/codegen.rs new file mode 100644 index 000000000000..fbcca7b19e52 --- /dev/null +++ b/crates/swc/src/codegen.rs @@ -0,0 +1,97 @@ +//! Code generation for programs produced by the direct compilation pipeline. + +use std::{path::PathBuf, sync::Arc}; + +use anyhow::Error; +use rustc_hash::FxHashMap; +use swc_atoms::Atom; +use swc_common::{comments::SingleThreadedComments, BytePos, SourceMap}; +use swc_compiler_base::{PrintArgs, SourceMapsConfig, TransformOutput}; +use swc_config::file_pattern::FilePattern; +use swc_ecma_ast::{EsVersion, Program}; + +/// Metadata captured after resolver hooks and before runtime plugins and +/// built-in transforms run. +#[derive(Default)] +pub(super) struct CodegenMetadata { + pub(super) source_map_names: FxHashMap, + pub(super) original_source_map: Option, +} + +/// Configuration required by the code-generation terminal. +pub(super) struct CodegenOptions { + pub(super) source_maps: SourceMapsConfig, + pub(super) output_path: Option, + pub(super) source_root: Option, + pub(super) source_file_name: Option, + pub(super) source_map_ignore_list: Option, + pub(super) inline_sources_content: bool, + pub(super) emit_source_map_columns: bool, + pub(super) preamble: String, + pub(super) source_map_url: Option, + pub(super) ascii_only: bool, + pub(super) minify: bool, + pub(super) emit_assert_for_import_attributes: bool, + pub(super) emit_source_map_scopes: bool, + pub(super) inline_script: bool, +} + +/// A fully transformed program together with metadata prepared for emit. +pub(super) struct CodegenInput { + pub(super) source_map: Arc, + pub(super) program: Program, + pub(super) comments: SingleThreadedComments, + pub(super) metadata: CodegenMetadata, + pub(super) transform_output: FxHashMap, + pub(super) target: EsVersion, + pub(super) options: CodegenOptions, +} + +impl CodegenInput { + /// Generates code from the prepared emit state. + pub(super) fn codegen(self) -> Result { + let Self { + source_map, + program, + comments, + metadata, + transform_output, + target, + options, + } = self; + let transform_output = if transform_output.is_empty() { + None + } else { + Some(transform_output) + }; + + swc_compiler_base::print( + source_map, + &program, + PrintArgs { + source_root: options.source_root.as_deref(), + source_file_name: options.source_file_name.as_deref(), + source_map_ignore_list: options.source_map_ignore_list, + output_path: options.output_path, + inline_sources_content: options.inline_sources_content, + source_map: options.source_maps, + source_map_names: &metadata.source_map_names, + orig: metadata.original_source_map, + comments: Some(&comments), + emit_source_map_columns: options.emit_source_map_columns, + emit_source_map_scopes: options.emit_source_map_scopes, + preamble: &options.preamble, + codegen_config: swc_ecma_codegen::Config::default() + .with_target(target) + .with_minify(options.minify) + .with_ascii_only(options.ascii_only) + .with_emit_assert_for_import_attributes( + options.emit_assert_for_import_attributes, + ) + .with_inline_script(options.inline_script), + output: transform_output, + source_map_url: options.source_map_url.as_deref(), + }, + ) + } +} diff --git a/crates/swc/src/config/legacy/README.md b/crates/swc/src/config/legacy/README.md new file mode 100644 index 000000000000..52b6802d692c --- /dev/null +++ b/crates/swc/src/config/legacy/README.md @@ -0,0 +1,52 @@ +# Frozen pass-building pipeline + +This directory preserves `Options::build_as_input`, `BuiltInput`, +`BuiltInput::with_pass`, and `ModuleConfig::build`. The legacy delayed-input +and custom-pass adapters remain in `../../legacy.rs`; primary compilation uses +the direct pipeline. + +```mermaid +flowchart TD + Entry["legacy entry point"] --> Config["merge config and parse or accept Program"] + Config --> ReactCompiler["React Compiler, when enabled"] + ReactCompiler --> Resolver["initial resolver"] + Resolver --> Build["derive options and construct most delayed passes"] + Build --> Builtins{"built-ins enabled?"} + Builtins -->|yes| BeforeFactory["custom-before factory"] + Builtins -->|no| Assemble["assemble final delayed pass graph"] + BeforeFactory --> Assemble + Assemble --> BuiltInput["BuiltInput { program, pass, emit options }"] + + BuiltInput -- "direct caller / bundler" --> Return["caller owns program and pass"] + BuiltInput -- "process_js_with_custom_pass" --> AfterFactory["custom-after factory"] + AfterFactory --> Append["append custom-after pass"] + Append --> Apply["capture output and apply delayed graph"] + Apply --> Flow["Flow script downgrade, when requested by parsing"] + Flow --> Comments["final comment pruning"] + Comments --> Codegen["codegen"] +``` + +With both custom passes, execution keeps the legacy order: + +```text +plugin-before? → lint → early syntax transforms → TypeScript/Flow stripping +→ plugin-after? → custom-before → React/optimizer → compatibility/module +→ AST minifier → hygiene/fixer → Jest/dropped-comment preservation → custom-after +``` + +- Both custom factories observe `BuiltInput.program` before the delayed graph + runs. "Before" and "after" describe where their returned passes execute. +- With built-ins disabled, `BuiltInput::pass` contains no built-in transforms + and custom-before is skipped. The custom-pass adapter still schedules + custom-after after the remaining pass. +- The frozen pre-parsed `Program` adapters do not repeat Flow script/module + classification. Source input is classified while parsing; a direct + `build_as_input` caller indicates whether a stripped Flow type-only module + must become a script. +- Direct `BuiltInput` consumers own the pass execution environment, metadata, + and emit. The `process_js_with_custom_pass` adapter owns those responsibilities + for legacy custom-pass callers. + +Treat pass order, factory timing, defaults, comments, source maps, and Flow +behavior as compatibility-sensitive. Keep this path compatibility-only; new +compilation behavior belongs in the direct pipeline. diff --git a/crates/swc/src/config/legacy/build.rs b/crates/swc/src/config/legacy/build.rs new file mode 100644 index 000000000000..4c813524a39d --- /dev/null +++ b/crates/swc/src/config/legacy/build.rs @@ -0,0 +1,1019 @@ +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; + +#[cfg(all(feature = "plugin", not(target_arch = "wasm32")))] +use anyhow::Context; +use anyhow::{bail, Error}; +use either::Either; +#[cfg(feature = "plugin")] +use swc_common::plugin::metadata::TransformPluginMetadataContext; +#[cfg(feature = "react-compiler")] +use swc_common::Spanned; +use swc_common::{ + comments::{Comments, SingleThreadedComments}, + errors::Handler, + FileName, Mark, SourceMap, +}; +use swc_compiler_base::SourceMapsConfig; +use swc_config::{ + file_pattern::FilePattern, + is_module::IsModule, + merge::Merge, + types::{BoolOr, BoolOrDataConfig}, +}; +use swc_ecma_ast::{noop_pass, EsVersion, Pass, Program}; +use swc_ecma_ext_transforms::jest; +#[cfg(feature = "lint")] +use swc_ecma_lints::rules::{lint_pass, LintParams}; +use swc_ecma_minifier::{ + js::{JsMinifyCommentOption, JsMinifyOptions}, + option::terser::TerserTopLevelOptions, +}; +use swc_ecma_parser::Syntax; +use swc_ecma_preset_env::{Caniuse, Feature}; +use swc_ecma_transforms::{ + fixer::{fixer, paren_remover}, + helpers, + hygiene::{self, hygiene_with_config}, + optimization::{const_modules, json_parse, simplifier}, + proposals::{ + decorators, explicit_resource_management::explicit_resource_management, + export_default_from, import_attributes, DecoratorVersion, + }, + react::{self, default_pragma, default_pragma_frag}, + resolver, + typescript::{self, TsImportExportAssignConfig}, + Assumptions, +}; +#[cfg(feature = "module")] +use swc_ecma_transforms_module::{ + self as modules, + path::{ImportResolver, Resolver}, + rewriter::import_rewriter, +}; +use swc_ecma_transforms_optimization::simplify::{ + dce::Config as DceConfig, Config as SimplifyConfig, +}; +use swc_ecma_visit::VisitMutWith; +use swc_visit::Optional; + +#[cfg(feature = "react-compiler")] +use super::super::{emit_react_compiler_diagnostics, react_compiler_options}; +use super::{ + super::{ + Config, InputSourceMap, JscConfig, JscOutputConfig, ModuleConfig, Options, OutputCharset, + SimplifyOption, + }, + minifier::MinifierPass, +}; +use crate::dropped_comments_preserver::dropped_comments_preserver; + +impl Options { + /// Builds a legacy program and delayed transform graph. + /// + /// The `parse` callback receives `(syntax, target, is_module)` and should + /// use the supplied comment storage. Its boolean result marks a Flow + /// type-only module that must become a script after type stripping. React + /// Compiler and the initial resolver run immediately; + /// [`BuiltInput::pass`] contains the remaining transforms. + #[allow(clippy::too_many_arguments)] + pub fn build_as_input<'a, P>( + &self, + cm: &Arc, + base: &FileName, + parse: impl FnOnce(Syntax, EsVersion, IsModule) -> Result<(Program, bool), Error>, + output_path: Option<&Path>, + source_root: Option, + source_file_name: Option, + source_map_ignore_list: Option, + + handler: &Handler, + config: Option, + comments: Option<&'a SingleThreadedComments>, + custom_before_pass: impl FnOnce(&Program) -> P, + ) -> Result>, Error> + where + P: 'a + Pass, + { + let mut cfg = self.config.clone(); + + cfg.merge(config.unwrap_or_default()); + + if let FileName::Real(base) = base { + cfg.adjust(base); + } + + let is_module = cfg.is_module.unwrap_or_default(); + + let mut source_maps = self.source_maps.clone(); + source_maps.merge(cfg.source_maps.clone()); + + let JscConfig { + assumptions, + transform, + syntax, + external_helpers, + target, + loose, + keep_class_names, + base_url, + paths, + minify: mut js_minify, + experimental, + #[cfg(feature = "lint")] + lints, + preserve_all_comments, + rewrite_relative_import_extensions, + preserve_symlinks, + .. + } = cfg.jsc; + let loose = loose.into_bool(); + let preserve_all_comments = preserve_all_comments.into_bool(); + let preserve_symlinks = preserve_symlinks.into_bool(); + let keep_class_names = keep_class_names.into_bool(); + let external_helpers = external_helpers.into_bool(); + + let mut assumptions = assumptions.unwrap_or_else(|| { + if loose { + Assumptions::all() + } else { + Assumptions::default() + } + }); + + let unresolved_mark = self.unresolved_mark.unwrap_or_default(); + let top_level_mark = self.top_level_mark.unwrap_or_default(); + + if target.is_some() && cfg.env.is_some() { + bail!("`env` and `jsc.target` cannot be used together"); + } + + let es_version = target.unwrap_or_default(); + + let syntax = syntax.unwrap_or_default(); + + let (mut program, flow_strip_script_like_module) = parse(syntax, es_version, is_module)?; + + let mut transform = transform.into_inner().unwrap_or_default(); + + #[cfg(feature = "react-compiler")] + if let Some(options) = react_compiler_options(transform.react_compiler.clone(), base) { + let fm = if program.span().is_dummy() { + cm.get_source_file(base) + } else { + cm.try_lookup_byte_offset(program.span().lo) + .ok() + .map(|source| source.sf) + }; + + if let Some(fm) = fm { + let source_type = swc_ecma_react_compiler::SourceType::from_program(&program) + .with_typescript(syntax.typescript()); + let result = swc_ecma_react_compiler::transform( + &program, + source_type, + &fm.src, + comments, + options, + ); + emit_react_compiler_diagnostics(handler, &result.diagnostics); + + if let Some(compiled) = result.program { + program = compiled; + } + } else { + handler + .struct_warn("React Compiler is enabled, but the source text is unavailable") + .emit(); + } + } + + #[cfg(not(feature = "react-compiler"))] + if transform.react_compiler.is_true() || transform.react_compiler.is_obj() { + handler + .struct_warn( + "React Compiler is configured, but swc was built without the `react-compiler` \ + feature", + ) + .emit(); + } + + // Resolve before constructing delayed transforms so custom passes can + // use syntax contexts for variable management. + if syntax.typescript() { + assumptions.set_class_methods |= !transform.use_define_for_class_fields.into_bool(); + } + + assumptions.set_public_class_fields |= !transform.use_define_for_class_fields.into_bool(); + + program.visit_mut_with(&mut resolver( + unresolved_mark, + top_level_mark, + syntax.typescript(), + )); + + let default_top_level = program.is_module() && !flow_strip_script_like_module; + + js_minify = js_minify.map(|mut c| { + let compress = c + .compress + .unwrap_as_option(|default| match default { + Some(true) => Some(Default::default()), + _ => None, + }) + .map(|mut c| { + if c.toplevel.is_none() { + c.toplevel = Some(TerserTopLevelOptions::Bool(default_top_level)); + } + + if matches!( + cfg.module, + None | Some(ModuleConfig::Es6(..) | ModuleConfig::NodeNext(..)) + ) { + c.module = true; + } + + c + }) + .map(BoolOrDataConfig::from_obj) + .unwrap_or_else(|| BoolOrDataConfig::from_bool(false)); + + let mangle = c + .mangle + .unwrap_as_option(|default| match default { + Some(true) => Some(Default::default()), + _ => None, + }) + .map(|mut c| { + if c.top_level.is_none() { + c.top_level = Some(default_top_level); + } + + c + }) + .map(BoolOrDataConfig::from_obj) + .unwrap_or_else(|| BoolOrDataConfig::from_bool(false)); + + if c.toplevel.is_none() { + c.toplevel = Some(default_top_level); + } + + JsMinifyOptions { + compress, + mangle, + ..c + } + }); + + if js_minify.is_some() && js_minify.as_ref().unwrap().keep_fnames { + js_minify = js_minify.map(|c| { + let compress = c + .compress + .unwrap_as_option(|default| match default { + Some(true) => Some(Default::default()), + _ => None, + }) + .map(|mut c| { + c.keep_fnames = true; + c + }) + .map(BoolOrDataConfig::from_obj) + .unwrap_or_else(|| BoolOrDataConfig::from_bool(false)); + let mangle = c + .mangle + .unwrap_as_option(|default| match default { + Some(true) => Some(Default::default()), + _ => None, + }) + .map(|mut c| { + c.keep_fn_names = true; + c + }) + .map(BoolOrDataConfig::from_obj) + .unwrap_or_else(|| BoolOrDataConfig::from_bool(false)); + JsMinifyOptions { + compress, + mangle, + ..c + } + }); + } + + if js_minify.is_some() && js_minify.as_ref().unwrap().keep_classnames { + js_minify = js_minify.map(|c| { + let compress = c + .compress + .unwrap_as_option(|default| match default { + Some(true) => Some(Default::default()), + _ => None, + }) + .map(|mut c| { + c.keep_classnames = true; + c + }) + .map(BoolOrDataConfig::from_obj) + .unwrap_or_else(|| BoolOrDataConfig::from_bool(false)); + let mangle = c + .mangle + .unwrap_as_option(|default| match default { + Some(true) => Some(Default::default()), + _ => None, + }) + .map(|mut c| { + c.keep_class_names = true; + c + }) + .map(BoolOrDataConfig::from_obj) + .unwrap_or_else(|| BoolOrDataConfig::from_bool(false)); + JsMinifyOptions { + compress, + mangle, + ..c + } + }); + } + + let preserve_comments = if preserve_all_comments { + BoolOr::Bool(true) + } else { + js_minify + .as_ref() + .map(|v| match v.format.comments.clone().into_inner() { + Some(v) => v, + None => BoolOr::Bool(true), + }) + .unwrap_or_else(|| { + BoolOr::Data(if cfg.minify.into_bool() { + JsMinifyCommentOption::PreserveSomeComments + } else { + JsMinifyCommentOption::PreserveAllComments + }) + }) + }; + + if syntax.typescript() { + transform.legacy_decorator = true.into(); + } + let optimizer = transform.optimizer; + + let const_modules = { + let enabled = transform.const_modules.is_some(); + let config = transform.const_modules.unwrap_or_default(); + + let globals = config.globals; + Optional::new(const_modules(cm.clone(), globals), enabled) + }; + + let json_parse_pass = { + optimizer + .as_ref() + .and_then(|v| v.jsonify) + .as_ref() + .map(|cfg| json_parse(cfg.min_cost)) + }; + + let simplifier_pass = { + if let Some(ref opts) = optimizer.as_ref().and_then(|o| o.simplify) { + match opts { + SimplifyOption::Bool(allow_simplify) => { + if *allow_simplify { + Some(simplifier(unresolved_mark, Default::default())) + } else { + None + } + } + SimplifyOption::Json(cfg) => Some(simplifier( + unresolved_mark, + SimplifyConfig { + dce: DceConfig { + preserve_imports_with_side_effects: cfg + .preserve_imports_with_side_effects, + ..Default::default() + }, + ..Default::default() + }, + )), + } + } else { + None + } + }; + + let optimization = { + optimizer + .and_then(|o| o.globals) + .map(|opts| opts.build(cm, handler)) + }; + + let pass = ( + const_modules, + optimization, + Optional::new(export_default_from(), syntax.export_default_from()), + simplifier_pass, + json_parse_pass, + ); + + let import_export_assign_config = match cfg.module { + Some(ModuleConfig::Es6(..)) => TsImportExportAssignConfig::EsNext, + Some(ModuleConfig::CommonJs(..)) + | Some(ModuleConfig::Amd(..)) + | Some(ModuleConfig::Umd(..)) + | Some(ModuleConfig::SystemJs(..)) => TsImportExportAssignConfig::Preserve, + Some(ModuleConfig::NodeNext(..)) => TsImportExportAssignConfig::NodeNext, + _ => TsImportExportAssignConfig::Classic, + }; + + let verbatim_module_syntax = transform.verbatim_module_syntax.into_bool(); + let ts_enum_is_mutable = transform.ts_enum_is_mutable.into_bool(); + + let charset = cfg.jsc.output.charset.or_else(|| { + if js_minify.as_ref()?.format.ascii_only { + Some(OutputCharset::Ascii) + } else { + None + } + }); + + // inline_script defaults to true, but it's case only if minify is enabled. + // This is because minifier API is compatible with Terser, and Terser + // defaults to true, while by default swc itself doesn't enable + // inline_script by default. + let codegen_inline_script = js_minify.as_ref().is_some_and(|v| v.format.inline_script); + + let preamble = if !cfg.jsc.output.preamble.is_empty() { + cfg.jsc.output.preamble + } else { + js_minify + .as_ref() + .map(|v| v.format.preamble.clone()) + .unwrap_or_default() + }; + + let paths = paths.into_iter().collect(); + let resolver = ModuleConfig::get_resolver( + &base_url, + paths, + base, + cfg.module.as_ref(), + preserve_symlinks, + ); + + let target = es_version; + let inject_helpers = !self.skip_helper_injection; + let fixer_enabled = !self.disable_fixer; + let hygiene_config = if self.disable_hygiene { + None + } else { + Some(hygiene::Config { + keep_class_names, + ..hygiene::Config::hygiene_default() + }) + }; + let env = cfg.env.map(Into::into); + + // Implementing finalize logic directly + #[cfg(feature = "module")] + let (need_analyzer, import_interop, ignore_dynamic) = match cfg.module { + Some(ModuleConfig::CommonJs(ref c)) => (true, c.import_interop(), c.ignore_dynamic), + Some(ModuleConfig::Amd(ref c)) => { + (true, c.config.import_interop(), c.config.ignore_dynamic) + } + Some(ModuleConfig::Umd(ref c)) => { + (true, c.config.import_interop(), c.config.ignore_dynamic) + } + Some(ModuleConfig::SystemJs(_)) + | Some(ModuleConfig::Es6(..)) + | Some(ModuleConfig::NodeNext(..)) + | None => (false, true.into(), true), + }; + + let feature_config = env + .as_ref() + .map(|e: &swc_ecma_preset_env::EnvConfig| e.get_feature_config()); + + // compat + let compat_pass = { + if let Some(env_config) = env { + Either::Left(swc_ecma_preset_env::transform_from_env( + unresolved_mark, + comments.map(|v| v as &dyn Comments), + env_config, + assumptions, + )) + } else { + Either::Right(swc_ecma_preset_env::transform_from_es_version( + unresolved_mark, + comments.map(|v| v as &dyn Comments), + target, + assumptions, + loose, + )) + } + }; + + let is_mangler_enabled = js_minify + .as_ref() + .map(|v| v.mangle.is_obj() || v.mangle.is_true()) + .unwrap_or(false); + + let jsx_preserve = transform.react.runtime == Some(react::Runtime::Preserve); + + #[cfg(feature = "module")] + let rewrite_import_pass: Box = { + let swc_import_rewriter: Box = match resolver.clone() { + Some((base, resolver)) => match cfg.module { + None | Some(ModuleConfig::Es6(..)) | Some(ModuleConfig::NodeNext(..)) => { + Box::new(import_rewriter(base, resolver)) + } + _ => Box::new(noop_pass()), + }, + None => Box::new(noop_pass()), + }; + + let typescript_import_rewriter = Optional::new( + modules::rewriter::typescript_import_rewriter(jsx_preserve), + rewrite_relative_import_extensions.into_bool(), + ); + + // swc_import_rewriter should be in front of typescript_import_rewriter + // because path aliases should be resolved before rewriting relative import + // extensions + Box::new((swc_import_rewriter, typescript_import_rewriter)) + }; + #[cfg(not(feature = "module"))] + let rewrite_import_pass: Box = { + let _ = &resolver; + let _ = &cfg.module; + let _ = rewrite_relative_import_extensions; + Box::new(noop_pass()) + }; + + #[cfg(feature = "module")] + let module_pass: Box = Box::new(( + // module / helper + Optional::new( + modules::import_analysis::import_analyzer(import_interop, ignore_dynamic), + need_analyzer, + ), + // Rewrite import pass should be before inject_helpers pass because typescript import + // rewriter may require ts_rewrite_relative_import_extension helper + rewrite_import_pass, + Optional::new(helpers::inject_helpers(unresolved_mark), inject_helpers), + ModuleConfig::build( + cm.clone(), + comments.map(|v| v as &dyn Comments), + cfg.module, + unresolved_mark, + resolver.clone(), + |f| { + feature_config + .as_ref() + .map_or_else(|| target.caniuse(f), |env| env.caniuse(f)) + }, + ), + )); + #[cfg(not(feature = "module"))] + let module_pass: Box = { + let _ = &cfg.module; + let _ = &resolver; + let _ = &feature_config; + Box::new(( + rewrite_import_pass, + Optional::new(helpers::inject_helpers(unresolved_mark), inject_helpers), + ModuleConfig::build( + cm.clone(), + comments.map(|v| v as &dyn Comments), + cfg.module, + unresolved_mark, + |_f| true, + ), + )) + }; + + let built_pass = ( + pass, + Optional::new( + paren_remover(comments.map(|v| v as &dyn Comments)), + fixer_enabled, + ), + compat_pass, + module_pass, + MinifierPass { + options: js_minify, + cm: cm.clone(), + comments: comments.map(|v| v as &dyn Comments), + top_level_mark, + }, + Optional::new( + hygiene_with_config(swc_ecma_transforms_base::hygiene::Config { + top_level_mark, + ..hygiene_config + .clone() + .unwrap_or_else(hygiene::Config::hygiene_default) + }), + hygiene_config.is_some() && !is_mangler_enabled, + ), + Optional::new(fixer(comments.map(|v| v as &dyn Comments)), fixer_enabled), + ); + + let keep_import_attributes = experimental.keep_import_attributes.into_bool(); + + #[cfg(feature = "plugin")] + let plugin_transforms: Box = { + let transform_filename = match base { + FileName::Real(path) => path.as_os_str().to_str().map(String::from), + FileName::Custom(filename) => Some(filename.to_owned()), + _ => None, + }; + let transform_metadata_context = Arc::new(TransformPluginMetadataContext::new( + transform_filename, + self.env_name.to_owned(), + None, + )); + + // Embedded runtime plugin target, based on assumption we have + // 1. filesystem access for the cache + // 2. embedded runtime can compiles & execute wasm + #[cfg(all(feature = "plugin", not(target_arch = "wasm32")))] + { + let plugin_runtime = self + .runtime_options + .plugin_runtime + .clone() + .context("plugin runtime not configured")?; + + if let Some(plugins) = &experimental.plugins { + crate::plugin::compile_wasm_plugins( + experimental.cache_root.as_deref(), + plugins, + &*plugin_runtime, + ) + .context("Failed to compile wasm plugins")?; + } + + Box::new(crate::plugin::plugins( + experimental.plugins, + experimental.plugin_env_vars, + transform_metadata_context, + comments.cloned(), + cm.clone(), + unresolved_mark, + plugin_runtime, + )) + } + + // Native runtime plugin target, based on assumption we have + // 1. no filesystem access, loading binary / cache management should be + // performed externally + // 2. native runtime compiles & execute wasm (i.e v8 on node, chrome) + #[cfg(all(feature = "plugin", target_arch = "wasm32"))] + { + handler.warn( + "Currently @swc/wasm does not support plugins, plugin transform will be \ + skipped. Refer https://github.com/swc-project/swc/issues/3934 for the details.", + ); + + Box::new(noop_pass()) + } + }; + + #[cfg(not(feature = "plugin"))] + let plugin_transforms: Box = { + if experimental.plugins.is_some() { + handler.warn( + "Plugin is not supported with current @swc/core. Plugin transform will be \ + skipped.", + ); + } + Box::new(noop_pass()) + }; + + let mut plugin_transforms = Some(plugin_transforms); + + let pass: Box = if experimental + .disable_builtin_transforms_for_internal_testing + .into_bool() + { + plugin_transforms.unwrap() + } else { + let jsx_enabled = syntax.jsx() && !jsx_preserve; + + let decorator_pass: Box = + match transform.decorator_version.unwrap_or_default() { + DecoratorVersion::V202112 => Box::new(decorators(decorators::Config { + legacy: transform.legacy_decorator.into_bool(), + emit_metadata: transform.decorator_metadata.into_bool(), + use_define_for_class_fields: !assumptions.set_public_class_fields, + })), + DecoratorVersion::V202203 => Box::new( + swc_ecma_transforms::proposals::decorator_2022_03::decorator_2022_03(), + ), + DecoratorVersion::V202311 => Box::new( + swc_ecma_transforms::proposals::decorator_2023_11::decorator_2023_11(), + ), + }; + #[cfg(feature = "lint")] + let lint = { + use swc_common::SyntaxContext; + let disable_all_lints = experimental.disable_all_lints.into_bool(); + let unresolved_ctxt = SyntaxContext::empty().apply_mark(unresolved_mark); + let top_level_ctxt = SyntaxContext::empty().apply_mark(top_level_mark); + Optional::new( + lint_pass(swc_ecma_lints::rules::all(LintParams { + program: &program, + lint_config: &lints, + top_level_ctxt, + unresolved_ctxt, + es_version, + source_map: cm.clone(), + })), + !disable_all_lints, + ) + }; + Box::new(( + ( + if experimental.run_plugin_first.into_bool() { + plugin_transforms.take() + } else { + None + }, + #[cfg(feature = "lint")] + lint, + // Decorators may use type information + Optional::new(decorator_pass, syntax.decorators()), + Optional::new( + explicit_resource_management(), + syntax.explicit_resource_management(), + ), + // The transform strips import attributes unless they are kept. + Optional::new(import_attributes(), !keep_import_attributes), + ), + ({ + let native_class_properties = !assumptions.set_public_class_fields + && feature_config.as_ref().map_or_else( + || target.caniuse(Feature::ClassProperties), + |env| env.caniuse(Feature::ClassProperties), + ); + + let ts_config = typescript::Config { + import_export_assign_config, + verbatim_module_syntax, + native_class_properties, + ts_enum_is_mutable, + flow_syntax: syntax.flow(), + ..Default::default() + }; + + ( + Optional::new( + typescript::typescript(ts_config, unresolved_mark, top_level_mark), + syntax.typescript() && !jsx_enabled, + ), + Optional::new( + typescript::tsx::>( + cm.clone(), + ts_config, + typescript::TsxConfig { + pragma: Some( + transform + .react + .pragma + .clone() + .unwrap_or_else(default_pragma), + ), + pragma_frag: Some( + transform + .react + .pragma_frag + .clone() + .unwrap_or_else(default_pragma_frag), + ), + }, + comments.map(|v| v as _), + unresolved_mark, + top_level_mark, + ), + syntax.typescript() && jsx_enabled, + ), + ) + }), + ( + plugin_transforms.take(), + custom_before_pass(&program), + // handle jsx + Optional::new( + react::react::<&dyn Comments>( + cm.clone(), + comments.map(|v| v as _), + transform.react, + top_level_mark, + unresolved_mark, + ), + jsx_enabled, + ), + built_pass, + Optional::new(jest::jest(), transform.hidden.jest.into_bool()), + Optional::new( + dropped_comments_preserver(comments.cloned()), + preserve_all_comments, + ), + ), + )) + }; + + Ok(BuiltInput { + program, + minify: cfg.minify.into_bool(), + pass, + external_helpers, + syntax, + target: es_version, + is_module, + source_maps: source_maps.unwrap_or(SourceMapsConfig::Bool(false)), + inline_sources_content: cfg.inline_sources_content.into_bool(), + input_source_map: cfg.input_source_map.clone().unwrap_or_default(), + output_path: output_path.map(|v| v.to_path_buf()), + source_root, + source_file_name, + source_map_ignore_list, + comments: comments.cloned(), + preserve_comments, + emit_source_map_columns: cfg.emit_source_map_columns.into_bool(), + output: JscOutputConfig { + charset, + preamble, + ..cfg.jsc.output + }, + emit_assert_for_import_attributes: experimental + .emit_assert_for_import_attributes + .into_bool(), + emit_source_map_scopes: experimental.emit_source_map_scopes.into_bool(), + codegen_inline_script, + flow_strip_script_like_module, + emit_isolated_dts: experimental.emit_isolated_dts.into_bool(), + unresolved_mark, + #[cfg(feature = "module")] + resolver, + }) + } +} + +/// A program, delayed transform graph, and emit configuration for one legacy +/// compilation. +/// +/// `program` has passed optional React Compiler and the initial resolver; +/// `pass` has not run. +#[non_exhaustive] +pub struct BuiltInput { + pub program: Program, + pub pass: P, + pub syntax: Syntax, + pub target: EsVersion, + /// Minification for **codegen**. Minifier transforms will be inserted into + /// `pass`. + pub minify: bool, + pub external_helpers: bool, + pub source_maps: SourceMapsConfig, + pub input_source_map: InputSourceMap, + pub is_module: IsModule, + pub output_path: Option, + + pub source_root: Option, + pub source_file_name: Option, + pub source_map_ignore_list: Option, + + pub comments: Option, + pub preserve_comments: BoolOr, + + pub inline_sources_content: bool, + pub emit_source_map_columns: bool, + + pub output: JscOutputConfig, + pub emit_assert_for_import_attributes: bool, + pub emit_source_map_scopes: bool, + pub codegen_inline_script: bool, + /// Whether a Flow type-only module must become a script after stripping. + pub flow_strip_script_like_module: bool, + + pub emit_isolated_dts: bool, + pub unresolved_mark: Mark, + #[cfg(feature = "module")] + pub resolver: Option<(FileName, Arc)>, +} + +impl

BuiltInput

+where + P: Pass, +{ + /// Replaces the delayed pass without applying it. + pub fn with_pass(self, map: impl FnOnce(P) -> N) -> BuiltInput + where + N: Pass, + { + BuiltInput { + program: self.program, + pass: map(self.pass), + syntax: self.syntax, + target: self.target, + minify: self.minify, + external_helpers: self.external_helpers, + source_maps: self.source_maps, + input_source_map: self.input_source_map, + is_module: self.is_module, + output_path: self.output_path, + source_root: self.source_root, + source_file_name: self.source_file_name, + source_map_ignore_list: self.source_map_ignore_list, + comments: self.comments, + preserve_comments: self.preserve_comments, + inline_sources_content: self.inline_sources_content, + emit_source_map_columns: self.emit_source_map_columns, + output: self.output, + emit_assert_for_import_attributes: self.emit_assert_for_import_attributes, + emit_source_map_scopes: self.emit_source_map_scopes, + codegen_inline_script: self.codegen_inline_script, + flow_strip_script_like_module: self.flow_strip_script_like_module, + emit_isolated_dts: self.emit_isolated_dts, + unresolved_mark: self.unresolved_mark, + #[cfg(feature = "module")] + resolver: self.resolver, + } + } +} + +#[cfg(feature = "module")] +impl ModuleConfig { + pub fn build<'cmt>( + cm: Arc, + comments: Option<&'cmt dyn Comments>, + config: Option, + unresolved_mark: Mark, + resolver: Option<(FileName, Arc)>, + caniuse: impl Fn(Feature) -> bool, + ) -> Box { + let resolver = if let Some((base, resolver)) = resolver { + Resolver::Real { base, resolver } + } else { + Resolver::Default + }; + + let support_block_scoping = caniuse(Feature::BlockScoping); + let support_arrow = caniuse(Feature::ArrowFunctions); + + let transform_pass = match config { + Some(ModuleConfig::CommonJs(config)) => Box::new(modules::common_js::common_js( + resolver, + unresolved_mark, + config, + modules::common_js::FeatureFlag { + support_block_scoping, + support_arrow, + }, + )) as Box, + Some(ModuleConfig::Umd(config)) => Box::new(modules::umd::umd( + cm, + resolver, + unresolved_mark, + config, + modules::umd::FeatureFlag { + support_block_scoping, + }, + )), + Some(ModuleConfig::Amd(config)) => Box::new(modules::amd::amd( + resolver, + unresolved_mark, + config, + modules::amd::FeatureFlag { + support_block_scoping, + support_arrow, + }, + comments, + )), + Some(ModuleConfig::SystemJs(config)) => Box::new(modules::system_js::system_js( + resolver, + unresolved_mark, + config, + )), + _ => Box::new(noop_pass()), + }; + + Box::new(transform_pass) + } +} + +/// Stub pass builder when the module feature is disabled. +#[cfg(not(feature = "module"))] +impl ModuleConfig { + /// Returns a noop pass when module feature is disabled. + pub fn build<'cmt>( + _cm: Arc, + _comments: Option<&'cmt dyn Comments>, + _config: Option, + _unresolved_mark: Mark, + _caniuse: impl Fn(Feature) -> bool, + ) -> Box { + Box::new(noop_pass()) + } +} diff --git a/crates/swc/src/builder.rs b/crates/swc/src/config/legacy/minifier.rs similarity index 100% rename from crates/swc/src/builder.rs rename to crates/swc/src/config/legacy/minifier.rs diff --git a/crates/swc/src/config/legacy/mod.rs b/crates/swc/src/config/legacy/mod.rs new file mode 100644 index 000000000000..09678fef7df8 --- /dev/null +++ b/crates/swc/src/config/legacy/mod.rs @@ -0,0 +1,12 @@ +//! Frozen pass-building compatibility layer. +//! +//! Primary compilation uses [`crate::Compiler::compile`]. +//! This module preserves the delayed-pass contracts behind +//! `Options::build_as_input`, `BuiltInput`, `ModuleConfig::build`, +//! [`crate::Compiler::parse_js_as_input`], and +//! [`crate::Compiler::process_js_with_custom_pass`]. + +mod build; +mod minifier; + +pub use build::BuiltInput; diff --git a/crates/swc/src/config/loader.rs b/crates/swc/src/config/loader.rs new file mode 100644 index 000000000000..ce597bb25537 --- /dev/null +++ b/crates/swc/src/config/loader.rs @@ -0,0 +1,201 @@ +//! `.swcrc` discovery, parsing, and loading. + +use std::{ + fs::read_to_string, + path::{Path, PathBuf}, +}; + +use anyhow::{bail, Context, Error}; +use jsonc_parser::{parse_to_serde_value, ParseOptions}; +use once_cell::sync::Lazy; +use serde_json::error::Category; +use swc_common::FileName; + +use super::{Config, ConfigFile, Options, Rc, RootMode}; +use crate::Compiler; + +impl Compiler { + #[cfg_attr(debug_assertions, tracing::instrument(target = "swc", skip_all))] + pub fn read_config(&self, opts: &Options, name: &FileName) -> Result, Error> { + static CUR_DIR: Lazy = Lazy::new(|| { + if cfg!(target_arch = "wasm32") { + PathBuf::new() + } else { + ::std::env::current_dir().unwrap() + } + }); + + self.run(|| -> Result<_, Error> { + let Options { + ref root, + root_mode, + swcrc, + config_file, + .. + } = opts; + + let root = root.as_ref().unwrap_or(&CUR_DIR); + + let swcrc_path = match config_file { + Some(ConfigFile::Str(s)) => Some(PathBuf::from(s.clone())), + _ => { + if *swcrc { + if let FileName::Real(ref path) = name { + // Canonicalize relative paths for proper parent traversal + let abs_path = if path.is_relative() { + root.join(path).canonicalize().ok() + } else { + path.canonicalize().ok() + }; + let found = abs_path.and_then(|p| find_swcrc(&p, root, *root_mode)); + + // "upward" mode requires a .swcrc to be found + if found.is_none() && *root_mode == RootMode::Upward { + bail!( + "Could not find .swcrc file while using rootMode \ + \"upward\".\nSearched from: {}", + path.display() + ); + } + + found + } else { + None + } + } else { + None + } + } + }; + + let config_file = match swcrc_path.as_deref() { + Some(s) => Some(load_swcrc(s)?), + _ => None, + }; + let filename_path = match name { + FileName::Real(p) => Some(&**p), + _ => None, + }; + + if let Some(filename_path) = filename_path { + if let Some(config) = config_file { + let dir = swcrc_path + .as_deref() + .and_then(|p| p.parent()) + .expect(".swcrc path should have parent dir"); + + let mut config = config + .into_config(Some(filename_path)) + .context("failed to process config file")?; + + if let Some(c) = &mut config { + if c.jsc.base_url != PathBuf::new() { + let joined = dir.join(&c.jsc.base_url); + c.jsc.base_url = if cfg!(target_os = "windows") + && c.jsc.base_url.as_os_str() == "." + { + dir.canonicalize().with_context(|| { + format!( + "failed to canonicalize base url using the path of \ + .swcrc\nDir: {}\n(Used logic for windows)", + dir.display(), + ) + })? + } else { + joined.canonicalize().with_context(|| { + format!( + "failed to canonicalize base url using the path of \ + .swcrc\nPath: {}\nDir: {}\nbaseUrl: {}", + joined.display(), + dir.display(), + c.jsc.base_url.display() + ) + })? + }; + } + } + + return Ok(config); + } + + let config_file = config_file.unwrap_or_default(); + let config = config_file.into_config(Some(filename_path))?; + + return Ok(config); + } + + let config = match config_file { + Some(config_file) => config_file.into_config(None)?, + None => Rc::default().into_config(None)?, + }; + + match config { + Some(config) => Ok(Some(config)), + None => { + bail!("no config matched for file ({name})") + } + } + }) + .with_context(|| format!("failed to read .swcrc file for input file at `{name}`")) + } +} + +fn find_swcrc(path: &Path, root: &Path, root_mode: RootMode) -> Option { + let mut parent = path.parent(); + while let Some(dir) = parent { + let swcrc = dir.join(".swcrc"); + + if swcrc.exists() { + return Some(swcrc); + } + + if dir == root && root_mode == RootMode::Root { + break; + } + parent = dir.parent(); + } + + None +} + +#[cfg_attr(debug_assertions, tracing::instrument(target = "swc", skip_all))] +fn load_swcrc(path: &Path) -> Result { + let content = read_to_string(path).context("failed to read config (.swcrc) file")?; + + parse_swcrc(&content) +} + +pub(super) fn parse_swcrc(s: &str) -> Result { + fn convert_json_err(e: serde_json::Error) -> Error { + let line = e.line(); + let column = e.column(); + + let msg = match e.classify() { + Category::Io => "io error", + Category::Syntax => "syntax error", + Category::Data => "unmatched data", + Category::Eof => "unexpected eof", + }; + Error::new(e).context(format!( + "failed to deserialize .swcrc (json) file: {msg}: {line}:{column}" + )) + } + + let v = parse_to_serde_value( + s.trim_start_matches('\u{feff}'), + &ParseOptions { + allow_comments: true, + allow_trailing_commas: true, + allow_loose_object_property_names: false, + }, + )? + .ok_or_else(|| Error::msg("failed to deserialize empty .swcrc (json) file"))?; + + if let Ok(rc) = serde_json::from_value(v.clone()) { + return Ok(rc); + } + + serde_json::from_value(v) + .map(Rc::Single) + .map_err(convert_json_err) +} diff --git a/crates/swc/src/config/mod.rs b/crates/swc/src/config/mod.rs index 637071e4de02..71b197f2a48e 100644 --- a/crates/swc/src/config/mod.rs +++ b/crates/swc/src/config/mod.rs @@ -5,87 +5,60 @@ use std::{ sync::Arc, }; -#[cfg(any( - feature = "module", - all(feature = "plugin", not(target_arch = "wasm32")) -))] +#[cfg(feature = "module")] use anyhow::Context; use anyhow::{bail, Error}; use bytes_str::BytesStr; use dashmap::DashMap; -use either::Either; use indexmap::IndexMap; use once_cell::sync::Lazy; use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; use serde::{Deserialize, Serialize}; use swc_atoms::Atom; -#[allow(unused)] -use swc_common::plugin::metadata::TransformPluginMetadataContext; -use swc_common::{ - comments::{Comments, SingleThreadedComments}, - errors::Handler, - FileName, Mark, SourceMap, -}; +use swc_common::{errors::Handler, FileName, Mark, SourceMap}; #[cfg(feature = "react-compiler")] -use swc_common::{BytePos, Span, Spanned}; +use swc_common::{BytePos, Span}; pub use swc_compiler_base::SourceMapsConfig; pub use swc_config::is_module::IsModule; +#[cfg(feature = "react-compiler")] +use swc_config::types::BoolOr; use swc_config::{ file_pattern::FilePattern, merge::Merge, - types::{BoolConfig, BoolOr, BoolOrDataConfig, MergingOption}, + types::{BoolConfig, BoolOrDataConfig, MergingOption}, }; -use swc_ecma_ast::{noop_pass, EsVersion, Expr, Pass, Program}; -use swc_ecma_ext_transforms::jest; +use swc_ecma_ast::{EsVersion, Expr, Pass}; #[cfg(feature = "lint")] -use swc_ecma_lints::{ - config::LintConfig, - rules::{lint_pass, LintParams}, -}; +use swc_ecma_lints::config::LintConfig; #[cfg(feature = "module")] use swc_ecma_loader::resolvers::{ lru::CachingResolver, node::NodeModulesResolver, tsc::TsConfigResolver, }; pub use swc_ecma_minifier::js::*; -use swc_ecma_minifier::option::terser::TerserTopLevelOptions; use swc_ecma_parser::{parse_file_as_expr, Syntax, TsSyntax}; -use swc_ecma_preset_env::{Caniuse, Feature}; pub use swc_ecma_transforms::proposals::DecoratorVersion; -use swc_ecma_transforms::{ - fixer::{fixer, paren_remover}, - helpers, - hygiene::{self, hygiene_with_config}, - optimization::{const_modules, json_parse, simplifier}, - proposals::{ - decorators, explicit_resource_management::explicit_resource_management, - export_default_from, import_attributes, - }, - react::{self, default_pragma, default_pragma_frag}, - resolver, - typescript::{self, TsImportExportAssignConfig}, - Assumptions, -}; +use swc_ecma_transforms::{react, Assumptions}; use swc_ecma_transforms_compat::es2015::regenerator; #[cfg(feature = "module")] use swc_ecma_transforms_module::{ self as modules, - path::{ImportResolver, NodeImportResolver, Resolver}, - rewriter::import_rewriter, + path::{ImportResolver, NodeImportResolver}, util, EsModuleConfig, }; -use swc_ecma_transforms_optimization::{ - inline_globals, - simplify::{dce::Config as DceConfig, Config as SimplifyConfig}, - GlobalExprMap, -}; +use swc_ecma_transforms_optimization::{inline_globals, GlobalExprMap}; use swc_ecma_utils::NodeIgnoringSpan; -use swc_ecma_visit::VisitMutWith; -use swc_visit::Optional; pub use crate::plugin::PluginConfig; + +mod legacy; +mod loader; + +pub use legacy::BuiltInput; + #[cfg(feature = "module")] -use crate::SwcImportResolver; -use crate::{builder::MinifierPass, dropped_comments_preserver::dropped_comments_preserver}; +type SwcImportResolver = Arc< + NodeImportResolver>>>, +>; #[cfg(test)] mod tests; @@ -256,795 +229,6 @@ impl Default for InputSourceMap { } } -impl Options { - /// `parse`: `(syntax, target, is_module)` - /// - /// `parse` should use `comments`. - #[allow(clippy::too_many_arguments)] - pub fn build_as_input<'a, P>( - &self, - cm: &Arc, - base: &FileName, - parse: impl FnOnce(Syntax, EsVersion, IsModule) -> Result<(Program, bool), Error>, - output_path: Option<&Path>, - source_root: Option, - source_file_name: Option, - source_map_ignore_list: Option, - - handler: &Handler, - config: Option, - comments: Option<&'a SingleThreadedComments>, - custom_before_pass: impl FnOnce(&Program) -> P, - ) -> Result>, Error> - where - P: 'a + Pass, - { - let mut cfg = self.config.clone(); - - cfg.merge(config.unwrap_or_default()); - - if let FileName::Real(base) = base { - cfg.adjust(base); - } - - let is_module = cfg.is_module.unwrap_or_default(); - - let mut source_maps = self.source_maps.clone(); - source_maps.merge(cfg.source_maps.clone()); - - let JscConfig { - assumptions, - transform, - syntax, - external_helpers, - target, - loose, - keep_class_names, - base_url, - paths, - minify: mut js_minify, - experimental, - #[cfg(feature = "lint")] - lints, - preserve_all_comments, - rewrite_relative_import_extensions, - preserve_symlinks, - .. - } = cfg.jsc; - let loose = loose.into_bool(); - let preserve_all_comments = preserve_all_comments.into_bool(); - let preserve_symlinks = preserve_symlinks.into_bool(); - let keep_class_names = keep_class_names.into_bool(); - let external_helpers = external_helpers.into_bool(); - - let mut assumptions = assumptions.unwrap_or_else(|| { - if loose { - Assumptions::all() - } else { - Assumptions::default() - } - }); - - let unresolved_mark = self.unresolved_mark.unwrap_or_default(); - let top_level_mark = self.top_level_mark.unwrap_or_default(); - - if target.is_some() && cfg.env.is_some() { - bail!("`env` and `jsc.target` cannot be used together"); - } - - let es_version = target.unwrap_or_default(); - - let syntax = syntax.unwrap_or_default(); - - let (mut program, flow_strip_script_like_module) = parse(syntax, es_version, is_module)?; - - let mut transform = transform.into_inner().unwrap_or_default(); - - #[cfg(feature = "react-compiler")] - if let Some(options) = react_compiler_options(transform.react_compiler.clone(), base) { - let fm = if program.span().is_dummy() { - cm.get_source_file(base) - } else { - cm.try_lookup_byte_offset(program.span().lo) - .ok() - .map(|source| source.sf) - }; - - if let Some(fm) = fm { - let source_type = swc_ecma_react_compiler::SourceType::from_program(&program) - .with_typescript(syntax.typescript()); - let result = swc_ecma_react_compiler::transform( - &program, - source_type, - &fm.src, - comments, - options, - ); - emit_react_compiler_diagnostics(handler, &result.diagnostics); - - if let Some(compiled) = result.program { - program = compiled; - } - } else { - handler - .struct_warn("React Compiler is enabled, but the source text is unavailable") - .emit(); - } - } - - #[cfg(not(feature = "react-compiler"))] - if transform.react_compiler.is_true() || transform.react_compiler.is_obj() { - handler - .struct_warn( - "React Compiler is configured, but swc was built without the `react-compiler` \ - feature", - ) - .emit(); - } - - // Do a resolver pass before everything. - // - // We do this before creating custom passes, so custom passses can use the - // variable management system based on the syntax contexts. - if syntax.typescript() { - assumptions.set_class_methods |= !transform.use_define_for_class_fields.into_bool(); - } - - assumptions.set_public_class_fields |= !transform.use_define_for_class_fields.into_bool(); - - program.visit_mut_with(&mut resolver( - unresolved_mark, - top_level_mark, - syntax.typescript(), - )); - - let default_top_level = program.is_module() && !flow_strip_script_like_module; - - js_minify = js_minify.map(|mut c| { - let compress = c - .compress - .unwrap_as_option(|default| match default { - Some(true) => Some(Default::default()), - _ => None, - }) - .map(|mut c| { - if c.toplevel.is_none() { - c.toplevel = Some(TerserTopLevelOptions::Bool(default_top_level)); - } - - if matches!( - cfg.module, - None | Some(ModuleConfig::Es6(..) | ModuleConfig::NodeNext(..)) - ) { - c.module = true; - } - - c - }) - .map(BoolOrDataConfig::from_obj) - .unwrap_or_else(|| BoolOrDataConfig::from_bool(false)); - - let mangle = c - .mangle - .unwrap_as_option(|default| match default { - Some(true) => Some(Default::default()), - _ => None, - }) - .map(|mut c| { - if c.top_level.is_none() { - c.top_level = Some(default_top_level); - } - - c - }) - .map(BoolOrDataConfig::from_obj) - .unwrap_or_else(|| BoolOrDataConfig::from_bool(false)); - - if c.toplevel.is_none() { - c.toplevel = Some(default_top_level); - } - - JsMinifyOptions { - compress, - mangle, - ..c - } - }); - - if js_minify.is_some() && js_minify.as_ref().unwrap().keep_fnames { - js_minify = js_minify.map(|c| { - let compress = c - .compress - .unwrap_as_option(|default| match default { - Some(true) => Some(Default::default()), - _ => None, - }) - .map(|mut c| { - c.keep_fnames = true; - c - }) - .map(BoolOrDataConfig::from_obj) - .unwrap_or_else(|| BoolOrDataConfig::from_bool(false)); - let mangle = c - .mangle - .unwrap_as_option(|default| match default { - Some(true) => Some(Default::default()), - _ => None, - }) - .map(|mut c| { - c.keep_fn_names = true; - c - }) - .map(BoolOrDataConfig::from_obj) - .unwrap_or_else(|| BoolOrDataConfig::from_bool(false)); - JsMinifyOptions { - compress, - mangle, - ..c - } - }); - } - - if js_minify.is_some() && js_minify.as_ref().unwrap().keep_classnames { - js_minify = js_minify.map(|c| { - let compress = c - .compress - .unwrap_as_option(|default| match default { - Some(true) => Some(Default::default()), - _ => None, - }) - .map(|mut c| { - c.keep_classnames = true; - c - }) - .map(BoolOrDataConfig::from_obj) - .unwrap_or_else(|| BoolOrDataConfig::from_bool(false)); - let mangle = c - .mangle - .unwrap_as_option(|default| match default { - Some(true) => Some(Default::default()), - _ => None, - }) - .map(|mut c| { - c.keep_class_names = true; - c - }) - .map(BoolOrDataConfig::from_obj) - .unwrap_or_else(|| BoolOrDataConfig::from_bool(false)); - JsMinifyOptions { - compress, - mangle, - ..c - } - }); - } - - let preserve_comments = if preserve_all_comments { - BoolOr::Bool(true) - } else { - js_minify - .as_ref() - .map(|v| match v.format.comments.clone().into_inner() { - Some(v) => v, - None => BoolOr::Bool(true), - }) - .unwrap_or_else(|| { - BoolOr::Data(if cfg.minify.into_bool() { - JsMinifyCommentOption::PreserveSomeComments - } else { - JsMinifyCommentOption::PreserveAllComments - }) - }) - }; - - if syntax.typescript() { - transform.legacy_decorator = true.into(); - } - let optimizer = transform.optimizer; - - let const_modules = { - let enabled = transform.const_modules.is_some(); - let config = transform.const_modules.unwrap_or_default(); - - let globals = config.globals; - Optional::new(const_modules(cm.clone(), globals), enabled) - }; - - let json_parse_pass = { - optimizer - .as_ref() - .and_then(|v| v.jsonify) - .as_ref() - .map(|cfg| json_parse(cfg.min_cost)) - }; - - let simplifier_pass = { - if let Some(ref opts) = optimizer.as_ref().and_then(|o| o.simplify) { - match opts { - SimplifyOption::Bool(allow_simplify) => { - if *allow_simplify { - Some(simplifier(unresolved_mark, Default::default())) - } else { - None - } - } - SimplifyOption::Json(cfg) => Some(simplifier( - unresolved_mark, - SimplifyConfig { - dce: DceConfig { - preserve_imports_with_side_effects: cfg - .preserve_imports_with_side_effects, - ..Default::default() - }, - ..Default::default() - }, - )), - } - } else { - None - } - }; - - let optimization = { - optimizer - .and_then(|o| o.globals) - .map(|opts| opts.build(cm, handler)) - }; - - let pass = ( - const_modules, - optimization, - Optional::new(export_default_from(), syntax.export_default_from()), - simplifier_pass, - json_parse_pass, - ); - - let import_export_assign_config = match cfg.module { - Some(ModuleConfig::Es6(..)) => TsImportExportAssignConfig::EsNext, - Some(ModuleConfig::CommonJs(..)) - | Some(ModuleConfig::Amd(..)) - | Some(ModuleConfig::Umd(..)) - | Some(ModuleConfig::SystemJs(..)) => TsImportExportAssignConfig::Preserve, - Some(ModuleConfig::NodeNext(..)) => TsImportExportAssignConfig::NodeNext, - _ => TsImportExportAssignConfig::Classic, - }; - - let verbatim_module_syntax = transform.verbatim_module_syntax.into_bool(); - let ts_enum_is_mutable = transform.ts_enum_is_mutable.into_bool(); - - let charset = cfg.jsc.output.charset.or_else(|| { - if js_minify.as_ref()?.format.ascii_only { - Some(OutputCharset::Ascii) - } else { - None - } - }); - - // inline_script defaults to true, but it's case only if minify is enabled. - // This is because minifier API is compatible with Terser, and Terser - // defaults to true, while by default swc itself doesn't enable - // inline_script by default. - let codegen_inline_script = js_minify.as_ref().is_some_and(|v| v.format.inline_script); - - let preamble = if !cfg.jsc.output.preamble.is_empty() { - cfg.jsc.output.preamble - } else { - js_minify - .as_ref() - .map(|v| v.format.preamble.clone()) - .unwrap_or_default() - }; - - let paths = paths.into_iter().collect(); - let resolver = ModuleConfig::get_resolver( - &base_url, - paths, - base, - cfg.module.as_ref(), - preserve_symlinks, - ); - - let target = es_version; - let inject_helpers = !self.skip_helper_injection; - let fixer_enabled = !self.disable_fixer; - let hygiene_config = if self.disable_hygiene { - None - } else { - Some(hygiene::Config { - keep_class_names, - ..hygiene::Config::hygiene_default() - }) - }; - let env = cfg.env.map(Into::into); - - // Implementing finalize logic directly - #[cfg(feature = "module")] - let (need_analyzer, import_interop, ignore_dynamic) = match cfg.module { - Some(ModuleConfig::CommonJs(ref c)) => (true, c.import_interop(), c.ignore_dynamic), - Some(ModuleConfig::Amd(ref c)) => { - (true, c.config.import_interop(), c.config.ignore_dynamic) - } - Some(ModuleConfig::Umd(ref c)) => { - (true, c.config.import_interop(), c.config.ignore_dynamic) - } - Some(ModuleConfig::SystemJs(_)) - | Some(ModuleConfig::Es6(..)) - | Some(ModuleConfig::NodeNext(..)) - | None => (false, true.into(), true), - }; - - let feature_config = env - .as_ref() - .map(|e: &swc_ecma_preset_env::EnvConfig| e.get_feature_config()); - - // compat - let compat_pass = { - if let Some(env_config) = env { - Either::Left(swc_ecma_preset_env::transform_from_env( - unresolved_mark, - comments.map(|v| v as &dyn Comments), - env_config, - assumptions, - )) - } else { - Either::Right(swc_ecma_preset_env::transform_from_es_version( - unresolved_mark, - comments.map(|v| v as &dyn Comments), - target, - assumptions, - loose, - )) - } - }; - - let is_mangler_enabled = js_minify - .as_ref() - .map(|v| v.mangle.is_obj() || v.mangle.is_true()) - .unwrap_or(false); - - let jsx_preserve = transform.react.runtime == Some(react::Runtime::Preserve); - - #[cfg(feature = "module")] - let rewrite_import_pass: Box = { - let swc_import_rewriter: Box = match resolver.clone() { - Some((base, resolver)) => match cfg.module { - None | Some(ModuleConfig::Es6(..)) | Some(ModuleConfig::NodeNext(..)) => { - Box::new(import_rewriter(base, resolver)) - } - _ => Box::new(noop_pass()), - }, - None => Box::new(noop_pass()), - }; - - let typescript_import_rewriter = Optional::new( - modules::rewriter::typescript_import_rewriter(jsx_preserve), - rewrite_relative_import_extensions.into_bool(), - ); - - // swc_import_rewriter should be in front of typescript_import_rewriter - // because path aliases should be resolved before rewriting relative import - // extensions - Box::new((swc_import_rewriter, typescript_import_rewriter)) - }; - #[cfg(not(feature = "module"))] - let rewrite_import_pass: Box = { - let _ = &resolver; - let _ = &cfg.module; - let _ = rewrite_relative_import_extensions; - Box::new(noop_pass()) - }; - - #[cfg(feature = "module")] - let module_pass: Box = Box::new(( - // module / helper - Optional::new( - modules::import_analysis::import_analyzer(import_interop, ignore_dynamic), - need_analyzer, - ), - // Rewrite import pass should be before inject_helpers pass because typescript import - // rewriter may require ts_rewrite_relative_import_extension helper - rewrite_import_pass, - Optional::new(helpers::inject_helpers(unresolved_mark), inject_helpers), - ModuleConfig::build( - cm.clone(), - comments.map(|v| v as &dyn Comments), - cfg.module, - unresolved_mark, - resolver.clone(), - |f| { - feature_config - .as_ref() - .map_or_else(|| target.caniuse(f), |env| env.caniuse(f)) - }, - ), - )); - #[cfg(not(feature = "module"))] - let module_pass: Box = { - let _ = &cfg.module; - let _ = &resolver; - let _ = &feature_config; - Box::new(( - rewrite_import_pass, - Optional::new(helpers::inject_helpers(unresolved_mark), inject_helpers), - ModuleConfig::build( - cm.clone(), - comments.map(|v| v as &dyn Comments), - cfg.module, - unresolved_mark, - |_f| true, - ), - )) - }; - - let built_pass = ( - pass, - Optional::new( - paren_remover(comments.map(|v| v as &dyn Comments)), - fixer_enabled, - ), - compat_pass, - module_pass, - MinifierPass { - options: js_minify, - cm: cm.clone(), - comments: comments.map(|v| v as &dyn Comments), - top_level_mark, - }, - Optional::new( - hygiene_with_config(swc_ecma_transforms_base::hygiene::Config { - top_level_mark, - ..hygiene_config - .clone() - .unwrap_or_else(hygiene::Config::hygiene_default) - }), - hygiene_config.is_some() && !is_mangler_enabled, - ), - Optional::new(fixer(comments.map(|v| v as &dyn Comments)), fixer_enabled), - ); - - let keep_import_attributes = experimental.keep_import_attributes.into_bool(); - - #[cfg(feature = "plugin")] - let plugin_transforms: Box = { - let transform_filename = match base { - FileName::Real(path) => path.as_os_str().to_str().map(String::from), - FileName::Custom(filename) => Some(filename.to_owned()), - _ => None, - }; - let transform_metadata_context = Arc::new(TransformPluginMetadataContext::new( - transform_filename, - self.env_name.to_owned(), - None, - )); - - // Embedded runtime plugin target, based on assumption we have - // 1. filesystem access for the cache - // 2. embedded runtime can compiles & execute wasm - #[cfg(all(feature = "plugin", not(target_arch = "wasm32")))] - { - let plugin_runtime = self - .runtime_options - .plugin_runtime - .clone() - .context("plugin runtime not configured")?; - - if let Some(plugins) = &experimental.plugins { - crate::plugin::compile_wasm_plugins( - experimental.cache_root.as_deref(), - plugins, - &*plugin_runtime, - ) - .context("Failed to compile wasm plugins")?; - } - - Box::new(crate::plugin::plugins( - experimental.plugins, - experimental.plugin_env_vars, - transform_metadata_context, - comments.cloned(), - cm.clone(), - unresolved_mark, - plugin_runtime, - )) - } - - // Native runtime plugin target, based on assumption we have - // 1. no filesystem access, loading binary / cache management should be - // performed externally - // 2. native runtime compiles & execute wasm (i.e v8 on node, chrome) - #[cfg(all(feature = "plugin", target_arch = "wasm32"))] - { - handler.warn( - "Currently @swc/wasm does not support plugins, plugin transform will be \ - skipped. Refer https://github.com/swc-project/swc/issues/3934 for the details.", - ); - - Box::new(noop_pass()) - } - }; - - #[cfg(not(feature = "plugin"))] - let plugin_transforms: Box = { - if experimental.plugins.is_some() { - handler.warn( - "Plugin is not supported with current @swc/core. Plugin transform will be \ - skipped.", - ); - } - Box::new(noop_pass()) - }; - - let mut plugin_transforms = Some(plugin_transforms); - - let pass: Box = if experimental - .disable_builtin_transforms_for_internal_testing - .into_bool() - { - plugin_transforms.unwrap() - } else { - let jsx_enabled = syntax.jsx() && !jsx_preserve; - - let decorator_pass: Box = - match transform.decorator_version.unwrap_or_default() { - DecoratorVersion::V202112 => Box::new(decorators(decorators::Config { - legacy: transform.legacy_decorator.into_bool(), - emit_metadata: transform.decorator_metadata.into_bool(), - use_define_for_class_fields: !assumptions.set_public_class_fields, - })), - DecoratorVersion::V202203 => Box::new( - swc_ecma_transforms::proposals::decorator_2022_03::decorator_2022_03(), - ), - DecoratorVersion::V202311 => Box::new( - swc_ecma_transforms::proposals::decorator_2023_11::decorator_2023_11(), - ), - }; - #[cfg(feature = "lint")] - let lint = { - use swc_common::SyntaxContext; - let disable_all_lints = experimental.disable_all_lints.into_bool(); - let unresolved_ctxt = SyntaxContext::empty().apply_mark(unresolved_mark); - let top_level_ctxt = SyntaxContext::empty().apply_mark(top_level_mark); - Optional::new( - lint_pass(swc_ecma_lints::rules::all(LintParams { - program: &program, - lint_config: &lints, - top_level_ctxt, - unresolved_ctxt, - es_version, - source_map: cm.clone(), - })), - !disable_all_lints, - ) - }; - Box::new(( - ( - if experimental.run_plugin_first.into_bool() { - plugin_transforms.take() - } else { - None - }, - #[cfg(feature = "lint")] - lint, - // Decorators may use type information - Optional::new(decorator_pass, syntax.decorators()), - Optional::new( - explicit_resource_management(), - syntax.explicit_resource_management(), - ), - // The transform strips import assertions, so it's only enabled if - // keep_import_assertions is false. - Optional::new(import_attributes(), !keep_import_attributes), - ), - ({ - let native_class_properties = !assumptions.set_public_class_fields - && feature_config.as_ref().map_or_else( - || target.caniuse(Feature::ClassProperties), - |env| env.caniuse(Feature::ClassProperties), - ); - - let ts_config = typescript::Config { - import_export_assign_config, - verbatim_module_syntax, - native_class_properties, - ts_enum_is_mutable, - flow_syntax: syntax.flow(), - ..Default::default() - }; - - ( - Optional::new( - typescript::typescript(ts_config, unresolved_mark, top_level_mark), - syntax.typescript() && !jsx_enabled, - ), - Optional::new( - typescript::tsx::>( - cm.clone(), - ts_config, - typescript::TsxConfig { - pragma: Some( - transform - .react - .pragma - .clone() - .unwrap_or_else(default_pragma), - ), - pragma_frag: Some( - transform - .react - .pragma_frag - .clone() - .unwrap_or_else(default_pragma_frag), - ), - }, - comments.map(|v| v as _), - unresolved_mark, - top_level_mark, - ), - syntax.typescript() && jsx_enabled, - ), - ) - }), - ( - plugin_transforms.take(), - custom_before_pass(&program), - // handle jsx - Optional::new( - react::react::<&dyn Comments>( - cm.clone(), - comments.map(|v| v as _), - transform.react, - top_level_mark, - unresolved_mark, - ), - jsx_enabled, - ), - built_pass, - Optional::new(jest::jest(), transform.hidden.jest.into_bool()), - Optional::new( - dropped_comments_preserver(comments.cloned()), - preserve_all_comments, - ), - ), - )) - }; - - Ok(BuiltInput { - program, - minify: cfg.minify.into_bool(), - pass, - external_helpers, - syntax, - target: es_version, - is_module, - source_maps: source_maps.unwrap_or(SourceMapsConfig::Bool(false)), - inline_sources_content: cfg.inline_sources_content.into_bool(), - input_source_map: cfg.input_source_map.clone().unwrap_or_default(), - output_path: output_path.map(|v| v.to_path_buf()), - source_root, - source_file_name, - source_map_ignore_list, - comments: comments.cloned(), - preserve_comments, - emit_source_map_columns: cfg.emit_source_map_columns.into_bool(), - output: JscOutputConfig { - charset, - preamble, - ..cfg.jsc.output - }, - emit_assert_for_import_attributes: experimental - .emit_assert_for_import_attributes - .into_bool(), - emit_source_map_scopes: experimental.emit_source_map_scopes.into_bool(), - codegen_inline_script, - flow_strip_script_like_module, - emit_isolated_dts: experimental.emit_isolated_dts.into_bool(), - unresolved_mark, - #[cfg(feature = "module")] - resolver, - }) - } -} - #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] pub enum RootMode { #[default] @@ -1322,83 +506,6 @@ impl Config { } } -/// One `BuiltConfig` per a directory with swcrc -#[non_exhaustive] -pub struct BuiltInput { - pub program: Program, - pub pass: P, - pub syntax: Syntax, - pub target: EsVersion, - /// Minification for **codegen**. Minifier transforms will be inserted into - /// `pass`. - pub minify: bool, - pub external_helpers: bool, - pub source_maps: SourceMapsConfig, - pub input_source_map: InputSourceMap, - pub is_module: IsModule, - pub output_path: Option, - - pub source_root: Option, - pub source_file_name: Option, - pub source_map_ignore_list: Option, - - pub comments: Option, - pub preserve_comments: BoolOr, - - pub inline_sources_content: bool, - pub emit_source_map_columns: bool, - - pub output: JscOutputConfig, - pub emit_assert_for_import_attributes: bool, - pub emit_source_map_scopes: bool, - pub codegen_inline_script: bool, - pub flow_strip_script_like_module: bool, - - pub emit_isolated_dts: bool, - pub unresolved_mark: Mark, - #[cfg(feature = "module")] - pub resolver: Option<(FileName, Arc)>, -} - -impl

BuiltInput

-where - P: Pass, -{ - pub fn with_pass(self, map: impl FnOnce(P) -> N) -> BuiltInput - where - N: Pass, - { - BuiltInput { - program: self.program, - pass: map(self.pass), - syntax: self.syntax, - target: self.target, - minify: self.minify, - external_helpers: self.external_helpers, - source_maps: self.source_maps, - input_source_map: self.input_source_map, - is_module: self.is_module, - output_path: self.output_path, - source_root: self.source_root, - source_file_name: self.source_file_name, - source_map_ignore_list: self.source_map_ignore_list, - comments: self.comments, - preserve_comments: self.preserve_comments, - inline_sources_content: self.inline_sources_content, - emit_source_map_columns: self.emit_source_map_columns, - output: self.output, - emit_assert_for_import_attributes: self.emit_assert_for_import_attributes, - emit_source_map_scopes: self.emit_source_map_scopes, - codegen_inline_script: self.codegen_inline_script, - flow_strip_script_like_module: self.flow_strip_script_like_module, - emit_isolated_dts: self.emit_isolated_dts, - unresolved_mark: self.unresolved_mark, - #[cfg(feature = "module")] - resolver: self.resolver, - } - } -} - /// `jsc` in `.swcrc`. #[derive(Debug, Default, Clone, Serialize, Deserialize, Merge)] #[serde(deny_unknown_fields, rename_all = "camelCase")] @@ -1446,7 +553,7 @@ pub struct JscConfig { #[serde(default)] pub output: JscOutputConfig, - /// https://www.typescriptlang.org/tsconfig/#rewriteRelativeImportExtensions + /// #[serde(default)] pub rewrite_relative_import_extensions: BoolConfig, @@ -1497,7 +604,7 @@ pub struct JscExperimental { pub plugins: Option>, #[serde(default)] pub plugin_env_vars: Option>, - /// If true, keeps import assertions in the output. + /// If true, keeps import attributes in the output. #[serde(default, alias = "keepImportAssertions")] pub keep_import_attributes: BoolConfig, @@ -1611,63 +718,6 @@ pub enum ModuleConfig { #[cfg(feature = "module")] impl ModuleConfig { - pub fn build<'cmt>( - cm: Arc, - comments: Option<&'cmt dyn Comments>, - config: Option, - unresolved_mark: Mark, - resolver: Option<(FileName, Arc)>, - caniuse: impl Fn(Feature) -> bool, - ) -> Box { - let resolver = if let Some((base, resolver)) = resolver { - Resolver::Real { base, resolver } - } else { - Resolver::Default - }; - - let support_block_scoping = caniuse(Feature::BlockScoping); - let support_arrow = caniuse(Feature::ArrowFunctions); - - let transform_pass = match config { - Some(ModuleConfig::CommonJs(config)) => Box::new(modules::common_js::common_js( - resolver, - unresolved_mark, - config, - modules::common_js::FeatureFlag { - support_block_scoping, - support_arrow, - }, - )) as Box, - Some(ModuleConfig::Umd(config)) => Box::new(modules::umd::umd( - cm, - resolver, - unresolved_mark, - config, - modules::umd::FeatureFlag { - support_block_scoping, - }, - )), - Some(ModuleConfig::Amd(config)) => Box::new(modules::amd::amd( - resolver, - unresolved_mark, - config, - modules::amd::FeatureFlag { - support_block_scoping, - support_arrow, - }, - comments, - )), - Some(ModuleConfig::SystemJs(config)) => Box::new(modules::system_js::system_js( - resolver, - unresolved_mark, - config, - )), - _ => Box::new(noop_pass()), - }; - - Box::new(transform_pass) - } - pub fn get_resolver( base_url: &Path, paths: CompiledPaths, @@ -1743,17 +793,6 @@ impl ModuleConfig { /// Stub impl when module feature is disabled #[cfg(not(feature = "module"))] impl ModuleConfig { - /// Returns a noop pass when module feature is disabled. - pub fn build<'cmt>( - _cm: Arc, - _comments: Option<&'cmt dyn Comments>, - _config: Option, - _unresolved_mark: Mark, - _caniuse: impl Fn(Feature) -> bool, - ) -> Box { - Box::new(noop_pass()) - } - /// Returns None when module feature is disabled. #[allow(clippy::type_complexity)] pub fn get_resolver( @@ -1798,11 +837,11 @@ pub struct TransformConfig { #[deprecated] pub treat_const_enum_as_enum: BoolConfig, - /// https://www.typescriptlang.org/tsconfig#useDefineForClassFields + /// #[serde(default)] pub use_define_for_class_fields: BoolConfig, - /// https://www.typescriptlang.org/tsconfig#verbatimModuleSyntax + /// #[serde(default)] pub verbatim_module_syntax: BoolConfig, @@ -2035,7 +1074,7 @@ impl From for swc_ecma_react_compiler::Dynamic } #[cfg(feature = "react-compiler")] -fn react_compiler_options( +pub(crate) fn react_compiler_options( config: BoolOrDataConfig, base: &FileName, ) -> Option { @@ -2053,7 +1092,7 @@ fn react_compiler_options( } #[cfg(feature = "react-compiler")] -fn emit_react_compiler_diagnostics( +pub(crate) fn emit_react_compiler_diagnostics( handler: &Handler, diagnostics: &[swc_ecma_react_compiler::diagnostics::DiagnosticMessage], ) { diff --git a/crates/swc/src/config/tests.rs b/crates/swc/src/config/tests.rs index c715d2c9301b..b35f7cddcaf2 100644 --- a/crates/swc/src/config/tests.rs +++ b/crates/swc/src/config/tests.rs @@ -1,8 +1,8 @@ use swc_config::types::BoolOr; -use crate::{ - config::{Rc, ReactCompilerCompilationMode, ReactCompilerOutputMode, ReactCompilerTarget}, - parse_swcrc, +use super::{ + loader::parse_swcrc, Rc, ReactCompilerCompilationMode, ReactCompilerOutputMode, + ReactCompilerTarget, }; #[test] diff --git a/crates/swc/src/flow.rs b/crates/swc/src/flow.rs new file mode 100644 index 000000000000..87da1d264551 --- /dev/null +++ b/crates/swc/src/flow.rs @@ -0,0 +1,253 @@ +//! Flow-aware parsing and script/module classification shared by the compiler +//! pipelines. + +use std::sync::Arc; + +use anyhow::{bail, Error}; +use swc_common::{comments::Comments, errors::Handler, SourceFile, Span, Spanned}; +use swc_ecma_ast::{ + Decl, DefaultDecl, EsVersion, Module, ModuleDecl, ModuleItem, Program, Script, TsNamespaceBody, +}; +use swc_ecma_parser::{error::SyntaxError, parse_file_as_program, parse_file_as_script, Syntax}; + +use crate::{config::IsModule, Compiler}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ScriptLikeModuleKind { + Script, + TypeOnlyModule, + RuntimeModule(Span), +} + +impl Compiler { + /// Parses transform input while preserving Flow's script-like module + /// semantics. + pub(crate) fn parse_js_as_transform_input( + &self, + fm: Arc, + handler: &Handler, + target: EsVersion, + syntax: Syntax, + is_module: IsModule, + comments: Option<&dyn Comments>, + ) -> Result<(Program, bool), Error> { + if !syntax.flow() { + return self + .parse_js(fm, handler, target, syntax, is_module, comments) + .map(|program| (program, false)); + } + + if matches!(is_module, IsModule::Bool(false)) { + let mut errors = Vec::new(); + match parse_file_as_script(&fm, syntax, target, comments, &mut errors) { + Ok(script) => { + emit_parser_recoverable_errors(handler, errors)?; + return Ok((Program::Script(script), false)); + } + Err(err) if matches!(err.kind(), SyntaxError::ImportExportInScript) => {} + Err(err) => { + emit_parser_recoverable_errors(handler, errors)?; + err.into_diagnostic(handler).emit(); + return Err(Error::msg("Syntax Error")); + } + } + + let mut errors = Vec::new(); + let program = parse_file_as_program(&fm, syntax, target, comments, &mut errors) + .map_err(|err| { + err.into_diagnostic(handler).emit(); + Error::msg("Syntax Error") + })?; + + emit_parser_recoverable_errors(handler, errors)?; + + match classify_script_like_module(&program) { + ScriptLikeModuleKind::Script => Ok((program, false)), + ScriptLikeModuleKind::TypeOnlyModule => Ok((program, true)), + ScriptLikeModuleKind::RuntimeModule(span) => { + handler + .struct_span_err(span, &SyntaxError::ImportExportInScript.msg()) + .emit(); + Err(Error::msg("Syntax Error")) + } + } + } else { + let program = self.parse_js(fm, handler, target, syntax, is_module, comments)?; + let flow_strip_script_like_module = matches!( + classify_script_like_module(&program), + ScriptLikeModuleKind::TypeOnlyModule + ) && matches!(is_module, IsModule::Unknown); + + Ok((program, flow_strip_script_like_module)) + } + } +} + +/// Converts a stripped Flow type-only module back into a script. +pub(crate) fn downgrade_script_like_module(program: Program) -> Result { + let Program::Module(module) = program else { + return Ok(program); + }; + + if module + .body + .iter() + .any(|module_item| matches!(module_item, ModuleItem::ModuleDecl(..))) + { + bail!( + "failed to downgrade Flow type-only module to script because module declarations \ + remain after stripping" + ); + } + + let Module { + span, + body, + shebang, + } = module; + + let body = body + .into_iter() + .map(|module_item| match module_item { + ModuleItem::Stmt(stmt) => Ok(stmt), + ModuleItem::ModuleDecl(..) => bail!( + "failed to downgrade Flow type-only module to script because module declarations \ + remain after stripping" + ), + }) + .collect::>()?; + + Ok(Program::Script(Script { + span, + body, + shebang, + })) +} + +fn emit_parser_recoverable_errors( + handler: &Handler, + errors: Vec, +) -> Result<(), Error> { + if errors.is_empty() { + return Ok(()); + } + + for error in errors { + error.into_diagnostic(handler).emit(); + } + + Err(Error::msg("Syntax Error")) +} + +fn classify_script_like_module(program: &Program) -> ScriptLikeModuleKind { + let Program::Module(module) = program else { + return ScriptLikeModuleKind::Script; + }; + + classify_script_like_module_body(module) +} + +fn classify_script_like_module_body(module: &Module) -> ScriptLikeModuleKind { + let mut saw_module_decl = false; + + for module_item in &module.body { + let Some(module_decl) = module_item.as_module_decl() else { + continue; + }; + + saw_module_decl = true; + + if is_runtime_module_decl(module_decl) { + return ScriptLikeModuleKind::RuntimeModule(module_item.span()); + } + } + + if saw_module_decl { + ScriptLikeModuleKind::TypeOnlyModule + } else { + let span = module + .body + .first() + .map(Spanned::span) + .unwrap_or(module.span); + ScriptLikeModuleKind::RuntimeModule(span) + } +} + +fn is_runtime_module_decl(module_decl: &ModuleDecl) -> bool { + match module_decl { + ModuleDecl::Import(import_decl) => !import_decl.type_only, + ModuleDecl::ExportDecl(export_decl) => is_runtime_decl(&export_decl.decl), + ModuleDecl::ExportNamed(named_export) => !named_export.type_only, + ModuleDecl::ExportDefaultDecl(export_default_decl) => { + is_runtime_default_decl(&export_default_decl.decl) + } + ModuleDecl::ExportDefaultExpr(..) => true, + ModuleDecl::ExportAll(export_all) => !export_all.type_only, + ModuleDecl::TsImportEquals(ts_import_equals_decl) => !ts_import_equals_decl.is_type_only, + ModuleDecl::TsExportAssignment(..) => true, + ModuleDecl::TsNamespaceExport(..) => false, + } +} + +fn is_runtime_decl(decl: &Decl) -> bool { + if is_declare_decl(decl) { + return false; + } + + match decl { + Decl::TsInterface(..) | Decl::TsTypeAlias(..) => false, + Decl::Fn(function_decl) => function_decl.function.body.is_some(), + Decl::Class(..) | Decl::Var(..) | Decl::Using(..) | Decl::TsEnum(..) => true, + Decl::TsModule(ts_module_decl) => ts_module_decl + .body + .as_ref() + .map(is_runtime_namespace_body) + .unwrap_or_default(), + } +} + +fn is_runtime_default_decl(default_decl: &DefaultDecl) -> bool { + match default_decl { + DefaultDecl::Class(..) => true, + DefaultDecl::Fn(function_expr) => function_expr.function.body.is_some(), + DefaultDecl::TsInterfaceDecl(..) => false, + } +} + +fn is_runtime_namespace_body(namespace_body: &TsNamespaceBody) -> bool { + match namespace_body { + TsNamespaceBody::TsModuleBlock(ts_module_block) => { + ts_module_block + .body + .iter() + .any(|module_item| match module_item { + ModuleItem::Stmt(stmt) => is_runtime_stmt(stmt), + ModuleItem::ModuleDecl(module_decl) => is_runtime_module_decl(module_decl), + }) + } + TsNamespaceBody::TsNamespaceDecl(ts_namespace_decl) => { + is_runtime_namespace_body(&ts_namespace_decl.body) + } + } +} + +fn is_runtime_stmt(stmt: &swc_ecma_ast::Stmt) -> bool { + match stmt { + swc_ecma_ast::Stmt::Empty(..) => false, + swc_ecma_ast::Stmt::Decl(decl) => is_runtime_decl(decl), + _ => true, + } +} + +fn is_declare_decl(decl: &Decl) -> bool { + match decl { + Decl::Class(class_decl) => class_decl.declare, + Decl::Fn(function_decl) => function_decl.declare, + Decl::Var(var_decl) => var_decl.declare, + Decl::Using(..) => false, + Decl::TsInterface(..) | Decl::TsTypeAlias(..) => true, + Decl::TsEnum(ts_enum_decl) => ts_enum_decl.declare, + Decl::TsModule(ts_module_decl) => ts_module_decl.declare || ts_module_decl.global, + } +} diff --git a/crates/swc/src/input_source_map.rs b/crates/swc/src/input_source_map.rs new file mode 100644 index 000000000000..0eade6a36243 --- /dev/null +++ b/crates/swc/src/input_source_map.rs @@ -0,0 +1,192 @@ +//! Input source-map discovery and decoding. + +use std::{fs::File, io::ErrorKind, path::PathBuf}; + +use anyhow::{bail, Context, Error}; +use base64::prelude::{Engine, BASE64_STANDARD}; +use swc_common::{comments::Comment, FileName, SourceFile}; +#[cfg(debug_assertions)] +use tracing::warn; +use url::Url; + +use crate::{config::InputSourceMap, sourcemap, Compiler}; + +impl Compiler { + pub(crate) fn get_orig_src_map( + &self, + fm: &SourceFile, + input_src_map: &InputSourceMap, + comments: &[Comment], + is_default: bool, + ) -> Result, Error> { + self.run(|| -> Result<_, Error> { + let name = &fm.name; + + let read_inline_sourcemap = + |data_url: &str| -> Result, Error> { + let url = Url::parse(data_url).with_context(|| { + format!("failed to parse inline source map url\n{data_url}") + })?; + + let idx = match url.path().find("base64,") { + Some(v) => v, + None => { + bail!("failed to parse inline source map: not base64: {url:?}") + } + }; + + let content = url.path()[idx + "base64,".len()..].trim(); + + let res = BASE64_STANDARD + .decode(content.as_bytes()) + .context("failed to decode base64-encoded source map")?; + + Ok(Some(sourcemap::SourceMap::from_slice(&res).context( + "failed to read input source map from inlined base64 encoded string", + )?)) + }; + + let read_file_sourcemap = + |data_url: Option<&str>| -> Result, Error> { + match &**name { + FileName::Real(filename) => { + let dir = match filename.parent() { + Some(v) => v, + None => { + bail!("unexpected: root directory is given as a input file") + } + }; + + let map_path = match data_url { + Some(data_url) => { + let mut map_path = dir.join(data_url); + if !map_path.exists() { + // Old behavior. This check would prevent regressions. + // Perhaps it shouldn't be supported. Sometimes developers + // don't want to expose their source code. Map files are for + // internal troubleshooting convenience. + let fallback_map_path = + PathBuf::from(format!("{}.map", filename.display())); + if fallback_map_path.exists() { + map_path = fallback_map_path; + } else { + bail!( + "failed to find input source map file {:?} in {:?} \ + file as either {:?} or with appended .map", + data_url, + filename.display(), + map_path.display(), + ) + } + } + + Some(map_path) + } + None => { + // Old behavior. + let map_path = + PathBuf::from(format!("{}.map", filename.display())); + if map_path.exists() { + Some(map_path) + } else { + None + } + } + }; + + match map_path { + Some(map_path) => { + let path = map_path.display().to_string(); + let file = File::open(&path); + + // If file is not found, we should return None. Some libraries + // generate source maps but omit them from the npm package. + // + // See + // https://github.com/swc-project/swc/issues/8789#issuecomment-2105055772 + if file + .as_ref() + .is_err_and(|err| err.kind() == ErrorKind::NotFound) + { + #[cfg(debug_assertions)] + warn!(target: "swc", + "source map is specified by sourceMappingURL but \ + there's no source map at `{}`", + path + ); + return Ok(None); + } + + // Old behavior. + let file = if !is_default { + file? + } else { + match file { + Ok(v) => v, + Err(_) => return Ok(None), + } + }; + + Ok(Some(sourcemap::SourceMap::from_reader(file).with_context( + || { + format!( + "failed to read input source map + from file at {path}" + ) + }, + )?)) + } + None => Ok(None), + } + } + _ => Ok(None), + } + }; + + let read_sourcemap = || -> Option { + let s = "sourceMappingURL="; + + let text = comments.iter().rev().find_map(|c| { + let idx = c.text.rfind(s)?; + let (_, url) = c.text.split_at(idx + s.len()); + + Some(url.trim()) + }); + + // Load original source map if possible + let result = match text { + Some(text) if text.starts_with("data:") => read_inline_sourcemap(text), + _ => read_file_sourcemap(text), + }; + match result { + Ok(r) => r, + Err(err) => { + #[cfg(debug_assertions)] + tracing::error!(target: "swc", "failed to read input source map: {:?}", err); + #[cfg(not(debug_assertions))] + let _ = err; + None + } + } + }; + + // Load original source map + match input_src_map { + InputSourceMap::Bool(false) => Ok(None), + InputSourceMap::Bool(true) => Ok(read_sourcemap()), + InputSourceMap::Str(ref s) => { + if s == "inline" { + Ok(read_sourcemap()) + } else { + // Load source map passed by user + Ok(Some( + swc_sourcemap::SourceMap::from_slice(s.as_bytes()).context( + "failed to read input source map from user-provided sourcemap", + )?, + )) + } + } + } + }) + } +} diff --git a/crates/swc/src/legacy.rs b/crates/swc/src/legacy.rs new file mode 100644 index 000000000000..f68f4bf55cf6 --- /dev/null +++ b/crates/swc/src/legacy.rs @@ -0,0 +1,319 @@ +//! Compatibility entry points for legacy compiler APIs. +//! +//! [`Compiler::process_js`] preserves its signature while delegating to the +//! direct pipeline. [`Compiler::parse_js_as_input`] and +//! [`Compiler::process_js_with_custom_pass`] remain backed by the frozen +//! pass-building pipeline for embedders that require a delayed pass graph or +//! the legacy custom-pass ABI. + +use std::sync::Arc; + +use anyhow::{bail, Error}; +use swc_common::{ + comments::{Comments, SingleThreadedComments}, + errors::Handler, + sync::Lrc, + FileName, SourceFile, Spanned, +}; +use swc_compiler_base::{PrintArgs, TransformOutput}; +use swc_config::types::BoolOr; +use swc_ecma_ast::{Pass, Program}; +use swc_ecma_parser::Syntax; +use swc_ecma_visit::VisitWith; +#[cfg(feature = "isolated-dts")] +use swc_typescript::fast_dts::FastDts; + +use crate::{ + config::{BuiltInput, Options, OutputCharset}, + flow::downgrade_script_like_module, + sourcemap, CompileInput, Compiler, +}; + +impl Compiler { + /// Builds a legacy delayed-pass input, or returns [`None`] if the file is + /// skipped. A pre-parsed program bypasses Flow script/module + /// classification. + /// + /// `before_pass` observes the program after optional React Compiler and the + /// initial resolver. It is skipped when built-ins are disabled for testing. + #[cfg_attr(debug_assertions, tracing::instrument(skip_all))] + pub fn parse_js_as_input<'a, P>( + &'a self, + fm: Lrc, + program: Option, + handler: &'a Handler, + opts: &Options, + name: &FileName, + comments: Option<&'a SingleThreadedComments>, + before_pass: impl 'a + FnOnce(&Program) -> P, + ) -> Result>, Error> + where + P: 'a + Pass, + { + self.run(move || { + if let FileName::Real(ref path) = name { + if !opts.config.matches(path)? { + return Ok(None); + } + } + + let config = self.read_config(opts, name)?; + let config = match config { + Some(v) => v, + None => return Ok(None), + }; + + let built = opts.build_as_input( + &self.cm, + name, + move |syntax, target, is_module| match program { + Some(v) => Ok((v, false)), + _ => self.parse_js_as_transform_input( + fm.clone(), + handler, + target, + syntax, + is_module, + comments.as_ref().map(|v| v as _), + ), + }, + opts.output_path.as_deref(), + opts.source_root.clone(), + opts.source_file_name.clone(), + config.source_map_ignore_list.clone(), + handler, + Some(config), + comments, + before_pass, + )?; + Ok(Some(built)) + }) + } + + /// Compiles with the legacy custom-pass ABI. + /// + /// # Factory and pass timing + /// + /// Both factories observe `BuiltInput::program` before delayed passes run. + /// + /// The custom-before pass runs after type stripping and the active plugin + /// checkpoint, before React and compatibility transforms. The custom-after + /// pass runs after SWC transforms and custom-before, but before the Flow + /// script downgrade and final comment policy. + /// + /// With built-ins disabled, custom-before is skipped and custom-after still + /// runs. A pre-parsed program bypasses Flow script/module classification. + #[cfg_attr(debug_assertions, tracing::instrument(skip_all))] + pub fn process_js_with_custom_pass( + &self, + fm: Arc, + program: Option, + handler: &Handler, + opts: &Options, + comments: SingleThreadedComments, + custom_before_pass: impl FnOnce(&Program) -> P1, + custom_after_pass: impl FnOnce(&Program) -> P2, + ) -> Result + where + P1: Pass, + P2: Pass, + { + self.run(|| -> Result<_, Error> { + let config = self.run(|| { + self.parse_js_as_input( + fm.clone(), + program, + handler, + opts, + &fm.name, + Some(&comments), + |program| custom_before_pass(program), + ) + })?; + let config = match config { + Some(v) => v, + None => { + bail!("cannot process file because it's ignored by .swcrc") + } + }; + + let after_pass = custom_after_pass(&config.program); + + let config = config.with_pass(|pass| (pass, after_pass)); + + let orig = if config.source_maps.enabled() { + self.get_orig_src_map( + &fm, + &config.input_source_map, + config + .comments + .get_trailing(config.program.span_hi()) + .as_deref() + .unwrap_or_default(), + false, + )? + } else { + None + }; + + self.apply_transforms(handler, comments.clone(), fm.clone(), orig, config) + }) + } + + /// Compiles an already parsed program through the direct pipeline while + /// preserving the legacy method signature. + /// + /// New code should use [`Compiler::compile`] with + /// [`crate::CompileInput::program`] and select an AST or codegen terminal. + #[cfg_attr(debug_assertions, tracing::instrument(skip_all))] + pub fn process_js( + &self, + handler: &Handler, + program: Program, + opts: &Options, + ) -> Result { + let loc = self.cm.lookup_char_pos(program.span().lo()); + let fm = loc.file; + + self.compile(handler, CompileInput::program(fm, program), opts) + .codegen() + } + + #[cfg_attr( + debug_assertions, + tracing::instrument(name = "swc::Compiler::apply_transforms", skip_all) + )] + fn apply_transforms( + &self, + handler: &Handler, + #[allow(unused)] comments: SingleThreadedComments, + #[allow(unused)] fm: Arc, + orig: Option, + config: BuiltInput, + ) -> Result { + self.run(|| { + let program = config.program; + let is_typescript_syntax = matches!(config.syntax, Syntax::Typescript(..)); + + if config.emit_isolated_dts && !is_typescript_syntax { + handler.warn( + "jsc.experimental.emitIsolatedDts is enabled but the syntax is not TypeScript", + ); + } + + let source_map_names = if config.source_maps.enabled() { + let mut v = swc_compiler_base::IdentCollector { + names: Default::default(), + }; + + program.visit_with(&mut v); + + v.names + } else { + Default::default() + }; + #[cfg(feature = "isolated-dts")] + let dts_code = if is_typescript_syntax && config.emit_isolated_dts { + use std::cell::RefCell; + + use swc_ecma_codegen::to_code_with_comments; + let (leading, trailing) = comments.borrow_all(); + + let leading = std::rc::Rc::new(RefCell::new(leading.clone())); + let trailing = std::rc::Rc::new(RefCell::new(trailing.clone())); + + let comments = SingleThreadedComments::from_leading_and_trailing(leading, trailing); + + let mut checker = + FastDts::new(fm.name.clone(), config.unresolved_mark, Default::default()); + let mut program = program.clone(); + + #[cfg(feature = "module")] + if let Some((base, resolver)) = config.resolver { + use swc_ecma_transforms_module::rewriter::import_rewriter; + + program.mutate(import_rewriter(base, resolver)); + } + + let issues = checker.transform(&mut program); + + for issue in issues { + handler + .struct_span_err(issue.range.span, &issue.message) + .emit(); + } + + let dts_code = to_code_with_comments(Some(&comments), &program); + Some(dts_code) + } else { + None + }; + + let pass = config.pass; + let (program, output) = swc_transform_common::output::capture(|| { + #[cfg(feature = "isolated-dts")] + { + if let Some(dts_code) = dts_code { + use swc_transform_common::output::experimental_emit; + experimental_emit("__swc_isolated_declarations__".into(), dts_code); + } + } + + self.run_transform(handler, config.external_helpers, || program.apply(pass)) + }); + + let program = if config.flow_strip_script_like_module { + downgrade_script_like_module(program)? + } else { + program + }; + + if let Some(comments) = &config.comments { + swc_compiler_base::minify_file_comments( + comments, + config.preserve_comments, + BoolOr::Bool(false), + config.output.preserve_annotations.into_bool(), + ); + } + + self.print( + &program, + PrintArgs { + source_root: config.source_root.as_deref(), + source_file_name: config.source_file_name.as_deref(), + source_map_ignore_list: config.source_map_ignore_list.clone(), + output_path: config.output_path, + inline_sources_content: config.inline_sources_content, + source_map: config.source_maps, + source_map_names: &source_map_names, + orig, + comments: config.comments.as_ref().map(|v| v as _), + emit_source_map_columns: config.emit_source_map_columns, + emit_source_map_scopes: config.emit_source_map_scopes, + preamble: &config.output.preamble, + codegen_config: swc_ecma_codegen::Config::default() + .with_target(config.target) + .with_minify(config.minify) + .with_ascii_only( + config + .output + .charset + .map(|v| matches!(v, OutputCharset::Ascii)) + .unwrap_or(false), + ) + .with_emit_assert_for_import_attributes( + config.emit_assert_for_import_attributes, + ) + .with_inline_script(config.codegen_inline_script), + output: if output.is_empty() { + None + } else { + Some(output) + }, + source_map_url: config.output.source_map_url.as_deref(), + }, + ) + }) + } +} diff --git a/crates/swc/src/lib.rs b/crates/swc/src/lib.rs index 90f1c3ff6dd2..ad686f3717de 100644 --- a/crates/swc/src/lib.rs +++ b/crates/swc/src/lib.rs @@ -9,11 +9,13 @@ //! //! ## Dependency version management //! -//! `swc` has [swc_css](https://docs.rs/swc_css), which re-exports required modules. +//! The [swc_css](https://docs.rs/swc_css) facade crate re-exports the modules +//! required to build CSS tooling. //! //! ## Testing //! -//! See [testing] and [swc_ecma_transforms_testing](https://docs.rs/swc_ecma_transforms_testing). +//! See [testing](https://docs.rs/testing) and +//! [swc_ecma_transforms_testing](https://docs.rs/swc_ecma_transforms_testing). //! //! ## Custom javascript transforms //! @@ -42,17 +44,18 @@ //! //! ### Variable management (Scoping) //! -//! See [swc_ecma_transforms_base::resolver::resolver_with_mark]. +//! See [swc_ecma_transforms::resolver]. //! //! #### How identifiers work //! //! See the doc on [swc_ecma_ast::Ident] or on -//! [swc_ecma_transforms_base::resolver::resolver_with_mark]. +//! [swc_ecma_transforms::resolver]. //! //! #### Comparing two identifiers //! -//! See [swc_ecma_utils::Id]. You can use [swc_ecma_utils::IdentLike::to_id] to -//! extract important parts of an [swc_ecma_ast::Ident]. +//! See [swc_ecma_ast::Id]. You can use +//! [swc_ecma_utils::ident::IdentLike::to_id] to extract important parts of an +//! [swc_ecma_ast::Ident]. //! //! #### Creating a unique identifier //! @@ -61,7 +64,7 @@ //! #### Prepending statements //! //! If you want to prepend statements to the beginning of a file, you can use -//! [swc_ecma_utils::prepend_stmts] or [swc_ecma_utils::prepend] if `len == 1`. +//! [swc_ecma_utils::prepend_stmts] or [swc_ecma_utils::prepend_stmt]. //! //! These methods are aware of the fact that `"use strict"` directive should be //! first in a file, and insert statements after directives. @@ -85,10 +88,6 @@ //! are static (e.g. `Object.prototype.hasOwnProperty`), you can use //! [swc_ecma_utils::member_expr]. //! -//! - If you want to create [swc_ecma_ast::MemberExpr], you can use -//! [swc_ecma_utils::ExprFactory::as_obj] to create object field. -//! -//! //! ### Reducing binary size //! //! The visitor expands to a lot of code. You can reduce it by using macros like @@ -97,9 +96,8 @@ //! - [noop_visit_mut_type](swc_ecma_visit::noop_visit_mut_type) //! - [noop_visit_type](swc_ecma_visit::noop_visit_type) //! -//! Note that this will make typescript-related nodes not processed, but it's -//! typically fine as `typescript::strip` is invoked at the start and it removes -//! typescript-specific nodes. +//! These macros skip type-specific nodes. Use them only at a boundary where the +//! program is known not to contain such nodes. //! //! ### Porting `expr.evaluate()` of babel //! @@ -116,308 +114,71 @@ extern crate swc_common as common; #[cfg_attr(docsrs, doc(cfg(feature = "react-compiler")))] pub extern crate swc_ecma_react_compiler as react_compiler; -use std::{ - fs::{read_to_string, File}, - io::ErrorKind, - path::{Path, PathBuf}, - sync::Arc, -}; +use std::sync::Arc; -use anyhow::{bail, Context, Error}; -use base64::prelude::{Engine, BASE64_STANDARD}; -use common::{ - comments::{Comment, SingleThreadedComments}, - errors::HANDLER, -}; -use jsonc_parser::{parse_to_serde_value, ParseOptions}; -use once_cell::sync::Lazy; -use serde_json::error::Category; +use anyhow::Error; use swc_common::{ - comments::Comments, errors::Handler, sync::Lrc, FileName, Mark, SourceFile, SourceMap, Span, - Spanned, GLOBALS, + comments::Comments, + errors::{Handler, HANDLER}, + SourceFile, SourceMap, GLOBALS, }; pub use swc_compiler_base::{PrintArgs, TransformOutput}; pub use swc_config::types::{BoolConfig, BoolOr, BoolOrDataConfig}; -use swc_ecma_ast::{ - noop_pass, Decl, DefaultDecl, EsVersion, Module, ModuleDecl, ModuleItem, Pass, Program, Script, - TsNamespaceBody, -}; +use swc_ecma_ast::{EsVersion, Program}; use swc_ecma_codegen::Node; -#[cfg(feature = "module")] -use swc_ecma_loader::resolvers::{ - lru::CachingResolver, node::NodeModulesResolver, tsc::TsConfigResolver, -}; -use swc_ecma_minifier::option::{MangleCache, MinifyOptions, TopLevelOptions}; -use swc_ecma_parser::{ - error::SyntaxError, parse_file_as_program, parse_file_as_script, EsSyntax, Syntax, -}; -use swc_ecma_transforms::{ - fixer, - helpers::{self, Helpers}, - hygiene, resolver, -}; -use swc_ecma_transforms_base::fixer::paren_remover; -#[cfg(feature = "module")] -use swc_ecma_transforms_module::path::NodeImportResolver; -use swc_ecma_visit::{FoldWith, VisitMutWith, VisitWith}; +use swc_ecma_parser::Syntax; +use swc_ecma_transforms::helpers::{self, HelperData, Helpers}; +use swc_ecma_visit::{FoldWith, VisitWith}; pub use swc_error_reporters::handler::{try_with_handler, HandlerOpts}; pub use swc_node_comments::SwcComments; pub use swc_sourcemap as sourcemap; -#[cfg(feature = "isolated-dts")] -use swc_typescript::fast_dts::FastDts; -#[cfg(debug_assertions)] -use tracing::warn; -use url::Url; - -use crate::config::{ - BuiltInput, Config, ConfigFile, InputSourceMap, IsModule, JsMinifyCommentOption, - JsMinifyOptions, Options, OutputCharset, Rc, RootMode, SourceMapsConfig, -}; -mod builder; +mod codegen; pub mod config; mod dropped_comments_preserver; +mod flow; +mod input_source_map; +mod legacy; +mod minify; +mod pipeline; mod plugin; -pub mod wasm_analysis; -pub mod resolver { - use std::path::PathBuf; - - use rustc_hash::FxHashMap; - use swc_ecma_loader::{ - resolvers::{lru::CachingResolver, node::NodeModulesResolver, tsc::TsConfigResolver}, - TargetEnv, - }; - - use crate::config::CompiledPaths; - - pub type NodeResolver = CachingResolver; - - pub fn paths_resolver( - target_env: TargetEnv, - alias: FxHashMap, - base_url: PathBuf, - paths: CompiledPaths, - preserve_symlinks: bool, - ) -> CachingResolver> { - let r = TsConfigResolver::new( - NodeModulesResolver::without_node_modules(target_env, alias, preserve_symlinks), - base_url, - paths, - ); - CachingResolver::new(40, r) - } - - pub fn environment_resolver( - target_env: TargetEnv, - alias: FxHashMap, - preserve_symlinks: bool, - ) -> NodeResolver { - CachingResolver::new( - 40, - NodeModulesResolver::new(target_env, alias, preserve_symlinks), - ) - } -} - -#[cfg(feature = "module")] -type SwcImportResolver = Arc< - NodeImportResolver>>>, ->; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum FlowScriptLikeModuleKind { - Script, - TypeOnlyModule, - RuntimeModule(Span), -} - -fn emit_parser_recoverable_errors( - handler: &Handler, - errors: Vec, -) -> Result<(), Error> { - if errors.is_empty() { - return Ok(()); - } - - for error in errors { - error.into_diagnostic(handler).emit(); - } - - Err(Error::msg("Syntax Error")) -} - -fn classify_flow_script_like_module(program: &Program) -> FlowScriptLikeModuleKind { - let Program::Module(module) = program else { - return FlowScriptLikeModuleKind::Script; - }; - - classify_flow_script_like_module_body(module) -} - -fn classify_flow_script_like_module_body(module: &Module) -> FlowScriptLikeModuleKind { - let mut saw_module_decl = false; - - for module_item in &module.body { - let Some(module_decl) = module_item.as_module_decl() else { - continue; - }; - - saw_module_decl = true; - - if is_runtime_module_decl(module_decl) { - return FlowScriptLikeModuleKind::RuntimeModule(module_item.span()); - } - } +pub mod resolver; - if saw_module_decl { - FlowScriptLikeModuleKind::TypeOnlyModule - } else { - let span = module - .body - .first() - .map(Spanned::span) - .unwrap_or(module.span); - FlowScriptLikeModuleKind::RuntimeModule(span) - } -} - -fn downgrade_flow_script_like_module(program: Program) -> Result { - let Program::Module(module) = program else { - return Ok(program); - }; - - if module - .body - .iter() - .any(|module_item| matches!(module_item, ModuleItem::ModuleDecl(..))) - { - bail!( - "failed to downgrade Flow type-only module to script because module declarations \ - remain after stripping" - ); - } - - let Module { - span, - body, - shebang, - } = module; - - let body = body - .into_iter() - .map(|module_item| match module_item { - ModuleItem::Stmt(stmt) => Ok(stmt), - ModuleItem::ModuleDecl(..) => bail!( - "failed to downgrade Flow type-only module to script because module declarations \ - remain after stripping" - ), - }) - .collect::>()?; - - Ok(Program::Script(Script { - span, - body, - shebang, - })) -} - -fn is_runtime_module_decl(module_decl: &ModuleDecl) -> bool { - match module_decl { - ModuleDecl::Import(import_decl) => !import_decl.type_only, - ModuleDecl::ExportDecl(export_decl) => is_runtime_decl(&export_decl.decl), - ModuleDecl::ExportNamed(named_export) => !named_export.type_only, - ModuleDecl::ExportDefaultDecl(export_default_decl) => { - is_runtime_default_decl(&export_default_decl.decl) - } - ModuleDecl::ExportDefaultExpr(..) => true, - ModuleDecl::ExportAll(export_all) => !export_all.type_only, - ModuleDecl::TsImportEquals(ts_import_equals_decl) => !ts_import_equals_decl.is_type_only, - ModuleDecl::TsExportAssignment(..) => true, - ModuleDecl::TsNamespaceExport(..) => false, - } -} - -fn is_runtime_decl(decl: &Decl) -> bool { - if is_declare_decl(decl) { - return false; - } - - match decl { - Decl::TsInterface(..) | Decl::TsTypeAlias(..) => false, - Decl::Fn(function_decl) => function_decl.function.body.is_some(), - Decl::Class(..) | Decl::Var(..) | Decl::Using(..) | Decl::TsEnum(..) => true, - Decl::TsModule(ts_module_decl) => ts_module_decl - .body - .as_ref() - .map(is_runtime_namespace_body) - .unwrap_or_default(), - } -} - -fn is_runtime_default_decl(default_decl: &DefaultDecl) -> bool { - match default_decl { - DefaultDecl::Class(..) => true, - DefaultDecl::Fn(function_expr) => function_expr.function.body.is_some(), - DefaultDecl::TsInterfaceDecl(..) => false, - } -} - -fn is_runtime_namespace_body(namespace_body: &TsNamespaceBody) -> bool { - match namespace_body { - TsNamespaceBody::TsModuleBlock(ts_module_block) => { - ts_module_block - .body - .iter() - .any(|module_item| match module_item { - ModuleItem::Stmt(stmt) => is_runtime_stmt(stmt), - ModuleItem::ModuleDecl(module_decl) => is_runtime_module_decl(module_decl), - }) - } - TsNamespaceBody::TsNamespaceDecl(ts_namespace_decl) => { - is_runtime_namespace_body(&ts_namespace_decl.body) - } - } -} - -fn is_runtime_stmt(stmt: &swc_ecma_ast::Stmt) -> bool { - match stmt { - swc_ecma_ast::Stmt::Empty(..) => false, - swc_ecma_ast::Stmt::Decl(decl) => is_runtime_decl(decl), - _ => true, - } -} - -fn is_declare_decl(decl: &Decl) -> bool { - match decl { - Decl::Class(class_decl) => class_decl.declare, - Decl::Fn(function_decl) => function_decl.declare, - Decl::Var(var_decl) => var_decl.declare, - Decl::Using(..) => false, - Decl::TsInterface(..) | Decl::TsTypeAlias(..) => true, - Decl::TsEnum(ts_enum_decl) => ts_enum_decl.declare, - Decl::TsModule(ts_module_decl) => ts_module_decl.declare || ts_module_decl.global, - } -} +pub use minify::JsMinifyExtras; +pub use pipeline::{ + CompileInput, CompileRequest, PipelineContext, PipelineHooks, TransformedProgram, +}; +pub mod wasm_analysis; -/// All methods accept [Handler], which is a storage for errors. +/// A compiler backed by a shared source map. /// -/// The caller should check if the handler contains any errors after calling -/// method. +/// Compilation and parsing methods that emit diagnostics accept a +/// [`swc_common::errors::Handler`]. The caller should inspect that handler +/// after the operation completes. Transform operations also require the caller +/// to install [`swc_common::GLOBALS`]. pub struct Compiler { - /// CodeMap + /// Source map used by parsing, transforms, diagnostics, and code + /// generation. pub cm: Arc, comments: SwcComments, } -/// These are **low-level** apis. impl Compiler { + pub fn new(cm: Arc) -> Self { + Compiler { + cm, + comments: Default::default(), + } + } + pub fn comments(&self) -> &SwcComments { &self.comments } - /// Runs `op` in current compiler's context. + /// Runs `op`, checking in debug builds that the caller installed + /// [`swc_common::GLOBALS`]. /// - /// Note: Other methods of `Compiler` already uses this internally. + /// This method does not install the globals. pub fn run(&self, op: F) -> R where F: FnOnce() -> R, @@ -430,483 +191,49 @@ impl Compiler { op() } - fn get_orig_src_map( - &self, - fm: &SourceFile, - input_src_map: &InputSourceMap, - comments: &[Comment], - is_default: bool, - ) -> Result, Error> { - self.run(|| -> Result<_, Error> { - let name = &fm.name; - - let read_inline_sourcemap = - |data_url: &str| -> Result, Error> { - let url = Url::parse(data_url).with_context(|| { - format!("failed to parse inline source map url\n{data_url}") - })?; - - let idx = match url.path().find("base64,") { - Some(v) => v, - None => { - bail!("failed to parse inline source map: not base64: {url:?}") - } - }; - - let content = url.path()[idx + "base64,".len()..].trim(); - - let res = BASE64_STANDARD - .decode(content.as_bytes()) - .context("failed to decode base64-encoded source map")?; - - Ok(Some(sourcemap::SourceMap::from_slice(&res).context( - "failed to read input source map from inlined base64 encoded string", - )?)) - }; - - let read_file_sourcemap = - |data_url: Option<&str>| -> Result, Error> { - match &**name { - FileName::Real(filename) => { - let dir = match filename.parent() { - Some(v) => v, - None => { - bail!("unexpected: root directory is given as a input file") - } - }; - - let map_path = match data_url { - Some(data_url) => { - let mut map_path = dir.join(data_url); - if !map_path.exists() { - // Old behavior. This check would prevent - // regressions. - // Perhaps it shouldn't be supported. Sometimes - // developers don't want to expose their source - // code. - // Map files are for internal troubleshooting - // convenience. - let fallback_map_path = - PathBuf::from(format!("{}.map", filename.display())); - if fallback_map_path.exists() { - map_path = fallback_map_path; - } else { - bail!( - "failed to find input source map file {:?} in \ - {:?} file as either {:?} or with appended .map", - data_url, - filename.display(), - map_path.display(), - ) - } - } - - Some(map_path) - } - None => { - // Old behavior. - let map_path = - PathBuf::from(format!("{}.map", filename.display())); - if map_path.exists() { - Some(map_path) - } else { - None - } - } - }; - - match map_path { - Some(map_path) => { - let path = map_path.display().to_string(); - let file = File::open(&path); - - // If file is not found, we should return None. - // Some libraries generates source map but omit them from the - // npm package. - // - // See https://github.com/swc-project/swc/issues/8789#issuecomment-2105055772 - if file - .as_ref() - .is_err_and(|err| err.kind() == ErrorKind::NotFound) - { - #[cfg(debug_assertions)] - warn!( - "source map is specified by sourceMappingURL but \ - there's no source map at `{}`", - path - ); - return Ok(None); - } - - // Old behavior. - let file = if !is_default { - file? - } else { - match file { - Ok(v) => v, - Err(_) => return Ok(None), - } - }; - - Ok(Some(sourcemap::SourceMap::from_reader(file).with_context( - || { - format!( - "failed to read input source map - from file at {path}" - ) - }, - )?)) - } - None => Ok(None), - } - } - _ => Ok(None), - } - }; - - let read_sourcemap = || -> Option { - let s = "sourceMappingURL="; - - let text = comments.iter().rev().find_map(|c| { - let idx = c.text.rfind(s)?; - let (_, url) = c.text.split_at(idx + s.len()); - - Some(url.trim()) - }); - - // Load original source map if possible - let result = match text { - Some(text) if text.starts_with("data:") => read_inline_sourcemap(text), - _ => read_file_sourcemap(text), - }; - match result { - Ok(r) => r, - Err(err) => { - #[cfg(debug_assertions)] - tracing::error!("failed to read input source map: {:?}", err); - #[cfg(not(debug_assertions))] - let _ = err; - None - } - } - }; - - // Load original source map - match input_src_map { - InputSourceMap::Bool(false) => Ok(None), - InputSourceMap::Bool(true) => Ok(read_sourcemap()), - InputSourceMap::Str(ref s) => { - if s == "inline" { - Ok(read_sourcemap()) - } else { - // Load source map passed by user - Ok(Some( - swc_sourcemap::SourceMap::from_slice(s.as_bytes()).context( - "failed to read input source map from user-provided sourcemap", - )?, - )) - } - } - } - }) - } - - /// This method parses a javascript / typescript file - pub fn parse_js( - &self, - fm: Arc, - handler: &Handler, - target: EsVersion, - syntax: Syntax, - is_module: IsModule, - comments: Option<&dyn Comments>, - ) -> Result { - swc_compiler_base::parse_js( - self.cm.clone(), - fm, - handler, - target, - syntax, - is_module, - comments, - ) - } - - fn parse_js_as_transform_input( - &self, - fm: Arc, - handler: &Handler, - target: EsVersion, - syntax: Syntax, - is_module: IsModule, - comments: Option<&dyn Comments>, - ) -> Result<(Program, bool), Error> { - if !syntax.flow() { - return self - .parse_js(fm, handler, target, syntax, is_module, comments) - .map(|program| (program, false)); - } - - if matches!(is_module, IsModule::Bool(false)) { - let mut errors = Vec::new(); - match parse_file_as_script(&fm, syntax, target, comments, &mut errors) { - Ok(script) => { - emit_parser_recoverable_errors(handler, errors)?; - return Ok((Program::Script(script), false)); - } - Err(err) if matches!(err.kind(), SyntaxError::ImportExportInScript) => {} - Err(err) => { - emit_parser_recoverable_errors(handler, errors)?; - err.into_diagnostic(handler).emit(); - return Err(Error::msg("Syntax Error")); - } - } - - let mut errors = Vec::new(); - let program = parse_file_as_program(&fm, syntax, target, comments, &mut errors) - .map_err(|err| { - err.into_diagnostic(handler).emit(); - Error::msg("Syntax Error") - })?; - - emit_parser_recoverable_errors(handler, errors)?; - - match classify_flow_script_like_module(&program) { - FlowScriptLikeModuleKind::Script => Ok((program, false)), - FlowScriptLikeModuleKind::TypeOnlyModule => Ok((program, true)), - FlowScriptLikeModuleKind::RuntimeModule(span) => { - handler - .struct_span_err(span, &SyntaxError::ImportExportInScript.msg()) - .emit(); - Err(Error::msg("Syntax Error")) - } - } - } else { - let program = self.parse_js(fm, handler, target, syntax, is_module, comments)?; - let flow_strip_script_like_module = matches!( - classify_flow_script_like_module(&program), - FlowScriptLikeModuleKind::TypeOnlyModule - ) && matches!(is_module, IsModule::Unknown); - - Ok((program, flow_strip_script_like_module)) - } - } - - /// Converts ast node to source string and sourcemap. - /// + /// Runs an AST transform with the helper and diagnostic scopes installed. /// - /// This method receives target file path, but does not write file to the - /// path. See: https://github.com/swc-project/swc/issues/1255 - #[allow(clippy::too_many_arguments)] - pub fn print(&self, node: &T, args: PrintArgs) -> Result + /// The caller must already have installed [`swc_common::GLOBALS`]. + pub fn run_transform(&self, handler: &Handler, external_helpers: bool, op: F) -> Ret where - T: Node + VisitWith, + F: FnOnce() -> Ret, { - swc_compiler_base::print(self.cm.clone(), node, args) + self.run_transform_scope(handler, external_helpers, op, |result, _| result) } -} - -/// High-level apis. -impl Compiler { - pub fn new(cm: Arc) -> Self { - Compiler { - cm, - comments: Default::default(), - } - } - - #[cfg_attr(debug_assertions, tracing::instrument(skip_all))] - pub fn read_config(&self, opts: &Options, name: &FileName) -> Result, Error> { - static CUR_DIR: Lazy = Lazy::new(|| { - if cfg!(target_arch = "wasm32") { - PathBuf::new() - } else { - ::std::env::current_dir().unwrap() - } - }); - self.run(|| -> Result<_, Error> { - let Options { - ref root, - root_mode, - swcrc, - config_file, - .. - } = opts; - - let root = root.as_ref().unwrap_or(&CUR_DIR); - - let swcrc_path = match config_file { - Some(ConfigFile::Str(s)) => Some(PathBuf::from(s.clone())), - _ => { - if *swcrc { - if let FileName::Real(ref path) = name { - // Canonicalize relative paths for proper parent traversal - let abs_path = if path.is_relative() { - root.join(path).canonicalize().ok() - } else { - path.canonicalize().ok() - }; - let found = abs_path.and_then(|p| find_swcrc(&p, root, *root_mode)); - - // "upward" mode requires a .swcrc to be found - if found.is_none() && *root_mode == RootMode::Upward { - bail!( - "Could not find .swcrc file while using rootMode \ - \"upward\".\nSearched from: {}", - path.display() - ); - } - - found - } else { - None - } - } else { - None - } - } - }; - - let config_file = match swcrc_path.as_deref() { - Some(s) => Some(load_swcrc(s)?), - _ => None, - }; - let filename_path = match name { - FileName::Real(p) => Some(&**p), - _ => None, - }; - - if let Some(filename_path) = filename_path { - if let Some(config) = config_file { - let dir = swcrc_path - .as_deref() - .and_then(|p| p.parent()) - .expect(".swcrc path should have parent dir"); - - let mut config = config - .into_config(Some(filename_path)) - .context("failed to process config file")?; - - if let Some(c) = &mut config { - if c.jsc.base_url != PathBuf::new() { - let joined = dir.join(&c.jsc.base_url); - c.jsc.base_url = if cfg!(target_os = "windows") - && c.jsc.base_url.as_os_str() == "." - { - dir.canonicalize().with_context(|| { - format!( - "failed to canonicalize base url using the path of \ - .swcrc\nDir: {}\n(Used logic for windows)", - dir.display(), - ) - })? - } else { - joined.canonicalize().with_context(|| { - format!( - "failed to canonicalize base url using the path of \ - .swcrc\nPath: {}\nDir: {}\nbaseUrl: {}", - joined.display(), - dir.display(), - c.jsc.base_url.display() - ) - })? - }; - } - } - - return Ok(config); - } - - let config_file = config_file.unwrap_or_default(); - let config = config_file.into_config(Some(filename_path))?; - - return Ok(config); - } - - let config = match config_file { - Some(config_file) => config_file.into_config(None)?, - None => Rc::default().into_config(None)?, - }; - - match config { - Some(config) => Ok(Some(config)), - None => { - bail!("no config matched for file ({name})") - } - } - }) - .with_context(|| format!("failed to read .swcrc file for input file at `{name}`")) - } - - /// This method returns [None] if a file should be skipped. - /// - /// This method handles merging of config. - /// - /// This method does **not** parse module. - #[cfg_attr(debug_assertions, tracing::instrument(skip_all))] - pub fn parse_js_as_input<'a, P>( - &'a self, - fm: Lrc, - program: Option, - handler: &'a Handler, - opts: &Options, - name: &FileName, - comments: Option<&'a SingleThreadedComments>, - before_pass: impl 'a + FnOnce(&Program) -> P, - ) -> Result>, Error> + /// Runs an AST transform and returns the helper requirements it recorded. + pub(crate) fn run_transform_with_helpers( + &self, + handler: &Handler, + external_helpers: bool, + op: F, + ) -> (Ret, HelperData) where - P: 'a + Pass, + F: FnOnce() -> Ret, { - self.run(move || { - if let FileName::Real(ref path) = name { - if !opts.config.matches(path)? { - return Ok(None); - } - } - - let config = self.read_config(opts, name)?; - let config = match config { - Some(v) => v, - None => return Ok(None), - }; - - let built = opts.build_as_input( - &self.cm, - name, - move |syntax, target, is_module| match program { - Some(v) => Ok((v, false)), - _ => self.parse_js_as_transform_input( - fm.clone(), - handler, - target, - syntax, - is_module, - comments.as_ref().map(|v| v as _), - ), - }, - opts.output_path.as_deref(), - opts.source_root.clone(), - opts.source_file_name.clone(), - config.source_map_ignore_list.clone(), - handler, - Some(config), - comments, - before_pass, - )?; - Ok(Some(built)) + self.run_transform_scope(handler, external_helpers, op, |result, helpers| { + (result, helpers.data()) }) } - pub fn run_transform(&self, handler: &Handler, external_helpers: bool, op: F) -> Ret + fn run_transform_scope( + &self, + handler: &Handler, + external_helpers: bool, + op: F, + finish: Finish, + ) -> Output where F: FnOnce() -> Ret, + Finish: FnOnce(Ret, &Helpers) -> Output, { self.run(|| { - helpers::HELPERS.set(&Helpers::new(external_helpers), || HANDLER.set(handler, op)) + let helpers = Helpers::new(external_helpers); + helpers::HELPERS.set(&helpers, || HANDLER.set(handler, || finish(op(), &helpers))) }) } + /// Applies a fold with the helper and diagnostic scopes installed. #[cfg_attr(debug_assertions, tracing::instrument(skip_all))] pub fn transform( &self, @@ -921,531 +248,61 @@ impl Compiler { }) } - /// `custom_after_pass` is applied after swc transforms are applied. - /// - /// `program`: If you already parsed `Program`, you can pass it. - /// - /// # Guarantee - /// - /// `swc` invokes `custom_before_pass` after - /// - /// - Handling decorators, if configured - /// - Applying `resolver` - /// - Stripping typescript nodes - /// - /// This means, you can use `noop_visit_type`, `noop_fold_type` and - /// `noop_visit_mut_type` in your visitor to reduce the binary size. - #[cfg_attr(debug_assertions, tracing::instrument(skip_all))] - pub fn process_js_with_custom_pass( - &self, - fm: Arc, - program: Option, - handler: &Handler, - opts: &Options, - comments: SingleThreadedComments, - custom_before_pass: impl FnOnce(&Program) -> P1, - custom_after_pass: impl FnOnce(&Program) -> P2, - ) -> Result - where - P1: Pass, - P2: Pass, - { - self.run(|| -> Result<_, Error> { - let config = self.run(|| { - self.parse_js_as_input( - fm.clone(), - program, - handler, - opts, - &fm.name, - Some(&comments), - |program| custom_before_pass(program), - ) - })?; - let config = match config { - Some(v) => v, - None => { - bail!("cannot process file because it's ignored by .swcrc") - } - }; - - let after_pass = custom_after_pass(&config.program); - - let config = config.with_pass(|pass| (pass, after_pass)); - - let orig = if config.source_maps.enabled() { - self.get_orig_src_map( - &fm, - &config.input_source_map, - config - .comments - .get_trailing(config.program.span_hi()) - .as_deref() - .unwrap_or_default(), - false, - )? - } else { - None - }; - - self.apply_transforms(handler, comments.clone(), fm.clone(), orig, config) - }) - } - - #[cfg_attr(debug_assertions, tracing::instrument(skip(self, handler, opts)))] - pub fn process_js_file( + /// Parses a JavaScript, TypeScript, or, when enabled, Flow file. + pub fn parse_js( &self, fm: Arc, handler: &Handler, - opts: &Options, - ) -> Result { - self.process_js_with_custom_pass( + target: EsVersion, + syntax: Syntax, + is_module: config::IsModule, + comments: Option<&dyn Comments>, + ) -> Result { + swc_compiler_base::parse_js( + self.cm.clone(), fm, - None, handler, - opts, - SingleThreadedComments::default(), - |_| noop_pass(), - |_| noop_pass(), + target, + syntax, + is_module, + comments, ) } - #[cfg_attr(debug_assertions, tracing::instrument(skip_all))] - pub fn minify( - &self, - fm: Arc, - handler: &Handler, - opts: &JsMinifyOptions, - extras: JsMinifyExtras, - ) -> Result { - self.run(|| { - let target = opts.ecma.clone().into(); - - let (source_map, orig, source_map_url) = opts - .source_map - .as_ref() - .map(|obj| -> Result<_, Error> { - let orig = obj.content.as_ref().map(|s| s.to_sourcemap()).transpose()?; - - Ok((SourceMapsConfig::Bool(true), orig, obj.url.as_deref())) - }) - .unwrap_as_option(|v| { - Some(Ok(match v { - Some(true) => (SourceMapsConfig::Bool(true), None, None), - _ => (SourceMapsConfig::Bool(false), None, None), - })) - }) - .unwrap()?; - - let mut min_opts = MinifyOptions { - compress: opts - .compress - .clone() - .unwrap_as_option(|default| match default { - Some(true) | None => Some(Default::default()), - _ => None, - }) - .map(|v| v.into_config(self.cm.clone())), - mangle: opts - .mangle - .clone() - .unwrap_as_option(|default| match default { - Some(true) | None => Some(Default::default()), - _ => None, - }), - ..Default::default() - }; - - // top_level defaults to true if module is true - - // https://github.com/swc-project/swc/issues/2254 - - if opts.keep_fnames { - if let Some(opts) = &mut min_opts.compress { - opts.keep_fnames = true; - } - if let Some(opts) = &mut min_opts.mangle { - opts.keep_fn_names = true; - } - } - - let comments = SingleThreadedComments::default(); - - let mut program = self - .parse_js( - fm.clone(), - handler, - target, - Syntax::Es(EsSyntax { - jsx: true, - decorators: true, - decorators_before_export: true, - import_attributes: true, - ..Default::default() - }), - opts.module, - Some(&comments), - ) - .context("failed to parse input file")?; - - if opts.toplevel == Some(true) || program.is_module() { - if let Some(opts) = &mut min_opts.compress { - if opts.top_level.is_none() { - opts.top_level = Some(TopLevelOptions { functions: true }); - } - } - - if let Some(opts) = &mut min_opts.mangle { - if opts.top_level.is_none() { - opts.top_level = Some(true); - } - } - } - - let source_map_names = if source_map.enabled() { - let mut v = swc_compiler_base::IdentCollector { - names: Default::default(), - }; - - program.visit_with(&mut v); - - v.names - } else { - Default::default() - }; - - let unresolved_mark = Mark::new(); - let top_level_mark = Mark::new(); - - let is_mangler_enabled = min_opts.mangle.is_some(); - - program = self.run_transform(handler, false, || { - program.mutate(&mut paren_remover(Some(&comments))); - - program.mutate(&mut resolver(unresolved_mark, top_level_mark, false)); - - let mut program = swc_ecma_minifier::optimize( - program, - self.cm.clone(), - Some(&comments), - None, - &min_opts, - &swc_ecma_minifier::option::ExtraOptions { - unresolved_mark, - top_level_mark, - mangle_name_cache: extras.mangle_name_cache, - }, - ); - - if !is_mangler_enabled { - program.visit_mut_with(&mut hygiene()) - } - program.mutate(&mut fixer(Some(&comments as &dyn Comments))); - program - }); - - let preserve_comments = opts - .format - .comments - .clone() - .into_inner() - .unwrap_or(BoolOr::Data(JsMinifyCommentOption::PreserveSomeComments)); - let extracted_comments = swc_compiler_base::minify_file_comments( - &comments, - preserve_comments, - opts.extract_comments - .clone() - .into_inner() - .unwrap_or(BoolOr::Bool(false)), - opts.format.preserve_annotations, - ); - - let ret = self.print( - &program, - PrintArgs { - source_root: None, - source_file_name: Some(&fm.name.to_string()), - output_path: opts.output_path.clone().map(From::from), - inline_sources_content: opts.inline_sources_content, - source_map, - source_map_ignore_list: opts.source_map_ignore_list.clone(), - source_map_names: &source_map_names, - orig, - comments: Some(&comments), - emit_source_map_columns: opts.emit_source_map_columns, - emit_source_map_scopes: false, - preamble: &opts.format.preamble, - codegen_config: swc_ecma_codegen::Config::default() - .with_target(target) - .with_minify(true) - .with_ascii_only(opts.format.ascii_only) - .with_emit_assert_for_import_attributes( - opts.format.emit_assert_for_import_attributes, - ) - .with_inline_script(opts.format.inline_script) - .with_reduce_escaped_newline( - min_opts - .compress - .unwrap_or_default() - .experimental - .reduce_escaped_newline, - ), - output: None, - source_map_url, - }, - ); - - ret.map(|mut output| { - if !extracted_comments.is_empty() { - output.extracted_comments = Some(extracted_comments); - } - output.diagnostics = handler.take_diagnostics(); - - output - }) - }) - } - - /// You can use custom pass with this method. + /// Converts an AST node to source code and an optional source map. /// - /// Pass building logic has been inlined into the configuration system. - #[cfg_attr(debug_assertions, tracing::instrument(skip_all))] - pub fn process_js( - &self, - handler: &Handler, - program: Program, - opts: &Options, - ) -> Result { - let loc = self.cm.lookup_char_pos(program.span().lo()); - let fm = loc.file; - - self.process_js_with_custom_pass( - fm, - Some(program), - handler, - opts, - SingleThreadedComments::default(), - |_| noop_pass(), - |_| noop_pass(), - ) + /// This method receives the target file path but does not write to it. See + /// . + #[allow(clippy::too_many_arguments)] + pub fn print(&self, node: &T, args: PrintArgs) -> Result + where + T: Node + VisitWith, + { + swc_compiler_base::print(self.cm.clone(), node, args) } + /// Compiles a source file with the direct linear pipeline. #[cfg_attr( debug_assertions, - tracing::instrument(name = "swc::Compiler::apply_transforms", skip_all) + tracing::instrument(target = "swc::pipeline", skip(self, handler, opts)) )] - fn apply_transforms( + pub fn process_js_file( &self, + fm: Arc, handler: &Handler, - #[allow(unused)] comments: SingleThreadedComments, - #[allow(unused)] fm: Arc, - orig: Option, - config: BuiltInput, + opts: &config::Options, ) -> Result { - self.run(|| { - let program = config.program; - let is_typescript_syntax = matches!(config.syntax, Syntax::Typescript(..)); - - if config.emit_isolated_dts && !is_typescript_syntax { - handler.warn( - "jsc.experimental.emitIsolatedDts is enabled but the syntax is not TypeScript", - ); - } - - let source_map_names = if config.source_maps.enabled() { - let mut v = swc_compiler_base::IdentCollector { - names: Default::default(), - }; - - program.visit_with(&mut v); - - v.names - } else { - Default::default() - }; - #[cfg(feature = "isolated-dts")] - let dts_code = if is_typescript_syntax && config.emit_isolated_dts { - use std::cell::RefCell; - - use swc_ecma_codegen::to_code_with_comments; - let (leading, trailing) = comments.borrow_all(); - - let leading = std::rc::Rc::new(RefCell::new(leading.clone())); - let trailing = std::rc::Rc::new(RefCell::new(trailing.clone())); - - let comments = SingleThreadedComments::from_leading_and_trailing(leading, trailing); - - let mut checker = - FastDts::new(fm.name.clone(), config.unresolved_mark, Default::default()); - let mut program = program.clone(); - - #[cfg(feature = "module")] - if let Some((base, resolver)) = config.resolver { - use swc_ecma_transforms_module::rewriter::import_rewriter; - - program.mutate(import_rewriter(base, resolver)); - } - - let issues = checker.transform(&mut program); - - for issue in issues { - handler - .struct_span_err(issue.range.span, &issue.message) - .emit(); - } - - let dts_code = to_code_with_comments(Some(&comments), &program); - Some(dts_code) - } else { - None - }; - - let pass = config.pass; - let (program, output) = swc_transform_common::output::capture(|| { - #[cfg(feature = "isolated-dts")] - { - if let Some(dts_code) = dts_code { - use swc_transform_common::output::experimental_emit; - experimental_emit("__swc_isolated_declarations__".into(), dts_code); - } - } - - helpers::HELPERS.set(&Helpers::new(config.external_helpers), || { - HANDLER.set(handler, || { - // Fold module - program.apply(pass) - }) - }) - }); - - let program = if config.flow_strip_script_like_module { - downgrade_flow_script_like_module(program)? - } else { - program - }; - - if let Some(comments) = &config.comments { - swc_compiler_base::minify_file_comments( - comments, - config.preserve_comments, - BoolOr::Bool(false), - config.output.preserve_annotations.into_bool(), - ); - } - - self.print( - &program, - PrintArgs { - source_root: config.source_root.as_deref(), - source_file_name: config.source_file_name.as_deref(), - source_map_ignore_list: config.source_map_ignore_list.clone(), - output_path: config.output_path, - inline_sources_content: config.inline_sources_content, - source_map: config.source_maps, - source_map_names: &source_map_names, - orig, - comments: config.comments.as_ref().map(|v| v as _), - emit_source_map_columns: config.emit_source_map_columns, - emit_source_map_scopes: config.emit_source_map_scopes, - preamble: &config.output.preamble, - codegen_config: swc_ecma_codegen::Config::default() - .with_target(config.target) - .with_minify(config.minify) - .with_ascii_only( - config - .output - .charset - .map(|v| matches!(v, OutputCharset::Ascii)) - .unwrap_or(false), - ) - .with_emit_assert_for_import_attributes( - config.emit_assert_for_import_attributes, - ) - .with_inline_script(config.codegen_inline_script), - output: if output.is_empty() { - None - } else { - Some(output) - }, - source_map_url: config.output.source_map_url.as_deref(), - }, - ) - }) - } -} - -#[non_exhaustive] -#[derive(Clone, Default)] -pub struct JsMinifyExtras { - pub mangle_name_cache: Option>, -} - -impl JsMinifyExtras { - pub fn with_mangle_name_cache( - mut self, - mangle_name_cache: Option>, - ) -> Self { - self.mangle_name_cache = mangle_name_cache; - self + self.compile(handler, CompileInput::source(fm), opts) + .codegen() } -} - -fn find_swcrc(path: &Path, root: &Path, root_mode: RootMode) -> Option { - let mut parent = path.parent(); - while let Some(dir) = parent { - let swcrc = dir.join(".swcrc"); - - if swcrc.exists() { - return Some(swcrc); - } - - if dir == root && root_mode == RootMode::Root { - break; - } - parent = dir.parent(); - } - - None -} - -#[cfg_attr(debug_assertions, tracing::instrument(skip_all))] -fn load_swcrc(path: &Path) -> Result { - let content = read_to_string(path).context("failed to read config (.swcrc) file")?; - parse_swcrc(&content) -} - -fn parse_swcrc(s: &str) -> Result { - fn convert_json_err(e: serde_json::Error) -> Error { - let line = e.line(); - let column = e.column(); - - let msg = match e.classify() { - Category::Io => "io error", - Category::Syntax => "syntax error", - Category::Data => "unmatched data", - Category::Eof => "unexpected eof", - }; - Error::new(e).context(format!( - "failed to deserialize .swcrc (json) file: {msg}: {line}:{column}" - )) - } - - let v = parse_to_serde_value( - s.trim_start_matches('\u{feff}'), - &ParseOptions { - allow_comments: true, - allow_trailing_commas: true, - allow_loose_object_property_names: false, - }, - )? - .ok_or_else(|| Error::msg("failed to deserialize empty .swcrc (json) file"))?; - - if let Ok(rc) = serde_json::from_value(v.clone()) { - return Ok(rc); + /// Creates a lazy request for the direct linear compilation pipeline. + pub fn compile<'a>( + &'a self, + handler: &'a Handler, + input: CompileInput, + options: &'a config::Options, + ) -> CompileRequest<'a> { + CompileRequest::new(self, handler, input, options) } - - serde_json::from_value(v) - .map(Rc::Single) - .map_err(convert_json_err) } diff --git a/crates/swc/src/minify.rs b/crates/swc/src/minify.rs new file mode 100644 index 000000000000..77d0a84e478b --- /dev/null +++ b/crates/swc/src/minify.rs @@ -0,0 +1,242 @@ +//! The standalone Terser-compatible minification API. +//! +//! This is independent of the optional minify stage in the compilation +//! pipeline. + +use std::sync::Arc; + +use anyhow::{Context, Error}; +use swc_common::{ + comments::{Comments, SingleThreadedComments}, + errors::Handler, + Mark, SourceFile, +}; +use swc_compiler_base::{PrintArgs, TransformOutput}; +use swc_config::types::BoolOr; +use swc_ecma_minifier::option::{MangleCache, MinifyOptions, TopLevelOptions}; +use swc_ecma_parser::{EsSyntax, Syntax}; +use swc_ecma_transforms::{fixer, hygiene, resolver}; +use swc_ecma_transforms_base::fixer::paren_remover; +use swc_ecma_visit::{VisitMutWith, VisitWith}; + +use crate::{ + config::{JsMinifyCommentOption, JsMinifyOptions, SourceMapsConfig}, + Compiler, +}; + +/// Additional state used by the standalone minifier. +#[non_exhaustive] +#[derive(Clone, Default)] +pub struct JsMinifyExtras { + /// Reuses mangled names across minification calls. + pub mangle_name_cache: Option>, +} + +impl JsMinifyExtras { + /// Sets the cache used to reuse mangled names across minification calls. + pub fn with_mangle_name_cache( + mut self, + mangle_name_cache: Option>, + ) -> Self { + self.mangle_name_cache = mangle_name_cache; + self + } +} + +impl Compiler { + #[cfg_attr(debug_assertions, tracing::instrument(target = "swc", skip_all))] + pub fn minify( + &self, + fm: Arc, + handler: &Handler, + opts: &JsMinifyOptions, + extras: JsMinifyExtras, + ) -> Result { + self.run(|| { + let target = opts.ecma.clone().into(); + + let (source_map, orig, source_map_url) = opts + .source_map + .as_ref() + .map(|obj| -> Result<_, Error> { + let orig = obj.content.as_ref().map(|s| s.to_sourcemap()).transpose()?; + + Ok((SourceMapsConfig::Bool(true), orig, obj.url.as_deref())) + }) + .unwrap_as_option(|v| { + Some(Ok(match v { + Some(true) => (SourceMapsConfig::Bool(true), None, None), + _ => (SourceMapsConfig::Bool(false), None, None), + })) + }) + .unwrap()?; + + let mut min_opts = MinifyOptions { + compress: opts + .compress + .clone() + .unwrap_as_option(|default| match default { + Some(true) | None => Some(Default::default()), + _ => None, + }) + .map(|v| v.into_config(self.cm.clone())), + mangle: opts + .mangle + .clone() + .unwrap_as_option(|default| match default { + Some(true) | None => Some(Default::default()), + _ => None, + }), + ..Default::default() + }; + + // top_level defaults to true if module is true + + // https://github.com/swc-project/swc/issues/2254 + + if opts.keep_fnames { + if let Some(opts) = &mut min_opts.compress { + opts.keep_fnames = true; + } + if let Some(opts) = &mut min_opts.mangle { + opts.keep_fn_names = true; + } + } + + let comments = SingleThreadedComments::default(); + + let mut program = self + .parse_js( + fm.clone(), + handler, + target, + Syntax::Es(EsSyntax { + jsx: true, + decorators: true, + decorators_before_export: true, + import_attributes: true, + ..Default::default() + }), + opts.module, + Some(&comments), + ) + .context("failed to parse input file")?; + + if opts.toplevel == Some(true) || program.is_module() { + if let Some(opts) = &mut min_opts.compress { + if opts.top_level.is_none() { + opts.top_level = Some(TopLevelOptions { functions: true }); + } + } + + if let Some(opts) = &mut min_opts.mangle { + if opts.top_level.is_none() { + opts.top_level = Some(true); + } + } + } + + let source_map_names = if source_map.enabled() { + let mut v = swc_compiler_base::IdentCollector { + names: Default::default(), + }; + + program.visit_with(&mut v); + + v.names + } else { + Default::default() + }; + + let unresolved_mark = Mark::new(); + let top_level_mark = Mark::new(); + + let is_mangler_enabled = min_opts.mangle.is_some(); + + program = self.run_transform(handler, false, || { + program.mutate(&mut paren_remover(Some(&comments))); + + program.mutate(&mut resolver(unresolved_mark, top_level_mark, false)); + + let mut program = swc_ecma_minifier::optimize( + program, + self.cm.clone(), + Some(&comments), + None, + &min_opts, + &swc_ecma_minifier::option::ExtraOptions { + unresolved_mark, + top_level_mark, + mangle_name_cache: extras.mangle_name_cache, + }, + ); + + if !is_mangler_enabled { + program.visit_mut_with(&mut hygiene()) + } + program.mutate(&mut fixer(Some(&comments as &dyn Comments))); + program + }); + + let preserve_comments = opts + .format + .comments + .clone() + .into_inner() + .unwrap_or(BoolOr::Data(JsMinifyCommentOption::PreserveSomeComments)); + let extracted_comments = swc_compiler_base::minify_file_comments( + &comments, + preserve_comments, + opts.extract_comments + .clone() + .into_inner() + .unwrap_or(BoolOr::Bool(false)), + opts.format.preserve_annotations, + ); + + let ret = self.print( + &program, + PrintArgs { + source_root: None, + source_file_name: Some(&fm.name.to_string()), + output_path: opts.output_path.clone().map(From::from), + inline_sources_content: opts.inline_sources_content, + source_map, + source_map_ignore_list: opts.source_map_ignore_list.clone(), + source_map_names: &source_map_names, + orig, + comments: Some(&comments), + emit_source_map_columns: opts.emit_source_map_columns, + emit_source_map_scopes: false, + preamble: &opts.format.preamble, + codegen_config: swc_ecma_codegen::Config::default() + .with_target(target) + .with_minify(true) + .with_ascii_only(opts.format.ascii_only) + .with_emit_assert_for_import_attributes( + opts.format.emit_assert_for_import_attributes, + ) + .with_inline_script(opts.format.inline_script) + .with_reduce_escaped_newline( + min_opts + .compress + .unwrap_or_default() + .experimental + .reduce_escaped_newline, + ), + output: None, + source_map_url, + }, + ); + + ret.map(|mut output| { + if !extracted_comments.is_empty() { + output.extracted_comments = Some(extracted_comments); + } + output.diagnostics = handler.take_diagnostics(); + + output + }) + }) + } +} diff --git a/crates/swc/src/pipeline.rs b/crates/swc/src/pipeline.rs new file mode 100644 index 000000000000..44ba67f1a45f --- /dev/null +++ b/crates/swc/src/pipeline.rs @@ -0,0 +1,105 @@ +//! The direct, linear JavaScript compilation pipeline. +//! +//! [`CompileRequest`] terminals use this pipeline. [`Options::build_as_input`] +//! remains a separate delayed-pass compatibility API. + +mod api; +mod finalize; +mod hooks; +#[cfg(feature = "lint")] +mod lint; +mod minify; +mod options; +mod parse; +mod plugin; +mod preparation; +mod resolve; +mod state; +mod terminal; +mod transform; + +use anyhow::Error; +pub use api::{CompileInput, CompileRequest, PipelineContext, PipelineHooks, TransformedProgram}; +use hooks::HookDispatch; +use options::ResolvedPipelineOptions; +use swc_common::errors::Handler; +use terminal::PipelineTerminal; + +use crate::{config::Options, Compiler}; + +pub(super) struct Pipeline<'a> { + compiler: &'a Compiler, + handler: &'a Handler, +} + +impl<'a> Pipeline<'a> { + pub(super) fn new(compiler: &'a Compiler, handler: &'a Handler) -> Self { + Self { compiler, handler } + } + + fn transform( + self, + input: CompileInput, + options: &Options, + hooks: &mut H, + terminal: T, + ) -> Result + where + H: HookDispatch, + T: PipelineTerminal, + { + let config = self.resolve_config(&input.source_file, options)?; + let context = config.context; + let external_helpers = config.config.jsc.external_helpers.into_bool(); + let mut unit = self.parse(input, &config)?; + + let ((result, helper_data), transform_output) = + swc_transform_common::output::capture(|| { + self.compiler + .run_transform_with_helpers(self.handler, external_helpers, || { + hooks.after_parse(&self, &mut unit, &context)?; + self.run_react_compiler(&mut unit, &config); + self.run_resolver(&mut unit, &config); + hooks.after_resolve(&self, &mut unit, &context)?; + + let ResolvedPipelineOptions { + plugin, + #[cfg(feature = "lint")] + lint, + transform, + minify, + finalize, + terminal: mut terminal_state, + } = self.resolve_pipeline_options(config, &unit, &terminal, options)?; + let mut plugin = self.create_runtime_plugin(&unit, &context, plugin)?; + terminal.prepare(&self, &unit, &context, &mut terminal_state)?; + + #[cfg(feature = "lint")] + let lint = self.prepare_lint(&unit, &context, lint); + plugin.process_before_syntax(&mut unit.program); + #[cfg(feature = "lint")] + self.run_lint(&unit, lint); + + let mut transform = transform; + self.transform_syntax(&mut unit, &context, transform.as_mut()); + plugin.process_after_syntax(&mut unit.program); + hooks.after_typescript(&self, &mut unit, &context)?; + self.transform_after_typescript(&mut unit, &context, transform); + self.minify(&mut unit, &context, minify); + self.finalize(&mut unit, &context, finalize)?; + + Ok::<_, Error>(terminal_state) + }) + }); + let terminal_state = result?; + + Ok(terminal.finish( + self.compiler, + unit, + context, + terminal_state, + helper_data, + transform_output, + )) + } +} diff --git a/crates/swc/src/pipeline/README.md b/crates/swc/src/pipeline/README.md new file mode 100644 index 000000000000..592f433a275d --- /dev/null +++ b/crates/swc/src/pipeline/README.md @@ -0,0 +1,46 @@ +# Direct compilation pipeline + +`Compiler::compile` creates a lazy `CompileRequest`; a terminal executes this +pipeline without using `Options::build_as_input`. + +```mermaid +flowchart TD + Request["CompileRequest"] --> Terminal{"terminal"} + Terminal -->|"codegen()"| EmitIntent["emit preparation enabled"] + Terminal -->|"transform() / into_program()"| AstIntent["emit preparation skipped"] + EmitIntent --> Config["resolve config"] + AstIntent --> Config + Config --> Parse["parse or accept Program"] + Parse --> ParseHooks["inspect → mutate after parse"] + ParseHooks --> Resolver["React Compiler → resolver"] + Resolver --> ResolveHooks["inspect → mutate after resolve"] + ResolveHooks --> Prepare{"codegen terminal?"} + Prepare -->|yes| EmitPrep["emit preparation: source maps / names / isolated DTS"] + Prepare -->|no| Syntax + EmitPrep --> Syntax["runtime plugin / lint / syntax and type transforms"] + Syntax --> TypeHooks["inspect → mutate after_typescript"] + TypeHooks --> Transform["React → optimizer → compatibility → module"] + Transform --> Minify["AST minify"] + Minify --> Finalize["hygiene → fixer → optional Jest / dropped-comment preservation → optional Flow downgrade → comment pruning"] + Finalize --> Output{"terminal"} + Output -->|"transform()"| Ast["TransformedProgram"] + Output -->|"into_program()"| Program["Program"] + Output -->|"codegen()"| Codegen["codegen outside pipeline"] +``` + +- Hooks run only at a terminal and always inspect before mutating. +- Input source-map loading, name collection, and isolated-DTS generation are + codegen-only stages after resolver hooks and before runtime plugins and + built-in transforms. +- The `after_typescript` boundary is also reached for JavaScript and Flow. It + follows early syntax transforms and the runtime-plugin checkpoint. +- Runtime plugins have one checkpoint: before lint with `runPluginFirst`, + otherwise after syntax/type transforms. +- AST and codegen minification are independent. `CompileInput::program` + preserves the supplied Module/Script variant, while resolved options still + select later transforms. Its spans and comments must refer to the supplied + `SourceFile`, which must be registered in the compiler's source map. +- `transform()` retains comments and helper requirements; `into_program()` + returns only the final AST. + +The frozen delayed-pass path is documented in `../config/legacy/README.md`. diff --git a/crates/swc/src/pipeline/api.rs b/crates/swc/src/pipeline/api.rs new file mode 100644 index 000000000000..08fdf1b5f959 --- /dev/null +++ b/crates/swc/src/pipeline/api.rs @@ -0,0 +1,446 @@ +use std::sync::Arc; + +use anyhow::Error; +use swc_common::{ + comments::SingleThreadedComments, errors::Handler, FileName, Mark, SourceFile, SourceMap, +}; +use swc_compiler_base::TransformOutput; +use swc_ecma_ast::{EsVersion, Program}; +use swc_ecma_parser::Syntax; +use swc_ecma_transforms::helpers::HelperData; + +use super::{ + hooks::HookDispatch, + terminal::{CodegenTerminal, PipelineTerminal, ProgramTerminal}, + Pipeline, +}; +use crate::{config::Options, Compiler}; + +/// Input to [`crate::Compiler::compile`]. +/// +/// A pre-parsed program, its spans, and any supplied comments must match the +/// source file registered in the [`crate::Compiler`]'s source map. +pub struct CompileInput { + pub(super) source_file: Arc, + pub(super) program: Option, + pub(super) comments: Option, +} + +impl CompileInput { + /// Creates input that will be parsed by the pipeline. + pub fn source(file: Arc) -> Self { + Self { + source_file: file, + program: None, + comments: None, + } + } + + /// Creates input from an already parsed program. + /// + /// The supplied [`Program`] variant is not reparsed or reclassified. The + /// resolved syntax and other options still control later transforms. + /// + /// Supply its parser comment storage with [`CompileInput::with_comments`]. + pub fn program(file: Arc, program: Program) -> Self { + Self { + source_file: file, + program: Some(program), + comments: None, + } + } + + /// Uses the supplied comment storage throughout transformation and, for a + /// codegen terminal, emit. + pub fn with_comments(mut self, comments: SingleThreadedComments) -> Self { + self.comments = Some(comments); + self + } +} + +/// A lazily executed request for the direct compilation pipeline. +/// +/// Creating a request does not resolve configuration, parse input, invoke +/// hooks, or transform the program. One of its terminal methods must be called +/// to perform compilation. +#[must_use = "a CompileRequest does nothing until a terminal method is called"] +pub struct CompileRequest<'a, H = ()> { + compiler: &'a Compiler, + handler: &'a Handler, + input: CompileInput, + options: &'a Options, + hooks: H, +} + +impl<'a> CompileRequest<'a> { + pub(crate) fn new( + compiler: &'a Compiler, + handler: &'a Handler, + input: CompileInput, + options: &'a Options, + ) -> Self { + Self { + compiler, + handler, + input, + options, + hooks: (), + } + } + + /// Attaches hooks that will run when a terminal method executes the + /// request. + pub fn with_hooks(self, hooks: H) -> CompileRequest<'a, H> { + CompileRequest { + compiler: self.compiler, + handler: self.handler, + input: self.input, + options: self.options, + hooks, + } + } + + /// Executes the pipeline and generates source code. + #[cfg_attr( + debug_assertions, + tracing::instrument(target = "swc::pipeline", skip_all) + )] + pub fn codegen(self) -> Result { + codegen(self) + } + + /// Executes the AST pipeline without emit-only preparation or codegen. + #[cfg_attr( + debug_assertions, + tracing::instrument(target = "swc::pipeline", skip_all) + )] + pub fn transform(self) -> Result { + transform(self) + } + + /// Executes the complete AST pipeline and returns only the transformed + /// program. + /// + /// This discards final comments and recorded helper requirements. Use + /// [`CompileRequest::transform`] to retain them. + pub fn into_program(self) -> Result { + self.transform().map(TransformedProgram::into_program) + } +} + +impl<'a, H> CompileRequest<'a, H> +where + H: PipelineHooks, +{ + /// Executes the pipeline with hooks and generates source code. + #[cfg_attr( + debug_assertions, + tracing::instrument(target = "swc::pipeline", skip_all) + )] + pub fn codegen(self) -> Result { + codegen(self) + } + + /// Executes the AST pipeline with hooks, without emit-only preparation or + /// codegen. + #[cfg_attr( + debug_assertions, + tracing::instrument(target = "swc::pipeline", skip_all) + )] + pub fn transform(self) -> Result { + transform(self) + } + + /// Executes the complete AST pipeline with hooks and returns only the + /// transformed program. + /// + /// This discards final comments and recorded helper requirements. Use + /// [`CompileRequest::transform`] to retain them. + pub fn into_program(self) -> Result { + self.transform().map(TransformedProgram::into_program) + } +} + +fn execute(request: CompileRequest<'_, H>, terminal: T) -> Result +where + H: HookDispatch, + T: PipelineTerminal, +{ + let CompileRequest { + compiler, + handler, + input, + options, + mut hooks, + } = request; + + compiler + .run(|| Pipeline::new(compiler, handler).transform(input, options, &mut hooks, terminal)) +} + +fn codegen(request: CompileRequest<'_, H>) -> Result +where + H: HookDispatch, +{ + execute(request, CodegenTerminal)?.codegen() +} + +fn transform(request: CompileRequest<'_, H>) -> Result +where + H: HookDispatch, +{ + execute(request, ProgramTerminal) +} + +/// A program produced by the AST-only terminal of [`CompileRequest`]. +/// +/// This retains final comments and helper requirements. AST-only terminals +/// skip emit preparation, so this type does not provide code generation. Choose +/// [`CompileRequest::codegen`] as the request terminal when emitted output is +/// required. +pub struct TransformedProgram { + program: Program, + comments: SingleThreadedComments, + helper_data: HelperData, +} + +impl TransformedProgram { + pub(super) fn new( + program: Program, + comments: SingleThreadedComments, + helper_data: HelperData, + ) -> Self { + Self { + program, + comments, + helper_data, + } + } + + /// Returns the transformed program. + pub fn program(&self) -> &Program { + &self.program + } + + /// Returns comments after the pipeline's final comment policy. + pub fn comments(&self) -> &SingleThreadedComments { + &self.comments + } + + /// Returns the helper requirements recorded while transforming the + /// program. + /// + /// This is primarily useful with [`Options::skip_helper_injection`], when + /// a later bundling or helper-injection phase must consume the + /// requirements. + pub fn helper_data(&self) -> HelperData { + self.helper_data + } + + /// Returns `(program, comments, helper requirements)`. + pub fn into_parts(self) -> (Program, SingleThreadedComments, HelperData) { + (self.program, self.comments, self.helper_data) + } + + /// Consumes this result and returns the transformed program, discarding + /// its comments and helper requirements. + pub fn into_program(self) -> Program { + self.program + } +} + +/// Borrowed compilation context supplied to pipeline hooks. +pub struct PipelineContext<'a> { + pub(super) source_map: &'a SourceMap, + pub(super) handler: &'a Handler, + pub(super) comments: &'a SingleThreadedComments, + pub(super) filename: &'a FileName, + pub(super) syntax: Syntax, + pub(super) target: EsVersion, + pub(super) unresolved_mark: Mark, + pub(super) top_level_mark: Mark, +} + +impl PipelineContext<'_> { + /// Returns the compiler source map. + pub fn source_map(&self) -> &SourceMap { + self.source_map + } + + /// Returns the diagnostic handler for this compilation. + pub fn handler(&self) -> &Handler { + self.handler + } + + /// Returns the compilation's comment storage. + pub fn comments(&self) -> &SingleThreadedComments { + self.comments + } + + /// Returns the input filename. + pub fn filename(&self) -> &FileName { + self.filename + } + + /// Returns the resolved syntax configuration. + pub fn syntax(&self) -> Syntax { + self.syntax + } + + /// Returns the resolved output target. + pub fn target(&self) -> EsVersion { + self.target + } + + /// Returns the mark used for unresolved identifiers. + /// + /// The mark has not been applied yet when the after-parse hooks run. + pub fn unresolved_mark(&self) -> Mark { + self.unresolved_mark + } + + /// Returns the mark used for top-level bindings. + /// + /// The mark has not been applied yet when the after-parse hooks run. + pub fn top_level_mark(&self) -> Mark { + self.top_level_mark + } +} + +/// Inspection and mutation callbacks for the direct compiler pipeline. +/// +/// Hooks run only when a [`CompileRequest`] terminal executes. At each stage, +/// the inspection callback runs before its paired mutation callback. If +/// inspection returns an error, the paired mutation and all later hooks are +/// skipped. +pub trait PipelineHooks { + /// Inspects the parsed or supplied program before React Compiler and the + /// initial resolver. + fn inspect_after_parse( + &mut self, + _program: &Program, + _context: &PipelineContext<'_>, + ) -> Result<(), Error> { + Ok(()) + } + + /// Mutates the parsed or supplied program before React Compiler and the + /// initial resolver. + fn mutate_after_parse( + &mut self, + _program: &mut Program, + _context: &PipelineContext<'_>, + ) -> Result<(), Error> { + Ok(()) + } + + /// Inspects the program after React Compiler and the initial resolver, + /// before codegen-only preparation, plugins, linting, and syntax + /// transforms. + fn inspect_after_resolve( + &mut self, + _program: &Program, + _context: &PipelineContext<'_>, + ) -> Result<(), Error> { + Ok(()) + } + + /// Mutates the program after React Compiler and the initial resolver, + /// before codegen-only preparation, plugins, linting, and syntax + /// transforms. + /// + /// Bindings introduced here are not processed by the initial resolver. + fn mutate_after_resolve( + &mut self, + _program: &mut Program, + _context: &PipelineContext<'_>, + ) -> Result<(), Error> { + Ok(()) + } + + /// Inspects the program after the early syntax stage and runtime-plugin + /// checkpoint, but before React, optimization, compatibility, and module + /// transforms. + /// + /// This boundary is reached for JavaScript, TypeScript, and Flow. With + /// built-ins enabled, the early syntax stage and type stripping are + /// complete. + fn inspect_after_typescript( + &mut self, + _program: &Program, + _context: &PipelineContext<'_>, + ) -> Result<(), Error> { + Ok(()) + } + + /// Mutates the program after the early syntax stage and runtime-plugin + /// checkpoint, but before React, optimization, compatibility, and module + /// transforms. + /// + /// This boundary is reached for JavaScript, TypeScript, and Flow. With + /// built-ins enabled, the early syntax stage and type stripping are + /// complete. + /// + /// Bindings introduced here are not processed by the initial resolver; + /// callers must assign any syntax contexts required by later transforms. + fn mutate_after_typescript( + &mut self, + _program: &mut Program, + _context: &PipelineContext<'_>, + ) -> Result<(), Error> { + Ok(()) + } +} + +impl PipelineHooks for &mut T +where + T: PipelineHooks + ?Sized, +{ + fn inspect_after_parse( + &mut self, + program: &Program, + context: &PipelineContext<'_>, + ) -> Result<(), Error> { + (**self).inspect_after_parse(program, context) + } + + fn mutate_after_parse( + &mut self, + program: &mut Program, + context: &PipelineContext<'_>, + ) -> Result<(), Error> { + (**self).mutate_after_parse(program, context) + } + + fn inspect_after_resolve( + &mut self, + program: &Program, + context: &PipelineContext<'_>, + ) -> Result<(), Error> { + (**self).inspect_after_resolve(program, context) + } + + fn mutate_after_resolve( + &mut self, + program: &mut Program, + context: &PipelineContext<'_>, + ) -> Result<(), Error> { + (**self).mutate_after_resolve(program, context) + } + + fn inspect_after_typescript( + &mut self, + program: &Program, + context: &PipelineContext<'_>, + ) -> Result<(), Error> { + (**self).inspect_after_typescript(program, context) + } + + fn mutate_after_typescript( + &mut self, + program: &mut Program, + context: &PipelineContext<'_>, + ) -> Result<(), Error> { + (**self).mutate_after_typescript(program, context) + } +} diff --git a/crates/swc/src/pipeline/finalize.rs b/crates/swc/src/pipeline/finalize.rs new file mode 100644 index 000000000000..fcb2435fba90 --- /dev/null +++ b/crates/swc/src/pipeline/finalize.rs @@ -0,0 +1,72 @@ +use anyhow::Error; +use swc_common::{comments::Comments, util::take::Take}; +use swc_config::types::BoolOr; +use swc_ecma_ext_transforms::jest; +use swc_ecma_transforms::{fixer::fixer, hygiene::hygiene_with_config}; + +use super::{ + state::{CompilationUnit, PipelineContextData}, + Pipeline, +}; +use crate::{ + config::JsMinifyCommentOption, dropped_comments_preserver::dropped_comments_preserver, +}; + +/// Final AST cleanup and comment policy. +pub(super) struct FinalizeStageOptions { + pub(super) builtins: Option, + pub(super) preserve_comments: BoolOr, + pub(super) preserve_annotations: bool, +} + +pub(super) struct BuiltinFinalizeOptions { + pub(super) hygiene_config: Option, + pub(super) run_fixer: bool, + pub(super) run_jest: bool, + pub(super) preserve_dropped_comments: bool, +} + +impl Pipeline<'_> { + pub(super) fn finalize( + &self, + unit: &mut CompilationUnit, + context: &PipelineContextData, + options: FinalizeStageOptions, + ) -> Result<(), Error> { + if let Some(builtins) = options.builtins { + if let Some(config) = builtins.hygiene_config { + unit.program.mutate(hygiene_with_config( + swc_ecma_transforms_base::hygiene::Config { + top_level_mark: context.top_level_mark, + ..config + }, + )); + } + + if builtins.run_fixer { + unit.program + .mutate(fixer(Some(&unit.comments as &dyn Comments))); + } + if builtins.run_jest { + unit.program.mutate(jest::jest()); + } + if builtins.preserve_dropped_comments { + unit.program + .mutate(dropped_comments_preserver(Some(unit.comments.clone()))); + } + } + + if unit.flow_strip_script_like_module { + unit.program = crate::flow::downgrade_script_like_module(unit.program.take())?; + } + + swc_compiler_base::minify_file_comments( + &unit.comments, + options.preserve_comments, + BoolOr::Bool(false), + options.preserve_annotations, + ); + + Ok(()) + } +} diff --git a/crates/swc/src/pipeline/hooks.rs b/crates/swc/src/pipeline/hooks.rs new file mode 100644 index 000000000000..bb8de4f5ed20 --- /dev/null +++ b/crates/swc/src/pipeline/hooks.rs @@ -0,0 +1,107 @@ +use anyhow::Error; +use swc_ecma_ast::Program; + +use super::{ + api::{PipelineContext, PipelineHooks}, + state::{CompilationUnit, PipelineContextData}, + Pipeline, +}; + +/// Invokes optional hooks at the public pipeline boundaries. +pub(super) trait HookDispatch { + fn after_parse( + &mut self, + _pipeline: &Pipeline<'_>, + _unit: &mut CompilationUnit, + _context: &PipelineContextData, + ) -> Result<(), Error> { + Ok(()) + } + + fn after_resolve( + &mut self, + _pipeline: &Pipeline<'_>, + _unit: &mut CompilationUnit, + _context: &PipelineContextData, + ) -> Result<(), Error> { + Ok(()) + } + + fn after_typescript( + &mut self, + _pipeline: &Pipeline<'_>, + _unit: &mut CompilationUnit, + _context: &PipelineContextData, + ) -> Result<(), Error> { + Ok(()) + } +} + +impl HookDispatch for () {} + +impl HookDispatch for H +where + H: PipelineHooks, +{ + fn after_parse( + &mut self, + pipeline: &Pipeline<'_>, + unit: &mut CompilationUnit, + context: &PipelineContextData, + ) -> Result<(), Error> { + invoke_hook(pipeline, unit, context, |program, context| { + self.inspect_after_parse(program, context)?; + self.mutate_after_parse(program, context) + }) + } + + fn after_resolve( + &mut self, + pipeline: &Pipeline<'_>, + unit: &mut CompilationUnit, + context: &PipelineContextData, + ) -> Result<(), Error> { + invoke_hook(pipeline, unit, context, |program, context| { + self.inspect_after_resolve(program, context)?; + self.mutate_after_resolve(program, context) + }) + } + + fn after_typescript( + &mut self, + pipeline: &Pipeline<'_>, + unit: &mut CompilationUnit, + context: &PipelineContextData, + ) -> Result<(), Error> { + invoke_hook(pipeline, unit, context, |program, context| { + self.inspect_after_typescript(program, context)?; + self.mutate_after_typescript(program, context) + }) + } +} + +fn invoke_hook( + pipeline: &Pipeline<'_>, + unit: &mut CompilationUnit, + stage_context: &PipelineContextData, + hook: impl FnOnce(&mut Program, &PipelineContext<'_>) -> Result<(), Error>, +) -> Result<(), Error> { + let CompilationUnit { + source_file, + program, + comments, + .. + } = unit; + let context = PipelineContext { + source_map: &pipeline.compiler.cm, + handler: pipeline.handler, + comments, + filename: &source_file.name, + syntax: stage_context.syntax, + target: stage_context.target, + unresolved_mark: stage_context.unresolved_mark, + top_level_mark: stage_context.top_level_mark, + }; + + hook(program, &context) +} diff --git a/crates/swc/src/pipeline/lint.rs b/crates/swc/src/pipeline/lint.rs new file mode 100644 index 000000000000..b9967f91510c --- /dev/null +++ b/crates/swc/src/pipeline/lint.rs @@ -0,0 +1,58 @@ +use swc_common::SyntaxContext; +use swc_ecma_ast::Program; +use swc_ecma_lints::{ + config::LintConfig, + rule::Rule, + rules::{self, LintParams}, +}; + +use super::{ + state::{CompilationUnit, PipelineContextData}, + Pipeline, +}; + +/// Lint configuration for the pre-syntax checkpoint. +pub(super) struct LintOptions { + pub(super) config: LintConfig, +} + +/// Lint rules constructed from the AST before an early runtime plugin runs. +pub(super) struct PreparedLint { + rules: Vec>, +} + +impl Pipeline<'_> { + /// Constructs lint rules before an early plugin can replace the program. + pub(super) fn prepare_lint( + &self, + unit: &CompilationUnit, + context: &PipelineContextData, + options: Option, + ) -> Option { + let options = options?; + let unresolved_ctxt = SyntaxContext::empty().apply_mark(context.unresolved_mark); + let top_level_ctxt = SyntaxContext::empty().apply_mark(context.top_level_mark); + let rules = rules::all(LintParams { + program: &unit.program, + lint_config: &options.config, + top_level_ctxt, + unresolved_ctxt, + es_version: context.target, + source_map: self.compiler.cm.clone(), + }); + + Some(PreparedLint { rules }) + } + + /// Runs lint at the pre-syntax checkpoint. + pub(super) fn run_lint(&self, unit: &CompilationUnit, lint: Option) { + let Some(mut lint) = lint else { + return; + }; + + match &unit.program { + Program::Module(program) => lint.rules.lint_module(program), + Program::Script(program) => lint.rules.lint_script(program), + } + } +} diff --git a/crates/swc/src/pipeline/minify.rs b/crates/swc/src/pipeline/minify.rs new file mode 100644 index 000000000000..43ad4e2d2c4a --- /dev/null +++ b/crates/swc/src/pipeline/minify.rs @@ -0,0 +1,89 @@ +use swc_common::{comments::Comments, util::take::Take, Mark}; +use swc_config::types::BoolOrDataConfig; +use swc_ecma_minifier::{ + optimize, + option::{ + terser::{TerserCompressorOptions, TerserTopLevelOptions}, + MangleOptions, MinifyOptions, + }, +}; +use swc_ecma_transforms::{hygiene::hygiene_with_config, resolver}; + +use super::{ + state::{CompilationUnit, PipelineContextData}, + Pipeline, +}; + +/// AST minifier options, distinct from code generator minification. +pub(super) struct MinifyStageOptions { + pub(super) compress: BoolOrDataConfig, + pub(super) mangle: BoolOrDataConfig, +} + +impl Pipeline<'_> { + pub(super) fn minify( + &self, + unit: &mut CompilationUnit, + context: &PipelineContextData, + options: Option, + ) { + let Some(options) = options else { + return; + }; + + let minify_options = MinifyOptions { + compress: options + .compress + .unwrap_as_option(|default| match default { + Some(true) => Some(Default::default()), + _ => None, + }) + .map(|mut compress| { + if compress.const_to_let.is_none() { + compress.const_to_let = Some(true); + } + if compress.toplevel.is_none() && unit.program.is_module() { + compress.toplevel = Some(TerserTopLevelOptions::Bool(true)); + } + if unit.program.is_script() { + compress.module = false; + } + compress.into_config(self.compiler.cm.clone()) + }), + mangle: options.mangle.unwrap_as_option(|default| match default { + Some(true) => Some(Default::default()), + _ => None, + }), + ..Default::default() + }; + + if minify_options.compress.is_none() && minify_options.mangle.is_none() { + return; + } + + unit.program.mutate(hygiene_with_config( + swc_ecma_transforms_base::hygiene::Config { + top_level_mark: context.top_level_mark, + ..swc_ecma_transforms_base::hygiene::Config::hygiene_default() + }, + )); + + let unresolved_mark = Mark::new(); + let top_level_mark = Mark::new(); + unit.program + .mutate(resolver(unresolved_mark, top_level_mark, false)); + + unit.program = optimize( + unit.program.take(), + self.compiler.cm.clone(), + Some(&unit.comments as &dyn Comments), + None, + &minify_options, + &swc_ecma_minifier::option::ExtraOptions { + unresolved_mark, + top_level_mark, + mangle_name_cache: None, + }, + ); + } +} diff --git a/crates/swc/src/pipeline/options.rs b/crates/swc/src/pipeline/options.rs new file mode 100644 index 000000000000..d327dbfaaca9 --- /dev/null +++ b/crates/swc/src/pipeline/options.rs @@ -0,0 +1,528 @@ +#[cfg(feature = "plugin")] +use std::sync::Arc; + +use anyhow::{bail, Error}; +use swc_common::{FileName, SourceFile}; +use swc_compiler_base::SourceMapsConfig; +use swc_config::{ + merge::Merge, + types::{BoolOr, BoolOrDataConfig}, +}; +use swc_ecma_minifier::option::terser::TerserTopLevelOptions; +use swc_ecma_transforms::{hygiene, typescript::TsImportExportAssignConfig}; + +#[cfg(feature = "lint")] +use super::lint::LintOptions; +use super::{ + finalize::{BuiltinFinalizeOptions, FinalizeStageOptions}, + minify::MinifyStageOptions, + plugin::{PluginOptions, PluginPlacement}, + preparation::PreparationOptions, + state::{CompilationUnit, PipelineContextData}, + terminal::{CodegenTerminalState, PipelineTerminal}, + transform::BuiltinTransformOptions, + Pipeline, +}; +use crate::{ + codegen::CodegenOptions, + config::{ + Config, JsMinifyCommentOption, JsMinifyOptions, JscConfig, JscOutputConfig, ModuleConfig, + Options, OutputCharset, + }, +}; + +/// Stage options resolved for one pipeline execution. +pub(super) struct ResolvedPipelineOptions { + pub(super) plugin: PluginOptions, + #[cfg(feature = "lint")] + pub(super) lint: Option, + pub(super) transform: Option, + pub(super) minify: Option, + pub(super) finalize: FinalizeStageOptions, + pub(super) terminal: T, +} + +/// Resolved configuration and hook context for one pipeline execution. +/// +/// Program-dependent options are resolved after the resolver hook boundary, +/// so hook mutations affect the defaults used by later stages. +pub(super) struct PipelineConfig { + pub(super) config: Config, + pub(super) context: PipelineContextData, + pub(super) inject_helpers: bool, + pub(super) fixer_enabled: bool, + pub(super) hygiene_enabled: bool, + #[cfg(feature = "plugin")] + pub(super) plugin_env_name: String, + #[cfg(feature = "plugin")] + pub(super) plugin_runtime: Option>, +} + +impl Pipeline<'_> { + pub(super) fn resolve_config( + &self, + source_file: &SourceFile, + options: &Options, + ) -> Result { + let base = source_file.name.clone(); + + if let FileName::Real(path) = &*base { + if !options.config.matches(path)? { + bail!("cannot process file because it's ignored by .swcrc"); + } + } + + let loaded_config = self.compiler.read_config(options, &base)?; + let mut loaded_config = loaded_config + .ok_or_else(|| Error::msg("cannot process file because it's ignored by .swcrc"))?; + // Source-map ignore rules come from the loaded file config and retain + // precedence over the caller's base config during emit. + let source_map_ignore_list = loaded_config.source_map_ignore_list.take(); + + let mut config = options.config.clone(); + config.merge(loaded_config); + if let FileName::Real(path) = &*base { + config.adjust(path); + } + config.source_map_ignore_list = source_map_ignore_list; + + if config.jsc.target.is_some() && config.env.is_some() { + bail!("`env` and `jsc.target` cannot be used together"); + } + + let syntax = config.jsc.syntax.unwrap_or_default(); + let target = config.jsc.target.unwrap_or_default(); + let unresolved_mark = options.unresolved_mark.unwrap_or_default(); + let top_level_mark = options.top_level_mark.unwrap_or_default(); + + Ok(PipelineConfig { + config, + context: PipelineContextData { + syntax, + target, + unresolved_mark, + top_level_mark, + }, + inject_helpers: !options.skip_helper_injection, + fixer_enabled: !options.disable_fixer, + hygiene_enabled: !options.disable_hygiene, + #[cfg(feature = "plugin")] + plugin_env_name: options.env_name.clone(), + #[cfg(feature = "plugin")] + plugin_runtime: options.runtime_options.plugin_runtime.clone(), + }) + } + + /// Resolves options that depend on the program at the after-resolve + /// boundary. + pub(super) fn resolve_pipeline_options( + &self, + config: PipelineConfig, + unit: &CompilationUnit, + terminal: &T, + options: &Options, + ) -> Result, Error> + where + T: PipelineTerminal, + { + let PipelineConfig { + config, + context, + inject_helpers, + fixer_enabled, + hygiene_enabled, + #[cfg(feature = "plugin")] + plugin_env_name, + #[cfg(feature = "plugin")] + plugin_runtime, + } = config; + let syntax = context.syntax; + + let JscConfig { + assumptions, + transform, + syntax: _, + external_helpers: _, + target: _, + loose, + keep_class_names, + base_url, + paths, + minify, + experimental, + #[cfg(feature = "lint")] + lints, + preserve_all_comments, + output, + rewrite_relative_import_extensions, + preserve_symlinks, + } = config.jsc; + + let loose = loose.into_bool(); + let preserve_all_comments = preserve_all_comments.into_bool(); + let preserve_symlinks = preserve_symlinks.into_bool(); + let keep_class_names = keep_class_names.into_bool(); + let mut assumptions = assumptions.unwrap_or_else(|| { + if loose { + swc_ecma_transforms::Assumptions::all() + } else { + swc_ecma_transforms::Assumptions::default() + } + }); + let mut transform = transform.into_inner().unwrap_or_default(); + if syntax.typescript() { + assumptions.set_class_methods |= !transform.use_define_for_class_fields.into_bool(); + transform.legacy_decorator = true.into(); + } + assumptions.set_public_class_fields |= !transform.use_define_for_class_fields.into_bool(); + + let default_top_level = unit.program.is_module() && !unit.flow_strip_script_like_module; + let ast_minify = resolve_minify_options(minify, default_top_level, config.module.as_ref()); + let is_mangler_enabled = ast_minify + .as_ref() + .is_some_and(|options| options.mangle.is_obj() || options.mangle.is_true()); + let codegen_minify = config.minify.into_bool(); + + let import_export_assign_config = match config.module { + Some(ModuleConfig::Es6(..)) => TsImportExportAssignConfig::EsNext, + Some(ModuleConfig::CommonJs(..)) + | Some(ModuleConfig::Amd(..)) + | Some(ModuleConfig::Umd(..)) + | Some(ModuleConfig::SystemJs(..)) => TsImportExportAssignConfig::Preserve, + Some(ModuleConfig::NodeNext(..)) => TsImportExportAssignConfig::NodeNext, + _ => TsImportExportAssignConfig::Classic, + }; + + let (minify, mut minify_format) = match ast_minify { + Some(JsMinifyOptions { + compress, + mangle, + format, + .. + }) => (Some(MinifyStageOptions { compress, mangle }), Some(format)), + None => (None, None), + }; + let minify_comments = minify_format.as_mut().map(|format| { + match std::mem::take(&mut format.comments).into_inner() { + Some(value) => value, + None => BoolOr::Bool(true), + } + }); + let preserve_comments = if preserve_all_comments { + BoolOr::Bool(true) + } else { + minify_comments.unwrap_or({ + BoolOr::Data(if codegen_minify { + JsMinifyCommentOption::PreserveSomeComments + } else { + JsMinifyCommentOption::PreserveAllComments + }) + }) + }; + + let JscOutputConfig { + charset, + preamble, + preserve_annotations, + source_map_url, + } = output; + + #[cfg(feature = "module")] + let module_resolver = { + let paths: crate::config::CompiledPaths = paths.into_iter().collect(); + ModuleConfig::get_resolver( + &base_url, + paths, + &unit.source_file.name, + config.module.as_ref(), + preserve_symlinks, + ) + }; + #[cfg(not(feature = "module"))] + let module_resolver = { + let _ = ( + &base_url, + paths, + &unit.source_file.name, + config.module.as_ref(), + preserve_symlinks, + ); + }; + + let env = config.env.map(Into::into); + let feature_config = env + .as_ref() + .map(|env: &swc_ecma_preset_env::EnvConfig| env.get_feature_config()); + let plugin_placement = if experimental.run_plugin_first.into_bool() { + PluginPlacement::BeforeSyntax + } else { + PluginPlacement::AfterSyntax + }; + let builtin_transforms_enabled = !experimental + .disable_builtin_transforms_for_internal_testing + .into_bool(); + let keep_import_attributes = experimental.keep_import_attributes.into_bool(); + #[cfg(feature = "lint")] + let lints_enabled = !experimental.disable_all_lints.into_bool(); + let plugin = PluginOptions { + placement: plugin_placement, + plugins: experimental.plugins, + #[cfg(feature = "plugin")] + plugin_env_vars: experimental.plugin_env_vars, + #[cfg(feature = "plugin")] + cache_root: experimental.cache_root, + #[cfg(feature = "plugin")] + env_name: plugin_env_name, + #[cfg(feature = "plugin")] + runtime: plugin_runtime, + }; + + let terminal = terminal.resolve_state(|| { + let mut source_maps = options.source_maps.clone(); + source_maps.merge(config.source_maps); + let source_maps = source_maps.unwrap_or(SourceMapsConfig::Bool(false)); + let input_source_map = if source_maps.enabled() { + Some(config.input_source_map.unwrap_or_default()) + } else { + None + }; + let emit_isolated_dts = experimental.emit_isolated_dts.into_bool(); + #[cfg(all(feature = "isolated-dts", feature = "module"))] + let isolated_dts_module_resolver = if emit_isolated_dts { + module_resolver.clone() + } else { + None + }; + let preparation = PreparationOptions { + input_source_map, + emit_isolated_dts, + #[cfg(all(feature = "isolated-dts", feature = "module"))] + module_resolver: isolated_dts_module_resolver, + }; + + let (minify_ascii_only, codegen_inline_script, minify_preamble) = minify_format + .map(|format| (format.ascii_only, format.inline_script, format.preamble)) + .unwrap_or_default(); + let mut preamble = preamble; + if preamble.is_empty() { + preamble = minify_preamble; + } + let ascii_only = match charset { + Some(OutputCharset::Ascii) => true, + Some(OutputCharset::Utf8) => false, + None => minify_ascii_only, + }; + let codegen = CodegenOptions { + source_maps, + output_path: options.output_path.clone(), + source_root: options.source_root.clone(), + source_file_name: options.source_file_name.clone(), + source_map_ignore_list: config.source_map_ignore_list, + inline_sources_content: config.inline_sources_content.into_bool(), + emit_source_map_columns: config.emit_source_map_columns.into_bool(), + preamble, + source_map_url, + ascii_only, + minify: codegen_minify, + emit_assert_for_import_attributes: experimental + .emit_assert_for_import_attributes + .into_bool(), + emit_source_map_scopes: experimental.emit_source_map_scopes.into_bool(), + inline_script: codegen_inline_script, + }; + + CodegenTerminalState { + preparation, + codegen, + metadata: Default::default(), + } + }); + + let run_jest = transform.hidden.jest.into_bool(); + #[cfg(feature = "lint")] + let lint = if builtin_transforms_enabled && lints_enabled { + Some(LintOptions { config: lints }) + } else { + None + }; + let transform = if builtin_transforms_enabled { + Some(BuiltinTransformOptions { + assumptions, + loose, + transform, + env, + feature_config, + module: config.module, + import_export_assign_config, + rewrite_relative_import_extensions: rewrite_relative_import_extensions.into_bool(), + module_resolver, + inject_helpers, + remove_parentheses: fixer_enabled, + keep_import_attributes, + }) + } else { + None + }; + + let minify = if builtin_transforms_enabled { + minify + } else { + None + }; + let finalize_builtins = if builtin_transforms_enabled { + Some(BuiltinFinalizeOptions { + hygiene_config: if hygiene_enabled && !is_mangler_enabled { + Some(hygiene::Config { + keep_class_names, + ..hygiene::Config::hygiene_default() + }) + } else { + None + }, + run_fixer: fixer_enabled, + run_jest, + preserve_dropped_comments: preserve_all_comments, + }) + } else { + None + }; + let finalize = FinalizeStageOptions { + builtins: finalize_builtins, + preserve_comments, + preserve_annotations: preserve_annotations.into_bool(), + }; + Ok(ResolvedPipelineOptions { + plugin, + #[cfg(feature = "lint")] + lint, + transform, + minify, + finalize, + terminal, + }) + } +} + +fn resolve_minify_options( + minify: Option, + default_top_level: bool, + module: Option<&ModuleConfig>, +) -> Option { + let mut minify = minify.map(|mut options| { + let compress = options + .compress + .unwrap_as_option(|default| match default { + Some(true) => Some(Default::default()), + _ => None, + }) + .map(|mut compress| { + if compress.toplevel.is_none() { + compress.toplevel = Some(TerserTopLevelOptions::Bool(default_top_level)); + } + if matches!( + module, + None | Some(ModuleConfig::Es6(..) | ModuleConfig::NodeNext(..)) + ) { + compress.module = true; + } + compress + }) + .map(BoolOrDataConfig::from_obj) + .unwrap_or_else(|| BoolOrDataConfig::from_bool(false)); + let mangle = options + .mangle + .unwrap_as_option(|default| match default { + Some(true) => Some(Default::default()), + _ => None, + }) + .map(|mut mangle| { + if mangle.top_level.is_none() { + mangle.top_level = Some(default_top_level); + } + mangle + }) + .map(BoolOrDataConfig::from_obj) + .unwrap_or_else(|| BoolOrDataConfig::from_bool(false)); + if options.toplevel.is_none() { + options.toplevel = Some(default_top_level); + } + JsMinifyOptions { + compress, + mangle, + ..options + } + }); + + if minify.as_ref().is_some_and(|options| options.keep_fnames) { + minify = minify.map(|options| { + let compress = options + .compress + .unwrap_as_option(|default| match default { + Some(true) => Some(Default::default()), + _ => None, + }) + .map(|mut compress| { + compress.keep_fnames = true; + compress + }) + .map(BoolOrDataConfig::from_obj) + .unwrap_or_else(|| BoolOrDataConfig::from_bool(false)); + let mangle = options + .mangle + .unwrap_as_option(|default| match default { + Some(true) => Some(Default::default()), + _ => None, + }) + .map(|mut mangle| { + mangle.keep_fn_names = true; + mangle + }) + .map(BoolOrDataConfig::from_obj) + .unwrap_or_else(|| BoolOrDataConfig::from_bool(false)); + JsMinifyOptions { + compress, + mangle, + ..options + } + }); + } + + if minify + .as_ref() + .is_some_and(|options| options.keep_classnames) + { + minify = minify.map(|options| { + let compress = options + .compress + .unwrap_as_option(|default| match default { + Some(true) => Some(Default::default()), + _ => None, + }) + .map(|mut compress| { + compress.keep_classnames = true; + compress + }) + .map(BoolOrDataConfig::from_obj) + .unwrap_or_else(|| BoolOrDataConfig::from_bool(false)); + let mangle = options + .mangle + .unwrap_as_option(|default| match default { + Some(true) => Some(Default::default()), + _ => None, + }) + .map(|mut mangle| { + mangle.keep_class_names = true; + mangle + }) + .map(BoolOrDataConfig::from_obj) + .unwrap_or_else(|| BoolOrDataConfig::from_bool(false)); + JsMinifyOptions { + compress, + mangle, + ..options + } + }); + } + + minify +} diff --git a/crates/swc/src/pipeline/parse.rs b/crates/swc/src/pipeline/parse.rs new file mode 100644 index 000000000000..747f86373f12 --- /dev/null +++ b/crates/swc/src/pipeline/parse.rs @@ -0,0 +1,39 @@ +use anyhow::Error; + +use super::{api::CompileInput, options::PipelineConfig, state::CompilationUnit, Pipeline}; + +impl Pipeline<'_> { + /// Parses source input or accepts an already parsed program unchanged. + pub(super) fn parse( + &self, + input: CompileInput, + config: &PipelineConfig, + ) -> Result { + let CompileInput { + source_file, + program, + comments, + } = input; + let comments = comments.unwrap_or_default(); + let is_module = config.config.is_module.unwrap_or_default(); + + let (program, flow_strip_script_like_module) = match program { + Some(program) => (program, false), + None => self.compiler.parse_js_as_transform_input( + source_file.clone(), + self.handler, + config.context.target, + config.context.syntax, + is_module, + Some(&comments), + )?, + }; + + Ok(CompilationUnit { + source_file, + program, + comments, + flow_strip_script_like_module, + }) + } +} diff --git a/crates/swc/src/pipeline/plugin.rs b/crates/swc/src/pipeline/plugin.rs new file mode 100644 index 000000000000..a13dde7d216e --- /dev/null +++ b/crates/swc/src/pipeline/plugin.rs @@ -0,0 +1,157 @@ +#[cfg(feature = "plugin")] +use std::sync::Arc; + +#[cfg(all(feature = "plugin", not(target_arch = "wasm32")))] +use anyhow::Context; +use anyhow::Error; +#[cfg(feature = "plugin")] +use swc_atoms::Atom; +#[cfg(all(feature = "plugin", not(target_arch = "wasm32")))] +use swc_common::FileName; +use swc_ecma_ast::{Pass, Program}; + +use super::{ + state::{CompilationUnit, PipelineContextData}, + Pipeline, +}; +use crate::config::PluginConfig; + +/// The single checkpoint at which a runtime plugin observes the program. +#[derive(Clone, Copy, PartialEq, Eq)] +pub(super) enum PluginPlacement { + BeforeSyntax, + AfterSyntax, +} + +/// Runtime plugin configuration and its syntax-stage placement. +pub(super) struct PluginOptions { + pub(super) placement: PluginPlacement, + pub(super) plugins: Option>, + #[cfg(feature = "plugin")] + pub(super) plugin_env_vars: Option>, + #[cfg(feature = "plugin")] + pub(super) cache_root: Option, + #[cfg(feature = "plugin")] + pub(super) env_name: String, + #[cfg(feature = "plugin")] + pub(super) runtime: Option>, +} + +/// A runtime plugin scheduled at one syntax-stage checkpoint. +pub(super) struct RuntimePlugin { + pass: Option>, + placement: PluginPlacement, +} + +impl RuntimePlugin { + fn process(&mut self, program: &mut Program) { + if let Some(pass) = &mut self.pass { + pass.process(program); + } + } + + /// Runs a plugin configured to observe the program before lint execution + /// and lowering. + pub(super) fn process_before_syntax(&mut self, program: &mut Program) { + if self.placement == PluginPlacement::BeforeSyntax { + self.process(program); + } + } + + /// Runs a plugin configured to observe the program after syntax lowering. + pub(super) fn process_after_syntax(&mut self, program: &mut Program) { + if self.placement == PluginPlacement::AfterSyntax { + self.process(program); + } + } +} + +impl Pipeline<'_> { + pub(super) fn create_runtime_plugin( + &self, + _unit: &CompilationUnit, + context: &PipelineContextData, + options: PluginOptions, + ) -> Result { + #[cfg(all(feature = "plugin", not(target_arch = "wasm32")))] + { + use swc_common::plugin::metadata::TransformPluginMetadataContext; + + let PluginOptions { + placement, + plugins, + plugin_env_vars, + cache_root, + env_name, + runtime, + } = options; + let transform_filename = match &*_unit.source_file.name { + FileName::Real(path) => path.as_os_str().to_str().map(String::from), + FileName::Custom(filename) => Some(filename.to_owned()), + _ => None, + }; + let transform_metadata_context = Arc::new(TransformPluginMetadataContext::new( + transform_filename, + env_name, + None, + )); + let plugin_runtime = runtime.context("plugin runtime not configured")?; + + if let Some(plugins) = &plugins { + crate::plugin::compile_wasm_plugins( + cache_root.as_deref(), + plugins, + &*plugin_runtime, + ) + .context("Failed to compile wasm plugins")?; + } + + let pass = crate::plugin::plugins( + plugins, + plugin_env_vars, + transform_metadata_context, + Some(_unit.comments.clone()), + self.compiler.cm.clone(), + context.unresolved_mark, + plugin_runtime, + ); + + Ok(RuntimePlugin { + pass: Some(Box::new(pass)), + placement, + }) + } + + #[cfg(all(feature = "plugin", target_arch = "wasm32"))] + { + let placement = options.placement; + let _ = (context, options); + self.handler.warn( + "Currently @swc/wasm does not support plugins, plugin transform will be skipped. \ + Refer https://github.com/swc-project/swc/issues/3934 for the details.", + ); + + return Ok(RuntimePlugin { + pass: None, + placement, + }); + } + + #[cfg(not(feature = "plugin"))] + { + let _ = context; + let placement = options.placement; + if options.plugins.is_some() { + self.handler.warn( + "Plugin is not supported with current @swc/core. Plugin transform will be \ + skipped.", + ); + } + + Ok(RuntimePlugin { + pass: None, + placement, + }) + } + } +} diff --git a/crates/swc/src/pipeline/preparation.rs b/crates/swc/src/pipeline/preparation.rs new file mode 100644 index 000000000000..e79814999ad9 --- /dev/null +++ b/crates/swc/src/pipeline/preparation.rs @@ -0,0 +1,113 @@ +#[cfg(feature = "isolated-dts")] +use std::{cell::RefCell, rc::Rc}; + +use anyhow::Error; +#[cfg(feature = "isolated-dts")] +use swc_common::comments::SingleThreadedComments; +use swc_common::{comments::Comments, Spanned}; +#[cfg(feature = "isolated-dts")] +use swc_ecma_codegen::to_code_with_comments; +use swc_ecma_parser::Syntax; +#[cfg(all(feature = "isolated-dts", feature = "module"))] +use swc_ecma_transforms_module::rewriter::import_rewriter; +use swc_ecma_visit::VisitWith; +#[cfg(feature = "isolated-dts")] +use swc_typescript::fast_dts::FastDts; + +#[cfg(all(feature = "isolated-dts", feature = "module"))] +use super::state::ModuleResolver; +use super::{ + state::{CompilationUnit, PipelineContextData}, + Pipeline, +}; +use crate::{codegen::CodegenMetadata, config::InputSourceMap}; + +/// Configuration for emit-only work after resolver hooks and before runtime +/// plugins and built-in transforms. +pub(super) struct PreparationOptions { + pub(super) input_source_map: Option, + pub(super) emit_isolated_dts: bool, + #[cfg(all(feature = "isolated-dts", feature = "module"))] + pub(super) module_resolver: ModuleResolver, +} + +impl Pipeline<'_> { + /// Captures source-map metadata before runtime plugins and built-ins run. + pub(super) fn prepare_codegen_metadata( + &self, + unit: &CompilationUnit, + options: &PreparationOptions, + ) -> Result { + let Some(input_source_map) = &options.input_source_map else { + return Ok(CodegenMetadata::default()); + }; + let trailing_comments = unit + .comments + .get_trailing(unit.program.span_hi()) + .unwrap_or_default(); + let original_source_map = self.compiler.get_orig_src_map( + &unit.source_file, + input_source_map, + &trailing_comments, + false, + )?; + let mut collector = swc_compiler_base::IdentCollector { + names: Default::default(), + }; + unit.program.visit_with(&mut collector); + + Ok(CodegenMetadata { + source_map_names: collector.names, + original_source_map, + }) + } + + /// Emits isolated declarations before runtime plugins and built-ins run. + pub(super) fn emit_isolated_declarations( + &self, + _unit: &CompilationUnit, + context: &PipelineContextData, + options: &PreparationOptions, + ) { + let is_typescript_syntax = matches!(context.syntax, Syntax::Typescript(..)); + + if options.emit_isolated_dts && !is_typescript_syntax { + self.handler.warn( + "jsc.experimental.emitIsolatedDts is enabled but the syntax is not TypeScript", + ); + } + + #[cfg(feature = "isolated-dts")] + if is_typescript_syntax && options.emit_isolated_dts { + let (leading, trailing) = _unit.comments.borrow_all(); + let leading = Rc::new(RefCell::new(leading.clone())); + let trailing = Rc::new(RefCell::new(trailing.clone())); + let comments = SingleThreadedComments::from_leading_and_trailing(leading, trailing); + + let mut checker = FastDts::new( + _unit.source_file.name.clone(), + context.unresolved_mark, + Default::default(), + ); + let mut program = _unit.program.clone(); + + #[cfg(feature = "module")] + if let Some((base, resolver)) = &options.module_resolver { + program.mutate(import_rewriter(base.clone(), resolver.clone())); + } + + let issues = checker.transform(&mut program); + for issue in issues { + self.handler + .struct_span_err(issue.range.span, &issue.message) + .emit(); + } + + let dts_code = to_code_with_comments(Some(&comments), &program); + swc_transform_common::output::experimental_emit( + "__swc_isolated_declarations__".into(), + dts_code, + ); + } + } +} diff --git a/crates/swc/src/pipeline/resolve.rs b/crates/swc/src/pipeline/resolve.rs new file mode 100644 index 000000000000..07011d9b9413 --- /dev/null +++ b/crates/swc/src/pipeline/resolve.rs @@ -0,0 +1,86 @@ +#[cfg(feature = "react-compiler")] +use swc_common::Spanned; +use swc_ecma_transforms::resolver; +use swc_ecma_visit::VisitMutWith; + +use super::{options::PipelineConfig, state::CompilationUnit, Pipeline}; +#[cfg(feature = "react-compiler")] +use crate::config::{emit_react_compiler_diagnostics, react_compiler_options}; + +impl Pipeline<'_> { + /// Applies React Compiler before SWC assigns resolver contexts. + pub(super) fn run_react_compiler(&self, unit: &mut CompilationUnit, config: &PipelineConfig) { + #[cfg(not(feature = "react-compiler"))] + let _ = unit; + + #[cfg(feature = "react-compiler")] + if let Some(options) = react_compiler_options( + config + .config + .jsc + .transform + .as_ref() + .map(|transform| transform.react_compiler.clone()) + .unwrap_or_default(), + &unit.source_file.name, + ) { + let source_file = if unit.program.span().is_dummy() { + self.compiler.cm.get_source_file(&unit.source_file.name) + } else { + self.compiler + .cm + .try_lookup_byte_offset(unit.program.span().lo) + .ok() + .map(|source| source.sf) + }; + + if let Some(source_file) = source_file { + let source_type = swc_ecma_react_compiler::SourceType::from_program(&unit.program) + .with_typescript(config.context.syntax.typescript()); + let result = swc_ecma_react_compiler::transform( + &unit.program, + source_type, + &source_file.src, + Some(&unit.comments), + options, + ); + emit_react_compiler_diagnostics(self.handler, &result.diagnostics); + + if let Some(program) = result.program { + unit.program = program; + } + } else { + self.handler + .struct_warn("React Compiler is enabled, but the source text is unavailable") + .emit(); + } + } + + #[cfg(not(feature = "react-compiler"))] + if config + .config + .jsc + .transform + .as_ref() + .is_some_and(|transform| { + transform.react_compiler.is_true() || transform.react_compiler.is_obj() + }) + { + self.handler + .struct_warn( + "React Compiler is configured, but swc was built without the `react-compiler` \ + feature", + ) + .emit(); + } + } + + /// Assigns the initial syntax contexts consumed by later stages. + pub(super) fn run_resolver(&self, unit: &mut CompilationUnit, config: &PipelineConfig) { + unit.program.visit_mut_with(&mut resolver( + config.context.unresolved_mark, + config.context.top_level_mark, + config.context.syntax.typescript(), + )); + } +} diff --git a/crates/swc/src/pipeline/state.rs b/crates/swc/src/pipeline/state.rs new file mode 100644 index 000000000000..6a0ddf98939d --- /dev/null +++ b/crates/swc/src/pipeline/state.rs @@ -0,0 +1,32 @@ +use std::sync::Arc; + +#[cfg(feature = "module")] +use swc_common::FileName; +use swc_common::{comments::SingleThreadedComments, Mark, SourceFile}; +use swc_ecma_ast::{EsVersion, Program}; +use swc_ecma_parser::Syntax; +#[cfg(feature = "module")] +use swc_ecma_transforms_module::path::ImportResolver; + +/// Context values exposed at each hook boundary. +#[derive(Clone, Copy)] +pub(super) struct PipelineContextData { + pub(super) syntax: Syntax, + pub(super) target: EsVersion, + pub(super) unresolved_mark: Mark, + pub(super) top_level_mark: Mark, +} + +#[cfg(feature = "module")] +pub(super) type ModuleResolver = Option<(FileName, Arc)>; + +#[cfg(not(feature = "module"))] +pub(super) type ModuleResolver = (); + +/// The program and associated source state for one compilation. +pub(super) struct CompilationUnit { + pub(super) source_file: Arc, + pub(super) program: Program, + pub(super) comments: SingleThreadedComments, + pub(super) flow_strip_script_like_module: bool, +} diff --git a/crates/swc/src/pipeline/terminal.rs b/crates/swc/src/pipeline/terminal.rs new file mode 100644 index 000000000000..aee1909f1c60 --- /dev/null +++ b/crates/swc/src/pipeline/terminal.rs @@ -0,0 +1,127 @@ +use anyhow::Error; +use rustc_hash::FxHashMap; +use swc_ecma_transforms::helpers::HelperData; + +use super::{ + preparation::PreparationOptions, + state::{CompilationUnit, PipelineContextData}, + Pipeline, TransformedProgram, +}; +use crate::{ + codegen::{CodegenInput, CodegenMetadata, CodegenOptions}, + Compiler, +}; + +/// Defines the output boundary selected before pipeline execution. +pub(super) trait PipelineTerminal { + type State; + type Output; + + fn resolve_state(&self, resolve: F) -> Self::State + where + F: FnOnce() -> CodegenTerminalState; + + fn prepare( + &self, + _pipeline: &Pipeline<'_>, + _unit: &CompilationUnit, + _context: &PipelineContextData, + _state: &mut Self::State, + ) -> Result<(), Error> { + Ok(()) + } + + fn finish( + self, + compiler: &Compiler, + unit: CompilationUnit, + context: PipelineContextData, + state: Self::State, + helper_data: HelperData, + transform_output: FxHashMap, + ) -> Self::Output; +} + +/// Emit configuration and metadata for the code-generation terminal. +pub(super) struct CodegenTerminalState { + pub(super) preparation: PreparationOptions, + pub(super) codegen: CodegenOptions, + pub(super) metadata: CodegenMetadata, +} + +pub(super) struct ProgramTerminal; + +impl PipelineTerminal for ProgramTerminal { + type Output = TransformedProgram; + type State = (); + + fn resolve_state(&self, _resolve: F) + where + F: FnOnce() -> CodegenTerminalState, + { + } + + fn finish( + self, + _compiler: &Compiler, + unit: CompilationUnit, + _context: PipelineContextData, + _state: (), + helper_data: HelperData, + _transform_output: FxHashMap, + ) -> TransformedProgram { + TransformedProgram::new(unit.program, unit.comments, helper_data) + } +} + +pub(super) struct CodegenTerminal; + +impl PipelineTerminal for CodegenTerminal { + type Output = CodegenInput; + type State = CodegenTerminalState; + + fn resolve_state(&self, resolve: F) -> CodegenTerminalState + where + F: FnOnce() -> CodegenTerminalState, + { + resolve() + } + + fn prepare( + &self, + pipeline: &Pipeline<'_>, + unit: &CompilationUnit, + context: &PipelineContextData, + state: &mut CodegenTerminalState, + ) -> Result<(), Error> { + state.metadata = pipeline.prepare_codegen_metadata(unit, &state.preparation)?; + pipeline.emit_isolated_declarations(unit, context, &state.preparation); + Ok(()) + } + + fn finish( + self, + compiler: &Compiler, + unit: CompilationUnit, + context: PipelineContextData, + state: CodegenTerminalState, + _helper_data: HelperData, + transform_output: FxHashMap, + ) -> CodegenInput { + let CodegenTerminalState { + codegen: options, + metadata, + .. + } = state; + + CodegenInput { + source_map: compiler.cm.clone(), + program: unit.program, + comments: unit.comments, + metadata, + transform_output, + target: context.target, + options, + } + } +} diff --git a/crates/swc/src/pipeline/transform.rs b/crates/swc/src/pipeline/transform.rs new file mode 100644 index 000000000000..3c937fa70949 --- /dev/null +++ b/crates/swc/src/pipeline/transform.rs @@ -0,0 +1,402 @@ +use std::sync::Arc; + +use swc_common::comments::Comments; +use swc_ecma_preset_env::{Caniuse, Feature}; +use swc_ecma_transforms::{ + fixer::paren_remover, + helpers, + optimization::{const_modules, json_parse, simplifier}, + proposals::{ + decorators, explicit_resource_management::explicit_resource_management, + export_default_from, import_attributes, + }, + react::{self, default_pragma, default_pragma_frag}, + typescript, +}; +#[cfg(feature = "module")] +use swc_ecma_transforms_module::{self as modules, rewriter::import_rewriter}; +use swc_ecma_transforms_optimization::simplify::{ + dce::Config as DceConfig, Config as SimplifyConfig, +}; + +use super::{ + state::{CompilationUnit, ModuleResolver, PipelineContextData}, + Pipeline, +}; +use crate::config::{DecoratorVersion, ModuleConfig, SimplifyOption, TransformConfig}; + +/// Configuration for the built-in syntax and lowering stages. +pub(super) struct BuiltinTransformOptions { + pub(super) assumptions: swc_ecma_transforms::Assumptions, + pub(super) loose: bool, + pub(super) transform: TransformConfig, + pub(super) env: Option, + pub(super) feature_config: Option>, + pub(super) module: Option, + pub(super) import_export_assign_config: typescript::TsImportExportAssignConfig, + pub(super) rewrite_relative_import_extensions: bool, + pub(super) module_resolver: ModuleResolver, + pub(super) inject_helpers: bool, + pub(super) remove_parentheses: bool, + pub(super) keep_import_attributes: bool, +} + +impl Pipeline<'_> { + /// Runs the early built-in syntax transforms. + pub(super) fn transform_syntax( + &self, + unit: &mut CompilationUnit, + context: &PipelineContextData, + options: Option<&mut BuiltinTransformOptions>, + ) { + let Some(options) = options else { + return; + }; + + self.run_decorator_transform(unit, context, options); + + if context.syntax.explicit_resource_management() { + unit.program.mutate(explicit_resource_management()); + } + if !options.keep_import_attributes { + unit.program.mutate(import_attributes()); + } + + self.run_typescript_transform(unit, context, options); + } + + /// Runs the remaining built-in transforms after the `after_typescript` + /// hook boundary. + pub(super) fn transform_after_typescript( + &self, + unit: &mut CompilationUnit, + context: &PipelineContextData, + stage: Option, + ) { + let Some(mut options) = stage else { + return; + }; + + self.run_react_transform(unit, context, &mut options); + self.run_optimizer_transforms(unit, context, &mut options); + self.run_compat_transform(unit, context, &mut options); + #[cfg(feature = "module")] + self.run_import_transforms(unit, &options); + if options.inject_helpers { + unit.program + .mutate(helpers::inject_helpers(context.unresolved_mark)); + } + self.run_module_transform(unit, context, &mut options); + } + + fn run_decorator_transform( + &self, + unit: &mut CompilationUnit, + context: &PipelineContextData, + options: &BuiltinTransformOptions, + ) { + if !context.syntax.decorators() { + return; + } + + match options.transform.decorator_version.unwrap_or_default() { + DecoratorVersion::V202112 => { + unit.program.mutate(decorators(decorators::Config { + legacy: options.transform.legacy_decorator.into_bool(), + emit_metadata: options.transform.decorator_metadata.into_bool(), + use_define_for_class_fields: !options.assumptions.set_public_class_fields, + })); + } + DecoratorVersion::V202203 => unit + .program + .mutate(swc_ecma_transforms::proposals::decorator_2022_03::decorator_2022_03()), + DecoratorVersion::V202311 => unit + .program + .mutate(swc_ecma_transforms::proposals::decorator_2023_11::decorator_2023_11()), + } + } + + fn run_typescript_transform( + &self, + unit: &mut CompilationUnit, + context: &PipelineContextData, + options: &BuiltinTransformOptions, + ) { + if !context.syntax.typescript() { + return; + } + + let jsx_enabled = context.syntax.jsx() + && options.transform.react.runtime != Some(react::Runtime::Preserve); + let native_class_properties = !options.assumptions.set_public_class_fields + && options.feature_config.as_ref().map_or_else( + || context.target.caniuse(Feature::ClassProperties), + |env| env.caniuse(Feature::ClassProperties), + ); + let ts_config = typescript::Config { + import_export_assign_config: options.import_export_assign_config, + verbatim_module_syntax: options.transform.verbatim_module_syntax.into_bool(), + native_class_properties, + ts_enum_is_mutable: options.transform.ts_enum_is_mutable.into_bool(), + flow_syntax: context.syntax.flow(), + ..Default::default() + }; + + if jsx_enabled { + unit.program.mutate(typescript::tsx( + self.compiler.cm.clone(), + ts_config, + typescript::TsxConfig { + pragma: Some( + options + .transform + .react + .pragma + .clone() + .unwrap_or_else(default_pragma), + ), + pragma_frag: Some( + options + .transform + .react + .pragma_frag + .clone() + .unwrap_or_else(default_pragma_frag), + ), + }, + Some(&unit.comments as &dyn Comments), + context.unresolved_mark, + context.top_level_mark, + )); + } else { + unit.program.mutate(typescript::typescript( + ts_config, + context.unresolved_mark, + context.top_level_mark, + )); + } + } + + fn run_react_transform( + &self, + unit: &mut CompilationUnit, + context: &PipelineContextData, + options: &mut BuiltinTransformOptions, + ) { + if !context.syntax.jsx() + || options.transform.react.runtime == Some(react::Runtime::Preserve) + { + return; + } + + unit.program.mutate(react::react( + self.compiler.cm.clone(), + Some(&unit.comments as &dyn Comments), + std::mem::take(&mut options.transform.react), + context.top_level_mark, + context.unresolved_mark, + )); + } + + fn run_optimizer_transforms( + &self, + unit: &mut CompilationUnit, + context: &PipelineContextData, + options: &mut BuiltinTransformOptions, + ) { + if let Some(config) = options.transform.const_modules.take() { + unit.program + .mutate(const_modules(self.compiler.cm.clone(), config.globals)); + } + + let Some(mut optimizer) = options.transform.optimizer.take() else { + if context.syntax.export_default_from() { + unit.program.mutate(export_default_from()); + } + return; + }; + + if let Some(globals) = optimizer.globals.take() { + unit.program + .mutate(globals.build(&self.compiler.cm, self.handler)); + } + if context.syntax.export_default_from() { + unit.program.mutate(export_default_from()); + } + if let Some(simplify) = optimizer.simplify.take() { + match simplify { + SimplifyOption::Bool(true) => unit + .program + .mutate(simplifier(context.unresolved_mark, Default::default())), + SimplifyOption::Bool(false) => {} + SimplifyOption::Json(config) => unit.program.mutate(simplifier( + context.unresolved_mark, + SimplifyConfig { + dce: DceConfig { + preserve_imports_with_side_effects: config + .preserve_imports_with_side_effects, + ..Default::default() + }, + ..Default::default() + }, + )), + } + } + if let Some(config) = optimizer.jsonify { + unit.program.mutate(json_parse(config.min_cost)); + } + } + + fn run_compat_transform( + &self, + unit: &mut CompilationUnit, + context: &PipelineContextData, + options: &mut BuiltinTransformOptions, + ) { + if options.remove_parentheses { + unit.program + .mutate(paren_remover(Some(&unit.comments as &dyn Comments))); + } + + if let Some(env) = options.env.take() { + unit.program.mutate(swc_ecma_preset_env::transform_from_env( + context.unresolved_mark, + Some(&unit.comments as &dyn Comments), + env, + options.assumptions, + )); + } else { + unit.program + .mutate(swc_ecma_preset_env::transform_from_es_version( + context.unresolved_mark, + Some(&unit.comments as &dyn Comments), + context.target, + options.assumptions, + options.loose, + )); + } + } + + #[cfg(feature = "module")] + fn run_import_transforms(&self, unit: &mut CompilationUnit, options: &BuiltinTransformOptions) { + let (need_analyzer, import_interop, ignore_dynamic) = match &options.module { + Some(ModuleConfig::CommonJs(config)) => { + (true, config.import_interop(), config.ignore_dynamic) + } + Some(ModuleConfig::Amd(config)) => ( + true, + config.config.import_interop(), + config.config.ignore_dynamic, + ), + Some(ModuleConfig::Umd(config)) => ( + true, + config.config.import_interop(), + config.config.ignore_dynamic, + ), + Some(ModuleConfig::SystemJs(_)) + | Some(ModuleConfig::Es6(_)) + | Some(ModuleConfig::NodeNext(_)) + | None => (false, true.into(), true), + }; + + if need_analyzer { + unit.program + .mutate(modules::import_analysis::import_analyzer( + import_interop, + ignore_dynamic, + )); + } + + if matches!( + options.module, + None | Some(ModuleConfig::Es6(_)) | Some(ModuleConfig::NodeNext(_)) + ) { + if let Some((base, resolver)) = options.module_resolver.clone() { + unit.program.mutate(import_rewriter(base, resolver)); + } + } + + if options.rewrite_relative_import_extensions { + let jsx_preserve = options.transform.react.runtime == Some(react::Runtime::Preserve); + unit.program + .mutate(modules::rewriter::typescript_import_rewriter(jsx_preserve)); + } + } + + #[cfg(feature = "module")] + fn run_module_transform( + &self, + unit: &mut CompilationUnit, + context: &PipelineContextData, + options: &mut BuiltinTransformOptions, + ) { + let target = context.target; + let feature_config = options.feature_config.as_ref(); + let caniuse = |feature| { + feature_config.map_or_else(|| target.caniuse(feature), |env| env.caniuse(feature)) + }; + let resolver = match options.module_resolver.clone() { + Some((base, resolver)) => modules::path::Resolver::Real { base, resolver }, + None => modules::path::Resolver::Default, + }; + let support_block_scoping = caniuse(Feature::BlockScoping); + let support_arrow = caniuse(Feature::ArrowFunctions); + + match options.module.take() { + Some(ModuleConfig::CommonJs(config)) => { + unit.program.mutate(modules::common_js::common_js( + resolver, + context.unresolved_mark, + config, + modules::common_js::FeatureFlag { + support_block_scoping, + support_arrow, + }, + )); + } + Some(ModuleConfig::Umd(config)) => { + unit.program.mutate(modules::umd::umd( + self.compiler.cm.clone(), + resolver, + context.unresolved_mark, + config, + modules::umd::FeatureFlag { + support_block_scoping, + }, + )); + } + Some(ModuleConfig::Amd(config)) => { + unit.program.mutate(modules::amd::amd( + resolver, + context.unresolved_mark, + config, + modules::amd::FeatureFlag { + support_block_scoping, + support_arrow, + }, + Some(&unit.comments as &dyn Comments), + )); + } + Some(ModuleConfig::SystemJs(config)) => { + unit.program.mutate(modules::system_js::system_js( + resolver, + context.unresolved_mark, + config, + )); + } + Some(ModuleConfig::Es6(_)) | Some(ModuleConfig::NodeNext(_)) | None => {} + } + } + + #[cfg(not(feature = "module"))] + fn run_module_transform( + &self, + _unit: &mut CompilationUnit, + _context: &PipelineContextData, + options: &mut BuiltinTransformOptions, + ) { + let _ = options.rewrite_relative_import_extensions; + let _ = &options.module_resolver; + let _ = options.module.take(); + } +} diff --git a/crates/swc/src/resolver.rs b/crates/swc/src/resolver.rs new file mode 100644 index 000000000000..fcaaeae85ce9 --- /dev/null +++ b/crates/swc/src/resolver.rs @@ -0,0 +1,39 @@ +//! Public module resolver types and constructors. + +use std::path::PathBuf; + +use rustc_hash::FxHashMap; +use swc_ecma_loader::{ + resolvers::{lru::CachingResolver, node::NodeModulesResolver, tsc::TsConfigResolver}, + TargetEnv, +}; + +use crate::config::CompiledPaths; + +pub type NodeResolver = CachingResolver; + +pub fn paths_resolver( + target_env: TargetEnv, + alias: FxHashMap, + base_url: PathBuf, + paths: CompiledPaths, + preserve_symlinks: bool, +) -> CachingResolver> { + let r = TsConfigResolver::new( + NodeModulesResolver::without_node_modules(target_env, alias, preserve_symlinks), + base_url, + paths, + ); + CachingResolver::new(40, r) +} + +pub fn environment_resolver( + target_env: TargetEnv, + alias: FxHashMap, + preserve_symlinks: bool, +) -> NodeResolver { + CachingResolver::new( + 40, + NodeModulesResolver::new(target_env, alias, preserve_symlinks), + ) +} diff --git a/crates/swc/tests/legacy.rs b/crates/swc/tests/legacy.rs new file mode 100644 index 000000000000..474e540c738c --- /dev/null +++ b/crates/swc/tests/legacy.rs @@ -0,0 +1,233 @@ +use std::{cell::RefCell, path::PathBuf, rc::Rc}; + +use swc::{ + config::{Config, JscConfig, ModuleConfig, Options}, + Compiler, +}; +use swc_common::{comments::SingleThreadedComments, SyntaxContext}; +use swc_ecma_ast::{ + fn_pass, noop_pass, ArrowExpr, EsVersion, Ident, JSXElement, Program, TsAsExpr, TsTypeAnn, +}; +#[cfg(feature = "flow")] +use swc_ecma_parser::FlowSyntax; +use swc_ecma_parser::{Syntax, TsSyntax}; +use swc_ecma_visit::{Visit, VisitWith}; + +#[derive(Default)] +struct ProgramShape { + has_typescript: bool, + has_jsx: bool, + has_arrow: bool, + render_ctxt: Option, +} + +impl Visit for ProgramShape { + fn visit_ts_as_expr(&mut self, node: &TsAsExpr) { + self.has_typescript = true; + node.visit_children_with(self); + } + + fn visit_ts_type_ann(&mut self, node: &TsTypeAnn) { + self.has_typescript = true; + node.visit_children_with(self); + } + + fn visit_jsx_element(&mut self, node: &JSXElement) { + self.has_jsx = true; + node.visit_children_with(self); + } + + fn visit_arrow_expr(&mut self, node: &ArrowExpr) { + self.has_arrow = true; + node.visit_children_with(self); + } + + fn visit_ident(&mut self, node: &Ident) { + if node.sym == *"render" { + self.render_ctxt = Some(node.ctxt); + } + } +} + +fn program_shape(program: &Program) -> ProgramShape { + let mut shape = ProgramShape::default(); + program.visit_with(&mut shape); + shape +} + +fn pipeline_options() -> Options { + Options { + config: Config { + jsc: JscConfig { + syntax: Some(Syntax::Typescript(TsSyntax { + tsx: true, + ..Default::default() + })), + target: Some(EsVersion::Es5), + ..Default::default() + }, + module: Some(ModuleConfig::CommonJs(Default::default())), + ..Default::default() + }, + swcrc: false, + ..Default::default() + } +} + +fn assert_factory_boundary(program: &Program) { + let shape = program_shape(program); + assert!(shape.has_typescript); + assert!(shape.has_jsx); + assert_ne!(shape.render_ctxt, Some(SyntaxContext::empty())); +} + +#[testing::fixture("tests/rust-api/pipeline/stages/input.tsx")] +fn legacy_custom_passes_keep_their_public_order(input: PathBuf) { + testing::run_test2(false, |cm, handler| { + let compiler = Compiler::new(cm.clone()); + let source_file = cm.load_file(&input).expect("failed to load fixture"); + let calls = Rc::new(RefCell::new(Vec::new())); + + compiler + .process_js_with_custom_pass( + source_file, + None, + &handler, + &pipeline_options(), + SingleThreadedComments::default(), + { + let calls = calls.clone(); + move |program| { + assert_factory_boundary(program); + calls.borrow_mut().push("before_factory"); + fn_pass(move |program| { + let shape = program_shape(program); + assert!(!shape.has_typescript); + assert!(shape.has_jsx); + assert!(shape.has_arrow); + calls.borrow_mut().push("before_pass"); + }) + } + }, + { + let calls = calls.clone(); + move |program| { + assert_factory_boundary(program); + calls.borrow_mut().push("after_factory"); + fn_pass(move |program| { + let shape = program_shape(program); + assert!(!shape.has_jsx); + assert!(!shape.has_arrow); + calls.borrow_mut().push("after_pass"); + }) + } + }, + ) + .expect("failed to compile with legacy custom passes"); + + assert_eq!( + calls.borrow().as_slice(), + [ + "before_factory", + "after_factory", + "before_pass", + "after_pass", + ] + ); + Ok(()) + }) + .unwrap(); +} + +/// Locks the public legacy split between `BuiltInput::program` and +/// `BuiltInput::pass` without depending on the pass graph's concrete shape. +#[testing::fixture("tests/rust-api/pipeline/stages/input.tsx")] +fn build_as_input_keeps_its_delayed_pass_boundary(input: PathBuf) { + testing::run_test2(false, |cm, handler| { + let compiler = Compiler::new(cm.clone()); + let source_file = cm.load_file(&input).expect("failed to load fixture"); + let comments = SingleThreadedComments::default(); + let options = pipeline_options(); + + let built = options + .build_as_input( + &cm, + &source_file.name, + |syntax, target, is_module| { + compiler + .parse_js( + source_file.clone(), + &handler, + target, + syntax, + is_module, + Some(&comments), + ) + .map(|program| (program, false)) + }, + None, + None, + None, + None, + &handler, + None, + Some(&comments), + |program| { + assert_factory_boundary(program); + noop_pass() + }, + ) + .expect("failed to build legacy input"); + + assert_factory_boundary(&built.program); + let external_helpers = built.external_helpers; + let program = compiler.run_transform(&handler, external_helpers, || { + built.program.apply(built.pass) + }); + let shape = program_shape(&program); + assert!(!shape.has_typescript); + assert!(!shape.has_jsx); + Ok(()) + }) + .unwrap(); +} + +#[cfg(feature = "flow")] +#[testing::fixture("tests/rust-api/pipeline/flow/input.js")] +fn legacy_custom_pass_api_transforms_flow(input: PathBuf) { + testing::run_test2(false, |cm, handler| { + let compiler = Compiler::new(cm.clone()); + let source_file = cm.load_file(&input).expect("failed to load fixture"); + let options = Options { + config: Config { + jsc: JscConfig { + syntax: Some(Syntax::Flow(FlowSyntax::default())), + ..Default::default() + }, + is_module: Some(swc::config::IsModule::Unknown), + minify: true.into(), + ..Default::default() + }, + swcrc: false, + ..Default::default() + }; + + let output = compiler + .process_js_with_custom_pass( + source_file, + None, + &handler, + &options, + SingleThreadedComments::default(), + |_| noop_pass(), + |_| noop_pass(), + ) + .expect("failed to compile Flow through the legacy API"); + + assert!(output.code.contains("unused")); + assert!(!output.code.contains("@flow")); + assert!(!output.code.contains("import type")); + Ok(()) + }) + .unwrap(); +} diff --git a/crates/swc/tests/pipeline.rs b/crates/swc/tests/pipeline.rs new file mode 100644 index 000000000000..f3ae6603238e --- /dev/null +++ b/crates/swc/tests/pipeline.rs @@ -0,0 +1,570 @@ +use std::{ + cell::{Cell, RefCell}, + io::{self, Write}, + path::PathBuf, + rc::Rc, + sync::{Arc, Mutex}, +}; + +use anyhow::{bail, Error}; +#[cfg(feature = "flow")] +use swc::{config::JsMinifyOptions, BoolOrDataConfig}; +use swc::{ + config::{ + Config, InputSourceMap, IsModule, JscConfig, JscExperimental, ModuleConfig, Options, + SourceMapsConfig, + }, + CompileInput, Compiler, PipelineContext, PipelineHooks, +}; +use swc_common::{ + comments::SingleThreadedComments, errors::Handler, FileName, Mark, SyntaxContext, +}; +use swc_ecma_ast::*; +#[cfg(feature = "flow")] +use swc_ecma_parser::FlowSyntax; +use swc_ecma_parser::{Syntax, TsSyntax}; +use swc_ecma_transforms::helpers::{inject_helpers, Helpers, HELPERS}; +use swc_ecma_visit::{Visit, VisitMutWith, VisitWith}; + +#[derive(Default)] +struct ProgramShape { + has_typescript: bool, + has_jsx: bool, + render_ctxt: Option, +} + +impl Visit for ProgramShape { + fn visit_ts_as_expr(&mut self, node: &TsAsExpr) { + self.has_typescript = true; + node.visit_children_with(self); + } + + fn visit_ts_type_ann(&mut self, node: &TsTypeAnn) { + self.has_typescript = true; + node.visit_children_with(self); + } + + fn visit_jsx_element(&mut self, node: &JSXElement) { + self.has_jsx = true; + node.visit_children_with(self); + } + + fn visit_ident(&mut self, node: &Ident) { + if node.sym == *"render" { + self.render_ctxt = Some(node.ctxt); + } + } +} + +fn program_shape(program: &Program) -> ProgramShape { + let mut shape = ProgramShape::default(); + program.visit_with(&mut shape); + shape +} + +#[derive(Default)] +struct HelperDeclarations { + has_class_call_check: bool, +} + +impl Visit for HelperDeclarations { + fn visit_fn_decl(&mut self, node: &FnDecl) { + self.has_class_call_check |= node.ident.sym == *"_class_call_check"; + node.visit_children_with(self); + } +} + +fn has_class_call_check_declaration(program: &Program) -> bool { + let mut declarations = HelperDeclarations::default(); + program.visit_with(&mut declarations); + declarations.has_class_call_check +} + +fn pipeline_options() -> Options { + Options { + config: Config { + jsc: JscConfig { + syntax: Some(Syntax::Typescript(TsSyntax { + tsx: true, + ..Default::default() + })), + target: Some(EsVersion::Es5), + preserve_all_comments: true.into(), + ..Default::default() + }, + module: Some(ModuleConfig::CommonJs(Default::default())), + ..Default::default() + }, + swcrc: false, + ..Default::default() + } +} + +#[cfg(feature = "flow")] +fn flow_pipeline_options(is_module: IsModule) -> Options { + Options { + config: Config { + jsc: JscConfig { + syntax: Some(Syntax::Flow(FlowSyntax::default())), + minify: Some(JsMinifyOptions { + compress: BoolOrDataConfig::from_bool(true), + mangle: BoolOrDataConfig::from_bool(false), + ..Default::default() + }), + ..Default::default() + }, + is_module: Some(is_module), + module: Some(ModuleConfig::CommonJs(Default::default())), + ..Default::default() + }, + swcrc: false, + ..Default::default() + } +} + +#[derive(Clone, Default)] +struct DiagnosticBuffer(Arc>>); + +impl DiagnosticBuffer { + fn contents(&self) -> String { + String::from_utf8(self.0.lock().unwrap().clone()).unwrap() + } +} + +impl Write for DiagnosticBuffer { + fn write(&mut self, bytes: &[u8]) -> io::Result { + self.0.lock().unwrap().extend_from_slice(bytes); + Ok(bytes.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +struct StageHooks { + calls: Rc>>, +} + +impl PipelineHooks for StageHooks { + fn inspect_after_parse( + &mut self, + program: &Program, + _context: &PipelineContext<'_>, + ) -> Result<(), Error> { + let shape = program_shape(program); + assert!(shape.has_typescript); + assert_eq!(shape.render_ctxt, Some(SyntaxContext::empty())); + self.calls.borrow_mut().push("inspect_after_parse"); + Ok(()) + } + + fn mutate_after_parse( + &mut self, + _program: &mut Program, + _context: &PipelineContext<'_>, + ) -> Result<(), Error> { + self.calls.borrow_mut().push("mutate_after_parse"); + Ok(()) + } + + fn inspect_after_resolve( + &mut self, + program: &Program, + _context: &PipelineContext<'_>, + ) -> Result<(), Error> { + let shape = program_shape(program); + assert!(shape.has_typescript); + assert_ne!(shape.render_ctxt, Some(SyntaxContext::empty())); + self.calls.borrow_mut().push("inspect_after_resolve"); + Ok(()) + } + + fn mutate_after_resolve( + &mut self, + _program: &mut Program, + _context: &PipelineContext<'_>, + ) -> Result<(), Error> { + self.calls.borrow_mut().push("mutate_after_resolve"); + Ok(()) + } + + fn inspect_after_typescript( + &mut self, + program: &Program, + _context: &PipelineContext<'_>, + ) -> Result<(), Error> { + let shape = program_shape(program); + assert!(!shape.has_typescript); + assert!(shape.has_jsx); + self.calls.borrow_mut().push("inspect_after_typescript"); + Ok(()) + } + + fn mutate_after_typescript( + &mut self, + _program: &mut Program, + _context: &PipelineContext<'_>, + ) -> Result<(), Error> { + self.calls.borrow_mut().push("mutate_after_typescript"); + Ok(()) + } +} + +struct FailingHooks { + mutate_resolve_called: Rc>, + typescript_hook_called: Rc>, +} + +impl PipelineHooks for FailingHooks { + fn inspect_after_resolve( + &mut self, + _program: &Program, + _context: &PipelineContext<'_>, + ) -> Result<(), Error> { + bail!("inspect_after_resolve failed") + } + + fn mutate_after_resolve( + &mut self, + _program: &mut Program, + _context: &PipelineContext<'_>, + ) -> Result<(), Error> { + self.mutate_resolve_called.set(true); + Ok(()) + } + + fn inspect_after_typescript( + &mut self, + _program: &Program, + _context: &PipelineContext<'_>, + ) -> Result<(), Error> { + self.typescript_hook_called.set(true); + Ok(()) + } +} + +#[testing::fixture("tests/rust-api/pipeline/stages/input.tsx")] +fn compile_input_source_and_program_are_equivalent(input: PathBuf) { + testing::run_test2(false, |cm, handler| { + let compiler = Compiler::new(cm.clone()); + let source_file = cm.load_file(&input).expect("failed to load fixture"); + let options = pipeline_options(); + let comments = SingleThreadedComments::default(); + let program = compiler + .parse_js( + source_file.clone(), + &handler, + EsVersion::Es5, + Syntax::Typescript(TsSyntax { + tsx: true, + ..Default::default() + }), + IsModule::Unknown, + Some(&comments), + ) + .expect("failed to parse fixture"); + + let source_output = compiler + .compile( + &handler, + CompileInput::source(source_file.clone()), + &options, + ) + .codegen() + .expect("failed to compile source input"); + let program_output = compiler + .compile( + &handler, + CompileInput::program(source_file, program).with_comments(comments), + &options, + ) + .codegen() + .expect("failed to compile program input"); + + assert_eq!(source_output.code, program_output.code); + Ok(()) + }) + .unwrap(); +} + +#[testing::fixture("tests/rust-api/pipeline/stages/input.tsx")] +fn ast_terminal_returns_program_and_comments(input: PathBuf) { + testing::run_test2(false, |cm, handler| { + let compiler = Compiler::new(cm.clone()); + let source_file = cm.load_file(&input).expect("failed to load fixture"); + let transformed = compiler + .compile( + &handler, + CompileInput::source(source_file.clone()), + &pipeline_options(), + ) + .transform() + .expect("failed to transform fixture"); + + let shape = program_shape(transformed.program()); + assert!(!shape.has_typescript); + assert!(!shape.has_jsx); + let (leading, trailing) = transformed.comments().borrow_all(); + assert!(!leading.is_empty() || !trailing.is_empty()); + + let program = compiler + .compile( + &handler, + CompileInput::source(source_file), + &pipeline_options(), + ) + .into_program() + .expect("failed to return the transformed program"); + assert!(!program_shape(&program).has_typescript); + Ok(()) + }) + .unwrap(); +} + +#[testing::fixture("tests/rust-api/pipeline/helpers/input.js")] +fn ast_terminal_returns_helper_requirements(input: PathBuf) { + testing::run_test2(false, |cm, handler| { + let compiler = Compiler::new(cm.clone()); + let source_file = cm.load_file(&input).expect("failed to load fixture"); + let unresolved_mark = Mark::new(); + let options = Options { + config: Config { + jsc: JscConfig { + target: Some(EsVersion::Es5), + ..Default::default() + }, + ..Default::default() + }, + swcrc: false, + skip_helper_injection: true, + unresolved_mark: Some(unresolved_mark), + ..Default::default() + }; + + let transformed = compiler + .compile(&handler, CompileInput::source(source_file), &options) + .transform() + .expect("failed to transform fixture"); + let (mut program, _comments, helper_data) = transformed.into_parts(); + + assert!(!has_class_call_check_declaration(&program)); + HELPERS.set(&Helpers::from_data(helper_data), || { + program.visit_mut_with(&mut inject_helpers(unresolved_mark)); + }); + assert!(has_class_call_check_declaration(&program)); + Ok(()) + }) + .unwrap(); +} + +#[test] +fn ast_terminal_skips_input_source_map_loading() { + testing::run_test2(false, |cm, handler| { + let compiler = Compiler::new(cm.clone()); + let source_file = cm.new_source_file(FileName::Anon.into(), "const value = 1;"); + let options = Options { + config: Config { + input_source_map: Some(InputSourceMap::Str("not a source map".into())), + source_maps: Some(SourceMapsConfig::Bool(true)), + ..Default::default() + }, + swcrc: false, + ..Default::default() + }; + + compiler + .compile( + &handler, + CompileInput::source(source_file.clone()), + &options, + ) + .transform() + .expect("AST terminal should not load the input source map"); + compiler + .compile(&handler, CompileInput::source(source_file), &options) + .codegen() + .expect_err("codegen terminal should load the input source map"); + Ok(()) + }) + .unwrap(); +} + +#[test] +fn ast_terminal_skips_isolated_dts_preparation() { + testing::run_test2(false, |cm, _handler| { + let compiler = Compiler::new(cm.clone()); + let source_file = cm.new_source_file(FileName::Anon.into(), "const value = 1;"); + let options = Options { + config: Config { + jsc: JscConfig { + experimental: JscExperimental { + emit_isolated_dts: true.into(), + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }, + swcrc: false, + ..Default::default() + }; + let diagnostics = DiagnosticBuffer::default(); + let handler = Handler::with_emitter_writer(Box::new(diagnostics.clone()), Some(cm.clone())); + + compiler + .compile( + &handler, + CompileInput::source(source_file.clone()), + &options, + ) + .transform() + .expect("AST terminal should skip isolated-DTS preparation"); + assert!(diagnostics.contents().is_empty()); + + compiler + .compile(&handler, CompileInput::source(source_file), &options) + .codegen() + .expect("codegen terminal should succeed"); + assert!(diagnostics + .contents() + .contains("emitIsolatedDts is enabled but the syntax is not TypeScript")); + Ok(()) + }) + .unwrap(); +} + +#[test] +fn compile_request_is_lazy() { + testing::run_test2(false, |cm, handler| { + let compiler = Compiler::new(cm.clone()); + let source_file = cm.new_source_file(FileName::Anon.into(), "const = ;"); + let options = Options { + swcrc: false, + ..Default::default() + }; + let request = compiler.compile(&handler, CompileInput::source(source_file), &options); + + assert_eq!(handler.err_count(), 0); + assert!(request.transform().is_err()); + assert_eq!(handler.err_count(), 1); + Ok(()) + }) + .unwrap(); +} + +#[cfg(feature = "flow")] +#[testing::fixture("tests/rust-api/pipeline/flow/input.js")] +fn flow_type_only_source_preserves_script_semantics(input: PathBuf) { + testing::run_test2(false, |cm, handler| { + let compiler = Compiler::new(cm.clone()); + let source_file = cm.load_file(&input).expect("failed to load fixture"); + let output = compiler + .compile( + &handler, + CompileInput::source(source_file), + &flow_pipeline_options(IsModule::Unknown), + ) + .codegen() + .expect("failed to compile Flow fixture"); + + assert!(output.code.contains("unused")); + assert!(!output.code.contains("import type")); + Ok(()) + }) + .unwrap(); +} + +#[cfg(feature = "flow")] +#[testing::fixture("tests/rust-api/pipeline/flow-module-program/input.js")] +fn preparsed_program_variant_is_authoritative(input: PathBuf) { + testing::run_test2(false, |cm, handler| { + let compiler = Compiler::new(cm.clone()); + let source_file = cm.load_file(&input).expect("failed to load fixture"); + let comments = SingleThreadedComments::default(); + let program = compiler + .parse_js( + source_file.clone(), + &handler, + EsVersion::default(), + Syntax::Flow(FlowSyntax::default()), + IsModule::Unknown, + Some(&comments), + ) + .expect("failed to parse Flow fixture"); + assert!(matches!(program, Program::Module(..))); + + let output = compiler + .compile( + &handler, + CompileInput::program(source_file, program).with_comments(comments), + &flow_pipeline_options(IsModule::Bool(false)), + ) + .codegen() + .expect("a supplied Program should be authoritative"); + + assert!(output.code.contains("await load")); + assert_eq!(handler.err_count(), 0); + Ok(()) + }) + .unwrap(); +} + +#[testing::fixture("tests/rust-api/pipeline/stages/input.tsx")] +fn pipeline_hooks_run_at_public_boundaries(input: PathBuf) { + testing::run_test2(false, |cm, handler| { + let compiler = Compiler::new(cm.clone()); + let source_file = cm.load_file(&input).expect("failed to load fixture"); + let calls = Rc::new(RefCell::new(Vec::new())); + let options = pipeline_options(); + let request = compiler + .compile(&handler, CompileInput::source(source_file), &options) + .with_hooks(StageHooks { + calls: calls.clone(), + }); + + assert!(calls.borrow().is_empty()); + request.codegen().expect("failed to compile with hooks"); + assert_eq!( + calls.borrow().as_slice(), + [ + "inspect_after_parse", + "mutate_after_parse", + "inspect_after_resolve", + "mutate_after_resolve", + "inspect_after_typescript", + "mutate_after_typescript", + ] + ); + Ok(()) + }) + .unwrap(); +} + +#[testing::fixture("tests/rust-api/pipeline/stages/input.tsx")] +fn inspect_hook_errors_skip_mutation_and_later_hooks(input: PathBuf) { + testing::run_test2(false, |cm, handler| { + let compiler = Compiler::new(cm.clone()); + let source_file = cm.load_file(&input).expect("failed to load fixture"); + let mutate_resolve_called = Rc::new(Cell::new(false)); + let typescript_hook_called = Rc::new(Cell::new(false)); + let error = compiler + .compile( + &handler, + CompileInput::source(source_file), + &pipeline_options(), + ) + .with_hooks(FailingHooks { + mutate_resolve_called: mutate_resolve_called.clone(), + typescript_hook_called: typescript_hook_called.clone(), + }) + .codegen() + .expect_err("inspect hook should fail"); + + assert_eq!(error.to_string(), "inspect_after_resolve failed"); + assert!(!mutate_resolve_called.get()); + assert!(!typescript_hook_called.get()); + Ok(()) + }) + .unwrap(); +} diff --git a/crates/swc/tests/projects.rs b/crates/swc/tests/projects.rs index a844b5b0a980..827840b420b9 100644 --- a/crates/swc/tests/projects.rs +++ b/crates/swc/tests/projects.rs @@ -4,27 +4,25 @@ use std::{ path::{Path, PathBuf}, }; -use anyhow::Context; +use anyhow::{Context, Error}; use par_iter::prelude::*; use swc::{ config::{ Config, FileMatcher, JsMinifyOptions, JscConfig, ModuleConfig, Options, Paths, SourceMapsConfig, TransformConfig, }, - try_with_handler, BoolOrDataConfig, Compiler, TransformOutput, + try_with_handler, BoolOrDataConfig, CompileInput, Compiler, PipelineContext, PipelineHooks, + TransformOutput, }; use swc_common::{ - comments::{Comment, SingleThreadedComments}, - errors::{EmitterWriter, Handler, HANDLER}, + errors::{EmitterWriter, Handler}, sync::Lrc, - BytePos, FileName, Globals, SourceMap, GLOBALS, + FileName, Globals, SourceMap, GLOBALS, }; -use swc_compiler_base::PrintArgs; use swc_config::{file_pattern::FilePattern, is_module::IsModule}; use swc_ecma_ast::*; use swc_ecma_minifier::option::MangleOptions; use swc_ecma_parser::{EsSyntax, Syntax, TsSyntax}; -use swc_ecma_transforms::helpers::{self, Helpers}; use swc_ecma_visit::{fold_pass, Fold}; use testing::{NormalizedOutput, StdErr, Tester}; use walkdir::WalkDir; @@ -693,6 +691,17 @@ impl Fold for Panicking { } } +impl PipelineHooks for Panicking { + fn mutate_after_typescript( + &mut self, + program: &mut Program, + _context: &PipelineContext<'_>, + ) -> Result<(), Error> { + program.mutate(fold_pass(Panicking)); + Ok(()) + } +} + #[test] #[should_panic = "visited"] fn should_visit() { @@ -707,12 +716,10 @@ fn should_visit() { const comp = () => ; ", ); - let comments = SingleThreadedComments::default(); - let config = c - .parse_js_as_input( - fm.clone(), - None, + let output = c + .compile( &handler, + CompileInput::source(fm), &swc::config::Options { config: swc::config::Config { jsc: JscConfig { @@ -726,54 +733,12 @@ fn should_visit() { }, ..Default::default() }, - &fm.name, - Some(&comments), - |_| noop_pass(), ) - .unwrap() - .unwrap(); - - dbg!(config.syntax); + .with_hooks(Panicking) + .codegen() + .expect("failed to compile JSX fixture"); - let config = config.with_pass(|pass| (fold_pass(Panicking), pass)); - - if config.minify { - let preserve_excl = |_: &BytePos, vc: &mut Vec| -> bool { - vc.retain(|c: &Comment| c.text.starts_with('!')); - !vc.is_empty() - }; - c.comments().leading.retain(preserve_excl); - c.comments().trailing.retain(preserve_excl); - } - let pass = config.pass; - let program = config.program; - let program = helpers::HELPERS.set(&Helpers::new(config.external_helpers), || { - HANDLER.set(&handler, || { - // Fold module - program.apply(pass) - }) - }); - - Ok(c.print( - &program, - PrintArgs { - source_root: None, - source_file_name: None, - output_path: config.output_path, - inline_sources_content: config.inline_sources_content, - source_map: config.source_maps, - orig: None, - // TODO: figure out sourcemaps - comments: Some(&comments), - emit_source_map_columns: config.emit_source_map_columns, - codegen_config: swc_ecma_codegen::Config::default() - .with_target(config.target) - .with_minify(config.minify), - ..Default::default() - }, - ) - .unwrap() - .code) + Ok(output.code) }) .unwrap(); } diff --git a/crates/swc/tests/rust-api/pipeline/flow-module-program/input.js b/crates/swc/tests/rust-api/pipeline/flow-module-program/input.js new file mode 100644 index 000000000000..8e1ef05031b3 --- /dev/null +++ b/crates/swc/tests/rust-api/pipeline/flow-module-program/input.js @@ -0,0 +1 @@ +await load(); diff --git a/crates/swc/tests/rust-api/pipeline/flow/input.js b/crates/swc/tests/rust-api/pipeline/flow/input.js new file mode 100644 index 000000000000..73772162fee1 --- /dev/null +++ b/crates/swc/tests/rust-api/pipeline/flow/input.js @@ -0,0 +1,4 @@ +/* @flow */ +import type { PipelineValue } from "./types"; + +const unused: PipelineValue = 1; diff --git a/crates/swc/tests/rust-api/pipeline/helpers/input.js b/crates/swc/tests/rust-api/pipeline/helpers/input.js new file mode 100644 index 000000000000..eca76d9e0b4b --- /dev/null +++ b/crates/swc/tests/rust-api/pipeline/helpers/input.js @@ -0,0 +1 @@ +class Example {} diff --git a/crates/swc/tests/rust-api/pipeline/stages/input.tsx b/crates/swc/tests/rust-api/pipeline/stages/input.tsx new file mode 100644 index 000000000000..245fbe3ab29f --- /dev/null +++ b/crates/swc/tests/rust-api/pipeline/stages/input.tsx @@ -0,0 +1,4 @@ +/*! pipeline API fixture */ +export const render = (value: number): JSX.Element => ( +

{value ?? 0}
+); diff --git a/crates/swc/tests/rust_api.rs b/crates/swc/tests/rust_api.rs index c952b36a2573..4eac91fb4f74 100644 --- a/crates/swc/tests/rust_api.rs +++ b/crates/swc/tests/rust_api.rs @@ -1,11 +1,12 @@ +use anyhow::Error; use swc::{ config::{Config, InputSourceMap, JscConfig, ModuleConfig, Options, SourceMapsConfig}, - Compiler, + CompileInput, Compiler, PipelineContext, PipelineHooks, }; -use swc_common::{comments::SingleThreadedComments, FileName}; +use swc_common::FileName; use swc_ecma_ast::*; use swc_ecma_parser::{EsSyntax, Syntax, TsSyntax}; -use swc_ecma_visit::{noop_visit_mut_type, visit_mut_pass, VisitMut}; +use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith}; struct PanicOnVisit; @@ -17,7 +18,18 @@ impl VisitMut for PanicOnVisit { } } -/// We ensure that typescript is stripped out before applying custom transforms. +impl PipelineHooks for PanicOnVisit { + fn mutate_after_typescript( + &mut self, + program: &mut Program, + _context: &PipelineContext<'_>, + ) -> Result<(), Error> { + program.visit_mut_with(self); + Ok(()) + } +} + +/// Ensures that TypeScript is stripped before the after-TypeScript hooks run. #[test] #[should_panic(expected = "Expected 5.0")] fn test_visit_mut() { @@ -31,25 +43,24 @@ fn test_visit_mut() { ", ); - let res = c.process_js_with_custom_pass( - fm, - None, - &handler, - &Options { - config: Config { - jsc: JscConfig { - syntax: Some(Syntax::Typescript(Default::default())), + let res = c + .compile( + &handler, + CompileInput::source(fm), + &Options { + config: Config { + jsc: JscConfig { + syntax: Some(Syntax::Typescript(Default::default())), + ..Default::default() + }, ..Default::default() }, + ..Default::default() }, - - ..Default::default() - }, - SingleThreadedComments::default(), - |_| visit_mut_pass(PanicOnVisit), - |_| noop_pass(), - ); + ) + .with_hooks(PanicOnVisit) + .codegen(); assert_ne!(res.unwrap().code, "console.log(5 as const)"); @@ -76,31 +87,26 @@ fn shopify_1_check_filename() { ", ); - let res = c.process_js_with_custom_pass( - fm, - None, - &handler, - &Options { - config: Config { - jsc: JscConfig { - syntax: Some(Syntax::Es(EsSyntax { - jsx: true, + let res = c + .compile( + &handler, + CompileInput::source(fm), + &Options { + config: Config { + jsc: JscConfig { + syntax: Some(Syntax::Es(EsSyntax { + jsx: true, + ..Default::default() + })), ..Default::default() - })), + }, + module: Some(ModuleConfig::CommonJs(Default::default())), ..Default::default() }, - module: Some(ModuleConfig::CommonJs(Default::default())), ..Default::default() }, - ..Default::default() - }, - SingleThreadedComments::default(), - |_| { - // Ensure comment API - noop_pass() - }, - |_| noop_pass(), - ); + ) + .codegen(); if res.is_err() { return Err(()); @@ -174,15 +180,9 @@ fn shopify_2_same_opt() { ", ); - let res = c.process_js_with_custom_pass( - fm, - None, - &handler, - &opts, - SingleThreadedComments::default(), - |_| noop_pass(), - |_| noop_pass(), - ); + let res = c + .compile(&handler, CompileInput::source(fm), &opts) + .codegen(); if res.is_err() { return Err(()); @@ -241,15 +241,9 @@ fn shopify_3_reduce_defaults() { ", ); - let res = c.process_js_with_custom_pass( - fm, - None, - &handler, - &opts, - SingleThreadedComments::default(), - |_| noop_pass(), - |_| noop_pass(), - ); + let res = c + .compile(&handler, CompileInput::source(fm), &opts) + .codegen(); if res.is_err() { return Err(()); @@ -303,15 +297,9 @@ fn shopify_4_reduce_more() { ", ); - let res = c.process_js_with_custom_pass( - fm, - None, - &handler, - &opts, - SingleThreadedComments::default(), - |_| noop_pass(), - |_| noop_pass(), - ); + let res = c + .compile(&handler, CompileInput::source(fm), &opts) + .codegen(); if res.is_err() { return Err(()); diff --git a/crates/swc_core/tests/fixture/stub_wasm/src/lib.rs b/crates/swc_core/tests/fixture/stub_wasm/src/lib.rs index f7269202a727..3252bb2e46dc 100644 --- a/crates/swc_core/tests/fixture/stub_wasm/src/lib.rs +++ b/crates/swc_core/tests/fixture/stub_wasm/src/lib.rs @@ -10,5 +10,10 @@ build_parse_sync!(#[wasm_bindgen(js_name = "parseSync")]); build_parse!(#[wasm_bindgen(js_name = "parse")]); build_print_sync!(#[wasm_bindgen(js_name = "printSync")]); build_print!(#[wasm_bindgen(js_name = "print")]); -build_transform_sync!(#[wasm_bindgen(js_name = "transformSync")]); +// Compile the custom-pass overload; binding_core_wasm covers the default path. +build_transform_sync!( + #[wasm_bindgen(js_name = "transformSync")], + |_| swc_core::ecma::ast::noop_pass(), + |_| swc_core::ecma::ast::noop_pass() +); build_transform!(#[wasm_bindgen(js_name = "transform")]);