├── .gitignore ├── README.md ├── _example ├── html-css │ ├── app │ │ └── index.jsx │ ├── assets │ │ ├── css │ │ │ ├── ie10.css │ │ │ ├── ie8.css │ │ │ └── ie9.css │ │ ├── images │ │ │ └── hi.png │ │ └── index.html │ ├── package.json │ └── webpack.config.js └── stylus-jade │ ├── images │ └── hi.png │ ├── index.jade │ ├── package.json │ ├── scripts │ └── index.js │ ├── styles │ └── index.styl │ └── webpack.config.js ├── es6 └── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /index.js 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Webpack](http://webpack.github.io) plugin that: 2 | 3 | * writes text resources to the file system; 4 | * in these resources, replaces selected paths with their public counterparts. 5 | 6 | Plays nicely with [webpack-dev-server](http://webpack.github.io/docs/webpack-dev-server.html); 7 | see `includeHash` option of [the constructor](#new-pathrewriteropts--undefined). 8 | 9 | 10 | ## Example 11 | 12 | index.jade: 13 | 14 | ```jade 15 | doctype html 16 | head 17 | title My awesome app 18 | meta( charset="utf-8" ) 19 | link( href="[[ app-*.css ]]", media="all", rel="stylesheet" ) 20 | body 21 | p Hi everyone! 22 | img( src="[[ images/hi.png ]]" ) 23 | script( src="[[ app-*.js ]]" ) 24 | ``` 25 | 26 | webpack.config.js: 27 | 28 | ```js 29 | var ExtractTextPlugin = require('extract-text-webpack-plugin'), 30 | PathRewriterPlugin = require('webpack-path-rewriter') 31 | 32 | module.exports = { 33 | entry: { 34 | app: './scripts/index' 35 | }, 36 | output: { 37 | path: '_dist', 38 | filename: 'app-[chunkhash].js', 39 | publicPath: '/public/path/' 40 | }, 41 | module: { 42 | loaders: [{ 43 | test: /[/]images[/]/, 44 | loader: 'file?name=[path][name]-[hash].[ext]' 45 | }, { 46 | test: /[.]styl$/, 47 | loader: ExtractTextPlugin.extract('css?sourceMap!stylus?sourceMap') 48 | }, { 49 | test: /[.]jade$/, 50 | loader: PathRewriterPlugin.rewriteAndEmit({ 51 | name: '[path][name].html', 52 | loader: 'jade-html?' + JSON.stringify({ pretty: true }) 53 | }) 54 | }] 55 | }, 56 | plugins: [ 57 | new ExtractTextPlugin('app-[contenthash].css', { allChunks: true }), 58 | new PathRewriterPlugin() 59 | ] 60 | } 61 | ``` 62 | 63 | After the build, `_dist/index.html` will contain the following: 64 | 65 | ```html 66 | 67 | 68 | My awesome app 69 | 70 | 71 | 72 | 73 |

Hi everyone!

74 | 75 | 76 | 77 | ``` 78 | 79 | 80 | ## Usage 81 | 82 | This plugin is content-agnostic, so it doesn't perform any parsing. You need to explicitly mark 83 | each path that needs to be rewritten. 84 | 85 | By default, you do this by wrapping such paths inside the `"[[original/path]]"` construction, which 86 | after rewriting transforms to the `"rewritten/path"`. You can control this behavior using options, 87 | both global and per-resource. 88 | 89 | There are two types of assets. 90 | 91 | ### 1. Normal assets 92 | 93 | Most of the assets already exist in the source tree before you run the build. To rewrite paths to 94 | such assets, just mark these paths with the special construction described above. 95 | 96 | For example, if you have `views/index.jade` and `images/hi.png`, in the index.jade you can specify 97 | `"[[../images/hi.png]]"` and it will be replaced with whatever public path `images/hi.png` is 98 | assigned to, e.g. `"/static/images/hi-somelonghash.png"`. 99 | 100 | ### 2. Generated assets 101 | 102 | Some of the assets appear as a result of the build. These include JavaScript bundles (chunks) 103 | and files produced by 104 | [extract-text-webpack-plugin](https://github.com/webpack/extract-text-webpack-plugin). 105 | To rewrite path to an asset of this kind, you need to replace any variable parts of this path 106 | with asterisk `*` symbols. 107 | 108 | For example, to rewrite path from `views/index.jade` to the JS bundle which gets placed to 109 | `[hash]/scripts/app-[chunkhash].js`, the path should be specified as `"[[../*/scripts/app-*.js]]"`. 110 | 111 | 112 | ## Customizing the path marker 113 | 114 | Sometimes it may be inconvenient to use the default `"[[...]]"` marker. It can be customized using 115 | three options: `pathRegExp`, `pathMatchIndex` (index of capturing group containing extracted path) 116 | and `pathReplacer` (template of the replacement string). 117 | 118 | For example, you can use the following options to rewrite all `src` and `href` HTML attributes that 119 | end with some extension (non-relative paths are automatically skipped): 120 | 121 | ```js 122 | { 123 | pathRegExp: /(src|href)\s*=\s*"(.*?\.[\w\d]{1,6})"/, 124 | pathMatchIndex: 2, 125 | pathReplacer: '[1]="[path]"' 126 | } 127 | ``` 128 | 129 | 130 | ## API 131 | 132 | #### `PathRewriter.rewriteAndEmit(loader | opts)` 133 | 134 | Marks a resource for rewriting paths and emitting to the file system. Use it in conjunction with the `new PathRewriter()` in the plugins list. 135 | 136 | Takes one argument, which is either string or object. If string, then it specifies the resource's 137 | loader string along with the options, e.g. 138 | 139 | ```js 140 | PathRewriter.rewriteAndEmit('?name=[path][name]-[hash].[ext]') 141 | PathRewriter.rewriteAndEmit('jade-html?pretty') 142 | PathRewriter.rewriteAndEmit('?name=[path][name].[ext]!jade-html?pretty') 143 | ``` 144 | 145 | Object form allows to pass the following options: 146 | 147 | * `loader` the resource's loader string. 148 | * `loaders` an array of loaders; mutually exclusive with the `loader` option. 149 | * `name` the path to the output file. Defaults to `"[path][name].[ext]"`. May contain the 150 | following tokens: 151 | - `[ext]` the extension of the resource; 152 | - `[name]` the basename of the resource; 153 | - `[path]` the path of the resource relative to the `context` option; 154 | - `[hash]` the hash of the resource's content; 155 | - `[:hash::]` see 156 | [loader-utils docs](https://github.com/webpack/loader-utils#interpolatename); 157 | - `[N]` content of N-th capturing group obtained from matching the resource's path 158 | against the `nameRegExp` option. 159 | * `nameRegExp, context` see `name`. 160 | * `publicPath` allows to override the global `output.publicPath` setting. 161 | * `pathRegExp, pathMatchIndex, pathReplacer` allow to override options for rewriting paths 162 | in this resource. 163 | 164 | For example: 165 | 166 | ```js 167 | PathRewriter.rewriteAndEmit({ 168 | name: '[path][name]-[hash].html', 169 | loader: 'jade-html?pretty' 170 | }) 171 | ``` 172 | 173 | #### `new PathRewriter(opts | undefined)` 174 | 175 | A plugin that emits to the filesystem all resources that were marked with the 176 | `PathRewriter.rewriteAndEmit()` loader. Options: 177 | 178 | * `silent` don't print rewritten paths. Defaults to `false`. 179 | * `emitStats` write `stats.json` file. May be string specifying the file's name. 180 | Defaults to `true`. 181 | * `pathRegExp` regular expression for matching paths. Defaults to `/"\[\[(.*?)\]\]"/`, which tests 182 | for `"[[...]]"` constructions and captures the string between the braces. 183 | * `pathMatchIndex` the index of capturing group in the `pathRegExp` that corresponds to a path. 184 | Defaults to `1`. 185 | * `pathReplacer` template for replacing matched path with the rewritten one. Defaults to 186 | `"[path]"`. May contain following tokens: 187 | - `[path]` the rewritten path; 188 | - `[N]` the content of N-th capturing group of `pathRegExp`. 189 | * `includeHash` make compilation's hash dependent on contents of this resource. Useful with live 190 | reload, as it causes the app to reload each time this resources changes. Defaults to `false`. 191 | -------------------------------------------------------------------------------- /_example/html-css/app/index.jsx: -------------------------------------------------------------------------------- 1 | require("../assets/index.html") 2 | 3 | require("../assets/css/ie8.css") 4 | require("../assets/css/ie9.css") 5 | require("../assets/css/ie10.css") 6 | -------------------------------------------------------------------------------- /_example/html-css/assets/css/ie10.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | background-color: #eee; 4 | } 5 | -------------------------------------------------------------------------------- /_example/html-css/assets/css/ie8.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | background-color: #bbb; 4 | } 5 | -------------------------------------------------------------------------------- /_example/html-css/assets/css/ie9.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | background-color: #ccc; 4 | } 5 | -------------------------------------------------------------------------------- /_example/html-css/assets/images/hi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skozin/webpack-path-rewriter/86816f35d998fb866d93873462872282ede3bd85/_example/html-css/assets/images/hi.png -------------------------------------------------------------------------------- /_example/html-css/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /_example/html-css/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-path-rewriter-example", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "build": "webpack" 6 | }, 7 | "devDependencies": { 8 | "babel-core": "^5.0.9", 9 | "babel-loader": "^5.0.0", 10 | "css-loader": "^0.9.1", 11 | "file-loader": "^0.8.1", 12 | "webpack": "^1.7.2", 13 | "webpack-path-rewriter": "^1.1.0" 14 | }, 15 | "dependencies": {} 16 | } 17 | -------------------------------------------------------------------------------- /_example/html-css/webpack.config.js: -------------------------------------------------------------------------------- 1 | var PathRewriterPlugin = require('webpack-path-rewriter') 2 | 3 | module.exports = { 4 | entry: { 5 | app: "./app/index.jsx" 6 | }, 7 | output: { 8 | path: '_dist', 9 | filename: 'app-[chunkhash].js', 10 | publicPath: '/public/path/' 11 | }, 12 | module: { 13 | loaders: [{ 14 | test: /[.]jsx?$/, 15 | exclude: /node_modules/, 16 | loader: 'babel?optional=runtime' 17 | }, { 18 | test: /[/]images[/]/, 19 | loader: 'file?name=[path][name]-[hash].[ext]' 20 | }, { 21 | test: /[.]css$/, 22 | loader: 'file?name=[path][name]-[hash].[ext]' 23 | }, { 24 | test: /[.]html$/, 25 | loader: PathRewriterPlugin.rewriteAndEmit({ 26 | name: '[name].html' 27 | }) 28 | }] 29 | }, 30 | plugins: [ 31 | new PathRewriterPlugin() 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /_example/stylus-jade/images/hi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skozin/webpack-path-rewriter/86816f35d998fb866d93873462872282ede3bd85/_example/stylus-jade/images/hi.png -------------------------------------------------------------------------------- /_example/stylus-jade/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | head 3 | title My awesome app 4 | meta( charset="utf-8" ) 5 | link( href="[[ app-*.css ]]", media="all", rel="stylesheet" ) 6 | body 7 | p Hi everyone! 8 | img( src="[[ ./images/hi.png ]]" ) 9 | script( src="[[ app-*.js ]]" ) 10 | -------------------------------------------------------------------------------- /_example/stylus-jade/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-path-rewriter-example", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "build": "webpack" 6 | }, 7 | "devDependencies": { 8 | "css-loader": "^0.9.1", 9 | "extract-text-webpack-plugin": "^0.3.8", 10 | "file-loader": "^0.8.1", 11 | "jade": "^1.9.2", 12 | "jade-html-loader": "bline/jade-html-loader", 13 | "stylus": "^0.49.3", 14 | "stylus-loader": "shama/stylus-loader", 15 | "webpack": "^1.7.2", 16 | "webpack-path-rewriter": "^1.1.2" 17 | }, 18 | "dependencies": {} 19 | } 20 | -------------------------------------------------------------------------------- /_example/stylus-jade/scripts/index.js: -------------------------------------------------------------------------------- 1 | require('../index.jade') 2 | require('../styles/index.styl') 3 | 4 | console.log('Hi everyone!') 5 | -------------------------------------------------------------------------------- /_example/stylus-jade/styles/index.styl: -------------------------------------------------------------------------------- 1 | 2 | body 3 | background-color #ccc 4 | -------------------------------------------------------------------------------- /_example/stylus-jade/webpack.config.js: -------------------------------------------------------------------------------- 1 | var ExtractTextPlugin = require('extract-text-webpack-plugin'), 2 | PathRewriterPlugin = require('webpack-path-rewriter') 3 | 4 | module.exports = { 5 | entry: { 6 | app: './scripts/index' 7 | }, 8 | output: { 9 | path: '_dist', 10 | filename: 'app-[chunkhash].js', 11 | publicPath: '/public/path/' 12 | }, 13 | module: { 14 | loaders: [{ 15 | test: /[/]images[/]/, 16 | loader: 'file?name=[path][name]-[hash].[ext]' 17 | }, { 18 | test: /[.]styl$/, 19 | loader: ExtractTextPlugin.extract('css?sourceMap!stylus?sourceMap') 20 | }, { 21 | test: /[.]jade$/, 22 | loader: PathRewriterPlugin.rewriteAndEmit({ 23 | name: '[path][name].html', 24 | loader: 'jade-html?' + JSON.stringify({ pretty: true }) 25 | }) 26 | }] 27 | }, 28 | plugins: [ 29 | new ExtractTextPlugin('app-[chunkhash].css', { allChunks: true }), 30 | new PathRewriterPlugin() 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /es6/index.js: -------------------------------------------------------------------------------- 1 | import loaderUtils from 'loader-utils' 2 | import path from 'path' 3 | 4 | 5 | const PATH_REGEXP = /"\[\[(.*?)\]\]"/g, 6 | PATH_MATCH_INDEX = 1, 7 | PATH_REPLACER = '"[path]"' 8 | 9 | const INLINE_REGEXP = /\[\[\s*INLINE\(([^)]*)\)\s*\]\]/g 10 | 11 | const ABS_PATH_REGEXP = /^[/]|^\w+:[/][/]/, 12 | HASH_REGEXP_SRC = '[\\w\\d_-]+[=]*' 13 | 14 | 15 | class PathRewriter 16 | { 17 | /** 18 | * Marks a resource for rewriting paths and emitting to the file system. 19 | * Use it in conjunction with the `new PathRewriter()` plugin. 20 | * 21 | * The `opts` argument is either string or object. If string, it specifies 22 | * the resource's loader string along with the options, e.g. 23 | * 24 | * PathRewriter.rewriteAndEmit('?name=[path][name]-[hash].[ext]') 25 | * PathRewriter.rewriteAndEmit('jade-html?pretty') 26 | * PathRewriter.rewriteAndEmit('?name=[path][name].[ext]!jade-html?pretty') 27 | * 28 | * Object form allows to specify the following options: 29 | * 30 | * - loader: the resource's loader string. 31 | * 32 | * - loaders: the array of loaders; mutually exclusive with the `loader` option. 33 | * 34 | * - name: the path to the output file, may contain the following tokens: 35 | * 36 | * - [ext] the extension of the resource; 37 | * - [name] the basename of the resource; 38 | * - [path] the path of the resource relative to the `context` option; 39 | * - [hash] the hash of the resource's content; 40 | * - [:hash::] 41 | * see https://github.com/webpack/loader-utils#interpolatename; 42 | * - [N] content of N-th capturing group obtained from matching 43 | * the resource's path against the `nameRegExp` option. 44 | * 45 | * Defaults to '[path][name].[ext]'. 46 | * 47 | * - nameRegExp, context: see `name`. 48 | * 49 | * - publicPath: allows to override the global output.publicPath setting. 50 | * 51 | * - pathRegExp, pathMatchIndex, pathReplacer: allow to override options for 52 | * rewriting paths in this resource. See documentation for the constructor. 53 | * 54 | * Example: 55 | * 56 | * PathRewriter.rewriteAndEmit({ 57 | * name: '[path][name]-[hash].html', 58 | * loader: 'jade-html?pretty' 59 | * }) 60 | */ 61 | static rewriteAndEmit(rwOpts) 62 | { 63 | var thisLoader = module.filename, 64 | loader = '' 65 | 66 | if ('string' == typeof rwOpts) { 67 | return thisLoader + (/^[?!]/.test(rwOpts) 68 | ? rwOpts 69 | : rwOpts && '!' + rwOpts 70 | ) 71 | } 72 | 73 | if (rwOpts.loader != undefined) { 74 | loader = rwOpts.loader 75 | } 76 | 77 | if (rwOpts.loaders != undefined) { 78 | if (rwOpts.loader != undefined) { 79 | throw new Error('cannot use both loader and loaders') 80 | } 81 | loader = rwOpts.loaders.join('!') 82 | } 83 | 84 | var query = extend({}, rwOpts, (key) => key != 'loader' && key != 'loaders') 85 | 86 | if (query.pathRegExp) { 87 | let re = query.pathRegExp; query.pathRegExp = re instanceof RegExp ? { 88 | source: re.source, 89 | flags: (re.ignoreCase ? 'i' : '') + (re.multiline ? 'm' : '') 90 | } : { 91 | source: '' + re, 92 | flags: '' 93 | } 94 | } 95 | 96 | if (query.inlineRegExp) { 97 | let re = query.inlineRegExp; query.inlineRegExp = re instanceof RegExp ? { 98 | source: re.source, 99 | flags: (re.ignoreCase ? 'i' : '') + (re.multiline ? 'm' : '') 100 | } : { 101 | source: '' + re, 102 | flags: '' 103 | } 104 | } 105 | 106 | // we need to temporarily replace all exclamation marks because they have 107 | // special meaning in loader strings v 108 | // 109 | return thisLoader + '?' + JSON.stringify(query).replace(/!/g, ':BANG:') + (/^!/.test(loader) 110 | ? loader 111 | : loader && '!' + loader 112 | ) 113 | } 114 | 115 | 116 | /** 117 | * A plugin that emits to the filesystem all resources that are marked 118 | * with `PathRewriter.rewriteAndEmit()` loader. 119 | * 120 | * Options: 121 | * 122 | * - silent: don't print rewritten paths. Defaults to false. 123 | * 124 | * - emitStats: write stats.json file. May be string specifying 125 | * the file's name. Defaults to true. 126 | * 127 | * - pathRegExp: regular expression for matching paths. Defaults 128 | * to /"\[\[(.*?)\]\]"/, which tests for \"[[...]]\" constructions, 129 | * capturing the string between the braces. 130 | * 131 | * - pathMatchIndex: the index of capturing group in the pathRegExp 132 | * that corresponds to a path. 133 | * 134 | * - pathReplacer: template for replacing matched path with the 135 | * rewritten one. May contain following tokens: 136 | * - [path] the rewritten path; 137 | * - [N] the content of N-th capturing group of pathRegExp. 138 | * Defaults to \"[path]\". 139 | * 140 | * - includeHash: make compilation's hash dependent on contents 141 | * of this resource. Useful with live reload, as it causes the app 142 | * to reload each time this resources changes. Defaults to false. 143 | */ 144 | constructor(opts) 145 | { 146 | this.opts = extend({ 147 | silent: false, 148 | emitStats: true, 149 | includeHash: false, 150 | pathRegExp: undefined, 151 | pathReplacer: undefined, 152 | pathMatchIndex: undefined 153 | }, opts) 154 | this.pathRegExp = PathRewriter.makeRegExp(this.opts.pathRegExp) || PATH_REGEXP 155 | this.pathMatchIndex = this.opts.pathMatchIndex == undefined 156 | ? PATH_MATCH_INDEX 157 | : this.opts.pathMatchIndex 158 | this.pathReplacer = this.opts.pathReplacer || PATH_REPLACER 159 | this.inlineRegExp = PathRewriter.makeRegExp(this.opts.inlineRegExp) || INLINE_REGEXP 160 | this.rwPathsCache = {} 161 | this.modules = [] 162 | this.modulesByRequest = {} 163 | this.compilationRwPathsCache = undefined 164 | this.compilationAssetsPaths = undefined 165 | } 166 | 167 | 168 | static loader(content) // loader entry point, called by Webpack; see PathRewriterEntry 169 | { 170 | this.cacheable && this.cacheable() 171 | 172 | var rewriter = this[ __dirname ] 173 | if (rewriter == undefined) { 174 | throw new Error( 175 | 'webpack-path-rewriter loader is used without the corresponding plugin;\n ' + 176 | 'add `new PathRewriter()` to the list of plugins in the Webpack config' 177 | ) 178 | } 179 | 180 | var query = loaderUtils.parseQuery(this.query && this.query.replace(/:BANG:/g, '!')), 181 | topLevelContext = this.options.context, 182 | publicPath = query.publicPath || this.options.output.publicPath || '' 183 | 184 | if (publicPath.length && publicPath[ publicPath.length - 1 ] != '/') { 185 | publicPath = publicPath + '/' 186 | } 187 | 188 | var url = loaderUtils.interpolateName(this, query.name || '[path][name].[ext]', { 189 | content: content, 190 | context: query.context || topLevelContext, 191 | regExp: query.nameRegExp 192 | }) 193 | 194 | var moduleData = {url, content, publicPath, topLevelContext, 195 | request: this.request, 196 | context: this.context, 197 | relPath: path.relative(topLevelContext, this.resourcePath), 198 | pathRegExp: PathRewriter.makeRegExp(query.pathRegExp) || rewriter.pathRegExp, 199 | pathReplacer: query.pathReplacer || rewriter.pathReplacer, 200 | pathMatchIndex: +(query.pathMatchIndex == undefined 201 | ? rewriter.pathMatchIndex 202 | : query.pathMatchIndex 203 | ), 204 | inlineRegExp: PathRewriter.makeRegExp(query.inlineRegExp) || rewriter.inlineRegExp 205 | } 206 | 207 | var exportStatement = 'module.exports = "' + publicPath + url + (rewriter.opts.includeHash 208 | ? '" // content hash: ' + loaderUtils.interpolateName(this, '[hash]', { content }) 209 | : '"' 210 | ) 211 | 212 | var callback = this.async(); PathRewriter.extractAssets(this, moduleData, assetRequests => 213 | { 214 | rewriter.addModule(moduleData) 215 | 216 | callback(null, assetRequests 217 | .map(req => `require(${ JSON.stringify(req) })`) 218 | .join('\n') + '\n' + exportStatement 219 | ) 220 | }) 221 | } 222 | 223 | 224 | static makeRegExp(desc) 225 | { 226 | if (desc == undefined) { 227 | return undefined 228 | } 229 | if (desc instanceof RegExp) { 230 | return new RegExp(desc.source, 'g' 231 | + (desc.ignoreCase ? 'i' : '') 232 | + (desc.multiline ? 'm' : '') 233 | ) 234 | } 235 | var src = desc.source || '' + desc, 236 | flags = desc.flags || 'g' 237 | return new RegExp(src, flags.indexOf('g') == -1 238 | ? flags + 'g' 239 | : flags 240 | ) 241 | } 242 | 243 | 244 | static extractAssets(loaderCtx, moduleData, cb) 245 | { 246 | var paths = PathRewriter.findNonWildcardPaths(moduleData), 247 | numPaths = paths.length, 248 | numPathsDone = 0, 249 | assetsData = [] 250 | if (paths.length == 0) { 251 | return done(assetsData) 252 | } 253 | paths.forEach(path => { 254 | var request = loaderUtils.urlToRequest(path) 255 | // we need to discard all possibly generated assets, i.e. those 256 | // that are not present in the source tree, because we cannot 257 | // require them 258 | loaderCtx.resolve(moduleData.context, request, (err, _) => { 259 | if (err == null) { 260 | assetsData.push({ path, request, rwPath: undefined }) 261 | } 262 | if (++numPathsDone == numPaths) { 263 | done(assetsData) 264 | } 265 | }) 266 | }) 267 | function done(assetsData) { 268 | var byPath = {}, byRequest = {} 269 | assetsData.forEach(data => { 270 | byRequest[ data.request ] = data 271 | byPath[ data.path ] = data 272 | }) 273 | moduleData.assetDataByPath = byPath 274 | moduleData.assetDataByRequest = byRequest 275 | cb(assetsData.map(data => data.request)) 276 | } 277 | } 278 | 279 | 280 | static findNonWildcardPaths({ content, pathRegExp, pathMatchIndex }) 281 | { 282 | var results = [], 283 | matches 284 | while (matches = pathRegExp.exec(content)) { 285 | var path = trim(matches[ pathMatchIndex ]) 286 | if (path 287 | && path.indexOf('*') == -1 288 | && results.indexOf(path) == -1 289 | && loaderUtils.isUrlRequest(path) 290 | ){ 291 | results.push(path) 292 | } 293 | } 294 | pathRegExp.lastIndex = 0 295 | return results 296 | } 297 | 298 | 299 | addModule(moduleData) 300 | { 301 | this.modules.push(moduleData) 302 | this.modulesByRequest[ moduleData.request ] = moduleData 303 | } 304 | 305 | 306 | apply(compiler) // plugin entry point, called by Webpack 307 | { 308 | compiler.plugin('compilation', (compilation) => { 309 | compilation.plugin('normal-module-loader', (loaderContext, module) => { 310 | if (loaderContext[ __dirname ]) 311 | throw new Error('cannot use more than one instance of PathRewriter in the plugins list') 312 | loaderContext[ __dirname ] = this 313 | }) 314 | }) 315 | compiler.plugin('after-compile', (compilation, callback) => { 316 | this.onAfterCompile(compilation, callback) 317 | }) 318 | compiler.plugin('emit', (compiler, callback) => { 319 | this.onEmit(compiler, callback) 320 | }) 321 | } 322 | 323 | 324 | onAfterCompile(compilation, callback) 325 | { 326 | compilation.modules.forEach(module => { 327 | var moduleData = this.modulesByRequest[ module.request ] 328 | moduleData && this.extractModuleAssetsPublicPaths(moduleData, module) 329 | }) 330 | callback() 331 | } 332 | 333 | 334 | extractModuleAssetsPublicPaths(moduleData, module) 335 | { 336 | var assetDataByRequest = moduleData.assetDataByRequest, 337 | deps = module.dependencies 338 | 339 | for (var i = 0; i < deps.length; ++i) { 340 | var dep = deps[i] 341 | if (!dep.request || !dep.module) continue 342 | 343 | var assetData = assetDataByRequest[ dep.request ] 344 | if (assetData == undefined) continue 345 | 346 | var assets = dep.module.assets && Object.keys(dep.module.assets) || [] 347 | if (assets.length != 1) { 348 | let paths = assets.map(a => `"${a}"`).join(', ') 349 | assetData.error = new PathRewriterError( 350 | `invalid number of assets for path "${ assetData.path }", assets: [${ paths }]`, 351 | moduleData 352 | ) 353 | continue 354 | } 355 | 356 | assetData.rwPath = assets[0] 357 | } 358 | } 359 | 360 | 361 | onEmit(compiler, callback) 362 | { 363 | var stats = compiler.getStats().toJson() 364 | 365 | this.compilationAssetsPaths = stats.assets.map(asset => asset.name) 366 | this.compilationRwPathsCache = {} 367 | 368 | this.modules.forEach(moduleData => { 369 | this.rewriteModulePaths(moduleData, compiler) 370 | }) 371 | 372 | this.modules = [] 373 | this.modulesByRequest = {} 374 | 375 | // WARN WTF 376 | // 377 | // We need to cache assets from previous compilations in watch mode 378 | // because, for some reason, some assets don't get included in the 379 | // assets list after recompilations. 380 | // 381 | extend(this.rwPathsCache, this.compilationRwPathsCache) 382 | 383 | if (this.opts.emitStats) { 384 | var statsJson = JSON.stringify(stats, null, ' '), 385 | statsPath = typeof this.opts.emitStats == 'string' 386 | ? this.opts.emitStats 387 | : 'stats.json' 388 | compiler.assets[ statsPath ] = { 389 | source: () => statsJson, 390 | size: () => statsJson.length 391 | } 392 | } 393 | 394 | callback() 395 | } 396 | 397 | 398 | rewriteModulePaths(moduleData, compiler) 399 | { 400 | var content = moduleData.content.replace(moduleData.pathRegExp, (...matches) => { 401 | var srcPath = trim(matches[ moduleData.pathMatchIndex ]) 402 | try { 403 | var rwPath = this.rewritePath(srcPath, moduleData) 404 | rwPath = this.prependPublicPath(moduleData.publicPath, rwPath) 405 | this.opts.silent || (srcPath != rwPath) && console.error( 406 | `PathRewriter[ ${ moduleData.relPath } ]: "${ srcPath }" -> "${ rwPath }"` 407 | ) 408 | return moduleData.pathReplacer.replace(/\[(path|\d+)\]/g, (_, t) => { 409 | return t == 'path' ? rwPath : matches[ +t ] 410 | }) 411 | } 412 | catch(e) { 413 | if (!(e instanceof PathRewriterError) && !this.opts.silent) { 414 | console.error(e.stack) 415 | } 416 | compiler.errors.push(e) 417 | return srcPath 418 | } 419 | }).replace(moduleData.inlineRegExp, (match, assetUrl) => { 420 | let asset = compiler.assets[ assetUrl ] 421 | if (!asset) { 422 | compiler.errors.push(new Error( 423 | `Cannot inline asset "${ assetUrl }" in ${ moduleData.relPath }: not found` 424 | )) 425 | return match 426 | } 427 | else if (!this.opts.silent) { 428 | console.error(`PathRewriter[ ${ moduleData.relPath } ]: inlined "${ assetUrl }"`) 429 | } 430 | return asset.source() 431 | }) 432 | compiler.assets[ moduleData.url ] = { 433 | source: () => content, 434 | size: () => content.length 435 | } 436 | } 437 | 438 | 439 | rewritePath(srcPath, moduleData) 440 | { 441 | var key = moduleData.context + '|' + srcPath, 442 | rwPath = this.compilationRwPathsCache[ key ] 443 | 444 | if (rwPath) 445 | return rwPath 446 | 447 | var assetData = moduleData.assetDataByPath[ srcPath ] 448 | if (assetData) { 449 | if (assetData.error) 450 | throw assetData.error 451 | rwPath = assetData.rwPath 452 | } 453 | else { 454 | rwPath = this.rewriteGeneratedAssetPath(srcPath, moduleData) 455 | } 456 | 457 | if (rwPath == undefined) { 458 | // in watch mode, sometimes some assets are not listed during 459 | // recompilations, so we need to use a long-term cache 460 | rwPath = this.rwPathsCache[ key ] 461 | } 462 | 463 | if (rwPath == undefined || rwPath.length == 0) { 464 | throw new PathRewriterError(`could not resolve path "${ srcPath }"`, moduleData) 465 | } 466 | 467 | this.compilationRwPathsCache[ key ] = rwPath 468 | return rwPath 469 | } 470 | 471 | 472 | rewriteGeneratedAssetPath(srcPath, moduleData) 473 | { 474 | if (ABS_PATH_REGEXP.test(srcPath)) 475 | return srcPath 476 | 477 | var parts = srcPath.split(/[*]+/) 478 | 479 | if (parts.join('').length == 0) { 480 | throw new PathRewriterError( 481 | `invalid wildcard path "${ srcPath }", must contain at least one non-wildcard symbol`, 482 | moduleData 483 | ) 484 | } 485 | 486 | var searchRE = new RegExp('^' + parts.map(escapeRegExp).join(HASH_REGEXP_SRC) + '$') 487 | 488 | for (var i = 0; i < this.compilationAssetsPaths.length; ++i) { 489 | var rwPath = this.compilationAssetsPaths[i] 490 | if (searchRE.test(rwPath)) { 491 | return rwPath 492 | } 493 | } 494 | 495 | return undefined 496 | } 497 | 498 | 499 | prependPublicPath(publicPath, path) 500 | { 501 | return ABS_PATH_REGEXP.test(path) 502 | ? path 503 | : publicPath + path 504 | } 505 | } 506 | 507 | 508 | function trim(s) { 509 | return s && s.replace(/^\s+|\s+$/g, '') 510 | } 511 | 512 | 513 | function escapeRegExp(s) { 514 | return s && s.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&') 515 | } 516 | 517 | 518 | function extend(dst, src, filter) { 519 | if (src) Object.keys(src).forEach(filter == undefined 520 | ? key => { dst[ key ] = src[ key ] } 521 | : key => { 522 | if (filter(key)) { 523 | dst[ key ] = src[ key ] 524 | } 525 | } 526 | ) 527 | return dst 528 | } 529 | 530 | 531 | function PathRewriterError(msg, moduleData) 532 | { 533 | Error.call(this) 534 | Error.captureStackTrace(this, PathRewriterError) 535 | this.name = 'PathRewriterError' 536 | this.message = moduleData.relPath + ': ' + msg 537 | } 538 | PathRewriterError.prototype = Object.create(Error.prototype) 539 | 540 | 541 | function PathRewriterEntry(arg) 542 | { 543 | return this instanceof PathRewriterEntry 544 | ? new PathRewriter(arg) // called with new => plugin 545 | : PathRewriter.loader.call(this, arg) // called as a funtion => loader 546 | } 547 | PathRewriterEntry.rewriteAndEmit = PathRewriter.rewriteAndEmit 548 | 549 | 550 | module.exports = PathRewriterEntry 551 | export default PathRewriterEntry 552 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-path-rewriter", 3 | "version": "1.1.4", 4 | "description": "Webpack plugin for replacing resources' paths with public ones", 5 | "author": "Sam Kozin ", 6 | "scripts": { 7 | "prepublish": "babel ./es6 --presets es2015 --out-dir .", 8 | "watch": "babel --watch ./es6 --presets es2015 --out-dir .", 9 | "test": "echo 'Sorry, no tests yet' && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/skozin/webpack-path-rewriter.git" 14 | }, 15 | "keywords": [ 16 | "webpack", 17 | "plugin", 18 | "assets", 19 | "hashing", 20 | "html" 21 | ], 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/skozin/webpack-path-rewriter/issues" 25 | }, 26 | "homepage": "https://github.com/skozin/webpack-path-rewriter", 27 | "dependencies": { 28 | "loader-utils": "^0.2.11" 29 | }, 30 | "devDependencies": { 31 | "babel-cli": "^6.1.18", 32 | "babel-preset-es2015": "^6.1.18" 33 | }, 34 | "files": [ 35 | "index.js", 36 | "es6/index.js" 37 | ], 38 | "main": "index.js" 39 | } 40 | --------------------------------------------------------------------------------