Skip to content

fix: Next.js worker and stylesheets, strict page CSP, package.json export - #70

Merged
alekswebnet merged 5 commits into
masterfrom
fix/bundled-apps
Nov 1, 2026
Merged

alekswebnet merged 5 commits into
masterfrom
fix/bundled-apps

Conversation

@alekswebnet

@alekswebnet alekswebnet commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes three of the four problems in #66, plus a strict page CSP problem found while investigating it. There's one commit per fix, and each passes the suite on its own. The fourth problem, the PDF.js data folders in bundled apps, stays a requirement that the README now explains (203b6d9).

Fixes

  1. Default runtime URLs in Next.js (39d3a0a). Next.js, with webpack and Turbopack, returns new URL('./file', import.meta.url).href as a root-relative path such as /_next/static/media/pdf.worker.min.<hash>.mjs. Inside the srcdoc iframe, PDF.js couldn't resolve the relative worker URL and silently ran on the main thread ("Setting up fake worker"). The resource defaults derived from the viewer's URL degraded to bare wasm/ and cmaps/. The defaults now go through getFullPath, like attribute values.

  2. ./package.json export (4f57b10). require.resolve('@missing-elements/pdfjs-viewer/package.json') failed with ERR_PACKAGE_PATH_NOT_EXPORTED.

  3. Default stylesheets from JS modules (3d526fa). The element referenced viewer.css and paper-and-ink.css with new URL('./file', import.meta.url), so consumer bundlers treated them as the app's own CSS. next build --webpack did two things with them:

    • It emitted both as empty files, so the viewer never initialized ("The container must be absolutely positioned").
    • It moved their contents into the host page's stylesheets. That restyled the app with the viewer's global rules: html, body { height: 100% }, the body background, and .hidden, [hidden] { display: none !important }. This happened with assets-base too, because it's a build-time effect.

    The same bundled CSS text now ships as two JS modules, and the element applies it as constructed stylesheets on the viewer document. assets-base, viewer-css-src and theme-css-src still use <link> elements, and dist still has both .css files for them.

  4. Strict page CSP (4f0bb74). The srcdoc iframe inherits the page's CSP. Under style-src 'self' (no 'unsafe-inline'), the shadow root's <style> was blocked: the element was inline and its iframe kept the default 150px height. injectViewerStyles was blocked as well. The host styles are now a constructed stylesheet, and with fix 3 the viewer and injected styles are too. A CSP doesn't block constructed stylesheets.

Behavior changes

  • Default CSS ships as JS chunks: _virtual_pdfjs-viewer-css-*.js (60 KB gzipped) and _virtual_pdfjs-viewer-theme-css-*.js (6.8 KB). Their text is byte-identical to dist/viewer.css and dist/paper-and-ink.css. The element entry grows from 3.95 to 4.17 KB gzipped.
  • injectViewerStyles adds constructed stylesheets, not <style data-pdfjs-viewer-runtime-style> elements. They still come after the viewer and theme styles.
  • theme-css-src without viewer-css-src: the linked theme precedes every constructed stylesheet, so the bundled viewer.css goes into @layer pdfjs-viewer to let the theme override it. In this setup the theme wins over viewer.css regardless of specificity, and viewer.css's !important declarations win over the theme's. Every other combination cascades as before.
  • The viewer document starts hidden until its constructed stylesheets are applied, so it never paints unstyled.
  • Older jsdom versions (jsdom 20, used by Jest 29) can't construct stylesheets. There the element keeps a <style> element, so importing it in an app's component tests doesn't throw.

Data folders

Bundlers still don't copy the folders PDF.js loads data from by name: wasm/ (JBIG2, CCITT, JPEG 2000, ICC, the scripting sandbox), cmaps/, standard_fonts/, iccs/ and the annotation icons. They stay a requirement instead of getting the BinaryDataFactory from #66. A factory can serve CMaps, fonts and WASM decoders, but PDF.js still loads the scripting sandbox, ICC profiles and annotation icons by folder, and switches ICC off when a custom factory is used. Bundled apps would need the folder anyway, and we'd maintain a second way of loading data.

The README now says what breaks without the folders and points at assets-base. Checked in Next.js (Turbopack and webpack) and Vite builds, with a postinstall copy of dist and assets-base="/pdfjs-viewer/":

Check Without the folder With the copy
JBIG2, CCITT, JPEG 2000 scans blank, black, blank render
CJK text in a non-embedded font missing present
PDF JavaScript doesn't start starts
Note icon missing loads

In the Vite build, the color-management WASM and the CMYK profile also load only with the copy.

