wojtekmaj / react-pdf

Display PDFs in your React app as easily as if they were images.
https://projects.wojtekmaj.pl/react-pdf
MIT License
8.97k stars 861 forks source link

Next.js and v9 on build Promise.withResolvers #1811

Closed Kasui92 closed 3 weeks ago

Kasui92 commented 1 month ago

Before you start - checklist

Description

I'm trying to implement the library in a Next.js 14.2.3 (App Router) project without turbopack, however when I go to use the proposed examples it reports the error in the title directly in the console during development or when it's launched the build command.

Having to use Node 20 because Vercel doesn't support 22, I immediately adopted legacy mode to solve the problem... but with poor results.

I also installed core-js and imported directly into the layout in root, but although it solves the error during development, it gives me an error when I build the app.

Can I ask if I'm missing any steps?

Steps to reproduce

  1. Install Next.js App
  2. Install React-Pdf v9
  3. Install core-js
  4. Use the files in information paragragh
  5. Try to build

Expected behavior

The build command should work.

Actual behavior

The build command reports an error "failed to parse input file" or "Promise.withResolvers" if core-js is not installed.

Additional information

/src/app/layout.js

import 'core-js/full/promise/with-resolvers'

export default function RootLayout({ children }) {
    return (
        <html lang="en">
            <body>
                <main >{children}</main>
            </body>
        </html>
    )
}

/src/app/page.js

import PdfViewer from './_components/PdfViewer'

export default async function ReactPDF() {
    return (
        <div>
                    <PdfViewer />
        </div>
    )
}

/src/app/PdfViewer.js

'use client'

import { useCallback, useState } from 'react'
import { pdfjs, Document, Page } from 'react-pdf'
import 'react-pdf/dist/esm/Page/AnnotationLayer.css'
import 'react-pdf/dist/esm/Page/TextLayer.css'

pdfjs.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/legacy/build/pdf.worker.min.mjs', import.meta.url).toString()

const options = {
    cMapUrl: '/cmaps/',
    standardFontDataUrl: '/standard_fonts/',
}

const maxWidth = 800

export default function PdfViewer() {
    const [file, setFile] = useState('./sample.pdf')
    const [numPages, setNumPages] = useState()
    const [containerRef, setContainerRef] = useState(null)
    const [containerWidth, setContainerWidth] = useState()

    const onResize = useCallback((entries) => {
        const [entry] = entries

        if (entry) {
            setContainerWidth(entry.contentRect.width)
        }
    }, [])

    function onFileChange(event) {
        const { files } = event.target

        const nextFile = files?.[0]

        if (nextFile) {
            setFile(nextFile)
        }
    }

    function onDocumentLoadSuccess({ numPages: nextNumPages }) {
        setNumPages(nextNumPages)
    }

    return (
        <div className="Example">
            <div className="Example__container">
                <div className="Example__container__document" ref={setContainerRef}>
                    <Document file={file} onLoadSuccess={onDocumentLoadSuccess} options={options}>
                        {Array.from(new Array(numPages), (el, index) => (
                            <Page
                                key={`page_${index + 1}`}
                                pageNumber={index + 1}
                                width={containerWidth ? Math.min(containerWidth, maxWidth) : maxWidth}
                            />
                        ))}
                    </Document>
                </div>
            </div>
        </div>
    )
}

The example PDF is the same one used in the sample, placed in /public.

Environment

lucaslosi commented 1 month ago

downgrading to 8.0.2 solved the issue

craigcartmell commented 1 month ago

Also seeing this issue on Next 14 during the build process.

8.0.2 has a high security vulnerability due to referencing pdfjs-dist@3.11.174, so it's not a great workaround.

allicanseenow commented 1 month ago

Also experiencing the same issue.

vpsk commented 1 month ago

I was able to resolve the issue by setting the dynamic component to load without server-side rendering (SSR). const PdfViewerComponent = dynamic(() => import("./PdfViewer"), { ssr: false, });

and make sure in next.config.js enable this config.resolve.alias.canvas = false;

craigcartmell commented 1 month ago

