diff options
author | 2023-06-26 12:49:20 -0700 | |
---|---|---|
committer | 2023-06-26 12:49:20 -0700 | |
commit | a732999da578ca92a1d9e633036225a32e77529d (patch) | |
tree | ca8341d01b087c7d200d54fef82eeff2fdbe87fe /src/bun.js/bindings/CommonJSModuleRecord.cpp | |
parent | 6d01e6e367fd518884883ce8194c3c7322043639 (diff) | |
download | bun-a732999da578ca92a1d9e633036225a32e77529d.tar.gz bun-a732999da578ca92a1d9e633036225a32e77529d.tar.zst bun-a732999da578ca92a1d9e633036225a32e77529d.zip |
Runtime support for `__esModule` annotations (#3393)
* Runtime support for `__esModule` annotations
* Ignore `__esModule` annotation when `"type": "module"` is set
---------
Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
Diffstat (limited to 'src/bun.js/bindings/CommonJSModuleRecord.cpp')
-rw-r--r-- | src/bun.js/bindings/CommonJSModuleRecord.cpp | 113 |
1 files changed, 104 insertions, 9 deletions
diff --git a/src/bun.js/bindings/CommonJSModuleRecord.cpp b/src/bun.js/bindings/CommonJSModuleRecord.cpp index 3615db774..c7dac89c2 100644 --- a/src/bun.js/bindings/CommonJSModuleRecord.cpp +++ b/src/bun.js/bindings/CommonJSModuleRecord.cpp @@ -586,13 +586,41 @@ void JSCommonJSModule::toSyntheticSource(JSC::JSGlobalObject* globalObject, auto result = this->exportsObject(); auto& vm = globalObject->vm(); - exportNames.append(vm.propertyNames->defaultKeyword); - exportValues.append(result); // This exists to tell ImportMetaObject.ts that this is a CommonJS module. exportNames.append(Identifier::fromUid(vm.symbolRegistry().symbolForKey("CommonJS"_s))); exportValues.append(jsNumber(0)); + // Bun's intepretation 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; + if (result.isObject()) { auto* exports = asObject(result); @@ -601,21 +629,78 @@ void JSCommonJSModule::toSyntheticSource(JSC::JSGlobalObject* globalObject, exportNames.reserveCapacity(size + 2); exportValues.ensureCapacity(size + 2); - if (canPerformFastEnumeration(structure)) { + auto catchScope = DECLARE_CATCH_SCOPE(vm); + + Identifier esModuleMarker = builtinNames(vm).__esModulePublicName(); + bool hasESModuleMarker = !this->ignoreESModuleAnnotation && exports->hasProperty(globalObject, esModuleMarker); + if (catchScope.exception()) { + catchScope.clearException(); + } + + if (hasESModuleMarker) { + if (canPerformFastEnumeration(structure)) { + exports->structure()->forEachProperty(vm, [&](const PropertyTableEntry& entry) -> bool { + auto key = entry.key(); + if (key->isSymbol() || entry.attributes() & PropertyAttribute::DontEnum || 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 (catchScope.exception()) { + catchScope.clearExceptionExceptTermination(); + return; + } + + for (auto property : properties) { + if (UNLIKELY(property.isEmpty() || property.isNull() || property == esModuleMarker || property.isPrivateName() || property.isSymbol())) + continue; + + // ignore constructor + if (property == vm.propertyNames->constructor) + continue; + + JSC::PropertySlot slot(exports, PropertySlot::InternalMethodType::Get); + if (!exports->getPropertySlot(globalObject, property, slot)) + 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 (catchScope.exception()) { + catchScope.clearException(); + getterResult = jsUndefined(); + } + + exportValues.append(getterResult); + + needsToAssignDefault = needsToAssignDefault && property != vm.propertyNames->defaultKeyword; + } + } + + } else if (canPerformFastEnumeration(structure)) { exports->structure()->forEachProperty(vm, [&](const PropertyTableEntry& entry) -> bool { auto key = entry.key(); - if (key->isSymbol() || key == vm.propertyNames->defaultKeyword || entry.attributes() & PropertyAttribute::DontEnum) + if (key->isSymbol() || entry.attributes() & PropertyAttribute::DontEnum || key == vm.propertyNames->defaultKeyword) return true; - exportNames.append(Identifier::fromUid(vm, key)); - JSValue value = exports->getDirect(entry.offset()); + exportNames.append(Identifier::fromUid(vm, key)); exportValues.append(value); return true; }); } else { - auto catchScope = DECLARE_CATCH_SCOPE(vm); JSC::PropertyNameArray properties(vm, JSC::PropertyNameMode::Strings, JSC::PrivateSymbolMode::Exclude); exports->methodTable()->getOwnPropertyNames(exports, globalObject, properties, DontEnumPropertiesMode::Exclude); if (catchScope.exception()) { @@ -624,11 +709,11 @@ void JSCommonJSModule::toSyntheticSource(JSC::JSGlobalObject* globalObject, } for (auto property : properties) { - if (UNLIKELY(property.isEmpty() || property.isNull() || property.isPrivateName() || property.isSymbol())) + if (UNLIKELY(property.isEmpty() || property.isNull() || property == vm.propertyNames->defaultKeyword || property.isPrivateName() || property.isSymbol())) continue; // ignore constructor - if (property == vm.propertyNames->constructor || property == vm.propertyNames->defaultKeyword) + if (property == vm.propertyNames->constructor) continue; JSC::PropertySlot slot(exports, PropertySlot::InternalMethodType::Get); @@ -650,6 +735,11 @@ void JSCommonJSModule::toSyntheticSource(JSC::JSGlobalObject* globalObject, } } } + + if (needsToAssignDefault) { + exportNames.append(vm.propertyNames->defaultKeyword); + exportValues.append(result); + } } JSValue JSCommonJSModule::exportsObject() @@ -759,6 +849,7 @@ bool JSCommonJSModule::evaluate( { auto& vm = globalObject->vm(); auto sourceProvider = Zig::SourceProvider::create(jsCast<Zig::GlobalObject*>(globalObject), source, JSC::SourceProviderSourceType::Program); + this->ignoreESModuleAnnotation = source.tag == ResolvedSourceTagPackageJSONTypeModule; JSC::SourceCode rawInputSource( WTFMove(sourceProvider)); @@ -766,6 +857,7 @@ bool JSCommonJSModule::evaluate( return true; this->sourceCode.set(vm, this, JSC::JSSourceCode::create(vm, WTFMove(rawInputSource))); + WTF::NakedPtr<JSC::Exception> exception; evaluateCommonJSModuleOnce(vm, globalObject, this, this->m_dirname.get(), this->m_filename.get(), exception); @@ -796,6 +888,7 @@ std::optional<JSC::SourceCode> createCommonJSModule( JSValue entry = globalObject->requireMap()->get(globalObject, specifierValue); auto sourceProvider = Zig::SourceProvider::create(jsCast<Zig::GlobalObject*>(globalObject), source, JSC::SourceProviderSourceType::Program); + bool ignoreESModuleAnnotation = source.tag == ResolvedSourceTagPackageJSONTypeModule; SourceOrigin sourceOrigin = sourceProvider->sourceOrigin(); if (entry) { @@ -827,6 +920,8 @@ std::optional<JSC::SourceCode> createCommonJSModule( globalObject->requireMap()->set(globalObject, requireMapKey, moduleObject); } + moduleObject->ignoreESModuleAnnotation = ignoreESModuleAnnotation; + return JSC::SourceCode( JSC::SyntheticSourceProvider::create( [](JSC::JSGlobalObject* lexicalGlobalObject, |