一、打包后文件分析
/**
* 01 打包后的文件就是一个函数自调用,当前函数调用时传入一个对象
* 02 这个对象我们为了方便将之称为是模块定义,它就是一个键值对
* 03 这个键名就是当前被加载模块的文件名与某个目录的拼接()
* 04 这个键值就是一个函数,和 node.js 里的模块加载有一些类似,会将被加载模块中的内容包裹于一个函数中
* 05 这个函数在将来某个时间点上会被调用,同时会接收到一定的参数,利用这些参数就可以实现模块的加载操作
*
* 06 针对于上述的代码就相当于是将 {}(模块定义) 传递给了 modules
*/
缓存处理,moduleId是入口文件的 路径 ./src/index.js
// Check if module is in cache
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
modules:如下
{
"./src/index.js":
/*! no static exports found */
(function (module, exports, __webpack_require__) {
let name = __webpack_require__(/*! ./login.js */ "./src/login.js")
console.log('index.js内容执行了')
console.log(name)
}),
"./src/login.js":
/*! no static exports found */
(function (module, exports) {
module.exports = '拉勾教育'
})
}
// 将模块定义保存一份,通过 m 属性挂载到自定义的方法身上
__webpack_require__.m = modules;
// 判断被传入的对象 obj 身上是否具有指定的属性 **** ,如果有则返回 true
__webpack_require__.o = function (object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
// 如果当前 exports 身上不具备 name 属性,则条件成立
if (!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
// define __esModule on exports
__webpack_require__.r = function (exports) {
// 下面的条件如果成立就说明是一个 esModule
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
// Object.prototype.toString.call(exports)
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
// 如果条件不成立,我们也直接在 exports 对象的身上添加一个 __esModule 属性,它的值就是true
Object.defineProperty(exports, '__esModule', { value: true });
};
__webpack_require__.t = function (value, mode) {
// 01 调用 t 方法之后,我们会拿到被加载模块中的内容 value
// 02 对于 value 来说我们可能会直接返回,也可能会处理之后再返回
if (mode & 1) value = __webpack_require__(value);
if (mode & 8) return value;
if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
var ns = Object.create(null);
__webpack_require__.r(ns);
Object.defineProperty(ns, 'default', { enumerable: true, value: value });
if (mode & 2 && typeof value != 'string') for (var key in value) __webpack_require__.d(ns, key, function (key) { return value[key]; }.bind(null, key));
return ns;
};
二、功能函数手写实现01
login.js
// module.exports = '拉勾教育'
export default 'zce是一个帅哥'
export const age = 40
index.js
import name, { age } from './login.js'
console.log('index.js 执行了')
console.log(name, '<------')
console.log(age, '<------')
mybuild.js
(function (modules) {
// 01 定义对象用于将来缓存被加载过的模块
let installedModules = {}
// 02 定义一个 __webpack_require__ 方法来替换 import require 加载操作
function __webpack_require__(moduleId) {
// 2-1 判断当前缓存中是否存在要被加载的模块内容,如果存在则直接返回
if (installedModules[moduleId]) {
return installedModules[moduleId].exports
}
// 2-2 如果当前缓存中不存在则需要我们自己定义{} 执行被导入的模块内容加载
let module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
}
// 2-3 调用当前 moduleId 对应的函数,然后完成内容的加载
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)
// 2-4 当上述的方法调用完成之后,我们就可以修改 l 的值用于表示当前模块内容已经加载完成了
module.l = true
// 2-5 加载工作完成之后,要将拿回来的内容返回至调用的位置
return module.exports
}
// 03 定义 m 属性用于保存 modules
__webpack_require__.m = modules
// 04 定义 c 属性用于保存 cache
__webpack_require__.c = installedModules
// 05 定义 o 方法用于判断对象的身上是否存在指定的属性
__webpack_require__.o = function (object, property) {
return Object.prototype.hasOwnProperty(object, property)
}
// 06 定义 d 方法用于在对象的身上添加指定的属性,同时给该属性提供一个 getter
__webpack_require__.d = function (exports, name, getter) {
if (!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter })
}
}
// 07 定义 r 方法用于标识当前模块是 es6 类型
__webpack_require__.r = function (exports) {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" })
}
Object.defineProperty(exports, '__esModule', { value: true })
}
// 08 定义 n 方法,用于设置具体的 getter
__webpack_require__.n = function (module) {
let getter = module && module.__esModule ?
function getDefault() { return module['default'] } :
function getModuleExports() { return module }
__webpack_require__.d(getter, 'a', getter)
return getter
}
// 09 定义 P 属性,用于保存资源访问路径
__webpack_require__.p = ""
// 10 调用 __webpack_require__ 方法执行模块导入与加载操作
return __webpack_require__(__webpack_require__.s = './src/index.js')
})
({
"./src/index.js":
(function (module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
var _login_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./login.js */ "./src/login.js");
console.log('index.js 执行了')
console.log(_login_js__WEBPACK_IMPORTED_MODULE_0__["default"], '<------')
console.log(_login_js__WEBPACK_IMPORTED_MODULE_0__["age"], '<------')
}),
"./src/login.js":
(function (module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, "age", function () { return age; });
__webpack_exports__["default"] = ('zce是一个帅哥');
const age = 40
})
})
三、t方法分析及实现
// 11 定义 t 方法,用于加载指定 value 的模块内容,之后对内容进行处理再返回
__webpack_require__.t = function (value, mode) {
// 01 加载 value 对应的模块内容( value 一般就是模块 id )
// 加载之后的内容又重新赋值给 value 变量
if (mode & 1) {
value = __webpack_require__(value)
}
if (mode & 8) { // 加载了可以直接返回使用的内容
return value
}
if ((mode & 4) && typeof value === 'object' && value && value.__esModule) {
return value
}
// 如果 8 和 4 都没有成立则需要自定义 ns 来通过 default 属性返回内容
let ns = Object.create(null)
__webpack_require__.r(ns)
Object.defineProperty(ns, 'default', { enumerable: true, value: value })
if (mode & 2 && typeof value !== 'string') {
for (var key in value) {
__webpack_require__.d(ns, key, function (key) {
return value[key]
}.bind(null, key))
}
}
return ns
}
四、单文件懒加载
index.js
let oBtn = document.getElementById('btn')
oBtn.addEventListener('click', function () {
import(/*webpackChunkName: "login"*/'./login.js').then((login) => {
console.log(login)
})
})
console.log('index.js执行了')
(function (modules) { // webpackBootstrap
// install a JSONP callback for chunk loading
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];
// add "moreModules" to the modules object,
// then flag all "chunkIds" as loaded and fire callback
var moduleId, chunkId, i = 0, resolves = [];
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
if (parentJsonpFunction) parentJsonpFunction(data);
while (resolves.length) {
resolves.shift()();
}
};
// The module cache
var installedModules = {};
// object to store loaded and loading chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// Promise = chunk loading, 0 = chunk loaded
var installedChunks = {
"main": 0
};
// script path function
function jsonpScriptSrc(chunkId) {
return __webpack_require__.p + "" + chunkId + ".built.js"
}
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
// This file contains only the entry chunk.
// The chunk loading function for additional chunks
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];
// JSONP chunk loading for javascript
var installedChunkData = installedChunks[chunkId];
if (installedChunkData !== 0) { // 0 means "already installed".
// a Promise means "currently loading".
if (installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// setup Promise in chunk cache
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise);
// start chunk loading
var script = document.createElement('script');
var onScriptComplete;
script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.src = jsonpScriptSrc(chunkId);
// create error before stack unwound to get useful stacktrace later
var error = new Error();
onScriptComplete = function (event) {
// avoid mem leaks in IE.
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if (chunk !== 0) {
if (chunk) {
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
error.name = 'ChunkLoadError';
error.type = errorType;
error.request = realSrc;
chunk[1](error);
}
installedChunks[chunkId] = undefined;
}
};
var timeout = setTimeout(function () {
onScriptComplete({ type: 'timeout', target: script });
}, 120000);
script.onerror = script.onload = onScriptComplete;
document.head.appendChild(script);
}
}
return Promise.all(promises);
};
// expose the modules object (__webpack_modules__)
__webpack_require__.m = modules;
// expose the module cache
__webpack_require__.c = installedModules;
// define getter function for harmony exports
__webpack_require__.d = function (exports, name, getter) {
if (!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
// define __esModule on exports
__webpack_require__.r = function (exports) {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
// create a fake namespace object
// mode & 1: value is a module id, require it
// mode & 2: merge all properties of value into the ns
// mode & 4: return value when already ns object
// mode & 8|1: behave like require
__webpack_require__.t = function (value, mode) {
if (mode & 1) value = __webpack_require__(value);
if (mode & 8) return value;
if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
var ns = Object.create(null);
__webpack_require__.r(ns);
Object.defineProperty(ns, 'default', { enumerable: true, value: value });
if (mode & 2 && typeof value != 'string') for (var key in value) __webpack_require__.d(ns, key, function (key) { return value[key]; }.bind(null, key));
return ns;
};
// getDefaultExport function for compatibility with non-harmony modules
__webpack_require__.n = function (module) {
var getter = module && module.__esModule ?
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };
__webpack_require__.d(getter, 'a', getter);
return getter;
};
// Object.prototype.hasOwnProperty.call
__webpack_require__.o = function (object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
// __webpack_public_path__
__webpack_require__.p = "";
// on error function for async loading
__webpack_require__.oe = function (err) { console.error(err); throw err; };
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
/************************************************************************/
({
"./src/index.js":
(function (module, exports, __webpack_require__) {
let oBtn = document.getElementById('btn')
oBtn.addEventListener('click', function () {
__webpack_require__.e(/*! import() | login */ "login").then(__webpack_require__.t.bind(null, /*! ./login.js */ "./src/login.js", 7)).then((login) => {
console.log(login)
})
})
console.log('index.js执行了')
})
});
五、webpack与tapable
Webpack编译流程
- 配置初始化
- 内容编译
- 输出编译后内容
事件驱动型事件流工作机制
- 负责编译的complier
- 负责创建bundies的compilation
complier和compilation都是tapable库的实例对象
tapable工作流程
- 实例化hook注册事件监听
- 通过hook触发事件监听
- 执行懒编译生成的可执行代码
hook本质是tapable实例对象
hook执行机制可分为同步和异步
hook执行特点
- Hook:普通钩子,监听器之间互相独立不干扰
- BailHook:熔断钩子,某个监听返回非undefined时后续不执行
- WaterfallHook:瀑布钩子,上一个监听的返回值可传递至下一个
- LoopHook:循环钩子,如果当前未返回false则一直执行
tapable库同步钩子
- SynckHook
- SyncBailHook
- SyncWaterfallHook
- SyncLoopHook
tapable库异步串行钩子
- AsyncSeriesHook
- AsyncSeriesBailHook
- AsyncSeriesWaterfallHook
SynckHook.js
const { SyncHook } = require('tapable')
let hook = new SyncHook(['name', 'age'])
hook.tap('fn1', function (name, age) {
console.log('fn1--->', name, age)
})
hook.tap('fn2', function (name, age) {
console.log('fn2--->', name, age)
})
hook.call('zoe', 18)
SyncBailHook
const { SyncBailHook } = require('tapable')
let hook = new SyncBailHook(['name', 'age'])
hook.tap('fn1', function (name, age) {
console.log('fn1--->', name, age)
})
hook.tap('fn2', function (name, age) {
console.log('fn2--->', name, age)
return 'ret2' // 有return 下面就不执行了
})
hook.tap('fn3', function (name, age) {
console.log('fn3--->', name, age)
})
hook.call('lg', 100)
const { SyncBailHook } = require('tapable')
let hook = new SyncBailHook(['name', 'age'])
hook.tap('fn1', function (name, age) {
console.log('fn1--->', name, age)
})
hook.tap('fn2', function (name, age) {
console.log('fn2--->', name, age)
return undefined // 下面继续执行
})
hook.tap('fn3', function (name, age) {
console.log('fn3--->', name, age)
})
hook.call('lg', 100)
SyncWaterfallHook
const { SyncWaterfallHook } = require('tapable')
let hook = new SyncWaterfallHook(['name', 'age'])
hook.tap('fn1', function (name, age) {
console.log('fn1--->', name, age)
return 'ret1'
})
hook.tap('fn2', function (name, age) {
console.log('fn2--->', name, age)
return 'ret2'
})
hook.tap('fn3', function (name, age) {
console.log('fn3--->', name, age)
return 'ret3'
})
hook.call('zce', 33)
LoopHook
const { SyncLoopHook } = require('tapable')
let hook = new SyncLoopHook(['name', 'age'])
let count1 = 0
let count2 = 0
let count3 = 0
hook.tap('fn1', function (name, age) {
console.log('fn1--->', name, age)
if (++count1 === 1) {
count1 = 0
return undefined
}
return true
})
hook.tap('fn2', function (name, age) {
console.log('fn2--->', name, age)
if (++count2 === 2) {
count2 = 0
return undefined
}
return true
})
hook.tap('fn3', function (name, age) {
console.log('fn3--->', name, age)
})
hook.call('foo', 100)
**AsyncParallelHook ** 异步并行
const { AsyncParallelHook } = require('tapable')
let hook = new AsyncParallelHook(['name'])
//对于异步钩子的使用,在添加事件监听时会存在三种方式: tap tapAsync tapPromise
hook.tap('fn1', function (name) {
console.log('fn1--->', name)
})
hook.tap('fn2', function (name) {
console.log('fn2--->', name)
})
hook.callAsync('zoe', function () {
console.log('最后执行了回调操作')
})
const { AsyncParallelHook } = require('tapable')
let hook = new AsyncParallelHook(['name'])
console.time('time')
hook.tapAsync('fn1', function (name, callback) {
setTimeout(() => {
console.log('fn1--->', name)
callback()
}, 1000)
})
hook.tapAsync('fn2', function (name, callback) {
setTimeout(() => {
console.log('fn2--->', name)
callback()
}, 2000)
})
hook.callAsync('lg', function () {
console.log('最后一个回调执行了')
console.timeEnd('time')
})
const { AsyncParallelHook } = require('tapable')
let hook = new AsyncParallelHook(['name'])
// 03 promise
console.time('time')
hook.tapPromise('fn1', function (name) {
return new Promise(function (resolve, reject) {
setTimeout(() => {
console.log('fn1--->', name)
resolve()
}, 1000)
})
})
hook.tapPromise('fn2', function (name) {
return new Promise(function (resolve, reject) {
setTimeout(() => {
console.log('fn2--->', name)
resolve()
}, 2000)
})
})
hook.promise('foo').then(() => {
console.log('end执行了')
console.timeEnd('time')
})
**AsyncParallelBailHook **
const { AsyncParallelBailHook } = require('tapable')
let hook = new AsyncParallelBailHook(['name'])
console.time('time')
hook.tapAsync('fn1', function (name, callback) {
setTimeout(() => {
console.log('fn1--->', name)
callback()
}, 1000)
})
hook.tapAsync('fn2', function (name, callback) {
setTimeout(() => {
console.log('fn2--->', name)
callback('err')
}, 2000)
})
hook.tapAsync('fn3', function (name, callback) {
setTimeout(() => {
console.log('fn3--->', name)
callback()
}, 3000)
})
hook.callAsync('zce', function () {
console.log('最后的回调执行了')
console.timeEnd('time')
})
**AsyncSeriesHook ** 串行
const { AsyncSeriesHook } = require('tapable')
let hook = new AsyncSeriesHook(['name'])
console.time('time')
hook.tapPromise('fn1', function (name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('fn1--->', name)
resolve()
}, 1000)
})
})
hook.tapPromise('fn2', function (name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('fn2--->', name)
resolve()
}, 2000)
})
})
hook.promise('foo').then(function () {
console.log('~~~~')
console.timeEnd('time')
})
六、手写SyncHook
useHook.js
const SyncHook = require('./SyncHook.js')
let hook = new SyncHook(['name', 'age'])
hook.tap('fn1', function (name, age) {
console.log('fn1-->', name, age)
})
hook.tap('fn2', function (name, age) {
console.log('fn2-->', name, age)
})
hook.call('zoe66', 18)
/**
* 01 实例化 hook , 定义 _x = [f1, f2, ...] taps = [{}, {}]
* 02 实例调用 tap taps = [{}, {}]
* 03 调用 call 方法, HookCodeFactory setup create
* 04 Hook SyncHook HookCodeFactory
*/
syncHook.js
let Hook = require('./Hook.js')
class HookCodeFactory {
args() {
return this.options.args.join(',') // ["name", "age"]===> name, age
}
head() {
return `var _x = this._x;`
}
content() {
let code = ``
for (var i = 0; i < this.options.taps.length; i++) {
code += `var _fn${i} = _x[${i}];_fn${i}(${this.args()});`
}
return code
}
setup(instance, options) { // 先准备后续需要使用到的数据
this.options = options // 这里的操作在源码中是通过 init 方法实现,而我们当前是直接挂在了 this 身上
instance._x = options.taps.map(o => o.fn) // this._x = [f1, f2, ....]
}
create() { // 核心就是创建一段可执行的代码体然后返回
let fn
// fn = new Function("name, age", "var _x = this._x, var _fn0 = _x[0]; _fn0(name, age);")
fn = new Function(
this.args(),
this.head() + this.content()
)
return fn
}
}
let factory = new HookCodeFactory()
class SyncHook extends Hook {
constructor(args) {
super(args)
}
compile(options) { // {taps: [{}, {}], args: [name, age]}
factory.setup(this, options)
return factory.create(options)
}
}
module.exports = SyncHook
hook.js
class Hook {
constructor(args = []) {
this.args = args
this.taps = [] // 将来用于存放组装好的 {}
this._x = undefined // 将来在代码工厂函数中会给 _x = [f1, f2, f3....]
}
tap(options, fn) {
if (typeof options === 'string') {
options = { name: options }
}
options = Object.assign({ fn }, options) // { fn:... name:fn1 }
// 调用以下方法将组装好的 options 添加至 []
this._insert(options)
}
_insert(options) {
this.taps[this.taps.length] = options
}
call(...args) {
// 01 创建将来要具体执行的函数代码结构
let callFn = this._createCall()
// 02 调用上述的函数(args传入进去)
return callFn.apply(this, args)
}
_createCall() {
return this.compile({
taps: this.taps,
args: this.args
})
}
}
module.exports = Hook
七、定位webpack打包入口
run.js
webpack打包时一开始核心的东西
let webpack = require('webpack')
let options = require('./webpack.config.js')
let complier = webpack(options)
complier.run(function (err, stats) {
console.log(err)
console.log(stats.toJson())
})
01 cmd 文件核心的作用就组装了 node *****/webpack/bin/webpack.js
02 webpack.js 中核心的操作就是 require 了 node_modules/webpack-cli/bin/cli.js
03 cli.js
01 当前文件一般有二个操作,处理参数,将参数交给不同的逻辑(分发业务)
02 options
03 complier
04 complier.run( 至于run 里面做了什么,后续再看,当前只关注代码入口点 )
run.js
let webpack = require('./lgPack')
let options = require('./webpack.config')
let compiler = webpack(options)
compiler.run((err, stats) => {
console.log(err)
console.log(stats.toJson({
entries: true,
chunks: false,
modules: false,
assets: false
}))
})
webpack.js
const Compiler = require('./Compiler')
const NodeEnvironmentPlugin = require('./node/NodeEnvironmentPlugin')
const webpack = function (options) {
// 01 实例化 compiler 对象
let compiler = new Compiler(options.context)
compiler.options = options
// 02 初始化 NodeEnvironmentPlugin(让compiler具体文件读写能力)
new NodeEnvironmentPlugin().apply(compiler)
// 03 挂载所有 plugins 插件至 compiler 对象身上
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
plugin.apply(compiler)
}
}
// 04 挂载所有 webpack 内置的插件(入口)
// compiler.options = new WebpackOptionsApply().process(options, compiler);
// 05 返回 compiler 对象即可
return compiler
}
module.exports = webpack
compiler.js
const {
Tapable,
AsyncSeriesHook
} = require('tapable')
class Compiler extends Tapable {
constructor(context) {
super()
this.context = context
this.hooks = {
done: new AsyncSeriesHook(["stats"]),
}
}
run(callback) {
callback(null, {
toJson() {
return {
entries: [], // 当前次打包的入口信息
chunks: [], // 当前次打包的 chunk 信息
modules: [], // 模块信息
assets: [], // 当前次打包最终生成的资源
}
}
})
}
}
module.exports = Compiler
NodeEnvironmentPlugin.js
const fs = require('fs')
class NodeEnvironmentPlugin {
constructor(options) {
this.options = options || {}
}
apply(complier) {
complier.inputFileSystem = fs
complier.outputFileSystem = fs
}
}
module.exports = NodeEnvironmentPlugin