@exodus/test 1.0.0-rc.97 → 1.0.0-rc.98

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
@@ -327,6 +327,9 @@ if (process.env.EXODUS_TEST_IGNORE) {
327
327
  ignore.push(process.env.EXODUS_TEST_IGNORE)
328
328
  }
329
329
 
330
+ // This might be used in presets, so has to be loaded before jest
331
+ if (options.flow && !options.bundle) args.push('--import', import.meta.resolve('../loader/flow.js'))
332
+
330
333
  // The comment below is disabled, we don't auto-mock @jest/globals anymore, and having our loader first is faster
331
334
  // [Disabled] Our loader should be last, as enabling module mocks confuses other loaders
332
335
  let jestConfig = null
package/loader/flow.js ADDED
@@ -0,0 +1,17 @@
1
+ import { registerHooks } from 'node:module' // 22.15+
2
+ import { readFileSync } from 'node:fs'
3
+ import flowRemoveTypes from '@exodus/test-bundler/flow-remove-types'
4
+
5
+ function load(url, context, nextLoad) {
6
+ if (url.startsWith('file://')) {
7
+ const source = readFileSync(new URL(url), 'utf8')
8
+ if (source.includes('@flow')) {
9
+ const stripped = flowRemoveTypes(source, { pretty: true }).toString()
10
+ return { format: context.format || 'commonjs', source: stripped, shortCircuit: true }
11
+ }
12
+ }
13
+
14
+ return nextLoad(url, context)
15
+ }
16
+
17
+ registerHooks({ load })
package/loader/jest.js CHANGED
@@ -1,4 +1,2 @@
1
- const { loadJestConfig, installJestEnvironment } = await import('../src/jest.config.js')
2
- await loadJestConfig()
3
- const { should, ...jestGlobals } = await import('../src/jest.js') // eslint-disable-line @typescript-eslint/no-unused-vars
4
- await installJestEnvironment(jestGlobals)
1
+ const { setupJest } = await import('../src/jest.setup.js')
2
+ await setupJest()
@@ -1,4 +1,5 @@
1
1
  import { readFile } from 'node:fs/promises'
2
+ import { stripTypeScriptTypes } from 'node:module' // 22.13.0+
2
3
  import { fileURLToPath } from 'node:url'
3
4
 
4
5
  const extensionsRegex = /\.[cm]?ts$/
@@ -12,18 +13,11 @@ function shouldProcessUrl(s) {
12
13
  }
13
14
  }
14
15
 