Not covered

  • The docs site: the bundler section with the copy script, for the release that ships this.

Verification

  • pnpm test -- --run: 36 passed (Firefox), and each commit passes on its own. The new tests fail on master:
    • Default URLs a bundler made relative. A fresh copy of the module is evaluated with a URL class that returns root-relative hrefs, as Next.js does.
    • Default stylesheets applied without any <link>.
    • Injected styles winning over a forced theme palette.
    • A theme-css-src theme overriding the bundled viewer.css. This one also fails with the cascade layer removed.
    • The element, the viewer and injected styles under style-src 'self'.
  • The LIGHT/DARK test now expects brightness(.9). The bundled CSS is minified, and the dev build used to link the unminified source.
  • tsc and pnpm build are clean.
  • Packed and installed into Next.js 16.4.3 and Vite 8.3.5 apps, checked in Chromium:
Check 4.4.1 This PR
next build --webpack: viewer starts no ("The container must be absolutely positioned") yes
next build --webpack: host page viewer and theme CSS loaded into it, body background changes untouched
next build --webpack: CSS files 2 empty files in static/media, 2 extracted into static/css none
next build (Turbopack): worker main thread ("Setting up fake worker") real worker
vite build real worker real worker, no CSS files
Page with style-src 'self' element inline, iframe 150px, injected styles blocked (Firefox) element block and all styles apply (Chromium and Firefox), no CSP violations (Chromium)
Import under jsdom 20 and 30 works works
require.resolve of package.json ERR_PACKAGE_PATH_NOT_EXPORTED resolves

Refs #66

🤖 Generated with Claude Code

alekswebnet and others added 5 commits November 1, 2026 02:21
Next.js, with webpack and Turbopack, returns `new URL('./file', import.meta.url).href` as a
root-relative path such as `/_next/static/media/pdf.worker.min.<hash>.mjs`. Inside the srcdoc
iframe, PDF.js can't resolve a relative worker URL and silently falls back to running on the main
thread, and the resource defaults derived from the viewer's URL degraded to bare `wasm/` and
`cmaps/`. The defaults now go through `getFullPath` like attribute values do.

Refs #66

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Scripts that locate the package with `require.resolve('@missing-elements/pdfjs-viewer/package.json')`,
for example to copy `dist` next to the app, failed with ERR_PACKAGE_PATH_NOT_EXPORTED.

Refs #66

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The element referenced viewer.css and paper-and-ink.css with `new URL('./file', import.meta.url)`,
so consumer bundlers treated them as the app's own CSS. `next build --webpack` emitted both as
empty files, so the viewer never initialized ("The `container` must be absolutely positioned"),
and moved their contents into the host page's stylesheets, restyling the app with the viewer's
global rules. That happened with `assets-base` too, since it is a build-time effect.

The same bundled CSS text now ships as two JS modules, and the element applies it as constructed
stylesheets on the viewer document. `assets-base`, `viewer-css-src` and `theme-css-src` still load
`<link>` elements, and dist still has both `.css` files for them.

- `injectViewerStyles` styles are constructed stylesheets too, so they still come after the theme.
- A theme from `theme-css-src` is a `<link>`, which precedes every constructed stylesheet. The
  bundled viewer stylesheet then goes into a cascade layer, so the theme still overrides it.
- The viewer document stays `hidden` until its stylesheets are applied, so it never paints unstyled.
- Constructed stylesheets aren't blocked by a page CSP without `'unsafe-inline'` for styles, which
  the srcdoc iframe inherits.

Refs #66

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shadow root's `<style>` was blocked by a page CSP without `'unsafe-inline'` for styles, so
the element was `inline` and its iframe kept the default 150px height. The host styles are now a
constructed stylesheet, which such a CSP doesn't block. With the previous commit, the viewer and
`injectViewerStyles` styles also apply under it.

Older jsdom versions, such as the one Jest 29 uses, can't construct stylesheets. There the element
keeps the `<style>` element, so importing it in an app's component tests doesn't throw.

Refs #66

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vite, webpack 5 and Next.js bundle the viewer but not the folders PDF.js loads its data from by
name. Without them, scans compressed with JBIG2, CCITT or JPEG 2000 render blank or black, CJK
text in fonts the PDF doesn't embed goes missing, and JavaScript in PDFs doesn't run. Checked in
Next.js (Turbopack and webpack) and Vite builds, which render all of it once a postinstall copy of
dist is served and assets-base points at it.

The note moves from "Why use it" to Install, where bundler users set the package up.

Refs #66

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant