@travetto/runtime 5.0.0-rc.2

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/types.ts ADDED
@@ -0,0 +1,12 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ export type Class<T = any> = abstract new (...args: any[]) => T;
3
+ export type ConcreteClass<T = any> = new (...args: any[]) => T;
4
+ export type ClassInstance<T = any> = T & {
5
+ constructor: ConcreteClass<T> & { Ⲑid: string };
6
+ };
7
+
8
+ export const TypedObject: {
9
+ keys<T = unknown, K extends keyof T = keyof T>(o: T): K[];
10
+ fromEntries<K extends string | symbol, V>(items: ([K, V] | readonly [K, V])[]): Record<K, V>;
11
+ entries<K extends Record<symbol | string, unknown>>(record: K): [keyof K, K[keyof K]][];
12
+ } & ObjectConstructor = Object;
package/src/util.ts ADDED
@@ -0,0 +1,104 @@
1
+ import crypto from 'node:crypto';
2
+ import timers from 'node:timers/promises';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+
6
+ type PromiseWithResolvers<T> = {
7
+ resolve: (v: T) => void;
8
+ reject: (err?: unknown) => void;
9
+ promise: Promise<T>;
10
+ };
11
+
12
+ type MapFn<T, U> = (val: T, i: number) => U | Promise<U>;
13
+
14
+ /**
15
+ * Grab bag of common utilities
16
+ */
17
+ export class Util {
18
+
19
+ /**
20
+ * Generate a random UUID
21
+ * @param len The length of the uuid to generate
22
+ */
23
+ static uuid(len: number = 32): string {
24
+ const bytes = crypto.randomBytes(Math.ceil(len / 2));
25
+ // eslint-disable-next-line no-bitwise
26
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
27
+ // eslint-disable-next-line no-bitwise
28
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
29
+ return bytes.toString('hex').substring(0, len);
30
+ }
31
+
32
+ /**
33
+ * Generate a proper sha512 hash from a src value
34
+ * @param src The seed value to build the hash from
35
+ * @param len The optional length of the hash to generate
36
+ */
37
+ static hash(src: string, len: number = -1): string {
38
+ const hash = crypto.createHash('sha512');
39
+ hash.update(src);
40
+ const ret = hash.digest('hex');
41
+ return len > 0 ? ret.substring(0, len) : ret;
42
+ }
43
+
44
+ /**
45
+ * Produce a promise that is externally resolvable
46
+ */
47
+ static resolvablePromise<T = void>(): PromiseWithResolvers<T> {
48
+ let ops: Pick<PromiseWithResolvers<T>, 'reject' | 'resolve'>;
49
+ const prom = new Promise<T>((resolve, reject) => ops = { resolve, reject });
50
+ return { ...ops!, promise: prom };
51
+ }
52
+
53
+ /**
54
+ * Map an async iterable with various mapping functions
55
+ */
56
+ static mapAsyncItr<T, U, V, W>(source: AsyncIterable<T>, fn1: MapFn<T, U>, fn2: MapFn<U, V>, fn3: MapFn<V, W>): AsyncIterable<W>;
57
+ static mapAsyncItr<T, U, V>(source: AsyncIterable<T>, fn1: MapFn<T, U>, fn2: MapFn<U, V>): AsyncIterable<V>;
58
+ static mapAsyncItr<T, U>(source: AsyncIterable<T>, fn: MapFn<T, U>): AsyncIterable<U>;
59
+ static async * mapAsyncItr<T>(source: AsyncIterable<T>, ...fns: MapFn<unknown, unknown>[]): AsyncIterable<unknown> {
60
+ let idx = -1;
61
+ for await (const el of source) {
62
+ if (el !== undefined) {
63
+ idx += 1;
64
+ let m = el;
65
+ for (const fn of fns) {
66
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
67
+ m = (await fn(m, idx)) as typeof m;
68
+ }
69
+ yield m;
70
+ }
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Write file and copy over when ready
76
+ */
77
+ static async bufferedFileWrite(file: string, content: string): Promise<void> {
78
+ const temp = path.resolve(path.dirname(file), `.${process.hrtime()[0]}.${path.basename(file)}`);
79
+ await fs.mkdir(path.dirname(file), { recursive: true });
80
+ await fs.writeFile(temp, content, 'utf8');
81
+ await fs.rename(temp, file);
82
+ }
83
+
84
+ /**
85
+ * Non-blocking timeout
86
+ */
87
+ static nonBlockingTimeout(time: number): Promise<void> {
88
+ return timers.setTimeout(time, undefined, { ref: false }).catch(() => { });
89
+ }
90
+
91
+ /**
92
+ * Blocking timeout
93
+ */
94
+ static blockingTimeout(time: number): Promise<void> {
95
+ return timers.setTimeout(time, undefined, { ref: true }).catch(() => { });
96
+ }
97
+
98
+ /**
99
+ * Queue new macrotask
100
+ */
101
+ static queueMacroTask(): Promise<void> {
102
+ return timers.setImmediate(undefined);
103
+ }
104
+ }
package/src/watch.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { RuntimeIndex } from './manifest-index';
2
+ import { ExecUtil } from './exec';
3
+ import { ShutdownManager } from './shutdown';
4
+ import { Util } from './util';
5
+
6
+ export type WatchEvent = { file: string, action: 'create' | 'update' | 'delete', output: string, module: string, time: number };
7
+
8
+ export async function* watchCompiler(cfg?: { restartOnExit?: boolean, signal?: AbortSignal }): AsyncIterable<WatchEvent> {
9
+ // Load at runtime
10
+ const { CompilerClient } = await import('@travetto/compiler/support/server/client');
11
+
12
+ const client = new CompilerClient(RuntimeIndex.manifest, {
13
+ warn(message, ...args): void { console.error('warn', message, ...args); },
14
+ debug(message, ...args): void { console.error('debug', message, ...args); },
15
+ error(message, ...args): void { console.error('error', message, ...args); },
16
+ info(message, ...args): void { console.error('info', message, ...args); },
17
+ });
18
+
19
+ const ctrl = new AbortController();
20
+ const remove = ShutdownManager.onGracefulShutdown(async () => ctrl.abort(), watchCompiler);
21
+
22
+ await client.waitForState(['compile-end', 'watch-start'], undefined, ctrl.signal);
23
+
24
+ if (!await client.isWatching()) { // If we get here, without a watch
25
+ while (!await client.isWatching()) { // Wait until watch starts
26
+ await Util.nonBlockingTimeout(1000 * 60);
27
+ }
28
+ } else {
29
+ yield* client.fetchEvents('change', { signal: ctrl.signal, enforceIteration: true });
30
+ }
31
+
32
+ remove();
33
+
34
+ if (cfg?.restartOnExit) {
35
+ // We are done, request restart
36
+ await ShutdownManager.gracefulShutdown(ExecUtil.RESTART_EXIT_CODE);
37
+ }
38
+ }
@@ -0,0 +1,113 @@
1
+ import ts from 'typescript';
2
+
3
+ import {
4
+ TransformerState, OnCall, LiteralUtil,
5
+ OnClass, AfterClass, OnMethod, AfterMethod, AfterFunction, OnFunction
6
+ } from '@travetto/transformer';
7
+
8
+ const CONSOLE_IMPORT = '@travetto/runtime/src/console';
9
+ const MANIFEST_MOD = '@travetto/manifest';
10
+
11
+ type CustomState = TransformerState & {
12
+ scope: { type: 'method' | 'class' | 'function', name: string }[];
13
+ imported?: ts.Identifier;
14
+ };
15
+
16
+ const VALID_LEVELS: Record<string, string> = {
17
+ log: 'info',
18
+ info: 'info',
19
+ debug: 'debug',
20
+ warn: 'warn',
21
+ error: 'error'
22
+ };
23
+
24
+ /**
25
+ * Allows for removal of debug log messages depending on whether app is running
26
+ * in prod mode.
27
+ */
28
+ export class ConsoleLogTransformer {
29
+
30
+ static initState(state: CustomState): void {
31
+ state.scope = state.scope ?? [];
32
+ }
33
+
34
+ @OnClass()
35
+ static startClassForLog(state: CustomState, node: ts.ClassDeclaration): typeof node {
36
+ this.initState(state);
37
+ state.scope.push({ type: 'class', name: node.name?.text ?? 'unknown' });
38
+ return node;
39
+ }
40
+
41
+ @AfterClass()
42
+ static leaveClassForLog(state: CustomState, node: ts.ClassDeclaration): typeof node {
43
+ state.scope.pop();
44
+ return node;
45
+ }
46
+
47
+ @OnMethod()
48
+ static startMethodForLog(state: CustomState, node: ts.MethodDeclaration): typeof node {
49
+ this.initState(state);
50
+ let name = 'unknown';
51
+ if (ts.isIdentifier(node.name) || ts.isPrivateIdentifier(node.name)) {
52
+ name = node.name?.text ?? name;
53
+ }
54
+ state.scope.push({ type: 'method', name });
55
+ return node;
56
+ }
57
+
58
+ @AfterMethod()
59
+ static leaveMethodForLog(state: CustomState, node: ts.MethodDeclaration): typeof node {
60
+ state.scope.pop();
61
+ return node;
62
+ }
63
+
64
+ @OnFunction()
65
+ static startFunctionForLog(state: CustomState, node: ts.FunctionDeclaration | ts.FunctionExpression): typeof node {
66
+ this.initState(state);
67
+ state.scope.push({ type: 'function', name: node.name?.text ?? 'unknown' });
68
+ return node;
69
+ }
70
+
71
+ @AfterFunction()
72
+ static leaveFunctionForLog(state: CustomState, node: ts.FunctionDeclaration | ts.FunctionExpression): typeof node {
73
+ state.scope.pop();
74
+ return node;
75
+ }
76
+
77
+ @OnCall()
78
+ static onLogCall(state: CustomState, node: ts.CallExpression): typeof node | ts.Identifier {
79
+ if (!ts.isPropertyAccessExpression(node.expression) || state.importName.startsWith(MANIFEST_MOD)) {
80
+ return node;
81
+ }
82
+
83
+ const chain = node.expression;
84
+ const name = chain.name;
85
+ const prop = chain.expression;
86
+
87
+ if (!ts.isIdentifier(prop) || prop.escapedText !== 'console' || !ts.isIdentifier(name)) {
88
+ return node;
89
+ }
90
+
91
+ const level = name.escapedText!;
92
+
93
+ if (VALID_LEVELS[level]) {
94
+ const ident = state.imported ??= state.importFile(CONSOLE_IMPORT, 'ᚕ_c').ident;
95
+ return state.factory.updateCallExpression(
96
+ node,
97
+ state.createAccess(ident, 'log'),
98
+ node.typeArguments,
99
+ [
100
+ LiteralUtil.fromLiteral(state.factory, {
101
+ level: state.factory.createStringLiteral(VALID_LEVELS[level]),
102
+ import: state.getModuleIdentifier(),
103
+ line: state.source.getLineAndCharacterOfPosition(node.getStart(state.source)).line + 1,
104
+ scope: state.scope?.map(x => x.name).join(':'),
105
+ args: node.arguments.slice(0)
106
+ }),
107
+ ]
108
+ );
109
+ } else {
110
+ return node;
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,137 @@
1
+ import ts from 'typescript';
2
+
3
+ import {
4
+ TransformerState, OnMethod, OnClass, AfterClass,
5
+ AfterFunction, CoreUtil, SystemUtil, Import
6
+ } from '@travetto/transformer';
7
+
8
+ import type { FunctionMetadataTag } from '../src/function';
9
+
10
+ const RUNTIME_MOD = '@travetto/runtime';
11
+ const RUNTIME_MOD_SRC = `${RUNTIME_MOD}/src`;
12
+ const REGISTER_IMPORT = `${RUNTIME_MOD_SRC}/function`;
13
+
14
+ const methods = Symbol.for(`${RUNTIME_MOD}:methods`);
15
+ const cls = Symbol.for(`${RUNTIME_MOD}:class`);
16
+ const fn = Symbol.for(`${RUNTIME_MOD}:function`);
17
+ const registerImport = Symbol.for(`${RUNTIME_MOD}:registerImport`);
18
+
19
+ interface MetadataInfo {
20
+ [registerImport]?: Import;
21
+ [methods]?: Record<string, FunctionMetadataTag>;
22
+ [cls]?: FunctionMetadataTag;
23
+ [fn]?: number;
24
+ }
25
+
26
+ /**
27
+ * Providing metadata for classes
28
+ */
29
+ export class RegisterTransformer {
30
+
31
+ static #tag(state: TransformerState, node: ts.Node): FunctionMetadataTag {
32
+ const hash = SystemUtil.naiveHash(node.getText());
33
+ try {
34
+ const range = CoreUtil.getRangeOf(state.source, node) ?? [0, 0];
35
+ return { hash, lines: range };
36
+ } catch (err) {
37
+ return { hash, lines: [0, 0] };
38
+ }
39
+ }
40
+
41
+ static #valid({ importName: imp }: TransformerState): boolean {
42
+ return !imp.startsWith(REGISTER_IMPORT);
43
+ }
44
+
45
+ /**
46
+ * Hash each class
47
+ */
48
+ @OnClass()
49
+ static collectClassMetadata(state: TransformerState & MetadataInfo, node: ts.ClassDeclaration): ts.ClassDeclaration {
50
+ if (!this.#valid(state)) {
51
+ return node; // Exclude self
52
+ }
53
+ state[cls] = this.#tag(state, node);
54
+ return node;
55
+ }
56
+
57
+ /**
58
+ * Hash each method
59
+ */
60
+ @OnMethod()
61
+ static collectMethodMetadata(state: TransformerState & MetadataInfo, node: ts.MethodDeclaration): ts.MethodDeclaration {
62
+ if (state[cls] && ts.isIdentifier(node.name) && !CoreUtil.isAbstract(node) && ts.isClassDeclaration(node.parent)) {
63
+ state[methods] ??= {};
64
+ state[methods]![node.name.escapedText.toString()] = this.#tag(state, node);
65
+ }
66
+ return node;
67
+ }
68
+
69
+ /**
70
+ * After visiting each class, register all the collected metadata
71
+ */
72
+ @AfterClass()
73
+ static registerClassMetadata(state: TransformerState & MetadataInfo, node: ts.ClassDeclaration): ts.ClassDeclaration {
74
+ if (!state[cls]) {
75
+ return node;
76
+ }
77
+
78
+ state[registerImport] ??= state.importFile(REGISTER_IMPORT);
79
+
80
+ const name = node.name?.escapedText.toString() ?? '';
81
+
82
+ const meta = state.factory.createCallExpression(
83
+ state.createAccess(state[registerImport].ident, 'register'),
84
+ [],
85
+ [
86
+ state.createIdentifier(name),
87
+ state.getModuleIdentifier(),
88
+ state.fromLiteral(state[cls]),
89
+ state.extendObjectLiteral(state[methods] || {}),
90
+ state.fromLiteral(CoreUtil.isAbstract(node)),
91
+ state.fromLiteral(name.endsWith(TransformerState.SYNTHETIC_EXT))
92
+ ]
93
+ );
94
+
95
+ state[methods] = {};
96
+ delete state[cls];
97
+
98
+ return state.factory.updateClassDeclaration(
99
+ node,
100
+ node.modifiers,
101
+ node.name,
102
+ node.typeParameters,
103
+ node.heritageClauses,
104
+ [
105
+ state.createStaticField('Ⲑinit', meta),
106
+ ...node.members
107
+ ]
108
+ );
109
+ }
110
+
111
+ /**
112
+ * Give proper functions a file name
113
+ */
114
+ @AfterFunction()
115
+ static registerFunctionMetadata(state: TransformerState & MetadataInfo, node: ts.FunctionDeclaration | ts.FunctionExpression): typeof node {
116
+ if (!this.#valid(state)) {
117
+ return node;
118
+ }
119
+
120
+ if (ts.isFunctionDeclaration(node) && node.name && node.parent && ts.isSourceFile(node.parent)) {
121
+ // If we have a class like function
122
+ state[registerImport] ??= state.importFile(REGISTER_IMPORT);
123
+ const tag = this.#tag(state, node);
124
+ const meta = state.factory.createCallExpression(
125
+ state.createAccess(state[registerImport].ident, 'register'),
126
+ [],
127
+ [
128
+ state.createIdentifier(node.name),
129
+ state.getModuleIdentifier(),
130
+ state.fromLiteral(tag),
131
+ ]
132
+ );
133
+ state.addStatements([state.factory.createExpressionStatement(meta)]);
134
+ }
135
+ return node;
136
+ }
137
+ }
@@ -0,0 +1,39 @@
1
+ import ts from 'typescript';
2
+
3
+ import { TransformerState, OnFile } from '@travetto/transformer';
4
+
5
+ const PATH_REGEX = /^['"](node:)?path['"]$/;
6
+ const PATH_TARGET = '@travetto/manifest/src/path';
7
+ const SKIP_SRC = /^@travetto\/manifest\/(src|support)/;
8
+
9
+ /**
10
+ * Rewriting path imports to use manifest's path
11
+ */
12
+ export class PathImportTransformer {
13
+
14
+ /**
15
+ * Hash each class
16
+ */
17
+ @OnFile()
18
+ static rewritePathImport(state: TransformerState, node: ts.SourceFile): ts.SourceFile {
19
+ if (SKIP_SRC.test(state.importName)) {
20
+ return node;
21
+ }
22
+
23
+ const stmt = node.statements.find((x): x is ts.ImportDeclaration =>
24
+ ts.isImportDeclaration(x) && PATH_REGEX.test(x.moduleSpecifier?.getText() ?? ''));
25
+ if (stmt) {
26
+ const updated = state.factory.updateImportDeclaration(
27
+ stmt,
28
+ stmt.modifiers,
29
+ stmt.importClause,
30
+ state.factory.createStringLiteral(PATH_TARGET),
31
+ stmt.attributes
32
+ );
33
+ return state.factory.updateSourceFile(node, node.statements.map(x =>
34
+ x === stmt ? updated : x
35
+ ));
36
+ }
37
+ return node;
38
+ }
39
+ }