mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
Compare commits
4 Commits
claude/fix
...
claude/rem
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a113328c4 | ||
|
|
d75589f2d6 | ||
|
|
8bdf0765c1 | ||
|
|
18752929dc |
@@ -945,130 +945,25 @@ void populateESMExports(
|
||||
JSC::JSGlobalObject* globalObject,
|
||||
JSValue result,
|
||||
Vector<JSC::Identifier, 4>& exportNames,
|
||||
JSC::MarkedArgumentBuffer& exportValues,
|
||||
bool ignoreESModuleAnnotation)
|
||||
JSC::MarkedArgumentBuffer& exportValues)
|
||||
{
|
||||
auto& vm = JSC::getVM(globalObject);
|
||||
const Identifier& esModuleMarker = vm.propertyNames->__esModule;
|
||||
|
||||
// Bun's interpretation of the "__esModule" annotation:
|
||||
//
|
||||
// - If a "default" export does not exist OR the __esModule annotation is not present, then we
|
||||
// set the default export to the exports object
|
||||
//
|
||||
// - If a "default" export also exists, then we set the default export
|
||||
// to the value of it (matching Babel behavior)
|
||||
//
|
||||
// https://stackoverflow.com/questions/50943704/whats-the-purpose-of-object-definepropertyexports-esmodule-value-0
|
||||
// https://github.com/nodejs/node/issues/40891
|
||||
// https://github.com/evanw/bundler-esm-cjs-tests
|
||||
// https://github.com/evanw/esbuild/issues/1591
|
||||
// https://github.com/oven-sh/bun/issues/3383
|
||||
//
|
||||
// Note that this interpretation is slightly different
|
||||
//
|
||||
// - We do not ignore when "type": "module" or when the file
|
||||
// extension is ".mjs". Build tools determine that based on the
|
||||
// caller's behavior, but in a JS runtime, there is only one ModuleNamespaceObject.
|
||||
//
|
||||
// It would be possible to match the behavior at runtime, but
|
||||
// it would need further engine changes which do not match the ES Module spec
|
||||
//
|
||||
// - We ignore the value of the annotation. We only look for the
|
||||
// existence of the value being set. This is for performance reasons, but also
|
||||
// this annotation is meant for tooling and the only usages of setting
|
||||
// it to something that does NOT evaluate to "true" I could find were in
|
||||
// unit tests of build tools. Happy to revisit this if users file an issue.
|
||||
bool needsToAssignDefault = true;
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
if (auto* exports = result.getObject()) {
|
||||
bool hasESModuleMarker = false;
|
||||
if (!ignoreESModuleAnnotation) {
|
||||
PropertySlot slot(exports, PropertySlot::InternalMethodType::VMInquiry, &vm);
|
||||
auto has = exports->getPropertySlot(globalObject, esModuleMarker, slot);
|
||||
scope.assertNoException();
|
||||
if (has) {
|
||||
JSValue value = slot.getValue(globalObject, esModuleMarker);
|
||||
CLEAR_IF_EXCEPTION(scope);
|
||||
if (!value.isUndefinedOrNull()) {
|
||||
if (value.pureToBoolean() == TriState::True) {
|
||||
hasESModuleMarker = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// After removing the __esModule workaround, CommonJS modules always
|
||||
// export the entire exports object as the default export.
|
||||
// Named exports are still extracted from the exports object.
|
||||
|
||||
if (auto* exports = result.getObject()) {
|
||||
auto* structure = exports->structure();
|
||||
|
||||
uint32_t size = structure->inlineSize() + structure->outOfLineSize();
|
||||
exportNames.reserveCapacity(size + 2);
|
||||
exportValues.ensureCapacity(size + 2);
|
||||
exportNames.reserveCapacity(size + 1);
|
||||
exportValues.ensureCapacity(size + 1);
|
||||
|
||||
CLEAR_IF_EXCEPTION(scope);
|
||||
|
||||
if (hasESModuleMarker) {
|
||||
if (canPerformFastEnumeration(structure)) {
|
||||
exports->structure()->forEachProperty(vm, [&](const PropertyTableEntry& entry) -> bool {
|
||||
auto key = entry.key();
|
||||
if (key->isSymbol() || key == esModuleMarker)
|
||||
return true;
|
||||
|
||||
needsToAssignDefault = needsToAssignDefault && key != vm.propertyNames->defaultKeyword;
|
||||
|
||||
JSValue value = exports->getDirect(entry.offset());
|
||||
|
||||
exportNames.append(Identifier::fromUid(vm, key));
|
||||
exportValues.append(value);
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
JSC::PropertyNameArray properties(vm, JSC::PropertyNameMode::Strings, JSC::PrivateSymbolMode::Exclude);
|
||||
exports->methodTable()->getOwnPropertyNames(exports, globalObject, properties, DontEnumPropertiesMode::Exclude);
|
||||
if (scope.exception()) [[unlikely]] {
|
||||
if (!vm.hasPendingTerminationException()) scope.clearException();
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto property : properties) {
|
||||
if (property.isEmpty() || property.isNull() || property == esModuleMarker || property.isPrivateName() || property.isSymbol()) [[unlikely]]
|
||||
continue;
|
||||
|
||||
// ignore constructor
|
||||
if (property == vm.propertyNames->constructor)
|
||||
continue;
|
||||
|
||||
JSC::PropertySlot slot(exports, PropertySlot::InternalMethodType::Get);
|
||||
auto has = exports->getPropertySlot(globalObject, property, slot);
|
||||
RETURN_IF_EXCEPTION(scope, );
|
||||
if (!has) continue;
|
||||
|
||||
// Allow DontEnum properties which are not getter/setters
|
||||
// https://github.com/oven-sh/bun/issues/4432
|
||||
if (slot.attributes() & PropertyAttribute::DontEnum) {
|
||||
if (!(slot.isValue() || slot.isCustom())) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
exportNames.append(property);
|
||||
|
||||
JSValue getterResult = slot.getValue(globalObject, property);
|
||||
|
||||
// If it throws, we keep them in the exports list, but mark it as undefined
|
||||
// This is consistent with what Node.js does.
|
||||
if (scope.exception()) [[unlikely]] {
|
||||
scope.clearException();
|
||||
getterResult = jsUndefined();
|
||||
}
|
||||
|
||||
exportValues.append(getterResult);
|
||||
|
||||
needsToAssignDefault = needsToAssignDefault && property != vm.propertyNames->defaultKeyword;
|
||||
}
|
||||
}
|
||||
|
||||
} else if (canPerformFastEnumeration(structure)) {
|
||||
if (canPerformFastEnumeration(structure)) {
|
||||
exports->structure()->forEachProperty(vm, [&](const PropertyTableEntry& entry) -> bool {
|
||||
auto key = entry.key();
|
||||
if (key->isSymbol() || key == vm.propertyNames->defaultKeyword)
|
||||
@@ -1125,10 +1020,9 @@ void populateESMExports(
|
||||
}
|
||||
}
|
||||
|
||||
if (needsToAssignDefault) {
|
||||
exportNames.append(vm.propertyNames->defaultKeyword);
|
||||
exportValues.append(result);
|
||||
}
|
||||
// Always assign the entire exports object as the default export
|
||||
exportNames.append(vm.propertyNames->defaultKeyword);
|
||||
exportValues.append(result);
|
||||
}
|
||||
|
||||
void JSCommonJSModule::toSyntheticSource(JSC::JSGlobalObject* globalObject,
|
||||
@@ -1140,7 +1034,7 @@ void JSCommonJSModule::toSyntheticSource(JSC::JSGlobalObject* globalObject,
|
||||
auto result = this->exportsObject();
|
||||
RETURN_IF_EXCEPTION(scope, );
|
||||
|
||||
RELEASE_AND_RETURN(scope, populateESMExports(globalObject, result, exportNames, exportValues, this->ignoreESModuleAnnotation));
|
||||
RELEASE_AND_RETURN(scope, populateESMExports(globalObject, result, exportNames, exportValues));
|
||||
}
|
||||
|
||||
void JSCommonJSModule::setExportsObject(JSC::JSValue exportsObject)
|
||||
@@ -1364,7 +1258,6 @@ void JSCommonJSModule::evaluate(
|
||||
}
|
||||
|
||||
auto sourceProvider = Zig::SourceProvider::create(jsCast<Zig::GlobalObject*>(globalObject), source, JSC::SourceProviderSourceType::Program, isBuiltIn);
|
||||
this->ignoreESModuleAnnotation = source.tag == ResolvedSourceTagPackageJSONTypeModule;
|
||||
if (this->hasEvaluated)
|
||||
return;
|
||||
|
||||
@@ -1433,7 +1326,6 @@ std::optional<JSC::SourceCode> createCommonJSModule(
|
||||
|
||||
JSValue entry = globalObject->requireMap()->get(globalObject, requireMapKey);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
bool ignoreESModuleAnnotation = source.tag == ResolvedSourceTagPackageJSONTypeModule;
|
||||
SourceOrigin sourceOrigin;
|
||||
|
||||
if (entry) {
|
||||
@@ -1481,8 +1373,6 @@ std::optional<JSC::SourceCode> createCommonJSModule(
|
||||
sourceOrigin = Zig::toSourceOrigin(sourceURL, isBuiltIn);
|
||||
}
|
||||
|
||||
moduleObject->ignoreESModuleAnnotation = ignoreESModuleAnnotation;
|
||||
|
||||
return JSC::SourceCode(
|
||||
JSC::SyntheticSourceProvider::create(
|
||||
[](JSC::JSGlobalObject* lexicalGlobalObject,
|
||||
|
||||
@@ -29,8 +29,7 @@ void populateESMExports(
|
||||
JSC::JSGlobalObject* globalObject,
|
||||
JSC::JSValue result,
|
||||
WTF::Vector<JSC::Identifier, 4>& exportNames,
|
||||
JSC::MarkedArgumentBuffer& exportValues,
|
||||
bool ignoreESModuleAnnotation);
|
||||
JSC::MarkedArgumentBuffer& exportValues);
|
||||
|
||||
class JSCommonJSModule final : public JSC::JSDestructibleObject {
|
||||
public:
|
||||
@@ -71,7 +70,6 @@ public:
|
||||
// compile function is not stored here, but in
|
||||
mutable JSC::WriteBarrier<Unknown> m_overriddenCompile;
|
||||
|
||||
bool ignoreESModuleAnnotation { false };
|
||||
JSC::SourceCode sourceCode = JSC::SourceCode();
|
||||
|
||||
static size_t estimatedSize(JSC::JSCell* cell, JSC::VM& vm);
|
||||
|
||||
@@ -416,11 +416,8 @@ pub fn createExportsForFile(
|
||||
c.graph.ast.items(.flags)[id].uses_exports_ref = true;
|
||||
}
|
||||
|
||||
// Decorate "module.exports" with the "__esModule" flag to indicate that
|
||||
// we used to be an ES module. This is done by wrapping the exports object
|
||||
// instead of by mutating the exports object because other modules in the
|
||||
// bundle (including the entry point module) may do "import * as" to get
|
||||
// access to the exports object and should NOT see the "__esModule" flag.
|
||||
// Export the ES module exports as CommonJS exports.
|
||||
// Wrap in __toCommonJS to add __esModule marker for interoperability
|
||||
if (force_include_exports_for_entry_point) {
|
||||
const toCommonJSRef = c.runtimeFunction("__toCommonJS");
|
||||
|
||||
|
||||
@@ -235,7 +235,6 @@ pub fn generateCodeForFileInChunkJS(
|
||||
if (prop.key == null or prop.key.?.data != .e_string or prop.value == null) continue;
|
||||
const name = prop.key.?.data.e_string.slice(temp_allocator);
|
||||
if (strings.eqlComptime(name, "default") or
|
||||
strings.eqlComptime(name, "__esModule") or
|
||||
!bun.js_lexer.isIdentifier(name)) continue;
|
||||
|
||||
if (resolved_exports.get(name)) |export_data| {
|
||||
|
||||
@@ -310,7 +310,7 @@ pub fn generateCodeForLazyExport(this: *LinkerContext, source_index: Index.Int)
|
||||
for (expr.data.e_object.properties.slice()) |property_| {
|
||||
const property: G.Property = property_;
|
||||
if (property.key == null or property.key.?.data != .e_string or property.value == null or
|
||||
property.key.?.data.e_string.eqlComptime("default") or property.key.?.data.e_string.eqlComptime("__esModule"))
|
||||
property.key.?.data.e_string.eqlComptime("default"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -116,21 +116,6 @@ export function overridableRequire(this: JSCommonJSModule, originalId: string, o
|
||||
// If we can pull out a ModuleNamespaceObject, let's do it.
|
||||
if (esm?.evaluated && (esm.state ?? 0) >= $ModuleReady) {
|
||||
const namespace = Loader.getModuleNamespaceObject(esm!.module);
|
||||
// In Bun, when __esModule is not defined, it's a CustomAccessor on the prototype.
|
||||
// Various libraries expect __esModule to be set when using ESM from require().
|
||||
// We don't want to always inject the __esModule export into every module,
|
||||
// And creating an Object wrapper causes the actual exports to not be own properties.
|
||||
// So instead of either of those, we make it so that the __esModule property can be set at runtime.
|
||||
// It only supports "true" and undefined. Anything non-truthy is treated as undefined.
|
||||
// https://github.com/oven-sh/bun/issues/14411
|
||||
if (namespace.__esModule === undefined) {
|
||||
try {
|
||||
namespace.__esModule = true;
|
||||
} catch {
|
||||
// https://github.com/oven-sh/bun/issues/17816
|
||||
}
|
||||
}
|
||||
|
||||
return (mod.exports = namespace["module.exports"] ?? namespace);
|
||||
}
|
||||
}
|
||||
@@ -323,21 +308,6 @@ export function requireESMFromHijackedExtension(this: JSCommonJSModule, id: stri
|
||||
// If we can pull out a ModuleNamespaceObject, let's do it.
|
||||
if (esm?.evaluated && (esm.state ?? 0) >= $ModuleReady) {
|
||||
const namespace = Loader.getModuleNamespaceObject(esm!.module);
|
||||
// In Bun, when __esModule is not defined, it's a CustomAccessor on the prototype.
|
||||
// Various libraries expect __esModule to be set when using ESM from require().
|
||||
// We don't want to always inject the __esModule export into every module,
|
||||
// And creating an Object wrapper causes the actual exports to not be own properties.
|
||||
// So instead of either of those, we make it so that the __esModule property can be set at runtime.
|
||||
// It only supports "true" and undefined. Anything non-truthy is treated as undefined.
|
||||
// https://github.com/oven-sh/bun/issues/14411
|
||||
if (namespace.__esModule === undefined) {
|
||||
try {
|
||||
namespace.__esModule = true;
|
||||
} catch {
|
||||
// https://github.com/oven-sh/bun/issues/17816
|
||||
}
|
||||
}
|
||||
|
||||
this.exports = namespace["module.exports"] ?? namespace;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -37,19 +37,14 @@ export var __reExport = (target, mod, secondTarget) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Converts the module from CommonJS to ESM. When in node mode (i.e. in an
|
||||
// ".mjs" file, package.json has "type: module", or the "__esModule" export
|
||||
// in the CommonJS file is falsy or missing), the "default" property is
|
||||
// overridden to point to the original CommonJS exports object instead.
|
||||
// Converts the module from CommonJS to ESM. With the __esModule workaround removed,
|
||||
// CommonJS modules are always treated as having a single default export.
|
||||
export var __toESM = (mod, isNodeMode, target) => {
|
||||
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
||||
const to =
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
||||
// Always set "default" to the CommonJS "module.exports" for consistency
|
||||
const to = __defProp(target, "default", { value: mod, enumerable: true });
|
||||
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
// Copy all properties from the CommonJS module to the ESM namespace
|
||||
for (let key of __getOwnPropNames(mod))
|
||||
if (!__hasOwnProp.call(to, key))
|
||||
__defProp(to, key, {
|
||||
|
||||
@@ -1105,6 +1105,7 @@ describe("bundler", () => {
|
||||
sideEffect(() => {});
|
||||
`,
|
||||
},
|
||||
backend: "cli",
|
||||
outdir: "/out",
|
||||
sourceMap: "external",
|
||||
minifySyntax: true,
|
||||
@@ -1113,7 +1114,7 @@ describe("bundler", () => {
|
||||
snapshotSourceMap: {
|
||||
"entry.js.map": {
|
||||
files: ["../node_modules/react/index.js", "../entry.js"],
|
||||
mappingsExactMatch: "qYACA,WAAW,IAAQ,EAAE,ICDrB,eACA,QAAQ,IAAI,CAAK",
|
||||
mappingsExactMatch: "8WACA,WAAW,IAAQ,EAAE,ICDrB,eACA,QAAQ,IAAI,CAAK",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -57,9 +57,9 @@ describe("bundler", () => {
|
||||
"../entry.tsx",
|
||||
],
|
||||
mappings: [
|
||||
["react.development.js:524:'getContextName'", "1:5412:Y1"],
|
||||
["react.development.js:524:'getContextName'", "1:5386:Y1"],
|
||||
["react.development.js:2495:'actScopeDepth'", "23:4082:GJ++"],
|
||||
["react.development.js:696:''Component'", '1:7474:\'Component "%s"'],
|
||||
["react.development.js:696:''Component'", '1:7448:\'Component "%s"'],
|
||||
["entry.tsx:6:'\"Content-Type\"'", '100:18809:"Content-Type"'],
|
||||
["entry.tsx:11:'<html>'", "100:19063:void"],
|
||||
["entry.tsx:23:'await'", "100:19163:await"],
|
||||
@@ -67,7 +67,7 @@ describe("bundler", () => {
|
||||
},
|
||||
},
|
||||
expectExactFilesize: {
|
||||
"out/entry.js": 221726,
|
||||
"out/entry.js": 221700,
|
||||
},
|
||||
run: {
|
||||
stdout: "<!DOCTYPE html><html><body><h1>Hello World</h1><p>This is an example.</p></body></html>",
|
||||
|
||||
@@ -10,59 +10,71 @@ import * as WithoutTypeModuleExportEsModuleNoAnnotation from "./without-type-mod
|
||||
|
||||
describe('without type: "module"', () => {
|
||||
test("module.exports = {}", () => {
|
||||
// CommonJS always exports entire module.exports as default
|
||||
expect(WithoutTypeModuleExportEsModuleAnnotationMissingDefault.default).toEqual({});
|
||||
expect(WithoutTypeModuleExportEsModuleAnnotationMissingDefault.__esModule).toBeUndefined();
|
||||
});
|
||||
|
||||
test("exports.__esModule = true", () => {
|
||||
// CommonJS exports entire module.exports as default, including __esModule property
|
||||
expect(WithoutTypeModuleExportEsModuleAnnotationNoDefault.default).toEqual({
|
||||
__esModule: true,
|
||||
});
|
||||
|
||||
// The module namespace object will not have the __esModule property.
|
||||
expect(WithoutTypeModuleExportEsModuleAnnotationNoDefault.__esModule).toBeUndefined();
|
||||
// The module namespace object should have __esModule as a named export
|
||||
expect(WithoutTypeModuleExportEsModuleAnnotationNoDefault.__esModule).toBe(true);
|
||||
});
|
||||
|
||||
test("exports.default = true; exports.__esModule = true;", () => {
|
||||
expect(WithoutTypeModuleExportEsModuleAnnotation.default).toBeTrue();
|
||||
expect(WithoutTypeModuleExportEsModuleAnnotation.__esModule).toBeUndefined();
|
||||
// CommonJS exports entire module.exports as default
|
||||
expect(WithoutTypeModuleExportEsModuleAnnotation.default).toEqual({
|
||||
default: true,
|
||||
__esModule: true,
|
||||
});
|
||||
expect(WithoutTypeModuleExportEsModuleAnnotation.__esModule).toBe(true);
|
||||
});
|
||||
|
||||
test("exports.default = true;", () => {
|
||||
// CommonJS exports entire module.exports as default
|
||||
expect(WithoutTypeModuleExportEsModuleNoAnnotation.default).toEqual({
|
||||
default: true,
|
||||
});
|
||||
expect(WithoutTypeModuleExportEsModuleAnnotation.__esModule).toBeUndefined();
|
||||
expect(WithoutTypeModuleExportEsModuleNoAnnotation.__esModule).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with type: "module"', () => {
|
||||
test("module.exports = {}", () => {
|
||||
// CommonJS always exports entire module.exports as default, regardless of type:module
|
||||
expect(WithTypeModuleExportEsModuleAnnotationMissingDefault.default).toEqual({});
|
||||
expect(WithTypeModuleExportEsModuleAnnotationMissingDefault.__esModule).toBeUndefined();
|
||||
});
|
||||
|
||||
test("exports.__esModule = true", () => {
|
||||
// CommonJS exports entire module.exports as default, including __esModule property
|
||||
expect(WithTypeModuleExportEsModuleAnnotationNoDefault.default).toEqual({
|
||||
__esModule: true,
|
||||
});
|
||||
|
||||
// The module namespace object WILL have the __esModule property.
|
||||
expect(WithTypeModuleExportEsModuleAnnotationNoDefault.__esModule).toBeTrue();
|
||||
// The module namespace object should have __esModule as a named export
|
||||
expect(WithTypeModuleExportEsModuleAnnotationNoDefault.__esModule).toBe(true);
|
||||
});
|
||||
|
||||
test("exports.default = true; exports.__esModule = true;", () => {
|
||||
// CommonJS exports entire module.exports as default
|
||||
expect(WithTypeModuleExportEsModuleAnnotation.default).toEqual({
|
||||
default: true,
|
||||
__esModule: true,
|
||||
});
|
||||
expect(WithTypeModuleExportEsModuleAnnotation.__esModule).toBeTrue();
|
||||
// __esModule should be available as a named export
|
||||
expect(WithTypeModuleExportEsModuleAnnotation.__esModule).toBe(true);
|
||||
});
|
||||
|
||||
test("exports.default = true;", () => {
|
||||
// CommonJS exports entire module.exports as default
|
||||
expect(WithTypeModuleExportEsModuleNoAnnotation.default).toEqual({
|
||||
default: true,
|
||||
});
|
||||
expect(WithTypeModuleExportEsModuleAnnotation.__esModule).toBeTrue();
|
||||
expect(WithTypeModuleExportEsModuleNoAnnotation.__esModule).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,12 +16,14 @@ test("__esModule is settable", () => {
|
||||
Self.__esModule = undefined;
|
||||
});
|
||||
|
||||
test("require of self sets __esModule", () => {
|
||||
test("require of self does NOT automatically set __esModule", () => {
|
||||
expect(Self.__esModule).toBeUndefined();
|
||||
{
|
||||
const Self = require("./esModule.test.ts");
|
||||
expect(Self.__esModule).toBe(true);
|
||||
// With new behavior, __esModule is not automatically added
|
||||
expect(Self.__esModule).toBeUndefined();
|
||||
}
|
||||
expect(Self.__esModule).toBe(true);
|
||||
// __esModule remains undefined since it's not automatically added
|
||||
expect(Self.__esModule).toBeUndefined();
|
||||
expect(Object.getOwnPropertyNames(Self)).toBeEmpty();
|
||||
});
|
||||
|
||||
297
test/regression/issue/09267-esmodule-removal.test.ts
Normal file
297
test/regression/issue/09267-esmodule-removal.test.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, normalizeBunSnapshot, tempDir } from "harness";
|
||||
|
||||
test("CommonJS module.exports function should be directly callable (#4506)", async () => {
|
||||
using dir = tempDir("test-cjs-function", {
|
||||
"package.json": JSON.stringify({ name: "test-pkg" }),
|
||||
"index.js": `
|
||||
module.exports = function isNatural(value) {
|
||||
return Number.isInteger(value) && value >= 0;
|
||||
};
|
||||
|
||||
module.exports.isPositive = function(value) {
|
||||
return Number.isInteger(value) && value > 0;
|
||||
};
|
||||
`,
|
||||
"test.js": `
|
||||
const isNatural = require('./index.js');
|
||||
|
||||
// Should be directly callable
|
||||
console.log(typeof isNatural === 'function' ? 'PASS: is function' : 'FAIL: not function');
|
||||
console.log(isNatural(5) === true ? 'PASS: isNatural(5)' : 'FAIL: isNatural(5)');
|
||||
console.log(isNatural(-1) === false ? 'PASS: isNatural(-1)' : 'FAIL: isNatural(-1)');
|
||||
|
||||
// Named export should also work
|
||||
console.log(typeof isNatural.isPositive === 'function' ? 'PASS: has isPositive' : 'FAIL: no isPositive');
|
||||
console.log(isNatural.isPositive(1) === true ? 'PASS: isPositive(1)' : 'FAIL: isPositive(1)');
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test.js"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stderr).toBe("");
|
||||
expect(normalizeBunSnapshot(stdout, dir)).toMatchInlineSnapshot(`
|
||||
"PASS: is function
|
||||
PASS: isNatural(5)
|
||||
PASS: isNatural(-1)
|
||||
PASS: has isPositive
|
||||
PASS: isPositive(1)"
|
||||
`);
|
||||
});
|
||||
|
||||
test("CommonJS exports object should be directly accessible", async () => {
|
||||
using dir = tempDir("test-cjs-object", {
|
||||
"package.json": JSON.stringify({ name: "test-pkg" }),
|
||||
"module.js": `
|
||||
exports.foo = "bar";
|
||||
exports.baz = 42;
|
||||
exports.func = function() { return "hello"; };
|
||||
`,
|
||||
"test.js": `
|
||||
const mod = require('./module.js');
|
||||
|
||||
console.log(mod.foo === "bar" ? 'PASS: foo' : 'FAIL: foo');
|
||||
console.log(mod.baz === 42 ? 'PASS: baz' : 'FAIL: baz');
|
||||
console.log(typeof mod.func === 'function' ? 'PASS: func is function' : 'FAIL: func not function');
|
||||
console.log(mod.func() === "hello" ? 'PASS: func()' : 'FAIL: func()');
|
||||
|
||||
// Should not have __esModule added
|
||||
console.log(mod.__esModule === undefined ? 'PASS: no __esModule' : 'FAIL: has __esModule');
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test.js"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stderr).toBe("");
|
||||
expect(normalizeBunSnapshot(stdout, dir)).toMatchInlineSnapshot(`
|
||||
"PASS: foo
|
||||
PASS: baz
|
||||
PASS: func is function
|
||||
PASS: func()
|
||||
PASS: no __esModule"
|
||||
`);
|
||||
});
|
||||
|
||||
test("ESM import of CommonJS default export", async () => {
|
||||
using dir = tempDir("test-esm-cjs-default", {
|
||||
"package.json": JSON.stringify({ name: "test-pkg", type: "module" }),
|
||||
"cjs-module.cjs": `
|
||||
module.exports = function myFunction() {
|
||||
return "default export";
|
||||
};
|
||||
|
||||
module.exports.namedExport = "named";
|
||||
`,
|
||||
"test.mjs": `
|
||||
import myFunction from './cjs-module.cjs';
|
||||
import * as mod from './cjs-module.cjs';
|
||||
|
||||
// Default import should be the entire module.exports
|
||||
console.log(typeof myFunction === 'function' ? 'PASS: default is function' : 'FAIL: default not function');
|
||||
console.log(myFunction() === 'default export' ? 'PASS: default()' : 'FAIL: default()');
|
||||
|
||||
// Named export should be accessible on the default
|
||||
console.log(myFunction.namedExport === 'named' ? 'PASS: default.namedExport' : 'FAIL: default.namedExport');
|
||||
|
||||
// Star import should have default pointing to module.exports
|
||||
console.log(typeof mod.default === 'function' ? 'PASS: mod.default is function' : 'FAIL: mod.default not function');
|
||||
console.log(mod.default() === 'default export' ? 'PASS: mod.default()' : 'FAIL: mod.default()');
|
||||
console.log(mod.namedExport === 'named' ? 'PASS: mod.namedExport' : 'FAIL: mod.namedExport');
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test.mjs"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stderr).toBe("");
|
||||
expect(normalizeBunSnapshot(stdout, dir)).toMatchInlineSnapshot(`
|
||||
"PASS: default is function
|
||||
PASS: default()
|
||||
PASS: default.namedExport
|
||||
PASS: mod.default is function
|
||||
PASS: mod.default()
|
||||
PASS: mod.namedExport"
|
||||
`);
|
||||
});
|
||||
|
||||
test("ESM import of CommonJS with exports object", async () => {
|
||||
using dir = tempDir("test-esm-cjs-exports", {
|
||||
"package.json": JSON.stringify({ name: "test-pkg", type: "module" }),
|
||||
"cjs-module.cjs": `
|
||||
exports.foo = "bar";
|
||||
exports.baz = 42;
|
||||
exports.func = function() { return "hello"; };
|
||||
`,
|
||||
"test.mjs": `
|
||||
import defaultExport from './cjs-module.cjs';
|
||||
import * as mod from './cjs-module.cjs';
|
||||
|
||||
// Default import should be the entire exports object
|
||||
console.log(typeof defaultExport === 'object' ? 'PASS: default is object' : 'FAIL: default not object');
|
||||
console.log(defaultExport.foo === 'bar' ? 'PASS: default.foo' : 'FAIL: default.foo');
|
||||
console.log(defaultExport.baz === 42 ? 'PASS: default.baz' : 'FAIL: default.baz');
|
||||
console.log(typeof defaultExport.func === 'function' ? 'PASS: default.func' : 'FAIL: default.func');
|
||||
|
||||
// Star import should have the same properties plus default
|
||||
console.log(mod.default === defaultExport ? 'PASS: mod.default === default' : 'FAIL: mod.default !== default');
|
||||
console.log(mod.foo === 'bar' ? 'PASS: mod.foo' : 'FAIL: mod.foo');
|
||||
console.log(mod.baz === 42 ? 'PASS: mod.baz' : 'FAIL: mod.baz');
|
||||
console.log(typeof mod.func === 'function' ? 'PASS: mod.func' : 'FAIL: mod.func');
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test.mjs"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stderr).toBe("");
|
||||
expect(normalizeBunSnapshot(stdout, dir)).toMatchInlineSnapshot(`
|
||||
"PASS: default is object
|
||||
PASS: default.foo
|
||||
PASS: default.baz
|
||||
PASS: default.func
|
||||
PASS: mod.default === default
|
||||
PASS: mod.foo
|
||||
PASS: mod.baz
|
||||
PASS: mod.func"
|
||||
`);
|
||||
});
|
||||
|
||||
test("CommonJS module with __esModule should be treated normally", async () => {
|
||||
using dir = tempDir("test-esmodule-flag", {
|
||||
"package.json": JSON.stringify({ name: "test-pkg", type: "module" }),
|
||||
"cjs-with-flag.cjs": `
|
||||
// This module manually sets __esModule, which should now be treated as a normal property
|
||||
exports.__esModule = true;
|
||||
exports.default = "explicit default";
|
||||
exports.foo = "bar";
|
||||
`,
|
||||
"test.mjs": `
|
||||
import defaultExport from './cjs-with-flag.cjs';
|
||||
import * as mod from './cjs-with-flag.cjs';
|
||||
|
||||
// With __esModule workaround removed, default import should be the entire exports object
|
||||
// NOT the value of exports.default
|
||||
console.log(typeof defaultExport === 'object' ? 'PASS: default is object' : 'FAIL: default not object');
|
||||
console.log(defaultExport.default === 'explicit default' ? 'PASS: has .default property' : 'FAIL: no .default property');
|
||||
console.log(defaultExport.foo === 'bar' ? 'PASS: has .foo property' : 'FAIL: no .foo property');
|
||||
console.log(defaultExport.__esModule === true ? 'PASS: has .__esModule property' : 'FAIL: no .__esModule property');
|
||||
|
||||
// Star import verification
|
||||
console.log(mod.default === defaultExport ? 'PASS: mod.default is exports object' : 'FAIL: mod.default not exports object');
|
||||
console.log(mod.foo === 'bar' ? 'PASS: mod.foo' : 'FAIL: mod.foo');
|
||||
console.log(mod.__esModule === true ? 'PASS: mod.__esModule' : 'FAIL: mod.__esModule');
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test.mjs"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stderr).toBe("");
|
||||
expect(normalizeBunSnapshot(stdout, dir)).toMatchInlineSnapshot(`
|
||||
"PASS: default is object
|
||||
PASS: has .default property
|
||||
PASS: has .foo property
|
||||
PASS: has .__esModule property
|
||||
PASS: mod.default is exports object
|
||||
PASS: mod.foo
|
||||
PASS: mod.__esModule"
|
||||
`);
|
||||
});
|
||||
|
||||
test("Bundler should handle CommonJS correctly without __esModule", async () => {
|
||||
using dir = tempDir("test-bundler-cjs", {
|
||||
"package.json": JSON.stringify({ name: "test-pkg" }),
|
||||
"module.js": `
|
||||
module.exports = function() { return "bundled"; };
|
||||
module.exports.extra = "data";
|
||||
`,
|
||||
"entry.js": `
|
||||
const mod = require('./module.js');
|
||||
console.log(typeof mod === 'function' ? 'PASS: is function' : 'FAIL: not function');
|
||||
console.log(mod() === 'bundled' ? 'PASS: call result' : 'FAIL: call result');
|
||||
console.log(mod.extra === 'data' ? 'PASS: extra property' : 'FAIL: extra property');
|
||||
`,
|
||||
});
|
||||
|
||||
// Build the bundle
|
||||
await using buildProc = Bun.spawn({
|
||||
cmd: [bunExe(), "build", "entry.js", "--outfile", "bundle.js"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [buildStdout, buildStderr, buildExitCode] = await Promise.all([
|
||||
buildProc.stdout.text(),
|
||||
buildProc.stderr.text(),
|
||||
buildProc.exited,
|
||||
]);
|
||||
|
||||
expect(buildExitCode).toBe(0);
|
||||
|
||||
// Run the bundle
|
||||
await using runProc = Bun.spawn({
|
||||
cmd: [bunExe(), "bundle.js"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [runStdout, runStderr, runExitCode] = await Promise.all([
|
||||
runProc.stdout.text(),
|
||||
runProc.stderr.text(),
|
||||
runProc.exited,
|
||||
]);
|
||||
|
||||
expect(runExitCode).toBe(0);
|
||||
expect(runStderr).toBe("");
|
||||
expect(normalizeBunSnapshot(runStdout, dir)).toMatchInlineSnapshot(`
|
||||
"PASS: is function
|
||||
PASS: call result
|
||||
PASS: extra property"
|
||||
`);
|
||||
});
|
||||
Reference in New Issue
Block a user