Repository: webpack-contrib/style-loader (js)

Issue:
Allow importing code to determine where styles are inserted
This is a feature request.

### Problem

As far as I can tell, the style loader only allows for styles to be inserted into one location, either the `<head>` or where specified in the `inserInto` string or function.

This approach does not work with composed/nested custom elements, as each can have their own shadow dom. If, for example,  `insertInto` specifies the root custom element the styles are not picked up in children shadow doms.

### Feature Request
Allow importing code to determine where styles are inserted.

### Possible Solution
1. Add an `option` for local insertion. Let's call it `localInsert`, which defaults to `false` so that code executes as it currently does.
2. When `localInsert` is `true`, the `update` function is not invoked and we attach a method (`localInsertTo` ?) to `exports`, which allows for the importing file(s) to determine where the styles are inserted.

### Example
In the example below each custom element will have their imported styles inserted into their respective shadow doms by calling the `localInsertTo`.

`Webpack`
```js
...
options: {
  localInsert: true
}
...
```

`Parent`
```js
import css from './parent.css';

export class Parent extends HTMLElement {
  constructor() {
    super();
    this.shadow = this.attachShadow({ mode: 'open' });
    this.shadow.innerHTML = `
    <div>Parent 
      <x-child></x-child>
    </div>`;
    css.localInsertTo(this.shadow);
  }
}

customElements.define('x-app', Parent);
```

`Child`
```js
import css from './child.css';

export class Child extends HTMLElement {
  constructor() {
    super();
    this.shadow = this.attachShadow({ mode: 'open' });
    this.shadow.innerHTML = `<div>child</div>`;
    css.localInsertTo(this.shadow);
  }
}

customElements.define('x-child', Child);
```



Pull request:
feat: allow to pass options to `insert` function through `style.use()`
Allow to pass additional parameter to `style.use(anything)` which will be passed to `insert` function. This allows to insert each tag in different places and especially useful for Shadow DOM

Resolves #328

<!--
  HOLY CRAP a Pull Request. We ❤️ those!

  If you remove or skip this template, you'll make the 🐼 sad and the mighty god
  of Github will appear and pile-drive the close button from a great height
  while making animal noises.

  Please place an x (no spaces!) in all [ ] that apply
-->

This PR contains a:

- [ ] **bugfix**
- [x] new **feature**
- [ ] **code refactor**
- [x] **test update** <!-- if bug or feature is checked, this should be too -->
- [ ] **typo fix**
- [ ] **metadata update**

### Motivation / Use-Case

I'm using style-loader in WebExtension and we heavily rely on Shadow DOM (we inject our widgets into 3rd party sites and want them look always nice, same on all sites). But it was a pain to inject styles into Shadow DOM (you better not peek on my old webpack config haha!). This small change allows to pass custom options (any object basically) to the `styles.use` call, which later will be passed to `insert` function. This allows customizing `insert` logic: now you can insert each style into different place and do whatever you want for each style individually

### Breaking Changes

This shouldn't break anything

### Additional Info

Resolves #328 (not in the proposed way tho)


Patch:
diff --git a/README.md b/README.md
index e7e7bec..a35fac1 100644
--- a/README.md
+++ b/README.md
@@ -540,6 +540,46 @@ module.exports = {
 
 Insert styles at top of `head` tag.
 
+You can pass any parameters to `style.use(anythingHere)` and this value will be passed to `insert` function. These options will be passed to `styleTagTransform` function too. 
+
+**webpack.config.js**
+
+```js
+module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\.css$/i,
+        use: [
+          {
+            loader: "style-loader",
+            options: {
+              insert: function insertIntoTarget(element, options) {
+                var parent = options.target || document.querySelector("head");
+                parent.appendChild(element);
+              },
+            },
+          },
+          "css-loader",
+        ],
+      },
+    ],
+  },
+};
+```
+
+Insert styles to the provided element or to the `head` tag if target isn't provided. Now you can inject styles into Shadow DOM (or any other element).
+
+**component.js**
+
+```js
+import style from "./file.css";
+
+style.use({
+  target: document.querySelector('#myShadowDom').shadowRoot,
+})
+```
+
 ### `styleTagTransform`
 
 Type: `String | Function`
diff --git a/src/index.js b/src/index.js
index 5f3c071..f0db579 100644
--- a/src/index.js
+++ b/src/index.js
@@ -125,7 +125,8 @@ ${getInsertOptionCode(insertType, options)}
 options.domAPI = ${getdomAPI(isAuto)};
 options.insertStyleElement = insertStyleElement;
 
-exported.use = function() {
+exported.use = function(insertOptions) {
+  options.insertOptions = insertOptions;
   if (!(refs++)) {
     update = API(content, options);
   }
diff --git a/src/runtime/insertStyleElement.js b/src/runtime/insertStyleElement.js
index 6219971..5295e18 100644
--- a/src/runtime/insertStyleElement.js
+++ b/src/runtime/insertStyleElement.js
@@ -4,7 +4,7 @@ function insertStyleElement(options) {
 
   options.setAttributes(style, options.attributes);
 
-  options.insert(style);
+  options.insert(style, options.insertOptions);
 
   return style;
 }
diff --git a/src/runtime/styleDomAPI.js b/src/runtime/styleDomAPI.js
index fe3eb9f..a9e2612 100644
--- a/src/runtime/styleDomAPI.js
+++ b/src/runtime/styleDomAPI.js
@@ -18,7 +18,7 @@ function apply(style, options, obj) {
 
   // For old IE
   /* istanbul ignore if  */
-  options.styleTagTransform(css, style);
+  options.styleTagTransform(css, style, options.insertOptions);
 }
 
 function removeStyleElement(style) {


Test patch:
diff --git a/test/fixtures/lazy-insertOptions.js b/test/fixtures/lazy-insertOptions.js
new file mode 100644
index 0000000..e4aea6d
--- /dev/null
+++ b/test/fixtures/lazy-insertOptions.js
@@ -0,0 +1,7 @@
+import style from './style.css';
+
+style.use({
+  insertInto: document.body,
+  additionalStyles: '.some-element {color: red;}'
+});
+
diff --git a/test/lazyStyleTag-insertOptions.test.js b/test/lazyStyleTag-insertOptions.test.js
new file mode 100644
index 0000000..53809d7
--- /dev/null
+++ b/test/lazyStyleTag-insertOptions.test.js
@@ -0,0 +1,53 @@
+/* eslint-env browser */
+
+import {
+  compile,
+  getCompiler,
+  getEntryByInjectType,
+  getErrors,
+  getWarnings,
+  runInJsDom,
+} from "./helpers/index";
+
+describe('lazyStyleTag insertOptions', () => {
+  it(`should pass "insertOption" to "insert" function`, async () => {
+    expect.assertions(3);
+
+    const entry = getEntryByInjectType("insertOptions.js", 'lazyStyleTag');
+    const compiler = getCompiler(entry, {
+      injectType: 'lazyStyleTag',
+      insert: (styleTag, options) => {
+        options.insertInto.appendChild(styleTag);
+      }
+    });
+    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 pass "insertOption" to "styleTagTransform" function`, async () => {
+    expect.assertions(3);
+
+    const entry = getEntryByInjectType("insertOptions.js", 'lazyStyleTag');
+    const compiler = getCompiler(entry, {
+      injectType: 'lazyStyleTag',
+      styleTagTransform: (css, styleTag, options) => {
+        // eslint-disable-next-line no-param-reassign
+        styleTag.innerHTML = `${css}.${options.additionalStyles}\n`;
+      }
+    });
+    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");
+  });
+});