Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions packages/plugin-rsc/src/transforms/cjs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ if (true) {
expect(await testTransform(input)).toMatchInlineSnapshot(`
"let __filename = "/test.js"; let __dirname = "/";
let exports = {}; const module = { exports };
function __cjs_interop__(m) {return m.__cjs_module_runner_transform || "default" in m && Object.keys(m).every((k) => k === "default" || m[k] === m.default[k]) ? m.default : m;}
function __cjs_interop__(m) {return m.__cjs_module_runner_transform || "module.exports" in m && m["module.exports"] === m.default || "default" in m && Object.keys(m).every((k) => k === "default" || m.default != null && m[k] === m.default[k]) ? m.default : m;}
if (true) {
module.exports = (__cjs_interop__(await import('./cjs/use-sync-external-store.production.js')));
} else {
Expand All @@ -69,7 +69,7 @@ if (true) {
expect(await testTransform(input)).toMatchInlineSnapshot(`
"let __filename = "/test.js"; let __dirname = "/";
let exports = {}; const module = { exports };
function __cjs_interop__(m) {return m.__cjs_module_runner_transform || "default" in m && Object.keys(m).every((k) => k === "default" || m[k] === m.default[k]) ? m.default : m;}
function __cjs_interop__(m) {return m.__cjs_module_runner_transform || "module.exports" in m && m["module.exports"] === m.default || "default" in m && Object.keys(m).every((k) => k === "default" || m.default != null && m[k] === m.default[k]) ? m.default : m;}
const __cjs_to_esm_hoist_0 = __cjs_interop__(await import("react"));
const __cjs_to_esm_hoist_1 = __cjs_interop__(await import("react-dom"));
"production" !== process.env.NODE_ENV && (function() {
Expand Down Expand Up @@ -100,7 +100,7 @@ function test() {
expect(await testTransform(input)).toMatchInlineSnapshot(`
"let __filename = "/test.js"; let __dirname = "/";
let exports = {}; const module = { exports };
function __cjs_interop__(m) {return m.__cjs_module_runner_transform || "default" in m && Object.keys(m).every((k) => k === "default" || m[k] === m.default[k]) ? m.default : m;}
function __cjs_interop__(m) {return m.__cjs_module_runner_transform || "module.exports" in m && m["module.exports"] === m.default || "default" in m && Object.keys(m).every((k) => k === "default" || m.default != null && m[k] === m.default[k]) ? m.default : m;}
const __cjs_to_esm_hoist_0 = __cjs_interop__(await import("te" + "st"));
const __cjs_to_esm_hoist_1 = __cjs_interop__(await import("test"));
const __cjs_to_esm_hoist_2 = __cjs_interop__(await import("test"));
Expand Down Expand Up @@ -196,6 +196,12 @@ function test() {
},
"depPrimitive": "[ok]",
"dualLib": "ok",
"interop": {
"defaultOnlyNull": true,
"markerCallable": true,
"markerFalsy": true,
"nullDefaultNamespace": true,
},
"testExternalFalsyPrimitive": {
"ok": true,
},
Expand Down
17 changes: 14 additions & 3 deletions packages/plugin-rsc/src/transforms/cjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,27 @@ import { buildScopeTree } from './scope'
// Runtime helper to handle CJS/ESM interop when transforming require() to import()
// Unwrap .default for modules
// 1. if it was transformed by this plugin (marked with __cjs_module_runner_transform)
// 2. if all named exports point to .default properties (common CJS pattern)
// 2. if Node marks .default as the exact CommonJS module.exports value
// https://nodejs.org/api/esm.html#commonjs-namespaces
// 3. if all named exports point to .default properties (common CJS pattern)
// this is particularly important for Node built-in modules consumptions;
// where the built-in modules are not transformed by this plugin but still follow the CJS export pattern
// see [getESMFacade](https://github.com/nodejs/node/blob/f200685d9930404d610a52d9e06513bf0a821ed4/lib/internal/bootstrap/realm.js#L347-L360)
//
// This ensures we don't incorrectly unwrap .default on genuine ESM modules
// Without a "module.exports" export, require(esm) returns the module namespace
// object and exposes an ESM default export as its .default property:
// https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require
// https://tc39.es/ecma262/#sec-module-namespace-exotic-objects
//
// Keep genuine ESM namespaces intact; the null guard also avoids dereferencing
// a nullish default when the namespace has named exports.
function __cjs_interop__(m: any) {
return m.__cjs_module_runner_transform ||
('module.exports' in m && m['module.exports'] === m.default) ||
('default' in m &&
Object.keys(m).every((k) => k === 'default' || m[k] === m.default[k]))
Object.keys(m).every(
(k) => k === 'default' || (m.default != null && m[k] === m.default[k]),
))
? m.default
: m
}
Expand Down
2 changes: 2 additions & 0 deletions packages/plugin-rsc/src/transforms/fixtures/cjs/entry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import testExternalFalsyPrimitive from './external-falsy-primitive.cjs'
import depFnRequire from './function-require.cjs'
import depFn from './function.cjs'
import cjsGlobals from './globals.cjs'
import interop from './interop.cjs'
import testNodeBuiltins from './node-builtins.cjs'
import depPrimitive from './primitive.cjs'

Expand All @@ -20,4 +21,5 @@ export {
cjsGlobals,
testNodeBuiltins,
testExternalFalsyPrimitive,
interop,
}
12 changes: 12 additions & 0 deletions packages/plugin-rsc/src/transforms/fixtures/cjs/interop.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Node require(esm) interop semantics:
// https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require
const callable = require('./marker-callable.mjs')
const falsy = require('./marker-falsy.mjs')
const defaultOnlyNull = require('./null-default-only.mjs')
const genuineEsm = require('./null-default.mjs')

exports.markerCallable = callable() === 'ok'
exports.markerFalsy = falsy === false
exports.defaultOnlyNull = defaultOnlyNull === null
exports.nullDefaultNamespace =
genuineEsm.default === null && genuineEsm.named === 'ok'
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const value = () => 'ok'

// Node uses this named export to return the value directly from require(esm).
// https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require
export { value as default, value as 'module.exports' }
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const value = false

export { value as default, value as 'module.exports' }
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default null
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const named = 'ok'
export default null