Skip to content

Commit

Permalink
next-swc: add next-font-loaders to crates/core (#40221)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
Hannes Bornö and ijjk committed Sep 21, 2022
1 parent 11dd1de commit 97b3187
Show file tree
Hide file tree
Showing 46 changed files with 745 additions and 1 deletion.
10 changes: 10 additions & 0 deletions packages/next-swc/crates/core/src/lib.rs
Expand Up @@ -36,6 +36,7 @@ use serde::Deserialize;
use std::cell::RefCell;
use std::rc::Rc;
use std::{path::PathBuf, sync::Arc};
use swc_core::ecma::atoms::JsWord;

use swc_core::{
base::config::ModuleConfig,
Expand All @@ -51,6 +52,7 @@ mod auto_cjs;
pub mod disallow_re_export_all_in_page;
pub mod hook_optimizer;
pub mod next_dynamic;
pub mod next_font_loaders;
pub mod next_ssg;
pub mod page_config;
pub mod react_remove_properties;
Expand Down Expand Up @@ -109,6 +111,9 @@ pub struct TransformOptions {

#[serde(default)]
pub modularize_imports: Option<modularize_imports::Config>,

#[serde(default)]
pub font_loaders: Option<Vec<JsWord>>,
}

pub fn custom_before_pass<'a, C: Comments + 'a>(
Expand Down Expand Up @@ -211,6 +216,11 @@ where
match &opts.modularize_imports {
Some(config) => Either::Left(modularize_imports::modularize_imports(config.clone())),
None => Either::Right(noop()),
},
match &opts.font_loaders {
Some(font_loaders) =>
Either::Left(next_font_loaders::next_font_loaders(font_loaders.clone())),
None => Either::Right(noop()),
}
)
}
Expand Down
@@ -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()
});
}
}
}
@@ -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()
});
}
}
}
}
}
}
@@ -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(())
}),
}
}

0 comments on commit 97b3187

Please sign in to comment.