From a89c0f982716d1093dbe6b12de47e4e8df1543f5 Mon Sep 17 00:00:00 2001 From: Alexey Lavinsky Date: Fri, 23 Oct 2020 00:35:02 +0300 Subject: [PATCH] feat: linkType option (#634) --- README.md | 76 +++++++++++++-- src/CssLoadingRuntimeModule.js | 4 +- src/index.js | 18 +++- src/plugin-options.json | 10 ++ .../__snapshots__/linkTag-option.test.js.snap | 55 +++++++++++ .../validate-plugin-options.test.js.snap | 36 +++++++ test/cases/hmr/expected/webpack-5/main.js | 3 +- .../expected/webpack-4/main.js | 3 +- .../expected/webpack-5/main.js | 3 +- test/linkTag-option.test.js | 97 +++++++++++++++++++ test/validate-plugin-options.test.js | 4 + 11 files changed, 289 insertions(+), 20 deletions(-) create mode 100644 test/__snapshots__/linkTag-option.test.js.snap create mode 100644 test/linkTag-option.test.js diff --git a/README.md b/README.md index 9ccdc9e4..7640b6db 100644 --- a/README.md +++ b/README.md @@ -75,13 +75,14 @@ module.exports = { ### Plugin Options -| Name | Type | Default | Description | -| :-----------------------------------: | :------------------: | :------------------------------------------------------------------------------: | :------------------------------------------------------- | -| **[`filename`](#filename)** | `{String\|Function}` | `[name].css` | This option determines the name of each output CSS file | -| **[`chunkFilename`](#chunkFilename)** | `{String\|Function}` | `based on filename` | This option determines the name of non-entry chunk files | -| **[`ignoreOrder`](#ignoreOrder)** | `{Boolean}` | `false` | Remove Order Warnings | -| **[`insert`](#insert)** | `{String\|Function}` | `var head = document.getElementsByTagName("head")[0];head.appendChild(linkTag);` | Inserts `` at the given position | -| **[`attributes`](#attributes)** | `{Object}` | `{}` | Adds custom attributes to tag | +| Name | Type | Default | Description | +| :-----------------------------------: | :------------------: | :-----------------------------------: | :--------------------------------------------------------- | +| **[`filename`](#filename)** | `{String\|Function}` | `[name].css` | This option determines the name of each output CSS file | +| **[`chunkFilename`](#chunkFilename)** | `{String\|Function}` | `based on filename` | This option determines the name of non-entry chunk files | +| **[`ignoreOrder`](#ignoreOrder)** | `{Boolean}` | `false` | Remove Order Warnings | +| **[`insert`](#insert)** | `{String\|Function}` | `document.head.appendChild(linkTag);` | Inserts `` at the given position | +| **[`attributes`](#attributes)** | `{Object}` | `{}` | Adds custom attributes to tag | +| **[`linkType`](#linkType)** | `{String\|Boolean}` | `text/css` | Allows loading asynchronous chunks with a custom link type | #### `filename` @@ -114,7 +115,7 @@ See [examples](#remove-order-warnings) below for details. #### `insert` Type: `String|Function` -Default: `var head = document.getElementsByTagName("head")[0]; head.appendChild(linkTag);` +Default: `document.head.appendChild(linkTag);` By default, the `extract-css-chunks-plugin` appends styles (`` elements) to `document.head` of the current `window`. @@ -196,6 +197,65 @@ module.exports = { Note: It's only applied to dynamically loaded css chunks, if you want to modify link attributes inside html file, please using [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin) +#### `linkType` + +Type: `String|Boolean` +Default: `text/css` + +This option allows loading asynchronous chunks with a custom link type, such as . + +##### `String` + +Possible values: `text/css` + +**webpack.config.js** + +```js +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +module.exports = { + plugins: [ + new MiniCssExtractPlugin({ + linkType: 'text/css', + }), + ], + module: { + rules: [ + { + test: /\.css$/i, + use: [MiniCssExtractPlugin.loader, 'css-loader'], + }, + ], + }, +}; +``` + +##### `Boolean` + +`false` disables the link `type` attribute + +**webpack.config.js** + +```js +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +module.exports = { + plugins: [ + new MiniCssExtractPlugin({ + linkType: false, + }), + ], + module: { + rules: [ + { + test: /\.css$/i, + use: [MiniCssExtractPlugin.loader, 'css-loader'], + }, + ], + }, +}; +``` + ### Loader Options | Name | Type | Default | Description | diff --git a/src/CssLoadingRuntimeModule.js b/src/CssLoadingRuntimeModule.js index e259fc5d..be0ff24e 100644 --- a/src/CssLoadingRuntimeModule.js +++ b/src/CssLoadingRuntimeModule.js @@ -57,7 +57,9 @@ module.exports = class CssLoadingRuntimeModule extends RuntimeModule { 'var linkTag = document.createElement("link");', this.runtimeOptions.attributes, 'linkTag.rel = "stylesheet";', - 'linkTag.type = "text/css";', + this.runtimeOptions.linkType + ? `linkTag.type = ${JSON.stringify(this.runtimeOptions.linkType)};` + : '', 'linkTag.onload = resolve;', 'linkTag.onerror = function(event) {', Template.indent([ diff --git a/src/index.js b/src/index.js index b2297432..7b91502d 100644 --- a/src/index.js +++ b/src/index.js @@ -42,14 +42,17 @@ class MiniCssExtractPlugin { `var target = document.querySelector("${options.insert}");`, `target.parentNode.insertBefore(linkTag, target.nextSibling);`, ]) - : Template.asString([ - 'var head = document.getElementsByTagName("head")[0];', - 'head.appendChild(linkTag);', - ]); + : Template.asString(['document.head.appendChild(linkTag);']); const attributes = typeof options.attributes === 'object' ? options.attributes : {}; + // Todo in next major release set default to "false" + const linkType = + options.linkType === true || typeof options.linkType === 'undefined' + ? 'text/css' + : options.linkType; + this.options = Object.assign( { filename: DEFAULT_FILENAME, @@ -60,6 +63,7 @@ class MiniCssExtractPlugin { this.runtimeOptions = { insert, + linkType, }; this.runtimeOptions.attributes = Template.asString( @@ -394,7 +398,11 @@ class MiniCssExtractPlugin { 'var linkTag = document.createElement("link");', this.runtimeOptions.attributes, 'linkTag.rel = "stylesheet";', - 'linkTag.type = "text/css";', + this.runtimeOptions.linkType + ? `linkTag.type = ${JSON.stringify( + this.runtimeOptions.linkType + )};` + : '', 'linkTag.onload = resolve;', 'linkTag.onerror = function(event) {', Template.indent([ diff --git a/src/plugin-options.json b/src/plugin-options.json index 6fd30ee0..94aa6d8e 100644 --- a/src/plugin-options.json +++ b/src/plugin-options.json @@ -39,6 +39,16 @@ "attributes": { "description": "Adds custom attributes to tag (https://github.com/webpack-contrib/mini-css-extract-plugin#attributes).", "type": "object" + }, + "linkType": { + "anyOf": [ + { + "enum": ["text/css"] + }, + { + "type": "boolean" + } + ] } } } diff --git a/test/__snapshots__/linkTag-option.test.js.snap b/test/__snapshots__/linkTag-option.test.js.snap new file mode 100644 index 00000000..8a4e62d9 --- /dev/null +++ b/test/__snapshots__/linkTag-option.test.js.snap @@ -0,0 +1,55 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`linkType option should work when linkType option is "false": DOM 1`] = ` +" + style-loader test + + + +

Body

+
+ + + +" +`; + +exports[`linkType option should work when linkType option is "false": errors 1`] = `Array []`; + +exports[`linkType option should work when linkType option is "false": warnings 1`] = `Array []`; + +exports[`linkType option should work when linkType option is "text/css": DOM 1`] = ` +" + style-loader test + + + +

Body

+
+ + + +" +`; + +exports[`linkType option should work when linkType option is "text/css": errors 1`] = `Array []`; + +exports[`linkType option should work when linkType option is "text/css": warnings 1`] = `Array []`; + +exports[`linkType option should work without linkType option: DOM 1`] = ` +" + style-loader test + + + +

Body

+
+ + + +" +`; + +exports[`linkType option should work without linkType option: errors 1`] = `Array []`; + +exports[`linkType option should work without linkType option: warnings 1`] = `Array []`; diff --git a/test/__snapshots__/validate-plugin-options.test.js.snap b/test/__snapshots__/validate-plugin-options.test.js.snap index 27b8b1a6..1a6b986c 100644 --- a/test/__snapshots__/validate-plugin-options.test.js.snap +++ b/test/__snapshots__/validate-plugin-options.test.js.snap @@ -59,3 +59,39 @@ exports[`validate options should throw an error on the "insert" option with "tru * options.insert should be a string. * options.insert should be an instance of function." `; + +exports[`validate options should throw an error on the "linkType" option with "[]" value 1`] = ` +"Invalid options object. Mini CSS Extract Plugin has been initialized using an options object that does not match the API schema. + - options.linkType should be one of these: + \\"text/css\\" | boolean + Details: + * options.linkType should be \\"text/css\\". + * options.linkType should be a boolean." +`; + +exports[`validate options should throw an error on the "linkType" option with "{}" value 1`] = ` +"Invalid options object. Mini CSS Extract Plugin has been initialized using an options object that does not match the API schema. + - options.linkType should be one of these: + \\"text/css\\" | boolean + Details: + * options.linkType should be \\"text/css\\". + * options.linkType should be a boolean." +`; + +exports[`validate options should throw an error on the "linkType" option with "1" value 1`] = ` +"Invalid options object. Mini CSS Extract Plugin has been initialized using an options object that does not match the API schema. + - options.linkType should be one of these: + \\"text/css\\" | boolean + Details: + * options.linkType should be \\"text/css\\". + * options.linkType should be a boolean." +`; + +exports[`validate options should throw an error on the "linkType" option with "invalid/type" value 1`] = ` +"Invalid options object. Mini CSS Extract Plugin has been initialized using an options object that does not match the API schema. + - options.linkType should be one of these: + \\"text/css\\" | boolean + Details: + * options.linkType should be \\"text/css\\". + * options.linkType should be a boolean." +`; diff --git a/test/cases/hmr/expected/webpack-5/main.js b/test/cases/hmr/expected/webpack-5/main.js index 661f7fdb..fea4f8c6 100644 --- a/test/cases/hmr/expected/webpack-5/main.js +++ b/test/cases/hmr/expected/webpack-5/main.js @@ -838,8 +838,7 @@ module.exports = function (urlString) { /******/ }; /******/ linkTag.href = fullhref; /******/ -/******/ var head = document.getElementsByTagName("head")[0]; -/******/ head.appendChild(linkTag); +/******/ document.head.appendChild(linkTag); /******/ return linkTag; /******/ }; /******/ var findStylesheet = (href, fullhref) => { diff --git a/test/cases/insert-undefined/expected/webpack-4/main.js b/test/cases/insert-undefined/expected/webpack-4/main.js index 40e53f2a..4f296ab3 100644 --- a/test/cases/insert-undefined/expected/webpack-4/main.js +++ b/test/cases/insert-undefined/expected/webpack-4/main.js @@ -116,8 +116,7 @@ /******/ }; /******/ linkTag.href = fullhref; /******/ -/******/ var head = document.getElementsByTagName("head")[0]; -/******/ head.appendChild(linkTag); +/******/ document.head.appendChild(linkTag); /******/ }).then(function() { /******/ installedCssChunks[chunkId] = 0; /******/ })); diff --git a/test/cases/insert-undefined/expected/webpack-5/main.js b/test/cases/insert-undefined/expected/webpack-5/main.js index 318b496c..d105a4b9 100644 --- a/test/cases/insert-undefined/expected/webpack-5/main.js +++ b/test/cases/insert-undefined/expected/webpack-5/main.js @@ -173,8 +173,7 @@ /******/ }; /******/ linkTag.href = fullhref; /******/ -/******/ var head = document.getElementsByTagName("head")[0]; -/******/ head.appendChild(linkTag); +/******/ document.head.appendChild(linkTag); /******/ return linkTag; /******/ }; /******/ var findStylesheet = (href, fullhref) => { diff --git a/test/linkTag-option.test.js b/test/linkTag-option.test.js new file mode 100644 index 00000000..1ab27bda --- /dev/null +++ b/test/linkTag-option.test.js @@ -0,0 +1,97 @@ +/* eslint-env browser */ +import path from 'path'; + +import MiniCssExtractPlugin from '../src/cjs'; + +import { + compile, + getCompiler, + getErrors, + getWarnings, + runInJsDom, +} from './helpers/index'; + +describe('linkType option', () => { + it(`should work without linkType option`, async () => { + const compiler = getCompiler( + 'attributes.js', + {}, + { + output: { + publicPath: '', + path: path.resolve(__dirname, '../outputs'), + filename: '[name].bundle.js', + }, + plugins: [ + new MiniCssExtractPlugin({ + filename: '[name].css', + }), + ], + } + ); + const stats = await compile(compiler); + + runInJsDom('main.bundle.js', compiler, stats, (dom) => { + expect(dom.serialize()).toMatchSnapshot('DOM'); + }); + + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it(`should work when linkType option is "false"`, async () => { + const compiler = getCompiler( + 'attributes.js', + {}, + { + output: { + publicPath: '', + path: path.resolve(__dirname, '../outputs'), + filename: '[name].bundle.js', + }, + plugins: [ + new MiniCssExtractPlugin({ + linkType: false, + filename: '[name].css', + }), + ], + } + ); + const stats = await compile(compiler); + + runInJsDom('main.bundle.js', compiler, stats, (dom) => { + expect(dom.serialize()).toMatchSnapshot('DOM'); + }); + + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it(`should work when linkType option is "text/css"`, async () => { + const compiler = getCompiler( + 'attributes.js', + {}, + { + output: { + publicPath: '', + path: path.resolve(__dirname, '../outputs'), + filename: '[name].bundle.js', + }, + plugins: [ + new MiniCssExtractPlugin({ + linkType: 'text/css', + filename: '[name].css', + }), + ], + } + ); + const stats = await compile(compiler); + + runInJsDom('main.bundle.js', compiler, stats, (dom) => { + expect(dom.serialize()).toMatchSnapshot('DOM'); + }); + + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); +}); diff --git a/test/validate-plugin-options.test.js b/test/validate-plugin-options.test.js index 808aa9e6..dfae508b 100644 --- a/test/validate-plugin-options.test.js +++ b/test/validate-plugin-options.test.js @@ -32,6 +32,10 @@ describe('validate options', () => { success: [{}, { id: 'id' }], failure: [true], }, + linkType: { + success: [true, false, 'text/css'], + failure: [1, {}, [], 'invalid/type'], + }, unknown: { success: [], // TODO failed in next release