diff --git a/.eslintignore b/.eslintignore index 001f1993..ee8c1ee4 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1 @@ -fixture \ No newline at end of file +fixture diff --git a/Cargo.lock b/Cargo.lock index 9ba064e4..ee9cdcf0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2069,6 +2069,7 @@ dependencies = [ "swc_atoms 2.0.0", "swc_common 4.0.1", "swc_ecma_ast", + "swc_ecma_quote_macros", "swc_ecma_transforms_base", "swc_ecma_transforms_testing", "swc_ecma_visit", @@ -2152,6 +2153,23 @@ dependencies = [ "typed-arena", ] +[[package]] +name = "swc_ecma_quote_macros" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88380844ce3b4634aa4f8ea5f01b8ae66c35c6e19858e142d729eb6485a06d92" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "swc_atoms 2.0.0", + "swc_common 4.0.1", + "swc_ecma_ast", + "swc_ecma_parser", + "swc_macros_common 1.0.0", + "syn 2.0.77", +] + [[package]] name = "swc_ecma_testing" version = "4.0.0" diff --git a/crates/swc-plugin-gem/Cargo.toml b/crates/swc-plugin-gem/Cargo.toml index 7410c390..277949db 100644 --- a/crates/swc-plugin-gem/Cargo.toml +++ b/crates/swc-plugin-gem/Cargo.toml @@ -15,7 +15,7 @@ lto = true node-resolve = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -swc_core = { workspace = true, features = ["ecma_plugin_transform"] } +swc_core = { workspace = true, features = ["ecma_quote", "ecma_plugin_transform"] } swc_ecma_visit = { workspace = true } swc_common = { workspace = true, features = ["concurrent"] } swc_ecma_ast = { workspace = true } diff --git a/crates/swc-plugin-gem/README.md b/crates/swc-plugin-gem/README.md index 82ef50a0..7e3c501a 100644 --- a/crates/swc-plugin-gem/README.md +++ b/crates/swc-plugin-gem/README.md @@ -4,3 +4,31 @@ - support [memo getter](https://github.com/tc39/proposal-decorators/issues/509#issuecomment-2226967170) - support minify style - resolve full path (for esm) + +# Example + +```json +{ + "$schema": "https://swc.rs/schema.json", + "jsc": { + "target": "es2024", + "parser": { "syntax": "typescript" }, + "experimental": { + "plugins": [ + [ + "swc_plugin_gem", + { + "autoImport": { + "extends": "gem", + "members": { + "test": ["test"] + } + }, + "autoImportDts": true + } + ] + ] + } + } +} +``` diff --git a/crates/swc-plugin-gem/package.json b/crates/swc-plugin-gem/package.json index 1d1b70c5..53604a6e 100644 --- a/crates/swc-plugin-gem/package.json +++ b/crates/swc-plugin-gem/package.json @@ -1,6 +1,6 @@ { "name": "swc-plugin-gem", - "version": "0.1.4", + "version": "0.1.5", "description": "swc plugin for Gem", "keywords": [ "swc-plugin", diff --git a/crates/swc-plugin-gem/src/lib.rs b/crates/swc-plugin-gem/src/lib.rs index a77005d9..1a2a3bf1 100644 --- a/crates/swc-plugin-gem/src/lib.rs +++ b/crates/swc-plugin-gem/src/lib.rs @@ -4,29 +4,29 @@ use swc_core::ecma::visit::VisitMutWith; use swc_core::plugin::metadata::TransformPluginMetadataContextKind; use swc_core::plugin::{plugin_transform, proxies::TransformPluginProgramMetadata}; use swc_ecma_ast::Program; -use visitors::import::gen_once_dts; -pub use visitors::import::import_transform; +pub use visitors::import::{import_transform, AutoImport}; pub use visitors::memo::memo_transform; pub use visitors::minify::minify_transform; pub use visitors::path::path_transform; +pub use visitors::preload::preload_transform; mod visitors; #[derive(Deserialize, Debug, Clone, PartialEq, Default)] -#[serde(rename_all = "camelCase")] +#[serde(default, rename_all = "camelCase")] struct PluginConfig { - #[serde(default)] pub style_minify: bool, - #[serde(default)] - pub auto_import: bool, - #[serde(default)] - /// 在安装时会尝试读取 .swcrc 生成,有些项目没有 .swcrc 文件,需要在正式变异时生成 + pub auto_import: AutoImport, + /// 写入当前目录的 src 目录中 pub auto_import_dts: bool, - #[serde(default)] /// 配合 import map 直接使用 esm pub resolve_path: bool, - #[serde(default)] + /// 依赖 url loadder, 顶级 await + pub preload: bool, + /// 未实现 pub hmr: bool, + /// 未实现 + pub lazy_view: bool, } #[plugin_transform] @@ -39,11 +39,6 @@ pub fn process_transform(mut program: Program, data: TransformPluginProgramMetad let filename = data.get_context(&TransformPluginMetadataContextKind::Filename); - // 执行在每个文件 - if config.auto_import_dts { - gen_once_dts(); - } - program.visit_mut_with(&mut ( Optional { // 只支持原生装饰器或 `runPluginFirst`,不然被转译了,改写不了 @@ -51,8 +46,12 @@ pub fn process_transform(mut program: Program, data: TransformPluginProgramMetad visitor: memo_transform(), }, Optional { - enabled: config.auto_import, - visitor: import_transform(), + enabled: match config.auto_import { + AutoImport::Gem(enabeld) => enabeld, + AutoImport::Custom(_) => true, + }, + // 执行在每个文件 + visitor: import_transform(config.auto_import, config.auto_import_dts), }, Optional { enabled: config.style_minify, @@ -62,6 +61,10 @@ pub fn process_transform(mut program: Program, data: TransformPluginProgramMetad enabled: config.resolve_path, visitor: path_transform(filename.clone()), }, + Optional { + enabled: config.preload, + visitor: preload_transform(), + }, )); program @@ -77,7 +80,7 @@ mod tests { assert_eq!( config, PluginConfig { - auto_import: true, + auto_import: AutoImport::Gem(true), ..Default::default() } ) diff --git a/crates/swc-plugin-gem/src/visitors/import.rs b/crates/swc-plugin-gem/src/visitors/import.rs index a1e18fe9..6979a1b1 100644 --- a/crates/swc-plugin-gem/src/visitors/import.rs +++ b/crates/swc-plugin-gem/src/visitors/import.rs @@ -1,8 +1,9 @@ use indexmap::{IndexMap, IndexSet}; -use once_cell::sync::Lazy; +use node_resolve::Resolver; +use once_cell::sync::{Lazy, OnceCell}; use regex::Regex; use serde::Deserialize; -use std::{collections::HashMap, fs}; +use std::{collections::HashMap, env, fs}; use swc_common::{SyntaxContext, DUMMY_SP}; use swc_core::{ atoms::Atom, @@ -14,6 +15,10 @@ use swc_ecma_ast::{ VarDeclarator, }; +static CREATED_DTS: OnceCell = OnceCell::new(); + +static GLOBAL_CONFIG: OnceCell = OnceCell::new(); + static CUSTOM_ELEMENT_REGEX: Lazy = Lazy::new(|| Regex::new(r"(?s)<(?\w+(-\w+)+)(\s|>)").unwrap()); @@ -24,13 +29,7 @@ enum MemberOrMemberAs { MemberAs([String; 2]), } -#[derive(Deserialize, Debug, Clone, PartialEq, Default)] -#[serde(rename_all = "camelCase")] -struct AutoImportContent { - members: HashMap>, - elements: IndexMap>, -} - +#[derive(Default)] struct AutoImportConfig { /// member -> package name member_map: HashMap, @@ -38,39 +37,8 @@ struct AutoImportConfig { tag_config: Vec<(Regex, String)>, } -static GEM_AUTO_IMPORT_CONFIG: Lazy = Lazy::new(|| { - let content: &str = include_str!("../auto-import.json"); - let content = serde_json::from_str::(content).expect("invalid json"); - - let mut member_map = HashMap::new(); - - for (package, import_vec) in content.members.iter() { - for member in import_vec { - // TODO: support `MemberAs` - if let MemberOrMemberAs::Member(name) = member { - member_map.insert(name.into(), package.into()); - } - } - } - - let mut tag_config = Vec::new(); - - for (package, import_map) in content.elements { - for (tag, path) in import_map { - if let Ok(reg) = Regex::new(&tag.replace("*", "(.*)")) { - tag_config.push((reg, format!("{}{}", package, path.replace("*", "$1")))); - } - } - } - - AutoImportConfig { - member_map, - tag_config, - } -}); - #[derive(Default)] -pub struct TransformVisitor { +struct TransformVisitor { used_members: IndexSet, defined_members: IndexSet, used_elements: IndexSet, @@ -137,7 +105,7 @@ impl VisitMut for TransformVisitor { self.inset_used_member(ident); } - for ele in node.tpl.quasis.iter() { + for ele in &node.tpl.quasis { for cap in CUSTOM_ELEMENT_REGEX.captures_iter(ele.raw.as_str()) { self.used_elements.insert(cap["tag"].to_string()); } @@ -191,9 +159,9 @@ impl VisitMut for TransformVisitor { let mut available_import: HashMap> = HashMap::new(); - for id in self.used_members.iter() { + for id in &self.used_members { if !self.defined_members.contains(id) { - let pkg = GEM_AUTO_IMPORT_CONFIG.member_map.get(id.0.as_str()); + let pkg = GLOBAL_CONFIG.get().unwrap().member_map.get(id.0.as_str()); if let Some(pkg) = pkg { let set = available_import .entry(pkg.into()) @@ -224,8 +192,8 @@ impl VisitMut for TransformVisitor { }); } - for tag in self.used_elements.iter() { - for (reg, path) in GEM_AUTO_IMPORT_CONFIG.tag_config.iter() { + for tag in &self.used_elements { + for (reg, path) in &GLOBAL_CONFIG.get().unwrap().tag_config { if reg.is_match(tag) { out.push(ImportDecl { specifiers: vec![], @@ -249,40 +217,147 @@ impl VisitMut for TransformVisitor { } } -pub fn import_transform() -> impl VisitMut { - TransformVisitor::default() +pub fn import_transform(auto_import: AutoImport, gen_dts: bool) -> impl VisitMut { + let visitor = TransformVisitor::default(); + + gen_once_config(auto_import); + if gen_dts { + gen_once_dts(); + } + + visitor } -static mut GEN_DTS: bool = false; +#[derive(Deserialize, Debug, Clone, PartialEq, Default)] +#[serde(rename_all = "camelCase")] +pub struct AutoImportContent { + extends: Option, + members: Option>>, + elements: Option>>, +} -pub fn gen_once_dts() { - unsafe { - if GEN_DTS { - return; +#[derive(Deserialize, Debug, Clone, PartialEq)] +#[serde(untagged)] +pub enum AutoImport { + Gem(bool), + Custom(AutoImportContent), +} + +impl Default for AutoImport { + fn default() -> Self { + AutoImport::Gem(false) + } +} + +fn merge_content( + content: AutoImportContent, + mut root: Vec, +) -> Vec { + let extends = content.extends.clone(); + root.push(content); + + if let Some(extends) = extends { + if extends == "gem" { + return merge_content(get_config_content(AutoImport::Gem(true)), root); + } else { + let resolver = Resolver::new() + .with_extensions(&["json"]) + .with_basedir(env::current_dir().expect("get current dir error")); + if let Ok(full_path) = resolver.resolve(&extends) { + if let Ok(json_str) = fs::read_to_string(full_path) { + if let Ok(json) = serde_json::from_str::(&json_str) { + return merge_content(json, root); + } + } + } } - GEN_DTS = true; } - let mut import_list: Vec = vec![]; - for (member, pkg) in GEM_AUTO_IMPORT_CONFIG.member_map.iter() { - import_list.push(format!( - "const {member}: typeof import('{pkg}')['{member}'];", - )); + + root +} + +fn get_config_content(config: AutoImport) -> AutoImportContent { + match config { + AutoImport::Gem(_) => { + let content: &str = include_str!("../auto-import.json"); + serde_json::from_str::(content).expect("invalid json") + } + AutoImport::Custom(content) => { + let chain = merge_content(content, vec![]); + + let mut elements = IndexMap::default(); + let mut members = HashMap::default(); + for lv in chain { + elements.extend(lv.elements.unwrap_or_default()); + members.extend(lv.members.unwrap_or_default()); + } + + AutoImportContent { + extends: None, + elements: Some(elements), + members: Some(members), + } + } } - fs::write( - // https://github.com/swc-project/swc/discussions/4997 - "/cwd/src/auto-import.d.ts", - format!( - r#" +} + +fn gen_once_config(auto_import: AutoImport) { + GLOBAL_CONFIG.get_or_init(|| { + let content = get_config_content(auto_import); + let mut member_map = HashMap::new(); + + for (package, import_vec) in &content.members.unwrap_or_default() { + for member in import_vec { + // TODO: support `MemberAs` + if let MemberOrMemberAs::Member(name) = member { + member_map.insert(name.into(), package.into()); + } + } + } + + let mut tag_config = Vec::new(); + + for (package, import_map) in content.elements.unwrap_or_default() { + for (tag, path) in import_map { + if let Ok(reg) = Regex::new(&tag.replace("*", "(.*)")) { + tag_config.push((reg, format!("{}{}", package, path.replace("*", "$1")))); + } + } + } + + AutoImportConfig { + member_map, + tag_config, + } + }); +} + +fn gen_once_dts() { + CREATED_DTS.get_or_init(|| { + let mut import_list: Vec = vec![]; + for (member, pkg) in &GLOBAL_CONFIG.get().unwrap().member_map { + import_list.push(format!( + "const {member}: typeof import('{pkg}')['{member}'];", + )); + } + fs::write( + // https://github.com/swc-project/swc/discussions/4997 + "/cwd/src/auto-import.d.ts", + format!( + r#" // AUTOMATICALLY GENERATED, DO NOT MODIFY MANUALLY. export {{}} declare global {{ {} }} "#, - import_list.join("\n") - ), - ) - .expect("create dts error"); + import_list.join("\n") + ), + ) + .expect("create dts error"); + + true + }); } #[cfg(test)] @@ -291,10 +366,34 @@ mod tests { #[test] fn should_return_default_config() { - let content: &str = include_str!("../auto-import.json"); - let config = serde_json::from_str::(content).unwrap(); assert_eq!( - format!("{:?}", config.elements.get("duoyun-ui").unwrap().keys()), + format!( + "{:?}", + get_config_content(AutoImport::Gem(true)) + .elements + .unwrap_or_default() + .get("duoyun-ui") + .unwrap() + .keys() + ), + r#"["dy-pat-*", "dy-input-*", "dy-*"]"# + ) + } + + #[test] + fn should_support_extend_config() { + assert_eq!( + format!( + "{:?}", + get_config_content(AutoImport::Custom( + serde_json::from_str::(r#"{"extends":"gem"}"#).unwrap() + )) + .elements + .unwrap_or_default() + .get("duoyun-ui") + .unwrap() + .keys() + ), r#"["dy-pat-*", "dy-input-*", "dy-*"]"# ) } diff --git a/crates/swc-plugin-gem/src/visitors/mod.rs b/crates/swc-plugin-gem/src/visitors/mod.rs index c96b8b55..28fea677 100644 --- a/crates/swc-plugin-gem/src/visitors/mod.rs +++ b/crates/swc-plugin-gem/src/visitors/mod.rs @@ -2,3 +2,4 @@ pub mod import; pub mod memo; pub mod minify; pub mod path; +pub mod preload; diff --git a/crates/swc-plugin-gem/src/visitors/path.rs b/crates/swc-plugin-gem/src/visitors/path.rs index 4b2efb7a..b2b42d5b 100644 --- a/crates/swc-plugin-gem/src/visitors/path.rs +++ b/crates/swc-plugin-gem/src/visitors/path.rs @@ -6,7 +6,7 @@ use swc_core::ecma::visit::{noop_visit_mut_type, VisitMut}; use swc_ecma_ast::{CallExpr, Callee, ExprOrSpread, ImportDecl, Lit, Str}; use typed_path::{Utf8Path, Utf8UnixEncoding, Utf8WindowsEncoding}; -fn converting(path_buf: &PathBuf) -> String { +fn converting_to_unix_path(path_buf: &PathBuf) -> String { let windows_path = Utf8Path::::new(path_buf.to_str().unwrap()); windows_path.with_encoding::().to_string() } @@ -22,11 +22,13 @@ impl TransformVisitor { let cwd = env::current_dir().expect("get current dir error"); let dir = cwd.join(filename).parent().unwrap().to_path_buf(); let resolver = Resolver::new() - .with_extensions(&["ts"]) + .with_extensions(&["ts", "js", ".mjs"]) .with_basedir(dir.clone()); if let Ok(full_path) = resolver.resolve(origin) { - if let Some(relative_path) = diff_paths(&converting(&full_path), &converting(&dir)) - { + if let Some(relative_path) = diff_paths( + &converting_to_unix_path(&full_path), + &converting_to_unix_path(&dir), + ) { if let Some(relative_path) = relative_path.to_str() { let relative_path = relative_path.replace(".ts", ".js"); if !relative_path.starts_with(".") { diff --git a/crates/swc-plugin-gem/src/visitors/preload.rs b/crates/swc-plugin-gem/src/visitors/preload.rs new file mode 100644 index 00000000..c3795e7e --- /dev/null +++ b/crates/swc-plugin-gem/src/visitors/preload.rs @@ -0,0 +1,79 @@ +use once_cell::sync::Lazy; +use regex::Regex; +use swc_core::{ + ecma::visit::{noop_visit_mut_type, VisitMut, VisitMutWith}, + quote, +}; +use swc_ecma_ast::{Ident, ImportDecl, ModuleItem, Str}; + +static IMG_REG: Lazy = + Lazy::new(|| Regex::new(r"(?i)\.(gif|jpe?g|tiff?|png|webp|bmp)$").unwrap()); + +enum AwaitItem { + Img(Ident), + ArrayBuffer(Ident), +} + +#[derive(Default)] +struct TransformVisitor { + await_items: Vec, +} + +impl VisitMut for TransformVisitor { + noop_visit_mut_type!(); + + fn visit_mut_import_decl(&mut self, node: &mut ImportDecl) { + if let Some((source, prefix)) = node.src.value.split_once("?") { + if prefix != "preload" { + return; + } + let ident = node + .specifiers + .get_mut(0) + .expect("preload only allow one specifier") + .local_mut(); + if IMG_REG.is_match(source) { + self.await_items.push(AwaitItem::Img(ident.clone())); + } else { + ident.sym = format!("_{}", ident.sym.as_str()).into(); + self.await_items.push(AwaitItem::ArrayBuffer(ident.clone())); + } + node.src = Box::new(Str::from([source, "url"].join("?"))); + } + } + + fn visit_mut_module_items(&mut self, node: &mut Vec) { + node.visit_mut_children_with(self); + + let mut out: Vec = vec![]; + + while let Some(item) = self.await_items.pop() { + match item { + AwaitItem::Img(source) => { + out.push(quote!( + " + await new Promise((onload, onerror) => Object.assign(new Image, {src: $source, onload, onerror})) + " as ModuleItem, + source: Ident = source + )); + } + AwaitItem::ArrayBuffer(source) => { + out.push(quote!( + " + const data = await fetch($source).then(e => e.arrayBuffer()) + " as ModuleItem, + source: Ident = source + )); + } + } + } + + let index = node.partition_point(|x| x.is_module_decl()); + + node.splice(index..index, out); + } +} + +pub fn preload_transform() -> impl VisitMut { + TransformVisitor::default() +} diff --git a/crates/swc-plugin-gem/tests/fixture.rs b/crates/swc-plugin-gem/tests/fixture.rs index d2b263a2..7f510010 100644 --- a/crates/swc-plugin-gem/tests/fixture.rs +++ b/crates/swc-plugin-gem/tests/fixture.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use swc_core::ecma::transforms::testing::test_fixture; use swc_ecma_parser::{Syntax, TsSyntax}; use swc_ecma_visit::visit_mut_pass; -use swc_plugin_gem::{import_transform, memo_transform, minify_transform, path_transform}; +use swc_plugin_gem::*; use testing::fixture; fn get_syntax() -> Syntax { @@ -19,7 +19,7 @@ fn fixture_auto_import(input: PathBuf) { test_fixture( get_syntax(), - &|_| visit_mut_pass(import_transform()), + &|_| visit_mut_pass(import_transform(AutoImport::Gem(true), false)), &input, &output, Default::default(), @@ -68,3 +68,16 @@ fn fixture_path(input: PathBuf) { Default::default(), ); } + +#[fixture("tests/fixture/preload/input.ts")] +fn fixture_preload(input: PathBuf) { + let output = input.parent().unwrap().join("output.ts"); + + test_fixture( + get_syntax(), + &|_| visit_mut_pass(preload_transform()), + &input, + &output, + Default::default(), + ); +} diff --git a/crates/swc-plugin-gem/tests/fixture/preload/input.ts b/crates/swc-plugin-gem/tests/fixture/preload/input.ts new file mode 100644 index 00000000..a5537081 --- /dev/null +++ b/crates/swc-plugin-gem/tests/fixture/preload/input.ts @@ -0,0 +1,4 @@ +// @ts-nocheck +import src from 'xxxx.png?preload'; +import data from 'xxxx.data?preload'; +console.log(1); \ No newline at end of file diff --git a/crates/swc-plugin-gem/tests/fixture/preload/output.ts b/crates/swc-plugin-gem/tests/fixture/preload/output.ts new file mode 100644 index 00000000..48c783ce --- /dev/null +++ b/crates/swc-plugin-gem/tests/fixture/preload/output.ts @@ -0,0 +1,10 @@ +// @ts-nocheck +import src from "xxxx.png?url"; +import _data from "xxxx.data?url"; +const data = await fetch(_data).then((e)=>e.arrayBuffer()); +await new Promise((onload, onerror)=>Object.assign(new Image, { + src: src, + onload, + onerror + })); +console.log(1); \ No newline at end of file