Repository navigation
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
AI generated, but ran the fix against a full live CJS project. The project uses
package.jsontype ascommonjswith all file extensions as.tsrunning NestJS 12 (ESM) and tests mocked using nodes test modulemock.method.I needed to upgrade to TypeScript 7.0.2 from 5.9.3 which ts-node doesn't support. tsx and other don't support legacy TypeScript decorators (or rather
emitDecoratorMetadata, which NestJS uses). I'm not aware of any other JIT that supports CJS projects needing emit decorator metadata on TS7+.Closes #905
Problem
In a
"type": "commonjs"package, a.tsfile that usesimport/exportruns as an ES module under oxc-node, even when tsconfig compiles it to CommonJS. Its namespace object is then frozen, somock.method()fromnode:testcan't replace its exports. Undertscit can.Node.js' own type stripping doesn't run this file as an ES module either: it rejects it with
SyntaxError: Cannot use import statement outside a module.Fix
A file with ES module syntax is compiled to CommonJS the way
tsccompiles it when all of these hold:"type": "commonjs"moduleiscommonjs,node16,node18,node20ornodenext(any case).ts,.tsx,.js,.jsxor noneimport.metanor top-level await, which have no CommonJS formexports,require,module,__filename,__dirname,__importDefault,__importStar,__exportStarAnywhere else, such as a package with no
"type","type": "module", or an ESmodulesetting, the file runs as an ES module, as before. Switching a project's"type"to"module"turns the compilation off.The output has
tsc's shape. It's built from the module runner transform's output:exports.a = void 0; … exports.a = a;). It's writable, somock.method(namespace, "a")reaches every importer. A hoisted function is exported up front.cjs-module-lexerdetects.require()'s result. Default and namespace imports go through__importDefault/__importStar,export *through__exportStar, andimport()staysimport().format: "commonjs", so an ES module importer gets the named exports.The compiled code
require()s./lib.jswhere the source imported./lib.js. SoModule._resolveFilenameinregister.mjsfalls back to the TypeScript source (.js→.ts/.tsx,.jsx→.tsx/.ts,.mjs→.mts,.cjs→.cts). For a TypeScript importer outsidenode_modules, it also prefers the source over compiled output sitting beside it, as the ES module resolver does since #840. The hook thread's helper tag is registered on the main thread too, so the compiled files canrequire()their helpers.Under
node16,node18andnodenext, the resolve hook no longer reports every.tsfile as an ES module; the package's"type"decides, as it does fortsc. This is the same change as #908. Whichever PR lands second drops it.Behaviour changes
These apply only to projects with
"type": "commonjs"and one of themodulesettings above:import/exportnow runs as CommonJS. An ES module's default import of it ismodule.exports, with the file's default export at.default, the same as TypeScript types it undernodenext.export *from another TypeScript file: Node.js only follows re-exports into.jsand.cjsfiles.node16/node18/nodenext, a.tsfile without module syntax is CommonJS, also when an ES module imports it.require()of an ES module from a compiled file loaded through the module hooks needs esm: populate separate cache for require(esm) in imported CJS nodejs/node#59679 (22.22.3 on the 22.x line). Earlier versions fail withTypeError: Cannot read properties of undefined (reading 'exports').The README has a new "ES module syntax in a CommonJS package" section covering this.
Tests
commonjs_loweringcovers eachmodulesetting, the output shape, and the cases that stay ES modules (import.meta, reserved names,esnext,preserve,.mts, a package without"type").packages/integrate-commonjspackage ("type": "commonjs",module: nodenext, run withoxnode --test), 15 tests:.jsnames finding.ts, with the source preferred over compiled output beside itimport.metaand top-level await staying ES modulescommonjs.test.tsfails to load onmain.helpers.test.tspasses on both and guards what must not change.pnpm test(591 vitest tests plus the threenode --testpackages),cargo test, clippy and fmt pass.