@travetto/compiler 3.3.2 → 3.4.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.
package/src/watch.ts CHANGED
@@ -1,19 +1,22 @@
1
1
  import { readFileSync } from 'fs';
2
+ import { setMaxListeners } from 'events';
2
3
 
3
4
  import {
4
- ManifestContext, ManifestModuleUtil, ManifestUtil, WatchEvent, ManifestModuleFolderType,
5
- ManifestModuleFileType, path, ManifestModule, watchFolders, WatchFolder, RootIndex, WatchStream
5
+ ManifestContext, ManifestModuleUtil, ManifestUtil, ManifestModuleFolderType, ManifestModuleFileType,
6
+ path, ManifestModule,
6
7
  } from '@travetto/manifest';
7
8
  import { getManifestContext } from '@travetto/manifest/bin/context';
8
9
 
10
+ import type { CompileStateEntry } from './types';
9
11
  import { CompilerState } from './state';
10
12
  import { CompilerUtil } from './util';
11
13
 
12
- type CompileWatchEvent = WatchEvent | { action: 'restart', file: string };
13
- const RESTART_SIGNAL = 'RESTART_SIGNAL';
14
+ import { WatchEvent, fileWatchEvents } from './internal/watch-core';
15
+
16
+ type DirtyFile = { modFolder: string, mod: string, remove?: boolean, moduleFile: string, folderKey: ManifestModuleFolderType, type: ManifestModuleFileType };
14
17
 
15
18
  /**
16
- * Utils for watching
19
+ * Watch support, based on compiler state and manifest details
17
20
  */
