@travetto/runtime 7.1.3 → 8.0.0-alpha.0

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 CHANGED
@@ -19,6 +19,8 @@ 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
+ * Encoding and Decoding Utilities
23
+ * Binary Utilities
22
24
  * JSON Utilities
23
25
  * Common Utilities
24
26
  * Time Utilities
@@ -67,6 +69,8 @@ class $Runtime {
67
69
  getImport(handle: Function): string;
68
70
  /** Import from a given path */
69
71
  async importFrom<T = unknown>(location?: string): Promise<T>;
72
+ /** Get an install command for a given npm module */
73
+ getInstallCommand(pkg: string, production = false): string;
70
74
  }
71
75
  ```
72
76
 
@@ -161,9 +165,9 @@ export class EnvProp<T> {
161
165
  ```
162
166
 
163
167
  ## Standard Error Support
164
- While the framework is 100 % compatible with standard `Error` instances, there are cases in which additional functionality is desired. Within the framework we use [AppError](https://github.com/travetto/travetto/tree/main/module/runtime/src/error.ts#L26) (or its derivatives) to represent framework errors. This class is available for use in your own projects. Some of the additional benefits of using this class is enhanced error reporting, as well as better integration with other modules (e.g. the [Web API](https://github.com/travetto/travetto/tree/main/module/web#readme "Declarative support for creating Web Applications") module and HTTP status codes).
168
+ While the framework is 100 % compatible with standard `Error` instances, there are cases in which additional functionality is desired. Within the framework we use [RuntimeError](https://github.com/travetto/travetto/tree/main/module/runtime/src/error.ts#L17) (or its derivatives) to represent framework errors. This class is available for use in your own projects. Some of the additional benefits of using this class is enhanced error reporting, as well as better integration with other modules (e.g. the [Web API](https://github.com/travetto/travetto/tree/main/module/web#readme "Declarative support for creating Web Applications") module and HTTP status codes).
165
169
 
166
- The [AppError](https://github.com/travetto/travetto/tree/main/module/runtime/src/error.ts#L26) takes in a message, and an optional payload and / or error classification. The currently supported error classifications are:
170
+ The [RuntimeError](https://github.com/travetto/travetto/tree/main/module/runtime/src/error.ts#L17) takes in a message, and an optional payload and / or error classification. The currently supported error classifications are:
167
171
  * `general` - General purpose errors
168
172
  * `system` - Synonym for `general`
169
173
  * `data` - Data format, content, etc are incorrect. Generally correlated to bad input.
@@ -247,25 +251,26 @@ $ DEBUG=express:*,@travetto/web npx trv run web
247
251
  ## Resource Access
248
252
  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.
249
253
 
250
- 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'.
254
+ The [FileLoader](https://github.com/travetto/travetto/tree/main/module/runtime/src/file-loader.ts#L11) 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 [RuntimeError](https://github.com/travetto/travetto/tree/main/module/runtime/src/error.ts#L17) with a category of 'notfound'.
251
255
 
252
- 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.
256
+ The [FileLoader](https://github.com/travetto/travetto/tree/main/module/runtime/src/file-loader.ts#L11) 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.
253
257
 
254
- ## JSON Utilities
255
- 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.
256
-
257
- * `parseSafe(input: string | Buffer)` parses JSON safely from a string or Buffer.
258
- * `stringifyBase64(value: any)` encodes a JSON value as a base64 encoded string.
259
- * `parseBase64(input: string)` decodes a JSON value from a base64 encoded string.
260
- * `readFile(file: string)` reads a JSON file asynchronously.
261
- * `readFileSync(file: string, onMissing?: any)` reads a JSON file synchronously.
258
+ ## Encoding and Decoding Utilities
259
+ The [CodecUtil](https://github.com/travetto/travetto/tree/main/module/runtime/src/codec.ts#L15) class provides a variety of static methods for encoding and decoding data. When working with JSON data, it also provide security checks to prevent prototype pollution. The utility supports the following formats:
260
+ * Hex
261
+ * Base64
262
+ * UTF8
263
+ * UTT8 Encoded JSON
264
+ * Base64 Encoded JSON
265
+ * New Line Delimited UTF8
262
266
 
263
267
  ## Common Utilities
264
- Common utilities used throughout the framework. Currently [Util](https://github.com/travetto/travetto/tree/main/module/runtime/src/util.ts#L11) includes:
268
+ Common utilities used throughout the framework. Currently [Util](https://github.com/travetto/travetto/tree/main/module/runtime/src/util.ts#L14) includes:
265
269
  * `uuid(len: number)` generates a simple uuid for use within the application.
266
270
  * `allowDenyMatcher(rules[])` builds a matching function that leverages the rules as an allow/deny list, where order of the rules matters. Negative rules are prefixed by '!'.
267
271
  * `hash(text: string, size?: number)` produces a full sha512 hash.
268
272
  * `resolvablePromise()` produces a `Promise` instance with the `resolve` and `reject` methods attached to the instance. This is extremely useful for integrating promises into async iterations, or any other situation in which the promise creation and the execution flow don't always match up.
273
+ * `bufferedFileWrite(file:string, content: string)` will write the file, using a temporary buffer file to ensure that the entire file is written before being moved to the final location. This helps minimize file watch noise when writing files.
269
274
 
270
275
  **Code: Sample makeTemplate Usage**
271
276
  ```typescript
@@ -275,53 +280,51 @@ tpl`{{age:20}} {{name: 'bob'}}</>;
275
280
  '**age: 20** **name: bob**'
276
281
  ```
277
282
 
283
+ ## Binary Utilities
284
+ The [BinaryUtil](https://github.com/travetto/travetto/tree/main/module/runtime/src/binary.ts#L59) class provides a unified interface for working with binary data across different formats, especially bridging the gap between Node.js specific types (`Buffer`, `Stream`) and Web Standard types (`Blob`, `ArrayBuffer`). The framework leverages this to allow for seamless handling of binary data, regardless of the source.
285
+
286
+ ## JSON Utilities
287
+ The [JSONUtil](https://github.com/travetto/travetto/tree/main/module/runtime/src/json.ts#L31) class provides a comprehensive set of utilities for working with JSON data, including serialization, deserialization, encoding, and deep cloning capabilities. The utility handles special types like `Date`, `BigInt`, and `Error` objects seamlessly. Key features include:
288
+ * `fromUTF8(input, config?)` - Parse JSON from a UTF-8 string
289
+ * `toUTF8(value, config?)` - Serialize a value to JSON string
290
+ * `toUTF8Pretty(value)` - Serialize with pretty formatting (2-space indent)
291
+ * `fromBinaryArray(input)` - Parse JSON from binary array
292
+ * `toBinaryArray(value, config?)` - Serialize to binary array (UTF-8 encoded)
293
+ * `toBase64(value)` - Encode JSON as base64 string
294
+ * `fromBase64(input)` - Decode JSON from base64 string
295
+ * `clone(input, config?)` - Deep clone objects with optional transformations
296
+ * `cloneForTransmit(input)` - Clone for transmission with error serialization
297
+ * `cloneFromTransmit(input)` - Clone from transmission with type restoration
298
+
299
+ The `TRANSMIT_REVIVER` automatically restores `Date` objects and `BigInt` values during deserialization, making it ideal for transmitting complex data structures across network boundaries.
300
+
278
301
  ## Time Utilities
279
- [TimeUtil](https://github.com/travetto/travetto/tree/main/module/runtime/src/time.ts#L20) contains general helper methods, created to assist with time-based inputs via environment variables, command line interfaces, and other string-heavy based input.
302
+ [TimeUtil](https://github.com/travetto/travetto/tree/main/module/runtime/src/time.ts#L21) contains general helper methods, created to assist with time-based inputs via environment variables, command line interfaces, and other string-heavy based input.
280
303
 
281
304
  **Code: Time Utilities**
282
305
  ```typescript
283
306
  export class TimeUtil {
284
307
  /**
285
308
  * Test to see if a string is valid for relative time
286
- * @param val
287
309
  */
288
310
  static isTimeSpan(value: string): value is TimeSpan;
289
311
  /**
290
- * Returns time units convert to ms
291
- * @param amount Number of units to extend
292
- * @param unit Time unit to extend ('ms', 's', 'm', 'h', 'd', 'w', 'y')
293
- */
294
- static asMillis(amount: Date | number | TimeSpan, unit?: TimeUnit): number;
295
- /**
296
- * Returns the time converted to seconds
297
- * @param date The date to convert
298
- */
299
- static asSeconds(date: Date | number | TimeSpan, unit?: TimeUnit): number;
300
- /**
301
- * Returns the time converted to a Date
302
- * @param date The date to convert
303
- */
304
- static asDate(date: Date | number | TimeSpan, unit?: TimeUnit): Date;
305
- /**
306
- * Resolve time or span to possible time
312
+ * Exposes the ability to create a duration succinctly
307
313
  */
308
- static fromValue(value: Date | number | string | undefined): number | undefined;
314
+ static duration(input: TimeSpan | number | string, outputUnit: TimeUnit): number;
309
315
  /**
310
316
  * Returns a new date with `amount` units into the future
311
- * @param amount Number of units to extend
312
- * @param unit Time unit to extend ('ms', 's', 'm', 'h', 'd', 'w', 'y')
313
317
  */
314
- static fromNow(amount: number | TimeSpan, unit: TimeUnit = 'ms'): Date;
318
+ static fromNow(input: TimeSpan | number | string): Date;
315
319
  /**
316
320
  * Returns a pretty timestamp
317
- * @param time Time in milliseconds
318
321
  */
319
- static asClock(time: number): string;
322
+ static asClock(input: TimeSpan | number | string): string;
320
323
  }
321
324
  ```
322
325
 
323
326
  ## Process Execution
324
- [ExecUtil](https://github.com/travetto/travetto/tree/main/module/runtime/src/exec.ts#L42) 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
+ [ExecUtil](https://github.com/travetto/travetto/tree/main/module/runtime/src/exec.ts#L41) 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.
325
328
 
326
329
  A simple example would be:
327
330
 
package/__index__.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  import type { } from './src/global.d.ts';
2
2
  import type { } from './src/trv.d.ts';
3
3
  export * from './src/binary.ts';
4
+ export * from './src/binary-metadata.ts';
4
5
  export * from './src/console.ts';
5
6
  export * from './src/context.ts';
6
7
  export * from './src/debug.ts';
7
8
  export * from './src/error.ts';
8
9
  export * from './src/exec.ts';
10
+ export * from './src/codec.ts';
9
11
  export * from './src/env.ts';
10
12
  export * from './src/file-loader.ts';
11
13
  export * from './src/function.ts';
@@ -17,4 +19,4 @@ export * from './src/shutdown.ts';
17
19
  export * from './src/time.ts';
18
20
  export * from './src/types.ts';
19
21
  export * from './src/watch.ts';
20
- export * from './src/util.ts';
22
+ export * from './src/util.ts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/runtime",
3
- "version": "7.1.3",
3
+ "version": "8.0.0-alpha.0",
4
4
  "type": "module",
5
5
  "description": "Runtime for travetto applications.",
6
6
  "keywords": [
@@ -26,12 +26,13 @@
26
26
  "directory": "module/runtime"
27
27
  },
28
28
  "dependencies": {
29
- "@travetto/manifest": "^7.1.2",
29
+ "@travetto/manifest": "^8.0.0-alpha.0",
30
30
  "@types/debug": "^4.1.12",
31
- "debug": "^4.4.3"
31
+ "debug": "^4.4.3",
32
+ "temporal-polyfill-lite": "^0.2.2"
32
33
  },
33
34
  "peerDependencies": {
34
- "@travetto/transformer": "^7.1.2"
35
+ "@travetto/transformer": "^8.0.0-alpha.0"
35
36
  },
36
37
  "peerDependenciesMeta": {
37
38
  "@travetto/transformer": {
@@ -0,0 +1,174 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { createReadStream, ReadStream } from 'node:fs';
5
+
6
+ import { BinaryUtil, type BinaryArray, type BinaryContainer, type BinaryStream, type BinaryType } from './binary.ts';
7
+ import { RuntimeError } from './error.ts';
8
+ import { CodecUtil } from './codec.ts';
9
+
10
+ type BlobInput = BinaryType | (() => (BinaryType | Promise<BinaryType>));
11
+
12
+ /** Range of bytes, inclusive */
13
+ export type ByteRange = { start: number, end?: number };
14
+
15
+ export interface BinaryMetadata {
16
+ /** Size of binary data */
17
+ size?: number;
18
+ /** Mime type of the content */
19
+ contentType?: string;
20
+ /** Hash of binary data contents */
21
+ hash?: string;
22
+ /** The original base filename of the file */
23
+ filename?: string;
24
+ /** Filenames title, optional for elements like images, audio, videos */
25
+ title?: string;
26
+ /** Content encoding */
27
+ contentEncoding?: string;
28
+ /** Content language */
29
+ contentLanguage?: string;
30
+ /** Cache control */
31
+ cacheControl?: string;
32
+ /** Byte range for binary data */
33
+ range?: Required<ByteRange>;
34
+ }
35
+
36
+ type HashConfig = {
37
+ length?: number;
38
+ hashAlgorithm?: 'sha1' | 'sha256' | 'sha512' | 'md5';
39
+ outputEncoding?: crypto.BinaryToTextEncoding;
40
+ };
41
+
42
+ const BinaryMetaSymbol = Symbol();
43
+
44
+ export class BinaryMetadataUtil {
45
+ /** Set metadata for a binary type */
46
+ static write(input: BinaryType, metadata: BinaryMetadata): BinaryMetadata {
47
+ const withMeta: BinaryType & { [BinaryMetaSymbol]?: BinaryMetadata } = input;
48
+ return withMeta[BinaryMetaSymbol] = metadata;
49
+ }
50
+
51
+ /** Read metadata for a binary type, if available */
52
+ static read(input: BinaryType): BinaryMetadata {
53
+ const withMeta: BinaryType & { [BinaryMetaSymbol]?: BinaryMetadata } = input;
54
+ return withMeta[BinaryMetaSymbol] ?? {};
55
+ }
56
+
57
+ /** Generate a hash from an input value * @param input The seed value to build the hash from
58
+ * @param length The optional length of the hash to generate
59
+ * @param hashAlgorithm The hash algorithm to use
60
+ * @param outputEncoding The output encoding format
61
+ */
62
+ static hash(input: string | BinaryArray, config?: HashConfig): string;
63
+ static hash(input: BinaryStream | BinaryContainer, config?: HashConfig): Promise<string>;
64
+ static hash(input: string | BinaryType, config?: HashConfig): string | Promise<string> {
65
+ const hashAlgorithm = config?.hashAlgorithm ?? 'sha512';
66
+ const outputEncoding = config?.outputEncoding ?? 'hex';
67
+ const length = config?.length;
68
+ const hash = crypto.createHash(hashAlgorithm).setEncoding(outputEncoding);
69
+
70
+ if (typeof input === 'string') {
71
+ input = CodecUtil.fromUTF8String(input);
72
+ }
73
+
74
+ if (BinaryUtil.isBinaryArray(input)) {
75
+ hash.update(BinaryUtil.binaryArrayToUint8Array(input));
76
+ return hash.digest(outputEncoding).substring(0, length);
77
+ } else {
78
+ return BinaryUtil.pipeline(input, hash).then(() =>
79
+ hash.digest(outputEncoding).substring(0, length)
80
+ );
81
+ }
82
+ }
83
+
84
+ /** Compute the length of the binary data to be returned */
85
+ static readLength(metadata: BinaryMetadata): number | undefined {
86
+ return metadata.range ? (metadata.range.end - metadata.range.start + 1) : metadata.size;
87
+ }
88
+
89
+ /** Compute metadata for a given binary input */
90
+ static async compute(input: BlobInput, base: BinaryMetadata = {}): Promise<BinaryMetadata> {
91
+ if (typeof input === 'function') {
92
+ input = await input();
93
+ }
94
+ const metadata = { ...BinaryMetadataUtil.read(input), ...base };
95
+
96
+ if (BinaryUtil.isBinaryContainer(input)) {
97
+ metadata.size ??= input.size;
98
+ metadata.contentType ??= input.type;
99
+ if (input instanceof File) {
100
+ metadata.filename ??= input.name;
101
+ }
102
+ } else if (BinaryUtil.isBinaryArray(input)) {
103
+ metadata.size ??= input.byteLength;
104
+ metadata.hash ??= this.hash(input, { hashAlgorithm: 'sha256' });
105
+ } else if (input instanceof ReadStream) {
106
+ const location = input.path.toString();
107
+ metadata.filename ??= path.basename(location);
108
+ metadata.contentEncoding ??= input.readableEncoding!;
109
+ metadata.size ??= (await fs.stat(location)).size;
110
+ metadata.hash ??= await this.hash(createReadStream(location), { hashAlgorithm: 'sha256' });
111
+ } else if (input && typeof input === 'object' && 'readableEncoding' in input && typeof input.readableEncoding === 'string') {
112
+ metadata.contentEncoding ??= input.readableEncoding!;
113
+ }
114
+
115
+ return metadata;
116
+ }
117
+
118
+ /**
119
+ * Rewrite a blob to support metadata, and provide a dynamic input source
120
+ */
121
+ static defineBlob<T extends Blob>(target: T, input: BlobInput, metadata: BinaryMetadata = {}): typeof target {
122
+ const inputFn = async (): Promise<BinaryType> => typeof input === 'function' ? await input() : input;
123
+ this.write(target, metadata);
124
+
125
+ Object.defineProperties(target, {
126
+ size: { get() { return BinaryMetadataUtil.readLength(metadata); } },
127
+ type: { get() { return metadata.contentType; } },
128
+ name: { get() { return metadata.filename; } },
129
+ arrayBuffer: { value: () => inputFn().then(BinaryUtil.toArrayBuffer) },
130
+ stream: { value: () => BinaryUtil.toReadableStream(BinaryUtil.toSynchronous(input)) },
131
+ bytes: { value: () => inputFn().then(BinaryUtil.toBuffer) },
132
+ text: { value: () => inputFn().then(BinaryUtil.toBinaryArray).then(CodecUtil.toUTF8String) },
133
+ slice: {
134
+ value: (start?: number, end?: number, _contentType?: string) => {
135
+ const result = target instanceof File ? new File([], '') : new Blob([]);
136
+ return BinaryMetadataUtil.defineBlob(result,
137
+ () => inputFn().then(BinaryUtil.toBinaryArray).then(data => BinaryUtil.sliceByteArray(data, start, end)),
138
+ {
139
+ ...metadata,
140
+ range: { start: start ?? 0, end: end ?? metadata.size! - 1 },
141
+ }
142
+ );
143
+ }
144
+ }
145
+ });
146
+ return target;
147
+ }
148
+
149
+ /**
150
+ * Make a blob that contains the appropriate metadata
151
+ */
152
+ static makeBlob(source: BlobInput, metadata?: BinaryMetadata): Blob {
153
+ return this.defineBlob(new Blob([]), source, metadata);
154
+ }
155
+
156
+ /**
157
+ * Enforce byte range for stream stream/file of a certain size
158
+ */
159
+ static enforceRange(range: ByteRange, metadata?: BinaryMetadata): Required<ByteRange> {
160
+ if (!metadata || metadata.size === undefined) {
161
+ throw new RuntimeError('Cannot enforce range on data with unknown size', { category: 'data' });
162
+ }
163
+ const size = metadata.size;
164
+
165
+ // End is inclusive
166
+ const [start, end] = [range.start, Math.min(range.end ?? (size - 1), size - 1)];
167
+
168
+ if (Number.isNaN(start) || Number.isNaN(end) || !Number.isFinite(start) || start >= size || start < 0 || start > end) {
169
+ throw new RuntimeError('Invalid position, out of range', { category: 'data', details: { start, end, size } });
170
+ }
171
+
172
+ return { start, end };
173
+ }
174
+ }