├── .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 |
--------------------------------------------------------------------------------