-
Notifications
You must be signed in to change notification settings - Fork 27.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
next-swc: add next-font-loaders to crates/core (#40221)
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 <[email protected]>
- Loading branch information
Showing
46 changed files
with
745 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
packages/next-swc/crates/core/src/next_font_loaders/find_functions_outside_module_scope.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
use swc_core::common::errors::HANDLER; | ||
use swc_core::ecma::ast::*; | ||
use swc_core::ecma::visit::noop_visit_type; | ||
use swc_core::ecma::visit::Visit; | ||
|
||
pub struct FindFunctionsOutsideModuleScope<'a> { | ||
pub state: &'a super::State, | ||
} | ||
|
||
impl<'a> Visit for FindFunctionsOutsideModuleScope<'a> { | ||
noop_visit_type!(); | ||
|
||
fn visit_ident(&mut self, ident: &Ident) { | ||
if self.state.font_functions.get(&ident.to_id()).is_some() | ||
&& self | ||
.state | ||
.font_functions_in_allowed_scope | ||
.get(&ident.span.lo) | ||
.is_none() | ||
{ | ||
HANDLER.with(|handler| { | ||
handler | ||
.struct_span_err( | ||
ident.span, | ||
"Font loaders must be called and assigned to a const in the module scope", | ||
) | ||
.emit() | ||
}); | ||
} | ||
} | ||
} |
68 changes: 68 additions & 0 deletions
68
packages/next-swc/crates/core/src/next_font_loaders/font_functions_collector.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
use swc_core::common::errors::HANDLER; | ||
use swc_core::ecma::ast::*; | ||
use swc_core::ecma::atoms::JsWord; | ||
use swc_core::ecma::visit::noop_visit_type; | ||
use swc_core::ecma::visit::Visit; | ||
|
||
pub struct FontFunctionsCollector<'a> { | ||
pub font_loaders: &'a [JsWord], | ||
pub state: &'a mut super::State, | ||
} | ||
|
||
impl<'a> Visit for FontFunctionsCollector<'a> { | ||
noop_visit_type!(); | ||
|
||
fn visit_import_decl(&mut self, import_decl: &ImportDecl) { | ||
if self.font_loaders.contains(&import_decl.src.value) { | ||
self.state | ||
.removeable_module_items | ||
.insert(import_decl.span.lo); | ||
for specifier in &import_decl.specifiers { | ||
match specifier { | ||
ImportSpecifier::Named(ImportNamedSpecifier { | ||
local, imported, .. | ||
}) => { | ||
self.state | ||
.font_functions_in_allowed_scope | ||
.insert(local.span.lo); | ||
|
||
let function_name = if let Some(ModuleExportName::Ident(ident)) = imported { | ||
ident.sym.clone() | ||
} else { | ||
local.sym.clone() | ||
}; | ||
self.state.font_functions.insert( | ||
local.to_id(), | ||
super::FontFunction { | ||
loader: import_decl.src.value.clone(), | ||
function_name: Some(function_name), | ||
}, | ||
); | ||
} | ||
ImportSpecifier::Default(ImportDefaultSpecifier { local, .. }) => { | ||
self.state | ||
.font_functions_in_allowed_scope | ||
.insert(local.span.lo); | ||
self.state.font_functions.insert( | ||
local.to_id(), | ||
super::FontFunction { | ||
loader: import_decl.src.value.clone(), | ||
function_name: None, | ||
}, | ||
); | ||
} | ||
ImportSpecifier::Namespace(_) => { | ||
HANDLER.with(|handler| { | ||
handler | ||
.struct_span_err( | ||
import_decl.span, | ||
"Font loaders can't have namespace imports", | ||
) | ||
.emit() | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
220 changes: 220 additions & 0 deletions
220
packages/next-swc/crates/core/src/next_font_loaders/font_imports_generator.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,220 @@ | ||
use serde_json::Value; | ||
use swc_core::common::errors::HANDLER; | ||
use swc_core::common::{Spanned, DUMMY_SP}; | ||
use swc_core::ecma::ast::*; | ||
use swc_core::ecma::atoms::JsWord; | ||
use swc_core::ecma::visit::{noop_visit_type, Visit}; | ||
|
||
pub struct FontImportsGenerator<'a> { | ||
pub state: &'a mut super::State, | ||
} | ||
|
||
impl<'a> FontImportsGenerator<'a> { | ||
fn check_call_expr(&mut self, call_expr: &CallExpr) -> Option<ImportDecl> { | ||
if let Callee::Expr(callee_expr) = &call_expr.callee { | ||
if let Expr::Ident(ident) = &**callee_expr { | ||
if let Some(font_function) = self.state.font_functions.get(&ident.to_id()) { | ||
self.state | ||
.font_functions_in_allowed_scope | ||
.insert(ident.span.lo); | ||
|
||
let json: Result<Vec<Value>, ()> = call_expr | ||
.args | ||
.iter() | ||
.map(|expr_or_spread| { | ||
if let Some(span) = expr_or_spread.spread { | ||
HANDLER.with(|handler| { | ||
handler | ||
.struct_span_err(span, "Font loaders don't accept spreads") | ||
.emit() | ||
}); | ||
} | ||
|
||
expr_to_json(&*expr_or_spread.expr) | ||
}) | ||
.collect(); | ||
|
||
if let Ok(json) = json { | ||
let mut json_values: Vec<String> = | ||
json.iter().map(|value| value.to_string()).collect(); | ||
let function_name = match &font_function.function_name { | ||
Some(function) => String::from(&**function), | ||
None => String::new(), | ||
}; | ||
let mut values = vec![function_name]; | ||
values.append(&mut json_values); | ||
|
||
return Some(ImportDecl { | ||
src: Str { | ||
value: JsWord::from(format!( | ||
"{}?{}", | ||
font_function.loader, | ||
values.join(";") | ||
)), | ||
raw: None, | ||
span: DUMMY_SP, | ||
}, | ||
specifiers: vec![], | ||
type_only: false, | ||
asserts: None, | ||
span: DUMMY_SP, | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
|
||
None | ||
} | ||
|
||
fn check_var_decl(&mut self, var_decl: &VarDecl) { | ||
if let Some(decl) = var_decl.decls.get(0) { | ||
let ident = match &decl.name { | ||
Pat::Ident(ident) => Ok(ident.id.clone()), | ||
pattern => Err(pattern), | ||
}; | ||
if let Some(expr) = &decl.init { | ||
if let Expr::Call(call_expr) = &**expr { | ||
let import_decl = self.check_call_expr(call_expr); | ||
|
||
if let Some(mut import_decl) = import_decl { | ||
self.state.removeable_module_items.insert(var_decl.span.lo); | ||
|
||
match var_decl.kind { | ||
VarDeclKind::Const => {} | ||
_ => { | ||
HANDLER.with(|handler| { | ||
handler | ||
.struct_span_err( | ||
var_decl.span, | ||
"Font loader calls must be assigned to a const", | ||
) | ||
.emit() | ||
}); | ||
} | ||
} | ||
|
||
match ident { | ||
Ok(ident) => { | ||
import_decl.specifiers = | ||
vec![ImportSpecifier::Default(ImportDefaultSpecifier { | ||
span: DUMMY_SP, | ||
local: ident, | ||
})]; | ||
|
||
self.state | ||
.font_imports | ||
.push(ModuleItem::ModuleDecl(ModuleDecl::Import(import_decl))); | ||
} | ||
Err(pattern) => { | ||
HANDLER.with(|handler| { | ||
handler | ||
.struct_span_err( | ||
pattern.span(), | ||
"Font loader calls must be assigned to an identifier", | ||
) | ||
.emit() | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
impl<'a> Visit for FontImportsGenerator<'a> { | ||
noop_visit_type!(); | ||
|
||
fn visit_module_item(&mut self, item: &ModuleItem) { | ||
if let ModuleItem::Stmt(Stmt::Decl(Decl::Var(var_decl))) = item { | ||
self.check_var_decl(var_decl); | ||
} | ||
} | ||
} | ||
|
||
fn object_lit_to_json(object_lit: &ObjectLit) -> Value { | ||
let mut values = serde_json::Map::new(); | ||
for prop in &object_lit.props { | ||
match prop { | ||
PropOrSpread::Prop(prop) => match &**prop { | ||
Prop::KeyValue(key_val) => { | ||
let key = match &key_val.key { | ||
PropName::Ident(ident) => Ok(String::from(&*ident.sym)), | ||
key => { | ||
HANDLER.with(|handler| { | ||
handler | ||
.struct_span_err(key.span(), "Unexpected object key type") | ||
.emit() | ||
}); | ||
Err(()) | ||
} | ||
}; | ||
let val = expr_to_json(&*key_val.value); | ||
if let (Ok(key), Ok(val)) = (key, val) { | ||
values.insert(key, val); | ||
} | ||
} | ||
key => HANDLER.with(|handler| { | ||
handler.struct_span_err(key.span(), "Unexpected key").emit(); | ||
}), | ||
}, | ||
PropOrSpread::Spread(spread_span) => HANDLER.with(|handler| { | ||
handler | ||
.struct_span_err(spread_span.dot3_token, "Unexpected spread") | ||
.emit(); | ||
}), | ||
} | ||
} | ||
|
||
Value::Object(values) | ||
} | ||
|
||
fn expr_to_json(expr: &Expr) -> Result<Value, ()> { | ||
match expr { | ||
Expr::Lit(Lit::Str(str)) => Ok(Value::String(String::from(&*str.value))), | ||
Expr::Lit(Lit::Bool(Bool { value, .. })) => Ok(Value::Bool(*value)), | ||
Expr::Lit(Lit::Num(Number { value, .. })) => { | ||
Ok(Value::Number(serde_json::Number::from_f64(*value).unwrap())) | ||
} | ||
Expr::Object(object_lit) => Ok(object_lit_to_json(object_lit)), | ||
Expr::Array(ArrayLit { | ||
elems, | ||
span: array_span, | ||
.. | ||
}) => { | ||
let elements: Result<Vec<Value>, ()> = elems | ||
.iter() | ||
.map(|e| { | ||
if let Some(expr) = e { | ||
match expr.spread { | ||
Some(spread_span) => HANDLER.with(|handler| { | ||
handler | ||
.struct_span_err(spread_span, "Unexpected spread") | ||
.emit(); | ||
Err(()) | ||
}), | ||
None => expr_to_json(&*expr.expr), | ||
} | ||
} else { | ||
HANDLER.with(|handler| { | ||
handler | ||
.struct_span_err(*array_span, "Unexpected empty value in array") | ||
.emit(); | ||
Err(()) | ||
}) | ||
} | ||
}) | ||
.collect(); | ||
|
||
elements.map(Value::Array) | ||
} | ||
lit => HANDLER.with(|handler| { | ||
handler | ||
.struct_span_err(lit.span(), "Unexpected value") | ||
.emit(); | ||
Err(()) | ||
}), | ||
} | ||
} |
Oops, something went wrong.