-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
watcher.js
195 lines (161 loc) · 5.74 KB
/
watcher.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
// @ts-check
// A script which uses Facebook's watchman to run `pnpm build` in different modules
// in a standard monorepo.
const { spawnSync } = require("child_process")
const help = spawnSync("watchman", ["--help"])
const hasWatchman = !help.error
if (!hasWatchman) {
const showError = process.env.DEBUG
const suffix = !showError ? "Run with DEBUG=* to see the error logs." : ""
// prettier-ignore
console.log(`Watchman failed to load, this is _OK_ but you will not get automatic builds of sub-projects like the tsconfig reference or playground. ` + suffix)
if (showError) {
console.error(help.error)
}
process.exit(0)
}
const watchman = require("fb-watchman")
const client = new watchman.Client({})
const chalk = require("chalk")
const { spawn, exec } = require("child_process")
const { join } = require("path")
const { existsSync, readFileSync } = require("fs")
const { log } = console
/**
* @typedef {Object} WatchmanFile - a User account
* @property {string} name - the full path
* @property {string} type - the type of file changed, f = file, the rest = meh
* @property {boolean} exists - was it deleted?
*/
/**
*
* @param {WatchmanFile} file
*/
const projectForFile = file => {
// Any output
if (file.name.includes("/dist/") || file.name.includes("/out/")) return
if (file.name.includes("/typescriptlang-org/")) return
if (file.name.includes(".test.ts")) return
if (file.name.startsWith("packages/")) {
return file.name.split("/")[1]
}
}
let upcomingCommand = null
let currentProcess = null
// All this is basically a bunch of boilerplate code to set up a watchman
// for the project which looks only at .ts and .md files in the repo.
// Startup watchman
function watcher(error, resp) {
if (error) {
console.error("Error initiating watch:", error)
return
}
if ("warning" in resp) {
log("warning: ", resp.warning)
}
// // The default subscribe behavior is to deliver a list of all current files
// // when you first subscribe, so you don't need to walk the tree for yourself
// // on startup. If you don't want this behavior, you should issue a `clock`
// // command and use it to give a logical time constraint on the subscription.
// // See further below for an example of this.
// // watch-project may re-use an existing watch at a higher level in the
// // filesystem. It will tell us the relative path to the directory that
// // we expressed interest in, so we need to adjust for it in our results
var path_prefix = ""
var root = resp.watch
if ("relative_path" in resp) {
path_prefix = resp.relative_path
}
// Subscribe to notifications about .js files
// https://facebook.github.io/watchman/docs/cmd/subscribe.html
client.command(
[
"subscribe",
root,
"Monorepo Builder",
{
expression: ["anyof", ["match", "*.ts"], ["match", "*.md"], ["match", "*.tsx"], ["match", "*.json"]],
relative_root: path_prefix,
fields: ["name", "exists", "type"],
},
],
function (error, resp) {
if (error) {
console.error("failed to subscribe: ", error)
return
}
log(`${chalk.green("success")} connected to Watchman`)
}
)
// @ts-ignore
client.on("subscription", function (resp) {
// NOOP for large amounts of files
if (resp.files.length > 10) return
const projectsToBuild = resp.files.map(projectForFile).filter(Boolean)
const uniqueProjects = Array.from(new Set(projectsToBuild))
// I don't wanna handle multiple processes
const commandToRun = uniqueProjects.map(project => {
const packageJSONPath = join("packages", project, "package.json")
if (!existsSync(packageJSONPath)) return
const packageJSON = JSON.parse(readFileSync(packageJSONPath, "utf8"))
if (!packageJSON.scripts || !packageJSON.scripts.build) return
if (packageJSON.scripts["build-fast"]) return `--filter=${packageJSON.name} run build-fast`
return `--filter=${packageJSON.name} run build`
})
if (commandToRun[0]) {
if (currentProcess) {
upcomingCommand = commandToRun[0]
} else {
runCommand(commandToRun[0])
}
}
})
}
// @ts-ignore
client.on("end", function () {
// Called when the connection to watchman is terminated
log("watch over")
})
// @ts-ignore
client.on("error", function (error) {
console.error("Error while talking to watchman: ", error)
})
client.capabilityCheck({ required: ["relative_root"], optional: [] }, function (error, resp) {
if (error) {
console.error("Error checking capabilities:", error)
return
}
// log("Talking to watchman version", resp.version)
})
const runCommand = argString => {
if (currentProcess) return
const prefix = chalk.gray("> ")
const cmd = chalk.bold("pnpm " + argString)
log(prefix + cmd)
const build = spawn("pnpm", argString.split(" "))
build.stdout.on("data", l => {
if (l.toString().includes("Done in")) return
log(" " + l.toString().trim())
})
build.stderr.on("data", l => console.error(" " + l.toString().trim()))
build.on("close", code => {
const codeString = code === 0 ? chalk.green("" + code) : chalk.bold.red("" + code)
log(`[${codeString}] --------- `)
if (process.platform === "darwin") {
exec(playCommand(".vscode/done.aiff", "0.05"))
}
currentProcess = null
if (upcomingCommand === argString || !upcomingCommand) {
// NOOP if you've tried running the same thing a few times
upcomingCommand = null
} else {
// re-launch the next command
const commandToRun = upcomingCommand
upcomingCommand = null
runCommand(commandToRun)
}
})
currentProcess = build
}
const playCommand = (path, volume) => `afplay \"${path}\" -v ${volume}`
client.command(["watch-project", process.cwd()], watcher)