Open ashtonsix opened 6 months ago
the current behavior of non-resolved imports in --target=browser is intentional. it is to use a function which checks for a global require function, otherwise throw with an error message. the reason mod
is a dynamic import is because it is marked external.
relevant codegen: (this is almost identical to what esbuild outputs)
// top of file:
var __require = ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x)(function(x) {
if (typeof require !== "undefined")
return require.apply(this, arguments);
throw Error('Dynamic require of "' + x + '" is not supported');
});
// callsite:
var x = __require("mod");
What version of Bun is running?
1.1.7+b0b7db5c0
What platform is your computer?
Darwin 23.1.0 arm64 arm
What steps can reproduce the bug?
What is the expected behavior?
Build output that doesn't fail upon execution when
require()
is unavailableWhat do you see instead?
An error message that misidentifies the
require()
call as dynamic in the build output, when it is clearly static:The artifact throws this in environments where
require()
is unavailable (such as the browser).Additional information
I created a plugin that replaces static
require()
calls with ESM imports, since Bun seems to have no problem with these. The correctness and performance are decent. And thanks to the plugin I'm not affected by this issue, however I think the community would be better-served ifBun.build
were modified rather than extended.For the plugin to consider a
require
call static it must:if (condition) var x = require("mod");
var x = require("mod");
Here's the plugin source code (taken from a private repository):
Replace static require() plugin
```ts import type { BunPlugin } from "bun"; import jsTokens from "js-tokens"; // uses tokens rather than AST for perf type TokenConsumer = (tokens: jsTokens.Token[]) => number; // consumes parens and any preceding content, such as "for await (...)" const consumeParens: TokenConsumer = (tokens) => { let length = 0; let token: jsTokens.Token; let level = 0; let seen = 0; while ((token = tokens.pop()!)) { length += token.value.length; if (token.type === "Punctuator" && /^[()]$/.test(token.value)) { seen += 1; level += token.value === "(" ? 1 : -1; } if (seen >= 1 && level === 0) { return length; } } return length; }; // consumes content until end of statement, terminated by "}", ";" or "\n" const consumeStatement: TokenConsumer = (tokens) => { let length = 0; let token: jsTokens.Token; let level = 0; enum State { Operator, NotOperator, NotOperatorNewline, } let state = State.NotOperator; while ((token = tokens.pop()!)) { length += token.value.length; let isOpenTag = false; let isClosedTag = false; if (/^Punctuator$|^Template(Head|Tail)$/.test(token.type)) { isOpenTag = /^[({[]$/.test(token.value) || /Head$/.test(token.type); isClosedTag = /^[\]})]$/.test(token.value) || /Tail$/.test(token.type); state = isOpenTag || isClosedTag ? State.NotOperator : State.Operator; } if (isOpenTag) level++; else if (isClosedTag) level--; if (level || /^WhiteSpace$|Comment$/.test(token.type)) continue; // match statement-terminating "}" or ";" if ( token.type === "Punctuator" && (token.value === "}" || token.value === ";") ) { return length; } // match statement-terminating newline using state machine // looking for sequence NotOperator -> Newline -> IdentifierOrLiteral if ( state === State.NotOperator && token.type === "LineTerminatorSequence" ) { state = State.NotOperatorNewline; } else if ( state === State.NotOperatorNewline && /^IdentifierName$|Literal$/.test(token.type) ) { tokens.push(token); // restore lookahead return length - token.value.length; } else if ( state === State.NotOperatorNewline && token.type !== "LineTerminatorSequence" ) { state = State.NotOperator; } } return length; }; const consumeSeqs = { if: [consumeParens, consumeStatement], else: [consumeStatement], for: [consumeParens, consumeStatement], while: [consumeParens, consumeStatement], do: [consumeStatement, consumeParens], with: [consumeParens, consumeStatement], } as RecordextractStaticCjs test suite
```ts import { expect, describe, it } from "bun:test"; import { extractStaticCjs } from "./parse-js"; describe("extractStaticCjs", () => { it("should return an empty array if no require statements are found", () => { const src = "const x = 10;"; const result = extractStaticCjs(src); expect(result.map((e) => e.original)).toEqual([]); }); it("should skip require calls without a declaration", () => { const src = `require("mod");`; const result = extractStaticCjs(src); expect(result.map((e) => e.original)).toEqual([]); }); it("should extract simple require declarations", () => { const src = ` const mod = require("mod"); some nonsense to ignore const {mod: alias} = require("mod"); const { mod3, mod4: alias2 } = require("mod"); `; const result = extractStaticCjs(src); expect(result.map((e) => e.original.replace(/\s+/g, " "))).toEqual([ 'const mod = require("mod");', 'const {mod: alias} = require("mod");', 'const { mod3, mod4: alias2 } = require("mod");', ]); }); it("should skip complex declarations", () => { // maybe _some_ of these should be extracted? const src = ` const mod1 = condition ? require("mod1") : require("mod1-alt"); const mod2 = require("mod2") + require("mod2-alt"); const mod3 = require("mod3").item; const mod4 = require("mod4"); `; const result = extractStaticCjs(src); expect(result.map((e) => e.original)).toEqual([ `const mod4 = require("mod4");`, ]); }); it("should skip require statements inside block statements", () => { const src = ` if (condition) { const modI = require("modI"); } label: { const modI = require("modI"); } const mod1 = require("mod1"); try { const modIgnore = require("modIgnore"); } catch (e) {} const mod2 = require("mod2"); function() { const modI = require("modI"); } `; const result = extractStaticCjs(src); expect(result.map((e) => e.original)).toEqual([ `const mod1 = require("mod1");`, `const mod2 = require("mod2");`, ]); }); it("should skip require statements inside nested statements not demarcated by blocks", () => { const src = ` const mod1 = require("mod1") if (condition); else var mod2 = require("mod2") for (let i = 0; i < 10; i++) var mod3 = require("mod3") // no semicolon const mod4 = require("mod4"); (sequence, var mod5 = require("mod5")); [array, var mod6 = require("mod6")]; // comment; var mod7 = require("mod7"); if (condition) something + // operator prevents termination by newline var mod8 = require("mod8") do var mod9 = require("mod9"); while (condition) const { modA } = require("modA") `; const result = extractStaticCjs(src); expect(result.map((e) => e.original.trim())).toEqual([ 'const mod1 = require("mod1")', 'const mod4 = require("mod4");', 'const { modA } = require("modA")', ]); }); }); ```