| author | |
| committer | |
| log | 6b198b1e34ecf21148334df6eb9d79edf857b4db |
| tree | d040e191b916154458d0fda0ffed526bd76dbf21 |
| parent | 82917100494eb8ac5a92d06fbc0dddadac986cef |
| signature | Commit is signed but in an unrecognized format. |
15 files changed, 173 insertions(+), 48 deletions(-)
lib/Events.ts+30-8| ... | @@ -50,16 +50,28 @@ export class Events<Map extends Events.Map> { | ... | @@ -50,16 +50,28 @@ export class Events<Map extends Events.Map> { |
| 50 | channel: C, | 50 | channel: C, |
| 51 | cb?: Events.Handler<Map[C]>, | 51 | cb?: Events.Handler<Map[C]>, |
| 52 | ): async.Cancelable<Map[C]> { | 52 | ): async.Cancelable<Map[C]> { |
| 53 | let dispose: ts.Dispose | null = null; | 53 | let disposeEvent: ts.Dispose | null = null; |
| 54 | let disposeError: ts.Dispose | null = null; | ||
| 55 | function dispose() { | ||
| 56 | if (!disposeEvent) return; | ||
| 57 | disposeEvent?.(); | ||
| 58 | UNWRAP(disposeError)(); | ||
| 59 | disposeEvent = disposeError = null; | ||
| 60 | } | ||
| 54 | return async.makeCancelable( | 61 | return async.makeCancelable( |
| 55 | new Promise((resolve) => | 62 | new Promise((resolve, reject) => { |
| 56 | dispose = this.on(channel, (...args) => { | 63 | disposeEvent = this.on(channel, (...args) => { |
| 57 | UNWRAP(dispose)(); | 64 | dispose(); |
| 58 | resolve(args); | 65 | resolve(args); |
| 59 | cb?.(...args); | 66 | cb?.(...args); |
| 60 | }) | 67 | }); |
| 61 | ), | 68 | disposeError = this.on(channel, (...args) => { |
| 62 | () => dispose?.(), | 69 | dispose(); |
| 70 | reject(args); | ||
| 71 | cb?.(...args); | ||
| 72 | }); | ||
| 73 | }), | ||
| 74 | () => dispose(), | ||
| 63 | ); | 75 | ); |
| 64 | } | 76 | } |
| 65 | /** listen for any event */ | 77 | /** listen for any event */ |
| ... | @@ -71,7 +83,17 @@ export class Events<Map extends Events.Map> { | ... | @@ -71,7 +83,17 @@ export class Events<Map extends Events.Map> { |
| 71 | } | 83 | } |
| 72 | /** emit an event on the specified channel */ | 84 | /** emit an event on the specified channel */ |
| 73 | emit<C extends keyof Map>(channel: C, ...args: Map[C]): void { | 85 | emit<C extends keyof Map>(channel: C, ...args: Map[C]): void { |
| 74 | this.#channels[channel]?.forEach((cb) => cb(...args)); | 86 | const handlers = this.#channels[channel]; |
| 87 | if (handlers) handlers.forEach((cb) => cb(...args)); | ||
| 88 | else if (channel === "error") { | ||
| 89 | this.#global?.forEach((cb) => cb(channel, args)); | ||
| 90 | throw args.length === 0 | ||
| 91 | ? new Error("Error event emitted") | ||
| 92 | : args.length === 1 | ||
| 93 | ? args[0] | ||
| 94 | : new AggregateError(args); | ||
| 95 | } | ||
| 96 | this.#global?.forEach((cb) => cb(channel, args)); | ||
| 75 | } | 97 | } |
| 76 | } | 98 | } |
| 77 | 99 |
lib/Lru.ts+6-4| ... | @@ -2,10 +2,12 @@ | ... | @@ -2,10 +2,12 @@ |
| 2 | * Least-recently-used cache. | 2 | * Least-recently-used cache. |
| 3 | * This module is intended to be imported via the main class. | 3 | * This module is intended to be imported via the main class. |
| 4 | * | 4 | * |
| 5 | * import { Lru } from '@clo/lib/Lru'; | 5 | * ```ts |
| 6 | * // Extra types are in a `declare namespace` | 6 | * import { Lru } from '@clo/lib/Lru'; |
| 7 | * const opts: Lru.Options = { capacity: 2 }; | 7 | * // Extra types are in a `declare namespace` |
| 8 | * const lru = new Lru(opts); | 8 | * const opts: Lru.Options = { capacity: 2 }; |
| 9 | * const lru = new Lru(opts); | ||
| 10 | * ``` | ||
| 9 | * | 11 | * |
| 10 | * @module | 12 | * @module |
| 11 | */ | 13 | */ |
lib/assert.ts+4-9| ... | @@ -3,6 +3,7 @@ | ... | @@ -3,6 +3,7 @@ |
| 3 | * intended to be imported by symbol. | 3 | * intended to be imported by symbol. |
| 4 | * @module | 4 | * @module |
| 5 | */ | 5 | */ |
| 6 | /* node:coverage disable */ | ||
| 6 | 7 | ||
| 7 | /** assert that `condition` is truthy. */ | 8 | /** assert that `condition` is truthy. */ |
| 8 | export function ASSERT(condition: unknown, msg?: Message): asserts condition { | 9 | export function ASSERT(condition: unknown, msg?: Message): asserts condition { |
| ... | @@ -21,14 +22,6 @@ export function UNWRAP<T>(nullable: T | null | undefined, msg?: Message): T { | ... | @@ -21,14 +22,6 @@ export function UNWRAP<T>(nullable: T | null | undefined, msg?: Message): T { |
| 21 | return nullable; | 22 | return nullable; |
| 22 | } | 23 | } |
| 23 | 24 | ||
| 24 | /** assert that `number` is not NaN. */ | ||
| 25 | export function FINITE(number: number, msg?: Message): number { | ||
| 26 | if (Number.isNaN(number)) { | ||
| 27 | throw new Error(`Unexpected NaN${msg ? `: ${messageToString(msg)}` : ""}`); | ||
| 28 | } | ||
| 29 | return number; | ||
| 30 | } | ||
| 31 | |||
| 32 | /** assert that a `default` case in a switch is never reached. */ | 25 | /** assert that a `default` case in a switch is never reached. */ |
| 33 | export function unreachableCase(value: never): never { | 26 | export function unreachableCase(value: never): never { |
| 34 | throw new Error(`Unexpected value in switch: ${inspect(value as unknown)}`); | 27 | throw new Error(`Unexpected value in switch: ${inspect(value as unknown)}`); |
| ... | @@ -44,8 +37,10 @@ function messageToString(msg: Message) { | ... | @@ -44,8 +37,10 @@ function messageToString(msg: Message) { |
| 44 | 37 | ||
| 45 | function inspect(object: unknown) { | 38 | function inspect(object: unknown) { |
| 46 | try { | 39 | try { |
| 47 | return require("node:util").inspect(object); | 40 | return node.builtin("util")?.inspect(object) ?? typeof object; |
| 48 | } catch { | 41 | } catch { |
| 49 | return typeof object; | 42 | return typeof object; |
| 50 | } | 43 | } |
| 51 | } | 44 | } |
| 45 | |||
| 46 | import * as node from "./node.ts"; |
lib/async.ts+5-3| ... | @@ -99,7 +99,7 @@ export class Watch<T> { | ... | @@ -99,7 +99,7 @@ export class Watch<T> { |
| 99 | 99 | ||
| 100 | until(condition: (v: T) => boolean): Promise<T> { | 100 | until(condition: (v: T) => boolean): Promise<T> { |
| 101 | if (condition(this.#value)) return Promise.resolve(this.#value); | 101 | if (condition(this.#value)) return Promise.resolve(this.#value); |
| 102 | return new Promise((resolve) => { | 102 | return new Promise<T>((resolve) => { |
| 103 | function check(next: T) { | 103 | function check(next: T) { |
| 104 | if (condition(next)) resolve(next), release(); | 104 | if (condition(next)) resolve(next), release(); |
| 105 | } | 105 | } |
| ... | @@ -220,12 +220,14 @@ export class PromiseAggregator { | ... | @@ -220,12 +220,14 @@ export class PromiseAggregator { |
| 220 | } | 220 | } |
| 221 | 221 | ||
| 222 | /** a promise that can be cancelled. */ | 222 | /** a promise that can be cancelled. */ |
| 223 | export type Cancelable<T> = Promise<T> & Disposable & { cancel: VoidFunction }; | 223 | export type Cancelable<T> = Promise<T> & Disposable & { |
| 224 | cancel: (reason?: unknown) => void; | ||
| 225 | }; | ||
| 224 | 226 | ||
| 225 | /** make an existing promise cancelable. */ | 227 | /** make an existing promise cancelable. */ |
| 226 | export function makeCancelable<T>( | 228 | export function makeCancelable<T>( |
| 227 | promise: Promise<T>, | 229 | promise: Promise<T>, |
| 228 | cancel: ts.VoidFunction, | 230 | cancel: (reason?: unknown) => void, |
| 229 | ): Cancelable<T> { | 231 | ): Cancelable<T> { |
| 230 | const p = promise as Cancelable<T>; | 232 | const p = promise as Cancelable<T>; |
| 231 | p[Symbol.dispose] = cancel; | 233 | p[Symbol.dispose] = cancel; |
lib/mime.ts+5| ... | @@ -1,3 +1,8 @@ | ... | @@ -1,3 +1,8 @@ |
| 1 | /** | ||
| 2 | * a small mime type library. | ||
| 3 | * @module | ||
| 4 | */ | ||
| 5 | |||
| 1 | /** | 6 | /** |
| 2 | * retrieves the 'Content-Type' header to use given a file extension. | 7 | * retrieves the 'Content-Type' header to use given a file extension. |
| 3 | * accepts: | 8 | * accepts: |
lib/node.ts+1| ... | @@ -48,6 +48,7 @@ interface Builtins { | ... | @@ -48,6 +48,7 @@ interface Builtins { |
| 48 | sep: string; | 48 | sep: string; |
| 49 | }; | 49 | }; |
| 50 | "util": { | 50 | "util": { |
| 51 | inspect(object: unknown): string; | ||
| 51 | formatWithOptions( | 52 | formatWithOptions( |
| 52 | options: { colors?: boolean }, | 53 | options: { colors?: boolean }, |
| 53 | ...args: unknown[] | 54 | ...args: unknown[] |
lib/readme.changes.md+45-9| ... | @@ -4,20 +4,57 @@ | ... | @@ -4,20 +4,57 @@ |
| 4 | 4 | ||
| 5 | ### breaking | 5 | ### breaking |
| 6 | 6 | ||
| 7 | - promote `log/progress.ts` to top level `progress.ts` | 7 | - `log` dispatching reworked |
| 8 | - rework Log dispatching | ||
| 9 | - delete `log/headless.ts` by moving it into `log` | 8 | - delete `log/headless.ts` by moving it into `log` |
| 10 | - headless scopes now emit `log.Message` objects instead of ANSI text, | 9 | - headless scopes now emit `log.Message` objects instead of ANSI text, |
| 11 | templating is done in the consumer of the headless logging scope. | 10 | templating is done in the consumer of the headless logging scope. |
| 11 | - rename `HeadlessWidgetEnv.wait` to `delay`, matching `async.delay` | ||
| 12 | - `progress` had everything besides `start` and `Node` heavily altered | ||
| 13 | - promote to top level namespace `lib/progress.ts` | ||
| 14 | - `Node.estimate` -> `Node.total` | ||
| 15 | - rename `.null` to `.nullNode` | ||
| 16 | - `.SortNode` -> `.ReadOnlyNode` | ||
| 17 | - `.getUnicodeBar` -> `.formatUnicodeBar` | ||
| 18 | - rework headless rendering story, rename `.Headless` -> `.Root` | ||
| 19 | - `.rootFromWidgetHost` to source from a widget host | ||
| 20 | - remove `.internals`, use `.Root` | ||
| 21 | - deleted `assert.FINITE` | ||
| 12 | 22 | ||
| 13 | ### features | 23 | ### features |
| 14 | 24 | ||
| 15 | - add `log.tee` (and `log.Scope.tee`) | 25 | - `progress` has expanded documentation and use cases. |
| 16 | - `log` in Node.js will inject into `console.*` to prevent interweaving logs | 26 | - create headless nodes with `Root` (subclass of `Events`) |
| 17 | with widget output text. this injection is enabled regardless of if widgets | 27 | - streaming over the wire via `encodeEventStream`, `encodeEventStream`, |
| 18 | are actually running, but do not otherwise change their behavior. | 28 | `encodeByteStream`, `encodeByteStream`. these APIs produce JSON and Binary |
| 19 | - `log` scopes render differently in the terminal now. | 29 | `ReadableStream`s. with work to make `log` better supported in the browser, |
| 20 | - `log` in the browser will call the correct log level function in the browser. | 30 | progress streams are fully consumable from within a web browser. |
| 31 | - `.attach` to display a decoded root | ||
| 32 | - expose `.formatAnsi` for external use | ||
| 33 | - `log` slowly moves towards maturity | ||
| 34 | - add `.tee` (and `log.Scope.tee`) | ||
| 35 | - in Node.js, `log` will inject into `console.*` to prevent interweaving logs | ||
| 36 | with widget output text. this injection is enabled regardless of if widgets | ||
| 37 | are actually running, but do not otherwise change their behavior. | ||
| 38 | - `log` scopes render differently in the terminal now | ||
| 39 | - browser support by calling the correct `console` api to signal warnings and | ||
| 40 | errors correctly. | ||
| 41 | - fix assertion failure when creating a widget in the same frame a log message | ||
| 42 | is written. | ||
| 43 | - new library `Events.ts` | ||
| 44 | - new library `bytes.ts` (subject to being moved as it is one function) | ||
| 45 | - new library `stream.ts` | ||
| 46 | - new library `testing.ts` | ||
| 47 | - `string` | ||
| 48 | - add `encodeUtf8` and `decodeUtf8` | ||
| 49 | - add `formatByteSize` and `formatBinaryByteSize` and `formatDurationLetters`, | ||
| 50 | all subject to change as `progress` is used more. | ||
| 51 | - `async` | ||
| 52 | - add `.Cancelable` type, constructed with `.makeCancelable` | ||
| 53 | - add `.delay` | ||
| 54 | - `ts` | ||
| 55 | - add `EmptyObject` | ||
| 56 | - add `Json`, `ToJson`, and `JsonValue` | ||
| 57 | - add `mixin` | ||
| 21 | 58 | ||
| 22 | ## v2 | 59 | ## v2 |
| 23 | 60 | ||
| ... | @@ -33,7 +70,6 @@ | ... | @@ -33,7 +70,6 @@ |
| 33 | ### features | 70 | ### features |
| 34 | 71 | ||
| 35 | - top level readme now explains general usage patterns. more docs overall. | 72 | - top level readme now explains general usage patterns. more docs overall. |
| 36 | - new library `render` | ||
| 37 | - new library `async.ts` | 73 | - new library `async.ts` |
| 38 | - new library `Lru.ts` | 74 | - new library `Lru.ts` |
| 39 | - new library `mime.ts` | 75 | - new library `mime.ts` |
lib/render.ts+1-1| ... | @@ -413,7 +413,7 @@ export function clsx(mix: ClassName): string { | ... | @@ -413,7 +413,7 @@ export function clsx(mix: ClassName): string { |
| 413 | } else if (typeof mix === "object") { | 413 | } else if (typeof mix === "object") { |
| 414 | if (Array.isArray(mix)) { | 414 | if (Array.isArray(mix)) { |
| 415 | for (k = 0; k < mix.length; k++) { | 415 | for (k = 0; k < mix.length; k++) { |
| 416 | if (mix[k] && (y = clsx(mix[k]))) { | 416 | if (mix[k] && (y = clsx(mix[k]!))) { |
| 417 | str && (str += " "); | 417 | str && (str += " "); |
| 418 | str += y; | 418 | str += y; |
| 419 | } | 419 | } |
lib/string.test.ts+16| ... | @@ -29,6 +29,22 @@ test("escapeShellArgument", () => { | ... | @@ -29,6 +29,22 @@ test("escapeShellArgument", () => { |
| 29 | assert.equal(string.escapeShellArgument("'string"), '"\'string"'); | 29 | assert.equal(string.escapeShellArgument("'string"), '"\'string"'); |
| 30 | assert.equal(string.escapeShellArgument("'str\"ing"), "\\''str\"ing'"); | 30 | assert.equal(string.escapeShellArgument("'str\"ing"), "\\''str\"ing'"); |
| 31 | }); | 31 | }); |
| 32 | test("formatByteSize", () => { | ||
| 33 | assert.equal(string.formatByteSize(0), "0B"); | ||
| 34 | assert.equal(string.formatByteSize(1), "1B"); | ||
| 35 | assert.equal(string.formatByteSize(1000), "1kB"); | ||
| 36 | assert.equal(string.formatByteSize(-1000), "-1kB"); | ||
| 37 | assert.equal(string.formatByteSize(1500), "1.5kB"); | ||
| 38 | assert.equal(string.formatByteSize(1578), "1.58kB"); | ||
| 39 | assert.equal(string.formatByteSize(5_100_000), "5.1MB"); | ||
| 40 | assert.equal(string.formatByteSize(-7_231_692_881), "-7.23GB"); | ||
| 41 | assert.equal(string.formatByteSize(-4_294_967_296), "-7.23GB"); | ||
| 42 | |||
| 43 | assert.equal(string.formatByteSize(53.251), "53.25B"); | ||
| 44 | assert.equal(string.formatByteSize(Infinity), "Infinity"); | ||
| 45 | assert.equal(string.formatByteSize(-Infinity), "-Infinity"); | ||
| 46 | assert.equal(string.formatByteSize(NaN), "NaN"); | ||
| 47 | }); | ||
| 32 | 48 | ||
| 33 | import { test } from "node:test"; | 49 | import { test } from "node:test"; |
| 34 | import assert from "node:assert"; | 50 | import assert from "node:assert"; |
lib/string.ts+1-2| ... | @@ -1,4 +1,3 @@ | ... | @@ -1,4 +1,3 @@ |
| 1 | // TODO: this function is not benchmarked | ||
| 2 | export function countNewlines(str: string): number { | 1 | export function countNewlines(str: string): number { |
| 3 | let count = 0; | 2 | let count = 0; |
| 4 | for (let i = 0, { length } = str; i < length; i += 1) { | 3 | for (let i = 0, { length } = str; i < length; i += 1) { |
| ... | @@ -43,7 +42,7 @@ export function decodeUtf8(input: Uint8Array): string { | ... | @@ -43,7 +42,7 @@ export function decodeUtf8(input: Uint8Array): string { |
| 43 | return td.decode(input); | 42 | return td.decode(input); |
| 44 | } | 43 | } |
| 45 | 44 | ||
| 46 | const byteUnits = "kMGTPEZYRQ"; | 45 | const byteUnits = "kMGTPEZYRQ"; // kilo, mega, giga, etc |
| 47 | export function formatByteSize(bytes: number) { | 46 | export function formatByteSize(bytes: number) { |
| 48 | if (!Number.isFinite(bytes)) return bytes.toString(); | 47 | if (!Number.isFinite(bytes)) return bytes.toString(); |
| 49 | let prefix = ""; | 48 | let prefix = ""; |
lib/string/ansi.ts+9-3| ... | @@ -1,9 +1,9 @@ | ... | @@ -1,9 +1,9 @@ |
| 1 | /** | 1 | /** |
| 2 | * ANSI escape sequences can be used to style text in a tty or perform other | 2 | * ANSI escape sequences can be used to style text in a tty or perform other |
| 3 | * interactions. This file contains many constants for escape codes, as well as | 3 | * interactions. this file contains many constants for escape codes, as well as |
| 4 | * functions for ones that take an argument, such as `cursorUp`. | 4 | * functions for ones that take an argument, such as `cursorUp`. |
| 5 | * | 5 | * |
| 6 | * Note that this file only produces escape sequences, and does not yet feature | 6 | * note that this file only produces escape sequences, and does not yet feature |
| 7 | * detection or fallback code. | 7 | * detection or fallback code. |
| 8 | * | 8 | * |
| 9 | * @module | 9 | * @module |
| ... | @@ -203,11 +203,12 @@ export function mergeStyles(code: string): string { | ... | @@ -203,11 +203,12 @@ export function mergeStyles(code: string): string { |
| 203 | export function debugAnsi(text: string): string { | 203 | export function debugAnsi(text: string): string { |
| 204 | // deno-fmt-ignore | 204 | // deno-fmt-ignore |
| 205 | const verbaitim: (keyof typeof self)[] = [ | 205 | const verbaitim: (keyof typeof self)[] = [ |
| 206 | "fgCyan", "fgBlue", | ||
| 206 | "bgReset", "bgBlack", "bgRed", "bgGreen", "bgYellow", "bgBlue", "bgMagenta", | 207 | "bgReset", "bgBlack", "bgRed", "bgGreen", "bgYellow", "bgBlue", "bgMagenta", |
| 207 | "bgCyan", "bgWhite", "bgBrightBlack", "bgBrightRed", "bgBrightGreen", | 208 | "bgCyan", "bgWhite", "bgBrightBlack", "bgBrightRed", "bgBrightGreen", |
| 208 | "bgBrightYellow", "bgBrightBlue", "bgBrightMagenta", "bgBrightCyan", | 209 | "bgBrightYellow", "bgBrightBlue", "bgBrightMagenta", "bgBrightCyan", |
| 209 | "bgBrightWhite", "fgReset", "fgBlack", "fgRed", "fgGreen", "fgYellow", | 210 | "bgBrightWhite", "fgReset", "fgBlack", "fgRed", "fgGreen", "fgYellow", |
| 210 | "fgBlue", "fgMagenta", "fgCyan", "fgWhite", "fgBrightBlack", "fgBrightRed", | 211 | "fgMagenta", "fgWhite", "fgBrightBlack", "fgBrightRed", |
| 211 | "fgBrightGreen", "fgBrightYellow", "fgBrightBlue", "fgBrightMagenta", | 212 | "fgBrightGreen", "fgBrightYellow", "fgBrightBlue", "fgBrightMagenta", |
| 212 | "fgBrightCyan", "fgBrightWhite", "clearToEndOfLine", "clearToStartOfLine", | 213 | "fgBrightCyan", "fgBrightWhite", "clearToEndOfLine", "clearToStartOfLine", |
| 213 | "clearFullLine", "syncStart", "syncEnd", "bold", "dim", "resetWeight" | 214 | "clearFullLine", "syncStart", "syncEnd", "bold", "dim", "resetWeight" |
| ... | @@ -320,6 +321,11 @@ const zeroWidthClusterRegex = | ... | @@ -320,6 +321,11 @@ const zeroWidthClusterRegex = |
| 320 | const leadingNonPrintingRegex = | 321 | const leadingNonPrintingRegex = |
| 321 | /^[\p{Default_Ignorable_Code_Point}\p{Control}\p{Format}\p{Mark}\p{Surrogate}]+/v; | 322 | /^[\p{Default_Ignorable_Code_Point}\p{Control}\p{Format}\p{Mark}\p{Surrogate}]+/v; |
| 322 | const rgiEmojiRegex = /^\p{RGI_Emoji}$/v; | 323 | const rgiEmojiRegex = /^\p{RGI_Emoji}$/v; |
| 324 | |||
| 325 | export function strip(text: string) { | ||
| 326 | return text.replace(ansiEscapes, ""); | ||
| 327 | } | ||
| 328 | |||
| 323 | function segmentWidth(segment: string) { | 329 | function segmentWidth(segment: string) { |
| 324 | if (zeroWidthClusterRegex.test(segment)) return 0; | 330 | if (zeroWidthClusterRegex.test(segment)) return 0; |
| 325 | if (rgiEmojiRegex.test(segment)) return 2; | 331 | if (rgiEmojiRegex.test(segment)) return 2; |
lib/subprocess/ffmpeg.ts+7-5| ... | @@ -6,15 +6,17 @@ export interface SpawnOptions { | ... | @@ -6,15 +6,17 @@ export interface SpawnOptions { |
| 6 | } | 6 | } |
| 7 | 7 | ||
| 8 | /** | 8 | /** |
| 9 | * Spawn an instance of `ffmpeg` and bind it's live status to the given | 9 | * spawn an instance of `ffmpeg` and bind it's live status to the given |
| 10 | * `progress.Node`. Logs are routed through the progress node, and dismissed on | 10 | * `progress.Node`. Logs are routed through the progress node, and dismissed on |
| 11 | * exit. `lib/progress.ts` will buffer and colocate the logs when running | 11 | * exit. `lib/progress.ts` will buffer and colocate the logs when running |
| 12 | * parallel jobs. | 12 | * parallel jobs. |
| 13 | * | 13 | * |
| 14 | * await ffmpeg.spawn({ | 14 | * ```ts |
| 15 | * cmd: ["-i", "hello.mov", "-c:v", "libsvtav1", "hello.mp4"], | 15 | * await ffmpeg.spawn({ |
| 16 | * progress: progress.start("encode hello.mov"), | 16 | * cmd: ["-i", "hello.mov", "-c:v", "libsvtav1", "hello.mp4"], |
| 17 | * }); | 17 | * progress: progress.start("encode hello.mov"), |
| 18 | * }); | ||
| 19 | * ``` | ||
| 18 | */ | 20 | */ |
| 19 | export async function spawn(options: SpawnOptions) { | 21 | export async function spawn(options: SpawnOptions) { |
| 20 | const { ffmpeg = "ffmpeg", args, cwd } = options; | 22 | const { ffmpeg = "ffmpeg", args, cwd } = options; |
lib/testing.ts+39| ... | @@ -109,6 +109,45 @@ export class SyncPromise<T> implements Promise<T> { | ... | @@ -109,6 +109,45 @@ export class SyncPromise<T> implements Promise<T> { |
| 109 | } | 109 | } |
| 110 | } | 110 | } |
| 111 | 111 | ||
| 112 | export class FakeTimers { | ||
| 113 | time = 0; | ||
| 114 | timers: Array<{ | ||
| 115 | duration: number; | ||
| 116 | resolve: () => void; | ||
| 117 | src: stack.Frame[]; | ||
| 118 | }> = []; | ||
| 119 | waitTime = 0; | ||
| 120 | |||
| 121 | now = () => { | ||
| 122 | return this.time; | ||
| 123 | }; | ||
| 124 | delay: typeof async.delay = (ms) => { | ||
| 125 | const src = stack.capture(2); | ||
| 126 | return async.makeCancelable( | ||
| 127 | new SyncPromise((resolve) => { | ||
| 128 | this.timers.push({ | ||
| 129 | duration: ms - this.waitTime, | ||
| 130 | resolve, | ||
| 131 | src, | ||
| 132 | }); | ||
| 133 | this.waitTime = ms; | ||
| 134 | }), | ||
| 135 | () => { | ||
| 136 | throw new Error("TODO"); | ||
| 137 | }, | ||
| 138 | ); | ||
| 139 | }; | ||
| 140 | } | ||
| 141 | |||
| 142 | export function removeUndefinedKeys(obj: unknown): unknown { | ||
| 143 | if (typeof obj !== "object" || !obj || Array.isArray(obj)) return obj; | ||
| 144 | return Object.fromEntries( | ||
| 145 | Object.getOwnPropertyNames(obj) | ||
| 146 | .map((k) => [k, (obj as any)[k]]) | ||
| 147 | .filter(([, v]) => v !== undefined), | ||
| 148 | ); | ||
| 149 | } | ||
| 150 | |||
| 112 | /** implements a log.HeadlessWidgetHost that acts as a fake screen. */ | 151 | /** implements a log.HeadlessWidgetHost that acts as a fake screen. */ |
| 113 | export class MockScreen implements Disposable, log.HeadlessWidgetHost { | 152 | export class MockScreen implements Disposable, log.HeadlessWidgetHost { |
| 114 | columns = 80; | 153 | columns = 80; |
run.js+1-1| ... | @@ -105,7 +105,7 @@ if (process.argv[1].startsWith(import.meta.filename.slice(0, -".js".length))) { | ... | @@ -105,7 +105,7 @@ if (process.argv[1].startsWith(import.meta.filename.slice(0, -".js".length))) { |
| 105 | ); | 105 | ); |
| 106 | }, | 106 | }, |
| 107 | ); | 107 | ); |
| 108 | } else { | 108 | } else if (!found.match(/\.test\.[tj]sx?$/)) { |
| 109 | log.warn( | 109 | log.warn( |
| 110 | hot.load("path").relative(".", found), | 110 | hot.load("path").relative(".", found), |
| 111 | 'does not export a "main" function', | 111 | 'does not export a "main" function', |
tsconfig.json+3-3| ... | @@ -5,7 +5,7 @@ | ... | @@ -5,7 +5,7 @@ |
| 5 | "incremental": true, | 5 | "incremental": true, |
| 6 | "jsx": "react-jsxdev", | 6 | "jsx": "react-jsxdev", |
| 7 | "jsxImportSource": "#jsx", | 7 | "jsxImportSource": "#jsx", |
| 8 | "lib": ["esnext", "dom", "dom.iterable"], | 8 | "lib": ["esnext", "es2024", "dom", "dom.iterable"], |
| 9 | "module": "nodenext", | 9 | "module": "nodenext", |
| 10 | "noEmit": true, | 10 | "noEmit": true, |
| 11 | "outDir": ".clover/ts", | 11 | "outDir": ".clover/ts", |
| ... | @@ -14,9 +14,9 @@ | ... | @@ -14,9 +14,9 @@ |
| 14 | "skipLibCheck": true, | 14 | "skipLibCheck": true, |
| 15 | "strict": true, | 15 | "strict": true, |
| 16 | "verbatimModuleSyntax": true, | 16 | "verbatimModuleSyntax": true, |
| 17 | "target": "es2024", | 17 | "target": "esnext", |
| 18 | "noUncheckedIndexedAccess": true, | 18 | "noUncheckedIndexedAccess": true, |
| 19 | "resolveJsonModule": true | 19 | "resolveJsonModule": true |
| 20 | }, | 20 | }, |
| 21 | "include": ["framework/**/*", "src/**/*", "lib/**/*", "framework/global.d.ts"] | 21 | "include": ["lib/**/*"] |
| 22 | } | 22 | } |