@travetto/runtime 5.0.0-rc.10

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/console.ts ADDED
@@ -0,0 +1,137 @@
1
+ import util from 'node:util';
2
+ import debug from 'debug';
3
+
4
+ import { RuntimeIndex } from './manifest-index';
5
+
6
+ export type ConsoleEvent = {
7
+ /** Time of event */
8
+ timestamp: Date;
9
+ /** The level of the console event */
10
+ level: 'info' | 'warn' | 'debug' | 'error';
11
+ /** The line number the console event was triggered from */
12
+ line: number;
13
+ /** The module name for the source file */
14
+ module: string;
15
+ /** The module path for the source file*/
16
+ modulePath: string;
17
+ /** The computed scope for the console. statement. */
18
+ scope?: string;
19
+ /** Arguments passed to the console call*/
20
+ args: unknown[];
21
+ };
22
+
23
+ export interface ConsoleListener {
24
+ log(ev: ConsoleEvent): void;
25
+ }
26
+
27
+ const DEBUG_OG = { formatArgs: debug.formatArgs, log: debug.log };
28
+
29
+ /**
30
+ * Provides a general abstraction against the console.* methods to allow for easier capture and redirection.
31
+ *
32
+ * The transpiler will replace all console.* calls in the typescript files for the framework and those provided by the user.
33
+ * Any console.log statements elsewhere will not be affected.
34
+ */
35
+ class $ConsoleManager implements ConsoleListener {
36
+
37
+ /**
38
+ * The current listener
39
+ */
40
+ #listener: ConsoleListener;
41
+
42
+ /**
43
+ * List of logging filters
44
+ */
45
+ #filters: Partial<Record<ConsoleEvent['level'], (x: ConsoleEvent) => boolean>> = {};
46
+
47
+ constructor(listener: ConsoleListener) {
48
+ this.set(listener);
49
+ this.enhanceDebug(true);
50
+ this.debug(false);
51
+ util.inspect.defaultOptions.depth = Math.max(util.inspect.defaultOptions.depth ?? 0, 4);
52
+ }
53
+
54
+ /**
55
+ * Add exclusion
56
+ * @private
57
+ */
58
+ filter(level: ConsoleEvent['level'], filter?: boolean | ((ctx: ConsoleEvent) => boolean)): void {
59
+ if (filter !== undefined) {
60
+ if (typeof filter === 'boolean') {
61
+ const v = filter;
62
+ filter = (): boolean => v;
63
+ }
64
+ this.#filters[level] = filter;
65
+ } else {
66
+ delete this.#filters[level];
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Enable/disable enhanced debugging
72
+ */
73
+ enhanceDebug(active: boolean): void {
74
+ Error.stackTraceLimit = active ? 50 : 10;
75
+ if (active) {
76
+ debug.formatArgs = function (args: string[]): void {
77
+ args.unshift(this.namespace);
78
+ args.push(debug.humanize(this.diff));
79
+ };
80
+ debug.log = (modulePath, ...args: string[]): void => this.log({
81
+ level: 'debug', module: '@npm:debug', modulePath,
82
+ args: [util.format(...args)], line: 0, timestamp: new Date()
83
+ });
84
+ } else {
85
+ debug.formatArgs = DEBUG_OG.formatArgs;
86
+ debug.log = DEBUG_OG.log;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Set logging debug level
92
+ */
93
+ debug(value: false | string): void {
94
+ if (value !== false) {
95
+ const active = RuntimeIndex.getModuleList('workspace', value);
96
+ active.add('@npm:debug');
97
+ this.filter('debug', ctx => active.has(ctx.module));
98
+ } else {
99
+ this.filter('debug', () => false);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Handle direct call in lieu of the console.* commands
105
+ */
106
+ log(ev: ConsoleEvent & { import?: [string, string] }): void {
107
+ const outEv = {
108
+ ...ev,
109
+ timestamp: new Date(),
110
+ module: ev.module ?? ev.import?.[0],
111
+ modulePath: ev.modulePath ?? ev.import?.[1]
112
+ };
113
+
114
+ if (this.#filters[outEv.level] && !this.#filters[outEv.level]!(outEv)) {
115
+ return; // Do nothing
116
+ } else {
117
+ return this.#listener.log(outEv);
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Set a new console listener
123
+ */
124
+ set(cons: ConsoleListener): void {
125
+ this.#listener = cons;
126
+ }
127
+
128
+ /**
129
+ * Get the listener
130
+ */
131
+ get(): ConsoleListener {
132
+ return this.#listener;
133
+ }
134
+ }
135
+
136
+ export const ConsoleManager = new $ConsoleManager({ log(ev): void { console![ev.level](...ev.args); } });
137
+ export const log = ConsoleManager.log.bind(ConsoleManager);
package/src/context.ts ADDED
@@ -0,0 +1,122 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import { type ManifestIndex, type ManifestContext, ManifestModuleUtil } from '@travetto/manifest';
5
+
6
+ import { Env } from './env';
7
+ import { RuntimeIndex } from './manifest-index';
8
+ import { describeFunction } from './function';
9
+
10
+ /** Constrained version of {@type ManifestContext} */
11
+ class $Runtime {
12
+
13
+ #idx: ManifestIndex;
14
+ #resourceOverrides?: Record<string, string>;
15
+
16
+ constructor(idx: ManifestIndex, resourceOverrides?: Record<string, string>) {
17
+ this.#idx = idx;
18
+ this.#resourceOverrides = resourceOverrides;
19
+ }
20
+
21
+ get #moduleAliases(): Record<string, string> {
22
+ return {
23
+ '@': this.#idx.mainModule.sourcePath,
24
+ '@@': this.#idx.manifest.workspace.path,
25
+ };
26
+ }
27
+
28
+ /** Get env name, with support for the default env */
29
+ get env(): string | undefined {
30
+ return Env.TRV_ENV.val || (!this.production ? this.#idx.manifest.workspace.defaultEnv : undefined);
31
+ }
32
+
33
+ /** Are we in development mode */
34
+ get production(): boolean {
35
+ return process.env.NODE_ENV === 'production';
36
+ }
37
+
38
+ /** Is the app in dynamic mode? */
39
+ get dynamic(): boolean {
40
+ return Env.TRV_DYNAMIC.isTrue;
41
+ }
42
+
43
+ /** Get debug value */
44
+ get debug(): false | string {
45
+ const val = Env.DEBUG.val ?? '';
46
+ return (!val && this.production) || Env.DEBUG.isFalse ? false : val;
47
+ }
48
+
49
+ /** Manifest main */
50
+ get main(): ManifestContext['main'] {
51
+ return this.#idx.manifest.main;
52
+ }
53
+
54
+ /** Manifest workspace */
55
+ get workspace(): ManifestContext['workspace'] {
56
+ return this.#idx.manifest.workspace;
57
+ }
58
+
59
+ /** Are we running from a mono-root? */
60
+ get monoRoot(): boolean {
61
+ return !!this.workspace.mono && !this.main.folder;
62
+ }
63
+
64
+ /** Main source path */
65
+ get mainSourcePath(): string {
66
+ return this.#idx.mainModule.sourcePath;
67
+ }
68
+
69
+ /** Produce a workspace relative path */
70
+ workspaceRelative(...rel: string[]): string {
71
+ return path.resolve(this.workspace.path, ...rel);
72
+ }
73
+
74
+ /** Strip off the workspace path from a file */
75
+ stripWorkspacePath(full: string): string {
76
+ return full === this.workspace.path ? '' : full.replace(`${this.workspace.path}/`, '');
77
+ }
78
+
79
+ /** Produce a workspace path for tooling, with '@' being replaced by node_module/name folder */
80
+ toolPath(...rel: string[]): string {
81
+ rel = rel.flatMap(x => x === '@' ? ['node_modules', this.#idx.manifest.main.name] : [x]);
82
+ return path.resolve(this.workspace.path, this.#idx.manifest.build.toolFolder, ...rel);
83
+ }
84
+
85
+ /** Resolve single module path */
86
+ modulePath(modulePath: string): string {
87
+ const [base, sub] = (this.#resourceOverrides?.[modulePath] ?? modulePath)
88
+ .replace(/^([^#]*)(#|$)/g, (_, v, r) => `${this.#moduleAliases[v] ?? v}${r}`)
89
+ .split('#');
90
+ return path.resolve(this.#idx.getModule(base)?.sourcePath ?? base, sub ?? '.');
91
+ }
92
+
93
+ /** Resolve resource paths */
94
+ resourcePaths(paths: string[] = []): string[] {
95
+ return [...new Set([...paths, ...Env.TRV_RESOURCES.list ?? [], '@#resources', '@@#resources'].map(v => this.modulePath(v)))];
96
+ }
97
+
98
+ /** Get source for function */
99
+ getSourceFile(fn: Function): string {
100
+ return this.#idx.getFromImport(this.getImport(fn))?.sourceFile!;
101
+ }
102
+
103
+ /** Get import for function */
104
+ getImport(fn: Function): string {
105
+ return describeFunction(fn).import;
106
+ }
107
+
108
+ /** Import from a given path */
109
+ importFrom<T = unknown>(imp?: string): Promise<T> {
110
+ const file = path.resolve(this.#idx.mainModule.sourcePath, imp!);
111
+ if (existsSync(file)) {
112
+ imp = this.#idx.getFromSource(file)?.import;
113
+ }
114
+ if (!imp) {
115
+ throw new Error(`Unable to find ${imp}, not in the manifest`);
116
+ }
117
+ imp = ManifestModuleUtil.withOutputExtension(imp);
118
+ return import(imp);
119
+ }
120
+ }
121
+
122
+ export const Runtime = new $Runtime(RuntimeIndex, Env.TRV_RESOURCE_OVERRIDES.object);
package/src/debug.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { Env } from './env';
2
+ import { ClassInstance } from './types';
3
+
4
+ /**
5
+ * The `@DebugBreak` indicates that a function inserts an optional debugger keyword to stop on entry
6
+ * @augments `@travetto/runtime:DebugBreak`
7
+ */
8
+ export function DebugBreak(): MethodDecorator {
9
+ return (inst: ClassInstance, prop: string | symbol, descriptor: PropertyDescriptor) => descriptor;
10
+
11
+ }
12
+
13
+ /**
14
+ * Determine if we should invoke the debugger
15
+ */
16
+ export function tryDebugger(): boolean {
17
+ return Env.TRV_DEBUG_BREAK.isTrue;
18
+ }
package/src/env.ts ADDED
@@ -0,0 +1,109 @@
1
+ import { castKey, castTo } from './types';
2
+
3
+ const IS_TRUE = /^(true|yes|on|1)$/i;
4
+ const IS_FALSE = /^(false|no|off|0)$/i;
5
+
6
+ export class EnvProp<T> {
7
+ constructor(public readonly key: string) { }
8
+
9
+ /** Set value according to prop type */
10
+ set(val: T | undefined | null): void {
11
+ if (val === undefined || val === null) {
12
+ delete process.env[this.key];
13
+ } else {
14
+ process.env[this.key] = Array.isArray(val) ? `${val.join(',')}` : `${val}`;
15
+ }
16
+ }
17
+
18
+ /** Remove value */
19
+ clear(): void {
20
+ this.set(null);
21
+ }
22
+
23
+ /** Export value */
24
+ export(val: T | undefined): Record<string, string> {
25
+ let out: string;
26
+ if (val === undefined || val === '' || val === null) {
27
+ out = '';
28
+ } else if (Array.isArray(val)) {
29
+ out = val.join(',');
30
+ } else if (typeof val === 'object') {
31
+ out = Object.entries(val).map(([k, v]) => `${k}=${v}`).join(',');
32
+ } else {
33
+ out = `${val}`;
34
+ }
35
+ return { [this.key]: out };
36
+ }
37
+
38
+ /** Read value as string */
39
+ get val(): string | undefined { return process.env[this.key] || undefined; }
40
+
41
+ /** Read value as list */
42
+ get list(): string[] | undefined {
43
+ const val = this.val;
44
+ return (val === undefined || val === '') ?
45
+ undefined : val.split(/[, ]+/g).map(x => x.trim()).filter(x => !!x);
46
+ }
47
+
48
+ /** Read value as object */
49
+ get object(): Record<string, string> | undefined {
50
+ const items = this.list;
51
+ return items ? Object.fromEntries(items.map(x => x.split(/[:=]/g))) : undefined;
52
+ }
53
+
54
+ /** Add values to list */
55
+ add(...items: string[]): void {
56
+ process.env[this.key] = [... new Set([...this.list ?? [], ...items])].join(',');
57
+ }
58
+
59
+ /** Read value as int */
60
+ get int(): number | undefined {
61
+ const vi = parseInt(this.val ?? '', 10);
62
+ return Number.isNaN(vi) ? undefined : vi;
63
+ }
64
+
65
+ /** Read value as boolean */
66
+ get bool(): boolean | undefined {
67
+ const val = this.val;
68
+ return (val === undefined || val === '') ? undefined : IS_TRUE.test(val);
69
+ }
70
+
71
+ /** Determine if the underlying value is truthy */
72
+ get isTrue(): boolean {
73
+ return IS_TRUE.test(this.val ?? '');
74
+ }
75
+
76
+ /** Determine if the underlying value is falsy */
77
+ get isFalse(): boolean {
78
+ return IS_FALSE.test(this.val ?? '');
79
+ }
80
+
81
+ /** Determine if the underlying value is set */
82
+ get isSet(): boolean {
83
+ const val = this.val;
84
+ return val !== undefined && val !== '';
85
+ }
86
+ }
87
+
88
+ type AllType = {
89
+ [K in keyof TravettoEnv]: Pick<EnvProp<TravettoEnv[K]>, 'key' | 'export' | 'val' | 'set' | 'clear' | 'isSet' |
90
+ (TravettoEnv[K] extends unknown[] ? 'list' | 'add' : never) |
91
+ (Extract<TravettoEnv[K], object> extends never ? never : 'object') |
92
+ (Extract<TravettoEnv[K], number> extends never ? never : 'int') |
93
+ (Extract<TravettoEnv[K], boolean> extends never ? never : 'bool' | 'isTrue' | 'isFalse')
94
+ >
95
+ };
96
+
97
+ function delegate<T extends object>(base: T): AllType & T {
98
+ return new Proxy(castTo(base), {
99
+ get(target, prop): unknown {
100
+ return typeof prop !== 'string' ? undefined :
101
+ (prop in base ? base[castKey(prop)] :
102
+ target[castKey<typeof target>(prop)] ??= castTo(new EnvProp(prop))
103
+ );
104
+ }
105
+ });
106
+ }
107
+
108
+ /** Basic utils for reading known environment variables */
109
+ export const Env = delegate({});
package/src/error.ts ADDED
@@ -0,0 +1,66 @@
1
+ import { castTo } from './types';
2
+
3
+ export type ErrorCategory =
4
+ 'general' |
5
+ 'notfound' |
6
+ 'data' |
7
+ 'permissions' |
8
+ 'authentication' |
9
+ 'timeout' |
10
+ 'unavailable';
11
+
12
+ /**
13
+ * Framework error class, with the aim of being extensible
14
+ */
15
+ export class AppError<T = unknown> extends Error {
16
+
17
+ /** Convert from JSON object */
18
+ static fromJSON(e: unknown): AppError | undefined {
19
+ if (!!e && typeof e === 'object' &&
20
+ ('message' in e && typeof e.message === 'string') &&
21
+ ('category' in e && typeof e.category === 'string') &&
22
+ ('type' in e && typeof e.type === 'string') &&
23
+ ('at' in e && typeof e.at === 'number')
24
+ ) {
25
+ const err = new AppError(e.message, castTo(e.category), 'details' in e ? e.details : undefined);
26
+ err.at = new Date(e.at);
27
+ err.type = e.type;
28
+ return err;
29
+ }
30
+ }
31
+
32
+ type: string;
33
+ at = new Date();
34
+ details: T;
35
+
36
+ /**
37
+ * Build an app error
38
+ *
39
+ * @param message The error message
40
+ * @param category The error category, can be mapped to HTTP statuses
41
+ * @param details Optional error payload
42
+ */
43
+ constructor(
44
+ message: string,
45
+ public category: ErrorCategory = 'general',
46
+ details?: T
47
+
48
+ ) {
49
+ super(message);
50
+ this.type = this.constructor.name;
51
+ this.details = details!;
52
+ }
53
+
54
+ /**
55
+ * The format of the JSON output
56
+ */
57
+ toJSON(): { message: string, category: string, type: string, at: string, details?: Record<string, unknown> } {
58
+ return {
59
+ message: this.message,
60
+ category: this.category,
61
+ type: this.type,
62
+ at: this.at.toISOString(),
63
+ details: castTo(this.details),
64
+ };
65
+ }
66
+ }
package/src/exec.ts ADDED
@@ -0,0 +1,156 @@
1
+ import { ChildProcess } from 'node:child_process';
2
+ import { Readable } from 'node:stream';
3
+ import { createInterface } from 'node:readline/promises';
4
+ import { castTo } from './types';
5
+
6
+ const MINUTE = (1000 * 60);
7
+
8
+ const RESULT = Symbol.for('@travetto/runtime:exec-result');
9
+
10
+ interface ExecutionBaseResult {
11
+ /**
12
+ * Exit code
13
+ */
14
+ code: number;
15
+ /**
16
+ * Execution result message, should be inline with code
17
+ */
18
+ message?: string;
19
+ /**
20
+ * Whether or not the execution completed successfully
21
+ */
22
+ valid: boolean;
23
+ }
24
+
25
+ /**
26
+ * Result of an execution
27
+ */
28
+ export interface ExecutionResult<T extends string | Buffer = string | Buffer> extends ExecutionBaseResult {
29
+ /**
30
+ * Stdout
31
+ */
32
+ stdout: T;
33
+ /**
34
+ * Stderr
35
+ */
36
+ stderr: T;
37
+ }
38
+
39
+ /**
40
+ * Standard utilities for managing executions
41
+ */
42
+ export class ExecUtil {
43
+
44
+ static RESTART_EXIT_CODE = 200;
45
+
46
+ /**
47
+ * Run with automatic restart support
48
+ * @param run The factory to produce the next running process
49
+ * @param maxRetriesPerMinute The number of times to allow a retry within a minute
50
+ */
51
+ static async withRestart(run: () => ChildProcess, maxRetriesPerMinute?: number): Promise<ExecutionResult> {
52
+ const maxRetries = maxRetriesPerMinute ?? 5;
53
+ const restarts: number[] = [];
54
+
55
+ for (; ;) {
56
+ const proc = run();
57
+
58
+ const toKill = (): void => { proc.kill('SIGKILL'); };
59
+ const toMessage = (v: unknown): void => { proc.send?.(v!); };
60
+
61
+ // Proxy kill requests
62
+ process.on('message', toMessage);
63
+ process.on('SIGINT', toKill);
64
+ proc.on('message', v => process.send?.(v));
65
+
66
+ const result = await this.getResult(proc, { catch: true });
67
+ if (result.code !== this.RESTART_EXIT_CODE) {
68
+ return result;
69
+ } else {
70
+ process.off('SIGINT', toKill);
71
+ process.off('message', toMessage);
72
+ restarts.unshift(Date.now());
73
+ if (restarts.length === maxRetries) {
74
+ if ((restarts[0] - restarts[maxRetries - 1]) <= MINUTE) {
75
+ console.error(`Bailing, due to ${maxRetries} restarts in under a minute`);
76
+ return result;
77
+ }
78
+ restarts.pop(); // Keep list short
79
+ }
80
+ console.error('Restarting...', { pid: process.pid });
81
+ }
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Take a child process, and some additional options, and produce a promise that
87
+ * represents the entire execution. On successful completion the promise will resolve, and
88
+ * on failed completion the promise will reject.
89
+ *
90
+ * @param proc The process to enhance
91
+ * @param options The options to use to enhance the process
92
+ */
93
+ static getResult(proc: ChildProcess): Promise<ExecutionResult<string>>;
94
+ static getResult(proc: ChildProcess, options: { catch?: boolean, binary?: false }): Promise<ExecutionResult<string>>;
95
+ static getResult(proc: ChildProcess, options: { catch?: boolean, binary: true }): Promise<ExecutionResult<Buffer>>;
96
+ static getResult<T extends string | Buffer>(proc: ChildProcess, options: { catch?: boolean, binary?: boolean } = {}): Promise<ExecutionResult<T>> {
97
+ const _proc: ChildProcess & { [RESULT]?: Promise<ExecutionResult> } = proc;
98
+ const res = _proc[RESULT] ??= new Promise<ExecutionResult>(resolve => {
99
+ const stdout: Buffer[] = [];
100
+ const stderr: Buffer[] = [];
101
+ let done = false;
102
+ const finish = (result: ExecutionBaseResult): void => {
103
+ if (done) {
104
+ return;
105
+ }
106
+ done = true;
107
+
108
+ const buffers = {
109
+ stdout: Buffer.concat(stdout),
110
+ stderr: Buffer.concat(stderr),
111
+ };
112
+
113
+ const final = {
114
+ stdout: options.binary ? buffers.stdout : buffers.stdout.toString('utf8'),
115
+ stderr: options.binary ? buffers.stderr : buffers.stderr.toString('utf8'),
116
+ ...result
117
+ };
118
+
119
+ resolve(!final.valid ?
120
+ { ...final, message: `${final.message || final.stderr || final.stdout || 'failed'}` } :
121
+ final
122
+ );
123
+ };
124
+
125
+ proc.stdout?.on('data', (d: string | Buffer) => stdout.push(Buffer.from(d)));
126
+ proc.stderr?.on('data', (d: string | Buffer) => stderr.push(Buffer.from(d)));
127
+
128
+ proc.on('error', (err: Error) =>
129
+ finish({ code: 1, message: err.message, valid: false }));
130
+
131
+ proc.on('close', (code: number) =>
132
+ finish({ code, valid: code === null || code === 0 }));
133
+
134
+ if (proc.exitCode !== null) { // We are already done
135
+ finish({ code: proc.exitCode, valid: proc.exitCode === 0 });
136
+ }
137
+ });
138
+
139
+ return castTo(options.catch ? res : res.then(v => {
140
+ if (v.valid) {
141
+ return v;
142
+ } else {
143
+ throw new Error(v.message);
144
+ }
145
+ }));
146
+ }
147
+
148
+ /**
149
+ * Consume lines
150
+ */
151
+ static async readLines(stream: Readable, handler: (input: string) => unknown | Promise<unknown>): Promise<void> {
152
+ for await (const item of createInterface(stream)) {
153
+ await handler(item);
154
+ }
155
+ }
156
+ }
@@ -0,0 +1,59 @@
1
+ import { createReadStream } from 'node:fs';
2
+ import { Readable } from 'node:stream';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+
6
+ import { AppError } from './error';
7
+
8
+ /**
9
+ * File loader that will search for files across the provided search paths
10
+ */
11
+ export class FileLoader {
12
+
13
+ #searchPaths: readonly string[];
14
+
15
+ constructor(paths: string[]) {
16
+ this.#searchPaths = [...new Set(paths)]; // Dedupe
17
+ }
18
+
19
+ /**
20
+ * The paths that will be searched on resolve
21
+ */
22
+ get searchPaths(): readonly string[] {
23
+ return this.#searchPaths;
24
+ }
25
+
26
+ /**
27
+ * Return the absolute path for the given relative path
28
+ * @param relativePath The path to resolve
29
+ */
30
+ async resolve(relativePath: string): Promise<string> {
31
+ for (const sub of this.searchPaths) {
32
+ const resolved = path.join(sub, relativePath);
33
+ if (await fs.stat(resolved).catch(() => false)) {
34
+ return resolved;
35
+ }
36
+ }
37
+ throw new AppError(`Unable to find: ${relativePath}, searched=${this.searchPaths.join(',')}`, 'notfound');
38
+ }
39
+
40
+ /**
41
+ * Read a file, after resolving the path
42
+ * @param relativePath The path to read
43
+ */
44
+ async read(relativePath: string, binary?: false): Promise<string>;
45
+ async read(relativePath: string, binary: true): Promise<Buffer>;
46
+ async read(relativePath: string, binary = false): Promise<string | Buffer> {
47
+ const file = await this.resolve(relativePath);
48
+ return fs.readFile(file, binary ? undefined : 'utf8');
49
+ }
50
+
51
+ /**
52
+ * Read a file as a stream
53
+ * @param relativePath The path to read
54
+ */
55
+ async readStream(relativePath: string, binary = true): Promise<Readable> {
56
+ const file = await this.resolve(relativePath);
57
+ return createReadStream(file, { encoding: binary ? undefined : 'utf8' });
58
+ }
59
+ }