Skip to content

0.18.14: browser bundlers fail with "parse" is not exported by @acemir/cssom/build/CSSOM.js #352

Open

Description

@ryangarber

Since 0.18.14 (#343), bundling linkedom for the browser fails, because @acemir/cssom points its browser field at a file with no exports.

esm/html/style-element.js does:

import {parse} from '@acemir/cssom';

@acemir/cssom@0.9.32 has "browser": "./build/CSSOM.js". That file is a plain script that only declares a global var CSSOM = {...} and exports nothing. Node resolves main (lib/index.js, which does export parse), so Node works. Any bundler that respects browser (Vite/Rolldown, webpack, etc.) gets the script instead and fails:

[MISSING_EXPORT] "parse" is not exported by "node_modules/@acemir/cssom/build/CSSOM.js".
   ╭─[ node_modules/linkedom/esm/html/style-element.js:1:9 ]
 1 │ import {parse} from '@acemir/cssom';
   │         ──┬──
   │           ╰──── Missing export

The original cssom had no browser field, so 0.18.13 bundles fine. This was also reported in a comment on #318 after it closed.

Repro

// entry.js
import { parseHTML } from "linkedom";
console.log(parseHTML);
npm i linkedom@0.18.14 vite
npx vite build --config vite.config.mjs
# vite.config.mjs: export default { build: { lib: { entry: "entry.js", formats: ["es"] } } }

With linkedom@0.18.13, the same build succeeds.

Possible fixes

  • Import the CommonJS entry directly, which skips the browser field. I checked that this builds with Vite in browser mode and parses correctly:
    import {parse} from '@acemir/cssom/lib/parse.js';
    (and the same change for the require in cjs/html/style-element.js)
  • Or get @acemir/cssom to ship an ESM/UMD browser build that exports parse, or to drop the field.

Activity

  1. sgcullen commented on Oct 25, 2026

    @sgcullen

    Adding a data point from a browser extension, because this affects browser bundles in two ways.

    We bundle linkedom into a Chrome MV3 extension (Vite 8 / Rolldown) to parse fetched HTML. That includes the background service worker, which has no DOMParser. Several content scripts use it too, and each gets its own copy.

    1. The build break. Same as reported above. From @acemir/cssom 0.9.31, the browser field points to build/CSSOM.js. That is a global-script build with no exports, so import {parse} from '@acemir/cssom' in esm/html/style-element.js fails with MISSING_EXPORT "parse". 0.9.30 had no browser field, and linkedom 0.18.13 (still on cssom 0.5) builds fine.

    2. The size cost, even once it builds. We redirected the import to @acemir/cssom/lib/index.js. That builds and .sheet works, but our output grows a lot:

    linkedom background bundle whole extension
    0.18.13 969 KB 12.2 MB
    0.18.14 (import redirected) 1,256 KB 15.1 MB

    That's +2.9 MB (+24%) across our entry points. style-element.js imports the parser statically, so every bundle carries it even when .sheet is never read. We never read it.

    Suggestions

    • Import an entry that works in bundlers, such as @acemir/cssom/lib/index.js, or get the browser field fixed upstream.
    • Consider making CSS parsing optional for browser builds. That could be a way to plug in a parser, with .sheet returning null or throwing when none is set, or an entry point without CSSOM support. Browser builds that don't need stylesheets could then leave it out.

    We're staying on 0.18.13 for now and are happy to test a fix.

  2. WebReflection commented on Oct 25, 2026

    @WebReflection
    Owner

    if I understand correctly this is a Worker bundle only matter, right?

  3. ryangarber commented on Oct 25, 2026

    @ryangarber
    Author

    No, the error occurs for me in a standard Vite web bundle. Any bundler that respects the browser export field will try to resolve the import when building for web and fail. I'll try to create a very minimal reproduction if I can today.

  4. ryangarber commented on Oct 25, 2026

    @ryangarber
    Author

    Done: https://github.com/ryangarber/linkedom-repro

    index.html

    <!doctype html>
    <html>
    	<head>
    		<title>linkedom repro</title>
    	</head>
    	<body>
    		<script type="module" src="./index.ts"></script>
    	</body>
    </html>

    index.ts

    import { parseHTML } from "linkedom";
    parseHTML("<html><head><title>parsed html</title></head><body>this should be parsed</body></html>");

    vite crashes

    Run npx vite (with default config) and load the page; Vite crashes due to the missing parse export even in dev mode.

    1:41:27 PM [vite] (client) error while updating dependencies:
    Error: Error during dependency optimization:
    
    Build failed with 1 error:
    
    [MISSING_EXPORT] "parse" is not exported by "node_modules/@acemir/cssom/build/CSSOM.js".
       ╭─[ node_modules/linkedom/esm/html/style-element.js:1:9 ]
       │
     1 │ import {parse} from '@acemir/cssom';
       │         ──┬──
       │           ╰──── Missing export
    ───╯
    
        at aggregateBindingErrorsIntoJsError (file:///Users/ryan/Misc/linkedom-repro/node_modules/rolldown/dist/shared/error-D0SD8_tL.mjs:49:18)
        at unwrapBindingResult (file:///Users/ryan/Misc/linkedom-repro/node_modules/rolldown/dist/shared/error-D0SD8_tL.mjs:19:128)
        at #build (file:///Users/ryan/Misc/linkedom-repro/node_modules/rolldown/dist/shared/rolldown-CMhwwZPA.mjs:145:34)
        at async Object.build (file:///Users/ryan/Misc/linkedom-repro/node_modules/vite/dist/node/chunks/node.js:28824:11)
    
  5. WebReflection commented on Oct 25, 2026

    @WebReflection
    Owner

    I don't understand ... there is zero usage for this module on an environment that provides native DOM parsing, or SVG, or whatever that is ... what is the use case to have LinkeDOM on the main thread? it was never meant to work like that, that's why you should import from the worker export, what am I not understanding?

  6. ryangarber commented on Oct 25, 2026

    @ryangarber
    Author

    I do not import or use it directly; my project imports a platform-neutral Markdown conversion library that uses linkedom internally. A library like that cannot rely on worker threads. And there's a second failure case that this current export issue can cause too: serverless/edge environments like Cloudflare Workers can fall back to the browser bundle, so linkedom would crash there too.

  7. lppedd commented on Oct 26, 2026

    @lppedd
    Contributor

    Folks, to recap:

    The build break

    Valid, although environment-dependent (browser). I'll see what I can do upstream, as linkedom itself should not change anything. For example, making CSS parsing optional is a big no no simply out of complexity. There is no point in re-designing linkedom at this point.

    The size cost

    This is inevitable. The acemir/cssom fork is larger in size because it supports a bunch of new CSS features.

    @ryangarber thanks for the repro, will use it to experiment a bit.

  8. paulirish commented on Oct 28, 2026

    @paulirish

    hey andrea

    Saw the cssom -> @acemir/cssom swap in #318 / #343 and wanted to throw out another option in case it's useful here.

    I've been working this year on a zero-dep, pure-ESM CSSOM parser called cssomnom. And linkedom has come in handy for it's own tests, so thx!

    Swapping @acemir/cssom for cssomnom/parse in esm/html/style-element.js is a 1-line change and... it passes your npm test with 100% coverage:

    • pure ESM with clean exports, so vite/rolldown/esbuild browser & worker builds work out of the box.
    • supports modern syntax (CSS nesting, @layer, @container, @scope, etc.) and stores declarations in an internal Map so it doesn't have the NV/cssom setProperty prototype-overwrite CVE. :D
    • CSSStyleDeclaration has a Proxy + [Symbol.iterator] + an onChange callback hook (I saw your notes in Duplicated CSSStyleDeclaration #92 and Export CSSOM classes #201 about needing a mutation hook for MutationObserver / custom elements.. we use that same pattern, in case you ever want el.style and sheet.cssRules[i].style to share one class and drop uhyphen).

    One trade-off to be upfront about on size: cssomnom/parse includes a full CSS Syntax L3 tokenizer, selector parser, and shorthand expansion, so linkedom/worker.js lands at ~685 KB (~152 KB gzip), compared to ~580 KB (~134 KB gzip) with @acemir/cssom or the old NV/cssom at ~402 KB. So if getting worker.js back down near 400 KB is the main goal, fixing @acemir/cssom's package.json (or patching NV/cssom) is still smaller.

    Anyway.. in my cssomnom branch I have it staged and happy if you want to kick the tires... also no worries if its not for you :)

  9. lppedd commented on Oct 28, 2026

    @lppedd
    Contributor

    Thanks @paulirish! I think in general my only observation is regarding toString() results, for example:

    -assert(node.style.toString(), 'background-color:blue;background-image:url("https://t.co/i.png")', 'cssText setter');
    +assert(node.style.toString(), 'background-color: blue; background-image: url("https://t.co/i.png");', 'cssText setter');

    I'm not sure whether this can be considered a breaking change for current users. I don't particularly care for my own use case, but @WebReflection will for sure know better.

    I'd say switching (again 😢) the fork is maybe a good choice, also it being from ChromeLabs and supporting the whole spec.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions