Felix Heox
Felix Heox

Reputation: 81

How can I modify the insert method for Webpack style-loader?

The default behaviour of Webpack style-loader is to inject a style tag as the last child of the head tag. However, I want to modify the default insert method so that the css is injected into a shadow-root. I looked at the docs and tested the provided code but it did not work for me.

{
      loader: "style-loader",
      options: {
          insert: function insertIntoTarget(element, options) {
             var parent = options.target || document.head;
              parent.appendChild(element);
              },
      },
}

Some context here is that I am developing a chrome extension, and one of my entry points has the items that get injected on to the active tab. However as not to interfere with the page's pre-existing styles I have dynamically created a shadow-root in my 'content' entry point, and trying to load the css inside the shadow-root instead of head.

Below is my current webpack config file:

//webpack.config.js
const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');
const HtmlPlugin = require('html-webpack-plugin');

const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const tailwindcss = require('tailwindcss')
const autoprefixer = require('autoprefixer')


module.exports = {
  entry: {
    popup: path.resolve('src/popup/index.tsx'),
    background: path.resolve('src/background/background.ts'),
    content: path.resolve('src/content/index.tsx'),
  },
  module: {
    rules: [
      {
        use: 'ts-loader',
        test: /\.tsx?$/,
        exclude: /node_modules/,
      },
      {
        test: /\.css$/i,
        use: [
          {
            loader: 'style-loader',
            options: {
              insert: function insertIntoTarget(element, options) {
                var parent = options.target || document.head;
                parent.appendChild(element);
              },
            }
          },
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
            },
          },
          {
            loader: 'postcss-loader', // postcss loader needed for tailwindcss
            options: {
              postcssOptions: {
                ident: 'postcss',
                plugins: [tailwindcss, autoprefixer],
              },
            },
          },
        ],
      },
      {
        type: 'assets/resource',
        test: /\.(png|jpg|jpeg|gif|woff|woff2|tff|eot|svg)$/,
      },
    ]
  },
  plugins: [
    new CleanWebpackPlugin({
      cleanStaleWebpackAssets: false
    }),
    new CopyPlugin({
      patterns: [{
        from: path.resolve('src/static'),
        to: path.resolve('dist')
      }]
    }),
    ...getHtmlPlugins([
      'popup',
      'content'
    ])
  ],
  resolve: {
    extensions: ['.tsx', '.js', '.ts']
  },
  output: {
    filename: '[name].js',
    path: path.join(__dirname, 'dist')
  },
  optimization: {
    splitChunks: {
      chunks(chunk) {
        return chunk.name !== 'content';
      },
    }
  }
}

function getHtmlPlugins(chunks) {
  return chunks.map(chunk => new HtmlPlugin({
    title: 'React Extension',
    filename: `${chunk}.html`,
    chunks: [chunk]
  }))
}

Below is my entry point, content/index.tsx:

import React from "react";
import { createRoot } from "react-dom/client";
import ActiveTabContextProvider from "../context/ActiveTabContextProvider";
import Content from "./content";
import '../assets/tailwind.css'


function init() {
    const appContainer = document.createElement('div')
    appContainer.id = "shadow-root-parent";
    if (!appContainer) {
        throw new Error("Can not find AppContainer");
    }
    const shadowRoot = appContainer.attachShadow({ mode: 'open' });
        const root = createRoot(shadowRoot);
    document.body.appendChild(appContainer);
    root.render(
        <React.StrictMode>
            <ActiveTabContextProvider>
                <Content />
            </ActiveTabContextProvider>
        </React.StrictMode>
    );
}

init();

According to the style-loader docs the style-loader options object has an insert field that can take on a custom function to target the html element object to inject into:

{
      loader: "style-loader",
      options: {
          insert: function insertIntoTarget(element, options) {
             var parent = options.target || document.head;
              parent.appendChild(element);
              },
      },
},

I am a bit confused on how this works. When do I pass the argument document.getElementById('shadow-root-parent').shadowRoot to the function? Will the shadow-root html element be available before this style-loader runs?

Upvotes: 1

Views: 369

Answers (1)

Felix Heox
Felix Heox

Reputation: 81

The solution that I found worked best was to write a quick function for the insert field:

function insertInShadow(element) {
  if (window.location.pathname === '/') {
    const appContainer = document.createElement('div');
    appContainer.id = 'my-shadow-root';
    const shadowRoot = appContainer.attachShadow({ mode: 'open' });
    shadowRoot.appendChild(element);
    document.body.appendChild(appContainer);
  } else {
    document.head.appendChild(element)
  }
}

I can then add this function in options:

{
    loader: 'style-loader',
    options: {
       insert: insertInShadow,
    }
}

The if statement checking window.location.pathname can tell what chunk the style-loader is being applied to. This way we can conditionally load styles into head or body.

The reason document.getElementByID in the insert function never worked was because the style loader runs before the code in index.tsx can create the shadowRoot. So we can change the code here to look for elements created by style-loader's insert:

function init() {
    const appContainer = document.getElementById('my-shadow-root').shadowRoot;
    if (!appContainer) {
        throw new Error("Can not find AppContainer");
    }
    
    const root = createRoot(appContainer)
    root.render(
        <React.StrictMode>
            <ActiveTabContextProvider>
                <Content />
            </ActiveTabContextProvider>
        </React.StrictMode>
    );
}

Upvotes: 0

Related Questions