Repository navigation
Conversation
The .cjs/.cts extension is unconditionally CommonJS in Node; 'export', 'import' statements and top-level 'await' are a SyntaxError there. Bun was classifying such files by content and silently running them as ES modules, so a package could ship a .cjs that was unloadable in Node. Also threads module_type through the off-thread transpiler (previously only the package.json 'type' field reached it, not the extension) and surfaces parser log errors that were dropped on that path when parse returned a well-formed AST.
|
Warning Review limit reached
Next review available in: 1 minute Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughChangesThe parser now rejects ESM syntax in runtime CommonJS CommonJS runtime transpilation
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
|
Not a duplicate: #33807 makes They share the |
….cts
The new check used esm_import_keyword/esm_export_keyword, which are also
set for constructs that aren't ES-module statement syntax: type-only
imports/exports (import type, export type, export interface, export
declare, export {type X}) and import.meta. A .cts containing
'import type {X} from ...; module.exports = 1' or a .cjs using
import.meta.dir (which Bun's CJS wrapper explicitly supports) now
errored incorrectly.
For imports, detect a real 'import' statement via its import record
(type-only imports and import.meta don't create one). For exports,
restore esm_export_keyword to its previous value at the type-only
S::TypeScript returns in t_export, matching the existing restore for
'export = value'.
'export import X = require(...)' is intentionally not whitelisted: Bun transpiles it to 'export const X = require(...)', a real ESM export in the printed output, so the new error is accurate for Bun's output and matches Node (which rejects import-equals in strip-only mode).
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/js_parser/parse/parse_stmt.rs`:
- Around line 914-922: Update the export handling branch for
StmtIdentifier::SNamespace, SAbstract, and SModule to restore esm_export_keyword
after p.parse_stmt(opts) when parsing produces StmtData::STypeScript, including
empty or type-only bodies. Preserve the keyword for runtime statements, and
ensure .cjs/.cts no-op namespace or module declarations do not trigger CommonJS
export errors.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 4c8aef9d-9137-4124-81d0-9bae7c23e1cf
📒 Files selected for processing (11)
src/js_parser/parse/parse_entry.rssrc/js_parser/parse/parse_stmt.rssrc/jsc/RuntimeTranspilerCache.rssrc/jsc/RuntimeTranspilerStore.rssrc/runtime/jsc_hooks.rstest/cli/run/module-type-fixture/cjs/import.cjstest/cli/run/module-type-fixture/esm/import.cjstest/cli/run/run-cjs.test.tstest/cli/run/run-detect-module-type.tstest/js/node/module/extensions-fixture/secretly_esm.customtest/js/node/module/require-extensions.test.ts
💤 Files with no reviewable changes (3)
- test/cli/run/module-type-fixture/cjs/import.cjs
- test/cli/run/module-type-fixture/esm/import.cjs
- test/cli/run/run-detect-module-type.ts
| StmtIdentifier::SNamespace | ||
| | StmtIdentifier::SAbstract | ||
| | StmtIdentifier::SModule | ||
| | StmtIdentifier::SInterface => { | ||
| | StmtIdentifier::SModule => { | ||
| // "export namespace Foo {}" | ||
| // "export abstract class Foo {}" | ||
| // "export module Foo {}" | ||
| opts.is_export = true; | ||
| return p.parse_stmt(opts); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the relevant parser file and the namespace parser implementation.
ast-grep outline src/js_parser/parse/parse_stmt.rs --view expanded || true
printf '\n---\n'
ast-grep outline src/js_parser/parse/parse_type_script_namespace_stmt.rs --view expanded || true
printf '\n---\n'
rg -n "parse_type_script_namespace_stmt|STypeScript|esm_export_keyword|SNamespace|SModule|SAbstract" src/js_parser/parse -SRepository: oven-sh/bun
Length of output: 8876
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '169,380p' src/js_parser/parse/parse_typescript.rs
printf '\n--- parse_stmt export branches ---\n'
sed -n '840,990p' src/js_parser/parse/parse_stmt.rs
printf '\n---\n'
sed -n '990,1135p' src/js_parser/parse/parse_stmt.rsRepository: oven-sh/bun
Length of output: 24241
Restore esm_export_keyword when export namespace / export module erases to TS-only. These forms can return StmtData::STypeScript for empty or type-only bodies, but this branch returns p.parse_stmt(opts) without resetting the export keyword. In .cjs/.cts files that turns a no-op namespace into a CommonJS export error.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/js_parser/parse/parse_stmt.rs` around lines 914 - 922, Update the export
handling branch for StmtIdentifier::SNamespace, SAbstract, and SModule to
restore esm_export_keyword after p.parse_stmt(opts) when parsing produces
StmtData::STypeScript, including empty or type-only bodies. Preserve the keyword
for runtime statements, and ensure .cjs/.cts no-op namespace or module
declarations do not trigger CommonJS export errors.
| StmtIdentifier::SNamespace | ||
| | StmtIdentifier::SAbstract | ||
| | StmtIdentifier::SModule | ||
| | StmtIdentifier::SInterface => { | ||
| | StmtIdentifier::SModule => { | ||
| // "export namespace Foo {}" | ||
| // "export abstract class Foo {}" | ||
| // "export module Foo {}" | ||
| opts.is_export = true; | ||
| return p.parse_stmt(opts); |
There was a problem hiding this comment.
🔴 Two more over-match cases in the same class as the open comment on export import X = require(...), at different t_export() arms that a fix to only the T::TImport arm would miss: the SNamespace | SAbstract | SModule arm here and the T::TEnum arm at ~line 851 both recurse without restoring esm_export_keyword. A .cts containing export namespace Foo { export type X = number } (type-only, erased by tsc — directly analogous to export interface, which the fix commit did handle) or export enum Foo { A = 1 } (tsc emits exports.Foo = ... for .cts) now fails with "Cannot use 'export' in a CommonJS module … Rename to .mts", regressing files that ran before this PR. The type-only namespace case is unambiguously valid .cts; export enum / value-emitting export namespace are a design decision, but the current diagnostic is misleading either way.
Extended reasoning...
What the bug is
The fix commit 8594007 addressed the resolved comment by restoring esm_export_keyword at every t_export() path that returns S::TypeScript{}, and by splitting StmtIdentifier::SInterface/SDeclare out to restore before recursing. But it left two sibling arms untouched that also recurse without restoring, and neither is covered by the still-open comment on export import X = require(...):
StmtIdentifier::SNamespace | SModule(parse_stmt.rs:914-921), grouped withSAbstract: setsopts.is_export = trueand recurses toparse_type_script_namespace_stmt.esm_export_keywordis never restored, regardless of whether the namespace is type-only (returnsS::TypeScript{}) or value-emitting (returnsS::Namespace).T::TEnum(~line 845-851): setsopts.is_export = trueand recurses toparse_typescript_enum_stmt, which returns a value-emittingS::Enum.esm_export_keywordis never restored.
Step-by-step proof (type-only namespace)
Take foo.cts:
export namespace Foo { export type X = number }
module.exports = 1;t_export()at parse_stmt.rs:803 setsp.esm_export_keyword = p.lexer.range()(opts.is_module_scopeis true).- Token is
TIdentifiernamespace→ matches theSNamespace | SAbstract | SModulearm at line 914, which setsopts.is_export = trueand doesreturn p.parse_stmt(opts)without restoringesm_export_keyword. - The recursion reaches
parse_stmt_fallthrough→TsStmtNamespace→parse_type_script_namespace_stmt(parse_typescript.rs). Inside the body, the innerexport type X = numberparses withis_namespace_scope: true, is_module_scope: false(so line 803 does not overwriteesm_export_keyword), returnsS::TypeScript{}, andparse_stmts_up_tofilters it out. stmts.len() == 0 == import_equal_count, soparse_type_script_namespace_stmtreturnsS::TypeScript{}— the namespace is erased exactly like an interface.esm_export_keywordis still set.- In
parse_entry.rs:module_type == Cjs(from the.ctsextension),commonjs_at_runtimetrue, ext is.cts,esm_export_keyword.len > 0→ emitsCannot use 'export' in a CommonJS module.
Before this PR the same file worked (classified as ESM, but the namespace erased to nothing and module.exports still ran). This is the exact analogue of export interface Foo {}, which the fix commit did split out to restore — a type-only (uninstantiated) namespace is erased by tsc and leaves valid CommonJS.
Step-by-step proof (export enum)
Take foo.cts:
export enum Foo { A = 1 }t_export()setsp.esm_export_keyword(line 803).T::TEnumarm (~line 845):opts.is_export = true; p.parse_stmt(opts)— no restore.- Recurses →
t_enum()→parse_typescript_enum_stmt→ returnsS::Enum(value-emitting, notS::TypeScript{}). esm_export_keyword.len > 0survives to the new check →Cannot use 'export' in a CommonJS module … Rename to .mts.
Before this PR this ran (as ESM). export enum in .cts is a real-world pattern for sharing constants between CJS packages; tsc compiles it to exports.Foo = .... The value-emitting export namespace Foo { export const x = 1 } case follows the same path via the SNamespace arm (returns S::Namespace since stmts.len() > import_equal_count).
Why nothing catches this
The negative-test matrix added in run-cjs.test.ts covers export type/export interface/export declare/export {type X} but not export namespace, export module, or export enum. The resolved PR comment enumerated export interface etc.; the unresolved PR comment covers export import X = require(...) (the T::TImport arm). These are different code sites — the SNamespace|SModule arm at line 914 and the T::TEnum arm at ~line 851 — that neither comment nor the fix commit touch.
On the value-emitting cases
The type-only namespace case is unambiguously valid .cts (same class as export interface). export enum and value-emitting export namespace are more nuanced: tsc under --verbatimModuleSyntax rejects them in .cts, and Bun's enum/namespace lowering currently prints export var Foo (not exports.Foo = ...), so simply classifying them as CJS wouldn't work without new lowering. Rejecting them may be defensible — but as-is it's an undocumented regression of previously-running code with a factually misleading diagnostic ("Rename to .mts" for TS-only syntax that default-mode tsc compiles to CJS in a .cts). If the intent is to reject them, they should get a distinct message and a test.
How to fix
At minimum, restore p.esm_export_keyword = previous_export_keyword in the SNamespace | SModule arm (splitting SAbstract out, since export abstract class is a value ESM export) so type-only namespaces pass. For export enum and value-emitting namespaces, either apply the same restore (matching the open export import = require comment) or emit a distinct diagnostic — and add negative tests for whichever decision is made:
["export namespace (type-only)", "export namespace Foo { export type X = number }\nmodule.exports = 1;\n"],
["export enum", "export enum Foo { A = 1 }\nmodule.exports = Foo;\n"],| p.esm_export_keyword = previous_export_keyword; // This wasn't an ESM export statement after all | ||
| return Ok(p.s(S::TypeScript {}, loc)); | ||
| } | ||
| StmtIdentifier::SNamespace | ||
| | StmtIdentifier::SAbstract | ||
| | StmtIdentifier::SModule | ||
| | StmtIdentifier::SInterface => { | ||
| | StmtIdentifier::SModule => { | ||
| // "export namespace Foo {}" | ||
| // "export abstract class Foo {}" | ||
| // "export module Foo {}" | ||
| opts.is_export = true; | ||
| return p.parse_stmt(opts); | ||
| } | ||
| StmtIdentifier::SInterface => { | ||
| // "export interface Foo {}" | ||
| p.esm_export_keyword = previous_export_keyword; // This wasn't an ESM export statement after all | ||
| opts.is_export = true; | ||
| return p.parse_stmt(opts); | ||
| } | ||
| StmtIdentifier::SDeclare => { | ||
| // "export declare class Foo {}" | ||
| p.esm_export_keyword = previous_export_keyword; // This wasn't an ESM export statement after all |
There was a problem hiding this comment.
🟡 The new esm_export_keyword = previous_export_keyword restores in t_export() are not gated on .cts/module_type/!bundle, so they change module classification for all TS files, not just .cts. A .ts file under "type":"commonjs" containing only export interface Foo {} was previously classified ExportsKind::Esm (implicit strict mode) and is now wrapped as CommonJS with no implicit strict mode; the bundler's is_file_considered_to_have_esm_exports also flips. This is arguably the more-correct behavior (type-only exports emit nothing), but it's a cross-cutting change beyond the PR's stated ".cjs/.cts at runtime" scope with no test coverage — worth either a test asserting the new classification or a note in the PR description.
Extended reasoning...
What changed
The eight new p.esm_export_keyword = previous_export_keyword restores in parse_stmt.rs (at the export as namespace, export type, export interface, export declare, export default <type-only>, and export {type X} returns) run unconditionally — they are not gated on .cts, module_type == Cjs, or !p.options.bundle. Before this PR, esm_export_keyword was restored only for export = value (the pre-existing T::TEquals arm at parse_stmt.rs:1350), matching esbuild.
What consumes esm_export_keyword
esm_export_keyword feeds several downstream classifiers that are unrelated to the new .cts check in parse_entry.rs:
parse_entry.rs:1684—exports_kind = Esmwhenesm_export_keyword.len > 0p.rs:3057-3059—is_file_considered_to_have_esm_exports(consumed atvisit_expr.rs:1910for the bundler's direct-eval()handling)p.rs:3064-3067—has_es_module_syntax |= esm_export_keyword.len > 0p.rs:3122-3124—ImplicitStrictModeExport(implicit strict mode)p.rs:8764—ast.export_keyword
The visit pass (which reads/writes these at p.rs:3057+) runs before the exports-kind classifier at parse_entry.rs:1684, so all of them observe the restored (empty) range.
Step-by-step proof
Take foo.ts under "type": "commonjs" containing only:
export interface Foo {}Before this PR: SInterface was grouped with SNamespace/SAbstract/SModule (no restore) → t_export() sets esm_export_keyword at line 803 and never restores it → p.rs:3122 sets ImplicitStrictModeExport, p.rs:3066 sets has_es_module_syntax = true, p.rs:3057 sets is_file_considered_to_have_esm_exports = true → parse_entry.rs:1684 short-circuits to ExportsKind::Esm. File runs as ESM in implicit strict mode.
After this PR: SInterface arm restores esm_export_keyword = previous_export_keyword (i.e. Range::NONE) → p.rs:3066's OR-in contributes false, and type-only exports never set has_es_module_syntax directly (they return S::TypeScript{}) → has_es_module_syntax = false, no ImplicitStrictModeExport, is_file_considered_to_have_esm_exports = false → parse_entry.rs:1684 is false, :1686 is false, falls through to :1774 ModuleType::Cjs → has_es_module_syntax == false → ExportsKind::Cjs + WrapMode::BunCommonjs. File now runs wrapped as CommonJS with module/exports in scope and not in implicit strict mode.
The same applies to export type Foo = ..., export declare ..., export {type X}, export as namespace, and the export default paths that yield S::TypeScript. For ModuleType::Unknown (no package.json), exports_kind still defaults to Esm at line 1837, but the strict-mode inference (p.rs:3122) and is_file_considered_to_have_esm_exports still change. Because the restores are not !p.options.bundle-gated, the bundler's classification also changes.
Why nothing catches this
The PR's new tests only exercise .cts files (where the restore is required for the new check to work). No test covers a non-.cts TS file whose only export is type-only, and no existing test asserts the old classification.
Impact assessment
The affected surface — files whose only export is type-only, under "type":"commonjs" — is narrow, and the new behavior is arguably more correct: a file whose exports all erase to nothing has no ESM syntax after erasure, and under "type":"commonjs" should be CJS. I can't construct a case where the new behavior is clearly wrong rather than just different. But it is a real, observable, cross-cutting behavior change that diverges from esbuild's classification and is outside the PR's stated ".cjs/.cts at runtime" scope.
Suggested action
Either (a) add a test asserting the new classification for a .ts file with only type-only exports under "type":"commonjs" and note the broader change in the PR description, or (b) if minimizing blast radius is preferred, track a separate "has real ESM export" flag for the .cts check instead of mutating esm_export_keyword globally.
| if let Some((range, what)) = found { | ||
| p.log().add_range_error_with_notes( | ||
| Some(p.source), | ||
| range, | ||
| format!("Cannot use {} in a CommonJS module", what).into_bytes(), | ||
| Box::new([bun_ast::Data { | ||
| text: std::borrow::Cow::Owned( | ||
| format!( | ||
| "This file is CommonJS because it has the \"{}\" extension. Rename it to end in {} to use ES module syntax.", | ||
| bstr::BStr::new(ext), | ||
| rename_to, | ||
| ) | ||
| .into_bytes(), | ||
| ), | ||
| ..Default::default() | ||
| }]), | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
🟡 A .cjs containing both an import statement and a CJS marker (e.g. import 'node:assert'; module.exports = 1;) now emits two errors on the same import record — the new ".cjs extension" error at line 1661, then the pre-existing "Cannot use import statement with CommonJS-only features" + "because 'module' was used" error at line 1765. The second note is misleading for .cjs (removing module.exports wouldn't make the import valid; the extension is authoritative). Gating the pre-existing check at 1692 on rename_to.is_none() (or equivalent) would keep the output to one clean error.
Extended reasoning...
What the bug is
The new .cjs/.cts check at parse_entry.rs:1661 pushes an error into log but does not early-return, set exports_kind, or otherwise signal that the file has already been diagnosed. Execution falls through to the pre-existing exports-kind classifier at lines 1682–1771. When a .cjs file contains both an import statement and a CommonJS marker (module, exports, top-level return, or with), the pre-existing check at line 1708 finds the same ImportKind::Stmt record and emits a second error at the same range.
Step-by-step trace
Take bad.cjs:
import 'node:assert';
module.exports = 1;module_type == Cjs(from.cjs),commonjs_at_runtimeis true, ext is.cjs→rename_to = Some(".mjs").esm_export_keyword.len == 0(noexport), so the check falls through toimport_stmt:p.import_recordscontains oneImportKind::Stmtrecord for'node:assert'→found = Some((record.range, "an 'import' statement")).- Line 1661 emits error Fix ?? operator #1: "Cannot use an 'import' statement in a CommonJS module" + note "This file is CommonJS because it has the ".cjs" extension. Rename it to end in .mjs to use ES module syntax."
- Execution continues.
is_deoptimized_commonjs()is false (module.exports = 1doesn't populatecommonjs_named_exports).esm_export_keyword.len == 0andtop_level_await_keyword.len == 0, so line 1684 is false. uses_module_refis true → line 1686 branch taken →exports_kind = Cjs,wrap_mode = BunCommonjs.- Line 1692–1705 iterates
p.import_recordswith the identical filter and finds the sameImportKind::Stmtrecord. - Line 1765 emits error Fix calling #private() functions in classes #2: "Cannot use import statement with CommonJS-only features" + note "Try require("node:assert") instead" + note "This file is CommonJS because 'module' was used".
Both errors go into log; both are printed.
Why the second error is now misleading
Before this PR, only error #2 fired for this input, and its note ("CommonJS because 'module' was used") was accurate — removing module.exports would have caused Bun to sniff the file as ESM and accept the import. After this PR, that note is misleading: removing module.exports still leaves the .cjs-extension error, because the extension is now authoritative. The two hints ("rename to .mjs" vs "try require()") also point at different remediations, though both are individually valid.
Why nothing prevents it
The new check is deliberately non-short-circuiting so the fall-through classifier still assigns exports_kind / wrap_mode correctly. But the fall-through classifier's own import-statement diagnostic at line 1708 has no visibility into whether the new check already fired — there is no flag threaded between them, and rename_to is scoped inside the new block.
Impact
Diagnostic-quality only. The file is correctly rejected either way (it was already rejected before this PR via error #2), and the primary (new, correct) error appears first. Nothing breaks if merged as-is.
How to fix
Skip the pre-existing import-statement diagnostic when the extension check already fired. One option: hoist rename_to (or a cjs_ext_error_emitted bool) out of the new block and gate line 1692's search on it being None/false. Alternatively, when the new check fires and esm_export_keyword.len == 0 && top_level_await_keyword.len == 0, set exports_kind = Cjs; wrap_mode = BunCommonjs; directly and skip the classifier entirely for .cjs/.cts.
What does this PR do?
The
.cjs/.ctsextension is unconditionally CommonJS in Node, regardless ofpackage.jsonor file content:export,importstatements, and top-levelawaitare aSyntaxErrorthere. Bun was classifying these files by content instead, so a.cjsfile containingexport const x = 7was silently run as an ES module. A package could ship a.cjsthat passed Bun-based CI and was unloadable in Node, with nothing telling the author their.cjswasn't what its extension said.Cause
In
js_parser::parse_entry,p.esm_export_keyword.len > 0short-circuited toExportsKind::Esmbeforep.options.module_typewas consulted, and theModuleType::Cjsarm fell back toExportsKind::Esmwhenhas_es_module_syntaxwas set. The extension contract was never enforced.This surfaced two more gaps on the off-thread transpile path:
RuntimeTranspilerStore::transpilederivedmodule_typeonly from the nearestpackage.json"type"field, not the file extension, so an imported.cjssawModuleType::Unknown. A.cjswith nomodule/exports/requirereferences loaded viaimportwas then sniffed as ESM (modulewasundefinedat runtime).logwhile still returning a well-formed AST were dropped byAsyncModule::fulfillwhenparse_errorwasNone.Fix
parse_entry.rs: whenmodule_type == Cjs,commonjs_at_runtime, not bundling, and the file is.cjs/.cts, emit a ranged error at the firstexport/import/awaitkeyword. Scoped to the extension (not all ofModuleType::Cjs) because Bun has long allowedexportin.js/.tsunder"type": "commonjs"and packages depend on it; the bundler continues to accept mixed input.RuntimeTranspilerStore.rs/jsc_hooks.rs: threadmodule_type(already computed from extension + package.json) through to the off-thread parse job instead of re-deriving fromResolvedSourceTag.RuntimeTranspilerStore.rs: setparse_errorwhenlog.errors > 0after a successful parse so the error reaches the import promise.parse_entry.rs/RuntimeTranspilerCache.rs: includemodule_typein the runtime transpiler hash and bump the cache version, since byte-identical sources now produce different results permodule_type.Two existing fixtures that relied on the old behavior were updated:
require-extensions.test.ts's "secretly sync esm" case now uses a.customextension (its intent is to testrequire.extensionswrapping, not the.cjscontract), and the unreferencedmodule-type-fixture/*/import.cjsfixtures were removed.This overlaps with #33807 on the
RuntimeTranspilerStoreplumbing and the features-hash addition; whichever lands second needs a trivial rebase there.How did you verify your code works?
test/cli/run/run-cjs.test.tscovers.cjsand.ctsas entrypoint, viaimport *, viaawait import(), viarequire(), withexport const/export default/bareimport/namedimport/top-levelawait, plus negative controls (dynamicimport()expression still allowed,.jsunder"type":"commonjs"withexportstill accepted, marker-less.cjsimported as CommonJS). 11 of 15 fail on the released binary and all 15 pass on this branch.