Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

publicPath can now be a function #373

Merged
merged 7 commits into from Apr 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
46 changes: 46 additions & 0 deletions README.md
Expand Up @@ -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**
Expand Down Expand Up @@ -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`.
Expand Down
12 changes: 11 additions & 1 deletion src/loader.js
Expand Up @@ -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';
Expand All @@ -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)
karlvr marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need here url as in file-loader because file-loader for other purpose (answer on above question)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@evilebottnawi in file-loader it's because file-loader can choose to output the file itself? Doesn't the plugin do exactly that?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@karlvr @evilebottnawi Just wanted to let you know that this introduces a breaking change in the case when the publicPath is set to an empty string meaning that resources are located in the same directory as the css file, e.g. dist/app.css, dist/aResource.png. With the introduced change the publicPath is transformed to a / and so the resource path becomes /aResource.png which is clearly not desired.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@evilebottnawi @rmja

That change was to follow the same behaviour in https://github.com/webpack-contrib/file-loader/blob/master/src/index.js#L37

I'm really sorry about this regression! @evilebottnawi shall we introduce a special case for an empty string here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@karlvr I have created #384

: this._compilation.outputOptions.publicPath;
const outputOptions = {
filename: childFilename,
Expand Down
19 changes: 19 additions & 0 deletions 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"
}
37 changes: 26 additions & 11 deletions test/TestCases.test.js
Expand Up @@ -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);
Expand Down
@@ -0,0 +1,2 @@
body { background: green; background-image: url(../../cd0bb358c45b584743d8ce4991777c42.svg); }

2 changes: 2 additions & 0 deletions test/cases/publicpath-function/expected/nested/style.css
@@ -0,0 +1,2 @@
body { background: red; background-image: url(../cd0bb358c45b584743d8ce4991777c42.svg); }

1 change: 1 addition & 0 deletions test/cases/publicpath-function/nested/again/style.css
@@ -0,0 +1 @@
body { background: green; background-image: url(../../react.svg); }
1 change: 1 addition & 0 deletions test/cases/publicpath-function/nested/style.css
@@ -0,0 +1 @@
body { background: red; background-image: url(../react.svg); }
1 change: 1 addition & 0 deletions test/cases/publicpath-function/react.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions 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',
}),
],
};
2 changes: 2 additions & 0 deletions test/cases/publicpath-trailing-slash/expected/main.css
@@ -0,0 +1,2 @@
body { background: red; background-image: url(/static/img/cd0bb358c45b584743d8ce4991777c42.svg); }

1 change: 1 addition & 0 deletions test/cases/publicpath-trailing-slash/index.js
@@ -0,0 +1 @@
import './style.css';
1 change: 1 addition & 0 deletions test/cases/publicpath-trailing-slash/react.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions test/cases/publicpath-trailing-slash/style.css
@@ -0,0 +1 @@
body { background: red; background-image: url(./react.svg); }
34 changes: 34 additions & 0 deletions 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',
}),
],
};