@travetto/compiler 4.0.0-rc.0 → 4.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,57 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import timers from 'node:timers/promises';
4
+
5
+ import type { ManifestContext } from '@travetto/manifest';
6
+
7
+ export class ProcessHandle {
8
+
9
+ #file: string;
10
+
11
+ constructor(ctx: ManifestContext, name: string) {
12
+ this.#file = path.resolve(ctx.workspace.path, ctx.build.toolFolder, `${name}.pid`);
13
+ }
14
+
15
+ writePid(pid: number): Promise<void> {
16
+ return fs.writeFile(this.#file, JSON.stringify(pid), 'utf8');
17
+ }
18
+
19
+ getPid(): Promise<number | undefined> {
20
+ return fs.readFile(this.#file, 'utf8').then(v => +v, () => undefined);
21
+ }
22
+
23
+ async isRunning(): Promise<boolean> {
24
+ const pid = await this.getPid();
25
+ if (!pid) { return false; }
26
+ try {
27
+ process.kill(pid, 0); // See if process is still running
28
+ return false;
29
+ } catch { }
30
+ return true; // Still running
31
+ }
32
+
33
+ async kill(): Promise<boolean> {
34
+ const pid = await this.getPid();
35
+ if (pid && await this.isRunning()) {
36
+ try {
37
+ return process.kill(pid);
38
+ } catch { }
39
+ }
40
+ return false;
41
+ }
42
+
43
+ async ensureKilled(gracePeriod: number = 3000): Promise<boolean> {
44
+ const start = Date.now();
45
+ const pid = await this.getPid();
46
+ while (pid && (Date.now() - start) < gracePeriod) { // Ensure its done
47
+ if (!await this.isRunning()) {
48
+ return true;
49
+ }
50
+ await timers.setTimeout(100);
51
+ }
52
+ try {
53
+ pid && process.kill(pid); // Force kill
54
+ } catch { }
55
+ return pid !== undefined;
56
+ }
57
+ }
@@ -4,12 +4,12 @@ import { rmSync } from 'node:fs';
4
4
 
5
5
  import type { ManifestContext, DeltaEvent } from '@travetto/manifest';
6
6
 
7
- import type { CompilerOp, CompilerEvent } from '../types';
7
+ import type { CompilerEvent, CompilerMode } from '../types';
8
8
  import { AsyncQueue } from '../queue';
9
9
  import { LogUtil } from '../log';
10
10
  import { CommonUtil } from '../util';
11
11
 
12
- const log = LogUtil.scoped('compiler-exec');
12
+ const log = LogUtil.logger('compiler-exec');
13
13
  const isEvent = (msg: unknown): msg is CompilerEvent => !!msg && typeof msg === 'object' && 'type' in msg;
14
14
 
15
15
  /**
@@ -20,8 +20,13 @@ export class CompilerRunner {
20
20
  /**
21
21
  * Run compile process
22
22
  */
23
- static async * runProcess(ctx: ManifestContext, changed: DeltaEvent[], op: CompilerOp, signal: AbortSignal): AsyncIterable<CompilerEvent> {
24
- const watch = op === 'watch';
23
+ static async * runProcess(ctx: ManifestContext, changed: DeltaEvent[], mode: CompilerMode, signal: AbortSignal): AsyncIterable<CompilerEvent> {
24
+ if (signal.aborted) {
25
+ log('debug', 'Skipping, shutting down');
26
+ return;
27
+ }
28
+
29
+ const watch = mode === 'watch';
25
30
  if (!changed.length && !watch) {
26
31
  yield { type: 'state', payload: { state: 'compile-end' } };
27
32
  log('debug', 'Skipped');
@@ -36,7 +41,6 @@ export class CompilerRunner {
36
41
  const changedFiles = changed[0]?.file === '*' ? ['*'] : changed.map(ev => ev.sourceFile);
37
42
 
38
43
  const queue = new AsyncQueue<CompilerEvent>();
39
- let kill: (() => void) | undefined;
40
44
 
41
45
  try {
42
46
  await CommonUtil.writeTextFile(deltaFile, changedFiles.join('\n'));
@@ -53,22 +57,23 @@ export class CompilerRunner {
53
57
  .on('message', msg => isEvent(msg) && queue.add(msg))
54
58
  .on('exit', () => queue.close());
55
59
 
56
- kill = (): unknown => proc.kill('SIGINT');
60
+ const kill = (): unknown => {
61
+ log('debug', 'Shutting down process');
62
+ return (proc.connected ? proc.send('shutdown', (err) => proc.kill()) : proc.kill());
63
+ };
57
64
 
58
65
  process.once('SIGINT', kill);
59
66
  signal.addEventListener('abort', kill);
60
67
 
61
68
  yield* queue;
62
69
 
63
- if (!proc.killed && proc.exitCode !== 0) {
64
- log('error', `Failed during compilation, code=${proc.exitCode}, killed=${proc.killed}`);
70
+ if (proc.exitCode !== 0) {
71
+ log('error', `Terminated during compilation, code=${proc.exitCode}, killed=${proc.killed}`);
65
72
  }
73
+ process.off('SIGINT', kill);
66
74
 
67
75
  log('debug', 'Finished');
68
76
  } finally {
69
- if (kill) {
70
- process.off('SIGINT', kill);
71
- }
72
77
  rmSync(deltaFile, { force: true });
73
78
  }
74
79
  }
@@ -1,36 +1,23 @@
1
1
  import http from 'node:http';
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
+ import { setMaxListeners } from 'node:events';
4
5
 
5
6
  import type { ManifestContext } from '@travetto/manifest';
6
7
 
7
- import type { CompilerMode, CompilerOp, CompilerProgressEvent, CompilerEvent, CompilerEventType, CompilerServerInfo } from '../types';
8
+ import type { CompilerMode, CompilerProgressEvent, CompilerEvent, CompilerEventType, CompilerServerInfo } from '../types';
8
9
  import { LogUtil } from '../log';
9
10
  import { CompilerClient } from './client';
10
11
  import { CommonUtil } from '../util';
12
+ import { ProcessHandle } from './process-handle';
11
13
 
12
- const log = LogUtil.scoped('compiler-server');
14
+ const log = LogUtil.logger('compiler-server');
13
15
 
14
16
  /**
15
17
  * Compiler Server Class
16
18
  */
17
19
  export class CompilerServer {
18
20
 
19
- static readJSONRequest<T>(req: http.IncomingMessage): Promise<T> {
20
- return new Promise<T>((res, rej) => {
21
- const body: Buffer[] = [];
22
- req.on('data', (chunk) => body.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk));
23
- req.on('end', () => {
24
- try {
25
- res(JSON.parse(Buffer.concat(body).toString('utf8')));
26
- } catch (err) {
27
- rej(err);
28
- }
29
- });
30
- req.on('error', rej);
31
- });
32
- }
33
-
34
21
  #ctx: ManifestContext;
35
22
  #server: http.Server;
36
23
  #listeners: { res: http.ServerResponse, type: CompilerEventType }[] = [];
@@ -39,15 +26,18 @@ export class CompilerServer {
39
26
  info: CompilerServerInfo;
40
27
  #client: CompilerClient;
41
28
  #url: string;
29
+ #handle: Record<'compiler' | 'server', ProcessHandle>;
42
30
 
43
- constructor(ctx: ManifestContext, op: CompilerOp) {
31
+ constructor(ctx: ManifestContext, mode: CompilerMode) {
44
32
  this.#ctx = ctx;
45
- this.#client = new CompilerClient(ctx, LogUtil.scoped('client.server'));
33
+ this.#client = new CompilerClient(ctx, LogUtil.logger('client.server'));
46
34
  this.#url = this.#client.url;
35
+ this.#handle = { server: new ProcessHandle(ctx, 'server'), compiler: new ProcessHandle(ctx, 'compiler') };
36
+
47
37
  this.info = {
48
38
  state: 'startup',
49
39
  iteration: Date.now(),
50
- mode: op === 'run' ? 'build' : op,
40
+ mode,
51
41
  serverPid: process.pid,
52
42
  compilerPid: -1,
53
43
  path: ctx.workspace.path,
@@ -60,7 +50,7 @@ export class CompilerServer {
60
50
  keepAliveTimeout: 1000 * 60 * 60,
61
51
  }, (req, res) => this.#onRequest(req, res));
62
52
 
63
- process.once('exit', () => this.#shutdown.abort());
53
+ setMaxListeners(1000, this.signal);
64
54
  }
65
55
 
66
56
  get mode(): CompilerMode {
@@ -80,9 +70,11 @@ export class CompilerServer {
80
70
  const info = await this.#client.info();
81
71
  resolve((info && info.mode === 'build' && this.mode === 'watch') ? 'retry' : 'running');
82
72
  } else {
73
+ log('warn', 'Failed in running server', err);
83
74
  reject(err);
84
75
  }
85
- });
76
+ })
77
+ .on('close', () => log('debug', 'Server close event'));
86
78
 
87
79
  const url = new URL(this.#url);
88
80
  setTimeout(() => this.#server.listen(+url.port, url.hostname), 1); // Run async
@@ -96,6 +88,8 @@ export class CompilerServer {
96
88
  // Let the server finish
97
89
  await this.#client.waitForState(['close'], 'Server closed', this.signal);
98
90
  return this.#tryListen(attempt + 1);
91
+ } else if (output === 'ok') {
92
+ await this.#handle.server.writePid(this.info.serverPid);
99
93
  }
100
94
 
101
95
  return output;
@@ -105,6 +99,10 @@ export class CompilerServer {
105
99
  res.writeHead(200);
106
100
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
107
101
  this.#listeners.push({ res, type: type as 'change' });
102
+ if (type === 'state') { // Send on initial connect
103
+ res.write(JSON.stringify({ state: this.info.state }));
104
+ }
105
+ res.write('\n'); // Send at least one byte on listen
108
106
  await new Promise(resolve => res.on('close', resolve));
109
107
  this.#listeners.splice(this.#listeners.findIndex(x => x.res === res), 1);
110
108
  res.end();
@@ -123,7 +121,9 @@ export class CompilerServer {
123
121
  log('info', 'Server disconnect requested');
124
122
  this.info.iteration = Date.now();
125
123
  await new Promise(r => setTimeout(r, 20));
126
- this.#server.closeAllConnections(); // Force reconnects
124
+ for (const el of this.#listeners) {
125
+ el.res.destroy();
126
+ }
127
127
  }
128
128
 
129
129
  async #clean(): Promise<{ clean: boolean }> {
@@ -144,10 +144,14 @@ export class CompilerServer {
144
144
 
145
145
  let out: unknown;
146
146
  switch (action) {
147
- case 'send-event': await this.#emitEvent(await CompilerServer.readJSONRequest(req)); out = { received: true }; break;
148
147
  case 'event': return await this.#addListener(subAction, res);
149
- case 'stop': out = await this.close(true); break;
150
148
  case 'clean': out = await this.#clean(); break;
149
+ case 'stop': {
150
+ // Must send immediately
151
+ res.end(JSON.stringify({ closing: true }));
152
+ await this.close();
153
+ break;
154
+ }
151
155
  case 'info':
152
156
  default: out = this.info ?? {}; break;
153
157
  }
@@ -158,10 +162,6 @@ export class CompilerServer {
158
162
  * Process events
159
163
  */
160
164
  async processEvents(src: (signal: AbortSignal) => AsyncIterable<CompilerEvent>): Promise<void> {
161
-
162
- LogUtil.log('compiler', 'debug', 'Started, streaming logs');
163
- LogUtil.consumeLogEvents(this.#client.fetchEvents('log', { signal: this.signal }));
164
-
165
165
  for await (const ev of CommonUtil.restartableEvents(src, this.signal, this.isResetEvent)) {
166
166
  if (ev.type === 'progress') {
167
167
  await LogUtil.logProgress?.(ev.payload);
@@ -171,10 +171,13 @@ export class CompilerServer {
171
171
 
172
172
  if (ev.type === 'state') {
173
173
  this.info.state = ev.payload.state;
174
+ await this.#handle.compiler.writePid(this.info.compilerPid);
174
175
  if (ev.payload.state === 'init' && ev.payload.extra && 'pid' in ev.payload.extra && typeof ev.payload.extra.pid === 'number') {
175
176
  this.info.compilerPid = ev.payload.extra.pid;
176
177
  }
177
178
  log('info', `State changed: ${this.info.state}`);
179
+ } else if (ev.type === 'log') {
180
+ LogUtil.logEvent(ev.payload);
178
181
  }
179
182
  if (this.isResetEvent(ev)) {
180
183
  await this.#disconnectActive();
@@ -182,31 +185,45 @@ export class CompilerServer {
182
185
  }
183
186
 
184
187
  // Terminate, after letting all remaining events emit
185
- await this.close(this.signal.aborted);
188
+ await this.close();
189
+
190
+ log('debug', 'Finished processing events');
186
191
  }
187
192
 
188
193
  /**
189
194
  * Close server
190
195
  */
191
- async close(force: boolean): Promise<unknown> {
196
+ async close(): Promise<void> {
192
197
  log('info', 'Closing down server');
193
- await new Promise(r => {
194
198
 
195
- if (force) {
196
- const cancel: CompilerProgressEvent = { complete: true, idx: 0, total: 0, message: 'Complete', operation: 'compile' };
197
- LogUtil.logProgress?.(cancel);
198
- this.#emitEvent({ type: 'progress', payload: cancel });
199
- }
199
+ // If we are in a place where progress exists
200
+ if (this.info.state === 'compile-start') {
201
+ const cancel: CompilerProgressEvent = { complete: true, idx: 0, total: 0, message: 'Complete', operation: 'compile' };
202
+ LogUtil.logProgress?.(cancel);
203
+ this.#emitEvent({ type: 'progress', payload: cancel });
204
+ }
200
205
 
201
- this.#server.close(r);
202
- this.#emitEvent({ type: 'state', payload: { state: 'close' } });
203
- this.#server.unref();
204
- setImmediate(() => {
205
- this.#server.closeAllConnections();
206
- this.#shutdown.abort();
206
+ try {
207
+ await new Promise((resolve, reject) => {
208
+ setTimeout(reject, 2000).unref(); // 2s max wait
209
+ this.#server.close(resolve);
210
+ this.#emitEvent({ type: 'state', payload: { state: 'close' } });
211
+ setImmediate(() => {
212
+ this.#server.closeAllConnections();
213
+ this.#shutdown.abort();
214
+ });
207
215
  });
208
- });
209
- return { closing: true };
216
+ } catch { // Timeout or other error
217
+ // Force shutdown
218
+ this.#server.closeAllConnections();
219
+ if (this.info.compilerPid) { // Ensure its killed
220
+ try {
221
+ process.kill(this.info.compilerPid);
222
+ } catch { }
223
+ }
224
+ }
225
+
226
+ log('info', 'Closed down server');
210
227
  }
211
228
 
212
229
  /**
package/support/setup.ts CHANGED
@@ -214,7 +214,7 @@ export class CompilerSetup {
214
214
  // Update all manifests when in mono repo
215
215
  if (delta.length && ctx.workspace.mono) {
216
216
  const names: string[] = [];
217
- const mods = Object.values(manifest.modules).filter(x => x.local && x.name !== ctx.workspace.name);
217
+ const mods = Object.values(manifest.modules).filter(x => x.workspace && x.name !== ctx.workspace.name);
218
218
  for (const mod of mods) {
219
219
  const modCtx = ManifestUtil.getModuleContext(ctx, mod.sourceFolder);
220
220
  const modManifest = await ManifestUtil.buildManifest(modCtx);
package/support/types.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  export type CompilerMode = 'build' | 'watch';
2
- export type CompilerOp = CompilerMode | 'run';
3
2
 
4
3
  export type CompilerStateType = 'startup' | 'init' | 'compile-start' | 'compile-end' | 'watch-start' | 'watch-end' | 'reset' | 'close';
5
- export type CompilerChangeEvent = { file: string, action: 'create' | 'update' | 'delete', folder: string, output: string, module: string, time: number };
4
+ export type CompilerChangeEvent = { file: string, action: 'create' | 'update' | 'delete', output: string, module: string, time: number };
6
5
  export type CompilerLogLevel = 'info' | 'debug' | 'warn' | 'error';
7
6
  export type CompilerLogEvent = { level: CompilerLogLevel, message: string, time: number, args?: unknown[], scope?: string };
8
7
  export type CompilerProgressEvent = { idx: number, total: number, message: string, operation: 'compile', complete?: boolean };
package/support/util.ts CHANGED
@@ -58,6 +58,7 @@ export class CommonUtil {
58
58
  * Restartable Event Stream
59
59
  */
60
60
  static async * restartableEvents<T>(src: (signal: AbortSignal) => AsyncIterable<T>, parent: AbortSignal, shouldRestart: (item: T) => boolean): AsyncIterable<T> {
61
+ const log = LogUtil.logger('event-stream');
61
62
  outer: while (!parent.aborted) {
62
63
  const controller = new AbortController();
63
64
  setMaxListeners(1000, controller.signal);
@@ -66,18 +67,19 @@ export class CommonUtil {
66
67
 
67
68
  const comp = src(controller.signal);
68
69
 
69
- LogUtil.log('event-stream', 'debug', 'Started event stream');
70
+ log('debug', 'Started event stream');
70
71
 
71
72
  // Wait for all events, close at the end
72
73
  for await (const ev of comp) {
73
74
  yield ev;
74
75
  if (shouldRestart(ev)) {
76
+ log('debug', 'Restarting stream');
75
77
  controller.abort(); // Ensure terminated of process
76
78
  continue outer;
77
79
  }
78
80
  }
79
81
 
80
- LogUtil.log('event-stream', 'debug', 'Finished event stream');
82
+ log('debug', 'Finished event stream');
81
83
 
82
84
  // Natural exit, we done
83
85
  if (!controller.signal.aborted) { // Shutdown source if still running
@@ -1,107 +0,0 @@
1
- import fs from 'node:fs/promises';
2
-
3
- import { IndexedModule, ManifestContext, ManifestModuleUtil, path } from '@travetto/manifest';
4
-
5
- import { AsyncQueue } from '../../support/queue';
6
-
7
- export type WatchEvent<T = {}> =
8
- ({ action: 'create' | 'update' | 'delete', file: string, folder: string } & T) |
9
- { action: 'reset', file: string };
10
-
11
- const CREATE_THRESHOLD = 50;
12
- const VALID_TYPES = new Set(['ts', 'typings', 'js', 'package-json']);
13
- type ToWatch = { file: string, actions: string[] };
14
-
15
- /** Watch file for reset */
16
- function watchForReset(q: AsyncQueue<WatchEvent>, root: string, files: ToWatch[], signal: AbortSignal): void {
17
- const watchers: Record<string, { folder: string, files: Map<string, (ToWatch & { name: string, actionSet: Set<string> })> }> = {};
18
- // Group by base path
19
- for (const el of files) {
20
- const full = path.resolve(root, el.file);
21
- const folder = path.dirname(full);
22
- const tgt = { ...el, name: path.basename(el.file), actionSet: new Set(el.actions) };
23
- const watcher = (watchers[folder] ??= { folder, files: new Map() });
24
- watcher.files.set(tgt.name, tgt);
25
- }
26
-
27
- // Fire them all off
28
- Object.values(watchers).map(async (watcher) => {
29
- try {
30
- for await (const ev of fs.watch(watcher.folder, { persistent: true, encoding: 'utf8', signal })) {
31
- const toWatch = watcher.files.get(ev.filename!);
32
- if (toWatch) {
33
- const stat = await fs.stat(path.resolve(root, ev.filename!)).catch(() => undefined);
34
- const action = !stat ? 'delete' : ((Date.now() - stat.ctimeMs) < CREATE_THRESHOLD) ? 'create' : 'update';
35
- if (toWatch.actionSet.has(action)) {
36
- q.add({ action: 'reset', file: ev.filename! });
37
- }
38
- }
39
- }
40
- } catch (err) {
41
- // Ignore
42
- }
43
- });
44
- }
45
-
46
- /** Watch recursive files for a given folder */
47
- async function watchFolder(ctx: ManifestContext, q: AsyncQueue<WatchEvent>, src: string, target: string, signal: AbortSignal): Promise<void> {
48
- const lib = await import('@parcel/watcher');
49
- const ignore = [
50
- 'node_modules', '**/.trv',
51
- ...((!ctx.workspace.mono || src === ctx.workspace.path) ? [ctx.build.compilerFolder, ctx.build.outputFolder] : []),
52
- ...(await fs.readdir(src)).filter(x => x.startsWith('.'))
53
- ];
54
-
55
- const cleanup = await lib.subscribe(src, async (err, events) => {
56
- if (err) {
57
- console.error('Watch Error', err);
58
- }
59
- for (const ev of events) {
60
- const finalEv = { action: ev.type, file: path.toPosix(ev.path), folder: target };
61
- if (ev.type !== 'delete') {
62
- const stats = await fs.stat(finalEv.file);
63
- if ((Date.now() - stats.ctimeMs) < CREATE_THRESHOLD) {
64
- ev.type = 'create'; // Force create on newly stated files
65
- }
66
- }
67
-
68
- if (ev.type === 'delete' && finalEv.file === src) {
69
- return q.close();
70
- }
71
-
72
- const matches = !finalEv.file.includes('/.') && VALID_TYPES.has(ManifestModuleUtil.getFileType(finalEv.file));
73
- if (matches) {
74
- q.add(finalEv);
75
- }
76
- }
77
- }, { ignore });
78
- signal.addEventListener('abort', () => cleanup.unsubscribe());
79
- }
80
-
81
- /** Watch files */
82
- export async function* fileWatchEvents(manifest: ManifestContext, modules: IndexedModule[], signal: AbortSignal): AsyncIterable<WatchEvent> {
83
- const q = new AsyncQueue<WatchEvent>(signal);
84
-
85
- for (const m of modules.filter(x => !manifest.workspace.mono || x.sourcePath !== manifest.workspace.path)) {
86
- await watchFolder(manifest, q, m.sourcePath, m.sourcePath, signal);
87
- }
88
-
89
- // Add monorepo folders
90
- if (manifest.workspace.mono) {
91
- const mono = modules.find(x => x.sourcePath === manifest.workspace.path)!;
92
- for (const folder of Object.keys(mono.files)) {
93
- if (!folder.startsWith('$')) {
94
- await watchFolder(manifest, q, path.resolve(mono.sourcePath, folder), mono.sourcePath, signal);
95
- }
96
- }
97
- }
98
-
99
- watchForReset(q, manifest.workspace.path, [
100
- { file: manifest.build.outputFolder, actions: ['delete'] },
101
- { file: manifest.build.compilerFolder, actions: ['delete'] },
102
- { file: 'package-lock.json', actions: ['delete', 'update', 'create'] },
103
- { file: 'package.json', actions: ['delete', 'update', 'create'] }
104
- ], signal);
105
-
106
- yield* q;
107
- }