| ... | @@ -41,6 +41,9 @@ | ... | @@ -41,6 +41,9 @@ |
| 41 | * `log.tee()` to duplicate all messages elsewhere. for example, a project may | 41 | * `log.tee()` to duplicate all messages elsewhere. for example, a project may |
| 42 | * configure logs to upload to a telemetry service. | 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 | * @module | 47 | * @module |
| 45 | */ | 48 | */ |
| 46 | | 49 | |
| ... | @@ -200,7 +203,7 @@ export function getDrawLock(mode: "long" | "short"): ts.Dispose { | ... | @@ -200,7 +203,7 @@ export function getDrawLock(mode: "long" | "short"): ts.Dispose { |
| 200 | return globalWidgetHost.getDrawLock(mode); | 203 | return globalWidgetHost.getDrawLock(mode); |
| 201 | } | 204 | } |
| 202 | | 205 | |
| 203 | export function headlessScope(dispatch: DispatchFunction): Scope { | 206 | export function headlessScope(dispatch: DispatchFunction): RootScope { |
| 204 | return new ScopeImpl(dispatch); | 207 | return new ScopeImpl(dispatch); |
| 205 | } | 208 | } |
| 206 | | 209 | |
| ... | @@ -699,12 +702,76 @@ const ScopeImpl = class Scope implements RootScope { | ... | @@ -699,12 +702,76 @@ const ScopeImpl = class Scope implements RootScope { |
| 699 | } | 702 | } |
| 700 | }; | 703 | }; |
| 701 | | 704 | |
| 702 | const globalWidgetHost = /* @__PURE__ */ (() => { | 705 | const globalWidgetHost = node.process |
| 703 | // TODO; write this in a more tree-shakable manner | 706 | ? /* @__PURE__*/ ((process: NonNullable<typeof node.process>) => { |
| 704 | const { process } = node; | 707 | const widget = headlessWidgetHost({ |
| 705 | if (!process || !process.stderr.isTTY) { | 708 | lockTerminal() { |
| 706 | // return a no-op | 709 | const { stdout, stderr } = process; |
| 707 | let warned = false; | 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 | return { | 775 | return { |
| 709 | write: (line: string) => console.log(line), | 776 | write: (line: string) => console.log(line), |
| 710 | getDrawLock: () => ts.defer(() => {}), | 777 | getDrawLock: () => ts.defer(() => {}), |
| ... | @@ -715,87 +782,14 @@ const globalWidgetHost = /* @__PURE__ */ (() => { | ... | @@ -715,87 +782,14 @@ const globalWidgetHost = /* @__PURE__ */ (() => { |
| 715 | "that does not support the Node.js 'process' API. Widgets " + | 782 | "that does not support the Node.js 'process' API. Widgets " + |
| 716 | "will not be visible.", | 783 | "will not be visible.", |
| 717 | ); | 784 | ); |
| | 785 | warned = true; |
| 718 | } | 786 | } |
| 719 | warned = true; | | |
| 720 | const close = w.onChange?.(() => {}); | 787 | const close = w.onChange?.(() => {}); |
| 721 | return ts.defer(close ?? (() => {})); | 788 | return ts.defer(close ?? (() => {})); |
| 722 | }, | 789 | }, |
| 723 | cancel: () => {}, | 790 | cancel: () => {}, |
| 724 | }; | 791 | }; |
| 725 | } | 792 | })(); |
| 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 | })(); | | |
| 799 | | 793 | |
| 800 | const levelToAnsi: Record<Message["level"], string> = { | 794 | const levelToAnsi: Record<Message["level"], string> = { |
| 801 | info: `${ansi.fgBlue}info`, | 795 | info: `${ansi.fgBlue}info`, |
| ... | @@ -809,6 +803,7 @@ let globalMessageFormatFunction: MessageFormatFunction = ( | ... | @@ -809,6 +803,7 @@ let globalMessageFormatFunction: MessageFormatFunction = ( |
| 809 | colors, | 803 | colors, |
| 810 | ) => { | 804 | ) => { |
| 811 | if (!text) return ""; | 805 | if (!text) return ""; |
| | 806 | if (newline === false) return text; |
| 812 | const prefix = colors | 807 | const prefix = colors |
| 813 | // colorful | 808 | // colorful |
| 814 | ? `${levelToAnsi[level]}${ | 809 | ? `${levelToAnsi[level]}${ |
| ... | @@ -818,7 +813,7 @@ let globalMessageFormatFunction: MessageFormatFunction = ( | ... | @@ -818,7 +813,7 @@ let globalMessageFormatFunction: MessageFormatFunction = ( |
| 818 | : scope | 813 | : scope |
| 819 | ? `${level}(${scope}): ` | 814 | ? `${level}(${scope}): ` |
| 820 | : `${level}: `; | 815 | : `${level}: `; |
| 821 | return prefix + text + (newline !== false ? "\n" : ""); | 816 | return prefix + text + "\n"; |
| 822 | }; | 817 | }; |
| 823 | let globalOutputFunction!: DispatchFunction; | 818 | let globalOutputFunction!: DispatchFunction; |
| 824 | const globalLog = /* @__PURE__ */ (() => { | 819 | const globalLog = /* @__PURE__ */ (() => { |