@travetto/compiler 7.0.3 → 7.0.4

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/src/state.ts CHANGED
@@ -1,17 +1,17 @@
1
- import ts from 'typescript';
1
+ import type { CompilerHost, SourceFile, CompilerOptions, Program, ScriptTarget } from 'typescript';
2
2
 
3
- import { path, ManifestModuleUtil, type ManifestModule, type ManifestRoot, ManifestIndex, ManifestModuleFolderType } from '@travetto/manifest';
4
- import { TransformerManager } from '@travetto/transformer';
5
-
6
- import { TypescriptUtil } from '../support/ts-util.ts';
3
+ import { path, ManifestModuleUtil, type ManifestModule, type ManifestRoot, type ManifestIndex, type ManifestModuleFolderType } from '@travetto/manifest';
4
+ import type { TransformerManager } from '@travetto/transformer';
7
5
 
8
6
  import { CompilerUtil } from './util.ts';
9
- import { CompileEmitError, CompileStateEntry } from './types.ts';
10
- import { CommonUtil } from '../support/util.ts';
7
+ import type { CompileEmitError, CompileStateEntry } from './types.ts';
8
+ import { CommonUtil } from './common.ts';
9
+ import { tsProxy as ts, tsProxyInit } from './ts-proxy.ts';
10
+
11
11
 
12
12
  const TYPINGS_FOLDER_KEYS = new Set<ManifestModuleFolderType>(['$index', 'support', 'src', '$package']);
13
13
 
