|
1 |
| -import * as cp from "child_process" |
| 1 | +import { spawn, fork, ChildProcess } from "child_process" |
| 2 | +import del from "del" |
| 3 | +import { promises as fs } from "fs" |
2 | 4 | import * as path from "path"
|
3 |
| -import { onLine } from "../../src/node/util" |
4 |
| - |
5 |
| -async function main(): Promise<void> { |
6 |
| - try { |
7 |
| - const watcher = new Watcher() |
8 |
| - await watcher.watch() |
9 |
| - } catch (error: any) { |
10 |
| - console.error(error.message) |
11 |
| - process.exit(1) |
12 |
| - } |
| 5 | +import { CompilationStats, onLine, OnLineCallback, VSCodeCompileStatus } from "../../src/node/util" |
| 6 | + |
| 7 | +interface DevelopmentCompilers { |
| 8 | + [key: string]: ChildProcess | undefined |
| 9 | + vscode: ChildProcess |
| 10 | + vscodeWebExtensions: ChildProcess |
| 11 | + codeServer: ChildProcess |
| 12 | + plugins: ChildProcess | undefined |
13 | 13 | }
|
14 | 14 |
|
15 | 15 | class Watcher {
|
16 |
| - private readonly rootPath = path.resolve(__dirname, "../..") |
17 |
| - private readonly vscodeSourcePath = path.join(this.rootPath, "vendor/modules/code-oss-dev") |
| 16 | + private rootPath = path.resolve(process.cwd()) |
| 17 | + private readonly paths = { |
| 18 | + /** Path to uncompiled VS Code source. */ |
| 19 | + vscodeDir: path.join(this.rootPath, "vendor", "modules", "code-oss-dev"), |
| 20 | + compilationStatsFile: path.join(this.rootPath, "out", "watcher.json"), |
| 21 | + pluginDir: process.env.PLUGIN_DIR, |
| 22 | + } |
| 23 | + |
| 24 | + //#region Web Server |
18 | 25 |
|
19 |
| - private static log(message: string, skipNewline = false): void { |
20 |
| - process.stdout.write(message) |
21 |
| - if (!skipNewline) { |
22 |
| - process.stdout.write("\n") |
| 26 | + /** Development web server. */ |
| 27 | + private webServer: ChildProcess | undefined |
| 28 | + |
| 29 | + private reloadWebServer = (): void => { |
| 30 | + if (this.webServer) { |
| 31 | + this.webServer.kill() |
23 | 32 | }
|
| 33 | + |
| 34 | + // Pass CLI args, save for `node` and the initial script name. |
| 35 | + const args = process.argv.slice(2) |
| 36 | + this.webServer = fork(path.join(this.rootPath, "out/node/entry.js"), args) |
| 37 | + const { pid } = this.webServer |
| 38 | + |
| 39 | + this.webServer.on("exit", () => console.log("[Code Server]", `Web process ${pid} exited`)) |
| 40 | + |
| 41 | + console.log("\n[Code Server]", `Spawned web server process ${pid}`) |
24 | 42 | }
|
25 | 43 |
|
26 |
| - public async watch(): Promise<void> { |
27 |
| - let server: cp.ChildProcess | undefined |
28 |
| - const restartServer = (): void => { |
29 |
| - if (server) { |
30 |
| - server.kill() |
31 |
| - } |
32 |
| - const s = cp.fork(path.join(this.rootPath, "out/node/entry.js"), process.argv.slice(2)) |
33 |
| - console.log(`[server] spawned process ${s.pid}`) |
34 |
| - s.on("exit", () => console.log(`[server] process ${s.pid} exited`)) |
35 |
| - server = s |
36 |
| - } |
| 44 | + //#endregion |
37 | 45 |
|
38 |
| - const vscode = cp.spawn("yarn", ["watch"], { cwd: this.vscodeSourcePath }) |
| 46 | + //#region Compilers |
39 | 47 |
|
40 |
| - const vscodeWebExtensions = cp.spawn("yarn", ["watch-web"], { cwd: this.vscodeSourcePath }) |
| 48 | + private readonly compilers: DevelopmentCompilers = { |
| 49 | + codeServer: spawn("tsc", ["--watch", "--pretty", "--preserveWatchOutput"], { cwd: this.rootPath }), |
| 50 | + vscode: spawn("yarn", ["watch"], { cwd: this.paths.vscodeDir }), |
| 51 | + vscodeWebExtensions: spawn("yarn", ["watch-web"], { cwd: this.paths.vscodeDir }), |
| 52 | + plugins: this.paths.pluginDir ? spawn("yarn", ["build", "--watch"], { cwd: this.paths.pluginDir }) : undefined, |
| 53 | + } |
41 | 54 |
|
42 |
| - const tsc = cp.spawn("tsc", ["--watch", "--pretty", "--preserveWatchOutput"], { cwd: this.rootPath }) |
43 |
| - const plugin = process.env.PLUGIN_DIR |
44 |
| - ? cp.spawn("yarn", ["build", "--watch"], { cwd: process.env.PLUGIN_DIR }) |
45 |
| - : undefined |
| 55 | + private vscodeCompileStatus = VSCodeCompileStatus.Loading |
46 | 56 |
|
47 |
| - const cleanup = (code?: number | null): void => { |
48 |
| - Watcher.log("killing vs code watcher") |
49 |
| - vscode.removeAllListeners() |
50 |
| - vscode.kill() |
| 57 | + public async initialize(): Promise<void> { |
| 58 | + for (const event of ["SIGINT", "SIGTERM"]) { |
| 59 | + process.on(event, () => this.dispose(0)) |
| 60 | + } |
51 | 61 |
|
52 |
| - Watcher.log("killing vs code web extension watcher") |
53 |
| - vscodeWebExtensions.removeAllListeners() |
54 |
| - vscodeWebExtensions.kill() |
| 62 | + if (!this.hasVerboseLogging) { |
| 63 | + console.log("\n[Watcher]", "Compiler logs will be minimal. Pass --log to show all output.") |
| 64 | + } |
55 | 65 |
|
56 |
| - Watcher.log("killing tsc") |
57 |
| - tsc.removeAllListeners() |
58 |
| - tsc.kill() |
| 66 | + this.cleanFiles() |
59 | 67 |
|
60 |
| - if (plugin) { |
61 |
| - Watcher.log("killing plugin") |
62 |
| - plugin.removeAllListeners() |
63 |
| - plugin.kill() |
64 |
| - } |
| 68 | + for (const [processName, devProcess] of Object.entries(this.compilers)) { |
| 69 | + if (!devProcess) continue |
65 | 70 |
|
66 |
| - if (server) { |
67 |
| - Watcher.log("killing server") |
68 |
| - server.removeAllListeners() |
69 |
| - server.kill() |
| 71 | + devProcess.on("exit", (code) => { |
| 72 | + this.log(`[${processName}]`, "Terminated unexpectedly") |
| 73 | + this.dispose(code) |
| 74 | + }) |
| 75 | + |
| 76 | + if (devProcess.stderr) { |
| 77 | + devProcess.stderr.on("data", (d: string | Uint8Array) => process.stderr.write(d)) |
70 | 78 | }
|
| 79 | + } |
| 80 | + |
| 81 | + onLine(this.compilers.vscode, this.parseVSCodeLine) |
| 82 | + onLine(this.compilers.codeServer, this.parseCodeServerLine) |
71 | 83 |
|
72 |
| - Watcher.log("killing watch") |
73 |
| - process.exit(code || 0) |
| 84 | + if (this.compilers.plugins) { |
| 85 | + onLine(this.compilers.plugins, this.parsePluginLine) |
74 | 86 | }
|
| 87 | + } |
| 88 | + |
| 89 | + //#endregion |
75 | 90 |
|
76 |
| - process.on("SIGINT", () => cleanup()) |
77 |
| - process.on("SIGTERM", () => cleanup()) |
| 91 | + //#region Line Parsers |
78 | 92 |
|
79 |
| - vscode.on("exit", (code) => { |
80 |
| - Watcher.log("vs code watcher terminated unexpectedly") |
81 |
| - cleanup(code) |
82 |
| - }) |
| 93 | + private parseVSCodeLine: OnLineCallback = (strippedLine, originalLine) => { |
| 94 | + if (!strippedLine.includes("watch-extensions") || this.hasVerboseLogging) { |
| 95 | + console.log("[VS Code]", originalLine) |
| 96 | + } |
83 | 97 |
|
84 |
| - vscodeWebExtensions.on("exit", (code) => { |
85 |
| - Watcher.log("vs code extension watcher terminated unexpectedly") |
86 |
| - cleanup(code) |
87 |
| - }) |
| 98 | + switch (this.vscodeCompileStatus) { |
| 99 | + case VSCodeCompileStatus.Loading: |
| 100 | + // Wait for watch-client since "Finished compilation" will appear multiple |
| 101 | + // times before the client starts building. |
| 102 | + if (strippedLine.includes("Starting 'watch-client'")) { |
| 103 | + console.log("[VS Code] 🚧 Compiling 🚧", "(This may take a moment!)") |
| 104 | + this.vscodeCompileStatus = VSCodeCompileStatus.Compiling |
| 105 | + } |
| 106 | + break |
| 107 | + case VSCodeCompileStatus.Compiling: |
| 108 | + if (strippedLine.includes("Finished compilation")) { |
| 109 | + console.log("[VS Code] ✨ Finished compiling! ✨", "(Refresh your web browser ♻️)") |
| 110 | + this.vscodeCompileStatus = VSCodeCompileStatus.Compiled |
| 111 | + |
| 112 | + this.emitCompilationStats() |
| 113 | + this.reloadWebServer() |
| 114 | + } |
| 115 | + break |
| 116 | + case VSCodeCompileStatus.Compiled: |
| 117 | + console.log("[VS Code] 🔔 Finished recompiling! 🔔", "(Refresh your web browser ♻️)") |
| 118 | + this.emitCompilationStats() |
| 119 | + this.reloadWebServer() |
| 120 | + break |
| 121 | + } |
| 122 | + } |
88 | 123 |
|
89 |
| - tsc.on("exit", (code) => { |
90 |
| - Watcher.log("tsc terminated unexpectedly") |
91 |
| - cleanup(code) |
92 |
| - }) |
| 124 | + private parseCodeServerLine: OnLineCallback = (strippedLine, originalLine) => { |
| 125 | + if (!strippedLine.length) return |
93 | 126 |
|
94 |
| - if (plugin) { |
95 |
| - plugin.on("exit", (code) => { |
96 |
| - Watcher.log("plugin terminated unexpectedly") |
97 |
| - cleanup(code) |
98 |
| - }) |
| 127 | + console.log("[Compiler][Code Server]", originalLine) |
| 128 | + |
| 129 | + if (strippedLine.includes("Watching for file changes")) { |
| 130 | + console.log("[Compiler][Code Server]", "Finished compiling!", "(Refresh your web browser ♻️)") |
| 131 | + |
| 132 | + this.reloadWebServer() |
99 | 133 | }
|
| 134 | + } |
| 135 | + |
| 136 | + private parsePluginLine: OnLineCallback = (strippedLine, originalLine) => { |
| 137 | + if (!strippedLine.length) return |
100 | 138 |
|
101 |
| - vscodeWebExtensions.stderr.on("data", (d) => process.stderr.write(d)) |
102 |
| - vscode.stderr.on("data", (d) => process.stderr.write(d)) |
103 |
| - tsc.stderr.on("data", (d) => process.stderr.write(d)) |
| 139 | + console.log("[Compiler][Plugin]", originalLine) |
104 | 140 |
|
105 |
| - if (plugin) { |
106 |
| - plugin.stderr.on("data", (d) => process.stderr.write(d)) |
| 141 | + if (strippedLine.includes("Watching for file changes...")) { |
| 142 | + this.reloadWebServer() |
107 | 143 | }
|
| 144 | + } |
108 | 145 |
|
109 |
| - onLine(vscode, (line, original) => { |
110 |
| - console.log("[vscode]", original) |
111 |
| - if (line.includes("Finished compilation")) { |
112 |
| - restartServer() |
113 |
| - } |
114 |
| - }) |
| 146 | + //#endregion |
115 | 147 |
|
116 |
| - onLine(tsc, (line, original) => { |
117 |
| - // tsc outputs blank lines; skip them. |
118 |
| - if (line !== "") { |
119 |
| - console.log("[tsc]", original) |
120 |
| - } |
121 |
| - if (line.includes("Watching for file changes")) { |
122 |
| - restartServer() |
123 |
| - } |
124 |
| - }) |
| 148 | + //#region Utilities |
125 | 149 |
|
126 |
| - if (plugin) { |
127 |
| - onLine(plugin, (line, original) => { |
128 |
| - // tsc outputs blank lines; skip them. |
129 |
| - if (line !== "") { |
130 |
| - console.log("[plugin]", original) |
131 |
| - } |
132 |
| - if (line.includes("Watching for file changes")) { |
133 |
| - restartServer() |
134 |
| - } |
135 |
| - }) |
| 150 | + /** |
| 151 | + * Cleans files from previous builds. |
| 152 | + */ |
| 153 | + private cleanFiles(): Promise<string[]> { |
| 154 | + console.log("[Watcher]", "Cleaning files from previous builds...") |
| 155 | + |
| 156 | + return del([ |
| 157 | + "out/**/*", |
| 158 | + // Included because the cache can sometimes enter bad state when debugging compiled files. |
| 159 | + ".cache/**/*", |
| 160 | + ]) |
| 161 | + } |
| 162 | + |
| 163 | + /** |
| 164 | + * Emits a file containing compilation data. |
| 165 | + * This is especially useful when Express needs to determine if VS Code is still compiling. |
| 166 | + */ |
| 167 | + private emitCompilationStats(): Promise<void> { |
| 168 | + const stats: CompilationStats = { |
| 169 | + status: this.vscodeCompileStatus, |
| 170 | + lastCompiledAt: new Date(), |
| 171 | + } |
| 172 | + |
| 173 | + this.log("Writing watcher stats...") |
| 174 | + return fs.writeFile(this.paths.compilationStatsFile, JSON.stringify(stats, null, 2)) |
| 175 | + } |
| 176 | + |
| 177 | + private log(...entries: string[]) { |
| 178 | + process.stdout.write(entries.join(" ")) |
| 179 | + } |
| 180 | + |
| 181 | + private dispose(code: number | null): void { |
| 182 | + for (const [processName, devProcess] of Object.entries(this.compilers)) { |
| 183 | + this.log(`[${processName}]`, "Killing...\n") |
| 184 | + devProcess?.removeAllListeners() |
| 185 | + devProcess?.kill() |
136 | 186 | }
|
| 187 | + process.exit(typeof code === "number" ? code : 0) |
| 188 | + } |
| 189 | + |
| 190 | + private get hasVerboseLogging() { |
| 191 | + return process.argv.includes("--log") |
| 192 | + } |
| 193 | + |
| 194 | + //#endregion |
| 195 | +} |
| 196 | + |
| 197 | +async function main(): Promise<void> { |
| 198 | + try { |
| 199 | + const watcher = new Watcher() |
| 200 | + await watcher.initialize() |
| 201 | + } catch (error: any) { |
| 202 | + console.error(error.message) |
| 203 | + process.exit(1) |
137 | 204 | }
|
138 | 205 | }
|
139 | 206 |
|
|
0 commit comments