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
|
path: .yarn
|
||||||
key: ${{ runner.os }}-build-${{env.cache-name}}-${{ hashFiles('**/yarn.lock') }}-node-${{ matrix.node-version }}
|
key: ${{ runner.os }}-build-${{env.cache-name}}-${{ hashFiles('**/yarn.lock') }}-node-${{ matrix.node-version }}
|
||||||
- if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }}
|
- if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }}
|
||||||
run: yarn
|
run: corepack enable && yarn
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -45,7 +45,7 @@ jobs:
|
||||||
path: .yarn
|
path: .yarn
|
||||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}-node-18
|
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}-node-18
|
||||||
- run: |
|
- run: |
|
||||||
yarn
|
corepack enable && yarn
|
||||||
yarn lint
|
yarn lint
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -53,7 +53,7 @@ jobs:
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [14, 16, 18]
|
node-version: [16, 18]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
@ -69,8 +69,8 @@ jobs:
|
||||||
path: .yarn
|
path: .yarn
|
||||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}-node-${{ matrix.node-version }}
|
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}-node-${{ matrix.node-version }}
|
||||||
- run: |
|
- run: |
|
||||||
yarn
|
corepack enable && yarn
|
||||||
yarn test --workers=2
|
yarn test --workers=2 --ts
|
||||||
|
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -93,5 +93,5 @@ jobs:
|
||||||
path: .yarn
|
path: .yarn
|
||||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}-node-${{ matrix.node-version }}
|
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}-node-${{ matrix.node-version }}
|
||||||
- run: |
|
- run: |
|
||||||
yarn
|
corepack enable && yarn
|
||||||
yarn build
|
yarn build
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -11,7 +11,6 @@ lerna-debug.log*
|
||||||
!.yarn/plugins
|
!.yarn/plugins
|
||||||
!.yarn/versions
|
!.yarn/versions
|
||||||
!.yarn/sdks
|
!.yarn/sdks
|
||||||
!.yarn/releases
|
|
||||||
.pnp.*
|
.pnp.*
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# 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
|
`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).
|
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",
|
"name": "works-on-my-machine",
|
||||||
"description": "A no-dependency test runner",
|
"description": "A no-dependency test runner",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"main": "dist/cli.js",
|
"main": "dist/index.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"packageManager": "yarn@3.5.0",
|
"packageManager": "yarn@3.5.0",
|
||||||
"files": [
|
"files": [
|
||||||
"dist/**/*.js"
|
"dist/**/*.js"
|
||||||
],
|
],
|
||||||
"bin": {
|
"bin": {
|
||||||
"womm": "dist/runner.js"
|
"womm": "dist/cli.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepack": "yarn build",
|
"prepack": "yarn build",
|
||||||
|
|
|
@ -11,6 +11,11 @@ export const FLAG_CONFIGURATION: Readonly<FlagConfigurationMap> = {
|
||||||
default: false,
|
default: false,
|
||||||
description: 'Displays the help text.',
|
description: 'Displays the help text.',
|
||||||
},
|
},
|
||||||
|
ts: {
|
||||||
|
requiresValue: false,
|
||||||
|
default: false,
|
||||||
|
description: 'Use ts-node to run tests (enables typescript support)',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
class MalformedArgumentError extends Error {}
|
class MalformedArgumentError extends Error {}
|
||||||
|
@ -63,6 +68,7 @@ function parseArgs(args: Array<string>): Args {
|
||||||
targets: argsWithoutFlags ?? [],
|
targets: argsWithoutFlags ?? [],
|
||||||
help: Boolean(parsedFlags.get('help') ?? FLAG_CONFIGURATION['help'].default),
|
help: Boolean(parsedFlags.get('help') ?? FLAG_CONFIGURATION['help'].default),
|
||||||
workers: Number(parsedFlags.get('workers') ?? FLAG_CONFIGURATION['workers'].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 helpText from './help'
|
||||||
import parseArgs from './argumentParser'
|
import parseArgs from './argumentParser'
|
||||||
import { getContext, redText } from './utils'
|
import { getContext, redText, assertTsNodeInstall } from './utils'
|
||||||
import run from './runner'
|
import run from './runner'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -16,7 +16,9 @@ import run from './runner'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const context = getContext(args.runtimePath)
|
if (args.ts) assertTsNodeInstall()
|
||||||
|
|
||||||
|
const context = getContext(args.runtimePath, args.ts)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
run(args, context)
|
run(args, context)
|
||||||
|
|
|
@ -4,11 +4,17 @@ import { type Context } from './types'
|
||||||
import { getContext, spawnProcess } from './utils'
|
import { getContext, spawnProcess } from './utils'
|
||||||
|
|
||||||
async function collectCases(context: Context, collectedPaths: Array<string>): Promise<number> {
|
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
|
let totalCases = 0
|
||||||
for await (const collectedPath of collectedPaths) {
|
for await (const collectedPath of collectedPaths) {
|
||||||
const collectedCount = await new Promise<number>((resolve, reject) => {
|
const collectedCount = await new Promise<number>((resolve, reject) => {
|
||||||
let count = 0
|
let count = 0
|
||||||
spawnProcess(context.nodeRuntime, [collectedPath], {
|
spawnProcess(runtime, [...extraArgs, collectedPath], {
|
||||||
extraEnv: { COLLECT: '1' },
|
extraEnv: { COLLECT: '1' },
|
||||||
|
|
||||||
onClose: (code) => {
|
onClose: (code) => {
|
||||||
|
@ -31,7 +37,9 @@ async function collectCases(context: Context, collectedPaths: Array<string>): Pr
|
||||||
*/
|
*/
|
||||||
async function work() {
|
async function work() {
|
||||||
const [, workerRuntime, ...assignedTestFiles] = process.argv
|
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)
|
const collectedCount = await collectCases(context, assignedTestFiles)
|
||||||
if (process.send) process.send(JSON.stringify({ total: collectedCount }))
|
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 { type Args, type Context, type WorkerReport, type CollectorReport } from './types'
|
||||||
|
|
||||||
import { promises as fs } from 'fs'
|
import { promises as fs } from 'fs'
|
||||||
|
@ -15,7 +15,7 @@ async function collectTests(roots: Array<string>): Promise<Array<string>> {
|
||||||
for (const root of roots) {
|
for (const root of roots) {
|
||||||
const rootStats = await fs.stat(root)
|
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)
|
collectedHere.push(root)
|
||||||
} else if (rootStats.isDirectory()) {
|
} else if (rootStats.isDirectory()) {
|
||||||
const content = await fs.readdir(root, { encoding: 'utf8' })
|
const content = await fs.readdir(root, { encoding: 'utf8' })
|
||||||
|
@ -50,6 +50,7 @@ async function collectCases(context: Context, collectedPaths: Array<string>, wor
|
||||||
onMessage: (message: string) => {
|
onMessage: (message: string) => {
|
||||||
collectorReport.totalCases += JSON.parse(message).total
|
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)
|
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('run:start')
|
||||||
performance.mark('test-collect:start')
|
performance.mark('test-collect:start')
|
||||||
const collectedTests = await collectTests(args.targets)
|
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')
|
performance.mark('test-collect:end')
|
||||||
const testCollectTime = performance.measure('test-collect', 'test-collect:start', 'test-collect:end').duration
|
const testCollectTime = performance.measure('test-collect', 'test-collect:start', 'test-collect:end').duration
|
||||||
|
|
||||||
console.log(
|
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')
|
performance.mark('case-collect:start')
|
||||||
const collectedCaseCount = await collectCases(context, collectedTests)
|
const collectedCaseCount = await collectCases(context, supportedTests)
|
||||||
performance.mark('case-collect:end')
|
performance.mark('case-collect:end')
|
||||||
const caseCollectTime = performance.measure('case-collect', 'case-collect:start', 'case-collect:end').duration
|
const caseCollectTime = performance.measure('case-collect', 'case-collect:start', 'case-collect:end').duration
|
||||||
console.log(
|
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
|
const hasFailed = Object.values(summary).filter((workerReport) => !workerReport.pass).length > 0
|
||||||
performance.mark('run:end')
|
performance.mark('run:end')
|
||||||
|
|
|
@ -8,6 +8,7 @@ export interface Context {
|
||||||
runnerRuntime: string
|
runnerRuntime: string
|
||||||
collectorRuntime: string
|
collectorRuntime: string
|
||||||
nodeRuntime: 'ts-node' | 'node'
|
nodeRuntime: 'ts-node' | 'node'
|
||||||
|
ts: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Args {
|
export interface Args {
|
||||||
|
@ -15,6 +16,7 @@ export interface Args {
|
||||||
runtimePath: string
|
runtimePath: string
|
||||||
help: boolean
|
help: boolean
|
||||||
workers: number
|
workers: number
|
||||||
|
ts: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MatcherReport {
|
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`
|
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 {
|
export function greenText(text: string | number): string {
|
||||||
return `\x1b[32m${text}\x1b[0m`
|
return `\x1b[32m${text}\x1b[0m`
|
||||||
}
|
}
|
||||||
|
@ -19,23 +23,40 @@ export function redText(text: string | number): string {
|
||||||
return `\x1b[31m${text}\x1b[0m`
|
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
|
* Generates a context object that contains general information
|
||||||
* about the test runner. The parameter here should always be
|
* about the test runner. The parameter here should always be
|
||||||
* `process.argv[1]`, which will allow all the other paths
|
* `process.argv[1]`, which will allow all the other paths
|
||||||
* to be set properly.
|
* to be set properly.
|
||||||
*/
|
*/
|
||||||
export function getContext(runnerPath: string): Context {
|
export function getContext(runnerPath: string, ts: boolean = false): Context {
|
||||||
const installDirectory = path.dirname(runnerPath)
|
const resolvedRunnerPath = require.resolve(runnerPath)
|
||||||
const runnerExtension = path.extname(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.
|
// 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 {
|
return {
|
||||||
workerRuntime: path.join(installDirectory, `worker${runnerExtension}`),
|
workerRuntime: path.join(installDirectory, `worker${runnerExtension}`),
|
||||||
runnerRuntime: runnerPath,
|
runnerRuntime: runnerPath,
|
||||||
collectorRuntime: path.join(installDirectory, `collector${runnerExtension}`),
|
collectorRuntime: path.join(installDirectory, `collector${runnerExtension}`),
|
||||||
nodeRuntime,
|
nodeRuntime,
|
||||||
|
ts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,9 +84,13 @@ export function splitIntoBatches<T>(data: Array<T>, desiredBatchCount: number =
|
||||||
export function forkWorker(
|
export function forkWorker(
|
||||||
runtime: string,
|
runtime: string,
|
||||||
args: Array<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 {
|
): childProcess.ChildProcess {
|
||||||
const workerProcess = childProcess.fork(runtime, args, {})
|
const workerProcess = childProcess.fork(runtime, args, { env: { ...process.env, ...(extraEnv ?? {}) } })
|
||||||
workerProcess.on('message', onMessage)
|
workerProcess.on('message', onMessage)
|
||||||
workerProcess.on('close', onClose)
|
workerProcess.on('close', onClose)
|
||||||
return workerProcess
|
return workerProcess
|
||||||
|
|
|
@ -22,15 +22,21 @@ function formatMessage(results: string, failed: boolean): string {
|
||||||
*/
|
*/
|
||||||
async function work() {
|
async function work() {
|
||||||
if (process?.send === undefined) throw Error('No process global found')
|
if (process?.send === undefined) throw Error('No process global found')
|
||||||
|
const tsMode = Boolean(process.env.TS === '1')
|
||||||
const [, workerRuntime, ...assignedTestFiles] = process.argv
|
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(
|
await Promise.all(
|
||||||
assignedTestFiles.map(
|
assignedTestFiles.map(
|
||||||
(testFilePath) =>
|
(testFilePath) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
spawnProcess(context.nodeRuntime, [path.resolve(testFilePath)], {
|
spawnProcess(runtime, [...extraArgs, path.resolve(testFilePath)], {
|
||||||
onClose: (code) => {
|
onClose: (code) => {
|
||||||
resolve(code)
|
resolve(code)
|
||||||
},
|
},
|
||||||
|
|
|
@ -262,7 +262,7 @@ __metadata:
|
||||||
ts-node: ^10.9.1
|
ts-node: ^10.9.1
|
||||||
typescript: ^4.0.0
|
typescript: ^4.0.0
|
||||||
bin:
|
bin:
|
||||||
womm: dist/runner.js
|
womm: dist/cli.js
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
|
Reference in a new issue