I was able to resolve the issue by setting the dynamic component to load without server-side rendering (SSR). const PdfViewerComponent = dynamic(() => import("./PdfViewer"), { ssr: false, });

and make sure in next.config.js enable this config.resolve.alias.canvas = false;

Which version of Next/React-PDF are you using?

Would you be able to paste your full component please?

wojtekmaj commented 1 month ago

All, please kindly see the updated samples:

and see updated upgrade guide for workarounds:

craigcartmell commented 1 month ago

@wojtekmaj - I think the upgrade guide should read: experimental.esmExternals: 'loose', not experiments.

NikkiHess commented 4 weeks ago

Regarding the workarounds... I'm unsure how to use the polyfill for Promise.withResolvers. Is there a guide somewhere on how to do this for react-pdf?

justinfarrelldev commented 3 weeks ago

I'm also experiencing this on Remix v2.9.1. Trying to polyfill Promise.withResolvers now

Edit: It seems that the error is coming from within the worker itself, making this difficult to polyfill. I have tried to place a polyfill both in the root as well as in the client component where I have included the worker, but neither worked for me.

wojtekmaj commented 3 weeks ago

Perhaps using a legacy worker would help? 🤔

justinfarrelldev commented 3 weeks ago

Perhaps using a legacy worker would help? 🤔

I tried using a legacy worker, but sadly that did not help. That being said, the following polyfill worked like a charm:

// @ts-expect-error This does not exist outside of polyfill which this is doing
if (typeof Promise.withResolvers === 'undefined') {
    if (window)
        // @ts-expect-error This does not exist outside of polyfill which this is doing
        window.Promise.withResolvers = function () {
            let resolve, reject;
            const promise = new Promise((res, rej) => {
                resolve = res;
                reject = rej;
            });
            return { promise, resolve, reject };
        };
}

pdfjs.GlobalWorkerOptions.workerSrc = new URL(
    'pdfjs-dist/legacy/build/pdf.worker.min.mjs',
    import.meta.url
).toString();

Once I added this, it worked fantastically! 😁

fate7bardiche commented 3 weeks ago

I referred to this commit and used a polyfill from core-js. Commit that added the polyfill

As a result, I was able to make it work.

If core-js is available, this method might also work.

justinwaite commented 2 weeks ago

Perhaps using a legacy worker would help? 🤔

I tried using a legacy worker, but sadly that did not help. That being said, the following polyfill worked like a charm:

// @ts-expect-error This does not exist outside of polyfill which this is doing
if (typeof Promise.withResolvers === 'undefined') {
    if (window)
        // @ts-expect-error This does not exist outside of polyfill which this is doing
        window.Promise.withResolvers = function () {
            let resolve, reject;
            const promise = new Promise((res, rej) => {
                resolve = res;
                reject = rej;
            });
            return { promise, resolve, reject };
        };
}

pdfjs.GlobalWorkerOptions.workerSrc = new URL(
    'pdfjs-dist/legacy/build/pdf.worker.min.mjs',
    import.meta.url
).toString();

Once I added this, it worked fantastically! 😁

@justinfarrelldev Where did you add this specifically? I assumed adding to entry.client would be the place, but that didn't resolve it for me.

justinfarrelldev commented 2 weeks ago

Perhaps using a legacy worker would help? 🤔

I tried using a legacy worker, but sadly that did not help. That being said, the following polyfill worked like a charm:

// @ts-expect-error This does not exist outside of polyfill which this is doing
if (typeof Promise.withResolvers === 'undefined') {
    if (window)
        // @ts-expect-error This does not exist outside of polyfill which this is doing
        window.Promise.withResolvers = function () {
            let resolve, reject;
            const promise = new Promise((res, rej) => {
                resolve = res;
                reject = rej;
            });
            return { promise, resolve, reject };
        };
}

pdfjs.GlobalWorkerOptions.workerSrc = new URL(
    'pdfjs-dist/legacy/build/pdf.worker.min.mjs',
    import.meta.url
).toString();

Once I added this, it worked fantastically! 😁

@justinfarrelldev Where did you add this specifically? I assumed adding to entry.client would be the place, but that didn't resolve it for me.

