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

Closed

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

  1. github-actions commented on Sep 20, 2026

    @github-actions
    Contributor

    Please add a sample or minimal reproduction. For configuration-only bugs with no file involved, exact package versions, configuration, commands and actual/expected behavior can be included inline. For rendering bugs, attach a sanitized file or send it to admin@flyfish.dev and record the date and filename. Screenshots alone do not reproduce a file failure.

  2. LimLLL commented on Sep 20, 2026

    @LimLLL
    Author

    Minimal reproduction: https://github.com/LimLLL/file-viewer-vite-cjs-cycle-repro

    pnpm install && pnpm build && pnpm check

    pnpm check loads the built entry chunk in Node and exits non-zero when module evaluation throws — no browser and no sample file needed, since the failure happens before any DOM access. Serving dist/ in a browser gives the same error and a blank page.

    This is a configuration-only bug, so there is no file to attach. Everything the bot asked for:

    Versions — @file-viewer/react 3.1.2, @file-viewer/preset-standard 3.1.2, @file-viewer/vite-plugin 3.1.2, Vite 7.3.6, React 19.

    Configuration — three files, nothing custom:

    // vite.config.ts
    export default defineConfig({
      plugins: [react(), fileViewerRenderers({ copyAssets: true })],
    });
    // src/main.tsx
    createRoot(document.getElementById('root')!).render(<FileViewer src="" />);

    Commands — pnpm build, then open dist/index.html.

    Expected — the page renders.
    Actual — blank page; module evaluation throws before the app mounts:

    TypeError: Cannot set properties of undefined (setting 'assign')              # V8
    TypeError: undefined is not an object (evaluating 'r.assign=function(a){…}')  # JavaScriptCore
    

    Two corrections to my original report, both from bisecting with this reproduction:

    1. It is not caused by the explicit options.preset import. My first report used that, and I assumed our off-the-guide integration was a factor. It is not — the plain documented Vite path reproduces it on its own:

    Variation Result
    3.1.1, plugin enabled throws
    3.1.2, plugin enabled throws
    copyAssets: false throws (asset copying is irrelevant)
    explicit options.preset static import instead of the plugin throws
    plugin removed, explicit options.preset static import only no throw

    The last row is the interesting one. With the plugin removed the same dependency graph produces a chunk layout that happens to initialise in a safe order; the plugin's module-graph injection is what turns it fatal. So the trigger looks like it sits in @file-viewer/vite-plugin, not in the preset.

    2. 3.1.2 is affected, so this was not fixed by today's release.


    Why this is worth fixing rather than documenting: the failure is invisible to every normal check. tsc, ESLint, the unit test suite and vite build itself all pass — the bad layout only exists after Rollup chunking, and dev never sees it because esbuild pre-bundles the CommonJS dependencies to ESM. In our case it reached a signed desktop release before anyone noticed, and because the app never mounted, its own updater component never ran either, so the shipped version could not update itself.

    Happy to test a patch against the reproduction repo.

  3. wybaby168 commented on Sep 20, 2026

    @wybaby168
    Member

    Reproduced with published 3.1.2 and Vite 7.3.6. A source fix now passes production builds in Chromium and WebKit; explicit preset plus the Vite plugin is supported. Keeping this open until the patched npm release is live and verified.

  4. wybaby168 commented on Oct 15, 2026

    @wybaby168
    Member

    The Vite CommonJS chunk-cycle fix is published in 3.2.1. Verified Vite 7/8 production builds in Chromium and WebKit, including the documented plugin path, explicit presets and manual chunks. This is build/browser validation, not a claim of a Tauri device test.

    Release 3.2.1

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