@travetto/compiler 3.0.0-rc.3 → 3.0.0-rc.6

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/compiler.ts CHANGED
@@ -1,187 +1,187 @@
1
- import { EventEmitter } from 'events';
2
- import * as sourceMapSupport from 'source-map-support';
3
- import * as ts from 'typescript';
1
+ import ts from 'typescript';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
4
 
5
- import { PathUtil, EnvUtil, AppCache } from '@travetto/boot';
6
- import { SourceIndex } from '@travetto/boot/src/internal/source';
7
- import { ModuleManager } from '@travetto/boot/src/internal/module';
8
- import { Dynamic } from '@travetto/base/src/internal/dynamic';
9
- import { TranspileUtil } from '@travetto/boot/src/internal/transpile-util';
5
+ import { ManifestState } from '@travetto/manifest';
6
+ import { GlobalTerminal, TerminalProgressEvent } from '@travetto/terminal';
10
7
 
11
- import { SourceHost } from './host';
12
- import { TransformerManager } from './transformer';
8
+ import { CompilerUtil } from './util';
9
+ import { CompilerState } from './state';
13
10
 
14
- type FileListener = (name: string) => void;
15
- type EventType = 'added' | 'removed' | 'changed';
11
+ export type TransformerProvider = {
12
+ init(checker: ts.TypeChecker): void;
13
+ get(): ts.CustomTransformers | undefined;
14
+ };
15
+
16
+ type EmitError = Error | readonly ts.Diagnostic[];
17
+ type Emitter = (file: string, newProgram?: boolean) => EmitError | undefined;
18
+ type EmitEvent = { file: string, i: number, total: number, err?: EmitError };
16
19
 
17
20
  /**
18
- * Compilation orchestrator, interfaces with watching, unloading, emitting and delegates appropriately
21
+ * Compilation support
19
22
  */
20
- @Dynamic('@travetto/compiler/support/dynamic.compiler')
21
- class $Compiler {
23
+ export class Compiler {
24
+
25
+ #bootTsconfig: string;
26
+ #state: CompilerState;
27
+ #transformers: string[];
28
+
29
+ init(manifestState: ManifestState): this {
30
+ this.#state = new CompilerState(manifestState);
31
+ this.#bootTsconfig = this.#state.resolveModuleFile('@travetto/compiler', 'tsconfig.trv.json');
32
+
33
+ this.#transformers = this.state.modules.flatMap(
34
+ x => (x.files.support ?? [])
35
+ .filter(([f, type]) => type === 'ts' && f.startsWith('support/transformer.'))
36
+ .map(([f]) =>
37
+ path.resolve(
38
+ this.#state.manifest.workspacePath,
39
+ this.#state.manifest.compilerFolder,
40
+ x.output,
41
+ f.replace(/[.][tj]s$/, '.js')
42
+ )
43
+ )
44
+ );
22
45
 
23
- #program: ts.Program | undefined;
24
- #transformerManager = new TransformerManager();
25
- #emitter = new EventEmitter();
26
- #host = new SourceHost();
46
+ return this;
47
+ }
27
48
 
28
- active = false;
49
+ get state(): CompilerState {
50
+ return this.#state;
51
+ }
29
52
 
30
53
  /**
31
- * Build typescript program
32
- *
33
- * @param forFile If this file is new, force a recompilation
54
+ * Watches local modules
34
55
  */
35
- #getProgram(forFile?: string): ts.Program {
56
+ async #watchLocalModules(emit: Emitter): Promise<() => Promise<void>> {
57
+ const folders = this.state.modules.filter(x => x.local).map(x => x.source);
58
+ const emitWithError = (file: string): void => {
59
+ const err = emit(file, true);
60
+ if (err) {
61
+ console.error(CompilerUtil.buildTranspileError(file, err));
62
+ } else {
63
+ console.error('Compiled', file.split('node_modules/')[1]);
64
+ }
65
+ };
66
+ const watcher = this.state.getWatcher({
67
+ create: (inputFile) => emitWithError(inputFile),
68
+ update: (inputFile) => emitWithError(inputFile),
69
+ delete: (outputFile) => fs.unlink(outputFile).catch(() => { })
70
+ });
71
+ return CompilerUtil.fileWatcher(folders, watcher);
72
+ }
36
73
 
