@travetto/compiler 5.0.10 → 5.0.12

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/common.js CHANGED
@@ -57,10 +57,11 @@ async function getEntry() {
57
57
  }
58
58
 
59
59
  // Load module on demand
60
+ /** @type {import('@travetto/manifest/src/context')} */
60
61
  const { getManifestContext } = await import(manifestJs);
61
62
 
62
63
  /** @type {Ctx} */
63
- const ctx = getManifestContext();
64
+ const ctx = getManifestContext(process.cwd(), process.env.TRV_MODULE);
64
65
  const target = getTarget.bind(null, ctx);
65
66
 
66
67
  // Setup Tsconfig
@@ -1,121 +1,56 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { createRequire } from 'node:module';
4
- const TOOL_FOLDER = '.trv/tool';
5
- const COMPILER_FOLDER = '.trv/compiler';
6
- const OUTPUT_FOLDER = '.trv/output';
7
- const TYPES_FOLDER = '.trv/types';
8
- const WS_ROOT = {};
9
- function readPackage(dir) {
10
- dir = dir.endsWith('.json') ? path.dirname(dir) : dir;
11
- try {
12
- const v = readFileSync(path.resolve(dir, 'package.json'), 'utf8');
13
- return ({ ...JSON.parse(v), path: path.resolve(dir) });
14
- }
15
- catch { }
16
- }
17
- function findPackage(dir) {
18
- let prev;
19
- let pkg, curr = path.resolve(dir);
20
- while (!pkg && curr !== prev) {
21
- pkg = readPackage(curr);
22
- [prev, curr] = [curr, path.dirname(curr)];
23
- }
24
- if (!pkg) {
25
- throw new Error('Could not find a package.json');
26
- }
27
- else {
28
- return pkg;
29
- }
30
- }
31
- function resolveWorkspace(base = process.cwd()) {
32
- if (base in WS_ROOT) {
33
- return WS_ROOT[base];
34
- }
35
- let folder = base;
4
+ const toPort = (pth) => (Math.abs([...pth].reduce((a, b) => (a * 33) ^ b.charCodeAt(0), 5381)) % 29000) + 20000;
5
+ const toPosix = (pth) => pth.replaceAll('\\', '/');
6
+ const readPackage = (file) => ({ ...JSON.parse(readFileSync(file, 'utf8')), path: toPosix(path.dirname(file)) });
7
+ function findPackage(base, pred) {
8
+ let folder = `${base}/.`;
36
9
  let prev;
37
10
  let pkg;
38
- while (prev !== folder) {
11
+ do {
39
12
  prev = folder;
40
- pkg = readPackage(folder) ?? pkg;
41
- if ((pkg && (!!pkg.workspaces || !!pkg.travetto?.build?.isolated)) ||
42
- existsSync(path.resolve(folder, '.git'))) {
43
- break;
44
- }
45
13
  folder = path.dirname(folder);
46
- }
14
+ const folderPkg = path.resolve(folder, 'package.json');
15
+ pkg = existsSync(folderPkg) ? readPackage(folderPkg) : pkg;
16
+ } while (prev !== folder &&
17
+ !pred(pkg) &&
18
+ !existsSync(path.resolve(folder, '.git')));
47
19
  if (!pkg) {
48
20
  throw new Error('Could not find a package.json');
49
21
  }
50
- return WS_ROOT[base] = {
51
- ...pkg,
52
- name: pkg.name ?? 'untitled',
53
- type: pkg.type,
54
- manager: existsSync(path.resolve(pkg.path, 'yarn.lock')) ? 'yarn' : 'npm',
55
- resolve: createRequire(`${pkg.path}/node_modules`).resolve.bind(null),
56
- stripRoot: (full) => full === pkg.path ? '' : full.replace(`${pkg.path}/`, ''),
57
- mono: !!pkg.workspaces
58
- };
59
- }
60
- function getCompilerUrl(ws) {
61
- const port = (Math.abs([...ws.path].reduce((a, b) => (a * 33) ^ b.charCodeAt(0), 5381)) % 29000) + 20000;
62
- return `http://localhost:${port}`;
63
- }
64
- function resolveModule(workspace, folder) {
65
- let mod;
66
- if (!folder && process.env.TRV_MODULE) {
67
- mod = process.env.TRV_MODULE;
68
- if (/[.][cm]?(t|j)sx?$/.test(mod)) {
69
- try {
70
- process.env.TRV_MODULE = mod = findPackage(path.dirname(mod)).name;
71
- }
72
- catch {
73
- process.env.TRV_MODULE = mod = '';
74
- }
75
- }
76
- }
77
- if (mod) {
78
- try {
79
- folder = path.dirname(workspace.resolve(`${mod}/package.json`));
80
- }
81
- catch {
82
- const workspacePkg = readPackage(workspace.path);
83
- if (workspacePkg?.name === mod) {
84
- folder = workspace.path;
85
- }
86
- else {
87
- throw new Error(`Unable to resolve location for ${folder}`);
88
- }
89
- }
90
- }
91
- return findPackage(folder ?? '.');
22
+ return pkg;
92
23
  }
93
- export function getManifestContext(folder) {
94
- const workspace = resolveWorkspace();
95
- const mod = resolveModule(workspace, folder);
24
+ export function getManifestContext(root, mod) {
25
+ const workspace = findPackage(root, pkg => !!pkg?.workspaces || !!pkg?.travetto?.build?.isolated);
96
26
  const build = workspace.travetto?.build ?? {};
27
+ const resolve = createRequire(path.resolve(workspace.path, 'node_modules')).resolve.bind(null);
28
+ const wsPrefix = `${workspace.path}/`;
29
+ const modPkg = mod ?
30
+ readPackage(resolve(`${mod}/package.json`)) :
31
+ findPackage(root, pkg => !!pkg) ?? workspace;
97
32
  return {
98
33
  workspace: {
99
- name: workspace.name,
34
+ name: workspace.name ?? 'untitled',
100
35
  path: workspace.path,
101
- mono: workspace.mono,
102
- manager: workspace.manager,
36
+ mono: !!workspace.workspaces,
37
+ manager: existsSync(path.resolve(workspace.path, 'yarn.lock')) ? 'yarn' : 'npm',
103
38
  type: workspace.type ?? 'commonjs',
104
39
  defaultEnv: workspace.travetto?.defaultEnv ?? 'local'
105
40
  },
106
41
  build: {
107
- compilerFolder: build.compilerFolder ?? COMPILER_FOLDER,
108
- compilerUrl: build.compilerUrl ?? getCompilerUrl(workspace),
109
- compilerModuleFolder: workspace.stripRoot(path.dirname(workspace.resolve('@travetto/compiler/package.json'))),
110
- outputFolder: build.outputFolder ?? OUTPUT_FOLDER,
111
- toolFolder: build.toolFolder ?? TOOL_FOLDER,
112
- typesFolder: build.typesFolder ?? TYPES_FOLDER
42
+ compilerUrl: build.compilerUrl ?? `http://localhost:${toPort(wsPrefix)}`,
43
+ compilerModuleFolder: toPosix(path.dirname(resolve('@travetto/compiler/package.json'))).replace(wsPrefix, ''),
44
+ compilerFolder: toPosix(build.compilerFolder ?? '.trv/compiler'),
45
+ outputFolder: toPosix(build.outputFolder ?? '.trv/output'),
46
+ toolFolder: toPosix(build.toolFolder ?? '.trv/tool'),
47
+ typesFolder: toPosix(build.typesFolder ?? '.trv/types')
113
48
  },
114
49
  main: {
115
- name: mod.name ?? 'untitled',
116
- folder: workspace.stripRoot(mod.path),
117
- version: mod.version,
118
- description: mod.description
50
+ name: modPkg.name ?? 'untitled',
51
+ folder: modPkg.path.replace(wsPrefix, ''),
52
+ version: modPkg.version,
53
+ description: modPkg.description
119
54
  }
120
55
  };
121
56
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/compiler",
3
- "version": "5.0.10",
3
+ "version": "5.0.12",
4
4
  "description": "The compiler infrastructure for the Travetto framework",
5
5
  "keywords": [
6
6
  "compiler",
@@ -33,12 +33,12 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@parcel/watcher": "^2.4.1",
36
- "@travetto/manifest": "^5.0.5",
37
- "@travetto/transformer": "^5.0.6",
36
+ "@travetto/manifest": "^5.0.6",
37
+ "@travetto/transformer": "^5.0.7",
38
38
  "@types/node": "^22.7.4"
39
39
  },
40
40
  "peerDependencies": {
41
- "@travetto/cli": "^5.0.7"
41
+ "@travetto/cli": "^5.0.9"
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,249 +12,222 @@ 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
-
166
- // Reindex at workspace root
167
- this.#state.manifestIndex.init(ManifestUtil.getManifestLocation(this.#state.manifest));
168
- }
169
130
 
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})`);
191
-
192
- const watchDog = this.#watchDog();
193
-
194
- for await (const events of this.#watchFolder(ROOT)) {
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)));
195
142
 
196
- const outEvents: CompilerWatchEvent[] = [];
143
+ log.debug('Ignore Globs', ignore);
144
+ log.debug('Watching', this.#root);
197
145
 
198
- for (const ev of events) {
199
- const { action, file: sourceFile } = ev;
146
+ await this.#cleanup.workspace?.();
200
147
 
201
- const relativeFile = sourceFile.replace(`${ROOT}/`, '');
148
+ const listener = await lib.subscribe(this.#root, async (err, events) => {
149
+ this.#lastWorkspaceModified = Date.now();
202
150
 
203
- if (
204
- relativeFile === ROOT_LOCK ||
205
- relativeFile === ROOT_PKG ||
206
- (action === 'delete' && relativeFile === COMPILER_ROOT)
207
- ) {
208
- this.#reset(ev);
151
+ try {
152
+ if (err) {
153
+ throw err instanceof Error ? err : new Error(`${err}`);
154
+ } else if (events.length > 25) {
155
+ throw new CompilerReset(`Large influx of file changes: ${events.length}`);
156
+ } else if (events.some(ev => packageFiles.has(path.toPosix(ev.path)))) {
157
+ throw new CompilerReset('Package information changed');
209
158
  }
210
159
 
211
- if (IGNORE_RE.test(relativeFile)) {
212
- continue;
213
- }
160
+ const items = events
161
+ .map(x => this.#toCandidateEvent(x.type, path.toPosix(x.path)))
162
+ .filter(x => this.#isValidEvent(x));
214
163
 
215
- watchDog.reset(relativeFile);
164
+ await this.#reconcileAddRemove(items);
216
165
 
217
- const fileType = ManifestModuleUtil.getFileType(sourceFile);
218
- if (!CompilerUtil.validFile(fileType)) {
219
- continue;
166
+ for (const item of items) {
167
+ this.#q.add(item);
220
168
  }
169
+ } catch (out) {
170
+ return this.#q.throw(out instanceof Error ? out : new Error(`${out}`));
171
+ }
172
+ }, { ignore });
221
173
 
222
- let entry = this.#state.getBySource(sourceFile);
174
+ this.#cleanup.workspace = (): Promise<void> => listener.unsubscribe();
175
+ }
223
176
 
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
- }
177
+ async #listenToolFolder(): Promise<void> {
178
+ const build = this.#state.manifest.build;
179
+ const toolRootFolder = path.dirname(path.resolve(this.#root, build.compilerFolder));
180
+ const toolFolders = new Set([
181
+ toolRootFolder, build.compilerFolder, build.typesFolder, build.outputFolder
182
+ ].map(x => path.resolve(this.#root, x)));
229
183
 
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
- }
184
+ log.debug('Tooling Folders', [...toolFolders].map(x => x.replace(`${this.#root}/`, '')));
185
+
186
+ await this.#cleanup.tool?.();
244
187
 
245
- outEvents.push({ action, file: entry.sourceFile, entry });
188
+ const listener = watch(toolRootFolder, { encoding: 'utf8' }, async (ev, f) => {
189
+ if (!f) {
190
+ return;
191
+ }
192
+ const full = path.resolve(toolRootFolder, f);
193
+ const stat = await fs.stat(full).catch(() => null);
194
+ if (toolFolders.has(full) && !stat) {
195
+ this.#q.throw(new CompilerReset(`Tooling folder removal ${full}`));
246
196
  }
197
+ });
198
+ this.#cleanup.tool = (): void => listener.close();
199
+ }
247
200
 
248
- try {
249
- await this.#rebuildManifestsIfNeeded(outEvents);
250
- } catch (err) {
251
- log.info('Restarting due to manifest rebuild failure', err);
252
- this.#reset(events[0]);
201
+ async #listenCanary(): Promise<void> {
202
+ await this.#cleanup.canary?.();
203
+ const full = path.resolve(this.#root, this.#watchCanary);
204
+ await ManifestFileUtil.bufferedFileWrite(full, '');
205
+
206
+ log.debug('Starting workspace canary');
207
+ const canaryId = setInterval(async () => {
208
+ const delta = Math.trunc((Date.now() - this.#lastWorkspaceModified) / 1000);
209
+ if (delta > 600) {
210
+ log.error('Restarting canary due to extra long delay');
211
+ this.#lastWorkspaceModified = Date.now(); // Reset
212
+ } else if (delta > this.#watchCanaryFreq * 2) {
213
+ this.#q.throw(new CompilerReset(`Workspace watch stopped responding ${delta}s ago`));
214
+ } else if (delta > this.#watchCanaryFreq) {
215
+ log.error('Restarting parcel due to inactivity');
216
+ await this.#listenWorkspace();
217
+ } else {
218
+ await fs.utimes(full, new Date(), new Date());
253
219
  }
254
- yield* outEvents;
255
- }
220
+ }, this.#watchCanaryFreq * 1000);
256
221
 
257
- watchDog.close();
222
+ this.#cleanup.canary = (): void => clearInterval(canaryId);
223
+ }
224
+
225
+ [Symbol.asyncIterator](): AsyncIterator<CompilerWatchEvent> {
226
+ if (!this.#cleanup.workspace) {
227
+ this.#listenWorkspace();
228
+ this.#listenToolFolder();
229
+ this.#listenCanary();
230
+ }
231
+ return this.#q[Symbol.asyncIterator]();
258
232
  }
259
233
  }