diff --git a/CHANGELOG.md b/CHANGELOG.md index 11b99f11..9fbfdb53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [3.1.0](https://github.com/webpack-contrib/style-loader/compare/v3.0.0...v3.1.0) (2021-07-12) + + +### Features + +* allow to specify the `insert` option from file, we strongly recommend do it, using the `insert` option from file will reduce your bundle size, [example](https://github.com/webpack-contrib/style-loader#absolute-path-to-function) ([#521](https://github.com/webpack-contrib/style-loader/issues/521)) ([56fc8f0](https://github.com/webpack-contrib/style-loader/commit/56fc8f021c69407e4ad03a5d345c614b04789389)) +* allow to specify the `styleTagTransform` option from file, we strongly recommend do it, using the `styleTagTransform` option from file will reduce your bundle size, [example](https://github.com/webpack-contrib/style-loader#string-1) + + +### Bug Fixes + +* reduce runtime ([#519](https://github.com/webpack-contrib/style-loader/issues/519)) ([8a26186](https://github.com/webpack-contrib/style-loader/commit/8a26186c364b45028fb6baeb4a05365c4d3526e2)) +* reduce runtime when you use custom options ([#520](https://github.com/webpack-contrib/style-loader/issues/520)) ([21c80c8](https://github.com/webpack-contrib/style-loader/commit/21c80c8c2f2ca751124f26f5984195e20f2ac665)) + ## [3.0.0](https://github.com/webpack-contrib/style-loader/compare/v2.0.0...v3.0.0) (2021-06-24) ### ⚠ BREAKING CHANGES diff --git a/README.md b/README.md index 4bf21e3c..f7c40dc6 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ module.exports = { | [**`injectType`**](#injecttype) | `{String}` | `styleTag` | Allows to setup how styles will be injected into the DOM | | [**`attributes`**](#attributes) | `{Object}` | `{}` | Adds custom attributes to tag | | [**`insert`**](#insert) | `{String\|Function}` | `head` | Inserts tag at the given position into the DOM | -| [**`styleTagTransform`**](#styleTagTransform) | `{Function}` | `undefined` | Transform tag and css when insert 'style' tag into the DOM | +| [**`styleTagTransform`**](#styleTagTransform) | `{String\|Function}` | `undefined` | Transform tag and css when insert 'style' tag into the DOM | | [**`base`**](#base) | `{Number}` | `true` | Sets module ID base (DLLPlugin) | | [**`esModule`**](#esmodule) | `{Boolean}` | `true` | Use ES modules syntax | @@ -433,6 +433,8 @@ If you target an [iframe](https://developer.mozilla.org/en-US/docs/Web/API/HTMLI #### `String` +##### `Selector` + Allows to setup custom [query selector](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector) where styles inject into the DOM. **webpack.config.js** @@ -458,6 +460,36 @@ module.exports = { }; ``` +##### `Absolute path to function` + +Allows to setup absolute path to custom function that allows to override default behavior and insert styles at any position. + +> ⚠ Do not forget that this code will be used in the browser and not all browsers support latest ECMA features like `let`, `const`, `arrow function expression` and etc. We recommend using [`babel-loader`](https://webpack.js.org/loaders/babel-loader/) for support latest ECMA features. +> ⚠ Do not forget that some DOM methods may not be available in older browsers, we recommended use only [DOM core level 2 properties](https://caniuse.com/#search=DOM%20Core), but it is depends what browsers you want to support + +**webpack.config.js** + +```js +module.exports = { + module: { + rules: [ + { + test: /\.css$/i, + use: [ + { + loader: "style-loader", + options: { + insert: require.resolve("modulePath"), + }, + }, + "css-loader", + ], + }, + ], + }, +}; +``` + A new ` + style-loader test + + + +

Body

+
+ + + +" +`; + +exports[`"insert" option should insert styles into "head" top when the "injectType" option is "lazySingletonStyleTag" and insert is object: errors 1`] = `Array []`; + +exports[`"insert" option should insert styles into "head" top when the "injectType" option is "lazySingletonStyleTag" and insert is object: warnings 1`] = `Array []`; + exports[`"insert" option should insert styles into "head" top when the "injectType" option is "lazySingletonStyleTag": DOM 1`] = ` " + style-loader test + + + +

Body

+
+ + + +" +`; + +exports[`"insert" option should insert styles into "head" top when the "injectType" option is "lazyStyleTag" and insert is object: errors 1`] = `Array []`; + +exports[`"insert" option should insert styles into "head" top when the "injectType" option is "lazyStyleTag" and insert is object: warnings 1`] = `Array []`; + exports[`"insert" option should insert styles into "head" top when the "injectType" option is "lazyStyleTag": DOM 1`] = ` " + + +

Body

+
+ + + +" +`; + +exports[`"insert" option should insert styles into "head" top when the "injectType" option is "linkTag" and insert is object: errors 1`] = `Array []`; + +exports[`"insert" option should insert styles into "head" top when the "injectType" option is "linkTag" and insert is object: warnings 1`] = `Array []`; + exports[`"insert" option should insert styles into "head" top when the "injectType" option is "linkTag": DOM 1`] = ` " style-loader test @@ -408,6 +474,30 @@ exports[`"insert" option should insert styles into "head" top when the "injectTy exports[`"insert" option should insert styles into "head" top when the "injectType" option is "linkTag": warnings 1`] = `Array []`; +exports[`"insert" option should insert styles into "head" top when the "injectType" option is "singletonStyleTag" and insert is object: DOM 1`] = ` +" + style-loader test + + + +

Body

+
+ + + +" +`; + +exports[`"insert" option should insert styles into "head" top when the "injectType" option is "singletonStyleTag" and insert is object: errors 1`] = `Array []`; + +exports[`"insert" option should insert styles into "head" top when the "injectType" option is "singletonStyleTag" and insert is object: warnings 1`] = `Array []`; + exports[`"insert" option should insert styles into "head" top when the "injectType" option is "singletonStyleTag": DOM 1`] = ` " + style-loader test + + + +

Body

+
+ + + +" +`; + +exports[`"insert" option should insert styles into "head" top when the "injectType" option is "styleTag" and insert is object: errors 1`] = `Array []`; + +exports[`"insert" option should insert styles into "head" top when the "injectType" option is "styleTag" and insert is object: warnings 1`] = `Array []`; + exports[`"insert" option should insert styles into "head" top when the "injectType" option is "styleTag": DOM 1`] = ` " + + +

Body

+
+ + + +" +`; + +exports[`"styleTagTransform" option should work when the "styleTagTransform" option is path to module and injectType lazyStyleTag: errors 1`] = `Array []`; + +exports[`"styleTagTransform" option should work when the "styleTagTransform" option is path to module and injectType lazyStyleTag: warnings 1`] = `Array []`; + exports[`"styleTagTransform" option should work when the "styleTagTransform" option is specify and injectType lazyStyleTag: DOM 1`] = ` " style-loader test diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap index f8b48aee..6bd14287 100644 --- a/test/__snapshots__/validate-options.test.js.snap +++ b/test/__snapshots__/validate-options.test.js.snap @@ -32,20 +32,22 @@ exports[`validate options should throw an error on the "insert" option with "tru exports[`validate options should throw an error on the "styleTagTransform" option with "[]" value 1`] = ` "Invalid options object. Style Loader has been initialized using an options object that does not match the API schema. - - options.styleTagTransform should be an instance of function. - -> Transform tag and css when insert 'style' tag into the DOM" + - options.styleTagTransform should be one of these: + string | function + -> Transform tag and css when insert 'style' tag into the DOM + Details: + * options.styleTagTransform should be a string. + * options.styleTagTransform should be an instance of function." `; exports[`validate options should throw an error on the "styleTagTransform" option with "true" value 1`] = ` "Invalid options object. Style Loader has been initialized using an options object that does not match the API schema. - - options.styleTagTransform should be an instance of function. - -> Transform tag and css when insert 'style' tag into the DOM" -`; - -exports[`validate options should throw an error on the "styleTagTransform" option with "true" value 2`] = ` -"Invalid options object. Style Loader has been initialized using an options object that does not match the API schema. - - options.styleTagTransform should be an instance of function. - -> Transform tag and css when insert 'style' tag into the DOM" + - options.styleTagTransform should be one of these: + string | function + -> Transform tag and css when insert 'style' tag into the DOM + Details: + * options.styleTagTransform should be a string. + * options.styleTagTransform should be an instance of function." `; exports[`validate options should throw an error on the "unknown" option with "/test/" value 1`] = ` diff --git a/test/fixtures/insertFn.js b/test/fixtures/insertFn.js new file mode 100644 index 00000000..1b5e7db8 --- /dev/null +++ b/test/fixtures/insertFn.js @@ -0,0 +1,19 @@ +function insert (element) { + const parent = document.querySelector("head"); + const lastInsertedElement = + // eslint-disable-next-line no-underscore-dangle + window._lastElementInsertedByStyleLoader; + + if (!lastInsertedElement) { + parent.insertBefore(element, parent.firstChild); + } else if (lastInsertedElement.nextSibling) { + parent.insertBefore(element, lastInsertedElement.nextSibling); + } else { + parent.appendChild(element); + } + + // eslint-disable-next-line no-underscore-dangle + window._lastElementInsertedByStyleLoader = element; +}; + +module.exports = insert; diff --git a/test/fixtures/styleTagTransform.js b/test/fixtures/styleTagTransform.js new file mode 100644 index 00000000..8ebce4e4 --- /dev/null +++ b/test/fixtures/styleTagTransform.js @@ -0,0 +1,8 @@ +function styleTagTransform(css, style) { + // eslint-disable-next-line no-param-reassign + style.innerHTML = `${css}.modify{}\n`; + + document.head.appendChild(style); +} + +module.exports = styleTagTransform; diff --git a/test/insert-option.test.js b/test/insert-option.test.js index d92d4e0b..30247829 100644 --- a/test/insert-option.test.js +++ b/test/insert-option.test.js @@ -136,6 +136,24 @@ describe('"insert" option', () => { expect(getErrors(stats)).toMatchSnapshot("errors"); }); + it(`should insert styles into "head" top when the "injectType" option is "${injectType}" and insert is object`, async () => { + expect.assertions(3); + + const entry = getEntryByInjectType("simple.js", injectType); + const compiler = getCompiler(entry, { + injectType, + insert: require.resolve("./fixtures/insertFn.js"), + }); + 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 insert styles into before "#existing-style" id when the "injectType" option is "${injectType}"`, async () => { expect.assertions(3); diff --git a/test/manual/src/index.js b/test/manual/src/index.js index d1030b66..b65b4a98 100644 --- a/test/manual/src/index.js +++ b/test/manual/src/index.js @@ -1,3 +1,242 @@ /* eslint-env browser */ /* eslint-disable no-console */ import "./style.css"; +import "./other-style.scss"; +import component from "./component.module.css"; +import styleLazy from "./style.lazy.css"; +import useUnse from "./use-unuse.lazy.css"; +import otherStyleLazy from "./other-style.lazy.scss"; +import componentLazy from "./component.lazy.module.css"; +import "./style.link.css"; +import "./order.css"; +import "./nested.css"; +import "./nested/style.css"; +import "./custom-square"; +import one from "./modules/one.module.css"; +import two from "./modules/two.module.css"; +import toolbar from "./modules/toolbar.module.css"; +import page from "./modules/page.module.css"; +import toogle from "./toogle.lazy.css"; +import { + namedExportRed, + namedExportGreen, + namedExportBlue, + namedExportBackground, +} from "./style.named-export.module.css"; +import api2, { + namedExportLazyRed, + namedExportLazyGreen, + namedExportLazyBlue, + namedExportLazyBackground, +} from "./style.named-export.lazy.module.css"; + +console.log("___LOCALS___"); +console.log(component); + +console.log("___LOCALS_LAZY___"); +console.log(componentLazy); + +styleLazy.use(); +otherStyleLazy.use(); + +const articleElement1 = document.createElement("article"); +const h3Element = document.createElement("h3"); +const h3TextNode = document.createTextNode("CSS modules"); + +const divElement1 = document.createElement("div"); +const divElement1Content = document.createTextNode("Red"); + +divElement1.className = component["module-red"]; +divElement1.appendChild(divElement1Content); + +const divElement2 = document.createElement("div"); +const divElement2Content = document.createTextNode("Green"); + +divElement2.className = component["module-green"]; +divElement2.appendChild(divElement2Content); + +const divElement3 = document.createElement("div"); +const divElement3Content = document.createTextNode("Blue"); + +divElement3.className = component["module-blue"]; +divElement3.appendChild(divElement3Content); + +const divElement4 = document.createElement("div"); + +divElement4.className = component["module-background"]; + +h3Element.appendChild(h3TextNode); +articleElement1.appendChild(h3Element); +articleElement1.appendChild(divElement1); +articleElement1.appendChild(divElement2); +articleElement1.appendChild(divElement3); +articleElement1.appendChild(divElement4); + +document.querySelectorAll("section")[0].appendChild(articleElement1); + +componentLazy.use(); + +const articleElement2 = document.createElement("article"); +const h3Element2 = document.createElement("h3"); +const h3TextNode2 = document.createTextNode("CSS modules"); + +const divElement5 = document.createElement("div"); +const divElement5Content = document.createTextNode("Red"); + +divElement5.className = componentLazy.locals["module-red"]; +divElement5.appendChild(divElement5Content); + +const divElement6 = document.createElement("div"); +const divElement6Content = document.createTextNode("Green"); + +divElement6.className = componentLazy.locals["module-green"]; +divElement6.appendChild(divElement6Content); + +const divElement7 = document.createElement("div"); +const divElement7Content = document.createTextNode("Blue"); + +divElement7.className = componentLazy.locals["module-blue"]; +divElement7.appendChild(divElement7Content); + +const divElement8 = document.createElement("div"); + +divElement8.className = componentLazy.locals["module-background"]; + +h3Element2.appendChild(h3TextNode2); +articleElement2.appendChild(h3Element2); +articleElement2.appendChild(divElement5); +articleElement2.appendChild(divElement6); +articleElement2.appendChild(divElement7); +articleElement2.appendChild(divElement8); + +document.querySelectorAll("section")[1].appendChild(articleElement2); + +const api = useUnse.use(); + +setTimeout(() => { + api.unuse(); +}, 6000); + +const selector1 = document.querySelector(".selector1"); +selector1.className = one.selector1; +const selector2 = document.querySelector(".selector2"); +selector2.className = two.selector2; +const toolbar1 = document.querySelector(".toolbar"); +toolbar1.className = toolbar.toolbar; +const common1 = document.querySelector(".common"); +common1.className = toolbar.common; +const pageBtn = document.querySelector(".page-btn"); +pageBtn.className = page["page-btn"]; + +const button = document.createElement("button"); + +button.innerText = "Toggle CSS"; + +let used = false; + +button.addEventListener("click", () => { + if (!used) { + console.log("toggle on"); + toogle.use(); + + used = true; + } else { + console.log("toggle off"); + + toogle.unuse(); + + used = false; + } +}); + +const toggleSection = document.getElementById("toggle-section"); + +toggleSection.appendChild(button); + +console.log("___NAMED_EXPORT___"); +console.log( + namedExportRed, + namedExportGreen, + namedExportBlue, + namedExportBackground +); + +const articleElement3 = document.createElement("article"); +const h3Element3 = document.createElement("h3"); +const h3TextNode3 = document.createTextNode("Named export"); + +const divElement9 = document.createElement("div"); +const divElement1Content1 = document.createTextNode("Red"); + +divElement9.className = namedExportRed; +divElement9.appendChild(divElement1Content1); + +const divElement10 = document.createElement("div"); +const divElement2Content1 = document.createTextNode("Green"); + +divElement10.className = namedExportGreen; +divElement10.appendChild(divElement2Content1); + +const divElement11 = document.createElement("div"); +const divElement3Content1 = document.createTextNode("Blue"); + +divElement11.className = namedExportBlue; +divElement11.appendChild(divElement3Content1); + +const divElement12 = document.createElement("div"); + +divElement12.className = namedExportBackground; + +h3Element3.appendChild(h3TextNode3); +articleElement3.appendChild(h3Element3); +articleElement3.appendChild(divElement9); +articleElement3.appendChild(divElement10); +articleElement3.appendChild(divElement11); +articleElement3.appendChild(divElement12); + +document.querySelectorAll("section")[0].appendChild(articleElement3); + +console.log("___LAZY_NAMED_EXPORT___"); +console.log( + namedExportLazyRed, + namedExportLazyGreen, + namedExportLazyBlue, + namedExportLazyBackground +); + +api2.use(); + +const articleElement4 = document.createElement("article"); +const h3Element4 = document.createElement("h3"); +const h3TextNode4 = document.createTextNode("Named export"); + +const divElement13 = document.createElement("div"); +const divElement5Content1 = document.createTextNode("Red"); + +divElement13.className = namedExportLazyRed; +divElement13.appendChild(divElement5Content1); + +const divElement14 = document.createElement("div"); +const divElement6Content2 = document.createTextNode("Green"); + +divElement14.className = namedExportLazyGreen; +divElement14.appendChild(divElement6Content2); + +const divElement15 = document.createElement("div"); +const divElement7Content2 = document.createTextNode("Blue"); + +divElement15.className = namedExportLazyBlue; +divElement15.appendChild(divElement7Content2); + +const divElement16 = document.createElement("div"); + +divElement16.className = namedExportLazyBackground; + +h3Element4.appendChild(h3TextNode4); +articleElement4.appendChild(h3Element4); +articleElement4.appendChild(divElement13); +articleElement4.appendChild(divElement14); +articleElement4.appendChild(divElement15); +articleElement4.appendChild(divElement16); + +document.querySelectorAll("section")[1].appendChild(articleElement4); diff --git a/test/runtime/injectStylesIntoLinkTag.test.js b/test/runtime/injectStylesIntoLinkTag.test.js index 57b6b6fc..871e0f60 100644 --- a/test/runtime/injectStylesIntoLinkTag.test.js +++ b/test/runtime/injectStylesIntoLinkTag.test.js @@ -6,20 +6,9 @@ import injectStylesIntoLinkTag from "../../src/runtime/injectStylesIntoLinkTag"; -import getTarget from "../../src/runtime/getTarget"; +import insertBySelector from "../../src/runtime/insertBySelector"; -const getInsertFn = (place) => - function insertFn(style) { - const target = getTarget(place); - - if (!target) { - throw new Error( - "Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid." - ); - } - - target.appendChild(style); - }; +const getInsertFn = (place) => insertBySelector.bind(null, place); function insertAtTop(element) { const parent = document.querySelector("head"); diff --git a/test/runtime/injectStylesIntoStyleTag.test.js b/test/runtime/injectStylesIntoStyleTag.test.js index 3208fd72..f16ea707 100644 --- a/test/runtime/injectStylesIntoStyleTag.test.js +++ b/test/runtime/injectStylesIntoStyleTag.test.js @@ -9,20 +9,9 @@ import injectStylesIntoStyleTag from "../../src/runtime/injectStylesIntoStyleTag import domAPI from "../../src/runtime/styleDomAPI"; import singletonApi from "../../src/runtime/singletonStyleDomAPI"; import insertStyleElement from "../../src/runtime/insertStyleElement"; -import getTarget from "../../src/runtime/getTarget"; +import insertBySelector from "../../src/runtime/insertBySelector"; -const getInsertFn = (place) => - function insertFn(style) { - const target = getTarget(place); - - if (!target) { - throw new Error( - "Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid." - ); - } - - target.appendChild(style); - }; +const getInsertFn = (place) => insertBySelector.bind(null, place); function styleTagTransform(css, style) { if (style.styleSheet) { diff --git a/test/styleTagTransform-option.test.js b/test/styleTagTransform-option.test.js index 43b6b660..4938032a 100644 --- a/test/styleTagTransform-option.test.js +++ b/test/styleTagTransform-option.test.js @@ -83,4 +83,20 @@ describe('"styleTagTransform" option', () => { expect(getWarnings(stats)).toMatchSnapshot("warnings"); expect(getErrors(stats)).toMatchSnapshot("errors"); }); + + it(`should work when the "styleTagTransform" option is path to module and injectType lazyStyleTag`, async () => { + const entry = getEntryByInjectType("simple.js", "lazyStyleTag"); + const compiler = getCompiler(entry, { + injectType: "lazyStyleTag", + styleTagTransform: require.resolve("./fixtures/styleTagTransform"), + }); + 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-options.test.js b/test/validate-options.test.js index d7925347..87f9d19f 100644 --- a/test/validate-options.test.js +++ b/test/validate-options.test.js @@ -28,8 +28,8 @@ describe("validate options", () => { }, styleTagTransform: { // eslint-disable-next-line func-names - success: [function () {}], - failure: ["true", true, []], + success: [function () {}, require.resolve("path")], + failure: [true, []], }, unknown: { success: [],