@travetto/runtime 7.0.0-rc.2 → 7.0.0-rc.3
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 +13 -9
- package/__index__.ts +1 -0
- package/package.json +3 -3
- package/src/context.ts +2 -6
- package/src/exec.ts +20 -69
- package/src/file-loader.ts +9 -0
- package/src/json.ts +74 -0
- package/src/trv.d.ts +0 -4
- package/src/types.ts +7 -0
- package/src/util.ts +1 -77
- package/src/watch.ts +61 -13
- package/support/transformer/metadata.ts +1 -1
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ Runtime is the foundation of all [Travetto](https://travetto.dev) applications.
|
|
|
19
19
|
* Standard Error Support
|
|
20
20
|
* Console Management
|
|
21
21
|
* Resource Access
|
|
22
|
+
* JSON Utilities
|
|
22
23
|
* Common Utilities
|
|
23
24
|
* Time Utilities
|
|
24
25
|
* Process Execution
|
|
@@ -38,8 +39,6 @@ class $Runtime {
|
|
|
38
39
|
get env(): string | undefined;
|
|
39
40
|
/** Are we in development mode */
|
|
40
41
|
get production(): boolean;
|
|
41
|
-
/** Is the app in dynamic mode? */
|
|
42
|
-
get dynamic(): boolean;
|
|
43
42
|
/** Get debug value */
|
|
44
43
|
get debug(): false | string;
|
|
45
44
|
/** Manifest main */
|
|
@@ -95,10 +94,6 @@ interface EnvData {
|
|
|
95
94
|
* Special role to run as, used to access additional files from the manifest during runtime.
|
|
96
95
|
*/
|
|
97
96
|
TRV_ROLE: Role;
|
|
98
|
-
/**
|
|
99
|
-
* Whether or not to run the program in dynamic mode, allowing for real-time updates
|
|
100
|
-
*/
|
|
101
|
-
TRV_DYNAMIC: boolean;
|
|
102
97
|
/**
|
|
103
98
|
* The folders to use for resource lookup
|
|
104
99
|
*/
|
|
@@ -258,9 +253,18 @@ $ DEBUG=express:*,@travetto/web npx trv run web
|
|
|
258
253
|
## Resource Access
|
|
259
254
|
The primary access patterns for resources, is to directly request a file, and to resolve that file either via file-system look up or leveraging the [Manifest](https://github.com/travetto/travetto/tree/main/module/manifest#readme "Support for project indexing, manifesting, along with file watching")'s data for what resources were found at manifesting time.
|
|
260
255
|
|
|
261
|
-
The [FileLoader](https://github.com/travetto/travetto/tree/main/module/runtime/src/file-loader.ts#
|
|
256
|
+
The [FileLoader](https://github.com/travetto/travetto/tree/main/module/runtime/src/file-loader.ts#L12) allows for accessing information about the resources, and subsequently reading the file as text/binary or to access the resource as a `Readable` stream. If a file is not found, it will throw an [AppError](https://github.com/travetto/travetto/tree/main/module/runtime/src/error.ts#L26) with a category of 'notfound'.
|
|
257
|
+
|
|
258
|
+
The [FileLoader](https://github.com/travetto/travetto/tree/main/module/runtime/src/file-loader.ts#L12) also supports tying itself to [Env](https://github.com/travetto/travetto/tree/main/module/runtime/src/env.ts#L114)'s `TRV_RESOURCES` information on where to attempt to find a requested resource.
|
|
259
|
+
|
|
260
|
+
## JSON Utilities
|
|
261
|
+
The framework provides utilities for working with JSON data. This module provides methods for reading and writing JSON files, as well as serializing and deserializing JSON data. It also provides support for working with Base64 encoded data for web safe transfer. The primary goal is ease of use, but also a centralized location for performance and security improvements over time.
|
|
262
262
|
|
|
263
|
-
|
|
263
|
+
* `parseSafe(input: string | Buffer)` parses JSON safely from a string or Buffer.
|
|
264
|
+
* `stringifyBase64(value: any)` encodes a JSON value as a base64 encoded string.
|
|
265
|
+
* `parseBase64(input: string)` decodes a JSON value from a base64 encoded string.
|
|
266
|
+
* `readFile(file: string)` reads a JSON file asynchronously.
|
|
267
|
+
* `readFileSync(file: string, onMissing?: any)` reads a JSON file synchronously.
|
|
264
268
|
|
|
265
269
|
## Common Utilities
|
|
266
270
|
Common utilities used throughout the framework. Currently [Util](https://github.com/travetto/travetto/tree/main/module/runtime/src/util.ts#L12) includes:
|
|
@@ -323,7 +327,7 @@ export class TimeUtil {
|
|
|
323
327
|
```
|
|
324
328
|
|
|
325
329
|
## Process Execution
|
|
326
|
-
[ExecUtil](https://github.com/travetto/travetto/tree/main/module/runtime/src/exec.ts#
|
|
330
|
+
[ExecUtil](https://github.com/travetto/travetto/tree/main/module/runtime/src/exec.ts#L40) exposes `getResult` as a means to wrap [child_process](https://nodejs.org/api/child_process.html)'s process object. This wrapper allows for a promise-based resolution of the subprocess with the ability to capture the stderr/stdout.
|
|
327
331
|
|
|
328
332
|
A simple example would be:
|
|
329
333
|
|
package/__index__.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from './src/exec.ts';
|
|
|
9
9
|
export * from './src/env.ts';
|
|
10
10
|
export * from './src/file-loader.ts';
|
|
11
11
|
export * from './src/function.ts';
|
|
12
|
+
export * from './src/json.ts';
|
|
12
13
|
export * from './src/manifest-index.ts';
|
|
13
14
|
export * from './src/queue.ts';
|
|
14
15
|
export * from './src/resources.ts';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/runtime",
|
|
3
|
-
"version": "7.0.0-rc.
|
|
3
|
+
"version": "7.0.0-rc.3",
|
|
4
4
|
"description": "Runtime for travetto applications.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"console-manager",
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"directory": "module/runtime"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@travetto/manifest": "^7.0.0-rc.
|
|
28
|
+
"@travetto/manifest": "^7.0.0-rc.2",
|
|
29
29
|
"@types/debug": "^4.1.12",
|
|
30
30
|
"debug": "^4.4.3"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@travetto/transformer": "^7.0.0-rc.
|
|
33
|
+
"@travetto/transformer": "^7.0.0-rc.3"
|
|
34
34
|
},
|
|
35
35
|
"peerDependenciesMeta": {
|
|
36
36
|
"@travetto/transformer": {
|
package/src/context.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { Env } from './env.ts';
|
|
|
7
7
|
import { RuntimeIndex } from './manifest-index.ts';
|
|
8
8
|
import { describeFunction } from './function.ts';
|
|
9
9
|
import { castTo } from './types.ts';
|
|
10
|
+
import { JSONUtil } from './json.ts';
|
|
10
11
|
|
|
11
12
|
/** Constrained version of {@type ManifestContext} */
|
|
12
13
|
class $Runtime {
|
|
@@ -36,11 +37,6 @@ class $Runtime {
|
|
|
36
37
|
return process.env.NODE_ENV === 'production';
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
/** Is the app in dynamic mode? */
|
|
40
|
-
get dynamic(): boolean {
|
|
41
|
-
return Env.TRV_DYNAMIC.isTrue;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
40
|
/** Get debug value */
|
|
45
41
|
get debug(): false | string {
|
|
46
42
|
const value = Env.DEBUG.value ?? '';
|
|
@@ -118,7 +114,7 @@ class $Runtime {
|
|
|
118
114
|
throw new Error(`Unable to find ${imp}, not in the manifest`);
|
|
119
115
|
} else if (imp.endsWith('.json')) {
|
|
120
116
|
imp = this.#idx.getFromImport(imp)?.sourceFile ?? imp;
|
|
121
|
-
return fs.readFile(imp, 'utf8').then(
|
|
117
|
+
return fs.readFile(imp, 'utf8').then(JSONUtil.parseSafe<T>);
|
|
122
118
|
}
|
|
123
119
|
|
|
124
120
|
if (!ManifestModuleUtil.SOURCE_EXT_REGEX.test(imp)) {
|
package/src/exec.ts
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
|
-
import { ChildProcess } from 'node:child_process';
|
|
2
|
-
import { Readable } from 'node:stream';
|
|
1
|
+
import { type ChildProcess, spawn, type SpawnOptions } from 'node:child_process';
|
|
2
|
+
import type { Readable } from 'node:stream';
|
|
3
3
|
import { createInterface } from 'node:readline/promises';
|
|
4
4
|
|
|
5
|
-
import { castTo } from './types.ts';
|
|
6
|
-
|
|
7
|
-
const MINUTE = (1000 * 60);
|
|
5
|
+
import { castTo, type Any } from './types.ts';
|
|
8
6
|
|
|
9
7
|
const ResultSymbol = Symbol();
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Result of an execution
|
|
11
|
+
*/
|
|
12
|
+
export interface ExecutionResult<T extends string | Buffer = string | Buffer> {
|
|
13
|
+
/**
|
|
14
|
+
* Stdout
|
|
15
|
+
*/
|
|
16
|
+
stdout: T;
|
|
17
|
+
/**
|
|
18
|
+
* Stderr
|
|
19
|
+
*/
|
|
20
|
+
stderr: T;
|
|
12
21
|
/**
|
|
13
22
|
* Exit code
|
|
14
23
|
*/
|
|
@@ -23,77 +32,19 @@ interface ExecutionBaseResult {
|
|
|
23
32
|
valid: boolean;
|
|
24
33
|
}
|
|
25
34
|
|
|
26
|
-
|
|
27
|
-
* Result of an execution
|
|
28
|
-
*/
|
|
29
|
-
export interface ExecutionResult<T extends string | Buffer = string | Buffer> extends ExecutionBaseResult {
|
|
30
|
-
/**
|
|
31
|
-
* Stdout
|
|
32
|
-
*/
|
|
33
|
-
stdout: T;
|
|
34
|
-
/**
|
|
35
|
-
* Stderr
|
|
36
|
-
*/
|
|
37
|
-
stderr: T;
|
|
38
|
-
}
|
|
35
|
+
type ExecutionBaseResult = Omit<ExecutionResult, 'stdout' | 'stderr'>;
|
|
39
36
|
|
|
40
37
|
/**
|
|
41
38
|
* Standard utilities for managing executions
|
|
42
39
|
*/
|
|
43
40
|
export class ExecUtil {
|
|
44
41
|
|
|
45
|
-
static RESTART_EXIT_CODE = 200;
|
|
46
|
-
|
|
47
42
|
/**
|
|
48
|
-
*
|
|
49
|
-
* @param run The factory to produce the next running process
|
|
50
|
-
* @param maxRetriesPerMinute The number of times to allow a retry within a minute
|
|
43
|
+
* Spawn wrapper that ensures performant invocation of trv commands
|
|
51
44
|
*/
|
|
52
|
-
static
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
): Promise<ExecutionResult> {
|
|
56
|
-
const maxRetries = config?.maxRetriesPerMinute ?? 5;
|
|
57
|
-
const relayInterrupt = config?.relayInterrupt ?? false;
|
|
58
|
-
|
|
59
|
-
const restarts: number[] = [];
|
|
60
|
-
|
|
61
|
-
if (!relayInterrupt) {
|
|
62
|
-
process.removeAllListeners('SIGINT'); // Remove any existing listeners
|
|
63
|
-
process.on('SIGINT', () => { }); // Prevents SIGINT from killing parent process, the child will handle
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
for (; ;) {
|
|
67
|
-
const subProcess = run();
|
|
68
|
-
const interrupt = (): void => { subProcess.kill('SIGINT'); };
|
|
69
|
-
const toMessage = (value: unknown): void => { subProcess.send?.(value!); };
|
|
70
|
-
|
|
71
|
-
// Proxy kill requests
|
|
72
|
-
process.on('message', toMessage);
|
|
73
|
-
if (relayInterrupt) {
|
|
74
|
-
process.on('SIGINT', interrupt);
|
|
75
|
-
}
|
|
76
|
-
subProcess.on('message', value => process.send?.(value));
|
|
77
|
-
|
|
78
|
-
const result = await this.getResult(subProcess, { catch: true });
|
|
79
|
-
if (result.code !== this.RESTART_EXIT_CODE) {
|
|
80
|
-
return result;
|
|
81
|
-
} else {
|
|
82
|
-
if (relayInterrupt) {
|
|
83
|
-
process.off('SIGINT', interrupt);
|
|
84
|
-
}
|
|
85
|
-
process.off('message', toMessage);
|
|
86
|
-
restarts.unshift(Date.now());
|
|
87
|
-
if (restarts.length === maxRetries) {
|
|
88
|
-
if ((restarts[0] - restarts[maxRetries - 1]) <= MINUTE) {
|
|
89
|
-
console.error(`Bailing, due to ${maxRetries} restarts in under a minute`);
|
|
90
|
-
return result;
|
|
91
|
-
}
|
|
92
|
-
restarts.pop(); // Keep list short
|
|
93
|
-
}
|
|
94
|
-
console.error('Restarting...', { pid: process.pid });
|
|
95
|
-
}
|
|
96
|
-
}
|
|
45
|
+
static spawnTrv(cmd: string, args: string[], options: SpawnOptions): ChildProcess {
|
|
46
|
+
const entry = (globalThis as Any).__entry_point__ ?? process.argv.at(1);
|
|
47
|
+
return spawn(process.argv0, [entry, cmd, ...args], options);
|
|
97
48
|
}
|
|
98
49
|
|
|
99
50
|
/**
|
package/src/file-loader.ts
CHANGED
|
@@ -4,6 +4,7 @@ import fs from 'node:fs/promises';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
|
|
6
6
|
import { AppError } from './error.ts';
|
|
7
|
+
import { JSONUtil } from './json.ts';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* File loader that will search for files across the provided search paths
|
|
@@ -64,4 +65,12 @@ export class FileLoader {
|
|
|
64
65
|
async readFile(relativePath: string): Promise<File> {
|
|
65
66
|
return new File([await this.read(relativePath, true)], path.basename(relativePath));
|
|
66
67
|
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Read relative file as JSON
|
|
71
|
+
*/
|
|
72
|
+
async readJSON<T>(relativePath: string): Promise<T> {
|
|
73
|
+
const location = await this.resolve(relativePath);
|
|
74
|
+
return JSONUtil.readFile<T>(location);
|
|
75
|
+
}
|
|
67
76
|
}
|
package/src/json.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
import type { Any } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* JSON Utility functions
|
|
8
|
+
*/
|
|
9
|
+
export class JSONUtil {
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse JSON safely
|
|
13
|
+
*/
|
|
14
|
+
static parseSafe<T>(input: string | Buffer, reviver?: (this: unknown, key: string, value: Any) => unknown): T {
|
|
15
|
+
if (typeof input !== 'string') {
|
|
16
|
+
input = input.toString('utf8');
|
|
17
|
+
}
|
|
18
|
+
// TODO: Ensure we aren't vulnerable to prototype pollution
|
|
19
|
+
return JSON.parse(input, reviver);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Encode JSON value as base64 encoded string
|
|
24
|
+
*/
|
|
25
|
+
static stringifyBase64<T>(value: T | undefined): string | undefined {
|
|
26
|
+
if (value === undefined) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const text = JSON.stringify(value);
|
|
30
|
+
return Buffer.from(text, 'utf8').toString('base64');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Decode JSON value from base64 encoded string
|
|
35
|
+
*/
|
|
36
|
+
static parseBase64<T>(input: string): T;
|
|
37
|
+
static parseBase64<T>(input?: string | undefined): T | undefined;
|
|
38
|
+
static parseBase64<T>(input?: string | undefined): T | undefined {
|
|
39
|
+
if (!input) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let decoded = Buffer.from(input, 'base64').toString('utf8');
|
|
44
|
+
|
|
45
|
+
// Read from encoded if it happens
|
|
46
|
+
if (decoded.startsWith('%')) {
|
|
47
|
+
decoded = decodeURIComponent(decoded);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return this.parseSafe(decoded);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Read JSON file asynchronously
|
|
55
|
+
* @param file
|
|
56
|
+
* @returns
|
|
57
|
+
*/
|
|
58
|
+
static async readFile<T>(file: string): Promise<T> {
|
|
59
|
+
const content = await fs.readFile(file, 'utf8');
|
|
60
|
+
return this.parseSafe(content);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Read JSON file synchronously
|
|
65
|
+
* @param file
|
|
66
|
+
*/
|
|
67
|
+
static readFileSync<T>(file: string, onMissing?: T): T {
|
|
68
|
+
if (!existsSync(file) && onMissing !== undefined) {
|
|
69
|
+
return onMissing;
|
|
70
|
+
}
|
|
71
|
+
const content = readFileSync(file, 'utf8');
|
|
72
|
+
return this.parseSafe(content);
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/trv.d.ts
CHANGED
|
@@ -21,10 +21,6 @@ declare module "@travetto/runtime" {
|
|
|
21
21
|
* Special role to run as, used to access additional files from the manifest during runtime.
|
|
22
22
|
*/
|
|
23
23
|
TRV_ROLE: Role;
|
|
24
|
-
/**
|
|
25
|
-
* Whether or not to run the program in dynamic mode, allowing for real-time updates
|
|
26
|
-
*/
|
|
27
|
-
TRV_DYNAMIC: boolean;
|
|
28
24
|
/**
|
|
29
25
|
* The folders to use for resource lookup
|
|
30
26
|
*/
|
package/src/types.ts
CHANGED
|
@@ -37,6 +37,7 @@ export const TypedObject: {
|
|
|
37
37
|
keys<T = unknown, K extends keyof T = keyof T & string>(value: T): K[];
|
|
38
38
|
fromEntries<K extends string | symbol, V>(items: ([K, V] | readonly [K, V])[]): Record<K, V>;
|
|
39
39
|
entries<K extends Record<symbol | string, unknown>>(record: K): [keyof K, K[keyof K]][];
|
|
40
|
+
assign<T extends {}, U extends T>(target: T, ...sources: U[]): U;
|
|
40
41
|
} & ObjectConstructor = Object;
|
|
41
42
|
|
|
42
43
|
export const safeAssign = <T extends {}, U extends {}>(target: T, ...sources: U[]): T & U =>
|
|
@@ -47,6 +48,7 @@ export function castTo<T>(input: unknown): T {
|
|
|
47
48
|
return input as T;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
export const isClass = (input: unknown): input is Class => typeof input === 'function' && 'Ⲑid' in input;
|
|
50
52
|
export const castKey = <T>(input: string | number | symbol): keyof T => castTo(input);
|
|
51
53
|
export const asFull = <T>(input: Partial<T>): T => castTo(input);
|
|
52
54
|
export const asConstructable = <Z = unknown>(input: Class | unknown): { constructor: Class<Z> } => castTo(input);
|
|
@@ -61,6 +63,11 @@ export const hasFunction = <T>(key: keyof T) => (value: unknown): value is T =>
|
|
|
61
63
|
|
|
62
64
|
export const hasToJSON = hasFunction<{ toJSON(): object }>('toJSON');
|
|
63
65
|
|
|
66
|
+
/**
|
|
67
|
+
* A type representing unknown type
|
|
68
|
+
*/
|
|
69
|
+
export class UnknownType { }
|
|
70
|
+
|
|
64
71
|
export function toConcrete<T extends unknown>(): Class<T> {
|
|
65
72
|
return arguments[0];
|
|
66
73
|
}
|
package/src/util.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import timers from 'node:timers/promises';
|
|
3
3
|
|
|
4
|
-
import { castTo
|
|
4
|
+
import { castTo } from './types.ts';
|
|
5
5
|
import { AppError } from './error.ts';
|
|
6
6
|
|
|
7
7
|
type MapFn<T, U> = (value: T, i: number) => U | Promise<U>;
|
|
@@ -120,82 +120,6 @@ export class Util {
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
/**
|
|
124
|
-
* Encode JSON value as base64 encoded string
|
|
125
|
-
*/
|
|
126
|
-
static encodeSafeJSON<T>(value: T | undefined): string | undefined {
|
|
127
|
-
if (value === undefined) {
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
const text = JSON.stringify(value);
|
|
131
|
-
return Buffer.from(text, 'utf8').toString('base64');
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Decode JSON value from base64 encoded string
|
|
136
|
-
*/
|
|
137
|
-
static decodeSafeJSON<T>(input: string): T;
|
|
138
|
-
static decodeSafeJSON<T>(input?: string | undefined): T | undefined;
|
|
139
|
-
static decodeSafeJSON<T>(input?: string | undefined): T | undefined {
|
|
140
|
-
if (!input) {
|
|
141
|
-
return undefined;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
let decoded = Buffer.from(input, 'base64').toString('utf8');
|
|
145
|
-
|
|
146
|
-
// Read from encoded if it happens
|
|
147
|
-
if (decoded.startsWith('%')) {
|
|
148
|
-
decoded = decodeURIComponent(decoded);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return JSON.parse(decoded, undefined);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Serialize to JSON
|
|
156
|
-
*/
|
|
157
|
-
static serializeToJSON<T>(out: T): string {
|
|
158
|
-
return JSON.stringify(out, function (key, value) {
|
|
159
|
-
const objectValue = this[key];
|
|
160
|
-
if (objectValue && objectValue instanceof Error) {
|
|
161
|
-
return {
|
|
162
|
-
$: true,
|
|
163
|
-
...hasToJSON(objectValue) ? objectValue.toJSON() : objectValue,
|
|
164
|
-
name: objectValue.name,
|
|
165
|
-
message: objectValue.message,
|
|
166
|
-
stack: objectValue.stack,
|
|
167
|
-
};
|
|
168
|
-
} else if (typeof value === 'bigint') {
|
|
169
|
-
return `${value.toString()}$n`;
|
|
170
|
-
} else {
|
|
171
|
-
return value;
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Deserialize from JSON
|
|
178
|
-
*/
|
|
179
|
-
static deserializeFromJson<T = unknown>(input: string): T {
|
|
180
|
-
return JSON.parse(input, function (key, value) {
|
|
181
|
-
if (value && typeof value === 'object' && '$' in value) {
|
|
182
|
-
const error = AppError.fromJSON(value) ?? new Error();
|
|
183
|
-
if (!(error instanceof AppError)) {
|
|
184
|
-
const { $: _, ...rest } = value;
|
|
185
|
-
Object.assign(error, rest);
|
|
186
|
-
}
|
|
187
|
-
error.message = value.message;
|
|
188
|
-
error.stack = value.stack;
|
|
189
|
-
error.name = value.name;
|
|
190
|
-
return error;
|
|
191
|
-
} else if (typeof value === 'string' && /^\d+[$]n$/.test(value)) {
|
|
192
|
-
return BigInt(value.split('$')[0]);
|
|
193
|
-
} else {
|
|
194
|
-
return value;
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
|
|
199
123
|
/**
|
|
200
124
|
* Retry an operation, with a custom conflict handler
|
|
201
125
|
* @param operation The operation to retry
|
package/src/watch.ts
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
|
+
import { ManifestModuleUtil, type ChangeEventType, type ManifestModuleFileType } from '@travetto/manifest';
|
|
2
|
+
|
|
1
3
|
import { RuntimeIndex } from './manifest-index.ts';
|
|
2
|
-
import { ExecUtil } from './exec.ts';
|
|
3
4
|
import { ShutdownManager } from './shutdown.ts';
|
|
4
5
|
import { Util } from './util.ts';
|
|
6
|
+
import { AppError } from './error.ts';
|
|
7
|
+
|
|
8
|
+
type WatchEvent = { file: string, action: ChangeEventType, output: string, module: string, import: string, time: number };
|
|
5
9
|
|
|
6
|
-
|
|
10
|
+
type WatchCompilerOptions = {
|
|
11
|
+
/**
|
|
12
|
+
* Restart the watch loop on compiler exit
|
|
13
|
+
*/
|
|
14
|
+
restartOnCompilerExit?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Run on restart
|
|
17
|
+
*/
|
|
18
|
+
onRestart?: () => void;
|
|
19
|
+
};
|
|
7
20
|
|
|
8
|
-
export async function* watchCompiler(config?:
|
|
21
|
+
export async function* watchCompiler(config?: WatchCompilerOptions): AsyncIterable<WatchEvent> {
|
|
9
22
|
// Load at runtime
|
|
10
23
|
const { CompilerClient } = await import('@travetto/compiler/support/server/client.ts');
|
|
11
24
|
|
|
@@ -19,21 +32,56 @@ export async function* watchCompiler(config?: { restartOnExit?: boolean, signal?
|
|
|
19
32
|
const controller = new AbortController();
|
|
20
33
|
const remove = ShutdownManager.onGracefulShutdown(async () => controller.abort());
|
|
21
34
|
|
|
22
|
-
|
|
35
|
+
const maxIterations = 10;
|
|
36
|
+
const maxWindow = 10 * 1000;
|
|
37
|
+
const iterations: number[] = [];
|
|
38
|
+
let iterationsExhausted = false;
|
|
39
|
+
|
|
40
|
+
while (
|
|
41
|
+
!controller.signal.aborted &&
|
|
42
|
+
!iterationsExhausted &&
|
|
43
|
+
(config?.restartOnCompilerExit || iterations.length === 0)
|
|
44
|
+
) {
|
|
45
|
+
if (iterations.length) { // Wait on next iteration
|
|
46
|
+
await Util.nonBlockingTimeout(10);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await client.waitForState(['compile-end', 'watch-start'], undefined, controller.signal);
|
|
23
50
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
51
|
+
if (!await client.isWatching()) { // If we get here, without a watch
|
|
52
|
+
throw new AppError('Compiler is not running');
|
|
53
|
+
} else {
|
|
54
|
+
if (iterations.length) {
|
|
55
|
+
config?.onRestart?.();
|
|
56
|
+
}
|
|
57
|
+
yield* client.fetchEvents('change', { signal: controller.signal, enforceIteration: true });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
iterations.push(Date.now());
|
|
61
|
+
if (iterations.length >= maxIterations) {
|
|
62
|
+
iterationsExhausted = (Date.now() - iterations[0]) > maxWindow;
|
|
63
|
+
iterations.shift();
|
|
27
64
|
}
|
|
28
|
-
} else {
|
|
29
|
-
yield* client.fetchEvents('change', { signal: controller.signal, enforceIteration: true });
|
|
30
65
|
}
|
|
31
66
|
|
|
32
67
|
remove();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function listenForSourceChanges(onChange: () => void, debounceDelay = 10): void {
|
|
71
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
72
|
+
|
|
73
|
+
const validFileTypes = new Set<ManifestModuleFileType>(['ts', 'js', 'package-json', 'typings']);
|
|
33
74
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
process.exit(ExecUtil.RESTART_EXIT_CODE);
|
|
75
|
+
function send(): void {
|
|
76
|
+
clearTimeout(timeout);
|
|
77
|
+
timeout = setTimeout(onChange, debounceDelay);
|
|
38
78
|
}
|
|
79
|
+
|
|
80
|
+
(async function (): Promise<void> {
|
|
81
|
+
for await (const item of watchCompiler({ restartOnCompilerExit: true, onRestart: send })) {
|
|
82
|
+
if (validFileTypes.has(ManifestModuleUtil.getFileType(item.file)) && RuntimeIndex.findModuleForArbitraryFile(item.file)) {
|
|
83
|
+
send();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
})();
|
|
39
87
|
}
|