@fluojs/platform-nodejs 1.0.6 → 2.0.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.ko.md +41 -11
- package/README.md +41 -11
- package/dist/index.d.ts +10 -55
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -55
- package/dist/internal.d.ts +3 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +2 -0
- package/dist/node/internal-node-compression.d.ts +22 -0
- package/dist/node/internal-node-compression.d.ts.map +1 -0
- package/dist/node/internal-node-compression.js +121 -0
- package/dist/node/internal-node-early-hints.d.ts +11 -0
- package/dist/node/internal-node-early-hints.d.ts.map +1 -0
- package/dist/node/internal-node-early-hints.js +99 -0
- package/dist/node/internal-node-listen.d.ts +24 -0
- package/dist/node/internal-node-listen.d.ts.map +1 -0
- package/dist/node/internal-node-listen.js +167 -0
- package/dist/node/internal-node-request.d.ts +163 -0
- package/dist/node/internal-node-request.d.ts.map +1 -0
- package/dist/node/internal-node-request.js +498 -0
- package/dist/node/internal-node-response-stream.d.ts +10 -0
- package/dist/node/internal-node-response-stream.d.ts.map +1 -0
- package/dist/node/internal-node-response-stream.js +69 -0
- package/dist/node/internal-node-response.d.ts +28 -0
- package/dist/node/internal-node-response.d.ts.map +1 -0
- package/dist/node/internal-node-response.js +181 -0
- package/dist/node/internal-node-shutdown.d.ts +34 -0
- package/dist/node/internal-node-shutdown.d.ts.map +1 -0
- package/dist/node/internal-node-shutdown.js +83 -0
- package/dist/node/internal-node.d.ts +114 -0
- package/dist/node/internal-node.d.ts.map +1 -0
- package/dist/node/internal-node.js +262 -0
- package/dist/node/json-logger.d.ts +8 -0
- package/dist/node/json-logger.d.ts.map +1 -0
- package/dist/node/json-logger.js +45 -0
- package/dist/node/logger.d.ts +37 -0
- package/dist/node/logger.d.ts.map +1 -0
- package/dist/node/logger.js +103 -0
- package/dist/node/node-compression.d.ts +2 -0
- package/dist/node/node-compression.d.ts.map +1 -0
- package/dist/node/node-compression.js +1 -0
- package/dist/node/node-request.d.ts +2 -0
- package/dist/node/node-request.d.ts.map +1 -0
- package/dist/node/node-request.js +1 -0
- package/dist/node/node-response.d.ts +2 -0
- package/dist/node/node-response.d.ts.map +1 -0
- package/dist/node/node-response.js +1 -0
- package/dist/node/node-shutdown.d.ts +2 -0
- package/dist/node/node-shutdown.d.ts.map +1 -0
- package/dist/node/node-shutdown.js +1 -0
- package/dist/node/node-static-assets.d.ts +26 -0
- package/dist/node/node-static-assets.d.ts.map +1 -0
- package/dist/node/node-static-assets.js +244 -0
- package/dist/node/node.d.ts +2 -0
- package/dist/node/node.d.ts.map +1 -0
- package/dist/node/node.js +1 -0
- package/package.json +17 -6
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ApplicationLogger } from '@fluojs/runtime';
|
|
2
|
+
/**
|
|
3
|
+
* Create json application logger.
|
|
4
|
+
*
|
|
5
|
+
* @returns The create json application logger result.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createJsonApplicationLogger(): ApplicationLogger;
|
|
8
|
+
//# sourceMappingURL=json-logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-logger.d.ts","sourceRoot":"","sources":["../../src/node/json-logger.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAqCzD;;;;GAIG;AACH,wBAAgB,2BAA2B,IAAI,iBAAiB,CAe/D"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { getCurrentRequestContext } from '@fluojs/http';
|
|
2
|
+
function buildEntry(level, message, context, error) {
|
|
3
|
+
const entry = {
|
|
4
|
+
level,
|
|
5
|
+
message,
|
|
6
|
+
timestamp: new Date().toISOString()
|
|
7
|
+
};
|
|
8
|
+
const requestId = getCurrentRequestContext()?.requestId;
|
|
9
|
+
if (requestId) {
|
|
10
|
+
entry.requestId = requestId;
|
|
11
|
+
}
|
|
12
|
+
if (context) {
|
|
13
|
+
entry.context = context;
|
|
14
|
+
}
|
|
15
|
+
if (error instanceof Error) {
|
|
16
|
+
entry.error = {
|
|
17
|
+
message: error.message,
|
|
18
|
+
name: error.name,
|
|
19
|
+
stack: error.stack
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return entry;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create json application logger.
|
|
27
|
+
*
|
|
28
|
+
* @returns The create json application logger result.
|
|
29
|
+
*/
|
|
30
|
+
export function createJsonApplicationLogger() {
|
|
31
|
+
return {
|
|
32
|
+
debug(message, context) {
|
|
33
|
+
process.stdout.write(`${JSON.stringify(buildEntry('debug', message, context))}\n`);
|
|
34
|
+
},
|
|
35
|
+
error(message, error, context) {
|
|
36
|
+
process.stderr.write(`${JSON.stringify(buildEntry('error', message, context, error))}\n`);
|
|
37
|
+
},
|
|
38
|
+
log(message, context) {
|
|
39
|
+
process.stdout.write(`${JSON.stringify(buildEntry('log', message, context))}\n`);
|
|
40
|
+
},
|
|
41
|
+
warn(message, context) {
|
|
42
|
+
process.stderr.write(`${JSON.stringify(buildEntry('warn', message, context))}\n`);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ApplicationLogger } from '@fluojs/runtime';
|
|
2
|
+
/** Severity threshold accepted by `createConsoleApplicationLogger(...)`. */
|
|
3
|
+
export type ConsoleApplicationLoggerLevel = 'debug' | 'error' | 'log' | 'warn';
|
|
4
|
+
/** Console formatting mode accepted by `createConsoleApplicationLogger(...)`. */
|
|
5
|
+
export type ConsoleApplicationLoggerMode = 'minimal' | 'pretty' | 'silent';
|
|
6
|
+
/** Options used to tune the Node console logger without replacing the runtime logger contract. */
|
|
7
|
+
export interface ConsoleApplicationLoggerOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Controls console formatting.
|
|
10
|
+
*
|
|
11
|
+
* - `pretty` keeps the historical timestamp, pid, level, context, and message format.
|
|
12
|
+
* - `minimal` writes only `[fluo] LEVEL [context] message`.
|
|
13
|
+
* - `silent` suppresses all logger methods.
|
|
14
|
+
*/
|
|
15
|
+
mode?: ConsoleApplicationLoggerMode;
|
|
16
|
+
/** Lowest severity emitted by the logger. Defaults to `debug`, preserving existing output. */
|
|
17
|
+
level?: ConsoleApplicationLoggerLevel;
|
|
18
|
+
/** Override TTY-aware ANSI color detection. */
|
|
19
|
+
color?: boolean;
|
|
20
|
+
/** Explicit host color environment passed from the application/bootstrap boundary. */
|
|
21
|
+
environment?: ConsoleApplicationLoggerEnvironment;
|
|
22
|
+
}
|
|
23
|
+
/** Host color environment used by `createConsoleApplicationLogger(...)` without reading globals directly. */
|
|
24
|
+
export interface ConsoleApplicationLoggerEnvironment {
|
|
25
|
+
/** Whether `NO_COLOR`-style color suppression is enabled. */
|
|
26
|
+
noColor?: boolean;
|
|
27
|
+
/** `FORCE_COLOR`-style value used to force ANSI color output when truthy. */
|
|
28
|
+
forceColor?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create console application logger.
|
|
32
|
+
*
|
|
33
|
+
* @param options Console logger mode, severity threshold, and color override.
|
|
34
|
+
* @returns The create console application logger result.
|
|
35
|
+
*/
|
|
36
|
+
export declare function createConsoleApplicationLogger(options?: ConsoleApplicationLoggerOptions): ApplicationLogger;
|
|
37
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/node/logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,4EAA4E;AAC5E,MAAM,MAAM,6BAA6B,GAAG,OAAO,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;AAC/E,iFAAiF;AACjF,MAAM,MAAM,4BAA4B,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE3E,kGAAkG;AAClG,MAAM,WAAW,+BAA+B;IAC9C;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,4BAA4B,CAAC;IACpC,8FAA8F;IAC9F,KAAK,CAAC,EAAE,6BAA6B,CAAC;IACtC,+CAA+C;IAC/C,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,sFAAsF;IACtF,WAAW,CAAC,EAAE,mCAAmC,CAAC;CACnD;AAED,6GAA6G;AAC7G,MAAM,WAAW,mCAAmC;IAClD,6DAA6D;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AA+DD;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAAC,OAAO,GAAE,+BAAoC,GAAG,iBAAiB,CA2C/G"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/** Severity threshold accepted by `createConsoleApplicationLogger(...)`. */
|
|
2
|
+
|
|
3
|
+
/** Console formatting mode accepted by `createConsoleApplicationLogger(...)`. */
|
|
4
|
+
|
|
5
|
+
/** Options used to tune the Node console logger without replacing the runtime logger contract. */
|
|
6
|
+
|
|
7
|
+
/** Host color environment used by `createConsoleApplicationLogger(...)` without reading globals directly. */
|
|
8
|
+
|
|
9
|
+
const RESET = '\u001B[0m';
|
|
10
|
+
const BRIGHT_GREEN = '\u001B[32m';
|
|
11
|
+
const BRIGHT_RED = '\u001B[31m';
|
|
12
|
+
const BRIGHT_YELLOW = '\u001B[33m';
|
|
13
|
+
const DIM = '\u001B[2m';
|
|
14
|
+
const LEVEL_PRIORITY = {
|
|
15
|
+
debug: 10,
|
|
16
|
+
log: 20,
|
|
17
|
+
warn: 30,
|
|
18
|
+
error: 40
|
|
19
|
+
};
|
|
20
|
+
function colorize(value, color, enabled) {
|
|
21
|
+
return enabled ? `${color}${value}${RESET}` : value;
|
|
22
|
+
}
|
|
23
|
+
function formatLog(level, context, message, color) {
|
|
24
|
+
const prefix = colorize('[fluo]', BRIGHT_GREEN, color);
|
|
25
|
+
const pid = colorize(String(process.pid), BRIGHT_YELLOW, color);
|
|
26
|
+
const timestamp = colorize(new Date().toLocaleString('en-US'), DIM, color);
|
|
27
|
+
const levelColor = level === 'ERROR' || level === 'WARN' ? BRIGHT_RED : BRIGHT_GREEN;
|
|
28
|
+
const levelLabel = colorize(level, levelColor, color);
|
|
29
|
+
const contextLabel = colorize(`[${context}]`, BRIGHT_YELLOW, color);
|
|
30
|
+
return `${prefix} ${pid} - ${timestamp} ${levelLabel} ${contextLabel} ${message}`;
|
|
31
|
+
}
|
|
32
|
+
function formatMinimalLog(level, context, message, color) {
|
|
33
|
+
const prefix = colorize('[fluo]', BRIGHT_GREEN, color);
|
|
34
|
+
const levelColor = level === 'ERROR' || level === 'WARN' ? BRIGHT_RED : BRIGHT_GREEN;
|
|
35
|
+
const levelLabel = colorize(level, levelColor, color);
|
|
36
|
+
const contextLabel = colorize(`[${context}]`, BRIGHT_YELLOW, color);
|
|
37
|
+
return `${prefix} ${levelLabel} ${contextLabel} ${message}`;
|
|
38
|
+
}
|
|
39
|
+
function shouldLog(currentLevel, configuredLevel) {
|
|
40
|
+
return LEVEL_PRIORITY[currentLevel] >= LEVEL_PRIORITY[configuredLevel];
|
|
41
|
+
}
|
|
42
|
+
function isForceColorEnabled(value) {
|
|
43
|
+
return value !== undefined && value !== '' && value !== '0' && value !== 'false' && value !== 'no' && value !== 'off';
|
|
44
|
+
}
|
|
45
|
+
function shouldUseColor(stream, environment) {
|
|
46
|
+
if (environment.noColor === true) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (isForceColorEnabled(environment.forceColor)) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
if (typeof stream.hasColors === 'function') {
|
|
53
|
+
return stream.hasColors();
|
|
54
|
+
}
|
|
55
|
+
return Boolean(stream.isTTY);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create console application logger.
|
|
60
|
+
*
|
|
61
|
+
* @param options Console logger mode, severity threshold, and color override.
|
|
62
|
+
* @returns The create console application logger result.
|
|
63
|
+
*/
|
|
64
|
+
export function createConsoleApplicationLogger(options = {}) {
|
|
65
|
+
const mode = options.mode ?? 'pretty';
|
|
66
|
+
const level = options.level ?? 'debug';
|
|
67
|
+
const environment = options.environment ?? {};
|
|
68
|
+
const format = mode === 'minimal' ? formatMinimalLog : formatLog;
|
|
69
|
+
if (mode === 'silent') {
|
|
70
|
+
return {
|
|
71
|
+
debug() {},
|
|
72
|
+
error() {},
|
|
73
|
+
log() {},
|
|
74
|
+
warn() {}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
debug(message, context = 'fluo') {
|
|
79
|
+
if (shouldLog('debug', level)) {
|
|
80
|
+
console.debug(format('DEBUG', context, message, options.color ?? shouldUseColor(process.stdout, environment)));
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
error(message, error, context = 'fluo') {
|
|
84
|
+
if (!shouldLog('error', level)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
console.error(format('ERROR', context, message, options.color ?? shouldUseColor(process.stderr, environment)));
|
|
88
|
+
if (error) {
|
|
89
|
+
console.error(error);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
log(message, context = 'fluo') {
|
|
93
|
+
if (shouldLog('log', level)) {
|
|
94
|
+
console.log(format('LOG', context, message, options.color ?? shouldUseColor(process.stdout, environment)));
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
warn(message, context = 'fluo') {
|
|
98
|
+
if (shouldLog('warn', level)) {
|
|
99
|
+
console.warn(format('WARN', context, message, options.color ?? shouldUseColor(process.stderr, environment)));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-compression.d.ts","sourceRoot":"","sources":["../../src/node/node-compression.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,6BAA6B,GAC9B,MAAM,gCAAgC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { compressNodeResponse, createNodeResponseCompression } from './internal-node-compression.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-request.d.ts","sourceRoot":"","sources":["../../src/node/node-request.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,8BAA8B,EAC9B,sBAAsB,EACtB,mBAAmB,EACnB,+BAA+B,EAC/B,2BAA2B,GAC5B,MAAM,4BAA4B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createDeferredFrameworkRequest, createFrameworkRequest, createRequestSignal, materializeFrameworkRequestBody, resolveRequestIdFromHeaders } from './internal-node-request.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-response.d.ts","sourceRoot":"","sources":["../../src/node/node-response.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,KAAK,wBAAwB,EAC7B,6BAA6B,GAC9B,MAAM,6BAA6B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createFrameworkResponse, writeNodeAdapterErrorResponse } from './internal-node-response.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-shutdown.d.ts","sourceRoot":"","sources":["../../src/node/node-shutdown.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oCAAoC,EACpC,0BAA0B,EAC1B,uBAAuB,GACxB,MAAM,6BAA6B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createNodeShutdownSignalRegistration, defaultNodeShutdownSignals, registerShutdownSignals } from './internal-node-shutdown.js';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { StaticAssetSource } from '@fluojs/http';
|
|
2
|
+
/** Precompressed representation policy for a Node filesystem asset source. */
|
|
3
|
+
export type NodeFileSystemAssetPrecompression = boolean | {
|
|
4
|
+
/** Whether `.br` siblings may be selected. Defaults to `true`. */
|
|
5
|
+
readonly brotli?: boolean;
|
|
6
|
+
/** Whether `.gz` siblings may be selected. Defaults to `true`. */
|
|
7
|
+
readonly gzip?: boolean;
|
|
8
|
+
};
|
|
9
|
+
/** Configuration for a Node filesystem-backed static asset source. */
|
|
10
|
+
export interface NodeFileSystemAssetSourceOptions {
|
|
11
|
+
/** Existing directory that contains all served asset files. */
|
|
12
|
+
readonly root: string;
|
|
13
|
+
/** Optional precompressed sibling selection policy. Defaults to `false`. */
|
|
14
|
+
readonly precompressed?: NodeFileSystemAssetPrecompression;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Creates a Node-only filesystem source for {@link StaticAssetSource}.
|
|
18
|
+
*
|
|
19
|
+
* @param options Existing root directory and optional precompressed sibling policy.
|
|
20
|
+
* @returns An explicit portable source that eagerly snapshots each verified
|
|
21
|
+
* selected file into memory, closes its `FileHandle`, and replays only that
|
|
22
|
+
* immutable byte snapshot through `StaticAsset.source`.
|
|
23
|
+
* @throws {TypeError} When the root is empty, missing, or not a directory.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createNodeFileSystemAssetSource(options: NodeFileSystemAssetSourceOptions): StaticAssetSource;
|
|
26
|
+
//# sourceMappingURL=node-static-assets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-static-assets.d.ts","sourceRoot":"","sources":["../../src/node/node-static-assets.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAIV,iBAAiB,EAClB,MAAM,cAAc,CAAC;AA0BtB,8EAA8E;AAC9E,MAAM,MAAM,iCAAiC,GAAG,OAAO,GAAG;IACxD,kEAAkE;IAClE,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,kEAAkE;IAClE,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,sEAAsE;AACtE,MAAM,WAAW,gCAAgC;IAC/C,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4EAA4E;IAC5E,QAAQ,CAAC,aAAa,CAAC,EAAE,iCAAiC,CAAC;CAC5D;AAED;;;;;;;;GAQG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,gCAAgC,GACxC,iBAAiB,CAoCnB"}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { constants, realpathSync, statSync } from 'node:fs';
|
|
3
|
+
import { open, realpath, stat } from 'node:fs/promises';
|
|
4
|
+
import { extname, isAbsolute, relative, resolve } from 'node:path';
|
|
5
|
+
const mimeTypes = {
|
|
6
|
+
'.avif': 'image/avif',
|
|
7
|
+
'.css': 'text/css; charset=utf-8',
|
|
8
|
+
'.gif': 'image/gif',
|
|
9
|
+
'.htm': 'text/html; charset=utf-8',
|
|
10
|
+
'.html': 'text/html; charset=utf-8',
|
|
11
|
+
'.ico': 'image/x-icon',
|
|
12
|
+
'.jpeg': 'image/jpeg',
|
|
13
|
+
'.jpg': 'image/jpeg',
|
|
14
|
+
'.js': 'application/javascript',
|
|
15
|
+
'.json': 'application/json',
|
|
16
|
+
'.map': 'application/json',
|
|
17
|
+
'.mjs': 'application/javascript',
|
|
18
|
+
'.pdf': 'application/pdf',
|
|
19
|
+
'.png': 'image/png',
|
|
20
|
+
'.svg': 'image/svg+xml',
|
|
21
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
22
|
+
'.wasm': 'application/wasm',
|
|
23
|
+
'.webp': 'image/webp',
|
|
24
|
+
'.woff': 'font/woff',
|
|
25
|
+
'.woff2': 'font/woff2',
|
|
26
|
+
'.xml': 'application/xml'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** Precompressed representation policy for a Node filesystem asset source. */
|
|
30
|
+
|
|
31
|
+
/** Configuration for a Node filesystem-backed static asset source. */
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Creates a Node-only filesystem source for {@link StaticAssetSource}.
|
|
35
|
+
*
|
|
36
|
+
* @param options Existing root directory and optional precompressed sibling policy.
|
|
37
|
+
* @returns An explicit portable source that eagerly snapshots each verified
|
|
38
|
+
* selected file into memory, closes its `FileHandle`, and replays only that
|
|
39
|
+
* immutable byte snapshot through `StaticAsset.source`.
|
|
40
|
+
* @throws {TypeError} When the root is empty, missing, or not a directory.
|
|
41
|
+
*/
|
|
42
|
+
export function createNodeFileSystemAssetSource(options) {
|
|
43
|
+
const root = resolveNodeAssetRoot(options);
|
|
44
|
+
const precompressed = resolvePrecompression(options.precompressed);
|
|
45
|
+
return {
|
|
46
|
+
async resolve(path, context) {
|
|
47
|
+
const logicalPath = resolveRelativeAssetPath(root, path);
|
|
48
|
+
if (!logicalPath) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
const selected = await selectRepresentation(root, logicalPath, context.acceptedEncodings, precompressed);
|
|
52
|
+
if (!selected) {
|
|
53
|
+
return (await hasExistingRepresentation(root, logicalPath, precompressed)) ? {
|
|
54
|
+
notAcceptable: true
|
|
55
|
+
} : undefined;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
contentEncoding: selected.representation.encoding,
|
|
59
|
+
contentType: mimeTypes[extname(logicalPath)] ?? 'application/octet-stream',
|
|
60
|
+
source: () => createSnapshotStream(selected.bytes),
|
|
61
|
+
size: selected.metadata.size,
|
|
62
|
+
validators: {
|
|
63
|
+
etag: {
|
|
64
|
+
opaqueValue: createAssetEtag(selected.bytes),
|
|
65
|
+
strength: 'strong'
|
|
66
|
+
},
|
|
67
|
+
lastModified: selected.metadata.lastModified
|
|
68
|
+
},
|
|
69
|
+
variesByEncoding: selected.variesByEncoding
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function resolveNodeAssetRoot(options) {
|
|
75
|
+
if (!options || typeof options !== 'object' || typeof options.root !== 'string' || options.root.trim() === '') {
|
|
76
|
+
throw new TypeError('Node filesystem static asset root must be a non-empty string.');
|
|
77
|
+
}
|
|
78
|
+
let root;
|
|
79
|
+
try {
|
|
80
|
+
root = realpathSync(resolve(options.root));
|
|
81
|
+
} catch {
|
|
82
|
+
throw new TypeError('Node filesystem static asset root must be an existing directory.');
|
|
83
|
+
}
|
|
84
|
+
const metadata = statSync(root, {
|
|
85
|
+
bigint: true
|
|
86
|
+
});
|
|
87
|
+
if (!metadata.isDirectory()) {
|
|
88
|
+
throw new TypeError('Node filesystem static asset root must be a directory.');
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
device: metadata.dev,
|
|
92
|
+
inode: metadata.ino,
|
|
93
|
+
path: root
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function resolvePrecompression(value) {
|
|
97
|
+
if (value === undefined || value === false) {
|
|
98
|
+
return {
|
|
99
|
+
brotli: false,
|
|
100
|
+
gzip: false
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (value === true) {
|
|
104
|
+
return {
|
|
105
|
+
brotli: true,
|
|
106
|
+
gzip: true
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (typeof value !== 'object') {
|
|
110
|
+
throw new TypeError('Node filesystem static asset precompressed must be a boolean or an object.');
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
brotli: value.brotli ?? true,
|
|
114
|
+
gzip: value.gzip ?? true
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function resolveRelativeAssetPath(root, path) {
|
|
118
|
+
if (typeof path !== 'string' || path === '' || path.includes('\0') || isAbsolute(path) || path.split('/').some(segment => segment === '' || segment === '.' || segment === '..' || segment.includes('\\'))) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
const candidate = resolve(root.path, ...path.split('/'));
|
|
122
|
+
return isWithinRoot(root.path, candidate) ? path : undefined;
|
|
123
|
+
}
|
|
124
|
+
async function selectRepresentation(root, logicalPath, acceptedEncodings, precompressed) {
|
|
125
|
+
for (const encoding of acceptedEncodings) {
|
|
126
|
+
if (encoding === 'br' && !precompressed.brotli || encoding === 'gzip' && !precompressed.gzip) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const extension = encoding === 'br' ? '.br' : encoding === 'gzip' ? '.gz' : '';
|
|
130
|
+
const candidate = `${logicalPath}${extension}`;
|
|
131
|
+
const opened = await openContainedAsset(root, candidate);
|
|
132
|
+
if (opened) {
|
|
133
|
+
return {
|
|
134
|
+
...opened,
|
|
135
|
+
representation: extension === '' ? {
|
|
136
|
+
path: candidate
|
|
137
|
+
} : {
|
|
138
|
+
encoding: encoding === 'identity' ? undefined : encoding,
|
|
139
|
+
path: candidate
|
|
140
|
+
},
|
|
141
|
+
variesByEncoding: await hasPrecompressedRepresentation(root, logicalPath, precompressed)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
async function hasExistingRepresentation(root, logicalPath, precompressed) {
|
|
148
|
+
for (const representation of supportedRepresentations(logicalPath, precompressed)) {
|
|
149
|
+
const opened = await openContainedAsset(root, representation.path);
|
|
150
|
+
if (opened) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
async function hasPrecompressedRepresentation(root, logicalPath, precompressed) {
|
|
157
|
+
return (await hasExistingRepresentation(root, logicalPath, {
|
|
158
|
+
brotli: precompressed.brotli,
|
|
159
|
+
gzip: precompressed.gzip
|
|
160
|
+
})) && (precompressed.brotli && (await representationExists(root, `${logicalPath}.br`)) || precompressed.gzip && (await representationExists(root, `${logicalPath}.gz`)));
|
|
161
|
+
}
|
|
162
|
+
function supportedRepresentations(logicalPath, precompressed) {
|
|
163
|
+
return [...(precompressed.brotli ? [{
|
|
164
|
+
encoding: 'br',
|
|
165
|
+
path: `${logicalPath}.br`
|
|
166
|
+
}] : []), ...(precompressed.gzip ? [{
|
|
167
|
+
encoding: 'gzip',
|
|
168
|
+
path: `${logicalPath}.gz`
|
|
169
|
+
}] : []), {
|
|
170
|
+
path: logicalPath
|
|
171
|
+
}];
|
|
172
|
+
}
|
|
173
|
+
async function representationExists(root, logicalPath) {
|
|
174
|
+
const opened = await openContainedAsset(root, logicalPath);
|
|
175
|
+
return opened !== undefined;
|
|
176
|
+
}
|
|
177
|
+
async function openContainedAsset(root, path) {
|
|
178
|
+
try {
|
|
179
|
+
const resolvedPath = await realpath(resolve(root.path, path));
|
|
180
|
+
if (!isWithinRoot(root.path, resolvedPath)) {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
const expected = await stat(resolvedPath, {
|
|
184
|
+
bigint: true
|
|
185
|
+
});
|
|
186
|
+
if (!expected.isFile()) {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
const handle = await open(resolvedPath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
190
|
+
try {
|
|
191
|
+
const metadata = await readAssetMetadata(handle);
|
|
192
|
+
if (!metadata || metadata.device !== expected.dev || metadata.inode !== expected.ino) {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
const bytes = new Uint8Array(await handle.readFile());
|
|
196
|
+
return {
|
|
197
|
+
bytes,
|
|
198
|
+
metadata: {
|
|
199
|
+
...metadata,
|
|
200
|
+
size: bytes.byteLength
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
} finally {
|
|
204
|
+
await handle.close();
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (isMissingFileError(error)) {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async function readAssetMetadata(handle) {
|
|
214
|
+
const metadata = await handle.stat({
|
|
215
|
+
bigint: true
|
|
216
|
+
});
|
|
217
|
+
if (!metadata.isFile() || metadata.size > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
device: metadata.dev,
|
|
222
|
+
inode: metadata.ino,
|
|
223
|
+
lastModified: new Date(Number(metadata.mtimeMs)),
|
|
224
|
+
size: Number(metadata.size)
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function createSnapshotStream(bytes) {
|
|
228
|
+
return new ReadableStream({
|
|
229
|
+
start(controller) {
|
|
230
|
+
controller.enqueue(bytes);
|
|
231
|
+
controller.close();
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
function createAssetEtag(bytes) {
|
|
236
|
+
return createHash('sha256').update(bytes).digest('base64url');
|
|
237
|
+
}
|
|
238
|
+
function isWithinRoot(root, target) {
|
|
239
|
+
const pathFromRoot = relative(root, target);
|
|
240
|
+
return pathFromRoot === '' || !pathFromRoot.startsWith('..') && !isAbsolute(pathFromRoot);
|
|
241
|
+
}
|
|
242
|
+
function isMissingFileError(error) {
|
|
243
|
+
return error instanceof Error && 'code' in error && (error.code === 'ELOOP' || error.code === 'ENOENT' || error.code === 'ENOTDIR');
|
|
244
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/node/node.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './internal-node.js';
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"platform",
|
|
10
10
|
"server"
|
|
11
11
|
],
|
|
12
|
-
"version": "
|
|
12
|
+
"version": "2.0.0",
|
|
13
13
|
"private": false,
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"repository": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"directory": "packages/platform-nodejs"
|
|
19
19
|
},
|
|
20
20
|
"engines": {
|
|
21
|
-
"node": ">=
|
|
21
|
+
"node": ">=24.0.0 <27"
|
|
22
22
|
},
|
|
23
23
|
"publishConfig": {
|
|
24
24
|
"access": "public"
|
|
@@ -28,23 +28,34 @@
|
|
|
28
28
|
".": {
|
|
29
29
|
"types": "./dist/index.d.ts",
|
|
30
30
|
"import": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./internal": {
|
|
33
|
+
"types": "./dist/internal.d.ts",
|
|
34
|
+
"import": "./dist/internal.js"
|
|
31
35
|
}
|
|
32
36
|
},
|
|
33
37
|
"main": "./dist/index.js",
|
|
34
38
|
"types": "./dist/index.d.ts",
|
|
39
|
+
"typesVersions": {
|
|
40
|
+
"*": {
|
|
41
|
+
"internal": [
|
|
42
|
+
"./dist/internal.d.ts"
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
},
|
|
35
46
|
"files": [
|
|
36
47
|
"dist"
|
|
37
48
|
],
|
|
38
49
|
"dependencies": {
|
|
39
|
-
"@fluojs/http": "^
|
|
40
|
-
"@fluojs/runtime": "^
|
|
50
|
+
"@fluojs/http": "^3.0.0",
|
|
51
|
+
"@fluojs/runtime": "^3.0.0"
|
|
41
52
|
},
|
|
42
53
|
"devDependencies": {
|
|
43
|
-
"vitest": "^
|
|
54
|
+
"vitest": "^4.1.11"
|
|
44
55
|
},
|
|
45
56
|
"scripts": {
|
|
46
57
|
"prebuild": "node ../../tooling/scripts/clean-dist.mjs",
|
|
47
|
-
"build": "pnpm exec babel src --extensions .ts --ignore 'src/**/*.test.ts' --out-dir dist --config-file ../../tooling/babel/babel.config.cjs && pnpm exec tsc -p tsconfig.build.json",
|
|
58
|
+
"build": "pnpm exec babel src --extensions .ts --ignore 'src/**/*.test.ts','src/**/*.test-fixture.ts' --out-dir dist --config-file ../../tooling/babel/babel.config.cjs && pnpm exec tsc -p tsconfig.build.json",
|
|
48
59
|
"typecheck": "pnpm exec tsc -p tsconfig.json --noEmit",
|
|
49
60
|
"test": "pnpm exec vitest run -c vitest.config.ts",
|
|
50
61
|
"test:watch": "pnpm exec vitest -c vitest.config.ts"
|