Skip to content

โŒ๐Ÿ“„ Removes log (or any specific) module imports and usages

License

Notifications You must be signed in to change notification settings

bendtherules/webpack-strip-log-loader

ย 
ย 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

webpack-strip-log-loader

Travis npm

A webpack loader to remove import and other statements containing reference to a module (usually, logger module).

Use case

Removing all lines which directly or indirectly reference to a third-party (or, inbuilt) logging module during production build.

For ex, removing all calls to winston logger or removing all console.* statements.

Example 1 :

Removes logger module and its usage

Pre:

import { Logger, defaultLogger } from "logger"; // strip-log

const myLogger = new Logger({ level: 2 });

var someInt = 123;
var someInt2 = someInt * 2;

myLogger.debug(someInt);
defaultLogger.log(someInt2);

Post:

var someInt = 123;
var someInt2 = someInt * 2;

Example 2 :

Removes console.* usage

Pre:

console; // strip-log

function abc() {
  var someObj = {};

  console.log(someObj);
  return someObj;
}

Post:

function abc() {
  var someObj = {};

  return someObj;
}

Please look in the usage and examples section to understand more.

Getting Started

To begin, you'll need to install webpack-strip-log-loader:

$ npm install webpack-strip-log-loader --save-dev

Then add the loader to your webpack config. For example:

// webpack.config.js
const path = require("path");

module.exports = {
  module: {
    rules: [
      {
        test: /\.js?$/,
        use: "webpack-strip-log-loader",
        // Options is optional and should include the module names whose usage (via import/require) will be stripped (in any matching file)
        options: {
          modules: ["remove-module-name"], // supports glob patterns here
          matchOptions: {} // optional, same as minimatch options
        }
      }
    ]
  }
};

And run webpack via your preferred method.

Note: We have only tested this with Webpack 3 and 4. For webpack 3, use its corresponding configuration format.

Terminologies

Knowing some terms will help you in the later section.

We will first try to show them directly within a pseudo code snippet and then proceed to describe them.

// RS = Restricted symbol
// RE = Restricted expression
// RM = Restricted module

import { Logger, defaultLogger } from "logger"; // strip-log
//      (^ RS ) (^ RS       )       (^ RM  )  (^ Trigger comment)

console; // strip-log
//       (^ Trigger comment)

const myLogger = new Logger({ level: 2 }); // normal comment
//   (^ RS   )   (^ RE                )

var someInt = 123;
var someInt2 = someInt * 2;

myLogger.debug(someInt);
//(^ RS)
//(^ RE              )

defaultLogger.log(someInt2);
//(^ RS)
//(^ RE                   )

Trigger comment - Comment with special content to mark its associated statement for further scrutiny.

Restricted module - Modules whose usage you want to remove. Its content is not changed. Typically, log modules can be marked as restricted.

Symbol - Symbols are equivalent to what we think of as variables. Wherever you use the same variable (variable hiding in inner scope creates new variable), you can think of them as the same symbol.

Restricted symbol - Restricted symbols are those symbols which either have some dependency on restricted modules or are explicitly marked as restricted. Any statement which uses this symbol will be removed. It is also used to find more related restricted symbols or expressions (for eg. when you assign a different variable/symbol to the value of an already restricted symbol, the assignee symbol also gets marked as restricted).

Restricted expression - These are those expressions which have some dependency on other restricted symbols or expressions. For eg. new abc() becomes a restricted expression if abc is a restricted symbol.

Usage

To remove the affected lines during build, there are two phases that you need to care about:

  1. Marking symbols as restricted
  2. Using restricted symbols in language constructs

1. Marking restricted symbols

The following types of statements are monitored for finding initial restricted symbols.

A. ES6 import and require calls (with strip-log comment)

Side-effect import :

import "some-css"; // strip-log

Default import :

import logger from "some-logger"; // strip-log

Namespace import :

import * as logger2 from "some-logger"; // strip-log

Named import :

import { log as speak, warn } from "some-logger"; // strip-log

Require statement:

var logger = require("some-logger"); // strip-log

Note: After build, this "marking" import / require statements will be removed.

B. Explicit symbol(s) marking

Restrict a symbol :

console; // strip-log

Restrict multiple symbols at once :

console1, console2; // strip-log

Note: After build, this "marking" statements will be removed.

C. Loader config

