diff --git a/Cargo.lock b/Cargo.lock index ccaa29eed763..a62c3ca94bcf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3854,20 +3854,26 @@ dependencies = [ "base64 0.22.1", "dashmap 5.5.3", "either", + "glob", "jsonc-parser 0.26.1", "rspack_ast", "rspack_core", "rspack_error", + "rspack_hook", "rspack_loader_runner", + "rspack_paths", "rspack_plugin_javascript", "rspack_util", + "rustc-hash 1.1.0", "serde", "serde_json", "stacker", "swc_config", "swc_core", "swc_plugin_import", + "swc_typescript", "tokio", + "tracing", "url", ] diff --git a/Cargo.toml b/Cargo.toml index 2b840a97e7b3..bf734a5de7c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,6 +85,7 @@ swc_error_reporters = { version = "=3.0.0" } swc_html = { version = "=3.0.0" } swc_html_minifier = { version = "=3.0.0", default-features = false } swc_node_comments = { version = "=2.0.0" } +swc_typescript = { version = "=2.0.0" } rspack_dojang = { version = "0.1.9" } [workspace.metadata.release] diff --git a/crates/node_binding/binding.d.ts b/crates/node_binding/binding.d.ts index 093940c896fa..cdb18bf3ba34 100644 --- a/crates/node_binding/binding.d.ts +++ b/crates/node_binding/binding.d.ts @@ -307,6 +307,7 @@ export enum BuiltinPluginName { LightningCssMinimizerRspackPlugin = 'LightningCssMinimizerRspackPlugin', BundlerInfoRspackPlugin = 'BundlerInfoRspackPlugin', CssExtractRspackPlugin = 'CssExtractRspackPlugin', + SwcDtsEmitRspackPlugin = 'SwcDtsEmitRspackPlugin', JsLoaderRspackPlugin = 'JsLoaderRspackPlugin', LazyCompilationPlugin = 'LazyCompilationPlugin' } @@ -1896,6 +1897,13 @@ export interface RawStatsOptions { colors: boolean } +export interface RawSwcDtsEmitRspackPluginOptions { + rootDir?: string + outDir?: string + include?: string + mode?: string +} + export interface RawSwcJsMinimizerOptions { compress: any mangle: any diff --git a/crates/rspack_binding_options/src/options/raw_builtins/mod.rs b/crates/rspack_binding_options/src/options/raw_builtins/mod.rs index 87358c7a828b..45d3e7f87a94 100644 --- a/crates/rspack_binding_options/src/options/raw_builtins/mod.rs +++ b/crates/rspack_binding_options/src/options/raw_builtins/mod.rs @@ -11,6 +11,7 @@ mod raw_mf; mod raw_progress; mod raw_runtime_chunk; mod raw_size_limits; +mod raw_swc_dts_emit; mod raw_swc_js_minimizer; use napi::{bindgen_prelude::FromNapiValue, Env, JsUnknown}; @@ -91,6 +92,7 @@ use self::{ raw_mf::{RawConsumeSharedPluginOptions, RawContainerReferencePluginOptions, RawProvideOptions}, raw_runtime_chunk::RawRuntimeChunkOptions, raw_size_limits::RawSizeLimitsPluginOptions, + raw_swc_dts_emit::RawSwcDtsEmitRspackPluginOptions, }; use crate::{ plugins::JsLoaderRspackPlugin, JsLoaderRunner, RawContextReplacementPluginOptions, @@ -176,6 +178,7 @@ pub enum BuiltinPluginName { LightningCssMinimizerRspackPlugin, BundlerInfoRspackPlugin, CssExtractRspackPlugin, + SwcDtsEmitRspackPlugin, // rspack js adapter plugins // naming format follow XxxRspackPlugin @@ -492,6 +495,13 @@ impl BuiltinPlugin { .boxed(); plugins.push(plugin); } + BuiltinPluginName::SwcDtsEmitRspackPlugin => { + let plugin = rspack_loader_swc::PluginSwcDtsEmit::new( + downcast_into::(self.options)?.try_into()?, + ) + .boxed(); + plugins.push(plugin); + } BuiltinPluginName::JsLoaderRspackPlugin => { plugins .push(JsLoaderRspackPlugin::new(downcast_into::(self.options)?).boxed()); diff --git a/crates/rspack_binding_options/src/options/raw_builtins/raw_swc_dts_emit.rs b/crates/rspack_binding_options/src/options/raw_builtins/raw_swc_dts_emit.rs new file mode 100644 index 000000000000..aa98bf73b396 --- /dev/null +++ b/crates/rspack_binding_options/src/options/raw_builtins/raw_swc_dts_emit.rs @@ -0,0 +1,32 @@ +use napi_derive::napi; +use rspack_error::Result; +use rspack_loader_swc::SwcDtsEmitOptions; + +#[napi(object, object_to_js = false)] +pub struct RawSwcDtsEmitRspackPluginOptions { + pub root_dir: Option, + pub out_dir: Option, + pub include: Option, + pub mode: Option, +} + +impl TryFrom for SwcDtsEmitOptions { + type Error = rspack_error::Error; + + fn try_from(value: RawSwcDtsEmitRspackPluginOptions) -> Result { + Ok(SwcDtsEmitOptions { + root_dir: value + .root_dir + .ok_or(rspack_error::error!("Failed to get 'root_dir'"))?, + out_dir: value + .out_dir + .ok_or(rspack_error::error!("Failed to get 'out_dir'"))?, + include: value + .include + .ok_or(rspack_error::error!("Failed to get 'include'"))?, + mode: value + .mode + .ok_or(rspack_error::error!("Failed to get 'mode'"))?, + }) + } +} diff --git a/crates/rspack_core/src/concatenated_module.rs b/crates/rspack_core/src/concatenated_module.rs index b891bf6bf88a..50f8ce4fe2f2 100644 --- a/crates/rspack_core/src/concatenated_module.rs +++ b/crates/rspack_core/src/concatenated_module.rs @@ -548,6 +548,7 @@ impl Module for ConcatenatedModule { json_data: Default::default(), top_level_declarations: Some(Default::default()), module_concatenation_bailout: Default::default(), + parse_meta: Default::default(), }; self.clear_diagnostics(); diff --git a/crates/rspack_core/src/module.rs b/crates/rspack_core/src/module.rs index 22ba2a962e93..63319d89a68c 100644 --- a/crates/rspack_core/src/module.rs +++ b/crates/rspack_core/src/module.rs @@ -14,7 +14,7 @@ use rspack_sources::Source; use rspack_util::atom::Atom; use rspack_util::ext::{AsAny, DynHash}; use rspack_util::source_map::ModuleSourceMapConfig; -use rustc_hash::FxHashSet as HashSet; +use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; use crate::concatenated_module::ConcatenatedModule; use crate::dependencies_block::dependencies_block_update_hash; @@ -56,6 +56,8 @@ pub struct BuildInfo { pub json_data: Option, pub top_level_declarations: Option>, pub module_concatenation_bailout: Option, + + pub parse_meta: HashMap, } impl Default for BuildInfo { @@ -74,6 +76,7 @@ impl Default for BuildInfo { json_data: None, top_level_declarations: None, module_concatenation_bailout: None, + parse_meta: HashMap::default(), } } } diff --git a/crates/rspack_core/src/normal_module.rs b/crates/rspack_core/src/normal_module.rs index d0ef103861bb..64733e44661e 100644 --- a/crates/rspack_core/src/normal_module.rs +++ b/crates/rspack_core/src/normal_module.rs @@ -485,6 +485,7 @@ impl Module for NormalModule { } build_info.cacheable = loader_result.cacheable; + build_info.parse_meta = loader_result.parse_meta.clone(); build_info.file_dependencies = loader_result.file_dependencies; build_info.context_dependencies = loader_result.context_dependencies; build_info.missing_dependencies = loader_result.missing_dependencies; diff --git a/crates/rspack_loader_swc/Cargo.toml b/crates/rspack_loader_swc/Cargo.toml index 1d1891a62a7e..c371ed1a297f 100644 --- a/crates/rspack_loader_swc/Cargo.toml +++ b/crates/rspack_loader_swc/Cargo.toml @@ -21,18 +21,24 @@ async-trait = { workspace = true } base64 = { version = "0.22" } dashmap = { workspace = true } either = { workspace = true } +glob = { workspace = true } jsonc-parser = { version = "0.26.0", features = ["serde"] } rspack_ast = { version = "0.1.0", path = "../rspack_ast" } rspack_core = { version = "0.1.0", path = "../rspack_core" } rspack_error = { version = "0.1.0", path = "../rspack_error" } +rspack_hook = { version = "0.1.0", path = "../rspack_hook" } rspack_loader_runner = { version = "0.1.0", path = "../rspack_loader_runner" } +rspack_paths = { version = "0.1.0", path = "../rspack_paths" } rspack_plugin_javascript = { version = "0.1.0", path = "../rspack_plugin_javascript" } rspack_util = { version = "0.1.0", path = "../rspack_util" } +rustc-hash = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } stacker = { workspace = true } swc_config = { workspace = true } swc_core = { workspace = true, features = ["base", "ecma_ast", "common"] } swc_plugin_import = { version = "0.1.5", path = "../swc_plugin_import" } +swc_typescript = { workspace = true } tokio = { workspace = true } +tracing = { workspace = true } url = "2.5.0" diff --git a/crates/rspack_loader_swc/src/lib.rs b/crates/rspack_loader_swc/src/lib.rs index d796ee989a1d..441d626cc788 100644 --- a/crates/rspack_loader_swc/src/lib.rs +++ b/crates/rspack_loader_swc/src/lib.rs @@ -2,13 +2,16 @@ mod compiler; mod options; +mod plugin; mod transformer; use std::default::Default; +use std::sync::Arc; use compiler::{IntoJsAst, SwcCompiler}; use options::SwcCompilerOptionsWithAdditional; pub use options::SwcLoaderJsOptions; +pub use plugin::{PluginSwcDtsEmit, SwcDtsEmitOptions}; use rspack_core::{rspack_sources::SourceMap, Mode, RunnerContext}; use rspack_error::{error, AnyhowError, Diagnostic, Result}; use rspack_loader_runner::{Identifiable, Identifier, Loader, LoaderContext}; @@ -18,7 +21,9 @@ use rspack_util::source_map::SourceMapKind; use swc_config::{config_types::MergingOption, merge::Merge}; use swc_core::base::config::SourceMapsConfig; use swc_core::base::config::{InputSourceMap, OutputCharset, TransformConfig}; +use swc_core::ecma::codegen::to_code_with_comments; use swc_core::ecma::visit::VisitWith; +use swc_typescript::fast_dts::FastDts; use transformer::IdentCollector; #[derive(Debug)] @@ -81,6 +86,8 @@ impl SwcLoader { swc_options }; + let filename = swc_options.filename.clone(); + let source_map_kind: SourceMapKind = match swc_options.config.source_maps { Some(SourceMapsConfig::Bool(false)) => SourceMapKind::empty(), _ => loader_context.context.module_source_map_kind, @@ -121,6 +128,25 @@ impl SwcLoader { inline_script: Some(false), keep_comments: Some(true), }; + let emit_dts = built.syntax.typescript() && built.emit_isolated_dts; + + let program = &built.program; + if emit_dts && program.is_module() { + let mut module = program.clone().expect_module(); + let mut checker = FastDts::new(Arc::new(swc_core::common::FileName::Custom( + filename.clone(), + ))); + let issues = checker.transform(&mut module); + for issue in issues { + error!(issue); + } + let dts_code = to_code_with_comments(Some(&built.comments), &module); + loader_context + .parse_meta + .entry(String::from("swc-dts-emit-plugin") + &filename[..]) + .and_modify(|v| v.push_str(&dts_code)) + .or_insert(dts_code); + } let program = tokio::task::block_in_place(|| c.transform(built).map_err(AnyhowError::from))?; if source_map_kind.enabled() { @@ -130,6 +156,7 @@ impl SwcLoader { program.visit_with(&mut v); codegen_options.source_map_config.names = v.names; } + let ast = c.into_js_ast(program); let TransformOutput { code, map } = ast::stringify(&ast, codegen_options)?; diff --git a/crates/rspack_loader_swc/src/plugin.rs b/crates/rspack_loader_swc/src/plugin.rs new file mode 100644 index 000000000000..277bd2a48e12 --- /dev/null +++ b/crates/rspack_loader_swc/src/plugin.rs @@ -0,0 +1,198 @@ +use std::cell::RefCell; +use std::sync::{Arc, Mutex}; + +use glob::glob_with; +use glob::{MatchOptions, Pattern as GlobPattern}; +use rspack_core::{ + rspack_sources::RawSource, AssetInfo, Chunk, Compilation, CompilationAsset, + CompilationFinishModules, CompilationProcessAssets, Logger, Plugin, PluginContext, +}; +use rspack_error::Result; +use rspack_hook::{plugin, plugin_hook}; +use rspack_paths::{AssertUtf8, Utf8Path, Utf8PathBuf}; +use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; + +#[derive(Debug)] +pub struct SwcDtsEmitOptions { + pub root_dir: String, + pub out_dir: String, + pub include: String, + pub mode: String, +} + +#[plugin] +#[derive(Debug)] +pub struct PluginSwcDtsEmit { + pub(crate) options: Arc, + pub(crate) dts_outputs: Arc>>, +} + +impl Eq for PluginSwcDtsEmit {} + +impl PartialEq for PluginSwcDtsEmit { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.options, &other.options) + } +} + +const PLUGIN_NAME: &str = "rspack.SwcDtsEmitPlugin"; + +impl PluginSwcDtsEmit { + pub fn new(options: SwcDtsEmitOptions) -> Self { + Self::new_inner(Arc::new(options), Arc::new(Mutex::new(HashMap::default()))) + } +} + +#[plugin_hook(CompilationFinishModules for PluginSwcDtsEmit)] +async fn finish_modules(&self, compilation: &mut Compilation) -> Result<()> { + let module_graph = compilation.get_module_graph(); + let mut dts_outputs = self.dts_outputs.lock().expect("error in dts_outputs"); + + for (_, a) in module_graph.modules() { + let meta = &a.build_info().expect("parse_meta").parse_meta; + let meta = meta.clone(); + + dts_outputs.extend(meta); + } + Ok(()) +} + +#[plugin_hook(CompilationProcessAssets for PluginSwcDtsEmit, stage = Compilation::PROCESS_ASSETS_STAGE_DERIVED)] +async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> { + let mode = self.options.mode.clone(); + let logger = compilation.get_logger("rspack.SwcDtsEmitRspackPlugin"); + let start = logger.time("run dts emit"); + + let dts_outputs = self.dts_outputs.lock().expect("error in dts_outputs"); + + let root_dir = self.options.root_dir.clone(); + let out_dir = self.options.out_dir.clone(); + + if mode != "plugin" { + for (key, source) in dts_outputs.iter() { + let key = key.to_string(); + let path = key.strip_prefix("swc-dts-emit-plugin"); + + let Some(source_filename) = path else { + continue; + }; + let Some(output_relative_path) = source_filename.strip_prefix(&root_dir) else { + continue; + }; + + let output_relative_path = Utf8PathBuf::from(output_relative_path); + + let output_filename = Utf8PathBuf::from(out_dir.clone()).join(output_relative_path); + + dbg!(&output_filename, &source); + + let asset_info = AssetInfo { + source_filename: Some(output_filename.to_string()), + ..Default::default() + }; + + compilation.emit_asset( + output_filename.to_string(), + CompilationAsset { + source: Some(Arc::new(RawSource::from(source.as_str()))), + info: asset_info, + }, + ); + } + + logger.time_end(start); + return Ok(()); + } + + // let context = compilation.options.context.as_path(); + + // let output_dir = context.join(&self.options.out_dir); + // let root_dir = context.join(&self.options.root_dir); + + // let include = self.options.include; + + // let include = if !include.contains('*') { + // let mut escaped = Utf8PathBuf::from(GlobPattern::escape(root_dir.as_str())); + // escaped.push("/**/*"); + // escaped.as_str().to_string() + // } else { + // include + // }; + + // let need_transform_files = glob_with( + // &include, + // // TODO: matchOptions + // MatchOptions { + // case_sensitive: true, + // require_literal_separator: Default::default(), + // require_literal_leading_dot: false, + // }, + // ) + // .expect("glob failed"); + + // need_transform_files.into_iter().for_each(|item| { + // let source_filename = + // Utf8PathBuf::from_path_buf(item.expect("wrong glob result")).expect("from utf8 error"); + // let output_relative_path = + // Utf8PathBuf::from(source_filename.strip_prefix(root_dir).expect("not in root")); + // let output_filename = Utf8PathBuf::from(root_dir).join(output_relative_path); + + // let source = source_filename; + + // let mut asset_info = AssetInfo { + // source_filename: Some(source_filename.to_string()), + // ..Default::default() + // }; + + // compilation.emit_asset( + // output_filename.to_string(), + // CompilationAsset { + // source: Some(Arc::new(result.source)), + // info: asset_info, + // }, + // ); + // }); + + Ok(()) +} + +// #[plugin_hook(CompilerCompilation for PluginSwcDtsEmit)] +// async fn compilation( +// &self, +// compilation: &mut Compilation, +// params: &mut CompilationParams, +// ) -> Result<()> { +// Ok(()) +// } + +impl Plugin for PluginSwcDtsEmit { + fn name(&self) -> &'static str { + PLUGIN_NAME + } + + fn apply( + &self, + ctx: PluginContext<&mut rspack_core::ApplyContext>, + _options: &rspack_core::CompilerOptions, + ) -> Result<()> { + // ctx + // .context + // .compiler_hooks + // .compilation + // .tap(compilation::new(self)); + + ctx + .context + .compilation_hooks + .finish_modules + .tap(finish_modules::new(self)); + + ctx + .context + .compilation_hooks + .process_assets + .tap(process_assets::new(self)); + + Ok(()) + } +} diff --git a/packages/rspack/etc/core.api.md b/packages/rspack/etc/core.api.md index 33d0a6be7c0c..1e1362034c26 100644 --- a/packages/rspack/etc/core.api.md +++ b/packages/rspack/etc/core.api.md @@ -67,6 +67,7 @@ import { RawProgressPluginOptions } from '@rspack/binding'; import { RawProvideOptions } from '@rspack/binding'; import { RawRuntimeChunkOptions } from '@rspack/binding'; import { RawSourceMapDevToolPluginOptions } from '@rspack/binding'; +import { RawSwcDtsEmitRspackPluginOptions } from '@rspack/binding'; import { registerGlobalTrace } from '@rspack/binding'; import { RspackOptionsNormalized as RspackOptionsNormalized_2 } from '.'; import sources = require('../compiled/webpack-sources'); @@ -4697,6 +4698,7 @@ declare namespace rspackExports { EvalSourceMapDevToolPlugin, EvalDevToolModulePlugin, CssExtractRspackPlugin, + SwcDtsEmitRspackPlugin, ContextReplacementPlugin, SwcLoaderEnvConfig, SwcLoaderEsParserConfig, @@ -9904,6 +9906,23 @@ type StringCallback = (err: NodeJS.ErrnoException | null, data?: string) => void // @public (undocumented) type StringOrBufferCallback = (err: NodeJS.ErrnoException | null, data?: string | Buffer) => void; +// @public (undocumented) +export class SwcDtsEmitRspackPlugin { + constructor(options: SwcDtsEmitRspackPluginOptions); + // (undocumented) + apply(compiler: Compiler): void; + // (undocumented) + normalizeOptions(options: SwcDtsEmitRspackPluginOptions): RawSwcDtsEmitRspackPluginOptions; + // (undocumented) + options: SwcDtsEmitRspackPluginOptions; +} + +// @public (undocumented) +interface SwcDtsEmitRspackPluginOptions { + // (undocumented) + rootDir: string; +} + // @public (undocumented) export const SwcJsMinimizerRspackPlugin: { new (options?: SwcJsMinimizerRspackPluginOptions | undefined): { diff --git a/packages/rspack/src/builtin-plugin/css-extract/index.ts b/packages/rspack/src/builtin-plugin/css-extract/index.ts index eaf6c4910a85..17421a136e01 100644 --- a/packages/rspack/src/builtin-plugin/css-extract/index.ts +++ b/packages/rspack/src/builtin-plugin/css-extract/index.ts @@ -4,7 +4,7 @@ import { } from "@rspack/binding"; import { join } from "node:path"; -import type { Compiler } from "../.."; +import type { Compiler } from "../../Compiler"; import { MODULE_TYPE } from "./loader"; import { PLUGIN_NAME } from "./utils"; diff --git a/packages/rspack/src/builtin-plugin/index.ts b/packages/rspack/src/builtin-plugin/index.ts index 05aba1e96648..a8fb6c4dbfbf 100644 --- a/packages/rspack/src/builtin-plugin/index.ts +++ b/packages/rspack/src/builtin-plugin/index.ts @@ -61,6 +61,7 @@ export * from "./LightningCssMinimizerRspackPlugin"; export * from "./RemoveDuplicateModulesPlugin"; export * from "./LightningCssMinimizerRspackPlugin"; export * from "./SwcJsMinimizerPlugin"; +export * from "./swc-dts-emit/index"; export * from "./WarnCaseSensitiveModulesPlugin"; export * from "./WebWorkerTemplatePlugin"; export * from "./WorkerPlugin"; diff --git a/packages/rspack/src/builtin-plugin/swc-dts-emit/index.ts b/packages/rspack/src/builtin-plugin/swc-dts-emit/index.ts new file mode 100644 index 000000000000..a933eb700d27 --- /dev/null +++ b/packages/rspack/src/builtin-plugin/swc-dts-emit/index.ts @@ -0,0 +1,31 @@ +import { + BuiltinPluginName, + type RawSwcDtsEmitRspackPluginOptions +} from "@rspack/binding"; +import { Compiler } from "../../Compiler"; + +export interface SwcDtsEmitRspackPluginOptions { + rootDir: string; + outDir?: string; + include?: string; + mode?: string; +} + +export class SwcDtsEmitRspackPlugin { + options: SwcDtsEmitRspackPluginOptions; + constructor(options: SwcDtsEmitRspackPluginOptions) { + this.options = options; + } + apply(compiler: Compiler) { + compiler.__internal__registerBuiltinPlugin({ + name: BuiltinPluginName.SwcDtsEmitRspackPlugin, + options: this.options + }); + } + normalizeOptions( + options: SwcDtsEmitRspackPluginOptions + ): RawSwcDtsEmitRspackPluginOptions { + const normalzedOptions: RawSwcDtsEmitRspackPluginOptions = options; + return normalzedOptions; + } +} diff --git a/packages/rspack/src/exports.ts b/packages/rspack/src/exports.ts index fae2e2d781ac..d93fadf38bab 100644 --- a/packages/rspack/src/exports.ts +++ b/packages/rspack/src/exports.ts @@ -261,6 +261,7 @@ export { SourceMapDevToolPlugin } from "./builtin-plugin"; export { EvalSourceMapDevToolPlugin } from "./builtin-plugin"; export { EvalDevToolModulePlugin } from "./builtin-plugin"; export { CssExtractRspackPlugin } from "./builtin-plugin"; +export { SwcDtsEmitRspackPlugin } from "./builtin-plugin"; export { ContextReplacementPlugin } from "./builtin-plugin"; ///// Rspack Postfixed Internal Loaders /////