diff --git a/README.md b/README.md index beb5620a..72c21d77 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,13 @@ npm install --save-dev mini-css-extract-plugin ### Configuration +#### `publicPath` + +Type: `String|Function` +Default: the `publicPath` in `webpackOptions.output` + +Specifies a custom public path for the target file(s). + #### Minimal example **webpack.config.js** @@ -74,6 +81,45 @@ module.exports = { } ``` +#### `publicPath` function example + +**webpack.config.js** + +```js +const MiniCssExtractPlugin = require("mini-css-extract-plugin"); +module.exports = { + plugins: [ + new MiniCssExtractPlugin({ + // Options similar to the same options in webpackOptions.output + // both options are optional + filename: "[name].css", + chunkFilename: "[id].css" + }) + ], + module: { + rules: [ + { + test: /\.css$/, + use: [ + { + loader: MiniCssExtractPlugin.loader, + options: { + publicPath: (resourcePath, context) => { + // publicPath is the relative path of the resource to the context + // e.g. for ./css/admin/main.css the publicPath will be ../../ + // while for ./css/main.css the publicPath will be ../ + return path.relative(path.dirname(resourcePath), context) + '/' + }, + } + }, + "css-loader" + ] + } + ] + } +} +``` + #### Advanced configuration example This plugin should be used only on `production` builds without `style-loader` in the loaders chain, especially if you want to have HMR in `development`. diff --git a/src/loader.js b/src/loader.js index 9801d590..5eafa7ab 100644 --- a/src/loader.js +++ b/src/loader.js @@ -6,6 +6,9 @@ import NodeTargetPlugin from 'webpack/lib/node/NodeTargetPlugin'; import LibraryTemplatePlugin from 'webpack/lib/LibraryTemplatePlugin'; import SingleEntryPlugin from 'webpack/lib/SingleEntryPlugin'; import LimitChunkCountPlugin from 'webpack/lib/optimize/LimitChunkCountPlugin'; +import validateOptions from 'schema-utils'; + +import schema from './options.json'; const MODULE_TYPE = 'css/mini-extract'; const pluginName = 'mini-css-extract-plugin'; @@ -29,12 +32,19 @@ const findModuleById = (modules, id) => { export function pitch(request) { const query = loaderUtils.getOptions(this) || {}; + + validateOptions(schema, query, 'Mini CSS Extract Plugin Loader'); + const loaders = this.loaders.slice(this.loaderIndex + 1); this.addDependency(this.resourcePath); const childFilename = '*'; // eslint-disable-line no-path-concat const publicPath = typeof query.publicPath === 'string' - ? query.publicPath + ? query.publicPath.endsWith('/') + ? query.publicPath + : `${query.publicPath}/` + : typeof query.publicPath === 'function' + ? query.publicPath(this.resourcePath, this.rootContext) : this._compilation.outputOptions.publicPath; const outputOptions = { filename: childFilename, diff --git a/src/options.json b/src/options.json new file mode 100644 index 00000000..1c6eef18 --- /dev/null +++ b/src/options.json @@ -0,0 +1,19 @@ +{ + "additionalProperties": true, + "properties": { + "publicPath": { + "anyOf": [ + { + "type": "string" + }, + { + "instanceof": "Function" + } + ] + } + }, + "errorMessages": { + "publicPath": "should be {String} or {Function} (https://github.com/webpack-contrib/mini-css-extract-plugin#publicpath)" + }, + "type": "object" + } diff --git a/test/TestCases.test.js b/test/TestCases.test.js index ee4cd52c..10a49bf0 100644 --- a/test/TestCases.test.js +++ b/test/TestCases.test.js @@ -59,18 +59,33 @@ describe('TestCases', () => { ); return; } - const expectedDirectory = path.resolve(directoryForCase, 'expected'); - for (const file of fs.readdirSync(expectedDirectory)) { - const content = fs.readFileSync( - path.resolve(expectedDirectory, file), - 'utf-8' - ); - const actualContent = fs.readFileSync( - path.resolve(outputDirectoryForCase, file), - 'utf-8' - ); - expect(actualContent).toEqual(content); + + function compareDirectory(actual, expected) { + for (const file of fs.readdirSync(expected, { + withFileTypes: true, + })) { + if (file.isFile()) { + const content = fs.readFileSync( + path.resolve(expected, file.name), + 'utf-8' + ); + const actualContent = fs.readFileSync( + path.resolve(actual, file.name), + 'utf-8' + ); + expect(actualContent).toEqual(content); + } else if (file.isDirectory()) { + compareDirectory( + path.resolve(actual, file.name), + path.resolve(expected, file.name) + ); + } + } } + + const expectedDirectory = path.resolve(directoryForCase, 'expected'); + compareDirectory(outputDirectoryForCase, expectedDirectory); + done(); }); }, 10000); diff --git a/test/cases/publicpath-function/expected/nested/again/style.css b/test/cases/publicpath-function/expected/nested/again/style.css new file mode 100644 index 00000000..7fbc7c53 --- /dev/null +++ b/test/cases/publicpath-function/expected/nested/again/style.css @@ -0,0 +1,2 @@ +body { background: green; background-image: url(../../cd0bb358c45b584743d8ce4991777c42.svg); } + diff --git a/test/cases/publicpath-function/expected/nested/style.css b/test/cases/publicpath-function/expected/nested/style.css new file mode 100644 index 00000000..aff4e2be --- /dev/null +++ b/test/cases/publicpath-function/expected/nested/style.css @@ -0,0 +1,2 @@ +body { background: red; background-image: url(../cd0bb358c45b584743d8ce4991777c42.svg); } + diff --git a/test/cases/publicpath-function/nested/again/style.css b/test/cases/publicpath-function/nested/again/style.css new file mode 100644 index 00000000..c21a33c8 --- /dev/null +++ b/test/cases/publicpath-function/nested/again/style.css @@ -0,0 +1 @@ +body { background: green; background-image: url(../../react.svg); } diff --git a/test/cases/publicpath-function/nested/style.css b/test/cases/publicpath-function/nested/style.css new file mode 100644 index 00000000..c6261084 --- /dev/null +++ b/test/cases/publicpath-function/nested/style.css @@ -0,0 +1 @@ +body { background: red; background-image: url(../react.svg); } diff --git a/test/cases/publicpath-function/react.svg b/test/cases/publicpath-function/react.svg new file mode 100644 index 00000000..5b3b22a4 --- /dev/null +++ b/test/cases/publicpath-function/react.svg @@ -0,0 +1 @@ +logo-on-dark-bg \ No newline at end of file diff --git a/test/cases/publicpath-function/webpack.config.js b/test/cases/publicpath-function/webpack.config.js new file mode 100644 index 00000000..e7154f5e --- /dev/null +++ b/test/cases/publicpath-function/webpack.config.js @@ -0,0 +1,41 @@ +const Self = require('../../../'); +const path = require('path') + +module.exports = { + entry: { + // Specific CSS entry point, with output to a nested folder + 'nested/style': './nested/style.css', + // Note that relative nesting of output is the same as that of the input + 'nested/again/style': './nested/again/style.css', + }, + module: { + rules: [ + { + test: /\.css$/, + use: [ + { + loader: Self.loader, + options: { + // Compute publicPath relative to the CSS output + publicPath: (resourcePath, context) => path.relative(path.dirname(resourcePath), context) + '/', + } + }, + 'css-loader', + ], + }, { + test: /\.(svg|png)$/, + use: [{ + loader: 'file-loader', + options: { + filename: '[name].[ext]' + } + }] + } + ], + }, + plugins: [ + new Self({ + filename: '[name].css', + }), + ], +}; diff --git a/test/cases/publicpath-trailing-slash/expected/main.css b/test/cases/publicpath-trailing-slash/expected/main.css new file mode 100644 index 00000000..6073c14a --- /dev/null +++ b/test/cases/publicpath-trailing-slash/expected/main.css @@ -0,0 +1,2 @@ +body { background: red; background-image: url(/static/img/cd0bb358c45b584743d8ce4991777c42.svg); } + diff --git a/test/cases/publicpath-trailing-slash/index.js b/test/cases/publicpath-trailing-slash/index.js new file mode 100644 index 00000000..aa3357bf --- /dev/null +++ b/test/cases/publicpath-trailing-slash/index.js @@ -0,0 +1 @@ +import './style.css'; diff --git a/test/cases/publicpath-trailing-slash/react.svg b/test/cases/publicpath-trailing-slash/react.svg new file mode 100644 index 00000000..5b3b22a4 --- /dev/null +++ b/test/cases/publicpath-trailing-slash/react.svg @@ -0,0 +1 @@ +logo-on-dark-bg \ No newline at end of file diff --git a/test/cases/publicpath-trailing-slash/style.css b/test/cases/publicpath-trailing-slash/style.css new file mode 100644 index 00000000..edcbc24a --- /dev/null +++ b/test/cases/publicpath-trailing-slash/style.css @@ -0,0 +1 @@ +body { background: red; background-image: url(./react.svg); } diff --git a/test/cases/publicpath-trailing-slash/webpack.config.js b/test/cases/publicpath-trailing-slash/webpack.config.js new file mode 100644 index 00000000..32180f0d --- /dev/null +++ b/test/cases/publicpath-trailing-slash/webpack.config.js @@ -0,0 +1,34 @@ +const Self = require('../../../'); + +module.exports = { + entry: './index.js', + module: { + rules: [ + { + test: /\.css$/, + use: [ + { + loader: Self.loader, + options: { + publicPath: '/static/img' + } + }, + 'css-loader', + ], + }, { + test: /\.(svg|png)$/, + use: [{ + loader: 'file-loader', + options: { + filename: '[name].[ext]' + } + }] + } + ], + }, + plugins: [ + new Self({ + filename: '[name].css', + }), + ], +};