Sorry, I could have been more clear - I put this snippet in the component where the Document and Page tags are (IE, the component used to show the PDF - I called it pdfViewer.client.tsx).

For more context, I also imported the TextLayer.css and AnnotationLayer.css within this file (at the top, as imports) as well as the worker src.

Basically, almost all react-pdf logic is isolated within this client component. I'll post the source code shortly (and edit this comment when I do).

Edit: Here's the source that works for me (this file is pdfViewer.client.tsx). Yes, my JSDoc is badly out of date (it was generated with Hygen boilerplate and I haven't updated it). Note that the memoization is to prevent re-renders of the PDF Viewer (causing it to load the PDF again):

/*
    Description: The viewer for pdfs. Needs a buffer to render
*/
import React, { FC, useMemo } from 'react';
import { pdfjs, Document, Page } from 'react-pdf';
import 'react-pdf/dist/Page/TextLayer.css';
import 'react-pdf/dist/Page/AnnotationLayer.css';

// @ts-expect-error This does not exist outside of polyfill which this is doing
if (typeof Promise.withResolvers === 'undefined') {
    if (window)
        // @ts-expect-error This does not exist outside of polyfill which this is doing
        window.Promise.withResolvers = function () {
            let resolve, reject;
            const promise = new Promise((res, rej) => {
                resolve = res;
                reject = rej;
            });
            return { promise, resolve, reject };
        };
}

pdfjs.GlobalWorkerOptions.workerSrc = new URL(
    'pdfjs-dist/legacy/build/pdf.worker.min.mjs',
    import.meta.url
).toString();

type Props = {
    buffer: { data: Buffer };
};

/**
 * The viewer for pdfs. Needs a buffer to render
 *
 * @param {Props} param0
 * @param {string} param0.children The children of this component.
 * @returns {React.ReactElement}
 */
export const PdfViewer: FC<Props> = ({ buffer }: Props): React.ReactElement => {
    const memoizedFile = useMemo(() => buffer.data, [buffer.data]); // Depend on buffer.data assuming buffer.data is stable and only changes if actual data changes

    // Memoize the file object to prevent unnecessary re-renders
    const fileProp = useMemo(() => ({ data: memoizedFile }), [memoizedFile]);

    return (
        <div>
            <Document
                file={fileProp}
                onLoadError={(err) =>
                    console.error(`Loading error from PDF viewer: ${err}`)
                }
                onLoadStart={() => console.log('Started loading pdf')}
                onLoadSuccess={(pdf) =>
                    console.log('Successfully loaded pdf:', pdf)
                }
            >
                <Page pageIndex={0} />
            </Document>
        </div>
    );
};
adrianbienias commented 1 week ago

The issue is still present.

After running the example app https://github.com/wojtekmaj/react-pdf/tree/main/sample/next-app

I'm getting error in the console TypeError: Promise.withResolvers is not a function

Using the proposed polyfills doesn't seem to solve it.

justinfarrelldev commented 1 week ago

The issue is still present.

After running the example app https://github.com/wojtekmaj/react-pdf/tree/main/sample/next-app

I'm getting error in the console TypeError: Promise.withResolvers is not a function

Using the proposed polyfills doesn't seem to solve it.

Try adding an "else" to the polyfill I posted above and then using "global" instead of window within that else statement. That should help to handle SSR

adrianbienias commented 1 week ago
if (typeof Promise.withResolvers === "undefined") {
  if (window) {
    // @ts-expect-error This does not exist outside of polyfill which this is doing
    window.Promise.withResolvers = function () {
      let resolve, reject
      const promise = new Promise((res, rej) => {
        resolve = res
        reject = rej
      })
      return { promise, resolve, reject }
    }
  } else {
    // @ts-expect-error This does not exist outside of polyfill which this is doing
    global.Promise.withResolvers = function () {
      let resolve, reject
      const promise = new Promise((res, rej) => {
        resolve = res
        reject = rej
      })
      return { promise, resolve, reject }
    }
  }
}

Nothing changes, the same error occurs.

I also tried https://github.com/wojtekmaj/react-pdf/commit/2ba89d8cb968af6e522e688329cbf2e412b80462 with the same result

maxess3 commented 1 week ago

