diff --git a/.eslintcache b/.eslintcache new file mode 100644 index 0000000..e92bebb --- /dev/null +++ b/.eslintcache @@ -0,0 +1 @@ +{"C:\\Users\\Adrian Miranda\\Sites\\ambox\\github\\adriancmiranda\\file-loader\\src\\cjs.js":{"size":45,"mtime":1496936755980,"hashOfConfig":"16xjw5j","results":{"filePath":"C:\\Users\\Adrian Miranda\\Sites\\ambox\\github\\adriancmiranda\\file-loader\\src\\cjs.js","messages":[],"errorCount":0,"warningCount":0}},"C:\\Users\\Adrian Miranda\\Sites\\ambox\\github\\adriancmiranda\\file-loader\\src\\helper.js":{"size":334,"mtime":1496934738398,"hashOfConfig":"16xjw5j","results":{"filePath":"C:\\Users\\Adrian Miranda\\Sites\\ambox\\github\\adriancmiranda\\file-loader\\src\\helper.js","messages":[],"errorCount":0,"warningCount":0}},"C:\\Users\\Adrian Miranda\\Sites\\ambox\\github\\adriancmiranda\\file-loader\\src\\index.js":{"size":3601,"mtime":1496941794969,"hashOfConfig":"16xjw5j","results":{"filePath":"C:\\Users\\Adrian Miranda\\Sites\\ambox\\github\\adriancmiranda\\file-loader\\src\\index.js","messages":[],"errorCount":0,"warningCount":0}},"C:\\Users\\Adrian Miranda\\Sites\\ambox\\github\\adriancmiranda\\file-loader\\test\\correct-filename.test.js":{"size":8332,"mtime":1496936927390,"hashOfConfig":"16xjw5j","results":{"filePath":"C:\\Users\\Adrian Miranda\\Sites\\ambox\\github\\adriancmiranda\\file-loader\\test\\correct-filename.test.js","messages":[],"errorCount":0,"warningCount":0}},"C:\\Users\\Adrian Miranda\\Sites\\ambox\\github\\adriancmiranda\\file-loader\\test\\optional-file-emission.test.js":{"size":628,"mtime":1496936290558,"hashOfConfig":"16xjw5j","results":{"filePath":"C:\\Users\\Adrian Miranda\\Sites\\ambox\\github\\adriancmiranda\\file-loader\\test\\optional-file-emission.test.js","messages":[],"errorCount":0,"warningCount":0}},"C:\\Users\\Adrian Miranda\\Sites\\ambox\\github\\adriancmiranda\\file-loader\\test\\helper.test.js":{"size":847,"mtime":1496949000783,"hashOfConfig":"16xjw5j","results":{"filePath":"C:\\Users\\Adrian Miranda\\Sites\\ambox\\github\\adriancmiranda\\file-loader\\test\\helper.test.js","messages":[],"errorCount":0,"warningCount":0}}} \ No newline at end of file diff --git a/.eslintignore b/.eslintignore index b2d59d1..8225baa 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,2 @@ /node_modules -/dist \ No newline at end of file +/dist diff --git a/.gitignore b/.gitignore index c6c08c9..836cf4b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,28 @@ +# samples +examples/*/dist/* + # development -/node_modules +node_modules/ /coverage - -logs -*.log -npm-debug.log* -yarn-debug.log* -.eslintcache /dist /local /reports -.DS_Store +logs +*.log +*.log* Thumbs.db -.idea -.vscode *.sublime-project -*.sublime-workspace \ No newline at end of file +*.sublime-workspace + +# dotfiles +.* +!.babelrc +!.editorconfig +!.eslintcache +!.eslintignore +!.eslintrc +!.gitattributes +!.gitignore +!.travis.yml +*~ +*#* diff --git a/README.md b/README.md index c4a6ed1..dc5fe2a 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,8 @@ use: "file-loader?name=[name].[ext]&publicPath=assets/foo/&outputPath=app/images } ``` +`cssOutputPath` also should be used (together with `useRelativePath` property) to define the output path context. + #### Filename template placeholders * `[ext]` the extension of the resource diff --git a/examples/relative-path-with-multiple-css-outputs/package.json b/examples/relative-path-with-multiple-css-outputs/package.json new file mode 100644 index 0000000..0a94b47 --- /dev/null +++ b/examples/relative-path-with-multiple-css-outputs/package.json @@ -0,0 +1,21 @@ +{ + "name": "use-relative-path", + "author": "@adriancmiranda", + "scripts": { + "dev": "webpack-dev-server --open --env.dev", + "prebuild": "rimraf dist/", + "build": "webpack -p" + }, + "optionalDependencies": { + "css-loader": "0.28.0", + "extract-text-webpack-plugin": "2.1.0", + "html-webpack-plugin": "2.28.0", + "ip": "1.1.5", + "rimraf": "2.6.1", + "style-loader": "0.16.1", + "webpack-dev-server": "2.4.2" + }, + "devDependencies": { + "webpack": "2.4.1" + } +} diff --git a/examples/relative-path-with-multiple-css-outputs/source/scripts/desktop.js b/examples/relative-path-with-multiple-css-outputs/source/scripts/desktop.js new file mode 100644 index 0000000..eb28b71 --- /dev/null +++ b/examples/relative-path-with-multiple-css-outputs/source/scripts/desktop.js @@ -0,0 +1 @@ +console.log('Desktop'); diff --git a/examples/relative-path-with-multiple-css-outputs/source/scripts/mobile.js b/examples/relative-path-with-multiple-css-outputs/source/scripts/mobile.js new file mode 100644 index 0000000..14dedd7 --- /dev/null +++ b/examples/relative-path-with-multiple-css-outputs/source/scripts/mobile.js @@ -0,0 +1 @@ +console.log('Mobile'); diff --git a/examples/relative-path-with-multiple-css-outputs/source/styles/desktop.css b/examples/relative-path-with-multiple-css-outputs/source/styles/desktop.css new file mode 100644 index 0000000..b0b6180 --- /dev/null +++ b/examples/relative-path-with-multiple-css-outputs/source/styles/desktop.css @@ -0,0 +1,50 @@ +/* Desktop styles */ +*, +*:after, +*:before { + -webkit-tap-highlight-color: transparent; + tap-highlight-color: transparent; + box-sizing: border-box; + outline-style: none; +} + +html, +body { + width: 100%; + height: 100%; +} + +body { + font-family: Helvetica, sans-serif; + font-size: 16px; + line-height: 10px; + margin: 0; +} + +div, +canvas { + position: absolute; + width: 100%; + height: 100%; + background-color: rgb(77, 155, 216); +} + +div { + background: rgba(0,0,0,0.2) url("../../../../.github/assets/file_loader_icon.svg") no-repeat 50% 50%; +} + +img { + position: absolute; + right: 10px; + bottom: 10px; + display: block; +} + +div:before { + position: absolute; + display: block; + margin-top: 10px; + margin-left: 10px; + color: #fff; + content: "file-loader / examples / relative-path-with-multiple-output / desktop"; +} diff --git a/examples/relative-path-with-multiple-css-outputs/source/styles/mobile.css b/examples/relative-path-with-multiple-css-outputs/source/styles/mobile.css new file mode 100644 index 0000000..58fb63d --- /dev/null +++ b/examples/relative-path-with-multiple-css-outputs/source/styles/mobile.css @@ -0,0 +1,50 @@ +/* Desktop styles */ +*, +*:after, +*:before { + -webkit-tap-highlight-color: transparent; + tap-highlight-color: transparent; + box-sizing: border-box; + outline-style: none; +} + +html, +body { + width: 100%; + height: 100%; +} + +body { + font-family: Helvetica, sans-serif; + font-size: 16px; + line-height: 10px; + margin: 0; +} + +div, +canvas { + position: absolute; + width: 100%; + height: 100%; + background-color: rgb(142, 214, 251); +} + +div { + background: rgba(0,0,0,0.2) url("../../../../.github/assets/file_loader_icon.svg") no-repeat 50% 50%; +} + +img { + position: absolute; + right: 10px; + bottom: 10px; + display: block; +} + +div:before { + position: absolute; + display: block; + margin-top: 10px; + margin-left: 10px; + color: #fff; + content: "file-loader / examples / relative-path-with-multiple-output / mobile"; +} diff --git a/examples/relative-path-with-multiple-css-outputs/source/views/desktop.html b/examples/relative-path-with-multiple-css-outputs/source/views/desktop.html new file mode 100644 index 0000000..42c8565 --- /dev/null +++ b/examples/relative-path-with-multiple-css-outputs/source/views/desktop.html @@ -0,0 +1,13 @@ + + + + + <%= htmlWebpackPlugin.options.title %> + + + +
+ +
+ + diff --git a/examples/relative-path-with-multiple-css-outputs/source/views/mobile.html b/examples/relative-path-with-multiple-css-outputs/source/views/mobile.html new file mode 100644 index 0000000..42c8565 --- /dev/null +++ b/examples/relative-path-with-multiple-css-outputs/source/views/mobile.html @@ -0,0 +1,13 @@ + + + + + <%= htmlWebpackPlugin.options.title %> + + + +
+ +
+ + diff --git a/examples/relative-path-with-multiple-css-outputs/webpack.config.js b/examples/relative-path-with-multiple-css-outputs/webpack.config.js new file mode 100644 index 0000000..592d4a3 --- /dev/null +++ b/examples/relative-path-with-multiple-css-outputs/webpack.config.js @@ -0,0 +1,109 @@ +const ip = require('ip'); +const path = require('path'); +const Text = require('extract-text-webpack-plugin'); +const Html = require('html-webpack-plugin'); +const webpack = require('webpack'); +const fileLoader = require.resolve('../../src/cjs'); + +const resolve = (...args) => path.resolve(__dirname, ...args); + +const OUTPUT = { + bundle: 'dist/', + img: 'media/images/', + css: 'styles/', + js: 'scripts/', +}; + +module.exports = (argv = {}) => { + const config = { + devtool: argv.dev ? '#cheap-module-eval-source-map' : '#source-map', + entry: { + desktop: ['./source/scripts/desktop.js', './source/styles/desktop.css'], + mobile: ['./source/scripts/mobile.js', './source/styles/mobile.css'], + }, + output: { + path: resolve(OUTPUT.bundle), + filename: `${OUTPUT.js}[name].js`, + }, + devServer: { + contentBase: resolve(OUTPUT.bundle), + historyApiFallback: true, + stats: 'errors-only', + host: ip.address(), + port: 3000, + }, + module: { + rules: [ + { + test: /\.css$/, + use: Text.extract({ + publicPath: argv.dev && OUTPUT.bundle, + fallback: 'style-loader', + use: [{ + loader: 'css-loader', + query: { + minimize: false, + }, + }], + }), + }, + { + test: /\.(jpe?g|png|gif|svg)(\?v=\d+\.\d+\.\d+)?$/, + loader: fileLoader, + options: { + useRelativePath: true, + + /* + |* If you need a multiple output path for any reason + |* @see https://github.com/webpack-contrib/file-loader/issues/149#issuecomment-294290509 + `*/ + cssOutputPath: OUTPUT.css, + + outputPath: OUTPUT.img, + name: '[name].[hash:7].[ext]', + }, + }, + ], + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: `'${argv.dev ? 'development' : 'production'}'`, + }, + }), + new Text({ + filename: `${OUTPUT.css}[name]${argv.dev ? '' : '.[chunkhash]'}.css`, + disable: !!argv.dev, + allChunks: true, + }), + new Html({ + title: 'file-loader // useRelativePath // mobile', + template: './source/views/mobile.html', + filename: 'mobile.html', + chunks: ['mobile'], + }), + new Html({ + title: 'file-loader // useRelativePath // desktop', + template: './source/views/desktop.html', + filename: 'index.html', + chunks: ['desktop'], + }), + new webpack.HotModuleReplacementPlugin({ quiet: true }), + new webpack.NoEmitOnErrorsPlugin(), + new webpack.NamedModulesPlugin(), + ], + }; + + if (argv.dev) { + const host = config.devServer.host; + const port = config.devServer.port; + Object.keys(config.entry).forEach((entry) => { + config.entry[entry] = [ + `webpack-dev-server/client?http://${host}:${port}`, + 'webpack/hot/only-dev-server', + ].concat(config.entry[entry]); + }); + } + + return config; +}; diff --git a/examples/relative-path-with-single-css-output/package.json b/examples/relative-path-with-single-css-output/package.json new file mode 100644 index 0000000..7db44af --- /dev/null +++ b/examples/relative-path-with-single-css-output/package.json @@ -0,0 +1,20 @@ +{ + "name": "use-relative-path", + "author": "@adriancmiranda", + "scripts": { + "dev": "webpack-dev-server --open --env.dev", + "prebuild": "rimraf dist/", + "build": "webpack -p" + }, + "optionalDependencies": { + "css-loader": "0.28.0", + "extract-text-webpack-plugin": "2.1.0", + "html-webpack-plugin": "2.28.0", + "rimraf": "2.6.1", + "style-loader": "0.16.1", + "webpack-dev-server": "2.4.2" + }, + "devDependencies": { + "webpack": "2.4.1" + } +} diff --git a/examples/relative-path-with-single-css-output/source/index.css b/examples/relative-path-with-single-css-output/source/index.css new file mode 100644 index 0000000..d0fc058 --- /dev/null +++ b/examples/relative-path-with-single-css-output/source/index.css @@ -0,0 +1,49 @@ +*, +*:after, +*:before { + -webkit-tap-highlight-color: transparent; + tap-highlight-color: transparent; + box-sizing: border-box; + outline-style: none; +} + +html, +body { + width: 100%; + height: 100%; +} + +body { + font-family: Helvetica, sans-serif; + font-size: 16px; + line-height: 10px; + margin: 0; +} + +div, +canvas { + position: absolute; + width: 100%; + height: 100%; + background-color: purple; +} + +div { + background: rgba(0,0,0,0.5) url("../../../.github/assets/file_loader_icon.svg") no-repeat 50% 50%; +} + +img { + position: absolute; + right: 10px; + bottom: 10px; + display: block; +} + +div:before { + position: absolute; + display: block; + margin-top: 10px; + margin-left: 10px; + color: #fff; + content: "file-loader / examples / relative-path-with-single-output"; +} diff --git a/examples/relative-path-with-single-css-output/source/index.html b/examples/relative-path-with-single-css-output/source/index.html new file mode 100644 index 0000000..539b6b0 --- /dev/null +++ b/examples/relative-path-with-single-css-output/source/index.html @@ -0,0 +1,13 @@ + + + + + <%= htmlWebpackPlugin.options.title %> + + + +
+ +
+ + diff --git a/examples/relative-path-with-single-css-output/source/index.js b/examples/relative-path-with-single-css-output/source/index.js new file mode 100644 index 0000000..bb6b422 --- /dev/null +++ b/examples/relative-path-with-single-css-output/source/index.js @@ -0,0 +1,146 @@ +const Utils = { + randomRange(min, max) { + return Math.random() * (max - min) + min; + }, + distance(x1, y1, x2, y2) { + return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); + }, +}; + +function FileIcon(context, scale) { + this.context = context; + this.canvas = this.context.canvas; + this.image = new Image(); + this.image.src = require('../../../.github/assets/file_loader_icon.svg'); + this.scale = scale; + this.x = Math.random() * this.canvas.clientWidth; + this.y = Math.random() * this.canvas.clientHeight; + this.point = { + x: Math.cos(Math.floor(Math.random() * 360)) * 1.1, + y: Math.sin(Math.floor(Math.random() * 360)) * 1.1, + }; +} + +FileIcon.prototype = { + constructor: FileIcon, + + update() { + this.bounds(); + this.x += this.point.x; + this.y += this.point.y; + }, + + bounds() { + const registrationPointX = this.image.width * this.scale * 0.5; + const registrationPointY = this.image.height * this.scale * 0.5; + const boundsX = this.canvas.clientWidth - registrationPointX; + const boundsY = this.canvas.clientHeight - registrationPointY; + if (this.x >= boundsX || this.x <= registrationPointX) { + this.point.x *= -1; + } + if (this.y >= boundsY || this.y <= registrationPointY) { + this.point.y *= -1; + } + if (this.x > boundsX) { + this.x = boundsX; + } + if (this.y > boundsY) { + this.y = boundsY; + } + if (this.x < registrationPointX) { + this.x = registrationPointX; + } + if (this.y < registrationPointY) { + this.y = registrationPointY; + } + }, + + draw() { + const imageX = this.image.width * this.scale; + const imageY = this.image.height * this.scale; + this.context.drawImage(this.image, + 0, 0, 128, 128, // source + this.x - imageX * 0.5, + this.y - imageY * 0.5, + imageX, + imageY, + ); + }, +}; + +function FilesMesh(selector, options) { + this.options = Object.assign(FilesMesh.defaults, options); + this.canvas = document.querySelector(selector); + this.context = this.canvas.getContext('2d'); + this.rgb = this.options.lineColor.match(/\d+/g); +} + +FilesMesh.defaults = { + lineColor: 'rgb(234, 239, 240)', + bondDistance: 300, + numFiles: 25, + minScale: 0.05, + maxScale: 0.4, +}; + +FilesMesh.prototype = { + constructor: FilesMesh, + + start() { + this.arrange = this.arrange.bind(this); + window.addEventListener('resize', this.arrange); + this.arrange(); + + this.files = []; + this.draw = this.draw.bind(this); + for (var id = 0, scale; id < this.options.numFiles; id++) { + scale = Utils.randomRange(this.options.minScale, this.options.maxScale); + this.files.push(new FileIcon(this.context, scale)); + } + this.drawFrameID = window.requestAnimationFrame(this.draw); + }, + + stop(flush) { + window.removeEventListener('resize', this.arrange); + window.cancelAnimationFrame(this.drawFrameID); + delete this.drawFrameID; + }, + + bindFiles(file, dependencies) { + for (let id = 0; id < dependencies.length; id++) { + const dependency = dependencies[id]; + const distance = Utils.distance(file.x, file.y, dependency.x, dependency.y); + const alpha = 1 - distance / this.options.bondDistance; + if (alpha) { + this.context.lineWidth = 0.5; + this.context.strokeStyle = `rgba(${this.rgb[0]},${this.rgb[1]},${this.rgb[2]},${alpha})`; + this.context.beginPath(); + this.context.moveTo(file.x, file.y); + this.context.lineTo(dependency.x, dependency.y); + this.context.closePath(); + this.context.stroke(); + } + } + }, + + draw() { + let id; + this.drawFrameID = window.requestAnimationFrame(this.draw); + this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); + for (id = 0; id < this.files.length; id++) { + this.bindFiles(this.files[id], this.files); + } + for (id = 0; id < this.files.length; id++) { + this.files[id].update(); + this.files[id].draw(); + } + }, + + arrange() { + this.canvas.width = window.innerWidth; + this.canvas.height = window.innerHeight; + }, +}; + +const mesh = new FilesMesh('#background'); +mesh.start(); diff --git a/examples/relative-path-with-single-css-output/webpack.config.js b/examples/relative-path-with-single-css-output/webpack.config.js new file mode 100644 index 0000000..f10f64f --- /dev/null +++ b/examples/relative-path-with-single-css-output/webpack.config.js @@ -0,0 +1,82 @@ +const path = require('path'); +const Text = require('extract-text-webpack-plugin'); +const Html = require('html-webpack-plugin'); +const webpack = require('webpack'); +const fileLoader = require.resolve('../../src/cjs'); + +const resolve = (...args) => path.resolve(__dirname, ...args); + +const OUTPUT = { + bundle: 'dist/', + img: 'media/images/', + css: 'styles/', + js: 'scripts/', +}; + +module.exports = (argv = {}) => { + return ({ + devtool: argv.dev ? '#eval-source-map' : '#source-map', + entry: (argv.dev ? [ + 'webpack-dev-server/client?http://localhost:8080', + 'webpack/hot/only-dev-server', + ] : []).concat([ + './source/index.css', + './source/index.js', + ]), + output: { + path: resolve(OUTPUT.bundle), + filename: `${OUTPUT.js}[name].js`, + }, + devServer: { + contentBase: resolve(OUTPUT.bundle), + historyApiFallback: true, + stats: 'errors-only', + }, + module: { + rules: [ + { + test: /\.css$/, + use: Text.extract({ + publicPath: argv.dev && OUTPUT.bundle, + fallback: 'style-loader', + use: [{ + loader: 'css-loader', + query: { + minimize: false, + }, + }], + }), + }, + { + test: /\.(jpe?g|png|gif|svg)(\?v=\d+\.\d+\.\d+)?$/, + loader: fileLoader, + options: { + useRelativePath: true, + cssOutputPath: OUTPUT.css, + outputPath: OUTPUT.img, + name: '[name].[hash:7].[ext]', + }, + }, + ], + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: `'${argv.dev ? 'development' : 'production'}'`, + }, + }), + new Text({ + filename: `${OUTPUT.css}theme.css`, + disable: !!argv.dev, + allChunks: true, + }), + new Html({ + title: 'file-loader // useRelativePath', + template: './source/index.html', + }), + new webpack.HotModuleReplacementPlugin({ quiet: true }), + new webpack.NoEmitOnErrorsPlugin(), + new webpack.NamedModulesPlugin(), + ], + }); +}; diff --git a/src/helper.js b/src/helper.js new file mode 100644 index 0000000..e4eee2e --- /dev/null +++ b/src/helper.js @@ -0,0 +1,12 @@ +export const is = function is(expected, value) { + return new RegExp(`(${expected})`).test(Object.prototype.toString.call(value)); +}; + +export const parsePath = function parsePath(property, slug) { + if (!property) { + return slug; + } else if (is('Function', property)) { + return property(slug); + } + return property + slug; +}; diff --git a/src/index.js b/src/index.js index 0fa18ee..fb65d01 100644 --- a/src/index.js +++ b/src/index.js @@ -4,6 +4,7 @@ */ import path from 'path'; import loaderUtils from 'loader-utils'; +import { is, parsePath } from './helper'; export default function fileLoader(content) { if (!this.emitFile) throw new Error('emitFile is required from module system'); @@ -11,71 +12,78 @@ export default function fileLoader(content) { const query = loaderUtils.getOptions(this) || {}; const configKey = query.config || 'fileLoader'; const options = this.options[configKey] || {}; - - const config = { - publicPath: undefined, + const config = Object.assign({ + regExp: undefined, + context: undefined, useRelativePath: false, + publicPath: undefined, + cssOutputPath: '', + outputPath: '', name: '[hash].[ext]', - }; - - // options takes precedence over config - Object.keys(options).forEach((attr) => { - config[attr] = options[attr]; - }); - - // query takes precedence over config and options - Object.keys(query).forEach((attr) => { - config[attr] = query[attr]; - }); + }, options, query); - const context = config.context || this.options.context; + const context = config.context || this.options.context || process.cwd(); + const issuer = (this._module && this._module.issuer) || {}; // eslint-disable-line no-underscore-dangle let url = loaderUtils.interpolateName(this, config.name, { + regExp: config.regExp, context, content, - regExp: config.regExp, }); - let outputPath = ''; if (config.outputPath) { // support functions as outputPath to generate them dynamically - outputPath = ( - typeof config.outputPath === 'function' ? config.outputPath(url) : config.outputPath - ); + config.outputPath = parsePath(config.outputPath, url); } - const filePath = this.resourcePath; if (config.useRelativePath) { - const issuerContext = this._module && this._module.issuer - && this._module.issuer.context || context; // eslint-disable-line no-mixed-operators - const relativeUrl = issuerContext && path.relative(issuerContext, filePath).split(path.sep).join('/'); - const relativePath = relativeUrl && `${path.dirname(relativeUrl)}/`; - if (~relativePath.indexOf('../')) { // eslint-disable-line no-bitwise - outputPath = path.posix.join(outputPath, relativePath, url); - } else { - outputPath = relativePath + url; + // Only the dirname is needed in this case. + config.outputPath = config.outputPath.replace(url, ''); + + // We have access only to entry point relationships. So we work with this relations. + issuer.context = issuer.context || context; + const relation = { path: issuer.context && path.relative(issuer.context, this.resourcePath) }; + relation.path = relation.path ? path.dirname(relation.path) : config.outputPath; + + // Output path + // If the `output.dirname` is pointing to up in relation to the `config.outputPath`. + // We forced him to the webpack output path config. Even though it is empty. + const output = this.options.output || {}; + output.dirname = relation.path.replace(/^(\.\.(\/|\\))+/g, '').split(path.sep).join('/'); + if (output.dirname.indexOf(config.outputPath) !== 0) output.dirname = config.outputPath; + config.outputPath = path.join(output.dirname, url).split(path.sep).join('/'); + + // Public path + // Entry files doesn't pass through the `file-loader`. + // So we haven't access to the files context to compare with your assets context + // then we need to create and the same way, force the `relation.path` to bundled files + // on the webpack output path config folder and manually the same with CSS file. + if (output.filename && path.extname(output.filename)) { + relation.path = output.dirname; + } else if (output.path && is('String', config.cssOutputPath)) { + output.bundle = output.path.replace(this.options.context + path.sep, ''); + output.issuer = path.join(context, output.bundle, config.cssOutputPath); + output.asset = path.join(context, output.bundle, output.dirname); + relation.path = path.relative(output.issuer, output.asset); } - url = relativePath + url; + url = path.join(relation.path, url).split(path.sep).join('/'); } else if (config.outputPath) { - // support functions as outputPath to generate them dynamically - outputPath = (typeof config.outputPath === 'function' ? config.outputPath(url) : config.outputPath + url); - url = outputPath; + url = config.outputPath; } else { - outputPath = url; + config.outputPath = url; } - let publicPath = `__webpack_public_path__ + ${JSON.stringify(url)}`; - if (config.publicPath !== undefined) { + if (is('String|Function', config.publicPath)) { // support functions as publicPath to generate them dynamically - publicPath = JSON.stringify( - typeof config.publicPath === 'function' ? config.publicPath(url) : config.publicPath + url, - ); + config.publicPath = JSON.stringify(parsePath(config.publicPath, url)); + } else { + config.publicPath = `__webpack_public_path__ + ${JSON.stringify(url)}`; } if (query.emitFile === undefined || query.emitFile) { - this.emitFile(outputPath, content); + this.emitFile(config.outputPath, content); } - return `export default = ${publicPath};`; + return `export default = ${config.publicPath};`; } export const raw = true; diff --git a/test/correct-filename.test.js b/test/correct-filename.test.js index 5b90331..0541299 100644 --- a/test/correct-filename.test.js +++ b/test/correct-filename.test.js @@ -1,14 +1,25 @@ /* eslint-disable no-useless-escape, no-unused-vars */ +import queryString from 'querystring'; import fileLoader from '../src'; -const run = function run(resourcePath, query, content = new Buffer('1234')) { +function run(resourcePath, query, content = new Buffer('1234')) { let file = null; + const queryObject = queryString.parse(query); + const webpackConfig = Object.assign({}, queryObject.webpackConfig); + delete queryObject.webpackConfig; + const context = { resourcePath, query: `?${query}`, options: { context: '/this/is/the/context', + output: webpackConfig.output, + }, + _module: { + issuer: { + context: webpackConfig._moduleIssuerContext, // eslint-disable-line no-underscore-dangle + }, }, emitFile(url, content2) { expect(content2).toEqual(content); @@ -17,21 +28,27 @@ const run = function run(resourcePath, query, content = new Buffer('1234')) { }; const result = fileLoader.call(context, content); + return { file, result }; +} - return { - file, - result, - }; -}; - -function runWithOptions(resourcePath, options, content = new Buffer('1234')) { +function runWithOptions(resourcePath, config, content = new Buffer('1234')) { let file = null; + const options = Object.assign({}, config); + const webpackConfig = Object.assign({}, options.webpackConfig); + delete options.webpackConfig; + const context = { resourcePath, options: { fileLoader: options, context: '/this/is/the/context', + output: webpackConfig.output, + }, + _module: { + issuer: { + context: webpackConfig._moduleIssuerContext, // eslint-disable-line no-underscore-dangle + }, }, emitFile(url, content2) { expect(content2).toEqual(content); @@ -40,15 +57,12 @@ function runWithOptions(resourcePath, options, content = new Buffer('1234')) { }; const result = fileLoader.call(context, content); - - return { - file, - result, - }; + return { file, result }; } -const test = function test(excepted, resourcePath, query, content) { + +function test(excepted, resourcePath, query, content) { expect(run(resourcePath, query, content).file).toEqual(excepted); -}; +} describe('correct-filename', () => { it('should process defaults correctly', () => { @@ -104,17 +118,106 @@ describe('publicPath option', () => { describe('useRelativePath option', () => { it('should be supported', () => { - expect(run('/this/is/the/context/file.txt', 'useRelativePath=true').result).toEqual( - 'export default = __webpack_public_path__ + \"./81dc9bdb52d04dc20036dbd8313ed055.txt\";', + expect(runWithOptions('/this/is/the/context/file.txt', { + useRelativePath: true, + webpackConfig: { + _moduleIssuerContext: '/this/is/the/context', + output: { + filename: '[name].js', + }, + }, + }).result).toEqual( + 'export default = __webpack_public_path__ + "81dc9bdb52d04dc20036dbd8313ed055.txt";', ); - expect(run('/this/is/file.txt', 'useRelativePath=true').result).toEqual( - 'export default = __webpack_public_path__ + \"../../81dc9bdb52d04dc20036dbd8313ed055.txt\";', + expect(runWithOptions('/this/is/file.txt', { + useRelativePath: true, + webpackConfig: { + _moduleIssuerContext: '/this/is/the/context', + output: { + filename: '[name].js', + }, + }, + }).result).toEqual( + 'export default = __webpack_public_path__ + "../81dc9bdb52d04dc20036dbd8313ed055.txt";', ); - expect(run('/this/file.txt', 'context=/this/is/the/&useRelativePath=true').result).toEqual( - 'export default = __webpack_public_path__ + \"../../81dc9bdb52d04dc20036dbd8313ed055.txt\";', + expect(runWithOptions('/this/file.txt', { + useRelativePath: true, + context: '/this/is/the/', + webpackConfig: { + _moduleIssuerContext: '/this/is/the/', + output: { + filename: '[name].js', + }, + }, + }).result).toEqual( + 'export default = __webpack_public_path__ + "../81dc9bdb52d04dc20036dbd8313ed055.txt";', ); - expect(run('/this/file.txt', 'context=/&useRelativePath=true').result).toEqual( - 'export default = __webpack_public_path__ + \"this/81dc9bdb52d04dc20036dbd8313ed055.txt\";', + expect(runWithOptions('/this/file.txt', { + useRelativePath: true, + context: '/', + webpackConfig: { + _moduleIssuerContext: '/', + output: { + filename: '[name].js', + }, + }, + }).result).toEqual( + 'export default = __webpack_public_path__ + "this/81dc9bdb52d04dc20036dbd8313ed055.txt";', + ); + }); +}); + +describe('cssOutputPath option', () => { + it('should be supported', () => { + expect(runWithOptions('/this/is/the/context/dist/file.txt', { + useRelativePath: true, + cssOutputPath: 'style', + webpackConfig: { + _moduleIssuerContext: '/this/is/the/context/source', + output: { + path: '/this/is/the/context/dist', + }, + }, + }).result).toEqual( + 'export default = __webpack_public_path__ + "../dist/81dc9bdb52d04dc20036dbd8313ed055.txt";', + ); + expect(runWithOptions('/this/is/file.txt', { + useRelativePath: true, + cssOutputPath: '', + webpackConfig: { + _moduleIssuerContext: '/this/is', + output: { + path: '/this/is/the/context', + }, + }, + }).result).toEqual( + 'export default = __webpack_public_path__ + "81dc9bdb52d04dc20036dbd8313ed055.txt";', + ); + expect(runWithOptions('/this/file.txt', { + useRelativePath: true, + cssOutputPath: '/this/is/the/style', + context: '/this/is/the/', + webpackConfig: { + _moduleIssuerContext: '/this/is/the/', + output: { + path: '/this/is/the/context/dist', + }, + }, + }).result).toEqual( + 'export default = __webpack_public_path__ + "../../../../../81dc9bdb52d04dc20036dbd8313ed055.txt";', + ); + expect(runWithOptions('/this/file.txt', { + useRelativePath: true, + cssOutputPath: '/style', + context: '/', + webpackConfig: { + _moduleIssuerContext: '/', + output: { + path: '/this/is/the/context/dist', + }, + }, + }).result).toEqual( + 'export default = __webpack_public_path__ + "../this/81dc9bdb52d04dc20036dbd8313ed055.txt";', ); }); }); @@ -125,7 +228,7 @@ describe('outputPath function', () => { const options = {}; options.outputPath = outputFunc; expect(runWithOptions('/this/is/the/context/file.txt', options).result).toEqual( - 'export default = __webpack_public_path__ + \"/path/set/by/func\";', + 'export default = __webpack_public_path__ + "/path/set/by/func";', ); }); it('should be ignored if you set useRelativePath', () => { @@ -134,7 +237,7 @@ describe('outputPath function', () => { options.outputPath = outputFunc; options.useRelativePath = true; expect(runWithOptions('/this/is/the/context/file.txt', options).result).toEqual( - 'export default = __webpack_public_path__ + \"./81dc9bdb52d04dc20036dbd8313ed055.txt\";', + 'export default = __webpack_public_path__ + "81dc9bdb52d04dc20036dbd8313ed055.txt";', ); }); }); diff --git a/test/helper.test.js b/test/helper.test.js new file mode 100644 index 0000000..7c39338 --- /dev/null +++ b/test/helper.test.js @@ -0,0 +1,26 @@ +import { is, parsePath } from '../src/helper'; + +describe('is', () => { + expect(is('String|Function', '/path/to/file.txt')).toBe(true); + expect(is('String|Function', () => '/path/to/file.txt')).toBe(true); + expect(is('Function', () => '/path/to/file.txt')).toBe(true); + expect(is('String', () => '/path/to/file.txt')).toBe(false); +}); + +describe('parsePath', () => { + const outputPath = filePath => `/path/to/${filePath}`; + const publicPath = '/public/path/'; + const url = 'file.txt'; + + it('should process function value correctly', () => { + expect(parsePath(outputPath, url)).toBe(`/path/to/${url}`); + }); + + it('should process string value correctly', () => { + expect(parsePath(publicPath, url)).toBe(publicPath + url); + }); + + it('should process void correctly', () => { + expect(parsePath(undefined, url)).toBe(url); + }); +});