Refactor: extract shared utils, clean up deps
This commit is contained in:
parent
7578b73f63
commit
5d2a4618d9
2
bun.lock
2
bun.lock
|
|
@ -12,8 +12,6 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/diff": "^8.0.0",
|
"@types/diff": "^8.0.0",
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,6 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"check": "bunx tsc --noEmit",
|
"check": "bunx tsc --noEmit",
|
||||||
"build": "./scripts/build.sh",
|
|
||||||
"cli:build": "bun run scripts/build.ts",
|
|
||||||
"cli:build:all": "bun run scripts/build.ts --all",
|
|
||||||
"cli:install": "bun cli:build && sudo cp dist/shout /usr/local/bin",
|
"cli:install": "bun cli:build && sudo cp dist/shout /usr/local/bin",
|
||||||
"cli:link": "ln -sf $(pwd)/src/cli/index.ts ~/.bun/bin/shout",
|
"cli:link": "ln -sf $(pwd)/src/cli/index.ts ~/.bun/bin/shout",
|
||||||
"cli:uninstall": "sudo rm /usr/local/bin",
|
"cli:uninstall": "sudo rm /usr/local/bin",
|
||||||
|
|
@ -25,9 +22,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/diff": "^8.0.0"
|
"@types/diff": "^8.0.0",
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,6 @@ program
|
||||||
pathDirs: opts.path,
|
pathDirs: opts.path,
|
||||||
envVars,
|
envVars,
|
||||||
timeout: timeoutMs,
|
timeout: timeoutMs,
|
||||||
verbose: opts.verbose ?? false,
|
|
||||||
onCommand: opts.verbose
|
onCommand: opts.verbose
|
||||||
? (cmd) => process.stderr.write(ansis.dim(` $ ${cmd.command}\n`))
|
? (cmd) => process.stderr.write(ansis.dim(` $ ${cmd.command}\n`))
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,11 @@ export function parseDuration(s: string): number {
|
||||||
if (!match) throw new Error(`Invalid duration: ${s}`)
|
if (!match) throw new Error(`Invalid duration: ${s}`)
|
||||||
|
|
||||||
const value = parseFloat(match[1]!)
|
const value = parseFloat(match[1]!)
|
||||||
const unit = match[2]!
|
const unit = match[2] as "ms" | "s" | "m"
|
||||||
|
|
||||||
switch (unit) {
|
switch (unit) {
|
||||||
case "ms": return value
|
case "ms": return value
|
||||||
case "s": return value * 1000
|
case "s": return value * 1000
|
||||||
case "m": return value * 60_000
|
case "m": return value * 60_000
|
||||||
default: throw new Error(`Unknown unit: ${unit}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,20 +72,21 @@ export function formatFailure(test: TestResult): string {
|
||||||
lines.push(` ${ansis.dim("$")} ${failure.result.command.command}`)
|
lines.push(` ${ansis.dim("$")} ${failure.result.command.command}`)
|
||||||
|
|
||||||
if (failure.diffLines.length > 0) {
|
if (failure.diffLines.length > 0) {
|
||||||
lines.push(ansis.red(" expected:"))
|
const expectedLines: string[] = []
|
||||||
|
const actualLines: string[] = []
|
||||||
for (const dl of failure.diffLines) {
|
for (const dl of failure.diffLines) {
|
||||||
|
const text = dl.kind === "context" ? ansis.dim(dl.text) : dl.text
|
||||||
if (dl.kind === "expected" || dl.kind === "equal" || dl.kind === "context") {
|
if (dl.kind === "expected" || dl.kind === "equal" || dl.kind === "context") {
|
||||||
const prefix = dl.kind === "expected" ? ansis.red(" > ") : " "
|
const prefix = dl.kind === "expected" ? ansis.red(" > ") : " "
|
||||||
lines.push(`${prefix}${dl.kind === "context" ? ansis.dim(dl.text) : dl.text}`)
|
expectedLines.push(`${prefix}${text}`)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
lines.push(ansis.green(" actual:"))
|
|
||||||
for (const dl of failure.diffLines) {
|
|
||||||
if (dl.kind === "actual" || dl.kind === "equal" || dl.kind === "context") {
|
if (dl.kind === "actual" || dl.kind === "equal" || dl.kind === "context") {
|
||||||
const prefix = dl.kind === "actual" ? ansis.green(" > ") : " "
|
const prefix = dl.kind === "actual" ? ansis.green(" > ") : " "
|
||||||
lines.push(`${prefix}${dl.kind === "context" ? ansis.dim(dl.text) : dl.text}`)
|
actualLines.push(`${prefix}${text}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
lines.push(ansis.red(" expected:"), ...expectedLines)
|
||||||
|
lines.push(ansis.green(" actual:"), ...actualLines)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (failure.exitCodeMismatch) {
|
if (failure.exitCodeMismatch) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
export type { Command, Directive, ShoutFile } from "./parse.ts"
|
export type { Command, ShoutFile } from "./parse.ts"
|
||||||
export type { CommandResult, FileResult } from "./run.ts"
|
export type { CommandResult, FileResult } from "./run.ts"
|
||||||
export type { DiffLine } from "./match.ts"
|
export type { DiffLine } from "./match.ts"
|
||||||
export type { TestResult } from "./format.ts"
|
export type { TestResult } from "./format.ts"
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { escapeRegex } from "./utils.ts"
|
||||||
|
|
||||||
export function matchLine(pattern: string, actual: string): boolean {
|
export function matchLine(pattern: string, actual: string): boolean {
|
||||||
if (!pattern.includes("...")) return pattern === actual
|
if (!pattern.includes("...")) return pattern === actual
|
||||||
|
|
||||||
|
|
@ -8,10 +10,6 @@ export function matchLine(pattern: string, actual: string): boolean {
|
||||||
return regex.test(actual)
|
return regex.test(actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeRegex(s: string): string {
|
|
||||||
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
||||||
}
|
|
||||||
|
|
||||||
export function matchOutput(
|
export function matchOutput(
|
||||||
expected: string[],
|
expected: string[],
|
||||||
actual: string[],
|
actual: string[],
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { trimTrailingEmpty } from "./utils.ts"
|
||||||
|
|
||||||
export type Command = {
|
export type Command = {
|
||||||
line: number
|
line: number
|
||||||
raw: string
|
raw: string
|
||||||
|
|
@ -48,12 +50,6 @@ function parseExitCode(lines: string[]): {
|
||||||
return { lines, exitCode: null }
|
return { lines, exitCode: null }
|
||||||
}
|
}
|
||||||
|
|
||||||
function trimTrailingEmpty(lines: string[]): string[] {
|
|
||||||
let end = lines.length
|
|
||||||
while (end > 0 && lines[end - 1] === "") end--
|
|
||||||
return lines.slice(0, end)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parse(path: string, content: string): ShoutFile {
|
export function parse(path: string, content: string): ShoutFile {
|
||||||
const rawLines = content.split("\n")
|
const rawLines = content.split("\n")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import type { ShoutFile } from "./parse.ts"
|
||||||
function makeFile(commands: { command: string; expected?: string[] }[]): ShoutFile {
|
function makeFile(commands: { command: string; expected?: string[] }[]): ShoutFile {
|
||||||
return {
|
return {
|
||||||
path: "test.shout",
|
path: "test.shout",
|
||||||
|
directives: [],
|
||||||
commands: commands.map((c, i) => ({
|
commands: commands.map((c, i) => ({
|
||||||
line: i + 1,
|
line: i + 1,
|
||||||
raw: `$ ${c.command}`,
|
raw: `$ ${c.command}`,
|
||||||
|
|
|
||||||
23
src/run.ts
23
src/run.ts
|
|
@ -3,6 +3,7 @@ import { tmpdir } from "node:os"
|
||||||
import { join } from "node:path"
|
import { join } from "node:path"
|
||||||
|
|
||||||
import type { Command, ShoutFile } from "./parse.ts"
|
import type { Command, ShoutFile } from "./parse.ts"
|
||||||
|
import { trimTrailingEmpty, escapeRegex } from "./utils.ts"
|
||||||
|
|
||||||
export type CommandResult = {
|
export type CommandResult = {
|
||||||
command: Command
|
command: Command
|
||||||
|
|
@ -22,13 +23,12 @@ type RunOptions = {
|
||||||
pathDirs?: string[]
|
pathDirs?: string[]
|
||||||
envVars?: Record<string, string>
|
envVars?: Record<string, string>
|
||||||
timeout: number
|
timeout: number
|
||||||
verbose: boolean
|
|
||||||
onCommand?: (cmd: Command) => void
|
onCommand?: (cmd: Command) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const SENTINEL_PREFIX = "__SHOUT_SENTINEL_"
|
const SENTINEL_PREFIX = "__SHOUT_SENTINEL_"
|
||||||
|
|
||||||
function buildScript(commands: Command[], sentinel: string): string {
|
function buildScript(commands: Command[]): string {
|
||||||
const lines: string[] = ["exec 2>&1"]
|
const lines: string[] = ["exec 2>&1"]
|
||||||
|
|
||||||
for (let i = 0; i < commands.length; i++) {
|
for (let i = 0; i < commands.length; i++) {
|
||||||
|
|
@ -37,7 +37,7 @@ function buildScript(commands: Command[], sentinel: string): string {
|
||||||
// Sentinel: printf to avoid echo interpretation issues
|
// Sentinel: printf to avoid echo interpretation issues
|
||||||
// Format: __SHOUT_SENTINEL_<exitcode>_<index>__
|
// Format: __SHOUT_SENTINEL_<exitcode>_<index>__
|
||||||
lines.push(
|
lines.push(
|
||||||
`printf '\\n${sentinel}%s_${i}__\\n' "$?"`,
|
`printf '\\n${SENTINEL_PREFIX}%s_${i}__\\n' "$?"`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,16 +99,6 @@ function parseSentinelOutput(
|
||||||
return { outputs, exitCodes }
|
return { outputs, exitCodes }
|
||||||
}
|
}
|
||||||
|
|
||||||
function trimTrailingEmpty(lines: string[]): string[] {
|
|
||||||
let end = lines.length
|
|
||||||
while (end > 0 && lines[end - 1] === "") end--
|
|
||||||
return lines.slice(0, end)
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeRegex(s: string): string {
|
|
||||||
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function runFile(
|
export async function runFile(
|
||||||
file: ShoutFile,
|
file: ShoutFile,
|
||||||
options: RunOptions,
|
options: RunOptions,
|
||||||
|
|
@ -119,8 +109,7 @@ export async function runFile(
|
||||||
return { file, results: [], tmpDir }
|
return { file, results: [], tmpDir }
|
||||||
}
|
}
|
||||||
|
|
||||||
const sentinel = SENTINEL_PREFIX
|
const script = buildScript(file.commands)
|
||||||
const script = buildScript(file.commands, sentinel)
|
|
||||||
|
|
||||||
const env: Record<string, string> = options.cleanEnv
|
const env: Record<string, string> = options.cleanEnv
|
||||||
? {}
|
? {}
|
||||||
|
|
@ -157,12 +146,12 @@ export async function runFile(
|
||||||
|
|
||||||
const { outputs, exitCodes } = parseSentinelOutput(
|
const { outputs, exitCodes } = parseSentinelOutput(
|
||||||
stdout,
|
stdout,
|
||||||
sentinel,
|
SENTINEL_PREFIX,
|
||||||
file.commands.length,
|
file.commands.length,
|
||||||
)
|
)
|
||||||
|
|
||||||
const results: CommandResult[] = file.commands.map((cmd, i) => {
|
const results: CommandResult[] = file.commands.map((cmd, i) => {
|
||||||
if (options.verbose && options.onCommand) {
|
if (options.onCommand) {
|
||||||
options.onCommand(cmd)
|
options.onCommand(cmd)
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { CommandResult } from "./run.ts"
|
import type { CommandResult } from "./run.ts"
|
||||||
import type { ShoutFile } from "./parse.ts"
|
import type { ShoutFile } from "./parse.ts"
|
||||||
import { matchOutput, matchLine } from "./match.ts"
|
import { matchOutput } from "./match.ts"
|
||||||
|
import { trimTrailingEmpty } from "./utils.ts"
|
||||||
|
|
||||||
export function rewriteFile(
|
export function rewriteFile(
|
||||||
file: ShoutFile,
|
file: ShoutFile,
|
||||||
|
|
@ -75,9 +76,3 @@ export function rewriteFile(
|
||||||
|
|
||||||
return output.join("\n")
|
return output.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
function trimTrailingEmpty(lines: string[]): string[] {
|
|
||||||
let end = lines.length
|
|
||||||
while (end > 0 && lines[end - 1] === "") end--
|
|
||||||
return lines.slice(0, end)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
9
src/utils.ts
Normal file
9
src/utils.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
export function trimTrailingEmpty(lines: string[]): string[] {
|
||||||
|
let end = lines.length
|
||||||
|
while (end > 0 && lines[end - 1] === "") end--
|
||||||
|
return lines.slice(0, end)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function escapeRegex(s: string): string {
|
||||||
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||||
|
}
|
||||||
|
|
@ -27,17 +27,6 @@
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
"noPropertyAccessFromIndexSignature": false,
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
"baseUrl": ".",
|
"baseUrl": "."
|
||||||
"paths": {
|
|
||||||
"$*": [
|
|
||||||
"./src/server/*"
|
|
||||||
],
|
|
||||||
"@*": [
|
|
||||||
"./src/shared/*"
|
|
||||||
],
|
|
||||||
"%*": [
|
|
||||||
"./src/lib/*"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user