@travetto/compiler 7.0.0-rc.0 → 7.0.0-rc.2
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/bin/entry.common.js +26 -26
- package/bin/gen.context.mjs +6 -6
- package/bin/trvc.js +16 -16
- package/package.json +4 -4
- package/src/compiler.ts +41 -40
- package/src/state.ts +16 -16
- package/src/types.ts +1 -1
- package/src/util.ts +4 -4
- package/src/watch.ts +43 -43
- package/support/entry.main.ts +12 -12
- package/support/log.ts +30 -28
- package/support/queue.ts +5 -13
- package/support/server/client.ts +33 -33
- package/support/server/process-handle.ts +19 -19
- package/support/server/runner.ts +9 -8
- package/support/server/server.ts +46 -46
- package/support/setup.ts +33 -28
- package/support/types.ts +2 -2
- package/support/util.ts +5 -5
package/src/watch.ts
CHANGED
|
@@ -19,15 +19,15 @@ export class CompilerWatcher {
|
|
|
19
19
|
#cleanup: Partial<Record<'tool' | 'workspace' | 'canary', () => (void | Promise<void>)>> = {};
|
|
20
20
|
#watchCanary: string = '.trv/canary.id';
|
|
21
21
|
#lastWorkspaceModified = Date.now();
|
|
22
|
-
#
|
|
22
|
+
#watchCanaryFrequency = 5;
|
|
23
23
|
#root: string;
|
|
24
|
-
#
|
|
24
|
+
#queue: AsyncQueue<CompilerWatchEvent>;
|
|
25
25
|
|
|
26
26
|
constructor(state: CompilerState, signal: AbortSignal) {
|
|
27
27
|
this.#state = state;
|
|
28
28
|
this.#root = state.manifest.workspace.path;
|
|
29
|
-
this.#
|
|
30
|
-
signal.addEventListener('abort', () => Object.values(this.#cleanup).forEach(
|
|
29
|
+
this.#queue = new AsyncQueue(signal);
|
|
30
|
+
signal.addEventListener('abort', () => Object.values(this.#cleanup).forEach(fn => fn()));
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
async #getWatchIgnores(): Promise<string[]> {
|
|
@@ -53,7 +53,7 @@ export class CompilerWatcher {
|
|
|
53
53
|
ignores.add(item);
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
return [...ignores].toSorted().map(
|
|
56
|
+
return [...ignores].toSorted().map(ignore => ignore.endsWith('/') ? ignore : `${ignore}/`);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
#toCandidateEvent(action: CompilerWatchEvent['action'], file: string): CompilerWatchEventCandidate {
|
|
@@ -69,16 +69,16 @@ export class CompilerWatcher {
|
|
|
69
69
|
return { entry, file: entry?.sourceFile ?? file, action };
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
#isValidEvent(
|
|
73
|
-
const relativeFile =
|
|
72
|
+
#isValidEvent(event: CompilerWatchEventCandidate): event is CompilerWatchEvent {
|
|
73
|
+
const relativeFile = event.file.replace(`${this.#root}/`, '');
|
|
74
74
|
if (relativeFile === this.#watchCanary) {
|
|
75
75
|
return false;
|
|
76
76
|
} else if (relativeFile.startsWith('.')) {
|
|
77
77
|
return false;
|
|
78
|
-
} else if (!
|
|
78
|
+
} else if (!event.entry) {
|
|
79
79
|
log.debug(`Skipping unknown file ${relativeFile}`);
|
|
80
80
|
return false;
|
|
81
|
-
} else if (
|
|
81
|
+
} else if (event.action === 'update' && !this.#state.checkIfSourceChanged(event.entry.sourceFile)) {
|
|
82
82
|
log.debug(`Skipping update, as contents unchanged ${relativeFile}`);
|
|
83
83
|
return false;
|
|
84
84
|
} else if (!CompilerUtil.validFile(ManifestModuleUtil.getFileType(relativeFile))) {
|
|
@@ -89,19 +89,19 @@ export class CompilerWatcher {
|
|
|
89
89
|
|
|
90
90
|
#getManifestUpdateEventsByParents(events: CompilerWatchEvent[]): Map<string, CompilerWatchEvent[]> {
|
|
91
91
|
const eventsByMod = new Map<string, CompilerWatchEvent[]>();
|
|
92
|
-
for (const
|
|
93
|
-
if (
|
|
92
|
+
for (const event of events) {
|
|
93
|
+
if (event.action === 'update') {
|
|
94
94
|
continue;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
const mod =
|
|
98
|
-
const moduleSet = new Set(this.#state.manifestIndex.getDependentModules(mod.name, 'parents').map(
|
|
97
|
+
const mod = event.entry.module;
|
|
98
|
+
const moduleSet = new Set(this.#state.manifestIndex.getDependentModules(mod.name, 'parents').map(indexedMod => indexedMod.name));
|
|
99
99
|
moduleSet.add(this.#state.manifest.workspace.name);
|
|
100
|
-
for (const
|
|
101
|
-
if (!eventsByMod.has(
|
|
102
|
-
eventsByMod.set(
|
|
100
|
+
for (const moduleName of moduleSet) {
|
|
101
|
+
if (!eventsByMod.has(moduleName)) {
|
|
102
|
+
eventsByMod.set(moduleName, []);
|
|
103
103
|
}
|
|
104
|
-
eventsByMod.get(
|
|
104
|
+
eventsByMod.get(moduleName)!.push(event);
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
return eventsByMod;
|
|
@@ -120,7 +120,7 @@ export class CompilerWatcher {
|
|
|
120
120
|
const roleType = ManifestModuleUtil.getFileRole(relativeFile)!;
|
|
121
121
|
|
|
122
122
|
const manifestModuleFiles = manifest.modules[moduleName].files[folderKey] ??= [];
|
|
123
|
-
const idx = manifestModuleFiles.findIndex(
|
|
123
|
+
const idx = manifestModuleFiles.findIndex(indexedFile => indexedFile[0] === relativeFile);
|
|
124
124
|
const wrappedIdx = idx < 0 ? manifestModuleFiles.length : idx;
|
|
125
125
|
|
|
126
126
|
switch (action) {
|
|
@@ -137,8 +137,8 @@ export class CompilerWatcher {
|
|
|
137
137
|
const moduleManifest = ManifestUtil.readManifestSync(manifestLocation);
|
|
138
138
|
|
|
139
139
|
log.debug('Updating manifest', { module: mod, events: events.length });
|
|
140
|
-
for (const
|
|
141
|
-
this.#updateManifestForEvent(
|
|
140
|
+
for (const event of events) {
|
|
141
|
+
this.#updateManifestForEvent(event, moduleManifest);
|
|
142
142
|
}
|
|
143
143
|
await ManifestUtil.writeManifest(moduleManifest);
|
|
144
144
|
}
|
|
@@ -149,44 +149,44 @@ export class CompilerWatcher {
|
|
|
149
149
|
async #listenWorkspace(): Promise<void> {
|
|
150
150
|
const lib = await import('@parcel/watcher');
|
|
151
151
|
const ignore = await this.#getWatchIgnores();
|
|
152
|
-
const packageFiles = new Set(['package-lock.json', 'yarn.lock', 'package.json'].map(
|
|
152
|
+
const packageFiles = new Set(['package-lock.json', 'yarn.lock', 'package.json'].map(file => path.resolve(this.#root, file)));
|
|
153
153
|
|
|
154
154
|
log.debug('Ignore Globs', ignore);
|
|
155
155
|
log.debug('Watching', this.#root);
|
|
156
156
|
|
|
157
157
|
await this.#cleanup.workspace?.();
|
|
158
158
|
|
|
159
|
-
const listener = await lib.subscribe(this.#root, async (
|
|
159
|
+
const listener = await lib.subscribe(this.#root, async (error, events) => {
|
|
160
160
|
this.#lastWorkspaceModified = Date.now();
|
|
161
161
|
|
|
162
162
|
try {
|
|
163
|
-
if (
|
|
164
|
-
throw
|
|
163
|
+
if (error) {
|
|
164
|
+
throw error instanceof Error ? error : new Error(`${error}`);
|
|
165
165
|
} else if (events.length > 25) {
|
|
166
166
|
throw new CompilerReset(`Large influx of file changes: ${events.length}`);
|
|
167
|
-
} else if (events.some(
|
|
167
|
+
} else if (events.some(event => packageFiles.has(path.toPosix(event.path)))) {
|
|
168
168
|
throw new CompilerReset('Package information changed');
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
const items = events
|
|
172
|
-
.map(
|
|
173
|
-
.filter(
|
|
172
|
+
.map(event => this.#toCandidateEvent(event.type, path.toPosix(event.path)))
|
|
173
|
+
.filter(event => this.#isValidEvent(event));
|
|
174
174
|
|
|
175
175
|
try {
|
|
176
176
|
await this.#reconcileManifestUpdates(items);
|
|
177
|
-
} catch (
|
|
178
|
-
log.info('Restarting due to manifest rebuild failure',
|
|
179
|
-
throw new CompilerReset(`Manifest rebuild failure: ${
|
|
177
|
+
} catch (manifestError) {
|
|
178
|
+
log.info('Restarting due to manifest rebuild failure', manifestError);
|
|
179
|
+
throw new CompilerReset(`Manifest rebuild failure: ${manifestError}`);
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
for (const item of items) {
|
|
183
|
-
this.#
|
|
183
|
+
this.#queue.add(item);
|
|
184
184
|
}
|
|
185
185
|
} catch (out) {
|
|
186
186
|
if (out instanceof Error && out.message.includes('Events were dropped by the FSEvents client.')) {
|
|
187
187
|
out = new CompilerReset('FSEvents failure, requires restart');
|
|
188
188
|
}
|
|
189
|
-
return this.#
|
|
189
|
+
return this.#queue.throw(out instanceof Error ? out : new Error(`${out}`));
|
|
190
190
|
}
|
|
191
191
|
}, { ignore });
|
|
192
192
|
|
|
@@ -198,20 +198,20 @@ export class CompilerWatcher {
|
|
|
198
198
|
const toolRootFolder = path.dirname(path.resolve(this.#root, build.compilerFolder));
|
|
199
199
|
const toolFolders = new Set([
|
|
200
200
|
toolRootFolder, build.compilerFolder, build.typesFolder, build.outputFolder
|
|
201
|
-
].map(
|
|
201
|
+
].map(folder => path.resolve(this.#root, folder)));
|
|
202
202
|
|
|
203
|
-
log.debug('Tooling Folders', [...toolFolders].map(
|
|
203
|
+
log.debug('Tooling Folders', [...toolFolders].map(folder => folder.replace(`${this.#root}/`, '')));
|
|
204
204
|
|
|
205
205
|
await this.#cleanup.tool?.();
|
|
206
206
|
|
|
207
|
-
const listener = watch(toolRootFolder, { encoding: 'utf8' }, async (
|
|
208
|
-
if (!
|
|
207
|
+
const listener = watch(toolRootFolder, { encoding: 'utf8' }, async (event, file) => {
|
|
208
|
+
if (!file) {
|
|
209
209
|
return;
|
|
210
210
|
}
|
|
211
|
-
const full = path.resolve(toolRootFolder,
|
|
211
|
+
const full = path.resolve(toolRootFolder, file);
|
|
212
212
|
const stat = await fs.stat(full).catch(() => null);
|
|
213
213
|
if (toolFolders.has(full) && !stat) {
|
|
214
|
-
this.#
|
|
214
|
+
this.#queue.throw(new CompilerReset(`Tooling folder removal ${full}`));
|
|
215
215
|
}
|
|
216
216
|
});
|
|
217
217
|
this.#cleanup.tool = (): void => listener.close();
|
|
@@ -228,15 +228,15 @@ export class CompilerWatcher {
|
|
|
228
228
|
if (delta > 600) {
|
|
229
229
|
log.error('Restarting canary due to extra long delay');
|
|
230
230
|
this.#lastWorkspaceModified = Date.now(); // Reset
|
|
231
|
-
} else if (delta > this.#
|
|
232
|
-
this.#
|
|
233
|
-
} else if (delta > this.#
|
|
231
|
+
} else if (delta > this.#watchCanaryFrequency * 2) {
|
|
232
|
+
this.#queue.throw(new CompilerReset(`Workspace watch stopped responding ${delta}s ago`));
|
|
233
|
+
} else if (delta > this.#watchCanaryFrequency) {
|
|
234
234
|
log.error('Restarting parcel due to inactivity');
|
|
235
235
|
await this.#listenWorkspace();
|
|
236
236
|
} else {
|
|
237
237
|
await fs.utimes(full, new Date(), new Date());
|
|
238
238
|
}
|
|
239
|
-
}, this.#
|
|
239
|
+
}, this.#watchCanaryFrequency * 1000);
|
|
240
240
|
|
|
241
241
|
this.#cleanup.canary = (): void => clearInterval(canaryId);
|
|
242
242
|
}
|
|
@@ -247,6 +247,6 @@ export class CompilerWatcher {
|
|
|
247
247
|
this.#listenToolFolder();
|
|
248
248
|
this.#listenCanary();
|
|
249
249
|
}
|
|
250
|
-
return this.#
|
|
250
|
+
return this.#queue[Symbol.asyncIterator]();
|
|
251
251
|
}
|
|
252
252
|
}
|
package/support/entry.main.ts
CHANGED
|
@@ -19,8 +19,8 @@ export const main = (ctx: ManifestContext) => {
|
|
|
19
19
|
Log.initLevel('error');
|
|
20
20
|
|
|
21
21
|
/** Main entry point for compilation */
|
|
22
|
-
const compile = async (
|
|
23
|
-
const server = await new CompilerServer(ctx,
|
|
22
|
+
const compile = async (operation: CompilerMode, setupOnly = false): Promise<void> => {
|
|
23
|
+
const server = await new CompilerServer(ctx, operation).listen();
|
|
24
24
|
const log = Log.scoped('main');
|
|
25
25
|
|
|
26
26
|
// Wait for build to be ready
|
|
@@ -29,20 +29,20 @@ export const main = (ctx: ManifestContext) => {
|
|
|
29
29
|
await server.processEvents(async function* (signal) {
|
|
30
30
|
const changed = await CompilerSetup.setup(ctx);
|
|
31
31
|
if (!setupOnly) {
|
|
32
|
-
yield* CompilerRunner.runProcess(ctx, changed,
|
|
32
|
+
yield* CompilerRunner.runProcess(ctx, changed, operation, signal);
|
|
33
33
|
}
|
|
34
34
|
});
|
|
35
35
|
log.debug('End Server');
|
|
36
36
|
} else {
|
|
37
37
|
log.info('Server already running, waiting for initial compile to complete');
|
|
38
|
-
const
|
|
39
|
-
Log.consumeProgressEvents(() => client.fetchEvents('progress', { until:
|
|
38
|
+
const controller = new AbortController();
|
|
39
|
+
Log.consumeProgressEvents(() => client.fetchEvents('progress', { until: event => !!event.complete, signal: controller.signal }));
|
|
40
40
|
await client.waitForState(['compile-end', 'watch-start'], 'Successfully built');
|
|
41
|
-
|
|
41
|
+
controller.abort();
|
|
42
42
|
}
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
const
|
|
45
|
+
const operations = {
|
|
46
46
|
/** Stop the server */
|
|
47
47
|
async stop(): Promise<void> {
|
|
48
48
|
if (await client.stop()) {
|
|
@@ -53,7 +53,7 @@ export const main = (ctx: ManifestContext) => {
|
|
|
53
53
|
},
|
|
54
54
|
|
|
55
55
|
/** Restart the server */
|
|
56
|
-
async restart(): Promise<void> { await client.stop().then(() =>
|
|
56
|
+
async restart(): Promise<void> { await client.stop().then(() => operations.watch()); },
|
|
57
57
|
|
|
58
58
|
/** Get server info */
|
|
59
59
|
info: (): Promise<CompilerServerInfo | undefined> => client.info(),
|
|
@@ -64,17 +64,17 @@ export const main = (ctx: ManifestContext) => {
|
|
|
64
64
|
return console.log(`Clean triggered ${ctx.workspace.path}:`, buildFolders);
|
|
65
65
|
} else {
|
|
66
66
|
try {
|
|
67
|
-
await Promise.all(buildFolders.map(
|
|
67
|
+
await Promise.all(buildFolders.map(file => fs.rm(CommonUtil.resolveWorkspace(ctx, file), { force: true, recursive: true })));
|
|
68
68
|
} catch { }
|
|
69
69
|
return console.log(`Cleaned ${ctx.workspace.path}:`, buildFolders);
|
|
70
70
|
}
|
|
71
71
|
},
|
|
72
72
|
|
|
73
73
|
/** Stream events */
|
|
74
|
-
events: async (type: string, handler: (
|
|
74
|
+
events: async (type: string, handler: (event: unknown) => unknown): Promise<void> => {
|
|
75
75
|
if (type === 'change' || type === 'log' || type === 'progress' || type === 'state' || type === 'all') {
|
|
76
76
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
77
|
-
for await (const
|
|
77
|
+
for await (const event of client.fetchEvents(type as 'change')) { await handler(event); }
|
|
78
78
|
} else {
|
|
79
79
|
throw new Error(`Unknown event type: ${type}`);
|
|
80
80
|
}
|
|
@@ -113,7 +113,7 @@ export const main = (ctx: ManifestContext) => {
|
|
|
113
113
|
await CompilerSetup.exportManifest(ctx, output, prod); return;
|
|
114
114
|
}
|
|
115
115
|
};
|
|
116
|
-
return
|
|
116
|
+
return operations;
|
|
117
117
|
};
|
|
118
118
|
|
|
119
119
|
export type Operations = ReturnType<typeof main>;
|
package/support/log.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CompilerLogEvent, CompilerLogLevel, CompilerProgressEvent } from './types.ts';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const LEVEL_TO_PRIORITY: Record<CompilerLogLevel | 'none', number> = { debug: 1, info: 2, warn: 3, error: 4, none: 5 };
|
|
4
4
|
const SCOPE_MAX = 15;
|
|
5
5
|
|
|
6
6
|
type LogConfig = {
|
|
@@ -31,7 +31,7 @@ export class Logger implements LogConfig, LogShape {
|
|
|
31
31
|
const done = process.stdout.write(`${ESC}1G${text}${ESC}0K`);
|
|
32
32
|
this.#linePartial = !!text;
|
|
33
33
|
if (!done) {
|
|
34
|
-
return new Promise<void>(
|
|
34
|
+
return new Promise<void>(resolve => process.stdout.once('drain', resolve));
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -42,25 +42,27 @@ export class Logger implements LogConfig, LogShape {
|
|
|
42
42
|
scope?: string;
|
|
43
43
|
parent?: Logger;
|
|
44
44
|
|
|
45
|
-
constructor(
|
|
46
|
-
Object.assign(this,
|
|
45
|
+
constructor(config: LogConfig = {}) {
|
|
46
|
+
Object.assign(this, config);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
valid(
|
|
50
|
-
return
|
|
49
|
+
valid(event: CompilerLogEvent): boolean {
|
|
50
|
+
return LEVEL_TO_PRIORITY[this.level ?? this.parent?.level!] <= LEVEL_TO_PRIORITY[event.level];
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/** Log event with filtering by level */
|
|
54
|
-
render(
|
|
55
|
-
if (!this.valid(
|
|
56
|
-
const params = [
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
render(event: CompilerLogEvent): void {
|
|
55
|
+
if (!this.valid(event)) { return; }
|
|
56
|
+
const params = [event.message, ...event.args ?? []]
|
|
57
|
+
.map(arg => typeof arg === 'string' ? arg.replaceAll(this.root ?? this.parent?.root!, '.') : arg);
|
|
58
|
+
|
|
59
|
+
if (event.scope ?? this.scope) {
|
|
60
|
+
params.unshift(`[${(event.scope ?? this.scope!).padEnd(SCOPE_MAX, ' ')}]`);
|
|
59
61
|
}
|
|
60
|
-
params.unshift(new Date().toISOString(), `${
|
|
62
|
+
params.unshift(new Date().toISOString(), `${event.level.padEnd(5)}`);
|
|
61
63
|
Logger.rewriteLine(''); // Clear out progress line, if active
|
|
62
64
|
// eslint-disable-next-line no-console
|
|
63
|
-
console[
|
|
65
|
+
console[event.level]!(...params);
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
info(message: string, ...args: unknown[]): void { return this.render({ level: 'info', message, args }); }
|
|
@@ -82,9 +84,9 @@ class $RootLogger extends Logger {
|
|
|
82
84
|
|
|
83
85
|
/** Set level for operation */
|
|
84
86
|
initLevel(defaultLevel: CompilerLogLevel | 'none'): void {
|
|
85
|
-
const
|
|
86
|
-
switch (
|
|
87
|
-
case 'debug': case 'warn': case 'error': case 'info': this.level =
|
|
87
|
+
const value = process.env.TRV_QUIET !== 'true' ? process.env.TRV_BUILD : 'none';
|
|
88
|
+
switch (value) {
|
|
89
|
+
case 'debug': case 'warn': case 'error': case 'info': this.level = value; break;
|
|
88
90
|
case undefined: this.level = defaultLevel; break;
|
|
89
91
|
case 'none': default: this.level = 'none';
|
|
90
92
|
}
|
|
@@ -96,23 +98,23 @@ class $RootLogger extends Logger {
|
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
/** Scope and provide a callback pattern for access to a logger */
|
|
99
|
-
wrap<T = unknown>(scope: string,
|
|
100
|
-
const
|
|
101
|
-
return basic ? (
|
|
101
|
+
wrap<T = unknown>(scope: string, operation: (log: Logger) => Promise<T>, basic = true): Promise<T> {
|
|
102
|
+
const logger = this.scoped(scope);
|
|
103
|
+
return basic ? (logger.debug('Started'), operation(logger).finally(() => logger.debug('Completed'))) : operation(logger);
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
/** Write progress event, if active */
|
|
105
|
-
onProgressEvent(
|
|
107
|
+
onProgressEvent(event: CompilerProgressEvent): void | Promise<void> {
|
|
106
108
|
if (!(this.logProgress)) { return; }
|
|
107
|
-
const
|
|
108
|
-
const text =
|
|
109
|
+
const progress = Math.trunc(event.idx * 100 / event.total);
|
|
110
|
+
const text = event.complete ? '' : `Compiling [${'#'.repeat(Math.trunc(progress / 10)).padEnd(10, ' ')}] [${event.idx}/${event.total}] ${event.message}`;
|
|
109
111
|
return Logger.rewriteLine(text);
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
/** Write all progress events if active */
|
|
113
|
-
async consumeProgressEvents(
|
|
115
|
+
async consumeProgressEvents(input: () => AsyncIterable<CompilerProgressEvent>): Promise<void> {
|
|
114
116
|
if (!(this.logProgress)) { return; }
|
|
115
|
-
for await (const
|
|
117
|
+
for await (const event of input()) { this.onProgressEvent(event); }
|
|
116
118
|
Logger.reset();
|
|
117
119
|
}
|
|
118
120
|
}
|
|
@@ -120,13 +122,13 @@ class $RootLogger extends Logger {
|
|
|
120
122
|
export const Log = new $RootLogger();
|
|
121
123
|
|
|
122
124
|
export class IpcLogger extends Logger {
|
|
123
|
-
render(
|
|
124
|
-
if (!this.valid(
|
|
125
|
+
render(event: CompilerLogEvent): void {
|
|
126
|
+
if (!this.valid(event)) { return; }
|
|
125
127
|
if (process.connected && process.send) {
|
|
126
|
-
process.send({ type: 'log', payload:
|
|
128
|
+
process.send({ type: 'log', payload: event });
|
|
127
129
|
}
|
|
128
130
|
if (!process.connected) {
|
|
129
|
-
super.render(
|
|
131
|
+
super.render(event);
|
|
130
132
|
}
|
|
131
133
|
}
|
|
132
134
|
}
|
package/support/queue.ts
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
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
1
|
export class AsyncQueue<X> implements AsyncIterator<X>, AsyncIterable<X> {
|
|
10
2
|
#queue: X[] = [];
|
|
11
3
|
#done = false;
|
|
12
|
-
#ready =
|
|
4
|
+
#ready = Promise.withResolvers<void>();
|
|
13
5
|
|
|
14
6
|
constructor(signal?: AbortSignal) {
|
|
15
7
|
signal?.addEventListener('abort', () => this.close());
|
|
@@ -22,15 +14,15 @@ export class AsyncQueue<X> implements AsyncIterator<X>, AsyncIterable<X> {
|
|
|
22
14
|
|
|
23
15
|
async next(): Promise<IteratorResult<X>> {
|
|
24
16
|
while (!this.#done && !this.#queue.length) {
|
|
25
|
-
await this.#ready;
|
|
26
|
-
this.#ready =
|
|
17
|
+
await this.#ready.promise;
|
|
18
|
+
this.#ready = Promise.withResolvers<void>();
|
|
27
19
|
}
|
|
28
20
|
return { value: (this.#queue.length ? this.#queue.shift() : undefined)!, done: this.#done };
|
|
29
21
|
}
|
|
30
22
|
|
|
31
|
-
async throw(
|
|
23
|
+
async throw(error?: Error): Promise<IteratorResult<X>> {
|
|
32
24
|
this.#done = true;
|
|
33
|
-
this.#ready.reject(
|
|
25
|
+
this.#ready.reject(error);
|
|
34
26
|
return { value: undefined, done: this.#done };
|
|
35
27
|
}
|
|
36
28
|
|
package/support/server/client.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { ProcessHandle } from './process-handle.ts';
|
|
|
11
11
|
|
|
12
12
|
type FetchEventsConfig<T> = {
|
|
13
13
|
signal?: AbortSignal;
|
|
14
|
-
until?: (
|
|
14
|
+
until?: (event: T) => boolean;
|
|
15
15
|
enforceIteration?: boolean;
|
|
16
16
|
};
|
|
17
17
|
|
|
@@ -44,26 +44,26 @@ export class CompilerClient {
|
|
|
44
44
|
return this.#url;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
async #fetch(
|
|
48
|
-
const
|
|
49
|
-
const
|
|
47
|
+
async #fetch(urlPath: string, options?: RequestInit & { timeout?: number }, logTimeout = true): Promise<{ ok: boolean, text: string }> {
|
|
48
|
+
const controller = new AbortController();
|
|
49
|
+
const timeoutController = new AbortController();
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
timers.setTimeout(
|
|
51
|
+
options?.signal?.addEventListener('abort', () => controller.abort());
|
|
52
|
+
timers.setTimeout(options?.timeout ?? 100, undefined, { ref: false, signal: timeoutController.signal })
|
|
53
53
|
.then(() => {
|
|
54
|
-
logTimeout && this.#log.error(`Timeout on request to ${this.#url}${
|
|
55
|
-
|
|
54
|
+
logTimeout && this.#log.error(`Timeout on request to ${this.#url}${urlPath}`);
|
|
55
|
+
controller.abort('TIMEOUT');
|
|
56
56
|
})
|
|
57
57
|
.catch(() => { });
|
|
58
|
-
const response = await fetch(`${this.#url}${
|
|
58
|
+
const response = await fetch(`${this.#url}${urlPath}`, { ...options, signal: controller.signal });
|
|
59
59
|
const out = { ok: response.ok, text: await response.text() };
|
|
60
|
-
|
|
60
|
+
timeoutController.abort();
|
|
61
61
|
return out;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/** Get server information, if server is running */
|
|
65
65
|
info(): Promise<CompilerServerInfo | undefined> {
|
|
66
|
-
return this.#fetch('/info', { timeout: 200 }, false).then(
|
|
66
|
+
return this.#fetch('/info', { timeout: 200 }, false).then(response => JSON.parse(response.text), () => undefined);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
async isWatching(): Promise<boolean> {
|
|
@@ -72,7 +72,7 @@ export class CompilerClient {
|
|
|
72
72
|
|
|
73
73
|
/** Clean the server */
|
|
74
74
|
clean(): Promise<boolean> {
|
|
75
|
-
return this.#fetch('/clean', { timeout: 300 }).then(
|
|
75
|
+
return this.#fetch('/clean', { timeout: 300 }).then(response => response.ok, () => false);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/** Stop server and wait for shutdown */
|
|
@@ -81,7 +81,7 @@ export class CompilerClient {
|
|
|
81
81
|
if (!info) {
|
|
82
82
|
this.#log.debug('Stopping server, info not found, manual killing');
|
|
83
83
|
return Promise.all([this.#handle.server.kill(), this.#handle.compiler.kill()])
|
|
84
|
-
.then(
|
|
84
|
+
.then(results => results.some(result => result));
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
await this.#fetch('/stop').catch(() => { }); // Trigger
|
|
@@ -94,9 +94,9 @@ export class CompilerClient {
|
|
|
94
94
|
fetchEvents<
|
|
95
95
|
V extends CompilerEventType,
|
|
96
96
|
T extends (CompilerEvent & { type: V })['payload']
|
|
97
|
-
>(type: V,
|
|
98
|
-
fetchEvents(type: 'all',
|
|
99
|
-
async * fetchEvents<T = unknown>(type: string,
|
|
97
|
+
>(type: V, config?: FetchEventsConfig<T>): AsyncIterable<T>;
|
|
98
|
+
fetchEvents(type: 'all', config?: FetchEventsConfig<CompilerEvent>): AsyncIterable<CompilerEvent>;
|
|
99
|
+
async * fetchEvents<T = unknown>(type: string, config: FetchEventsConfig<T> = {}): AsyncIterable<T> {
|
|
100
100
|
let info = await this.info();
|
|
101
101
|
if (!info) {
|
|
102
102
|
return;
|
|
@@ -104,40 +104,40 @@ export class CompilerClient {
|
|
|
104
104
|
|
|
105
105
|
this.#log.debug(`Starting watch for events of type "${type}"`);
|
|
106
106
|
|
|
107
|
-
let signal =
|
|
107
|
+
let signal = config.signal;
|
|
108
108
|
|
|
109
109
|
// Ensure we capture end of process at least
|
|
110
110
|
if (!signal) {
|
|
111
|
-
const
|
|
112
|
-
process.on('SIGINT', () =>
|
|
113
|
-
signal =
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
process.on('SIGINT', () => controller.abort());
|
|
113
|
+
signal = controller.signal;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
const { iteration } = info;
|
|
117
117
|
|
|
118
118
|
for (; ;) {
|
|
119
|
-
const
|
|
120
|
-
const quit = (): void =>
|
|
119
|
+
const controller = new AbortController();
|
|
120
|
+
const quit = (): void => controller.abort();
|
|
121
121
|
try {
|
|
122
122
|
signal.addEventListener('abort', quit);
|
|
123
123
|
const response = await new Promise<http.IncomingMessage>((resolve, reject) =>
|
|
124
|
-
http.get(`${this.#url}/event/${type}`, { agent: streamAgent, signal:
|
|
124
|
+
http.get(`${this.#url}/event/${type}`, { agent: streamAgent, signal: controller.signal }, resolve)
|
|
125
125
|
.on('error', reject)
|
|
126
126
|
);
|
|
127
127
|
|
|
128
128
|
for await (const line of rl.createInterface(response)) {
|
|
129
129
|
if (line.trim().charAt(0) === '{') {
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
130
|
+
const event: T = JSON.parse(line);
|
|
131
|
+
if (config.until?.(event)) {
|
|
132
132
|
await CommonUtil.queueMacroTask();
|
|
133
|
-
|
|
133
|
+
controller.abort();
|
|
134
134
|
}
|
|
135
|
-
yield
|
|
135
|
+
yield event;
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
-
} catch (
|
|
139
|
-
const aborted =
|
|
140
|
-
if (!aborted) { throw
|
|
138
|
+
} catch (error) {
|
|
139
|
+
const aborted = controller.signal.aborted || (typeof error === 'object' && error && 'code' in error && error.code === 'ECONNRESET');
|
|
140
|
+
if (!aborted) { throw error; }
|
|
141
141
|
}
|
|
142
142
|
signal.removeEventListener('abort', quit);
|
|
143
143
|
|
|
@@ -145,12 +145,12 @@ export class CompilerClient {
|
|
|
145
145
|
|
|
146
146
|
info = await this.info();
|
|
147
147
|
|
|
148
|
-
if (
|
|
148
|
+
if (controller.signal.reason === 'TIMEOUT') {
|
|
149
149
|
this.#log.debug('Failed due to timeout');
|
|
150
150
|
return;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
if (
|
|
153
|
+
if (controller.signal.aborted || !info || (config.enforceIteration && info.iteration !== iteration)) { // If health check fails, or aborted
|
|
154
154
|
this.#log.debug(`Stopping watch for events of type "${type}"`);
|
|
155
155
|
return;
|
|
156
156
|
} else {
|
|
@@ -164,7 +164,7 @@ export class CompilerClient {
|
|
|
164
164
|
const set = new Set(states);
|
|
165
165
|
// Loop until
|
|
166
166
|
this.#log.debug(`Waiting for states, ${states.join(', ')}`);
|
|
167
|
-
for await (const _ of this.fetchEvents('state', { signal, until:
|
|
167
|
+
for await (const _ of this.fetchEvents('state', { signal, until: event => set.has(event.state) })) { }
|
|
168
168
|
this.#log.debug(`Found state, one of ${states.join(', ')} `);
|
|
169
169
|
if (message) {
|
|
170
170
|
this.#log.info(message);
|