@travetto/compiler 3.0.0-rc.14 → 3.0.0-rc.16

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.
@@ -2,7 +2,10 @@ import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
 
4
4
  import type { ManifestContext } from '@travetto/manifest';
5
- import { TranspileUtil } from './transpile';
5
+
6
+ import { TranspileUtil, CompileResult } from './transpile';
7
+ import { LockManager } from './lock';
8
+ import { LogUtil } from './log';
6
9
 
7
10
  const SOURCE_SEED = ['package.json', 'index.ts', '__index__.ts', 'src', 'support', 'bin'];
8
11
  const PRECOMPILE_MODS = ['@travetto/terminal', '@travetto/manifest', '@travetto/transformer', '@travetto/compiler'];
@@ -10,59 +13,31 @@ const PRECOMPILE_MODS = ['@travetto/terminal', '@travetto/manifest', '@travetto/
10
13
  const importManifest = (ctx: ManifestContext): Promise<typeof import('@travetto/manifest')> =>
11
14
  import(path.resolve(ctx.workspacePath, ctx.compilerFolder, 'node_modules', '@travetto/manifest/__index__.js'));
12
15
 
13
- /**
14
- * Recompile folder if stale
15
- */
16
- async function compileIfStale(ctx: ManifestContext, scope: string, mod: string, seed: string[]): Promise<string[]> {
17
- const files = await TranspileUtil.getModuleSources(ctx, mod, seed);
18
- const changes = files.filter(x => x.stale).map(x => x.input);
19
- const out: string[] = [];
20
-
21
- try {
22
- await TranspileUtil.withLogger(scope, async log => {
23
- if (files.some(f => f.stale)) {
24
- log('debug', 'Starting');
25
- for (const file of files.filter(x => x.stale)) {
26
- await TranspileUtil.transpileFile(ctx, file.input, file.output);
27
- }
28
- if (changes.length) {
29
- out.push(...changes.map(x => `${mod}/${x}`));
30
- log('debug', `Source changed: ${changes.join(', ')}`);
31
- }
32
- log('debug', 'Completed');
33
- } else {
34
- log('debug', 'Skipped');
35
- }
36
- }, false, [mod]);
37
- } catch (err) {
38
- console.error(err);
39
- }
40
- return out;
41
- }
42
-
43
16
  /**
44
17
  * Run the compiler
45
18
  */
46
- async function compile(ctx: ManifestContext, op?: 'watch' | 'build'): Promise<void> {
19
+ async function compile(ctx: ManifestContext, op: 'watch' | 'build' | undefined, onMessage: (msg: unknown) => void): Promise<CompileResult> {
47
20
  let changes = 0;
48
21
 
49
- await TranspileUtil.withLogger('precompile', async () => {
22
+ await LogUtil.withLogger('precompile', async () => {
50
23
  for (const mod of PRECOMPILE_MODS) {
51
- changes += (await compileIfStale(ctx, 'precompile', mod, SOURCE_SEED)).length;
24
+ changes += (await TranspileUtil.compileIfStale(ctx, 'precompile', mod, SOURCE_SEED)).length;
52
25
  }
53
26
  });
54
27
 
55
- const { ManifestUtil, ManifestDeltaUtil } = await importManifest(ctx);
28
+ const { ManifestUtil, ManifestDeltaUtil, PackageUtil } = await importManifest(ctx);
56
29
 
57
- const manifest = await TranspileUtil.withLogger('manifest', async () => ManifestUtil.buildManifest(ctx));
30
+ PackageUtil.clearCache();
58
31
 
59
- await TranspileUtil.withLogger('transformers', async () => {
32
+ const manifest = await LogUtil.withLogger('manifest', async () => ManifestUtil.buildManifest(ctx));
33
+
34
+ await LogUtil.withLogger('transformers', async () => {
60
35
  for (const mod of Object.values(manifest.modules).filter(m => m.files.$transformer?.length)) {
61
- changes += (await compileIfStale(ctx, 'transformers', mod.name, ['package.json', ...mod.files.$transformer!.map(x => x[0])])).length;
36
+ changes += (await TranspileUtil.compileIfStale(ctx, 'transformers', mod.name, ['package.json', ...mod.files.$transformer!.map(x => x[0])])).length;
62
37
  }
63
38
  });
64
39
 
65
- const delta = await TranspileUtil.withLogger('delta', async log => {
40
+ const delta = await LogUtil.withLogger('delta', async log => {
66
41
  if (changes) {
67
42
  log('debug', 'Skipping, everything changed');
68
43
  return [{ type: 'changed', file: '*', module: ctx.mainModule } as const];
@@ -72,14 +47,14 @@ async function compile(ctx: ManifestContext, op?: 'watch' | 'build'): Promise<vo
72
47
  });
73
48
 
74
49
  if (changes) {
75
- await TranspileUtil.withLogger('reset', async log => {
50
+ await LogUtil.withLogger('reset', async log => {
76
51
  await fs.rm(path.resolve(ctx.workspacePath, ctx.outputFolder), { recursive: true, force: true });
77
52
  log('info', 'Clearing output due to compiler changes');
78
53
  }, false);
79
54
  }
80
55
 
81
56
  // Write manifest
82
- await TranspileUtil.withLogger('manifest', async log => {
57
+ await LogUtil.withLogger('manifest', async log => {
83
58
  await ManifestUtil.writeManifest(ctx, manifest);
84
59
  log('debug', `Wrote manifest ${ctx.mainModule}`);
85
60
 
@@ -91,19 +66,21 @@ async function compile(ctx: ManifestContext, op?: 'watch' | 'build'): Promise<vo
91
66
  await ManifestUtil.rewriteManifest(path.resolve(ctx.workspacePath, mod.sourceFolder));
92
67
  names.push(mod.name);
93
68
  }
94
- log('debug', `Changes triggered ${delta.map(x => `${x.type}:${x.module}:${x.file}`)}`);
95
- log('debug', `Rewrote monorepo manifests [changes=${delta.length}] ${names.join(', ')}`);
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(', ')}`);
96
71
  }
97
72
  });
98
73
 
99
- await TranspileUtil.withLogger('compile', async log => {
74
+ return await LogUtil.withLogger('compile', async log => {
100
75
  const changed = delta.filter(x => x.type === 'added' || x.type === 'changed');
101
- log('debug', `Started action=${op} changed=${changed.map(x => `${x.module}/${x.file}`)}`);
76
+ log('debug', `Started action=${op} changed=${changed.slice(0, 10).map(x => `${x.module}/${x.file}`)}`);
102
77
  if (changed.length || op === 'watch') {
103
- await TranspileUtil.runCompiler(ctx, manifest, changed, op === 'watch');
78
+ const res = await TranspileUtil.runCompiler(ctx, manifest, changed, op === 'watch', onMessage);
104
79
  log('debug', 'Finished');
80
+ return res;
105
81
  } else {
106
82
  log('debug', 'Skipped');
83
+ return 'skipped';
107
84
  }
108
85
  }, false);
109
86
  }
@@ -113,26 +90,15 @@ async function compile(ctx: ManifestContext, op?: 'watch' | 'build'): Promise<vo
113
90
  */
114
91
  async function exportManifest(ctx: ManifestContext, output?: string, env = 'dev'): Promise<void> {
115
92
  const { ManifestUtil } = await importManifest(ctx);
116
- const manifest = await ManifestUtil.buildManifest(ctx);
93
+ let manifest = await ManifestUtil.buildManifest(ctx);
117
94
 
118
95
  // If in prod mode, only include std modules
119
96
  if (/^prod/i.test(env)) {
120
- manifest.modules = Object.fromEntries(
121
- Object.values(manifest.modules)
122
- .filter(x => x.profiles.includes('std'))
123
- .map(m => [m.name, m])
124
- );
125
- // Mark output folder/workspace path as portable
126
- manifest.outputFolder = '';
127
- manifest.workspacePath = '';
97
+ manifest = ManifestUtil.createProductionManifest(manifest);
128
98
  }
129
99
  if (output) {
130
- if (!output.endsWith('.json')) {
131
- output = path.resolve(output, 'manifest.json');
132
- }
133
-
134
- await TranspileUtil.writeTextFile(output, JSON.stringify(manifest));
135
- TranspileUtil.log('manifest', [], 'info', `Wrote manifest ${output}`);
100
+ output = await ManifestUtil.writeManifestToFile(output, manifest);
101
+ LogUtil.log('manifest', [], 'info', `Wrote manifest ${output}`);
136
102
  } else {
137
103
  console.log(JSON.stringify(manifest, null, 2));
138
104
  }
@@ -141,13 +107,27 @@ async function exportManifest(ctx: ManifestContext, output?: string, env = 'dev'
141
107
  /**
142
108
  * Launch
143
109
  */
144
- export async function launch(ctx: ManifestContext, op?: 'build' | 'watch' | 'manifest', args: (string | undefined)[] = []): Promise<void> {
145
- if (op !== 'manifest') {
146
- await compile(ctx, op);
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
+ });
147
126
  }
127
+
148
128
  switch (op) {
149
129
  case 'manifest': return exportManifest(ctx, ...args);
150
- case 'build': return TranspileUtil.log('build', [], 'info', 'Successfully built');
130
+ case 'build': return LogUtil.log('build', [], 'info', 'Successfully built');
151
131
  case undefined: {
152
132
  // TODO: Externalize somehow?
153
133
  const outputPath = path.resolve(ctx.workspacePath, ctx.outputFolder);
@@ -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
+ });
@@ -0,0 +1,237 @@
1
+ import { Stats, watchFile, unwatchFile, rmSync, mkdirSync, writeFileSync, existsSync } from 'fs';
2
+ import fs from 'fs/promises';
3
+ import { Worker } from 'worker_threads';
4
+ import path from 'path';
5
+
6
+ import type { ManifestContext } from '@travetto/manifest';
7
+
8
+ import { CompilerLogger, LogUtil } from './log';
9
+
10
+ type LockStatus = 'complete' | 'stale';
11
+ type LockDetails = {
12
+ pid: number | undefined;
13
+ file: string;
14
+ };
15
+
16
+ export type LockType = 'build' | 'watch';
17
+ export type LockCompileAction = 'skip' | 'build';
18
+ type LockAction = LockCompileAction | 'retry';
19
+
20
+ const STALE_THRESHOLD = 1000;
21
+
22
+ /**
23
+ * Manager for all lock activity
24
+ */
25
+ export class LockManager {
26
+
27
+ /**
28
+ * Get the lock file name
29
+ */
30
+ static #getFileName(ctx: ManifestContext, type: LockType): string {
31
+ return path.resolve(ctx.workspacePath, ctx.toolFolder, `${type}.pid`);
32
+ }
33
+
34
+ /**
35
+ * Determine if the given stats are stale for modification time
36
+ */
37
+ static #isStale(stat?: Stats): boolean {
38
+ return !!stat && stat.mtimeMs < (Date.now() - STALE_THRESHOLD * 1.1);
39
+ }
40
+
41
+ /**
42
+ * Get the lock file details
43
+ */
44
+ static async #getDetails(ctx: ManifestContext, type: LockType): Promise<LockDetails> {
45
+ const file = this.#getFileName(ctx, type);
46
+ const stat = await fs.stat(file).catch(() => undefined);
47
+ const stale = this.#isStale(stat);
48
+ let pid: number | undefined;
49
+ if (stat) {
50
+ const content = await fs.readFile(file, 'utf8');
51
+ const filePid = parseInt(content, 10);
52
+ if (stale) {
53
+ LogUtil.log('lock', [], 'warn', `${type} file is stale: ${stat.mtimeMs} vs ${Date.now()}`);
54
+ } else {
55
+ pid = filePid;
56
+ }
57
+ }
58
+ return { pid, file };
59
+ }
60
+
61
+ /**
62
+ * Acquire the lock file, and register a cleanup on exit
63
+ */
64
+ static #acquireFile(ctx: ManifestContext, type: LockType): void {
65
+ const file = this.#getFileName(ctx, type);
66
+ mkdirSync(path.dirname(file), { recursive: true });
67
+ LogUtil.log('lock', [], 'debug', `Acquiring ${type}`);
68
+ writeFileSync(file, `${process.pid}`, 'utf8');
69
+ }
70
+
71
+ /**
72
+ * Release the lock file (i.e. deleting)
73
+ */
74
+ static #releaseFile(ctx: ManifestContext, type: LockType): void {
75
+ const file = this.#getFileName(ctx, type);
76
+ if (existsSync(file)) {
77
+ rmSync(file, { force: true });
78
+ LogUtil.log('lock', [], 'debug', `Releasing ${type}`);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Wait until a lock file is released, or it becomes stale
84
+ */
85
+ static async #waitForRelease(ctx: ManifestContext, type: LockType): Promise<LockStatus> {
86
+ const file = this.#getFileName(ctx, type);
87
+ let remove: (() => void) | undefined = undefined;
88
+
89
+ const prom = new Promise<LockStatus>(resolve => {
90
+ let timer: NodeJS.Timeout | undefined = undefined;
91
+ const handler = async (): Promise<void> => {
92
+ if (timer) {
93
+ clearTimeout(timer);
94
+ }
95
+ const stats = await fs.stat(file).catch(() => undefined);
96
+ if (!stats) {
97
+ resolve('complete');
98
+ } else if (this.#isStale(stats)) {
99
+ resolve('stale');
100
+ } else {
101
+ timer = setTimeout(handler, STALE_THRESHOLD * 1.1);
102
+ }
103
+ };
104
+
105
+ watchFile(file, handler);
106
+ handler();
107
+
108
+ remove = (): void => {
109
+ clearInterval(timer);
110
+ unwatchFile(file, handler);
111
+ };
112
+ });
113
+
114
+ return prom.finally(remove);
115
+ }
116
+
117
+ /**
118
+ * Read the watch lock file and determine its result, communicating with the user as necessary
119
+ */
120
+ static async #getWatchAction(ctx: ManifestContext, log: CompilerLogger, lockType: LockType | undefined, buildState: LockDetails): Promise<LockAction> {
121
+ const level = lockType ? 'info' : 'debug';
122
+ if (lockType === 'watch') {
123
+ log(level, 'Already running');
124
+ return 'skip';
125
+ } else {
126
+ if (buildState.pid) {
127
+ log('info', 'Already running, waiting for build to finish');
128
+ switch (await this.#waitForRelease(ctx, 'build')) {
129
+ case 'complete': {
130
+ log(level, 'Completed build');
131
+ return 'skip';
132
+ }
133
+ case 'stale': {
134
+ log(level, 'Became stale, retrying');
135
+ return 'retry';
136
+ }
137
+ }
138
+ } else {
139
+ log(level, 'Already running, and has built');
140
+ return 'skip';
141
+ }
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Read the build lock file and determine its result, communicating with the user as necessary
147
+ */
148
+ static async #getBuildAction(ctx: ManifestContext, log: CompilerLogger, lockType: LockType | undefined): Promise<LockAction> {
149
+ const level = lockType ? 'info' : 'debug';
150
+ if (lockType === 'watch') {
151
+ log('info', 'Build already running, waiting to begin watch');
152
+ const res = await this.#waitForRelease(ctx, 'build');
153
+ log(level, `Finished with status of ${res}, retrying`);
154
+ return 'retry';
155
+ } else {
156
+ log('info', 'Already running, waiting for completion');
157
+ switch (await this.#waitForRelease(ctx, lockType ?? 'build')) {
158
+ case 'complete': {
159
+ log(level, 'Completed');
160
+ return 'skip';
161
+ }
162
+ case 'stale': {
163
+ log(level, 'Became stale, retrying');
164
+ return 'retry';
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Run code with support for lock acquire and release
172
+ */
173
+ static async withLocks(ctx: ManifestContext, fn: (acquire: (type: LockType) => void, release: (type: LockType) => void) => Promise<unknown>): Promise<void> {
174
+ const activeLockTypes = new Set<LockType>();
175
+
176
+ const pinger = path.resolve(ctx.workspacePath, ctx.compilerFolder, 'node_modules', '@travetto/compiler/support/lock-pinger.js');
177
+ const worker = new Worker(pinger, {
178
+ workerData: {
179
+ interval: STALE_THRESHOLD,
180
+ files: []
181
+ }
182
+ });
183
+
184
+ const notify = (): void => worker.postMessage({ files: [...activeLockTypes].map(t => this.#getFileName(ctx, t)) });
185
+
186
+ const stop = (): void => {
187
+ worker.postMessage('stop');
188
+ for (const type of activeLockTypes) {
189
+ this.#releaseFile(ctx, type);
190
+ }
191
+ worker.terminate().then(() => { });
192
+ };
193
+
194
+ process.on('SIGINT', stop);
195
+ process.on('exit', stop);
196
+
197
+ try {
198
+ await new Promise(r => worker.on('online', r));
199
+ await fn(
200
+ type => {
201
+ if (!activeLockTypes.has(type)) {
202
+ activeLockTypes.add(type);
203
+ this.#acquireFile(ctx, type);
204
+ notify();
205
+ }
206
+ },
207
+ type => {
208
+ if (activeLockTypes.has(type)) {
209
+ activeLockTypes.delete(type);
210
+ this.#releaseFile(ctx, type);
211
+ notify();
212
+ }
213
+ }
214
+ );
215
+ } finally {
216
+ stop();
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Reads the lock file states (build + watch) to determine what action should be taken for the compiler
222
+ */
223
+ static async getCompileAction(ctx: ManifestContext, lockType: LockType | undefined): Promise<LockCompileAction> {
224
+ let result: LockAction;
225
+ do {
226
+ result = 'build';
227
+ const buildState = await this.#getDetails(ctx, 'build');
228
+ const watchState = await this.#getDetails(ctx, 'watch');
229
+ if (watchState.pid) { // Existing watch operation
230
+ result = await LogUtil.withLogger('lock', log => this.#getWatchAction(ctx, log, lockType, buildState), true, ['watch', `pid=${watchState.pid}`]);
231
+ } else if (buildState.pid) { // Existing build operation
232
+ result = await LogUtil.withLogger('lock', log => this.#getBuildAction(ctx, log, lockType), true, ['build', `pid=${buildState.pid}`]);
233
+ }
234
+ } while (result === 'retry');
235
+ return result;
236
+ }
237
+ }
package/support/log.ts ADDED
@@ -0,0 +1,31 @@
1
+ export type CompilerLogEvent = [level: 'info' | 'debug' | 'warn', message: string];
2
+ export type CompilerLogger = (...args: CompilerLogEvent) => void;
3
+ export type WithLogger<T> = (log: CompilerLogger) => Promise<T>;
4
+
5
+ const LEVELS = { warn: true, debug: /^debug$/.test(process.env.TRV_BUILD ?? ''), info: !/^warn$/.test(process.env.TRV_BUILD ?? '') };
6
+ const SCOPE_MAX = 15;
7
+
8
+ export class LogUtil {
9
+
10
+ /**
11
+ * Is object a log event
12
+ */
13
+ static isLogEvent = (o: unknown): o is CompilerLogEvent => o !== null && o !== undefined && Array.isArray(o);
14
+
15
+ /**
16
+ * Log message with filtering by level
17
+ */
18
+ static log(scope: string, args: string[], ...[level, msg]: CompilerLogEvent): void {
19
+ const message = msg.replaceAll(process.cwd(), '.');
20
+ LEVELS[level] && console.debug(new Date().toISOString(), `[${scope.padEnd(SCOPE_MAX, ' ')}]`, ...args, message);
21
+ }
22
+
23
+ /**
24
+ * With logger
25
+ */
26
+ static withLogger<T>(scope: string, op: WithLogger<T>, basic = true, args: string[] = []): Promise<T> {
27
+ const log = this.log.bind(null, scope, args);
28
+ basic && log('debug', 'Started');
29
+ return op(log).finally(() => basic && log('debug', 'Completed'));
30
+ }
31
+ }
@@ -6,43 +6,24 @@ import { createRequire } from 'module';
6
6
 
7
7
  import { DeltaEvent, ManifestContext, ManifestRoot, Package } from '@travetto/manifest';
8
8
 
9
- export type CompilerLogEvent = [level: 'info' | 'debug' | 'warn', message: string];
9
+ import { LogUtil } from './log';
10
+
10
11
  type ModFile = { input: string, output: string, stale: boolean };
11
- type WithLogger<T> = (log: (...ev: CompilerLogEvent) => void) => Promise<T>;
12
+ export type CompileResult = 'restart' | 'complete' | 'skipped';
12
13
 
13
14
  const OPT_CACHE: Record<string, import('typescript').CompilerOptions> = {};
14
15
  const SRC_REQ = createRequire(path.resolve('node_modules'));
15
- const LEVELS = { warn: true, debug: /^debug$/.test(process.env.TRV_BUILD ?? ''), info: !/^warn$/.test(process.env.TRV_BUILD ?? '') };
16
- const SCOPE_MAX = 15;
17
16
  const RECENT_STAT = (stat: { ctimeMs: number, mtimeMs: number }): number => Math.max(stat.ctimeMs, stat.mtimeMs);
18
- const IS_LOG_EV = (o: unknown): o is CompilerLogEvent => o !== null && o !== undefined && Array.isArray(o);
19
17
 
20
18
  /**
21
19
  * Transpile utilities for launching
22
20
  */
23
21
  export class TranspileUtil {
24
- /**
25
- * Log message with filtering by level
26
- */
27
- static log(scope: string, args: string[], ...[level, msg]: CompilerLogEvent): void {
28
- const message = msg.replaceAll(process.cwd(), '.');
29
- LEVELS[level] && console.debug(new Date().toISOString(), `[${scope.padEnd(SCOPE_MAX, ' ')}]`, ...args, message);
30
- }
31
-
32
- /**
33
- * With logger
34
- */
35
- static withLogger<T>(scope: string, op: WithLogger<T>, basic = true, args: string[] = []): Promise<T> {
36
- const log = this.log.bind(null, scope, args);
37
- basic && log('debug', 'Started');
38
- return op(log).finally(() => basic && log('debug', 'Completed'));
39
- }
40
-
41
22
  /**
42
23
  * Write text file, and ensure folder exists
43
24
  */
44
25
  static writeTextFile = (file: string, content: string): Promise<void> =>
45
- fs.mkdir(path.dirname(file), { recursive: true }).then(() => fs.writeFile(file, content));
26
+ fs.mkdir(path.dirname(file), { recursive: true }).then(() => fs.writeFile(file, content, 'utf8'));
46
27
 
47
28
  /**
48
29
  * Returns the compiler options
@@ -147,10 +128,40 @@ export class TranspileUtil {
147
128
  return out;
148
129
  }
149
130
 
131
+ /**
132
+ * Recompile folder if stale
133
+ */
134
+ static async compileIfStale(ctx: ManifestContext, scope: string, mod: string, seed: string[]): Promise<string[]> {
135
+ const files = await this.getModuleSources(ctx, mod, seed);
136
+ const changes = files.filter(x => x.stale).map(x => x.input);
137
+ const out: string[] = [];
138
+
139
+ try {
140
+ await LogUtil.withLogger(scope, async log => {
141
+ if (files.some(f => f.stale)) {
142
+ log('debug', 'Starting');
143
+ for (const file of files.filter(x => x.stale)) {
144
+ await this.transpileFile(ctx, file.input, file.output);
145
+ }
146
+ if (changes.length) {
147
+ out.push(...changes.map(x => `${mod}/${x}`));
148
+ log('debug', `Source changed: ${changes.join(', ')}`);
149
+ }
150
+ log('debug', 'Completed');
151
+ } else {
152
+ log('debug', 'Skipped');
153
+ }
154
+ }, false, [mod]);
155
+ } catch (err) {
156
+ console.error(err);
157
+ }
158
+ return out;
159
+ }
160
+
150
161
  /**
151
162
  * Run compiler
152
163
  */
153
- static async runCompiler(ctx: ManifestContext, manifest: ManifestRoot, changed: DeltaEvent[], watch = false): Promise<void> {
164
+ static async runCompiler(ctx: ManifestContext, manifest: ManifestRoot, changed: DeltaEvent[], watch: boolean, onMessage: (msg: unknown) => void): Promise<CompileResult> {
154
165
  const compiler = path.resolve(ctx.workspacePath, ctx.compilerFolder);
155
166
  const main = path.resolve(compiler, 'node_modules', '@travetto/compiler/support/compiler-entry.js');
156
167
  const deltaFile = path.resolve(os.tmpdir(), `manifest-delta.${Date.now()}.${Math.random()}.json`);
@@ -159,18 +170,33 @@ export class TranspileUtil {
159
170
  path.resolve(manifest.workspacePath, manifest.modules[ev.module].sourceFolder, ev.file)
160
171
  );
161
172
 
162
- await this.writeTextFile(deltaFile, changedFiles.join('\n'));
163
-
164
- await this.withLogger('compiler-exec', log => new Promise((res, rej) =>
165
- cp.spawn(process.argv0, [main, deltaFile, `${watch}`], {
166
- env: {
167
- ...process.env,
168
- TRV_MANIFEST: path.resolve(ctx.workspacePath, ctx.outputFolder, 'node_modules', ctx.mainModule),
169
- },
170
- stdio: [0, 1, 2, 'ipc'],
171
- })
172
- .on('message', msg => IS_LOG_EV(msg) && log(...msg))
173
- .on('exit', code => (code !== null && code > 0) ? rej() : res(null))
174
- )).finally(() => fs.unlink(deltaFile));
173
+ let proc: cp.ChildProcess | undefined;
174
+
175
+ try {
176
+ await this.writeTextFile(deltaFile, changedFiles.join('\n'));
177
+
178
+ return await LogUtil.withLogger('compiler-exec', log => new Promise<CompileResult>((res, rej) => {
179
+ proc = cp.spawn(process.argv0, [main, deltaFile, `${watch}`], {
180
+ env: {
181
+ ...process.env,
182
+ TRV_MANIFEST: path.resolve(ctx.workspacePath, ctx.outputFolder, 'node_modules', ctx.mainModule),
183
+ },
184
+ stdio: [0, 1, 2, 'ipc'],
185
+ })
186
+ .on('message', msg => {
187
+ if (LogUtil.isLogEvent(msg)) {
188
+ log(...msg);
189
+ } else if (msg === 'restart') {
190
+ res(msg);
191
+ } else {
192
+ onMessage(msg);
193
+ }
194
+ })
195
+ .on('exit', code => (code !== null && code > 0) ? rej(new Error('Failed during compilation')) : res('complete'));
196
+ }));
197
+ } finally {
198
+ if (proc?.killed === false) { proc.kill('SIGKILL'); }
199
+ await fs.rm(deltaFile, { force: true });
200
+ }
175
201
  }
176
202
  }