15
- let transformSync
16
-
17
16
  export async function load(url, context, nextLoad) {
18
17
  if (shouldProcessUrl(url)) {
19
- if (!transformSync) {
20
- const amaro = await import('amaro')
21
- transformSync = amaro.transformSync
22
- }
23
-
24
18
  const sourceBuf = await readFile(new URL(url))
25
19
  const source = sourceBuf.toString('utf8')
26
- const { code: transformed } = transformSync(source, { isModule: true })
20
+ const transformed = stripTypeScriptTypes(source, { mode: 'strip' })
27
21
  const transformedBuf = Buffer.from(transformed)
28
22
  if (sourceBuf.length !== transformedBuf.length) throw new Error('length mismatch')
29
23
  // eslint-disable-next-line unicorn/no-for-loop
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/test",
3
- "version": "1.0.0-rc.97",
3
+ "version": "1.0.0-rc.98",
4
4
  "author": "Exodus Movement, Inc.",
5
5
  "description": "A test suite runner",
6
6
  "homepage": "https://github.com/ExodusMovement/test",
@@ -48,6 +48,7 @@
48
48
  "loader/babel.cjs",
49
49
  "loader/esbuild.js",
50
50
  "loader/esbuild.optional.js",
51
+ "loader/flow.js",
51
52
  "loader/jest.js",
52
53
  "loader/typescript.js",
53
54
  "loader/typescript.loader.js",
@@ -66,6 +67,7 @@
66
67
  "src/jest.environment.js",
67
68
  "src/jest.fn.js",
68
69
  "src/jest.mock.js",
70
+ "src/jest.setup.js",
69
71
  "src/jest.snapshot.js",
70
72
  "src/jest.timers.js",
71
73
  "src/node.js",
@@ -84,7 +86,7 @@
84
86
  "scripts": {
85
87
  "test:_bundle": "EXODUS_TEST_IGNORE='tests/{{jest-extended,inband}/**,jest-when/when.test.*,jest/jest.resetModules.*,jest/mock/jest.mock.mocks-dir.test.js}' npm run test --",
86
88
  "test": "npm run test:jest --",
87
- "test:all": "npm run test:simple && npm run test:jest && npm run test:tape && npm run test:native && npm run test:esbuild && npm run test:pure && npm run test:typescript && npm run test:fetch && npm run test:jsdom && npm run test:bundle",
89
+ "test:all": "npm run test:simple && npm run test:jest && npm run test:tape && npm run test:native && npm run test:esbuild && npm run test:pure && npm run test:fetch && npm run test:jsdom && npm run test:bundle",
88
90
  "test:native": "EXODUS_TEST_IGNORE='{**/typescript/**,**/jest-repo/**/user.test.js}' ./bin/index.js --jest 'tests/**/*.test.{js,cjs,mjs}'",
89
91
  "test:typescript": "node ./bin/index.js --jest --typescript tests/typescript.test.ts",
90
92
  "test:jest": "node ./bin/index.js --jest --esbuild=ts,user.test.js,sum.test.js",
@@ -128,8 +130,7 @@
128
130
  "optionalDependencies": {
129
131
  "@chalker/queue": "^1.0.1",
130
132
  "@exodus/replay": "^1.0.0-rc.9",
131
- "@exodus/test-bundler": "1.0.0-rc.3",
132
- "amaro": "^0.0.5",
133
+ "@exodus/test-bundler": "1.0.0-rc.4",
133
134
  "c8": "^9.1.0",
134
135
  "expect": "^29.7.0",
135
136
  "fast-glob": "^3.2.11",
@@ -81,6 +81,8 @@ export async function loadJestConfig(...args) {
81
81
  if (config.globalTeardown) config.globalTeardown = req.resolve(config.globalTeardown) // eslint-disable-line @exodus/mutable/no-param-reassign-prop-only
82
82
  }
83
83
 
84
+ const presetExtension = /\.([cm]?js|json)$/u
85
+ const suffixes = ['/jest-preset.json', '/jest-preset.js', '/jest-preset.cjs', '/jest-preset.mjs']
84
86
  if (needPreset(rawConfig) || rawConfig.globalSetup || rawConfig.globalTeardown) {
85
87
  rawConfig.preset = cleanFile(rawConfig.preset) // relative to root dir only at top level, presets shouldn't use <rootDir>
86
88
  if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
@@ -93,15 +95,11 @@ export async function loadJestConfig(...args) {
93
95
  let requireConfig = createRequire(resolve(rawConfig.rootDir, 'package.json'))
94
96
  resolveGlobalSetup(rawConfig, requireConfig)
95
97
  while (needPreset(rawConfig)) {
96
- const suffixes = rawConfig.preset.startsWith('.')
97
- ? ['']
98
- : ['/jest-preset.json', '/jest-preset.js', '/jest-preset.cjs', '/jest-preset.mjs']
99
-
100
98
  let baseConfig
101
- for (const suffix of suffixes) {
102
- if (baseConfig) break
99
+
100
+ const attemptLoad = async (file) => {
103
101
  try {
104
- const resolved = requireConfig.resolve(`${rawConfig.preset}${suffix}`)
102
+ const resolved = requireConfig.resolve(file)
105
103
  // FIXME: fix linter to allow this
106
104
  // const meta = resolved.toLowerCase().endsWith('.json') ? { with: { type: 'json' } } : undefined
107
105
  // const presetModule = await import(pathToFileURL(resolved), meta)
@@ -111,6 +109,17 @@ export async function loadJestConfig(...args) {
111
109
  } catch {}
112
110
  }
113
111
 
112
+ // Even if it is relative, it could be a path to module
113
+ for (const suffix of suffixes) {
114
+ if (!baseConfig) await attemptLoad(`${rawConfig.preset}${suffix}`)
115
+ }
116
+
117
+ // If it's a path to a file
118
+ if (!baseConfig && rawConfig.preset[0] === '.' && presetExtension.test(rawConfig.preset)) {
119
+ const { statSync } = await import('node:fs')
120
+ if (statSync(rawConfig.preset).isFile()) await attemptLoad(rawConfig.preset)
121
+ }
122
+
114
123
  assert(baseConfig, `Could not load preset: ${rawConfig.preset} `)
115
124
  resolveGlobalSetup(baseConfig, requireConfig)
116
125
  rawConfig = {
package/src/jest.mock.js CHANGED
@@ -11,44 +11,55 @@ import {
11
11
  import { jestfn } from './jest.fn.js'
12
12
  import { loadExpect } from './expect.cjs'
13
13
  import { loadPrettyFormat } from './pretty-format.cjs'
14
- import { makeEsbuildMockable, insideEsbuild } from './dark.cjs'
14
+ import { makeEsbuildMockable, insideEsbuild, createCallerLocationHook } from './dark.cjs'
15
15
 
16
16
  const mapMocks = new Map()
17
17
  const mapActual = new Map()
18
18
  const nodeMocks = new Map()
19
19
  const overridenBuiltins = new Set()
20
20
 
21
- function wrap(impl) {
22
- return function (...args) {
23
- impl(...args)
24
- return this
25
- }
26
- }
27
-
21
+ // TODO: support correct relative locations in other engines too (and bundles)
22
+ const { getCallerLocation: getLoc } = createCallerLocationHook()
28
23
  export const jestModuleMocks = {
29
- mock: wrap((name, mock) => jestmock(name, mock, { override: true })),
30
- doMock: wrap((name, mock) => jestmock(name, mock)),
31
- setMock: wrap((name, mock) => jestmock(name, () => mock)), // like doMock, does not hoist to top, tested
32
- unmock: wrap(unmock),
33
- dontMock: wrap(unmock),
34
- createMockFromModule: (name) => mockClone(requireActual(name)),
35
- requireMock,
36
- requireActual,
24
+ mock(name, mock) {
25
+ jestmock(name, mock, { override: true, loc: getLoc() })
26
+ return this
27
+ },
28
+ doMock(name, mock) {
29
+ jestmock(name, mock, { loc: getLoc() })
30
+ return this
31
+ },
32
+ setMock(name, mock) {
33
+ jestmock(name, () => mock, { loc: getLoc() }) // like doMock, does not hoist to top, tested
34
+ return this
35
+ },
36
+ unmock(name) {
37
+ unmock(name, { loc: getLoc() })
38
+ return this
39
+ },
40
+ createMockFromModule: (name) => mockClone(requireActual(name, { loc: getLoc() })),
41
+ requireMock: (name) => requireMock(name, { loc: getLoc() }),
42
+ requireActual: (name) => requireActual(name, { loc: getLoc() }),
37
43
  resetModules,
38
44
  }
39
45
 
46
+ jestModuleMocks.dontMock = jestModuleMocks.unmock
47
+
40
48
  if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
41
49
  globalThis.EXODUS_TEST_MOCK_BUILTINS = new Map()
42
50
  Object.assign(jestModuleMocks, {
43
- __mockBundle: wrap((name, builtin, actual, mock) =>
51
+ __mockBundle(name, builtin, actual, mock) {
44
52
  jestmock(name, mock, { actual, builtin, override: true })
45
- ),
46
- __doMockBundle: wrap((name, builtin, actual, mock) =>
53
+ return this
54
+ },
55
+ __doMockBundle(name, builtin, actual, mock) {
47
56
  jestmock(name, mock, { actual, builtin })
48
- ),
49
- __setMockBundle: wrap((name, builtin, actual, mock) =>
57
+ return this
58
+ },
59
+ __setMockBundle(name, builtin, actual, mock) {
50
60
  jestmock(name, () => mock, { actual, builtin })
51
- ),
61
+ return this
62
+ },
52
63
  })
53
64
  }
54
65
 
@@ -56,7 +67,7 @@ if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
56
67
  const cjsSet = typeof __mocksCJSPossible === 'undefined' ? null : __mocksCJSPossible // eslint-disable-line no-undef
57
68
  const esmSet = typeof __mocksESMPossible === 'undefined' ? null : __mocksESMPossible // eslint-disable-line no-undef
58
69
 
59
- function resolveModule(name) {
70
+ function resolveModule(name, loc) {
60
71
  if (process.env.EXODUS_TEST_ENVIRONMENT === 'bundle') {
61
72
  assert(name.startsWith('bundle:'), `Can't mock unresolved ${name} in bundle, use static syntax`)
62
73
  assert(cjsSet && esmSet, 'Module mocking not installed correctly in bundle')
@@ -72,30 +83,33 @@ function resolveModule(name) {
72
83
  return id
73
84
  }
74
85
 
75
- assert(requireIsRelative || /^[@a-zA-Z]/u.test(name), 'Mocking relative paths is not possible')
76
86
  const unprefixed = name.replace(/^node:/, '')
77
87
  if (builtinModules.includes(unprefixed)) return unprefixed
88
+ if (loc?.[2]) return require('node:module').createRequire(loc?.[2]).resolve(name)
89
+ assert(requireIsRelative || /^[@a-zA-Z]/u.test(name), 'Mocking relative paths is not possible')
78
90
  return require.resolve(name)
79
91
  }
80
92
 
81
- function resolveImport(name) {
93
+ function resolveImport(name, loc) {
82
94
  try {
83
- const { fileURLToPath } = require('node:url')
84
- return fileURLToPath(import.meta.resolve(name))
95
+ const { fileURLToPath, pathToFileURL } = require('node:url')
96
+ let parent
97
+ if (loc?.[2]) parent = loc[2].startsWith('file:') ? loc[2] : pathToFileURL(loc[2])
98
+ return fileURLToPath(import.meta.resolve(name, parent))
85
99
  } catch {
86
100
  return null
87
101
  }
88
102
  }
89
103
 
90
- function requireActual(name) {
91
- const resolved = resolveModule(name)
104
+ function requireActual(name, { loc } = {}) {
105
+ const resolved = resolveModule(name, loc)
92
106
  if (mapActual.has(resolved)) return mapActual.get(resolved)
93
107
  if (!mapMocks.has(resolved)) return require(resolved)
94
108
  throw new Error('Module can not been loaded')
95
109
  }
96
110
 
97
- function requireMock(name) {
98
- const resolved = resolveModule(name)
111
+ function requireMock(name, { loc } = {}) {
112
+ const resolved = resolveModule(name, loc)
99
113
  assert(mapMocks.has(resolved), 'Module is not mocked')
100
114
  return mapMocks.get(resolved)
101
115
  }
@@ -112,8 +126,8 @@ function resetModules() {
112
126
  }
113
127
  }
114
128
 
115
- function unmock(name) {
116
- const resolved = resolveModule(name)
129
+ function unmock(name, { loc } = {}) {
130
+ const resolved = resolveModule(name, loc)
117
131
  assert(mapMocks.has(resolved), 'Module is not mocked')
118
132
  if (mockModule) nodeMocks.get(resolved).restore()
119
133
  delete require.cache[resolved]
@@ -281,7 +295,7 @@ if (process.env.EXODUS_TEST_ENVIRONMENT !== 'bundle') {
281
295
  }
282
296
  }
283
297
 
284
- function jestmock(name, mocker, { override = false, actual, builtin } = {}) {
298
+ function jestmock(name, mocker, { override = false, actual, builtin, loc } = {}) {
285
299
  // Loaded ESM: isn't mocked
286
300
  // Loaded CJS: mocked via object overriding
287
301
  // Loaded built-ins: mocked via object overriding where possible
@@ -294,7 +308,7 @@ function jestmock(name, mocker, { override = false, actual, builtin } = {}) {
294
308
 
295
309
  const mockFromMocks = mocker ? undefined : loadMocksDirMock?.(name)
296
310
 
297
- const resolved = resolveModule(name)
311
+ const resolved = resolveModule(name, loc)
298
312
  const isBuiltIn = builtinModules.includes(resolved)
299
313
  if (!mocker && mockFromMocks && mapMocks.get(resolved) === mockFromMocks) return
300
314
  assert(!mapMocks.has(resolved), 'Re-mocking the same module is not supported')
@@ -346,8 +360,8 @@ function jestmock(name, mocker, { override = false, actual, builtin } = {}) {
346
360
  return this
347
361
  }
348
362
 
349
- const topLevelESM = isTopLevelESM()
350
- let likelyESM = topLevelESM && !insideEsbuild() && ![null, resolved].includes(resolveImport(name))
363
+ const topESM = isTopLevelESM()
364
+ let likelyESM = topESM && !insideEsbuild() && ![null, resolved].includes(resolveImport(name, loc))
351
365
  let isOverridenBuiltinSynchedWithESM = false
352
366
  const isNodeCache = (x) => x && x.id && x.path && x.filename && x.children && x.paths && x.loaded
353
367
  if (isBuiltIn && !isNodeCache(require.cache[resolved])) {
@@ -386,7 +400,7 @@ function jestmock(name, mocker, { override = false, actual, builtin } = {}) {
386
400
  }
387
401
 
388
402
  const mocksNodeVersionNote = 'mocks are available only on Node.js >=20.18 <21 || >=22.3'
389
- if (likelyESM || (!isOverridenBuiltinSynchedWithESM && topLevelESM)) {
403
+ if (likelyESM || (!isOverridenBuiltinSynchedWithESM && topESM)) {
390
404
  // Native module mocks is required if loading ESM or __from__ ESM
391
405
  // No good way to check the locations that import the module, but we can check top-level file
392
406
  // Built-in modules are fine though
@@ -395,17 +409,19 @@ function jestmock(name, mocker, { override = false, actual, builtin } = {}) {
395
409
  assert(mockModule, `Native non-overriding node:* ${mocksNodeVersionNote}`)
396
410
  }
397
411
 
398
- if (value[Symbol.toStringTag] === 'Module') value.__esModule = true
412
+ if (value?.[Symbol.toStringTag] === 'Module') value.__esModule = true
399
413
  const obj = { defaultExport: value }
400
414
  if (isBuiltIn && isObject(value)) obj.namedExports = value
401
415
  if (insideEsbuild()) {
402
416
  // esbuild handles unwrapping just default exports for us
403
417
  assert(!likelyESM) // should not be reachable
404
- const { default: defaultExport, __esModule, ...namedExports } = value // eslint-disable-line @typescript-eslint/no-unused-vars
405
- // Don't override defaultExport, as that's processed with esbuild
406
- // Add named exports though for further static named imports from that module
407
- // type:module and esbuild can be combined e.g. when testing typescript packages
408
- if (__esModule) obj.namedExports = namedExports
418
+ if (isObject(value)) {
419
+ const { default: defaultExport, __esModule, ...namedExports } = value // eslint-disable-line @typescript-eslint/no-unused-vars
420
+ // Don't override defaultExport, as that's processed with esbuild
421
+ // Add named exports though for further static named imports from that module
422
+ // type:module and esbuild can be combined e.g. when testing typescript packages
423
+ if (__esModule) obj.namedExports = namedExports
424
+ }
409
425
  } else if (likelyESM && isObject(value) && value.__esModule === true) {
410
426
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
411
427
  const { default: defaultExport, __esModule, ...namedExports } = value
@@ -0,0 +1,7 @@
1
+ import { loadJestConfig, installJestEnvironment } from './jest.config.js'
2
+
3
+ export async function setupJest() {
4
+ await loadJestConfig()
5
+ const { should, ...jestGlobals } = await import('./jest.js') // eslint-disable-line @typescript-eslint/no-unused-vars
6
+ await installJestEnvironment(jestGlobals)
7
+ }