97b31873e1
For some context: https://vercel.slack.com/archives/CGU8HUTUH/p1662124179102509 Transforms call expressions of imported functions, only affects imports specified in SWC options. Each argument is turned into JSON and appended to the import as a query. The query can be read in a webpack loader, i.e. the call expression is only evaluated at build time ### Transform From ```tsx import { Fn } from "package" const res = Fn(1, "2", { three: true }) ``` To ```tsx import res from 'package?Fn;1;"2";{"three":true}' ``` ### Visitors #### NextFontLoaders (mod.rs) Creates several visitors that updates the state and reports errors. This is where the AST is mutated. After all other visitors are done the call expressions and original imports are removed. The newly generated imports are added instead. #### FontFunctionsCollector Finds imports from the specified packages. Function calls of these imports should be transformed. #### FontImportsGenerator Creates import declarations, call expression arguments are turned into JSON and added to the import as a query. #### FindFunctionsOutsideModuleScope Makes sure that there's no reference of the functions anywhere else but the module scope. Co-authored-by: JJ Kasper <jj@jjsweb.site>
109 lines
3.4 KiB
Rust
109 lines
3.4 KiB
Rust
use next_swc::{custom_before_pass, TransformOptions};
|
|
use serde::de::DeserializeOwned;
|
|
use std::path::{Path, PathBuf};
|
|
use swc_core::{
|
|
base::Compiler,
|
|
ecma::parser::{Syntax, TsConfig},
|
|
ecma::transforms::base::pass::noop,
|
|
};
|
|
use testing::{NormalizedOutput, Tester};
|
|
|
|
#[testing::fixture("tests/full/**/input.js")]
|
|
fn full(input: PathBuf) {
|
|
test(&input, true);
|
|
}
|
|
|
|
#[testing::fixture("tests/loader/**/input.js")]
|
|
fn loader(input: PathBuf) {
|
|
test(&input, false);
|
|
}
|
|
|
|
fn test(input: &Path, minify: bool) {
|
|
let output = input.parent().unwrap().join("output.js");
|
|
|
|
Tester::new()
|
|
.print_errors(|cm, handler| {
|
|
let c = Compiler::new(cm.clone());
|
|
|
|
let fm = cm.load_file(input).expect("failed to load file");
|
|
|
|
let options = TransformOptions {
|
|
swc: swc_core::base::config::Options {
|
|
swcrc: true,
|
|
output_path: Some(output.clone()),
|
|
|
|
config: swc_core::base::config::Config {
|
|
is_module: swc_core::base::config::IsModule::Bool(true),
|
|
|
|
jsc: swc_core::base::config::JscConfig {
|
|
minify: if minify {
|
|
Some(assert_json("{ \"compress\": true, \"mangle\": true }"))
|
|
} else {
|
|
None
|
|
},
|
|
syntax: Some(Syntax::Typescript(TsConfig {
|
|
tsx: true,
|
|
..Default::default()
|
|
})),
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
},
|
|
disable_next_ssg: false,
|
|
disable_page_config: false,
|
|
pages_dir: None,
|
|
is_page_file: false,
|
|
is_development: true,
|
|
is_server: false,
|
|
server_components: None,
|
|
styled_components: Some(assert_json("{}")),
|
|
remove_console: None,
|
|
react_remove_properties: None,
|
|
relay: None,
|
|
shake_exports: None,
|
|
emotion: Some(assert_json("{}")),
|
|
modularize_imports: None,
|
|
font_loaders: None,
|
|
};
|
|
|
|
let options = options.patch(&fm);
|
|
|
|
match c.process_js_with_custom_pass(
|
|
fm.clone(),
|
|
None,
|
|
&handler,
|
|
&options.swc,
|
|
|_, comments| {
|
|
custom_before_pass(
|
|
cm.clone(),
|
|
fm.clone(),
|
|
&options,
|
|
comments.clone(),
|
|
Default::default(),
|
|
)
|
|
},
|
|
|_, _| noop(),
|
|
) {
|
|
Ok(v) => {
|
|
NormalizedOutput::from(v.code)
|
|
.compare_to_file(output)
|
|
.unwrap();
|
|
}
|
|
Err(err) => panic!("Error: {:?}", err),
|
|
};
|
|
|
|
Ok(())
|
|
})
|
|
.map(|_| ())
|
|
.expect("failed");
|
|
}
|
|
|
|
/// Using this, we don't have to break code by adding field.s
|
|
fn assert_json<T>(json_str: &str) -> T
|
|
where
|
|
T: DeserializeOwned,
|
|
{
|
|
serde_json::from_str(json_str).expect("failed to deserialize")
|
|
}
|