14
- export class CompilerState implements ts.CompilerHost {
14
+ export class CompilerState implements CompilerHost {
15
15
 
16
16
  static async get(idx: ManifestIndex): Promise<CompilerState> {
17
17
  return new CompilerState().init(idx);
@@ -29,15 +29,15 @@ export class CompilerState implements ts.CompilerHost {
29
29
  #tscOutputFileToOuptut = new Map<string, string>();
30
30
 
31
31
  #sourceContents = new Map<string, string | undefined>();
32
- #sourceFileObjects = new Map<string, ts.SourceFile>();
32
+ #sourceFileObjects = new Map<string, SourceFile>();
33
33
  #sourceHashes = new Map<string, number>();
34
34
 
35
35
  #manifestIndex: ManifestIndex;
36
36
  #manifest: ManifestRoot;
37
37
  #modules: ManifestModule[];
38
38
  #transformerManager: TransformerManager;
39
- #compilerOptions: ts.CompilerOptions;
40
- #program: ts.Program;
39
+ #compilerOptions: CompilerOptions;
40
+ #program: Program;
41
41
 
42
42
  #readFile(sourceFile: string): string | undefined {
43
43
  return ts.sys.readFile(this.#sourceToEntry.get(sourceFile)?.sourceFile ?? sourceFile);
@@ -57,44 +57,69 @@ export class CompilerState implements ts.CompilerHost {
57
57
  }
58
58
  }
59
59
 
60
+ async #initCompilerOptions(): Promise<CompilerOptions> {
61
+ const tsconfigFile = CommonUtil.resolveWorkspace(this.#manifest, 'tsconfig.json');
62
+ if (!ts.sys.fileExists(tsconfigFile)) {
63
+ ts.sys.writeFile(tsconfigFile, JSON.stringify({ extends: '@travetto/compiler/tsconfig.trv.json' }, null, 2));
64
+ }
65
+
66
+ const { options } = ts.parseJsonSourceFileConfigFileContent(
67
+ ts.readJsonConfigFile(tsconfigFile, ts.sys.readFile),
68
+ ts.sys,
69
+ this.#manifest.workspace.path
70
+ );
71
+
72
+ return {
73
+ ...options,
74
+ noEmit: false,
75
+ emitDeclarationOnly: false,
76
+ allowJs: true,
77
+ resolveJsonModule: true,
78
+ sourceRoot: this.#manifest.workspace.path,
79
+ rootDir: this.#manifest.workspace.path,
80
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
81
+ module: ts.ModuleKind.ESNext,
82
+ outDir: this.#outputPath
83
+ };
84
+ }
85
+
60
86
  async init(idx: ManifestIndex): Promise<this> {
61
87
  this.#manifestIndex = idx;
62
88
  this.#manifest = idx.manifest;
63
89
  this.#outputPath = path.resolve(this.#manifest.workspace.path, this.#manifest.build.outputFolder);
64
90
  this.#typingsPath = path.resolve(this.#manifest.workspace.path, this.#manifest.build.typesFolder);
65
91
 
66
- this.#compilerOptions = {
67
- ...await TypescriptUtil.getCompilerOptions(this.#manifest),
68
- rootDir: this.#manifest.workspace.path,
69
- outDir: this.#outputPath
70
- };
71
-
72
92
  this.#modules = Object.values(this.#manifest.modules);
73
93
 
74
94
  // Register all inputs
75
- for (const mod of this.#modules) {
76
- const base = mod?.files ?? {};
95
+ for (const module of this.#modules) {
96
+ const base = module?.files ?? {};
77
97
  const files = [
78
98
  ...base.bin ?? [],
79
99
  ...base.src ?? [],
80
100
  ...base.support ?? [],
81
101
  ...base.doc ?? [],
82
102
  ...base.test ?? [],
103
+ ...base.$transformer ?? [],
83
104
  ...base.$index ?? [],
84
105
  ...base.$package ?? []
85
106
  ];
86
107
  for (const [file, type] of files) {
87
- if (CompilerUtil.validFile(type)) {
88
- this.registerInput(mod, file);
108
+ if (ManifestModuleUtil.isSourceType(type)) {
109
+ this.registerInput(module, file);
89
110
  }
90
111
  }
91
112
  }
92
-
93
- this.#transformerManager = await TransformerManager.create(this.#manifestIndex);
94
-
95
113
  return this;
96
114
  }
97
115
 
116
+ async initializeTypescript(): Promise<void> {
117
+ await tsProxyInit();
118
+ this.#compilerOptions = await this.#initCompilerOptions();
119
+ const { TransformerManager } = await import('@travetto/transformer');
120
+ this.#transformerManager ??= await TransformerManager.create(this.#manifestIndex);
121
+ }
122
+
98
123
  get manifest(): ManifestRoot {
99
124
  return this.#manifest;
100
125
  }
@@ -109,13 +134,13 @@ export class CompilerState implements ts.CompilerHost {
109
134
 
110
135
  getArbitraryInputFile(): string {
111
136
  const randomSource = this.#manifestIndex.getWorkspaceModules()
112
- .filter(mod => mod.files.src?.length)[0]
137
+ .filter(module => module.files.src?.length)[0]
113
138
  .files.src[0].sourceFile;
114
139
 
115
140
  return this.getBySource(randomSource)!.sourceFile;
116
141
  }
117
142
 
118
- async createProgram(force = false): Promise<ts.Program> {
143
+ async createProgram(force = false): Promise<Program> {
119
144
  if (force || !this.#program) {
120
145
  this.#program = ts.createProgram({ rootNames: this.getAllFiles(), host: this, options: this.#compilerOptions, oldProgram: this.#program });
121
146
  this.#transformerManager.init(this.#program.getTypeChecker());
@@ -162,6 +187,11 @@ export class CompilerState implements ts.CompilerHost {
162
187
  return this.#sourceToEntry.get(sourceFile);
163
188
  }
164
189
 
190
+ isCompilerFile(file: string): boolean {
191
+ const entry = this.getBySource(file);
192
+ return (entry?.moduleFile && ManifestModuleUtil.getFileRole(entry.moduleFile) === 'compile') || entry?.module.roles.includes('compile') || false;
193
+ }
194
+
165
195
  registerInput(module: ManifestModule, moduleFile: string): CompileStateEntry {
166
196
  const relativeSource = `${module.sourceFolder || '.'}/${moduleFile}`;
167
197
  const relativeOutput = `${module.outputFolder}/${moduleFile}`;
@@ -172,7 +202,7 @@ export class CompilerState implements ts.CompilerHost {
172
202
  const tscOutputFile = path.resolve(this.#outputPath, ManifestModuleUtil.withOutputExtension(relativeSource));
173
203
  const outputFile = path.resolve(this.#outputPath, ManifestModuleUtil.withOutputExtension(relativeOutput));
174
204
 
175
- const entry: CompileStateEntry = { sourceFile, outputFile, module, tscOutputFile, import: `${module.name}/${moduleFile}` };
205
+ const entry: CompileStateEntry = { sourceFile, outputFile, module, tscOutputFile, import: `${module.name}/${moduleFile}`, moduleFile };
176
206
 
177
207
  this.#outputToEntry.set(outputFile, entry);
178
208
  this.#sourceFiles.add(sourceFile);
@@ -199,7 +229,7 @@ export class CompilerState implements ts.CompilerHost {
199
229
  if (!contents || (contents.length === 0 && prevHash)) {
200
230
  return false; // Ignore empty file
201
231
  }
202
- const currentHash = CommonUtil.naiveHash(contents);
232
+ const currentHash = CompilerUtil.naiveHash(contents);
203
233
  const changed = prevHash !== currentHash;
204
234
  if (changed) {
205
235
  this.#sourceHashes.set(sourceFile, currentHash);
@@ -235,7 +265,7 @@ export class CompilerState implements ts.CompilerHost {
235
265
  /* Start Compiler Host */
236
266
  getCanonicalFileName(file: string): string { return file; }
237
267
  getCurrentDirectory(): string { return this.#manifest.workspace.path; }
238
- getDefaultLibFileName(options: ts.CompilerOptions): string { return ts.getDefaultLibFileName(options); }
268
+ getDefaultLibFileName(options: CompilerOptions): string { return ts.getDefaultLibFileName(options); }
239
269
  getNewLine(): string { return ts.sys.newLine; }
240
270
  useCaseSensitiveFileNames(): boolean { return ts.sys.useCaseSensitiveFileNames; }
241
271
  getDefaultLibLocation(): string { return path.dirname(ts.getDefaultLibFilePath(this.#compilerOptions)); }
@@ -275,7 +305,7 @@ export class CompilerState implements ts.CompilerHost {
275
305
  return contents;
276
306
  }
277
307
 
278
- getSourceFile(sourceFile: string, language: ts.ScriptTarget): ts.SourceFile {
308
+ getSourceFile(sourceFile: string, language: ScriptTarget): SourceFile {
279
309
  if (!this.#sourceFileObjects.has(sourceFile)) {
280
310
  const content = this.readFile(sourceFile)!;
281
311
  this.#sourceFileObjects.set(sourceFile, ts.createSourceFile(sourceFile, content ?? '', language));
@@ -0,0 +1,11 @@
1
+ /* eslint-disable @typescript-eslint/consistent-type-assertions */
2
+ import type ts from 'typescript';
3
+
4
+ let state: typeof ts | undefined;
5
+ export const tsProxyInit = (): Promise<unknown> => import('typescript').then(module => { state = module.default; });
6
+
7
+ export const tsProxy = new Proxy({}!, {
8
+ get(_, prop: string): unknown {
9
+ return state![prop as keyof typeof ts];
10
+ }
11
+ }) as typeof ts;
package/src/types.ts CHANGED
@@ -2,9 +2,41 @@ import type ts from 'typescript';
2
2
 
3
3
  import type { ChangeEventType, ManifestModule } from '@travetto/manifest';
4
4
 
5
+ export type CompilerStateType = 'startup' | 'init' | 'compile-start' | 'compile-end' | 'watch-start' | 'watch-end' | 'reset' | 'closed';
6
+ export type CompilerLogLevel = 'info' | 'debug' | 'warn' | 'error';
7
+
5
8
  export type CompileEmitError = Error | readonly ts.Diagnostic[];
6
9
  export type CompileEmitter = (file: string, newProgram?: boolean) => Promise<CompileEmitError | undefined>;
7
10
  export type CompileEmitEvent = { file: string, i: number, total: number, error?: CompileEmitError, duration: number };
8
- export type CompileStateEntry = { sourceFile: string, tscOutputFile: string, outputFile?: string, module: ManifestModule, import: string };
9
- export type CompilerWatchEvent = { action: ChangeEventType, file: string, entry: CompileStateEntry };
11
+ export type CompileStateEntry = { sourceFile: string, tscOutputFile: string, outputFile?: string, module: ManifestModule, import: string, moduleFile: string };
12
+ export type CompilerWatchEvent = { action: ChangeEventType, file: string, entry: CompileStateEntry, moduleFile: string };
13
+
14
+ export type CompilerChangeEvent = { file: string, action: ChangeEventType, output: string, module: string, import: string, time: number };
15
+ export type CompilerLogEvent = { level: CompilerLogLevel, message: string, time?: number, args?: unknown[], scope?: string };
16
+ export type CompilerProgressEvent = { idx: number, total: number, message: string, operation: 'compile', complete?: boolean };
17
+ export type CompilerStateEvent = { state: CompilerStateType, extra?: Record<string, unknown> };
18
+ export type FileChangeEvent = { files: { file: string, action: ChangeEventType }[], time: number };
19
+
20
+ export type CompilerEvent =
21
+ { type: 'file', payload: FileChangeEvent } |
22
+ { type: 'change', payload: CompilerChangeEvent } |
23
+ { type: 'log', payload: CompilerLogEvent } |
24
+ { type: 'progress', payload: CompilerProgressEvent } |
25
+ { type: 'state', payload: CompilerStateEvent } |
26
+ { type: 'all', payload: unknown };
27
+
28
+ export type CompilerEventType = CompilerEvent['type'];
29
+ export type CompilerEventPayload<V> = (CompilerEvent & { type: V })['payload'];
30
+
31
+ export type CompilerServerInfo = {
32
+ path: string;
33
+ serverProcessId: number;
34
+ compilerProcessId: number;
35
+ state: CompilerStateType;
36
+ watching: boolean;
37
+ iteration: number;
38
+ url: string;
39
+ env?: Record<string, string>;
40
+ };
41
+
10
42
  export class CompilerReset extends Error { }
package/src/util.ts CHANGED
@@ -1,6 +1,7 @@
1
- import ts from 'typescript';
1
+ import { ManifestModuleUtil, type ManifestRoot, type Package } from '@travetto/manifest';
2
2
 
3
- import { ManifestModuleFileType, ManifestModuleUtil, ManifestRoot, Package } from '@travetto/manifest';
3
+ import type { CompileEmitError } from './types.ts';
4
+ import { tsProxy as ts } from './ts-proxy.ts';
4
5
 
5
6
  const nativeCwd = process.cwd();
6
7
 
@@ -9,11 +10,6 @@ const nativeCwd = process.cwd();
9
10
  */
10
11
  export class CompilerUtil {
11
12
 
12
- /**
13
- * Determine if this is a manifest file we care about
14
- */
15
- static validFile = (type: ManifestModuleFileType): boolean => type === 'ts' || type === 'package-json' || type === 'js' || type === 'typings';
16
-
17
13
  /**
18
14
  * Rewrites the package.json to target output file names, and pins versions
19
15
  * @param manifest
@@ -47,7 +43,7 @@ export class CompilerUtil {
47
43
  * @param filename The name of the file
48
44
  * @param diagnostics The diagnostic errors
49
45
  */
50
- static buildTranspileError(filename: string, diagnostics: Error | readonly ts.Diagnostic[]): Error {
46
+ static buildTranspileError(filename: string, diagnostics: CompileEmitError): Error {
51
47
  if (diagnostics instanceof Error) {
52
48
  return diagnostics;
53
49
  }
@@ -67,4 +63,18 @@ export class CompilerUtil {
67
63
  }
68
64
  return new Error(`Transpiling ${filename.replace(nativeCwd, '.')} failed: \n${errors.join('\n')}`);
69
65
  }
66
+
67
+ /**
68
+ * Naive hashing
69
+ */
70
+ static naiveHash(text: string): number {
71
+ let hash = 5381;
72
+
73
+ for (let i = 0; i < text.length; i++) {
74
+ // eslint-disable-next-line no-bitwise
75
+ hash = (hash * 33) ^ text.charCodeAt(i);
76
+ }
77
+
78
+ return Math.abs(hash);
79
+ }
70
80
  }
package/src/watch.ts CHANGED
@@ -1,14 +1,13 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import { watch } from 'node:fs';
3
3
 
4
- import { ManifestFileUtil, ManifestModuleUtil, ManifestRoot, ManifestUtil, PackageUtil, path } from '@travetto/manifest';
4
+ import { ManifestFileUtil, ManifestModuleUtil, ManifestUtil, PackageUtil, path } from '@travetto/manifest';
5
5
 
6
6
  import { CompilerReset, type CompilerWatchEvent, type CompileStateEntry } from './types.ts';
7
- import { CompilerState } from './state.ts';
8
- import { CompilerUtil } from './util.ts';
7
+ import type { CompilerState } from './state.ts';
9
8
 
10
- import { AsyncQueue } from '../support/queue.ts';
11
- import { IpcLogger } from '../support/log.ts';
9
+ import { AsyncQueue } from './queue.ts';
10
+ import { IpcLogger } from './log.ts';
12
11
  import { EventUtil } from './event.ts';
13
12
 
14
13
  const log = new IpcLogger({ level: 'debug' });
@@ -17,7 +16,7 @@ type CompilerWatchEventCandidate = Omit<CompilerWatchEvent, 'entry'> & { entry?:
17
16
 
18
17
  export class CompilerWatcher {
19
18
  #state: CompilerState;
20
- #cleanup: Partial<Record<'tool' | 'workspace' | 'canary', () => (void | Promise<void>)>> = {};
19
+ #cleanup: Partial<Record<'tool' | 'workspace' | 'canary' | 'git', () => (void | Promise<void>)>> = {};
21
20
  #watchCanary: string = '.trv/canary.id';
22
21
  #lastWorkspaceModified = Date.now();
23
22
  #watchCanaryFrequency = 5;
@@ -28,7 +27,7 @@ export class CompilerWatcher {
28
27
  this.#state = state;
29
28
  this.#root = state.manifest.workspace.path;
30
29
  this.#queue = new AsyncQueue(signal);
31
- signal.addEventListener('abort', () => Object.values(this.#cleanup).forEach(fn => fn()));
30
+ signal.addEventListener('abort', () => Object.values(this.#cleanup).forEach(fn => fn?.()));
32
31
  }
33
32
 
34
33
  async #getWatchIgnores(): Promise<string[]> {
@@ -38,7 +37,7 @@ export class CompilerWatcher {
38
37
  '**/node_modules',
39
38
  '.*/**/node_modules'
40
39
  ];
41
- const ignores = new Set(['node_modules', '.git']);
40
+ const ignores = new Set(['node_modules', '.git', this.#state.resolveOutputFile('.')]);
42
41
  for (const item of patterns) {
43
42
  if (item.includes('*')) {
44
43
  for await (const sub of fs.glob(item, { cwd: this.#root })) {
@@ -59,15 +58,17 @@ export class CompilerWatcher {
59
58
 
60
59
  #toCandidateEvent({ action, file }: Pick<CompilerWatchEvent, 'action' | 'file'>): CompilerWatchEventCandidate {
61
60
  let entry = this.#state.getBySource(file);
62
- const mod = entry?.module ?? this.#state.manifestIndex.findModuleForArbitraryFile(file);
63
- if (mod && action === 'create' && !entry) {
64
- const modRoot = mod.sourceFolder || this.#root;
65
- const moduleFile = file.includes(`${modRoot}/`) ? file.split(`${modRoot}/`)[1] : file;
66
- entry = this.#state.registerInput(mod, moduleFile);
61
+ const module = entry?.module ?? this.#state.manifestIndex.findModuleForArbitraryFile(file);
62
+
63
+ if (module && action === 'create' && !entry) {
64
+ const moduleRoot = module.sourceFolder || this.#root;
65
+ const moduleFile = file.includes(`${moduleRoot}/`) ? file.split(`${moduleRoot}/`)[1] : file;
66
+ entry = this.#state.registerInput(module, moduleFile);
67
67
  } else if (action === 'delete' && entry) {
68
68
  this.#state.removeSource(entry.sourceFile); // Ensure we remove it
69
69
  }
70
- return { entry, file: entry?.sourceFile ?? file, action };
70
+
71
+ return { entry, file: entry?.sourceFile ?? file, action, moduleFile: entry?.moduleFile! };
71
72
  }
72
73
 
73
74
  #isValidFile(file: string): boolean {
@@ -88,65 +89,24 @@ export class CompilerWatcher {
88
89
  const relativeFile = event.file.replace(`${this.#root}/`, '');
89
90
  log.debug(`Skipping update, as contents unchanged ${relativeFile}`);
90
91
  return false;
91
- } else if (!CompilerUtil.validFile(ManifestModuleUtil.getFileType(event.file))) {
92
+ } else if (!ManifestModuleUtil.isSourceType(event.file)) {
92
93
  return false;
93
94
  }
94
95
  return true;
95
96
  }
96
97
 
97
- #getManifestUpdateEventsByParents(events: CompilerWatchEvent[]): Map<string, CompilerWatchEvent[]> {
98
- const eventsByMod = new Map<string, CompilerWatchEvent[]>();
99
- for (const event of events) {
100
- if (event.action === 'update') {
101
- continue;
102
- }
103
-
104
- const mod = event.entry.module;
105
- const moduleSet = new Set(this.#state.manifestIndex.getDependentModules(mod.name, 'parents').map(indexedMod => indexedMod.name));
106
- moduleSet.add(this.#state.manifest.workspace.name);
107
- for (const moduleName of moduleSet) {
108
- if (!eventsByMod.has(moduleName)) {
109
- eventsByMod.set(moduleName, []);
110
- }
111
- eventsByMod.get(moduleName)!.push(event);
112
- }
113
- }
114
- return eventsByMod;
115
- }
116
-
117
- #updateManifestForEvent({ action, file, entry }: CompilerWatchEvent, manifest: ManifestRoot): void {
118
- if (action === 'update') {
119
- return;
120
- }
121
-
122
- const moduleName = entry.module.name;
123
- const moduleRoot = entry.module.sourceFolder || this.#root;
124
- const relativeFile = file.includes(moduleRoot) ? file.split(`${moduleRoot}/`)[1] : file;
125
- const folderKey = ManifestModuleUtil.getFolderKey(relativeFile);
126
- const fileType = ManifestModuleUtil.getFileType(relativeFile);
127
- const roleType = ManifestModuleUtil.getFileRole(relativeFile)!;
128
-
129
- const manifestModuleFiles = manifest.modules[moduleName].files[folderKey] ??= [];
130
- const idx = manifestModuleFiles.findIndex(indexedFile => indexedFile[0] === relativeFile);
131
- const wrappedIdx = idx < 0 ? manifestModuleFiles.length : idx;
132
-
133
- switch (action) {
134
- case 'create': manifestModuleFiles[wrappedIdx] = [relativeFile, fileType, Date.now(), roleType]; break;
135
- case 'delete': idx >= 0 && manifestModuleFiles.splice(idx, 1); break;
136
- }
137
- }
138
-
139
- async #reconcileManifestUpdates(compilerEvents: CompilerWatchEvent[]): Promise<void> {
140
- for (const [mod, events] of this.#getManifestUpdateEventsByParents(compilerEvents).entries()) {
141
- const moduleRoot = this.#state.manifestIndex.getManifestModule(mod)!.sourceFolder;
142
- const moduleContext = ManifestUtil.getModuleContext(this.#state.manifest, moduleRoot);
143
- const manifestLocation = ManifestUtil.getManifestLocation(moduleContext, mod);
144
- const moduleManifest = ManifestUtil.readManifestSync(manifestLocation);
98
+ async #updateManifestWithEvents(compilerEvents: CompilerWatchEvent[]): Promise<void> {
99
+ const eventsByModule = this.#state.manifestIndex.groupByLineage(
100
+ compilerEvents.map(event => ({ item: event, module: event.entry!.module.name }))
101
+ .filter(x => x.item.action !== 'update')
102
+ );
145
103
 
146
- log.debug('Updating manifest', { module: mod, events: events.length });
147
- for (const event of events) {
148
- this.#updateManifestForEvent(event, moduleManifest);
104
+ for (const [moduleName, events] of eventsByModule.entries()) {
105
+ const moduleManifest = this.#state.manifestIndex.resolveDependentManifest(moduleName);
106
+ for (const { moduleFile, action, entry } of events) {
107
+ ManifestUtil.updateManifest(moduleManifest, entry.module.name, moduleFile, action);
149
108
  }
109
+ log.debug('Updating manifest', { module: moduleName, events: events.length });
150
110
  await ManifestUtil.writeManifest(moduleManifest);
151
111
  }
152
112
 
@@ -176,11 +136,18 @@ export class CompilerWatcher {
176
136
  }
177
137
 
178
138
  // One event per file set
179
- const filesChanged = events.map(e => ({ file: path.toPosix(e.path), action: e.type })).filter(e => this.#isValidFile(e.file));
139
+ const filesChanged = events
140
+ .map(event => ({ file: path.toPosix(event.path), action: event.type }))
141
+ .filter(event => this.#isValidFile(event.file));
142
+
180
143
  if (filesChanged.length) {
181
144
  EventUtil.sendEvent('file', { time: Date.now(), files: filesChanged });
182
145
  }
183
146
 
147
+ if (filesChanged.some(item => this.#state.isCompilerFile(item.file))) {
148
+ throw new CompilerReset('Compiler has changed, restarting');
149
+ }
150
+
184
151
  const items = filesChanged
185
152
  .map(event => this.#toCandidateEvent(event))
186
153
  .filter(event => this.#isValidEvent(event));
@@ -190,10 +157,10 @@ export class CompilerWatcher {
190
157
  }
191
158
 
192
159
  try {
193
- await this.#reconcileManifestUpdates(items);
160
+ await this.#updateManifestWithEvents(items);
194
161
  } catch (manifestError) {
195
162
  log.info('Restarting due to manifest rebuild failure', manifestError);
196
- throw new CompilerReset(`Manifest rebuild failure: ${manifestError}`);
163
+ throw new CompilerReset(`Manifest rebuild failure: ${manifestError} `);
197
164
  }
198
165
 
199
166
  for (const item of items) {
@@ -203,7 +170,7 @@ export class CompilerWatcher {
203
170
  if (out instanceof Error && out.message.includes('Events were dropped by the FSEvents client.')) {
204
171
  out = new CompilerReset('FSEvents failure, requires restart');
205
172
  }
206
- return this.#queue.throw(out instanceof Error ? out : new Error(`${out}`));
173
+ return this.#queue.throw(out instanceof Error ? out : new Error(`${out} `));
207
174
  }
208
175
  }, { ignore });
209
176
 
@@ -212,10 +179,9 @@ export class CompilerWatcher {
212
179
 
213
180
  async #listenToolFolder(): Promise<void> {
214
181
  const build = this.#state.manifest.build;
215
- const toolRootFolder = path.dirname(path.resolve(this.#root, build.compilerFolder));
216
- const toolFolders = new Set([
217
- toolRootFolder, build.compilerFolder, build.typesFolder, build.outputFolder
218
- ].map(folder => path.resolve(this.#root, folder)));
182
+ const toolRootFolder = path.dirname(path.resolve(this.#root, build.outputFolder));
183
+ const toolFolders = new Set([toolRootFolder, build.typesFolder, build.outputFolder]
184
+ .map(folder => path.resolve(this.#root, folder)));
219
185
 
220
186
  log.debug('Tooling Folders', [...toolFolders].map(folder => folder.replace(`${this.#root}/`, '')));
221
187
 
@@ -258,11 +224,27 @@ export class CompilerWatcher {
258
224
  this.#cleanup.canary = (): void => clearInterval(canaryId);
259
225
  }
260
226
 
227
+ async #listenGitChanges(): Promise<void> {
228
+ const gitFolder = path.resolve(this.#root, '.git');
229
+ if (!await fs.stat(gitFolder).catch(() => false)) { return; }
230
+ log.debug('Starting git canary');
231
+ const listener = watch(gitFolder, { encoding: 'utf8' }, async (event, file) => {
232
+ if (!file) {
233
+ return;
234
+ }
235
+ if (file === 'HEAD') {
236
+ this.#queue.throw(new CompilerReset('Git branch change detected'));
237
+ }
238
+ });
239
+ this.#cleanup.git = (): void => listener.close();
240
+ }
241
+
261
242
  [Symbol.asyncIterator](): AsyncIterator<CompilerWatchEvent> {
262
243
  if (!this.#cleanup.workspace) {
263
244
  this.#listenWorkspace();
264
245
  this.#listenToolFolder();
265
246
  this.#listenCanary();
247
+ this.#listenGitChanges();
266
248
  }
267
249
  return this.#queue[Symbol.asyncIterator]();
268
250
  }
@@ -0,0 +1,96 @@
1
+ // @trv-no-transform
2
+ import { getManifestContext, ManifestUtil } from '@travetto/manifest';
3
+
4
+ import { Log } from '../src/log.ts';
5
+ import { CompilerManager } from '../src/server/manager.ts';
6
+ import { CompilerClient } from '../src/server/client.ts';
7
+ import { CommonUtil } from '../src/common.ts';
8
+ import { EventUtil } from '../src/event.ts';
9
+
10
+ const HELP = `
11
+ npx trvc [command]
12
+
13
+ Available Commands:
14
+ * start|watch - Run the compiler in watch mode
15
+ * stop - Stop the compiler if running
16
+ * restart - Restart the compiler in watch mode
17
+ * build - Ensure the project is built and upto date
18
+ * clean - Clean out the output and compiler caches
19
+ * info - Retrieve the compiler information, if running
20
+ * event <log|progress|state> - Watch events in realtime as newline delimited JSON
21
+ * exec <file> [...args] - Allow for compiling and executing an entrypoint file
22
+ * manifest --prod [output] - Generate the project manifest
23
+ `;
24
+
25
+ /**
26
+ * Invoke the compiler
27
+ */
28
+ export async function invoke(operation?: string, args: string[] = []): Promise<unknown> {
29
+ if (operation === undefined) {
30
+ [operation, ...args] = process.argv.slice(2);
31
+ }
32
+ const ctx = getManifestContext();
33
+ const client = new CompilerClient(ctx, Log.scoped('client'));
34
+
35
+ Log.initLevel('error');
36
+ Log.root = ctx.workspace.path;
37
+
38
+ switch (operation) {
39
+ case undefined:
40
+ case 'help': console.log(HELP); break;
41
+ case 'start':
42
+ case 'watch': return CompilerManager.compile(ctx, client, { watch: true });
43
+ case 'build': return CompilerManager.compile(ctx, client, { watch: false });
44
+ case 'restart': return CompilerManager.compile(ctx, client, { watch: true, forceRestart: true });
45
+ case 'info': {
46
+ const info = await client.info();
47
+ return CommonUtil.writeStdout(2, info);
48
+ }
49
+ case 'event': {
50
+ if (!EventUtil.isComplilerEventType(args[0])) {
51
+ throw new Error(`Unknown event type: ${args[0]}`);
52
+ }
53
+ for await (const event of client.fetchEvents(args[0])) {
54
+ await CommonUtil.writeStdout(0, event);
55
+ }
56
+ break;
57
+ }
58
+ case 'manifest:production':
59
+ case 'manifest': {
60
+ let manifest = await ManifestUtil.buildManifest(ctx);
61
+ if (operation === 'manifest:production') {
62
+ manifest = await ManifestUtil.createProductionManifest(manifest);
63
+ }
64
+ if (args[0]) {
65
+ await ManifestUtil.writeManifestToFile(args[0], manifest);
66
+ console.log(`Wrote manifest to ${args[0]}`);
67
+ } else {
68
+ await CommonUtil.writeStdout(2, manifest);
69
+ }
70
+ break;
71
+ }
72
+ case 'clean': {
73
+ await client.clean(true);
74
+ console.log(`Clean triggered ${ctx.workspace.path}`);
75
+ break;
76
+ }
77
+ case 'stop': {
78
+ if (await client.stop()) {
79
+ console.log(`Stopped server ${ctx.workspace.path}: ${client.url}`);
80
+ } else {
81
+ console.log(`Server not running ${ctx.workspace.path}: ${client.url}`);
82
+ }
83
+ break;
84
+ }
85
+ case 'exec': {
86
+ await CompilerManager.compileIfNecessary(ctx, client);
87
+ Log.initLevel('none');
88
+ process.env.TRV_MANIFEST = CommonUtil.resolveWorkspace(ctx, ctx.build.outputFolder, 'node_modules', ctx.main.name); // Setup for running
89
+ const importTarget = CommonUtil.resolveWorkspace(ctx, ctx.build.outputFolder, 'node_modules', args[0]);
90
+ process.argv = [process.argv0, importTarget, ...args.slice(1)];
91
+ // Return function to run import on a module
92
+ return import(importTarget);
93
+ }
94
+ default: console.error(`\nUnknown trvc operation: ${operation}\n${HELP}`);
95
+ }
96
+ }