@travetto/compiler 3.0.0-rc.9 → 3.0.0
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 +68 -14
- package/__index__.ts +4 -1
- package/bin/trv.js +57 -63
- package/package.json +8 -6
- package/src/compiler.ts +74 -113
- package/src/log.ts +18 -0
- package/src/state.ts +160 -166
- package/src/types.ts +14 -0
- package/src/util.ts +32 -87
- package/src/watch.ts +142 -0
- package/support/compiler-entry.ts +2 -0
- package/support/launcher.ts +152 -0
- package/support/lock-pinger.ts +25 -0
- package/support/lock.ts +234 -0
- package/support/log.ts +51 -0
- package/support/transpile.ts +210 -0
- package/tsconfig.trv.json +1 -0
- package/bin/transpile.d.ts +0 -32
- package/bin/transpile.js +0 -227
- package/support/bin/compiler-bootstrap.ts +0 -151
- package/support/bin/utils.ts +0 -116
- package/support/main.output.ts +0 -11
package/src/watch.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ManifestContext, ManifestModuleUtil, ManifestUtil, WatchEvent, ManifestModuleFolderType,
|
|
6
|
+
ManifestModuleFileType, path, ManifestModule, watchFolders, WatchEventListener
|
|
7
|
+
} from '@travetto/manifest';
|
|
8
|
+
import { getManifestContext } from '@travetto/manifest/bin/context';
|
|
9
|
+
|
|
10
|
+
import { CompilerState } from './state';
|
|
11
|
+
import { CompilerUtil } from './util';
|
|
12
|
+
import { CompileEmitter, CompileWatcherHandler } from './types';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Utils for watching
|
|
16
|
+
*/
|
|
17
|
+
export class CompilerWatcher {
|
|
18
|
+
|
|
19
|
+
#sourceHashes = new Map<string, number>();
|
|
20
|
+
#manifestContexts = new Map<string, ManifestContext>();
|
|
21
|
+
#dirtyFiles: { modFolder: string, mod: string, moduleFile?: string, folderKey?: ManifestModuleFolderType, type?: ManifestModuleFileType }[] = [];
|
|
22
|
+
#state: CompilerState;
|
|
23
|
+
|
|
24
|
+
constructor(state: CompilerState) {
|
|
25
|
+
this.#state = state;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async #rebuildManifestsIfNeeded(): Promise<void> {
|
|
29
|
+
if (!this.#dirtyFiles.length) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const mods = [...new Set(this.#dirtyFiles.map(x => x.modFolder))];
|
|
33
|
+
const contexts = await Promise.all(mods.map(async folder => {
|
|
34
|
+
if (!this.#manifestContexts.has(folder)) {
|
|
35
|
+
const ctx = await getManifestContext(folder);
|
|
36
|
+
this.#manifestContexts.set(folder, ctx);
|
|
37
|
+
}
|
|
38
|
+
return this.#manifestContexts.get(folder)!;
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
const files = this.#dirtyFiles;
|
|
42
|
+
this.#dirtyFiles = [];
|
|
43
|
+
|
|
44
|
+
for (const ctx of [...contexts, this.#state.manifest]) {
|
|
45
|
+
const newManifest = await ManifestUtil.buildManifest(ctx);
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
if (file.folderKey && file.moduleFile && file.type && file.mod in newManifest.modules) {
|
|
48
|
+
newManifest.modules[file.mod].files[file.folderKey] ??= [];
|
|
49
|
+
newManifest.modules[file.mod].files[file.folderKey]!.push([file.moduleFile, file.type, Date.now()]);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
await ManifestUtil.writeManifest(ctx, newManifest);
|
|
53
|
+
}
|
|
54
|
+
// Reindex
|
|
55
|
+
this.#state.manifestIndex.init(this.#state.manifestIndex.manifestFile);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#getModuleMap(): Record<string, ManifestModule> {
|
|
59
|
+
return Object.fromEntries(
|
|
60
|
+
Object.values(this.#state.manifest.modules).map(x => [path.resolve(this.#state.manifest.workspacePath, x.sourceFolder), x])
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get a watcher for a given compiler state
|
|
66
|
+
* @param state
|
|
67
|
+
* @param handler
|
|
68
|
+
* @returns
|
|
69
|
+
*/
|
|
70
|
+
#getWatcher(handler: CompileWatcherHandler): WatchEventListener {
|
|
71
|
+
const mods = this.#getModuleMap();
|
|
72
|
+
|
|
73
|
+
return async ({ file: sourceFile, action }: WatchEvent, folder: string): Promise<void> => {
|
|
74
|
+
const mod = mods[folder];
|
|
75
|
+
const moduleFile = mod.sourceFolder ?
|
|
76
|
+
(sourceFile.includes(mod.sourceFolder) ? sourceFile.split(`${mod.sourceFolder}/`)[1] : sourceFile) :
|
|
77
|
+
sourceFile.replace(`${this.#state.manifest.workspacePath}/`, '');
|
|
78
|
+
switch (action) {
|
|
79
|
+
case 'create': {
|
|
80
|
+
const fileType = ManifestModuleUtil.getFileType(moduleFile);
|
|
81
|
+
this.#dirtyFiles.push({
|
|
82
|
+
mod: mod.name,
|
|
83
|
+
modFolder: folder,
|
|
84
|
+
moduleFile,
|
|
85
|
+
folderKey: ManifestModuleUtil.getFolderKey(moduleFile),
|
|
86
|
+
type: ManifestModuleUtil.getFileType(moduleFile)
|
|
87
|
+
});
|
|
88
|
+
if (CompilerUtil.validFile(fileType)) {
|
|
89
|
+
await this.#rebuildManifestsIfNeeded();
|
|
90
|
+
|
|
91
|
+
const hash = CompilerUtil.naiveHash(readFileSync(sourceFile, 'utf8'));
|
|
92
|
+
const input = this.#state.registerInput(mod, moduleFile);
|
|
93
|
+
this.#sourceHashes.set(sourceFile, hash);
|
|
94
|
+
handler.create(input);
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case 'update': {
|
|
99
|
+
await this.#rebuildManifestsIfNeeded();
|
|
100
|
+
const entry = this.#state.getBySource(sourceFile);
|
|
101
|
+
if (entry) {
|
|
102
|
+
const hash = CompilerUtil.naiveHash(readFileSync(sourceFile, 'utf8'));
|
|
103
|
+
if (this.#sourceHashes.get(sourceFile) !== hash) {
|
|
104
|
+
this.#state.resetInputSource(entry.input);
|
|
105
|
+
this.#sourceHashes.set(sourceFile, hash);
|
|
106
|
+
handler.update(entry.input);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
case 'delete': {
|
|
112
|
+
const entry = this.#state.getBySource(sourceFile);
|
|
113
|
+
if (entry) {
|
|
114
|
+
this.#state.removeInput(entry.input);
|
|
115
|
+
if (entry.output) {
|
|
116
|
+
this.#dirtyFiles.push({ mod: mod.name, modFolder: folder });
|
|
117
|
+
handler.delete(entry.output);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Watch files based on root index
|
|
127
|
+
*/
|
|
128
|
+
watchFiles(emit: CompileEmitter): Promise<() => Promise<void>> {
|
|
129
|
+
return watchFolders(
|
|
130
|
+
this.#state.manifestIndex.getLocalInputFolderMapping(),
|
|
131
|
+
this.#getWatcher({
|
|
132
|
+
create: emit,
|
|
133
|
+
update: emit,
|
|
134
|
+
delete: (outputFile) => fs.rm(outputFile, { force: true })
|
|
135
|
+
}),
|
|
136
|
+
{
|
|
137
|
+
filter: ev => ev.file.endsWith('.ts') || ev.file.endsWith('.js') || ev.file.endsWith('package.json'),
|
|
138
|
+
ignore: ['node_modules']
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
import type { ManifestContext } from '@travetto/manifest';
|
|
5
|
+
|
|
6
|
+
import { TranspileUtil, CompileResult } from './transpile';
|
|
7
|
+
import { LockManager } from './lock';
|
|
8
|
+
import { LogUtil } from './log';
|
|
9
|
+
|
|
10
|
+
const SOURCE_SEED = ['package.json', 'index.ts', '__index__.ts', 'src', 'support', 'bin'];
|
|
11
|
+
const PRECOMPILE_MODS = ['@travetto/terminal', '@travetto/manifest', '@travetto/transformer', '@travetto/compiler'];
|
|
12
|
+
|
|
13
|
+
const importManifest = (ctx: ManifestContext): Promise<typeof import('@travetto/manifest')> =>
|
|
14
|
+
import(path.resolve(ctx.workspacePath, ctx.compilerFolder, 'node_modules', '@travetto/manifest/__index__.js'));
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Run the compiler
|
|
18
|
+
*/
|
|
19
|
+
async function compile(ctx: ManifestContext, op: 'watch' | 'build' | undefined, onMessage: (msg: unknown) => void): Promise<CompileResult> {
|
|
20
|
+
let changes = 0;
|
|
21
|
+
|
|
22
|
+
await LogUtil.withLogger('precompile', async () => {
|
|
23
|
+
for (const mod of PRECOMPILE_MODS) {
|
|
24
|
+
const count = (await TranspileUtil.compileIfStale(ctx, 'precompile', mod, SOURCE_SEED)).length;
|
|
25
|
+
if (mod !== '@travetto/terminal') {
|
|
26
|
+
changes += count;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const { ManifestUtil, ManifestDeltaUtil, PackageUtil } = await importManifest(ctx);
|
|
32
|
+
|
|
33
|
+
PackageUtil.clearCache();
|
|
34
|
+
|
|
35
|
+
const manifest = await LogUtil.withLogger('manifest', async () => ManifestUtil.buildManifest(ctx));
|
|
36
|
+
|
|
37
|
+
await LogUtil.withLogger('transformers', async () => {
|
|
38
|
+
for (const mod of Object.values(manifest.modules).filter(m => m.files.$transformer?.length)) {
|
|
39
|
+
changes += (await TranspileUtil.compileIfStale(ctx, 'transformers', mod.name, ['package.json', ...mod.files.$transformer!.map(x => x[0])])).length;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const delta = await LogUtil.withLogger('delta', async log => {
|
|
44
|
+
if (changes) {
|
|
45
|
+
log('debug', 'Skipping, everything changed');
|
|
46
|
+
return [{ type: 'changed', file: '*', module: ctx.mainModule } as const];
|
|
47
|
+
} else {
|
|
48
|
+
return ManifestDeltaUtil.produceDelta(ctx, manifest);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (changes) {
|
|
53
|
+
await LogUtil.withLogger('reset', async log => {
|
|
54
|
+
await fs.rm(path.resolve(ctx.workspacePath, ctx.outputFolder), { recursive: true, force: true });
|
|
55
|
+
log('info', 'Clearing output due to compiler changes');
|
|
56
|
+
}, false);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Write manifest
|
|
60
|
+
await LogUtil.withLogger('manifest', async log => {
|
|
61
|
+
await ManifestUtil.writeManifest(ctx, manifest);
|
|
62
|
+
log('debug', `Wrote manifest ${ctx.mainModule}`);
|
|
63
|
+
|
|
64
|
+
// Update all manifests
|
|
65
|
+
if (delta.length && ctx.monoRepo && !ctx.mainFolder) {
|
|
66
|
+
const names: string[] = [];
|
|
67
|
+
const mods = Object.values(manifest.modules).filter(x => x.local && x.name !== ctx.mainModule);
|
|
68
|
+
for (const mod of mods) {
|
|
69
|
+
await ManifestUtil.rewriteManifest(path.resolve(ctx.workspacePath, mod.sourceFolder));
|
|
70
|
+
names.push(mod.name);
|
|
71
|
+
}
|
|
72
|
+
log('debug', `Changes triggered ${delta.slice(0, 10).map(x => `${x.type}:${x.module}:${x.file}`)}`);
|
|
73
|
+
log('debug', `Rewrote monorepo manifests [changes=${delta.length}] ${names.slice(0, 10).join(', ')}`);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return await LogUtil.withLogger('compile', async log => {
|
|
78
|
+
const changed = delta.filter(x => x.type === 'added' || x.type === 'changed');
|
|
79
|
+
log('debug', `Started action=${op} changed=${changed.slice(0, 10).map(x => `${x.module}/${x.file}`)}`);
|
|
80
|
+
if (changed.length || op === 'watch') {
|
|
81
|
+
const res = await TranspileUtil.runCompiler(ctx, manifest, changed, op === 'watch', onMessage);
|
|
82
|
+
log('debug', 'Finished');
|
|
83
|
+
return res;
|
|
84
|
+
} else {
|
|
85
|
+
log('debug', 'Skipped');
|
|
86
|
+
return 'skipped';
|
|
87
|
+
}
|
|
88
|
+
}, false);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Export manifests
|
|
93
|
+
*/
|
|
94
|
+
async function exportManifest(ctx: ManifestContext, output?: string, env = 'dev'): Promise<void> {
|
|
95
|
+
const { ManifestUtil } = await importManifest(ctx);
|
|
96
|
+
let manifest = await ManifestUtil.buildManifest(ctx);
|
|
97
|
+
|
|
98
|
+
// If in prod mode, only include std modules
|
|
99
|
+
if (/^prod/i.test(env)) {
|
|
100
|
+
manifest = ManifestUtil.createProductionManifest(manifest);
|
|
101
|
+
}
|
|
102
|
+
if (output) {
|
|
103
|
+
output = await ManifestUtil.writeManifestToFile(output, manifest);
|
|
104
|
+
LogUtil.log('manifest', [], 'info', `Wrote manifest ${output}`);
|
|
105
|
+
} else {
|
|
106
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Launch
|
|
112
|
+
*/
|
|
113
|
+
export async function launch(ctx: ManifestContext, root: ManifestContext, op?: 'build' | 'watch' | 'manifest', args: (string | undefined)[] = []): Promise<void> {
|
|
114
|
+
// If quiet enabled, turn off all output by default
|
|
115
|
+
LogUtil.level = process.env.TRV_BUILD ?? (process.env.TRV_QUIET ? 'none' : (!op ? 'warn' : 'info'));
|
|
116
|
+
|
|
117
|
+
if (op !== 'manifest' && await LockManager.getCompileAction(root, op) === 'build') {
|
|
118
|
+
|
|
119
|
+
// Ready signal
|
|
120
|
+
process.send?.('ready');
|
|
121
|
+
|
|
122
|
+
await LockManager.withLocks(root, async (acquire, release) => {
|
|
123
|
+
let action: CompileResult;
|
|
124
|
+
do {
|
|
125
|
+
acquire(op ?? 'build');
|
|
126
|
+
if (op === 'watch') {
|
|
127
|
+
acquire('build');
|
|
128
|
+
}
|
|
129
|
+
action = await compile(root, op, msg => {
|
|
130
|
+
switch (msg) {
|
|
131
|
+
case 'build-complete': {
|
|
132
|
+
release('build');
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
} while (action === 'restart');
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
switch (op) {
|
|
142
|
+
case 'manifest': return exportManifest(ctx, ...args);
|
|
143
|
+
case 'build': return LogUtil.log('build', [], 'info', 'Successfully built');
|
|
144
|
+
case undefined: {
|
|
145
|
+
// TODO: Externalize somehow?
|
|
146
|
+
const outputPath = path.resolve(ctx.workspacePath, ctx.outputFolder);
|
|
147
|
+
process.env.TRV_MANIFEST = path.resolve(outputPath, 'node_modules', ctx.mainModule);
|
|
148
|
+
const cliMain = path.join(outputPath, 'node_modules', '@travetto/cli/support/cli.js');
|
|
149
|
+
return import(cliMain);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { workerData, parentPort } from 'worker_threads';
|
|
2
|
+
import { utimesSync } from 'fs';
|
|
3
|
+
|
|
4
|
+
const data: { files: string[], interval: number } = workerData;
|
|
5
|
+
const files = data.files;
|
|
6
|
+
|
|
7
|
+
const interval = setInterval(() => {
|
|
8
|
+
const now = Date.now() / 1000;
|
|
9
|
+
for (const file of files) {
|
|
10
|
+
try {
|
|
11
|
+
utimesSync(file, now, now);
|
|
12
|
+
} catch { }
|
|
13
|
+
}
|
|
14
|
+
}, data.interval);
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
parentPort?.on('message', val => {
|
|
18
|
+
if (val === 'stop') {
|
|
19
|
+
files.splice(0, files.length);
|
|
20
|
+
clearInterval(interval);
|
|
21
|
+
} else if (val && typeof val === 'object' && 'files' in val && Array.isArray(val.files)) {
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
23
|
+
files.splice(0, files.length, ...val.files as string[]);
|
|
24
|
+
}
|
|
25
|
+
});
|
package/support/lock.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { Stats, watchFile, unwatchFile, rmSync, mkdirSync, writeFileSync, existsSync } from 'fs';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import { Worker } from 'worker_threads';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
import type { ManifestContext } from '@travetto/manifest';
|
|
7
|
+
|
|
8
|
+
import { CompilerLogger, LogUtil } from './log';
|
|
9
|
+
|
|
10
|
+
type LockStatus = 'complete' | 'stale';
|
|
11
|
+
type LockDetails = {
|
|
12
|
+
pid: number | undefined;
|
|
13
|
+
file: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type LockType = 'build' | 'watch';
|
|
17
|
+
export type LockCompileAction = 'skip' | 'build';
|
|
18
|
+
type LockAction = LockCompileAction | 'retry';
|
|
19
|
+
|
|
20
|
+
const STALE_THRESHOLD = 1000;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Manager for all lock activity
|
|
24
|
+
*/
|
|
25
|
+
export class LockManager {
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get the lock file name
|
|
29
|
+
*/
|
|
30
|
+
static #getFileName(ctx: ManifestContext, type: LockType): string {
|
|
31
|
+
return path.resolve(ctx.workspacePath, ctx.toolFolder, `${type}.lock`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Determine if the given stats are stale for modification time
|
|
36
|
+
*/
|
|
37
|
+
static #isStale(stat?: Stats): boolean {
|
|
38
|
+
return !!stat && stat.mtimeMs < (Date.now() - STALE_THRESHOLD * 1.1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get the lock file details
|
|
43
|
+
*/
|
|
44
|
+
static async #getDetails(ctx: ManifestContext, type: LockType): Promise<LockDetails> {
|
|
45
|
+
const file = this.#getFileName(ctx, type);
|
|
46
|
+
const stat = await fs.stat(file).catch(() => undefined);
|
|
47
|
+
const stale = this.#isStale(stat);
|
|
48
|
+
let pid: number | undefined;
|
|
49
|
+
if (stat) {
|
|
50
|
+
const { pid: filePid } = JSON.parse(await fs.readFile(file, 'utf8'));
|
|
51
|
+
if (stale) {
|
|
52
|
+
LogUtil.log('lock', [], 'debug', `${type} file is stale: ${stat.mtimeMs} vs ${Date.now()}`);
|
|
53
|
+
} else {
|
|
54
|
+
pid = filePid;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { pid, file };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Acquire the lock file, and register a cleanup on exit
|
|
62
|
+
*/
|
|
63
|
+
static #acquireFile(ctx: ManifestContext, type: LockType): void {
|
|
64
|
+
const file = this.#getFileName(ctx, type);
|
|
65
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
66
|
+
LogUtil.log('lock', [], 'debug', `Acquiring ${type}`);
|
|
67
|
+
writeFileSync(file, JSON.stringify({ pid: process.pid }), 'utf8');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Release the lock file (i.e. deleting)
|
|
72
|
+
*/
|
|
73
|
+
static #releaseFile(ctx: ManifestContext, type: LockType): void {
|
|
74
|
+
const file = this.#getFileName(ctx, type);
|
|
75
|
+
if (existsSync(file)) {
|
|
76
|
+
rmSync(file, { force: true });
|
|
77
|
+
LogUtil.log('lock', [], 'debug', `Releasing ${type}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Wait until a lock file is released, or it becomes stale
|
|
83
|
+
*/
|
|
84
|
+
static async #waitForRelease(ctx: ManifestContext, type: LockType): Promise<LockStatus> {
|
|
85
|
+
const file = this.#getFileName(ctx, type);
|
|
86
|
+
let remove: (() => void) | undefined = undefined;
|
|
87
|
+
|
|
88
|
+
const prom = new Promise<LockStatus>(resolve => {
|
|
89
|
+
let timer: NodeJS.Timeout | undefined = undefined;
|
|
90
|
+
const handler = async (): Promise<void> => {
|
|
91
|
+
if (timer) {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
}
|
|
94
|
+
const stats = await fs.stat(file).catch(() => undefined);
|
|
95
|
+
if (!stats) {
|
|
96
|
+
resolve('complete');
|
|
97
|
+
} else if (this.#isStale(stats)) {
|
|
98
|
+
resolve('stale');
|
|
99
|
+
} else {
|
|
100
|
+
timer = setTimeout(handler, STALE_THRESHOLD * 1.1);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
watchFile(file, handler);
|
|
105
|
+
handler();
|
|
106
|
+
|
|
107
|
+
remove = (): void => {
|
|
108
|
+
clearInterval(timer);
|
|
109
|
+
unwatchFile(file, handler);
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return prom.finally(remove);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Read the watch lock file and determine its result, communicating with the user as necessary
|
|
118
|
+
*/
|
|
119
|
+
static async #getWatchAction(ctx: ManifestContext, log: CompilerLogger, lockType: LockType | undefined, buildState: LockDetails): Promise<LockAction> {
|
|
120
|
+
if (lockType === 'watch') {
|
|
121
|
+
log('info', 'Already running');
|
|
122
|
+
return 'skip';
|
|
123
|
+
} else {
|
|
124
|
+
if (buildState.pid) {
|
|
125
|
+
log('warn', 'Already running, waiting for build to finish');
|
|
126
|
+
switch (await this.#waitForRelease(ctx, 'build')) {
|
|
127
|
+
case 'complete': {
|
|
128
|
+
log('info', 'Completed build');
|
|
129
|
+
return 'skip';
|
|
130
|
+
}
|
|
131
|
+
case 'stale': {
|
|
132
|
+
log('info', 'Became stale, retrying');
|
|
133
|
+
return 'retry';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
log('info', 'Already running, and has built');
|
|
138
|
+
return 'skip';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Read the build lock file and determine its result, communicating with the user as necessary
|
|
145
|
+
*/
|
|
146
|
+
static async #getBuildAction(ctx: ManifestContext, log: CompilerLogger, lockType: LockType | undefined): Promise<LockAction> {
|
|
147
|
+
if (lockType === 'watch') {
|
|
148
|
+
log('warn', 'Build already running, waiting to begin watch');
|
|
149
|
+
const res = await this.#waitForRelease(ctx, 'build');
|
|
150
|
+
log('info', `Finished with status of ${res}, retrying`);
|
|
151
|
+
return 'retry';
|
|
152
|
+
} else {
|
|
153
|
+
log('warn', 'Already running, waiting for completion');
|
|
154
|
+
switch (await this.#waitForRelease(ctx, lockType ?? 'build')) {
|
|
155
|
+
case 'complete': {
|
|
156
|
+
log('info', 'Completed');
|
|
157
|
+
return 'skip';
|
|
158
|
+
}
|
|
159
|
+
case 'stale': {
|
|
160
|
+
log('info', 'Became stale, retrying');
|
|
161
|
+
return 'retry';
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Run code with support for lock acquire and release
|
|
169
|
+
*/
|
|
170
|
+
static async withLocks(ctx: ManifestContext, fn: (acquire: (type: LockType) => void, release: (type: LockType) => void) => Promise<unknown>): Promise<void> {
|
|
171
|
+
const activeLockTypes = new Set<LockType>();
|
|
172
|
+
|
|
173
|
+
const pinger = path.resolve(ctx.workspacePath, ctx.compilerFolder, 'node_modules', '@travetto/compiler/support/lock-pinger.js');
|
|
174
|
+
const worker = new Worker(pinger, {
|
|
175
|
+
workerData: {
|
|
176
|
+
interval: STALE_THRESHOLD,
|
|
177
|
+
files: []
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const notify = (): void => worker.postMessage({ files: [...activeLockTypes].map(t => this.#getFileName(ctx, t)) });
|
|
182
|
+
|
|
183
|
+
const stop = (): void => {
|
|
184
|
+
worker.postMessage('stop');
|
|
185
|
+
for (const type of activeLockTypes) {
|
|
186
|
+
this.#releaseFile(ctx, type);
|
|
187
|
+
}
|
|
188
|
+
worker.terminate().then(() => { });
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
process.on('SIGINT', stop);
|
|
192
|
+
process.on('exit', stop);
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
await new Promise(r => worker.on('online', r));
|
|
196
|
+
await fn(
|
|
197
|
+
type => {
|
|
198
|
+
if (!activeLockTypes.has(type)) {
|
|
199
|
+
activeLockTypes.add(type);
|
|
200
|
+
this.#acquireFile(ctx, type);
|
|
201
|
+
notify();
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
type => {
|
|
205
|
+
if (activeLockTypes.has(type)) {
|
|
206
|
+
activeLockTypes.delete(type);
|
|
207
|
+
this.#releaseFile(ctx, type);
|
|
208
|
+
notify();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
} finally {
|
|
213
|
+
stop();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Reads the lock file states (build + watch) to determine what action should be taken for the compiler
|
|
219
|
+
*/
|
|
220
|
+
static async getCompileAction(ctx: ManifestContext, lockType: LockType | undefined): Promise<LockCompileAction> {
|
|
221
|
+
let result: LockAction;
|
|
222
|
+
do {
|
|
223
|
+
result = 'build';
|
|
224
|
+
const buildState = await this.#getDetails(ctx, 'build');
|
|
225
|
+
const watchState = await this.#getDetails(ctx, 'watch');
|
|
226
|
+
if (watchState.pid) { // Existing watch operation
|
|
227
|
+
result = await LogUtil.withLogger('lock', log => this.#getWatchAction(ctx, log, lockType, buildState), true, ['watch', `pid=${watchState.pid}`]);
|
|
228
|
+
} else if (buildState.pid) { // Existing build operation
|
|
229
|
+
result = await LogUtil.withLogger('lock', log => this.#getBuildAction(ctx, log, lockType), true, ['build', `pid=${buildState.pid}`]);
|
|
230
|
+
}
|
|
231
|
+
} while (result === 'retry');
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
}
|
package/support/log.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type CompilerLogEvent = [level: 'info' | 'debug' | 'warn', message: string];
|
|
2
|
+
export type CompilerLogger = (...args: CompilerLogEvent) => void;
|
|
3
|
+
export type WithLogger<T> = (log: CompilerLogger) => Promise<T>;
|
|
4
|
+
|
|
5
|
+
const SCOPE_MAX = 15;
|
|
6
|
+
|
|
7
|
+
export class LogUtil {
|
|
8
|
+
|
|
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
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Is object a log event
|
|
25
|
+
*/
|
|
26
|
+
static isLogEvent = (o: unknown): o is CompilerLogEvent => o !== null && o !== undefined && Array.isArray(o);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Log message with filtering by level
|
|
30
|
+
*/
|
|
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
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* With logger
|
|
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);
|
|
48
|
+
basic && log('debug', 'Started');
|
|
49
|
+
return op(log).finally(() => basic && log('debug', 'Completed'));
|
|
50
|
+
}
|
|
51
|
+
}
|