Skip to content

Commit

Permalink
Replace babel with a regex-based transformation
Browse files Browse the repository at this point in the history
This regex replacement is not 100% safe because transforming JavaScript requires an actual parser.
However, parsing (e.g. via babel) comes with its own problems because now the parser needs to
be aware of syntax extensions which might not be supported by the parser, but the underlying
JavaScript engine. In fact, rewire used to have babel in place but required an extra
transform for the object spread operator (check out commit d9a81c0
or see #128). It was also notable slower
(see #132).
There is another issue: replacing const with let is not safe because of their different behavior.
That's why we also have ESLint in place which tries to identify this error case.
There is one edge case though: when a new syntax is used *and* a const re-assignment happens,
rewire would compile happily in this situation but the actual code wouldn't work.
However, since most projects have a seperate linting step which catches these const re-assignment
errors anyway, it's probably still a reasonable trade-off.

Fixes #132
  • Loading branch information
jhnns committed Apr 9, 2018
1 parent cbb2802 commit 9b77ed9
Show file tree
Hide file tree
Showing 7 changed files with 1,018 additions and 326 deletions.
68 changes: 53 additions & 15 deletions lib/moduleEnv.js
@@ -1,17 +1,48 @@
"use strict";

// TODO: Use https://www.npmjs.com/package/pirates here?

var Module = require("module"),
fs = require("fs"),
babelCore = require("babel-core"),
eslint = require("eslint"),
coffee;

var moduleWrapper0 = Module.wrapper[0],
moduleWrapper1 = Module.wrapper[1],
originalExtensions = {},
babelPlugins = [
"babel-plugin-transform-es2015-block-scoping",
"babel-plugin-transform-object-rest-spread"
],
linter = new eslint.Linter(),
eslintOptions = {
env: {
es6: true,
},
parserOptions: {
ecmaVersion: 6,
ecmaFeatures: {
globalReturn: true,
jsx: true,
experimentalObjectRestSpread: true
},
},
rules: {
"no-const-assign": 2
}
},
// The following regular expression is used to replace const declarations with let.
// This regex replacement is not 100% safe because transforming JavaScript requires an actual parser.
// However, parsing (e.g. via babel) comes with its own problems because now the parser needs to
// be aware of syntax extensions which might not be supported by the parser, but the underlying
// JavaScript engine. In fact, rewire used to have babel in place here but required an extra
// transform for the object spread operator (check out commit d9a81c0cdacf6995b24d205b4a2068adbd8b34ff
// or see https://github.com/jhnns/rewire/pull/128). It was also notable slower
// (see https://github.com/jhnns/rewire/issues/132).
// There is another issue: replacing const with let is not safe because of their different behavior.
// That's why we also have ESLint in place which tries to identify this error case.
// There is one edge case though: when a new syntax is used *and* a const re-assignment happens,
// rewire would compile happily in this situation but the actual code wouldn't work.
// However, since most projects have a seperate linting step which catches these const re-assignment
// errors anyway, it's probably still a reasonable trade-off.
// Test the regular expresssion at https://regex101.com/r/dvnZPv/2 and also check out testLib/constModule.js.
matchConst = /(^|\s|\}|;)const(\/\*|\s)/gm,
nodeRequire,
currentModule;

Expand Down Expand Up @@ -72,17 +103,27 @@ function restoreExtensions() {
}
}

function isNoConstAssignMessage(message) {
return message.ruleId === "no-const-assign";
}

function jsExtension(module, filename) {
var _compile = module._compile;

module._compile = function (content, filename) {
content = babelCore.transform(content, {
plugins: babelPlugins,
retainLines: true,
filename: filename,
babelrc: false
}).code;
_compile.call(module, content, filename);
const noConstAssignMessage = linter.verify(content, eslintOptions).find(isNoConstAssignMessage);

if (noConstAssignMessage !== undefined) {
const { line, column } = noConstAssignMessage;

throw new TypeError(`Assignment to constant variable at ${ filename }:${ line }:${ column }`);
}

_compile.call(
module,
content.replace(matchConst, "$1let$2"), // replace const with let
filename
);
};

restoreExtensions();
Expand Down Expand Up @@ -113,9 +154,6 @@ function stripBOM(content) {
return content;
}

// Prepopulate require.cache with babel plugins because otherwise it will be lazy-loaded by Babel during rewire()
babelPlugins.forEach(require);

try {
coffee = require("coffee-script");
} catch (err) {
Expand Down

0 comments on commit 9b77ed9

Please sign in to comment.