Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Problem bundling simple jsdom example - request.resolve #1311

Closed
jugglingcats opened this issue May 24, 2021 · 7 comments
Closed

Problem bundling simple jsdom example - request.resolve #1311

jugglingcats opened this issue May 24, 2021 · 7 comments

Comments

@jugglingcats
Copy link

I have a simple jsdom example program:

const { JSDOM } = require("jsdom")

const { document } = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`).window
document.createElement("div")

I am bundling with the following command:

esbuild in.js --bundle --outfile=out.js --platform=node --external:canvas

This gives the following warning:

 > ../../node_modules/jsdom/lib/jsdom/living/xhr/XMLHttpRequest-impl.js:31:57: warning: "./xhr-sync-worker.js" should be marked as external for use with "require.resolve"
    31 │ const syncWorkerFile = require.resolve ? require.resolve("./xhr-sync-worker.js") : null;
       ╵                                                          ~~~~~~~~~~~~~~~~~~~~~~

When running node out.js (not surprisingly) it gives an error:

Error: Cannot find module './xhr-sync-worker.js'

Am not sure how to get past this. I tried using the API and custom resolver / loader plugins but it seems this is not the right approach because in both cases the require.resolve is still emitted.

Many thanks

@evanw
Copy link
Owner

evanw commented May 26, 2021

At the moment the use case for require.resolve that works the best with esbuild is external packages, not relative paths like this. You can get this example to work by manually building this code into two separate files:

esbuild in.js node_modules/jsdom/lib/jsdom/living/xhr/xhr-sync-worker.js --entry-names=[name] --outdir=out --bundle --platform=node --external:canvas

The bundler doesn't yet do this automatically for you though.

@jugglingcats
Copy link
Author

Thanks @evanw that seems like a reasonable workaround for this unusual scenario. I actually removed the need for jsdom to get around it in my project but satisfied myself your solution would work with a bit of jigging around!

@alexgorbatchev
Copy link

For people having an issues with jsdom specifically, you can get it work "properly" via a plugin:

const fs = require('fs');

const jsdomPatch = {
  name: 'jsdom-patch',
  setup(build) {
    build.onLoad({ filter: /jsdom\/living\/xhr\/XMLHttpRequest-impl\.js$/ }, async args => {
      let contents = await fs.promises.readFile(args.path, 'utf8');

      contents = contents.replace(
        'const syncWorkerFile = require.resolve ? require.resolve("./xhr-sync-worker.js") : null;',
        `const syncWorkerFile = "${require.resolve('jsdom/lib/jsdom/living/xhr/xhr-sync-worker.js')}";`,
      );

      return { contents, loader: 'js' };
    });
  },
};

@bbugh
Copy link

bbugh commented Nov 9, 2021

Thanks for that @alexgorbatchev!

Looks like some of the paths have changed since your post, this works for jsdom 18:

const fs = require('fs');
 const jsdomPatch = {
   name: 'jsdom-patch',
   setup(build) {
-    build.onLoad({ filter: /jsdom\/living\/xhr\/XMLHttpRequest-impl\.js$/ }, async args => {
+    build.onLoad({ filter: /jsdom\/living\/xmlhttprequest\.js$/ }, async (args) => {
       let contents = await fs.promises.readFile(args.path, 'utf8');
 
       contents = contents.replace(
         'const syncWorkerFile = require.resolve ? require.resolve("./xhr-sync-worker.js") : null;',
-        `const syncWorkerFile = "${require.resolve('jsdom/lib/jsdom/living/xhr/xhr-sync-worker.js')}";`,
+        `const syncWorkerFile = "${require.resolve('jsdom/lib/jsdom/living/xhr-sync-worker.js')}";`,
       );
 
       return { contents, loader: 'js' };
     });
   },
 };

@jamesamcl
Copy link

And changed again. This works for jsdom 19:

const fs = require('fs');
 const jsdomPatch = {
   name: 'jsdom-patch',
   setup(build) {
    build.onLoad({ filter: /jsdom\/living\/xhr\/XMLHttpRequest-impl\.js$/ }, async (args) => {
       let contents = await fs.promises.readFile(args.path, 'utf8');
 
       contents = contents.replace(
         'const syncWorkerFile = require.resolve ? require.resolve("./xhr-sync-worker.js") : null;',
        `const syncWorkerFile = "${require.resolve('jsdom/lib/jsdom/living/xhr/xhr-sync-worker.js')}";`,
       );
 
       return { contents, loader: 'js' };
     });
   },
 };

@Maxou44
Copy link

Maxou44 commented Nov 14, 2022

Update for latest version and support windows file path:

const jsdomPatch = {
    name: 'jsdom-patch',
    setup(build) {
        build.onLoad({ filter: /XMLHttpRequest-impl\.js$/ }, async (args) => {
            let contents = await fs.promises.readFile(args.path, 'utf8');
            contents = contents.replace(
                'const syncWorkerFile = require.resolve ? require.resolve("./xhr-sync-worker.js") : null;',
                `const syncWorkerFile = "${require.resolve('jsdom/lib/jsdom/living/xhr/xhr-sync-worker.js')}";`.replaceAll('\\', process.platform === 'win32' ? '\\\\' : '\\'),
            );
            return { contents, loader: 'js' };
        });
    },
};

@flupke
Copy link

flupke commented Jan 18, 2023

Here is a version of the fix that works from esbuild.mjs, with code stolen from resolve-mjs and find-package-json:

// esbuild.mjs

import fs from "fs"
import module from "module"
import path from "path"

import esbuild from "esbuild"
import { sassPlugin } from "esbuild-sass-plugin"
import svg from "esbuild-plugin-svg"
import postcss from "postcss"
import copyAssets from "postcss-copy-assets"

function parse(data) {
  data = data.toString("utf-8")

  //
  // Remove a possible UTF-8 BOM (byte order marker) as this can lead to parse
  // values when passed in to the JSON.parse.
  //
  if (data.charCodeAt(0) === 0xfeff) data = data.slice(1)

  try {
    return JSON.parse(data)
  } catch (e) {
    return false
  }
}

var iteratorSymbol =
  typeof Symbol === "function" && typeof Symbol.iterator === "symbol"
    ? Symbol.iterator
    : null

function addSymbolIterator(result) {
  if (!iteratorSymbol) {
    return result
  }
  result[iteratorSymbol] = function () {
    return this
  }
  return result
}

function findPackageJson(root) {
  root = root || process.cwd()
  if (typeof root !== "string") {
    if (typeof root === "object" && typeof root.filename === "string") {
      root = root.filename
    } else {
      throw new Error(
        "Must pass a filename string or a module object to finder"
      )
    }
  }
  return addSymbolIterator({
    /**
     * Return the parsed package.json that we find in a parent folder.
     *
     * @returns {Object} Value, filename and indication if the iteration is done.
     * @api public
     */
    next: function next() {
      if (root.match(/^(\w:\\|\/)$/))
        return addSymbolIterator({
          value: undefined,
          filename: undefined,
          done: true,
        })

      var file = path.join(root, "package.json"),
        data

      root = path.resolve(root, "..")

      if (fs.existsSync(file) && (data = parse(fs.readFileSync(file)))) {
        data.__path = file

        return addSymbolIterator({
          value: data,
          filename: file,
          done: false,
        })
      }

      return next()
    },
  })
}

const EXTENSIONS = {
  ".cjs": "dynamic",
  ".mjs": "module",
  ".es": "module",
  ".es6": "module",
  ".node": "addon",
  ".json": "json",
  ".wasm": "wasm",
}

async function requireResolve(specifier, parent, system) {
  try {
    // Let the default resolve algorithm try first
    let { url, format } = system(specifier, parent)

    // Resolve symlinks
    if (url.startsWith("file://")) {
      const realpath = await fs.promises.realpath(url.replace("file://", ""))
      url = `file://${realpath}`
    }

    return { url, format }
  } catch (error) {
    const base = parent
      ? path.dirname(parent.replace("file://", ""))
      : process.cwd()
    const require = module.createRequire(path.join(base, specifier))

    let modulePath
    try {
      modulePath = require.resolve(specifier)
    } catch (e) {
      // .cjs is apparently not part of the default resolution algorithm,
      // so check if .cjs file exists before bailing completely
      modulePath = require.resolve(`${specifier}.cjs`)
    }

    const ext = path.extname(modulePath)

    let format = EXTENSIONS[ext] || "module"

    // Mimic default behavior of treating .js[x]? as ESM iff
    // relevant package.json contains { "type": "module" }
    if (!ext || [".js", ".jsx"].includes(ext)) {
      const dir = path.dirname(modulePath)
      const pkgdef = findPackageJson(dir).next()
      const type = pkgdef && pkgdef.value && pkgdef.value.type
      format = type === "module" ? "module" : "dynamic"
    }

    modulePath = await fs.promises.realpath(modulePath)

    return { url: `file://${path}`, format }
  }
}

