magic-akari / swc_mut_cjs_exports

[SWC plugin] mutable CJS exports
https://www.npmjs.com/package/swc_mut_cjs_exports
MIT License
57 stars 16 forks source link

Jest commonjs gives ReferenceError: Cannot access before initialization when jest.mock and exported class #92

Open smvv opened 1 year ago

smvv commented 1 year ago

I've read #79 but I think I'm seeing a related issue.

minimal reproducible code

There are two input source files:

// 1. application/web/foo.test.ts 
const run = jest.fn()                    

const fakeBar = {                        
    __esModule: true,                    
    default: {                           
        run,                             
    },                                   
}                                        
jest.mock('./bar', () => fakeBar)        

import bar from './bar'                  

describe('Test example', () => {         
    it('has a passing test', () => {           
        expect(bar.run).not.toBeCalled() 
    })                                   
})

// 2. application/web/bar.ts
export default class Bar {
    static run() {}
}                                

The swc config used for transforming TS to JS in jest is:

{                                                     
  "$schema": "http://json.schemastore.org/swcrc",     
  "sourceMaps": true,                                 
  "jsc": {                                            
    "parser": {                                       
      "syntax": "typescript",                         
      "tsx": true,                                    
      "dynamicImport": true,                          
      "decorators": true                              
    },                                                
    "preserveAllComments": true,                      
    "transform": null,                                
    "target": "es2017",                               
    "loose": false,                                   
    "externalHelpers": false,                         
    "keepClassNames": false,                          
    "experimental": {                                 
      "plugins": [                                    
        [                                             
          "swc_mut_cjs_exports",                      
          {}                                          
        ]                                             
      ]                                               
    }                                                 
  },                                                  
  "module": {                                         
    "type": "commonjs"                                
  }                                                   
}                                                     

The tsc config is:

{
  "transpileOnly": true,
  "compilerOptions": {
    "target": "es2017",
    "module": "commonjs",
    "jsx": "react-jsx",
    "esModuleInterop": true,
    "sourceMap": false,
    "allowJs": true
  }
}

tsc transpiled

Test output:

+ TS_LOADER=tsc yarn -s test application/web/foo.test.ts
 PASS  application/web/foo.test.ts
  Test example
    ✓ has a passing test (1 ms)
// tsc js output of foo.test.ts
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const run = jest.fn();
const fakeBar = {
  __esModule: true,
  default: {
    run,
  },
};
jest.mock("./bar", () => fakeBar);
const bar_1 = tslib_1.__importDefault(require("./bar"));
describe("Test example", () => {
  it("has a passing test", () => {
    expect(bar_1.default.run).not.toBeCalled();
  });
});

// tsc js output of bar.ts
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
class Bar {
  static run() {}
}
exports.default = Bar;

swc transpiled

Test output:

+ TS_LOADER=swc yarn -s test application/web/foo.test.ts
 FAIL  application/web/foo.test.ts
  ● Test suite failed to run

    ReferenceError: Cannot access 'fakeBar' before initialization

       7 |  },
       8 | }
    >  9 | jest.mock('./bar', () => fakeBar)
         |                          ^
      10 |
      11 | import bar from './bar'
      12 |

      at fakeBar (application/web/foo.test.ts:9:26)
      at Object.<anonymous> (application/web/foo.test.ts:6:39)
// swc js output of foo.test.ts
"use strict";
Object.defineProperty(exports, "__esModule", {
  value: true,
});
const _bar = _interop_require_default(require("./bar"));
function _interop_require_default(obj) {
  return obj && obj.__esModule
    ? obj
    : {
        default: obj,
      };
}
const run = jest.fn();
const fakeBar = {
  __esModule: true,
  default: {
    run,
  },
};
jest.mock("./bar", () => fakeBar);
describe("Test example", () => {
  it("has a passing test", () => {
    expect(_bar.default.run).not.toBeCalled();
  });
})

// swc js output of bar.ts
"use strict";
Object.defineProperty(exports, "__esModule", {
  value: true,
});
Object.defineProperty(exports, "default", {
  enumerable: true,
  get() {
    return Bar;
  },
  set(v) {
    Bar = v;
  },
  configurable: true,
});
var Bar;
Bar = class Bar {
  static run() {}
};

Notes

SWC version info:

@swc/core@1.3.95
@swc/jest@0.2.29
@swc/helpers@0.5.3
swc-loader@0.2.3
swc_mut_cjs_exports@0.86.17

Tsc generates this import order:

jest.mock("./bar", () => fakeBar);
const bar_1 = tslib_1.__importDefault(require("./bar"));

while swc generates:

const _bar = _interop_require_default(require("./bar"));
function _interop_require_default(obj) { ... }
const run = jest.fn();
const fakeBar = { ... }
jest.mock("./bar", () => fakeBar);

Could that ordering be an issue that causes the runtime error?

I'm using the jest preset ts-jest with a custom transformer (to allow switching at runtime using TS_LOADER between tsc and swc).

Could a jest plugin like this hoisting of mock calls be related?

I hope this issue contains all the information. Let me know if a reproducible git repo would be helpful to narrow down the problem. Thank you!

smvv commented 1 year ago

note: it seems that esbuild-jest is transforming the test source file using babel when the source code contains the string ock(: https://github.com/aelbore/esbuild-jest/commit/e94d4c181108efc4341956995e36edb3d7d81b45#diff-a2a171449d862fe29692ce031981047d7ab755ae7f84c707aef80701b3ea0c80R34

magic-akari commented 1 year ago

Could that ordering be an issue that causes the runtime error?

Yes. All import statements will be hoisted. In fact, It's a bug of tsc. see https://github.com/microsoft/TypeScript/issues/16166

Could a jest plugin like this hoisting of mock calls be related?

It's possible to hoist jest related methods.

I haven't tested it myself. Have you tried manually editing the statement order of the swc compiled files? If it is feasible, It's better to send a feature request to https://github.com/swc-project/plugins/tree/main/packages/jest , since it is related to import hoisting and this repository focuses on the mutability of export.