├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── esm-webpack-plugin.js ├── package-lock.json ├── package.json └── test ├── fixtures ├── cjs_module │ ├── index.js │ └── webpack.config.js ├── code_splitting │ ├── a.js │ ├── index.js │ └── webpack.config.js ├── esm_externals │ ├── bar-external.js │ ├── foo-external.js │ ├── index.js │ └── webpack.config.js ├── esm_module │ ├── index.js │ └── webpack.config.js ├── export_globals │ ├── index.js │ ├── math.js │ └── webpack.config.js ├── external_esmodule │ ├── foo-external.js │ ├── index.js │ └── webpack.config.js ├── global_externals │ ├── index.js │ └── webpack.config.js └── multi_module_entry │ ├── include.js │ ├── index.js │ ├── skip.js │ └── webpack.config.js ├── index.test.js └── utils.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | indent_size = 4 10 | indent_style = space 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | tmp/ 64 | my.* 65 | me.* 66 | .idea 67 | .idea/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Paul T. 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 | # esm-webpack-plugin 2 | 3 | Output an ESM library from your bundle. Adds `export` statements to the end of the bundle for the exported members. Ideal for consumption by Javascript environments that support the ESM spec (aka: all major modern browsers). 4 | 5 | 6 | > **IMPORTANT: This Plugin is currently only supported with Webpack 4.x.** 7 | > __________ 8 | > The purpose of this plugin was to provide a temporary workaround until support for ESM lands in Webpack, which at one point was targeted for [v5.0](https://webpack.js.org/blog/2020-10-10-webpack-5-release/) but unfortunately it did not land. My current understanding is that the Webapck team will continue to make progress in introducing full support for ESM output bundles at the v5.x level, but unclear what version it will actually land on. At this time, I don't have plans to release a version of this Plugin that supports Webpack v5.0, but if someone wants to contribute the necessary changes to make this compatible with v5.x, I will consider it 🙏 . 9 | > 10 | > __________ 11 | 12 | 13 | ## Install 14 | 15 | ```bash 16 | npm i -D @purtuga/esm-webpack-plugin 17 | ``` 18 | 19 | ## Usage 20 | 21 | In your webpack configuration (`webpack.config.js`): 22 | 23 | ```javascript 24 | const EsmWebpackPlugin = require("@purtuga/esm-webpack-plugin"); 25 | 26 | module.exports = { 27 | mode: "production", 28 | entry: "index.js", 29 | output: { 30 | library: "LIB", 31 | libraryTarget: "var" 32 | }, 33 | //... 34 | plugins: [ 35 | new EsmWebpackPlugin() 36 | ] 37 | } 38 | ``` 39 | 40 | Notice the use of `output.library` and `output.libraryTarget`, which indicates a library is being built and the bundle will expose it via a scoped variable named `LIB`. 41 | 42 | > __NOTE__: the value for `output.library` should NOT match the name of an exported library member. 43 | 44 | > If using this plugin on a CommonJS source project, see the FAQ below for more information. 45 | 46 | ## Options 47 | 48 | Options are supported by providing an object to the Plugin constructor. All are optional. Example: 49 | 50 | ```javascript 51 | const EsmWebpackPlugin = require("@purtuga/esm-webpack-plugin"); 52 | 53 | module.exports = { 54 | //... 55 | plugins: [ 56 | new EsmWebpackPlugin({ 57 | /*... Plugin Options here ...*/ 58 | }) 59 | ] 60 | } 61 | ``` 62 | 63 | ### Supported options: 64 | 65 | - `exclude {Function}`: A callback function that will be used to determine if a given file name (a named output file) should be excluded from processing. By default, all files whose file extension does **not** end with `.js` or `.mjs` will be excluded (meaning: no ESM `export` statements will be added to the output file). Note that callback is applied to the named output chunks that webpack outputs. 66 | Function callback will receive two arguments - the `fileName` that is being process and webpack's `chunk` object that contains that file name. 67 | ```javascript 68 | new EsmWebpackPlugin({ 69 | exclude(fileName, chunk) { 70 | // exclude if not a .js/.mjs/.cjs file 71 | return !/\.[cm]?js/i.test(fileName); 72 | } 73 | }) 74 | ``` 75 | 76 | - `skipModule {Function}`: A callback function that can be used to skip over certain modules whose exports should not be included. Useful for when certain development plugins from webpack are used (like the `devServer`). The callback is provided with two arguments - the file name for the given module and the Webpack module class instance. 77 | Example - don't include webpack devServer generated bundles and modules: 78 | ```javascript 79 | new EsmWebpackPlugin({ 80 | exclude(fileName) { 81 | // Exclude if: 82 | // a. not a js file 83 | // b. is a devServer.hot file 84 | return !/\.[cm]?js$/i.test(fileName) || 85 | /\.hot-update\.js$/i.test(fileName); 86 | }, 87 | skipModule(fileName, module) { 88 | return /[\\\/]webpack(-dev-server)?[\\\/]/.test(moduleName); 89 | } 90 | }) 91 | ``` 92 | 93 | - `moduleExternals {boolean}`: A boolean that determines whether [webpack externals](https://webpack.js.org/configuration/externals/#root) should be imported as ES modules or not. Defaults to `false`. 94 | When set to true, the defined webpack `externals` will be added to the output ES module as `import`'s. Example: Given the following code module 95 | ```javascript 96 | import foo from 'foo-mod' 97 | 98 | export const doFoo = () => foo(); 99 | ``` 100 | with a webpack configuration containing the following: 101 | ```javascript 102 | const EsmWebpackPlugin = require("@purtuga/esm-webpack-plugin"); 103 | 104 | module.exports = { 105 | //... 106 | externals: { 107 | 'foo-mod': '/some/external/location/foo-mod.js' 108 | }, 109 | plugins: [ 110 | new EsmWebpackPlugin({ 111 | /*... Plugin Options here ...*/ 112 | }) 113 | ] 114 | } 115 | ``` 116 | would generate an ESM with the following `import`: 117 | ```javascript 118 | import * as __WEBPACK_EXTERNAL_MODULE__0__ from '/some/external/location/foo-mod.js'; 119 | 120 | var LIB = 121 | /******/ (function(modules) { // webpackBootstrap 122 | //... 123 | })(); 124 | //... 125 | export { 126 | _LIB$doFoo as doFoo 127 | } 128 | ``` 129 | 130 | - `esModuleExternals {boolean}`: This option applies only when `moduleExternals` options is `true` (see above). A boolean that determines whether esm-webpack-plugin will add the `__esModule` property to all imported externals. This can be helpful for improving interop between CJS and ESM modules, since webpack treats modules with the `__esModule` property differently than modules without them. Defaults to `true`. 131 | 132 | To add the `__esModule` property, esm-webpack-plugin uses a function `cloneWithEsModuleProperty()` which creates a new object that proxies to the original module, since ES modules are not extensible. 133 | 134 | ## Example 135 | 136 | Given the above Usage example: 137 | 138 | ### Entry File: `index.js` 139 | 140 | ```javascript 141 | import {horn} from "lib/noises" 142 | export {bark} from "lib/noises" 143 | 144 | export function makeHornNoise() { 145 | horn(); 146 | } 147 | 148 | export default makeHornNoise; 149 | 150 | ``` 151 | 152 | ### Library module `lib/noises.js` 153 | 154 | ```javascript 155 | export function horn() { 156 | return "honk honk"; 157 | } 158 | 159 | export function bark() { 160 | return "woof woof"; 161 | } 162 | 163 | ``` 164 | 165 | ### Output Bundle 166 | 167 | ```javascript 168 | var LIB = (/******/ (function(modules){/* webpack bundle code */})); 169 | 170 | export const bark = LIB['bark']; 171 | export const makeHornNoise = LIB['makeHornNoise']; 172 | export default LIB['default']; 173 | 174 | ``` 175 | 176 | 177 | ### Example of usage on the Browser 178 | 179 | In the browser: 180 | 181 | ```html 182 | 186 | ``` 187 | 188 | Or: 189 | 190 | ```html 191 | 196 | ``` 197 | 198 | # FAQ 199 | 200 | ## When using the generated ESM library, un-used exports are not removed from final output (not three-shaken) 201 | 202 | This is, unfortunately, a drawback and limitation of this plugin. This plugin does not change how the code is bundled or structured by webpack and only adds `export` statements to the end of file in order to enable its use via ES6 `import`. Because of that, tree-shaking is not possible - all code is already bundled and stored using webpack's internal structure. The ability to possibly support tree-shaking can only truly be supported when webpack itself introduces support for generating ESM output targets. 203 | 204 | My advice is to use the generated ESM modules at runtime when no build/bundling pipeline exists on a project and to `import` source directly (if that is coded as ESM) when a pipeline does exists. 205 | 206 | 207 | ## With CommonJS project, individual `exports` are not available in the output ESM module 208 | This project was created primarily for use in sources that are developed using ESM. The default behavior, if the plugin is unable to identify explicit `export`'s is to expose the entire library object (the `LIB` variable as seen in the examples above). A workaround that might work is to create an `ESM` entry source file whose sole purpose is to expose the desired members and use that as your webpack `entry` file. Here is an example: 209 | 210 | File `/index.cjs`: 211 | ```javascript 212 | exports.libA = require("./lib-a.cjs").libA; 213 | exports.cjsIndex = function cjsIndex() { 214 | console.log("src-cjs/index.cjs loaded!"); 215 | } 216 | ``` 217 | 218 | File `/index.mjs` (use this with webpack): 219 | ```javascript 220 | import * as cjs from "./index.cjs"; 221 | 222 | const { libA, cjsIndex } = cjs; 223 | 224 | export default cjs; 225 | export { 226 | libA, 227 | cjsIndex 228 | }; 229 | ``` 230 | 231 | Note that in order for this work, I believe (have not confirmed) that webpack's mode needs to be `javascript/auto` which I think is currently the default. 232 | 233 | 234 | ## Uncaught SyntaxError: Identifier 'MyLibrary' has already been declared 235 | 236 | Where `MyLibrary` is the same name as the `output.library` value in your `webpack.config.js` file. 237 | 238 | This occurs when your library exports a member that is named the same as the value found in the `output.library` value. It is suggested that you use an obscure value for `output.library` - one that has low probability of matching an exported member's name. 239 | 240 | 241 | ## TypeError: chunk.entryModule.buildMeta.providedExports.reduce is not a function 242 | 243 | Console output: 244 | ``` 245 | /home/prj/node_modules/@purtuga/esm-webpack-plugin/esm-webpack-plugin.js:45 246 | chunk.entryModule.buildMeta.providedExports.reduce((esm_exports, exportName) => { 247 | ^ 248 | 249 | TypeError: chunk.entryModule.buildMeta.providedExports.reduce is not a function 250 | ``` 251 | 252 | In order to create an ESM package, webpack must be able to identify your module exports. This error is likey due to the fact that it was not able to do that. You can run your build with `--bail --display-optimization-bailout` to see if the following message is output against your entry module: 253 | `ModuleConcatenation bailout: Module exports are unknown` 254 | 255 | The root cause is likely due to exporting modules using the `*` syntax where different modules have an export named exactly the same. Example: 256 | 257 | `index.js` 258 | ```javascript 259 | export * from "mod1.js"; 260 | export * from "mod1.js" 261 | ``` 262 | 263 | Where both modules have an export name `foo`. To address this issue, try using named exports instead: 264 | 265 | ```javascript 266 | export {foo} from "mod1.js"; 267 | export {foo as foo2} from "mod2.js" 268 | ``` 269 | 270 | 271 | 272 | # License 273 | 274 | [MIT](LICENSE) 275 | -------------------------------------------------------------------------------- /esm-webpack-plugin.js: -------------------------------------------------------------------------------- 1 | const ConcatSource = require("webpack-sources").ConcatSource; 2 | const MultiModule = require("webpack/lib/MultiModule"); 3 | const Template = require("webpack/lib/Template"); 4 | const PLUGIN_NAME = "EsmWebpackPlugin"; 5 | const warn = msg => console.warn(`[${PLUGIN_NAME}] ${msg}`); 6 | const IS_JS_FILE = /\.[cm]?js$/i; 7 | 8 | const defaultOptions = { 9 | // Exclude non-js files 10 | exclude: fileName => !IS_JS_FILE.test(fileName), 11 | 12 | // Skip Nothing 13 | skipModule: () => false, 14 | 15 | // Treat externals as globals, by default 16 | moduleExternals: false, 17 | 18 | // Add __esModule property to all externals 19 | esModuleExternals: true 20 | }; 21 | 22 | 23 | /** 24 | * Add ESM `export` statements to the bottom of a webpack chunk 25 | * with the exposed exports. 26 | */ 27 | module.exports = class EsmWebpackPlugin { 28 | /** 29 | * 30 | * @param {Object} [options] 31 | * @param {Function} [options.exclude] 32 | * A callback function to evaluate each output file name and determine if it should be 33 | * excluded from being wrapped with ESM exports. By default, all files whose 34 | * file extension is not `.js` or `.mjs` will be excluded. 35 | * The provided callback will receive two input arguments: 36 | * - `{String} fileName`: the file name being evaluated 37 | * - `{Chunk} chunk`: the webpack `chunk` being worked on. 38 | * @param {Function} [options.skipModule] 39 | * A callback function to evaluate each single module in the bundle and if its list of 40 | * exported members should be included. 41 | * @param {boolean} [options.moduleExternals] 42 | * A boolean that determines whether to treat webpack externals as ES modules or not. 43 | * Defaults to false. 44 | */ 45 | constructor(options) { 46 | this._options = { 47 | ...defaultOptions, 48 | ...options 49 | }; 50 | } 51 | 52 | apply(compiler) { 53 | compiler.hooks.compilation.tap(PLUGIN_NAME, compilationTap.bind(this)); 54 | } 55 | }; 56 | 57 | function exportsForModule(module, libVar, pluginOptions) { 58 | let exports = ""; 59 | const namedExports = []; 60 | const moduleName = typeof module.nameForCondition === 'function' 61 | ? module.nameForCondition() 62 | : undefined; 63 | 64 | if (moduleName && pluginOptions.skipModule(moduleName, module)) { 65 | return ''; 66 | } 67 | 68 | if (module instanceof MultiModule) { 69 | module.dependencies.forEach(dependency => { 70 | exports += exportsForModule(dependency.module, libVar, pluginOptions); 71 | }); 72 | } else if (Array.isArray(module.buildMeta.providedExports)) { 73 | module.buildMeta.providedExports.forEach(exportName => { 74 | if (exportName === "default") { 75 | exports += `export default ${libVar}['${exportName}'];\n` 76 | } else { 77 | const scopedExportVarName = `_${libVar}$${exportName}`; 78 | exports += `const ${scopedExportVarName} = ${libVar}['${exportName}'];\n`; 79 | namedExports.push(` ${scopedExportVarName} as ${exportName}`); 80 | } 81 | }); 82 | } else { 83 | exports += `export default ${libVar};\nexport { ${libVar} };\n` 84 | } 85 | return ` 86 | ${ 87 | exports.length > 0 && namedExports.length > 0 88 | ? `${libVar} === undefined && console.error('esm-webpack-plugin: nothing exported!');` 89 | : '' 90 | } 91 | ${exports}${ 92 | namedExports.length ? 93 | `\nexport {\n${namedExports.join(",\n")}\n}` : 94 | "" 95 | }`; 96 | } 97 | 98 | function importsForModule(chunk, pluginOptions) { 99 | if (pluginOptions.moduleExternals) { 100 | const externals = chunk.getModules().filter(m => m.external); 101 | const importStatements = externals.map(m => { 102 | const request = typeof m.request === 'object' ? m.request.amd : m.request; 103 | const identifier = `__WEBPACK_EXTERNAL_MODULE_${Template.toIdentifier(`${m.id}`)}__`; 104 | 105 | return pluginOptions.esModuleExternals 106 | ? `import * as $${identifier} from '${request}'; var ${identifier} = cloneWithEsModuleProperty($${identifier});` 107 | : `import * as ${identifier} from '${request}';` 108 | }) 109 | 110 | const result = [importStatements.join("\n")]; 111 | 112 | if (pluginOptions.esModuleExternals) { 113 | // The code here was originally copied from https://github.com/joeldenning/add-esmodule 114 | result.push(Template.asString([ 115 | "\n", 116 | "function cloneWithEsModuleProperty(ns) {", 117 | Template.indent([ 118 | "const result = Object.create(null);", 119 | `Object.defineProperty(result, "__esModule", {`, 120 | Template.indent([ 121 | `value: true,`, 122 | `enumerable: false,`, 123 | `configurable: true`, 124 | ]), 125 | "});", 126 | `const propertyNames = Object.getOwnPropertyNames(ns);`, 127 | `for (let i = 0; i < propertyNames.length; i++) {`, 128 | Template.indent([ 129 | `const propertyName = propertyNames[i];`, 130 | `Object.defineProperty(result, propertyName, {`, 131 | Template.indent([ 132 | `get: function () {`, 133 | Template.indent([ 134 | `return ns[propertyName];` 135 | ]), 136 | `},`, 137 | `enumerable: true,`, 138 | `configurable: false,` 139 | ]), 140 | `});` 141 | ]), 142 | `}`, 143 | `if (Object.getOwnPropertySymbols) {`, 144 | Template.indent([ 145 | `const symbols = Object.getOwnPropertySymbols(ns);`, 146 | `for (let i = 0; i < symbols.length; i++) {`, 147 | Template.indent([ 148 | `const symbol = symbols[i];`, 149 | `Object.defineProperty(result, symbol, {`, 150 | Template.indent([ 151 | `get: function () {`, 152 | Template.indent([ 153 | `return ns[symbol];` 154 | ]), 155 | `},`, 156 | `enumerable: false,`, 157 | `configurable: false,`, 158 | ]), 159 | `});`, 160 | ]), 161 | "}", 162 | ]), 163 | `}`, 164 | `Object.preventExtensions(result);`, 165 | `Object.seal(result);`, 166 | `if (Object.freeze) {`, 167 | Template.indent([ 168 | `Object.freeze(result);`, 169 | ]), 170 | `}`, 171 | `return result;`, 172 | ]), 173 | `}` 174 | ])); 175 | } 176 | 177 | result.push("\n"); 178 | 179 | return result; 180 | } else { 181 | // Use default webpack behavior 182 | return []; 183 | } 184 | } 185 | 186 | function compilationTap(compilation) { 187 | const libVar = compilation.outputOptions.library; 188 | const exclude = this._options.exclude; 189 | 190 | if (!libVar) { 191 | warn("output.library is expected to be set!"); 192 | } 193 | 194 | if ( 195 | compilation.outputOptions.libraryTarget && 196 | compilation.outputOptions.libraryTarget !== "var" && 197 | compilation.outputOptions.libraryTarget !== "assign" 198 | ) { 199 | warn(`output.libraryTarget (${compilation.outputOptions.libraryTarget}) expected to be 'var' or 'assign'!`); 200 | } 201 | 202 | if (this._options.moduleExternals) { 203 | compilation.hooks.buildModule.tap(PLUGIN_NAME, (module) => { 204 | if (module.external) { 205 | // See https://webpack.js.org/configuration/externals/#externalstype 206 | // We want AMD because it references __WEBPACK_EXTERNAL_MODULE_ instead 207 | // of the raw external request string. 208 | module.externalType = 'amd'; 209 | } 210 | }); 211 | } 212 | 213 | compilation.hooks.optimizeChunkAssets.tapAsync(PLUGIN_NAME, (chunks, done) => { 214 | chunks.forEach(chunk => { 215 | if (chunk.entryModule && chunk.entryModule.buildMeta.providedExports) { 216 | chunk.files.forEach(fileName => { 217 | if (exclude && exclude(fileName, chunk)) { 218 | return; 219 | } 220 | 221 | // Add the exports to the bottom of the file (expecting only one file) and 222 | // add that file back to the compilation 223 | compilation.assets[fileName] = new ConcatSource( 224 | ...importsForModule(chunk, this._options), 225 | compilation.assets[fileName], 226 | "\n\n", 227 | exportsForModule(chunk.entryModule, libVar, this._options) 228 | ); 229 | }); 230 | } 231 | }); 232 | 233 | done(); 234 | }); 235 | } 236 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@purtuga/esm-webpack-plugin", 3 | "version": "1.5.0", 4 | "description": "A webpack plugin that changes the output to be an ESM library", 5 | "main": "esm-webpack-plugin.js", 6 | "scripts": { 7 | "preversion": "npm run test", 8 | "test": "mocha -r esm test/*.test.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/purtuga/esm-webpack-plugin.git" 13 | }, 14 | "keywords": [ 15 | "webpack", 16 | "plugin", 17 | "esm", 18 | "module", 19 | "export", 20 | "library" 21 | ], 22 | "author": "Paul Tavares", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/purtuga/esm-webpack-plugin/issues" 26 | }, 27 | "homepage": "https://github.com/purtuga/esm-webpack-plugin#readme", 28 | "peerDependencies": { 29 | "webpack": "^4.0.0" 30 | }, 31 | "dependencies": { 32 | "webpack-sources": "^1.0.0" 33 | }, 34 | "devDependencies": { 35 | "esm": "^3.2.25", 36 | "mocha": "^6.2.2", 37 | "webpack": "^4.41.4", 38 | "webpack-cli": "^3.3.10" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/fixtures/cjs_module/index.js: -------------------------------------------------------------------------------- 1 | // @see https://github.com/purtuga/esm-webpack-plugin/issues/9 2 | 3 | exports.fnA = function A() { 4 | return "A"; 5 | }; 6 | 7 | exports.fnB = function() { 8 | return "B"; 9 | }; -------------------------------------------------------------------------------- /test/fixtures/cjs_module/webpack.config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { createSingleWebpackConfig } from "../../utils.js"; 3 | 4 | export const webpackConfig = createSingleWebpackConfig( 5 | path.resolve(__dirname, "index.js"), 6 | "cjs_module.js" 7 | ); 8 | -------------------------------------------------------------------------------- /test/fixtures/code_splitting/a.js: -------------------------------------------------------------------------------- 1 | 2 | export const fnA = () => "hello fnA"; 3 | export const fnB = () => "hello fnB"; -------------------------------------------------------------------------------- /test/fixtures/code_splitting/index.js: -------------------------------------------------------------------------------- 1 | // @see https://github.com/purtuga/esm-webpack-plugin/issues/4 2 | 3 | export function loadA() { 4 | return import("./a.js") 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/code_splitting/webpack.config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { createSingleWebpackConfig } from "../../utils.js"; 3 | 4 | const webpackConfig = createSingleWebpackConfig( 5 | path.resolve(__dirname, "index.js"), 6 | "code_splitting.js" 7 | ); 8 | 9 | // For this one, because we're running test in nodeJS, we need to set the target so 10 | // that the bundle is not looking for `window` 11 | webpackConfig.target = "node"; 12 | 13 | export { 14 | webpackConfig 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/esm_externals/bar-external.js: -------------------------------------------------------------------------------- 1 | export default 'bar external'; -------------------------------------------------------------------------------- /test/fixtures/esm_externals/foo-external.js: -------------------------------------------------------------------------------- 1 | export default 'foo external'; -------------------------------------------------------------------------------- /test/fixtures/esm_externals/index.js: -------------------------------------------------------------------------------- 1 | import * as foo from 'foo'; 2 | import * as bar from 'bar'; 3 | 4 | export const externals = {foo, bar}; -------------------------------------------------------------------------------- /test/fixtures/esm_externals/webpack.config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { createSingleWebpackConfig } from "../../utils.js"; 3 | 4 | const config = createSingleWebpackConfig( 5 | path.resolve(__dirname, "index.js"), 6 | "esm_externals.js", 7 | { 8 | moduleExternals: true 9 | } 10 | ); 11 | 12 | // Set some externals, which will be loaded via `import` at runtime. 13 | config.externals = { 14 | foo: '../../test/fixtures/esm_externals/foo-external.js', 15 | bar: '../../test/fixtures/esm_externals/bar-external.js', 16 | }; 17 | 18 | export const webpackConfig = config; 19 | -------------------------------------------------------------------------------- /test/fixtures/esm_module/index.js: -------------------------------------------------------------------------------- 1 | 2 | function fnDefault() { 3 | return "default function"; 4 | } 5 | 6 | const STATIC_ONE = "one"; 7 | 8 | function getStaticOne() { 9 | return STATIC_ONE; 10 | } 11 | 12 | export default fnDefault; 13 | export { 14 | fnDefault, 15 | STATIC_ONE, 16 | getStaticOne 17 | } -------------------------------------------------------------------------------- /test/fixtures/esm_module/webpack.config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { createSingleWebpackConfig } from "../../utils.js"; 3 | 4 | export const webpackConfig = createSingleWebpackConfig( 5 | path.resolve(__dirname, "index.js"), 6 | "esm_module.js" 7 | ); 8 | -------------------------------------------------------------------------------- /test/fixtures/export_globals/index.js: -------------------------------------------------------------------------------- 1 | // From Issue 12 2 | // https://github.com/purtuga/esm-webpack-plugin/issues/12 3 | import * as Math from "./math.js"; 4 | 5 | export { 6 | Math, 7 | }; -------------------------------------------------------------------------------- /test/fixtures/export_globals/math.js: -------------------------------------------------------------------------------- 1 | export const square = x => Math.pow(x, 2); -------------------------------------------------------------------------------- /test/fixtures/export_globals/webpack.config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import {createSingleWebpackConfig} from "../../utils.js"; 3 | 4 | 5 | export const webpackConfig =createSingleWebpackConfig( 6 | path.resolve(__dirname, "index.js"), 7 | "export_globals.js" 8 | ); -------------------------------------------------------------------------------- /test/fixtures/external_esmodule/foo-external.js: -------------------------------------------------------------------------------- 1 | export default 'foo external'; -------------------------------------------------------------------------------- /test/fixtures/external_esmodule/index.js: -------------------------------------------------------------------------------- 1 | import * as foo from 'foo'; 2 | 3 | export const externalsHaveEsModule = () => Object.hasOwnProperty.call(foo, '__esModule'); -------------------------------------------------------------------------------- /test/fixtures/external_esmodule/webpack.config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { createSingleWebpackConfig } from "../../utils.js"; 3 | 4 | const config = createSingleWebpackConfig( 5 | path.resolve(__dirname, "index.js"), 6 | "external_esmodule.js", 7 | { 8 | moduleExternals: true, 9 | esModuleExternals: true 10 | } 11 | ); 12 | 13 | config.externals = { 14 | foo: '../../test/fixtures/external_esmodule/foo-external.js', 15 | } 16 | 17 | export const webpackConfig = config 18 | -------------------------------------------------------------------------------- /test/fixtures/global_externals/index.js: -------------------------------------------------------------------------------- 1 | import * as foo from 'foo'; 2 | import * as bar from 'bar'; 3 | 4 | export const externals = {foo, bar}; -------------------------------------------------------------------------------- /test/fixtures/global_externals/webpack.config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { createSingleWebpackConfig } from "../../utils.js"; 3 | 4 | const config = createSingleWebpackConfig( 5 | path.resolve(__dirname, "index.js"), 6 | "global_externals.js", 7 | { 8 | moduleExternals: false 9 | } 10 | ); 11 | 12 | // Set some externals, which will be referenced as global variables 13 | config.externals = { 14 | foo: 'globalFoo', 15 | bar: 'globalBar', 16 | } 17 | 18 | export const webpackConfig = config 19 | -------------------------------------------------------------------------------- /test/fixtures/multi_module_entry/include.js: -------------------------------------------------------------------------------- 1 | 2 | export const includeFn = () => { 3 | console.log('includeFn() called!'); 4 | }; 5 | -------------------------------------------------------------------------------- /test/fixtures/multi_module_entry/index.js: -------------------------------------------------------------------------------- 1 | // For issue: 15 2 | // https://github.com/purtuga/esm-webpack-plugin/issues/15 3 | export * from './include.js'; 4 | 5 | 6 | export default function defFn() { 7 | return 'defFn() returned'; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/multi_module_entry/skip.js: -------------------------------------------------------------------------------- 1 | // File has no exports. 2 | const foo = 'baar'; 3 | const logFoo = () => console.log(foo); 4 | -------------------------------------------------------------------------------- /test/fixtures/multi_module_entry/webpack.config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import {createSingleWebpackConfig} from "../../utils.js"; 3 | import EsmWebpackPlugin from "../../../esm-webpack-plugin.js"; 4 | 5 | const indexJs = path.resolve(__dirname, "index.js"); 6 | const skipJs = path.resolve(__dirname, 'skip.js'); 7 | 8 | export const webpackConfig =createSingleWebpackConfig( 9 | indexJs, 10 | "multi_module_entry.js" 11 | ); 12 | 13 | // Define an array entry point, and add `skipModule` callback to not include 14 | // exports for the first module 15 | webpackConfig.entry = [skipJs, indexJs]; 16 | webpackConfig.plugins = [ 17 | new EsmWebpackPlugin({ 18 | skipModule(fileName) { 19 | return /skip\.js$/.test(fileName); 20 | } 21 | }) 22 | ]; 23 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import {describe, it} from "mocha"; 2 | import assert from "assert"; 3 | import {buildFixtures} from "./utils.js"; 4 | 5 | describe("When esm-webpack-plugin is invoked", () => { 6 | const build = buildFixtures(); 7 | 8 | it("should build fixtures", async () => { 9 | await build; 10 | assert.ok("fixtures compile"); 11 | }); 12 | 13 | it("should export ESM modules", async () => { 14 | // https://github.com/purtuga/esm-webpack-plugin/issues/9 15 | const buildResults = await build; 16 | const module = await buildResults.esm_module.import(); 17 | assert.ok(module.default); 18 | assert.ok(module.getStaticOne); 19 | assert.ok(module.STATIC_ONE); 20 | assert.ok(module.default === module.fnDefault); 21 | }); 22 | 23 | it("should handle code splitting", async () => { 24 | // https://github.com/purtuga/esm-webpack-plugin/issues/4 25 | const buildResults = await build; 26 | const module = await buildResults.code_splitting.import(); 27 | assert.ok(module.loadA); 28 | assert.ok(!module.fnA); 29 | 30 | const loadAResponse = await module.loadA(); 31 | assert.equal(loadAResponse.fnA(), "hello fnA"); 32 | assert.equal(loadAResponse.fnB(), "hello fnB"); 33 | }); 34 | 35 | it("should export CommonJS modules as `default`", async () => { 36 | // https://github.com/purtuga/esm-webpack-plugin/issues/9 37 | const buildResults = await build; 38 | const module = await buildResults.cjs_module.import(); 39 | assert.ok(module.default.fnA); 40 | assert.ok(module.default.fnB); 41 | }); 42 | 43 | it("should prevents GLOBALs conflicts when naming exports", async () => { 44 | // See issue: https://github.com/purtuga/esm-webpack-plugin/issues/12 45 | const buildResults = await build; 46 | const module = await buildResults.export_globals.import(); 47 | assert.ok(module.Math); 48 | assert.equal(module.Math.square(2), 4); 49 | }); 50 | 51 | it("should support skipModule option", async () => { 52 | const buildResults = await build; 53 | const module = await buildResults.multi_module_entry.import(); 54 | assert.ok(module.includeFn); 55 | assert.ok(module.default); 56 | }); 57 | 58 | it("should import esm externals", async () => { 59 | const buildResults = await build; 60 | const module = await buildResults.esm_externals.import(); 61 | assert.strictEqual(module.externals.foo.default, 'foo external'); 62 | assert.strictEqual(module.externals.bar.default, 'bar external'); 63 | }); 64 | 65 | it("should import global externals", async () => { 66 | const buildResults = await build; 67 | global.globalFoo = "foo value"; 68 | global.globalBar = "bar value"; 69 | const module = await buildResults.global_externals.import(); 70 | assert.strictEqual(module.externals.foo, "foo value"); 71 | assert.strictEqual(module.externals.bar, "bar value"); 72 | delete global.globalFoo; 73 | delete global.globalBar; 74 | }); 75 | 76 | it("should add __esModule to externals, with esModuleExternals option set", async () => { 77 | const buildResults = await build; 78 | const module = await buildResults.external_esmodule.import(); 79 | assert.strictEqual(module.externalsHaveEsModule(), true); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | import {promisify} from "util"; 2 | import webpack from "webpack"; 3 | import {readdir} from "fs"; 4 | import path from "path"; 5 | import assert from "assert"; 6 | import EsmWebpackPlugin from "../esm-webpack-plugin.js"; 7 | 8 | 9 | //===[ MODULE ]=========================================================== 10 | 11 | const webpackAsync = promisify(webpack); 12 | const readdirAsync = promisify(readdir); 13 | 14 | const fixturesDir = path.resolve(__dirname, "fixtures"); 15 | const fixturesBuildOutputDir = path.join(path.resolve(__dirname, ".."), "tmp", "fixture_test"); 16 | 17 | async function getWebpackConfigs() { 18 | const configs = await Promise.all( 19 | (await readdirAsync(fixturesDir)) 20 | .map(dirName => import(path.join(fixturesDir, dirName, "webpack.config.js")).then(module => module.webpackConfig))); 21 | return configs; 22 | } 23 | 24 | function b() { 25 | return Promise.resolve({ 26 | a: { 27 | import() { 28 | }, results: {} 29 | } 30 | }); 31 | } 32 | 33 | /** 34 | * Builds all the fixtures 35 | * 36 | * @returns {Promise<{ fixture_name: { import: (function()), result: {}}}>} 37 | */ 38 | async function buildFixtures() { 39 | const webpackConfig = await getWebpackConfigs(); 40 | const results = await webpackAsync(webpackConfig); 41 | results.stats.some(buildStats => { 42 | if (buildStats.hasErrors()) { 43 | throw new Error(buildStats.toString()); 44 | } 45 | }); 46 | return webpackConfig.reduce((fixtures, config, i) => { 47 | fixtures[path.basename(path.dirname(Array.isArray(config.entry) ? config.entry[0] : config.entry))] = { 48 | import: () => import(path.join(config.output.path, config.output.filename)), 49 | result: results.stats[i] 50 | }; 51 | return fixtures; 52 | }, {}); 53 | } 54 | 55 | function createSingleWebpackConfig(entryFilePath, outputFilename, pluginOptions = {}) { 56 | assert.equal(typeof entryFilePath, "string"); 57 | assert.equal(typeof outputFilename, "string"); 58 | 59 | return { 60 | mode: "production", 61 | entry: entryFilePath, 62 | output: { 63 | library: "LIB", 64 | libraryTarget: "var", 65 | filename: outputFilename, 66 | path: fixturesBuildOutputDir 67 | }, 68 | plugins: [ 69 | new EsmWebpackPlugin(pluginOptions) 70 | ], 71 | optimization: { 72 | minimize: false, 73 | splitChunks: {chunks: 'all'} 74 | } 75 | }; 76 | } 77 | 78 | //===[ EXPORTS ]======================================================== 79 | export { 80 | fixturesBuildOutputDir, 81 | buildFixtures, 82 | createSingleWebpackConfig 83 | }; 84 | 85 | --------------------------------------------------------------------------------