fix: typescript support when packaged (#18)
* build: corepack, stop bundling yarn executable * chore: bin, main in package.json * feat: allow ts flag to use ts-node to run tests * wip: refine support * ci: add ts support to ci * ci: leftover node 14
This commit is contained in:
parent
972d9e6edc
commit
cf98db4d39
14 changed files with 93 additions and 903 deletions
12
.github/workflows/nodejs.yml
vendored
12
.github/workflows/nodejs.yml
vendored
|
@ -26,7 +26,7 @@ jobs:
|
|||
path: .yarn
|
||||
key: ${{ runner.os }}-build-${{env.cache-name}}-${{ hashFiles('**/yarn.lock') }}-node-${{ matrix.node-version }}
|
||||
- if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }}
|
||||
run: yarn
|
||||
run: corepack enable && yarn
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -45,7 +45,7 @@ jobs:
|
|||
path: .yarn
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}-node-18
|
||||
- run: |
|
||||
yarn
|
||||
corepack enable && yarn
|
||||
yarn lint
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -53,7 +53,7 @@ jobs:
|
|||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14, 16, 18]
|
||||
node-version: [16, 18]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
@ -69,8 +69,8 @@ jobs:
|
|||
path: .yarn
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}-node-${{ matrix.node-version }}
|
||||
- run: |
|
||||
yarn
|
||||
yarn test --workers=2
|
||||
corepack enable && yarn
|
||||
yarn test --workers=2 --ts
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -93,5 +93,5 @@ jobs:
|
|||
path: .yarn
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}-node-${{ matrix.node-version }}
|
||||
- run: |
|
||||
yarn
|
||||
corepack enable && yarn
|
||||
yarn build
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -11,7 +11,6 @@ lerna-debug.log*
|
|||
!.yarn/plugins
|
||||
!.yarn/versions
|
||||
!.yarn/sdks
|
||||
!.yarn/releases
|
||||
.pnp.*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
|
|
873
.yarn/releases/yarn-3.5.0.cjs
vendored
873
.yarn/releases/yarn-3.5.0.cjs
vendored
File diff suppressed because one or more lines are too long
|
@ -1 +0,0 @@
|
|||
yarnPath: .yarn/releases/yarn-3.5.0.cjs
|
|
@ -19,3 +19,9 @@ constraints:
|
|||
|
||||
`womm` is an opinionated implementation of Typescript/Javascript testing libraries we've all come to get used to. You
|
||||
can peek at the opinions baked into this [here](./DESIGN_DECISIONS.md).
|
||||
|
||||
## Development
|
||||
|
||||
This uses `corepack`, which comes bundled with `node>=16` to manage which Yarn version to use.
|
||||
|
||||
To get started, just `corepack enable` before using `yarn` commands.
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
"name": "works-on-my-machine",
|
||||
"description": "A no-dependency test runner",
|
||||
"version": "0.0.0",
|
||||
"main": "dist/cli.js",
|
||||
"main": "dist/index.js",
|
||||
"license": "MIT",
|
||||
"packageManager": "yarn@3.5.0",
|
||||
"files": [
|
||||
"dist/**/*.js"
|
||||
],
|
||||
"bin": {
|
||||
"womm": "dist/runner.js"
|
||||
"womm": "dist/cli.js"
|
||||
},
|
||||
"scripts": {
|
||||
"prepack": "yarn build",
|
||||
|
|
|
@ -11,6 +11,11 @@ export const FLAG_CONFIGURATION: Readonly<FlagConfigurationMap> = {
|
|||
default: false,
|
||||
description: 'Displays the help text.',
|
||||
},
|
||||
ts: {
|
||||
requiresValue: false,
|
||||
default: false,
|
||||
description: 'Use ts-node to run tests (enables typescript support)',
|
||||
},
|
||||
}
|
||||
|
||||
class MalformedArgumentError extends Error {}
|
||||
|
@ -63,6 +68,7 @@ function parseArgs(args: Array<string>): Args {
|
|||
targets: argsWithoutFlags ?? [],
|
||||
help: Boolean(parsedFlags.get('help') ?? FLAG_CONFIGURATION['help'].default),
|
||||
workers: Number(parsedFlags.get('workers') ?? FLAG_CONFIGURATION['workers'].default),
|
||||
ts: Boolean(parsedFlags.get('ts') ?? FLAG_CONFIGURATION['ts'].default),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import helpText from './help'
|
||||
import parseArgs from './argumentParser'
|
||||
import { getContext, redText } from './utils'
|
||||
import { getContext, redText, assertTsNodeInstall } from './utils'
|
||||
import run from './runner'
|
||||
|
||||
/*
|
||||
|
@ -16,7 +16,9 @@ import run from './runner'
|
|||
return
|
||||
}
|
||||
|
||||
const context = getContext(args.runtimePath)
|
||||
if (args.ts) assertTsNodeInstall()
|
||||
|
||||
const context = getContext(args.runtimePath, args.ts)
|
||||
|
||||
try {
|
||||
run(args, context)
|
||||
|
|
|
@ -4,11 +4,17 @@ import { type Context } from './types'
|
|||
import { getContext, spawnProcess } from './utils'
|
||||
|
||||
async function collectCases(context: Context, collectedPaths: Array<string>): Promise<number> {
|
||||
const extraArgs: Array<string> = []
|
||||
|
||||
if (context.ts) extraArgs.push('--transpile-only')
|
||||
|
||||
const runtime = context.ts ? 'ts-node' : 'node'
|
||||
|
||||
let totalCases = 0
|
||||
for await (const collectedPath of collectedPaths) {
|
||||
const collectedCount = await new Promise<number>((resolve, reject) => {
|
||||
let count = 0
|
||||
spawnProcess(context.nodeRuntime, [collectedPath], {
|
||||
spawnProcess(runtime, [...extraArgs, collectedPath], {
|
||||
extraEnv: { COLLECT: '1' },
|
||||
|
||||
onClose: (code) => {
|
||||
|
@ -31,7 +37,9 @@ async function collectCases(context: Context, collectedPaths: Array<string>): Pr
|
|||
*/
|
||||
async function work() {
|
||||
const [, workerRuntime, ...assignedTestFiles] = process.argv
|
||||
const context = getContext(workerRuntime)
|
||||
const tsMode = Boolean(process.env.TS === '1')
|
||||
|
||||
const context = getContext(workerRuntime, tsMode)
|
||||
const collectedCount = await collectCases(context, assignedTestFiles)
|
||||
if (process.send) process.send(JSON.stringify({ total: collectedCount }))
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { forkWorker, greenText, redText, boldText, splitIntoBatches } from './utils'
|
||||
import { forkWorker, greenText, yellowText, redText, boldText, splitIntoBatches } from './utils'
|
||||
import { type Args, type Context, type WorkerReport, type CollectorReport } from './types'
|
||||
|
||||
import { promises as fs } from 'fs'
|
||||
|
@ -15,7 +15,7 @@ async function collectTests(roots: Array<string>): Promise<Array<string>> {
|
|||
for (const root of roots) {
|
||||
const rootStats = await fs.stat(root)
|
||||
|
||||
if (rootStats.isFile() && path.basename(root).endsWith('.test.ts')) {
|
||||
if (rootStats.isFile() && (path.basename(root).endsWith('.test.ts') || path.basename(root).endsWith('.test.js'))) {
|
||||
collectedHere.push(root)
|
||||
} else if (rootStats.isDirectory()) {
|
||||
const content = await fs.readdir(root, { encoding: 'utf8' })
|
||||
|
@ -50,6 +50,7 @@ async function collectCases(context: Context, collectedPaths: Array<string>, wor
|
|||
onMessage: (message: string) => {
|
||||
collectorReport.totalCases += JSON.parse(message).total
|
||||
},
|
||||
extraEnv: { TS: context.nodeRuntime === 'ts-node' ? '1' : '0' },
|
||||
})
|
||||
}),
|
||||
),
|
||||
|
@ -102,6 +103,7 @@ async function assignTestsToWorkers(
|
|||
|
||||
console.log(workerMessage.results)
|
||||
},
|
||||
extraEnv: { TS: context.nodeRuntime === 'ts-node' ? '1' : '0' },
|
||||
})
|
||||
}),
|
||||
),
|
||||
|
@ -117,21 +119,29 @@ async function run(args: Args, context: Context) {
|
|||
performance.mark('run:start')
|
||||
performance.mark('test-collect:start')
|
||||
const collectedTests = await collectTests(args.targets)
|
||||
|
||||
const supportedTests = collectedTests.filter((testPath) => {
|
||||
const supported = (testPath.endsWith('.test.ts') && context.ts) || (!context.ts && !testPath.endsWith('.test.ts'))
|
||||
|
||||
if (!supported) console.log(yellowText(`WARN: ${testPath} is not supported without --ts and will be ignored`))
|
||||
|
||||
return supported
|
||||
})
|
||||
performance.mark('test-collect:end')
|
||||
const testCollectTime = performance.measure('test-collect', 'test-collect:start', 'test-collect:end').duration
|
||||
|
||||
console.log(
|
||||
`Collected ${boldText(collectedTests.length)} test files in ${boldText((testCollectTime / 1000).toFixed(3))}s`,
|
||||
`Collected ${boldText(supportedTests.length)} test files in ${boldText((testCollectTime / 1000).toFixed(3))}s`,
|
||||
)
|
||||
|
||||
performance.mark('case-collect:start')
|
||||
const collectedCaseCount = await collectCases(context, collectedTests)
|
||||
const collectedCaseCount = await collectCases(context, supportedTests)
|
||||
performance.mark('case-collect:end')
|
||||
const caseCollectTime = performance.measure('case-collect', 'case-collect:start', 'case-collect:end').duration
|
||||
console.log(
|
||||
`Collected ${boldText(collectedCaseCount)} test files in ${boldText((caseCollectTime / 1000).toFixed(3))}s`,
|
||||
`Collected ${boldText(collectedCaseCount)} test cases in ${boldText((caseCollectTime / 1000).toFixed(3))}s`,
|
||||
)
|
||||
const summary = await assignTestsToWorkers(context, collectedTests, args.workers)
|
||||
const summary = await assignTestsToWorkers(context, supportedTests, args.workers)
|
||||
|
||||
const hasFailed = Object.values(summary).filter((workerReport) => !workerReport.pass).length > 0
|
||||
performance.mark('run:end')
|
||||
|
|
|
@ -8,6 +8,7 @@ export interface Context {
|
|||
runnerRuntime: string
|
||||
collectorRuntime: string
|
||||
nodeRuntime: 'ts-node' | 'node'
|
||||
ts: boolean
|
||||
}
|
||||
|
||||
export interface Args {
|
||||
|
@ -15,6 +16,7 @@ export interface Args {
|
|||
runtimePath: string
|
||||
help: boolean
|
||||
workers: number
|
||||
ts: boolean
|
||||
}
|
||||
|
||||
export interface MatcherReport {
|
||||
|
|
37
src/utils.ts
37
src/utils.ts
|
@ -11,6 +11,10 @@ export function boldText(text: string | number): string {
|
|||
return `\x1b[1m${text}\x1b[0m`
|
||||
}
|
||||
|
||||
export function yellowText(text: string | number): string {
|
||||
return `\x1b[33m${text}\x1b[0m`
|
||||
}
|
||||
|
||||
export function greenText(text: string | number): string {
|
||||
return `\x1b[32m${text}\x1b[0m`
|
||||
}
|
||||
|
@ -19,23 +23,40 @@ export function redText(text: string | number): string {
|
|||
return `\x1b[31m${text}\x1b[0m`
|
||||
}
|
||||
|
||||
/*
|
||||
* To support typescript source directly, womm uses ts-node in
|
||||
* workers to execute test files.
|
||||
*
|
||||
* If ts-node is not installed, this throws.
|
||||
*/
|
||||
export function assertTsNodeInstall() {
|
||||
try {
|
||||
require.resolve('ts-node')
|
||||
} catch (e) {
|
||||
console.log(redText('Cannot use --ts without also having ts-node installed.'))
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Generates a context object that contains general information
|
||||
* about the test runner. The parameter here should always be
|
||||
* `process.argv[1]`, which will allow all the other paths
|
||||
* to be set properly.
|
||||
*/
|
||||
export function getContext(runnerPath: string): Context {
|
||||
const installDirectory = path.dirname(runnerPath)
|
||||
const runnerExtension = path.extname(runnerPath)
|
||||
export function getContext(runnerPath: string, ts: boolean = false): Context {
|
||||
const resolvedRunnerPath = require.resolve(runnerPath)
|
||||
const installDirectory = path.dirname(resolvedRunnerPath)
|
||||
const runnerExtension = path.extname(resolvedRunnerPath)
|
||||
// TODO: We probably don't need this if we transform TS to JS before execution.
|
||||
const nodeRuntime = runnerExtension === '.ts' ? 'ts-node' : 'node'
|
||||
const nodeRuntime = ts ? 'ts-node' : 'node'
|
||||
|
||||
return {
|
||||
workerRuntime: path.join(installDirectory, `worker${runnerExtension}`),
|
||||
runnerRuntime: runnerPath,
|
||||
collectorRuntime: path.join(installDirectory, `collector${runnerExtension}`),
|
||||
nodeRuntime,
|
||||
ts,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,9 +84,13 @@ export function splitIntoBatches<T>(data: Array<T>, desiredBatchCount: number =
|
|||
export function forkWorker(
|
||||
runtime: string,
|
||||
args: Array<string>,
|
||||
{ onClose, onMessage }: { onClose: (code: number) => void; onMessage: (message: string) => void },
|
||||
{
|
||||
onClose,
|
||||
onMessage,
|
||||
extraEnv,
|
||||
}: { onClose: (code: number) => void; onMessage: (message: string) => void; extraEnv?: { [key: string]: string } },
|
||||
): childProcess.ChildProcess {
|
||||
const workerProcess = childProcess.fork(runtime, args, {})
|
||||
const workerProcess = childProcess.fork(runtime, args, { env: { ...process.env, ...(extraEnv ?? {}) } })
|
||||
workerProcess.on('message', onMessage)
|
||||
workerProcess.on('close', onClose)
|
||||
return workerProcess
|
||||
|
|
|
@ -22,15 +22,21 @@ function formatMessage(results: string, failed: boolean): string {
|
|||
*/
|
||||
async function work() {
|
||||
if (process?.send === undefined) throw Error('No process global found')
|
||||
|
||||
const tsMode = Boolean(process.env.TS === '1')
|
||||
const [, workerRuntime, ...assignedTestFiles] = process.argv
|
||||
const context = getContext(workerRuntime)
|
||||
const context = getContext(workerRuntime, tsMode)
|
||||
|
||||
const extraArgs: Array<string> = []
|
||||
|
||||
if (context.ts) extraArgs.push('--transpile-only')
|
||||
|
||||
const runtime = context.ts ? 'ts-node' : 'node'
|
||||
|
||||
await Promise.all(
|
||||
assignedTestFiles.map(
|
||||
(testFilePath) =>
|
||||
new Promise((resolve, reject) => {
|
||||
spawnProcess(context.nodeRuntime, [path.resolve(testFilePath)], {
|
||||
spawnProcess(runtime, [...extraArgs, path.resolve(testFilePath)], {
|
||||
onClose: (code) => {
|
||||
resolve(code)
|
||||
},
|
||||
|
|
|
@ -262,7 +262,7 @@ __metadata:
|
|||
ts-node: ^10.9.1
|
||||
typescript: ^4.0.0
|
||||
bin:
|
||||
womm: dist/runner.js
|
||||
womm: dist/cli.js
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
|
|
Reference in a new issue