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 a swc transform for removal of console.* calls. #31449

Merged
merged 6 commits into from Nov 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
}