From dfafcf14d3e0ad7e1689a8d5a87ece6b532f1df9 Mon Sep 17 00:00:00 2001 From: mantou132 <709922234@qq.com> Date: Tue, 10 Dec 2024 20:33:09 +0800 Subject: [PATCH] Support member as --- Cargo.lock | 11 + Cargo.toml | 1 + crates/swc-plugin-gem/Cargo.toml | 6 +- crates/swc-plugin-gem/src/lib.rs | 11 +- crates/swc-plugin-gem/src/visitors/import.rs | 252 +++++++++--------- crates/swc-plugin-gem/tests/fixture.rs | 21 +- .../fixture/auto-import/members/input.ts | 1 + .../fixture/auto-import/members/output.ts | 6 +- crates/zed-plugin-gem/Cargo.toml | 4 +- crates/zed-plugin-gem/extension.toml | 2 +- 10 files changed, 176 insertions(+), 139 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4c8148d8..c0a5a44d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1763,6 +1763,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_regex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf" +dependencies = [ + "regex", + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2357,6 +2367,7 @@ dependencies = [ "regex", "serde", "serde_json", + "serde_regex", "swc_common 4.0.1", "swc_core", "swc_ecma_ast", diff --git a/Cargo.toml b/Cargo.toml index 901c53f9..829b28ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ indexmap = { version = "2.6.0" } regex = { version = "1.10.4", default-features = false } serde = "1.0.203" serde_json = "1.0.117" +serde_regex = "1.1.0" swc_core = "5.0.0" swc_ecma_parser = "5.0.0" swc_ecma_visit = "4.0.0" diff --git a/crates/swc-plugin-gem/Cargo.toml b/crates/swc-plugin-gem/Cargo.toml index 277949db..1aa77f0d 100644 --- a/crates/swc-plugin-gem/Cargo.toml +++ b/crates/swc-plugin-gem/Cargo.toml @@ -15,7 +15,11 @@ lto = true node-resolve = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -swc_core = { workspace = true, features = ["ecma_quote", "ecma_plugin_transform"] } +serde_regex = { workspace = true } +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/src/lib.rs b/crates/swc-plugin-gem/src/lib.rs index b105f802..5f8db122 100644 --- a/crates/swc-plugin-gem/src/lib.rs +++ b/crates/swc-plugin-gem/src/lib.rs @@ -10,7 +10,7 @@ use swc_core::{ use swc_ecma_ast::Program; pub use visitors::{ hmr::hmr_transform, - import::{import_transform, AutoImport}, + import::{import_transform, AutoImport, AutoImportContent, AutoImportDts, MemberOrMemberAs}, memo::memo_transform, minify::minify_transform, path::path_transform, @@ -23,12 +23,13 @@ mod visitors; #[serde(default, rename_all = "camelCase")] struct PluginConfig { pub style_minify: bool, + /// e.g: https://github.com/mantou132/gem/blob/main/crates/swc-plugin-gem/README.md#example pub auto_import: AutoImport, - /// Write into the src directory - pub auto_import_dts: bool, + /// Generate .d.ts file, use src/auto-import.d.ts when true + pub auto_import_dts: AutoImportDts, /// Use esm directly with import map pub resolve_path: bool, - ///depend on URL loader, top await + ///depend on URL loader & top await pub preload: bool, /// Under development, need add `@mantou/gem/helper/hmr` to entry pub hmr: bool, @@ -55,7 +56,7 @@ pub fn process_transform(mut program: Program, data: TransformPluginProgramMetad Optional { enabled: match config.auto_import { AutoImport::Gem(enabled) => enabled, - AutoImport::Custom(_) => true, + AutoImport::CustomContent(_) => true, }, visitor: import_transform(config.auto_import, config.auto_import_dts), }, diff --git a/crates/swc-plugin-gem/src/visitors/import.rs b/crates/swc-plugin-gem/src/visitors/import.rs index 0862485e..fcd4e9f9 100644 --- a/crates/swc-plugin-gem/src/visitors/import.rs +++ b/crates/swc-plugin-gem/src/visitors/import.rs @@ -1,51 +1,90 @@ -use std::{collections::HashMap, env, fs}; +use std::{collections::HashMap, env, fs, path::Path}; use indexmap::{IndexMap, IndexSet}; use node_resolve::Resolver; -use once_cell::sync::{Lazy, OnceCell}; +use once_cell::sync::Lazy; use regex::Regex; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use swc_common::{SyntaxContext, DUMMY_SP}; use swc_core::{ atoms::Atom, ecma::visit::{noop_visit_mut_type, VisitMut, VisitMutWith}, }; use swc_ecma_ast::{ - Callee, Class, ClassDecl, ClassExpr, Decorator, FnDecl, FnExpr, Id, Ident, ImportDecl, - ImportNamedSpecifier, ImportSpecifier, JSXElementName, ModuleDecl, ModuleItem, Str, TaggedTpl, - VarDeclarator, + ClassDecl, ClassExpr, FnDecl, FnExpr, Id, Ident, ImportDecl, ImportNamedSpecifier, + ImportSpecifier, ModuleDecl, ModuleExportName, ModuleItem, Str, TaggedTpl, VarDeclarator, }; static CUSTOM_ELEMENT_REGEX: Lazy = Lazy::new(|| Regex::new(r"(?s)<(?\w+(-\w+)+)(\s|>)").unwrap()); -// FIXME: 每个文件作为 loader 重新执行整个程序,这个全局缓存并没有生效 -static CREATED_DTS: OnceCell = OnceCell::new(); -static GLOBAL_CONFIG: OnceCell = OnceCell::new(); - #[derive(Deserialize, Debug, Clone, PartialEq)] #[serde(untagged)] -enum MemberOrMemberAs { +pub enum MemberOrMemberAs { Member(String), MemberAs([String; 2]), } -#[derive(Default)] +#[derive(Debug, Serialize, Deserialize)] +struct RegexStringPair { + #[serde(with = "serde_regex")] + regex: Regex, + path: String, +} + +#[derive(Deserialize, Serialize, Default)] struct AutoImportConfig { - /// member -> package name - member_map: HashMap, - /// tag reg, path reg - tag_config: Vec<(Regex, String)>, + /// local -> (imported, package name) + member_map: HashMap, String)>, + tag_config: Vec, } #[derive(Default)] struct TransformVisitor { + config: AutoImportConfig, used_members: IndexSet, defined_members: IndexSet, used_elements: IndexSet, } impl TransformVisitor { + fn gen_dts(&self, gen_dts: AutoImportDts) { + let path = match gen_dts { + AutoImportDts::Src(true) => "src/auto-import.d.ts".into(), + AutoImportDts::Src(false) => "".into(), + AutoImportDts::CustomPath(custom) => custom, + }; + if path.is_empty() { + return; + } + + // https://github.com/swc-project/swc/discussions/4997 + let path = Path::new("/cwd").join(path); + + if path.exists() { + return; + } + + let mut import_list: Vec = vec![ + "// AUTOMATICALLY GENERATED, DO NOT MODIFY MANUALLY.".into(), + "// DELETING WILL REGENERATE".into(), + "".into(), + "export {}".into(), + "declare global {".into(), + ]; + for (local, (imported, pkg)) in &self.config.member_map { + let member = imported + .as_ref() + .map_or(local.clone(), |x| x.as_str().to_string()); + import_list.push(format!( + " const {local}: typeof import('{pkg}')['{member}'];", + )); + } + import_list.push("}".into()); + + fs::write(path, import_list.join("\n")).expect("create dts error"); + } + fn inset_used_member(&mut self, ident: &Ident) { self.used_members.insert(ident.to_id()); } @@ -53,21 +92,6 @@ impl TransformVisitor { fn inset_defined_member(&mut self, ident: &Ident) { self.defined_members.insert(ident.to_id()); } - - fn visit_mut_class(&mut self, node: &Class) { - if let Some(expr) = &node.super_class { - if let Some(ident) = expr.as_ident() { - self.inset_used_member(ident); - } - // support decorators transform - // class PageHomeElement extends (_GemElement = GemElement) {} - if let Some(assign) = expr.as_assign() { - if let Some(ident) = assign.right.as_ident() { - self.inset_used_member(ident); - } - } - } - } } impl VisitMut for TransformVisitor { @@ -77,35 +101,13 @@ impl VisitMut for TransformVisitor { self.inset_defined_member(node.local()); } - fn visit_mut_callee(&mut self, node: &mut Callee) { - if let Callee::Expr(expr) = &node { - if let Some(ident) = expr.as_ident() { - self.inset_used_member(ident); - } - } - } - - fn visit_mut_jsx_element_name(&mut self, node: &mut JSXElementName) { - if let JSXElementName::Ident(ident) = node { - self.inset_used_member(ident); - } - } - - fn visit_mut_decorator(&mut self, node: &mut Decorator) { - node.visit_mut_children_with(self); - - if let Some(ident) = node.expr.as_ident() { - self.inset_used_member(ident); - } + fn visit_mut_ident(&mut self, node: &mut Ident) { + self.inset_used_member(node); } fn visit_mut_tagged_tpl(&mut self, node: &mut TaggedTpl) { node.visit_mut_children_with(self); - if let Some(ident) = node.tag.as_ident() { - self.inset_used_member(ident); - } - 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()); @@ -116,14 +118,12 @@ impl VisitMut for TransformVisitor { fn visit_mut_class_decl(&mut self, node: &mut ClassDecl) { node.visit_mut_children_with(self); - self.visit_mut_class(&node.class); self.inset_defined_member(&node.ident); } fn visit_mut_class_expr(&mut self, node: &mut ClassExpr) { node.visit_mut_children_with(self); - self.visit_mut_class(&node.class); if let Some(ident) = &node.ident { self.inset_defined_member(ident); } @@ -157,26 +157,28 @@ impl VisitMut for TransformVisitor { node.visit_mut_children_with(self); let mut out: Vec = vec![]; - let mut available_import: HashMap> = - HashMap::new(); + let mut available_import: IndexMap< + String, + IndexMap<&Atom, (Option<&Atom>, &SyntaxContext)>, + > = IndexMap::new(); for id in &self.used_members { if !self.defined_members.contains(id) { - let pkg = GLOBAL_CONFIG.get().unwrap().member_map.get(id.0.as_str()); - if let Some(pkg) = pkg { + let res = self.config.member_map.get(id.0.as_str()); + if let Some((imported, pkg)) = res { let set = available_import.entry(pkg.into()).or_default(); - set.insert(&id.0, (&id.0, &id.1)); + set.insert(&id.0, (imported.as_ref(), &id.1)); } } } for (pkg, set) in available_import { let mut specifiers: Vec = vec![]; - for (member, (_member_as, ctx)) in set { + for (member_as, (member, ctx)) in set { specifiers.push(ImportSpecifier::Named(ImportNamedSpecifier { - local: Ident::new(member.clone(), DUMMY_SP, *ctx), + local: Ident::new(member_as.clone(), DUMMY_SP, *ctx), span: DUMMY_SP, - imported: None, + imported: member.map(|x| ModuleExportName::Ident(x.clone().into())), is_type_only: false, })); } @@ -193,11 +195,11 @@ impl VisitMut for TransformVisitor { } for tag in &self.used_elements { - for (reg, path) in &GLOBAL_CONFIG.get().unwrap().tag_config { - if reg.is_match(tag) { + for RegexStringPair { regex, path } in &self.config.tag_config { + if regex.is_match(tag) { out.push(ImportDecl { specifiers: vec![], - src: Box::new(Str::from(reg.replace(tag, path))), + src: Box::new(Str::from(regex.replace(tag, path))), span: DUMMY_SP, type_only: false, with: None, @@ -217,13 +219,13 @@ impl VisitMut for TransformVisitor { } } -pub fn import_transform(auto_import: AutoImport, gen_dts: bool) -> impl VisitMut { - let visitor = TransformVisitor::default(); +pub fn import_transform(auto_import: AutoImport, gen_dts: AutoImportDts) -> impl VisitMut { + let visitor = TransformVisitor { + config: get_config(auto_import), + ..Default::default() + }; - gen_once_config(auto_import); - if gen_dts { - gen_once_dts(); - } + visitor.gen_dts(gen_dts); visitor } @@ -231,16 +233,16 @@ pub fn import_transform(auto_import: AutoImport, gen_dts: bool) -> impl VisitMut #[derive(Deserialize, Debug, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] pub struct AutoImportContent { - extends: Option, - members: Option>>, - elements: Option>>, + pub extends: Option, + pub members: Option>>, + pub elements: Option>>, } #[derive(Deserialize, Debug, Clone, PartialEq)] #[serde(untagged)] pub enum AutoImport { Gem(bool), - Custom(AutoImportContent), + CustomContent(AutoImportContent), } impl Default for AutoImport { @@ -249,6 +251,19 @@ impl Default for AutoImport { } } +#[derive(Deserialize, Debug, Clone, PartialEq)] +#[serde(untagged)] +pub enum AutoImportDts { + Src(bool), + CustomPath(String), +} + +impl Default for AutoImportDts { + fn default() -> Self { + AutoImportDts::Src(false) + } +} + fn merge_content( content: AutoImportContent, mut root: Vec, @@ -282,7 +297,7 @@ fn get_config_content(config: AutoImport) -> AutoImportContent { let content: &str = include_str!("../auto-import.json"); serde_json::from_str::(content).expect("invalid json") } - AutoImport::Custom(content) => { + AutoImport::CustomContent(content) => { let chain = merge_content(content, vec![]); let mut elements = IndexMap::default(); @@ -301,63 +316,44 @@ fn get_config_content(config: AutoImport) -> AutoImportContent { } } -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(); +fn get_config(auto_import: AutoImport) -> AutoImportConfig { + // TODO: use cache + 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()); + for (package, import_vec) in &content.members.unwrap_or_default() { + for member in import_vec { + match member { + MemberOrMemberAs::Member(name) => { + member_map.insert(name.into(), (None, 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")))); + MemberOrMemberAs::MemberAs([name, member_as]) => { + member_map.insert( + member_as.into(), + (Some(name.clone().into()), package.into()), + ); } } } + } - AutoImportConfig { - member_map, - tag_config, - } - }); -} + let mut tag_config = Vec::new(); -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}'];", - )); + for (package, import_map) in content.elements.unwrap_or_default() { + for (tag, path) in import_map { + if let Ok(regex) = Regex::new(&tag.replace("*", "(.*)")) { + tag_config.push(RegexStringPair { + regex, + path: format!("{}{}", package, path.replace("*", "$1")), + }); + } } - 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"); + } - true - }); + AutoImportConfig { + member_map, + tag_config, + } } #[cfg(test)] @@ -385,7 +381,7 @@ mod tests { assert_eq!( format!( "{:?}", - get_config_content(AutoImport::Custom( + get_config_content(AutoImport::CustomContent( serde_json::from_str::(r#"{"extends":"gem"}"#).unwrap() )) .elements diff --git a/crates/swc-plugin-gem/tests/fixture.rs b/crates/swc-plugin-gem/tests/fixture.rs index 7f510010..50a054ae 100644 --- a/crates/swc-plugin-gem/tests/fixture.rs +++ b/crates/swc-plugin-gem/tests/fixture.rs @@ -19,7 +19,26 @@ fn fixture_auto_import(input: PathBuf) { test_fixture( get_syntax(), - &|_| visit_mut_pass(import_transform(AutoImport::Gem(true), false)), + &|_| { + visit_mut_pass(import_transform( + AutoImport::CustomContent(AutoImportContent { + extends: Some("gem".to_string()), + members: Some( + vec![( + "test".to_string(), + vec![MemberOrMemberAs::MemberAs([ + "name".to_string(), + "alias".to_string(), + ])], + )] + .into_iter() + .collect(), + ), + elements: None, + }), + AutoImportDts::Src(false), + )) + }, &input, &output, Default::default(), diff --git a/crates/swc-plugin-gem/tests/fixture/auto-import/members/input.ts b/crates/swc-plugin-gem/tests/fixture/auto-import/members/input.ts index 80c49b97..f6544a80 100644 --- a/crates/swc-plugin-gem/tests/fixture/auto-import/members/input.ts +++ b/crates/swc-plugin-gem/tests/fixture/auto-import/members/input.ts @@ -14,3 +14,4 @@ class MyElement extends GemElement { } } class MyElement1 extends (_GemElement = GemElement) {} +alias(); diff --git a/crates/swc-plugin-gem/tests/fixture/auto-import/members/output.ts b/crates/swc-plugin-gem/tests/fixture/auto-import/members/output.ts index be71526e..00bd3c8a 100644 --- a/crates/swc-plugin-gem/tests/fixture/auto-import/members/output.ts +++ b/crates/swc-plugin-gem/tests/fixture/auto-import/members/output.ts @@ -1,5 +1,6 @@ // @ts-nocheck -import { css, adoptedStyle, customElement, attribute, emitter, template, styleMap, html } from "@mantou/gem"; +import { css, adoptedStyle, customElement, attribute, emitter, template, html, styleMap } from "@mantou/gem"; +import { name as alias } from "test"; import { render, Emitter, GemElement } from '@mantou/gem'; const style = css``; @adoptedStyle(style) @@ -14,4 +15,5 @@ class MyElement extends GemElement { return html`
`; } } -class MyElement1 extends (_GemElement = GemElement) {} \ No newline at end of file +class MyElement1 extends (_GemElement = GemElement) {} +alias(); \ No newline at end of file diff --git a/crates/zed-plugin-gem/Cargo.toml b/crates/zed-plugin-gem/Cargo.toml index d83032d6..a351bd78 100644 --- a/crates/zed-plugin-gem/Cargo.toml +++ b/crates/zed-plugin-gem/Cargo.toml @@ -1,7 +1,9 @@ [package] name = "gem" version = "0.0.1" -edition = "2021" +edition = { workspace = true } +rust-version = { workspace = true } +publish = false [lib] crate-type = ["cdylib"] diff --git a/crates/zed-plugin-gem/extension.toml b/crates/zed-plugin-gem/extension.toml index 2b77df2e..394f0b6f 100644 --- a/crates/zed-plugin-gem/extension.toml +++ b/crates/zed-plugin-gem/extension.toml @@ -1,6 +1,6 @@ id = "gem" name = "Gem" -description = "Gem plugin for VS Code" +description = "Gem plugin for Zed" version = "0.0.1" schema_version = 1 authors = ["mantou132 <709922234@qq.com>"]