swc-project / swc

Rust-based platform for the Web

Home Page:https://swc.rs

Repository from Github https://github.comswc-project/swcRepository from Github https://github.comswc-project/swc

require('@swc/helpers/_/_object_spread') use `@swc/helpers/esm/_object_spread.js`, but not `@swc/helpers/cjs/_object_spread.cjs`

liuwenzhuang opened this issue · comments

Describe the bug

// index.cjs
const objectSpreadPath = require.resolve('@swc/helpers/_/_object_spread');
console.log(objectSpreadPath);

the objectSpreadPath expected to be @swc/helpers/cjs/_object_spread.cjs, but actually to be @swc/helpers/esm/_object_spread.js

Input code

Config

Link to the code that reproduces this issue

Blank

SWC Info output

No response

Expected behavior

the topest order of module-sync definition in exports field in package.json lead cjs file cannot be resolved.

Actual behavior

No response

Version

@swc/helpers@0.5.17

Additional context

No response

Can you provide a minimal reproduction that shows why this is a problem?

Can you provide a minimal reproduction that shows why this is a problem?

Sorry, my project is a private company project. In my condition, I have a dual package which depends on @swc/helpers, but the cjs file use the esm file from @swc/helpers. When my package use in some old framework project, like umi3 which parse config only support cjs format, the project will throw error.

But just consider the @swc/helpers package, although cjs and esm format provided, the cjs format file could not load with require().

I think it's not require, but it's require.resolve, right?

Guessing because there's no repro

I think it's not require, but it's require.resolve, right?

Guessing because there's no repro

Same for require and require.resolve. The reproduce step:

  1. npm init -y
  2. pnpm add @swc/helpers
  3. touch index.cjs and fill below code:
const { _: objectSpread } = require("@swc/helpers/_/_object_spread");

console.log(objectSpread({}, { a: 1 }, { b: 2 }));
  1. seprately add console.log to function of @swc/helpers/esm/_object_spread.js and @swc/helpers/cjs/_object_spread.cjs
  2. node index.cjs

the log is from @swc/helpers/esm/_object_spread.js

I don't think it's a bug of SWC.
Also, please create a github repository instead of writing down the steps

I don't think it's a bug of SWC. Also, please create a github repository instead of writing down the steps

Minimal reproduction repository: https://github.com/liuwenzhuang/swc-helpers-cjs-bug

I think it's a bug indeed, @swc/helpers supply cjs format, but could not use with require.

cc @magic-akari What do you think?

This is actually intended behavior and by design. For Node.js versions that support module-sync, the resolution follows Node's native module resolution logic, which prioritizes the module-sync condition. The resolution behavior you're seeing is intentional and typically shouldn't be noticeable to users in normal usage.

Is there a specific use case or requirement that's causing issues with this behavior?

module-sync implicitly overlap the import resolution config, and not support top-level await in its module graph. Maybe explicitly use import , require and default is clearer. And a cjs toolchain may exists some points that not support esm gramma, especially for some old framework like umi3. @magic-akari

About the @swc/helpers Module Format and Distribution

@swc/helpers provides both ESM (ECMAScript modules) and CJS (CommonJS) builds to ensure smooth operation across different JavaScript environments. As part of a recent refactor, we unified import paths and now use the exports field in package.json to control distribution and module resolution. For more background, see the related discussion in swc-project/swc#7157.

On the "Dual Package" Problem

This setup works well in most cases: ESM runtimes use the ESM build, and CJS runtimes use the CJS build. However, issues can arise with bundlers like webpack, which need to interoperate between ESM and CJS before generating a final bundle.

This leads to the dual package problem: the same library may be included twice—once as ESM and once as CJS. Besides increasing bundle size, this can break singleton assumptions. For example, in packages/helpers/esm/_async_generator.js, we check value instanceof _overload_yield. If value comes from a different instance of _overload_yield, the instanceof check will fail—which is unacceptable.

To prevent this, and because webpack already handles ESM/CJS interop internally, we explicitly direct webpack to use the ESM build via the webpack condition in the exports field. This avoids the dual package issue.

Note: Most helper code snippets are adapted from Babel. To maintain consistency and ease of maintenance, we prefer not to modify them.

On Node.js Supporting require() of ESM

The dual package issue primarily occurs in bundler environments. However, modern Node.js versions also allow requiring ESM from CJS, which can reintroduce the same problem.

Our solution is similar: we use the module-sync export condition to prefer the ESM build even when it is required from a CJS context in Node.js versions that support it. Older Node.js versions that do not recognize module-sync or support require(ESM) will fall back to the CJS build. Newer versions that understand module-sync will resolve to ESM, preventing duplicate helper copies in the module graph.

A Note on These Resolution Details

These resolution mechanisms are intended to be transparent to end users. However, some tools may inspect or rely on specific resolution behaviors. Historically, it was common to assume that require and require.resolve would return CJS paths. With Node.js now supporting require(ESM), that assumption is no longer universally valid.

Therefore, the behavior you're observing—where require or require.resolve returns an ESM path—is expected on newer Node.js versions. This is not a bug in SWC but a natural consequence of Node.js's expanded capabilities.

In the future, more and more libraries will be published as ESM, and tools will need to adapt to this reality.

If a tool like umi3 relies on the old expectation that require always resolves to CJS, it implies a dependency on older Node.js behavior. For compatibility with newer Node.js versions, such tools would need to adapt accordingly.

If any maintainers of umi see this, we are open to further discussion.

As a workaround, you can always require the specific path directly, for example: require("@swc/helpers/cjs/_object_spread.cjs") or require.resolve("@swc/helpers/cjs/_object_spread.cjs").

It's important to note, however, that these paths are internal implementation details of SWC's transform process, and end users should not typically need to be aware of them.

ok, convinced me, but I still have reservations about using module-sync.