diff --git a/README.md b/README.md index 072cb1ff..7e89e61f 100644 --- a/README.md +++ b/README.md @@ -268,6 +268,17 @@ module.exports = { } ``` +#### Filename as function instead of string + +By using a function instead of a string, you can use chunk data to customize the filename. This is particularly useful when dealing with multiple entry points and wanting to get more control out of the filename for a given entry point/chunk. In the example below, the we'll change the filename to output the css to a different directory. + +```javascript +const miniCssExtractPlugin = new MiniCssExtractPlugin({ + filename: (chunkData) => + `${chunkData.name.replace('/js/', '/css/')}.[chunkhash:8].css` +}) +``` + #### Long Term Caching For long term caching use `filename: "[contenthash].css"`. Optionally add `[name]`. diff --git a/package-lock.json b/package-lock.json index d3a4647e..205b5be4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2907,9 +2907,9 @@ } }, "connect-history-api-fallback": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz", - "integrity": "sha1-sGhzk0vF40T+9hGhlqb6rgruAVo=", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", "dev": true }, "console-browserify": { @@ -5665,9 +5665,9 @@ } }, "follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.6.1.tgz", + "integrity": "sha512-t2JCjbzxQpWvbhts3l6SH1DKzSrx8a+SsaVf4h6bG4kOXUuPYS/kg2Lr4gQSb7eemaHqJkOThF1BGyjlUkO1GQ==", "dev": true, "requires": { "debug": "=3.1.0" @@ -6802,9 +6802,9 @@ "dev": true }, "handle-thing": { - "version": "1.2.5", - "resolved": "http://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz", - "integrity": "sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz", + "integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==", "dev": true }, "handlebars": { @@ -13345,32 +13345,75 @@ "dev": true }, "spdy": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-3.4.7.tgz", - "integrity": "sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.0.tgz", + "integrity": "sha512-ot0oEGT/PGUpzf/6uk4AWLqkq+irlqHXkrdbk51oWONh3bxQmBuljxPNl66zlRRcIJStWq0QkLUCPOPjgjvU0Q==", "dev": true, "requires": { - "debug": "^2.6.8", - "handle-thing": "^1.2.5", + "debug": "^4.1.0", + "handle-thing": "^2.0.0", "http-deceiver": "^1.2.7", - "safe-buffer": "^5.0.1", "select-hose": "^2.0.0", - "spdy-transport": "^2.0.18" + "spdy-transport": "^3.0.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } } }, "spdy-transport": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-2.1.1.tgz", - "integrity": "sha512-q7D8c148escoB3Z7ySCASadkegMmUZW8Wb/Q1u0/XBgDKMO880rLQDj8Twiew/tYi7ghemKUi/whSYOwE17f5Q==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", "dev": true, "requires": { - "debug": "^2.6.8", - "detect-node": "^2.0.3", + "debug": "^4.1.0", + "detect-node": "^2.0.4", "hpack.js": "^2.1.6", - "obuf": "^1.1.1", - "readable-stream": "^2.2.9", - "safe-buffer": "^5.0.1", - "wbuf": "^1.7.2" + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "readable-stream": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.1.1.tgz", + "integrity": "sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } } }, "split": { @@ -15907,9 +15950,9 @@ } }, "webpack-dev-server": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.1.10.tgz", - "integrity": "sha512-RqOAVjfqZJtQcB0LmrzJ5y4Jp78lv9CK0MZ1YJDTaTmedMZ9PU9FLMQNrMCfVu8hHzaVLVOJKBlGEHMN10z+ww==", + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.1.14.tgz", + "integrity": "sha512-mGXDgz5SlTxcF3hUpfC8hrQ11yhAttuUQWf1Wmb+6zo3x6rb7b9mIfuQvAPLdfDRCGRGvakBWHdHOa0I9p/EVQ==", "dev": true, "requires": { "ansi-html": "0.0.7", @@ -15931,12 +15974,14 @@ "portfinder": "^1.0.9", "schema-utils": "^1.0.0", "selfsigned": "^1.9.1", + "semver": "^5.6.0", "serve-index": "^1.7.2", "sockjs": "0.3.19", "sockjs-client": "1.3.0", - "spdy": "^3.4.1", + "spdy": "^4.0.0", "strip-ansi": "^3.0.0", "supports-color": "^5.1.0", + "url": "^0.11.0", "webpack-dev-middleware": "3.4.0", "webpack-log": "^2.0.0", "yargs": "12.0.2" @@ -16041,13 +16086,13 @@ } }, "execa": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", - "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", "dev": true, "requires": { "cross-spawn": "^6.0.0", - "get-stream": "^3.0.0", + "get-stream": "^4.0.0", "is-stream": "^1.1.0", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", @@ -16224,6 +16269,15 @@ "locate-path": "^3.0.0" } }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, "glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", @@ -16389,20 +16443,20 @@ } }, "os-locale": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.0.1.tgz", - "integrity": "sha512-7g5e7dmXPtzcP4bgsZ8ixDVqA7oWYuEz4lOSujeWyliPai4gfVDiFIcwBg3aGCPnmSGfzOKTK3ccPn0CKv3DBw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", "dev": true, "requires": { - "execa": "^0.10.0", + "execa": "^1.0.0", "lcid": "^2.0.0", "mem": "^4.0.0" } }, "p-limit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz", - "integrity": "sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz", + "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==", "dev": true, "requires": { "p-try": "^2.0.0" @@ -16432,6 +16486,12 @@ "find-up": "^3.0.0" } }, + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", + "dev": true + }, "yargs": { "version": "12.0.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.2.tgz", diff --git a/package.json b/package.json index 0d4f3dac..7d6db645 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "webpack": "^4.14.0", "webpack-cli": "^2.0.13", "webpack-defaults": "^2.3.0", - "webpack-dev-server": "^3.1.1" + "webpack-dev-server": "^3.1.14" }, "keywords": [ "webpack" diff --git a/src/index.js b/src/index.js index 3e2a9001..6bdecf1d 100644 --- a/src/index.js +++ b/src/index.js @@ -14,6 +14,8 @@ const pluginName = 'mini-css-extract-plugin'; const REGEXP_CHUNKHASH = /\[chunkhash(?::(\d+))?\]/i; const REGEXP_CONTENTHASH = /\[contenthash(?::(\d+))?\]/i; const REGEXP_NAME = /\[name\]/i; +const REGEXP_PLACEHOLDERS = /\[(name|id|chunkhash)\]/g; +const DEFAULT_FILENAME = '[name].css'; class CssDependency extends webpack.Dependency { constructor( @@ -112,24 +114,25 @@ class MiniCssExtractPlugin { constructor(options) { this.options = Object.assign( { - filename: '[name].css', + filename: DEFAULT_FILENAME, }, options ); if (!this.options.chunkFilename) { const { filename } = this.options; - const hasName = filename.includes('[name]'); - const hasId = filename.includes('[id]'); - const hasChunkHash = filename.includes('[chunkhash]'); // Anything changing depending on chunk is fine - if (hasChunkHash || hasName || hasId) { - this.options.chunkFilename = filename; + if (typeof filename === 'string') { + if (REGEXP_PLACEHOLDERS.test(filename)) { + this.options.chunkFilename = filename; + } else { + // Elsewise prefix '[id].' in front of the basename to make it changing + this.options.chunkFilename = filename.replace( + /(^|\/)([^/]*(?:\?|$))/, + '$1[id].$2' + ); + } } else { - // Elsewise prefix '[id].' in front of the basename to make it changing - this.options.chunkFilename = filename.replace( - /(^|\/)([^/]*(?:\?|$))/, - '$1[id].$2' - ); + this.options.chunkFilename = `[id].${DEFAULT_FILENAME}`; } } } @@ -170,6 +173,10 @@ class MiniCssExtractPlugin { const renderedModules = Array.from(chunk.modulesIterable).filter( (module) => module.type === MODULE_TYPE ); + const { filename } = this.options; + const filenameTemplate = + typeof filename === 'function' ? filename(chunk) : filename; + if (renderedModules.length > 0) { result.push({ render: () => @@ -179,7 +186,7 @@ class MiniCssExtractPlugin { renderedModules, compilation.runtimeTemplate.requestShortener ), - filenameTemplate: this.options.filename, + filenameTemplate, pathOptions: { chunk, contentHashType: MODULE_TYPE, diff --git a/test/TestCases.test.js b/test/TestCases.test.js index ee4cd52c..c0f16be3 100644 --- a/test/TestCases.test.js +++ b/test/TestCases.test.js @@ -60,16 +60,16 @@ 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' + + for (const file of walkSync(expectedDirectory)) { + const actualFilePath = file.replace( + expectedDirectory, + path.join(outputDirectory, directory) ); - expect(actualContent).toEqual(content); + const expectedContent = fs.readFileSync(file, 'utf-8'); + const actualContent = fs.readFileSync(actualFilePath, 'utf-8'); + + expect(actualContent).toEqual(expectedContent); } done(); }); @@ -77,3 +77,20 @@ describe('TestCases', () => { } } }); + +/** + * Synchronously traverse directory of files + * @param {String} dir + * @returns {String} path to file or directory + */ +function* walkSync(dir) { + for (const file of fs.readdirSync(dir)) { + const pathToFile = path.join(dir, file); + const isDirectory = fs.statSync(pathToFile).isDirectory(); + if (isDirectory) { + yield* walkSync(pathToFile); + } else { + yield pathToFile; + } + } +} diff --git a/test/cases/composes-async/expected/1.css b/test/cases/composes-async/expected/1.1.css similarity index 100% rename from test/cases/composes-async/expected/1.css rename to test/cases/composes-async/expected/1.1.css diff --git a/test/cases/composes-async/expected/2.css b/test/cases/composes-async/expected/2.2.css similarity index 100% rename from test/cases/composes-async/expected/2.css rename to test/cases/composes-async/expected/2.2.css diff --git a/test/cases/filename/expected/demo/css/main.css b/test/cases/filename/expected/demo/css/main.css new file mode 100644 index 00000000..d2743ef5 --- /dev/null +++ b/test/cases/filename/expected/demo/css/main.css @@ -0,0 +1,2 @@ +body { background: purple; } + diff --git a/test/cases/filename/index.js b/test/cases/filename/index.js new file mode 100644 index 00000000..aa3357bf --- /dev/null +++ b/test/cases/filename/index.js @@ -0,0 +1 @@ +import './style.css'; diff --git a/test/cases/filename/style.css b/test/cases/filename/style.css new file mode 100644 index 00000000..484aaf18 --- /dev/null +++ b/test/cases/filename/style.css @@ -0,0 +1 @@ +body { background: purple; } diff --git a/test/cases/filename/webpack.config.js b/test/cases/filename/webpack.config.js new file mode 100644 index 00000000..7f572970 --- /dev/null +++ b/test/cases/filename/webpack.config.js @@ -0,0 +1,24 @@ +const Self = require('../../../'); + +module.exports = { + entry: { + 'demo/js/main': './index.js', + }, + module: { + rules: [ + { + test: /\.css$/, + use: [Self.loader, 'css-loader'], + }, + ], + }, + output: { + filename: '[name].js', + }, + plugins: [ + new Self({ + filename: ({ name }) => + `${name.replace('/js/', '/css/')}.css`, + }), + ], +}; diff --git a/test/cases/multiple-entry/expected/async-one.css b/test/cases/multiple-entry/expected/2.async-one.css similarity index 100% rename from test/cases/multiple-entry/expected/async-one.css rename to test/cases/multiple-entry/expected/2.async-one.css diff --git a/test/cases/multiple-entry/expected/async-two.css b/test/cases/multiple-entry/expected/3.async-two.css similarity index 100% rename from test/cases/multiple-entry/expected/async-two.css rename to test/cases/multiple-entry/expected/3.async-two.css diff --git a/test/cases/shared-import/expected/1.css b/test/cases/shared-import/expected/1.1.css similarity index 100% rename from test/cases/shared-import/expected/1.css rename to test/cases/shared-import/expected/1.1.css diff --git a/test/cases/simple-async-load-css-fallback/expected/async-one.css b/test/cases/simple-async-load-css-fallback/expected/1.async-one.css similarity index 100% rename from test/cases/simple-async-load-css-fallback/expected/async-one.css rename to test/cases/simple-async-load-css-fallback/expected/1.async-one.css diff --git a/test/cases/simple-async-load-css-fallback/expected/async-two.css b/test/cases/simple-async-load-css-fallback/expected/2.async-two.css similarity index 100% rename from test/cases/simple-async-load-css-fallback/expected/async-two.css rename to test/cases/simple-async-load-css-fallback/expected/2.async-two.css diff --git a/test/cases/simple-async/expected/1.css b/test/cases/simple-async/expected/1.1.css similarity index 100% rename from test/cases/simple-async/expected/1.css rename to test/cases/simple-async/expected/1.1.css diff --git a/test/cases/simple-async/expected/2.css b/test/cases/simple-async/expected/2.2.css similarity index 100% rename from test/cases/simple-async/expected/2.css rename to test/cases/simple-async/expected/2.2.css diff --git a/test/cases/split-chunks-single/expected/styles.css b/test/cases/split-chunks-single/expected/1.styles.css similarity index 100% rename from test/cases/split-chunks-single/expected/styles.css rename to test/cases/split-chunks-single/expected/1.styles.css