@travetto/compiler 3.3.2 → 3.4.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.
@@ -0,0 +1,189 @@
1
+ import http from 'http';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+
5
+ import type { ManifestContext } from '@travetto/manifest';
6
+
7
+ import type { CompilerMode, CompilerOp, CompilerServerEvent, CompilerServerEventType, CompilerServerInfo } from '../types';
8
+ import { LogUtil } from '../log';
9
+ import { CompilerClientUtil } from './client';
10
+ import { CommonUtil } from '../util';
11
+
12
+ const log = LogUtil.log.bind(LogUtil, 'compiler-server');
13
+
14
+ /**
15
+ * Compiler Server Class
16
+ */
17
+ export class CompilerServer {
18
+
19
+ #ctx: ManifestContext;
20
+ #server: http.Server;
21
+ #listeners: { res: http.ServerResponse, type: CompilerServerEventType }[] = [];
22
+ #shutdown = new AbortController();
23
+ signal = this.#shutdown.signal;
24
+ info: CompilerServerInfo;
25
+
26
+ constructor(ctx: ManifestContext, op: CompilerOp) {
27
+ this.#ctx = ctx;
28
+ this.info = {
29
+ state: 'startup',
30
+ iteration: Date.now(),
31
+ mode: op === 'run' ? 'build' : op,
32
+ serverPid: process.pid,
33
+ compilerPid: -1,
34
+ path: ctx.workspacePath,
35
+ url: ctx.compilerUrl
36
+ };
37
+
38
+ this.#server = http.createServer({
39
+ keepAlive: true,
40
+ requestTimeout: 1000 * 60 * 60,
41
+ keepAliveTimeout: 1000 * 60 * 60,
42
+ }, (req, res) => this.#onRequest(req, res));
43
+
44
+ // Connect
45
+ process.on('SIGINT', () => this.#shutdown.abort());
46
+ }
47
+
48
+ get mode(): CompilerMode {
49
+ return this.info.mode;
50
+ }
51
+
52
+ isResetEvent(ev: CompilerServerEvent): boolean {
53
+ return ev.type === 'state' && ev.payload.state === 'reset';
54
+ }
55
+
56
+ async #tryListen(attempt = 0): Promise<'ok' | 'running'> {
57
+ const output = await new Promise<'ok' | 'running' | 'retry'>((resolve, reject) => {
58
+ this.#server
59
+ .on('listening', () => resolve('ok'))
60
+ .on('error', async err => {
61
+ if ('code' in err && err.code === 'EADDRINUSE') {
62
+ const info = await CompilerClientUtil.getServerInfo(this.#ctx);
63
+ resolve((info && info.mode === 'build' && this.mode === 'watch') ? 'retry' : 'running');
64
+ } else {
65
+ reject(err);
66
+ }
67
+ });
68
+
69
+ const url = new URL(this.#ctx.compilerUrl);
70
+ setTimeout(() => this.#server.listen(+url.port, url.hostname), 1); // Run async
71
+ });
72
+
73
+ if (output === 'retry') {
74
+ if (attempt >= 5) {
75
+ throw new Error('Unable to verify compilation server');
76
+ }
77
+ log('info', 'Waiting for build to finish, before retrying');
78
+ // Let the server finish
79
+ await CompilerClientUtil.waitForState(this.#ctx, ['close'], this.signal);
80
+ return this.#tryListen(attempt + 1);
81
+ }
82
+
83
+ return output;
84
+ }
85
+
86
+ async #addListener(type: string, res: http.ServerResponse): Promise<void> {
87
+ res.writeHead(200);
88
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
89
+ this.#listeners.push({ res, type: type as 'change' });
90
+ await new Promise(resolve => res.on('close', resolve));
91
+ this.#listeners.splice(this.#listeners.findIndex(x => x.res === res), 1);
92
+ res.end();
93
+ }
94
+
95
+ #emitEvent(ev: CompilerServerEvent): void {
96
+ const msg = `${JSON.stringify(ev.payload)}\n`;
97
+ for (const el of this.#listeners) {
98
+ if (!el.res.closed && el.type === ev.type) {
99
+ el.res.write(msg);
100
+ }
101
+ }
102
+ }
103
+
104
+ async #disconnectActive(): Promise<void> {
105
+ log('info', 'Server disconnect requested');
106
+ this.info.iteration = Date.now();
107
+ await new Promise(r => setTimeout(r, 20));
108
+ this.#server.closeAllConnections(); // Force reconnects
109
+ }
110
+
111
+ async #clean(): Promise<{ clean: boolean }> {
112
+ await Promise.all([this.#ctx.compilerFolder, this.#ctx.outputFolder]
113
+ .map(f => fs.rm(path.resolve(this.#ctx.workspacePath, f), { recursive: true, force: true })));
114
+ return { clean: true };
115
+ }
116
+
117
+ /**
118
+ * Request handler
119
+ */
120
+ async #onRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
121
+ res.setHeader('Content-Type', 'application/json');
122
+
123
+ const [, action, subAction] = new URL(`${this.#ctx.compilerUrl}${req.url}`).pathname.split('/');
124
+
125
+ log('debug', 'Receive request', { action, subAction });
126
+
127
+ let out: unknown;
128
+ switch (action) {
129
+ case 'event': return await this.#addListener(subAction, res);
130
+ case 'stop': out = await this.close(); break;
131
+ case 'clean': out = await this.#clean(); break;
132
+ case 'info':
133
+ default: out = this.info ?? {}; break;
134
+ }
135
+ res.end(JSON.stringify(out));
136
+ }
137
+
138
+ /**
139
+ * Process events
140
+ */
141
+ async processEvents(src: (signal: AbortSignal) => AsyncIterable<CompilerServerEvent>): Promise<void> {
142
+
143
+ CompilerClientUtil.streamLogs(this.#ctx, this.signal); // Send logs to stdout
144
+
145
+ for await (const ev of CommonUtil.restartableEvents(src, this.signal, this.isResetEvent)) {
146
+ this.#emitEvent(ev);
147
+
148
+ if (ev.type === 'state') {
149
+ this.info.state = ev.payload.state;
150
+ if (ev.payload.state === 'init' && ev.payload.extra && 'pid' in ev.payload.extra && typeof ev.payload.extra.pid === 'number') {
151
+ this.info.compilerPid = ev.payload.extra.pid;
152
+ }
153
+ log('info', `State changed: ${this.info.state}`);
154
+ }
155
+ if (this.isResetEvent(ev)) {
156
+ await this.#disconnectActive();
157
+ }
158
+ }
159
+
160
+ // Terminate, after letting all remaining events emit
161
+ setImmediate(() => this.close());
162
+ }
163
+
164
+ /**
165
+ * Close server
166
+ */
167
+ async close(): Promise<unknown> {
168
+ log('info', 'Closing down server');
169
+ setTimeout(async () => {
170
+ this.#shutdown.abort();
171
+ this.#emitEvent({ type: 'state', payload: { state: 'close' } });
172
+ this.#server.unref();
173
+ await new Promise(r => {
174
+ this.#server.close(r);
175
+ setImmediate(() => this.#server.closeAllConnections());
176
+ });
177
+ }, 10);
178
+ return { closing: true };
179
+ }
180
+
181
+ /**
182
+ * Start the server listening
183
+ */
184
+ async listen(): Promise<CompilerServer | undefined> {
185
+ const running = await this.#tryListen() === 'ok';
186
+ log('info', running ? 'Starting server' : 'Server already running under a different process', this.#ctx.compilerUrl);
187
+ return running ? this : undefined;
188
+ }
189
+ }
@@ -0,0 +1,229 @@
1
+ import path from 'path';
2
+ import fs from 'fs/promises';
3
+
4
+ import { type DeltaEvent, type ManifestContext, type ManifestRoot, Package } from '@travetto/manifest';
5
+
6
+ import { LogUtil } from './log';
7
+ import { CommonUtil } from './util';
8
+
9
+ type ModFile = { input: string, output: string, stale: boolean };
10
+
11
+ const SOURCE_SEED = ['package.json', 'index.ts', '__index__.ts', 'src', 'support', 'bin'];
12
+ const PRECOMPILE_MODS = ['@travetto/terminal', '@travetto/manifest', '@travetto/transformer', '@travetto/compiler'];
13
+ const RECENT_STAT = (stat: { ctimeMs: number, mtimeMs: number }): number => Math.max(stat.ctimeMs, stat.mtimeMs);
14
+
15
+ /**
16
+ * Compiler Setup Utilities
17
+ */
18
+ export class CompilerSetup {
19
+
20
+ /**
21
+ * Import a compiled manifest
22
+ */
23
+ static #importManifest = (ctx: ManifestContext): Promise<typeof import('@travetto/manifest')> =>
24
+ import(path.resolve(ctx.workspacePath, ctx.compilerFolder, 'node_modules', '@travetto/manifest/__index__.js'));
25
+
26
+ /** Convert a file to a given ext */
27
+ static #sourceToExtension(inputFile: string, ext: string): string {
28
+ return inputFile.replace(/[.][tj]sx?$/, ext);
29
+ }
30
+
31
+ /**
32
+ * Get the output file name for a given input
33
+ */
34
+ static #sourceToOutputExt(inputFile: string): string {
35
+ return this.#sourceToExtension(inputFile, '.js');
36
+ }
37
+
38
+ /**
39
+ * Output a file, support for ts, js, and package.json
40
+ */
41
+ static async #transpileFile(ctx: ManifestContext, inputFile: string, outputFile: string): Promise<void> {
42
+ const type = CommonUtil.getFileType(inputFile);
43
+ if (type === 'js' || type === 'ts') {
44
+ const compilerOut = path.resolve(ctx.workspacePath, ctx.compilerFolder, 'node_modules');
45
+
46
+ const text = (await fs.readFile(inputFile, 'utf8'))
47
+ .replace(/from '([.][^']+)'/g, (_, i) => `from '${i.replace(/[.]js$/, '')}.js'`)
48
+ .replace(/from '(@travetto\/(.*?))'/g, (_, i, s) => `from '${path.resolve(compilerOut, `${i}${s.includes('/') ? '.js' : '/__index__.js'}`)}'`);
49
+
50
+ const ts = (await import('typescript')).default;
51
+ const content = ts.transpile(text, {
52
+ ...await CommonUtil.getCompilerOptions(ctx),
53
+ sourceMap: false,
54
+ inlineSourceMap: true,
55
+ }, inputFile);
56
+ await CommonUtil.writeTextFile(outputFile, content);
57
+ } else if (type === 'package-json') {
58
+ const pkg: Package = JSON.parse(await fs.readFile(inputFile, 'utf8'));
59
+ const main = pkg.main ? this.#sourceToOutputExt(pkg.main) : undefined;
60
+ const files = pkg.files?.map(x => this.#sourceToOutputExt(x));
61
+
62
+ const content = JSON.stringify({ ...pkg, main, type: ctx.moduleType, files }, null, 2);
63
+ await CommonUtil.writeTextFile(outputFile, content);
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Scan directory to find all project sources for comparison
69
+ */
70
+ static async #getModuleSources(ctx: ManifestContext, module: string, seed: string[]): Promise<ModFile[]> {
71
+ const inputFolder = (ctx.mainModule === module) ?
72
+ process.cwd() :
73
+ CommonUtil.resolveModuleFolder(module);
74
+
75
+ const folders = seed.filter(x => !/[.]/.test(x)).map(x => path.resolve(inputFolder, x));
76
+ const files = seed.filter(x => /[.]/.test(x)).map(x => path.resolve(inputFolder, x));
77
+
78
+ while (folders.length) {
79
+ const sub = folders.pop();
80
+ if (!sub) {
81
+ continue;
82
+ }
83
+
84
+ for (const file of await fs.readdir(sub).catch(() => [])) {
85
+ if (file.startsWith('.')) {
86
+ continue;
87
+ }
88
+ const resolvedInput = path.resolve(sub, file);
89
+ const stat = await fs.stat(resolvedInput);
90
+
91
+ if (stat.isDirectory()) {
92
+ folders.push(resolvedInput);
93
+ } else {
94
+ switch (CommonUtil.getFileType(file)) {
95
+ case 'js': case 'ts': files.push(resolvedInput);
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ const outputFolder = path.resolve(ctx.workspacePath, ctx.compilerFolder, 'node_modules', module);
102
+ const out: ModFile[] = [];
103
+ for (const input of files) {
104
+ const output = this.#sourceToOutputExt(input.replace(inputFolder, outputFolder));
105
+ const inputTs = await fs.stat(input).then(RECENT_STAT, () => 0);
106
+ if (inputTs) {
107
+ const outputTs = await fs.stat(output).then(RECENT_STAT, () => 0);
108
+ await fs.mkdir(path.dirname(output), { recursive: true, });
109
+ out.push({ input, output, stale: inputTs > outputTs });
110
+ }
111
+ }
112
+
113
+ return out;
114
+ }
115
+
116
+ /**
117
+ * Recompile folder if stale
118
+ */
119
+ static async #compileIfStale(ctx: ManifestContext, scope: string, mod: string, seed: string[]): Promise<string[]> {
120
+ const files = await this.#getModuleSources(ctx, mod, seed);
121
+ const changes = files.filter(x => x.stale).map(x => x.input);
122
+ const out: string[] = [];
123
+
124
+ try {
125
+ await LogUtil.withLogger(scope, async log => {
126
+ if (files.some(f => f.stale)) {
127
+ log('debug', 'Starting', mod);
128
+ for (const file of files.filter(x => x.stale)) {
129
+ await this.#transpileFile(ctx, file.input, file.output);
130
+ }
131
+ if (changes.length) {
132
+ out.push(...changes.map(x => `${mod}/${x}`));
133
+ log('debug', `Source changed: ${changes.join(', ')}`, mod);
134
+ }
135
+ log('debug', 'Completed', mod);
136
+ } else {
137
+ log('debug', 'Skipped', mod);
138
+ }
139
+ }, false);
140
+ } catch (err) {
141
+ console.error(err);
142
+ }
143
+ return out;
144
+ }
145
+
146
+ /**
147
+ * Export manifest
148
+ */
149
+ static async exportManifest(ctx: ManifestContext, output?: string, env = 'dev'): Promise<void> {
150
+ const { ManifestUtil } = await this.#importManifest(ctx);
151
+ let manifest = await ManifestUtil.buildManifest(ctx);
152
+
153
+ // If in prod mode, only include std modules
154
+ if (/^prod/i.test(env)) {
155
+ manifest = ManifestUtil.createProductionManifest(manifest);
156
+ }
157
+ if (output) {
158
+ output = await ManifestUtil.writeManifestToFile(output, manifest);
159
+ } else {
160
+ console.log(JSON.stringify(manifest, null, 2));
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Sets up compiler, and produces a manifest and set of changes that need to be processed
166
+ */
167
+ static async setup(ctx: ManifestContext): Promise<{ manifest: ManifestRoot, changed: DeltaEvent[] }> {
168
+ let changes = 0;
169
+
170
+ await LogUtil.withLogger('precompile', async () => {
171
+ for (const mod of PRECOMPILE_MODS) {
172
+ const count = (await this.#compileIfStale(ctx, 'precompile', mod, SOURCE_SEED)).length;
173
+ if (mod !== '@travetto/terminal') {
174
+ changes += count;
175
+ }
176
+ }
177
+ });
178
+
179
+ const { ManifestUtil, ManifestDeltaUtil, PackageUtil } = await this.#importManifest(ctx);
180
+
181
+ PackageUtil.clearCache();
182
+
183
+ const manifest = await LogUtil.withLogger('manifest', () => ManifestUtil.buildManifest(ctx));
184
+
185
+ await LogUtil.withLogger('transformers', async () => {
186
+ for (const mod of Object.values(manifest.modules).filter(m => m.files.$transformer?.length)) {
187
+ changes += (await this.#compileIfStale(ctx, 'transformers', mod.name, ['package.json', ...mod.files.$transformer!.map(x => x[0])])).length;
188
+ }
189
+ });
190
+
191
+ const delta = await LogUtil.withLogger('delta', async log => {
192
+ if (changes) {
193
+ log('debug', 'Skipping, everything changed');
194
+ return [{ type: 'changed', file: '*', module: ctx.mainModule } as const];
195
+ } else {
196
+ return ManifestDeltaUtil.produceDelta(ctx, manifest);
197
+ }
198
+ });
199
+
200
+ if (changes) {
201
+ await LogUtil.withLogger('reset', async log => {
202
+ await fs.rm(path.resolve(ctx.workspacePath, ctx.outputFolder), { recursive: true, force: true });
203
+ log('info', 'Clearing output due to compiler changes');
204
+ }, false);
205
+ }
206
+
207
+ // Write manifest
208
+ await LogUtil.withLogger('manifest', async log => {
209
+ await ManifestUtil.writeManifest(ctx, manifest);
210
+ log('debug', `Wrote manifest ${ctx.mainModule}`);
211
+
212
+ // Update all manifests
213
+ if (delta.length && ctx.monoRepo && !ctx.mainFolder) {
214
+ const names: string[] = [];
215
+ const mods = Object.values(manifest.modules).filter(x => x.local && x.name !== ctx.mainModule);
216
+ for (const mod of mods) {
217
+ await ManifestUtil.rewriteManifest(path.resolve(ctx.workspacePath, mod.sourceFolder));
218
+ names.push(mod.name);
219
+ }
220
+ log('debug', `Changes triggered ${delta.slice(0, 10).map(x => `${x.type}:${x.module}:${x.file}`)}`);
221
+ log('debug', `Rewrote monorepo manifests [changes=${delta.length}] ${names.slice(0, 10).join(', ')}`);
222
+ }
223
+ });
224
+
225
+ const changed = delta.filter(x => x.type === 'added' || x.type === 'changed');
226
+
227
+ return { manifest, changed };
228
+ }
229
+ }
@@ -0,0 +1,27 @@
1
+ export type CompilerMode = 'build' | 'watch';
2
+ export type CompilerOp = CompilerMode | 'run';
3
+
4
+ export type CompilerStateType = 'startup' | 'init' | 'compile-start' | 'compile-end' | 'watch-start' | 'watch-end' | 'reset' | 'close';
5
+ export type CompilerChangeEvent = { file: string, action: 'create' | 'update' | 'delete', folder: string, output: string, module: string, time: number };
6
+ export type CompilerLogLevel = 'info' | 'debug' | 'warn' | 'error';
7
+ export type CompilerLogEvent = { level: CompilerLogLevel, message: string, time: number, args?: unknown[], scope?: string };
8
+ export type CompilerProgressEvent = { idx: number, total: number, message: string, operation: 'compile', complete?: boolean };
9
+ export type CompilerStateEvent = { state: CompilerStateType, extra?: Record<string, unknown> };
10
+
11
+ export type CompilerServerEvent =
12
+ { type: 'change', payload: CompilerChangeEvent } |
13
+ { type: 'log', payload: CompilerLogEvent } |
14
+ { type: 'progress', payload: CompilerProgressEvent } |
15
+ { type: 'state', payload: CompilerStateEvent };
16
+
17
+ export type CompilerServerEventType = CompilerServerEvent['type'];
18
+
19
+ export type CompilerServerInfo = {
20
+ path: string;
21
+ serverPid: number;
22
+ compilerPid: number;
23
+ state: CompilerStateType;
24
+ mode: CompilerMode,
25
+ iteration: number;
26
+ url: string;
27
+ };
@@ -0,0 +1,108 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { createRequire } from 'module';
4
+
5
+ import type { ManifestContext } from '@travetto/manifest';
6
+
7
+ import { LogUtil } from './log';
8
+
9
+ const OPT_CACHE: Record<string, import('typescript').CompilerOptions> = {};
10
+ const SRC_REQ = createRequire(path.resolve('node_modules'));
11
+
12
+ export class CommonUtil {
13
+ /**
14
+ * Returns the compiler options
15
+ */
16
+ static async getCompilerOptions(ctx: ManifestContext): Promise<{}> {
17
+ if (!(ctx.workspacePath in OPT_CACHE)) {
18
+ let tsconfig = path.resolve(ctx.workspacePath, 'tsconfig.json');
19
+
20
+ if (!await fs.stat(tsconfig).then(_ => true, _ => false)) {
21
+ tsconfig = SRC_REQ.resolve('@travetto/compiler/tsconfig.trv.json');
22
+ }
23
+
24
+ const ts = (await import('typescript')).default;
25
+
26
+ const { options } = ts.parseJsonSourceFileConfigFileContent(
27
+ ts.readJsonConfigFile(tsconfig, ts.sys.readFile), ts.sys, ctx.workspacePath
28
+ );
29
+
30
+ OPT_CACHE[ctx.workspacePath] = {
31
+ ...options,
32
+ allowJs: true,
33
+ resolveJsonModule: true,
34
+ sourceRoot: ctx.workspacePath,
35
+ rootDir: ctx.workspacePath,
36
+ outDir: path.resolve(ctx.workspacePath),
37
+ module: ctx.moduleType === 'commonjs' ? ts.ModuleKind.CommonJS : ts.ModuleKind.ESNext,
38
+ };
39
+ }
40
+ return OPT_CACHE[ctx.workspacePath];
41
+ }
42
+
43
+ /**
44
+ * Resolve module location
45
+ */
46
+ static resolveModuleFolder(mod: string): string {
47
+ return path.dirname(SRC_REQ.resolve(`${mod}/package.json`));
48
+ }
49
+
50
+ /**
51
+ * Determine file type
52
+ */
53
+ static getFileType(file: string): 'ts' | 'js' | 'package-json' | 'typings' | undefined {
54
+ return file.endsWith('package.json') ? 'package-json' :
55
+ (file.endsWith('.js') ? 'js' :
56
+ (file.endsWith('.d.ts') ? 'typings' : (/[.]tsx?$/.test(file) ? 'ts' : undefined)));
57
+ }
58
+
59
+ /**
60
+ * Write text file, and ensure folder exists
61
+ */
62
+ static writeTextFile = (file: string, content: string): Promise<void> =>
63
+ fs.mkdir(path.dirname(file), { recursive: true }).then(() => fs.writeFile(file, content, 'utf8'));
64
+
65
+ /**
66
+ * Restartable Event Stream
67
+ */
68
+ static async * restartableEvents<T>(src: (signal: AbortSignal) => AsyncIterable<T>, parent: AbortSignal, shouldRestart: (item: T) => boolean): AsyncIterable<T> {
69
+ outer: while (!parent.aborted) {
70
+ const controller = new AbortController();
71
+ // Chain
72
+ parent.addEventListener('abort', () => controller.abort());
73
+
74
+ const comp = src(controller.signal);
75
+
76
+ LogUtil.log('event-stream', 'debug', 'Started event stream');
77
+
78
+ // Wait for all events, close at the end
79
+ for await (const ev of comp) {
80
+ yield ev;
81
+ if (shouldRestart(ev)) {
82
+ controller.abort(); // Ensure terminated of process
83
+ continue outer;
84
+ }
85
+ }
86
+
87
+ LogUtil.log('event-stream', 'debug', 'Finished event stream');
88
+
89
+ // Natural exit, we done
90
+ if (!controller.signal.aborted) { // Shutdown source if still running
91
+ controller.abort();
92
+ }
93
+ return;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Create a module loader given a context, and assuming build is complete
99
+ * @param ctx
100
+ */
101
+ static moduleLoader(ctx: ManifestContext): (mod: string) => Promise<unknown> {
102
+ return (mod) => {
103
+ const outputRoot = path.resolve(ctx.workspacePath, ctx.outputFolder);
104
+ process.env.TRV_MANIFEST = path.resolve(outputRoot, 'node_modules', ctx.mainModule); // Setup for running
105
+ return import(path.join(outputRoot, 'node_modules', mod)); // Return function to run import on a module
106
+ };
107
+ }
108
+ }
package/tsconfig.trv.json CHANGED
@@ -24,8 +24,7 @@
24
24
  "watchOptions": {
25
25
  "excludeDirectories": [
26
26
  "**/node_modules",
27
- "**/.trv_output",
28
- "**/.trv_compiler",
27
+ "**/.trv",
29
28
  "**/.git",
30
29
  ]
31
30
  }