Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(swc/plugin_runner): improve resolver support for npm #3566

Merged
merged 2 commits into from
Feb 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}