@exodus/test 1.0.0-rc.20 → 1.0.0-rc.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/index.js CHANGED
@@ -141,16 +141,6 @@ if (options.coverage) {
141
141
  }
142
142
  }
143
143
 
144
- if (options.esbuild) {
145
- assert(resolveImport)
146
- args.push('--import', resolveImport('tsx'))
147
- }
148
-
149
- if (options.babel) {
150
- assert(!options.esbuild, 'Options --babel and --esbuild are mutually exclusive')
151
- args.push('-r', resolveRequire('./babel.cjs'))
152
- }
153
-
154
144
  const ignore = ['**/node_modules']
155
145
  let filter
156
146
  if (process.env.EXODUS_TEST_IGNORE) {
@@ -159,7 +149,8 @@ if (process.env.EXODUS_TEST_IGNORE) {
159
149
  ignore.push(process.env.EXODUS_TEST_IGNORE)
160
150
  }
161
151
 
162
- // Our loader should be last, as enabling module mocks confuses other loaders
152
+ // The comment below is disabled, we don't auto-mock @jest/globals anymore, and having our loader first is faster
153
+ // [Disabled] Our loader should be last, as enabling module mocks confuses other loaders
163
154
  if (options.jest) {
164
155
  const { loadJestConfig } = await import('../src/jest.config.js')
165
156
  const config = await loadJestConfig(process.cwd())
@@ -196,6 +187,16 @@ if (options.jest) {
196
187
  }
197
188
  }
198
189
 
190
+ if (options.esbuild) {
191
+ assert(resolveImport)
192
+ args.push('--import', resolveImport('tsx'))
193
+ }
194
+
195
+ if (options.babel) {
196
+ assert(!options.esbuild, 'Options --babel and --esbuild are mutually exclusive')
197
+ args.push('-r', resolveRequire('./babel.cjs'))
198
+ }
199
+
199
200
  if (patterns.length === 0) patterns.push(...DEFAULT_PATTERNS) // defaults
200
201
  const globbed = await glob(patterns, { ignore })
201
202
  const allfiles = filter ? globbed.filter(filter) : globbed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/test",
3
- "version": "1.0.0-rc.20",
3
+ "version": "1.0.0-rc.21",
4
4
  "author": "Exodus Movement, Inc.",
5
5
  "description": "A test suite runner",
6
6
  "homepage": "https://github.com/ExodusMovement/test",
package/src/dark.cjs CHANGED
@@ -4,7 +4,7 @@ const mayBeUrlToPath = (str) => (str.startsWith('file://') ? fileURLToPath(str)
4
4
 
5
5
  let locForNextTest
6
6
 
7
- let installLocationInNextTest = function (loc) {
7
+ const installLocationInNextTest = function (loc) {
8
8
  locForNextTest = loc
9
9
  }
10
10
 
@@ -107,4 +107,31 @@ function getTestNamePath(t) {
107
107
  return [t.name] // last resort
108
108
  }
109
109
 
110
- module.exports = { createCallerLocationHook, getTestNamePath }
110
+ function makeEsbuildMockable() {
111
+ const usingTsx = process.execArgv.some((x) => x.endsWith('node_modules/tsx/dist/loader.mjs'))
112
+ if (!usingTsx) return
113
+ // Hook into tsx/esbuild transpiled module conversion magic to make loaded modules mockable in runtime
114
+ // We want all modules to be .configurable = true, so we can override them
115
+ const defineProperty = Object.defineProperty
116
+ const obj = Object.create(null)
117
+ Object.defineProperty = (target, name, options) => {
118
+ if (options.get) {
119
+ const stackTraceLimit = Error.stackTraceLimit
120
+ Error.stackTraceLimit = 2
121
+ Error.captureStackTrace(obj, Object.defineProperty)
122
+ Error.stackTraceLimit = stackTraceLimit
123
+ // This is for speed, we don't want to work with text
124
+ const prepareStackTrace = Error.prepareStackTrace
125
+ // eslint-disable-next-line handle-callback-err
126
+ Error.prepareStackTrace = (err, callsites) => callsites.map((site) => site.getFunctionName())
127
+ const stack = obj.stack
128
+ Error.prepareStackTrace = prepareStackTrace
129
+ // eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only
130
+ if (stack[0] === '__copyProps' && stack[1] === '__toCommonJS') options.configurable = true
131
+ }
132
+
133
+ return defineProperty(target, name, options)
134
+ }
135
+ }
136
+
137
+ module.exports = { createCallerLocationHook, getTestNamePath, makeEsbuildMockable }
package/src/jest.mock.js CHANGED
@@ -4,6 +4,7 @@ import { existsSync } from 'node:fs'
4
4
  import { normalize } from 'node:path'
5
5
  import { mock } from 'node:test'
6
6
  import { jestfn } from './jest.fn.js'
7
+ import { makeEsbuildMockable } from './dark.cjs'
7
8
 
8
9
  const files = process.argv.slice(1)
9
10
  const baseUrl = files.length === 1 && existsSync(files[0]) ? normalize(files[0]) : undefined
@@ -11,6 +12,7 @@ const mapMocks = new Map()
11
12
  const mapActual = new Map()
12
13
 
13
14
  const require = createRequire(baseUrl || import.meta.url)
15
+ const isTopLevelESM = () => !baseUrl || !Object.hasOwn(require.cache, baseUrl) // assume ESM otherwise
14
16
 
15
17
  export const relativeRequire = require
16
18
 
@@ -62,7 +64,9 @@ function override(resolved, lax = false) {
62
64
  Object.defineProperties(current, definitions)
63
65
  const proto = Object.getPrototypeOf(value)
64
66
  if (Object.getPrototypeOf(current) !== proto) Object.setPrototypeOf(current, proto)
65
- if (!lax) assert.deepEqual({ ...current }, value)
67
+ const checked = { ...current }
68
+ if (current.__esModule === true) checked.__esModule = current.__esModule
69
+ if (!lax) assert.deepEqual(checked, value)
66
70
  }
67
71
 
68
72
  function mockClone(obj, cache = new Map()) {
@@ -86,6 +90,12 @@ function mockCloneItem(obj, cache) {
86
90
  }
87
91
 
88
92
  if (typeof obj === 'object') {
93
+ // Special path, as .default might be a getter and we want to unwrap it
94
+ if (obj.__esModule === true) {
95
+ const { __esModule, default: def, ...rest } = obj
96
+ return { __esModule, ...mockClone({ default: def, ...rest }, cache) }
97
+ }
98
+
89
99
  const prototype = Object.getPrototypeOf(obj)
90
100
  const clone = Object.create(prototype === null ? null : Object.prototype)
91
101
  cache.set(obj, clone)
@@ -142,23 +152,37 @@ export function jestmock(name, mocker) {
142
152
  const value = mocker ? { ...mocker() } : mockClone(mapActual.get(resolved))
143
153
  mapMocks.set(resolved, value)
144
154
 
155
+ let likelyESM = false
156
+ const isBuiltIn = builtinModules.includes(resolved)
145
157
  if (Object.hasOwn(require.cache, resolved)) {
146
158
  assert.equal(mapActual.get(resolved), require.cache[resolved].exports)
147
159
  // If we did't have this prior but have now, it means we just loaded it and there are no leaked instances
148
160
  if (havePrior) override(resolved)
149
161
  require.cache[resolved].exports = value
150
- } else if (builtinModules.includes(resolved)) {
162
+ } else if (isBuiltIn) {
151
163
  override(resolved, true) // Override builtin modules
152
164
  syncBuiltinESMExports()
153
165
  } else {
154
166
  // The module doesn't exist or is ESM
155
- assert(mock.module, 'ESM module mocks are available only on Node.js >=22.3')
167
+ likelyESM = true
156
168
  }
157
169
 
158
- mock.module?.(resolved, {
159
- defaultExport: value.default ?? value,
160
- namedExports: isObject(value) ? value : {},
161
- })
170
+ if (!mock.module && !isBuiltIn) {
171
+ // Native module mocks is required if loading ESM or __from__ ESM
172
+ // No good way to check the locations that import the module, but we can check top-level file
173
+ // Built-in modules are fine though
174
+ assert(!likelyESM && !isTopLevelESM(), 'ESM module mocks are available only on Node.js >=22.3')
175
+ }
176
+
177
+ if (likelyESM && isObject(value) && value.__esModule === true) {
178
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
179
+ const { default: defaultExport, __esModule, ...namedExports } = value
180
+ mock.module?.(resolved, { defaultExport, namedExports })
181
+ } else {
182
+ mock.module?.(resolved, { defaultExport: value })
183
+ }
162
184
 
163
185
  return this
164
186
  }
187
+
188
+ makeEsbuildMockable()