Skip to content

[bug]: Production build breaks with pako "undefined is not an object (r.assign=...)" — CJS wrapper hoisted into preset-standard chunk forms an import cycle #311

Description

@LimLLL

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:

  1. 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.
  2. 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.

Activity

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions