Skip to content

Instantly share code, notes, and snippets.

@hyrious
Last active March 23, 2024 02:33
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hyrious/7120a56c593937457c0811443563e017 to your computer and use it in GitHub Desktop.
Save hyrious/7120a56c593937457c0811443563e017 to your computer and use it in GitHub Desktop.
plugin to get rid of '__require' in esbuild
var RequireToImportPlugin = {
name: 'require-to-import',
setup({ onResolve, onLoad, esbuild }) {
function matchBrace(text, from) {
if (!(text[from] === '(')) return -1;
let i, k = 1;
for (i = from + 1; i < text.length && k > 0; ++i) {
if (text[i] === '(') k++;
if (text[i] === ')') k--;
}
let to = i - 1;
if (!(text[to] === ')') || k !== 0) return -1;
return to;
}
function makeName(path) {
return path.replace(/-(\w)/g, (_, x) => x.toUpperCase())
.replace(/[^$_a-zA-Z0-9]/g, '_');
}
onLoad({ filter: /\.c?js/ }, async args => {
let contents = await fs.readFile(args.path, 'utf8')
let warnings
try {
({ warnings } = await esbuild.transform(contents, { format: 'esm', logLevel: 'silent' }))
} catch (err) {
({ warnings } = err)
}
let lines = contents.split('\n')
if (warnings && warnings.some(e => e.text.includes('"require" to "esm"'))) {
let modifications = [], imports = []
for (const { location: { line, lineText, column, length } } of warnings) {
// "require|here|("
let left = column + length
// "require('a'|here|)"
let right = matchBrace(lineText, left)
if (right === -1) continue;
// "'a'"
let raw = lineText.slice(left + 1, right)
let path
try {
// 'a'
path = eval(raw) // or, write a real js lexer to parse that
if (typeof path !== 'string') continue; // print warnings about dynamic require
} catch (e) {
continue
}
let name = `__import_${makeName(path)}`
// "import __import_a from 'a'"
let import_statement = `import ${name} from ${raw};`
// rewrite "require('a')" -> "__import_a"
let offset = lines.slice(0, line - 1).map(line => line.length).reduce((a, b) => a + 1 + b, 0)
modifications.push([offset + column, offset + right + 1, name])
imports.push(import_statement)
}
if (imports.length === 0) return null;
imports = [...new Set(imports)]
let offset = 0
for (const [start, end, name] of modifications) {
contents = contents.slice(0, start + offset) + name + contents.slice(end + offset)
offset += name.length - (end - start)
}
contents = [...imports, 'module.exports', contents].join(';') // put imports at the first line, so sourcemaps will be ok
return { contents }
}
})
}
}
@hyrious
Copy link
Author

hyrious commented Jan 13, 2022

Some todos:

  • add a max search length in matchBrace, since the input code maybe require("no end string literal..
  • prevent name conflict in makeName, just test contents.includes() should be enough
  • write a tiny lexer that only parses js strings, so that matchBrace can also be removed
  • sourcemaps, it that needed?

@hyrious
Copy link
Author

hyrious commented Jan 13, 2022

After some tests, I found that I can bundle export { render } from "react-dom" correctly in minifySyntax: true, but not minifySyntax: false. This is because it has a top level if statement:

// ./cjs/react-dom.development.js
if (process.env.NODE_ENV !== "production") {
  module.exports = "..."
}

When setting process.env.NODE_ENV to "production", this file becomes empty and esbuild can not know whether its esm or cjs -- in esm, it exports nothing; in cjs, it exports a default {}; esbuild will treat it as esm.

Then, in the entry file of react-dom, it imports that file:

import dev_module from './cjs/react-dom.development.js'
if (process.env.NODE_ENV !== "production") {
  module.exports = dev_module
} else { "..." }

This import statement will cause that error: esbuild cannot find a default export from that file.


So, a more safer way: we import the namespace: import * as ns from "some-file"; const default_value = ns["default"]. But as a prop access statement, esbuild won't perform any tree-shaking on it.

Besides, esbuild will also print another warning on it 🤷‍♂️:

 [WARNING] Import "default" will always be undefined because there is no matching export in "node_modules/react-dom/cjs/react-dom.development.js"

    node_modules/react-dom/index.js:37:58:
      37    module.exports = __import___cjs_reactDom_development_js.default;
                                                                    ~~~~~~~

Another way is we always provide a export in cjs modules, but it will includes some verbose code:

import dev_module from './cjs/react-dom.development.js'
module.exports // just a simple prop access, esbuild won't remove it and will mark this file as cjs

@hyrious
Copy link
Author

hyrious commented Jan 13, 2022

The last way is most cheap and seems ok. I'll use that.

@hyrious
Copy link
Author

hyrious commented Jan 18, 2022

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment