1
1
// @ts -check
2
2
3
+ import chokidar from "chokidar" ;
3
4
import { $ as _$ } from "execa" ;
4
5
import { glob } from "glob" ;
5
6
import { task } from "hereby" ;
7
+ import assert from "node:assert" ;
6
8
import crypto from "node:crypto" ;
7
9
import fs from "node:fs" ;
8
10
import path from "node:path" ;
9
11
import url from "node:url" ;
10
12
import { parseArgs } from "node:util" ;
13
+ import pc from "picocolors" ;
11
14
import which from "which" ;
12
15
13
16
const __filename = url . fileURLToPath ( new URL ( import . meta. url ) ) ;
@@ -71,22 +74,30 @@ function isInstalled(tool) {
71
74
return ! ! which . sync ( tool , { nothrow : true } ) ;
72
75
}
73
76
74
- export const generateLibs = task ( {
75
- name : "lib" ,
76
- run : async ( ) => {
77
- await fs . promises . mkdir ( "./built/local" , { recursive : true } ) ;
77
+ const libsDir = "./internal/bundled/libs" ;
78
+ const libsRegexp = / (?: ^ | [ \\ / ] ) i n t e r n a l [ \\ / ] b u n d l e d [ \\ / ] l i b s [ \\ / ] / ;
78
79
79
- const libsDir = "./internal/bundled/libs" ;
80
- const libs = await fs . promises . readdir ( libsDir ) ;
80
+ async function generateLibs ( ) {
81
+ await fs . promises . mkdir ( "./built/local" , { recursive : true } ) ;
81
82
82
- await Promise . all ( libs . map ( async lib => {
83
- fs . promises . copyFile ( `${ libsDir } /${ lib } ` , `./built/local/${ lib } ` ) ;
84
- } ) ) ;
85
- } ,
83
+ const libs = await fs . promises . readdir ( libsDir ) ;
84
+
85
+ await Promise . all ( libs . map ( async lib => {
86
+ fs . promises . copyFile ( `${ libsDir } /${ lib } ` , `./built/local/${ lib } ` ) ;
87
+ } ) ) ;
88
+ }
89
+
90
+ export const lib = task ( {
91
+ name : "lib" ,
92
+ run : generateLibs ,
86
93
} ) ;
87
94
88
- function buildExecutableToBuilt ( packagePath ) {
89
- return $ `go build ${ options . race ? [ "-race" ] : [ ] } -tags=noembed -o ./built/local/ ${ packagePath } ` ;
95
+ /**
96
+ * @param {string } packagePath
97
+ * @param {AbortSignal } [abortSignal]
98
+ */
99
+ function buildExecutableToBuilt ( packagePath , abortSignal ) {
100
+ return $ ( { cancelSignal : abortSignal } ) `go build ${ options . race ? [ "-race" ] : [ ] } -tags=noembed -o ./built/local/ ${ packagePath } ` ;
90
101
}
91
102
92
103
export const tsgoBuild = task ( {
@@ -98,7 +109,7 @@ export const tsgoBuild = task({
98
109
99
110
export const tsgo = task ( {
100
111
name : "tsgo" ,
101
- dependencies : [ generateLibs , tsgoBuild ] ,
112
+ dependencies : [ lib , tsgoBuild ] ,
102
113
} ) ;
103
114
104
115
export const local = task ( {
@@ -111,6 +122,41 @@ export const build = task({
111
122
dependencies : [ local ] ,
112
123
} ) ;
113
124
125
+ export const buildWatch = task ( {
126
+ name : "build:watch" ,
127
+ run : async ( ) => {
128
+ await watchDebounced ( "build:watch" , async ( paths , abortSignal ) => {
129
+ let libsChanged = false ;
130
+ let goChanged = false ;
131
+
132
+ for ( const p of paths ) {
133
+ if ( libsRegexp . test ( p ) ) {
134
+ libsChanged = true ;
135
+ }
136
+ else if ( p . endsWith ( ".go" ) ) {
137
+ goChanged = true ;
138
+ }
139
+ if ( libsChanged && goChanged ) {
140
+ break ;
141
+ }
142
+ }
143
+
144
+ if ( libsChanged ) {
145
+ console . log ( "Generating libs..." ) ;
146
+ await generateLibs ( ) ;
147
+ }
148
+
149
+ if ( goChanged ) {
150
+ console . log ( "Building tsgo..." ) ;
151
+ await buildExecutableToBuilt ( "./cmd/tsgo" , abortSignal ) ;
152
+ }
153
+ } , {
154
+ paths : [ "cmd" , "internal" ] ,
155
+ ignored : path => / [ \\ / ] t e s t d a t a [ \\ / ] / . test ( path ) ,
156
+ } ) ;
157
+ } ,
158
+ } ) ;
159
+
114
160
export const cleanBuilt = task ( {
115
161
name : "clean:built" ,
116
162
hiddenFromTaskList : true ,
@@ -311,3 +357,176 @@ function rimraf(p) {
311
357
// The rimraf package uses maxRetries=10 on Windows, but Node's fs.rm does not have that special case.
312
358
return fs . promises . rm ( p , { recursive : true , force : true , maxRetries : process . platform === "win32" ? 10 : 0 } ) ;
313
359
}
360
+
361
+ /** @typedef {{
362
+ * name: string;
363
+ * paths: string | string[];
364
+ * ignored?: (path: string) => boolean;
365
+ * run: (paths: Set<string>, abortSignal: AbortSignal) => void | Promise<unknown>;
366
+ * }} WatchTask */
367
+ void 0 ;
368
+
369
+ /**
370
+ * @param {string } name
371
+ * @param {(paths: Set<string>, abortSignal: AbortSignal) => void | Promise<unknown> } run
372
+ * @param {object } options
373
+ * @param {string | string[] } options.paths
374
+ * @param {(path: string) => boolean } [options.ignored]
375
+ * @param {string } [options.name]
376
+ */
377
+ async function watchDebounced ( name , run , options ) {
378
+ let watching = true ;
379
+ let running = true ;
380
+ let lastChangeTimeMs = Date . now ( ) ;
381
+ let changedDeferred = /** @type {Deferred<void> } */ ( new Deferred ( ) ) ;
382
+ let abortController = new AbortController ( ) ;
383
+
384
+ const debouncer = new Debouncer ( 1_000 , endRun ) ;
385
+ const watcher = chokidar . watch ( options . paths , {
386
+ ignored : options . ignored ,
387
+ ignorePermissionErrors : true ,
388
+ alwaysStat : true ,
389
+ } ) ;
390
+ // The paths that have changed since the last run.
391
+ let paths = new Set ( ) ;
392
+
393
+ process . on ( "SIGINT" , endWatchMode ) ;
394
+ process . on ( "beforeExit" , endWatchMode ) ;
395
+ watcher . on ( "all" , onChange ) ;
396
+
397
+ while ( watching ) {
398
+ const promise = changedDeferred . promise ;
399
+ const token = abortController . signal ;
400
+ if ( ! token . aborted ) {
401
+ running = true ;
402
+ try {
403
+ const thePaths = paths ;
404
+ paths = new Set ( ) ;
405
+ await run ( thePaths , token ) ;
406
+ }
407
+ catch {
408
+ // ignore
409
+ }
410
+ running = false ;
411
+ }
412
+ if ( watching ) {
413
+ console . log ( pc . yellowBright ( `[${ name } ] run complete, waiting for changes...` ) ) ;
414
+ await promise ;
415
+ }
416
+ }
417
+
418
+ console . log ( "end" ) ;
419
+
420
+ /**
421
+ * @param {'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir' | 'all' | 'ready' | 'raw' | 'error' } eventName
422
+ * @param {string } path
423
+ * @param {fs.Stats | undefined } stats
424
+ */
425
+ function onChange ( eventName , path , stats ) {
426
+ switch ( eventName ) {
427
+ case "change" :
428
+ case "unlink" :
429
+ case "unlinkDir" :
430
+ break ;
431
+ case "add" :
432
+ case "addDir" :
433
+ // skip files that are detected as 'add' but haven't actually changed since the last time we ran.
434
+ if ( stats && stats . mtimeMs <= lastChangeTimeMs ) {
435
+ return ;
436
+ }
437
+ break ;
438
+ }
439
+ beginRun ( path ) ;
440
+ }
441
+
442
+ /**
443
+ * @param {string } path
444
+ */
445
+ function beginRun ( path ) {
446
+ if ( debouncer . empty ) {
447
+ console . log ( pc . yellowBright ( `[${ name } ] changed due to '${ path } ', restarting...` ) ) ;
448
+ if ( running ) {
449
+ console . log ( pc . yellowBright ( `[${ name } ] aborting in-progress run...` ) ) ;
450
+ }
451
+ abortController . abort ( ) ;
452
+ abortController = new AbortController ( ) ;
453
+ }
454
+
455
+ debouncer . enqueue ( ) ;
456
+ paths . add ( path ) ;
457
+ }
458
+
459
+ function endRun ( ) {
460
+ lastChangeTimeMs = Date . now ( ) ;
461
+ changedDeferred . resolve ( ) ;
462
+ changedDeferred = /** @type {Deferred<void> } */ ( new Deferred ( ) ) ;
463
+ }
464
+
465
+ function endWatchMode ( ) {
466
+ if ( watching ) {
467
+ watching = false ;
468
+ console . log ( pc . yellowBright ( `[${ name } ] exiting watch mode...` ) ) ;
469
+ abortController . abort ( ) ;
470
+ watcher . close ( ) ;
471
+ }
472
+ }
473
+ }
474
+
475
+ /**
476
+ * @template T
477
+ */
478
+ export class Deferred {
479
+ constructor ( ) {
480
+ /** @type {Promise<T> } */
481
+ this . promise = new Promise ( ( resolve , reject ) => {
482
+ this . resolve = resolve ;
483
+ this . reject = reject ;
484
+ } ) ;
485
+ }
486
+ }
487
+
488
+ export class Debouncer {
489
+ /**
490
+ * @param {number } timeout
491
+ * @param {() => Promise<any> | void } action
492
+ */
493
+ constructor ( timeout , action ) {
494
+ this . _timeout = timeout ;
495
+ this . _action = action ;
496
+ }
497
+
498
+ get empty ( ) {
499
+ return ! this . _deferred ;
500
+ }
501
+
502
+ enqueue ( ) {
503
+ if ( this . _timer ) {
504
+ clearTimeout ( this . _timer ) ;
505
+ this . _timer = undefined ;
506
+ }
507
+
508
+ if ( ! this . _deferred ) {
509
+ this . _deferred = new Deferred ( ) ;
510
+ }
511
+
512
+ this . _timer = setTimeout ( ( ) => this . run ( ) , 100 ) ;
513
+ return this . _deferred . promise ;
514
+ }
515
+
516
+ run ( ) {
517
+ if ( this . _timer ) {
518
+ clearTimeout ( this . _timer ) ;
519
+ this . _timer = undefined ;
520
+ }
521
+
522
+ const deferred = this . _deferred ;
523
+ assert ( deferred ) ;
524
+ this . _deferred = undefined ;
525
+ try {
526
+ deferred . resolve ( this . _action ( ) ) ;
527
+ }
528
+ catch ( e ) {
529
+ deferred . reject ( e ) ;
530
+ }
531
+ }
532
+ }
0 commit comments