|
1 | 1 | import {spawn} from "node:child_process";
|
2 |
| -import {existsSync} from "node:fs"; |
| 2 | +import {existsSync, statSync} from "node:fs"; |
3 | 3 | import {mkdir, open, rename, unlink} from "node:fs/promises";
|
4 | 4 | import {dirname, join} from "node:path";
|
5 | 5 | import {maybeStat, prepareOutput} from "./files.js";
|
@@ -71,58 +71,71 @@ export class Loader {
|
71 | 71 | * to the source root; this is within the .observablehq/cache folder within
|
72 | 72 | * the source root.
|
73 | 73 | */
|
74 |
| - async load(): Promise<string> { |
| 74 | + async load({verbose = true}: {verbose?: boolean} = {}): Promise<string> { |
75 | 75 | let command = runningCommands.get(this.path);
|
76 |
| - if (command) return command; |
77 |
| - command = (async () => { |
| 76 | + if (!command) { |
| 77 | + command = (async () => { |
| 78 | + const outputPath = join(".observablehq", "cache", this.targetPath); |
| 79 | + const cachePath = join(this.sourceRoot, outputPath); |
| 80 | + const loaderStat = await maybeStat(this.path); |
| 81 | + const cacheStat = await maybeStat(cachePath); |
| 82 | + if (cacheStat && cacheStat.mtimeMs > loaderStat!.mtimeMs) return outputPath; |
| 83 | + const tempPath = join(this.sourceRoot, ".observablehq", "cache", `${this.targetPath}.${process.pid}`); |
| 84 | + await prepareOutput(tempPath); |
| 85 | + const tempFd = await open(tempPath, "w"); |
| 86 | + const tempFileStream = tempFd.createWriteStream({highWaterMark: 1024 * 1024}); |
| 87 | + const subprocess = spawn(this.command, this.args, {windowsHide: true, stdio: ["ignore", "pipe", "inherit"]}); |
| 88 | + subprocess.stdout.pipe(tempFileStream); |
| 89 | + const code = await new Promise((resolve, reject) => { |
| 90 | + subprocess.on("error", reject); |
| 91 | + subprocess.on("close", resolve); |
| 92 | + }); |
| 93 | + await tempFd.close(); |
| 94 | + if (code === 0) { |
| 95 | + await mkdir(dirname(cachePath), {recursive: true}); |
| 96 | + await rename(tempPath, cachePath); |
| 97 | + } else { |
| 98 | + await unlink(tempPath); |
| 99 | + throw new Error(`loader exited with code ${code}`); |
| 100 | + } |
| 101 | + return outputPath; |
| 102 | + })(); |
| 103 | + command.finally(() => runningCommands.delete(this.path)).catch(() => {}); |
| 104 | + runningCommands.set(this.path, command); |
| 105 | + } |
| 106 | + if (verbose) { |
78 | 107 | console.info(`${this.path} start`);
|
79 |
| - const time = performance.now(); |
80 |
| - const outputPath = join(".observablehq", "cache", this.targetPath); |
81 |
| - const cachePath = join(this.sourceRoot, outputPath); |
82 |
| - const loaderStat = await maybeStat(this.path); |
83 |
| - const cacheStat = await maybeStat(cachePath); |
84 |
| - if (cacheStat && cacheStat.mtimeMs > loaderStat!.mtimeMs) return outputPath; |
85 |
| - const tempPath = join(this.sourceRoot, ".observablehq", "cache", `${this.targetPath}.${process.pid}`); |
86 |
| - await prepareOutput(tempPath); |
87 |
| - const tempFd = await open(tempPath, "w"); |
88 |
| - const tempFileStream = tempFd.createWriteStream({highWaterMark: 1024 * 1024}); |
89 |
| - const subprocess = spawn(this.command, this.args, {windowsHide: true, stdio: ["ignore", "pipe", "inherit"]}); |
90 |
| - subprocess.stdout.pipe(tempFileStream); |
91 |
| - const code = await new Promise((resolve, reject) => { |
92 |
| - subprocess.on("error", reject); |
93 |
| - subprocess.on("close", resolve); |
94 |
| - }); |
95 |
| - await tempFd.close(); |
96 |
| - console.info( |
97 |
| - `${this.path} ${`${code === 0 ? success("success") : error("error")} ${outputBytes( |
98 |
| - (await maybeStat(tempPath))?.size |
99 |
| - )} in ${Math.floor(performance.now() - time)}ms`}` |
| 108 | + const start = performance.now(); |
| 109 | + command.then( |
| 110 | + (path) => { |
| 111 | + console.info( |
| 112 | + `${this.path} ${green("success")} ${formatSize(statSync(path).size)} in ${formatElapsed(start)}` |
| 113 | + ); |
| 114 | + }, |
| 115 | + (error) => { |
| 116 | + console.info(`${this.path} ${red("error")} after ${formatElapsed(start)}: ${error.message}`); |
| 117 | + } |
100 | 118 | );
|
101 |
| - if (code === 0) { |
102 |
| - await mkdir(dirname(cachePath), {recursive: true}); |
103 |
| - await rename(tempPath, cachePath); |
104 |
| - } else { |
105 |
| - await unlink(tempPath); |
106 |
| - throw new Error(`loader exited with code ${code}`); |
107 |
| - } |
108 |
| - return outputPath; |
109 |
| - })(); |
110 |
| - command.finally(() => runningCommands.delete(this.path)).catch(() => {}); |
111 |
| - runningCommands.set(this.path, command); |
| 119 | + } |
112 | 120 | return command;
|
113 | 121 | }
|
114 | 122 | }
|
115 | 123 |
|
116 |
| -const error = color(31); |
117 |
| -const success = color(32); |
118 |
| -const warning = color(33); |
| 124 | +const red = color(31); |
| 125 | +const green = color(32); |
| 126 | +const yellow = color(33); |
119 | 127 |
|
120 | 128 | function color(code) {
|
121 | 129 | return process.stdout.isTTY ? (text) => `\x1b[${code}m${text}\x1b[0m` : String;
|
122 | 130 | }
|
123 | 131 |
|
124 |
| -function outputBytes(size) { |
125 |
| - if (!size) return warning("empty output"); |
| 132 | +function formatSize(size) { |
| 133 | + if (!size) return yellow("empty output"); |
126 | 134 | const e = Math.floor(Math.log(size) / Math.log(1024));
|
127 | 135 | return `output ${+(size / 1024 ** e).toFixed(2)} ${["bytes", "KiB", "MiB", "GiB", "TiB"][e]}`;
|
128 | 136 | }
|
| 137 | + |
| 138 | +function formatElapsed(start) { |
| 139 | + const elapsed = performance.now() - start; |
| 140 | + return `${Math.floor(elapsed)}ms`; |
| 141 | +} |
0 commit comments