diff --git a/CHANGELOG.md b/CHANGELOG.md index 671a4e9a595..fe0b8b94163 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,39 @@ # Changelog +## Unreleased + +* Add a shim function for unbundled uses of `require` ([#1202](https://github.com/evanw/esbuild/issues/1202)) + + Modules in CommonJS format automatically get three variables injected into their scope: `module`, `exports`, and `require`. These allow the code to import other modules and to export things from itself. The bundler automatically rewrites uses of `module` and `exports` to refer to the module's exports and certain uses of `require` to a helper function that loads the imported module. + + Not all uses of `require` can be converted though, and un-converted uses of `require` will end up in the output. This is problematic because `require` is only present at run-time if the output is run as a CommonJS module. Otherwise `require` is undefined, which means esbuild's behavior is inconsistent between compile-time and run-time. The `module` and `exports` variables are objects at compile-time and run-time but `require` is a function at compile-time and undefined at run-time. This causes code that checks for `typeof require` to have inconsistent behavior: + + ```js + if (typeof require === 'function' && typeof exports === 'object' && typeof module === 'object') { + console.log('CommonJS detected') + } + ``` + + In the above example, ideally `CommonJS detected` would always be printed since the code is being bundled with a CommonJS-aware bundler. To fix this, esbuild will now substitute references to `require` with a stub `__require` function when bundling if the output format is something other than CommonJS. This should ensure that `require` is now consistent between compile-time and run-time. When bundled, code that uses unbundled references to `require` will now look something like this: + + ```js + var __require = (x) => { + if (typeof require !== "undefined") + return require(x); + throw new Error('Dynamic require of "' + x + '" is not supported'); + }; + + var __commonJS = (cb, mod) => () => (mod || cb((mod = {exports: {}}).exports, mod), mod.exports); + + var require_example = __commonJS((exports, module) => { + if (typeof __require === "function" && typeof exports === "object" && typeof module === "object") { + console.log("CommonJS detected"); + } + }); + + require_example(); + ``` + ## 0.11.22 * Add support for the "import assertions" proposal diff --git a/internal/ast/ast.go b/internal/ast/ast.go index 8dd89c8d0c7..77776cf6c80 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -89,6 +89,9 @@ type ImportRecord struct { // Tell the printer to wrap this call to "require()" in "__toModule(...)" WrapWithToModule bool + // Tell the printer to use the runtime "__require()" instead of "require()" + CallRuntimeRequire bool + // True for the following cases: // // try { require('x') } catch { handle } diff --git a/internal/bundler/bundler_default_test.go b/internal/bundler/bundler_default_test.go index d89c6abee3f..f0aeca0dd21 100644 --- a/internal/bundler/bundler_default_test.go +++ b/internal/bundler/bundler_default_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + "github.com/evanw/esbuild/internal/compat" "github.com/evanw/esbuild/internal/config" "github.com/evanw/esbuild/internal/js_ast" "github.com/evanw/esbuild/internal/js_lexer" @@ -906,6 +907,8 @@ func TestConditionalRequireResolve(t *testing.T) { entryPaths: []string{"/a.js"}, options: config.Options{ Mode: config.ModeBundle, + Platform: config.PlatformNode, + OutputFormat: config.FormatCommonJS, AbsOutputFile: "/out.js", ExternalModules: config.ExternalModules{ NodeModules: map[string]bool{ @@ -1155,6 +1158,7 @@ func TestRequirePropertyAccessCommonJS(t *testing.T) { entryPaths: []string{"/entry.js"}, options: config.Options{ Mode: config.ModeBundle, + Platform: config.PlatformNode, OutputFormat: config.FormatCommonJS, AbsOutputFile: "/out.js", }, @@ -3563,6 +3567,8 @@ func TestRequireResolve(t *testing.T) { entryPaths: []string{"/entry.js"}, options: config.Options{ Mode: config.ModeBundle, + Platform: config.PlatformNode, + OutputFormat: config.FormatCommonJS, AbsOutputFile: "/out.js", ExternalModules: config.ExternalModules{ AbsPaths: map[string]bool{ @@ -4120,6 +4126,7 @@ func TestRequireMainCacheCommonJS(t *testing.T) { entryPaths: []string{"/entry.js"}, options: config.Options{ Mode: config.ModeBundle, + Platform: config.PlatformNode, AbsOutputFile: "/out.js", OutputFormat: config.FormatCommonJS, }, @@ -4465,3 +4472,38 @@ outside-node-modules/package.json: note: The original "b" is here `, }) } + +func TestRequireShimSubstitution(t *testing.T) { + default_suite.expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + console.log([ + require, + typeof require, + require('./example.json'), + require('./example.json', { type: 'json' }), + require(window.SOME_PATH), + module.require('./example.json'), + module.require('./example.json', { type: 'json' }), + module.require(window.SOME_PATH), + require.resolve('some-path'), + require.resolve(window.SOME_PATH), + import('some-path'), + import(window.SOME_PATH), + ]) + `, + "/example.json": `{ "works": true }`, + }, + entryPaths: []string{"/entry.js"}, + options: config.Options{ + Mode: config.ModeBundle, + AbsOutputDir: "/out", + ExternalModules: config.ExternalModules{ + NodeModules: map[string]bool{ + "some-path": true, + }, + }, + UnsupportedJSFeatures: compat.DynamicImport, + }, + }) +} diff --git a/internal/bundler/linker.go b/internal/bundler/linker.go index d14fe8ccd2f..cbd4f944269 100644 --- a/internal/bundler/linker.go +++ b/internal/bundler/linker.go @@ -1398,6 +1398,7 @@ func (c *linkerContext) scanImportsAndExports() { // Encode import-specific constraints in the dependency graph for partIndex, part := range repr.AST.Parts { toModuleUses := uint32(0) + runtimeRequireUses := uint32(0) // Imports of wrapped files must depend on the wrapper for _, importRecordIndex := range part.ImportRecordIndices { @@ -1405,12 +1406,22 @@ func (c *linkerContext) scanImportsAndExports() { // Don't follow external imports (this includes import() expressions) if !record.SourceIndex.IsValid() || c.isExternalDynamicImport(record, sourceIndex) { - // This is an external import, so it needs the "__toModule" wrapper as - // long as it's not a bare "require()" - if record.Kind != ast.ImportRequire && (!c.options.OutputFormat.KeepES6ImportExportSyntax() || - (record.Kind == ast.ImportDynamic && c.options.UnsupportedJSFeatures.Has(compat.DynamicImport))) { - record.WrapWithToModule = true - toModuleUses++ + // This is an external import. Check if it will be a "require()" call. + if record.Kind == ast.ImportRequire || !c.options.OutputFormat.KeepES6ImportExportSyntax() || + (record.Kind == ast.ImportDynamic && c.options.UnsupportedJSFeatures.Has(compat.DynamicImport)) { + // We should use "__require" instead of "require" if we're not + // generating a CommonJS output file, since it won't exist otherwise + if config.ShouldCallRuntimeRequire(c.options.Mode, c.options.OutputFormat) { + record.CallRuntimeRequire = true + runtimeRequireUses++ + } + + // It needs the "__toModule" wrapper if it wasn't originally a + // CommonJS import (i.e. it wasn't a "require()" call). + if record.Kind != ast.ImportRequire { + record.WrapWithToModule = true + toModuleUses++ + } } continue } @@ -1452,6 +1463,10 @@ func (c *linkerContext) scanImportsAndExports() { // "__toModule" symbol from the runtime to wrap the result of "require()" c.graph.GenerateRuntimeSymbolImportAndUse(sourceIndex, uint32(partIndex), "__toModule", toModuleUses) + // If there are unbundled calls to "require()" and we're not generating + // code for node, then substitute a "__require" wrapper for "require". + c.graph.GenerateRuntimeSymbolImportAndUse(sourceIndex, uint32(partIndex), "__require", runtimeRequireUses) + // If there's an ES6 export star statement of a non-ES6 module, then we're // going to need the "__reExport" symbol from the runtime reExportUses := uint32(0) @@ -3317,6 +3332,7 @@ func (c *linkerContext) generateCodeForFileInChunkJS( commonJSRef js_ast.Ref, esmRef js_ast.Ref, toModuleRef js_ast.Ref, + runtimeRequireRef js_ast.Ref, result *compileResultJS, dataForSourceMaps []dataForSourceMap, ) { @@ -3520,6 +3536,7 @@ func (c *linkerContext) generateCodeForFileInChunkJS( MangleSyntax: c.options.MangleSyntax, ASCIIOnly: c.options.ASCIIOnly, ToModuleRef: toModuleRef, + RuntimeRequireRef: runtimeRequireRef, LegalComments: c.options.LegalComments, UnsupportedFeatures: c.options.UnsupportedJSFeatures, AddSourceMappings: addSourceMappings, @@ -4059,6 +4076,7 @@ func (c *linkerContext) generateChunkJS(chunks []chunkInfo, chunkIndex int, chun commonJSRef := js_ast.FollowSymbols(c.graph.Symbols, runtimeMembers["__commonJS"].Ref) esmRef := js_ast.FollowSymbols(c.graph.Symbols, runtimeMembers["__esm"].Ref) toModuleRef := js_ast.FollowSymbols(c.graph.Symbols, runtimeMembers["__toModule"].Ref) + runtimeRequireRef := js_ast.FollowSymbols(c.graph.Symbols, runtimeMembers["__require"].Ref) r := c.renameSymbolsInChunk(chunk, chunk.filesInChunkInOrder, timer) dataForSourceMaps := c.dataForSourceMaps() @@ -4093,6 +4111,7 @@ func (c *linkerContext) generateChunkJS(chunks []chunkInfo, chunkIndex int, chun commonJSRef, esmRef, toModuleRef, + runtimeRequireRef, compileResult, dataForSourceMaps, ) diff --git a/internal/bundler/snapshots/snapshots_default.txt b/internal/bundler/snapshots/snapshots_default.txt index 70be39484cd..6daf9c5cf1b 100644 --- a/internal/bundler/snapshots/snapshots_default.txt +++ b/internal/bundler/snapshots/snapshots_default.txt @@ -288,8 +288,8 @@ var require_b = __commonJS({ }); // a.js -x ? require("a") : y ? require_b() : require("c"); -x ? y ? require("a") : require_b() : require(c); +x ? __require("a") : y ? require_b() : __require("c"); +x ? y ? __require("a") : require_b() : __require(c); ================================================================================ TestConditionalRequireResolve @@ -1964,7 +1964,7 @@ TestNestedRequireWithoutCall ---------- /out.js ---------- // entry.js (() => { - const req = require; + const req = __require; req("./entry"); })(); @@ -2202,11 +2202,11 @@ class Bar { TestRequireAndDynamicImportInvalidTemplate ---------- /out.js ---------- // entry.js -require(tag`./b`); -require(`./${b}`); +__require(tag`./b`); +__require(`./${b}`); try { - require(tag`./b`); - require(`./${b}`); + __require(tag`./b`); + __require(`./${b}`); } catch { } (async () => { @@ -2227,11 +2227,11 @@ try { TestRequireBadArgumentCount ---------- /out.js ---------- // entry.js -require(); -require("a", "b"); +__require(); +__require("a", "b"); try { - require(); - require("a", "b"); + __require(); + __require("a", "b"); } catch { } @@ -2359,6 +2359,32 @@ console.log(false); console.log(true); console.log(true); +================================================================================ +TestRequireShimSubstitution +---------- /out/entry.js ---------- +// example.json +var require_example = __commonJS({ + "example.json"(exports, module) { + module.exports = {works: true}; + } +}); + +// entry.js +console.log([ + __require, + typeof __require, + require_example(), + __require("./example.json", {type: "json"}), + __require(window.SOME_PATH), + require_example(), + __require("./example.json", {type: "json"}), + __require(window.SOME_PATH), + __require.resolve("some-path"), + __require.resolve(window.SOME_PATH), + Promise.resolve().then(() => __toModule(__require("some-path"))), + Promise.resolve().then(() => __toModule(__require(window.SOME_PATH))) +]); + ================================================================================ TestRequireTxt ---------- /out.js ---------- @@ -2379,7 +2405,7 @@ TestRequireWithCallInsideTry var require_entry = __commonJS({ "entry.js"(exports) { try { - const supportsColor = require("supports-color"); + const supportsColor = __require("supports-color"); if (supportsColor && (supportsColor.stderr || supportsColor).level >= 2) { exports.colors = []; } @@ -2407,7 +2433,7 @@ console.log(require_b()); TestRequireWithoutCall ---------- /out.js ---------- // entry.js -var req = require; +var req = __require; req("./entry"); ================================================================================ @@ -2416,7 +2442,7 @@ TestRequireWithoutCallInsideTry // entry.js try { oldLocale = globalLocale._abbr; - aliasedRequire = require; + aliasedRequire = __require; aliasedRequire("./locale/" + name); getSetGlobalLocale(oldLocale); } catch (e) { diff --git a/internal/bundler/snapshots/snapshots_importstar.txt b/internal/bundler/snapshots/snapshots_importstar.txt index b7293c03aea..fb62addc897 100644 --- a/internal/bundler/snapshots/snapshots_importstar.txt +++ b/internal/bundler/snapshots/snapshots_importstar.txt @@ -822,7 +822,7 @@ var mod = (() => { __export(entry_exports, { out: () => out }); - var out = __toModule(require("foo")); + var out = __toModule(__require("foo")); return entry_exports; })(); @@ -868,7 +868,7 @@ TestReExportStarExternalIIFE var mod = (() => { // entry.js var entry_exports = {}; - __reExport(entry_exports, __toModule(require("foo"))); + __reExport(entry_exports, __toModule(__require("foo"))); return entry_exports; })(); diff --git a/internal/bundler/snapshots/snapshots_splitting.txt b/internal/bundler/snapshots/snapshots_splitting.txt index 8c66d0f054a..8ef12df8f03 100644 --- a/internal/bundler/snapshots/snapshots_splitting.txt +++ b/internal/bundler/snapshots/snapshots_splitting.txt @@ -205,19 +205,19 @@ TestSplittingDynamicAndNotDynamicCommonJSIntoES6 import { __toModule, require_foo -} from "./chunk-Y5X7B3LP.js"; +} from "./chunk-76OUUS4B.js"; // entry.js var import_foo = __toModule(require_foo()); -import("./foo-WILI25IU.js").then(({default: {bar: b}}) => console.log(import_foo.bar, b)); +import("./foo-O3QA5LZB.js").then(({default: {bar: b}}) => console.log(import_foo.bar, b)); ----------- /out/foo-WILI25IU.js ---------- +---------- /out/foo-O3QA5LZB.js ---------- import { require_foo -} from "./chunk-Y5X7B3LP.js"; +} from "./chunk-76OUUS4B.js"; export default require_foo(); ----------- /out/chunk-Y5X7B3LP.js ---------- +---------- /out/chunk-76OUUS4B.js ---------- // foo.js var require_foo = __commonJS({ "foo.js"(exports) { @@ -260,9 +260,9 @@ export { TestSplittingDynamicCommonJSIntoES6 ---------- /out/entry.js ---------- // entry.js -import("./foo-G6GTLWFE.js").then(({default: {bar}}) => console.log(bar)); +import("./foo-WDO3WAB7.js").then(({default: {bar}}) => console.log(bar)); ----------- /out/foo-G6GTLWFE.js ---------- +---------- /out/foo-WDO3WAB7.js ---------- // foo.js var require_foo = __commonJS({ "foo.js"(exports) { @@ -317,7 +317,7 @@ TestSplittingHybridESMAndCJSIssue617 import { foo, init_a -} from "./chunk-3XIBLUW3.js"; +} from "./chunk-OU4GWHY4.js"; init_a(); export { foo @@ -327,7 +327,7 @@ export { import { a_exports, init_a -} from "./chunk-3XIBLUW3.js"; +} from "./chunk-OU4GWHY4.js"; // b.js var bar = (init_a(), a_exports); @@ -335,7 +335,7 @@ export { bar }; ----------- /out/chunk-3XIBLUW3.js ---------- +---------- /out/chunk-OU4GWHY4.js ---------- // a.js var a_exports = {}; __export(a_exports, { @@ -485,7 +485,7 @@ TestSplittingSharedCommonJSIntoES6 ---------- /out/a.js ---------- import { require_shared -} from "./chunk-2VTMOLG3.js"; +} from "./chunk-SDUKV4IM.js"; // a.js var {foo} = require_shared(); @@ -494,13 +494,13 @@ console.log(foo); ---------- /out/b.js ---------- import { require_shared -} from "./chunk-2VTMOLG3.js"; +} from "./chunk-SDUKV4IM.js"; // b.js var {foo} = require_shared(); console.log(foo); ----------- /out/chunk-2VTMOLG3.js ---------- +---------- /out/chunk-SDUKV4IM.js ---------- // shared.js var require_shared = __commonJS({ "shared.js"(exports) { diff --git a/internal/config/config.go b/internal/config/config.go index 0847943eb7b..a8e6d3f2b54 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -375,6 +375,10 @@ func IsTreeShakingEnabled(mode Mode, outputFormat Format) bool { return mode == ModeBundle || (mode == ModeConvertFormat && outputFormat == FormatIIFE) } +func ShouldCallRuntimeRequire(mode Mode, outputFormat Format) bool { + return mode == ModeBundle && outputFormat != FormatCommonJS +} + type InjectedDefine struct { Source logger.Source Data js_ast.E diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index 7914edb6bca..c1677fd8e5c 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -1570,7 +1570,7 @@ func (p *parser) ignoreUsage(ref js_ast.Ref) { // the value is ignored because that's what the TypeScript compiler does. } -func (p *parser) callRuntime(loc logger.Loc, name string, args []js_ast.Expr) js_ast.Expr { +func (p *parser) importFromRuntime(loc logger.Loc, name string) js_ast.Expr { ref, ok := p.runtimeImports[name] if !ok { ref = p.newSymbol(js_ast.SymbolOther, name) @@ -1578,12 +1578,26 @@ func (p *parser) callRuntime(loc logger.Loc, name string, args []js_ast.Expr) js p.runtimeImports[name] = ref } p.recordUsage(ref) + return js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: ref}} +} + +func (p *parser) callRuntime(loc logger.Loc, name string, args []js_ast.Expr) js_ast.Expr { return js_ast.Expr{Loc: loc, Data: &js_ast.ECall{ - Target: js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: ref}}, + Target: p.importFromRuntime(loc, name), Args: args, }} } +func (p *parser) valueToSubstituteForRequire(loc logger.Loc) js_ast.Expr { + if p.source.Index != runtime.SourceIndex && + config.ShouldCallRuntimeRequire(p.options.mode, p.options.outputFormat) { + return p.importFromRuntime(loc, "__require") + } + + p.recordUsage(p.requireRef) + return js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: p.requireRef}} +} + func (p *parser) makePromiseRef() js_ast.Ref { if p.promiseRef == js_ast.InvalidRef { p.promiseRef = p.newSymbol(js_ast.SymbolUnbound, "Promise") @@ -9701,7 +9715,7 @@ func (p *parser) jsxStringsToMemberExpression(loc logger.Loc, parts []string) js // Substitute user-specified defines if define.Data.DefineFunc != nil { - return p.valueForDefine(loc, js_ast.AssignTargetNone, false, define.Data.DefineFunc) + return p.valueForDefine(loc, define.Data.DefineFunc, identifierOpts{}) } } } @@ -9918,6 +9932,7 @@ func (p *parser) maybeRewritePropertyAccess( p.recordUsage(item.Ref) return p.handleIdentifier(nameLoc, &js_ast.EIdentifier{Ref: item.Ref}, identifierOpts{ assignTarget: assignTarget, + isCallTarget: isCallTarget, isDeleteTarget: isDeleteTarget, preferQuotedKey: preferQuotedKey, @@ -9931,6 +9946,10 @@ func (p *parser) maybeRewritePropertyAccess( // See https://github.com/webpack/webpack/pull/7750 for more info. if isCallTarget && id.Ref == p.moduleRef && name == "require" { p.ignoreUsage(p.moduleRef) + + // This uses "require" instead of a reference to our "__require" + // function so that the code coming up that detects calls to + // "require" will recognize it. p.recordUsage(p.requireRef) return js_ast.Expr{Loc: nameLoc, Data: &js_ast.EIdentifier{Ref: p.requireRef}}, true } @@ -10435,6 +10454,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO case *js_ast.EImportMeta: isDeleteTarget := e == p.deleteTarget + isCallTarget := e == p.callTarget // Check both user-specified defines and known globals if defines, ok := p.options.defines.DotDefines["meta"]; ok { @@ -10442,7 +10462,11 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO if p.isDotDefineMatch(expr, define.Parts) { // Substitute user-specified defines if define.Data.DefineFunc != nil { - return p.valueForDefine(expr.Loc, in.assignTarget, isDeleteTarget, define.Data.DefineFunc), exprOut{} + return p.valueForDefine(expr.Loc, define.Data.DefineFunc, identifierOpts{ + assignTarget: in.assignTarget, + isCallTarget: isCallTarget, + isDeleteTarget: isDeleteTarget, + }), exprOut{} } } } @@ -10458,6 +10482,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO e.Value = p.visitExpr(e.Value) case *js_ast.EIdentifier: + isCallTarget := e == p.callTarget isDeleteTarget := e == p.deleteTarget name := p.loadNameFromRef(e.Ref) if p.isStrictMode() && js_lexer.StrictModeReservedWords[name] { @@ -10486,7 +10511,11 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO if p.symbols[e.Ref.InnerIndex].Kind == js_ast.SymbolUnbound && !result.isInsideWithScope && e != p.deleteTarget { if data, ok := p.options.defines.IdentifierDefines[name]; ok { if data.DefineFunc != nil { - new := p.valueForDefine(expr.Loc, in.assignTarget, isDeleteTarget, data.DefineFunc) + new := p.valueForDefine(expr.Loc, data.DefineFunc, identifierOpts{ + assignTarget: in.assignTarget, + isCallTarget: isCallTarget, + isDeleteTarget: isDeleteTarget, + }) // Don't substitute an identifier for a non-identifier if this is an // assignment target, since it'll cause a syntax error @@ -10507,6 +10536,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO return p.handleIdentifier(expr.Loc, e, identifierOpts{ assignTarget: in.assignTarget, + isCallTarget: isCallTarget, isDeleteTarget: isDeleteTarget, wasOriginallyIdentifier: true, }), exprOut{} @@ -11345,7 +11375,11 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO if p.isDotDefineMatch(expr, define.Parts) { // Substitute user-specified defines if define.Data.DefineFunc != nil { - return p.valueForDefine(expr.Loc, in.assignTarget, isDeleteTarget, define.Data.DefineFunc), exprOut{} + return p.valueForDefine(expr.Loc, define.Data.DefineFunc, identifierOpts{ + assignTarget: in.assignTarget, + isCallTarget: isCallTarget, + isDeleteTarget: isDeleteTarget, + }), exprOut{} } // Copy the side effect flags over in case this expression is unused @@ -11820,7 +11854,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO if p.options.unsupportedJSFeatures.Has(compat.DynamicImport) { var then js_ast.Expr value := p.callRuntime(arg.Loc, "__toModule", []js_ast.Expr{{Loc: expr.Loc, Data: &js_ast.ECall{ - Target: js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EIdentifier{Ref: p.requireRef}}, + Target: p.valueToSubstituteForRequire(expr.Loc), Args: []js_ast.Expr{arg}, }}}) body := js_ast.FnBody{Loc: expr.Loc, Stmts: []js_ast.Stmt{{Loc: expr.Loc, Data: &js_ast.SReturn{Value: &value}}}} @@ -11903,37 +11937,39 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO // Recognize "require.resolve()" calls if couldBeRequireResolve { if dot, ok := e.Target.Data.(*js_ast.EDot); ok { - if id, ok := dot.Target.Data.(*js_ast.EIdentifier); ok && id.Ref == p.requireRef { - return p.maybeTransposeIfExprChain(e.Args[0], func(arg js_ast.Expr) js_ast.Expr { - if str, ok := e.Args[0].Data.(*js_ast.EString); ok { - // Ignore calls to require.resolve() if the control flow is provably - // dead here. We don't want to spend time scanning the required files - // if they will never be used. - if p.isControlFlowDead { - return js_ast.Expr{Loc: arg.Loc, Data: &js_ast.ENull{}} - } + if id, ok := dot.Target.Data.(*js_ast.EIdentifier); ok { + if id.Ref == p.requireRef { + p.ignoreUsage(p.requireRef) + return p.maybeTransposeIfExprChain(e.Args[0], func(arg js_ast.Expr) js_ast.Expr { + if str, ok := e.Args[0].Data.(*js_ast.EString); ok { + // Ignore calls to require.resolve() if the control flow is provably + // dead here. We don't want to spend time scanning the required files + // if they will never be used. + if p.isControlFlowDead { + return js_ast.Expr{Loc: arg.Loc, Data: &js_ast.ENull{}} + } - importRecordIndex := p.addImportRecord(ast.ImportRequireResolve, e.Args[0].Loc, js_lexer.UTF16ToString(str.Value), nil) - p.importRecords[importRecordIndex].HandlesImportErrors = p.fnOrArrowDataVisit.tryBodyCount != 0 - p.importRecordsForCurrentPart = append(p.importRecordsForCurrentPart, importRecordIndex) + importRecordIndex := p.addImportRecord(ast.ImportRequireResolve, e.Args[0].Loc, js_lexer.UTF16ToString(str.Value), nil) + p.importRecords[importRecordIndex].HandlesImportErrors = p.fnOrArrowDataVisit.tryBodyCount != 0 + p.importRecordsForCurrentPart = append(p.importRecordsForCurrentPart, importRecordIndex) - // Create a new expression to represent the operation - p.ignoreUsage(p.requireRef) - return js_ast.Expr{Loc: arg.Loc, Data: &js_ast.ERequireResolveString{ - ImportRecordIndex: importRecordIndex, - }} - } + // Create a new expression to represent the operation + return js_ast.Expr{Loc: arg.Loc, Data: &js_ast.ERequireResolveString{ + ImportRecordIndex: importRecordIndex, + }} + } - // Otherwise just return a clone of the "require.resolve()" call - return js_ast.Expr{Loc: arg.Loc, Data: &js_ast.ECall{ - Target: js_ast.Expr{Loc: e.Target.Loc, Data: &js_ast.EDot{ - Target: js_ast.Expr{Loc: dot.Target.Loc, Data: &js_ast.EIdentifier{Ref: id.Ref}}, - Name: dot.Name, - NameLoc: dot.NameLoc, - }}, - Args: []js_ast.Expr{arg}, - }} - }), exprOut{} + // Otherwise just return a clone of the "require.resolve()" call + return js_ast.Expr{Loc: arg.Loc, Data: &js_ast.ECall{ + Target: js_ast.Expr{Loc: e.Target.Loc, Data: &js_ast.EDot{ + Target: p.valueToSubstituteForRequire(dot.Target.Loc), + Name: dot.Name, + NameLoc: dot.NameLoc, + }}, + Args: []js_ast.Expr{arg}, + }} + }), exprOut{} + } } } } @@ -12035,6 +12071,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO if p.options.mode == config.ModeBundle { // There must be one argument if len(e.Args) == 1 { + p.ignoreUsage(p.requireRef) return p.maybeTransposeIfExprChain(e.Args[0], func(arg js_ast.Expr) js_ast.Expr { // The argument must be a string if str, ok := arg.Data.(*js_ast.EString); ok { @@ -12050,7 +12087,6 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO p.importRecordsForCurrentPart = append(p.importRecordsForCurrentPart, importRecordIndex) // Create a new expression to represent the operation - p.ignoreUsage(p.requireRef) return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ERequireString{ ImportRecordIndex: importRecordIndex, }} @@ -12063,7 +12099,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO // Otherwise just return a clone of the "require()" call return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ECall{ - Target: js_ast.Expr{Loc: e.Target.Loc, Data: &js_ast.EIdentifier{Ref: id.Ref}}, + Target: p.valueToSubstituteForRequire(e.Target.Loc), Args: []js_ast.Expr{arg}, }} }), exprOut{} @@ -12072,6 +12108,11 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO text := fmt.Sprintf("This call to \"require\" will not be bundled because it has %d arguments", len(e.Args)) p.log.AddRangeDebug(&p.tracker, r, text) } + + return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ECall{ + Target: p.valueToSubstituteForRequire(e.Target.Loc), + Args: e.Args, + }}, exprOut{} } else if p.options.outputFormat == config.FormatESModule && !omitWarnings { r := js_lexer.RangeOfIdentifier(p.source, e.Target.Loc) p.log.AddRangeWarning(&p.tracker, r, "Converting \"require\" to \"esm\" is currently not supported") @@ -12230,24 +12271,22 @@ func (p *parser) warnAboutImportNamespaceCallOrConstruct(target js_ast.Expr, isC } } -func (p *parser) valueForDefine(loc logger.Loc, assignTarget js_ast.AssignTarget, isDeleteTarget bool, defineFunc config.DefineFunc) js_ast.Expr { +func (p *parser) valueForDefine(loc logger.Loc, defineFunc config.DefineFunc, opts identifierOpts) js_ast.Expr { expr := js_ast.Expr{Loc: loc, Data: defineFunc(config.DefineArgs{ Loc: loc, FindSymbol: p.findSymbolHelper, SymbolForDefine: p.symbolForDefineHelper, })} if id, ok := expr.Data.(*js_ast.EIdentifier); ok { - return p.handleIdentifier(loc, id, identifierOpts{ - assignTarget: assignTarget, - isDeleteTarget: isDeleteTarget, - wasOriginallyIdentifier: true, - }) + opts.wasOriginallyIdentifier = true + return p.handleIdentifier(loc, id, opts) } return expr } type identifierOpts struct { assignTarget js_ast.AssignTarget + isCallTarget bool isDeleteTarget bool preferQuotedKey bool wasOriginallyIdentifier bool @@ -12302,6 +12341,11 @@ func (p *parser) handleIdentifier(loc logger.Loc, e *js_ast.EIdentifier, opts id } } + // Swap references to the global "require" function with our "__require" stub + if ref == p.requireRef && !opts.isCallTarget { + return p.valueToSubstituteForRequire(loc) + } + return js_ast.Expr{Loc: loc, Data: e} } diff --git a/internal/js_printer/js_printer.go b/internal/js_printer/js_printer.go index da8ac2b8dfe..8607d2ea899 100644 --- a/internal/js_printer/js_printer.go +++ b/internal/js_printer/js_printer.go @@ -1235,8 +1235,16 @@ func (p *printer) printRequireOrImportExpr( p.print("(") defer p.print(")") } - p.printSpaceBeforeIdentifier() - p.print("require(") + + // Potentially substitute our own "__require" stub for "require" + if record.CallRuntimeRequire { + p.printSymbol(p.options.RuntimeRequireRef) + } else { + p.printSpaceBeforeIdentifier() + p.print("require") + } + + p.print("(") p.addSourceMapping(record.Range.Loc) p.printQuotedUTF8(record.Path.Text, true /* allowBacktick */) p.print(")") @@ -1261,8 +1269,15 @@ func (p *printer) printRequireOrImportExpr( defer p.print(")") } - p.printSpaceBeforeIdentifier() - p.print("require(") + // Potentially substitute our own "__require" stub for "require" + if record.CallRuntimeRequire { + p.printSymbol(p.options.RuntimeRequireRef) + } else { + p.printSpaceBeforeIdentifier() + p.print("require") + } + + p.print("(") defer p.print(")") } if len(leadingInteriorComments) > 0 { @@ -3148,6 +3163,7 @@ type Options struct { AddSourceMappings bool Indent int ToModuleRef js_ast.Ref + RuntimeRequireRef js_ast.Ref UnsupportedFeatures compat.JSFeature RequireOrImportMetaForSource func(uint32) RequireOrImportMeta diff --git a/internal/runtime/runtime.go b/internal/runtime/runtime.go index f6d02964ea5..c0e357d5b11 100644 --- a/internal/runtime/runtime.go +++ b/internal/runtime/runtime.go @@ -115,6 +115,12 @@ func code(isES6 bool) string { // Tells importing modules that this can be considered an ES6 module export var __name = (target, value) => __defProp(target, 'name', { value, configurable: true }) + // This fallback "require" function exists so that "typeof require" can naturally be "function" + export var __require = x => { + if (typeof require !== 'undefined') return require(x) + throw new Error('Dynamic require of "' + x + '" is not supported') + } + // For object rest patterns export var __restKey = key => typeof key === 'symbol' ? key : key + '' export var __objRest = (source, exclude) => { diff --git a/scripts/end-to-end-tests.js b/scripts/end-to-end-tests.js index 9b51a6a930f..0434cd12662 100644 --- a/scripts/end-to-end-tests.js +++ b/scripts/end-to-end-tests.js @@ -592,6 +592,42 @@ setTimeout(() => require('./a'), 0) `, }), + + // Test the run-time value of "typeof require" + test(['--bundle', 'in.js', '--outfile=out.js', '--format=iife'], { + 'in.js': `check(typeof require)`, + 'node.js': ` + const out = require('fs').readFileSync(__dirname + '/out.js', 'utf8') + const check = x => value = x + let value + new Function('check', 'require', out)(check) + if (value !== 'function') throw 'fail' + `, + }), + test(['--bundle', 'in.js', '--outfile=out.js', '--format=esm'], { + 'in.js': `check(typeof require)`, + 'node.js': ` + import fs from 'fs' + import path from 'path' + import url from 'url' + const __dirname = path.dirname(url.fileURLToPath(import.meta.url)) + const out = fs.readFileSync(__dirname + '/out.js', 'utf8') + const check = x => value = x + let value + new Function('check', 'require', out)(check) + if (value !== 'function') throw 'fail' + `, + }), + test(['--bundle', 'in.js', '--outfile=out.js', '--format=cjs'], { + 'in.js': `check(typeof require)`, + 'node.js': ` + const out = require('fs').readFileSync(__dirname + '/out.js', 'utf8') + const check = x => value = x + let value + new Function('check', 'require', out)(check) + if (value !== 'undefined') throw 'fail' + `, + }), ) // Test internal CommonJS export diff --git a/scripts/js-api-tests.js b/scripts/js-api-tests.js index 8fd4364cdca..cd9b53ac7a5 100644 --- a/scripts/js-api-tests.js +++ b/scripts/js-api-tests.js @@ -1967,11 +1967,10 @@ console.log("success"); write: false, bundle: true, external: ['/assets/*.png'], + platform: 'node', }) - assert.strictEqual(outputFiles[0].text, `(() => { - // - require("/assets/file.png"); -})(); + assert.strictEqual(outputFiles[0].text, `// +require("/assets/file.png"); `) }, diff --git a/scripts/plugin-tests.js b/scripts/plugin-tests.js index 1002172d571..5bf797cff3a 100644 --- a/scripts/plugin-tests.js +++ b/scripts/plugin-tests.js @@ -1772,6 +1772,7 @@ let pluginTests = { bundle: true, write: false, logLevel: 'silent', + platform: 'node', plugins: [{ name: 'plugin', setup(build) {