@exodus/test 1.0.0-rc.4 → 1.0.0-rc.40
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/LICENSE +21 -0
- package/README.md +87 -28
- package/bin/babel-worker.cjs +62 -0
- package/bin/bundle.js +305 -0
- package/bin/index.js +449 -60
- package/bin/jest.js +4 -0
- package/bin/typescript.js +3 -0
- package/bin/typescript.loader.js +24 -0
- package/package.json +116 -16
- package/src/bundle-apis/ansi-styles.cjs +49 -0
- package/src/bundle-apis/assert-strict.cjs +1 -0
- package/src/bundle-apis/child_process.cjs +10 -0
- package/src/bundle-apis/crypto.cjs +5 -0
- package/src/bundle-apis/empty/function-throw.cjs +4 -0
- package/src/bundle-apis/empty/module-throw.cjs +1 -0
- package/src/bundle-apis/fs-promises.cjs +1 -0
- package/src/bundle-apis/fs.cjs +88 -0
- package/src/bundle-apis/globals.cjs +185 -0
- package/src/bundle-apis/http.cjs +119 -0
- package/src/bundle-apis/https.cjs +11 -0
- package/src/bundle-apis/jest-message-util.js +5 -0
- package/src/bundle-apis/jest-util.js +22 -0
- package/src/bundle-apis/node-buffer.cjs +3 -0
- package/src/bundle-apis/util-format.cjs +41 -0
- package/src/bundle-apis/ws.cjs +20 -0
- package/src/dark.cjs +145 -0
- package/src/engine.js +22 -0
- package/src/engine.node.cjs +41 -0
- package/src/engine.pure.cjs +469 -0
- package/src/engine.select.cjs +5 -0
- package/src/jest.config.fs.js +54 -0
- package/src/jest.config.js +138 -0
- package/src/jest.environment.js +76 -0
- package/src/jest.fn.js +30 -27
- package/src/jest.js +226 -0
- package/src/jest.mock.js +265 -0
- package/src/jest.snapshot.js +179 -0
- package/src/jest.timers.js +98 -0
- package/src/node.js +10 -0
- package/src/replay.js +103 -0
- package/src/tape.cjs +15 -0
- package/src/tape.js +160 -0
- package/src/version.js +18 -0
- package/bin/preload.js +0 -3
- package/src/index.js +0 -141
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Exodus Movement, Inc
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,18 +1,84 @@
|
|
|
1
1
|
# @exodus/test
|
|
2
2
|
|
|
3
|
+
A runner for `node:test`, `jest`, and `tape` test suites on top of `node:test` (and any runtime)
|
|
4
|
+
|
|
3
5
|
Most likely it will just work on your simple jest tests as as drop-in replacement
|
|
4
6
|
|
|
7
|
+
Comes with typescript support, optional esm/cjs interop, and also loading babel transforms!
|
|
8
|
+
|
|
9
|
+
Use `--coverage` to generate coverage output
|
|
10
|
+
|
|
11
|
+
Default `NODE_ENV` value is "test", use `NODE_ENV=` to override (e.g. to empty)
|
|
12
|
+
|
|
13
|
+
## Why?
|
|
14
|
+
|
|
15
|
+
- Can run your tests on Node.js, Bun, Deno, JavaScriptCore and Hermes without extra churn
|
|
16
|
+
|
|
17
|
+
- Unlike `jest`, it is fast
|
|
18
|
+
|
|
19
|
+
- Unlike `node:test`, it is a drop-in replacement for `jest`
|
|
20
|
+
|
|
21
|
+
- With `expect`, support for snapshots, mocks and matchers
|
|
22
|
+
|
|
23
|
+
- `jest-when` and `jest-extended` are fully compatible and can just be used
|
|
24
|
+
|
|
25
|
+
- Snapshots are compatible with Jest and can just be used both ways
|
|
26
|
+
|
|
27
|
+
- Also compatible to `node:test`
|
|
28
|
+
|
|
29
|
+
- Unlike `bun:test`, it runs all test files in isolated contexts
|
|
30
|
+
|
|
31
|
+
Bun leaks globals / side effects between test files and has incompatible `test()` lifecycle / order
|
|
32
|
+
|
|
33
|
+
- Can use Jest config
|
|
34
|
+
|
|
35
|
+
- Native coverage support (enable via `--coverage`)
|
|
36
|
+
|
|
37
|
+
- Can record / replay `fetch` and `WebSocket` sessions. And run them on all runtimes (including Hermes)
|
|
38
|
+
|
|
39
|
+
- Automatic polyfills for JavaScriptCore / Hermes, including crypto
|
|
40
|
+
|
|
41
|
+
- Hanging tests error by default (unlike `jest`)
|
|
42
|
+
|
|
43
|
+
- Native ESM out of the box
|
|
44
|
+
|
|
45
|
+
- Esbuild on the fly for babelified ESM interop (enable via `--esbuild`)
|
|
46
|
+
|
|
47
|
+
- TypeScript support in both transform (enable via `--esbuild`) and typestrip (via `--typescript`) modes
|
|
48
|
+
|
|
49
|
+
- Babel support, picks up your Babel config (enable via `--babel`)
|
|
50
|
+
|
|
51
|
+
- `--drop-network` support for guaranteed offline testing
|
|
52
|
+
|
|
5
53
|
## Library
|
|
6
54
|
|
|
55
|
+
### Using with `node:test` natively
|
|
56
|
+
|
|
57
|
+
You can just use pure [`node:test`](https://nodejs.org/api/test.html) in your tests,
|
|
58
|
+
this runner is fully compatible with that (and will set version-specific options for you)!
|
|
59
|
+
|
|
7
60
|
### Moving from jest
|
|
8
61
|
|
|
9
|
-
|
|
62
|
+
```js
|
|
63
|
+
import {
|
|
64
|
+
jest,
|
|
65
|
+
expect,
|
|
66
|
+
describe,
|
|
67
|
+
it,
|
|
68
|
+
beforeEach,
|
|
69
|
+
afterEach,
|
|
70
|
+
beforeAll,
|
|
71
|
+
afterAll,
|
|
72
|
+
} from '@exodus/test/jest'
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Or, run with [`--jest` option](#options) to register jest globals
|
|
10
76
|
|
|
11
77
|
### Moving from tap/tape
|
|
12
78
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
79
|
+
```js
|
|
80
|
+
import test from '@exodus/test/tape'
|
|
81
|
+
```
|
|
16
82
|
|
|
17
83
|
### Running tests asynchronously
|
|
18
84
|
|
|
@@ -20,38 +86,19 @@ Add `{ concurrency: true }`, like this: `describe('my testsuite', { concurrency:
|
|
|
20
86
|
|
|
21
87
|
### List of exports
|
|
22
88
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
- `jest` -- jest mock adapter
|
|
26
|
-
- `tap` -- tap/tape adapter
|
|
27
|
-
- `mock`
|
|
28
|
-
|
|
29
|
-
Assertions:
|
|
89
|
+
- `@exodus/test/jest` -- `jest` mock
|
|
30
90
|
|
|
31
|
-
- `
|
|
32
|
-
- `expect` -- expect with additional features for function mocks
|
|
33
|
-
|
|
34
|
-
Suite:
|
|
35
|
-
|
|
36
|
-
- `describe`
|
|
37
|
-
- `test`
|
|
38
|
-
- `it` -- alias for `test`
|
|
39
|
-
- `beforeEach`
|
|
40
|
-
- `afterEach`
|
|
41
|
-
- `before` -- alias for `beforeAll`
|
|
42
|
-
- `after` -- alias for `afterAll`
|
|
91
|
+
- `@exodus/test/tape` -- `tape` mock (can also be helpful when moving from `tap`)
|
|
43
92
|
|
|
44
93
|
## Binary
|
|
45
94
|
|
|
46
|
-
Just use `"test: "exodus-test"`
|
|
95
|
+
Just use `"test": "exodus-test"`
|
|
47
96
|
|
|
48
97
|
### Options
|
|
49
98
|
|
|
50
|
-
- `--
|
|
51
|
-
|
|
52
|
-
- `--typescript` -- use typescript loader (which also compiles esm to cjs where needed)
|
|
99
|
+
- `--jest` -- register jest test helpers as global variables, also load `jest.config.*` configuration options
|
|
53
100
|
|
|
54
|
-
- `--esbuild` -- use esbuild loader
|
|
101
|
+
- `--esbuild` -- use esbuild loader, also enables Typescript support
|
|
55
102
|
|
|
56
103
|
- `--babel` -- use babel loader (slower than `--esbuild`, makes sense if you have a special config)
|
|
57
104
|
|
|
@@ -61,4 +108,16 @@ Just use `"test: "exodus-test"`
|
|
|
61
108
|
|
|
62
109
|
- `--coverage-engine node` -- use Node.js builtint coverage engine
|
|
63
110
|
|
|
111
|
+
- `--watch` -- operate in watch mode and re-run tests on file changes
|
|
112
|
+
|
|
113
|
+
- `--only` -- only run the tests marked with `test.only`
|
|
114
|
+
|
|
64
115
|
- `--passWithNoTests` -- do not error when no test files were found
|
|
116
|
+
|
|
117
|
+
- `--write-snapshots` -- write snapshots instead of verifying them (has `--test-update-snapshots` alias)
|
|
118
|
+
|
|
119
|
+
- `--test-force-exit` -- force exit after tests are done (useful in integration tests where it could be unfeasible to resolve all open handles)
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
[MIT](./LICENSE)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const { Worker, MessageChannel, isMainThread, parentPort } = require('node:worker_threads')
|
|
2
|
+
const { once } = require('node:events')
|
|
3
|
+
const { availableParallelism } = require('node:os')
|
|
4
|
+
|
|
5
|
+
if (isMainThread) {
|
|
6
|
+
const maxWorkers = availableParallelism() >= 4 ? 2 : 1
|
|
7
|
+
const workers = []
|
|
8
|
+
|
|
9
|
+
const getWorker = () => {
|
|
10
|
+
const idle = workers.find((info) => info.busy === 0)
|
|
11
|
+
if (idle) return idle
|
|
12
|
+
|
|
13
|
+
if (workers.length < maxWorkers) {
|
|
14
|
+
const worker = new Worker(__filename)
|
|
15
|
+
worker.unref()
|
|
16
|
+
// unhandled top-level errors will crash automatically, which is desired behavior, no need to listen to error
|
|
17
|
+
workers.unshift({ worker, busy: 0 })
|
|
18
|
+
} else if (workers.length > 1) {
|
|
19
|
+
workers.sort((a, b) => a.busy - b.busy)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return workers[0]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const transformAsync = async (code, options) => {
|
|
26
|
+
const info = getWorker()
|
|
27
|
+
info.busy++
|
|
28
|
+
const channel = new MessageChannel()
|
|
29
|
+
info.worker.postMessage({ port: channel.port1, code, options }, [channel.port1])
|
|
30
|
+
const [{ result, error }] = await once(channel.port2, 'message')
|
|
31
|
+
info.busy--
|
|
32
|
+
if (error) throw error
|
|
33
|
+
return result
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { transformAsync }
|
|
37
|
+
} else {
|
|
38
|
+
const babel = require('@babel/core')
|
|
39
|
+
const tryLoadPlugin = (name) => {
|
|
40
|
+
// Try unwrapping plugin names, as otherwise Babel tries to require them from the wrong dir,
|
|
41
|
+
// which breaks strict directory structure under pnpm in some setups
|
|
42
|
+
try {
|
|
43
|
+
if (typeof name === 'string' && name.startsWith('@babel/plugin-')) return require(name)
|
|
44
|
+
} catch {}
|
|
45
|
+
|
|
46
|
+
return name
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
parentPort.on('message', ({ port, code: input, options }) => {
|
|
50
|
+
try {
|
|
51
|
+
// eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only
|
|
52
|
+
if (options.plugins) options.plugins = options.plugins.map((name) => tryLoadPlugin(name))
|
|
53
|
+
const { code, sourcetype, map } = babel.transformSync(input, options) // async here is useless and slower
|
|
54
|
+
// additional properties are deleted as we don't want to transfer e.g. Plugin instances
|
|
55
|
+
port.postMessage({ result: { code, sourcetype, map } })
|
|
56
|
+
} catch (error) {
|
|
57
|
+
port.postMessage({ error })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
port.close()
|
|
61
|
+
})
|
|
62
|
+
}
|
package/bin/bundle.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { readFile } from 'node:fs/promises'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
5
|
+
import { basename, dirname, extname, resolve, join } from 'node:path'
|
|
6
|
+
import { createRequire } from 'node:module'
|
|
7
|
+
import { randomUUID as uuid, randomBytes } from 'node:crypto'
|
|
8
|
+
import * as esbuild from 'esbuild'
|
|
9
|
+
import glob from 'fast-glob'
|
|
10
|
+
|
|
11
|
+
const require = createRequire(import.meta.url)
|
|
12
|
+
const resolveRequire = (query) => require.resolve(query)
|
|
13
|
+
const resolveImport = import.meta.resolve && ((query) => fileURLToPath(import.meta.resolve(query)))
|
|
14
|
+
|
|
15
|
+
const readSnapshots = async (files, resolvers) => {
|
|
16
|
+
const snapshots = []
|
|
17
|
+
for (const file of files) {
|
|
18
|
+
for (const resolver of resolvers) {
|
|
19
|
+
const snapshotFile = join(...resolver(dirname(file), basename(file)))
|
|
20
|
+
try {
|
|
21
|
+
snapshots.push([snapshotFile, await readFile(snapshotFile, 'utf8')])
|
|
22
|
+
} catch (e) {
|
|
23
|
+
if (e.code !== 'ENOENT') throw e
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return snapshots
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// These packages throw on import
|
|
32
|
+
const blockedDeps = ['@pollyjs/adapter-node-http', '@pollyjs/node-server']
|
|
33
|
+
const loadPipeline = [
|
|
34
|
+
function (source, filepath) {
|
|
35
|
+
return source
|
|
36
|
+
.replace(/\bimport\.meta\.url\b/g, JSON.stringify(pathToFileURL(filepath)))
|
|
37
|
+
.replace(/\b(__dirname|import\.meta\.dirname)\b/g, JSON.stringify(dirname(filepath)))
|
|
38
|
+
.replace(/\b(__filename|import\.meta\.filename)\b/g, JSON.stringify(filepath))
|
|
39
|
+
},
|
|
40
|
+
function (source, filepath) {
|
|
41
|
+
// Just a convenience wrapper to show pretty errors instead of generic bundle-apis/empty/module-throw.cjs
|
|
42
|
+
for (const pkg of blockedDeps) {
|
|
43
|
+
const str = `require(${JSON.stringify(pkg)})`
|
|
44
|
+
assert(!str.includes("'"))
|
|
45
|
+
const err = `module unsupported in bundled form: ${pkg}\n loaded from ${filepath}`
|
|
46
|
+
const rep = `((() => { throw new Error(${JSON.stringify(err)}) })())`
|
|
47
|
+
for (const sub of [str, str.replaceAll('"', "'")]) source = source.replace(sub, rep)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return source
|
|
51
|
+
},
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
const options = {}
|
|
55
|
+
|
|
56
|
+
export const init = async ({ platform, jest, flow, target, jestConfig, outdir }) => {
|
|
57
|
+
Object.assign(options, { platform, jest, flow, target, jestConfig, outdir })
|
|
58
|
+
|
|
59
|
+
if (options.flow) {
|
|
60
|
+
const { default: flowRemoveTypes } = await import('flow-remove-types')
|
|
61
|
+
loadPipeline.unshift((source) => flowRemoveTypes(source, { pretty: true }).toString())
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (options.platform === 'hermes') {
|
|
65
|
+
const babel = await import('./babel-worker.cjs')
|
|
66
|
+
loadPipeline.push(async (source) => {
|
|
67
|
+
const result = await babel.transformAsync(source, {
|
|
68
|
+
compact: false,
|
|
69
|
+
babelrc: false,
|
|
70
|
+
configFile: false,
|
|
71
|
+
plugins: [
|
|
72
|
+
'@babel/plugin-syntax-typescript',
|
|
73
|
+
'@babel/plugin-transform-block-scoping',
|
|
74
|
+
'@babel/plugin-transform-class-properties',
|
|
75
|
+
'@babel/plugin-transform-classes',
|
|
76
|
+
'@babel/plugin-transform-private-methods',
|
|
77
|
+
],
|
|
78
|
+
})
|
|
79
|
+
return result.code
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const hermesSupported = {
|
|
85
|
+
arrow: false,
|
|
86
|
+
class: false, // we get a safeguard check this way that it's not used
|
|
87
|
+
'async-generator': false,
|
|
88
|
+
'const-and-let': false, // have to explicitly set for esbuild to not emit that in helpers, also to get a safeguard check
|
|
89
|
+
'for-await': false,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const getPackageFiles = async (dir) => {
|
|
93
|
+
// Returns an empty list on errors
|
|
94
|
+
let patterns
|
|
95
|
+
try {
|
|
96
|
+
patterns = JSON.parse(await readFile(resolve(dir, 'package.json'), 'utf8')).files
|
|
97
|
+
} catch {}
|
|
98
|
+
|
|
99
|
+
if (!patterns) {
|
|
100
|
+
const parent = dirname(dir)
|
|
101
|
+
if (parent !== dir) return getPackageFiles(parent)
|
|
102
|
+
return []
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Hack for now, TODO: fix this
|
|
106
|
+
const expanded = patterns.flatMap((x) => (x.includes('.') ? [x] : [x, `${x}/**/*`]))
|
|
107
|
+
return glob(expanded, { ignore: ['**/node_modules'], cwd: dir, absolute: true })
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const loadCache = new Map()
|
|
111
|
+
const loadSourceFile = async (filepath) => {
|
|
112
|
+
if (!loadCache.has(filepath)) {
|
|
113
|
+
const load = async () => {
|
|
114
|
+
let contents = await readFile(filepath, 'utf8')
|
|
115
|
+
for (const transform of loadPipeline) contents = await transform(contents, filepath)
|
|
116
|
+
return contents
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
loadCache.set(filepath, load())
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return loadCache.get(filepath)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const build = async (...files) => {
|
|
126
|
+
const input = []
|
|
127
|
+
const importSource = async (file) => input.push(await loadSourceFile(resolveRequire(file)))
|
|
128
|
+
const importFile = (...args) => input.push(`await import(${JSON.stringify(resolve(...args))});`)
|
|
129
|
+
const stringify = (x) => ([undefined, null].includes(x) ? `${x}` : JSON.stringify(x))
|
|
130
|
+
|
|
131
|
+
if (!['node'].includes(options.platform)) {
|
|
132
|
+
if (['jsc', 'hermes', 'd8'].includes(options.platform)) {
|
|
133
|
+
const entropy = randomBytes(5 * 1024).toString('base64')
|
|
134
|
+
input.push(`globalThis.EXODUS_TEST_CRYPTO_ENTROPY = ${stringify(entropy)};`)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await importSource('../src/bundle-apis/globals.cjs')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (options.jest) {
|
|
141
|
+
const { jestConfig } = options
|
|
142
|
+
const preload = [...(jestConfig.setupFiles || []), ...(jestConfig.setupFilesAfterEnv || [])]
|
|
143
|
+
if (jestConfig.testEnvironment && jestConfig.testEnvironment !== 'node') {
|
|
144
|
+
const { specialEnvironments } = await import('../src/jest.environment.js')
|
|
145
|
+
assert(Object.hasOwn(specialEnvironments, jestConfig.testEnvironment))
|
|
146
|
+
preload.push(...(specialEnvironments[jestConfig.testEnvironment].dependencies || []))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (preload.length === 0) {
|
|
150
|
+
input.push(`globalThis.EXODUS_TEST_PRELOADED = []`)
|
|
151
|
+
} else {
|
|
152
|
+
assert(jestConfig.rootDir)
|
|
153
|
+
const local = createRequire(resolve(jestConfig.rootDir, 'package.json'))
|
|
154
|
+
const w = (f) => `[${stringify(f)}, () => require(${stringify(local.resolve(f))})]`
|
|
155
|
+
input.push(`globalThis.EXODUS_TEST_PRELOADED = [${preload.map((f) => w(f)).join(', ')}]`)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
await importSource('./jest.js')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (const file of files) importFile(file)
|
|
162
|
+
|
|
163
|
+
const filename = files.length === 1 ? `${files[0]}-${uuid().slice(0, 8)}` : `bundle-${uuid()}`
|
|
164
|
+
const outfile = `${join(options.outdir, filename)}.js`
|
|
165
|
+
const EXODUS_TEST_SNAPSHOTS = await readSnapshots(files, [
|
|
166
|
+
(dir, name) => [dir, `${name}.snapshot`], // node:test
|
|
167
|
+
(dir, name) => [dir, '__snapshots__', `${name}.snap`], // jest
|
|
168
|
+
])
|
|
169
|
+
const EXODUS_TEST_RECORDINGS = await readSnapshots(files, [
|
|
170
|
+
(dir, name) => [dir, '__recordings__', 'fetch', `${name}.json`],
|
|
171
|
+
(dir, name) => [dir, '__recordings__', 'websocket', `${name}.json`],
|
|
172
|
+
])
|
|
173
|
+
const buildWrap = async (opts) => esbuild.build(opts).catch((err) => err)
|
|
174
|
+
let main = input.join(';\n')
|
|
175
|
+
if (['jsc', 'hermes', 'd8'].includes(options.platform)) {
|
|
176
|
+
const exit = `EXODUS_TEST_PROCESS.exitCode = 1; EXODUS_TEST_PROCESS._maybeProcessExitCode();`
|
|
177
|
+
main = `try {\n${main}\n} catch (err) { print(err); ${exit} }`
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const fsfiles = await getPackageFiles(filename ? dirname(resolve(filename)) : process.cwd())
|
|
181
|
+
|
|
182
|
+
const hasBuffer = ['node', 'bun'].includes(options.platform)
|
|
183
|
+
const api = (f) => resolveRequire(join('../src/bundle-apis', f))
|
|
184
|
+
const res = await buildWrap({
|
|
185
|
+
logLevel: 'silent',
|
|
186
|
+
stdin: {
|
|
187
|
+
contents: `(async function () {\n${main}\n})()`,
|
|
188
|
+
resolveDir: dirname(fileURLToPath(import.meta.url)),
|
|
189
|
+
},
|
|
190
|
+
bundle: true,
|
|
191
|
+
outdir: options.outdir,
|
|
192
|
+
entryNames: filename,
|
|
193
|
+
platform: 'neutral',
|
|
194
|
+
mainFields: ['browser', 'module', 'main'],
|
|
195
|
+
define: {
|
|
196
|
+
'process.env.FORCE_COLOR': stringify('0'),
|
|
197
|
+
'process.env.NO_COLOR': stringify('1'),
|
|
198
|
+
'process.env.NODE_ENV': stringify(process.env.NODE_ENV),
|
|
199
|
+
'process.env.EXODUS_TEST_CONTEXT': stringify('pure'),
|
|
200
|
+
'process.env.EXODUS_TEST_ENVIRONMENT': stringify('bundle'), // always 'bundle'
|
|
201
|
+
'process.env.EXODUS_TEST_PLATFORM': stringify(process.env.EXODUS_TEST_PLATFORM), // e.g. 'hermes', 'node'
|
|
202
|
+
'process.env.EXODUS_TEST_ENGINE': stringify(process.env.EXODUS_TEST_ENGINE), // e.g. 'hermes:bundle', 'node:bundle'
|
|
203
|
+
'process.env.EXODUS_TEST_JEST_CONFIG': stringify(JSON.stringify(options.jestConfig)),
|
|
204
|
+
'process.env.EXODUS_TEST_EXECARGV': stringify(process.env.EXODUS_TEST_EXECARGV),
|
|
205
|
+
'process.env.EXODUS_TEST_ONLY': stringify(process.env.EXODUS_TEST_ONLY),
|
|
206
|
+
'process.env.NODE_DEBUG': stringify(),
|
|
207
|
+
'process.env.DEBUG': stringify(),
|
|
208
|
+
'process.env.READABLE_STREAM': stringify(),
|
|
209
|
+
'process.env.CI': stringify(process.env.CI),
|
|
210
|
+
'process.env.CI_ENABLE_VERBOSE_LOGS': stringify(process.env.CI_ENABLE_VERBOSE_LOGS),
|
|
211
|
+
'process.browser': stringify(true),
|
|
212
|
+
'process.emitWarning': 'undefined',
|
|
213
|
+
'process.stderr': 'undefined',
|
|
214
|
+
'process.stdout': 'undefined',
|
|
215
|
+
'process.type': 'undefined',
|
|
216
|
+
'process.version': stringify('v22.5.1'), // shouldn't depend on currently used Node.js version
|
|
217
|
+
'process.versions.node': stringify('22.5.1'), // see line above
|
|
218
|
+
EXODUS_TEST_FILES: stringify(files.map((f) => [dirname(f), basename(f)])),
|
|
219
|
+
EXODUS_TEST_SNAPSHOTS: stringify(EXODUS_TEST_SNAPSHOTS),
|
|
220
|
+
EXODUS_TEST_RECORDINGS: stringify(EXODUS_TEST_RECORDINGS),
|
|
221
|
+
EXODUS_TEST_FSFILES: stringify(fsfiles), // TODO: can we safely use relative paths?
|
|
222
|
+
},
|
|
223
|
+
alias: {
|
|
224
|
+
// Jest and tape
|
|
225
|
+
'@jest/globals': resolveImport('../src/jest.js'),
|
|
226
|
+
tape: resolveImport('../src/tape.cjs'),
|
|
227
|
+
'tape-promise/tape': resolveImport('../src/tape.cjs'),
|
|
228
|
+
// Node browserify
|
|
229
|
+
'node:assert': dirname(dirname(resolveRequire('assert/'))),
|
|
230
|
+
'node:assert/strict': api('assert-strict.cjs'),
|
|
231
|
+
'node:fs': api('fs.cjs'),
|
|
232
|
+
'node:fs/promises': api('fs-promises.cjs'),
|
|
233
|
+
fs: api('fs.cjs'),
|
|
234
|
+
'fs/promises': api('fs-promises.cjs'),
|
|
235
|
+
assert: dirname(dirname(resolveRequire('assert/'))),
|
|
236
|
+
buffer: hasBuffer ? api('node-buffer.cjs') : dirname(resolveRequire('buffer/')),
|
|
237
|
+
child_process: api('child_process.cjs'),
|
|
238
|
+
constants: resolveRequire('constants-browserify'),
|
|
239
|
+
crypto: api('crypto.cjs'),
|
|
240
|
+
events: dirname(resolveRequire('events/')),
|
|
241
|
+
http: api('http.cjs'),
|
|
242
|
+
https: api('https.cjs'),
|
|
243
|
+
os: resolveRequire('os-browserify'),
|
|
244
|
+
path: resolveRequire('path-browserify'),
|
|
245
|
+
querystring: resolveRequire('querystring-es3'),
|
|
246
|
+
stream: resolveRequire('stream-browserify'),
|
|
247
|
+
timers: resolveRequire('timers-browserify'),
|
|
248
|
+
url: dirname(resolveRequire('url/')),
|
|
249
|
+
util: dirname(resolveRequire('util/')),
|
|
250
|
+
zlib: resolveRequire('browserify-zlib'),
|
|
251
|
+
// expect-related deps
|
|
252
|
+
'ansi-styles': api('ansi-styles.cjs'),
|
|
253
|
+
'jest-util': api('jest-util.js'),
|
|
254
|
+
'jest-message-util': api('jest-message-util.js'),
|
|
255
|
+
// unwanted deps
|
|
256
|
+
bindings: api('empty/function-throw.cjs'),
|
|
257
|
+
'node-gyp-build': api('empty/function-throw.cjs'),
|
|
258
|
+
ws: api('ws.cjs'),
|
|
259
|
+
// unsupported deps
|
|
260
|
+
...Object.fromEntries(blockedDeps.map((n) => [n, api('empty/module-throw.cjs')])),
|
|
261
|
+
},
|
|
262
|
+
sourcemap: ['hermes', 'jsc', 'd8'].includes(options.platform) ? 'inline' : 'linked', // FIXME?
|
|
263
|
+
sourcesContent: false,
|
|
264
|
+
keepNames: true,
|
|
265
|
+
format: 'iife',
|
|
266
|
+
target: options.target || `node${process.versions.node}`,
|
|
267
|
+
supported: {
|
|
268
|
+
bigint: true,
|
|
269
|
+
...(options.platform === 'hermes' ? hermesSupported : {}),
|
|
270
|
+
},
|
|
271
|
+
plugins: [
|
|
272
|
+
{
|
|
273
|
+
name: 'exodus-test.bundle',
|
|
274
|
+
setup({ onLoad }) {
|
|
275
|
+
onLoad({ filter: /\.[cm]?[jt]sx?$/, namespace: 'file' }, async (args) => {
|
|
276
|
+
let filepath = args.path
|
|
277
|
+
// Resolve .native versions
|
|
278
|
+
// TODO: move flag to engine options
|
|
279
|
+
// TODO: maybe follow package.json for this
|
|
280
|
+
if (['jsc', 'hermes'].includes(options.platform)) {
|
|
281
|
+
const maybeNative = filepath.replace(/(\.[cm]?[jt]sx?)$/u, '.native$1')
|
|
282
|
+
if (existsSync(maybeNative)) filepath = maybeNative
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const loader = extname(filepath).replace(/^\.[cm]?/, '') // TODO: a flag to force jsx/tsx perhaps
|
|
286
|
+
assert(['js', 'ts', 'jsx', 'tx'].includes(loader))
|
|
287
|
+
|
|
288
|
+
return { contents: await loadSourceFile(filepath), loader }
|
|
289
|
+
})
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
})
|
|
294
|
+
assert.equal(res instanceof Error, res.errors.length > 0)
|
|
295
|
+
|
|
296
|
+
// if (res.errors.length === 0) require('fs').copyFileSync(outfile, 'tempout.cjs') // DEBUG
|
|
297
|
+
|
|
298
|
+
// We treat warnings as errors, so just merge all them
|
|
299
|
+
const errors = []
|
|
300
|
+
const formatOpts = { color: process.stdout.hasColors?.(), terminalWidth: process.stdout.columns }
|
|
301
|
+
const formatMessages = (list, kind) => esbuild.formatMessages(list, { kind, ...formatOpts })
|
|
302
|
+
if (res.warnings.length > 0) errors.push(...(await formatMessages(res.warnings, 'warning')))
|
|
303
|
+
if (res.errors.length > 0) errors.push(...(await formatMessages(res.errors, 'error')))
|
|
304
|
+
return { file: outfile, errors }
|
|
305
|
+
}
|