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:
Marc 2023-04-10 23:43:21 -04:00
parent 972d9e6edc
commit cf98db4d39
Signed by: marc
GPG key ID: 048E042F22B5DC79
14 changed files with 93 additions and 903 deletions

View file

@ -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
View file

@ -11,7 +11,6 @@ lerna-debug.log*
!.yarn/plugins
!.yarn/versions
!.yarn/sdks
!.yarn/releases
.pnp.*
# Diagnostic reports (https://nodejs.org/api/report.html)

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
yarnPath: .yarn/releases/yarn-3.5.0.cjs

View file

@ -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.

View file

@ -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",

View file

@ -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),
}
}

View file

@ -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)

View file

@ -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 }))
}

View file

@ -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')

View file

@ -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 {

View file

@ -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

View file

@ -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)
},

View file

@ -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