diff --git a/Cargo.lock b/Cargo.lock index 10ef754b..9ba064e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1070,6 +1070,22 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "node-builtins" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "237ad7a9faa3cd869e4bb27d19c2c8a3127d66401cdc4c724640cac9be340761" + +[[package]] +name = "node-resolve" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6a90e3b24b73097866f78daad37dba3bc693222b3b59fbdc4dd5adb3c8e2399" +dependencies = [ + "node-builtins", + "serde_json", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1219,6 +1235,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -2310,7 +2332,9 @@ name = "swc_plugin_gem" version = "0.1.0" dependencies = [ "indexmap", + "node-resolve", "once_cell", + "pathdiff", "regex", "serde", "serde_json", @@ -2321,6 +2345,7 @@ dependencies = [ "swc_ecma_visit", "testing 0.42.1", "tracing", + "typed-path", ] [[package]] @@ -2738,6 +2763,12 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" +[[package]] +name = "typed-path" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82205ffd44a9697e34fc145491aa47310f9871540bb7909eaa9365e0a9a46607" + [[package]] name = "typenum" version = "1.17.0" diff --git a/Cargo.toml b/Cargo.toml index 2533fdc0..901c53f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ license = "Apache-2.0" repository = "https://github.com/mantou132/gem.git" [workspace.dependencies] +node-resolve = "2.2.0" indexmap = { version = "2.6.0" } regex = { version = "1.10.4", default-features = false } serde = "1.0.203" @@ -24,3 +25,5 @@ swc_common = "4.0.0" testing = "0.42.0" once_cell = "1.19.0" tracing = "0.1.40" +pathdiff = "0.2.3" +typed-path = { version = "0.9.3", default-features = false } diff --git a/crates/swc-plugin-gem/.gitignore b/crates/swc-plugin-gem/.gitignore index 36f5295c..894144f5 100644 --- a/crates/swc-plugin-gem/.gitignore +++ b/crates/swc-plugin-gem/.gitignore @@ -2,3 +2,4 @@ *.wasm .swcrc .swc +*.d.ts \ No newline at end of file diff --git a/crates/swc-plugin-gem/Cargo.toml b/crates/swc-plugin-gem/Cargo.toml index 60a50b58..7410c390 100644 --- a/crates/swc-plugin-gem/Cargo.toml +++ b/crates/swc-plugin-gem/Cargo.toml @@ -12,6 +12,7 @@ crate-type = ["cdylib", "rlib"] lto = true [dependencies] +node-resolve = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } swc_core = { workspace = true, features = ["ecma_plugin_transform"] } @@ -22,6 +23,8 @@ once_cell = { workspace = true } tracing = { workspace = true } regex = { workspace = true } indexmap = { workspace = true, features = ["serde"] } +pathdiff = { workspace = true } +typed-path = { workspace = true } [dev-dependencies] swc_ecma_parser = { workspace = true } diff --git a/crates/swc-plugin-gem/package.json b/crates/swc-plugin-gem/package.json index d97272cd..013d080a 100644 --- a/crates/swc-plugin-gem/package.json +++ b/crates/swc-plugin-gem/package.json @@ -10,13 +10,9 @@ "main": "swc_plugin_gem.wasm", "files": [], "scripts": { - "install": "node -e \"require('@swc/core').transform('',{filename:'try-auto-import-dts'})\"", "prepublishOnly": "cross-env CARGO_TARGET_DIR=target cargo build-wasi --release && cp target/wasm32-wasip1/release/swc_plugin_gem.wasm .", "test": "cross-env RUST_LOG=info cargo watch -x test" }, - "devDependencies": { - "@swc/core": "^1.9.3" - }, "preferUnplugged": true, "author": "mantou132", "license": "ISC", diff --git a/crates/swc-plugin-gem/src/lib.rs b/crates/swc-plugin-gem/src/lib.rs index 07e20cb8..1258d04c 100644 --- a/crates/swc-plugin-gem/src/lib.rs +++ b/crates/swc-plugin-gem/src/lib.rs @@ -23,6 +23,7 @@ struct PluginConfig { /// 在安装时会尝试读取 .swcrc 生成,有些项目没有 .swcrc 文件,需要在正式变异时生成 pub auto_import_dts: bool, #[serde(default)] + /// 配合 import map 直接使用 esm pub resolve_path: bool, #[serde(default)] pub hmr: bool, @@ -36,7 +37,7 @@ pub fn process_transform(mut program: Program, data: TransformPluginProgramMetad let config = serde_json::from_str::(plugin_config).expect("invalid config for gem plugin"); - let _file_name = data.get_context(&TransformPluginMetadataContextKind::Filename); + let filename = data.get_context(&TransformPluginMetadataContextKind::Filename); // 执行在每个文件 if config.auto_import_dts { @@ -58,7 +59,7 @@ pub fn process_transform(mut program: Program, data: TransformPluginProgramMetad }, Optional { enabled: config.resolve_path, - visitor: path_transform(), + visitor: path_transform(filename.clone()), }, )); diff --git a/crates/swc-plugin-gem/src/visitors/import.rs b/crates/swc-plugin-gem/src/visitors/import.rs index 2045f7ea..735f1810 100644 --- a/crates/swc-plugin-gem/src/visitors/import.rs +++ b/crates/swc-plugin-gem/src/visitors/import.rs @@ -1,13 +1,12 @@ -use indexmap::IndexMap; +use indexmap::{IndexMap, IndexSet}; use once_cell::sync::Lazy; use regex::Regex; use serde::Deserialize; use std::{ collections::{HashMap, HashSet}, fs, - path::Path, }; -use swc_common::DUMMY_SP; +use swc_common::{SyntaxContext, DUMMY_SP}; use swc_core::ecma::visit::{noop_visit_mut_type, VisitMut, VisitMutWith}; use swc_ecma_ast::{ Callee, Class, ClassDecl, ClassExpr, Decorator, Ident, ImportDecl, ImportNamedSpecifier, @@ -81,16 +80,24 @@ impl IdentString for Ident { #[derive(Default)] pub struct TransformVisitor { - used_members: Vec, - defined_members: HashSet, - used_elements: Vec, + used_members: HashMap, + imported_members: HashSet, + used_elements: IndexSet, } impl TransformVisitor { + fn inset_used_member(&mut self, ident: &Ident) { + let name = ident.to_name(); + // 只保存顶层使用成员,会复用 SyntaxContext + if !self.used_members.contains_key(&name) { + self.used_members.insert(name, ident.ctxt); + } + } + fn visit_mut_class(&mut self, node: &Box) { if let Some(expr) = &node.super_class { if let Some(ident) = expr.as_ident() { - self.used_members.push(ident.to_name()); + self.inset_used_member(ident); } } } @@ -100,20 +107,20 @@ impl VisitMut for TransformVisitor { noop_visit_mut_type!(); fn visit_mut_import_specifier(&mut self, node: &mut ImportSpecifier) { - self.defined_members.insert(node.local().to_name()); + self.imported_members.insert(node.local().to_name()); } fn visit_mut_callee(&mut self, node: &mut Callee) { if let Callee::Expr(expr) = &node { if let Some(ident) = expr.as_ident() { - self.used_members.push(ident.to_name()); + self.inset_used_member(ident); } } } fn visit_mut_jsx_element_name(&mut self, node: &mut JSXElementName) { if let JSXElementName::Ident(ident) = node { - self.used_members.push(ident.to_name()); + self.inset_used_member(ident); } } @@ -121,7 +128,7 @@ impl VisitMut for TransformVisitor { node.visit_mut_children_with(self); if let Some(ident) = node.expr.as_ident() { - self.used_members.push(ident.to_name()); + self.inset_used_member(ident); } } @@ -129,12 +136,12 @@ impl VisitMut for TransformVisitor { node.visit_mut_children_with(self); if let Some(ident) = node.tag.as_ident() { - self.used_members.push(ident.to_name()); + self.inset_used_member(ident); } for ele in node.tpl.quasis.iter() { for cap in CUSTOM_ELEMENT_REGEX.captures_iter(ele.raw.as_str()) { - self.used_elements.push(cap["tag"].to_string()); + self.used_elements.insert(cap["tag"].to_string()); } } } @@ -158,25 +165,26 @@ 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: HashMap> = + HashMap::new(); - for used_member in self.used_members.iter() { - if !self.defined_members.contains(used_member) { + for (used_member, ctx) in self.used_members.iter() { + if !self.imported_members.contains(used_member) { let pkg = GEM_AUTO_IMPORT_CONFIG.member_map.get(used_member); if let Some(pkg) = pkg { let set = available_import .entry(pkg.into()) .or_insert(Default::default()); - set.insert(used_member.into(), used_member.into()); + set.insert(used_member.into(), (used_member.into(), ctx)); } } } for (pkg, set) in available_import { let mut specifiers: Vec = vec![]; - for (member, _) in set { + for (member, (_, ctx)) in set { specifiers.push(ImportSpecifier::Named(ImportNamedSpecifier { - local: member.into(), + local: Ident::new(member.into(), DUMMY_SP, ctx.clone()), span: DUMMY_SP, imported: None, is_type_only: false, @@ -231,19 +239,18 @@ pub fn gen_once_dts() { } GEN_DTS = true; } - // https://github.com/swc-project/swc/discussions/4997 - let types_dir = "/cwd/node_modules/@types/auto-import"; 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}'];", )); } - fs::create_dir_all(types_dir).expect("create auto import dir error"); fs::write( - Path::new(types_dir).join("index.d.ts"), + // 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 {{ {} diff --git a/crates/swc-plugin-gem/src/visitors/path.rs b/crates/swc-plugin-gem/src/visitors/path.rs index 3d20e20b..4b2efb7a 100644 --- a/crates/swc-plugin-gem/src/visitors/path.rs +++ b/crates/swc-plugin-gem/src/visitors/path.rs @@ -1,20 +1,52 @@ -///! 验证模块是否为文件,是就添加 .js 否就添加 /index.js -///! 识别模块是否为相对路径,如何是 ts 需要处理 +use std::{env, path::PathBuf}; + +use node_resolve::Resolver; +use pathdiff::diff_paths; 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 resolve_path(origin: &str) -> Str { - return format!("{}.js", origin).into(); +fn converting(path_buf: &PathBuf) -> String { + let windows_path = Utf8Path::::new(path_buf.to_str().unwrap()); + windows_path.with_encoding::().to_string() } #[derive(Default)] -struct TransformVisitor {} +struct TransformVisitor { + filename: Option, +} + +impl TransformVisitor { + fn resolve_path(&self, origin: &str) -> Str { + if let Some(filename) = &self.filename { + 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_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) = relative_path.to_str() { + let relative_path = relative_path.replace(".ts", ".js"); + if !relative_path.starts_with(".") { + return format!("./{}", relative_path).into(); + } else { + return relative_path.into(); + } + } + } + } + } + origin.into() + } +} impl VisitMut for TransformVisitor { noop_visit_mut_type!(); fn visit_mut_import_decl(&mut self, node: &mut ImportDecl) { - node.src = resolve_path(node.src.value.as_str()).into(); + node.src = self.resolve_path(node.src.value.as_str()).into(); } // 只处理 string 的动态导入 @@ -23,13 +55,16 @@ impl VisitMut for TransformVisitor { if let Some(Some(Lit::Str(source))) = node.args.get(0).map(|e| e.expr.as_lit()) { node.args = vec![ExprOrSpread { spread: None, - expr: resolve_path(source.value.as_str()).into(), + expr: self.resolve_path(source.value.as_str()).into(), }] } } } } -pub fn path_transform() -> impl VisitMut { - TransformVisitor::default() +pub fn path_transform(filename: Option) -> impl VisitMut { + TransformVisitor { + filename, + ..Default::default() + } } diff --git a/crates/swc-plugin-gem/tests/fixture.rs b/crates/swc-plugin-gem/tests/fixture.rs index f34b2091..d2b263a2 100644 --- a/crates/swc-plugin-gem/tests/fixture.rs +++ b/crates/swc-plugin-gem/tests/fixture.rs @@ -58,7 +58,11 @@ fn fixture_path(input: PathBuf) { test_fixture( get_syntax(), - &|_| visit_mut_pass(path_transform()), + &|_| { + visit_mut_pass(path_transform(Some( + "tests/fixture/path/input.ts".to_string(), + ))) + }, &input, &output, Default::default(), diff --git a/crates/swc-plugin-gem/tests/fixture/auto-import/elements/input.ts b/crates/swc-plugin-gem/tests/fixture/auto-import/elements/input.ts index a41d2cb7..bc3b7b88 100644 --- a/crates/swc-plugin-gem/tests/fixture/auto-import/elements/input.ts +++ b/crates/swc-plugin-gem/tests/fixture/auto-import/elements/input.ts @@ -3,6 +3,7 @@ export class MyElement extends GemElement { render() { return html` + ${html``} diff --git a/crates/swc-plugin-gem/tests/fixture/auto-import/elements/output.ts b/crates/swc-plugin-gem/tests/fixture/auto-import/elements/output.ts index 8040f5e6..c641b813 100644 --- a/crates/swc-plugin-gem/tests/fixture/auto-import/elements/output.ts +++ b/crates/swc-plugin-gem/tests/fixture/auto-import/elements/output.ts @@ -7,6 +7,7 @@ export class MyElement extends GemElement { render() { return html` + ${html``} `; diff --git a/crates/swc-plugin-gem/tests/fixture/path/input.ts b/crates/swc-plugin-gem/tests/fixture/path/input.ts index 6078e99f..abcc7247 100644 --- a/crates/swc-plugin-gem/tests/fixture/path/input.ts +++ b/crates/swc-plugin-gem/tests/fixture/path/input.ts @@ -1,3 +1,2 @@ // @ts-nocheck -import '@mantou/gem'; -import '@mantou/gem/helper/react-shim'; +import './output'; \ No newline at end of file diff --git a/crates/swc-plugin-gem/tests/fixture/path/output.ts b/crates/swc-plugin-gem/tests/fixture/path/output.ts index 0af3c23d..2829f681 100644 --- a/crates/swc-plugin-gem/tests/fixture/path/output.ts +++ b/crates/swc-plugin-gem/tests/fixture/path/output.ts @@ -1,3 +1,2 @@ // @ts-nocheck -import "@mantou/gem.js"; -import "@mantou/gem/helper/react-shim.js"; +import "./output.js"; \ No newline at end of file