Skip to content

Commit

Permalink
replace unbundled "require" with "__require" shim
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed May 15, 2021
1 parent 1bfe04d commit 869500e
Show file tree
Hide file tree
Showing 14 changed files with 316 additions and 86 deletions.
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 3 additions & 0 deletions internal/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
42 changes: 42 additions & 0 deletions internal/bundler/bundler_default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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",
},
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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,
},
Expand Down Expand Up @@ -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,
},
})
}
31 changes: 25 additions & 6 deletions internal/bundler/linker.go
Original file line number Diff line number Diff line change
Expand Up @@ -1398,19 +1398,30 @@ 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 {
record := &repr.AST.ImportRecords[importRecordIndex]

// 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
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -4093,6 +4111,7 @@ func (c *linkerContext) generateChunkJS(chunks []chunkInfo, chunkIndex int, chun
commonJSRef,
esmRef,
toModuleRef,
runtimeRequireRef,
compileResult,
dataForSourceMaps,
)
Expand Down
54 changes: 40 additions & 14 deletions internal/bundler/snapshots/snapshots_default.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1964,7 +1964,7 @@ TestNestedRequireWithoutCall
---------- /out.js ----------
// entry.js
(() => {
const req = require;
const req = __require;
req("./entry");
})();

Expand Down Expand Up @@ -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 () => {
Expand All @@ -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 {
}

Expand Down Expand Up @@ -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 ----------
Expand All @@ -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 = [];
}
Expand Down Expand Up @@ -2407,7 +2433,7 @@ console.log(require_b());
TestRequireWithoutCall
---------- /out.js ----------
// entry.js
var req = require;
var req = __require;
req("./entry");

================================================================================
Expand All @@ -2416,7 +2442,7 @@ TestRequireWithoutCallInsideTry
// entry.js
try {
oldLocale = globalLocale._abbr;
aliasedRequire = require;
aliasedRequire = __require;
aliasedRequire("./locale/" + name);
getSetGlobalLocale(oldLocale);
} catch (e) {
Expand Down
4 changes: 2 additions & 2 deletions internal/bundler/snapshots/snapshots_importstar.txt
Original file line number Diff line number Diff line change
Expand Up @@ -822,7 +822,7 @@ var mod = (() => {
__export(entry_exports, {
out: () => out
});
var out = __toModule(require("foo"));
var out = __toModule(__require("foo"));
return entry_exports;
})();

Expand Down Expand Up @@ -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;
})();

Expand Down
Loading

0 comments on commit 869500e

Please sign in to comment.