-
-
Save hyrious/7120a56c593937457c0811443563e017 to your computer and use it in GitHub Desktop.
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 } | |
} | |
}) | |
} | |
} |
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
The last way is most cheap and seems ok. I'll use that.
Some todos:
matchBrace
, since the input code mayberequire("no end string literal..
makeName
, just testcontents.includes()
should be enoughmatchBrace
can also be removed