diff --git a/.eslintrc.js b/.eslintrc.js index 1c83eb91..e491db6d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -61,6 +61,7 @@ module.exports = { 'packages/gem-examples/src/**/*.ts', 'packages/gem-devtools/src/scripts/*.ts', 'packages/gem/src/helper/logger.ts', + 'packages/vscode-gem-plugin/esbuild.ts', ], rules: { 'no-console': 'off', diff --git a/crates/swc-plugin-gem/.gitignore b/crates/swc-plugin-gem/.gitignore new file mode 100644 index 00000000..36f5295c --- /dev/null +++ b/crates/swc-plugin-gem/.gitignore @@ -0,0 +1,4 @@ +# local test file +*.wasm +.swcrc +.swc diff --git a/crates/swc-plugin-gem/package.json b/crates/swc-plugin-gem/package.json index 87e59f94..cbf6e7cc 100644 --- a/crates/swc-plugin-gem/package.json +++ b/crates/swc-plugin-gem/package.json @@ -10,9 +10,13 @@ "main": "swc_plugin_gem.wasm", "files": [], "scripts": { + "install": "node -e \"require('@swc/core').transform('',{filename:'auto-import'})\"", "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 7e1bcd33..917bf51d 100644 --- a/crates/swc-plugin-gem/src/lib.rs +++ b/crates/swc-plugin-gem/src/lib.rs @@ -3,6 +3,7 @@ use swc_common::pass::Optional; use swc_core::ecma::visit::VisitMutWith; use swc_core::plugin::{plugin_transform, proxies::TransformPluginProgramMetadata}; use swc_ecma_ast::Program; +use visitors::import::gen_dts; pub use visitors::import::import_transform; pub use visitors::memo::memo_transform; pub use visitors::minify::minify_transform; @@ -12,6 +13,8 @@ mod visitors; #[derive(Deserialize, Debug, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] struct PluginConfig { + #[serde(default)] + pub style_minify: bool, #[serde(default)] pub auto_import: bool, #[serde(default)] @@ -19,9 +22,9 @@ struct PluginConfig { #[serde(default)] pub resolve_path: bool, #[serde(default)] - pub style_minify: bool, - #[serde(default)] pub esm_provider: String, + #[serde(default)] + pub hmr: bool, } #[plugin_transform] @@ -32,6 +35,10 @@ pub fn process_transform(mut program: Program, data: TransformPluginProgramMetad let config = serde_json::from_str::(plugin_config).expect("invalid config for gem plugin"); + if config.auto_import_dts { + gen_dts(); + } + program.visit_mut_with(&mut ( Optional { enabled: true, diff --git a/crates/swc-plugin-gem/src/visitors/import.rs b/crates/swc-plugin-gem/src/visitors/import.rs index 46f65761..e0e62358 100644 --- a/crates/swc-plugin-gem/src/visitors/import.rs +++ b/crates/swc-plugin-gem/src/visitors/import.rs @@ -1,12 +1,16 @@ use once_cell::sync::Lazy; use regex::Regex; use serde::Deserialize; -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + fs, + path::Path, +}; use swc_common::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, - ImportSpecifier, ModuleDecl, ModuleItem, Str, TaggedTpl, + ImportSpecifier, JSXElementName, ModuleDecl, ModuleItem, Str, TaggedTpl, }; static CUSTOM_ELEMENT_REGEX: Lazy = @@ -106,6 +110,12 @@ impl VisitMut for TransformVisitor { } } + fn visit_mut_jsx_element_name(&mut self, node: &mut JSXElementName) { + if let JSXElementName::Ident(ident) = node { + self.used_members.push(ident.to_name()); + } + } + fn visit_mut_decorator(&mut self, node: &mut Decorator) { node.visit_mut_children_with(self); @@ -173,6 +183,7 @@ impl VisitMut for TransformVisitor { } out.push(ImportDecl { specifiers, + // 也许可以支持替换:'@mantou/gem/{:pascal:}' + ColorPicker -> '@mantou/gem/ColorPicker' src: Box::new(Str::from(pkg)), span: DUMMY_SP, type_only: false, @@ -208,3 +219,28 @@ impl VisitMut for TransformVisitor { pub fn import_transform() -> impl VisitMut { TransformVisitor::default() } + +pub fn gen_dts() { + // 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"), + format!( + r#" + export {{}} + declare global {{ + {} + }} + "#, + import_list.join("\n") + ), + ) + .expect("create dts error"); +} diff --git a/crates/swc-plugin-gem/src/visitors/memo.rs b/crates/swc-plugin-gem/src/visitors/memo.rs index 4e3f23f7..6f026abe 100644 --- a/crates/swc-plugin-gem/src/visitors/memo.rs +++ b/crates/swc-plugin-gem/src/visitors/memo.rs @@ -39,9 +39,9 @@ fn is_memo_getter(node: &mut PrivateMethod) -> bool { } node.function.decorators.iter().any(|x| { - if let Expr::Call(ref call_expr) = *x.expr { - if let Callee::Expr(ref b) = call_expr.callee { - if let Expr::Ident(ref ident) = **b { + if let Some(call_expr) = x.expr.as_call() { + if let Callee::Expr(b) = &call_expr.callee { + if let Some(ident) = b.as_ident() { return ident.sym.as_str() == "memo"; } } diff --git a/crates/swc-plugin-gem/src/visitors/minify.rs b/crates/swc-plugin-gem/src/visitors/minify.rs index e59928c8..8ec583b3 100644 --- a/crates/swc-plugin-gem/src/visitors/minify.rs +++ b/crates/swc-plugin-gem/src/visitors/minify.rs @@ -1,5 +1,34 @@ +use once_cell::sync::Lazy; +use regex::Regex; +use swc_common::DUMMY_SP; use swc_core::ecma::visit::{noop_visit_mut_type, VisitMut, VisitMutWith}; -use swc_ecma_ast::TaggedTpl; +use swc_ecma_ast::{Callee, KeyValueProp, TaggedTpl, Tpl, TplElement}; + +static HEAD_REG: Lazy = Lazy::new(|| Regex::new(r"(?s)\s*(\{)\s*").unwrap()); +static TAIL_REG: Lazy = Lazy::new(|| Regex::new(r"(?s)(;|})\s+").unwrap()); +static SPACE_REG: Lazy = Lazy::new(|| Regex::new(r"(?s)\s+").unwrap()); + +fn minify_tpl(tpl: &Tpl) -> Tpl { + Tpl { + span: DUMMY_SP, + exprs: tpl.exprs.clone(), + quasis: tpl + .quasis + .iter() + .map(|x| { + let remove_head = &HEAD_REG.replace_all(x.raw.as_str(), "$1"); + let remove_tail = &TAIL_REG.replace_all(remove_head, "$1"); + let remove_space = SPACE_REG.replace_all(remove_tail, " "); + TplElement { + span: DUMMY_SP, + tail: x.tail, + cooked: None, + raw: remove_space.trim().into(), + } + }) + .collect(), + } +} #[derive(Default)] struct TransformVisitor {} @@ -10,8 +39,27 @@ impl VisitMut for TransformVisitor { fn visit_mut_tagged_tpl(&mut self, node: &mut TaggedTpl) { node.visit_mut_children_with(self); - for _ele in node.tpl.quasis.iter() { - // TODO: implement + if let Some(ident) = node.tag.as_ident() { + let tag_fn = ident.sym.as_str(); + if tag_fn == "css" || tag_fn == "styled" { + node.tpl = Box::new(minify_tpl(&node.tpl)); + } + } + } + + fn visit_mut_callee(&mut self, node: &mut Callee) { + if let Callee::Expr(expr) = &node { + if let Some(ident) = expr.as_ident() { + if ident.sym.as_str() == "css" { + node.visit_mut_children_with(self); + } + } + } + } + + fn visit_mut_key_value_prop(&mut self, node: &mut KeyValueProp) { + if let Some(tpl) = node.value.as_tpl() { + node.value = minify_tpl(tpl).into(); } } } diff --git a/crates/swc-plugin-gem/tests/fixture.rs b/crates/swc-plugin-gem/tests/fixture.rs index 515696b9..82e4f9fd 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}; +use swc_plugin_gem::{import_transform, memo_transform, minify_transform}; use testing::fixture; fn get_syntax() -> Syntax { @@ -38,3 +38,16 @@ fn fixture_memo(input: PathBuf) { Default::default(), ); } + +#[fixture("tests/fixture/minify/input.ts")] +fn fixture_minify(input: PathBuf) { + let output = input.parent().unwrap().join("output.ts"); + + test_fixture( + get_syntax(), + &|_| visit_mut_pass(minify_transform()), + &input, + &output, + Default::default(), + ); +} diff --git a/crates/swc-plugin-gem/tests/fixture/minify/input.ts b/crates/swc-plugin-gem/tests/fixture/minify/input.ts index 3ef36b32..6584b16c 100644 --- a/crates/swc-plugin-gem/tests/fixture/minify/input.ts +++ b/crates/swc-plugin-gem/tests/fixture/minify/input.ts @@ -1,6 +1,11 @@ // @ts-nocheck const style = css` :host { - color: red; + color: ${' red'}; } ` +const style2 = css({ + $: ` + color: ${' red'}; + ` +}) \ No newline at end of file diff --git a/crates/swc-plugin-gem/tests/fixture/minify/output.ts b/crates/swc-plugin-gem/tests/fixture/minify/output.ts index 1c464d00..3d0374a2 100644 --- a/crates/swc-plugin-gem/tests/fixture/minify/output.ts +++ b/crates/swc-plugin-gem/tests/fixture/minify/output.ts @@ -1,2 +1,5 @@ // @ts-nocheck -const style = css`:host {color: red;}` +const style = css`:host{color:${' red'};}`; +const style2 = css({ + $: `color:${' red'};` +}); \ No newline at end of file diff --git a/crates/zed-plugin-gem/extension.toml b/crates/zed-plugin-gem/extension.toml index 39bf0feb..2b77df2e 100644 --- a/crates/zed-plugin-gem/extension.toml +++ b/crates/zed-plugin-gem/extension.toml @@ -7,6 +7,7 @@ authors = ["mantou132 <709922234@qq.com>"] repository = "https://github.com/mantou132/gem" snippets = "./snippets/typescript.json" -[grammars.typescript] -repository = "https://github.com/tree-sitter/tree-sitter-typescript" -commit = "f975a621f4e7f532fe322e13c4f79495e0a7b2e7" +[language_servers.gem] +name = "Gem language server" +languages = ["TypeScript", "TSX", "JavaScript", "JSDoc"] +language_ids = { "TypeScript" = "typescript", "TSX" = "typescriptreact", "JavaScript" = "javascript" } diff --git a/crates/zed-plugin-gem/src/lib.rs b/crates/zed-plugin-gem/src/lib.rs index 1c7b630c..d95eda52 100644 --- a/crates/zed-plugin-gem/src/lib.rs +++ b/crates/zed-plugin-gem/src/lib.rs @@ -1,16 +1,97 @@ -use zed_extension_api as zed; +use std::{env, fs}; +use zed::settings::LspSettings; +use zed_extension_api::{self as zed, LanguageServerId, Result}; -struct MyExtension { - // ... state +const NPM_PKG_NAME: &str = "vscode-gem-languageservice"; +const LS_BIN_PATH: &str = "node_modules/.bin/vscode-gem-languageservice"; + +#[derive(Default)] +struct GemExtension { + did_find_server: bool, } -impl zed::Extension for MyExtension { - fn new() -> Self - where - Self: Sized, - { - MyExtension {} +impl GemExtension { + fn server_exists(&self) -> bool { + fs::metadata(LS_BIN_PATH).map_or(false, |stat| stat.is_file()) + } + + fn server_script_path(&mut self, language_server_id: &zed::LanguageServerId) -> Result { + let server_exists = self.server_exists(); + if self.did_find_server && server_exists { + return Ok(LS_BIN_PATH.to_string()); + } + + zed::set_language_server_installation_status( + language_server_id, + &zed::LanguageServerInstallationStatus::CheckingForUpdate, + ); + let version = zed::npm_package_latest_version(NPM_PKG_NAME)?; + + if !server_exists + || zed::npm_package_installed_version(NPM_PKG_NAME)?.as_ref() != Some(&version) + { + zed::set_language_server_installation_status( + language_server_id, + &zed::LanguageServerInstallationStatus::Downloading, + ); + let result = zed::npm_install_package(NPM_PKG_NAME, &version); + match result { + Ok(()) => { + if !self.server_exists() { + Err(format!( + "installed package '{NPM_PKG_NAME}' did not contain expected path '{LS_BIN_PATH}'", + ))?; + } + } + Err(error) => { + if !self.server_exists() { + Err(error)?; + } + } + } + } + + self.did_find_server = true; + Ok(LS_BIN_PATH.to_string()) + } +} + +impl zed::Extension for GemExtension { + fn new() -> Self { + Self::default() + } + + fn language_server_command( + &mut self, + language_server_id: &LanguageServerId, + _worktree: &zed::Worktree, + ) -> Result { + let server_path = self.server_script_path(language_server_id)?; + Ok(zed::Command { + command: zed::node_binary_path()?, + args: vec![ + env::current_dir() + .unwrap() + .join(&server_path) + .to_string_lossy() + .to_string(), + "--stdio".to_string(), + ], + env: Default::default(), + }) + } + + fn language_server_workspace_configuration( + &mut self, + server_id: &LanguageServerId, + worktree: &zed::Worktree, + ) -> Result> { + let settings = LspSettings::for_worktree(server_id.as_ref(), worktree) + .ok() + .and_then(|lsp_settings| lsp_settings.settings.clone()) + .unwrap_or_default(); + Ok(Some(settings)) } } -zed::register_extension!(MyExtension); +zed::register_extension!(GemExtension); diff --git a/packages/language-service/package.json b/packages/language-service/package.json index 04f7d2b0..abab4203 100644 --- a/packages/language-service/package.json +++ b/packages/language-service/package.json @@ -8,9 +8,7 @@ ], "type": "module", "module": "src/index.ts", - "bin": { - "gem-language-server": "src/index.ts" - }, + "bin": "src/index.ts", "files": [ "/src/" ], diff --git a/packages/vscode-gem-plugin/esbuild.js b/packages/vscode-gem-plugin/esbuild.js deleted file mode 100644 index ed9d8b3c..00000000 --- a/packages/vscode-gem-plugin/esbuild.js +++ /dev/null @@ -1,55 +0,0 @@ -const esbuild = require('esbuild'); - -const production = process.argv.includes('--production'); -const watch = process.argv.includes('--watch'); - -/** - * @type {import('esbuild').Plugin} - */ -const esbuildProblemMatcherPlugin = { - name: 'esbuild-problem-matcher', - - setup(build) { - build.onStart(() => { - console.log('[watch] build started'); - }); - build.onEnd((result) => { - result.errors.forEach(({ text, location }) => { - console.error(`✘ [ERROR] ${text}`); - console.error(` ${location.file}:${location.line}:${location.column}:`); - }); - console.log('[watch] build finished'); - }); - }, -}; - -async function main() { - const ctx = await esbuild.context({ - entryPoints: ['src/extension.ts', 'src/server.ts'], - bundle: true, - format: 'cjs', - minify: production, - sourcemap: !production, - sourcesContent: false, - platform: 'node', - outdir: 'dist', - external: ['vscode'], - logLevel: 'silent', - mainFields: ['module', 'main'], - plugins: [ - /* add to the end of plugins array */ - esbuildProblemMatcherPlugin, - ], - }); - if (watch) { - await ctx.watch(); - } else { - await ctx.rebuild(); - await ctx.dispose(); - } -} - -main().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/packages/vscode-gem-plugin/esbuild.ts b/packages/vscode-gem-plugin/esbuild.ts new file mode 100644 index 00000000..a98b0ebf --- /dev/null +++ b/packages/vscode-gem-plugin/esbuild.ts @@ -0,0 +1,50 @@ +import { context } from 'esbuild'; + +const production = process.argv.includes('--production'); +const watch = process.argv.includes('--watch'); + +async function main() { + const ctx = await context({ + entryPoints: ['src/extension.ts', 'src/server.ts'], + bundle: true, + format: 'cjs', + minify: production, + sourcemap: !production, + sourcesContent: false, + platform: 'node', + outdir: 'dist', + external: ['vscode'], + logLevel: 'silent', + mainFields: ['module', 'main'], + plugins: [ + { + name: 'esbuild-problem-matcher', + setup(build) { + build.onStart(() => { + console.log('[watch] build started'); + }); + build.onEnd((result) => { + result.errors.forEach(({ text, location }) => { + console.error(`✘ [ERROR] ${text}`); + if (location) { + console.error(` ${location.file}:${location.line}:${location.column}:`); + } + }); + console.log('[watch] build finished'); + }); + }, + }, + ], + }); + if (watch) { + await ctx.watch(); + } else { + await ctx.rebuild(); + await ctx.dispose(); + } +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/packages/vscode-gem-plugin/package.json b/packages/vscode-gem-plugin/package.json index ca8ec058..41d9718b 100644 --- a/packages/vscode-gem-plugin/package.json +++ b/packages/vscode-gem-plugin/package.json @@ -150,7 +150,7 @@ ] }, "scripts": { - "compile": "node esbuild.js", + "compile": "node --experimental-transform-types esbuild.ts", "package": "pnpm compile --production", "watch": "pnpm compile --watch", "pretest": "tsc -p . --outDir out && pnpm compile",