├── .babelrc ├── .eslintrc ├── .flowconfig ├── .gitignore ├── LICENSE ├── README.md ├── lib └── AssetModulePlugin.js ├── package.json └── src └── AssetModulePlugin.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "whitelist": [ 3 | "flow", 4 | "strict", 5 | "es6.arrowFunctions", 6 | "es6.destructuring", 7 | "es6.modules", 8 | "es7.classProperties" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | 4 | "ecmaFeatures": { 5 | "modules": true, 6 | "jsx": true 7 | }, 8 | 9 | "env": { 10 | "es6": true, 11 | "jasmine": true, 12 | "node": true 13 | }, 14 | 15 | // Map from global var to bool specifying if it can be redefined 16 | "globals": { 17 | "__DEV__": false, 18 | "Promise": false, 19 | "XMLHttpRequest": false, 20 | "fetch": false, 21 | "requestAnimationFrame": false, 22 | }, 23 | 24 | "rules": { 25 | "no-alert": 1, 26 | "no-array-constructor": 1, 27 | "no-bitwise": 0, 28 | "no-caller": 1, 29 | "no-catch-shadow": 0, 30 | "no-class-assign": 0, 31 | "no-cond-assign": 1, 32 | "no-console": 0, 33 | "no-const-assign": 2, 34 | "no-constant-condition": 1, 35 | "no-continue": 0, 36 | "no-control-regex": 1, 37 | "no-debugger": 1, 38 | "no-delete-var": 2, 39 | "no-div-regex": 1, 40 | "no-dupe-keys": 2, 41 | "no-dupe-args": 2, 42 | "no-duplicate-case": 2, 43 | "no-else-return": 0, 44 | "no-empty": 0, 45 | "no-empty-character-class": 1, 46 | "no-empty-label": 1, 47 | "no-eq-null": 0, 48 | "no-eval": 1, 49 | "no-ex-assign": 1, 50 | "no-extend-native": 1, 51 | "no-extra-bind": 1, 52 | "no-extra-boolean-cast": 1, 53 | "no-extra-parens": 0, 54 | "no-extra-semi": 1, 55 | "no-fallthrough": 1, 56 | "no-floating-decimal": 1, 57 | "no-func-assign": 2, 58 | "no-implied-eval": 1, 59 | "no-inline-comments": 0, 60 | "no-inner-declarations": [0, "functions"], 61 | "no-invalid-regexp": 2, 62 | "no-irregular-whitespace": 1, 63 | "no-iterator": 1, 64 | "no-label-var": 1, 65 | "no-labels": 1, 66 | "no-lone-blocks": 1, 67 | "no-lonely-if": 0, 68 | "no-loop-func": 0, 69 | "no-mixed-requires": [0, false], 70 | "no-mixed-spaces-and-tabs": [1, false], 71 | "no-multi-spaces": 0, 72 | "no-multi-str": 0, 73 | "no-multiple-empty-lines": [0, {"max": 2}], 74 | "no-native-reassign": 0, 75 | "no-negated-in-lhs": 1, 76 | "no-nested-ternary": 0, 77 | "no-new": 1, 78 | "no-new-func": 1, 79 | "no-new-object": 1, 80 | "no-new-require": 1, 81 | "no-new-wrappers": 1, 82 | "no-obj-calls": 1, 83 | "no-octal": 1, 84 | "no-octal-escape": 1, 85 | "no-param-reassign": 0, 86 | "no-path-concat": 1, 87 | "no-plusplus": 0, 88 | "no-process-env": 0, 89 | "no-process-exit": 0, 90 | "no-proto": 1, 91 | "no-redeclare": 1, 92 | "no-regex-spaces": 0, 93 | "no-reserved-keys": 0, 94 | "no-restricted-modules": 0, 95 | "no-return-assign": 1, 96 | "no-script-url": 1, 97 | "no-self-compare": 1, 98 | "no-sequences": 1, 99 | "no-shadow": 0, 100 | "no-shadow-restricted-names": 1, 101 | "no-spaced-func": 1, 102 | "no-sparse-arrays": 1, 103 | "no-sync": 0, 104 | "no-ternary": 0, 105 | "no-trailing-spaces": 1, 106 | "no-this-before-super": 0, 107 | "no-throw-literal": 1, 108 | "no-undef": 2, 109 | "no-undef-init": 0, 110 | "no-undefined": 0, 111 | "no-unexpected-multiline": 1, 112 | "no-underscore-dangle": 0, 113 | "no-unneeded-ternary": 1, 114 | "no-unreachable": 1, 115 | "no-unused-expressions": 1, 116 | "no-unused-vars": [1, {"vars": "all", "args": "none"}], 117 | "no-use-before-define": 0, 118 | "no-useless-call": 0, 119 | "no-void": 1, 120 | "no-var": 0, 121 | "no-warning-comments": [0, { "terms": ["todo", "fixme", "xxx"], "location": "start" }], 122 | "no-with": 1, 123 | 124 | "array-bracket-spacing": [1, "never"], 125 | "arrow-parens": 0, 126 | "arrow-spacing": [1, { "before": true, "after": true }], 127 | "accessor-pairs": 0, 128 | "block-scoped-var": 0, 129 | "brace-style": [0, "1tbs"], 130 | "callback-return": 0, 131 | "camelcase": 0, 132 | "comma-dangle": [1, "always-multiline"], 133 | "comma-spacing": [1, {"before": false, "after": true}], 134 | "comma-style": [1, "last"], 135 | "complexity": [0, 11], 136 | "computed-property-spacing": [0, "never"], 137 | "consistent-return": 0, 138 | "consistent-this": [0, "self"], 139 | "constructor-super": 1, 140 | "curly": [1, "all"], 141 | "default-case": 0, 142 | "dot-location": [1, "property"], 143 | "dot-notation": [0, { "allowKeywords": true }], 144 | "eol-last": 1, 145 | "eqeqeq": [1, "smart"], 146 | "func-names": 0, 147 | "func-style": [0, "declaration"], 148 | "generator-star-spacing": [1, "after"], 149 | "guard-for-in": 0, 150 | "handle-callback-err": [1, "^(err|error)$"], 151 | "indent": [0, 2], 152 | "init-declarations": 0, 153 | "key-spacing": [0, { "beforeColon": false, "afterColon": true }], 154 | "linebreak-style": [0, "unix"], 155 | "lines-around-comment": 0, 156 | "max-depth": [0, 4], 157 | "max-len": [0, 80, 4], 158 | "max-nested-callbacks": [0, 2], 159 | "max-params": [0, 3], 160 | "max-statements": [0, 10], 161 | "new-cap": 0, 162 | "new-parens": 1, 163 | "newline-after-var": 0, 164 | "object-curly-spacing": [0, "always", { "objectsInObjects": false }], 165 | "object-shorthand": [1, "always"], 166 | "one-var": [0, "never"], 167 | "operator-assignment": [0, "always"], 168 | "operator-linebreak": [1, "after"], 169 | "padded-blocks": 0, 170 | "prefer-const": 0, 171 | "prefer-spread": 1, 172 | "quote-props": 0, 173 | "quotes": [1, "single", "avoid-escape"], 174 | "radix": 1, 175 | "require-yield": 0, 176 | "semi": 1, 177 | "semi-spacing": [1, { "before": false, "after": true }], 178 | "sort-vars": 0, 179 | "space-after-keywords": [1, "always"], 180 | "space-before-blocks": [1, "always"], 181 | "space-before-function-paren": [1, { "anonymous": "never", "named": "never" }], 182 | "space-in-parens": [1, "never"], 183 | "space-infix-ops": 1, 184 | "space-return-throw-case": 1, 185 | "space-unary-ops": [0, { "words": true, "nonwords": false }], 186 | "spaced-comment": 0, 187 | "strict": [1, "global"], 188 | "use-isnan": 2, 189 | "valid-jsdoc": 0, 190 | "valid-typeof": 2, 191 | "vars-on-top": 0, 192 | "wrap-iife": 0, 193 | "wrap-regex": 0, 194 | "yoda": [1, "never", { "exceptRange": false }] 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/src/ 3 | 4 | [include] 5 | lib 6 | 7 | [libs] 8 | 9 | [options] 10 | module.system=node 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Exponent 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AssetModulePlugin 2 | A webpack plugin that emits JS modules for your assets such as images. This allows Node to load the modules that webpack generated for your assets without having to run webpack's bundle on the server. 3 | 4 | ## Purpose 5 | 6 | This plugin was written to facilitate server-side rendering of React components. The traditional approach is to build two webpack bundles, one for the browser and one for the server. This works, but there a few things about it that are unappealing: 7 | 8 | - You need to write a second webpack config for the server-side bundle 9 | - The generated bundle is harder to debug and requires configuring Node to use source maps 10 | - Node has to read and parse the entire bundle the first time it is required, rather than lazily parsing components as needed 11 | 12 | What we really wanted to do was run our JS files on Node directly instead of a bundle. The [enhanced-require](https://github.com/webpack/enhanced-require) package looks like the most comprehensive solution, as it polyfills webpack for the server. However, it is unmaintained, and we needed only two features anyway: We wanted to be able to require CSS/LESS files without throwing an error, and we wanted `require('./icon.png')` to return the full CDN path to the icon. My goal was for this code to run: 13 | 14 | ```js 15 | import './Button.less'; 16 | 17 | class Button extends React.Component { 18 | render() { 19 | return ( 20 | 24 | ); 25 | } 26 | } 27 | ``` 28 | 29 | ## How it works 30 | 31 | The plugin runs after compilation and translates the path of the source asset to a destination path (you configure this with the `sourceBase` and `destinationBase` options). It then writes the full path of the asset to a file at the destination path. It emit files like this: 32 | 33 | ```js 34 | // build/icon.png 35 | // It's actually a JS module with a png extension, which Node can evaluate. The 36 | // full asset path is derived from the public path and asset's relative path. 37 | module.exports = "http://static.example.com/eelZITCY0q9Gbj00z8HI.png"; 38 | ``` 39 | 40 | ## How to use it 41 | 42 | In your webpack configuration file, add this to your list of plugins: 43 | 44 | ```js 45 | new AssetModulePlugin({ 46 | sourceBase: path.join(__dirname, 'src'), 47 | destinationBase: path.join(__dirname, 'build'), 48 | test: /\.(?!js)$/, 49 | exclude: /node_modules/, 50 | fileSystems: [AssetModulePlugin.DefaultFileSystem, require('fs')], 51 | }) 52 | ``` 53 | 54 | This takes your assets whose filenames don't end in ".js" and aren't under "node_modules" and moves them from "src" to "build". The original asset's path relative to "src" will be the same as the emitted module's path relative to "build". 55 | 56 | ## Requirements 57 | 58 | We use the `class` keyword so io.js 2.0+ is required. 59 | 60 | ## Ideas 61 | 62 | If you have ideas for how to improve server-side rendering we'd be happy to chat. Open up a GitHub issue or join our Slack chat at http://exp.host/community. 63 | 64 | ## Contributing 65 | 66 | Run `npm run-script build` to build `lib` from `src`. 67 | -------------------------------------------------------------------------------- /lib/AssetModulePlugin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { 4 | value: true 5 | }); 6 | 7 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 8 | 9 | var _mkdirp = require('mkdirp'); 10 | 11 | var _mkdirp2 = _interopRequireDefault(_mkdirp); 12 | 13 | var _path = require('path'); 14 | 15 | var _path2 = _interopRequireDefault(_path); 16 | 17 | var DEFAULT_FILE_SYSTEM_SYMBOL = Symbol('DefaultFileSystem'); 18 | 19 | class AssetModulePlugin { 20 | 21 | constructor(options) { 22 | this.options = options; 23 | } 24 | 25 | apply(compiler) { 26 | var _this = this; 27 | 28 | compiler.plugin('compilation', function (compilation, parameters) { 29 | var addedResources = new Set(); 30 | compilation._modulesToEmit = []; 31 | compilation.plugin('succeed-module', function (module) { 32 | var resource = module.resource; 33 | 34 | if (addedResources.has(resource) || !_this._shouldEmit(module)) { 35 | return; 36 | } 37 | addedResources.add(resource); 38 | compilation._modulesToEmit.push(module); 39 | }); 40 | }); 41 | 42 | compiler.plugin('after-emit', function (compilation, callback) { 43 | Promise.all(compilation._modulesToEmit.map(function (module) { 44 | return _this._emitAssetModule(compiler, compilation, module); 45 | })).then(function (result) { 46 | callback(null, result); 47 | }, function (error) { 48 | console.warn(error.stack); 49 | callback(error); 50 | }); 51 | }); 52 | } 53 | 54 | _emitAssetModule(compiler, compilation, module) { 55 | var _this2 = this; 56 | 57 | var _options = this.options; 58 | var sourceBase = _options.sourceBase; 59 | var destinationBase = _options.destinationBase; 60 | var resource = module.resource; 61 | 62 | var relativePath = _path2.default.relative(sourceBase, resource); 63 | var destinationPath = _path2.default.resolve(destinationBase, relativePath); 64 | if (resource === destinationPath) { 65 | var message = `Destination path for ${ resource } matches the source path; skipping instead of overwriting the file`; 66 | compilation.warnings.push(new Error(message)); 67 | return Promise.resolve(null); 68 | } 69 | 70 | var source = this._getAssetModuleSource(compilation, module); 71 | 72 | if (!this.options.fileSystems) { 73 | var fileSystem = compiler.outputFileSystem; 74 | return this._writeFile(destinationPath, source, fileSystem); 75 | } 76 | 77 | var fileSystems = this.options.fileSystems.map(function (fileSystem) { 78 | if (fileSystem === DEFAULT_FILE_SYSTEM_SYMBOL) { 79 | return compiler.outputFileSystem; 80 | } 81 | return fileSystem; 82 | }); 83 | 84 | var promises = fileSystems.map(function (fileSystem) { 85 | return _this2._writeFile(destinationPath, source, fileSystem).catch(function (error) { 86 | compilation.errors.push(error); 87 | throw error; 88 | }); 89 | }); 90 | return Promise.all(promises); 91 | } 92 | 93 | _getAssetModuleSource(compilation, module) { 94 | var sourceBase = this.options.sourceBase; 95 | var resource = module.resource; 96 | var assets = module.assets; 97 | 98 | var publicPath = compilation.mainTemplate.getPublicPath({ 99 | hash: compilation.hash 100 | }); 101 | 102 | var assetFilename; 103 | var assetFilenames = Object.keys(assets); 104 | if (assetFilenames.length === 0) { 105 | assetFilename = _path2.default.relative(sourceBase, resource); 106 | } else { 107 | if (assetFilenames.length > 1) { 108 | var message = `Module at ${ resource } generated more than one asset; using the first one`; 109 | compilation.warnings.push(new Error(message)); 110 | } 111 | assetFilename = assetFilenames[0]; 112 | } 113 | 114 | var fullAssetPath = publicPath + assetFilename; 115 | return `module.exports = ${ JSON.stringify(fullAssetPath) };\n`; 116 | } 117 | 118 | _writeFile(filename, content, fileSystem) { 119 | var makeDirectories = this._getMakeDirectoriesFunction(fileSystem); 120 | return new Promise(function (resolve, reject) { 121 | makeDirectories(_path2.default.dirname(filename), function (error) { 122 | if (error) { 123 | reject(error); 124 | return; 125 | } 126 | 127 | fileSystem.writeFile(filename, content, function (error) { 128 | if (error) { 129 | reject(error); 130 | } else { 131 | resolve(null); 132 | } 133 | }); 134 | }); 135 | }); 136 | } 137 | 138 | _getMakeDirectoriesFunction(fileSystem) { 139 | if (fileSystem.mkdirp) { 140 | return fileSystem.mkdirp.bind(fileSystem); 141 | } 142 | return function (path, callback) { 143 | return (0, _mkdirp2.default)(path, { fs: fileSystem }, callback); 144 | }; 145 | } 146 | 147 | _shouldEmit(module) { 148 | var _options2 = this.options; 149 | var test = _options2.test; 150 | var include = _options2.include; 151 | var exclude = _options2.exclude; 152 | var resource = module.resource; 153 | 154 | if (test && !this._matches(test, resource)) { 155 | return false; 156 | } 157 | if (include && !this._matches(include, resource)) { 158 | return false; 159 | } 160 | if (exclude && this._matches(exclude, resource)) { 161 | return false; 162 | } 163 | 164 | return true; 165 | } 166 | 167 | _matches(pattern, string) { 168 | if (pattern instanceof RegExp) { 169 | return pattern.test(string); 170 | } 171 | 172 | if (typeof pattern === 'string') { 173 | var escapedPattern = '^' + pattern.replace(/[-[\]{}()*+?.\\^$|]/g, '\\$&'); 174 | var regex = new RegExp(escapedPattern); 175 | return regex.test(string); 176 | } 177 | 178 | if (typeof pattern === 'function') { 179 | return pattern(string); 180 | } 181 | 182 | throw new Error(`Unsupported pattern: ${ pattern }`); 183 | } 184 | } 185 | 186 | AssetModulePlugin.DefaultFileSystem = DEFAULT_FILE_SYSTEM_SYMBOL; 187 | 188 | exports.default = AssetModulePlugin; 189 | module.exports = exports.default; 190 | 191 | /** 192 | * The base directory of the source assets. This is the portion of the asset 193 | * path that will be replaced with the destination base directory. 194 | * 195 | * For example, if `sourceBase` is `'src/web'` and `destinationBase` is 196 | * `'build/web'`, then an asset at `src/web/assets/icon.png` would produce a 197 | * module at `build/web/assets/icon.png`. 198 | * 199 | * The source assets don't need to reside in the source base directory. This 200 | * plugin computes the relative path from the source base directory to each 201 | * asset and applies the same relative path to the destination base directory. 202 | * For example, with the previous example's configuration, an asset at 203 | * `src/favicons/favicon.png` would result in a module at 204 | * `build/favicons/favicon.png`. 205 | */ 206 | 207 | /** 208 | * The base directory of the emitted modules. See the documentation for 209 | * `sourceBase`. 210 | */ 211 | 212 | /** 213 | * A test applied to each asset's resource path to determine if this plugin 214 | * should emit a module for the asset. The test can be any of the types that 215 | * webpack's loader patterns support. 216 | */ 217 | 218 | /** 219 | * A test applied to each asset's resource path to determine if this plugin 220 | * should emit a module for the asset. 221 | */ 222 | 223 | /** 224 | * A test applied to each asset's resource path to determine if this plugin 225 | * should not emit a module for the asset. 226 | */ 227 | 228 | /** 229 | * Overrides the file systems used to write the emitted modules. By default, 230 | * the plugin writes to the compiler's `outputFileSystem`, but you may want 231 | * to write the modules to another file system as well. 232 | * 233 | * Specify `AssetModulePlugin.DefaultFileSystem` in the array to write to the 234 | * compiler's `outputFileSystem`. 235 | */ -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asset-module-webpack-plugin", 3 | "version": "0.0.2", 4 | "description": "A webpack plugin that emits JS modules for your assets such as images. This allows Node to load the modules that webpack generated for your assets without having to run webpack's bundle on the server.", 5 | "main": "lib/AssetModulePlugin.js", 6 | "scripts": { 7 | "build": "babel --out-dir lib src" 8 | }, 9 | "engines": { 10 | "node": ">=2" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/exponentjs/asset-module-webpack-plugin.git" 15 | }, 16 | "keywords": [ 17 | "webpack", 18 | "webpack-plugin", 19 | "assets", 20 | "server-side-rendering" 21 | ], 22 | "author": "James Ide", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/exponentjs/asset-module-webpack-plugin/issues" 26 | }, 27 | "homepage": "https://github.com/exponentjs/asset-module-webpack-plugin#readme", 28 | "devDependencies": { 29 | "babel": "^5.8.21", 30 | "babel-eslint": "^4.0.10", 31 | "eslint": "^1.2.1" 32 | }, 33 | "dependencies": { 34 | "mkdirp": "^0.5.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/AssetModulePlugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | import mkdirp from 'mkdirp'; 5 | import path from 'path'; 6 | 7 | var DEFAULT_FILE_SYSTEM_SYMBOL = Symbol('DefaultFileSystem'); 8 | 9 | type Pattern = string | RegExp | ((value: string) => bool) | Array; 10 | 11 | type FileSystem = { 12 | mkdir: Function; 13 | stat: Function; 14 | writeFile: Function; 15 | mkdirp?: Function; 16 | }; 17 | 18 | type Options = { 19 | /** 20 | * The base directory of the source assets. This is the portion of the asset 21 | * path that will be replaced with the destination base directory. 22 | * 23 | * For example, if `sourceBase` is `'src/web'` and `destinationBase` is 24 | * `'build/web'`, then an asset at `src/web/assets/icon.png` would produce a 25 | * module at `build/web/assets/icon.png`. 26 | * 27 | * The source assets don't need to reside in the source base directory. This 28 | * plugin computes the relative path from the source base directory to each 29 | * asset and applies the same relative path to the destination base directory. 30 | * For example, with the previous example's configuration, an asset at 31 | * `src/favicons/favicon.png` would result in a module at 32 | * `build/favicons/favicon.png`. 33 | */ 34 | sourceBase: string; 35 | /** 36 | * The base directory of the emitted modules. See the documentation for 37 | * `sourceBase`. 38 | */ 39 | destinationBase: string; 40 | /** 41 | * A test applied to each asset's resource path to determine if this plugin 42 | * should emit a module for the asset. The test can be any of the types that 43 | * webpack's loader patterns support. 44 | */ 45 | test?: Pattern; 46 | /** 47 | * A test applied to each asset's resource path to determine if this plugin 48 | * should emit a module for the asset. 49 | */ 50 | include?: Pattern; 51 | /** 52 | * A test applied to each asset's resource path to determine if this plugin 53 | * should not emit a module for the asset. 54 | */ 55 | exclude?: Pattern; 56 | /** 57 | * Overrides the file systems used to write the emitted modules. By default, 58 | * the plugin writes to the compiler's `outputFileSystem`, but you may want 59 | * to write the modules to another file system as well. 60 | * 61 | * Specify `AssetModulePlugin.DefaultFileSystem` in the array to write to the 62 | * compiler's `outputFileSystem`. 63 | */ 64 | fileSystems?: Array; 65 | }; 66 | 67 | class AssetModulePlugin { 68 | options: Options; 69 | 70 | constructor(options: Options) { 71 | this.options = options; 72 | } 73 | 74 | apply(compiler: any) { 75 | compiler.plugin('compilation', (compilation, parameters) => { 76 | var addedResources = new Set(); 77 | compilation._modulesToEmit = []; 78 | compilation.plugin('succeed-module', module => { 79 | var { resource } = module; 80 | if (addedResources.has(resource) || !this._shouldEmit(module)) { 81 | return; 82 | } 83 | addedResources.add(resource); 84 | compilation._modulesToEmit.push(module); 85 | }); 86 | }); 87 | 88 | compiler.plugin('after-emit', (compilation, callback) => { 89 | Promise.all(compilation._modulesToEmit.map(module => { 90 | return this._emitAssetModule(compiler, compilation, module); 91 | })).then(result => { 92 | callback(null, result); 93 | }, error => { 94 | console.warn(error.stack); 95 | callback(error); 96 | }); 97 | }); 98 | } 99 | 100 | _emitAssetModule(compiler: any, compilation: any, module: any) { 101 | var { sourceBase, destinationBase } = this.options; 102 | var { resource } = module; 103 | 104 | var relativePath = path.relative(sourceBase, resource); 105 | var destinationPath = path.resolve(destinationBase, relativePath); 106 | if (resource === destinationPath) { 107 | var message = `Destination path for ${resource} matches the source path; skipping instead of overwriting the file`; 108 | compilation.warnings.push(new Error(message)); 109 | return Promise.resolve(null); 110 | } 111 | 112 | var source = this._getAssetModuleSource(compilation, module); 113 | 114 | if (!this.options.fileSystems) { 115 | var fileSystem = compiler.outputFileSystem; 116 | return this._writeFile(destinationPath, source, fileSystem); 117 | } 118 | 119 | var fileSystems = this.options.fileSystems.map(fileSystem => { 120 | if (fileSystem === DEFAULT_FILE_SYSTEM_SYMBOL) { 121 | return compiler.outputFileSystem; 122 | } 123 | return fileSystem; 124 | }); 125 | 126 | var promises = fileSystems.map(fileSystem => { 127 | return this._writeFile(destinationPath, source, fileSystem).catch(error => { 128 | compilation.errors.push(error); 129 | throw error; 130 | }); 131 | }); 132 | return Promise.all(promises); 133 | } 134 | 135 | _getAssetModuleSource(compilation: any, module: any) { 136 | var { sourceBase } = this.options; 137 | var { resource, assets } = module; 138 | 139 | var publicPath = compilation.mainTemplate.getPublicPath({ 140 | hash: compilation.hash, 141 | }); 142 | 143 | var assetFilename; 144 | var assetFilenames = Object.keys(assets); 145 | if (assetFilenames.length === 0) { 146 | assetFilename = path.relative(sourceBase, resource); 147 | } else { 148 | if (assetFilenames.length > 1) { 149 | var message = `Module at ${resource} generated more than one asset; using the first one`; 150 | compilation.warnings.push(new Error(message)); 151 | } 152 | assetFilename = assetFilenames[0]; 153 | } 154 | 155 | var fullAssetPath = publicPath + assetFilename; 156 | return `module.exports = ${JSON.stringify(fullAssetPath)};\n`; 157 | } 158 | 159 | _writeFile(filename: string, content: string, fileSystem: FileSystem) { 160 | var makeDirectories = this._getMakeDirectoriesFunction(fileSystem); 161 | return new Promise((resolve, reject) => { 162 | makeDirectories(path.dirname(filename), error => { 163 | if (error) { 164 | reject(error); 165 | return; 166 | } 167 | 168 | fileSystem.writeFile(filename, content, error => { 169 | if (error) { 170 | reject(error); 171 | } else { 172 | resolve(null); 173 | } 174 | }); 175 | }); 176 | }); 177 | } 178 | 179 | _getMakeDirectoriesFunction(fileSystem: FileSystem) { 180 | if (fileSystem.mkdirp) { 181 | return fileSystem.mkdirp.bind(fileSystem); 182 | } 183 | return (path, callback) => mkdirp(path, { fs: fileSystem }, callback); 184 | } 185 | 186 | _shouldEmit(module: Object): bool { 187 | var { test, include, exclude } = this.options; 188 | var { resource } = module; 189 | 190 | if (test && !this._matches(test, resource)) { 191 | return false; 192 | } 193 | if (include && !this._matches(include, resource)) { 194 | return false; 195 | } 196 | if (exclude && this._matches(exclude, resource)) { 197 | return false; 198 | } 199 | 200 | return true; 201 | } 202 | 203 | _matches(pattern: Pattern, string: string): bool { 204 | if (pattern instanceof RegExp) { 205 | return pattern.test(string); 206 | } 207 | 208 | if (typeof pattern === 'string') { 209 | var escapedPattern = 210 | '^' + pattern.replace(/[-[\]{}()*+?.\\^$|]/g, '\\$&'); 211 | var regex = new RegExp(escapedPattern); 212 | return regex.test(string); 213 | } 214 | 215 | if (typeof pattern === 'function') { 216 | return pattern(string); 217 | } 218 | 219 | throw new Error(`Unsupported pattern: ${pattern}`); 220 | } 221 | } 222 | 223 | AssetModulePlugin.DefaultFileSystem = DEFAULT_FILE_SYSTEM_SYMBOL; 224 | 225 | export default AssetModulePlugin; 226 | --------------------------------------------------------------------------------