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 @@
+
\ 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 @@
+
\ 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',
+ }),
+ ],
+};