I referred to this commit and used a polyfill from core-js. Commit that added the polyfill

As a result, I was able to make it work.

If core-js is available, this method might also work.

  • Node.js 19.7.0
  • "core-js": "^3.37.1"
  • "react-pdf": "^9.0.0"

Thank you, It works for me with the same configuration.

siinghd commented 1 week ago

Any update on this?, none of above worked for me

Ori2846 commented 1 week ago

Loading a Polyfill and putting import './polyfills.mjs'; at the top of my next.config.mjs worked for me. This made Promise.withResolvers available in both browser and server environments.

Here's the code I used:


import 'core-js/full/promise/with-resolvers.js';

// Polyfill for environments where window is not available (e.g., server-side rendering)
if (typeof Promise.withResolvers === 'undefined') {
  if (typeof window !== 'undefined') {
    window.Promise.withResolvers = function () {
      let resolve, reject;
      const promise = new Promise((res, rej) => {
        resolve = res;
        reject = rej;
      });
      return { promise, resolve, reject };
    };
  } else {
    global.Promise.withResolvers = function () {
      let resolve, reject;
      const promise = new Promise((res, rej) => {
        resolve = res;
        reject = rej;
      });
      return { promise, resolve, reject };
    };
  }
}
DonikaV commented 4 days ago

Updated to latest one 9.1.0 and now getting this error, downgraded and still same :| Basically every time when I update version I getting some errors...

ldiqual commented 3 days ago

Super hacky, but this is how we added the polyfill to the pdfjs worker itself, in vite.config.ts:

function transformPdfJsWorker(): Plugin {
  return {
    name: 'transform-pdf-js-worker',
    generateBundle(options, bundle) {
      for (const [fileName, chunkOrAsset] of Object.entries(bundle)) {
        if (!fileName.includes('pdf.worker') || chunkOrAsset.type !== 'asset') {
          continue
        }
        const prepend = Buffer.from(
          `if (typeof Promise.withResolvers === "undefined") {
            Promise.withResolvers = function () {
              let resolve, reject
              const promise = new Promise((res, rej) => {
                resolve = res
                reject = rej
              })
              return { promise, resolve, reject }
            }
          }
          `,
          'utf-8'
        )
        const sourceBuffer = Buffer.isBuffer(chunkOrAsset.source)
          ? chunkOrAsset.source
          : Buffer.from(chunkOrAsset.source)
        chunkOrAsset.source = Buffer.concat([prepend, sourceBuffer])
      }
    },
  }
}

export default defineConfig({
  plugins: [
    transformPdfJsWorker(),
  ],
})
allicanseenow commented 2 days ago
if (typeof Promise.withResolvers === "undefined") {
  if (window) {
    // @ts-expect-error This does not exist outside of polyfill which this is doing
    window.Promise.withResolvers = function () {
      let resolve, reject
      const promise = new Promise((res, rej) => {
        resolve = res
        reject = rej
      })
      return { promise, resolve, reject }
    }
  } else {
    // @ts-expect-error This does not exist outside of polyfill which this is doing
    global.Promise.withResolvers = function () {
      let resolve, reject
      const promise = new Promise((res, rej) => {
        resolve = res
        reject = rej
      })
      return { promise, resolve, reject }
    }
  }
}

Nothing changes, the same error occurs.

I also tried 2ba89d8 with the same result

This works for me if I put the polyfill in _app.tsx. It works in both local and production.

gabsoftware commented 1 day ago

For anyone still struggling with this. Don't bother loading polyfills yourself, it will not work.

You have to load the Legacy version of PdjJS worker which is already polyfilled. See https://github.com/mozilla/pdf.js/wiki/Frequently-Asked-Questions#faq-support

Like so:

import { pdfjs } from "react-pdf";
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
    'pdfjs-dist/legacy/build/pdf.worker.min.mjs',
    import.meta.url,
).toString();
gabsoftware commented 1 day ago

Okay, so after a few tests, you still have to polyfill Promise.withResolvers because react-pdf uses non-legacy pdf.mjs even if you choose the legacy pdf worker. So legacy pdf worker + polyfilled Promise.withResolvers + react-pdf works as intended.