@travetto/compiler 5.0.9 → 5.0.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/compiler",
3
- "version": "5.0.9",
3
+ "version": "5.0.11",
4
4
  "description": "The compiler infrastructure for the Travetto framework",
5
5
  "keywords": [
6
6
  "compiler",
@@ -38,7 +38,7 @@
38
38
  "@types/node": "^22.7.4"
39
39
  },
40
40
  "peerDependencies": {
41
- "@travetto/cli": "^5.0.7"
41
+ "@travetto/cli": "^5.0.8"
42
42
  },
43
43
  "peerDependenciesMeta": {
44
44
  "@travetto/cli": {
package/src/compiler.ts CHANGED
@@ -6,7 +6,7 @@ import { ManifestIndex, ManifestModuleUtil } from '@travetto/manifest';
6
6
  import { CompilerUtil } from './util';
7
7
  import { CompilerState } from './state';
8
8
  import { CompilerWatcher } from './watch';
9
- import { CompileEmitEvent, CompileEmitter } from './types';
9
+ import { CompileEmitEvent, CompileEmitter, CompilerReset } from './types';
10
10
  import { EventUtil } from './event';
11
11
 
12
12
  import { IpcLogger } from '../support/log';
@@ -74,7 +74,7 @@ export class Compiler {
74
74
  break;
75
75
  }
76
76
  case 'reset': {
77
- log.info('Triggering reset due to change in core files', err?.cause);
77
+ log.info('Reset due to', err?.message);
78
78
  EventUtil.sendEvent('state', { state: 'reset' });
79
79
  process.exitCode = 0;
80
80
  break;
@@ -164,7 +164,7 @@ export class Compiler {
164
164
 
165
165
  EventUtil.sendEvent('state', { state: 'watch-start' });
166
166
  try {
167
- for await (const ev of new CompilerWatcher(this.#state, this.#signal).watchChanges()) {
167
+ for await (const ev of new CompilerWatcher(this.#state, this.#signal)) {
168
168
  if (ev.action !== 'delete') {
169
169
  const err = await emitter(ev.entry.sourceFile, true);
170
170
  if (err) {
@@ -193,7 +193,7 @@ export class Compiler {
193
193
 
194
194
  } catch (err) {
195
195
  if (err instanceof Error) {
196
- this.#shutdown(err.message === 'RESET' ? 'reset' : 'error', err);
196
+ this.#shutdown(err instanceof CompilerReset ? 'reset' : 'error', err);
197
197
  }
198
198
  }
199
199
  }
package/src/types.ts CHANGED
@@ -6,3 +6,5 @@ export type CompileEmitError = Error | readonly ts.Diagnostic[];
6
6
  export type CompileEmitter = (file: string, newProgram?: boolean) => Promise<CompileEmitError | undefined>;
7
7
  export type CompileEmitEvent = { file: string, i: number, total: number, err?: CompileEmitError };
8
8
  export type CompileStateEntry = { sourceFile: string, tscOutputFile: string, outputFile?: string, module: ManifestModule };
9
+ export type CompilerWatchEvent = { action: 'create' | 'update' | 'delete', file: string, entry: CompileStateEntry };
10
+ export class CompilerReset extends Error { }
package/src/watch.ts CHANGED
@@ -1,8 +1,9 @@
1
- import os from 'node:os';
1
+ import fs from 'node:fs/promises';
2
+ import { watch } from 'node:fs';
2
3
 
3
- import { ManifestContext, type ManifestModuleFileType, type ManifestModuleFolderType, ManifestModuleUtil, ManifestUtil, PackageUtil, path } from '@travetto/manifest';
4
+ import { ManifestFileUtil, ManifestModuleUtil, ManifestUtil, PackageUtil, path } from '@travetto/manifest';
4
5
 
5
- import type { CompileStateEntry } from './types';
6
+ import { CompilerReset, type CompilerWatchEvent, type CompileStateEntry } from './types';
6
7
  import { CompilerState } from './state';
7
8
  import { CompilerUtil } from './util';
8
9
 
@@ -11,244 +12,221 @@ import { IpcLogger } from '../support/log';
11
12
 
12
13
  const log = new IpcLogger({ level: 'debug' });
13
14
 
14
- type WatchAction = 'create' | 'update' | 'delete';
15
- type WatchEvent = { action: WatchAction, file: string };
16
- type CompilerWatchEvent = WatchEvent & { entry: CompileStateEntry };
17
- type FileShape = {
18
- mod: string;
19
- folderKey: ManifestModuleFolderType;
20
- fileType: ManifestModuleFileType;
21
- moduleFile: string;
22
- action: WatchAction;
23
- };
24
-
25
- const DEFAULT_WRITE_LIMIT = 1000 * 60 * 5;
26
-
27
- /**
28
- * Watch support, based on compiler state and manifest details
29
- */
15
+ type CompilerWatchEventCandidate = Omit<CompilerWatchEvent, 'entry'> & { entry?: CompileStateEntry };
16
+
30
17
  export class CompilerWatcher {
31
18
  #state: CompilerState;
32
- #signal: AbortSignal;
19
+ #cleanup: Partial<Record<'tool' | 'workspace' | 'canary', () => (void | Promise<void>)>> = {};
20
+ #watchCanary: string = '.trv/canary.id';
21
+ #lastWorkspaceModified = Date.now();
22
+ #watchCanaryFreq = 5;
23
+ #root: string;
24
+ #q: AsyncQueue<CompilerWatchEvent>;
33
25
 
34
26
  constructor(state: CompilerState, signal: AbortSignal) {
35
27
  this.#state = state;
36
- this.#signal = signal;
28
+ this.#root = state.manifest.workspace.path;
29
+ this.#q = new AsyncQueue(signal);
30
+ signal.addEventListener('abort', () => Object.values(this.#cleanup).forEach(x => x()));
37
31
  }
38
32
 
39
- #watchDog(): { reset(file: string): void, close(): void } {
40
- let lastWrite = Date.now();
41
- let writeThreshold = DEFAULT_WRITE_LIMIT;
42
- log.info('Starting watchdog');
43
- const value = setInterval(() => {
44
- if (Date.now() > (lastWrite + writeThreshold)) {
45
- const delta = (Date.now() - lastWrite) / (1000 * 60);
46
- log.info(`Watch has not seen changes in ${Math.trunc(delta)}m`);
47
- writeThreshold += DEFAULT_WRITE_LIMIT;
48
- }
49
- }, DEFAULT_WRITE_LIMIT / 10);
50
- return {
51
- close: (): void => {
52
- log.info('Closing watchdog');
53
- clearInterval(value);
54
- },
55
- reset: (file: string): void => {
56
- log.debug('Resetting watchdog', file);
57
- lastWrite = Date.now();
58
- writeThreshold = DEFAULT_WRITE_LIMIT;
33
+ async #getWatchIgnores(): Promise<string[]> {
34
+ const pkg = PackageUtil.readPackage(this.#root);
35
+ const patterns = [
36
+ ...pkg?.travetto?.build?.watchIgnores ?? [],
37
+ '**/node_modules',
38
+ '.*/**/node_modules'
39
+ ];
40
+ const ignores = new Set(['node_modules', '.git']);
41
+ for (const item of patterns) {
42
+ if (item.includes('*')) {
43
+ for await (const sub of fs.glob(item, { cwd: this.#root })) {
44
+ if (sub.startsWith('node_modules')) {
45
+ continue;
46
+ } else if (sub.endsWith('/node_modules')) {
47
+ ignores.add(sub.split('/node_modules')[0]);
48
+ } else {
49
+ ignores.add(sub);
50
+ }
51
+ }
52
+ } else {
53
+ ignores.add(item);
59
54
  }
60
- };
61
- }
62
-
63
- #reset(ev: WatchEvent): never {
64
- throw new Error('RESET', { cause: `${ev.action}:${ev.file}` });
55
+ }
56
+ return [...ignores].sort().map(x => x.endsWith('/') ? x : `${x}/`);
65
57
  }
66
58
 
67
- #getIgnores(): string[] {
68
- // TODO: Read .gitignore?
69
- let ignores = PackageUtil.readPackage(this.#state.manifest.workspace.path)?.travetto?.build?.watchIgnores;
70
-
71
- if (!ignores) {
72
- ignores = ['node_modules/**'];
59
+ #toCandidateEvent(action: CompilerWatchEvent['action'], file: string): CompilerWatchEventCandidate {
60
+ let entry = this.#state.getBySource(file);
61
+ const mod = entry?.module ?? this.#state.manifestIndex.findModuleForArbitraryFile(file);
62
+ if (mod && action === 'create' && !entry) {
63
+ const modRoot = mod.sourceFolder || this.#root;
64
+ const moduleFile = file.includes(modRoot) ? file.split(`${modRoot}/`)[1] : file;
65
+ entry = this.#state.registerInput(mod, moduleFile);
73
66
  }
74
-
75
- // TODO: Fix once node/parcel sort this out
76
- return os.platform() === 'linux' ? [] : [
77
- ...ignores,
78
- '.git', '**/.git',
79
- `${this.#state.manifest.build.outputFolder}/node_modules/**`,
80
- `${this.#state.manifest.build.compilerFolder}/node_modules/**`,
81
- `${this.#state.manifest.build.toolFolder}/**`
82
- ];
67
+ return { entry, file: entry?.sourceFile ?? file, action };
83
68
  }
84
69
 
85
- /** Watch files */
86
- async * #watchFolder(rootPath: string): AsyncIterable<WatchEvent[]> {
87
- const q = new AsyncQueue<WatchEvent[]>(this.#signal);
88
- const lib = await import('@parcel/watcher');
89
- const ignore = this.#getIgnores();
90
-
91
- const cleanup = await lib.subscribe(rootPath, (err, events) => {
92
- if (err) {
93
- q.throw(err instanceof Error ? err : new Error(`${err}`));
94
- return;
95
- }
96
- q.add(events.map(ev => ({ action: ev.type, file: path.toPosix(ev.path) })));
97
- }, { ignore });
98
-
99
- if (this.#signal.aborted) { // If already aborted, can happen async
100
- cleanup.unsubscribe();
101
- return;
70
+ #isValidEvent(ev: CompilerWatchEventCandidate): ev is CompilerWatchEvent {
71
+ const relativeFile = ev.file.replace(`${this.#root}/`, '');
72
+ if (relativeFile === this.#watchCanary) {
73
+ return false;
74
+ } else if (relativeFile.startsWith('.')) {
75
+ return false;
76
+ } else if (!ev.entry) {
77
+ log.debug(`Skipping unknown file ${relativeFile}`);
78
+ return false;
79
+ } else if (ev.action === 'update' && !this.#state.checkIfSourceChanged(ev.entry.sourceFile)) {
80
+ log.debug(`Skipping update, as contents unchanged ${relativeFile}`);
81
+ return false;
82
+ } else if (!CompilerUtil.validFile(ManifestModuleUtil.getFileType(relativeFile))) {
83
+ return false;
102
84
  }
103
-
104
- this.#signal.addEventListener('abort', () => cleanup.unsubscribe());
105
-
106
- yield* q;
85
+ return true;
107
86
  }
108
87
 
109
- async #rebuildManifestsIfNeeded(events: CompilerWatchEvent[]): Promise<void> {
110
- events = events.filter(x => x.entry.outputFile && x.action !== 'update');
111
-
112
- if (!events.length) {
88
+ async #reconcileAddRemove(compilerEvents: CompilerWatchEvent[]): Promise<void> {
89
+ const nonUpdates = compilerEvents.filter(x => x.entry.outputFile && x.action !== 'update');
90
+ if (!nonUpdates.length) {
113
91
  return;
114
92
  }
115
93
 
116
- const mods = [...new Set(events.map(v => v.entry.module.name))];
117
-
118
- const parents = new Map<string, string[]>(
119
- mods.map(m => [m, this.#state.manifestIndex.getDependentModules(m, 'parents').map(x => x.name)])
120
- );
121
-
122
- const moduleToFiles = new Map<string, { context: ManifestContext, files: FileShape[] }>(
123
- [...mods, ...parents.values()].flat().map(m => [m, {
124
- context: ManifestUtil.getModuleContext(this.#state.manifest, this.#state.manifestIndex.getManifestModule(m)!.sourceFolder),
125
- files: []
126
- }])
127
- );
128
-
129
- const allFiles = events.map(ev => {
130
- const modRoot = ev.entry.module.sourceFolder || this.#state.manifest.workspace.path;
131
- const moduleFile = ev.file.includes(modRoot) ? ev.file.split(`${modRoot}/`)[1] : ev.file;
132
- const folderKey = ManifestModuleUtil.getFolderKey(moduleFile);
133
- const fileType = ManifestModuleUtil.getFileType(moduleFile);
134
- return { mod: ev.entry.module.name, action: ev.action, moduleFile, folderKey, fileType };
135
- });
94
+ try {
95
+ const eventsByMod = new Map<string, CompilerWatchEvent[]>();
136
96
 
137
- for (const file of allFiles) {
138
- for (const parent of parents.get(file.mod)!) {
139
- const mod = moduleToFiles.get(parent);
140
- if (!mod || !mod.files) {
141
- this.#reset({ action: file.action, file: `${file.mod}/${file.moduleFile}` });
97
+ for (const ev of nonUpdates) {
98
+ const mod = ev.entry.module;
99
+ if (ev.action === 'delete') {
100
+ this.#state.removeSource(ev.entry.sourceFile);
101
+ }
102
+ for (const m of [mod, ...this.#state.manifestIndex.getDependentModules(mod.name, 'parents')]) {
103
+ if (!eventsByMod.has(m.name)) {
104
+ eventsByMod.set(m.name, []);
105
+ }
106
+ eventsByMod.get(m.name)!.push(ev);
142
107
  }
143
- mod.files.push(file);
144
108
  }
145
- }
146
109
 
147
- for (const { context, files } of moduleToFiles.values()) {
148
- const newManifest = await ManifestUtil.buildManifest(context);
149
- for (const { action, mod, fileType, moduleFile, folderKey } of files) {
150
- const modFiles = newManifest.modules[mod].files[folderKey] ??= [];
151
- const idx = modFiles.findIndex(x => x[0] === moduleFile);
152
-
153
- if (action === 'create' && idx < 0) {
154
- modFiles.push([moduleFile, fileType, Date.now()]);
155
- } else if (idx >= 0) {
156
- if (action === 'delete') {
157
- modFiles.splice(idx, 1);
158
- } else {
159
- modFiles[idx] = [moduleFile, fileType, Date.now()];
110
+ for (const [mod, events] of eventsByMod.entries()) {
111
+ const modRoot = this.#state.manifestIndex.getManifestModule(mod)!.sourceFolder;
112
+ const context = ManifestUtil.getModuleContext(this.#state.manifest, modRoot);
113
+ const newManifest = ManifestUtil.readManifestSync(ManifestUtil.getManifestLocation(context));
114
+ log.debug('Updating manifest', { module: mod });
115
+ for (const { action, file } of events) {
116
+ const resolvedRoot = modRoot || this.#root;
117
+ const moduleFile = file.includes(resolvedRoot) ? file.split(`${resolvedRoot}/`)[1] : file;
118
+ const folderKey = ManifestModuleUtil.getFolderKey(moduleFile);
119
+ const fileType = ManifestModuleUtil.getFileType(moduleFile);
120
+
121
+ const modFiles = newManifest.modules[mod].files[folderKey] ??= [];
122
+ const idx = modFiles.findIndex(x => x[0] === moduleFile);
123
+ switch (action) {
124
+ case 'create': modFiles[idx < 0 ? modFiles.length : idx] = [moduleFile, fileType, Date.now()]; break;
125
+ case 'delete': modFiles.splice(idx, 1); break;
160
126
  }
161
127
  }
128
+ await ManifestUtil.writeManifest(newManifest);
162
129
  }
163
- await ManifestUtil.writeManifest(newManifest);
164
- }
165
130
 
166
- // Reindex at workspace root
167
- this.#state.manifestIndex.init(ManifestUtil.getManifestLocation(this.#state.manifest));
168
- }
169
-
170
- /**
171
- * Get a watcher for a given compiler state
172
- * @param state
173
- * @param handler
174
- * @returns
175
- */
176
- async * watchChanges(): AsyncIterable<CompilerWatchEvent> {
177
- if (this.#signal.aborted) {
178
- return;
131
+ this.#state.manifestIndex.init(ManifestUtil.getManifestLocation(this.#state.manifest));
132
+ } catch (mErr) {
133
+ log.info('Restarting due to manifest rebuild failure', mErr);
134
+ throw new CompilerReset(`Manifest rebuild failure: ${mErr}`);
179
135
  }
136
+ }
180
137
 
181
- const manifest = this.#state.manifest;
182
- const ROOT = manifest.workspace.path;
183
- const ROOT_LOCK = 'package-lock.json';
184
- const ROOT_PKG = 'package.json';
185
- const OUTPUT_PATH = manifest.build.outputFolder;
186
- const COMPILER_PATH = manifest.build.compilerFolder;
187
- const TYPES_PATH = manifest.build.typesFolder;
188
- const COMPILER_ROOT = path.dirname(COMPILER_PATH);
189
-
190
- const IGNORE_RE = new RegExp(`^[.]|(${COMPILER_PATH}|${TYPES_PATH}|${OUTPUT_PATH})`);
138
+ async #listenWorkspace(): Promise<void> {
139
+ const lib = await import('@parcel/watcher');
140
+ const ignore = await this.#getWatchIgnores();
141
+ const packageFiles = new Set(['package-lock.json', 'yarn.lock', 'package.json'].map(x => path.resolve(this.#root, x)));
191
142
 
192
- const watchDog = this.#watchDog();
143
+ log.debug('Ignore Globs', ignore);
193
144
 
194
- for await (const events of this.#watchFolder(ROOT)) {
145
+ await this.#cleanup.workspace?.();
195
146
 
196
- const outEvents: CompilerWatchEvent[] = [];
147
+ const listener = await lib.subscribe(this.#root, async (err, events) => {
148
+ this.#lastWorkspaceModified = Date.now();
197
149
 
198
- for (const ev of events) {
199
- const { action, file: sourceFile } = ev;
150
+ try {
151
+ if (err) {
152
+ throw err instanceof Error ? err : new Error(`${err}`);
153
+ } else if (events.length > 25) {
154
+ throw new CompilerReset(`Large influx of file changes: ${events.length}`);
155
+ } else if (events.some(ev => packageFiles.has(path.toPosix(ev.path)))) {
156
+ throw new CompilerReset('Package information changed');
157
+ }
200
158
 
201
- const relativeFile = sourceFile.replace(`${ROOT}/`, '');
159
+ const items = events
160
+ .map(x => this.#toCandidateEvent(x.type, path.toPosix(x.path)))
161
+ .filter(x => this.#isValidEvent(x));
202
162
 
203
- if (
204
- relativeFile === ROOT_LOCK ||
205
- relativeFile === ROOT_PKG ||
206
- (action === 'delete' && relativeFile === COMPILER_ROOT)
207
- ) {
208
- this.#reset(ev);
209
- }
163
+ await this.#reconcileAddRemove(items);
210
164
 
211
- if (IGNORE_RE.test(relativeFile)) {
212
- continue;
165
+ for (const item of items) {
166
+ this.#q.add(item);
213
167
  }
168
+ } catch (out) {
169
+ return this.#q.throw(out instanceof Error ? out : new Error(`${out}`));
170
+ }
171
+ }, { ignore });
214
172
 
215
- watchDog.reset(relativeFile);
173
+ this.#cleanup.workspace = (): Promise<void> => listener.unsubscribe();
174
+ }
216
175
 
217
- const fileType = ManifestModuleUtil.getFileType(sourceFile);
218
- if (!CompilerUtil.validFile(fileType)) {
219
- continue;
220
- }
176
+ async #listenToolFolder(): Promise<void> {
177
+ const build = this.#state.manifest.build;
178
+ const toolRootFolder = path.dirname(path.resolve(this.#root, build.compilerFolder));
179
+ const toolFolders = new Set([
180
+ toolRootFolder, build.compilerFolder, build.typesFolder, build.outputFolder
181
+ ].map(x => path.resolve(this.#root, x)));
221
182
 
222
- let entry = this.#state.getBySource(sourceFile);
183
+ log.debug('Tooling Folders', [...toolFolders].map(x => x.replace(`${this.#root}/`, '')));
223
184
 
224
- const mod = entry?.module ?? this.#state.manifestIndex.findModuleForArbitraryFile(sourceFile);
225
- if (!mod) { // Unknown module
226
- log.debug(`Unknown module for a given file ${relativeFile}`);
227
- continue;
228
- }
185
+ await this.#cleanup.tool?.();
229
186
 
230
- const modRoot = mod.sourceFolder || this.#state.manifest.workspace.path;
231
- const moduleFile = sourceFile.includes(modRoot) ? sourceFile.split(`${modRoot}/`)[1] : sourceFile;
232
-
233
- if (action === 'create') {
234
- entry = this.#state.registerInput(mod, moduleFile);
235
- } else if (!entry) {
236
- log.debug(`Unknown file ${relativeFile}`);
237
- continue;
238
- } else if (action === 'update' && !this.#state.checkIfSourceChanged(entry.sourceFile)) {
239
- log.debug(`Skipping update, as contents unchanged ${relativeFile}`);
240
- continue;
241
- } else if (action === 'delete') {
242
- this.#state.removeSource(entry.sourceFile);
243
- }
187
+ const listener = watch(toolRootFolder, { encoding: 'utf8' }, async (ev, f) => {
188
+ if (!f) {
189
+ return;
190
+ }
191
+ const full = path.resolve(toolRootFolder, f);
192
+ const stat = await fs.stat(full).catch(() => null);
193
+ if (toolFolders.has(full) && !stat) {
194
+ this.#q.throw(new CompilerReset(`Tooling folder removal ${full}`));
195
+ }
196
+ });
197
+ this.#cleanup.tool = (): void => listener.close();
198
+ }
244
199
 
245
- outEvents.push({ action, file: entry.sourceFile, entry });
200
+ async #listenCanary(): Promise<void> {
201
+ await this.#cleanup.canary?.();
202
+ const full = path.resolve(this.#root, this.#watchCanary);
203
+ await ManifestFileUtil.bufferedFileWrite(full, '');
204
+
205
+ log.debug('Starting workspace canary');
206
+ const canaryId = setInterval(async () => {
207
+ const delta = Math.trunc((Date.now() - this.#lastWorkspaceModified) / 1000);
208
+ if (delta > 600) {
209
+ log.error('Restarting canary due to extra long delay');
210
+ this.#lastWorkspaceModified = Date.now(); // Reset
211
+ } else if (delta > this.#watchCanaryFreq * 2) {
212
+ this.#q.throw(new CompilerReset(`Workspace watch stopped responding ${delta}s ago`));
213
+ } else if (delta > this.#watchCanaryFreq) {
214
+ log.error('Restarting parcel due to inactivity');
215
+ await this.#listenWorkspace();
216
+ } else {
217
+ await fs.utimes(full, new Date(), new Date());
246
218
  }
219
+ }, this.#watchCanaryFreq * 1000);
247
220
 
248
- await this.#rebuildManifestsIfNeeded(outEvents);
249
- yield* outEvents;
250
- }
221
+ this.#cleanup.canary = (): void => clearInterval(canaryId);
222
+ }
251
223
 
252
- watchDog.close();
224
+ [Symbol.asyncIterator](): AsyncIterator<CompilerWatchEvent> {
225
+ if (!this.#cleanup.workspace) {
226
+ this.#listenWorkspace();
227
+ this.#listenToolFolder();
228
+ this.#listenCanary();
229
+ }
230
+ return this.#q[Symbol.asyncIterator]();
253
231
  }
254
232
  }