diff --git a/examples/remove-console/.gitignore b/examples/remove-console/.gitignore
new file mode 100644
index 000000000000000..1437c53f70bc211
--- /dev/null
+++ b/examples/remove-console/.gitignore
@@ -0,0 +1,34 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+# vercel
+.vercel
diff --git a/examples/remove-console/README.md b/examples/remove-console/README.md
new file mode 100644
index 000000000000000..aeb9c98a281f0e3
--- /dev/null
+++ b/examples/remove-console/README.md
@@ -0,0 +1,27 @@
+# Remove Console Example
+
+This example shows how to use the `removeConsole` config option to remove all `console.*` calls.
+
+## Preview
+
+Preview the example live on [StackBlitz](http://stackblitz.com/):
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/vercel/next.js/tree/canary/examples/remove-console)
+
+## Deploy your own
+
+Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example):
+
+[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/remove-console&project-name=remove-console&repository-name=remove-console)
+
+## How to use
+
+Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:
+
+```bash
+npx create-next-app --example remove-console remove-console-app
+# or
+yarn create next-app --example remove-console remove-console-app
+```
+
+Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
diff --git a/examples/remove-console/next.config.js b/examples/remove-console/next.config.js
new file mode 100644
index 000000000000000..afad83efd0b78b4
--- /dev/null
+++ b/examples/remove-console/next.config.js
@@ -0,0 +1,9 @@
+module.exports = {
+ experimental: {
+ removeConsole: {
+ exclude: ['error'],
+ },
+ // Uncomment this to suppress all logs.
+ // removeConsole: true,
+ },
+}
diff --git a/examples/remove-console/package.json b/examples/remove-console/package.json
new file mode 100644
index 000000000000000..f9170ae254fa3b5
--- /dev/null
+++ b/examples/remove-console/package.json
@@ -0,0 +1,13 @@
+{
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start"
+ },
+ "dependencies": {
+ "next": "latest",
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
+ }
+}
diff --git a/examples/remove-console/pages/index.js b/examples/remove-console/pages/index.js
new file mode 100644
index 000000000000000..802d5c9f9ae5346
--- /dev/null
+++ b/examples/remove-console/pages/index.js
@@ -0,0 +1,10 @@
+const Index = () => (
+
+
The console should be empty.
+
+)
+
+console.log('log from index.js')
+console.error('error log from index.js')
+
+export default Index
diff --git a/packages/next/build/swc/options.js b/packages/next/build/swc/options.js
index 1475914d19271af..a058eca259fe689 100644
--- a/packages/next/build/swc/options.js
+++ b/packages/next/build/swc/options.js
@@ -66,6 +66,7 @@ function getBaseSWCOptions({
displayName: Boolean(development),
}
: null,
+ removeConsole: nextConfig?.experimental?.removeConsole,
}
}
diff --git a/packages/next/build/swc/src/lib.rs b/packages/next/build/swc/src/lib.rs
index 45d30fb928b1a2b..ba091950fb087e8 100644
--- a/packages/next/build/swc/src/lib.rs
+++ b/packages/next/build/swc/src/lib.rs
@@ -60,7 +60,9 @@ pub mod minify;
pub mod next_dynamic;
pub mod next_ssg;
pub mod page_config;
+pub mod remove_console;
pub mod styled_jsx;
+mod top_level_binding_collector;
mod transform;
mod util;
@@ -87,6 +89,9 @@ pub struct TransformOptions {
#[serde(default)]
pub styled_components: Option,
+
+ #[serde(default)]
+ pub remove_console: Option,
}
pub fn custom_before_pass(file: Arc, opts: &TransformOptions) -> impl Fold {
@@ -113,7 +118,12 @@ pub fn custom_before_pass(file: Arc, opts: &TransformOptions) -> imp
Optional::new(
page_config::page_config(opts.is_development, opts.is_page_file),
!opts.disable_page_config
- )
+ ),
+ match &opts.remove_console {
+ Some(config) if config.truthy() =>
+ Either::Left(remove_console::remove_console(config.clone())),
+ _ => Either::Right(noop()),
+ },
)
}
diff --git a/packages/next/build/swc/src/remove_console.rs b/packages/next/build/swc/src/remove_console.rs
new file mode 100644
index 000000000000000..dd7cb46cb0f8a61
--- /dev/null
+++ b/packages/next/build/swc/src/remove_console.rs
@@ -0,0 +1,146 @@
+use serde::Deserialize;
+use swc_atoms::JsWord;
+use swc_common::collections::AHashSet;
+use swc_common::DUMMY_SP;
+use swc_ecmascript::ast::*;
+use swc_ecmascript::utils::ident::IdentLike;
+use swc_ecmascript::utils::Id;
+use swc_ecmascript::visit::{noop_fold_type, Fold, FoldWith};
+
+use crate::top_level_binding_collector::collect_top_level_decls;
+
+#[derive(Clone, Debug, Deserialize)]
+#[serde(untagged)]
+pub enum Config {
+ All(bool),
+ WithOptions(Options),
+}
+
+impl Config {
+ pub fn truthy(&self) -> bool {
+ match self {
+ Config::All(b) => *b,
+ Config::WithOptions(_) => true,
+ }
+ }
+}
+
+#[derive(Clone, Debug, Deserialize)]
+pub struct Options {
+ #[serde(default)]
+ pub exclude: Vec,
+}
+
+struct RemoveConsole {
+ exclude: Vec,
+ bindings: Vec>,
+ in_function_params: bool,
+}
+
+impl RemoveConsole {
+ fn is_global_console(&self, ident: &Ident) -> bool {
+ &ident.sym == "console"
+ && self
+ .bindings
+ .iter()
+ .find(|x| x.contains(&ident.to_id()))
+ .is_none()
+ }
+
+ fn should_remove_call(&mut self, n: &CallExpr) -> bool {
+ let callee = &n.callee;
+ let member_expr = match callee {
+ ExprOrSuper::Expr(e) => match &**e {
+ Expr::Member(m) => m,
+ _ => return false,
+ },
+ _ => return false,
+ };
+
+ // Don't attempt to evaluate computed properties.
+ if member_expr.computed {
+ return false;
+ }
+
+ // Only proceed if the object is the global `console` object.
+ match &member_expr.obj {
+ ExprOrSuper::Expr(e) => match &**e {
+ Expr::Ident(i) if self.is_global_console(i) => {}
+ _ => return false,
+ },
+ _ => return false,
+ }
+
+ // Check if the property is requested to be excluded.
+ // Here we do an O(n) search on the list of excluded properties because the size
+ // should be small.
+ match &*member_expr.prop {
+ Expr::Ident(i) if self.exclude.iter().find(|x| **x == i.sym).is_none() => {}
+ _ => return false,
+ }
+
+ true
+ }
+}
+
+impl Fold for RemoveConsole {
+ noop_fold_type!();
+
+ fn fold_stmt(&mut self, stmt: Stmt) -> Stmt {
+ match &stmt {
+ Stmt::Expr(e) => match &*e.expr {
+ Expr::Call(c) => {
+ if self.should_remove_call(c) {
+ return Stmt::Empty(EmptyStmt { span: DUMMY_SP });
+ }
+ }
+ _ => {}
+ },
+ _ => {}
+ }
+ stmt.fold_children_with(self)
+ }
+
+ fn fold_function(&mut self, mut func: Function) -> Function {
+ self.in_function_params = true;
+ let mut new_params: AHashSet = AHashSet::default();
+ for param in &func.params {
+ new_params.extend(collect_top_level_decls(param));
+ }
+ self.in_function_params = false;
+
+ self.bindings.push(new_params);
+ self.bindings.push(collect_top_level_decls(&func));
+ func.body = func.body.fold_with(self);
+ self.bindings.pop().unwrap();
+ self.bindings.pop().unwrap();
+ func
+ }
+
+ fn fold_module(&mut self, module: Module) -> Module {
+ self.bindings.push(collect_top_level_decls(&module));
+ let m = module.fold_children_with(self);
+ self.bindings.pop().unwrap();
+ m
+ }
+
+ fn fold_script(&mut self, script: Script) -> Script {
+ self.bindings.push(collect_top_level_decls(&script));
+ let s = script.fold_with(self);
+ self.bindings.pop().unwrap();
+ s
+ }
+}
+
+pub fn remove_console(config: Config) -> impl Fold {
+ let exclude = match config {
+ Config::WithOptions(x) => x.exclude,
+ _ => vec![],
+ };
+ let remover = RemoveConsole {
+ exclude,
+ bindings: Default::default(),
+ in_function_params: false,
+ };
+ remover
+}
diff --git a/packages/next/build/swc/src/top_level_binding_collector.rs b/packages/next/build/swc/src/top_level_binding_collector.rs
new file mode 100644
index 000000000000000..a5b31386bc3fb7f
--- /dev/null
+++ b/packages/next/build/swc/src/top_level_binding_collector.rs
@@ -0,0 +1,128 @@
+use std::hash::Hash;
+use swc_common::{collections::AHashSet, SyntaxContext, DUMMY_SP};
+use swc_ecmascript::{
+ ast::{
+ ClassDecl, FnDecl, Ident, ImportDefaultSpecifier, ImportNamedSpecifier,
+ ImportStarAsSpecifier, Invalid, ModuleItem, ObjectPatProp, Param, Pat, Stmt, VarDeclarator,
+ },
+ utils::ident::IdentLike,
+ visit::{noop_visit_type, Node, Visit, VisitWith},
+};
+
+// Modified from swc_ecma_utils/src/lib.rs:BindingCollector.
+pub struct TopLevelBindingCollector
+where
+ I: IdentLike + Eq + Hash + Send + Sync,
+{
+ only: Option,
+ bindings: AHashSet,
+ is_pat_decl: bool,
+}
+
+impl TopLevelBindingCollector
+where
+ I: IdentLike + Eq + Hash + Send + Sync,
+{
+ fn add(&mut self, i: &Ident) {
+ if let Some(only) = self.only {
+ if only != i.span.ctxt {
+ return;
+ }
+ }
+
+ self.bindings.insert(I::from_ident(i));
+ }
+}
+
+impl Visit for TopLevelBindingCollector
+where
+ I: IdentLike + Eq + Hash + Send + Sync,
+{
+ noop_visit_type!();
+
+ fn visit_class_decl(&mut self, node: &ClassDecl, _: &dyn Node) {
+ self.add(&node.ident);
+ }
+
+ fn visit_fn_decl(&mut self, node: &FnDecl, _: &dyn Node) {
+ self.add(&node.ident);
+ }
+
+ fn visit_pat(&mut self, node: &Pat, _: &dyn Node) {
+ if self.is_pat_decl {
+ match node {
+ Pat::Ident(i) => self.add(&i.id),
+ Pat::Object(o) => {
+ for prop in o.props.iter() {
+ match prop {
+ ObjectPatProp::Assign(a) => self.add(&a.key),
+ ObjectPatProp::KeyValue(k) => k.value.visit_with(k, self),
+ ObjectPatProp::Rest(_) => {}
+ }
+ }
+ }
+ Pat::Array(a) => {
+ for elem in a.elems.iter() {
+ elem.visit_with(a, self);
+ }
+ }
+ _ => {}
+ }
+ }
+ }
+
+ fn visit_param(&mut self, node: &Param, _: &dyn Node) {
+ let old = self.is_pat_decl;
+ self.is_pat_decl = true;
+ node.visit_children_with(self);
+ self.is_pat_decl = old;
+ }
+
+ fn visit_import_default_specifier(&mut self, node: &ImportDefaultSpecifier, _: &dyn Node) {
+ self.add(&node.local);
+ }
+
+ fn visit_import_named_specifier(&mut self, node: &ImportNamedSpecifier, _: &dyn Node) {
+ self.add(&node.local);
+ }
+
+ fn visit_import_star_as_specifier(&mut self, node: &ImportStarAsSpecifier, _: &dyn Node) {
+ self.add(&node.local);
+ }
+
+ fn visit_module_items(&mut self, nodes: &[ModuleItem], _: &dyn Node) {
+ for node in nodes {
+ node.visit_children_with(self)
+ }
+ }
+
+ fn visit_stmts(&mut self, nodes: &[Stmt], _: &dyn Node) {
+ for node in nodes {
+ node.visit_children_with(self)
+ }
+ }
+
+ fn visit_var_declarator(&mut self, node: &VarDeclarator, _: &dyn Node) {
+ let old = self.is_pat_decl;
+ self.is_pat_decl = true;
+ node.name.visit_with(node, self);
+
+ self.is_pat_decl = false;
+ node.init.visit_with(node, self);
+ self.is_pat_decl = old;
+ }
+}
+
+pub fn collect_top_level_decls(n: &N) -> AHashSet
+where
+ I: IdentLike + Eq + Hash + Send + Sync,
+ N: VisitWith>,
+{
+ let mut v = TopLevelBindingCollector {
+ only: None,
+ bindings: Default::default(),
+ is_pat_decl: false,
+ };
+ n.visit_with(&Invalid { span: DUMMY_SP }, &mut v);
+ v.bindings
+}
diff --git a/packages/next/build/swc/tests/fixture.rs b/packages/next/build/swc/tests/fixture.rs
index e9bcdf4ae6cc33e..56ad91991da1b6a 100644
--- a/packages/next/build/swc/tests/fixture.rs
+++ b/packages/next/build/swc/tests/fixture.rs
@@ -1,6 +1,6 @@
use next_swc::{
amp_attributes::amp_attributes, next_dynamic::next_dynamic, next_ssg::next_ssg,
- page_config::page_config_test, styled_jsx::styled_jsx,
+ page_config::page_config_test, remove_console::remove_console, styled_jsx::styled_jsx,
};
use std::path::PathBuf;
use swc_common::{chain, comments::SingleThreadedComments, FileName, Mark, Span, DUMMY_SP};
@@ -119,3 +119,14 @@ fn page_config_fixture(input: PathBuf) {
let output = input.parent().unwrap().join("output.js");
test_fixture(syntax(), &|_tr| page_config_test(), &input, &output);
}
+
+#[fixture("tests/fixture/remove-console/**/input.js")]
+fn remove_console_fixture(input: PathBuf) {
+ let output = input.parent().unwrap().join("output.js");
+ test_fixture(
+ syntax(),
+ &|_tr| remove_console(next_swc::remove_console::Config::All(true)),
+ &input,
+ &output,
+ );
+}
diff --git a/packages/next/build/swc/tests/fixture/remove-console/all/simple/input.js b/packages/next/build/swc/tests/fixture/remove-console/all/simple/input.js
new file mode 100644
index 000000000000000..1bb8ab88ac90c7d
--- /dev/null
+++ b/packages/next/build/swc/tests/fixture/remove-console/all/simple/input.js
@@ -0,0 +1,42 @@
+console.log("remove console test at top level");
+
+export function shouldRemove() {
+ console.log("remove console test in function");
+ console.error("remove console test in function / error");
+}
+
+export function locallyDefinedConsole() {
+ let console = {
+ log: () => { },
+ };
+ console.log();
+}
+
+export function capturedConsole() {
+ let console = {
+ log: () => { },
+ };
+ function innerFunc() {
+ console.log();
+ }
+}
+
+export function overrideInParam(console) {
+ console.log("")
+}
+
+export function overrideInParamObjectPatPropAssign({ console }) {
+ console.log("")
+}
+
+export function overrideInParamObjectPatPropKeyValue({ c: console }) {
+ console.log("")
+}
+
+export function overrideInParamObjectPatPropKeyValueNested({ c: { console } }) {
+ console.log("")
+}
+
+export function overrideInParamArray([ console ]) {
+ console.log("")
+}
diff --git a/packages/next/build/swc/tests/fixture/remove-console/all/simple/output.js b/packages/next/build/swc/tests/fixture/remove-console/all/simple/output.js
new file mode 100644
index 000000000000000..ee23146e5d62996
--- /dev/null
+++ b/packages/next/build/swc/tests/fixture/remove-console/all/simple/output.js
@@ -0,0 +1,36 @@
+;
+export function shouldRemove() {
+ ;
+ ;
+}
+export function locallyDefinedConsole() {
+ let console = {
+ log: ()=>{
+ }
+ };
+ console.log();
+}
+export function capturedConsole() {
+ let console = {
+ log: ()=>{
+ }
+ };
+ function innerFunc() {
+ console.log();
+ }
+}
+export function overrideInParam(console) {
+ console.log("");
+}
+export function overrideInParamObjectPatPropAssign({ console }) {
+ console.log("");
+}
+export function overrideInParamObjectPatPropKeyValue({ c: console }) {
+ console.log("");
+}
+export function overrideInParamObjectPatPropKeyValueNested({ c: { console } }) {
+ console.log("");
+}
+export function overrideInParamArray([console]) {
+ console.log("");
+}
diff --git a/packages/next/build/swc/tests/fixture/remove-console/all/toplevel-override/input.js b/packages/next/build/swc/tests/fixture/remove-console/all/toplevel-override/input.js
new file mode 100644
index 000000000000000..150e9ddd78c3fb3
--- /dev/null
+++ b/packages/next/build/swc/tests/fixture/remove-console/all/toplevel-override/input.js
@@ -0,0 +1,9 @@
+let console = {
+ log: (msg) => {},
+};
+
+function func1() {
+ console.log("remove console test in function");
+}
+
+console.log("remove console test at top level");
diff --git a/packages/next/build/swc/tests/fixture/remove-console/all/toplevel-override/output.js b/packages/next/build/swc/tests/fixture/remove-console/all/toplevel-override/output.js
new file mode 100644
index 000000000000000..2d3b8aa02a89996
--- /dev/null
+++ b/packages/next/build/swc/tests/fixture/remove-console/all/toplevel-override/output.js
@@ -0,0 +1,8 @@
+let console = {
+ log: (msg)=>{
+ }
+};
+function func1() {
+ console.log("remove console test in function");
+}
+console.log("remove console test at top level");
diff --git a/packages/next/build/swc/tests/full.rs b/packages/next/build/swc/tests/full.rs
index 38ab5603ed6d317..9b1dbf6bc9db132 100644
--- a/packages/next/build/swc/tests/full.rs
+++ b/packages/next/build/swc/tests/full.rs
@@ -57,6 +57,7 @@ fn test(input: &Path, minify: bool) {
is_page_file: false,
is_development: true,
styled_components: Some(assert_json("{}")),
+ remove_console: None,
};
let options = options.patch(&fm);
diff --git a/packages/next/server/config-shared.ts b/packages/next/server/config-shared.ts
index aa20df71b079069..b15a32f39117a3e 100644
--- a/packages/next/server/config-shared.ts
+++ b/packages/next/server/config-shared.ts
@@ -126,6 +126,11 @@ export type NextConfig = { [key: string]: any } & {
crossOrigin?: false | 'anonymous' | 'use-credentials'
swcMinify?: boolean
experimental?: {
+ removeConsole?:
+ | boolean
+ | {
+ exclude?: string[]
+ }
styledComponents?: boolean
swcMinify?: boolean
cpus?: number