From 932d32c95cb93b5bc0a01545f573407e660c7672 Mon Sep 17 00:00:00 2001 From: OJ Kwon <1210596+kwonoj@users.noreply.github.com> Date: Tue, 7 Mar 2023 12:15:49 -0800 Subject: [PATCH] feat(turbopack_ecmascript): support partial tsconfig for the transform (vercel/turbo#3995) Resolves WEB-667, WEB-659. This PR allows to specify partial tsconfig (specifically, `useDefineForClassFields` / legacy decorators for now) into ecmatransform. There are few tsconfig options affect to the runtime output of typescript need to be specified when performing transform, useDefineForClassFields is one of them. For now, PR attempts to fix `test/development/basic/define-class-fields.test.ts` / `test/development/basic/legacy-decorators.test.ts` by enabling one options. .swcrc is still not being honored in this changes. --- crates/turbopack-ecmascript/Cargo.toml | 1 + crates/turbopack-ecmascript/src/parse.rs | 21 +++++- .../turbopack-ecmascript/src/transform/mod.rs | 70 +++++++++++++++++-- crates/turbopack/src/module_options/mod.rs | 5 +- 4 files changed, 91 insertions(+), 6 deletions(-) diff --git a/crates/turbopack-ecmascript/Cargo.toml b/crates/turbopack-ecmascript/Cargo.toml index 724e3af85c299..e6bc843d353ab 100644 --- a/crates/turbopack-ecmascript/Cargo.toml +++ b/crates/turbopack-ecmascript/Cargo.toml @@ -52,6 +52,7 @@ swc_core = { workspace = true, features = [ "ecma_transforms_module", "ecma_transforms_react", "ecma_transforms_typescript", + "ecma_transforms_proposal", "ecma_quote", "ecma_visit", "ecma_visit_path", diff --git a/crates/turbopack-ecmascript/src/parse.rs b/crates/turbopack-ecmascript/src/parse.rs index 84c1aa9170618..6fbb0f472e5f9 100644 --- a/crates/turbopack-ecmascript/src/parse.rs +++ b/crates/turbopack-ecmascript/src/parse.rs @@ -23,10 +23,12 @@ use turbo_tasks::{ primitives::{StringVc, U64Vc}, Value, ValueToString, }; -use turbo_tasks_fs::{FileContent, FileSystemPath}; +use turbo_tasks_fs::{FileContent, FileJsonContentVc, FileSystemPath}; use turbo_tasks_hash::hash_xxh3_hash64; use turbopack_core::{ asset::{Asset, AssetContent, AssetVc}, + resolve::{find_context_file, node::node_cjs_resolve_options, FindContextFileResult}, + source_asset::SourceAssetVc, source_map::{GenerateSourceMap, GenerateSourceMapVc, SourceMapVc}, }; use turbopack_swc_utils::emitter::IssueEmitter; @@ -35,6 +37,7 @@ use super::EcmascriptModuleAssetType; use crate::{ analyzer::graph::EvalContext, transform::{EcmascriptInputTransformsVc, TransformContext}, + typescript::resolve::{read_tsconfigs, tsconfig}, utils::WrapFuture, EcmascriptInputTransform, }; @@ -138,6 +141,19 @@ pub async fn parse( let fs_path = &*source.ident().path().await?; let file_path_hash = *hash_ident(source.ident().to_string()).await? as u128; let ty = ty.into_value(); + let tsconfig = find_context_file(source.ident().path(), tsconfig()); + let tsconfig = match *tsconfig.await? { + FindContextFileResult::Found(path, _) => Some( + read_tsconfigs( + path.read(), + SourceAssetVc::new(path).into(), + node_cjs_resolve_options(path.root()), + ) + .await?, + ), + FindContextFileResult::NotFound(_) => None, + }; + Ok(match &*content.await? { AssetContent::File(file) => match &*file.await? { FileContent::NotFound => ParseResult::NotFound.cell(), @@ -151,6 +167,7 @@ pub async fn parse( source, ty, transforms, + tsconfig, ) .await { @@ -178,6 +195,7 @@ async fn parse_content( source: AssetVc, ty: EcmascriptModuleAssetType, transforms: &[EcmascriptInputTransform], + tsconfig: Option>, ) -> Result { let source_map: Arc = Default::default(); let handler = Handler::with_emitter( @@ -284,6 +302,7 @@ async fn parse_content( file_path_str: &fs_path.path, file_name_str: fs_path.file_name(), file_name_hash: file_path_hash, + tsconfig: &tsconfig, }; for transform in transforms.iter() { transform.apply(&mut parsed_program, &context).await?; diff --git a/crates/turbopack-ecmascript/src/transform/mod.rs b/crates/turbopack-ecmascript/src/transform/mod.rs index c741015761002..28b3fce964c86 100644 --- a/crates/turbopack-ecmascript/src/transform/mod.rs +++ b/crates/turbopack-ecmascript/src/transform/mod.rs @@ -15,6 +15,7 @@ use swc_core::{ preset_env::{self, Targets}, transforms::{ base::{feature::FeatureFlag, helpers::inject_helpers, resolver, Assumptions}, + proposal::decorators, react::react, }, visit::{FoldWith, VisitMutWith}, @@ -24,8 +25,10 @@ use turbo_tasks::{ primitives::{StringVc, StringsVc}, trace::TraceRawVcs, }; -use turbo_tasks_fs::{json::parse_json_with_source_context, FileSystemPathVc}; -use turbopack_core::environment::EnvironmentVc; +use turbo_tasks_fs::{ + json::parse_json_with_source_context, FileJsonContent, FileJsonContentVc, FileSystemPathVc, +}; +use turbopack_core::{asset::AssetVc, environment::EnvironmentVc}; use self::server_to_client_proxy::{create_proxy_module, is_client_module}; @@ -78,6 +81,9 @@ pub enum EcmascriptInputTransform { StyledComponents, StyledJsx, TypeScript, + // Apply ecma decorators transform. This is not part of Typescript transform, even though + // decorators can be ts-specific (legacy decorartors) since there's ecma decorators for js. + Decorators, } #[turbo_tasks::value(transparent, serialization = "auto_for_input")] @@ -102,6 +108,7 @@ pub struct TransformContext<'a> { pub file_path_str: &'a str, pub file_name_str: &'a str, pub file_name_hash: u128, + pub tsconfig: &'a Option>, } impl EcmascriptInputTransform { @@ -116,6 +123,7 @@ impl EcmascriptInputTransform { file_path_str, file_name_str, file_name_hash, + tsconfig, }: &TransformContext<'_>, ) -> Result<()> { match *self { @@ -198,9 +206,63 @@ impl EcmascriptInputTransform { FileName::Anon, )); } + EcmascriptInputTransform::Decorators => { + // TODO: Currently this only supports legacy decorators from tsconfig / jsconfig + // options. + if let Some(tsconfig) = tsconfig { + // Selectively picks up tsconfig.json values to construct + // swc transform's stripconfig. It doesn't account .swcrc config currently. + for (value, _) in tsconfig { + let value = &*value.await?; + if let FileJsonContent::Content(value) = value { + let legacy_decorators = value["compilerOptions"] + ["experimentalDecorators"] + .as_bool() + .unwrap_or(false); + + if legacy_decorators { + // TODO: `fn decorators` does not support visitMut yet + let p = + std::mem::replace(program, Program::Module(Module::dummy())); + *program = p.fold_with(&mut chain!( + decorators(decorators::Config { + legacy: true, + emit_metadata: true, + use_define_for_class_fields: value["compilerOptions"] + ["useDefineForClassFields"] + .as_bool() + .unwrap_or(false), + }), + inject_helpers(unresolved_mark), + )); + } + } + } + }; + } EcmascriptInputTransform::TypeScript => { - use swc_core::ecma::transforms::typescript::strip; - program.visit_mut_with(&mut strip(top_level_mark)); + use swc_core::ecma::transforms::typescript::{strip_with_config, Config}; + + let config = if let Some(tsconfig) = tsconfig { + let mut config = Config { + ..Default::default() + }; + + for (value, _) in tsconfig { + let value = &*value.await?; + if let FileJsonContent::Content(value) = value { + let use_define_for_class_fields = + &value["compilerOptions"]["useDefineForClassFields"]; + config.use_define_for_class_fields = + use_define_for_class_fields.as_bool().unwrap_or(false); + } + } + config + } else { + Default::default() + }; + + program.visit_mut_with(&mut strip_with_config(config, top_level_mark)); } EcmascriptInputTransform::ClientDirective(transition_name) => { let transition_name = &*transition_name.await?; diff --git a/crates/turbopack/src/module_options/mod.rs b/crates/turbopack/src/module_options/mod.rs index 012f734296570..9892476dad517 100644 --- a/crates/turbopack/src/module_options/mod.rs +++ b/crates/turbopack/src/module_options/mod.rs @@ -83,7 +83,10 @@ impl ModuleOptionsVc { } } } - let mut transforms = custom_ecmascript_app_transforms.clone(); + // We apply decorators _before_ any ts transforms, as some of decorator requires + // type information. + let mut transforms = vec![EcmascriptInputTransform::Decorators]; + transforms.extend(custom_ecmascript_app_transforms.iter().cloned()); transforms.extend(custom_ecmascript_transforms.iter().cloned()); // Order of transforms is important. e.g. if the React transform occurs before