@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.
- package/README.md +29 -29
- package/bin/common.js +41 -28
- package/bin/trvc.js +5 -6
- package/package.json +6 -6
- package/src/compiler.ts +68 -37
- package/src/event.ts +1 -1
- package/src/state.ts +39 -21
- package/src/types.ts +1 -1
- package/src/util.ts +4 -4
- package/src/watch.ts +101 -98
- package/support/entry.trvc.ts +47 -22
- package/support/log.ts +22 -24
- package/support/queue.ts +6 -0
- package/support/server/client.ts +59 -31
- package/support/server/process-handle.ts +57 -0
- package/support/server/runner.ts +16 -11
- package/support/server/server.ts +62 -45
- package/support/setup.ts +1 -1
- package/support/types.ts +1 -2
- package/support/util.ts +4 -2
- package/src/internal/watch-core.ts +0 -107
package/src/watch.ts
CHANGED
|
@@ -1,24 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
ManifestModuleUtil, ManifestUtil, ManifestModuleFolderType, ManifestModuleFileType, path, ManifestModule,
|
|
5
|
-
} from '@travetto/manifest';
|
|
1
|
+
import { ManifestContext, ManifestModuleUtil, ManifestUtil, RuntimeIndex, path } from '@travetto/manifest';
|
|
6
2
|
|
|
7
3
|
import type { CompileStateEntry } from './types';
|
|
8
4
|
import { CompilerState } from './state';
|
|
9
5
|
import { CompilerUtil } from './util';
|
|
10
6
|
|
|
11
|
-
import {
|
|
7
|
+
import { AsyncQueue } from '../support/queue';
|
|
12
8
|
|
|
13
|
-
type
|
|
9
|
+
type WatchAction = 'create' | 'update' | 'delete';
|
|
10
|
+
type WatchEvent = { action: WatchAction, file: string };
|
|
11
|
+
type CompilerWatchEvent = WatchEvent & { entry: CompileStateEntry };
|
|
14
12
|
|
|
15
13
|
/**
|
|
16
14
|
* Watch support, based on compiler state and manifest details
|
|
17
15
|
*/
|
|
18
16
|
export class CompilerWatcher {
|
|
19
|
-
|
|
20
|
-
#sourceHashes = new Map<string, number>();
|
|
21
|
-
#dirtyFiles: DirtyFile[] = [];
|
|
22
17
|
#state: CompilerState;
|
|
23
18
|
#signal: AbortSignal;
|
|
24
19
|
|
|
@@ -27,54 +22,75 @@ export class CompilerWatcher {
|
|
|
27
22
|
this.#signal = signal;
|
|
28
23
|
}
|
|
29
24
|
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
/** Watch files */
|
|
26
|
+
async * #watchFolder(rootPath: string): AsyncIterable<WatchEvent> {
|
|
27
|
+
const q = new AsyncQueue<WatchEvent>(this.#signal);
|
|
28
|
+
const lib = await import('@parcel/watcher');
|
|
29
|
+
|
|
30
|
+
const cleanup = await lib.subscribe(rootPath, (err, events) => {
|
|
31
|
+
if (err) {
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
33
|
+
q.throw(err instanceof Error ? err : new Error((err as Error).message));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
for (const ev of events) {
|
|
37
|
+
q.add({ action: ev.type, file: path.toPosix(ev.path) });
|
|
38
|
+
}
|
|
39
|
+
}, {
|
|
40
|
+
// TODO: Read .gitignore?
|
|
41
|
+
ignore: [
|
|
42
|
+
'node_modules', '**/node_modules', '.git', '**/.git',
|
|
43
|
+
`${this.#state.manifest.build.outputFolder}/node_modules/**`,
|
|
44
|
+
`${this.#state.manifest.build.compilerFolder}/node_modules/**`,
|
|
45
|
+
`${this.#state.manifest.build.toolFolder}/**`
|
|
46
|
+
]
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (this.#signal.aborted) { // If already aborted, can happen async
|
|
50
|
+
cleanup.unsubscribe();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.#signal.addEventListener('abort', () => cleanup.unsubscribe());
|
|
55
|
+
|
|
56
|
+
yield* q;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async #rebuildManifestsIfNeeded(event: CompilerWatchEvent, moduleFile: string): Promise<void> {
|
|
60
|
+
if (!event.entry.outputFile || event.action === 'update') {
|
|
32
61
|
return;
|
|
33
62
|
}
|
|
34
|
-
const mods = [...new Set(this.#dirtyFiles.map(x => x.modFolder))];
|
|
35
|
-
const contexts = await Promise.all(mods.map(folder =>
|
|
36
|
-
ManifestUtil.getModuleContext(this.#state.manifest, folder)
|
|
37
|
-
));
|
|
38
63
|
|
|
39
|
-
const
|
|
40
|
-
|
|
64
|
+
const toUpdate: ManifestContext[] = RuntimeIndex.getDependentModules(event.entry.module.name, 'parents')
|
|
65
|
+
.map(el => ManifestUtil.getModuleContext(this.#state.manifest, el.sourceFolder));
|
|
66
|
+
|
|
67
|
+
toUpdate.push(this.#state.manifest);
|
|
68
|
+
|
|
69
|
+
const mod = event.entry.module;
|
|
70
|
+
const folderKey = ManifestModuleUtil.getFolderKey(moduleFile);
|
|
71
|
+
const fileType = ManifestModuleUtil.getFileType(moduleFile);
|
|
41
72
|
|
|
42
|
-
for (const ctx of
|
|
73
|
+
for (const ctx of toUpdate) {
|
|
43
74
|
const newManifest = await ManifestUtil.buildManifest(ctx);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
modFiles[idx] = [file.moduleFile, file.type, Date.now()];
|
|
56
|
-
}
|
|
75
|
+
if (mod.name in newManifest.modules) {
|
|
76
|
+
const modFiles = newManifest.modules[mod.name].files[folderKey] ??= [];
|
|
77
|
+
const idx = modFiles.findIndex(x => x[0] === moduleFile);
|
|
78
|
+
|
|
79
|
+
if (event.action === 'create' && idx < 0) {
|
|
80
|
+
modFiles.push([moduleFile, fileType, Date.now()]);
|
|
81
|
+
} else if (idx >= 0) {
|
|
82
|
+
if (event.action === 'delete') {
|
|
83
|
+
modFiles.splice(idx, 1);
|
|
84
|
+
} else {
|
|
85
|
+
modFiles[idx] = [moduleFile, fileType, Date.now()];
|
|
57
86
|
}
|
|
58
87
|
}
|
|
59
88
|
}
|
|
60
89
|
await ManifestUtil.writeManifest(newManifest);
|
|
61
90
|
}
|
|
62
|
-
// Reindex
|
|
63
|
-
this.#state.manifestIndex.init(this.#state.manifestIndex.manifestFile);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
#getModuleMap(): Record<string, ManifestModule> {
|
|
67
|
-
return Object.fromEntries(
|
|
68
|
-
Object.values(this.#state.manifest.modules).map(x => [path.resolve(this.#state.manifest.workspace.path, x.sourceFolder), x])
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
91
|
|
|
72
|
-
|
|
73
|
-
this.#
|
|
74
|
-
mod: mod.name, modFolder: folder, remove, moduleFile,
|
|
75
|
-
folderKey: ManifestModuleUtil.getFolderKey(moduleFile),
|
|
76
|
-
type: ManifestModuleUtil.getFileType(moduleFile),
|
|
77
|
-
});
|
|
92
|
+
// Reindex at workspace root
|
|
93
|
+
this.#state.manifestIndex.init(ManifestUtil.getManifestLocation(this.#state.manifest));
|
|
78
94
|
}
|
|
79
95
|
|
|
80
96
|
/**
|
|
@@ -83,69 +99,56 @@ export class CompilerWatcher {
|
|
|
83
99
|
* @param handler
|
|
84
100
|
* @returns
|
|
85
101
|
*/
|
|
86
|
-
async * watchChanges(): AsyncIterable<
|
|
102
|
+
async * watchChanges(): AsyncIterable<CompilerWatchEvent> {
|
|
87
103
|
if (this.#signal.aborted) {
|
|
88
|
-
yield* [];
|
|
89
104
|
return;
|
|
90
105
|
}
|
|
91
106
|
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
107
|
+
const manifest = this.#state.manifest;
|
|
108
|
+
const ROOT_LOCK = path.resolve(manifest.workspace.path, 'package-lock.json');
|
|
109
|
+
const ROOT_PKG = path.resolve(manifest.workspace.path, 'package.json');
|
|
110
|
+
const OUTPUT_PATH = path.resolve(manifest.workspace.path, manifest.build.outputFolder);
|
|
111
|
+
const COMPILER_PATH = path.resolve(manifest.workspace.path, manifest.build.compilerFolder);
|
|
112
|
+
|
|
113
|
+
for await (const ev of this.#watchFolder(this.#state.manifest.workspace.path)) {
|
|
114
|
+
const { action, file: sourceFile } = ev;
|
|
115
|
+
|
|
116
|
+
if (
|
|
117
|
+
sourceFile === ROOT_LOCK ||
|
|
118
|
+
sourceFile === ROOT_PKG ||
|
|
119
|
+
(action === 'delete' && (sourceFile === OUTPUT_PATH || sourceFile === COMPILER_PATH))
|
|
120
|
+
) {
|
|
121
|
+
throw new Error('RESET');
|
|
102
122
|
}
|
|
103
123
|
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
sourceFile.replace(`${this.#state.manifest.workspace.path}/`, '');
|
|
124
|
+
const fileType = ManifestModuleUtil.getFileType(sourceFile);
|
|
125
|
+
if (!CompilerUtil.validFile(fileType)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
109
128
|
|
|
110
129
|
let entry = this.#state.getBySource(sourceFile);
|
|
111
130
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
this.#addDirtyFile(mod, folder, moduleFile);
|
|
116
|
-
if (CompilerUtil.validFile(fileType)) {
|
|
117
|
-
const hash = CompilerUtil.naiveHash(readFileSync(sourceFile, 'utf8'));
|
|
118
|
-
entry = this.#state.registerInput(mod, moduleFile);
|
|
119
|
-
this.#sourceHashes.set(sourceFile, hash);
|
|
120
|
-
}
|
|
121
|
-
break;
|
|
122
|
-
}
|
|
123
|
-
case 'update': {
|
|
124
|
-
if (entry) {
|
|
125
|
-
const hash = CompilerUtil.naiveHash(readFileSync(sourceFile, 'utf8'));
|
|
126
|
-
if (this.#sourceHashes.get(sourceFile) !== hash) {
|
|
127
|
-
this.#state.resetInputSource(entry.input);
|
|
128
|
-
this.#sourceHashes.set(sourceFile, hash);
|
|
129
|
-
} else {
|
|
130
|
-
entry = undefined;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
135
|
-
case 'delete': {
|
|
136
|
-
if (entry) {
|
|
137
|
-
this.#state.removeInput(entry.input);
|
|
138
|
-
if (entry.output) {
|
|
139
|
-
this.#addDirtyFile(mod, folder, moduleFile, true);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
131
|
+
const mod = entry?.module ?? this.#state.manifestIndex.findModuleForArbitraryFile(sourceFile);
|
|
132
|
+
if (!mod) { // Unknown module
|
|
133
|
+
continue;
|
|
143
134
|
}
|
|
144
135
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
136
|
+
const modRoot = mod.sourceFolder || this.#state.manifest.workspace.path;
|
|
137
|
+
const moduleFile = sourceFile.includes(modRoot) ? sourceFile.split(`${modRoot}/`)[1] : sourceFile;
|
|
138
|
+
|
|
139
|
+
if (action === 'create') {
|
|
140
|
+
entry = this.#state.registerInput(mod, moduleFile);
|
|
141
|
+
} else if (!entry) {
|
|
142
|
+
continue;
|
|
143
|
+
} else if (action === 'update' && !this.#state.checkIfSourceChanged(entry.inputFile)) {
|
|
144
|
+
continue;
|
|
145
|
+
} else if (action === 'delete') {
|
|
146
|
+
this.#state.removeInput(entry.inputFile);
|
|
148
147
|
}
|
|
148
|
+
|
|
149
|
+
const result: CompilerWatchEvent = { action, file: entry.sourceFile, entry };
|
|
150
|
+
await this.#rebuildManifestsIfNeeded(result, moduleFile);
|
|
151
|
+
yield result;
|
|
149
152
|
}
|
|
150
153
|
}
|
|
151
154
|
}
|
package/support/entry.trvc.ts
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
import type { ManifestContext } from '@travetto/manifest';
|
|
5
5
|
|
|
6
|
-
import type {
|
|
6
|
+
import type { CompilerLogLevel, CompilerMode, CompilerServerInfo } from './types';
|
|
7
7
|
import { LogUtil } from './log';
|
|
8
8
|
import { CommonUtil } from './util';
|
|
9
9
|
import { CompilerSetup } from './setup';
|
|
@@ -13,9 +13,35 @@ import { CompilerClient } from './server/client';
|
|
|
13
13
|
|
|
14
14
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
15
15
|
export const main = (ctx: ManifestContext) => {
|
|
16
|
-
const
|
|
16
|
+
const log = LogUtil.logger('client.main');
|
|
17
|
+
const client = new CompilerClient(ctx, log);
|
|
17
18
|
const buildFolders = [ctx.build.outputFolder, ctx.build.compilerFolder];
|
|
18
19
|
|
|
20
|
+
/** Main entry point for compilation */
|
|
21
|
+
const compile = async (op: CompilerMode, logLevel: CompilerLogLevel, setupOnly = false): Promise<void> => {
|
|
22
|
+
LogUtil.initLogs(ctx, logLevel ?? 'info');
|
|
23
|
+
|
|
24
|
+
const server = await new CompilerServer(ctx, op).listen();
|
|
25
|
+
|
|
26
|
+
// Wait for build to be ready
|
|
27
|
+
if (server) {
|
|
28
|
+
log('debug', 'Start Server');
|
|
29
|
+
await server.processEvents(async function* (signal) {
|
|
30
|
+
const changed = await CompilerSetup.setup(ctx);
|
|
31
|
+
if (!setupOnly) {
|
|
32
|
+
yield* CompilerRunner.runProcess(ctx, changed, op, signal);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
log('debug', 'End Server');
|
|
36
|
+
} else {
|
|
37
|
+
log('info', 'Server already running, waiting for initial compile to complete');
|
|
38
|
+
const ctrl = new AbortController();
|
|
39
|
+
LogUtil.consumeProgressEvents(() => client.fetchEvents('progress', { until: ev => !!ev.complete, signal: ctrl.signal }));
|
|
40
|
+
await client.waitForState(['compile-end', 'watch-start'], 'Successfully built');
|
|
41
|
+
ctrl.abort();
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
19
45
|
const ops = {
|
|
20
46
|
/** Stop the server */
|
|
21
47
|
async stop(): Promise<void> {
|
|
@@ -26,6 +52,9 @@ export const main = (ctx: ManifestContext) => {
|
|
|
26
52
|
}
|
|
27
53
|
},
|
|
28
54
|
|
|
55
|
+
/** Restart the server */
|
|
56
|
+
async restart(): Promise<void> { await client.stop().then(() => ops.watch()); },
|
|
57
|
+
|
|
29
58
|
/** Get server info */
|
|
30
59
|
info: (): Promise<CompilerServerInfo | undefined> => client.info(),
|
|
31
60
|
|
|
@@ -40,37 +69,33 @@ export const main = (ctx: ManifestContext) => {
|
|
|
40
69
|
},
|
|
41
70
|
|
|
42
71
|
/** Stream events */
|
|
43
|
-
events: async (type:
|
|
72
|
+
events: async (type: string, handler: (ev: unknown) => unknown): Promise<void> => {
|
|
44
73
|
LogUtil.initLogs(ctx, 'error');
|
|
45
|
-
|
|
74
|
+
if (type === 'change' || type === 'log' || type === 'progress' || type === 'state') {
|
|
75
|
+
for await (const ev of client.fetchEvents(type)) { await handler(ev); }
|
|
76
|
+
} else {
|
|
77
|
+
throw new Error(`Unknown event type: ${type}`);
|
|
78
|
+
}
|
|
46
79
|
},
|
|
47
80
|
|
|
48
|
-
/**
|
|
49
|
-
async
|
|
50
|
-
LogUtil.initLogs(ctx, op === 'run' ? 'error' : 'info');
|
|
81
|
+
/** Build the project */
|
|
82
|
+
async build(): Promise<void> { await compile('build', 'info'); },
|
|
51
83
|
|
|
52
|
-
|
|
84
|
+
/** Build and watch the project */
|
|
85
|
+
async watch(): Promise<void> { await compile('watch', 'info'); },
|
|
53
86
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
yield* CompilerRunner.runProcess(ctx, changed, op, signal);
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
} else {
|
|
63
|
-
const ctrl = new AbortController();
|
|
64
|
-
LogUtil.consumeProgressEvents(() => client.fetchEvents('progress', { until: ev => !!ev.complete, signal: ctrl.signal }));
|
|
65
|
-
await client.waitForState(['compile-end', 'watch-start'], 'Successfully built');
|
|
66
|
-
ctrl.abort();
|
|
87
|
+
/** Build and return a loader */
|
|
88
|
+
async getLoader(): Promise<(mod: string) => Promise<unknown>> {
|
|
89
|
+
// Short circuit if we can
|
|
90
|
+
if (!(await client.isWatching())) {
|
|
91
|
+
await compile('build', 'error');
|
|
67
92
|
}
|
|
68
93
|
return CommonUtil.moduleLoader(ctx);
|
|
69
94
|
},
|
|
70
95
|
|
|
71
96
|
/** Manifest entry point */
|
|
72
97
|
async manifest(output?: string, prod?: boolean): Promise<void> {
|
|
73
|
-
await
|
|
98
|
+
await compile('build', 'error', true);
|
|
74
99
|
await CompilerSetup.exportManifest(ctx, output, prod); return;
|
|
75
100
|
}
|
|
76
101
|
};
|
package/support/log.ts
CHANGED
|
@@ -18,6 +18,18 @@ export class LogUtil {
|
|
|
18
18
|
|
|
19
19
|
static logProgress?: ProgressWriter;
|
|
20
20
|
|
|
21
|
+
static linePartial = false;
|
|
22
|
+
|
|
23
|
+
static #rewriteLine(text: string): Promise<void> | void {
|
|
24
|
+
// Move to 1st position, and clear after text
|
|
25
|
+
const done = process.stdout.write(`\x1b[1G${text}\x1b[0K`);
|
|
26
|
+
this.linePartial = !!text;
|
|
27
|
+
if (!done) {
|
|
28
|
+
return new Promise<void>(r => process.stdout.once('drain', r));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
21
33
|
/**
|
|
22
34
|
* Set level for operation
|
|
23
35
|
*/
|
|
@@ -28,20 +40,13 @@ export class LogUtil {
|
|
|
28
40
|
this.logLevel = build || defaultLevel;
|
|
29
41
|
}
|
|
30
42
|
this.root = ctx.workspace.path;
|
|
31
|
-
|
|
32
|
-
if (this.isLevelActive('info') && process.stdout.isTTY) {
|
|
33
|
-
this.logProgress = this.#logProgressEvent;
|
|
34
|
-
}
|
|
43
|
+
this.logProgress = (this.isLevelActive('info') && process.stdout.isTTY) ? this.#logProgressEvent : undefined;
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
static #logProgressEvent(ev: CompilerProgressEvent): Promise<void> | void {
|
|
38
47
|
const pct = Math.trunc(ev.idx * 100 / ev.total);
|
|
39
48
|
const text = ev.complete ? '' : `Compiling [${'#'.repeat(Math.trunc(pct / 10)).padEnd(10, ' ')}] [${ev.idx}/${ev.total}] ${ev.message}`;
|
|
40
|
-
|
|
41
|
-
const done = process.stdout.write(`\x1b[1G${text}\x1b[0K`);
|
|
42
|
-
if (!done) {
|
|
43
|
-
return new Promise<void>(r => process.stdout.once('drain', r));
|
|
44
|
-
}
|
|
49
|
+
return this.#rewriteLine(text);
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
/**
|
|
@@ -52,18 +57,18 @@ export class LogUtil {
|
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
/**
|
|
55
|
-
* Log
|
|
60
|
+
* Log event with filtering by level
|
|
56
61
|
*/
|
|
57
|
-
static
|
|
58
|
-
static log(scope: string, ...args: Parameters<CompilerLogger>): void;
|
|
59
|
-
static log(scopeOrEvent: string | CompilerLogEvent, level?: CompilerLogLevel, message?: string, ...args: unknown[]): void {
|
|
60
|
-
const ev = typeof scopeOrEvent === 'string' ? { scope: scopeOrEvent, level: level!, message, args } : scopeOrEvent;
|
|
62
|
+
static logEvent(ev: CompilerLogEvent): void {
|
|
61
63
|
if (this.isLevelActive(ev.level)) {
|
|
62
64
|
const params = [ev.message, ...ev.args ?? []].map(x => typeof x === 'string' ? x.replaceAll(this.root, '.') : x);
|
|
63
65
|
if (ev.scope) {
|
|
64
66
|
params.unshift(`[${ev.scope.padEnd(SCOPE_MAX, ' ')}]`);
|
|
65
67
|
}
|
|
66
68
|
params.unshift(new Date().toISOString(), `${ev.level.padEnd(5)}`);
|
|
69
|
+
if (this.linePartial) {
|
|
70
|
+
this.#rewriteLine(''); // Clear out progress line
|
|
71
|
+
}
|
|
67
72
|
// eslint-disable-next-line no-console
|
|
68
73
|
console[ev.level]!(...params);
|
|
69
74
|
}
|
|
@@ -73,7 +78,7 @@ export class LogUtil {
|
|
|
73
78
|
* With logger
|
|
74
79
|
*/
|
|
75
80
|
static withLogger<T>(scope: string, op: WithLogger<T>, basic = true): Promise<T> {
|
|
76
|
-
const log = this.
|
|
81
|
+
const log = this.logger(scope);
|
|
77
82
|
basic && log('debug', 'Started');
|
|
78
83
|
return op(log).finally(() => basic && log('debug', 'Completed'));
|
|
79
84
|
}
|
|
@@ -81,15 +86,8 @@ export class LogUtil {
|
|
|
81
86
|
/**
|
|
82
87
|
* With scope
|
|
83
88
|
*/
|
|
84
|
-
static
|
|
85
|
-
return this.
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Stream Compiler log events to console
|
|
90
|
-
*/
|
|
91
|
-
static async consumeLogEvents(src: AsyncIterable<CompilerLogEvent>): Promise<void> {
|
|
92
|
-
for await (const ev of src) { this.log(ev); }
|
|
89
|
+
static logger(scope: string): CompilerLogger {
|
|
90
|
+
return (level, message, ...args) => this.logEvent({ scope, message, level, args, time: Date.now() });
|
|
93
91
|
}
|
|
94
92
|
|
|
95
93
|
/**
|
package/support/queue.ts
CHANGED
|
@@ -28,6 +28,12 @@ export class AsyncQueue<X> implements AsyncIterator<X>, AsyncIterable<X> {
|
|
|
28
28
|
return { value: (this.#queue.length ? this.#queue.shift() : undefined)!, done: this.#done };
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
async throw(e?: Error): Promise<IteratorResult<X>> {
|
|
32
|
+
this.#done = true;
|
|
33
|
+
this.#ready.reject(e);
|
|
34
|
+
return { value: undefined, done: this.#done };
|
|
35
|
+
}
|
|
36
|
+
|
|
31
37
|
add(item: X): void {
|
|
32
38
|
this.#queue.push(item);
|
|
33
39
|
this.#ready.resolve();
|
package/support/server/client.ts
CHANGED
|
@@ -6,10 +6,7 @@ import { ManifestContext } from '@travetto/manifest';
|
|
|
6
6
|
|
|
7
7
|
import type { CompilerEvent, CompilerEventType, CompilerServerInfo, CompilerStateType } from '../types';
|
|
8
8
|
import type { CompilerLogger } from '../log';
|
|
9
|
-
|
|
10
|
-
declare global {
|
|
11
|
-
interface RequestInit { timeout?: number }
|
|
12
|
-
}
|
|
9
|
+
import { ProcessHandle } from './process-handle';
|
|
13
10
|
|
|
14
11
|
type FetchEventsConfig<T> = {
|
|
15
12
|
signal?: AbortSignal;
|
|
@@ -23,11 +20,13 @@ type FetchEventsConfig<T> = {
|
|
|
23
20
|
export class CompilerClient {
|
|
24
21
|
|
|
25
22
|
#url: string;
|
|
26
|
-
#log
|
|
23
|
+
#log: CompilerLogger;
|
|
24
|
+
#handle: Record<'compiler' | 'server', ProcessHandle>;
|
|
27
25
|
|
|
28
|
-
constructor(ctx: ManifestContext, log
|
|
26
|
+
constructor(ctx: ManifestContext, log: CompilerLogger) {
|
|
29
27
|
this.#url = ctx.build.compilerUrl.replace('localhost', '127.0.0.1');
|
|
30
28
|
this.#log = log;
|
|
29
|
+
this.#handle = { compiler: new ProcessHandle(ctx, 'compiler'), server: new ProcessHandle(ctx, 'server') };
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
toString(): string {
|
|
@@ -38,20 +37,49 @@ export class CompilerClient {
|
|
|
38
37
|
return this.#url;
|
|
39
38
|
}
|
|
40
39
|
|
|
40
|
+
async #fetch(rel: string, opts?: RequestInit & { timeout?: number }): Promise<Response> {
|
|
41
|
+
const ctrl = new AbortController();
|
|
42
|
+
opts?.signal?.addEventListener('abort', () => ctrl.abort());
|
|
43
|
+
const timeoutId = setTimeout(() => {
|
|
44
|
+
this.#log('error', `Timeout on request to ${this.#url}${rel}`);
|
|
45
|
+
ctrl.abort('TIMEOUT');
|
|
46
|
+
}, 100).unref();
|
|
47
|
+
try {
|
|
48
|
+
return await fetch(`${this.#url}${rel}`, { ...opts, signal: ctrl.signal });
|
|
49
|
+
} finally {
|
|
50
|
+
clearTimeout(timeoutId);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
41
54
|
/** Get server information, if server is running */
|
|
42
55
|
info(): Promise<CompilerServerInfo | undefined> {
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
return this.#fetch('/info').then(v => v.json(), () => undefined)
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
58
|
+
.then(v => v as CompilerServerInfo);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async isWatching(): Promise<boolean> {
|
|
62
|
+
return (await this.info())?.state === 'watch-start';
|
|
45
63
|
}
|
|
46
64
|
|
|
47
65
|
/** Clean the server */
|
|
48
66
|
clean(): Promise<boolean> {
|
|
49
|
-
return fetch(
|
|
67
|
+
return this.#fetch('/clean').then(v => v.ok, () => false);
|
|
50
68
|
}
|
|
51
69
|
|
|
52
|
-
/** Stop server */
|
|
53
|
-
stop(): Promise<boolean> {
|
|
54
|
-
|
|
70
|
+
/** Stop server and wait for shutdown */
|
|
71
|
+
async stop(): Promise<boolean> {
|
|
72
|
+
const info = await this.info();
|
|
73
|
+
if (!info) {
|
|
74
|
+
this.#log('debug', 'Stopping server, info not found, manual killing');
|
|
75
|
+
return Promise.all([this.#handle.server.kill(), this.#handle.compiler.kill()])
|
|
76
|
+
.then(v => v.some(x => x));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
await this.#fetch('/stop').catch(() => { }); // Trigger
|
|
80
|
+
this.#log('debug', 'Waiting for compiler to exit');
|
|
81
|
+
await this.#handle.compiler.ensureKilled();
|
|
82
|
+
return true;
|
|
55
83
|
}
|
|
56
84
|
|
|
57
85
|
/** Fetch compiler events */
|
|
@@ -64,7 +92,7 @@ export class CompilerClient {
|
|
|
64
92
|
return;
|
|
65
93
|
}
|
|
66
94
|
|
|
67
|
-
this.#log
|
|
95
|
+
this.#log('debug', `Starting watch for events of type "${type}"`);
|
|
68
96
|
|
|
69
97
|
let signal = cfg.signal;
|
|
70
98
|
|
|
@@ -75,17 +103,15 @@ export class CompilerClient {
|
|
|
75
103
|
signal = ctrl.signal;
|
|
76
104
|
}
|
|
77
105
|
|
|
78
|
-
|
|
79
106
|
const { iteration } = info;
|
|
80
107
|
|
|
81
108
|
for (; ;) {
|
|
82
109
|
const ctrl = new AbortController();
|
|
110
|
+
const quit = (): void => ctrl.abort();
|
|
83
111
|
try {
|
|
84
|
-
signal.addEventListener('abort',
|
|
85
|
-
const stream = await fetch(
|
|
86
|
-
|
|
87
|
-
timeout: 1000 * 60 * 60
|
|
88
|
-
});
|
|
112
|
+
signal.addEventListener('abort', quit);
|
|
113
|
+
const stream = await this.#fetch(`/event/${type}`, { signal: ctrl.signal, keepalive: true });
|
|
114
|
+
|
|
89
115
|
for await (const line of rl.createInterface(Readable.fromWeb(stream.body!))) {
|
|
90
116
|
if (line.trim().charAt(0) === '{') {
|
|
91
117
|
const val = JSON.parse(line);
|
|
@@ -96,17 +122,25 @@ export class CompilerClient {
|
|
|
96
122
|
yield val;
|
|
97
123
|
}
|
|
98
124
|
}
|
|
99
|
-
} catch (err) {
|
|
125
|
+
} catch (err) {
|
|
126
|
+
if (!ctrl.signal.aborted) { throw err; }
|
|
127
|
+
}
|
|
128
|
+
signal.removeEventListener('abort', quit);
|
|
100
129
|
|
|
101
130
|
await timers.setTimeout(1);
|
|
102
131
|
|
|
103
132
|
info = await this.info();
|
|
104
133
|
|
|
134
|
+
if (ctrl.signal.reason === 'TIMEOUT') {
|
|
135
|
+
this.#log('debug', 'Failed due to timeout');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
105
139
|
if (ctrl.signal.aborted || !info || (cfg.enforceIteration && info.iteration !== iteration)) { // If health check fails, or aborted
|
|
106
|
-
this.#log
|
|
140
|
+
this.#log('debug', `Stopping watch for events of type "${type}"`);
|
|
107
141
|
return;
|
|
108
142
|
} else {
|
|
109
|
-
this.#log
|
|
143
|
+
this.#log('debug', `Restarting watch for events of type "${type}"`);
|
|
110
144
|
}
|
|
111
145
|
}
|
|
112
146
|
}
|
|
@@ -114,18 +148,12 @@ export class CompilerClient {
|
|
|
114
148
|
/** Wait for one of N states to be achieved */
|
|
115
149
|
async waitForState(states: CompilerStateType[], message?: string, signal?: AbortSignal): Promise<void> {
|
|
116
150
|
const set = new Set(states);
|
|
117
|
-
const existing = await this.info();
|
|
118
|
-
this.#log?.('debug', `Existing: ${JSON.stringify(existing)}`);
|
|
119
|
-
if (existing && set.has(existing.state)) {
|
|
120
|
-
this.#log?.('debug', `Waited for state, ${existing.state} in server info`);
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
151
|
// Loop until
|
|
124
|
-
this.#log
|
|
152
|
+
this.#log('debug', `Waiting for states, ${states.join(', ')}`);
|
|
125
153
|
for await (const _ of this.fetchEvents('state', { signal, until: s => set.has(s.state) })) { }
|
|
126
|
-
this.#log
|
|
154
|
+
this.#log('debug', `Found state, one of ${states.join(', ')} `);
|
|
127
155
|
if (message) {
|
|
128
|
-
this.#log
|
|
156
|
+
this.#log('info', message);
|
|
129
157
|
}
|
|
130
158
|
}
|
|
131
159
|
}
|