feat(plugin/api): Determine plugin api (#2199)

This commit is contained in:
Donny/강동윤 2021-10-09 17:47:42 +09:00 committed by GitHub
parent 2379fe1ce0
commit 7a31a3f530
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1683 additions and 39 deletions

View File

@ -150,6 +150,7 @@ jobs:
- swc_stylis - swc_stylis
- swc_visit - swc_visit
- swc_visit_macros - swc_visit_macros
- swd
- testing - testing
- testing_macros - testing_macros
- wasm - wasm

1039
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@
members = [ members = [
"css", "css",
"css/stylis", "css/stylis",
"dev/cli",
"ecmascript", "ecmascript",
"ecmascript/babel/compat", "ecmascript/babel/compat",
"ecmascript/jsdoc", "ecmascript/jsdoc",

23
dev/cli/Cargo.toml Normal file
View File

@ -0,0 +1,23 @@
[package]
authors = ["강동윤 <kdy1997.dev@gmail.com>"]
description = "Command line for writting swc plugins in rust"
documentation = "https://rustdoc.swc.rs/swc_plugin/"
edition = "2018"
license = "Apache-2.0/MIT"
name = "swd"
repository = "https://github.com/swc-project/swc.git"
version = "0.0.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.41"
cargo = "0.56.0"
cargo-edit = "0.8.0"
cargo_metadata = "0.14.0"
clap = "2.33.3"
structopt = "0.3.21"
tokio = {version = "1.10.1", features = ["rt", "rt-multi-thread", "time", "process", "macros", "sync"]}
tracing = "0.1.26"
tracing-subscriber = "0.2.20"
url = "2"

15
dev/cli/README.md Normal file
View File

@ -0,0 +1,15 @@
# swc-dev
# Usage
```sh
# Dependency
cargo install cargo-edit
cargo install swc-dev
```
## Managing plugins
```
swc-dev plugin --help
```

8
dev/cli/scripts/invoke.sh Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -eu
cargo install --path . --debug
export RUST_LOG=debug
(cd plugins/packages/jest && swc-dev $@)

32
dev/cli/src/main.rs Normal file
View File

@ -0,0 +1,32 @@
use anyhow::Error;
use plugin::PluginCommand;
use structopt::StructOpt;
use tracing::Level;
mod plugin;
mod util;
#[derive(Debug, StructOpt)]
pub enum Cmd {
Plugin(PluginCommand),
}
#[tokio::main]
async fn main() -> Result<(), Error> {
tracing_subscriber::fmt::Subscriber::builder()
.with_target(false)
.with_max_level(Level::DEBUG)
.pretty()
.init();
let cmd = Cmd::from_args();
match cmd {
Cmd::Plugin(cmd) => {
cmd.run().await?;
}
}
Ok(())
}

View File

@ -0,0 +1,50 @@
use crate::util::cargo::cargo_target_dir;
use anyhow::{bail, Context, Error};
use std::env;
use structopt::StructOpt;
use tokio::process::Command;
/// Build your plugin using `cargo`.
#[derive(Debug, StructOpt)]
pub struct BuildCommand {
/// Build for production.
#[structopt(long)]
pub release: bool,
}
impl BuildCommand {
pub async fn run(self) -> Result<(), Error> {
run_cargo_build(self.release).await?;
let path = env::current_dir().context("failed to get current directory")?;
let target_dir = cargo_target_dir(&path).await?;
tracing::debug!("Cargo target directory: {}", target_dir.display());
Ok(())
}
}
pub async fn run_cargo_build(release: bool) -> Result<(), Error> {
tracing::info!(
"Running cargo build ({})",
if release { "release" } else { "debug" },
);
let mut c = Command::new("cargo");
c.arg("build");
if release {
c.arg("--release");
}
let status = c
.status()
.await
.context("`status()` for `cargo build` failed")?;
if !status.success() {
bail!("`cargo build` failed with status code {}", status);
}
Ok(())
}

View File

@ -0,0 +1,33 @@
use crate::util::cargo::add::run_cargo_add;
use anyhow::{bail, Context, Error};
use std::process::Stdio;
use structopt::StructOpt;
use tokio::process::Command;
/// Initializes a plugin project.
#[derive(Debug, StructOpt)]
pub struct InitCommand {}
impl InitCommand {
pub async fn run(self) -> Result<(), Error> {
let mut c = Command::new("cargo");
c.arg("init").stderr(Stdio::inherit());
let status = c
.arg("--lib")
.status()
.await
.with_context(|| format!("failed to run `cargo init`"))?;
if !status.success() {
bail!("failed to initialize a cargo project")
}
run_cargo_add("abi_stable").await?;
run_cargo_add("swc_atoms").await?;
run_cargo_add("swc_common").await?;
run_cargo_add("swc_plugin").await?;
Ok(())
}
}

46
dev/cli/src/plugin/mod.rs Normal file
View File

@ -0,0 +1,46 @@
use self::{
build::BuildCommand, init::InitCommand, package::PackageCommand, publish::PublishCommand,
upgrade_deps::UpgradeDepsCommand,
};
use anyhow::{Context, Error};
use structopt::StructOpt;
pub mod build;
pub mod init;
pub mod package;
pub mod publish;
pub mod upgrade_deps;
/// Manages the plugin. Used for developing plugins.
#[derive(Debug, StructOpt)]
pub enum PluginCommand {
Init(InitCommand),
Build(BuildCommand),
Package(PackageCommand),
Publish(PublishCommand),
UpgradeDeps(UpgradeDepsCommand),
}
impl PluginCommand {
pub async fn run(self) -> Result<(), Error> {
match self {
PluginCommand::Init(cmd) => {
cmd.run()
.await
.context("failed to initialize a plugin project")?;
}
PluginCommand::Build(cmd) => {
cmd.run()
.await
.context("failed to build a plugin project")?;
}
PluginCommand::Package(_) => todo!(),
PluginCommand::Publish(_) => todo!(),
PluginCommand::UpgradeDeps(cmd) => {
cmd.run().await.context("failed to upgrade dependencies")?;
}
}
Ok(())
}
}

View File

@ -0,0 +1,8 @@
use structopt::StructOpt;
#[derive(Debug, StructOpt)]
pub struct PackageCommand {
/// Build for production.
#[structopt(long)]
pub release: bool,
}

View File

@ -0,0 +1,4 @@
use structopt::StructOpt;
#[derive(Debug, StructOpt)]
pub struct PublishCommand {}

View File

@ -0,0 +1,23 @@
use crate::util::cargo::upgrade::upgrade_dep;
use anyhow::Error;
use structopt::StructOpt;
use tracing::info;
/// Upgrade dependencies related to `swc`.
#[derive(Debug, StructOpt)]
pub struct UpgradeDepsCommand {
/// Run upgrade command for all crates in the current workspace.
#[structopt(long)]
pub workspace: bool,
}
impl UpgradeDepsCommand {
pub async fn run(self) -> Result<(), Error> {
for crate_name in &["swc_atoms", "swc_common", "swc_plugin"] {
info!("Upgrading {}", crate_name);
upgrade_dep(&crate_name, self.workspace).await?;
}
Ok(())
}
}

View File

@ -0,0 +1,16 @@
use anyhow::{bail, Context, Error};
use tokio::process::Command;
pub async fn run_cargo_add(crate_name: &str) -> Result<(), Error> {
let status = Command::new("cargo")
.arg("add")
.arg(crate_name)
.status()
.await
.with_context(|| format!("failed to run `cargo add {}`", crate_name))?;
if status.success() {
Ok(())
} else {
bail!("failed to run `cargo add {}`", crate_name)
}
}

View File

@ -0,0 +1,32 @@
use anyhow::{Context, Error};
use cargo_metadata::MetadataCommand;
use std::path::{Path, PathBuf};
use tokio::task::spawn_blocking;
pub mod add;
pub mod upgrade;
pub async fn cargo_metadata(
mut cmd: MetadataCommand,
from: &Path,
) -> Result<cargo_metadata::Metadata, Error> {
let from = from.to_path_buf();
spawn_blocking(move || {
let result = cmd
.current_dir(&from)
.exec()
.context("failed to execute `cargo metadata`")?;
Ok(result)
})
.await
.context("failed to join the task for `cargo metadata`")?
}
pub async fn cargo_target_dir(from: &Path) -> Result<PathBuf, Error> {
let mut cmd = MetadataCommand::new();
cmd.no_deps();
let md = cargo_metadata(cmd, from).await?;
Ok(md.target_directory.as_std_path().to_path_buf())
}

View File

@ -0,0 +1,280 @@
use crate::util::{cargo::cargo_metadata, CargoEditResultExt};
use anyhow::{anyhow, Context, Error};
use cargo_edit::{find, get_latest_dependency, CrateName, Dependency, LocalManifest};
use std::{
collections::HashMap,
env,
path::{Path, PathBuf},
};
use url::Url;
/// A collection of manifests.
struct Manifests(Vec<(LocalManifest, cargo_metadata::Package)>);
/// Helper function to check whether a `cargo_metadata::Dependency` is a version
/// dependency.
fn is_version_dep(dependency: &cargo_metadata::Dependency) -> bool {
match dependency.source {
// This is the criterion cargo uses (in `SourceId::from_url`) to decide whether a
// dependency has the 'registry' kind.
Some(ref s) => s.splitn(2, '+').next() == Some("registry"),
_ => false,
}
}
impl Manifests {
/// Get all manifests in the workspace.
async fn get_all(manifest_path: &Option<PathBuf>) -> Result<Self, Error> {
let cur_dir = env::current_dir().context("failed to get current directory")?;
let mut cmd = cargo_metadata::MetadataCommand::new();
cmd.no_deps();
if let Some(path) = manifest_path {
cmd.manifest_path(path);
}
let result = cargo_metadata(cmd, &cur_dir).await?;
result
.packages
.into_iter()
.map(|package| {
Ok((
LocalManifest::try_new(Path::new(&package.manifest_path))
.map_err_op("create cargo_edit::LocalManifest")?,
package,
))
})
.collect::<Result<Vec<_>, Error>>()
.map(Manifests)
}
/// Get the manifest specified by the manifest path. Try to make an educated
/// guess if no path is provided.
async fn get_local_one(manifest_path: &Option<PathBuf>) -> Result<Self, Error> {
let cur_dir = env::current_dir().context("failed to get current directory")?;
let resolved_manifest_path: String = find(manifest_path)
.map_err_op("invoke cargo_edit::find")?
.to_string_lossy()
.into();
let manifest = LocalManifest::find(manifest_path)
.map_err_op("invoke cargo_edit::LocalManifeat::find")?;
let mut cmd = cargo_metadata::MetadataCommand::new();
cmd.no_deps();
if let Some(path) = manifest_path {
cmd.manifest_path(path);
}
let result = cargo_metadata(cmd, &cur_dir).await?;
let packages = result.packages;
let package = packages
.iter()
.find(|p| p.manifest_path == resolved_manifest_path)
// If we have successfully got metadata, but our manifest path does not correspond to a
// package, we must have been called against a virtual manifest.
.with_context(|| {
"Found virtual manifest, but this command requires running against an actual \
package in this workspace. Try adding `--workspace`."
})?;
Ok(Manifests(vec![(manifest, package.to_owned())]))
}
/// Get the the combined set of dependencies to upgrade. If the user has
/// specified per-dependency desired versions, extract those here.
fn get_dependencies(
&self,
only_update: Vec<String>,
exclude: Vec<String>,
) -> Result<DesiredUpgrades, Error> {
// Map the names of user-specified dependencies to the (optionally) requested
// version.
let selected_dependencies = only_update
.into_iter()
.map(|name| -> Result<_, Error> {
if let Some(dependency) = CrateName::new(&name)
.parse_as_version()
.map_err_op("parse the version of dependency")?
{
Ok((
dependency.name.clone(),
dependency.version().map(String::from),
))
} else {
Ok((name, None))
}
})
.collect::<Result<HashMap<_, _>, _>>()?;
Ok(DesiredUpgrades(
self.0
.iter()
.flat_map(|&(_, ref package)| package.dependencies.clone())
.filter(is_version_dep)
.filter(|dependency| !exclude.contains(&dependency.name))
// Exclude renamed dependecies aswell
.filter(|dependency| {
dependency
.rename
.as_ref()
.map_or(true, |rename| !exclude.contains(rename))
})
.filter_map(|dependency| {
let is_prerelease = dependency.req.to_string().contains('-');
if selected_dependencies.is_empty() {
// User hasn't asked for any specific dependencies to be upgraded,
// so upgrade all the dependencies.
let mut dep = Dependency::new(&dependency.name);
if let Some(rename) = dependency.rename {
dep = dep.set_rename(&rename);
}
Some((
dep,
UpgradeMetadata {
registry: dependency.registry,
version: None,
is_prerelease,
},
))
} else {
// User has asked for specific dependencies. Check if this dependency
// was specified, populating the registry from the lockfile metadata.
match selected_dependencies.get(&dependency.name) {
Some(version) => Some((
Dependency::new(&dependency.name),
UpgradeMetadata {
registry: dependency.registry,
version: version.clone(),
is_prerelease,
},
)),
None => None,
}
}
})
.collect(),
))
}
/// Upgrade the manifests on disk following the previously-determined
/// upgrade schema.
fn upgrade(
self,
upgraded_deps: &ActualUpgrades,
dry_run: bool,
skip_compatible: bool,
) -> Result<(), Error> {
for (mut manifest, package) in self.0 {
println!("{}:", package.name);
for (dep, version) in &upgraded_deps.0 {
let mut new_dep = Dependency::new(&dep.name).set_version(version);
if let Some(rename) = dep.rename() {
new_dep = new_dep.set_rename(rename);
}
manifest
.upgrade(&new_dep, dry_run, skip_compatible)
.map_err_op("invoke cargo_edit::upgrade")?;
}
}
Ok(())
}
}
// Some metadata about the dependency
// we're trying to upgrade.
struct UpgradeMetadata {
registry: Option<String>,
// `Some` if the user has specified an explicit
// version to upgrade to.
version: Option<String>,
is_prerelease: bool,
}
/// The set of dependencies to be upgraded, alongside the registries returned
/// from cargo metadata, and the desired versions, if specified by the user.
struct DesiredUpgrades(HashMap<Dependency, UpgradeMetadata>);
impl DesiredUpgrades {
/// Transform the dependencies into their upgraded forms. If a version is
/// specified, all dependencies will get that version.
fn get_upgraded(
self,
allow_prerelease: bool,
manifest_path: &Path,
) -> Result<ActualUpgrades, Error> {
self.0
.into_iter()
.map(
|(
dep,
UpgradeMetadata {
registry,
version,
is_prerelease,
},
)| {
let name = dep.name.clone();
if let Some(v) = version {
Ok((dep, v))
} else {
let registry_url = match registry {
Some(x) => Some(
Url::parse(&x)
.map_err(|err| anyhow!("invalid cargo config: {}", err))?,
),
None => None,
};
let allow_prerelease = allow_prerelease || is_prerelease;
get_latest_dependency(
&dep.name,
allow_prerelease,
manifest_path,
&registry_url,
)
.map(|new_dep| {
(
dep,
new_dep
.version()
.expect("Invalid dependency type")
.to_string(),
)
})
.map_err_op("invoke cargo_edit::get_latest_dependency")
.with_context(|| format!("failed to get new version of {}", name))
}
},
)
.collect::<Result<_, _>>()
.map(ActualUpgrades)
}
}
/// The complete specification of the upgrades that will be performed. Map of
/// the dependency names to the new versions.
struct ActualUpgrades(HashMap<Dependency, String>);
/// `cargo upgrade`, from `cargo-edit`.
pub async fn upgrade_dep(crate_name: &str, workspace: bool) -> Result<(), Error> {
let manifests = if workspace {
Manifests::get_all(&None).await
} else {
Manifests::get_local_one(&None).await
}
.context("failed to fetch manifest for `cargo-edit`")?;
let existing_dependencies =
manifests.get_dependencies(Default::default(), Default::default())?;
let upgraded_dependencies = existing_dependencies
.get_upgraded(false, &find(&None).map_err_op("invoke cargo_edit::find")?)?;
manifests
.upgrade(&upgraded_dependencies, false, false)
.with_context(|| format!("failed to upgrade {}", crate_name))
}

13
dev/cli/src/util/mod.rs Normal file
View File

@ -0,0 +1,13 @@
use anyhow::{anyhow, Error};
use std::fmt::Display;
pub mod cargo;
pub(crate) trait CargoEditResultExt<T>: Into<cargo_edit::Result<T>> {
fn map_err_op(self, op: impl Display) -> Result<T, Error> {
self.into()
.map_err(|err| anyhow!("failed to {}: {:?}", op, err))
}
}
impl<T> CargoEditResultExt<T> for cargo_edit::Result<T> {}

View File

@ -18,5 +18,4 @@ serde_json = "1.0.64"
swc_atoms = {version = "0.2.7", path = "../atoms"} swc_atoms = {version = "0.2.7", path = "../atoms"}
swc_common = {version = "0.13.0", path = "../common"} swc_common = {version = "0.13.0", path = "../common"}
swc_ecma_ast = {version = "0.54.0", path = "../ecmascript/ast"} swc_ecma_ast = {version = "0.54.0", path = "../ecmascript/ast"}
swc_ecma_utils = {version = "0.46.0", path = "../ecmascript/utils"}
swc_ecma_visit = {version = "0.40.0", path = "../ecmascript/visit"} swc_ecma_visit = {version = "0.40.0", path = "../ecmascript/visit"}

View File

@ -6,12 +6,9 @@ use abi_stable::{
std_types::{RResult, RStr, RString}, std_types::{RResult, RStr, RString},
StableAbi, StableAbi,
}; };
use anyhow::Context;
pub mod ecmascript { use serde::de::DeserializeOwned;
pub extern crate swc_ecma_ast as ast; use swc_ecma_ast::Program;
pub extern crate swc_ecma_utils as utils;
pub extern crate swc_ecma_visit as visit;
}
#[repr(C)] #[repr(C)]
#[derive(StableAbi)] #[derive(StableAbi)]
@ -30,3 +27,73 @@ impl RootModule for SwcPluginRef {
const NAME: &'static str = "swc_plugin"; const NAME: &'static str = "swc_plugin";
const VERSION_STRINGS: VersionStrings = package_version_strings!(); const VERSION_STRINGS: VersionStrings = package_version_strings!();
} }
#[doc(hidden)]
pub fn invoke_js_plugin<C, F>(
op: fn(C) -> F,
config_json: RStr,
ast_json: RString,
) -> RResult<RString, RString>
where
C: DeserializeOwned,
F: swc_ecma_visit::Fold,
{
use swc_ecma_visit::FoldWith;
let config = serde_json::from_str(config_json.as_str())
.context("failed to deserialize config string as json");
let config: C = match config {
Ok(v) => v,
Err(err) => return RResult::RErr(format!("{:?}", err).into()),
};
let ast =
serde_json::from_str(ast_json.as_str()).context("failed to deserialize ast string as json");
let ast: Program = match ast {
Ok(v) => v,
Err(err) => return RResult::RErr(format!("{:?}", err).into()),
};
let mut tr = op(config);
let ast = ast.fold_with(&mut tr);
let res = match serde_json::to_string(&ast) {
Ok(v) => v,
Err(err) => {
return RResult::RErr(
format!(
"failed to serialize swc_ecma_ast::Program as json: {:?}",
err
)
.into(),
)
}
};
RResult::ROk(res.into())
}
#[macro_export]
macro_rules! define_js_plugin {
($fn_name:ident) => {
#[abi_stable::export_root_module]
pub fn swc_library() -> $crate::SwcPluginRef {
extern "C" fn swc_js_plugin(
config_json: abi_stable::std_types::RStr,
ast_json: abi_stable::std_types::RString,
) -> abi_stable::std_types::RResult<
abi_stable::std_types::RString,
abi_stable::std_types::RString,
> {
$crate::invoke_js_plugin($fn_name, config_json, ast_json)
}
use abi_stable::prefix_type::PrefixTypeTrait;
$crate::SwcPlugin {
process_js: Some(swc_js_plugin),
}
.leak_into_prefix()
}
};
}

14
plugin/tests/js.rs Normal file
View File

@ -0,0 +1,14 @@
//! Ensure that worng macro definitions are catched by swc monorepo.
use swc_ecma_visit::Fold;
use swc_plugin::define_js_plugin;
define_js_plugin!(drop_console);
fn drop_console(_: ()) -> impl Fold {
DropConsole
}
struct DropConsole;
impl Fold for DropConsole {}

4
scripts/cli.sh Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -eu
cargo install --offline --debug --path cli