wojtekmaj / react-pdf

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

Failed to compile due to Webpack errors - Module parse failed: Unexpected character '�' using Docusaurus #1765

Open tobiasbueschel opened 6 months ago

tobiasbueschel commented 6 months ago

Before you start - checklist

Description

Context: using react-pdf in the latest Docusaurus version 3.2.1

image
[ERROR] Error: Unable to build website for locale en.
    at tryToBuildLocale (/test-repo/node_modules/@docusaurus/core/lib/commands/build.js:53:19)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async /test-repo/node_modules/@docusaurus/core/lib/commands/build.js:64:9
    at async mapAsyncSequential (/test-repo/node_modules/@docusaurus/utils/lib/jsUtils.js:20:24)
    at async Command.build (/test-repo/node_modules/@docusaurus/core/lib/commands/build.js:62:5) {
  [cause]: Error: Failed to compile due to Webpack errors.
  Module parse failed: Unexpected character '�' (1:0)
  You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
  (Source code omitted for this binary file)
      at /test-repo/node_modules/@docusaurus/core/lib/webpack/utils.js:230:24
      at /test-repo/node_modules/webpack/lib/MultiCompiler.js:596:14
      at processQueueWorker (/test-repo/node_modules/webpack/lib/MultiCompiler.js:533:6)
      at process.processTicksAndRejections (node:internal/process/task_queues:77:11)

However, running npm start works without any problems and I can see the PDF rendered correctly in the Docusaurus website.

Steps to reproduce

  1. Create a new docusaurus skeleton setup: npx create-docusaurus@latest my-website classic
  2. npm install react-pdf
  3. Add a new PdfViewer component to src/components/PdfViewer.jsx
import React, { useState } from 'react';
import { pdfjs, Document, Page } from 'react-pdf';

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

export default function PdfViewer({ fileName }) {
  const [numPages, setNumPages] = useState(null);

  function onDocumentLoadSuccess({ numPages }) {
    setNumPages(numPages);
  }

  function openPdf() {
    window.open(fileName, '_blank', 'fullscreen=yes');
  }

  return (
    <>
      <button onClick={openPdf} className="button button--primary mb-3">
        Open PDF here
      </button>
      <Document file={fileName} onLoadSuccess={onDocumentLoadSuccess}>
        {Array.from(new Array(numPages), (el, index) => (
          <Page
            key={`page_${index + 1}`}
            pageNumber={index + 1}
            renderAnnotationLayer={false}
            renderTextLayer={false}
            onItemClick={openPdf}
            className="w-full [&>canvas]:!w-full [&>canvas]:!h-full"
          />
        ))}
      </Document>
    </>
  );
}

(I've also tried with <BrowserOnly /> inside the component, but that led to the same issue)

  1. Add the following to your docusaurus.config.js:
// ...
plugins: [
    async function configureWebpack(context, options) {
      return {
        name: 'docusaurus-webpack-config',
        configureWebpack(config, isServer, utils) {
          if (isServer) {
            return {
              resolve: {
                alias: {
                  canvas: false,
                  encoding: false,
                },
              },
            };
          } else {
            return;
          }
        },
      };
    },
]
// ...

(reference from: https://github.com/syed-ahmed/docusaurus-plugin-react-pdf/blob/main/src/index.ts#L31-L42)

  1. In any MDX doc, import the component and render a PDF.
<PdfViewer fileName={require('./docs/example.pdf').default} />

Expected behavior

The build succeeds.

Actual behavior

The build fails with the above mentioned error message.

Additional information

Potentially related issues & comments:

Environment

wojtekmaj commented 5 months ago

This definitely look like a flavor of #1508 and #1620.

You did everything just the way I'd do. I'm puzzled. If importing PdfViewer asynchronously in <BrowserOnly> doesn't help, maybe it'd be better to ask on Docusaurus repo why that could be the case? I don't see any reason for BrowserOnly to be executed on the server side!

hzhang20902 commented 3 months ago

I just had this specific problem with almost the exact same setup. The differences being that as of now, July 27, 2024, I have more recent versions of React-PDF (9.1.0); webpack (5.93.0); and docusaurus(3.4). That being said, I built my client within a docker container, but the issues encountered were all from the build step, or a result of the build.

This solution I imagine should work also for a Next.js app that has static site generation issues (replace lazy with dynamic, and maybe also browserOnly with something else) because the plugin workaround you listed above led me to a solution to the build issue, but created a render issue, which is what the Promise.withResolvers part from the #1508 thread is for. Then, the page rendered correctly and the component could input a pdf to load, but could not load it, which led to the worker import workaround, which led to resize issues, which led to recreating the useResizeObserver hook and then it all finally worked:

if (typeof Promise.withResolvers === "undefined") {
    if (window) {
      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 }
      }
    }
  }
pdfjs.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.min.mjs`
import React, { lazy, Suspense } from 'react'

const AnyName = lazy(() => import("./PDFViewerComponent"))

export const StaticPDF = () => {  
    return (
    <Suspense fallback={<p>Loading...</p>}>
        <AnyName />        
    </Suspense>
    );
  };
# MDX Page

### Content

 <BrowserOnly>
        {() => <StaticPDF />}
 </BrowserOnly>

import { StaticPDF } from "@site/src/components/StaticPDF"
import BrowserOnly from '@docusaurus/BrowserOnly'
import { useEffect } from 'react';

/**
 * Observes a given element using ResizeObserver.
 *
 * @param {Element} [element] Element to attach ResizeObserver to
 * @param {ResizeObserverOptions} [options] ResizeObserver options. WARNING! If you define the
 *   object in component body, make sure to memoize it.
 * @param {ResizeObserverCallback} observerCallback ResizeObserver callback. WARNING! If you define
 *   the function in component body, make sure to memoize it.
 * @returns {void}
 */
export default function useResizeObserver(
  element: Element | null,
  options: ResizeObserverOptions | undefined,
  observerCallback: ResizeObserverCallback,
): void {
  useEffect(() => {
    if (!element || !('ResizeObserver' in window)) {
      return undefined;
    }

    const observer = new ResizeObserver(observerCallback);

    observer.observe(element, options);

    return () => {
      observer.disconnect();
    };
  }, [element, options, observerCallback]);
}

This should make everything work. I am very tired right now though and may have forgotten to mention other things I did, but hopefully this helps you and others who have the same issue.

impiia commented 3 months ago

I just had this specific problem with almost the exact same setup. The differences being that as of now, July 27, 2024, I have more recent versions of React-PDF (9.1.0); webpack (5.93.0); and docusaurus(3.4). That being said, I built my client within a docker container, but the issues encountered were all from the build step, or a result of the build.

This solution I imagine should work also for a Next.js app that has static site generation issues (replace lazy with dynamic, and maybe also browserOnly with something else) because the plugin workaround you listed above led me to a solution to the build issue, but created a render issue, which is what the Promise.withResolvers part from the #1508 thread is for. Then, the page rendered correctly and the component could input a pdf to load, but could not load it, which led to the worker import workaround, which led to resize issues, which led to recreating the useResizeObserver hook and then it all finally worked:

  • do an entirely clean npx for the docusaurus;
  • reinstall all packages EXCEPT for the @wojtekmaj/react-hooks;
  • keep all things related to react-pdf isolated as a single component; do NOT add configuration for webpack;
  • within the script for the component, add this after imports and before pdfjs gets the worker:
if (typeof Promise.withResolvers === "undefined") {
    if (window) {
      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 }
      }
    }
  }
  • import your worker directly from a CDN, and match the version exactly by using the version prop:
pdfjs.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.min.mjs`
  • make sure the PDF viewer component is the default export within the script (rafce not rafc);
  • create a loader component and lazyload the PDF viewer component:
import React, { lazy, Suspense } from 'react'

const AnyName = lazy(() => import("./PDFViewerComponent"))

export const StaticPDF = () => {  
    return (
    <Suspense fallback={<p>Loading...</p>}>
        <AnyName />        
    </Suspense>
    );
  };
  • import that component into the MDX file, and wrap it in browseronly:
# MDX Page

### Content

 <BrowserOnly>
        {() => <StaticPDF />}
 </BrowserOnly>

import { StaticPDF } from "@site/src/components/StaticPDF"
import BrowserOnly from '@docusaurus/BrowserOnly'
  • recreate useResizeObserver hook as a separate file from @wojtekmaj source code:
import { useEffect } from 'react';

/**
 * Observes a given element using ResizeObserver.
 *
 * @param {Element} [element] Element to attach ResizeObserver to
 * @param {ResizeObserverOptions} [options] ResizeObserver options. WARNING! If you define the
 *   object in component body, make sure to memoize it.
 * @param {ResizeObserverCallback} observerCallback ResizeObserver callback. WARNING! If you define
 *   the function in component body, make sure to memoize it.
 * @returns {void}
 */
export default function useResizeObserver(
  element: Element | null,
  options: ResizeObserverOptions | undefined,
  observerCallback: ResizeObserverCallback,
): void {
  useEffect(() => {
    if (!element || !('ResizeObserver' in window)) {
      return undefined;
    }

    const observer = new ResizeObserver(observerCallback);

    observer.observe(element, options);

    return () => {
      observer.disconnect();
    };
  }, [element, options, observerCallback]);
}

This should make everything work. I am very tired right now though and may have forgotten to mention other things I did, but hopefully this helps you and others who have the same issue.

For me, this worked: pdfjs.GlobalWorkerOptions.workerSrc = https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.min.mjs;

and adding the dynamic import:

const PdfViewer = dynamic(
  () => import('../../../components/pdfViewer/component').then((mod) => mod.PdfViewer as any),
  { ssr: false }
);

Thank you!

github-actions[bot] commented 1 week ago

This issue is stale because it has been open 90 days with no activity. Remove stale label or comment or this issue will be closed in 14 days.