18
21
  export class CompilerWatcher {
19
22
 
@@ -22,13 +25,13 @@ export class CompilerWatcher {
22
25
  * @param state
23
26
  * @returns
24
27
  */
25
- static watch(state: CompilerState): AsyncIterable<CompileWatchEvent> {
28
+ static watch(state: CompilerState): AsyncIterable<WatchEvent<{ entry: CompileStateEntry }>> {
26
29
  return new CompilerWatcher(state).watchChanges();
27
30
  }
28
31
 
29
32
  #sourceHashes = new Map<string, number>();
30
33
  #manifestContexts = new Map<string, ManifestContext>();
31
- #dirtyFiles: { modFolder: string, mod: string, moduleFile?: string, folderKey?: ManifestModuleFolderType, type?: ManifestModuleFileType }[] = [];
34
+ #dirtyFiles: DirtyFile[] = [];
32
35
  #state: CompilerState;
33
36
 
34
37
  constructor(state: CompilerState) {
@@ -48,15 +51,25 @@ export class CompilerWatcher {
48
51
  return this.#manifestContexts.get(folder)!;
49
52
  }));
50
53
 
51
- const files = this.#dirtyFiles;
54
+ const files = this.#dirtyFiles.slice(0);
52
55
  this.#dirtyFiles = [];
53
56
 
54
57
  for (const ctx of [...contexts, this.#state.manifest]) {
55
58
  const newManifest = await ManifestUtil.buildManifest(ctx);
56
59
  for (const file of files) {
57
- if (file.folderKey && file.moduleFile && file.type && file.mod in newManifest.modules) {
58
- newManifest.modules[file.mod].files[file.folderKey] ??= [];
59
- newManifest.modules[file.mod].files[file.folderKey]!.push([file.moduleFile, file.type, Date.now()]);
60
+ if (file.mod in newManifest.modules) {
61
+ const modFiles = newManifest.modules[file.mod].files[file.folderKey] ??= [];
62
+ const idx = modFiles.findIndex(x => x[0] === file.moduleFile);
63
+
64
+ if (!file.remove && idx < 0) {
65
+ modFiles.push([file.moduleFile, file.type, Date.now()]);
66
+ } else if (idx >= 0) {
67
+ if (file.remove) {
68
+ modFiles.splice(idx, 1);
69
+ } else {
70
+ modFiles[idx] = [file.moduleFile, file.type, Date.now()];
71
+ }
72
+ }
60
73
  }
61
74
  }
62
75
  await ManifestUtil.writeManifest(ctx, newManifest);
@@ -71,131 +84,79 @@ export class CompilerWatcher {
71
84
  );
72
85
  }
73
86
 
74
- /**
75
- * Get a watcher for a given compiler state
76
- * @param state
77
- * @param handler
78
- * @returns
79
- */
80
- async * watchChanges(): AsyncIterable<CompileWatchEvent> {
81
- const stream = this.#watchFiles();
87
+ #addDirtyFile(mod: ManifestModule, folder: string, moduleFile: string, remove = false): void {
88
+ this.#dirtyFiles.push({
89
+ mod: mod.name, modFolder: folder, remove, moduleFile,
90
+ folderKey: ManifestModuleUtil.getFolderKey(moduleFile),
91
+ type: ManifestModuleUtil.getFileType(moduleFile),
92
+ });
93
+ }
82
94
 
95
+ /**
96
+ * Get a watcher for a given compiler state
97
+ * @param state
98
+ * @param handler
99
+ * @returns
100
+ */
101
+ async * watchChanges(): AsyncIterable<WatchEvent<{ entry: CompileStateEntry }>> {
83
102
  const mods = this.#getModuleMap();
84
- for await (const { file: sourceFile, action, folder } of stream) {
103
+ const ctrl = new AbortController();
104
+ setMaxListeners(1000, ctrl.signal);
105
+
106
+ const modules = [...this.#state.manifestIndex.getModuleList('all')].map(x => this.#state.manifestIndex.getModule(x)!);
107
+
108
+ const stream = fileWatchEvents(this.#state.manifest, modules, ctrl.signal);
109
+ for await (const ev of stream) {
85
110
 
86
- if (folder === RESTART_SIGNAL) {
87
- yield { action: 'restart', file: sourceFile };
111
+ if (ev.action === 'reset') {
112
+ yield ev;
113
+ ctrl.abort();
88
114
  return;
89
115
  }
90
116
 
117
+ const { action, file: sourceFile, folder } = ev;
91
118
  const mod = mods[folder];
92
119
  const moduleFile = mod.sourceFolder ?
93
120
  (sourceFile.includes(mod.sourceFolder) ? sourceFile.split(`${mod.sourceFolder}/`)[1] : sourceFile) :
94
121
  sourceFile.replace(`${this.#state.manifest.workspacePath}/`, '');
122
+
123
+ let entry = this.#state.getBySource(sourceFile);
124
+
95
125
  switch (action) {
96
126
  case 'create': {
97
127
  const fileType = ManifestModuleUtil.getFileType(moduleFile);
98
- this.#dirtyFiles.push({
99
- mod: mod.name,
100
- modFolder: folder,
101
- moduleFile,
102
- folderKey: ManifestModuleUtil.getFolderKey(moduleFile),
103
- type: ManifestModuleUtil.getFileType(moduleFile)
104
- });
128
+ this.#addDirtyFile(mod, folder, moduleFile);
105
129
  if (CompilerUtil.validFile(fileType)) {
106
- await this.#rebuildManifestsIfNeeded();
107
-
108
130
  const hash = CompilerUtil.naiveHash(readFileSync(sourceFile, 'utf8'));
109
- const input = this.#state.registerInput(mod, moduleFile);
131
+ entry = this.#state.registerInput(mod, moduleFile);
110
132
  this.#sourceHashes.set(sourceFile, hash);
111
- yield { action, file: input, folder };
112
133
  }
113
134
  break;
114
135
  }
115
136
  case 'update': {
116
- await this.#rebuildManifestsIfNeeded();
117
- const entry = this.#state.getBySource(sourceFile);
118
137
  if (entry) {
119
138
  const hash = CompilerUtil.naiveHash(readFileSync(sourceFile, 'utf8'));
120
139
  if (this.#sourceHashes.get(sourceFile) !== hash) {
121
140
  this.#state.resetInputSource(entry.input);
122
141
  this.#sourceHashes.set(sourceFile, hash);
123
- yield { action, file: entry.input, folder };
124
142
  }
125
143
  }
126
144
  break;
127
145
  }
128
146
  case 'delete': {
129
- const entry = this.#state.getBySource(sourceFile);
130
147
  if (entry) {
131
148
  this.#state.removeInput(entry.input);
132
149
  if (entry.output) {
133
- this.#dirtyFiles.push({ mod: mod.name, modFolder: folder });
134
- yield { action, file: entry.output, folder };
150
+ this.#addDirtyFile(mod, folder, moduleFile, true);
135
151
  }
136
152
  }
137
153
  }
138
154
  }
139
- }
140
- }
141
155
 
142
- /**
143
- * Watch files based on root index
144
- */
145
- #watchFiles(): WatchStream {
146
- const idx = this.#state.manifestIndex;
147
- const modules = [...idx.getModuleList('all')].map(x => idx.getModule(x)!);
148
- const options: Partial<WatchFolder> = {
149
- filter: (ev: WatchEvent): boolean => {
150
- const type = ManifestModuleUtil.getFileType(ev.file);
151
- return type === 'ts' || type === 'typings' || type === 'js' || type === 'package-json';
152
- },
153
- ignore: ['node_modules', '**/.trv_*'],
154
- };
155
-
156
- const moduleFolders: WatchFolder[] = modules
157
- .filter(x => !idx.manifest.monoRepo || x.sourcePath !== idx.manifest.workspacePath)
158
- .map(x => ({ src: x.sourcePath, target: x.sourcePath }));
159
-
160
- // Add monorepo folders
161
- if (idx.manifest.monoRepo) {
162
- const mono = modules.find(x => x.sourcePath === idx.manifest.workspacePath)!;
163
- for (const folder of Object.keys(mono.files)) {
164
- if (!folder.startsWith('$')) {
165
- moduleFolders.push({ src: path.resolve(mono.sourcePath, folder), target: mono.sourcePath });
166
- }
156
+ if (entry) {
157
+ await this.#rebuildManifestsIfNeeded();
158
+ yield { action, file: entry.source, folder, entry };
167
159
  }
168
- moduleFolders.push({ src: mono.sourcePath, target: mono.sourcePath, immediate: true });
169
160
  }
170
-
171
- // Watch output folders
172
- const outputWatch = (root: string, sources: string[]): WatchFolder => {
173
- const valid = new Set(sources.map(src => path.resolve(root, src)));
174
- return {
175
- src: root, target: RESTART_SIGNAL, immediate: true, includeHidden: true,
176
- filter: ev => ev.action === 'delete' && valid.has(path.resolve(root, ev.file))
177
- };
178
- };
179
-
180
- const topLevelFiles = (root: string, files: string[]): WatchFolder => {
181
- const valid = new Set(files.map(src => path.resolve(root, src)));
182
- return {
183
- src: root, target: RESTART_SIGNAL, immediate: true,
184
- filter: ev => valid.has(path.resolve(root, ev.file))
185
- };
186
- };
187
-
188
- moduleFolders.push(
189
- outputWatch(RootIndex.manifest.workspacePath, [
190
- RootIndex.manifest.outputFolder,
191
- RootIndex.manifest.compilerFolder
192
- ]),
193
- topLevelFiles(RootIndex.manifest.workspacePath, [
194
- 'package.json',
195
- 'package-lock.json'
196
- ])
197
- );
198
-
199
- return watchFolders(moduleFolders, options);
200
161
  }
201
162
  }
@@ -0,0 +1,73 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ import type { ManifestContext } from '@travetto/manifest';
5
+
6
+ import { LogUtil } from './log';
7
+ import { CompilerSetup } from './setup';
8
+ import { CompilerServer } from './server/server';
9
+ import { CompilerRunner } from './server/runner';
10
+ import type { CompilerOp, CompilerServerInfo } from './types';
11
+ import { CompilerClientUtil } from './server/client';
12
+ import { CommonUtil } from './util';
13
+
14
+ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
15
+ export const main = (root: ManifestContext, ctx: ManifestContext) => {
16
+ const ops = {
17
+ /** Stop the server */
18
+ async stop(): Promise<void> {
19
+ if (await fetch(`${ctx.compilerUrl}/stop`).then(v => v.ok, () => false)) {
20
+ console.log(`Stopped server ${ctx.workspacePath}: [${ctx.compilerUrl}]`);
21
+ } else {
22
+ console.log(`Server not running ${ctx.workspacePath}: [${ctx.compilerUrl}]`);
23
+ }
24
+ },
25
+
26
+ /** Get server info */
27
+ info(): Promise<CompilerServerInfo> {
28
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
29
+ return fetch(ctx.compilerUrl).then(v => v.json(), () => ({ state: 'Server not running' })) as Promise<CompilerServerInfo>;
30
+ },
31
+
32
+ /** Clean the server */
33
+ async clean(): Promise<void> {
34
+ const folders = [ctx.outputFolder, ctx.compilerFolder];
35
+ if (await fetch(`${ctx.compilerUrl}/clean`).then(v => v.ok, () => false)) {
36
+ return console.log(`Clean triggered ${ctx.workspacePath}:`, folders);
37
+ } else {
38
+ await Promise.all(folders.map(f => fs.rm(path.resolve(ctx.workspacePath, f), { force: true, recursive: true })));
39
+ return console.log(`Cleaned ${ctx.workspacePath}:`, folders);
40
+ }
41
+ },
42
+
43
+ /** Main entry point for compilation */
44
+ async compile(op: CompilerOp, setupOnly = false): Promise<(mod: string) => Promise<unknown>> {
45
+ LogUtil.initLogs(ctx, op === 'run' ? 'error' : 'info');
46
+
47
+ const server = await new CompilerServer(root, op).listen();
48
+
49
+ // Wait for build to be ready
50
+ if (server) {
51
+ await server.processEvents(async function* (signal) {
52
+ const { changed, manifest } = await CompilerSetup.setup(root);
53
+ if (!setupOnly) {
54
+ yield* CompilerRunner.runProcess(root, manifest, changed, op, signal);
55
+ } else {
56
+ yield* [];
57
+ }
58
+ });
59
+ } else {
60
+ await CompilerClientUtil.waitForBuild(root);
61
+ }
62
+ return CommonUtil.moduleLoader(ctx);
63
+ },
64
+
65
+ /** Manifest entry point */
66
+ async manifest(args: (string | undefined)[] = []): Promise<void> {
67
+ await ops.compile('run', true);
68
+ await CompilerSetup.exportManifest(ctx, ...args.filter(x => !x?.startsWith('-'))); return;
69
+ }
70
+ };
71
+ return ops;
72
+ };
73
+
package/support/log.ts CHANGED
@@ -1,51 +1,69 @@
1
- export type CompilerLogEvent = [level: 'info' | 'debug' | 'warn', message: string];
2
- export type CompilerLogger = (...args: CompilerLogEvent) => void;
1
+ import type { ManifestContext } from '@travetto/manifest';
2
+ import type { CompilerLogEvent, CompilerLogLevel } from './types';
3
+
4
+ export type CompilerLogger = (level: CompilerLogLevel, message: string, ...args: unknown[]) => void;
3
5
  export type WithLogger<T> = (log: CompilerLogger) => Promise<T>;
4
6
 
7
+ const LEVEL_TO_PRI: Record<CompilerLogLevel, number> = { debug: 1, info: 2, warn: 3, error: 4 };
8
+
5
9
  const SCOPE_MAX = 15;
6
10
 
7
11
  export class LogUtil {
8
12
 
9
- static levels: {
10
- debug: boolean;
11
- info: boolean;
12
- warn: boolean;
13
- };
14
-
15
- static set level(value: string) {
16
- this.levels = {
17
- warn: /^(debug|info|warn)$/.test(value),
18
- info: /^(debug|info)$/.test(value),
19
- debug: /^debug$/.test(value),
20
- };
13
+ static root = process.cwd();
14
+
15
+ static logLevel: CompilerLogLevel = 'error';
16
+
17
+ /**
18
+ * Set level for operation
19
+ */
20
+ static initLogs(ctx: ManifestContext, defaultLevel: CompilerLogLevel): void {
21
+ // Listen only if we aren't in quiet
22
+ if ((process.env.TRV_BUILD || !process.env.TRV_QUIET)) {
23
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
24
+ this.logLevel = (process.env.TRV_BUILD as 'debug') || defaultLevel;
25
+ }
26
+ this.root = ctx.workspacePath;
21
27
  }
22
28
 
23
29
  /**
24
- * Is object a log event
30
+ * Is the log level active?
25
31
  */
26
- static isLogEvent = (o: unknown): o is CompilerLogEvent => o !== null && o !== undefined && Array.isArray(o);
32
+ static isLevelActive(lvl: CompilerLogLevel): boolean {
33
+ return LEVEL_TO_PRI[this.logLevel] <= LEVEL_TO_PRI[lvl];
34
+ }
27
35
 
28
36
  /**
29
37
  * Log message with filtering by level
30
38
  */
31
- static log(scope: string, args: string[], ...[level, msg]: CompilerLogEvent): void {
32
- const message = msg.replaceAll(process.cwd(), '.');
33
- if (LogUtil.levels[level]) {
34
- const params = [`[${scope.padEnd(SCOPE_MAX, ' ')}]`, ...args, message];
35
- if (!/(0|false|off|no)$/i.test(process.env.TRV_LOG_TIME ?? '')) {
36
- params.unshift(new Date().toISOString());
37
- }
38
- // eslint-disable-next-line no-console
39
- console[level]!(...params);
40
- }
39
+ static log(scope: string, level: CompilerLogLevel, message: string, ...args: unknown[]): void {
40
+ LogUtil.sendLogEventToConsole({ level, scope, message, args, time: Date.now() });
41
41
  }
42
42
 
43
43
  /**
44
44
  * With logger
45
45
  */
46
- static withLogger<T>(scope: string, op: WithLogger<T>, basic = true, args: string[] = []): Promise<T> {
47
- const log = this.log.bind(null, scope, args);
46
+ static withLogger<T>(scope: string, op: WithLogger<T>, basic = true): Promise<T> {
47
+ const log = this.log.bind(null, scope);
48
48
  basic && log('debug', 'Started');
49
49
  return op(log).finally(() => basic && log('debug', 'Completed'));
50
50
  }
51
+
52
+ /**
53
+ * Compiler log event to console
54
+ */
55
+ static sendLogEventToConsole(ev: CompilerLogEvent): void {
56
+ if (this.isLevelActive(ev.level)) {
57
+ const params = [ev.message, ...ev.args ?? []].map(x => typeof x === 'string' ? x.replaceAll(LogUtil.root, '.') : x);
58
+ if (ev.scope) {
59
+ params.unshift(`[${ev.scope.padEnd(SCOPE_MAX, ' ')}]`);
60
+ }
61
+ params.unshift(`${ev.level.padEnd(5)}`);
62
+ if (!/(0|false|off|no)$/i.test(process.env.TRV_LOG_TIME ?? '')) {
63
+ params.unshift(new Date().toISOString());
64
+ }
65
+ // eslint-disable-next-line no-console
66
+ console[ev.level]!(...params);
67
+ }
68
+ }
51
69
  }
@@ -0,0 +1,41 @@
1
+ type PromiseResolver<T> = { resolve: (v: T) => void, reject: (err?: unknown) => void };
2
+
3
+ function resolvablePromise<T = void>(): Promise<T> & PromiseResolver<T> {
4
+ let ops: PromiseResolver<T>;
5
+ const prom = new Promise<T>((resolve, reject) => ops = { resolve, reject });
6
+ return Object.assign(prom, ops!);
7
+ }
8
+
9
+ export class AsyncQueue<X> implements AsyncIterator<X>, AsyncIterable<X> {
10
+ #queue: X[] = [];
11
+ #done = false;
12
+ #ready = resolvablePromise();
13
+
14
+ constructor(signal?: AbortSignal) {
15
+ signal?.addEventListener('abort', () => this.close());
16
+ }
17
+
18
+ [Symbol.asyncIterator](): AsyncIterator<X> { return this; }
19
+
20
+ async next(): Promise<IteratorResult<X>> {
21
+ while (!this.#done && !this.#queue.length) {
22
+ await this.#ready;
23
+ this.#ready = resolvablePromise();
24
+ }
25
+ return { value: (this.#queue.length ? this.#queue.shift() : undefined)!, done: this.#done };
26
+ }
27
+
28
+ add(item: X, immediate = false): void {
29
+ if (!immediate) {
30
+ this.#queue.push(item);
31
+ } else {
32
+ this.#queue.unshift(item);
33
+ }
34
+ this.#ready.resolve();
35
+ }
36
+
37
+ close(): void {
38
+ this.#done = true;
39
+ this.#ready.resolve();
40
+ }
41
+ }
@@ -0,0 +1,118 @@
1
+ import rl from 'readline/promises';
2
+ import { Readable } from 'stream';
3
+
4
+ import type { ManifestContext } from '@travetto/manifest';
5
+ import type { CompilerServerEvent, CompilerServerEventType, CompilerServerInfo, CompilerStateType } from '../types';
6
+
7
+ import { LogUtil } from '../log';
8
+
9
+ declare global {
10
+ interface RequestInit { timeout?: number }
11
+ }
12
+
13
+ const log = LogUtil.log.bind(LogUtil, 'compiler-client');
14
+
15
+ function getSignal(input?: AbortSignal): AbortSignal {
16
+ // Ensure we capture end of process at least
17
+ if (!input) {
18
+ const ctrl = new AbortController();
19
+ process.on('SIGINT', () => ctrl.abort());
20
+ input = ctrl.signal;
21
+ }
22
+ return input;
23
+ }
24
+
25
+ /**
26
+ * Compiler Client Operations
27
+ */
28
+ export class CompilerClientUtil {
29
+
30
+ /**
31
+ * Get server information, if server is running
32
+ */
33
+ static async getServerInfo(ctx: ManifestContext): Promise<CompilerServerInfo | undefined> {
34
+ try {
35
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
36
+ const res = (await fetch(`${ctx.compilerUrl}/info`).then(v => v.json())) as CompilerServerInfo;
37
+ return res;
38
+ } catch (err) {
39
+ return undefined;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Fetch compiler events
45
+ */
46
+ static async * fetchEvents<
47
+ V extends CompilerServerEventType,
48
+ T extends (CompilerServerEvent & { type: V })['payload']
49
+ >(ctx: ManifestContext, type: V, signal?: AbortSignal, until?: (ev: T) => boolean): AsyncIterable<T> {
50
+ log('debug', `Starting watch for events of type "${type}"`);
51
+
52
+ signal = getSignal(signal);
53
+
54
+ for (; ;) {
55
+ const ctrl = new AbortController();
56
+ try {
57
+ signal.addEventListener('abort', () => ctrl.abort());
58
+ const stream = await fetch(`${ctx.compilerUrl}/event/${type}`, {
59
+ signal: ctrl.signal,
60
+ timeout: 1000 * 60 * 60
61
+ });
62
+ for await (const line of rl.createInterface(Readable.fromWeb(stream.body!))) {
63
+ if (line.trim().charAt(0) === '{') {
64
+ const val = JSON.parse(line);
65
+ if (until?.(val)) {
66
+ setTimeout(() => ctrl.abort(), 1);
67
+ }
68
+ yield val;
69
+ }
70
+ }
71
+ } catch (err) { }
72
+
73
+ if (ctrl.signal.aborted || !(await this.getServerInfo(ctx))) { // If health check fails, or aborted
74
+ log('debug', `Stopping watch for events of type "${type}"`);
75
+ return;
76
+ } else {
77
+ log('debug', `Restarting watch for events of type "${type}"`);
78
+ }
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Wait for one of N states to be achieved
84
+ */
85
+ static async waitForState(ctx: ManifestContext, states: CompilerStateType[], signal?: AbortSignal): Promise<void> {
86
+ const set = new Set(states);
87
+ const existing = await this.getServerInfo(ctx);
88
+ log('debug', `Existing: ${JSON.stringify(existing)}`);
89
+ if (existing && set.has(existing.state)) {
90
+ log('debug', `Waited for state, ${existing.state} in server info`);
91
+ return;
92
+ }
93
+ // Loop until
94
+ log('debug', `Waiting for states, ${states.join(', ')}`);
95
+ for await (const _ of this.fetchEvents(ctx, 'state', signal, s => set.has(s.state))) { }
96
+ log('debug', `Found state, one of ${states.join(', ')}`);
97
+ }
98
+
99
+ /**
100
+ * Stream logs
101
+ */
102
+ static async streamLogs(ctx: ManifestContext, signal?: AbortSignal): Promise<void> {
103
+ if (!LogUtil.logLevel) {
104
+ return;
105
+ }
106
+ for await (const ev of this.fetchEvents(ctx, 'log', signal!)) {
107
+ LogUtil.sendLogEventToConsole(ev);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Wait for build
113
+ */
114
+ static async waitForBuild(ctx: ManifestContext, signal?: AbortSignal): Promise<void> {
115
+ await this.waitForState(ctx, ['compile-end', 'watch-start'], signal);
116
+ log('info', 'Successfully built');
117
+ }
118
+ }
@@ -0,0 +1,107 @@
1
+ import cp from 'child_process';
2
+ import path from 'path';
3
+
4
+ import type { ManifestContext, ManifestRoot, DeltaEvent } from '@travetto/manifest';
5
+
6
+ import type { CompilerOp, CompilerProgressEvent, CompilerServerEvent } from '../types';
7
+ import { AsyncQueue } from '../queue';
8
+ import { LogUtil } from '../log';
9
+ import { CommonUtil } from '../util';
10
+ import { CompilerClientUtil } from './client';
11
+
12
+ const log = LogUtil.log.bind(null, 'compiler-exec');
13
+
14
+ /**
15
+ * Running the compiler
16
+ */
17
+ export class CompilerRunner {
18
+
19
+ /**
20
+ * Track compiler progress
21
+ */
22
+ static async trackProgress(ctx: ManifestContext, src: AsyncIterable<CompilerProgressEvent>): Promise<void> {
23
+ const compiler = path.resolve(ctx.workspacePath, ctx.compilerFolder);
24
+ const main = path.resolve(compiler, 'node_modules', '@travetto/terminal/__index__.js');
25
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
26
+ const { GlobalTerminal } = await import(main) as typeof import('@travetto/terminal');
27
+
28
+ await GlobalTerminal.init();
29
+ await GlobalTerminal.trackProgress(src,
30
+ x => ({ ...x, text: `Compiling [%idx/%total] -- ${x.message}` }),
31
+ { position: 'bottom', minDelay: 50, staticMessage: 'Compiling' }
32
+ );
33
+ }
34
+
35
+ /**
36
+ * Run compile process
37
+ */
38
+ static async* runProcess(ctx: ManifestContext, manifest: ManifestRoot, changed: DeltaEvent[], op: CompilerOp, signal: AbortSignal): AsyncIterable<CompilerServerEvent> {
39
+ const watch = op === 'watch';
40
+ if (!changed.length && !watch) {
41
+ yield { type: 'state', payload: { state: 'compile-end' } };
42
+ log('debug', 'Skipped');
43
+ return;
44
+ } else {
45
+ log('debug', `Started watch=${watch} changed=${changed.slice(0, 10).map(x => `${x.module}/${x.file}`)}`);
46
+ }
47
+
48
+ // Track progress if not in run mode
49
+ if (op !== 'run') {
50
+ this.trackProgress(ctx, CompilerClientUtil.fetchEvents(ctx, 'progress', signal, ev => !!ev.complete));
51
+ }
52
+
53
+ const compiler = path.resolve(ctx.workspacePath, ctx.compilerFolder);
54
+ const main = path.resolve(compiler, 'node_modules', '@travetto/compiler/support/entry.compiler.js');
55
+ const deltaFile = path.resolve(ctx.workspacePath, ctx.toolFolder, 'manifest-delta.json');
56
+
57
+ const changedFiles = changed[0]?.file === '*' ? ['*'] : changed.map(ev =>
58
+ path.resolve(manifest.workspacePath, manifest.modules[ev.module].sourceFolder, ev.file)
59
+ );
60
+
61
+ let proc: cp.ChildProcess | undefined;
62
+ let kill: (() => void) | undefined;
63
+
64
+ const queue = new AsyncQueue<CompilerServerEvent>(signal);
65
+
66
+ try {
67
+ await CommonUtil.writeTextFile(deltaFile, changedFiles.join('\n'));
68
+
69
+ log('info', 'Launching compiler');
70
+ proc = cp.spawn(process.argv0, [main, deltaFile, `${watch}`], {
71
+ env: {
72
+ ...process.env,
73
+ TRV_MANIFEST: path.resolve(ctx.workspacePath, ctx.outputFolder, 'node_modules', ctx.mainModule),
74
+ },
75
+ stdio: ['pipe', 'pipe', 2, 'ipc'],
76
+ })
77
+ .on('message', msg => {
78
+ if (msg && typeof msg === 'object' && 'type' in msg) {
79
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
80
+ queue.add(msg as CompilerServerEvent);
81
+ }
82
+ })
83
+ .on('exit', code => {
84
+ if (code !== null && code > 0) {
85
+ log('error', 'Failed during compilation');
86
+ }
87
+ queue.close();
88
+ });
89
+
90
+ kill = (): void => { proc?.kill('SIGKILL'); };
91
+ signal.addEventListener('abort', kill);
92
+ process.on('exit', kill);
93
+
94
+ yield* queue;
95
+
96
+ log('debug', `exit code: ${proc?.exitCode}`);
97
+ log('debug', 'Finished');
98
+ } finally {
99
+ if (proc?.killed === false) {
100
+ proc.kill('SIGKILL');
101
+ }
102
+ if (kill) {
103
+ process.off('exit', kill);
104
+ }
105
+ }
106
+ }
107
+ }