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: Add experimental babel plugin runner #404

Merged
merged 46 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
fba73b5
init
kdy1 Jan 30, 2025
a03878c
cargo lockfile
kdy1 Jan 30, 2025
937c8b4
pnpm lockfile
kdy1 Jan 30, 2025
817f93d
Dep on rquickjs
kdy1 Jan 30, 2025
886fc71
Rename to babel
kdy1 Jan 30, 2025
f9b1a24
cargo lockfile
kdy1 Jan 30, 2025
392494c
pnpm lockfile
kdy1 Jan 30, 2025
f90e4dc
Dep
kdy1 Jan 30, 2025
81ce99b
type work
kdy1 Jan 30, 2025
debfad5
cargo lockfile
kdy1 Jan 30, 2025
e274d43
Dep
kdy1 Jan 30, 2025
9a12190
cargo lockfile
kdy1 Jan 30, 2025
9007003
Move
kdy1 Jan 30, 2025
036c46d
from_swc
kdy1 Jan 31, 2025
2fd2fb8
apply_transform
kdy1 Jan 31, 2025
1b359ce
Dep on once_cell
kdy1 Jan 31, 2025
ce3563a
cahce.rs
kdy1 Jan 31, 2025
ffaffd1
cargo lockfile
kdy1 Jan 31, 2025
50509c2
pub
kdy1 Jan 31, 2025
d017513
with_quickjs_context
kdy1 Jan 31, 2025
f329898
Rename
kdy1 Jan 31, 2025
5ad2ac6
more work
kdy1 Jan 31, 2025
6f0acfc
impl<'a> IntoJs<'a> for TransformOutput {
kdy1 Jan 31, 2025
f90f25b
call
kdy1 Jan 31, 2025
34135b4
done?
kdy1 Jan 31, 2025
e1c7327
lint
kdy1 Jan 31, 2025
6fcb14e
babel
kdy1 Jan 31, 2025
ba6d2dd
pub
kdy1 Jan 31, 2025
70c3a88
fixture
kdy1 Jan 31, 2025
51e8637
`Pass`
kdy1 Jan 31, 2025
ca64401
transform module
kdy1 Jan 31, 2025
21b3404
fix build
kdy1 Jan 31, 2025
305e331
eval
kdy1 Jan 31, 2025
7c3f0e1
append
kdy1 Jan 31, 2025
4061aa2
docs(changeset): Initialize experiment
kdy1 Jan 31, 2025
05028fb
RELEASING: Releasing 1 package(s)
kdy1 Jan 31, 2025
9a342eb
docs(changeset): Verify
kdy1 Jan 31, 2025
292f8b6
RELEASING: Releasing 1 package(s)
kdy1 Jan 31, 2025
b29b882
Merge CHANGELOG into README
kdy1 Jan 31, 2025
2de309f
test
kdy1 Jan 31, 2025
86c9e7a
test
kdy1 Jan 31, 2025
43c17ba
package
kdy1 Jan 31, 2025
fb92a97
cargo lockfile
kdy1 Jan 31, 2025
cc813bf
Remove npm
kdy1 Jan 31, 2025
005094e
cargo lockfile
kdy1 Jan 31, 2025
35df8aa
pnpm lockfile
kdy1 Jan 31, 2025
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
47 changes: 47 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ testing = "5.0.0"
tracing = "0.1.40"
widestring = "1.0.2"

rquickjs = "0.8.1"
rustc-hash = "2.1.0"
swc_icu_messageformat_parser = { version = "1.0.0", path = "./crates/swc_icu_messageformat_parser" }

Expand Down
32 changes: 32 additions & 0 deletions crates/swc_experimental_babel/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[package]

description = "AST Transforms for experimental babel plugin"

name = "swc_experimental_babel"

authors = { workspace = true }
edition = { workspace = true }
homepage = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
rust-version = { workspace = true }
version = "0.1.0"

[dependencies]
anyhow = { workspace = true }
once_cell = { workspace = true }
rquickjs = { workspace = true }
serde = { workspace = true, features = ["derive"] }
swc_atoms = { workspace = true }
swc_common = { workspace = true }
swc_core = { workspace = true, features = [] }
swc_ecma_ast = { workspace = true }
swc_ecma_codegen = { workspace = true }
swc_ecma_parser = { workspace = true }
swc_ecma_visit = { workspace = true }

[dev-dependencies]
swc_ecma_parser = { workspace = true }
swc_ecma_transforms_base = { workspace = true }
swc_ecma_transforms_testing = { workspace = true }
testing = { workspace = true }
149 changes: 149 additions & 0 deletions crates/swc_experimental_babel/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
use anyhow::{Context as _, Result};
use rquickjs::{function::Args, FromJs, Function, IntoJs, Module};
use serde::{Deserialize, Serialize};
use swc_common::{sync::Lrc, FileName, SourceMap, SourceMapper};
use swc_ecma_ast::{EsVersion, Pass, Program, SourceMapperExt};
use swc_ecma_codegen::{text_writer::JsWriter, Emitter, Node};
use swc_ecma_parser::parse_file_as_program;

use crate::qjs::with_quickjs_context;

mod qjs;

pub struct Transform<'a, S>
where
S: SourceMapper + SourceMapperExt,
{
pub transform_code: &'a str,
pub cm: Lrc<S>,
pub filename: Lrc<FileName>,
}

#[derive(Serialize, Deserialize)]
pub struct TransformOutput {
pub code: String,
#[serde(default)]
pub map: Option<String>,
}

impl TransformOutput {
pub fn from_swc<S>(cm: Lrc<S>, program: &Program) -> Result<Self>
where
S: SourceMapper + SourceMapperExt,
{
let dummy_cm = Lrc::new(SourceMap::default());
let mut buf = vec![];

{
let mut wr = JsWriter::new(dummy_cm, "\n", &mut buf, None);
let mut emitter = Emitter {
cfg: Default::default(),
cm,
comments: None,
wr: &mut wr,
};

program.emit_with(&mut emitter)?;
}

Ok(Self {
code: String::from_utf8(buf).context("failed to convert the generated code to utf8")?,
map: None,
})
}

pub fn parse(self, filename: Lrc<FileName>) -> Result<Program> {
let cm = Lrc::new(SourceMap::default());
let fm = cm.new_source_file(filename, self.code);

let program = parse_file_as_program(
&fm,
Default::default(),
EsVersion::latest(),
None,
&mut vec![],
)
.map_err(|err| anyhow::anyhow!("failed to parse the file: {:?}", err))?;

Ok(program)
}
}

impl<S> Transform<'_, S>
where
S: SourceMapper + SourceMapperExt,
{
fn apply_transform(&self, input: TransformOutput) -> Result<TransformOutput> {
with_quickjs_context(|ctx| {
dbg!("declaring module", &self.transform_code);

let module = Module::declare(ctx.clone(), "babel-transform", self.transform_code)
.context("failed to declare the module")?
.eval()
.context("failed to evaluate the module")?
.0;

dbg!("declared module");

let function: Function = module
.get("transform")
.context("failed to get the default export")?;

dbg!("got function");

let mut args = Args::new(ctx.clone(), 1);
args.push_arg(input)?;

let output = function
.call_arg(args)
.context("failed to call the transform function")?;

Ok(output)
})
}
}

impl<S> Pass for Transform<'_, S>
where
S: SourceMapper + SourceMapperExt,
{
fn process(&mut self, program: &mut Program) {
let input = TransformOutput::from_swc(self.cm.clone(), program)
.expect("failed to convert swc program to babel input");
let output = self
.apply_transform(input)
.expect("failed to apply the babel transform");

let new_program = output
.parse(self.filename.clone())
.expect("failed to parse the output");

*program = new_program;
}
}

impl<'a> IntoJs<'a> for TransformOutput {
fn into_js(self, ctx: &rquickjs::Ctx<'a>) -> rquickjs::Result<rquickjs::Value<'a>> {
let obj = rquickjs::Object::new(ctx.clone())?;

obj.set("code", self.code)?;
obj.set("map", self.map)?;

obj.into_js(ctx)
}
}

impl<'a> FromJs<'a> for TransformOutput {
fn from_js(_: &rquickjs::Ctx<'a>, value: rquickjs::Value<'a>) -> rquickjs::Result<Self> {
let obj = value.into_object().ok_or_else(|| rquickjs::Error::FromJs {
from: "Value",
to: "Object",
message: Some("expected an object".to_string()),
})?;

Ok(Self {
code: obj.get::<_, String>("code")?,
map: obj.get::<_, Option<String>>("map")?,
})
}
}
19 changes: 19 additions & 0 deletions crates/swc_experimental_babel/src/qjs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use once_cell::unsync::Lazy;
use rquickjs::{Context, Runtime};

thread_local! {
static QUICKJS_RUNTIME: Lazy<Runtime> = Lazy::new(|| Runtime::new().expect("failed to create quickjs runtime"));
}

thread_local! {
static QUICKJS_CONTEXT: Lazy<Context> = Lazy::new(|| {
QUICKJS_RUNTIME.with(|rt| Context::full(rt).expect("failed to create context"))
});
}

pub fn with_quickjs_context<F, R>(f: F) -> R
where
F: FnOnce(rquickjs::Ctx) -> R,
{
QUICKJS_CONTEXT.with(|ctx| ctx.with(f))
}
10 changes: 10 additions & 0 deletions crates/swc_experimental_babel/tests/basic-transform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@


export function transform({ code, map }) {


return {
code: code + ';\nconsole.log("hello")',
map,
};
}
5 changes: 5 additions & 0 deletions crates/swc_experimental_babel/tests/basic/basic/input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createContext } from 'preact';

export function aaa() {
const context = createContext();
}
6 changes: 6 additions & 0 deletions crates/swc_experimental_babel/tests/basic/basic/output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createContext } from 'preact';
export function aaa() {
const context = createContext();
}
;
console.log("hello");
31 changes: 31 additions & 0 deletions crates/swc_experimental_babel/tests/fixture.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use std::path::PathBuf;

use swc_common::{sync::Lrc, FileName, Mark};
use swc_ecma_transforms_base::resolver;
use swc_ecma_transforms_testing::test_fixture;

#[testing::fixture("tests/basic/**/input.js")]

fn fixture(input: PathBuf) {
let output = input
.parent()
.expect("should have parent directory")
.join("output.js");

test_fixture(
Default::default(),
&|tr| {
(
resolver(Mark::new(), Mark::new(), false),
swc_experimental_babel::Transform {
transform_code: include_str!("./basic-transform.js"),
filename: Lrc::new(FileName::Real(input.clone())),
cm: tr.cm.clone(),
},
)
},
&input,
&output,
Default::default(),
);
}
Loading