Summary
@swc-node/core's transformOption constructs jsc.experimental as a hardcoded { keepImportAttributes: true } literal with no way for callers to extend it (see here). This makes it effectively impossible to enable SWC plugins (e.g. @swc-contrib/mut-cjs-exports) when going through @swc-node/core or @swc-node/jest without re-implementing the entire transformOption body.
This is especially painful with @swc-node/jest: the only knob exposed by its public API is the Options argument, but Options has no experimental field, and overriding via swc: { jsc: {...} } clobbers every other tsconfig-derived jsc setting (target, paths, parser, transform, etc). See also #700.
Motivation
The most common reason users want to pass a plugin through @swc-node/jest is to make jest.mock / jest.requireActual / Sinon stubs work on modules with circular imports (exactly the use case in #713). The recommended fix in the SWC ecosystem is the @swc-contrib/mut-cjs-exports plugin, which turns ESM export const bindings (compiled to CJS getters by SWC) into plain assignments so they can be mocked.
There is currently no clean way to wire that plugin into @swc-node/jest. Users either:
- Drop
@swc-node/jest and switch to @swc/jest + .swcrc (loses tsconfig integration), or
- Write a custom Jest transformer that calls
core.transformJest with a manually-rebuilt jsc (~70 lines of code that re-implements transformOption), or
- Patch
node_modules/@swc-node/core/lib/index.js (what I'm currently doing via patch-package).
Proposed change
Allow callers to pass an experimental field on Options and merge it into the constructed jsc.experimental. One spread, no behavior change for existing callers.
packages/core/index.ts: extend the Options interface:
export interface Options {
// ...existing fields...
swc?: SwcOptions
ignoreDynamic?: boolean
+ experimental?: {
+ plugins?: Array<[string, Record<string, unknown>]>
+ keepImportAttributes?: boolean
+ }
}
packages/core/index.ts: merge inside transformOption:
baseUrl: opts.baseUrl,
experimental: {
keepImportAttributes: true,
+ ...(opts.experimental ?? {}),
},
After this change, @swc-node/jest users get plugin support for free, with no other changes needed in @swc-node/jest:
// jest.config.js
module.exports = {
transform: {
"^.+\\.[tj]sx?$": [
"@swc-node/jest",
{
experimental: {
plugins: [["@swc-contrib/mut-cjs-exports", {}]],
},
},
],
},
};
Backwards compatibility
- New optional field; all existing call sites unaffected.
keepImportAttributes: true remains the default; user can opt out by passing experimental: { keepImportAttributes: false }.
- No changes to
@swc-node/jest's public API, it already spreads its second-arg options into core.transformJest.
Related
Summary
@swc-node/core'stransformOptionconstructsjsc.experimentalas a hardcoded{ keepImportAttributes: true }literal with no way for callers to extend it (see here). This makes it effectively impossible to enable SWC plugins (e.g.@swc-contrib/mut-cjs-exports) when going through@swc-node/coreor@swc-node/jestwithout re-implementing the entiretransformOptionbody.This is especially painful with
@swc-node/jest: the only knob exposed by its public API is theOptionsargument, butOptionshas noexperimentalfield, and overriding viaswc: { jsc: {...} }clobbers every other tsconfig-derivedjscsetting (target, paths, parser, transform, etc). See also #700.Motivation
The most common reason users want to pass a plugin through
@swc-node/jestis to makejest.mock/jest.requireActual/ Sinon stubs work on modules with circular imports (exactly the use case in #713). The recommended fix in the SWC ecosystem is the@swc-contrib/mut-cjs-exportsplugin, which turns ESMexport constbindings (compiled to CJS getters by SWC) into plain assignments so they can be mocked.There is currently no clean way to wire that plugin into
@swc-node/jest. Users either:@swc-node/jestand switch to@swc/jest+.swcrc(loses tsconfig integration), orcore.transformJestwith a manually-rebuiltjsc(~70 lines of code that re-implementstransformOption), ornode_modules/@swc-node/core/lib/index.js(what I'm currently doing viapatch-package).Proposed change
Allow callers to pass an
experimentalfield onOptionsand merge it into the constructedjsc.experimental. One spread, no behavior change for existing callers.packages/core/index.ts: extend theOptionsinterface:export interface Options { // ...existing fields... swc?: SwcOptions ignoreDynamic?: boolean + experimental?: { + plugins?: Array<[string, Record<string, unknown>]> + keepImportAttributes?: boolean + } }packages/core/index.ts: merge insidetransformOption:baseUrl: opts.baseUrl, experimental: { keepImportAttributes: true, + ...(opts.experimental ?? {}), },After this change,
@swc-node/jestusers get plugin support for free, with no other changes needed in@swc-node/jest:Backwards compatibility
keepImportAttributes: trueremains the default; user can opt out by passingexperimental: { keepImportAttributes: false }.@swc-node/jest's public API, it already spreads its second-arg options intocore.transformJest.Related
@swc-node/jestdoesn't pass options from tsconfig to swc properly (related:swc: { jsc: {...} }is currently the only override path and is too coarse).