Skip to content

fix: resolve #standard-fonts/* to one file under every condition - #1782

Merged
blikblum merged 1 commit into
foliojs:masterfrom
mendrinos:fix/standard-fonts-import-condition
Aug 26, 2026
Merged

fix: resolve #standard-fonts/* to one file under every condition#1782
blikblum merged 1 commit into
foliojs:masterfrom
mendrinos:fix/standard-fonts-import-condition

Conversation

@mendrinos

@mendrinos mendrinos commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Problem

js/pdfkit.node.mjs loads the standard font metrics lazily through createRequire:

const require$1 = createRequire(import.meta.url);
const STANDARD_FONTS = {
  Courier: () => require$1('#standard-fonts/Courier'),
  ...
};

package.json#imports maps those specifiers two ways:

"#standard-fonts/*": {
  "require": "./js/standard-fonts/*.cjs",
  "default": "./js/standard-fonts/*.mjs"
}

The calls are require, so at runtime Node and Bun load the .cjs files —
tests/package-resolution.mjs already asserts as much. But a bundler or file tracer
walks the same file as ESM and resolves the same specifiers under default,
landing on the .mjs twins. The two disagree, and for anything that packages a traced
dependency set, the tracer wins.

With @vercel/nft — the tracer behind Vercel,
Nitro/Nuxt and @vercel/ncc — tracing an ESM entry that imports pdfkit collects all
14 js/standard-fonts/*.mjs and none of the .cjs files:

import { nodeFileTrace } from '@vercel/nft'
// entry.mjs: `import PDFDocument from 'pdfkit'; export default PDFDocument`
const { fileList } = await nodeFileTrace(['entry.mjs'])
;[...fileList].filter(f => f.includes('standard-fonts'))
// → every *.mjs, no *.cjs

The build succeeds and the package looks complete in the output. Then the first
document throws:

Cannot find module '#standard-fonts/Helvetica' from '/app/.output/server/node_modules/pdfkit/js/pdfkit.node.mjs'

Reproducible by copying that traced file list into a directory with no ancestor
node_modules and constructing a PDFDocument — fails under both Node 23 and Bun 1.4.

Fix

Nothing reachable at runtime takes the import condition here. The browser builds
register fonts through registerStdFonts and never reference #standard-fonts
(grep -c standard-fonts js/pdfkit.browser.mjs → 0); the only consumer is
lib/document.node.js, reached by require from both node builds. The default
branch is therefore unreachable and serves only to point resolvers at files pdfkit
never loads.

"#standard-fonts/*": "./js/standard-fonts/*.cjs"

Runtime behaviour is unchanged — same files, still lazy, still one font at a time.
Tracers now resolve what Node resolves, and traced output gets slightly smaller
because the unused ESM twins are no longer dragged along.

The public ./standard-fonts/* export is untouched: consumers importing a standard
font by name still get the ESM build, and rollup still emits both variants.

Alternative

If the conditional mapping should stay, the other way out is to have the build emit
explicit relative specifiers (require('./standard-fonts/Courier.cjs')) in the node
ESM bundle — unambiguous to every analyser, no condition resolution involved. Happy
to redo it that way if you prefer; it is a rollup-config change rather than a manifest
one.

Test

tests/package-resolution.mjs gains a check that both conditions agree, walking every
standard font in the exports map rather than a single hardcoded one. It fails on
master at the first font:

+ actual   - expected
+ '.../js/standard-fonts/Courier.mjs'
- '.../js/standard-fonts/Courier.cjs'

yarn test:package, yarn test:unit (432 passing), yarn lint and yarn prettier
are all green with the change.

Context

Hit downstream on a Nuxt/Nitro app: receipt PDFs quietly stopped being attached to
payment emails (the generation error was caught and logged) and every other PDF route
500'd, while CI stayed green — nothing in a normal test run exercises the packaged
output. Same class as #1779, one level further down.

The node ESM build loads the standard font metrics lazily through
`createRequire`, so the require condition is the only one ever taken at
runtime. Bundlers and file tracers walk `js/pdfkit.node.mjs` as ESM and
resolve the same specifiers under the import condition, landing on the
`.mjs` twins instead. Anything that packages a traced dependency set ships
the modules pdfkit never loads and omits the ones it does, so the first
`new PDFDocument()` throws `Cannot find module`.

Nothing reachable at runtime uses the import condition here: the browser
builds register their fonts through `registerStdFonts` and never reference
`#standard-fonts`, and both node builds arrive via `require`. Point the
internal mapping at the CommonJS files so every resolver agrees with Node.

The public `./standard-fonts/*` export is untouched, so consumers importing
a standard font by name still get the ESM build.
@mendrinos
mendrinos force-pushed the fix/standard-fonts-import-condition branch from 32bbaed to 39841d9 Compare August 26, 2026 14:42
@blikblum

Copy link
Copy Markdown
Member

LGTM

@blikblum
blikblum merged commit c6f57c6 into foliojs:master Aug 26, 2026
3 checks passed
@mendrinos
mendrinos deleted the fix/standard-fonts-import-condition branch August 27, 2026 12:51
@mendrinos

Copy link
Copy Markdown
Contributor Author

Would it be possible to release another patch? We are shipping it in production in a couple of days. Many thanks!

@mendrinos

Copy link
Copy Markdown
Contributor Author

Looking forward to the patch release, thank you @blikblum !

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.

2 participants