In webpack config file, pass options to this loader in the form of {modules: string[], matchOptions?: minimatch.IOptions } where:

  1. modules is an array of globally restricted module names (globs, matched using minimatch).
  2. matchOptions: options object as supported by minimatch (documented in https://github.com/isaacs/minimatch#options)

This is equivalent to marking all import statements to "some-logger" in all files with comment strip-log.

    module: {
      rules: [
        {
          test: /\.js$/,
          use: {
            loader: 'webpack-strip-log-loader',
            options: {
                "modules": "some-logger"
            },
          },
        },
      ],
    },

Example of glob: Setting "modules": "logger-*" will remove both statements import 'logger-1'; import log from 'logger-99';

2. Using restricted symbols in language constructs

Following type of language constructs are looped through to find more restricted expression or symbol.

A. Function call

Pre:

import defaultLogger from "some-logger"; // strip-log
defaultLogger("Init");

someOtherFunction();

Post:

someOtherFunction();

The whole function call expression for defautLogger becomes a restricted expression if the function itself is restricted.

B. Simple assignment

Pre:

import defaultLogger from "some-logger"; // strip-log

var logger = defaultLogger;
var a = 1; // some other assignment

logger("Init");

Post:

var a = 1; // some other assignment

The variable/symbol logger also becomes restricted as it was assigned to the same value as defaultLogger, which is a restricted symbol. Hence, function calls to logger would also be marked as restricted expression.

C. New call

Pre:

import Logger from "some-logger"; // strip-log

var someLogger = new Logger({ level: 3 });
someLogger("Init");

var a = new List(); // some other new call

Post:

var a = new List(); // some other new call

The variable/symbol someLogger becomes restricted as it was assigned to the new expression, which becomes restricted as the newee Logger symbol is restricted.

D. Property access (by dot notation)

Pre:

import defaultLogger from "some-logger"; // strip-log
defaultLogger.log("Init");

someOtherFunction();

Post:

someOtherFunction();

The expression defaultLogger.log becomes restricted as defaultLogger symbol is restricted. Eventually any call with that expression as the callee also becomes restricted as mentioned in previous Function call section.

Note: Combinations of this various constructs should work in most cases.

Removing statements, expressions and comments

Any restricted expression has to get removed during build. But replacing any expression with blank space is dangerous.

Consider the following:

Pre:

import LogError from "some-logger";

throw new LogError("Output stream not found");

If we just replaced the restricted new expression with blank, it would look like:

Post:

import LogError from 'some-logger';

throw ;

which would be an invalid javascript syntax (as blank is not an expression).

We could have replaced expressions with undefined which would be an appropriate replacement expression (although it might not have looked good ). But instead we have decided to (move up the AST tree from any restricted expression to) find out the its parent statement and remove the complete statement with blank.

Right now, this creates a empty new line for removed statements and doesn't remove any comment (if any) on that line.

Note: We have future plans for removing those comments which are used to only restrict symbols or import statements. Other comments should be handled with separate webpack plugins.

More examples -

Examples showing more complex cases due to mixing of multiple language constructs will be added soon.

Philosophy & Approach

Usually, we prefer to keep logs enabled in developemnt build, and supress them in production build.

Typical solution to this is to make the log function calls no-op depending on some environment variable. But with this solution, the function call statements and other log setup statements are still present in the built source code - even though they don't cause anything.

Instead, what if we could completely remove those statements during build by static analysis? That should cause slight performance improvement and cleanup the code a little bit. This was our motivation to build such a webpack plugin.

Our approach is to use Typescript compiler API to parse and understand the source file, then find the variables and expressions which depend on the restricted module and finally remove those occurences with string replacement.

There are a few finder methods which move through the whole AST to find more restricted symbols and expressions from the existing ones (already identified restricted symbols / expressions). We use multiple passes (each pass calls all the finders sequentially) to find this information. Whenever any of the finders add a new restricted symbol / expression, we do one more pass. When a complete pass doesn't find anything new, finding is considered complete.

Scope

We support a few language constructs using finders, which we think are mostly needed to use log modules. We will always try to stay true to our original goals and don't aim to grow this as a generic dependency removal plugin.

Contributing

Contribution is highly welcome from anyone. Please look into the scope before raising a new feature request. Having said that, if you have a use case where we should support more things, please let us know.

We are eager to help the community by building better tools.

Publishing to npm

One-step:

Depending on required version bump, run one of these:

npm run <release-patch|release-minor|release-major>

These commands are just shortcut for:

  1. npm version <patch|minor|major>
  2. npm publish

npm version will internally:

  1. run test and build
  2. create version commit and tag
  3. git push with tags

Thanks

Lots of thanks to

  • FusionCharts for letting me build this during office hours
  • And my colleagues there for the awesome ideas and feedback regarding this project

About

โŒ๐Ÿ“„ Removes log (or any specific) module imports and usages

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • TypeScript 98.1%
  • JavaScript 1.9%