@travetto/compiler 3.0.0-rc.2 → 3.0.0-rc.21
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/README.md +8 -5
- package/__index__.ts +6 -0
- package/bin/trv.js +71 -0
- package/package.json +32 -15
- package/src/compiler.ts +106 -147
- package/src/log.ts +18 -0
- package/src/state.ts +213 -0
- package/src/types.ts +14 -0
- package/src/util.ts +147 -0
- package/src/watch.ts +142 -0
- package/support/compiler-entry.ts +2 -0
- package/support/launcher.ts +139 -0
- package/support/lock-pinger.ts +25 -0
- package/support/lock.ts +237 -0
- package/support/log.ts +31 -0
- package/support/transpile.ts +202 -0
- package/tsconfig.trv.json +23 -0
- package/LICENSE +0 -21
- package/index.ts +0 -3
- package/src/host.ts +0 -142
- package/src/transformer.ts +0 -75
- package/support/dynamic.compiler.ts +0 -72
- package/support/phase.init.ts +0 -13
- package/support/phase.reset.ts +0 -10
package/src/state.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
|
|
4
|
+
import { path, ManifestModuleUtil, ManifestModule, ManifestRoot, ManifestIndex } from '@travetto/manifest';
|
|
5
|
+
import { TransformerManager } from '@travetto/transformer';
|
|
6
|
+
|
|
7
|
+
import { CompilerUtil } from './util';
|
|
8
|
+
import { TranspileUtil } from '../support/transpile';
|
|
9
|
+
import { CompileStateEntry } from './types';
|
|
10
|
+
|
|
11
|
+
export class CompilerState implements ts.CompilerHost {
|
|
12
|
+
|
|
13
|
+
static async get(idx: ManifestIndex): Promise<CompilerState> {
|
|
14
|
+
return new CompilerState().init(idx);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private constructor() { }
|
|
18
|
+
|
|
19
|
+
#rootDir = path.resolve(os.tmpdir(), '_');
|
|
20
|
+
#outputPath: string;
|
|
21
|
+
#inputFiles = new Set<string>();
|
|
22
|
+
#inputDirectoryToSource = new Map<string, string>();
|
|
23
|
+
#inputToEntry = new Map<string, CompileStateEntry>();
|
|
24
|
+
#sourceToEntry = new Map<string, CompileStateEntry>();
|
|
25
|
+
#outputToEntry = new Map<string, CompileStateEntry>();
|
|
26
|
+
|
|
27
|
+
#sourceContents = new Map<string, string | undefined>();
|
|
28
|
+
#sourceFileObjects = new Map<string, ts.SourceFile>();
|
|
29
|
+
|
|
30
|
+
#manifestIndex: ManifestIndex;
|
|
31
|
+
#manifest: ManifestRoot;
|
|
32
|
+
#modules: ManifestModule[];
|
|
33
|
+
#transformerManager: TransformerManager;
|
|
34
|
+
#compilerOptions: ts.CompilerOptions;
|
|
35
|
+
|
|
36
|
+
async init(idx: ManifestIndex): Promise<this> {
|
|
37
|
+
this.#manifestIndex = idx;
|
|
38
|
+
this.#manifest = idx.manifest;
|
|
39
|
+
this.#outputPath = path.resolve(this.#manifest.workspacePath, this.#manifest.outputFolder);
|
|
40
|
+
this.#modules = Object.values(this.#manifest.modules);
|
|
41
|
+
|
|
42
|
+
// Register all inputs
|
|
43
|
+
for (const x of this.#modules) {
|
|
44
|
+
const files = [
|
|
45
|
+
...x.files.bin ?? [],
|
|
46
|
+
...x.files.src ?? [],
|
|
47
|
+
...x.files.support ?? [],
|
|
48
|
+
...x.files.doc ?? [],
|
|
49
|
+
...x.files.test ?? [],
|
|
50
|
+
...x.files.$index ?? [],
|
|
51
|
+
...x.files.$package ?? []
|
|
52
|
+
];
|
|
53
|
+
for (const [file, type] of files) {
|
|
54
|
+
if (CompilerUtil.validFile(type) || type === 'typings') {
|
|
55
|
+
this.registerInput(x, file);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.#transformerManager = await TransformerManager.create(this.#manifestIndex);
|
|
61
|
+
|
|
62
|
+
this.#compilerOptions = {
|
|
63
|
+
...await TranspileUtil.getCompilerOptions(this.#manifest),
|
|
64
|
+
rootDir: this.#rootDir,
|
|
65
|
+
outDir: this.#outputPath
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get manifest(): ManifestRoot {
|
|
72
|
+
return this.#manifest;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get manifestIndex(): ManifestIndex {
|
|
76
|
+
return this.#manifestIndex;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
resolveOutputFile(file: string): string {
|
|
80
|
+
return path.resolve(this.#manifest.workspacePath, this.#manifest.outputFolder, file);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getArbitraryInputFile(): string {
|
|
84
|
+
return this.getBySource(this.#manifestIndex.getModule('@travetto/manifest')!.files.src[0].sourceFile)!.input;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
createProgram(oldProgram?: ts.Program): ts.Program {
|
|
88
|
+
const prog = ts.createProgram({ rootNames: this.getAllFiles(), host: this, options: this.#compilerOptions, oldProgram });
|
|
89
|
+
this.#transformerManager.init(prog.getTypeChecker());
|
|
90
|
+
return prog;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
writeInputFile(program: ts.Program, inputFile: string): ts.EmitResult | undefined {
|
|
94
|
+
if (inputFile.endsWith('.json')) {
|
|
95
|
+
this.writeFile(this.#inputToEntry.get(inputFile)!.output!, this.readFile(inputFile)!, false);
|
|
96
|
+
} else if (inputFile.endsWith('.js')) {
|
|
97
|
+
this.writeFile(this.#inputToEntry.get(inputFile)!.output!, ts.transpile(this.readFile(inputFile)!, this.#compilerOptions), false);
|
|
98
|
+
} else if (inputFile.endsWith('.ts')) {
|
|
99
|
+
return program.emit(
|
|
100
|
+
program.getSourceFile(inputFile)!,
|
|
101
|
+
(...args) => this.writeFile(...args), undefined, false,
|
|
102
|
+
this.#transformerManager.get()
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getBySource(sourceFile: string): CompileStateEntry | undefined {
|
|
108
|
+
return this.#sourceToEntry.get(sourceFile);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
registerInput(module: ManifestModule, moduleFile: string): string {
|
|
112
|
+
const relativeInput = `${module.outputFolder}/${moduleFile}`;
|
|
113
|
+
const sourceFile = path.toPosix(path.resolve(this.#manifest.workspacePath, module.sourceFolder, moduleFile));
|
|
114
|
+
const sourceFolder = path.dirname(sourceFile);
|
|
115
|
+
const inputFile = path.resolve(this.#rootDir, relativeInput); // Ensure input is isolated
|
|
116
|
+
const inputFolder = path.dirname(inputFile);
|
|
117
|
+
const fileType = ManifestModuleUtil.getFileType(moduleFile);
|
|
118
|
+
const outputFile = fileType === 'typings' ?
|
|
119
|
+
undefined :
|
|
120
|
+
path.resolve(this.#outputPath, CompilerUtil.inputToOutput(relativeInput));
|
|
121
|
+
|
|
122
|
+
const entry = { source: sourceFile, input: inputFile, output: outputFile, module, relativeInput };
|
|
123
|
+
|
|
124
|
+
this.#inputToEntry.set(inputFile, entry);
|
|
125
|
+
this.#sourceToEntry.set(sourceFile, entry);
|
|
126
|
+
this.#inputDirectoryToSource.set(inputFolder, sourceFolder);
|
|
127
|
+
|
|
128
|
+
if (outputFile) {
|
|
129
|
+
this.#outputToEntry.set(outputFile, entry);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.#inputFiles.add(inputFile);
|
|
133
|
+
|
|
134
|
+
return inputFile;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
removeInput(inputFile: string): void {
|
|
138
|
+
const { output, source } = this.#inputToEntry.get(inputFile)!;
|
|
139
|
+
if (output) {
|
|
140
|
+
this.#outputToEntry.delete(output);
|
|
141
|
+
}
|
|
142
|
+
this.#sourceToEntry.delete(source);
|
|
143
|
+
this.#inputToEntry.delete(inputFile);
|
|
144
|
+
this.#inputFiles.delete(inputFile);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
resetInputSource(inputFile: string): void {
|
|
148
|
+
this.#sourceFileObjects.delete(inputFile);
|
|
149
|
+
this.#sourceContents.delete(inputFile);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
getAllFiles(): string[] {
|
|
153
|
+
return [...this.#inputFiles];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Used for translating non-project files (e.g. dependencies/node_modules) to the faux-root
|
|
158
|
+
* that is used during compilation.
|
|
159
|
+
*/
|
|
160
|
+
translateRawInput(input: string): string {
|
|
161
|
+
return input.replace(this.#rootDir, this.#manifest.workspacePath);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* Start Compiler Host */
|
|
165
|
+
getCanonicalFileName(file: string): string { return file; }
|
|
166
|
+
getCurrentDirectory(): string { return path.cwd(); }
|
|
167
|
+
getDefaultLibFileName(opts: ts.CompilerOptions): string { return ts.getDefaultLibFileName(opts); }
|
|
168
|
+
getNewLine(): string { return ts.sys.newLine; }
|
|
169
|
+
useCaseSensitiveFileNames(): boolean { return ts.sys.useCaseSensitiveFileNames; }
|
|
170
|
+
getDefaultLibLocation(): string { return path.dirname(ts.getDefaultLibFilePath(this.#compilerOptions)); }
|
|
171
|
+
|
|
172
|
+
fileExists(inputFile: string): boolean {
|
|
173
|
+
return this.#inputToEntry.has(inputFile) || ts.sys.fileExists(this.translateRawInput(inputFile));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
directoryExists(inputDir: string): boolean {
|
|
177
|
+
return this.#inputDirectoryToSource.has(inputDir) || ts.sys.directoryExists(this.translateRawInput(inputDir));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
writeFile(
|
|
181
|
+
outputFile: string,
|
|
182
|
+
text: string,
|
|
183
|
+
bom: boolean,
|
|
184
|
+
onError?: (message: string) => void,
|
|
185
|
+
sourceFiles?: readonly ts.SourceFile[],
|
|
186
|
+
data?: ts.WriteFileCallbackData
|
|
187
|
+
): void {
|
|
188
|
+
if (outputFile.endsWith('package.json')) {
|
|
189
|
+
text = CompilerUtil.rewritePackageJSON(this.#manifest, text);
|
|
190
|
+
} else if (!this.#compilerOptions.inlineSourceMap && this.#compilerOptions.sourceMap && outputFile.endsWith('.map')) {
|
|
191
|
+
text = CompilerUtil.rewriteSourceMap(this.#manifest, text, f => this.#outputToEntry.get(f.replace(/[.]map$/, ''))!);
|
|
192
|
+
} else if (this.#compilerOptions.inlineSourceMap && CompilerUtil.isSourceMapUrlPosData(data)) {
|
|
193
|
+
text = CompilerUtil.rewriteInlineSourceMap(this.#manifest, text, f => this.#outputToEntry.get(f)!, data);
|
|
194
|
+
}
|
|
195
|
+
ts.sys.writeFile(outputFile, text, bom);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
readFile(inputFile: string): string | undefined {
|
|
199
|
+
const res = this.#sourceContents.get(inputFile) ?? ts.sys.readFile(
|
|
200
|
+
this.#inputToEntry.get(inputFile)?.source ?? this.translateRawInput(inputFile)
|
|
201
|
+
);
|
|
202
|
+
this.#sourceContents.set(inputFile, res);
|
|
203
|
+
return res;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
getSourceFile(inputFile: string, language: ts.ScriptTarget): ts.SourceFile {
|
|
207
|
+
if (!this.#sourceFileObjects.has(inputFile)) {
|
|
208
|
+
const content = this.readFile(inputFile)!;
|
|
209
|
+
this.#sourceFileObjects.set(inputFile, ts.createSourceFile(inputFile, content ?? '', language));
|
|
210
|
+
}
|
|
211
|
+
return this.#sourceFileObjects.get(inputFile)!;
|
|
212
|
+
}
|
|
213
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type ts from 'typescript';
|
|
2
|
+
|
|
3
|
+
import type { ManifestModule } from '@travetto/manifest';
|
|
4
|
+
|
|
5
|
+
export type CompileEmitError = Error | readonly ts.Diagnostic[];
|
|
6
|
+
export type CompileEmitter = (file: string, newProgram?: boolean) => Promise<CompileEmitError | undefined>;
|
|
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 };
|
|
9
|
+
|
|
10
|
+
export type CompileWatcherHandler = {
|
|
11
|
+
create: (inputFile: string) => void;
|
|
12
|
+
update: (inputFile: string) => void;
|
|
13
|
+
delete: (outputFile: string) => void;
|
|
14
|
+
};
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
|
|
3
|
+
import { ManifestContext, ManifestModuleFileType, ManifestRoot, Package, path } from '@travetto/manifest';
|
|
4
|
+
|
|
5
|
+
type OutputToSource = (outputFile: string) => ({ source: string } | undefined);
|
|
6
|
+
export type FileWatchEvent = { type: 'create' | 'delete' | 'update', path: string };
|
|
7
|
+
|
|
8
|
+
const nativeCwd = process.cwd();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Standard utilities for compiler
|
|
12
|
+
*/
|
|
13
|
+
export class CompilerUtil {
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Determine if this is a manifest file we care about
|
|
17
|
+
*/
|
|
18
|
+
static validFile = (type: ManifestModuleFileType): boolean => type === 'ts' || type === 'package-json' || type === 'js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Map input file to output format, generally converting ts extensions to js
|
|
22
|
+
* @param file
|
|
23
|
+
* @returns
|
|
24
|
+
*/
|
|
25
|
+
static inputToOutput(file: string): string {
|
|
26
|
+
return file.replace(/[.][tj]s$/, '.js');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Determines if write callback data has sourcemap information
|
|
31
|
+
* @param data
|
|
32
|
+
* @returns
|
|
33
|
+
*/
|
|
34
|
+
static isSourceMapUrlPosData(data?: ts.WriteFileCallbackData): data is { sourceMapUrlPos: number } {
|
|
35
|
+
return data !== undefined && data !== null && typeof data === 'object' && ('sourceMapUrlPos' in data);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Rewrite's sourcemap locations to real folders
|
|
40
|
+
* @returns
|
|
41
|
+
*/
|
|
42
|
+
static rewriteSourceMap(ctx: ManifestContext, text: string, outputToSource: OutputToSource): string {
|
|
43
|
+
const data: { sourceRoot?: string, sources: string[] } = JSON.parse(text);
|
|
44
|
+
const output = this.inputToOutput(path.resolve(ctx.workspacePath, ctx.outputFolder, data.sources[0]));
|
|
45
|
+
const { source: file } = outputToSource(output) ?? {};
|
|
46
|
+
|
|
47
|
+
if (file) {
|
|
48
|
+
delete data.sourceRoot;
|
|
49
|
+
data.sources = [file];
|
|
50
|
+
text = JSON.stringify(data);
|
|
51
|
+
}
|
|
52
|
+
return text;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Rewrite's inline sourcemap locations to real folders
|
|
57
|
+
* @param text
|
|
58
|
+
* @param outputToSource
|
|
59
|
+
* @param writeData
|
|
60
|
+
* @returns
|
|
61
|
+
*/
|
|
62
|
+
static rewriteInlineSourceMap(
|
|
63
|
+
ctx: ManifestContext,
|
|
64
|
+
text: string,
|
|
65
|
+
outputToSource: OutputToSource,
|
|
66
|
+
{ sourceMapUrlPos }: ts.WriteFileCallbackData & { sourceMapUrlPos: number }
|
|
67
|
+
): string {
|
|
68
|
+
const sourceMapUrl = text.substring(sourceMapUrlPos);
|
|
69
|
+
const [prefix, sourceMapData] = sourceMapUrl.split('base64,');
|
|
70
|
+
const rewritten = this.rewriteSourceMap(ctx, Buffer.from(sourceMapData, 'base64url').toString('utf8'), outputToSource);
|
|
71
|
+
return [
|
|
72
|
+
text.substring(0, sourceMapUrlPos),
|
|
73
|
+
prefix,
|
|
74
|
+
'base64,',
|
|
75
|
+
Buffer.from(rewritten, 'utf8').toString('base64url')
|
|
76
|
+
].join('');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Rewrites the package.json to target .js files instead of .ts files, and pins versions
|
|
81
|
+
* @param manifest
|
|
82
|
+
* @param file
|
|
83
|
+
* @param text
|
|
84
|
+
* @returns
|
|
85
|
+
*/
|
|
86
|
+
static rewritePackageJSON(manifest: ManifestRoot, text: string): string {
|
|
87
|
+
const pkg: Package = JSON.parse(text);
|
|
88
|
+
if (pkg.files) {
|
|
89
|
+
pkg.files = pkg.files.map(x => this.inputToOutput(x));
|
|
90
|
+
}
|
|
91
|
+
if (pkg.main) {
|
|
92
|
+
pkg.main = this.inputToOutput(pkg.main);
|
|
93
|
+
}
|
|
94
|
+
pkg.type = manifest.moduleType;
|
|
95
|
+
for (const key of ['devDependencies', 'dependencies', 'peerDependencies'] as const) {
|
|
96
|
+
if (key in pkg) {
|
|
97
|
+
for (const dep of Object.keys(pkg[key] ?? {})) {
|
|
98
|
+
if (dep in manifest.modules) {
|
|
99
|
+
pkg[key]![dep] = manifest.modules[dep].version;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return JSON.stringify(pkg, null, 2);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Build transpilation error
|
|
109
|
+
* @param filename The name of the file
|
|
110
|
+
* @param diagnostics The diagnostic errors
|
|
111
|
+
*/
|
|
112
|
+
static buildTranspileError(filename: string, diagnostics: Error | readonly ts.Diagnostic[]): Error {
|
|
113
|
+
if (diagnostics instanceof Error) {
|
|
114
|
+
return diagnostics;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const errors: string[] = diagnostics.slice(0, 5).map(diag => {
|
|
118
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
119
|
+
const message = ts.flattenDiagnosticMessageText(diag.messageText, '\n');
|
|
120
|
+
if (diag.file) {
|
|
121
|
+
const { line, character } = diag.file.getLineAndCharacterOfPosition(diag.start!);
|
|
122
|
+
return ` @ ${diag.file.fileName.replace(nativeCwd, '.')}(${line + 1}, ${character + 1}): ${message}`;
|
|
123
|
+
} else {
|
|
124
|
+
return ` ${message}`;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (diagnostics.length > 5) {
|
|
129
|
+
errors.push(`${diagnostics.length - 5} more ...`);
|
|
130
|
+
}
|
|
131
|
+
return new Error(`Transpiling ${filename.replace(nativeCwd, '.')} failed: \n${errors.join('\n')}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Naive hashing
|
|
136
|
+
*/
|
|
137
|
+
static naiveHash(text: string): number {
|
|
138
|
+
let hash = 5381;
|
|
139
|
+
|
|
140
|
+
for (let i = 0; i < text.length; i++) {
|
|
141
|
+
// eslint-disable-next-line no-bitwise
|
|
142
|
+
hash = (hash * 33) ^ text.charCodeAt(i);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return Math.abs(hash);
|
|
146
|
+
}
|
|
147
|
+
}
|
package/src/watch.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ManifestContext, ManifestModuleUtil, ManifestUtil, WatchEvent, ManifestModuleFolderType,
|
|
6
|
+
ManifestModuleFileType, path, ManifestModule, watchFolders, WatchEventListener
|
|
7
|
+
} from '@travetto/manifest';
|
|
8
|
+
import { getManifestContext } from '@travetto/manifest/bin/context';
|
|
9
|
+
|
|
10
|
+
import { CompilerState } from './state';
|
|
11
|
+
import { CompilerUtil } from './util';
|
|
12
|
+
import { CompileEmitter, CompileWatcherHandler } from './types';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Utils for watching
|
|
16
|
+
*/
|
|
17
|
+
export class CompilerWatcher {
|
|
18
|
+
|
|
19
|
+
#sourceHashes = new Map<string, number>();
|
|
20
|
+
#manifestContexts = new Map<string, ManifestContext>();
|
|
21
|
+
#dirtyFiles: { modFolder: string, mod: string, moduleFile?: string, folderKey?: ManifestModuleFolderType, type?: ManifestModuleFileType }[] = [];
|
|
22
|
+
#state: CompilerState;
|
|
23
|
+
|
|
24
|
+
constructor(state: CompilerState) {
|
|
25
|
+
this.#state = state;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async #rebuildManifestsIfNeeded(): Promise<void> {
|
|
29
|
+
if (!this.#dirtyFiles.length) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const mods = [...new Set(this.#dirtyFiles.map(x => x.modFolder))];
|
|
33
|
+
const contexts = await Promise.all(mods.map(async folder => {
|
|
34
|
+
if (!this.#manifestContexts.has(folder)) {
|
|
35
|
+
const ctx = await getManifestContext(folder);
|
|
36
|
+
this.#manifestContexts.set(folder, ctx);
|
|
37
|
+
}
|
|
38
|
+
return this.#manifestContexts.get(folder)!;
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
const files = this.#dirtyFiles;
|
|
42
|
+
this.#dirtyFiles = [];
|
|
43
|
+
|
|
44
|
+
for (const ctx of [...contexts, this.#state.manifest]) {
|
|
45
|
+
const newManifest = await ManifestUtil.buildManifest(ctx);
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
if (
|
|
48
|
+
file.folderKey && file.moduleFile && file.type &&
|
|
49
|
+
file.mod in newManifest.modules && file.folderKey in newManifest.modules[file.mod].files
|
|
50
|
+
) {
|
|
51
|
+
newManifest.modules[file.mod].files[file.folderKey]!.push([file.moduleFile, file.type, Date.now()]);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
await ManifestUtil.writeManifest(ctx, newManifest);
|
|
55
|
+
}
|
|
56
|
+
// Reindex
|
|
57
|
+
this.#state.manifestIndex.init(this.#state.manifestIndex.manifestFile);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#getModuleMap(): Record<string, ManifestModule> {
|
|
61
|
+
return Object.fromEntries(
|
|
62
|
+
Object.values(this.#state.manifest.modules).map(x => [path.resolve(this.#state.manifest.workspacePath, x.sourceFolder), x])
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get a watcher for a given compiler state
|
|
68
|
+
* @param state
|
|
69
|
+
* @param handler
|
|
70
|
+
* @returns
|
|
71
|
+
*/
|
|
72
|
+
#getWatcher(handler: CompileWatcherHandler): WatchEventListener {
|
|
73
|
+
const mods = this.#getModuleMap();
|
|
74
|
+
|
|
75
|
+
return async ({ file: sourceFile, action }: WatchEvent, folder: string): Promise<void> => {
|
|
76
|
+
const mod = mods[folder];
|
|
77
|
+
const moduleFile = sourceFile.includes(mod.sourceFolder) ? sourceFile.split(`${mod.sourceFolder}/`)[1] : sourceFile;
|
|
78
|
+
switch (action) {
|
|
79
|
+
case 'create': {
|
|
80
|
+
const fileType = ManifestModuleUtil.getFileType(moduleFile);
|
|
81
|
+
this.#dirtyFiles.push({
|
|
82
|
+
mod: mod.name,
|
|
83
|
+
modFolder: folder,
|
|
84
|
+
moduleFile,
|
|
85
|
+
folderKey: ManifestModuleUtil.getFolderKey(sourceFile),
|
|
86
|
+
type: ManifestModuleUtil.getFileType(sourceFile)
|
|
87
|
+
});
|
|
88
|
+
if (CompilerUtil.validFile(fileType)) {
|
|
89
|
+
await this.#rebuildManifestsIfNeeded();
|
|
90
|
+
|
|
91
|
+
const hash = CompilerUtil.naiveHash(readFileSync(sourceFile, 'utf8'));
|
|
92
|
+
const input = this.#state.registerInput(mod, moduleFile);
|
|
93
|
+
this.#sourceHashes.set(sourceFile, hash);
|
|
94
|
+
handler.create(input);
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case 'update': {
|
|
99
|
+
await this.#rebuildManifestsIfNeeded();
|
|
100
|
+
const entry = this.#state.getBySource(sourceFile);
|
|
101
|
+
if (entry) {
|
|
102
|
+
const hash = CompilerUtil.naiveHash(readFileSync(sourceFile, 'utf8'));
|
|
103
|
+
if (this.#sourceHashes.get(sourceFile) !== hash) {
|
|
104
|
+
this.#state.resetInputSource(entry.input);
|
|
105
|
+
this.#sourceHashes.set(sourceFile, hash);
|
|
106
|
+
handler.update(entry.input);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
case 'delete': {
|
|
112
|
+
const entry = this.#state.getBySource(sourceFile);
|
|
113
|
+
if (entry) {
|
|
114
|
+
this.#state.removeInput(entry.input);
|
|
115
|
+
if (entry.output) {
|
|
116
|
+
this.#dirtyFiles.push({ mod: mod.name, modFolder: folder });
|
|
117
|
+
handler.delete(entry.output);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Watch files based on root index
|
|
127
|
+
*/
|
|
128
|
+
watchFiles(emit: CompileEmitter): Promise<() => Promise<void>> {
|
|
129
|
+
return watchFolders(
|
|
130
|
+
this.#state.manifestIndex.getLocalInputFolderMapping(),
|
|
131
|
+
this.#getWatcher({
|
|
132
|
+
create: emit,
|
|
133
|
+
update: emit,
|
|
134
|
+
delete: (outputFile) => fs.rm(outputFile, { force: true })
|
|
135
|
+
}),
|
|
136
|
+
{
|
|
137
|
+
filter: ev => ev.file.endsWith('.ts') || ev.file.endsWith('.js') || ev.file.endsWith('package.json'),
|
|
138
|
+
ignore: ['node_modules']
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
import type { ManifestContext } from '@travetto/manifest';
|
|
5
|
+
|
|
6
|
+
import { TranspileUtil, CompileResult } from './transpile';
|
|
7
|
+
import { LockManager } from './lock';
|
|
8
|
+
import { LogUtil } from './log';
|
|
9
|
+
|
|
10
|
+
const SOURCE_SEED = ['package.json', 'index.ts', '__index__.ts', 'src', 'support', 'bin'];
|
|
11
|
+
const PRECOMPILE_MODS = ['@travetto/terminal', '@travetto/manifest', '@travetto/transformer', '@travetto/compiler'];
|
|
12
|
+
|
|
13
|
+
const importManifest = (ctx: ManifestContext): Promise<typeof import('@travetto/manifest')> =>
|
|
14
|
+
import(path.resolve(ctx.workspacePath, ctx.compilerFolder, 'node_modules', '@travetto/manifest/__index__.js'));
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Run the compiler
|
|
18
|
+
*/
|
|
19
|
+
async function compile(ctx: ManifestContext, op: 'watch' | 'build' | undefined, onMessage: (msg: unknown) => void): Promise<CompileResult> {
|
|
20
|
+
let changes = 0;
|
|
21
|
+
|
|
22
|
+
await LogUtil.withLogger('precompile', async () => {
|
|
23
|
+
for (const mod of PRECOMPILE_MODS) {
|
|
24
|
+
changes += (await TranspileUtil.compileIfStale(ctx, 'precompile', mod, SOURCE_SEED)).length;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const { ManifestUtil, ManifestDeltaUtil, PackageUtil } = await importManifest(ctx);
|
|
29
|
+
|
|
30
|
+
PackageUtil.clearCache();
|
|
31
|
+
|
|
32
|
+
const manifest = await LogUtil.withLogger('manifest', async () => ManifestUtil.buildManifest(ctx));
|
|
33
|
+
|
|
34
|
+
await LogUtil.withLogger('transformers', async () => {
|
|
35
|
+
for (const mod of Object.values(manifest.modules).filter(m => m.files.$transformer?.length)) {
|
|
36
|
+
changes += (await TranspileUtil.compileIfStale(ctx, 'transformers', mod.name, ['package.json', ...mod.files.$transformer!.map(x => x[0])])).length;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const delta = await LogUtil.withLogger('delta', async log => {
|
|
41
|
+
if (changes) {
|
|
42
|
+
log('debug', 'Skipping, everything changed');
|
|
43
|
+
return [{ type: 'changed', file: '*', module: ctx.mainModule } as const];
|
|
44
|
+
} else {
|
|
45
|
+
return ManifestDeltaUtil.produceDelta(ctx, manifest);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (changes) {
|
|
50
|
+
await LogUtil.withLogger('reset', async log => {
|
|
51
|
+
await fs.rm(path.resolve(ctx.workspacePath, ctx.outputFolder), { recursive: true, force: true });
|
|
52
|
+
log('info', 'Clearing output due to compiler changes');
|
|
53
|
+
}, false);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Write manifest
|
|
57
|
+
await LogUtil.withLogger('manifest', async log => {
|
|
58
|
+
await ManifestUtil.writeManifest(ctx, manifest);
|
|
59
|
+
log('debug', `Wrote manifest ${ctx.mainModule}`);
|
|
60
|
+
|
|
61
|
+
// Update all manifests
|
|
62
|
+
if (delta.length && ctx.monoRepo && !ctx.mainFolder) {
|
|
63
|
+
const names: string[] = [];
|
|
64
|
+
const mods = Object.values(manifest.modules).filter(x => x.local && x.name !== ctx.mainModule);
|
|
65
|
+
for (const mod of mods) {
|
|
66
|
+
await ManifestUtil.rewriteManifest(path.resolve(ctx.workspacePath, mod.sourceFolder));
|
|
67
|
+
names.push(mod.name);
|
|
68
|
+
}
|
|
69
|
+
log('debug', `Changes triggered ${delta.slice(0, 10).map(x => `${x.type}:${x.module}:${x.file}`)}`);
|
|
70
|
+
log('debug', `Rewrote monorepo manifests [changes=${delta.length}] ${names.slice(0, 10).join(', ')}`);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return await LogUtil.withLogger('compile', async log => {
|
|
75
|
+
const changed = delta.filter(x => x.type === 'added' || x.type === 'changed');
|
|
76
|
+
log('debug', `Started action=${op} changed=${changed.slice(0, 10).map(x => `${x.module}/${x.file}`)}`);
|
|
77
|
+
if (changed.length || op === 'watch') {
|
|
78
|
+
const res = await TranspileUtil.runCompiler(ctx, manifest, changed, op === 'watch', onMessage);
|
|
79
|
+
log('debug', 'Finished');
|
|
80
|
+
return res;
|
|
81
|
+
} else {
|
|
82
|
+
log('debug', 'Skipped');
|
|
83
|
+
return 'skipped';
|
|
84
|
+
}
|
|
85
|
+
}, false);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Export manifests
|
|
90
|
+
*/
|
|
91
|
+
async function exportManifest(ctx: ManifestContext, output?: string, env = 'dev'): Promise<void> {
|
|
92
|
+
const { ManifestUtil } = await importManifest(ctx);
|
|
93
|
+
let manifest = await ManifestUtil.buildManifest(ctx);
|
|
94
|
+
|
|
95
|
+
// If in prod mode, only include std modules
|
|
96
|
+
if (/^prod/i.test(env)) {
|
|
97
|
+
manifest = ManifestUtil.createProductionManifest(manifest);
|
|
98
|
+
}
|
|
99
|
+
if (output) {
|
|
100
|
+
output = await ManifestUtil.writeManifestToFile(output, manifest);
|
|
101
|
+
LogUtil.log('manifest', [], 'info', `Wrote manifest ${output}`);
|
|
102
|
+
} else {
|
|
103
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Launch
|
|
109
|
+
*/
|
|
110
|
+
export async function launch(ctx: ManifestContext, root: ManifestContext, op?: 'build' | 'watch' | 'manifest', args: (string | undefined)[] = []): Promise<void> {
|
|
111
|
+
if (op !== 'manifest' && await LockManager.getCompileAction(root, op) === 'build') {
|
|
112
|
+
await LockManager.withLocks(root, async (acquire, release) => {
|
|
113
|
+
let action: CompileResult;
|
|
114
|
+
do {
|
|
115
|
+
acquire(op ?? 'build');
|
|
116
|
+
if (op === 'watch') {
|
|
117
|
+
acquire('build');
|
|
118
|
+
}
|
|
119
|
+
action = await compile(root, op, msg => {
|
|
120
|
+
switch (msg) {
|
|
121
|
+
case 'build-complete': release('build'); break;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
} while (action === 'restart');
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
switch (op) {
|
|
129
|
+
case 'manifest': return exportManifest(ctx, ...args);
|
|
130
|
+
case 'build': return LogUtil.log('build', [], 'info', 'Successfully built');
|
|
131
|
+
case undefined: {
|
|
132
|
+
// TODO: Externalize somehow?
|
|
133
|
+
const outputPath = path.resolve(ctx.workspacePath, ctx.outputFolder);
|
|
134
|
+
process.env.TRV_MANIFEST = path.resolve(outputPath, 'node_modules', ctx.mainModule);
|
|
135
|
+
const cliMain = path.join(outputPath, 'node_modules', '@travetto/cli/support/cli.js');
|
|
136
|
+
return import(cliMain);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { workerData, parentPort } from 'worker_threads';
|
|
2
|
+
import { utimesSync } from 'fs';
|
|
3
|
+
|
|
4
|
+
const data: { files: string[], interval: number } = workerData;
|
|
5
|
+
const files = data.files;
|
|
6
|
+
|
|
7
|
+
const interval = setInterval(() => {
|
|
8
|
+
const now = Date.now() / 1000;
|
|
9
|
+
for (const file of files) {
|
|
10
|
+
try {
|
|
11
|
+
utimesSync(file, now, now);
|
|
12
|
+
} catch { }
|
|
13
|
+
}
|
|
14
|
+
}, data.interval);
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
parentPort?.on('message', val => {
|
|
18
|
+
if (val === 'stop') {
|
|
19
|
+
files.splice(0, files.length);
|
|
20
|
+
clearInterval(interval);
|
|
21
|
+
} else if (val && typeof val === 'object' && 'files' in val && Array.isArray(val.files)) {
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
23
|
+
files.splice(0, files.length, ...val.files as string[]);
|
|
24
|
+
}
|
|
25
|
+
});
|