| ... | ... | @@ -41,6 +41,9 @@ |
| 41 | 41 | * `log.tee()` to duplicate all messages elsewhere. for example, a project may |
| 42 | 42 | * configure logs to upload to a telemetry service. |
| 43 | 43 | * |
| 44 | * if bundling for the browser, see the details in `./node.ts` on how to |
| 45 | * trigger tree-shaking to eliminate the node.js bindings in your bundle. |
| 46 | * |
| 44 | 47 | * @module |
| 45 | 48 | */ |
| 46 | 49 | |
| ... | ... | @@ -200,7 +203,7 @@ export function getDrawLock(mode: "long" | "short"): ts.Dispose { |
| 200 | 203 | return globalWidgetHost.getDrawLock(mode); |
| 201 | 204 | } |
| 202 | 205 | |
| 203 | | export function headlessScope(dispatch: DispatchFunction): Scope { |
| 206 | export function headlessScope(dispatch: DispatchFunction): RootScope { |
| 204 | 207 | return new ScopeImpl(dispatch); |
| 205 | 208 | } |
| 206 | 209 | |
| ... | ... | @@ -699,12 +702,76 @@ const ScopeImpl = class Scope implements RootScope { |
| 699 | 702 | } |
| 700 | 703 | }; |
| 701 | 704 | |
| 702 | | const globalWidgetHost = /* @__PURE__ */ (() => { |
| 703 | | // TODO; write this in a more tree-shakable manner |
| 704 | | const { process } = node; |
| 705 | | if (!process || !process.stderr.isTTY) { |
| 706 | | // return a no-op |
| 707 | | let warned = false; |
| 705 | const globalWidgetHost = node.process |
| 706 | ? /* @__PURE__*/ ((process: NonNullable<typeof node.process>) => { |
| 707 | const widget = headlessWidgetHost({ |
| 708 | lockTerminal() { |
| 709 | const { stdout, stderr } = process; |
| 710 | let disposed = false; |
| 711 | |
| 712 | function patch<T, A extends unknown[]>( |
| 713 | fn: (this: T, ...args: A) => void, |
| 714 | ) { |
| 715 | return function (this: T, ...args: A) { |
| 716 | using _ = disposed ? null : widget.getDrawLock("short"); |
| 717 | fn.apply(this, args); |
| 718 | }; |
| 719 | } |
| 720 | |
| 721 | // patch calls to `process.std{out,err}` |
| 722 | // note: `pipe` uses managed calls to `write`, so this is plenty |
| 723 | const stdoutWrite = stdout.write; |
| 724 | const stderrWrite = stderr.write; |
| 725 | const stdoutEnd = stdout.end; |
| 726 | const stderrEnd = stderr.end; |
| 727 | const newStdoutWrite = stdout.write = patch(stdoutWrite); |
| 728 | const newStderrWrite = stderr.write = patch(stderrWrite); |
| 729 | const newStdoutEnd = stdout.end = patch(stdoutEnd); |
| 730 | const newStderrEnd = stderr.end = patch(stderrEnd); |
| 731 | |
| 732 | // non-node runtimes will typically implement console in a way that |
| 733 | // doesn't use `node:process`, so it must also get patched |
| 734 | const console = globalThis |
| 735 | .console as unknown as Record<string, () => void>; |
| 736 | const restoreConsole: [string, old: () => void, patch: () => void][] = |
| 737 | []; |
| 738 | for (const [key, old] of Object.entries(console)) { |
| 739 | if (typeof old !== "function") continue; |
| 740 | try { |
| 741 | const patched = console[key] = patch(old); |
| 742 | restoreConsole.push([key, old, patched]); |
| 743 | } catch { /* skip */ } |
| 744 | } |
| 745 | |
| 746 | return { |
| 747 | writeOutput: (string) => stdoutWrite.call(stderr, string), |
| 748 | writeInteractive: (string) => stderrWrite.call(stderr, string), |
| 749 | getSize: () => process.stderr, |
| 750 | temporarilyUnlock() { |
| 751 | // no action needed |
| 752 | }, |
| 753 | close() { |
| 754 | disposed = true; |
| 755 | // leave patches in place if something else tampered with it. |
| 756 | if (stdout.write === newStdoutWrite) stdout.write = stdoutWrite; |
| 757 | if (stderr.write === newStderrWrite) stdout.write = stderrWrite; |
| 758 | if (stdout.end === newStdoutEnd) stdout.end = stdoutEnd; |
| 759 | if (stderr.end === newStderrEnd) stdout.end = stderrEnd; |
| 760 | for (const [key, old, patched] of restoreConsole) { |
| 761 | if (console[key] === patched) console[key] = old; |
| 762 | } |
| 763 | }, |
| 764 | }; |
| 765 | }, |
| 766 | now: () => performance.now(), |
| 767 | delay: async.delay, |
| 768 | }); |
| 769 | process.addListener("beforeExit", () => widget.cancel()); |
| 770 | process.addListener("exit", () => widget.cancel()); |
| 771 | |
| 772 | return widget; |
| 773 | })(node.process) |
| 774 | : /* @__PURE__ */ ((warned = false) => { |
| 708 | 775 | return { |
| 709 | 776 | write: (line: string) => console.log(line), |
| 710 | 777 | getDrawLock: () => ts.defer(() => {}), |
| ... | ... | @@ -715,87 +782,14 @@ const globalWidgetHost = /* @__PURE__ */ (() => { |
| 715 | 782 | "that does not support the Node.js 'process' API. Widgets " + |
| 716 | 783 | "will not be visible.", |
| 717 | 784 | ); |
| 785 | warned = true; |
| 718 | 786 | } |
| 719 | | warned = true; |
| 720 | 787 | const close = w.onChange?.(() => {}); |
| 721 | 788 | return ts.defer(close ?? (() => {})); |
| 722 | 789 | }, |
| 723 | 790 | cancel: () => {}, |
| 724 | 791 | }; |
| 725 | | } |
| 726 | | const widget = headlessWidgetHost({ |
| 727 | | lockTerminal() { |
| 728 | | const { stdout, stderr } = process; |
| 729 | | let disposed = false; |
| 730 | | |
| 731 | | function patch<T, A extends unknown[]>( |
| 732 | | fn: (this: T, ...args: A) => void, |
| 733 | | ) { |
| 734 | | return function (this: T, ...args: A) { |
| 735 | | using _ = disposed ? null : widget.getDrawLock(); |
| 736 | | fn.apply(this, args); |
| 737 | | }; |
| 738 | | } |
| 739 | | |
| 740 | | // patch calls to `process.std{out,err}` |
| 741 | | // note: `pipe` uses managed calls to `write`, so this is plenty |
| 742 | | const stdoutWrite = stdout.write; |
| 743 | | const stderrWrite = stderr.write; |
| 744 | | const stdoutEnd = stdout.end; |
| 745 | | const stderrEnd = stderr.end; |
| 746 | | const newStdoutWrite = stdout.write = patch( |
| 747 | | stdoutWrite === node.builtin("stream")?.Writable.prototype.write |
| 748 | | ? widget.write |
| 749 | | : stdoutWrite, |
| 750 | | ); |
| 751 | | const newStderrWrite = stderr.write = patch(stderrWrite); |
| 752 | | const newStdoutEnd = stdout.end = patch(stdoutEnd); |
| 753 | | const newStderrEnd = stderr.end = patch(stderrEnd); |
| 754 | | |
| 755 | | // non-node runtimes will typically implement console in a way that |
| 756 | | // doesn't use `node:process`, so it must also get patched |
| 757 | | const console = globalThis |
| 758 | | .console as unknown as Record<string, () => void>; |
| 759 | | const restoreConsole: [string, old: () => void, patch: () => void][] = []; |
| 760 | | for (const [key, old] of Object.entries(console)) { |
| 761 | | if (typeof old !== "function") continue; |
| 762 | | try { |
| 763 | | const patched = console[key] = patch(old); |
| 764 | | restoreConsole.push([key, old, patched]); |
| 765 | | } catch { /* skip */ } |
| 766 | | } |
| 767 | | |
| 768 | | return { |
| 769 | | writeOutput: (string) => stdoutWrite.call(stdout, string), |
| 770 | | writeInteractive: (string) => stderrWrite.call(stderr, string), |
| 771 | | getSize: () => process.stderr, |
| 772 | | temporarilyUnlock() { |
| 773 | | // no action needed |
| 774 | | }, |
| 775 | | close() { |
| 776 | | disposed = true; |
| 777 | | // leave patches in place if something else tampered with it. |
| 778 | | if (stdout.write === newStdoutWrite) stdout.write = stdoutWrite; |
| 779 | | if (stderr.write === newStderrWrite) stdout.write = stderrWrite; |
| 780 | | if (stdout.end === newStdoutEnd) stdout.end = stdoutEnd; |
| 781 | | if (stderr.end === newStderrEnd) stdout.end = stderrEnd; |
| 782 | | for (const [key, old, patched] of restoreConsole) { |
| 783 | | if (console[key] === patched) console[key] = old; |
| 784 | | } |
| 785 | | }, |
| 786 | | }; |
| 787 | | }, |
| 788 | | writeOutputTemporaryLock(buffer: string) { |
| 789 | | process.stdout.write(buffer); |
| 790 | | }, |
| 791 | | now: () => performance.now(), |
| 792 | | delay: async.delay, |
| 793 | | }); |
| 794 | | process.addListener("beforeExit", () => widget.cancel()); |
| 795 | | process.addListener("exit", () => widget.cancel()); |
| 796 | | |
| 797 | | return widget; |
| 798 | | })(); |
| 792 | })(); |
| 799 | 793 | |
| 800 | 794 | const levelToAnsi: Record<Message["level"], string> = { |
| 801 | 795 | info: `${ansi.fgBlue}info`, |
| ... | ... | @@ -809,6 +803,7 @@ let globalMessageFormatFunction: MessageFormatFunction = ( |
| 809 | 803 | colors, |
| 810 | 804 | ) => { |
| 811 | 805 | if (!text) return ""; |
| 806 | if (newline === false) return text; |
| 812 | 807 | const prefix = colors |
| 813 | 808 | // colorful |
| 814 | 809 | ? `${levelToAnsi[level]}${ |
| ... | ... | @@ -818,7 +813,7 @@ let globalMessageFormatFunction: MessageFormatFunction = ( |
| 818 | 813 | : scope |
| 819 | 814 | ? `${level}(${scope}): ` |
| 820 | 815 | : `${level}: `; |
| 821 | | return prefix + text + (newline !== false ? "\n" : ""); |
| 816 | return prefix + text + "\n"; |
| 822 | 817 | }; |
| 823 | 818 | let globalOutputFunction!: DispatchFunction; |
| 824 | 819 | const globalLog = /* @__PURE__ */ (() => { |