Skip to content

Runtime fetch() for styles breaks with bundlers #89

@DmitrySharabin

Description

@DmitrySharabin

Summary

nude-element loads shadow DOM styles at runtime via fetch(). Each element class declares a static property (e.g., static styles = "./my-slider.css") and at runtime the library resolves the full URL, fetches the CSS text, creates a CSSStyleSheet, and adopts it onto the shadow root.

This breaks with all bundlers in production (CSS files are never emitted, so fetch() 404s), and additionally in Vite's dev server (which serves .css as JS modules).

The issue affects all five static style properties that go through the defineStyles()fetch() path: styles, shadowStyles, globalStyles, lightStyles, and documentStyles.

Bundler Dev server Production build
Vite ❌ Wrong Content-Type ❌ CSS not emitted
webpack ✅ Works ❌ CSS not emitted
Rollup / esbuild ✅ Works (no dev server) ❌ CSS not emitted
Parcel ✅ Likely works ❌ CSS not emitted

Root Cause

static styles = "./my-slider.css" is an opaque string literal — not an ES import. No bundler tracks it as a dependency, so the .css files are never emitted to the output directory.

The fetch() path in code

  1. Class setupfirst_constructor_static hook in src/plugins/styles/base.js calls this.defineStyles(this.styles).
  2. defineStyles() — if the value is a string, wraps it as { url: string } and resolves the full URL.
  3. connected hook — calls adoptStyle(options, roots) from src/plugins/styles/util/adopt-style.js.
  4. adoptStyle() — three branches:
    • ref instanceof CSSStyleSheet → use directly
    • ref.cssnew CSSStyleSheet() + replaceSync(css)
    • URL fallback → cachedFetch(fullUrl)fetch() ❌ breaks with bundlers

The { css: string } branch bypasses fetch() entirely, which is why inlining CSS at build time works.

Possible Solutions

  1. import with CSS module attributes — Use import "./foo.css" with {type: "css"} which bundlers can track as a dependency. Supported almost everywhere except Safari, but could be used with a fetch() fallback for unsupported browsers. A helper function could detect support and use either method accordingly.

  2. Document the { css } pattern as the recommended approach for bundled projects (already works, zero library changes needed).

  3. Ship an official Vite plugin as a package (e.g., nude-element/vite). A proof-of-concept plugin that rewrites style string literals into ?inline CSS imports (which Vite resolves as plain CSS strings):

    function inlineShadowStyles() {
      const RE = /\bstatic\s+(styles|shadowStyles|globalStyles|lightStyles|documentStyles)\s*=\s*(['"])(\.\/[^'"]+\.css)\2/g;
    
      return {
        name: "inline-shadow-styles",
        transform(code, id) {
          if (!id.endsWith(".js")) return null;
    
          RE.lastIndex = 0;
          if (!RE.test(code)) return null;
          RE.lastIndex = 0;
    
          const imports = [];
          let i = 0;
          const newCode = code.replace(RE, (_match, prop, _quote, cssPath) => {
            const varName = `__shadow_styles_${i++}`;
            imports.push(`import ${varName} from "${cssPath}?inline";`);
            return `static ${prop} = [{ css: ${varName} }]`;
          });
    
          return { code: imports.join("\n") + "\n" + newCode, map: null };
        },
      };
    }

    This handles all five static style properties and works for both dev and production builds.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions