@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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  <!-- This file was generated by @travetto/doc and should not be modified directly -->
2
- <!-- Please modify https://github.com/travetto/travetto/tree/main/module/compiler/doc.ts and execute "npx trv doc" to rebuild -->
2
+ <!-- Please modify https://github.com/travetto/travetto/tree/main/module/compiler/DOC.ts and execute "npx trv doc" to rebuild -->
3
3
  # Compiler
4
- ## Node-integration of Typescript Compiler with advanced functionality for detecting changes in classes and methods.
4
+ ## Compiler
5
5
 
6
6
  **Install: @travetto/compiler**
7
7
  ```bash
@@ -17,9 +17,12 @@ This module expands upon [Typescript](https://typescriptlang.org), with suppleme
17
17
  * Support for detecting changes in sources files at runtime
18
18
  * Allows for hot-reloading of classes during development
19
19
  * Utilizes `es2015` `Proxy`s to allow for swapping out implementation at runtime
20
-
21
20
  Additionally, there is support for common AST transformations via [Transformation](https://github.com/travetto/travetto/tree/main/module/transformer#readme "Functionality for AST transformations, with transformer registration, and general utils")
22
-
23
21
  ## Debugging
24
-
25
22
  When dealing with transformers, logging is somewhat tricky as the compiler executes before the code is loaded. To that end, the file `compiler.log` is created in the cache directory during the compilation process. This is a location that transformers should be free to log to, for debugging, and any additional feedback.
23
+
24
+ ## CLI
25
+
26
+ The module provides the ability to clear the compilation cache to handle any inconsistencies that may arise.
27
+
28
+ TODO: Describe cli behavior
package/__index__.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './src/compiler';
2
+ export * from './src/state';
3
+ export * from './src/util';
package/bin/trv.js ADDED
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+
3
+ // @ts-check
4
+ import fs from 'fs/promises';
5
+ import path from 'path';
6
+ import { createRequire } from 'module';
7
+
8
+ import { getManifestContext } from '@travetto/manifest/bin/context.js';
9
+
10
+ const VALID_OPS = { watch: 'watch', build: 'build', clean: 'clean', manifest: 'manifest' };
11
+
12
+ /**
13
+ * @param {import('@travetto/manifest').ManifestContext} ctx
14
+ * @return {Promise<import('@travetto/compiler/support/launcher')>}
15
+ */
16
+ const $getLauncher = async (ctx) => {
17
+ const compPkg = createRequire(path.resolve('node_modules')).resolve('@travetto/compiler/package.json');
18
+ const files = [];
19
+
20
+ for (const file of ['support/launcher.js', 'support/transpile.js', 'package.json']) {
21
+ const target = path.resolve(ctx.workspacePath, ctx.compilerFolder, 'node_modules', '@travetto/compiler', file);
22
+ const src = compPkg.replace('package.json', file.replace(/[.]js$/, '.ts'));
23
+
24
+ const targetTime = await fs.stat(target).then(s => Math.max(s.mtimeMs, s.ctimeMs)).catch(() => 0);
25
+ const srcTime = await fs.stat(src).then(s => Math.max(s.mtimeMs, s.ctimeMs));
26
+ // If stale
27
+ if (srcTime > targetTime) {
28
+ const ts = (await import('typescript')).default;
29
+ const module = ctx.moduleType === 'module' ? ts.ModuleKind.ESNext : ts.ModuleKind.CommonJS;
30
+ await fs.mkdir(path.dirname(target), { recursive: true });
31
+ const text = await fs.readFile(src, 'utf8');
32
+ if (file.endsWith('.js')) {
33
+ let content = ts.transpile(text, {
34
+ target: ts.ScriptTarget.ES2020, module, esModuleInterop: true, allowSyntheticDefaultImports: true
35
+ });
36
+ if (ctx.moduleType === 'module') {
37
+ content = content.replace(/^((?:im|ex)port .*from '[.][^']+)(')/mg, (_, a, b) => `${a}.js${b}`)
38
+ .replace(/^(import [^\n]*from '[^.][^\n/]+[/][^\n/]+[/][^\n']+)(')/mg, (_, a, b) => `${a}.js${b}`);
39
+ }
40
+ await fs.writeFile(target, content, 'utf8');
41
+ } else {
42
+ const pkg = JSON.parse(text);
43
+ pkg.type = ctx.moduleType;
44
+ await fs.writeFile(target, JSON.stringify(pkg, null, 2), 'utf8');
45
+ }
46
+ // Compile
47
+ }
48
+ files.push(target);
49
+ }
50
+
51
+ try { return await require(files[0]); }
52
+ catch { return import(files[0]); }
53
+ };
54
+
55
+ /**
56
+ * Parse arguments
57
+ * @param {string[]} args
58
+ * @returns {{ op?: keyof typeof VALID_OPS, clean?: boolean, outputPath?: string, env?: string }}
59
+ */
60
+ function parseArgs(args) {
61
+ const op = VALID_OPS[args.find(x => !x.startsWith('-')) ?? ''];
62
+ return {
63
+ op,
64
+ clean: args.includes('--clean') || args.includes('-c'),
65
+ ...(op === 'manifest' ? { outputPath: args[1], env: args[2] } : {})
66
+ };
67
+ }
68
+
69
+ const exec = async () => {
70
+ const ctx = await getManifestContext();
71
+ const { op, outputPath, env, ...flags } = parseArgs(process.argv.slice(2));
72
+
73
+ // Clean if needed
74
+ if (op === 'clean' || (op && flags.clean)) {
75
+ for (const f of [ctx.outputFolder, ctx.compilerFolder]) {
76
+ await fs.rm(path.resolve(ctx.workspacePath, f), { force: true, recursive: true });
77
+ }
78
+ }
79
+
80
+ if (op === 'clean') { // Clean needs to not attempt to compile/load launcher
81
+ return console.log(`Cleaned ${ctx.workspacePath}: [${ctx.outputFolder}, ${ctx.compilerFolder}]`);
82
+ }
83
+
84
+ const { compile, launchMain, exportManifest } = await $getLauncher(ctx);
85
+
86
+ switch (op) {
87
+ case 'manifest': return exportManifest(ctx, outputPath ?? '', env);
88
+ case 'watch':
89
+ case 'build': return compile(ctx, op);
90
+ default:
91
+ await compile(ctx, op);
92
+ return launchMain(ctx);
93
+ }
94
+ };
95
+
96
+ exec();
package/package.json CHANGED
@@ -1,16 +1,11 @@
1
1
  {
2
2
  "name": "@travetto/compiler",
3
- "displayName": "Compiler",
4
- "version": "3.0.0-rc.1",
5
- "description": "Node-integration of Typescript Compiler with advanced functionality for detecting changes in classes and methods.",
3
+ "version": "3.0.0-rc.12",
4
+ "description": "Compiler",
6
5
  "keywords": [
7
- "tsc",
8
- "typescript",
9
6
  "compiler",
10
- "caching",
11
- "change-detection",
12
- "real-time",
13
- "travetto"
7
+ "travetto",
8
+ "typescript"
14
9
  ],
15
10
  "homepage": "https://travetto.io",
16
11
  "license": "MIT",
@@ -18,21 +13,43 @@
18
13
  "email": "travetto.framework@gmail.com",
19
14
  "name": "Travetto Framework"
20
15
  },
16
+ "type": "module",
21
17
  "files": [
22
- "index.ts",
18
+ "__index__.ts",
23
19
  "src",
24
- "support"
20
+ "bin",
21
+ "support",
22
+ "tsconfig.trv.json"
25
23
  ],
26
- "main": "index.ts",
24
+ "main": "__index__.ts",
25
+ "bin": {
26
+ "trv": "bin/trv.js"
27
+ },
27
28
  "repository": {
28
29
  "url": "https://github.com/travetto/travetto.git",
29
30
  "directory": "module/compiler"
30
31
  },
31
32
  "dependencies": {
32
- "@travetto/base": "^3.0.0-rc.0",
33
- "@travetto/transformer": "^3.0.0-rc.1",
34
- "@travetto/watch": "^3.0.0-rc.0"
33
+ "@parcel/watcher": "^2.1.0",
34
+ "@travetto/manifest": "^3.0.0-rc.7",
35
+ "@travetto/terminal": "^3.0.0-rc.6",
36
+ "@travetto/transformer": "^3.0.0-rc.10"
37
+ },
38
+ "peerDependencies": {
39
+ "@travetto/cli": "^3.0.0-rc.9"
40
+ },
41
+ "peerDependenciesMeta": {
42
+ "@travetto/cli": {
43
+ "optional": true
44
+ }
45
+ },
46
+ "travetto": {
47
+ "displayName": "Compiler",
48
+ "profiles": [
49
+ "compile"
50
+ ]
35
51
  },
52
+ "private": false,
36
53
  "publishConfig": {
37
54
  "access": "public"
38
55
  }
package/src/compiler.ts CHANGED
@@ -1,187 +1,185 @@
1
- import { EventEmitter } from 'events';
2
- import * as sourceMapSupport from 'source-map-support';
3
- import * as ts from 'typescript';
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';
10
-
11
- import { SourceHost } from './host';
12
- import { TransformerManager } from './transformer';
1
+ import util from 'util';
2
+ import { install } from 'source-map-support';
3
+ import ts from 'typescript';
4
+ import fs from 'fs/promises';
5
+
6
+ import { GlobalTerminal, TerminalProgressEvent } from '@travetto/terminal';
7
+ import { RootIndex, watchFolders } from '@travetto/manifest';
8
+ import { TransformerManager } from '@travetto/transformer';
9
+
10
+ import { CompilerUtil } from './util';
11
+ import { CompilerState } from './state';
12
+ import type { CompilerLogEvent } from '../support/transpile';
13
+
14
+ export type TransformerProvider = {
15
+ init(checker: ts.TypeChecker): void;
16
+ get(): ts.CustomTransformers | undefined;
17
+ };
18
+
19
+ type EmitError = Error | readonly ts.Diagnostic[];
20
+ type Emitter = (file: string, newProgram?: boolean) => Promise<EmitError | undefined>;
21
+ type EmitEvent = { file: string, i: number, total: number, err?: EmitError };
22
+
23
+ function log(level: 'info' | 'debug', message: string, ...args: unknown[]): void {
24
+ if (process.send) {
25
+ const ev: CompilerLogEvent = [level, util.format(message, ...args)];
26
+ process.send(ev);
27
+ } else {
28
+ // eslint-disable-next-line no-console
29
+ console[level](message, ...args);
30
+ }
31
+ }
13
32
 
14
- type FileListener = (name: string) => void;
15
- type EventType = 'added' | 'removed' | 'changed';
33
+ const debug = log.bind(null, 'debug');
34
+ const info = log.bind(null, 'info');
16
35
 
17
36
  /**
18
- * Compilation orchestrator, interfaces with watching, unloading, emitting and delegates appropriately
37
+ * Compilation support
19
38
  */
20
- @Dynamic('@travetto/compiler/support/dynamic.compiler')
21
- class $Compiler {
22
-
23
- #program: ts.Program | undefined;
24
- #transformerManager = new TransformerManager();
25
- #emitter = new EventEmitter();
26
- #host = new SourceHost();
27
-
28
- active = false;
39
+ export class Compiler {
29
40
 
30
41
  /**
31
- * Build typescript program
32
- *
33
- * @param forFile If this file is new, force a recompilation
42
+ * Run compiler as a main entry point
34
43
  */
35
- #getProgram(forFile?: string): ts.Program {
44
+ static async main(): Promise<void> {
45
+ const [dirty, watch] = process.argv.slice(2);
46
+ install();
47
+ const dirtyFiles = (await fs.readFile(dirty, 'utf8')).split(/\n/).filter(x => !!x);
48
+ return new Compiler(dirtyFiles).run(watch === 'true');
49
+ }
36
50
 
37
- const rootFiles = this.#host.getRootFiles();
51
+ #state: CompilerState;
52
+ #dirtyFiles: string[];
38
53
 
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;
54
+ constructor(dirtyFiles: string[]) {
55
+ this.#state = new CompilerState(RootIndex.manifest);
56
+ this.#dirtyFiles = dirtyFiles[0] === '*' ?
57
+ this.#state.getAllFiles() :
58
+ dirtyFiles.map(f => this.#state.resolveInput(f));
59
+ }
60
+
61
+ get state(): CompilerState {
62
+ return this.#state;
53
63
  }
54
64
 
55
65
  /**
56
- * Perform actual transpilation
66
+ * Watches local modules
57
67
  */
58
- #transpile(filename: string, force = false): string {
59
- if (force || !AppCache.hasEntry(filename)) {
60
- console.debug('Emitting', { filename: filename.replace(PathUtil.cwd, '.') });
61
-
62
- 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()
71
- );
72
-
73
- TranspileUtil.checkTranspileErrors(filename, result.diagnostics);
74
- } catch (err) {
75
- if (!(err instanceof Error)) {
76
- throw err;
77
- }
78
- const errContent = TranspileUtil.transpileError(filename, err);
79
- this.#host.contents.set(filename, errContent);
68
+ async #watchLocalModules(emit: Emitter): Promise<() => Promise<void>> {
69
+ const emitWithError = async (file: string): Promise<void> => {
70
+ const err = await emit(file, true);
71
+ if (err) {
72
+ info('Compilation Error', CompilerUtil.buildTranspileError(file, err));
73
+ } else {
74
+ info(`Compiled ${file.split('node_modules/')[1]}`);
80
75
  }
81
- // Save writing for typescript program (`writeFile`)
82
- } else {
83
- this.#host.fetchFile(filename);
84
- }
85
-
86
- return this.#host.contents.get(filename)!;
76
+ };
77
+ const watcher = this.state.getWatcher({
78
+ create: emitWithError,
79
+ update: emitWithError,
80
+ delete: (outputFile) => fs.unlink(outputFile).catch(() => { })
81
+ });
82
+ return watchFolders(RootIndex.getLocalInputFolders(), watcher, {
83
+ filter: ev => ev.file.endsWith('.ts') || ev.file.endsWith('.js'),
84
+ ignore: ['node_modules']
85
+ });
87
86
  }
88
87
 
89
- /**
90
- * Get program
91
- * @private
92
- */
93
- getProgram(): ts.Program {
94
- return this.#getProgram();
88
+ async createTransformerProvider(): Promise<TransformerProvider> {
89
+ return TransformerManager.create(this.state.transformers);
95
90
  }
96
91
 
97
92
  /**
98
- * Initialize the compiler
93
+ * Compile in a single pass, only emitting dirty files
99
94
  */
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
- }
95
+ async getCompiler(): Promise<Emitter> {
96
+ let program: ts.Program;
113
97
 
114
- ModuleManager.onUnload((f, unlink) => this.#host.unload(f, unlink)); // Remove source
98
+ const transformers = await this.createTransformerProvider();
99
+ const options = await this.state.getCompilerOptions();
100
+ const host = this.state.getCompilerHost(options);
115
101
 
116
- // Update source map support to read from transpiler cache
117
- sourceMapSupport.install({
118
- retrieveFile: p => this.#host.contents.get(PathUtil.toUnixTs(p))!
119
- });
102
+ const emit = async (file: string, needsNewProgram = program === undefined): Promise<EmitError | undefined> => {
103
+ try {
104
+ if (needsNewProgram) {
105
+ program = ts.createProgram({ rootNames: this.#state.getAllFiles(), host, options, oldProgram: program });
106
+ transformers.init(program.getTypeChecker());
107
+ }
108
+ if (file.endsWith('.json')) {
109
+ host.writeFile(file, host.readFile(file)!, false);
110
+ } else if (file.endsWith('.js')) {
111
+ host.writeFile(file, ts.transpile(host.readFile(file)!, options), false);
112
+ } else {
113
+ const result = program.emit(
114
+ program.getSourceFile(file)!, host.writeFile, undefined, false, transformers.get()
115
+ );
116
+
117
+ if (result.diagnostics?.length) {
118
+ return result.diagnostics;
119
+ }
120
+ }
121
+ } catch (err) {
122
+ if (err instanceof Error) {
123
+ return err;
124
+ } else {
125
+ throw err;
126
+ }
127
+ }
128
+ };
120
129
 
121
- console.debug('Initialized', { duration: (Date.now() - start) / 1000 });
130
+ return emit;
122
131
  }
123
132
 
124
133
  /**
125
- * Reset the compiler
134
+ * Emit all files as a stream
126
135
  */
127
- reset(): void {
128
- if (!EnvUtil.isReadonly()) {
129
- this.#transformerManager.reset();
130
- this.#host.reset();
131
- this.#program = undefined;
136
+ async * emit(files: string[], emitter: Emitter): AsyncIterable<EmitEvent> {
137
+ let i = 0;
138
+ for (const file of files) {
139
+ const err = await emitter(file);
140
+ const imp = file.replace(/.*node_modules\//, '');
141
+ yield { file: imp, i: i += 1, err, total: files.length };
132
142
  }
133
- ModuleManager.clearUnloadHandlers();
134
- SourceIndex.reset();
135
- this.active = false;
143
+ debug(`Compiled ${i} files`);
136
144
  }
137
145
 
138
146
  /**
139
- * Notify of an add/remove/change event
147
+ * Run the compiler
140
148
  */
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
- }
149
+ async run(watch?: boolean): Promise<void> {
150
+ debug('Compilation started');
145
151
 
146
- /**
147
- * Listen for events
148
- */
149
- on(type: EventType, handler: FileListener): this {
150
- this.#emitter.on(type, handler);
151
- return this;
152
- }
152
+ const emitter = await this.getCompiler();
153
+ let failed = false;
153
154
 
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);
160
- }
161
- // Load Synchronously
162
- require(filename);
163
- this.notify('added', filename);
164
- }
155
+ debug('Compiler loaded');
165
156
 
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
- }
157
+ const resolveEmittedFile = ({ file, total, i, err }: EmitEvent): TerminalProgressEvent => {
158
+ if (err) {
159
+ failed = true;
160
+ console.error(CompilerUtil.buildTranspileError(file, err));
161
+ }
162
+ return { idx: i, total, text: `Compiling [%idx/%total] -- ${file}` };
163
+ };
164
+
165
+ if (this.#dirtyFiles.length) {
166
+ await GlobalTerminal.trackProgress(this.emit(this.#dirtyFiles, emitter), resolveEmittedFile, { position: 'bottom' });
167
+ if (failed) {
168
+ debug('Compilation failed');
169
+ process.exit(1);
170
+ } else {
171
+ debug('Compilation succeeded');
172
+ }
173
+ }
173
174
 
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);
175
+ if (watch) {
176
+ if (!this.#dirtyFiles.length) {
177
+ const resolved = this.state.resolveInput(RootIndex.getModule('@travetto/manifest')!.files.src[0].sourceFile);
178
+ await emitter(resolved, true);
179
+ }
180
+ info('Watch is ready');
181
+ await this.#watchLocalModules(emitter);
182
+ await new Promise(r => setTimeout(r, 1000 * 60 * 60 * 24));
183
183
  }
184
184
  }
185
- }
186
-
187
- export const Compiler = new $Compiler();
185
+ }