You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
0.18.14: browser bundlers fail with "parse" is not exported by @acemir/cssom/build/CSSOM.js #352
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.
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.
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.
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)
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?
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.
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.
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/cssomsetProperty 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 :)
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.
Activity
sgcullen commentedon Oct 25, 2026
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/cssom0.9.31, thebrowserfield points tobuild/CSSOM.js. That is a global-script build with no exports, soimport {parse} from '@acemir/cssom'inesm/html/style-element.jsfails withMISSING_EXPORT "parse". 0.9.30 had nobrowserfield, and linkedom 0.18.13 (still oncssom0.5) builds fine.2. The size cost, even once it builds. We redirected the import to
@acemir/cssom/lib/index.js. That builds and.sheetworks, but our output grows a lot:That's +2.9 MB (+24%) across our entry points.
style-element.jsimports the parser statically, so every bundle carries it even when.sheetis never read. We never read it.Suggestions
@acemir/cssom/lib/index.js, or get thebrowserfield fixed upstream..sheetreturningnullor 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.
WebReflection commentedon Oct 25, 2026
if I understand correctly this is a Worker bundle only matter, right?
ryangarber commentedon Oct 25, 2026
No, the error occurs for me in a standard Vite web bundle. Any bundler that respects the
browserexport 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.ryangarber commentedon Oct 25, 2026
Done: https://github.com/ryangarber/linkedom-repro
index.html
index.ts
vite crashes
Run
npx vite(with default config) and load the page; Vite crashes due to the missingparseexport even in dev mode.WebReflection commentedon Oct 25, 2026
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?
ryangarber commentedon Oct 25, 2026
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
browserbundle, so linkedom would crash there too.lppedd commentedon Oct 26, 2026
Folks, to recap:
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.
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.
paulirish commentedon Oct 28, 2026
hey andrea
Saw the
cssom->@acemir/cssomswap 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/cssomforcssomnom/parseinesm/html/style-element.jsis a 1-line change and... it passes yournpm testwith 100% coverage:exports, so vite/rolldown/esbuild browser & worker builds work out of the box.@layer,@container,@scope, etc.) and stores declarations in an internalMapso it doesn't have theNV/cssomsetPropertyprototype-overwrite CVE. :DCSSStyleDeclarationhas a Proxy +[Symbol.iterator]+ anonChangecallback hook (I saw your notes in Duplicated CSSStyleDeclaration #92 and Export CSSOM classes #201 about needing a mutation hook forMutationObserver/ custom elements.. we use that same pattern, in case you ever wantel.styleandsheet.cssRules[i].styleto share one class and dropuhyphen).One trade-off to be upfront about on size:
cssomnom/parseincludes a full CSS Syntax L3 tokenizer, selector parser, and shorthand expansion, solinkedom/worker.jslands at ~685 KB (~152 KB gzip), compared to ~580 KB (~134 KB gzip) with@acemir/cssomor the oldNV/cssomat ~402 KB. So if gettingworker.jsback down near 400 KB is the main goal, fixing@acemir/cssom's package.json (or patchingNV/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 :)
lppedd commentedon Oct 28, 2026
Thanks @paulirish! I think in general my only observation is regarding
toString()results, for example: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.