Skip to content

Commit

Permalink
feat(plugin/runner): Improve resolver support for npm (#3566)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwonoj authored Feb 15, 2022
1 parent ad6f24a commit d6477a7
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 181 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 9 additions & 3 deletions crates/swc/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
23 changes: 20 additions & 3 deletions crates/swc/src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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,
}
Expand All @@ -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.
Expand All @@ -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)?;

Expand All @@ -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,
Expand Down
28 changes: 28 additions & 0 deletions crates/swc_cli/src/commands/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down
1 change: 1 addition & 0 deletions crates/swc_plugin_runner/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ 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"
Expand Down
177 changes: 2 additions & 175 deletions crates/swc_plugin_runner/src/resolve.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
Expand All @@ -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)
}

1 comment on commit d6477a7

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Benchmark suite Current: d6477a7 Previous: c624fed Ratio
full_es2015 222454001 ns/iter (± 33321978) 172350264 ns/iter (± 5439665) 1.29
full_es2016 223538930 ns/iter (± 31968297) 177072046 ns/iter (± 6472213) 1.26
full_es2017 235559082 ns/iter (± 39459585) 176888053 ns/iter (± 6054331) 1.33
full_es2018 229596253 ns/iter (± 33033290) 176013073 ns/iter (± 5546095) 1.30
full_es2019 215559024 ns/iter (± 40821240) 175452353 ns/iter (± 6117821) 1.23
full_es2020 186643122 ns/iter (± 32316056) 160368403 ns/iter (± 5297820) 1.16
full_es3 289750679 ns/iter (± 35580836) 220302938 ns/iter (± 4967752) 1.32
full_es5 279051286 ns/iter (± 39483818) 219854583 ns/iter (± 10778123) 1.27
parser 919756 ns/iter (± 291493) 709211 ns/iter (± 14218) 1.30

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.