javascriptcssreactjsangularjavascript-import

Programmatically import a CSS file by its relative file path


Background

I'm hosting legacy React components in an Angular app using a method similar to this one. There are lots of React components to bring in. They all import their own CSS with JS module imports:

import "./Admin.css";

This means every component is responsible for importing its own stylesheet, which is great. If I could just get the compiler to be happy with this like it was in the React app, that would be the best possible outcome.

The problem

The problem is this doesn't work when the React component is hosted in Angular. Importing a CSS file this way breaks compilation:

X [ERROR] Could not resolve "./Admin.css"

src/app/components/react/Admin/Admin.tsx:3:7:
  3 │ import "./Admin.css";
    ╵        ~~~~~~~~~~~~~

Instead, I have to use CSS imports to include the React component's stylesheet in the .scss file of the Angular host:

// Inside AdminHost.component.scss
@import '../../react/Admin/Admin';

This comes with a much bigger problem: Now that each React component isn't responsible for its own styles, I have to dig through all of Admin.tsx's child components and manually import all their stylesheets as above. Then their children, and so on. Even if a component is the child of several parents, I have to repeat this process every time unless I happen to remember all the stylesheets it needs. There are a lot of components in the project, so it's a ton of tree-walking.

Attempt 1

I tried programmatically importing the CSS files with a helper function:

// sanitizer is injected by the Angular host
export function importCss(cssUrl: string, sanitizer: DomSanitizer) {
  const head = document.getElementsByTagName('head')[0];
  const style = document.createElement('link');
  style.rel = 'stylesheet';
  style.href = sanitizer.sanitize(SecurityContext.URL, cssUrl);
  head.appendChild(style);
}

// inside Admin.tsx:
importCss("path/to/Admin.css");

You may have already spotted the problem: cssUrl is a URL, not a relative file path. This means I can no longer import "./Admin.css". I have to use a URL, and that means Admin.css has to live in a static assets folder. I explored this a little before deciding it wouldn't save me any work.

Attempt 2

My current approach is to use a convention. Whenever I bring over a React component, I use the following rules:

  1. The stylesheet of the base React component gets imported into the Angular host's .scss.
  2. Each React component should have a .scss file of the same name in the same folder.
  3. If a component has children, it should import its immediate children's stylesheets into its own .scss. Each level is responsible for providing its own styles and importing the next level.
  4. The React stylesheets are all .css files. Whenever I complete this process for a component and its entire descendant tree, I rename the stylesheet to .scss. This way, if some other component includes it as a child, I know I can just import that one sheet and don't have to dig through all its descendants looking for styles.

The advantage here is it saves a lot of repeated effort. The problem is it doesn't save enough. There's still a lot of pain and manual tree-walking. I'm hoping there's a better way.

Ideas

  1. In a perfect world, there's a way to make Angular's compiler recognize the import "./Admin.css" line and play nicely with it. All my attempts to do this have failed. (I should say I'm just assuming this is from Angular's compiler, since it worked in the React app.)
  2. I could use a Bash script to dump all the style files in a static assets folder. That would make the idea from Attempt 1 pretty doable. The downside is they wouldn't be as easy to find when you're working on a component, and since some have duplicate names, the proper way to do it would be to recreate the same folder structure. That seems a little awkward, and it's pushing the limits of my Bash skills.
  3. There might be a way to import the CSS file as a blob, turn it into a data URL, and use that in a link tag. Then Attempt 1 would work. I don't know how to do that, though.

Solution

  • Unfortunately, all solutions involving Webpack failed for me, despite Webpack's own documentation recommending them. This is probably due to the uniqueness of my situation: The components aren't exactly running in an Angular environment, and they aren't exactly running in React either. No matter what I tried, I couldn't construct an import statement that would give me the raw contents of a .css file.

    What finally did work is require:

    const css = require("./my-css.css");
    

    This required one change to angular.json, only supported since Angular 17:

    "architect": {
      "build": {
        "options": {
          "loader": {
            ".css": "text"
          },
    

    As soon as I could get the raw content, the hard part was over. This left me free to pursue Idea 3, which is now working great:

    const registry = new Set();
    
    export function registerCss(...fileContents: string[]): void {
      fileContents.forEach(cssCode => {
        const cssBase64 = btoa(cssCode);
    
        if (!registry.has(cssBase64)) {
          registry.add(cssBase64);
    
          const styleElem = document.createElement('link');
          styleElem.setAttribute('rel', 'stylesheet');
          styleElem.setAttribute('href', `data:text/css;base64,${cssBase64}`);
          document.head.appendChild(styleElem);
        }
      });
    }
    

    I even made a version to support .scss files:

    export function registerScss(...fileContents: string[]): void {
      const cssContents = fileContents.map(fileContent => compileString(fileContent).css);
      registerCss(...cssContents);
    }
    

    Usage in hosted React components is simple:

    useEffect(() => {
      registerCss(
        require("./Comments.css"),
        require("./AddCommentModal.css"),
      );
    }, []);
    

    I'm currently deciding whether it's better to reuse the raw base64 CSS code as the registry key or use a simple hashing function like iMurmurhash. It would have to be as fast and resource-cheap as possible. Any advice on that is welcome.

    Hope this helps someone!