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

Add __nextjs_pure helper #57286

Merged
merged 18 commits into from
Oct 23, 2023
104 changes: 104 additions & 0 deletions packages/next-swc/crates/core/src/import_analyzer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use turbopack_binding::swc::core::{
atoms::JsWord,
common::collections::{AHashMap, AHashSet},
ecma::{
ast::{
Expr, Id, ImportDecl, ImportNamedSpecifier, ImportSpecifier, MemberExpr, MemberProp,
Module, ModuleExportName,
},
visit::{noop_visit_type, Visit, VisitWith},
},
};

#[derive(Debug, Default)]
pub(crate) struct ImportMap {
/// Map from module name to (module path, exported symbol)
imports: AHashMap<Id, (JsWord, JsWord)>,

namespace_imports: AHashMap<Id, JsWord>,

imported_modules: AHashSet<JsWord>,
}

#[allow(unused)]
impl ImportMap {
pub fn is_module_imported(&mut self, module: &JsWord) -> bool {
self.imported_modules.contains(module)
}

/// Returns true if `e` is an import of `orig_name` from `module`.
pub fn is_import(&self, e: &Expr, module: &str, orig_name: &str) -> bool {
match e {
Expr::Ident(i) => {
if let Some((i_src, i_sym)) = self.imports.get(&i.to_id()) {
i_src == module && i_sym == orig_name
} else {
false
}
}

Expr::Member(MemberExpr {
obj: box Expr::Ident(obj),
prop: MemberProp::Ident(prop),
..
}) => {
if let Some(obj_src) = self.namespace_imports.get(&obj.to_id()) {
obj_src == module && prop.sym == *orig_name
} else {
false
}
}

_ => false,
}
}

pub fn analyze(m: &Module) -> Self {
let mut data = ImportMap::default();

m.visit_with(&mut Analyzer { data: &mut data });

data
}
}

struct Analyzer<'a> {
data: &'a mut ImportMap,
}

impl Visit for Analyzer<'_> {
noop_visit_type!();

fn visit_import_decl(&mut self, import: &ImportDecl) {
self.data.imported_modules.insert(import.src.value.clone());

for s in &import.specifiers {
let (local, orig_sym) = match s {
ImportSpecifier::Named(ImportNamedSpecifier {
local, imported, ..
}) => match imported {
Some(imported) => (local.to_id(), orig_name(imported)),
_ => (local.to_id(), local.sym.clone()),
},
ImportSpecifier::Default(s) => (s.local.to_id(), "default".into()),
ImportSpecifier::Namespace(s) => {
self.data
.namespace_imports
.insert(s.local.to_id(), import.src.value.clone());
continue;
}
};

self.data
.imports
.insert(local, (import.src.value.clone(), orig_sym));
}
}
}

fn orig_name(n: &ModuleExportName) -> JsWord {
match n {
ModuleExportName::Ident(v) => v.sym.clone(),
ModuleExportName::Str(v) => v.value.clone(),
}
}
3 changes: 3 additions & 0 deletions packages/next-swc/crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,13 @@ pub mod amp_attributes;
mod auto_cjs;
pub mod cjs_optimizer;
pub mod disallow_re_export_all_in_page;
mod import_analyzer;
pub mod named_import_transform;
pub mod next_ssg;
pub mod optimize_barrel;
pub mod optimize_server_react;
pub mod page_config;
pub mod pure;
pub mod react_server_components;
pub mod server_actions;
pub mod shake_exports;
Expand Down Expand Up @@ -188,6 +190,7 @@ where
};

chain!(
pure::pure_magic(comments.clone()),
disallow_re_export_all_in_page::disallow_re_export_all_in_page(opts.is_page_file),
match &opts.server_components {
Some(config) if config.truthy() =>
Expand Down
83 changes: 83 additions & 0 deletions packages/next-swc/crates/core/src/pure.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use turbopack_binding::swc::core::{
common::{comments::Comments, errors::HANDLER, util::take::Take, Spanned, DUMMY_SP},
ecma::{
ast::{CallExpr, Callee, EmptyStmt, Expr, Module, ModuleDecl, ModuleItem, Stmt},
visit::{as_folder, noop_visit_mut_type, Fold, VisitMut, VisitMutWith},
},
};

use crate::import_analyzer::ImportMap;

pub fn pure_magic<C>(comments: C) -> impl VisitMut + Fold
where
C: Comments,
{
as_folder(PureTransform {
imports: Default::default(),
comments,
})
}

struct PureTransform<C>
where
C: Comments,
{
imports: ImportMap,
comments: C,
}

const MODULE: &str = "next/dist/build/swc/helpers";
const FN_NAME: &str = "__nextjs_pure";

impl<C> VisitMut for PureTransform<C>
where
C: Comments,
{
fn visit_mut_expr(&mut self, e: &mut Expr) {
e.visit_mut_children_with(self);

if let Expr::Call(CallExpr {
span,
callee: Callee::Expr(callee),
args,
..
}) = e
{
if !self.imports.is_import(callee, MODULE, FN_NAME) {
return;
}

if args.len() != 1 {
HANDLER.with(|handler| {
handler
.struct_span_err(*span, "markAsPure() does not support multiple arguments")
.emit();
});
return;
}

*e = *args[0].expr.take();

self.comments.add_pure_comment(e.span().lo);
}
}

fn visit_mut_module(&mut self, m: &mut Module) {
self.imports = ImportMap::analyze(m);

m.visit_mut_children_with(self);
}

fn visit_mut_module_item(&mut self, m: &mut ModuleItem) {
if let ModuleItem::ModuleDecl(ModuleDecl::Import(import)) = m {
if import.src.value == MODULE {
*m = ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP }));
return;
}
}

m.visit_mut_children_with(self);
}

noop_visit_mut_type!();
}
21 changes: 21 additions & 0 deletions packages/next-swc/crates/core/tests/fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use next_swc::{
optimize_barrel::optimize_barrel,
optimize_server_react::optimize_server_react,
page_config::page_config_test,
pure::pure_magic,
react_server_components::server_components,
server_actions::{
server_actions, {self},
Expand Down Expand Up @@ -571,3 +572,23 @@ where
{
serde_json::from_str(s).expect("failed to deserialize")
}

#[fixture("tests/fixture/pure/**/input.js")]
fn pure(input: PathBuf) {
let output = input.parent().unwrap().join("output.js");
test_fixture(
syntax(),
&|tr| {
let unresolved_mark = Mark::new();
let top_level_mark = Mark::new();

chain!(
resolver(unresolved_mark, top_level_mark, false),
pure_magic(tr.comments.clone())
)
},
&input,
&output,
Default::default(),
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { __nextjs_pure } from 'not-next-magic'

__nextjs_pure(console.log('test!'))
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { __nextjs_pure } from 'not-next-magic';
__nextjs_pure(console.log("test!"));
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { __nextjs_pure } from 'next/dist/build/swc/helpers'

__nextjs_pure(console.log('test!'))
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
;
/*#__PURE__*/ console.log("test!");
3 changes: 3 additions & 0 deletions packages/next/src/build/swc/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function __nextjs_pure<T>(expr: T): T {
return expr
}
Loading