forked from microsoft/TypeScript-Website
-
Notifications
You must be signed in to change notification settings - Fork 0
/
watcher.js
169 lines (143 loc) · 5.02 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
// @ts-check
// A script which uses Facebook's watchman to run `yarn build` in different modules
// in a standard monorepo.
const watchman = require('fb-watchman')
const client = new watchman.Client({})
const chalk = require('chalk').default
const { spawn } = 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.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
client.command(['watch-project', process.cwd()], function (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
log('(re)using project watch at ', root, ', our dir is relative: ', path_prefix)
}
// 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('subscription ' + resp.subscribe + ' established')
}
)
// @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
const buildCommand = `workspace ${packageJSON.name} run build`
return buildCommand
})
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'] }, 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('yarn ' + argString)
log(prefix + cmd)
const build = spawn('yarn', 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}] --------- `)
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
}