Skip to content

Commit

Permalink
feat: Port edge assert plugin to turbopack (vercel#69041)
Browse files Browse the repository at this point in the history
### What?

Port `MiddlewarePlugin` to turbopack

### Why?

To make turbopack behave identically to webpack mode.

### How?

Closes PACK-3196
  • Loading branch information
kdy1 authored Aug 19, 2024
1 parent 0c74267 commit cc079c8
Show file tree
Hide file tree
Showing 12 changed files with 241 additions and 8 deletions.
7 changes: 7 additions & 0 deletions crates/next-core/src/next_server/transforms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::{
get_server_actions_transform_rule, next_amp_attributes::get_next_amp_attr_rule,
next_cjs_optimizer::get_next_cjs_optimizer_rule,
next_disallow_re_export_all_in_page::get_next_disallow_export_all_in_page_rule,
next_edge_node_api_assert::next_edge_node_api_assert,
next_middleware_dynamic_assert::get_middleware_dynamic_assert_rule,
next_page_static_info::get_next_page_static_info_assert_rule,
next_pure::get_next_pure_rule, server_actions::ActionsTransform,
Expand Down Expand Up @@ -133,6 +134,12 @@ pub async fn get_next_server_transforms_rules(

if let NextRuntime::Edge = next_runtime {
rules.push(get_middleware_dynamic_assert_rule(mdx_rs));
if matches!(context_ty, ServerContextType::Middleware { .. }) {
rules.push(next_edge_node_api_assert(
mdx_rs,
matches!(*mode.await?, NextMode::Build),
));
}
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/next-core/src/next_shared/transforms/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub(crate) mod next_amp_attributes;
pub(crate) mod next_cjs_optimizer;
pub(crate) mod next_disallow_re_export_all_in_page;
pub(crate) mod next_dynamic;
pub(crate) mod next_edge_node_api_assert;
pub(crate) mod next_font;
pub(crate) mod next_middleware_dynamic_assert;
pub(crate) mod next_optimize_server_react;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use anyhow::Result;
use async_trait::async_trait;
use next_custom_transforms::transforms::warn_for_edge_runtime::warn_for_edge_runtime;
use swc_core::{
common::SyntaxContext,
ecma::{ast::*, utils::ExprCtx, visit::VisitWith},
};
use turbo_tasks::Vc;
use turbopack::module_options::{ModuleRule, ModuleRuleEffect};
use turbopack_ecmascript::{CustomTransformer, EcmascriptInputTransform, TransformContext};

use super::module_rule_match_js_no_url;

pub fn next_edge_node_api_assert(enable_mdx_rs: bool, should_error: bool) -> ModuleRule {
let transformer =
EcmascriptInputTransform::Plugin(Vc::cell(
Box::new(NextEdgeNodeApiAssert { should_error }) as _,
));
ModuleRule::new(
module_rule_match_js_no_url(enable_mdx_rs),
vec![ModuleRuleEffect::ExtendEcmascriptTransforms {
prepend: Vc::cell(vec![]),
append: Vc::cell(vec![transformer]),
}],
)
}

#[derive(Debug)]
struct NextEdgeNodeApiAssert {
should_error: bool,
}

#[async_trait]
impl CustomTransformer for NextEdgeNodeApiAssert {
#[tracing::instrument(level = tracing::Level::TRACE, name = "next_edge_node_api_assert", skip_all)]
async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()> {
let mut visitor = warn_for_edge_runtime(
ctx.source_map.clone(),
ExprCtx {
is_unresolved_ref_safe: false,
unresolved_ctxt: SyntaxContext::empty().apply_mark(ctx.unresolved_mark),
},
self.should_error,
);
program.visit_with(&mut visitor);
Ok(())
}
}
3 changes: 2 additions & 1 deletion crates/next-custom-transforms/src/transforms/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod fonts;
pub mod import_analyzer;
pub mod middleware_dynamic;
pub mod next_ssg;
pub mod optimize_barrel;
pub mod optimize_server_react;
pub mod page_config;
pub mod page_static_info;
Expand All @@ -16,7 +17,7 @@ pub mod react_server_components;
pub mod server_actions;
pub mod shake_exports;
pub mod strip_page_exports;
pub mod warn_for_edge_runtime;

//[TODO] PACK-1564: need to decide reuse vs. turbopack specific
pub mod named_import_transform;
pub mod optimize_barrel;
102 changes: 102 additions & 0 deletions crates/next-custom-transforms/src/transforms/warn_for_edge_runtime.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use std::sync::Arc;

use swc_core::{
common::{errors::HANDLER, SourceMap, Span},
ecma::{
ast::{Expr, Ident, MemberExpr, MemberProp},
utils::{ExprCtx, ExprExt},
visit::{Visit, VisitWith},
},
};

pub fn warn_for_edge_runtime(cm: Arc<SourceMap>, ctx: ExprCtx, should_error: bool) -> impl Visit {
WarnForEdgeRuntime {
cm,
ctx,
should_error,
}
}

struct WarnForEdgeRuntime {
cm: Arc<SourceMap>,
ctx: ExprCtx,
should_error: bool,
}

const EDGE_UNSUPPORTED_NODE_APIS: &[&str] = &[
"clearImmediate",
"setImmediate",
"BroadcastChannel",
"ByteLengthQueuingStrategy",
"CompressionStream",
"CountQueuingStrategy",
"DecompressionStream",
"DomException",
"MessageChannel",
"MessageEvent",
"MessagePort",
"ReadableByteStreamController",
"ReadableStreamBYOBRequest",
"ReadableStreamDefaultController",
"TransformStreamDefaultController",
"WritableStreamDefaultController",
];

impl WarnForEdgeRuntime {
fn build_unsupported_api_error(&self, span: Span, api_name: &str) -> Option<()> {
let loc = self.cm.lookup_line(span.lo).ok()?;

let msg=format!("A Node.js API is used ({api_name} at line: {}) which is not supported in the Edge Runtime.
Learn more: https://nextjs.org/docs/api-reference/edge-runtime",loc.line+1);

HANDLER.with(|h| {
if self.should_error {
h.struct_span_err(span, &msg).emit();
} else {
h.struct_span_warn(span, &msg).emit();
}
});

None
}

fn is_in_middleware_layer(&self) -> bool {
true
}

fn warn_for_unsupported_process_api(&self, span: Span, prop: &Ident) {
if !self.is_in_middleware_layer() || prop.sym == "env" {
return;
}

self.build_unsupported_api_error(span, &format!("process.{}", prop.sym));
}
}

impl Visit for WarnForEdgeRuntime {
fn visit_member_expr(&mut self, n: &MemberExpr) {
if n.obj.is_global_ref_to(&self.ctx, "process") {
if let MemberProp::Ident(prop) = &n.prop {
self.warn_for_unsupported_process_api(n.span, prop);
return;
}
}

n.visit_children_with(self);
}

fn visit_expr(&mut self, n: &Expr) {
if let Expr::Ident(ident) = n {
if ident.span.ctxt == self.ctx.unresolved_ctxt {
for api in EDGE_UNSUPPORTED_NODE_APIS {
if self.is_in_middleware_layer() && ident.sym == *api {
self.build_unsupported_api_error(ident.span, api);
return;
}
}
}
}

n.visit_children_with(self);
}
}
74 changes: 69 additions & 5 deletions crates/next-custom-transforms/tests/fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,24 @@ use next_custom_transforms::transforms::{
page_config::page_config_test,
pure::pure_magic,
react_server_components::server_components,
server_actions::{
server_actions, {self},
},
server_actions::{self, server_actions},
shake_exports::{shake_exports, Config as ShakeExportsConfig},
strip_page_exports::{next_transform_strip_page_exports, ExportFilter},
warn_for_edge_runtime::warn_for_edge_runtime,
};
use serde::de::DeserializeOwned;
use swc_core::{
common::{chain, comments::SingleThreadedComments, FileName, Mark, SyntaxContext},
ecma::{
ast::{Module, Script},
parser::{EsSyntax, Syntax},
transforms::{
base::resolver,
react::jsx,
testing::{test, test_fixture},
testing::{test, test_fixture, FixtureTestConfig},
},
visit::as_folder,
utils::ExprCtx,
visit::{as_folder, noop_fold_type, Fold, Visit},
},
};
use swc_relay::{relay, RelayLanguageConfig};
Expand Down Expand Up @@ -672,3 +673,66 @@ fn test_debug_name(input: PathBuf) {
Default::default(),
);
}

#[fixture("tests/fixture/edge-assert/**/input.js")]
fn test_edge_assert(input: PathBuf) {
let output = input.parent().unwrap().join("output.js");

test_fixture(
syntax(),
&|t| {
let top_level_mark = Mark::fresh(Mark::root());
let unresolved_mark = Mark::fresh(Mark::root());

chain!(
swc_core::ecma::transforms::base::resolver(unresolved_mark, top_level_mark, true),
lint_to_fold(warn_for_edge_runtime(
t.cm.clone(),
ExprCtx {
is_unresolved_ref_safe: false,
unresolved_ctxt: SyntaxContext::empty().apply_mark(unresolved_mark),
},
true
))
)
},
&input,
&output,
FixtureTestConfig {
allow_error: true,
..Default::default()
},
);
}

fn lint_to_fold<R>(r: R) -> impl Fold
where
R: Visit,
{
LintFolder(r)
}

struct LintFolder<R>(R)
where
R: Visit;

impl<R> Fold for LintFolder<R>
where
R: Visit,
{
noop_fold_type!();

#[inline(always)]
fn fold_module(&mut self, program: Module) -> Module {
self.0.visit_module(&program);

program
}

#[inline(always)]
fn fold_script(&mut self, program: Script) -> Script {
self.0.visit_script(&program);

program
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log(process.loadEnvFile())
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log(process.loadEnvFile());
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
x A Node.js API is used (process.loadEnvFile at line: 1) which is not supported in the Edge Runtime.
| Learn more: https://nextjs.org/docs/api-reference/edge-runtime
,-[input.js:1:1]
1 | console.log(process.loadEnvFile())
: ^^^^^^^^^^^^^^^^^^^
`----
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log(process.env.NODE_ENV)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log(process.env.NODE_ENV);
4 changes: 2 additions & 2 deletions test/turbopack-build-tests-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9022,8 +9022,7 @@
"runtimeError": false
},
"test/integration/edge-runtime-with-node.js-apis/test/index.test.ts": {
"passed": [],
"failed": [
"passed": [
"Edge route using Node.js API production mode does not warn on using process.arch",
"Edge route using Node.js API production mode does not warn on using process.version",
"Edge route using Node.js API production mode warns for BroadcastChannel during build",
Expand Down Expand Up @@ -9067,6 +9066,7 @@
"Middleware using Node.js API production mode warns for process.getuid during build",
"Middleware using Node.js API production mode warns for setImmediate during build"
],
"failed": [],
"pending": [
"Edge route using Node.js API development mode does not throw on using process.arch",
"Edge route using Node.js API development mode does not throw on using process.version",
Expand Down

0 comments on commit cc079c8

Please sign in to comment.