@travetto/compiler 3.0.0-rc.1 → 3.0.0-rc.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/state.ts ADDED
@@ -0,0 +1,211 @@
1
+ import ts from 'typescript';
2
+ import { readFileSync } from 'fs';
3
+
4
+ import { path, ManifestModuleUtil, ManifestModule, ManifestModuleFileType, ManifestRoot, WatchEvent } from '@travetto/manifest';
5
+
6
+ import { CompilerUtil } from './util';
7
+ import { TranspileUtil } from '../support/transpile';
8
+
9
+ const validFile = (type: ManifestModuleFileType): boolean => type === 'ts' || type === 'package-json' || type === 'js';
10
+
11
+ export class CompilerState {
12
+
13
+ #inputFiles: Set<string>;
14
+ #inputToSource = new Map<string, string>();
15
+ #stagedOutputToOutput = new Map<string, string>();
16
+ #inputToOutput = new Map<string, string | undefined>();
17
+ #inputDirectoryToSource = new Map<string, string>();
18
+ #sourceInputOutput = new Map<string, { source: string, input: string, stagedOutput?: string, output?: string, module: ManifestModule }>();
19
+
20
+ #sourceContents = new Map<string, string | undefined>();
21
+ #sourceFileObjects = new Map<string, ts.SourceFile>();
22
+ #sourceHashes = new Map<string, number>();
23
+
24
+ #manifest: ManifestRoot;
25
+ #modules: ManifestModule[];
26
+ #transformers: string[];
27
+
28
+ constructor(manifest: ManifestRoot) {
29
+ this.#manifest = manifest;
30
+ this.#modules = Object.values(this.#manifest.modules);
31
+ this.#inputFiles = new Set(this.#modules.flatMap(
32
+ x => [
33
+ ...x.files.bin ?? [],
34
+ ...x.files.src ?? [],
35
+ ...x.files.support ?? [],
36
+ ...x.files.doc ?? [],
37
+ ...x.files.test ?? [],
38
+ ...x.files.$index ?? [],
39
+ ...x.files.$package ?? []
40
+ ]
41
+ .filter(([file, type]) => validFile(type) || type === 'typings')
42
+ .map(([f]) => this.registerInput(x, f))
43
+ ));
44
+
45
+ this.#transformers = this.#modules.flatMap(
46
+ x => (x.files.$transformer ?? []).map(([f]) =>
47
+ path.resolve(manifest.workspacePath, x.sourceFolder, f)
48
+ )
49
+ );
50
+ }
51
+
52
+ async getCompilerOptions(): Promise<ts.CompilerOptions> {
53
+ return {
54
+ ...await TranspileUtil.getCompilerOptions(this.#manifest),
55
+ outDir: this.#manifest.workspacePath, // Force to root
56
+ };
57
+ }
58
+
59
+ resolveInput(file: string): string {
60
+ return this.#sourceInputOutput.get(file)!.input;
61
+ }
62
+
63
+ registerInput(module: ManifestModule, moduleFile: string): string {
64
+ const relativeInput = `${module.outputFolder}/${moduleFile}`;
65
+ const sourceFile = path.toPosix(path.resolve(this.#manifest.workspacePath, module.sourceFolder, moduleFile));
66
+ const sourceFolder = path.dirname(sourceFile);
67
+ const inputFile = path.resolve(this.#manifest.workspacePath, '##', relativeInput); // Ensure input is isolated
68
+ const inputFolder = path.dirname(inputFile);
69
+ const fileType = ManifestModuleUtil.getFileType(moduleFile);
70
+ const outputFile = fileType === 'typings' ?
71
+ undefined :
72
+ path.resolve(
73
+ this.#manifest.workspacePath,
74
+ this.#manifest.outputFolder,
75
+ CompilerUtil.inputToOutput(relativeInput)
76
+ );
77
+
78
+ // Rewrite stagedOutput to final output form
79
+ const stagedOutputFile = CompilerUtil.inputToOutput(inputFile);
80
+
81
+ this.#inputToSource.set(inputFile, sourceFile);
82
+ this.#sourceInputOutput.set(sourceFile, { source: sourceFile, input: inputFile, stagedOutput: stagedOutputFile, output: outputFile, module });
83
+ this.#inputToOutput.set(inputFile, outputFile);
84
+ this.#inputDirectoryToSource.set(inputFolder, sourceFolder);
85
+
86
+ if (stagedOutputFile) {
87
+ this.#stagedOutputToOutput.set(stagedOutputFile, outputFile!);
88
+ this.#stagedOutputToOutput.set(`${stagedOutputFile}.map`, `${outputFile!}.map`);
89
+ }
90
+
91
+ return inputFile;
92
+ }
93
+
94
+ removeInput(inputFile: string): void {
95
+ const source = this.#inputToSource.get(inputFile)!;
96
+ const { stagedOutput } = this.#sourceInputOutput.get(source)!;
97
+ this.#stagedOutputToOutput.delete(stagedOutput!);
98
+ this.#sourceInputOutput.delete(source);
99
+ this.#inputToSource.delete(inputFile);
100
+ this.#inputToOutput.delete(inputFile);
101
+ this.#inputFiles.delete(inputFile);
102
+ }
103
+
104
+ resetInputSource(inputFile: string): void {
105
+ this.#sourceFileObjects.delete(inputFile);
106
+ this.#sourceContents.delete(inputFile);
107
+ }
108
+
109
+ get modules(): ManifestModule[] {
110
+ return this.#modules;
111
+ }
112
+
113
+ get transformers(): string[] {
114
+ return this.#transformers;
115
+ }
116
+
117
+ getAllFiles(): string[] {
118
+ return [...this.#inputFiles];
119
+ }
120
+
121
+ // Build watcher
122
+ getWatcher(handler: {
123
+ create: (inputFile: string) => void;
124
+ update: (inputFile: string) => void;
125
+ delete: (outputFile: string) => void;
126
+ }): (ev: WatchEvent, folder: string) => void {
127
+ const mods = Object.fromEntries(this.modules.map(x => [path.resolve(this.#manifest.workspacePath, x.sourceFolder), x]));
128
+ return ({ file: sourceFile, action }: WatchEvent, folder: string): void => {
129
+ const mod = mods[folder];
130
+ const moduleFile = sourceFile.includes(mod.sourceFolder) ? sourceFile.split(`${mod.sourceFolder}/`)[1] : sourceFile;
131
+ switch (action) {
132
+ case 'create': {
133
+ const fileType = ManifestModuleUtil.getFileType(moduleFile);
134
+ if (validFile(fileType)) {
135
+ const hash = CompilerUtil.naiveHash(readFileSync(sourceFile, 'utf8'));
136
+ const input = this.registerInput(mod, moduleFile);
137
+ this.#sourceHashes.set(sourceFile, hash);
138
+ handler.create(input);
139
+ }
140
+ break;
141
+ }
142
+ case 'update': {
143
+ const io = this.#sourceInputOutput.get(sourceFile);
144
+ if (io) {
145
+ const hash = CompilerUtil.naiveHash(readFileSync(sourceFile, 'utf8'));
146
+ if (this.#sourceHashes.get(sourceFile) !== hash) {
147
+ this.resetInputSource(io.input);
148
+ this.#sourceHashes.set(sourceFile, hash);
149
+ handler.update(io.input);
150
+ }
151
+ }
152
+ break;
153
+ }
154
+ case 'delete': {
155
+ const io = this.#sourceInputOutput.get(sourceFile);
156
+ if (io) {
157
+ this.removeInput(io.input);
158
+ if (io.output) {
159
+ handler.delete(io.output);
160
+ }
161
+ }
162
+ }
163
+ }
164
+ };
165
+ }
166
+
167
+ // ts.CompilerHost
168
+ getCompilerHost(options: ts.CompilerOptions): ts.CompilerHost {
169
+ const host: ts.CompilerHost = {
170
+ getCanonicalFileName: (file: string): string => file,
171
+ getCurrentDirectory: path.cwd,
172
+ getDefaultLibFileName: (opts: ts.CompilerOptions): string => ts.getDefaultLibFileName(opts),
173
+ getNewLine: (): string => ts.sys.newLine,
174
+ useCaseSensitiveFileNames: (): boolean => ts.sys.useCaseSensitiveFileNames,
175
+ getDefaultLibLocation: (): string => path.dirname(ts.getDefaultLibFilePath(options)),
176
+ fileExists: (inputFile: string): boolean => this.#inputToSource.has(inputFile) || ts.sys.fileExists(inputFile),
177
+ directoryExists: (inputFolder: string): boolean => this.#inputDirectoryToSource.has(inputFolder) || ts.sys.directoryExists(inputFolder),
178
+ readFile: (inputFile: string): string | undefined => {
179
+ const res = this.#sourceContents.get(inputFile) ?? ts.sys.readFile(this.#inputToSource.get(inputFile) ?? inputFile);
180
+ this.#sourceContents.set(inputFile, res);
181
+ return res;
182
+ },
183
+ writeFile: (
184
+ outputFile: string,
185
+ text: string,
186
+ bom: boolean,
187
+ onError?: (message: string) => void,
188
+ sourceFiles?: readonly ts.SourceFile[],
189
+ data?: ts.WriteFileCallbackData
190
+ ): void => {
191
+ if (outputFile.endsWith('package.json')) {
192
+ text = CompilerUtil.rewritePackageJSON(this.#manifest, text, options);
193
+ } else if (!options.inlineSourceMap && options.sourceMap && outputFile.endsWith('.map')) {
194
+ text = CompilerUtil.rewriteSourceMap(this.#manifest.workspacePath, text, f => this.#sourceInputOutput.get(this.#inputToSource.get(f)!));
195
+ } else if (options.inlineSourceMap && CompilerUtil.isSourceMapUrlPosData(data)) {
196
+ text = CompilerUtil.rewriteInlineSourceMap(this.#manifest.workspacePath, text, f => this.#sourceInputOutput.get(this.#inputToSource.get(f)!), data);
197
+ }
198
+ outputFile = this.#stagedOutputToOutput.get(outputFile) ?? outputFile;
199
+ ts.sys.writeFile(outputFile, text, bom);
200
+ },
201
+ getSourceFile: (inputFile: string, language: ts.ScriptTarget, __onErr?: unknown): ts.SourceFile => {
202
+ if (!this.#sourceFileObjects.has(inputFile)) {
203
+ const content = host.readFile(inputFile)!;
204
+ this.#sourceFileObjects.set(inputFile, ts.createSourceFile(inputFile, content ?? '', language));
205
+ }
206
+ return this.#sourceFileObjects.get(inputFile)!;
207
+ }
208
+ };
209
+ return host;
210
+ }
211
+ }
package/src/util.ts ADDED
@@ -0,0 +1,143 @@
1
+ import ts from 'typescript';
2
+
3
+ import { ManifestRoot, Package, path } from '@travetto/manifest';
4
+
5
+ type InputToSource = (inputFile: 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
+ * Map input file to output format, generally converting ts extensions to js
17
+ * @param file
18
+ * @returns
19
+ */
20
+ static inputToOutput(file: string): string {
21
+ return file.replace(/[.][tj]s$/, '.js');
22
+ }
23
+
24
+ /**
25
+ * Determines if write callback data has sourcemap information
26
+ * @param data
27
+ * @returns
28
+ */
29
+ static isSourceMapUrlPosData(data?: ts.WriteFileCallbackData): data is { sourceMapUrlPos: number } {
30
+ return data !== undefined && data !== null && typeof data === 'object' && ('sourceMapUrlPos' in data);
31
+ }
32
+
33
+ /**
34
+ * Rewrite's sourcemap locations to real folders
35
+ * @returns
36
+ */
37
+ static rewriteSourceMap(root: string, text: string, inputToSource: InputToSource): string {
38
+ const data: { sourceRoot: string, sources: string[] } = JSON.parse(text);
39
+ const src = path.resolve(data.sourceRoot, data.sources[0]);
40
+
41
+ const { source: file } = inputToSource(src) ?? {};
42
+ if (file) {
43
+ data.sourceRoot = root;
44
+ data.sources = [file];
45
+ text = JSON.stringify(data);
46
+ }
47
+
48
+ return text;
49
+ }
50
+
51
+ /**
52
+ * Rewrite's inline sourcemap locations to real folders
53
+ * @param text
54
+ * @param inputToSource
55
+ * @param writeData
56
+ * @returns
57
+ */
58
+ static rewriteInlineSourceMap(
59
+ root: string,
60
+ text: string,
61
+ inputToSource: InputToSource,
62
+ { sourceMapUrlPos }: ts.WriteFileCallbackData & { sourceMapUrlPos: number }
63
+ ): string {
64
+ const sourceMapUrl = text.substring(sourceMapUrlPos);
65
+ const [prefix, sourceMapData] = sourceMapUrl.split('base64,');
66
+ const rewritten = this.rewriteSourceMap(root, Buffer.from(sourceMapData, 'base64url').toString('utf8'), inputToSource);
67
+ return [
68
+ text.substring(0, sourceMapUrlPos),
69
+ prefix,
70
+ 'base64,',
71
+ Buffer.from(rewritten, 'utf8').toString('base64url')
72
+ ].join('');
73
+ }
74
+
75
+ /**
76
+ * Rewrites the package.json to target .js files instead of .ts files, and pins versions
77
+ * @param manifest
78
+ * @param file
79
+ * @param text
80
+ * @returns
81
+ */
82
+ static rewritePackageJSON(manifest: ManifestRoot, text: string, opts: ts.CompilerOptions): string {
83
+ const pkg: Package = JSON.parse(text);
84
+ if (pkg.files) {
85
+ pkg.files = pkg.files.map(x => this.inputToOutput(x));
86
+ }
87
+ if (pkg.main) {
88
+ pkg.main = this.inputToOutput(pkg.main);
89
+ }
90
+ pkg.type = opts.module !== ts.ModuleKind.CommonJS ? 'module' : 'commonjs';
91
+ for (const key of ['devDependencies', 'dependencies', 'peerDependencies'] as const) {
92
+ if (key in pkg) {
93
+ for (const dep of Object.keys(pkg[key] ?? {})) {
94
+ if (dep in manifest.modules) {
95
+ pkg[key]![dep] = manifest.modules[dep].version;
96
+ }
97
+ }
98
+ }
99
+ }
100
+ return JSON.stringify(pkg, null, 2);
101
+ }
102
+
103
+ /**
104
+ * Build transpilation error
105
+ * @param filename The name of the file
106
+ * @param diagnostics The diagnostic errors
107
+ */
108
+ static buildTranspileError(filename: string, diagnostics: Error | readonly ts.Diagnostic[]): Error {
109
+ if (diagnostics instanceof Error) {
110
+ return diagnostics;
111
+ }
112
+
113
+ const errors: string[] = diagnostics.slice(0, 5).map(diag => {
114
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
115
+ const message = ts.flattenDiagnosticMessageText(diag.messageText, '\n');
116
+ if (diag.file) {
117
+ const { line, character } = diag.file.getLineAndCharacterOfPosition(diag.start!);
118
+ return ` @ ${diag.file.fileName.replace(nativeCwd, '.')}(${line + 1}, ${character + 1}): ${message}`;
119
+ } else {
120
+ return ` ${message}`;
121
+ }
122
+ });
123
+
124
+ if (diagnostics.length > 5) {
125
+ errors.push(`${diagnostics.length - 5} more ...`);
126
+ }
127
+ return new Error(`Transpiling ${filename.replace(nativeCwd, '.')} failed: \n${errors.join('\n')}`);
128
+ }
129
+
130
+ /**
131
+ * Naive hashing
132
+ */
133
+ static naiveHash(text: string): number {
134
+ let hash = 5381;
135
+
136
+ for (let i = 0; i < text.length; i++) {
137
+ // eslint-disable-next-line no-bitwise
138
+ hash = (hash * 33) ^ text.charCodeAt(i);
139
+ }
140
+
141
+ return Math.abs(hash);
142
+ }
143
+ }
@@ -0,0 +1,2 @@
1
+ import { Compiler } from '../src/compiler';
2
+ Compiler.main();
@@ -0,0 +1,160 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { Module } from 'module';
4
+
5
+ import type { ManifestContext } from '@travetto/manifest';
6
+ import { TranspileUtil } from './transpile';
7
+
8
+ const SOURCE_SEED = ['package.json', 'index.ts', '__index__.ts', 'src', 'support', 'bin'];
9
+ const PRECOMPILE_MODS = ['@travetto/terminal', '@travetto/manifest', '@travetto/transformer', '@travetto/compiler'];
10
+
11
+ const importManifest = (ctx: ManifestContext): Promise<typeof import('@travetto/manifest')> =>
12
+ import(path.resolve(ctx.workspacePath, ctx.compilerFolder, 'node_modules', '@travetto/manifest/__index__.js'));
13
+
14
+ /**
15
+ * Recompile folder if stale
16
+ */
17
+ async function compileIfStale(ctx: ManifestContext, scope: string, mod: string, seed: string[]): Promise<string[]> {
18
+ const files = await TranspileUtil.getModuleSources(ctx, mod, seed);
19
+ const changes = files.filter(x => x.stale).map(x => x.input);
20
+ const out: string[] = [];
21
+
22
+ try {
23
+ await TranspileUtil.withLogger(scope, { args: [mod], basic: false }, async log => {
24
+ if (files.some(f => f.stale)) {
25
+ log('debug', 'Starting');
26
+ for (const file of files.filter(x => x.stale)) {
27
+ await TranspileUtil.transpileFile(ctx, file.input, file.output);
28
+ }
29
+ if (changes.length) {
30
+ out.push(...changes.map(x => `${mod}/${x}`));
31
+ log('debug', `Source changed: ${changes.join(', ')}`);
32
+ }
33
+ log('debug', 'Completed');
34
+ } else {
35
+ log('debug', 'Skipped');
36
+ }
37
+ });
38
+ } catch (err) {
39
+ console.error(err);
40
+ }
41
+ return out;
42
+ }
43
+
44
+ /**
45
+ * Run the compiler
46
+ */
47
+ export async function compile(ctx: ManifestContext, op?: 'watch' | 'build'): Promise<void> {
48
+ let changes = 0;
49
+
50
+ await TranspileUtil.withLogger('precompile', async () => {
51
+ for (const mod of PRECOMPILE_MODS) {
52
+ changes += (await compileIfStale(ctx, 'precompile', mod, SOURCE_SEED)).length;
53
+ }
54
+ });
55
+
56
+ const { ManifestUtil, ManifestDeltaUtil } = await importManifest(ctx);
57
+
58
+ const manifest = await TranspileUtil.withLogger('manifest', async () => ManifestUtil.buildManifest(ctx));
59
+
60
+ await TranspileUtil.withLogger('transformers', async () => {
61
+ for (const mod of Object.values(manifest.modules).filter(m => m.files.$transformer?.length)) {
62
+ changes += (await compileIfStale(ctx, 'transformers', mod.name, ['package.json', ...mod.files.$transformer!.map(x => x[0])])).length;
63
+ }
64
+ });
65
+
66
+ const delta = await TranspileUtil.withLogger('delta', async log => {
67
+ if (changes) {
68
+ log('debug', 'Skipping, everything changed');
69
+ return [{ type: 'changed', file: '*', module: ctx.mainModule } as const];
70
+ } else {
71
+ return ManifestDeltaUtil.produceDelta(ctx, manifest);
72
+ }
73
+ });
74
+
75
+ if (changes) {
76
+ await fs.rm(path.resolve(ctx.workspacePath, ctx.outputFolder), { recursive: true, force: true });
77
+ TranspileUtil.log('reset', [], 'info', 'Clearing output due to compiler changes');
78
+ }
79
+
80
+ // Write manifest
81
+ await TranspileUtil.withLogger('manifest', async log => {
82
+ await ManifestUtil.writeManifest(ctx, manifest);
83
+ log('debug', `Wrote manifest ${ctx.mainModule}`);
84
+
85
+ // Update all manifests
86
+ if (delta.length && ctx.monoRepo && !ctx.mainFolder) {
87
+ const names: string[] = [];
88
+ const mods = Object.values(manifest.modules).filter(x => x.local && x.name !== ctx.mainModule);
89
+ for (const mod of mods) {
90
+ await ManifestUtil.rewriteManifest(path.resolve(ctx.workspacePath, mod.sourceFolder));
91
+ names.push(mod.name);
92
+ }
93
+ log('debug', `Changes triggered ${delta.map(x => `${x.type}:${x.module}:${x.file}`)}`);
94
+ log('debug', `Rewrote monorepo manifests [changes=${delta.length}] ${names.join(', ')}`);
95
+ }
96
+ });
97
+
98
+ await TranspileUtil.withLogger('compile', { args: [], basic: false }, async log => {
99
+ const changed = delta.filter(x => x.type === 'added' || x.type === 'changed');
100
+ log('debug', `Started action=${op} changed=${changed.map(x => `${x.module}/${x.file}`)}`);
101
+ if (changed.length || op === 'watch') {
102
+ await TranspileUtil.runCompiler(ctx, manifest, changed, op === 'watch');
103
+ log('debug', 'Finished');
104
+ } else {
105
+ log('debug', 'Skipped');
106
+ }
107
+ });
108
+
109
+ if (op === 'build') {
110
+ TranspileUtil.log('build', [], 'info', 'Successfully built');
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Export manifests
116
+ */
117
+ export async function exportManifest(ctx: ManifestContext, output?: string, env = 'dev'): Promise<string | undefined> {
118
+ const { ManifestUtil } = await importManifest(ctx);
119
+ const manifest = await ManifestUtil.buildManifest(ctx);
120
+
121
+ // If in prod mode, only include std modules
122
+ if (/^prod/i.test(env)) {
123
+ manifest.modules = Object.fromEntries(
124
+ Object.values(manifest.modules)
125
+ .filter(x => x.profiles.includes('std'))
126
+ .map(m => [m.name, m])
127
+ );
128
+ // Mark output folder/workspace path as portable
129
+ manifest.outputFolder = '';
130
+ manifest.workspacePath = '';
131
+ }
132
+ if (output) {
133
+ if (!output.endsWith('.json')) {
134
+ output = path.resolve(output, 'manifest.json');
135
+ }
136
+
137
+ await TranspileUtil.writeTextFile(output, JSON.stringify(manifest));
138
+ TranspileUtil.log('manifest', [], 'info', `Wrote manifest ${output}`);
139
+ return output;
140
+ } else {
141
+ console.log(JSON.stringify(manifest, null, 2));
142
+ return;
143
+ }
144
+ }
145
+
146
+ export async function launchMain(ctx: ManifestContext): Promise<void> {
147
+ // Rewriting node_path
148
+ const nodeOut = path.resolve(ctx.workspacePath, ctx.outputFolder, 'node_modules');
149
+ const og = process.env.NODE_PATH;
150
+ process.env.NODE_PATH = [nodeOut, og].join(path.delimiter);
151
+ // @ts-expect-error
152
+ Module._initPaths();
153
+ process.env.NODE_PATH = og; // Restore
154
+
155
+ process.env.TRV_MANIFEST = path.resolve(nodeOut, ctx.mainModule);
156
+
157
+ // TODO: Externalize somehow?
158
+ const cliMain = path.join(nodeOut, '@travetto/cli/support/cli.js');
159
+ return await import(cliMain);
160
+ }