@travetto/compiler 3.4.4 → 4.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/state.ts CHANGED
@@ -3,9 +3,10 @@ import ts from 'typescript';
3
3
  import { path, ManifestModuleUtil, ManifestModule, ManifestRoot, ManifestIndex } from '@travetto/manifest';
4
4
  import { TransformerManager } from '@travetto/transformer';
5
5
 
6
+ import { CommonUtil } from '../support/util';
7
+
6
8
  import { CompilerUtil } from './util';
7
9
  import { CompileStateEntry } from './types';
8
- import { CommonUtil } from '../support/util';
9
10
 
10
11
  function folderMapper(root: string, prefix: string): { dir: string, translate: (val: string) => string } {
11
12
  let matched: string = '~~';
@@ -26,7 +27,7 @@ export class CompilerState implements ts.CompilerHost {
26
27
  private constructor() { }
27
28
 
28
29
  #rootDir: string;
29
- #inputPathToSource: (file: string) => string;
30
+ #inputPathToSourcePath: (file: string) => string;
30
31
  #outputPath: string;
31
32
  #inputFiles = new Set<string>();
32
33
  #inputDirectoryToSource = new Map<string, string>();
@@ -36,6 +37,7 @@ export class CompilerState implements ts.CompilerHost {
36
37
 
37
38
  #sourceContents = new Map<string, string | undefined>();
38
39
  #sourceFileObjects = new Map<string, ts.SourceFile>();
40
+ #sourceHashes = new Map<string, number>();
39
41
 
40
42
  #manifestIndex: ManifestIndex;
41
43
  #manifest: ManifestRoot;
@@ -43,14 +45,18 @@ export class CompilerState implements ts.CompilerHost {
43
45
  #transformerManager: TransformerManager;
44
46
  #compilerOptions: ts.CompilerOptions;
45
47
 
48
+ #readFile(inputFile: string): string | undefined {
49
+ return ts.sys.readFile(this.#inputToEntry.get(inputFile)?.sourceFile ?? this.#inputPathToSourcePath(inputFile));
50
+ }
51
+
46
52
  async init(idx: ManifestIndex): Promise<this> {
47
53
  this.#manifestIndex = idx;
48
54
  this.#manifest = idx.manifest;
49
- const mapper = folderMapper(this.#manifest.workspacePath, '##');
55
+ const mapper = folderMapper(this.#manifest.workspace.path, '##');
50
56
  this.#rootDir = mapper.dir;
51
- this.#inputPathToSource = mapper.translate;
57
+ this.#inputPathToSourcePath = mapper.translate;
52
58
 
53
- this.#outputPath = path.resolve(this.#manifest.workspacePath, this.#manifest.outputFolder);
59
+ this.#outputPath = path.resolve(this.#manifest.workspace.path, this.#manifest.build.outputFolder);
54
60
  this.#modules = Object.values(this.#manifest.modules);
55
61
 
56
62
  // Register all inputs
@@ -91,11 +97,11 @@ export class CompilerState implements ts.CompilerHost {
91
97
  }
92
98
 
93
99
  resolveOutputFile(file: string): string {
94
- return path.resolve(this.#manifest.workspacePath, this.#manifest.outputFolder, file);
100
+ return path.resolve(this.#manifest.workspace.path, this.#manifest.build.outputFolder, file);
95
101
  }
96
102
 
97
103
  getArbitraryInputFile(): string {
98
- return this.getBySource(this.#manifestIndex.getModule('@travetto/manifest')!.files.src[0].sourceFile)!.input;
104
+ return this.getBySource(this.#manifestIndex.getModule('@travetto/manifest')!.files.src[0].sourceFile)!.inputFile;
99
105
  }
100
106
 
101
107
  createProgram(oldProgram?: ts.Program): ts.Program {
@@ -107,9 +113,9 @@ export class CompilerState implements ts.CompilerHost {
107
113
  writeInputFile(program: ts.Program, inputFile: string): ts.EmitResult | undefined | void {
108
114
  switch (ManifestModuleUtil.getFileType(inputFile)) {
109
115
  case 'package-json':
110
- return this.writeFile(this.#inputToEntry.get(inputFile)!.output!, this.readFile(inputFile)!, false);
116
+ return this.writeFile(this.#inputToEntry.get(inputFile)!.outputFile!, this.readFile(inputFile)!, false);
111
117
  case 'js':
112
- return this.writeFile(this.#inputToEntry.get(inputFile)!.output!, ts.transpile(this.readFile(inputFile)!, this.#compilerOptions), false);
118
+ return this.writeFile(this.#inputToEntry.get(inputFile)!.outputFile!, ts.transpile(this.readFile(inputFile)!, this.#compilerOptions), false);
113
119
  case 'ts':
114
120
  return program.emit(
115
121
  program.getSourceFile(inputFile)!,
@@ -125,7 +131,7 @@ export class CompilerState implements ts.CompilerHost {
125
131
 
126
132
  registerInput(module: ManifestModule, moduleFile: string): CompileStateEntry {
127
133
  const relativeInput = `${module.outputFolder}/${moduleFile}`;
128
- const sourceFile = path.resolve(this.#manifest.workspacePath, module.sourceFolder, moduleFile);
134
+ const sourceFile = path.resolve(this.#manifest.workspace.path, module.sourceFolder, moduleFile);
129
135
  const sourceFolder = path.dirname(sourceFile);
130
136
  const inputFile = path.resolve(this.#rootDir, relativeInput); // Ensure input is isolated
131
137
  const inputFolder = path.dirname(inputFile);
@@ -134,7 +140,7 @@ export class CompilerState implements ts.CompilerHost {
134
140
  undefined :
135
141
  path.resolve(this.#outputPath, ManifestModuleUtil.sourceToOutputExt(relativeInput));
136
142
 
137
- const entry = { source: sourceFile, input: inputFile, output: outputFile, module, relativeInput };
143
+ const entry = { sourceFile, inputFile, outputFile, module };
138
144
 
139
145
  this.#inputToEntry.set(inputFile, entry);
140
146
  this.#sourceToEntry.set(sourceFile, entry);
@@ -145,23 +151,37 @@ export class CompilerState implements ts.CompilerHost {
145
151
  }
146
152
 
147
153
  this.#inputFiles.add(inputFile);
148
-
154
+ this.#sourceHashes.set(sourceFile, -1); // Unknown
149
155
  return entry;
150
156
  }
151
157
 
152
- removeInput(inputFile: string): void {
153
- const { output, source } = this.#inputToEntry.get(inputFile)!;
154
- if (output) {
155
- this.#outputToEntry.delete(output);
158
+ checkIfSourceChanged(inputFile: string): boolean {
159
+ const contents = this.#readFile(inputFile);
160
+ const prevHash = this.#sourceHashes.get(inputFile);
161
+ if (!contents || (contents.length === 0 && prevHash)) {
162
+ return false; // Ignore empty file
156
163
  }
157
- this.#sourceToEntry.delete(source);
158
- this.#inputToEntry.delete(inputFile);
159
- this.#inputFiles.delete(inputFile);
164
+ const currentHash = CompilerUtil.naiveHash(contents);
165
+ const changed = prevHash !== currentHash;
166
+ if (changed) {
167
+ this.#sourceHashes.set(inputFile, currentHash);
168
+ this.#sourceContents.set(inputFile, contents);
169
+ this.#sourceFileObjects.delete(inputFile);
170
+ }
171
+ return changed;
160
172
  }
161
173
 
162
- resetInputSource(inputFile: string): void {
174
+ removeInput(inputFile: string): void {
175
+ const { outputFile, sourceFile } = this.#inputToEntry.get(inputFile)!;
176
+ if (outputFile) {
177
+ this.#outputToEntry.delete(outputFile);
178
+ }
163
179
  this.#sourceFileObjects.delete(inputFile);
164
180
  this.#sourceContents.delete(inputFile);
181
+ this.#sourceHashes.delete(inputFile);
182
+ this.#sourceToEntry.delete(sourceFile);
183
+ this.#inputToEntry.delete(inputFile);
184
+ this.#inputFiles.delete(inputFile);
165
185
  }
166
186
 
167
187
  getAllFiles(): string[] {
@@ -177,11 +197,11 @@ export class CompilerState implements ts.CompilerHost {
177
197
  getDefaultLibLocation(): string { return path.dirname(ts.getDefaultLibFilePath(this.#compilerOptions)); }
178
198
 
179
199
  fileExists(inputFile: string): boolean {
180
- return this.#inputToEntry.has(inputFile) || ts.sys.fileExists(this.#inputPathToSource(inputFile));
200
+ return this.#inputToEntry.has(inputFile) || ts.sys.fileExists(this.#inputPathToSourcePath(inputFile));
181
201
  }
182
202
 
183
203
  directoryExists(inputDir: string): boolean {
184
- return this.#inputDirectoryToSource.has(inputDir) || ts.sys.directoryExists(this.#inputPathToSource(inputDir));
204
+ return this.#inputDirectoryToSource.has(inputDir) || ts.sys.directoryExists(this.#inputPathToSourcePath(inputDir));
185
205
  }
186
206
 
187
207
  writeFile(
@@ -203,9 +223,7 @@ export class CompilerState implements ts.CompilerHost {
203
223
  }
204
224
 
205
225
  readFile(inputFile: string): string | undefined {
206
- const res = this.#sourceContents.get(inputFile) ?? ts.sys.readFile(
207
- this.#inputToEntry.get(inputFile)?.source ?? this.#inputPathToSource(inputFile)
208
- );
226
+ const res = this.#sourceContents.get(inputFile) ?? this.#readFile(inputFile);
209
227
  this.#sourceContents.set(inputFile, res);
210
228
  return res;
211
229
  }
package/src/types.ts CHANGED
@@ -5,4 +5,4 @@ import type { ManifestModule } from '@travetto/manifest';
5
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
- export type CompileStateEntry = { source: string, input: string, relativeInput: string, output?: string, module: ManifestModule };
8
+ export type CompileStateEntry = { sourceFile: string, inputFile: string, outputFile?: string, module: ManifestModule };
package/src/util.ts CHANGED
@@ -2,7 +2,7 @@ import ts from 'typescript';
2
2
 
3
3
  import { ManifestContext, ManifestModuleFileType, ManifestModuleUtil, ManifestRoot, Package, path } from '@travetto/manifest';
4
4
 
5
- type OutputToSource = (outputFile: string) => ({ source: string } | undefined);
5
+ type OutputToSource = (outputFile: string) => ({ sourceFile: string } | undefined);
6
6
 
7
7
  const nativeCwd = process.cwd();
8
8
 
@@ -31,12 +31,12 @@ export class CompilerUtil {
31
31
  */
32
32
  static rewriteSourceMap(ctx: ManifestContext, text: string, outputToSource: OutputToSource): string {
33
33
  const data: { sourceRoot?: string, sources: string[] } = JSON.parse(text);
34
- const output = ManifestModuleUtil.sourceToOutputExt(path.resolve(ctx.workspacePath, ctx.outputFolder, data.sources[0]));
35
- const { source: file } = outputToSource(output) ?? {};
34
+ const output = ManifestModuleUtil.sourceToOutputExt(path.resolve(ctx.workspace.path, ctx.build.outputFolder, data.sources[0]));
35
+ const { sourceFile } = outputToSource(output) ?? {};
36
36
 
37
- if (file) {
37
+ if (sourceFile) {
38
38
  delete data.sourceRoot;
39
- data.sources = [file];
39
+ data.sources = [sourceFile];
40
40
  text = JSON.stringify(data);
41
41
  }
42
42
  return text;
@@ -81,7 +81,7 @@ export class CompilerUtil {
81
81
  if (pkg.main) {
82
82
  pkg.main = ManifestModuleUtil.sourceToOutputExt(pkg.main);
83
83
  }
84
- pkg.type = manifest.moduleType;
84
+ pkg.type = manifest.workspace.type;
85
85
  for (const key of ['devDependencies', 'dependencies', 'peerDependencies'] as const) {
86
86
  if (key in pkg) {
87
87
  for (const dep of Object.keys(pkg[key] ?? {})) {
package/src/watch.ts CHANGED
@@ -1,95 +1,96 @@
1
- import { readFileSync } from 'fs';
2
- import { setMaxListeners } from 'events';
3
-
4
- import {
5
- ManifestContext, ManifestModuleUtil, ManifestUtil, ManifestModuleFolderType, ManifestModuleFileType,
6
- path, ManifestModule,
7
- } from '@travetto/manifest';
8
- import { getManifestContext } from '@travetto/manifest/bin/context';
1
+ import { ManifestContext, ManifestModuleUtil, ManifestUtil, RuntimeIndex, path } from '@travetto/manifest';
9
2
 
10
3
  import type { CompileStateEntry } from './types';
11
4
  import { CompilerState } from './state';
12
5
  import { CompilerUtil } from './util';
13
6
 
14
- import { WatchEvent, fileWatchEvents } from './internal/watch-core';
7
+ import { AsyncQueue } from '../support/queue';
15
8
 
16
- type DirtyFile = { modFolder: string, mod: string, remove?: boolean, moduleFile: string, folderKey: ManifestModuleFolderType, type: ManifestModuleFileType };
9
+ type WatchAction = 'create' | 'update' | 'delete';
10
+ type WatchEvent = { action: WatchAction, file: string };
11
+ type CompilerWatchEvent = WatchEvent & { entry: CompileStateEntry };
17
12
 
18
13
  /**
19
14
  * Watch support, based on compiler state and manifest details
20
15
  */
21
16
  export class CompilerWatcher {
17
+ #state: CompilerState;
18
+ #signal: AbortSignal;
22
19
 
23
- /**
24
- * Watch state
25
- * @param state
26
- * @returns
27
- */
28
- static watch(state: CompilerState): AsyncIterable<WatchEvent<{ entry: CompileStateEntry }>> {
29
- return new CompilerWatcher(state).watchChanges();
20
+ constructor(state: CompilerState, signal: AbortSignal) {
21
+ this.#state = state;
22
+ this.#signal = signal;
30
23
  }
31
24
 
32
- #sourceHashes = new Map<string, number>();
33
- #manifestContexts = new Map<string, ManifestContext>();
34
- #dirtyFiles: DirtyFile[] = [];
35
- #state: CompilerState;
25
+ /** Watch files */
26
+ async * #watchFolder(rootPath: string): AsyncIterable<WatchEvent> {
27
+ const q = new AsyncQueue<WatchEvent>(this.#signal);
28
+ const lib = await import('@parcel/watcher');
36
29
 
37
- constructor(state: CompilerState) {
38
- this.#state = state;
30
+ const cleanup = await lib.subscribe(rootPath, (err, events) => {
31
+ if (err) {
32
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
33
+ q.throw(err instanceof Error ? err : new Error((err as Error).message));
34
+ return;
35
+ }
36
+ for (const ev of events) {
37
+ q.add({ action: ev.type, file: path.toPosix(ev.path) });
38
+ }
39
+ }, {
40
+ // TODO: Read .gitignore?
41
+ ignore: [
42
+ 'node_modules', '**/node_modules', '.git', '**/.git',
43
+ `${this.#state.manifest.build.outputFolder}/node_modules/**`,
44
+ `${this.#state.manifest.build.compilerFolder}/node_modules/**`,
45
+ `${this.#state.manifest.build.toolFolder}/**`
46
+ ]
47
+ });
48
+
49
+ if (this.#signal.aborted) { // If already aborted, can happen async
50
+ cleanup.unsubscribe();
51
+ return;
52
+ }
53
+
54
+ this.#signal.addEventListener('abort', () => cleanup.unsubscribe());
55
+
56
+ yield* q;
39
57
  }
40
58
 
41
- async #rebuildManifestsIfNeeded(): Promise<void> {
42
- if (!this.#dirtyFiles.length) {
59
+ async #rebuildManifestsIfNeeded(event: CompilerWatchEvent, moduleFile: string): Promise<void> {
60
+ if (!event.entry.outputFile || event.action === 'update') {
43
61
  return;
44
62
  }
45
- const mods = [...new Set(this.#dirtyFiles.map(x => x.modFolder))];
46
- const contexts = await Promise.all(mods.map(async folder => {
47
- if (!this.#manifestContexts.has(folder)) {
48
- const ctx = await getManifestContext(folder);
49
- this.#manifestContexts.set(folder, ctx);
50
- }
51
- return this.#manifestContexts.get(folder)!;
52
- }));
53
63
 
54
- const files = this.#dirtyFiles.slice(0);
55
- this.#dirtyFiles = [];
64
+ const toUpdate: ManifestContext[] = RuntimeIndex.getDependentModules(event.entry.module.name, 'parents')
65
+ .map(el => ManifestUtil.getModuleContext(this.#state.manifest, el.sourceFolder));
56
66
 
57
- for (const ctx of [...contexts, this.#state.manifest]) {
67
+ toUpdate.push(this.#state.manifest);
68
+
69
+ const mod = event.entry.module;
70
+ const folderKey = ManifestModuleUtil.getFolderKey(moduleFile);
71
+ const fileType = ManifestModuleUtil.getFileType(moduleFile);
72
+
73
+ for (const ctx of toUpdate) {
58
74
  const newManifest = await ManifestUtil.buildManifest(ctx);
59
- for (const file of files) {
60
- if (file.mod in newManifest.modules) {
61
- const modFiles = newManifest.modules[file.mod].files[file.folderKey] ??= [];
62
- const idx = modFiles.findIndex(x => x[0] === file.moduleFile);
63
-
64
- if (!file.remove && idx < 0) {
65
- modFiles.push([file.moduleFile, file.type, Date.now()]);
66
- } else if (idx >= 0) {
67
- if (file.remove) {
68
- modFiles.splice(idx, 1);
69
- } else {
70
- modFiles[idx] = [file.moduleFile, file.type, Date.now()];
71
- }
75
+ if (mod.name in newManifest.modules) {
76
+ const modFiles = newManifest.modules[mod.name].files[folderKey] ??= [];
77
+ const idx = modFiles.findIndex(x => x[0] === moduleFile);
78
+
79
+ if (event.action === 'create' && idx < 0) {
80
+ modFiles.push([moduleFile, fileType, Date.now()]);
81
+ } else if (idx >= 0) {
82
+ if (event.action === 'delete') {
83
+ modFiles.splice(idx, 1);
84
+ } else {
85
+ modFiles[idx] = [moduleFile, fileType, Date.now()];
72
86
  }
73
87
  }
74
88
  }
75
- await ManifestUtil.writeManifest(ctx, newManifest);
89
+ await ManifestUtil.writeManifest(newManifest);
76
90
  }
77
- // Reindex
78
- this.#state.manifestIndex.init(this.#state.manifestIndex.manifestFile);
79
- }
80
-
81
- #getModuleMap(): Record<string, ManifestModule> {
82
- return Object.fromEntries(
83
- Object.values(this.#state.manifest.modules).map(x => [path.resolve(this.#state.manifest.workspacePath, x.sourceFolder), x])
84
- );
85
- }
86
91
 
87
- #addDirtyFile(mod: ManifestModule, folder: string, moduleFile: string, remove = false): void {
88
- this.#dirtyFiles.push({
89
- mod: mod.name, modFolder: folder, remove, moduleFile,
90
- folderKey: ManifestModuleUtil.getFolderKey(moduleFile),
91
- type: ManifestModuleUtil.getFileType(moduleFile),
92
- });
92
+ // Reindex at workspace root
93
+ this.#state.manifestIndex.init(ManifestUtil.getManifestLocation(this.#state.manifest));
93
94
  }
94
95
 
95
96
  /**
@@ -98,67 +99,56 @@ export class CompilerWatcher {
98
99
  * @param handler
99
100
  * @returns
100
101
  */
101
- async * watchChanges(): AsyncIterable<WatchEvent<{ entry: CompileStateEntry }>> {
102
- const mods = this.#getModuleMap();
103
- const ctrl = new AbortController();
104
- setMaxListeners(1000, ctrl.signal);
105
-
106
- const modules = [...this.#state.manifestIndex.getModuleList('all')].map(x => this.#state.manifestIndex.getModule(x)!);
107
-
108
- const stream = fileWatchEvents(this.#state.manifest, modules, ctrl.signal);
109
- for await (const ev of stream) {
102
+ async * watchChanges(): AsyncIterable<CompilerWatchEvent> {
103
+ if (this.#signal.aborted) {
104
+ return;
105
+ }
110
106
 
111
- if (ev.action === 'reset') {
112
- yield ev;
113
- ctrl.abort();
114
- return;
107
+ const manifest = this.#state.manifest;
108
+ const ROOT_LOCK = path.resolve(manifest.workspace.path, 'package-lock.json');
109
+ const ROOT_PKG = path.resolve(manifest.workspace.path, 'package.json');
110
+ const OUTPUT_PATH = path.resolve(manifest.workspace.path, manifest.build.outputFolder);
111
+ const COMPILER_PATH = path.resolve(manifest.workspace.path, manifest.build.compilerFolder);
112
+
113
+ for await (const ev of this.#watchFolder(this.#state.manifest.workspace.path)) {
114
+ const { action, file: sourceFile } = ev;
115
+
116
+ if (
117
+ sourceFile === ROOT_LOCK ||
118
+ sourceFile === ROOT_PKG ||
119
+ (action === 'delete' && (sourceFile === OUTPUT_PATH || sourceFile === COMPILER_PATH))
120
+ ) {
121
+ throw new Error('RESET');
115
122
  }
116
123
 
117
- const { action, file: sourceFile, folder } = ev;
118
- const mod = mods[folder];
119
- const moduleFile = mod.sourceFolder ?
120
- (sourceFile.includes(mod.sourceFolder) ? sourceFile.split(`${mod.sourceFolder}/`)[1] : sourceFile) :
121
- sourceFile.replace(`${this.#state.manifest.workspacePath}/`, '');
124
+ const fileType = ManifestModuleUtil.getFileType(sourceFile);
125
+ if (!CompilerUtil.validFile(fileType)) {
126
+ continue;
127
+ }
122
128
 
123
129
  let entry = this.#state.getBySource(sourceFile);
124
130
 
125
- switch (action) {
126
- case 'create': {
127
- const fileType = ManifestModuleUtil.getFileType(moduleFile);
128
- this.#addDirtyFile(mod, folder, moduleFile);
129
- if (CompilerUtil.validFile(fileType)) {
130
- const hash = CompilerUtil.naiveHash(readFileSync(sourceFile, 'utf8'));
131
- entry = this.#state.registerInput(mod, moduleFile);
132
- this.#sourceHashes.set(sourceFile, hash);
133
- }
134
- break;
135
- }
136
- case 'update': {
137
- if (entry) {
138
- const hash = CompilerUtil.naiveHash(readFileSync(sourceFile, 'utf8'));
139
- if (this.#sourceHashes.get(sourceFile) !== hash) {
140
- this.#state.resetInputSource(entry.input);
141
- this.#sourceHashes.set(sourceFile, hash);
142
- } else {
143
- entry = undefined;
144
- }
145
- }
146
- break;
147
- }
148
- case 'delete': {
149
- if (entry) {
150
- this.#state.removeInput(entry.input);
151
- if (entry.output) {
152
- this.#addDirtyFile(mod, folder, moduleFile, true);
153
- }
154
- }
155
- }
131
+ const mod = entry?.module ?? this.#state.manifestIndex.findModuleForArbitraryFile(sourceFile);
132
+ if (!mod) { // Unknown module
133
+ continue;
156
134
  }
157
135
 
158
- if (entry) {
159
- await this.#rebuildManifestsIfNeeded();
160
- yield { action, file: entry.source, folder, entry };
136
+ const modRoot = mod.sourceFolder || this.#state.manifest.workspace.path;
137
+ const moduleFile = sourceFile.includes(modRoot) ? sourceFile.split(`${modRoot}/`)[1] : sourceFile;
138
+
139
+ if (action === 'create') {
140
+ entry = this.#state.registerInput(mod, moduleFile);
141
+ } else if (!entry) {
142
+ continue;
143
+ } else if (action === 'update' && !this.#state.checkIfSourceChanged(entry.inputFile)) {
144
+ continue;
145
+ } else if (action === 'delete') {
146
+ this.#state.removeInput(entry.inputFile);
161
147
  }
148
+
149
+ const result: CompilerWatchEvent = { action, file: entry.sourceFile, entry };
150
+ await this.#rebuildManifestsIfNeeded(result, moduleFile);
151
+ yield result;
162
152
  }
163
153
  }
164
154
  }
@@ -1,73 +1,103 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
3
 
4
4
  import type { ManifestContext } from '@travetto/manifest';
5
5
 
6
+ import type { CompilerLogLevel, CompilerMode, CompilerServerInfo } from './types';
6
7
  import { LogUtil } from './log';
8
+ import { CommonUtil } from './util';
7
9
  import { CompilerSetup } from './setup';
8
10
  import { CompilerServer } from './server/server';
9
11
  import { CompilerRunner } from './server/runner';
10
- import type { CompilerOp, CompilerServerInfo } from './types';
11
- import { CompilerClientUtil } from './server/client';
12
- import { CommonUtil } from './util';
12
+ import { CompilerClient } from './server/client';
13
13
 
14
14
  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
15
- export const main = (root: ManifestContext, ctx: ManifestContext) => {
15
+ export const main = (ctx: ManifestContext) => {
16
+ const log = LogUtil.logger('client.main');
17
+ const client = new CompilerClient(ctx, log);
18
+ const buildFolders = [ctx.build.outputFolder, ctx.build.compilerFolder];
19
+
20
+ /** Main entry point for compilation */
21
+ const compile = async (op: CompilerMode, logLevel: CompilerLogLevel, setupOnly = false): Promise<void> => {
22
+ LogUtil.initLogs(ctx, logLevel ?? 'info');
23
+
24
+ const server = await new CompilerServer(ctx, op).listen();
25
+
26
+ // Wait for build to be ready
27
+ if (server) {
28
+ log('debug', 'Start Server');
29
+ await server.processEvents(async function* (signal) {
30
+ const changed = await CompilerSetup.setup(ctx);
31
+ if (!setupOnly) {
32
+ yield* CompilerRunner.runProcess(ctx, changed, op, signal);
33
+ }
34
+ });
35
+ log('debug', 'End Server');
36
+ } else {
37
+ log('info', 'Server already running, waiting for initial compile to complete');
38
+ const ctrl = new AbortController();
39
+ LogUtil.consumeProgressEvents(() => client.fetchEvents('progress', { until: ev => !!ev.complete, signal: ctrl.signal }));
40
+ await client.waitForState(['compile-end', 'watch-start'], 'Successfully built');
41
+ ctrl.abort();
42
+ }
43
+ };
44
+
16
45
  const ops = {
17
46
  /** Stop the server */
18
47
  async stop(): Promise<void> {
19
- if (await fetch(`${ctx.compilerUrl}/stop`).then(v => v.ok, () => false)) {
20
- console.log(`Stopped server ${ctx.workspacePath}: [${ctx.compilerUrl}]`);
48
+ if (await client.stop()) {
49
+ console.log(`Stopped server ${ctx.workspace.path}: ${client}`);
21
50
  } else {
22
- console.log(`Server not running ${ctx.workspacePath}: [${ctx.compilerUrl}]`);
51
+ console.log(`Server not running ${ctx.workspace.path}: ${client}`);
23
52
  }
24
53
  },
25
54
 
55
+ /** Restart the server */
56
+ async restart(): Promise<void> { await client.stop().then(() => ops.watch()); },
57
+
26
58
  /** Get server info */
27
- info(): Promise<CompilerServerInfo> {
28
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
29
- return fetch(ctx.compilerUrl).then(v => v.json(), () => ({ state: 'Server not running' })) as Promise<CompilerServerInfo>;
30
- },
59
+ info: (): Promise<CompilerServerInfo | undefined> => client.info(),
31
60
 
32
61
  /** Clean the server */
33
62
  async clean(): Promise<void> {
34
- const folders = [ctx.outputFolder, ctx.compilerFolder];
35
- if (await fetch(`${ctx.compilerUrl}/clean`).then(v => v.ok, () => false)) {
36
- return console.log(`Clean triggered ${ctx.workspacePath}:`, folders);
63
+ if (await client.clean()) {
64
+ return console.log(`Clean triggered ${ctx.workspace.path}:`, buildFolders);
65
+ } else {
66
+ await Promise.all(buildFolders.map(f => fs.rm(path.resolve(ctx.workspace.path, f), { force: true, recursive: true })));
67
+ return console.log(`Cleaned ${ctx.workspace.path}:`, buildFolders);
68
+ }
69
+ },
70
+
71
+ /** Stream events */
72
+ events: async (type: string, handler: (ev: unknown) => unknown): Promise<void> => {
73
+ LogUtil.initLogs(ctx, 'error');
74
+ if (type === 'change' || type === 'log' || type === 'progress' || type === 'state') {
75
+ for await (const ev of client.fetchEvents(type)) { await handler(ev); }
37
76
  } else {
38
- await Promise.all(folders.map(f => fs.rm(path.resolve(ctx.workspacePath, f), { force: true, recursive: true })));
39
- return console.log(`Cleaned ${ctx.workspacePath}:`, folders);
77
+ throw new Error(`Unknown event type: ${type}`);
40
78
  }
41
79
  },
42
80
 
43
- /** Main entry point for compilation */
44
- async compile(op: CompilerOp, setupOnly = false): Promise<(mod: string) => Promise<unknown>> {
45
- LogUtil.initLogs(ctx, op === 'run' ? 'error' : 'info');
81
+ /** Build the project */
82
+ async build(): Promise<void> { await compile('build', 'info'); },
46
83
 
47
- const server = await new CompilerServer(root, op).listen();
84
+ /** Build and watch the project */
85
+ async watch(): Promise<void> { await compile('watch', 'info'); },
48
86
 
49
- // Wait for build to be ready
50
- if (server) {
51
- await server.processEvents(async function* (signal) {
52
- const { changed, manifest } = await CompilerSetup.setup(root);
53
- if (!setupOnly) {
54
- yield* CompilerRunner.runProcess(root, manifest, changed, op, signal);
55
- } else {
56
- yield* [];
57
- }
58
- });
59
- } else {
60
- await CompilerClientUtil.waitForBuild(root);
87
+ /** Build and return a loader */
88
+ async getLoader(): Promise<(mod: string) => Promise<unknown>> {
89
+ // Short circuit if we can
90
+ if (!(await client.isWatching())) {
91
+ await compile('build', 'error');
61
92
  }
62
93
  return CommonUtil.moduleLoader(ctx);
63
94
  },
64
95
 
65
96
  /** Manifest entry point */
66
97
  async manifest(output?: string, prod?: boolean): Promise<void> {
67
- await ops.compile('run', true);
98
+ await compile('build', 'error', true);
68
99
  await CompilerSetup.exportManifest(ctx, output, prod); return;
69
100
  }
70
101
  };
71
102
  return ops;
72
- };
73
-
103
+ };