Environment
|
|
@file-viewer/react |
3.1.1 |
@file-viewer/preset-standard |
3.1.1 |
@file-viewer/core |
3.1.1 |
@file-viewer/vite-plugin |
3.1.1 |
| Vite |
7.3.6 |
| Host app |
React 19, Tauri v2 desktop (WKWebView), macOS 15.7.2 |
Also reproduces the same way on 3.1.2 (the chunk layout is unchanged).
What happens
The production build (vite build) throws during module evaluation, before the app mounts. Blank white screen, no app. vite dev is completely fine.
TypeError: undefined is not an object (evaluating
'r.assign=function(a){for(var o=Array.prototype.slice.call(arguments,1);o.length;){
var s=o.shift();if(s){if(typeof s!="object")throw new TypeError(s+"must be non-object");
for(var f in s)t(s,f)&&(a[f]=s[f])}}return a}')
at file-viewer-preset-standard-<hash>.js
That code is pako@1.0.11's lib/utils/common.js (pulled in transitively by pdf-lib, jszip and utif).
Root cause: the CJS wrapper for pako is hoisted into the preset-standard chunk, which is part of an import cycle
In the emitted bundle, pako's CommonJS wrapper lands in file-viewer-preset-standard-<hash>.js:
var vi = {}, ba;
function ur() {
return ba || (ba = 1, (function (r) { // r === vi
...
r.assign = function (a) { ... } // <-- throws, r is undefined
})(vi)), vi;
}
var vi = {} is only assigned when that chunk's body runs. But the chunk sits in a cycle:
file-viewer-preset-standard ──imports──▶ file-viewer-{archive,email,image,media,ofd,
pdf,presentation-pptx,spreadsheet,text,word}
▲ │
└──────────────────── imports ───────────────────────┘
The renderer chunks are dependencies of preset-standard, so they evaluate first; during their evaluation they call back into ur(), and at that moment vi has not been assigned yet — hence r === undefined.
heic2any, docx-preview, UTIF and @file-viewer/core chunks also import back into preset-standard, i.e. that chunk is effectively acting as the shared-vendor sink for the whole renderer graph while also depending on it.
This never shows up in dev because Vite pre-bundles dependencies with esbuild, which converts CJS to ESM up front. It only appears after rollup does its own chunking — so type-check, lint and the entire unit test suite stay green and the failure reaches the packaged application.
Integration shape
Both the Vite plugin and an explicit preset are in play:
// vite.config.ts
fileViewerRenderers({ copyAssets: true })
// app code — static import
import standardRenderers from '@file-viewer/preset-standard';
export const FILE_VIEWER_OPTIONS = {
preset: standardRenderers,
rendererMode: 'replace',
autoRenderers: false,
locale: 'zh-CN',
theme: 'light',
styleIsolation: 'shadow',
} satisfies ViewerOptions;
We pass options.preset explicitly (rather than relying on plugin auto-discovery) because we derive the set of previewable extensions from that same object, so the admission check and the actual registry can never drift. I realise the guide steers Vite users toward auto-discovery and lists explicit options.preset under the non-Vite bundlers — if the two are not meant to be combined, it would help a lot to say so explicitly in the Vite guide, since the failure mode is a production-only white screen.
Workaround
Forcing the shared CJS dependencies into their own leaf chunk breaks the cycle:
// vite.config.ts
build: {
rollupOptions: {
output: {
manualChunks(id: string) {
if (/node_modules[\\/](pako|jszip|utif|pdf-lib|@pdf-lib[\\/][^\\/]+)[\\/]/.test(id)) {
return 'vendor-cjs-interop';
}
return undefined;
},
},
},
},
They then have no import back into any @file-viewer/* chunk, so they always initialise before their consumers.
This is fragile from our side though: which vendor module gets hoisted into which chunk is Rollup's decision, so any new CommonJS dependency can reproduce this with an unchanged config.
Suggestion
@file-viewer/vite-plugin already ships stabilizeInteropChunks for a very similar class of problem (the CodeMirror / Lezer / Sandpack TDZ issue), so the mechanism is presumably familiar. Two thoughts:
- That stabilisation currently only wraps an existing host
manualChunks function. A project with no manualChunks at all — which is the default Vite setup — gets no protection.
- CommonJS-only transitive dependencies shared by several renderer packages (
pako, jszip, utif) could be pinned to their own chunk by the plugin, independently of whether the host defines manualChunks.
Happy to test a patch or provide a minimal reproduction repo if that would help. Thanks for the library — the v3 modular split cut our bundle from 255 MB to 29 MB, which is exactly why we moved to it.
Environment
@file-viewer/react@file-viewer/preset-standard@file-viewer/core@file-viewer/vite-pluginAlso reproduces the same way on 3.1.2 (the chunk layout is unchanged).
What happens
The production build (
vite build) throws during module evaluation, before the app mounts. Blank white screen, no app.vite devis completely fine.That code is
pako@1.0.11'slib/utils/common.js(pulled in transitively bypdf-lib,jszipandutif).Root cause: the CJS wrapper for
pakois hoisted into thepreset-standardchunk, which is part of an import cycleIn the emitted bundle, pako's CommonJS wrapper lands in
file-viewer-preset-standard-<hash>.js:var vi = {}is only assigned when that chunk's body runs. But the chunk sits in a cycle:The renderer chunks are dependencies of
preset-standard, so they evaluate first; during their evaluation they call back intour(), and at that momentvihas not been assigned yet — hencer === undefined.heic2any,docx-preview,UTIFand@file-viewer/corechunks also import back intopreset-standard, i.e. that chunk is effectively acting as the shared-vendor sink for the whole renderer graph while also depending on it.This never shows up in dev because Vite pre-bundles dependencies with esbuild, which converts CJS to ESM up front. It only appears after
rollupdoes its own chunking — so type-check, lint and the entire unit test suite stay green and the failure reaches the packaged application.Integration shape
Both the Vite plugin and an explicit preset are in play:
We pass
options.presetexplicitly (rather than relying on plugin auto-discovery) because we derive the set of previewable extensions from that same object, so the admission check and the actual registry can never drift. I realise the guide steers Vite users toward auto-discovery and lists explicitoptions.presetunder the non-Vite bundlers — if the two are not meant to be combined, it would help a lot to say so explicitly in the Vite guide, since the failure mode is a production-only white screen.Workaround
Forcing the shared CJS dependencies into their own leaf chunk breaks the cycle:
They then have no import back into any
@file-viewer/*chunk, so they always initialise before their consumers.This is fragile from our side though: which vendor module gets hoisted into which chunk is Rollup's decision, so any new CommonJS dependency can reproduce this with an unchanged config.
Suggestion
@file-viewer/vite-pluginalready shipsstabilizeInteropChunksfor a very similar class of problem (the CodeMirror / Lezer / Sandpack TDZ issue), so the mechanism is presumably familiar. Two thoughts:manualChunksfunction. A project with nomanualChunksat all — which is the default Vite setup — gets no protection.pako,jszip,utif) could be pinned to their own chunk by the plugin, independently of whether the host definesmanualChunks.Happy to test a patch or provide a minimal reproduction repo if that would help. Thanks for the library — the v3 modular split cut our bundle from 255 MB to 29 MB, which is exactly why we moved to it.