37
- const rootFiles = this.#host.getRootFiles();
74
+ async createTransformerProvider(): Promise<TransformerProvider> {
75
+ const { TransformerManager } = await import('@travetto/transformer');
76
+ return TransformerManager.create(this.#transformers, this.state.manifest);
77
+ }
38
78
 
39
- if (!this.#program || (forFile && !rootFiles.has(forFile))) {
40
- console.debug('Loading program', { size: rootFiles.size, src: forFile });
41
- if (forFile) {
42
- rootFiles.add(forFile);
43
- }
44
- this.#program = ts.createProgram({
45
- rootNames: [...rootFiles],
46
- options: TranspileUtil.compilerOptions,
47
- host: this.#host,
48
- oldProgram: this.#program
49
- });
50
- this.#transformerManager.build(this.#program.getTypeChecker());
51
- }
52
- return this.#program;
79
+ async writeRawFile(file: string, contents: string, mode?: string): Promise<void> {
80
+ const outFile = path.resolve(
81
+ this.#state.manifest.workspacePath,
82
+ this.#state.manifest.outputFolder,
83
+ file
84
+ );
85
+ await fs.mkdir(path.dirname(outFile), { recursive: true });
86
+ await fs.writeFile(outFile, contents, { encoding: 'utf8', mode });
87
+ }
88
+
89
+ async outputInit(): Promise<void> {
90
+ // Write manifest
91
+ await this.writeRawFile(this.#state.manifest.manifestFile, JSON.stringify(this.state.manifest));
92
+ // TODO: This needs to be isolated, just like in the bootstrap
93
+ await this.writeRawFile('trv', '#!/bin/sh\nnode node_modules/@travetto/cli/support/main.cli.js $@\n', '755');
94
+ await this.writeRawFile('trv.cmd', 'node node_modules/@travetto/cli/support/main.cli.js %*\n', '755');
53
95
  }
54
96
 
55
97
  /**
56
- * Perform actual transpilation
98
+ * Compile in a single pass, only emitting dirty files
57
99
  */
58
- #transpile(filename: string, force = false): string {
59
- if (force || !AppCache.hasEntry(filename)) {
60
- console.debug('Emitting', { filename: filename.replace(PathUtil.cwd, '.') });
61
-
100
+ async getCompiler(): Promise<Emitter> {
101
+ let program: ts.Program;
102
+
103
+ const transformers = await this.createTransformerProvider();
104
+ const options = await CompilerUtil.getCompilerOptions(
105
+ path.resolve(
106
+ this.#state.manifest.workspacePath,
107
+ this.#state.manifest.outputFolder,
108
+ ),
109
+ this.#bootTsconfig,
110
+ this.#state.manifest.workspacePath
111
+ );
112
+ const host = this.state.getCompilerHost(options);
113
+
114
+ const emit = (file: string, needsNewProgram = program === undefined): EmitError | undefined => {
115
+ if (needsNewProgram) {
116
+ program = ts.createProgram({ rootNames: this.#state.getAllFiles(), host, options, oldProgram: program });
117
+ transformers.init(program.getTypeChecker());
118
+ }
62
119
  try {
63
- const prog = this.#getProgram(filename);
64
-
65
- const result = prog.emit(
66
- prog.getSourceFile(filename),
67
- undefined,
68
- undefined,
69
- false,
70
- this.#transformerManager.getTransformers()
120
+ const result = program.emit(
121
+ program.getSourceFile(file)!, host.writeFile, undefined, false, transformers.get()
71
122
  );
72
123
 
73
- TranspileUtil.checkTranspileErrors(filename, result.diagnostics);
124
+ if (result.diagnostics?.length) {
125
+ return result.diagnostics;
126
+ }
74
127
  } catch (err) {
75
- if (!(err instanceof Error)) {
128
+ if (err instanceof Error) {
129
+ return err;
130
+ } else {
76
131
  throw err;
77
132
  }
78
- const errContent = TranspileUtil.transpileError(filename, err);
79
- this.#host.contents.set(filename, errContent);
80
133
  }
81
- // Save writing for typescript program (`writeFile`)
82
- } else {
83
- this.#host.fetchFile(filename);
84
- }
85
-
86
- return this.#host.contents.get(filename)!;
87
- }
88
-
89
- /**
90
- * Get program
91
- * @private
92
- */
93
- getProgram(): ts.Program {
94
- return this.#getProgram();
95
- }
96
-
97
- /**
98
- * Initialize the compiler
99
- */
100
- async init(): Promise<void> {
101
- if (this.active) {
102
- return;
103
- }
104
-
105
- const start = Date.now();
106
- this.active = true;
107
-
108
- if (!EnvUtil.isReadonly()) {
109
- await this.#transformerManager.init();
110
- // Enhance transpilation, with custom transformations
111
- ModuleManager.setTranspiler(tsf => this.#transpile(tsf));
112
- }
113
-
114
- ModuleManager.onUnload((f, unlink) => this.#host.unload(f, unlink)); // Remove source
115
-
116
- // Update source map support to read from transpiler cache
117
- sourceMapSupport.install({
118
- retrieveFile: p => this.#host.contents.get(PathUtil.toUnixTs(p))!
119
- });
134
+ };
120
135
 
121
- console.debug('Initialized', { duration: (Date.now() - start) / 1000 });
136
+ return emit;
122
137
  }
123
138
 
124
139
  /**
125
- * Reset the compiler
140
+ * Emit all files as a stream
126
141
  */
127
- reset(): void {
128
- if (!EnvUtil.isReadonly()) {
129
- this.#transformerManager.reset();
130
- this.#host.reset();
131
- this.#program = undefined;
142
+ async * emit(files: string[], emitter: Emitter): AsyncIterable<EmitEvent> {
143
+ let i = 0;
144
+ const manifest = this.#state.manifest;
145
+ for (const file of files) {
146
+ const err = emitter(file);
147
+ const outputFile = file
148
+ .replace(/[.]ts$/, '.js')
149
+ .replace(manifest.compilerFolder, manifest.outputFolder);
150
+ yield { file: outputFile, i: i += 1, err, total: files.length };
132
151
  }
133
- ModuleManager.clearUnloadHandlers();
134
- SourceIndex.reset();
135
- this.active = false;
136
152
  }
137
153
 
138
154
  /**
139
- * Notify of an add/remove/change event
155
+ * Run the compiler
140
156
  */
141
- notify(type: EventType, filename: string): void {
142
- console.debug('File Event', { type, filename: filename.replace(PathUtil.cwd, '.') });
143
- this.#emitter.emit(type, filename);
144
- }
145
-
146
- /**
147
- * Listen for events
148
- */
149
- on(type: EventType, handler: FileListener): this {
150
- this.#emitter.on(type, handler);
151
- return this;
152
- }
157
+ async run(watch?: boolean): Promise<void> {
158
+ await this.outputInit();
159
+ const emitter = await this.getCompiler();
160
+ let failed = false;
161
+
162
+ const resolveEmittedFile = ({ file, total, i, err }: EmitEvent): TerminalProgressEvent => {
163
+ if (err) {
164
+ failed = true;
165
+ console.error(CompilerUtil.buildTranspileError(file, err));
166
+ }
167
+ return { idx: i, total, text: `Compiling [%idx/%total] -- ${file.split('node_modules/')[1]}` };
168
+ };
153
169
 
154
- /**
155
- * Unload if file is known
156
- */
157
- added(filename: string): void {
158
- if (filename in require.cache) { // if already loaded
159
- ModuleManager.unload(filename);
170
+ let files = this.state.getDirtyFiles();
171
+ if (!watch && !files.length) {
172
+ files = this.state.getAllFiles();
160
173
  }
161
- // Load Synchronously
162
- require(filename);
163
- this.notify('added', filename);
164
- }
165
174
 
166
- /**
167
- * Handle when a file is removed during watch
168
- */
169
- removed(filename: string): void {
170
- ModuleManager.unload(filename, true);
171
- this.notify('removed', filename);
172
- }
175
+ if (files.length) {
176
+ await GlobalTerminal.trackProgress(this.emit(files, emitter), resolveEmittedFile, { position: 'bottom' });
177
+ if (failed) {
178
+ process.exit(1);
179
+ }
180
+ }
173
181
 
174
- /**
175
- * When a file changes during watch
176
- */
177
- changed(filename: string): void {
178
- if (this.#host.hashChanged(filename)) {
179
- ModuleManager.unload(filename);
180
- // Load Synchronously
181
- require(filename);
182
- this.notify('changed', filename);
182
+ if (watch) {
183
+ await this.#watchLocalModules(emitter);
184
+ await new Promise(r => setTimeout(r, 1000 * 60 * 60 * 24));
183
185
  }
184
186
  }
185
- }
186
-
187
- export const Compiler = new $Compiler();
187
+ }
package/src/state.ts ADDED
@@ -0,0 +1,225 @@
1
+ import ts from 'typescript';
2
+ import { mkdirSync, readFileSync, writeFile } from 'fs';
3
+
4
+ import {
5
+ path,
6
+ ManifestModuleUtil, ManifestDelta, ManifestModule,
7
+ ManifestModuleFileType, ManifestRoot, ManifestState
8
+ } from '@travetto/manifest';
9
+
10
+ import { CompilerUtil, FileWatchEvent } from './util';
11
+
12
+ const validFile = (type: ManifestModuleFileType): boolean => type === 'ts' || type === 'package-json' || type === 'js';
13
+
14
+ export class CompilerState {
15
+
16
+ #inputFiles: Set<string>;
17
+ #relativeInputToSource = new Map<string, { source: string, module: ManifestModule }>();
18
+ #inputToSource = new Map<string, string>();
19
+ #inputToOutput = new Map<string, string | undefined>();
20
+ #inputDirectoryToSource = new Map<string, string>();
21
+ #sourceInputOutput = new Map<string, { input: string, output?: string, relativeInput: string, module: ManifestModule }>();
22
+
23
+ #sourceContents = new Map<string, string | undefined>();
24
+ #sourceFileObjects = new Map<string, ts.SourceFile>();
25
+ #sourceHashes = new Map<string, number>();
26
+
27
+ #manifest: ManifestRoot;
28
+ #delta: ManifestDelta;
29
+ #modules: ManifestModule[];
30
+
31
+ constructor({ manifest, delta }: ManifestState) {
32
+ this.#manifest = manifest;
33
+ this.#delta = delta;
34
+ this.#modules = Object.values(this.#manifest.modules);
35
+ this.#inputFiles = new Set(this.#modules.flatMap(
36
+ x => [
37
+ ...x.files.bin ?? [],
38
+ ...x.files.src ?? [],
39
+ ...x.files.support ?? [],
40
+ ...x.files.doc ?? [],
41
+ ...x.files.test ?? [],
42
+ ...x.files.$index ?? [],
43
+ ...x.files.$package ?? []
44
+ ]
45
+ .filter(([file, type]) => validFile(type) || type === 'typings')
46
+ .map(([f]) => this.registerInput(x, f))
47
+ ));
48
+
49
+ return this;
50
+ }
51
+
52
+ registerInput(module: ManifestModule, moduleFile: string): string {
53
+ const relativeInput = `${module.output}/${moduleFile}`;
54
+ const sourceFile = `${module.source}/${moduleFile}`;
55
+ const sourceFolder = path.dirname(sourceFile);
56
+ const inputFile = path.resolve(relativeInput);
57
+ const inputFolder = path.dirname(inputFile);
58
+ const fileType = ManifestModuleUtil.getFileType(moduleFile);
59
+ const outputFile = fileType === 'typings' ?
60
+ undefined :
61
+ path.resolve(
62
+ this.#manifest.workspacePath,
63
+ this.#manifest.outputFolder,
64
+ (fileType === 'ts' ? relativeInput.replace(/[.]ts$/, '.js') : relativeInput)
65
+ );
66
+
67
+ this.#inputToSource.set(inputFile, sourceFile);
68
+ this.#sourceInputOutput.set(sourceFile, { input: inputFile, output: outputFile, relativeInput, module });
69
+ this.#inputToOutput.set(inputFile, outputFile);
70
+ this.#inputDirectoryToSource.set(inputFolder, sourceFolder);
71
+ this.#relativeInputToSource.set(relativeInput, { source: sourceFile, module });
72
+
73
+ return inputFile;
74
+ }
75
+
76
+ removeInput(inputFile: string): void {
77
+ const source = this.#inputToSource.get(inputFile)!;
78
+ const { relativeInput } = this.#sourceInputOutput.get(source)!;
79
+ this.#sourceInputOutput.delete(source);
80
+ this.#inputToSource.delete(inputFile);
81
+ this.#inputToOutput.delete(inputFile);
82
+ this.#relativeInputToSource.delete(relativeInput);
83
+ this.#inputFiles.delete(inputFile);
84
+ }
85
+
86
+ resetInputSource(inputFile: string): void {
87
+ this.#sourceFileObjects.delete(inputFile);
88
+ this.#sourceContents.delete(inputFile);
89
+ }
90
+
91
+ get manifest(): ManifestRoot {
92
+ return this.#manifest;
93
+ }
94
+
95
+ get modules(): ManifestModule[] {
96
+ return this.#modules;
97
+ }
98
+
99
+ getDirtyFiles(): string[] {
100
+ if (this.#delta && Object.keys(this.#delta).length) { // If we have any changes
101
+ const files: string[] = [];
102
+ for (const [modName, events] of Object.entries(this.#delta)) {
103
+ const mod = this.#manifest.modules[modName];
104
+ for (const { file } of events) {
105
+ const fileType = ManifestModuleUtil.getFileType(file);
106
+ if (validFile(fileType)) {
107
+ files.push(path.resolve(mod.output, file));
108
+ }
109
+ }
110
+ }
111
+ return files;
112
+ } else {
113
+ return [];
114
+ }
115
+ }
116
+
117
+ getAllFiles(): string[] {
118
+ return [...this.#inputFiles];
119
+ }
120
+
121
+ resolveModuleFile(module: string, file: string): string {
122
+ return `${this.modules.find(m => m.name === module)!.source}/${file}`;
123
+ }
124
+
125
+ // Build watcher
126
+ getWatcher(handler: {
127
+ create: (inputFile: string) => void;
128
+ update: (inputFile: string) => void;
129
+ delete: (outputFile: string) => void;
130
+ }): (ev: FileWatchEvent, folder: string) => void {
131
+ const mods = Object.fromEntries(this.modules.map(x => [x.source, x]));
132
+ return ({ path: sourceFile, type }: FileWatchEvent, folder: string): void => {
133
+ const mod = mods[folder];
134
+ const moduleFile = sourceFile.replace(`${mod.source}/`, '');
135
+ switch (type) {
136
+ case 'create': {
137
+ const fileType = ManifestModuleUtil.getFileType(moduleFile);
138
+ if (validFile(fileType)) {
139
+ const hash = CompilerUtil.naiveHash(readFileSync(sourceFile, 'utf8'));
140
+ const input = this.registerInput(mod, moduleFile);
141
+ this.#sourceHashes.set(sourceFile, hash);
142
+ handler.create(input);
143
+ }
144
+ break;
145
+ }
146
+ case 'update': {
147
+ const io = this.#sourceInputOutput.get(sourceFile);
148
+ if (io) {
149
+ const hash = CompilerUtil.naiveHash(readFileSync(sourceFile, 'utf8'));
150
+ if (this.#sourceHashes.get(sourceFile) !== hash) {
151
+ this.resetInputSource(io.input);
152
+ this.#sourceHashes.set(sourceFile, hash);
153
+ handler.update(io.input);
154
+ }
155
+ }
156
+ break;
157
+ }
158
+ case 'delete': {
159
+ const io = this.#sourceInputOutput.get(sourceFile);
160
+ if (io) {
161
+ this.removeInput(io.input);
162
+ if (io.output) {
163
+ handler.delete(io.output);
164
+ }
165
+ }
166
+ }
167
+ }
168
+
169
+ // Update manifest on every change
170
+ writeFile(
171
+ path.resolve(
172
+ this.#manifest.workspacePath,
173
+ this.#manifest.outputFolder,
174
+ this.#manifest.manifestFile
175
+ ),
176
+ JSON.stringify(this.#manifest),
177
+ () => { });
178
+ };
179
+ }
180
+
181
+ // ts.CompilerHost
182
+ getCompilerHost(options: ts.CompilerOptions): ts.CompilerHost {
183
+ const host: ts.CompilerHost = {
184
+ getCanonicalFileName: (file: string): string => file,
185
+ getCurrentDirectory: path.cwd,
186
+ getDefaultLibFileName: (opts: ts.CompilerOptions): string => ts.getDefaultLibFileName(opts),
187
+ getNewLine: (): string => ts.sys.newLine,
188
+ useCaseSensitiveFileNames: (): boolean => ts.sys.useCaseSensitiveFileNames,
189
+ getDefaultLibLocation: (): string => path.dirname(ts.getDefaultLibFilePath(options)),
190
+ fileExists: (inputFile: string): boolean => this.#inputToSource.has(inputFile) || ts.sys.fileExists(inputFile),
191
+ directoryExists: (inputFolder: string): boolean => this.#inputDirectoryToSource.has(inputFolder) || ts.sys.directoryExists(inputFolder),
192
+ readFile: (inputFile: string): string | undefined => {
193
+ const res = this.#sourceContents.get(inputFile) ?? ts.sys.readFile(this.#inputToSource.get(inputFile) ?? inputFile);
194
+ this.#sourceContents.set(inputFile, res);
195
+ return res;
196
+ },
197
+ writeFile: (
198
+ outputFile: string,
199
+ text: string,
200
+ bom: boolean,
201
+ onError?: (message: string) => void,
202
+ sourceFiles?: readonly ts.SourceFile[],
203
+ data?: ts.WriteFileCallbackData
204
+ ): void => {
205
+ mkdirSync(path.dirname(outputFile), { recursive: true });
206
+ if (outputFile.endsWith('package.json')) {
207
+ text = CompilerUtil.rewritePackageJSON(this.manifest, text, options);
208
+ } else if (!options.inlineSourceMap && options.sourceMap && outputFile.endsWith('.map')) {
209
+ text = CompilerUtil.rewriteSourceMap(text, f => this.#relativeInputToSource.get(f));
210
+ } else if (options.inlineSourceMap && CompilerUtil.isSourceMapUrlPosData(data)) {
211
+ text = CompilerUtil.rewriteInlineSourceMap(text, f => this.#relativeInputToSource.get(f), data);
212
+ }
213
+ ts.sys.writeFile(outputFile, text, bom);
214
+ },
215
+ getSourceFile: (inputFile: string, language: ts.ScriptTarget, __onErr?: unknown): ts.SourceFile => {
216
+ if (!this.#sourceFileObjects.has(inputFile)) {
217
+ const content = host.readFile(inputFile)!;
218
+ this.#sourceFileObjects.set(inputFile, ts.createSourceFile(inputFile, content ?? '', language));
219
+ }
220
+ return this.#sourceFileObjects.get(inputFile)!;
221
+ }
222
+ };
223
+ return host;
224
+ }
225
+ }