├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json ├── test ├── library.spec.js ├── test-utils.js ├── test-webpack │ ├── index.js │ └── modules │ │ ├── module-a │ │ └── index.js │ │ ├── module-b │ │ └── index.js │ │ └── module-c │ │ └── index.js └── webpack.spec.js └── utils.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es2020: true, 5 | node: true, 6 | mocha: true, 7 | }, 8 | extends: 'eslint:recommended', 9 | parserOptions: { 10 | ecmaVersion: 12, 11 | }, 12 | rules: { 13 | 'no-prototype-builtins': 0, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "singleQuote": true 4 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | before_install: 3 | - "npm install npm -g" 4 | node_js: 5 | - "lts/*" 6 | env: 7 | - TEST_SUITE=unit 8 | script: 9 | - npm run $TEST_SUITE 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## 3.0.0 (2021-04-21) 5 | ### Added 6 | - Excluding Webpack 5 module federation (automatically adding to allowlist) from [@jacob-ebey](https://github.com/jacob-ebey) 7 | 8 | ### Changed 9 | - Better arguments handling for the exported function 10 | - Changed code syntax to ES6 11 | 12 | ### Removed 13 | - Removed support for Node < 6 14 | 15 | ## 2.5.2 (2020-08-24) 16 | ### Changed 17 | - Changed exported function signature - to remove deprecation notice when used in Webpack 5 18 | 19 | ## 2.5.0 (2020-07-12) 20 | ### Added 21 | - Options validation - throwing an error when using a mispell of one of the options -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Liad Yosef 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Webpack node modules externals 2 | ============================== 3 | > Easily exclude node modules in Webpack 4 | 5 | [![Version](https://img.shields.io/npm/v/webpack-node-externals.svg)](https://www.npmjs.org/package/webpack-node-externals) 6 | [![Downloads](https://img.shields.io/npm/dm/webpack-node-externals.svg)](https://www.npmjs.org/package/webpack-node-externals) 7 | [![Build Status](https://travis-ci.org/liady/webpack-node-externals.svg?branch=master)](https://travis-ci.org/liady/webpack-node-externals) 8 | 9 | Webpack allows you to define [*externals*](https://webpack.js.org/configuration/externals) - modules that should not be bundled. 10 | 11 | When bundling with Webpack for the backend - you usually don't want to bundle its `node_modules` dependencies. 12 | This library creates an *externals* function that ignores `node_modules` when bundling in Webpack.
(Inspired by the great [Backend apps with Webpack](http://jlongster.com/Backend-Apps-with-Webpack--Part-I) series) 13 | 14 | ## Quick usage 15 | ```sh 16 | npm install webpack-node-externals --save-dev 17 | ``` 18 | 19 | In your `webpack.config.js`: 20 | ```js 21 | const nodeExternals = require('webpack-node-externals'); 22 | ... 23 | module.exports = { 24 | ... 25 | target: 'node', // in order to ignore built-in modules like path, fs, etc. 26 | externals: [nodeExternals()], // in order to ignore all modules in node_modules folder 27 | ... 28 | }; 29 | ``` 30 | And that's it. All node modules will no longer be bundled but will be left as `require('module')`. 31 | 32 | **Note**: For Webpack 5, in addition to `target: 'node'` also include the `externalsPreset` object: 33 | ```js 34 | // Webpack 5 35 | 36 | const nodeExternals = require('webpack-node-externals'); 37 | ... 38 | module.exports = { 39 | ... 40 | target: 'node', 41 | externalsPresets: { node: true }, // in order to ignore built-in modules like path, fs, etc. 42 | externals: [nodeExternals()], // in order to ignore all modules in node_modules folder 43 | ... 44 | }; 45 | ``` 46 | 47 | ## Detailed overview 48 | ### Description 49 | This library scans the `node_modules` folder for all node_modules names, and builds an *externals* function that tells Webpack not to bundle those modules, or any sub-modules of theirs. 50 | 51 | ### Configuration 52 | This library accepts an `options` object. 53 | 54 | #### `options.allowlist (=[])` 55 | An array for the `externals` to allow, so they **will** be included in the bundle. Can accept exact strings (`'module_name'`), regex patterns (`/^module_name/`), or a function that accepts the module name and returns whether it should be included. 56 |
**Important** - if you have set aliases in your webpack config with the exact same names as modules in *node_modules*, you need to allowlist them so Webpack will know they should be bundled. 57 | 58 | #### `options.importType (='commonjs')` 59 | The method in which unbundled modules will be required in the code. Best to leave as `commonjs` for node modules. 60 | May be one of [documented options](https://webpack.js.org/configuration/externals/#externals) or function `callback(moduleName)` which returns custom code to be returned as import type, e.g: 61 | ```js 62 | options.importType = function (moduleName) { 63 | return 'amd ' + moduleName; 64 | } 65 | ``` 66 | 67 | #### `options.modulesDir (='node_modules')` 68 | The folder in which to search for the node modules. 69 | 70 | #### `options.additionalModuleDirs (='[]')` 71 | Additional folders to look for node modules. 72 | 73 | #### `options.modulesFromFile (=false)` 74 | Read the modules from the `package.json` file instead of the `node_modules` folder. 75 |
Accepts a boolean or a configuration object: 76 | ```js 77 | { 78 | modulesFromFile: true, 79 | /* or */ 80 | modulesFromFile: { 81 | fileName: /* path to package.json to read from */, 82 | includeInBundle: [/* whole sections to include in the bundle, i.e 'devDependencies' */], 83 | excludeFromBundle: [/* whole sections to explicitly exclude from the bundle, i.e only 'dependencies' */] 84 | } 85 | } 86 | ``` 87 | 88 | ## Usage example 89 | ```js 90 | var nodeExternals = require('webpack-node-externals'); 91 | ... 92 | module.exports = { 93 | ... 94 | target: 'node', // important in order not to bundle built-in modules like path, fs, etc. 95 | externals: [nodeExternals({ 96 | // this WILL include `jquery` and `webpack/hot/dev-server` in the bundle, as well as `lodash/*` 97 | allowlist: ['jquery', 'webpack/hot/dev-server', /^lodash/] 98 | })], 99 | ... 100 | }; 101 | ``` 102 | 103 | For most use cases, the defaults of `importType` and `modulesDir` should be used. 104 | 105 | ## Q&A 106 | #### Why not just use a regex in the Webpack config? 107 | Webpack allows inserting [regex](https://webpack.js.org/configuration/externals/#regex) in the *externals* array, to capture non-relative modules: 108 | ```js 109 | { 110 | externals: [ 111 | // Every non-relative module is external 112 | // abc -> require("abc") 113 | /^[a-z\-0-9]+$/ 114 | ] 115 | } 116 | ``` 117 | However, this will leave unbundled **all non-relative requires**, so it does not account for aliases that may be defined in webpack itself. 118 | This library scans the `node_modules` folder, so it only leaves unbundled the actual node modules that are being used. 119 | 120 | #### How can I bundle required assets (i.e css files) from node_modules? 121 | Using the `allowlist` option, this is possible. We can simply tell Webpack to bundle all files with extensions that are not js/jsx/json, using this [regex](https://regexper.com/#%5C.(%3F!(%3F%3Ajs%7Cjson)%24).%7B1%2C5%7D%24): 122 | ```js 123 | ... 124 | nodeExternals({ 125 | // load non-javascript files with extensions, presumably via loaders 126 | allowlist: [/\.(?!(?:jsx?|json)$).{1,5}$/i], 127 | }), 128 | ... 129 | ``` 130 | Thanks @wmertens for this idea. 131 | 132 | #### Why is not bundling node_modules a good thing? 133 | 134 | When writing a node library, for instance, you may want to split your code to several files, and use Webpack to bundle them. However - you wouldn't want to bundle your code with its entire node_modules dependencies, for two reasons: 135 | 136 | 1. It will bloat your library on npm. 137 | 2. It goes against the entire npm dependencies management. If you're using Lodash, and the consumer of your library also has the same Lodash dependency, npm makes sure that it will be added only once. But bundling Lodash in your library will actually make it included twice, since npm is no longer managing this dependency. 138 | 139 | As a consumer of a library, I want the library code to include only its logic, and just state its dependencies so they could me merged/resolved with the rest of the dependencies in my project. Bundling your code with your dependencies makes it virtually impossible. 140 | 141 | In short: **It's useful if your code is used by something that has dependencies managed by npm** 142 | 143 | ## Contribute 144 | Contributions and pull requests are welcome. Please run the tests to make sure nothing breaks. 145 | ### Test 146 | ```sh 147 | npm run test 148 | ``` 149 | 150 | ## License 151 | MIT 152 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const utils = require('./utils'); 2 | 3 | const scopedModuleRegex = new RegExp( 4 | '@[a-zA-Z0-9][\\w-.]+/[a-zA-Z0-9][\\w-.]+([a-zA-Z0-9./]+)?', 5 | 'g' 6 | ); 7 | 8 | function getModuleName(request, includeAbsolutePaths) { 9 | let req = request; 10 | const delimiter = '/'; 11 | 12 | if (includeAbsolutePaths) { 13 | req = req.replace(/^.*?\/node_modules\//, ''); 14 | } 15 | // check if scoped module 16 | if (scopedModuleRegex.test(req)) { 17 | // reset regexp 18 | scopedModuleRegex.lastIndex = 0; 19 | return req.split(delimiter, 2).join(delimiter); 20 | } 21 | return req.split(delimiter)[0]; 22 | } 23 | 24 | module.exports = function nodeExternals(options) { 25 | options = options || {}; 26 | const mistakes = utils.validateOptions(options) || []; 27 | if (mistakes.length) { 28 | mistakes.forEach((mistake) => { 29 | utils.error(mistakes.map((mistake) => mistake.message)); 30 | utils.log(mistake.message); 31 | }); 32 | } 33 | const webpackInternalAllowlist = [/^webpack\/container\/reference\//]; 34 | const allowlist = [] 35 | .concat(webpackInternalAllowlist) 36 | .concat(options.allowlist || []); 37 | const binaryDirs = [].concat(options.binaryDirs || ['.bin']); 38 | const importType = options.importType || 'commonjs'; 39 | const modulesDir = options.modulesDir || 'node_modules'; 40 | const modulesFromFile = !!options.modulesFromFile; 41 | const includeAbsolutePaths = !!options.includeAbsolutePaths; 42 | const additionalModuleDirs = options.additionalModuleDirs || []; 43 | 44 | // helper function 45 | function isNotBinary(x) { 46 | return !utils.contains(binaryDirs, x); 47 | } 48 | 49 | // create the node modules list 50 | let nodeModules = modulesFromFile 51 | ? utils.readFromPackageJson(options.modulesFromFile) 52 | : utils.readDir(modulesDir).filter(isNotBinary); 53 | additionalModuleDirs.forEach(function (additionalDirectory) { 54 | nodeModules = nodeModules.concat( 55 | utils.readDir(additionalDirectory).filter(isNotBinary) 56 | ); 57 | }); 58 | 59 | // return an externals function 60 | return function (...args) { 61 | const [arg1, arg2, arg3] = args; 62 | // let context = arg1; 63 | let request = arg2; 64 | let callback = arg3; 65 | // in case of webpack 5 66 | if (arg1 && arg1.context && arg1.request) { 67 | // context = arg1.context; 68 | request = arg1.request; 69 | callback = arg2; 70 | } 71 | const moduleName = getModuleName(request, includeAbsolutePaths); 72 | if ( 73 | utils.contains(nodeModules, moduleName) && 74 | !utils.containsPattern(allowlist, request) 75 | ) { 76 | if (typeof importType === 'function') { 77 | return callback(null, importType(request)); 78 | } 79 | // mark this module as external 80 | // https://webpack.js.org/configuration/externals/ 81 | return callback(null, importType + ' ' + request); 82 | } 83 | callback(); 84 | }; 85 | }; 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-node-externals", 3 | "version": "3.0.0", 4 | "description": "Easily exclude node_modules in Webpack bundle", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/liady/webpack-node-externals.git" 9 | }, 10 | "dependencies": {}, 11 | "devDependencies": { 12 | "chai": "^3.5.0", 13 | "eslint": "^7.7.0", 14 | "eslint-plugin-import": "^2.22.0", 15 | "mocha": "^2.5.3", 16 | "mock-fs": "^4.12.0", 17 | "ncp": "^2.0.0", 18 | "webpack": "^4.44.1" 19 | }, 20 | "scripts": { 21 | "unit": "mocha --colors ./test/*.spec.js", 22 | "unit-watch": "mocha --colors -w ./test/*.spec.js", 23 | "test": "npm run unit-watch" 24 | }, 25 | "keywords": [ 26 | "webpack", 27 | "node_modules", 28 | "node", 29 | "bundle", 30 | "externals" 31 | ], 32 | "author": { 33 | "name": "Liad Yosef", 34 | "url": "https://github.com/liady" 35 | }, 36 | "files": [ 37 | "LICENSE", 38 | "README.md", 39 | "index.js", 40 | "utils.js" 41 | ], 42 | "bugs": { 43 | "url": "https://github.com/liady/webpack-node-externals/issues" 44 | }, 45 | "engines": { 46 | "node": ">=6" 47 | }, 48 | "homepage": "https://github.com/liady/webpack-node-externals", 49 | "license": "MIT" 50 | } 51 | -------------------------------------------------------------------------------- /test/library.spec.js: -------------------------------------------------------------------------------- 1 | const nodeExternals = require('../index.js'); 2 | const path = require('path'); 3 | const utils = require('../utils.js'); 4 | const testUtils = require('./test-utils.js'); 5 | const mockNodeModules = testUtils.mockNodeModules; 6 | const restoreMock = testUtils.restoreMock; 7 | const context={}; 8 | const assertResult = testUtils.buildAssertion.bind(null, context); 9 | const assertResultWebpack5 = testUtils.buildAssertionWebpack5.bind(null, context); 10 | const chai = require('chai'); 11 | const expect = chai.expect; 12 | 13 | // Test basic functionality 14 | describe('invocation with no settings', function() { 15 | 16 | before(function(){ 17 | mockNodeModules(); 18 | context.instance = nodeExternals(); 19 | }); 20 | 21 | describe('should invoke a commonjs callback', function(){ 22 | it('when given an existing module', assertResult('moduleA', 'commonjs moduleA')); 23 | it('when given another existing module', assertResult('moduleB', 'commonjs moduleB')); 24 | it('when given another existing module for scoped package', assertResult('@organisation/moduleA', 'commonjs @organisation/moduleA')); 25 | it('when given an existing sub-module', assertResult('moduleA/sub-module', 'commonjs moduleA/sub-module')); 26 | it('when given an existing file in a sub-module', assertResult('moduleA/another-sub/index.js', 'commonjs moduleA/another-sub/index.js')); 27 | it('when given an existing file in a scoped package', assertResult('@organisation/moduleA/index.js', 'commonjs @organisation/moduleA/index.js')) 28 | it('when given an another existing file in a scoped package', assertResult('@organisation/base-node/vs/base/common/paths', 'commonjs @organisation/base-node/vs/base/common/paths')) 29 | 30 | }); 31 | 32 | describe('should invoke an empty callback', function(){ 33 | it('when given a non-node module', assertResult('non-node-module', undefined)); 34 | it('when given a module in the file but not in folder', assertResult('moduleE', undefined)); 35 | it('when given a relative path', assertResult('./src/index.js', undefined)); 36 | it('when given a different absolute path', assertResult('/test/node_modules/non-node-module', undefined)); 37 | it('when given a complex different absolute path', assertResult('/test/node_modules/non-node-module/node_modules/moduleA', undefined)); 38 | it('when given an absolute path', assertResult('/test/node_modules/moduleA', undefined)); 39 | it('when given an existing sub-module inside node_modules', assertResult('/moduleA/node_modules/moduleB', undefined)); 40 | }); 41 | 42 | after(function(){ 43 | restoreMock() 44 | }); 45 | }); 46 | 47 | // Test different "importType" 48 | describe('invocation with a different importType', function() { 49 | 50 | before(function(){ 51 | mockNodeModules(); 52 | context.instance = nodeExternals({importType: 'var'}); 53 | }); 54 | 55 | describe('should invoke a var callback', function(){ 56 | it('when given an existing module', assertResult('moduleA', 'var moduleA')); 57 | it('when given another existing module', assertResult('moduleB', 'var moduleB')); 58 | it('when given another existing module for scoped package', assertResult('@organisation/moduleA', 'var @organisation/moduleA')); 59 | it('when given an existing sub-module', assertResult('moduleA/sub-module', 'var moduleA/sub-module')); 60 | it('when given an existing file in a sub-module', assertResult('moduleA/another-sub/index.js', 'var moduleA/another-sub/index.js')); 61 | it('when given an existing file in a scoped package', assertResult('@organisation/moduleA/index.js', 'var @organisation/moduleA/index.js')) 62 | 63 | }); 64 | 65 | describe('should invoke an empty callback', function(){ 66 | it('when given a non-node module', assertResult('non-node-module', undefined)); 67 | it('when given a relative path', assertResult('./src/index.js', undefined)); 68 | }); 69 | 70 | describe('should invoke a custom function', function(){ 71 | before(function(){ 72 | context.instance = nodeExternals({ importType: function(moduleName) { 73 | return 'commonjs ' + moduleName; 74 | }}); 75 | }); 76 | 77 | it('when given an existing module', assertResult('moduleA', 'commonjs moduleA')); 78 | it('when given a non-node module', assertResult('non-node-module', undefined)); 79 | }); 80 | 81 | after(function(){ 82 | restoreMock() 83 | }); 84 | }); 85 | 86 | // Test reading from file 87 | describe('reads from a file', function() { 88 | 89 | before(function(){ 90 | mockNodeModules(); 91 | context.instance = nodeExternals({modulesFromFile: true}); 92 | }); 93 | 94 | describe('should invoke a commonjs callback', function(){ 95 | it('when given an existing module in the file', assertResult('moduleE', 'commonjs moduleE')); 96 | it('when given an existing module for scoped package in the file', assertResult('@organisation/moduleE', 'commonjs @organisation/moduleE')); 97 | it('when given an existing file in a sub-module', assertResult('moduleG/another-sub/index.js', 'commonjs moduleG/another-sub/index.js')); 98 | it('when given an existing file in a scoped package', assertResult('@organisation/moduleG/index.js', 'commonjs @organisation/moduleG/index.js')) 99 | }); 100 | 101 | describe('should invoke an empty callback', function(){ 102 | it('when given a non-node module', assertResult('non-node-module', undefined)); 103 | it('when given a module in the folder but not in the file', assertResult('moduleA', undefined)); 104 | it('when given a module of scoped package in the folder but not in the file', assertResult('@organisation/moduleA', undefined)); 105 | it('when given a relative path', assertResult('./src/index.js', undefined)); 106 | }); 107 | 108 | describe('should accept options from file', function(){ 109 | describe(' > include', function(){ 110 | before(function(){ 111 | context.instance = nodeExternals({ modulesFromFile: { include: ['dependencies']}}); 112 | }); 113 | it('when given a module in the include', assertResult('moduleE', 'commonjs moduleE')); 114 | it('when given a module not in the include', assertResult('moduleG', undefined)); 115 | }); 116 | describe(' > excludeFromBundle', function(){ 117 | before(function(){ 118 | context.instance = nodeExternals({ modulesFromFile: { excludeFromBundle: ['dependencies']}}); 119 | }); 120 | it('when given a module to exclude from bundle', assertResult('moduleE', 'commonjs moduleE')); 121 | it('when given a module not to exclude from bundle', assertResult('moduleG', undefined)); 122 | }); 123 | 124 | describe(' > exclude', function(){ 125 | before(function(){ 126 | context.instance = nodeExternals({ modulesFromFile: { exclude: ['dependencies']}}); 127 | }); 128 | it('when given a module in the exclude', assertResult('moduleE', undefined)); 129 | it('when given a module not in the exclude', assertResult('moduleG', 'commonjs moduleG')); 130 | }); 131 | 132 | describe(' > includeInBundle', function(){ 133 | before(function(){ 134 | context.instance = nodeExternals({ modulesFromFile: { includeInBundle: ['dependencies']}}); 135 | }); 136 | it('when given a module to include in bundle', assertResult('moduleE', undefined)); 137 | it('when given a module not to include in bundle', assertResult('moduleG', 'commonjs moduleG')); 138 | }); 139 | 140 | describe(' > file name', function(){ 141 | before(function(){ 142 | context.instance = nodeExternals({ modulesFromFile: { fileName: 'noFile.json'}}); 143 | }); 144 | it('when given any module, return empty', assertResult('moduleE', undefined)); 145 | }); 146 | }); 147 | 148 | after(function(){ 149 | restoreMock() 150 | }); 151 | }); 152 | 153 | // Test allowlist 154 | describe('respects an allowlist', function() { 155 | 156 | before(function(){ 157 | mockNodeModules(); 158 | context.instance = nodeExternals({ 159 | allowlist: ['moduleA/sub-module', 'moduleA/another-sub/index.js', 'moduleC', function (m) { 160 | return m == 'moduleF'; 161 | }, /^moduleD/] 162 | }); 163 | }); 164 | 165 | describe('should invoke a commonjs callback', function(){ 166 | it('when given an existing module', assertResult('moduleB', 'commonjs moduleB')); 167 | it('when given an existing sub-module', assertResult('moduleB/sub-module', 'commonjs moduleB/sub-module')); 168 | it('when given a module which is the parent on an ignored path', assertResult('moduleA', 'commonjs moduleA')); 169 | it('when given a sub-module of an ignored module', assertResult('moduleC/sub-module', 'commonjs moduleC/sub-module')); 170 | it('when given a sub-module of an module ignored by a function', assertResult('moduleF/sub-module', 'commonjs moduleF/sub-module')); 171 | }); 172 | 173 | describe('should invoke an empty callback', function(){ 174 | it('when given module path ignored by a function', assertResult('moduleC', undefined)); 175 | it('when given an ignored module path', assertResult('moduleF', undefined)); 176 | it('when given an ignored sub-module path', assertResult('moduleA/sub-module', undefined)); 177 | it('when given an ignored file path', assertResult('moduleA/another-sub/index.js', undefined)); 178 | it('when given an ignored regex path', assertResult('moduleD', undefined)); 179 | it('when given an ignored regex sub-module path', assertResult('moduleD/sub-module', undefined)); 180 | it('when given a non-node module', assertResult('non-node-module', undefined)); 181 | it('when given a relative path', assertResult('./src/index.js', undefined)); 182 | }); 183 | 184 | describe('should respect webpack 5 internal allowlist', function() { 185 | it('should ignore the specific path (empty callback)', assertResult('webpack/container/reference/', undefined)); 186 | it('should invoke a commonjs callback', assertResult('moduleB', 'commonjs moduleB')); 187 | }); 188 | 189 | after(function(){ 190 | restoreMock() 191 | }); 192 | }); 193 | 194 | // Test absolute path support 195 | describe('invocation with an absolute path setting', function() { 196 | 197 | before(function(){ 198 | mockNodeModules(); 199 | context.instance = nodeExternals({ 200 | includeAbsolutePaths: true 201 | }); 202 | }); 203 | 204 | describe('should invoke a commonjs callback', function(){ 205 | it('when given an existing module', assertResult('moduleA', 'commonjs moduleA')); 206 | it('when given another existing module', assertResult('moduleB', 'commonjs moduleB')); 207 | it('when given another existing module for scoped package', assertResult('@organisation/moduleA', 'commonjs @organisation/moduleA')); 208 | it('when given an existing sub-module', assertResult('moduleA/sub-module', 'commonjs moduleA/sub-module')); 209 | it('when given an existing file in a sub-module', assertResult('moduleA/another-sub/index.js', 'commonjs moduleA/another-sub/index.js')); 210 | it('when given an existing file in a scoped package', assertResult('@organisation/moduleA/index.js', 'commonjs @organisation/moduleA/index.js')); 211 | it('when given an absolute path', assertResult('/test/node_modules/moduleA', 'commonjs /test/node_modules/moduleA')); 212 | it('when given another absolute path', assertResult('../../test/node_modules/moduleA', 'commonjs ../../test/node_modules/moduleA')); 213 | it('when given another absolute path for scoped package', assertResult('/test/node_modules/@organisation/moduleA', 'commonjs /test/node_modules/@organisation/moduleA')); 214 | it('when given an existing sub-module inside node_modules', assertResult('/moduleA/node_modules/moduleB', 'commonjs /moduleA/node_modules/moduleB')); 215 | }); 216 | 217 | describe('should invoke an empty callback', function(){ 218 | it('when given a non-node module', assertResult('non-node-module', undefined)); 219 | it('when given a module in the file but not in folder', assertResult('moduleE', undefined)); 220 | it('when given a relative path', assertResult('./src/index.js', undefined)); 221 | it('when given a different absolute path', assertResult('/test/node_modules/non-node-module', undefined)); 222 | 223 | it('when given a complex different absolute path', assertResult('/test/node_modules/non-node-module/node_modules/moduleA', undefined)); 224 | it('when given a complex different absolute path for scoped package', assertResult('/test/node_modules/non-node-module/node_modules/@organisation/moduleA', undefined)); 225 | 226 | it('when given another complex different absolute path', assertResult('../../node_modules/non-node-module/node_modules/moduleA', undefined)); 227 | it('when given another complex different absolute path for scoped package', assertResult('../../node_modules/non-node-module/node_modules/@organisation/moduleA', undefined)); 228 | 229 | }); 230 | 231 | after(function(){ 232 | restoreMock() 233 | }); 234 | }); 235 | 236 | describe('when modules dir does not exist', function() { 237 | before(function() { 238 | mockNodeModules(); 239 | }) 240 | it('should not log ENOENT error', function() { 241 | const log = global.console.log; 242 | let errorLogged = false; 243 | 244 | // wrap console.log to catch error message 245 | global.console.log = function(error) { 246 | if (error instanceof Error && error.message.indexOf("ENOENT, no such file or directory 'node_modules/somepackage/node_modules") !== -1) { 247 | errorLogged = true; 248 | } 249 | log.apply(null, arguments); 250 | } 251 | 252 | context.instance = nodeExternals({ 253 | modulesDir: 'node_modules/somepackage/node_modules' 254 | }); 255 | 256 | // cleanup specific testcase env changes 257 | global.console.log = log; 258 | 259 | expect(errorLogged, 'ENOENT not logged').to.be.equal(false); 260 | }); 261 | it('should process like node_modules is empty', function(done) { 262 | context.instance = nodeExternals({ 263 | modulesDir: 'node_modules/somepackage/node_modules' 264 | }); 265 | testUtils.buildAssertion(context, 'somepackage', undefined)(done); 266 | }); 267 | after(function(){ 268 | restoreMock() 269 | }); 270 | }) 271 | 272 | // Test basic functionality 273 | describe('invocation with no settings - webpack 5', function() { 274 | 275 | before(function(){ 276 | mockNodeModules(); 277 | context.instance = nodeExternals(); 278 | }); 279 | 280 | describe('should invoke a commonjs callback', function(){ 281 | it('when given an existing module', assertResultWebpack5('moduleA', 'commonjs moduleA')); 282 | it('when given another existing module', assertResultWebpack5('moduleB', 'commonjs moduleB')); 283 | it('when given another existing module for scoped package', assertResultWebpack5('@organisation/moduleA', 'commonjs @organisation/moduleA')); 284 | it('when given an existing sub-module', assertResultWebpack5('moduleA/sub-module', 'commonjs moduleA/sub-module')); 285 | it('when given an existing file in a sub-module', assertResultWebpack5('moduleA/another-sub/index.js', 'commonjs moduleA/another-sub/index.js')); 286 | it('when given an existing file in a scoped package', assertResultWebpack5('@organisation/moduleA/index.js', 'commonjs @organisation/moduleA/index.js')) 287 | it('when given an another existing file in a scoped package', assertResultWebpack5('@organisation/base-node/vs/base/common/paths', 'commonjs @organisation/base-node/vs/base/common/paths')) 288 | 289 | }); 290 | 291 | describe('should invoke an empty callback', function(){ 292 | it('when given a non-node module', assertResultWebpack5('non-node-module', undefined)); 293 | it('when given a module in the file but not in folder', assertResultWebpack5('moduleE', undefined)); 294 | it('when given a relative path', assertResultWebpack5('./src/index.js', undefined)); 295 | it('when given a different absolute path', assertResultWebpack5('/test/node_modules/non-node-module', undefined)); 296 | it('when given a complex different absolute path', assertResultWebpack5('/test/node_modules/non-node-module/node_modules/moduleA', undefined)); 297 | it('when given an absolute path', assertResultWebpack5('/test/node_modules/moduleA', undefined)); 298 | it('when given an existing sub-module inside node_modules', assertResultWebpack5('/moduleA/node_modules/moduleB', undefined)); 299 | }); 300 | 301 | after(function(){ 302 | restoreMock() 303 | }); 304 | }); 305 | 306 | describe('validate options', function () { 307 | it('should identify misspelled terms', function () { 308 | const results = utils.validateOptions({ whitelist: [], moduledirs: [] }); 309 | expect(results.length).to.be.equal(2); 310 | expect(results[0].correctTerm).to.be.equal('allowlist'); 311 | expect(results[1].correctTerm).to.be.equal('modulesDir'); 312 | }); 313 | it('should ignore duplications', function () { 314 | const results = utils.validateOptions({ whitelist: [], moduledirs: [], allowlist: [] }); 315 | expect(results.length).to.be.equal(1); 316 | expect(results[0].correctTerm).to.be.equal('modulesDir'); 317 | }); 318 | it('should identify wrong casing', function () { 319 | const results = utils.validateOptions({ allowList: [], modulesdir: [] }); 320 | expect(results.length).to.be.equal(2); 321 | expect(results[0].correctTerm).to.be.equal('allowlist'); 322 | expect(results[1].correctTerm).to.be.equal('modulesDir'); 323 | }); 324 | it('should no identify undefineds', function () { 325 | const results = utils.validateOptions({ allowlist: undefined, modulesdir: [] }); 326 | expect(results.length).to.be.equal(1); 327 | expect(results[0].correctTerm).to.be.equal('modulesDir'); 328 | }); 329 | }); 330 | 331 | describe('file path', function() { 332 | it('should return the correct file path', function() { 333 | expect(utils.getFilePath({ fileName: 'file.json' })).to.be.equal(path.resolve(process.cwd(), 'file.json')); 334 | expect(utils.getFilePath({})).to.be.equal(path.resolve(process.cwd(), 'package.json')); 335 | expect(utils.getFilePath({ fileName: path.resolve(__dirname) })).to.be.equal(path.resolve(__dirname)); 336 | }) 337 | }) 338 | -------------------------------------------------------------------------------- /test/test-utils.js: -------------------------------------------------------------------------------- 1 | const mockDir = require('mock-fs'); 2 | const nodeExternals = require('../index.js'); 3 | const webpack = require('webpack'); 4 | const fs = require('fs'); 5 | const ncp = require('ncp').ncp; 6 | const path = require('path'); 7 | const relative = path.join.bind(path, __dirname); 8 | const chai = require('chai'); 9 | const expect = chai.expect; 10 | 11 | /** 12 | * Creates an assertion function that makes sure to output expectedResult when given moduleName 13 | * @param {object} context context object that holds the instance 14 | * @param {string} moduleName given module name 15 | * @param {string} expectedResult expected external module string 16 | * @return {function} the assertion function 17 | */ 18 | exports.buildAssertion = function buildAssertion(context, moduleName, expectedResult){ 19 | return function(done) { 20 | context.instance(relative(), moduleName, function(noarg, externalModule) { 21 | expect(externalModule).to.be.equal(expectedResult); 22 | done(); 23 | }) 24 | }; 25 | } 26 | 27 | /** 28 | * Creates an assertion function that makes sure to output expectedResult when given moduleName 29 | * <> 30 | * @param {object} context context object that holds the instance 31 | * @param {string} moduleName given module name 32 | * @param {string} expectedResult expected external module string 33 | * @return {function} the assertion function 34 | */ 35 | exports.buildAssertionWebpack5 = function buildAssertion(context, moduleName, expectedResult){ 36 | return function(done) { 37 | context.instance({ context: relative(), request: moduleName }, function(noarg, externalModule) { 38 | expect(externalModule).to.be.equal(expectedResult); 39 | done(); 40 | }) 41 | }; 42 | } 43 | 44 | /** 45 | * Mocks the fs module to output a desired structure 46 | * @param {object} structure the requested structure 47 | * @return {void} 48 | */ 49 | exports.mockNodeModules = function mockNodeModules(structure){ 50 | structure = structure || { 51 | 'moduleA' : { 52 | 'sub-module':{}, 53 | 'another-sub':{ 54 | 'index.js' : '' 55 | }, 56 | }, 57 | 'moduleB' : { 58 | 'sub-module':{} 59 | }, 60 | 'moduleC' : {}, 61 | 'moduleD' : { 62 | 'sub-module':{} 63 | }, 64 | 'moduleF' : {}, 65 | '@organisation/moduleA':{}, 66 | '@organisation/base-node':{}, 67 | }; 68 | 69 | mockDir({ 70 | 'node_modules' : structure, 71 | 'package.json': JSON.stringify({ 72 | dependencies: { 73 | 'moduleE': '1.0.0', 74 | 'moduleF': '1.0.0', 75 | '@organisation/moduleE': '1.0.0', 76 | }, 77 | devDependencies: { 78 | 'moduleG': '1.0.0', 79 | '@organisation/moduleG': '1.0.0', 80 | }, 81 | }) 82 | }); 83 | } 84 | 85 | /** 86 | * Restores the fs module 87 | * @return {void} 88 | */ 89 | exports.restoreMock = function restoreMock(){ 90 | mockDir.restore(); 91 | } 92 | 93 | exports.copyModules = function(moduleNames) { 94 | return Promise.all(moduleNames.map(function(moduleName) { 95 | return copyDir(relative('test-webpack', 'modules', moduleName), relative('../node_modules', moduleName)); 96 | })); 97 | } 98 | 99 | exports.removeModules = function(moduleNames) { 100 | moduleNames.forEach(function(moduleName){ 101 | removeDir(relative('../node_modules', moduleName)); 102 | }); 103 | } 104 | 105 | /** 106 | * Creates an assertion function that makes sure the result contains/doesnt contain expected modules 107 | * @param {object} nodeExternalsConfig The node externals configuration 108 | * @param {object} externals expected externals 109 | * @param {object} nonExternals expected non externals 110 | * @return {function} the assertion function 111 | */ 112 | exports.webpackAssertion = function webpackAssertion(nodeExternalsConfig, externals, nonExternals){ 113 | return function() { 114 | return generateWithWebpack(nodeExternalsConfig).then(function(result) { 115 | assertExternals(result, externals, nonExternals); 116 | }); 117 | }; 118 | } 119 | 120 | /** 121 | * Generates the result file with Webpack, using our nodeExternals 122 | * @param {object} context The context object to hang the result on 123 | * @param {object} nodeExternalsConfig The node externals configuration 124 | * @return {Promise} 125 | */ 126 | function generateWithWebpack(nodeExternalsConfig) { 127 | const testDir = relative('test-webpack'); 128 | const outputFileName = 'bundle.js'; 129 | const outputFile = path.join(testDir, outputFileName); 130 | return new Promise(function(resolve, reject) { 131 | webpack({ 132 | entry: path.join(testDir, 'index.js'), 133 | output: { 134 | filename: outputFileName, 135 | path: testDir 136 | }, 137 | externals: [nodeExternals(nodeExternalsConfig)], 138 | resolve: { 139 | alias: { 140 | 'module-c' : path.join(testDir, './modules/module-c') 141 | } 142 | } 143 | }, function(err){ 144 | if(err) { 145 | reject(err); 146 | } else { 147 | const contents = fs.readFileSync(outputFile, 'utf-8'); 148 | fs.unlinkSync(outputFile); 149 | resolve(contents); 150 | } 151 | }); 152 | }); 153 | } 154 | 155 | function assertExternals(result , externals, nonExternals) { 156 | externals.forEach(function(moduleName) { 157 | expect(result).to.not.contain(bundled(moduleName)); 158 | expect(result).to.contain(external(moduleName)); 159 | }); 160 | nonExternals.forEach(function(moduleName) { 161 | expect(result).to.not.contain(external(moduleName)); 162 | expect(result).to.contain(bundled(moduleName)); 163 | }); 164 | } 165 | 166 | function bundled(moduleName) { 167 | return moduleName + ':bundled'; 168 | } 169 | 170 | function external(moduleName) { 171 | return 'require("'+ moduleName +'")'; 172 | } 173 | 174 | function removeDir(dirName) { 175 | if(fs.existsSync(dirName) ) { 176 | fs.readdirSync(dirName).forEach(function(file){ 177 | fs.unlinkSync(path.join(dirName, file)); 178 | }); 179 | fs.rmdirSync(dirName); 180 | } 181 | } 182 | 183 | function copyDir(source, dest) { 184 | return new Promise(function(resolve, reject) { 185 | ncp(source, dest, function(err) { 186 | if(err) { 187 | reject(err) 188 | } else { 189 | resolve() 190 | } 191 | }) 192 | }) 193 | } -------------------------------------------------------------------------------- /test/test-webpack/index.js: -------------------------------------------------------------------------------- 1 | const x = require('module-a'); 2 | const y = require('module-b'); 3 | const z = require('module-c'); 4 | 5 | module.exports = { 6 | x: x, 7 | y: y, 8 | z: z, 9 | }; 10 | -------------------------------------------------------------------------------- /test/test-webpack/modules/module-a/index.js: -------------------------------------------------------------------------------- 1 | function a() { 2 | console.log('module-a:bundled'); 3 | } 4 | 5 | module.exports = a; -------------------------------------------------------------------------------- /test/test-webpack/modules/module-b/index.js: -------------------------------------------------------------------------------- 1 | function b() { 2 | console.log('module-b:bundled'); 3 | } 4 | 5 | module.exports = b; -------------------------------------------------------------------------------- /test/test-webpack/modules/module-c/index.js: -------------------------------------------------------------------------------- 1 | function c() { 2 | console.log('module-c:bundled'); 3 | } 4 | 5 | module.exports = c; -------------------------------------------------------------------------------- /test/webpack.spec.js: -------------------------------------------------------------------------------- 1 | const testUtils = require('./test-utils.js'); 2 | const webpackAssertion = testUtils.webpackAssertion 3 | 4 | // Test actual webpack output 5 | describe('actual webpack bundling', function() { 6 | 7 | before(function() { 8 | return testUtils.copyModules(['module-a', 'module-b']); 9 | }); 10 | 11 | describe('basic tests', function() { 12 | it('should output modules without bundling', webpackAssertion({}, ['module-a', 'module-b'], ['module-c'])); 13 | it('should honor a allowlist', webpackAssertion({ allowlist: ['module-a'] }, ['module-b'], ['module-a', 'module-c'])); 14 | }); 15 | 16 | describe('with webpack aliased module in node_modules', function() { 17 | before(function() { 18 | return testUtils.copyModules(['module-c']); 19 | }); 20 | it('should bundle aliased modules', webpackAssertion({ allowlist: ['module-c'] }, ['module-a', 'module-b'], ['module-c'])); 21 | }) 22 | 23 | after(function() { 24 | testUtils.removeModules(['module-a', 'module-b', 'module-c']); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | exports.contains = function contains(arr, val) { 5 | return arr && arr.indexOf(val) !== -1; 6 | }; 7 | 8 | const atPrefix = new RegExp('^@', 'g'); 9 | exports.readDir = function readDir(dirName) { 10 | if (!fs.existsSync(dirName)) { 11 | return []; 12 | } 13 | 14 | try { 15 | return fs 16 | .readdirSync(dirName) 17 | .map(function (module) { 18 | if (atPrefix.test(module)) { 19 | // reset regexp 20 | atPrefix.lastIndex = 0; 21 | try { 22 | return fs 23 | .readdirSync(path.join(dirName, module)) 24 | .map(function (scopedMod) { 25 | return module + '/' + scopedMod; 26 | }); 27 | } catch (e) { 28 | return [module]; 29 | } 30 | } 31 | return module; 32 | }) 33 | .reduce(function (prev, next) { 34 | return prev.concat(next); 35 | }, []); 36 | } catch (e) { 37 | return []; 38 | } 39 | }; 40 | 41 | function getFilePath(options) { 42 | return path.resolve(process.cwd(), options.fileName || 'package.json'); 43 | } 44 | exports.getFilePath = getFilePath; 45 | 46 | exports.readFromPackageJson = function readFromPackageJson(options) { 47 | if (typeof options !== 'object') { 48 | options = {}; 49 | } 50 | const includeInBundle = options.exclude || options.includeInBundle; 51 | const excludeFromBundle = options.include || options.excludeFromBundle; 52 | 53 | // read the file 54 | let packageJson; 55 | try { 56 | const packageJsonString = fs.readFileSync(getFilePath(options), 'utf8'); 57 | packageJson = JSON.parse(packageJsonString); 58 | } catch (e) { 59 | return []; 60 | } 61 | // sections to search in package.json 62 | let sections = [ 63 | 'dependencies', 64 | 'devDependencies', 65 | 'peerDependencies', 66 | 'optionalDependencies', 67 | ]; 68 | if (excludeFromBundle) { 69 | sections = [].concat(excludeFromBundle); 70 | } 71 | if (includeInBundle) { 72 | sections = sections.filter(function (section) { 73 | return [].concat(includeInBundle).indexOf(section) === -1; 74 | }); 75 | } 76 | // collect dependencies 77 | const deps = {}; 78 | sections.forEach(function (section) { 79 | Object.keys(packageJson[section] || {}).forEach(function (dep) { 80 | deps[dep] = true; 81 | }); 82 | }); 83 | return Object.keys(deps); 84 | }; 85 | 86 | exports.containsPattern = function containsPattern(arr, val) { 87 | return ( 88 | arr && 89 | arr.some(function (pattern) { 90 | if (pattern instanceof RegExp) { 91 | return pattern.test(val); 92 | } else if (typeof pattern === 'function') { 93 | return pattern(val); 94 | } else { 95 | return pattern == val; 96 | } 97 | }) 98 | ); 99 | }; 100 | 101 | exports.validateOptions = function (options) { 102 | options = options || {}; 103 | const results = []; 104 | const mistakes = { 105 | allowlist: ['allowslist', 'whitelist', 'allow'], 106 | importType: ['import', 'importype', 'importtype'], 107 | modulesDir: ['moduledir', 'moduledirs'], 108 | modulesFromFile: ['modulesfile'], 109 | includeAbsolutePaths: ['includeAbsolutesPaths'], 110 | additionalModuleDirs: ['additionalModulesDirs', 'additionalModulesDir'], 111 | }; 112 | const optionsKeys = Object.keys(options); 113 | const optionsKeysLower = optionsKeys.map(function (optionName) { 114 | return optionName && optionName.toLowerCase(); 115 | }); 116 | Object.keys(mistakes).forEach(function (correctTerm) { 117 | if (!options.hasOwnProperty(correctTerm)) { 118 | mistakes[correctTerm] 119 | .concat(correctTerm.toLowerCase()) 120 | .forEach(function (mistake) { 121 | const ind = optionsKeysLower.indexOf(mistake.toLowerCase()); 122 | if (ind > -1) { 123 | results.push({ 124 | message: `Option '${optionsKeys[ind]}' is not supported. Did you mean '${correctTerm}'?`, 125 | wrongTerm: optionsKeys[ind], 126 | correctTerm: correctTerm, 127 | }); 128 | } 129 | }); 130 | } 131 | }); 132 | return results; 133 | }; 134 | 135 | exports.log = function (message) { 136 | console.log(`[webpack-node-externals] : ${message}`); 137 | }; 138 | 139 | exports.error = function (errors) { 140 | throw new Error( 141 | errors 142 | .map(function (error) { 143 | return `[webpack-node-externals] : ${error}`; 144 | }) 145 | .join('\r\n') 146 | ); 147 | }; 148 | --------------------------------------------------------------------------------