const jsdomPatch = {
  name: "jsdom-patch",
  setup(build) {
    build.onLoad({ filter: /XMLHttpRequest-impl\.js$/ }, async (args) => {
      let contents = await fs.promises.readFile(args.path, "utf8")
      contents = contents.replace(
        'const syncWorkerFile = require.resolve ? require.resolve("./xhr-sync-worker.js") : null;',
        `const syncWorkerFile = "${await requireResolve(
          "jsdom/lib/jsdom/living/xhr/xhr-sync-worker.js"
        )}";`.replaceAll("\\", process.platform === "win32" ? "\\\\" : "\\")
      )
      return { contents, loader: "js" }
    })
  },
}

esbuild
  .build({
    entryPoints: ["src/cli.ts"],
    platform: "node",
    bundle: true,
    sourcemap: true,
    outfile: "dist/cli.cjs",
    plugins: [
      sassPlugin({
        type: "style",
        async transform(source, _resolveDir, filePath) {
          const { css } = await postcss()
            .use(copyAssets({ base: `public` }))
            .process(source, { from: filePath, to: `public/index.css` })
          return css
        },
      }),
      svg(),
      jsdomPatch,
    ],
    loader: { ".js": "jsx", ".tsx": "tsx", ".ts": "tsx" },
    jsxFactory: "h",
    jsxFragment: "Fragment",
    jsx: "automatic",
    external: ["canvas"],
    inject: ["./src/document_shim.ts"],
  })
  .catch(() => process.exit(1))
// src/document_shim.js

import { JSDOM } from "jsdom"

const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`)
global.document = dom.window.document

ngmtine pushed a commit to ngmtine/middler that referenced this issue Jun 9, 2024
 memo: esbuildでjsdomバンドルできない問題の対処は以下から
 evanw/esbuild#1311
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants