Skip to content

Commit

Permalink
Add a swc transform for removal of console.* calls. (#31449)
Browse files Browse the repository at this point in the history
  • Loading branch information
losfair committed Nov 16, 2021
1 parent aedb865 commit a39a896
Show file tree
Hide file tree
Showing 16 changed files with 492 additions and 2 deletions.
34 changes: 34 additions & 0 deletions 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
27 changes: 27 additions & 0 deletions 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)).
9 changes: 9 additions & 0 deletions examples/remove-console/next.config.js
@@ -0,0 +1,9 @@
module.exports = {
experimental: {
removeConsole: {
exclude: ['error'],
},
// Uncomment this to suppress all logs.
// removeConsole: true,
},
}
13 changes: 13 additions & 0 deletions 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"
}
}
10 changes: 10 additions & 0 deletions examples/remove-console/pages/index.js
@@ -0,0 +1,10 @@
const Index = () => (
<div>
<p>The console should be empty.</p>
</div>
)

console.log('log from index.js')
console.error('error log from index.js')

export default Index
1 change: 1 addition & 0 deletions packages/next/build/swc/options.js
Expand Up @@ -66,6 +66,7 @@ function getBaseSWCOptions({
displayName: Boolean(development),
}
: null,
removeConsole: nextConfig?.experimental?.removeConsole,
}
}

Expand Down
12 changes: 11 additions & 1 deletion packages/next/build/swc/src/lib.rs
Expand Up @@ -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;

Expand All @@ -87,6 +89,9 @@ pub struct TransformOptions {

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

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

pub fn custom_before_pass(file: Arc<SourceFile>, opts: &TransformOptions) -> impl Fold {
Expand All @@ -113,7 +118,12 @@ pub fn custom_before_pass(file: Arc<SourceFile>, 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()),
},
)
}

Expand Down
146 changes: 146 additions & 0 deletions 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<JsWord>,
}

struct RemoveConsole {
exclude: Vec<JsWord>,
bindings: Vec<AHashSet<Id>>,
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<Id> = 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
}

0 comments on commit a39a896

Please sign in to comment.