Repository: swc-project/swc (rust)

Issue:
Plugin template scaffold generates package.json for easier publish
### Describe the feature

https://github.com/swc-project/swc/discussions/3540#discussioncomment-2163346

When we create a plugin via template, we expect it'll be published as npm package in most cases. Having default package.json would be helpful. 

We could do

- create package.json by default 
- having a flag to indicate to create package.json.

For now I'm leaning on first approach, as said expect most of cases we expect plugin will be published as npm pkg and installed next to `@swc/core`.

### Babel plugin or link to the feature description

_No response_

### Additional context

_No response_

Pull request description:
feat(swc/plugin_runner): improve resolver support for npm
<!-- Thanks for submitting a pull request! Please provide enough information so that others can review your pull request. -->

**Description:**

PR attempts to close #3548 to generate simple package.json.

Also change includes resolver changes for the plugin. Now plugin is a single packge to .wasm instead of having per-platform optionaldeps, instead of using custom resolver tries to reuse existing node_resolver. It seems to work reasonably, however might need second eye if it's right location to create resolver.

**Related issue (if exists):**

- closes https://github.com/swc-project/swc/issues/3548

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 5523df3f27..fb11a8b392 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3470,6 +3470,7 @@ dependencies = [
  "swc_atoms",
  "swc_common",
  "swc_ecma_ast",
+ "swc_ecma_loader",
  "swc_ecma_parser",
  "swc_ecma_visit",
  "testing",
diff --git a/crates/swc/Cargo.toml b/crates/swc/Cargo.toml
index a50aa45570..a47aecf592 100644
--- a/crates/swc/Cargo.toml
+++ b/crates/swc/Cargo.toml
@@ -39,7 +39,7 @@ dashmap = "4.0.2"
 either = "1"
 indexmap = {version = "1", features = ["serde"]}
 lru = "0.7.1"
-once_cell = "1"
+once_cell = "1.9.0"
 pathdiff = "0.2.0"
 regex = "1"
 serde = {version = "1", features = ["derive"]}
diff --git a/crates/swc/src/config/mod.rs b/crates/swc/src/config/mod.rs
index 43aac163a0..4e39921685 100644
--- a/crates/swc/src/config/mod.rs
+++ b/crates/swc/src/config/mod.rs
@@ -32,8 +32,9 @@ use swc_ecma_lints::{
     config::LintConfig,
     rules::{lint_to_fold, LintParams},
 };
-use swc_ecma_loader::resolvers::{
-    lru::CachingResolver, node::NodeModulesResolver, tsc::TsConfigResolver,
+use swc_ecma_loader::{
+    resolvers::{lru::CachingResolver, node::NodeModulesResolver, tsc::TsConfigResolver},
+    TargetEnv,
 };
 use swc_ecma_minifier::option::{
     terser::{TerserCompressorOptions, TerserEcmaVersion, TerserTopLevelOptions},
@@ -419,6 +420,11 @@ impl Options {
                 comments,
             );
 
+        let plugin_resolver = CachingResolver::new(
+            40,
+            NodeModulesResolver::new(TargetEnv::Node, Default::default(), true),
+        );
+
         let pass = chain!(
             lint_to_fold(swc_ecma_lints::rules::all(LintParams {
                 program: &program,
@@ -451,7 +457,7 @@ impl Options {
                 ),
                 syntax.typescript()
             ),
-            crate::plugin::plugins(experimental),
+            crate::plugin::plugins(plugin_resolver, experimental),
             custom_before_pass(&program),
             // handle jsx
             Optional::new(
diff --git a/crates/swc/src/plugin.rs b/crates/swc/src/plugin.rs
index bcec5c1e90..f5e34aed37 100644
--- a/crates/swc/src/plugin.rs
+++ b/crates/swc/src/plugin.rs
@@ -6,6 +6,7 @@
 use serde::{Deserialize, Serialize};
 #[cfg(feature = "plugin")]
 use swc_ecma_ast::*;
+use swc_ecma_loader::resolvers::{lru::CachingResolver, node::NodeModulesResolver};
 #[cfg(not(feature = "plugin"))]
 use swc_ecma_transforms::pass::noop;
 use swc_ecma_visit::{noop_fold_type, Fold};
@@ -14,13 +15,17 @@ use swc_ecma_visit::{noop_fold_type, Fold};
 #[serde(deny_unknown_fields, rename_all = "camelCase")]
 pub struct PluginConfig(String, serde_json::Value);
 
-pub fn plugins(config: crate::config::JscExperimental) -> impl Fold {
+pub fn plugins(
+    resolver: CachingResolver<NodeModulesResolver>,
+    config: crate::config::JscExperimental,
+) -> impl Fold {
     #[cfg(feature = "plugin")]
     {
         let cache_root =
             swc_plugin_runner::resolve::resolve_plugin_cache_root(config.cache_root).ok();
 
         RustPlugins {
+            resolver,
             plugins: config.plugins,
             plugin_cache: cache_root,
         }
@@ -33,6 +38,7 @@ pub fn plugins(config: crate::config::JscExperimental) -> impl Fold {
 }
 
 struct RustPlugins {
+    resolver: CachingResolver<NodeModulesResolver>,
     plugins: Option<Vec<PluginConfig>>,
     /// TODO: it is unclear how we'll support plugin itself in wasm target of
     /// swc, as well as cache.
@@ -43,8 +49,11 @@ struct RustPlugins {
 impl RustPlugins {
     #[cfg(feature = "plugin")]
     fn apply(&mut self, n: Program) -> Result<Program, anyhow::Error> {
+        use std::{path::PathBuf, sync::Arc};
+
         use anyhow::Context;
-        use swc_common::plugin::Serialized;
+        use swc_common::{plugin::Serialized, FileName};
+        use swc_ecma_loader::resolve::Resolve;
 
         let mut serialized = Serialized::serialize(&n)?;
 
@@ -60,7 +69,15 @@ impl RustPlugins {
                     .context("failed to serialize plugin config as json")?;
                 let config_json = Serialized::serialize(&config_json)?;
 
-                let path = swc_plugin_runner::resolve::resolve(&p.0)?;
+                let resolved_path = self
+                    .resolver
+                    .resolve(&FileName::Real(PathBuf::from(&p.0)), &p.0)?;
+
+                let path = if let FileName::Real(value) = resolved_path {
+                    Arc::new(value)
+                } else {
+                    anyhow::bail!("Failed to resolve plugin path: {:?}", resolved_path);
+                };
 
                 serialized = swc_plugin_runner::apply_js_plugin(
                     &p.0,
diff --git a/crates/swc_bundler/Cargo.toml b/crates/swc_bundler/Cargo.toml
index 9428904497..c120de0bc5 100644
--- a/crates/swc_bundler/Cargo.toml
+++ b/crates/swc_bundler/Cargo.toml
@@ -33,7 +33,7 @@ crc = "1.8"
 dashmap = {version = "4.0.2", optional = true}
 indexmap = "1.6"
 is-macro = "0.2.0"
-once_cell = "1"
+once_cell = "1.9.0"
 parking_lot = "0.11"
 petgraph = "0.5"
 radix_fmt = "1"
diff --git a/crates/swc_cli/src/commands/plugin.rs b/crates/swc_cli/src/commands/plugin.rs
index f8f32cfa95..c259074a5b 100644
--- a/crates/swc_cli/src/commands/plugin.rs
+++ b/crates/swc_cli/src/commands/plugin.rs
@@ -181,6 +181,34 @@ target = "{}""#,
         )
         .context("failed to write config toml file")?;
 
+        // Create package.json for npm package publishing.
+        let dist_output_path = format!("target/{}/release/{}.wasm", build_target, name);
+        fs::write(
+            &path.join("package.json"),
+            format!(
+                r#"{{
+    "name": "{}",
+    "version": "0.1.0",
+    "description": "",
+    "main": "{}",
+    "scripts": {{
+        "test": "echo \"Error: no test specified\" && exit 1"
+    }},
+    "keywords": [],
+    "author": "",
+    "license": "ISC",
+    "files": [
+        "{}",
+        "README.md"
+    ]
+}}
+"#,
+                name, dist_output_path, dist_output_path
+            )
+            .as_bytes(),
+        )
+        .context("failed to write Cargo.toml file")?;
+
         // Create entrypoint src file
         let src_path = path.join("src");
         create_dir_all(&src_path)?;
diff --git a/crates/swc_common/Cargo.toml b/crates/swc_common/Cargo.toml
index e37022792c..431dd98657 100644
--- a/crates/swc_common/Cargo.toml
+++ b/crates/swc_common/Cargo.toml
@@ -37,7 +37,7 @@ debug_unreachable = "0.1.1"
 either = "1.5"
 from_variant = {version = "0.1.3", path = "../from_variant"}
 num-bigint = "0.2"
-once_cell = "1"
+once_cell = "1.9.0"
 owning_ref = "0.4"
 parking_lot = {version = "0.11", optional = true}
 rkyv = {version = "0.7.28", optional = true}
diff --git a/crates/swc_ecma_codegen/Cargo.toml b/crates/swc_ecma_codegen/Cargo.toml
index 54a44cb55e..c7927a5ed5 100644
--- a/crates/swc_ecma_codegen/Cargo.toml
+++ b/crates/swc_ecma_codegen/Cargo.toml
@@ -13,7 +13,7 @@ version = "0.90.0"
 bitflags = "1"
 memchr = "2.4.1"
 num-bigint = {version = "0.2", features = ["serde"]}
-once_cell = "1.8.0"
+once_cell = "1.9.0"
 sourcemap = "6"
 swc_atoms = {version = "0.2", path = "../swc_atoms"}
 swc_common = {version = "0.17.3", path = "../swc_common"}
diff --git a/crates/swc_ecma_loader/Cargo.toml b/crates/swc_ecma_loader/Cargo.toml
index 3e336b7e31..49620062ba 100644
--- a/crates/swc_ecma_loader/Cargo.toml
+++ b/crates/swc_ecma_loader/Cargo.toml
@@ -25,7 +25,7 @@ ahash = "0.7.4"
 anyhow = "1.0.41"
 dashmap = {version = "4.0.2", optional = true}
 lru = {version = "0.7.1", optional = true}
-once_cell = {version = "1.8.0", optional = true}
+once_cell = {version = "1.9.0", optional = true}
 path-clean = {version = "=0.1.0", optional = true}
 regex = {version = "1", optional = true}
 serde = {version = "1", features = ["derive"]}
diff --git a/crates/swc_ecma_minifier/Cargo.toml b/crates/swc_ecma_minifier/Cargo.toml
index 4afd510c24..3d756944ae 100644
--- a/crates/swc_ecma_minifier/Cargo.toml
+++ b/crates/swc_ecma_minifier/Cargo.toml
@@ -20,7 +20,7 @@ debug = ["backtrace"]
 ahash = "0.7.6"
 backtrace = {version = "0.3.61", optional = true}
 indexmap = "1.7.0"
-once_cell = "1.5.2"
+once_cell = "1.9.0"
 pretty_assertions = {version = "0.7.2", optional = true}
 rayon = "1.5.1"
 regex = "1.5.3"
diff --git a/crates/swc_ecma_preset_env/Cargo.toml b/crates/swc_ecma_preset_env/Cargo.toml
index 24e5485656..c960fb397c 100644
--- a/crates/swc_ecma_preset_env/Cargo.toml
+++ b/crates/swc_ecma_preset_env/Cargo.toml
@@ -15,7 +15,7 @@ anyhow = "1"
 browserslist-rs = "=0.7.0"
 dashmap = "4.0.2"
 indexmap = "1.6.2"
-once_cell = "1.2.0"
+once_cell = "1.9.0"
 semver = {version = "1.0.4", features = ["serde"]}
 serde = {version = "1", features = ["derive"]}
 serde_json = "1"
diff --git a/crates/swc_ecma_transforms_base/Cargo.toml b/crates/swc_ecma_transforms_base/Cargo.toml
index a3fa641083..bfb4e81d85 100644
--- a/crates/swc_ecma_transforms_base/Cargo.toml
+++ b/crates/swc_ecma_transforms_base/Cargo.toml
@@ -15,7 +15,7 @@ concurrent = [
 ]
 
 [dependencies]
-once_cell = "1.5.2"
+once_cell = "1.9.0"
 phf = {version = "0.8.0", features = ["macros"]}
 rayon = {version = "1", optional = true}
 scoped-tls = "1.0.0"
diff --git a/crates/swc_ecma_transforms_optimization/Cargo.toml b/crates/swc_ecma_transforms_optimization/Cargo.toml
index e64e56051d..daf7a25757 100644
--- a/crates/swc_ecma_transforms_optimization/Cargo.toml
+++ b/crates/swc_ecma_transforms_optimization/Cargo.toml
@@ -22,7 +22,7 @@ concurrent = [
 ahash = "0.7.4"
 dashmap = "4.0.2"
 indexmap = "1.6.1"
-once_cell = "1.5.2"
+once_cell = "1.9.0"
 rayon = {version = "1.5.1", optional = true}
 serde_json = "1.0.61"
 swc_atoms = {version = "0.2", path = "../swc_atoms"}
diff --git a/crates/swc_ecma_transforms_react/Cargo.toml b/crates/swc_ecma_transforms_react/Cargo.toml
index 0eb4b13d9e..623820c0bd 100644
--- a/crates/swc_ecma_transforms_react/Cargo.toml
+++ b/crates/swc_ecma_transforms_react/Cargo.toml
@@ -15,7 +15,7 @@ ahash = "0.7.4"
 base64 = "0.13.0"
 dashmap = "4.0.2"
 indexmap = "1.6.1"
-once_cell = "1.5.2"
+once_cell = "1.9.0"
 regex = "1.4.2"
 serde = {version = "1.0.118", features = ["derive"]}
 sha-1 = "0.9.4"
diff --git a/crates/swc_ecma_utils/Cargo.toml b/crates/swc_ecma_utils/Cargo.toml
index 69e556ac7d..d93cda0e03 100644
--- a/crates/swc_ecma_utils/Cargo.toml
+++ b/crates/swc_ecma_utils/Cargo.toml
@@ -21,7 +21,7 @@ concurrent = [
 
 [dependencies]
 indexmap = "1"
-once_cell = "1"
+once_cell = "1.9.0"
 rayon = {version = "1.5.1", optional = true}
 swc_atoms = {version = "0.2.0", path = "../swc_atoms"}
 swc_common = {version = "0.17.0", path = "../swc_common"}
diff --git a/crates/swc_node_bundler/Cargo.toml b/crates/swc_node_bundler/Cargo.toml
index f5b600c249..878a0830c1 100644
--- a/crates/swc_node_bundler/Cargo.toml
+++ b/crates/swc_node_bundler/Cargo.toml
@@ -19,7 +19,7 @@ swc_v2 = []
 anyhow = "1"
 dashmap = "4.0.2"
 is-macro = "0.2.0"
-once_cell = "1"
+once_cell = "1.9.0"
 regex = "1"
 serde = {version = "1", features = ["derive"]}
 serde_json = "1"
diff --git a/crates/swc_plugin_runner/Cargo.toml b/crates/swc_plugin_runner/Cargo.toml
index c1c71e62fb..1b2dca6cab 100644
--- a/crates/swc_plugin_runner/Cargo.toml
+++ b/crates/swc_plugin_runner/Cargo.toml
@@ -11,13 +11,14 @@ version = "0.32.0"
 [dependencies]
 anyhow = "1.0.42"
 libloading = "0.7.0"
-once_cell = "1.8.0"
+once_cell = "1.9.0"
 parking_lot = "0.11"
 serde = {version = "1.0.126", features = ["derive"]}
 serde_json = "1.0.64"
 swc_atoms = {version = "0.2.7", path = '../swc_atoms'}
 swc_common = {version = "0.17.0", path = "../swc_common", features = ["plugin-rt"]}
 swc_ecma_ast = {version = "0.65.0", path = "../swc_ecma_ast", features = ["rkyv-impl"]}
+swc_ecma_loader = { version = "0.28.0", path = "../swc_ecma_loader" }
 swc_ecma_parser = {version = "0.88.0", path = "../swc_ecma_parser"}
 wasmer = "2.1.1"
 wasmer-cache = "2.1.1"
diff --git a/crates/swc_plugin_runner/src/resolve.rs b/crates/swc_plugin_runner/src/resolve.rs
index a886167ed3..fd907cf15e 100644
--- a/crates/swc_plugin_runner/src/resolve.rs
+++ b/crates/swc_plugin_runner/src/resolve.rs
@@ -1,15 +1,6 @@
-use std::{
-    env::current_dir,
-    fs::read_to_string,
-    path::{Path, PathBuf},
-    sync::Arc,
-};
+use std::{env::current_dir, path::PathBuf};
 
-use anyhow::{anyhow, bail, Context, Error};
-use once_cell::sync::Lazy;
-use parking_lot::Mutex;
-use serde::Deserialize;
-use swc_common::collections::AHashMap;
+use anyhow::{Context, Error};
 use wasmer_cache::FileSystemCache;
 
 /// Type of cache to store compiled bytecodes of plugins.
@@ -19,40 +10,6 @@ pub enum PluginCache {
     File(FileSystemCache),
 }
 
-/// TODO: Cache
-pub fn resolve(name: &str) -> Result<Arc<PathBuf>, Error> {
-    let cwd = current_dir().context("failed to get current directory")?;
-    let mut dir = Some(&*cwd);
-
-    // If given name is a resolvable local path, returns it directly.
-    // It should be a path to the plugin file to be loaded, per-platform path
-    // interop is caller's responsibility.
-    let local_path = PathBuf::from(name);
-    if local_path.is_file() {
-        return Ok(Arc::new(local_path));
-    }
-
-    let mut errors = vec![];
-
-    while let Some(base_dir) = dir {
-        let res = check_node_modules(base_dir, name);
-        match res {
-            Ok(Some(path)) => {
-                return Ok(path);
-            }
-            Err(err) => {
-                errors.push(err);
-            }
-
-            Ok(None) => {}
-        }
-
-        dir = base_dir.parent();
-    }
-
-    bail!("failed to resolve plugin `{}`:\n{:?}", name, errors)
-}
-
 /// Build a path to cache location where plugin's bytecode cache will be stored.
 /// This fn does a side effect to create path to cache if given path is not
 /// resolvable. If root is not specified, it'll generate default root for cache
@@ -79,133 +36,3 @@ pub fn resolve_plugin_cache_root(root: Option<String>) -> Result<PluginCache, Er
         .map(PluginCache::File)
         .context("Failed to create cache location for the plugins")
 }
-
-#[derive(Deserialize)]
-struct PkgJson {
-    main: String,
-}
-
-fn read_main_field(dir: &Path, json_path: &Path) -> Result<PathBuf, Error> {
-    let json_str = read_to_string(&json_path)?;
-    let json: PkgJson = serde_json::from_str(&json_str)?;
-
-    Ok(dir.join(&json.main))
-}
-
-fn pkg_name_without_scope(pkg_name: &str) -> &str {
-    if pkg_name.contains('/') {
-        pkg_name.split('/').nth(1).unwrap()
-    } else {
-        pkg_name
-    }
-}
-
-const fn is_musl() -> bool {
-    cfg!(target_env = "musl")
-}
-
-fn resolve_using_package_json(dir: &Path, pkg_name: &str) -> Result<PathBuf, Error> {
-    let node_modules = dir
-        .parent()
-        .ok_or_else(|| anyhow!("cannot resolve plugin in root directory"))?;
-
-    let pkg_json = dir.join("package.json");
-    let json = read_to_string(&pkg_json).context("failed to read package.json of main package")?;
-
-    let pkg: serde_json::Value =
-        serde_json::from_str(&json).context("failed to parse package.json of main package")?;
-
-    let pkg_obj = pkg
-        .as_object()
-        .ok_or_else(|| anyhow!("package.json is not an object"))?;
-
-    let opt_deps = pkg_obj
-        .get("optionalDependencies")
-        .ok_or_else(|| anyhow!("package.json does not contain optionalDependencies"))?
-        .as_object()
-        .ok_or_else(|| anyhow!("`optionalDependencies` of main package.json is not an object"))?;
-
-    for dep_pkg_name in opt_deps.keys() {
-        if dep_pkg_name.starts_with(&pkg_name) {
-            if is_musl() && !dep_pkg_name.contains("musl") {
-                continue;
-            }
-
-            let dep_pkg_dir_name = pkg_name_without_scope(dep_pkg_name);
-
-            let dep_pkg = node_modules.join(dep_pkg_dir_name);
-
-            if dep_pkg.exists() {
-                return read_main_field(&dep_pkg, &dep_pkg.join("package.json"));
-            }
-        }
-    }
-
-    bail!(
-        "tried to resolve `{}` using package.json in `{}`",
-        pkg_name,
-        dir.display()
-    )
-}
-
-fn check_node_modules(base_dir: &Path, name: &str) -> Result<Option<Arc<PathBuf>>, Error> {
-    fn inner(base_dir: &Path, name: &str) -> Result<Option<PathBuf>, Error> {
-        let node_modules_dir = base_dir.join("node_modules");
-        if !node_modules_dir.is_dir() {
-            return Ok(None);
-        }
-
-        let mut errors = vec![];
-
-        if !name.contains('@') {
-            let swc_plugin_dir = node_modules_dir
-                .join("@swc")
-                .join(format!("plugin-{}", name));
-            if swc_plugin_dir.is_dir() {
-                let res =
-                    resolve_using_package_json(&swc_plugin_dir, &format!("@swc/plugin-{}", name));
-
-                match res {
-                    Ok(v) => return Ok(Some(v)),
-                    Err(err) => {
-                        errors.push(err);
-                    }
-                }
-            }
-        }
-
-        {
-            let exact = node_modules_dir.join(name);
-            if exact.is_dir() {
-                let res = resolve_using_package_json(&exact, pkg_name_without_scope(name));
-
-                match res {
-                    Ok(v) => return Ok(Some(v)),
-                    Err(err) => {
-                        errors.push(err);
-                    }
-                }
-            }
-        }
-
-        if errors.is_empty() {
-            Ok(None)
-        } else {
-            Err(anyhow!("failed to resolve plugin `{}`: {:?}", name, errors))
-        }
-    }
-
-    static CACHE: Lazy<Mutex<AHashMap<(PathBuf, String), Option<Arc<PathBuf>>>>> =
-        Lazy::new(Default::default);
-
-    let key = (base_dir.to_path_buf(), name.to_string());
-    if let Some(cached) = CACHE.lock().get(&key).cloned() {
-        return Ok(cached);
-    }
-
-    let path = inner(base_dir, name)?.map(Arc::new);
-
-    CACHE.lock().insert(key, path.clone());
-
-    Ok(path)
-}
diff --git a/crates/wasm/Cargo.toml b/crates/wasm/Cargo.toml
index b9873674f6..85675aec19 100644
--- a/crates/wasm/Cargo.toml
+++ b/crates/wasm/Cargo.toml
@@ -19,7 +19,7 @@ swc_v2 = []
 [dependencies]
 anyhow = "1.0.42"
 console_error_panic_hook = "0.1.6"
-once_cell = "1.3.1"
+once_cell = "1.9.0"
 parking_lot_core = "0.9.1"
 path-clean = "0.1"
 serde = {version = "1", features = ["derive"]}


Test patch:
diff --git a/crates/testing/Cargo.toml b/crates/testing/Cargo.toml
index 1c6d1a6143..64ddb97ff4 100644
--- a/crates/testing/Cargo.toml
+++ b/crates/testing/Cargo.toml
@@ -11,7 +11,7 @@ version = "0.18.1"
 [dependencies]
 ansi_term = "0.12.1"
 difference = "2"
-once_cell = "1"
+once_cell = "1.9.0"
 pretty_assertions = "0.7.2"
 regex = "1"
 serde_json = "1.0.71"