@fluojs/runtime 1.0.0-beta.1
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/LICENSE +21 -0
- package/README.ko.md +182 -0
- package/README.md +182 -0
- package/dist/abort.d.ts +19 -0
- package/dist/abort.d.ts.map +1 -0
- package/dist/abort.js +39 -0
- package/dist/adapters/internal-http-adapter.d.ts +2 -0
- package/dist/adapters/internal-http-adapter.d.ts.map +1 -0
- package/dist/adapters/internal-http-adapter.js +1 -0
- package/dist/adapters/internal-request-response-factory.d.ts +2 -0
- package/dist/adapters/internal-request-response-factory.d.ts.map +1 -0
- package/dist/adapters/internal-request-response-factory.js +1 -0
- package/dist/adapters/request-response-factory.d.ts +17 -0
- package/dist/adapters/request-response-factory.d.ts.map +1 -0
- package/dist/adapters/request-response-factory.js +27 -0
- package/dist/bootstrap.d.ts +73 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +870 -0
- package/dist/errors.d.ts +39 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +88 -0
- package/dist/health/diagnostics.d.ts +56 -0
- package/dist/health/diagnostics.d.ts.map +1 -0
- package/dist/health/diagnostics.js +155 -0
- package/dist/health/health.d.ts +18 -0
- package/dist/health/health.d.ts.map +1 -0
- package/dist/health/health.js +82 -0
- package/dist/http-adapter-shared.d.ts +88 -0
- package/dist/http-adapter-shared.d.ts.map +1 -0
- package/dist/http-adapter-shared.js +199 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/internal-http-adapter.d.ts +2 -0
- package/dist/internal-http-adapter.d.ts.map +1 -0
- package/dist/internal-http-adapter.js +1 -0
- package/dist/internal-node.d.ts +2 -0
- package/dist/internal-node.d.ts.map +1 -0
- package/dist/internal-node.js +1 -0
- package/dist/internal-request-response-factory.d.ts +2 -0
- package/dist/internal-request-response-factory.d.ts.map +1 -0
- package/dist/internal-request-response-factory.js +1 -0
- package/dist/internal.d.ts +2 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +1 -0
- package/dist/logging/json-logger.d.ts +3 -0
- package/dist/logging/json-logger.d.ts.map +1 -0
- package/dist/logging/json-logger.js +39 -0
- package/dist/logging/logger.d.ts +3 -0
- package/dist/logging/logger.d.ts.map +1 -0
- package/dist/logging/logger.js +36 -0
- package/dist/module-graph.d.ts +26 -0
- package/dist/module-graph.d.ts.map +1 -0
- package/dist/module-graph.js +248 -0
- package/dist/multipart.d.ts +45 -0
- package/dist/multipart.d.ts.map +1 -0
- package/dist/multipart.js +195 -0
- package/dist/node/internal-node-compression.d.ts +7 -0
- package/dist/node/internal-node-compression.d.ts.map +1 -0
- package/dist/node/internal-node-compression.js +68 -0
- package/dist/node/internal-node-request.d.ts +34 -0
- package/dist/node/internal-node-request.d.ts.map +1 -0
- package/dist/node/internal-node-request.js +195 -0
- package/dist/node/internal-node-response.d.ts +8 -0
- package/dist/node/internal-node-response.d.ts.map +1 -0
- package/dist/node/internal-node-response.js +166 -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 +80 -0
- package/dist/node/internal-node.d.ts.map +1 -0
- package/dist/node/internal-node.js +209 -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.d.ts +2 -0
- package/dist/node/node.d.ts.map +1 -0
- package/dist/node/node.js +1 -0
- package/dist/node.d.ts +5 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +3 -0
- package/dist/platform-contract.d.ts +140 -0
- package/dist/platform-contract.d.ts.map +1 -0
- package/dist/platform-contract.js +1 -0
- package/dist/platform-shell.d.ts +45 -0
- package/dist/platform-shell.d.ts.map +1 -0
- package/dist/platform-shell.js +368 -0
- package/dist/request-transaction.d.ts +17 -0
- package/dist/request-transaction.d.ts.map +1 -0
- package/dist/request-transaction.js +39 -0
- package/dist/tokens.d.ts +27 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +24 -0
- package/dist/types.d.ts +161 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/web.d.ts +58 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +431 -0
- package/package.json +86 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { createCorsMiddleware, createErrorResponse, createSecurityHeadersMiddleware, matchRoutePattern, normalizeRoutePattern, NotFoundException } from '@fluojs/http';
|
|
2
|
+
import { bootstrapApplication } from './bootstrap.js';
|
|
3
|
+
import { createConsoleApplicationLogger } from './logging/logger.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Input type for configuring CORS in an HTTP adapter.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolved target for an HTTP server listener.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Common middleware options for HTTP adapters.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Options for bootstrapping an HTTP adapter application.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Options for running an HTTP adapter application with shutdown management.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Function type for registering custom application shutdown logic.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Bootstraps an HTTP application with the provided adapter and options.
|
|
31
|
+
*
|
|
32
|
+
* @param rootModule The root application module class.
|
|
33
|
+
* @param options Bootstrap configuration for middleware and logging.
|
|
34
|
+
* @param adapter The HTTP platform adapter to use.
|
|
35
|
+
* @returns A promise that resolves to the initialized application instance.
|
|
36
|
+
*/
|
|
37
|
+
export async function bootstrapHttpAdapterApplication(rootModule, options, adapter) {
|
|
38
|
+
return bootstrapApplication({
|
|
39
|
+
...options,
|
|
40
|
+
adapter,
|
|
41
|
+
logger: options.logger ?? createConsoleApplicationLogger(),
|
|
42
|
+
middleware: createHttpAdapterMiddleware(options),
|
|
43
|
+
rootModule
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolves the final middleware chain for an HTTP adapter based on options.
|
|
49
|
+
*
|
|
50
|
+
* @param options Middleware configuration including CORS and prefix settings.
|
|
51
|
+
* @returns An array of middleware instances to be registered in the adapter.
|
|
52
|
+
*/
|
|
53
|
+
export function createHttpAdapterMiddleware(options) {
|
|
54
|
+
const middleware = [...(options.middleware ?? [])];
|
|
55
|
+
if (options.securityHeaders !== false) {
|
|
56
|
+
middleware.unshift(createSecurityHeadersMiddleware(typeof options.securityHeaders === 'object' ? options.securityHeaders : undefined));
|
|
57
|
+
}
|
|
58
|
+
if (options.globalPrefix) {
|
|
59
|
+
middleware.unshift(createGlobalPrefixMiddleware(options.globalPrefix, options.globalPrefixExclude));
|
|
60
|
+
}
|
|
61
|
+
if (options.cors !== undefined && options.cors !== false) {
|
|
62
|
+
middleware.unshift(createCorsMiddleware(resolveCorsOptions(options.cors)));
|
|
63
|
+
}
|
|
64
|
+
return middleware;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Formats a log message indicating that the HTTP adapter is listening on a specific target.
|
|
69
|
+
*
|
|
70
|
+
* @param target - The listen target containing the URL and bind target.
|
|
71
|
+
* @returns A formatted string message.
|
|
72
|
+
*/
|
|
73
|
+
export function formatHttpAdapterListenMessage(target) {
|
|
74
|
+
return target.url.endsWith(target.bindTarget) ? `Listening on ${target.url}` : `Listening on ${target.url} (bound to ${target.bindTarget})`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Boots and runs an HTTP application using the provided adapter and options,
|
|
79
|
+
* including setup for shutdown management and logging.
|
|
80
|
+
*
|
|
81
|
+
* @param rootModule - The root application module class.
|
|
82
|
+
* @param options - Run configuration including shutdown and logging settings.
|
|
83
|
+
* @param adapter - The managed HTTP platform adapter to use.
|
|
84
|
+
* @returns A promise that resolves to the running application instance.
|
|
85
|
+
*/
|
|
86
|
+
export async function runHttpAdapterApplication(rootModule, options, adapter) {
|
|
87
|
+
const logger = options.logger ?? createConsoleApplicationLogger();
|
|
88
|
+
const app = await bootstrapApplication({
|
|
89
|
+
...options,
|
|
90
|
+
adapter,
|
|
91
|
+
logger,
|
|
92
|
+
middleware: createHttpAdapterMiddleware(options),
|
|
93
|
+
rootModule
|
|
94
|
+
});
|
|
95
|
+
try {
|
|
96
|
+
await app.listen();
|
|
97
|
+
logger.log(formatHttpAdapterListenMessage(adapter.getListenTarget()), 'FluoFactory');
|
|
98
|
+
} catch (error) {
|
|
99
|
+
logger.error('Failed to start application.', error, 'FluoFactory');
|
|
100
|
+
if (app.state !== 'closed') {
|
|
101
|
+
try {
|
|
102
|
+
await app.close('bootstrap-failed');
|
|
103
|
+
} catch (closeError) {
|
|
104
|
+
logger.error('Failed to close application after startup failure.', closeError, 'FluoFactory');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
const unregisterShutdownSignals = options.shutdownRegistration?.(app, logger, options.forceExitTimeoutMs) ?? (() => {});
|
|
110
|
+
const close = app.close.bind(app);
|
|
111
|
+
let shutdownSignalsUnregistered = false;
|
|
112
|
+
app.close = async signal => {
|
|
113
|
+
if (!shutdownSignalsUnregistered) {
|
|
114
|
+
unregisterShutdownSignals();
|
|
115
|
+
shutdownSignalsUnregistered = true;
|
|
116
|
+
}
|
|
117
|
+
await close(signal);
|
|
118
|
+
};
|
|
119
|
+
return app;
|
|
120
|
+
}
|
|
121
|
+
function createGlobalPrefixMiddleware(prefix, exclude) {
|
|
122
|
+
const normalizedPrefix = normalizeRoutePattern(prefix);
|
|
123
|
+
if (normalizedPrefix === '/') {
|
|
124
|
+
return {
|
|
125
|
+
async handle(_context, next) {
|
|
126
|
+
await next();
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const exclusions = [...(exclude ?? [])].map(path => normalizeRoutePattern(path));
|
|
131
|
+
return {
|
|
132
|
+
async handle(context, next) {
|
|
133
|
+
const requestPath = normalizeRoutePattern(context.request.path);
|
|
134
|
+
if (matchesExcludedPath(requestPath, exclusions)) {
|
|
135
|
+
await next();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (shouldRejectGlobalPrefixRequest(requestPath, normalizedPrefix, exclusions)) {
|
|
139
|
+
await writeGlobalPrefixNotFound(context.requestContext.requestId, context.response);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const strippedPath = stripGlobalPrefix(requestPath, normalizedPrefix);
|
|
143
|
+
context.request = rewriteGlobalPrefixRequest(context.request, requestPath, strippedPath);
|
|
144
|
+
await next();
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function shouldRejectGlobalPrefixRequest(requestPath, normalizedPrefix, exclusions) {
|
|
149
|
+
if (!matchesPrefix(requestPath, normalizedPrefix)) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
return matchesExcludedPath(stripGlobalPrefix(requestPath, normalizedPrefix), exclusions);
|
|
153
|
+
}
|
|
154
|
+
function rewriteGlobalPrefixRequest(request, requestPath, strippedPath) {
|
|
155
|
+
return {
|
|
156
|
+
...request,
|
|
157
|
+
path: strippedPath,
|
|
158
|
+
url: rewritePrefixedUrl(request.url, requestPath, strippedPath)
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function writeGlobalPrefixNotFound(requestId, response) {
|
|
162
|
+
const error = new NotFoundException('Resource not found.');
|
|
163
|
+
response.setStatus(error.status);
|
|
164
|
+
return Promise.resolve(response.send(createErrorResponse(error, requestId)));
|
|
165
|
+
}
|
|
166
|
+
function matchesPrefix(path, prefix) {
|
|
167
|
+
return path === prefix || path.startsWith(`${prefix}/`);
|
|
168
|
+
}
|
|
169
|
+
function stripGlobalPrefix(path, prefix) {
|
|
170
|
+
if (path === prefix) {
|
|
171
|
+
return '/';
|
|
172
|
+
}
|
|
173
|
+
return normalizeRoutePattern(path.slice(prefix.length));
|
|
174
|
+
}
|
|
175
|
+
function matchesExcludedPath(path, exclusions) {
|
|
176
|
+
return exclusions.some(pattern => matchRoutePattern(pattern, path));
|
|
177
|
+
}
|
|
178
|
+
function rewritePrefixedUrl(url, originalPath, rewrittenPath) {
|
|
179
|
+
if (!url.startsWith(originalPath)) {
|
|
180
|
+
return rewrittenPath;
|
|
181
|
+
}
|
|
182
|
+
return rewrittenPath + url.slice(originalPath.length);
|
|
183
|
+
}
|
|
184
|
+
function resolveCorsOptions(cors) {
|
|
185
|
+
const defaults = {
|
|
186
|
+
allowHeaders: ['Authorization', 'Content-Type'],
|
|
187
|
+
exposeHeaders: ['X-Request-Id']
|
|
188
|
+
};
|
|
189
|
+
if (typeof cors === 'string' || Array.isArray(cors)) {
|
|
190
|
+
return {
|
|
191
|
+
...defaults,
|
|
192
|
+
allowOrigin: cors
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
...defaults,
|
|
197
|
+
...cors
|
|
198
|
+
};
|
|
199
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './abort.js';
|
|
2
|
+
export * from './bootstrap.js';
|
|
3
|
+
export * from './health/diagnostics.js';
|
|
4
|
+
export * from './errors.js';
|
|
5
|
+
export * from './health/health.js';
|
|
6
|
+
export type { MultipartOptions, MultipartRequestLike, MultipartResult, UploadedFile, } from './multipart.js';
|
|
7
|
+
export type { PersistencePlatformStatusSnapshot, PlatformCheckResult, PlatformComponent, PlatformComponentInput, PlatformComponentRegistration, PlatformDiagnosticIssue, PlatformHealthReport, PlatformOptionsBase, PlatformReadinessReport, PlatformShell, PlatformShellSnapshot, PlatformSnapshot, PlatformState, PlatformValidationResult, } from './platform-contract.js';
|
|
8
|
+
export * from './request-transaction.js';
|
|
9
|
+
export { APPLICATION_LOGGER, PLATFORM_SHELL } from './tokens.js';
|
|
10
|
+
export * from './types.js';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,yBAAyB,CAAC;AACxC,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,YAAY,EACV,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,EACf,YAAY,GACb,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,iCAAiC,EACjC,mBAAmB,EACnB,iBAAiB,EACjB,sBAAsB,EACtB,6BAA6B,EAC7B,uBAAuB,EACvB,oBAAoB,EACpB,mBAAmB,EACnB,uBAAuB,EACvB,aAAa,EACb,qBAAqB,EACrB,gBAAgB,EAChB,aAAa,EACb,wBAAwB,GACzB,MAAM,wBAAwB,CAAC;AAChC,cAAc,0BAA0B,CAAC;AACzC,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACjE,cAAc,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './abort.js';
|
|
2
|
+
export * from './bootstrap.js';
|
|
3
|
+
export * from './health/diagnostics.js';
|
|
4
|
+
export * from './errors.js';
|
|
5
|
+
export * from './health/health.js';
|
|
6
|
+
export * from './request-transaction.js';
|
|
7
|
+
export { APPLICATION_LOGGER, PLATFORM_SHELL } from './tokens.js';
|
|
8
|
+
export * from './types.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internal-http-adapter.d.ts","sourceRoot":"","sources":["../src/internal-http-adapter.ts"],"names":[],"mappings":"AAAA,cAAc,qCAAqC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './adapters/internal-http-adapter.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internal-node.d.ts","sourceRoot":"","sources":["../src/internal-node.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './node/internal-node.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internal-request-response-factory.d.ts","sourceRoot":"","sources":["../src/internal-request-response-factory.ts"],"names":[],"mappings":"AAAA,cAAc,iDAAiD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './adapters/internal-request-response-factory.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,wBAAwB,EACxB,cAAc,EACd,iBAAiB,GAClB,MAAM,aAAa,CAAC"}
|
package/dist/internal.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { APPLICATION_LOGGER, COMPILED_MODULES, HTTP_APPLICATION_ADAPTER, PLATFORM_SHELL, RUNTIME_CONTAINER } from './tokens.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-logger.d.ts","sourceRoot":"","sources":["../../src/logging/json-logger.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAqCrD,wBAAgB,2BAA2B,IAAI,iBAAiB,CAe/D"}
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
export function createJsonApplicationLogger() {
|
|
25
|
+
return {
|
|
26
|
+
debug(message, context) {
|
|
27
|
+
process.stdout.write(JSON.stringify(buildEntry('debug', message, context)) + '\n');
|
|
28
|
+
},
|
|
29
|
+
error(message, error, context) {
|
|
30
|
+
process.stderr.write(JSON.stringify(buildEntry('error', message, context, error)) + '\n');
|
|
31
|
+
},
|
|
32
|
+
log(message, context) {
|
|
33
|
+
process.stdout.write(JSON.stringify(buildEntry('log', message, context)) + '\n');
|
|
34
|
+
},
|
|
35
|
+
warn(message, context) {
|
|
36
|
+
process.stderr.write(JSON.stringify(buildEntry('warn', message, context)) + '\n');
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/logging/logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAuBrD,wBAAgB,8BAA8B,IAAI,iBAAiB,CAmBlE"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const RESET = '\u001B[0m';
|
|
2
|
+
const BRIGHT_GREEN = '\u001B[32m';
|
|
3
|
+
const BRIGHT_RED = '\u001B[31m';
|
|
4
|
+
const BRIGHT_YELLOW = '\u001B[33m';
|
|
5
|
+
const DIM = '\u001B[2m';
|
|
6
|
+
function colorize(value, color, enabled) {
|
|
7
|
+
return enabled ? `${color}${value}${RESET}` : value;
|
|
8
|
+
}
|
|
9
|
+
function formatLog(level, context, message, color) {
|
|
10
|
+
const prefix = colorize('[fluo]', BRIGHT_GREEN, color);
|
|
11
|
+
const pid = colorize(String(process.pid), BRIGHT_YELLOW, color);
|
|
12
|
+
const timestamp = colorize(new Date().toLocaleString('en-US'), DIM, color);
|
|
13
|
+
const levelColor = level === 'ERROR' || level === 'WARN' ? BRIGHT_RED : BRIGHT_GREEN;
|
|
14
|
+
const levelLabel = colorize(level, levelColor, color);
|
|
15
|
+
const contextLabel = colorize(`[${context}]`, BRIGHT_YELLOW, color);
|
|
16
|
+
return `${prefix} ${pid} - ${timestamp} ${levelLabel} ${contextLabel} ${message}`;
|
|
17
|
+
}
|
|
18
|
+
export function createConsoleApplicationLogger() {
|
|
19
|
+
return {
|
|
20
|
+
debug(message, context = 'fluo') {
|
|
21
|
+
console.debug(formatLog('DEBUG', context, message, Boolean(process.stdout.isTTY)));
|
|
22
|
+
},
|
|
23
|
+
error(message, error, context = 'fluo') {
|
|
24
|
+
console.error(formatLog('ERROR', context, message, Boolean(process.stderr.isTTY)));
|
|
25
|
+
if (error) {
|
|
26
|
+
console.error(error);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
log(message, context = 'fluo') {
|
|
30
|
+
console.log(formatLog('LOG', context, message, Boolean(process.stdout.isTTY)));
|
|
31
|
+
},
|
|
32
|
+
warn(message, context = 'fluo') {
|
|
33
|
+
console.warn(formatLog('WARN', context, message, Boolean(process.stderr.isTTY)));
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Provider } from '@fluojs/di';
|
|
2
|
+
import type { Token } from '@fluojs/core';
|
|
3
|
+
import type { BootstrapModuleOptions, CompiledModule, ModuleType } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Returns the public token represented by a provider declaration.
|
|
6
|
+
*
|
|
7
|
+
* @param provider Provider declaration or class provider shortcut.
|
|
8
|
+
* @returns The token that should be registered and resolved for the provider.
|
|
9
|
+
*/
|
|
10
|
+
export declare function providerToken(provider: Provider): Token;
|
|
11
|
+
/**
|
|
12
|
+
* Collects runtime provider tokens into a set for visibility and validation checks.
|
|
13
|
+
*
|
|
14
|
+
* @param providers Runtime providers supplied through bootstrap options.
|
|
15
|
+
* @returns A set containing each provider token exactly once.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createRuntimeTokenSet(providers?: Provider[]): Set<Token>;
|
|
18
|
+
/**
|
|
19
|
+
* Compiles and validates the reachable module graph for a bootstrap entry module.
|
|
20
|
+
*
|
|
21
|
+
* @param rootModule Root application module used as the graph entrypoint.
|
|
22
|
+
* @param options Bootstrap options that contribute runtime providers and validation tokens.
|
|
23
|
+
* @returns Compiled modules in dependency order after visibility and injection validation succeed.
|
|
24
|
+
*/
|
|
25
|
+
export declare function compileModuleGraph(rootModule: ModuleType, options?: BootstrapModuleOptions): CompiledModule[];
|
|
26
|
+
//# sourceMappingURL=module-graph.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module-graph.d.ts","sourceRoot":"","sources":["../src/module-graph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAK1C,OAAO,KAAK,EAAE,sBAAsB,EAAE,cAAc,EAAoB,UAAU,EAAE,MAAM,YAAY,CAAC;AAEvG;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,KAAK,CAMvD;AAyDD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,GAAE,QAAQ,EAAO,GAAG,GAAG,CAAC,KAAK,CAAC,CAE5E;AAyTD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,GAAE,sBAA2B,GAAG,cAAc,EAAE,CASjH"}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { getClassDiMetadata, getModuleMetadata, getOwnClassDiMetadata } from '@fluojs/core/internal';
|
|
2
|
+
import { ModuleGraphError, ModuleInjectionMetadataError, ModuleVisibilityError } from './errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* Returns the public token represented by a provider declaration.
|
|
5
|
+
*
|
|
6
|
+
* @param provider Provider declaration or class provider shortcut.
|
|
7
|
+
* @returns The token that should be registered and resolved for the provider.
|
|
8
|
+
*/
|
|
9
|
+
export function providerToken(provider) {
|
|
10
|
+
if (typeof provider === 'function') {
|
|
11
|
+
return provider;
|
|
12
|
+
}
|
|
13
|
+
return provider.provide;
|
|
14
|
+
}
|
|
15
|
+
function getEffectiveClassDiMetadata(target) {
|
|
16
|
+
const metadata = getClassDiMetadata(target);
|
|
17
|
+
if (!metadata) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
inject: metadata.inject ? [...metadata.inject] : undefined
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function isForwardRef(value) {
|
|
25
|
+
return typeof value === 'object' && value !== null && '__forwardRef__' in value && value.__forwardRef__ === true;
|
|
26
|
+
}
|
|
27
|
+
function isOptionalToken(value) {
|
|
28
|
+
return typeof value === 'object' && value !== null && '__optional__' in value && value.__optional__ === true;
|
|
29
|
+
}
|
|
30
|
+
function resolveInjectionToken(t) {
|
|
31
|
+
if (isForwardRef(t)) return t.forwardRef();
|
|
32
|
+
if (isOptionalToken(t)) return t.token;
|
|
33
|
+
return t;
|
|
34
|
+
}
|
|
35
|
+
function providerDependencies(provider) {
|
|
36
|
+
if (typeof provider === 'function') {
|
|
37
|
+
return [...(getEffectiveClassDiMetadata(provider)?.inject ?? [])];
|
|
38
|
+
}
|
|
39
|
+
if ('useFactory' in provider) {
|
|
40
|
+
return provider.inject ?? [];
|
|
41
|
+
}
|
|
42
|
+
if ('useClass' in provider) {
|
|
43
|
+
return provider.inject ?? [...(getEffectiveClassDiMetadata(provider.useClass)?.inject ?? [])];
|
|
44
|
+
}
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
function controllerDependencies(controller) {
|
|
48
|
+
return [...(getEffectiveClassDiMetadata(controller)?.inject ?? [])];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Collects runtime provider tokens into a set for visibility and validation checks.
|
|
53
|
+
*
|
|
54
|
+
* @param providers Runtime providers supplied through bootstrap options.
|
|
55
|
+
* @returns A set containing each provider token exactly once.
|
|
56
|
+
*/
|
|
57
|
+
export function createRuntimeTokenSet(providers = []) {
|
|
58
|
+
return new Set(providers.map(provider => providerToken(provider)));
|
|
59
|
+
}
|
|
60
|
+
function mergeRuntimeTokenSets(providers = [], validationTokens = []) {
|
|
61
|
+
return new Set([...createRuntimeTokenSet(providers), ...validationTokens]);
|
|
62
|
+
}
|
|
63
|
+
function requiredConstructorParameters(target) {
|
|
64
|
+
if (getOwnClassDiMetadata(target)?.inject !== undefined) {
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
return target.length;
|
|
68
|
+
}
|
|
69
|
+
function validateClassInjectionMetadata(subject, implementation, inject, scope, remedy) {
|
|
70
|
+
const required = requiredConstructorParameters(implementation);
|
|
71
|
+
if (required === 0 || inject.length >= required) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const missingIndex = inject.length;
|
|
75
|
+
const configured = inject.length;
|
|
76
|
+
const parameterWord = required === 1 ? 'parameter' : 'parameters';
|
|
77
|
+
const tokenWord = configured === 1 ? 'token is' : 'tokens are';
|
|
78
|
+
throw new ModuleInjectionMetadataError(`${subject} in ${scope} declares ${required} constructor ${parameterWord} but only ${configured} injection ${tokenWord} configured. Add ${remedy} for constructor parameter #${missingIndex}.`, {
|
|
79
|
+
module: scope,
|
|
80
|
+
phase: 'injection metadata validation',
|
|
81
|
+
hint: `Ensure ${subject} has a matching @Inject(...) decorator or provider.inject array that covers all ${required} constructor parameters. Use @Inject() for an explicit empty override.`
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function validateProviderInjectionMetadata(provider, scope) {
|
|
85
|
+
if (typeof provider === 'function') {
|
|
86
|
+
validateClassInjectionMetadata(`Provider ${provider.name || '<anonymous>'}`, provider, getEffectiveClassDiMetadata(provider)?.inject ?? [], scope, '@Inject(...) metadata');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if ('useClass' in provider) {
|
|
90
|
+
const providedName = String(provider.provide);
|
|
91
|
+
const implementationName = provider.useClass.name || '<anonymous>';
|
|
92
|
+
const subject = provider.provide === provider.useClass ? `Provider ${implementationName}` : `Provider ${providedName} (${implementationName})`;
|
|
93
|
+
validateClassInjectionMetadata(subject, provider.useClass, provider.inject ?? getEffectiveClassDiMetadata(provider.useClass)?.inject ?? [], scope, provider.inject ? 'provider.inject entries' : '@Inject(...) metadata or provider.inject entries');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function validateControllerInjectionMetadata(controller, scope) {
|
|
97
|
+
validateClassInjectionMetadata(`Controller ${controller.name || '<anonymous>'}`, controller, getEffectiveClassDiMetadata(controller)?.inject ?? [], scope, '@Inject(...) metadata');
|
|
98
|
+
}
|
|
99
|
+
function normalizeModuleDefinition(rawDefinition) {
|
|
100
|
+
if (!rawDefinition) {
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
global: rawDefinition.global ?? false,
|
|
105
|
+
imports: rawDefinition.imports ?? [],
|
|
106
|
+
providers: rawDefinition.providers ?? [],
|
|
107
|
+
controllers: rawDefinition.controllers ?? [],
|
|
108
|
+
exports: rawDefinition.exports ?? [],
|
|
109
|
+
middleware: rawDefinition.middleware ?? []
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function compileModule(moduleType, runtimeProviderTokens, compiled = new Map(), visiting = new Set(), ordered = []) {
|
|
113
|
+
if (compiled.has(moduleType)) {
|
|
114
|
+
const existing = compiled.get(moduleType);
|
|
115
|
+
if (existing) {
|
|
116
|
+
return existing;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (visiting.has(moduleType)) {
|
|
120
|
+
throw new ModuleGraphError(`Circular module import detected for ${moduleType.name}.`, {
|
|
121
|
+
module: moduleType.name,
|
|
122
|
+
phase: 'module graph compilation',
|
|
123
|
+
hint: 'Break the import cycle by extracting shared providers into a separate module that both sides can import independently.'
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
visiting.add(moduleType);
|
|
127
|
+
const definition = normalizeModuleDefinition(getModuleMetadata(moduleType));
|
|
128
|
+
for (const imported of definition.imports ?? []) {
|
|
129
|
+
compileModule(imported, runtimeProviderTokens, compiled, visiting, ordered);
|
|
130
|
+
}
|
|
131
|
+
const providerTokens = new Set((definition.providers ?? []).map(provider => providerToken(provider)));
|
|
132
|
+
const compiledModule = {
|
|
133
|
+
type: moduleType,
|
|
134
|
+
definition,
|
|
135
|
+
exportedTokens: new Set(),
|
|
136
|
+
providerTokens
|
|
137
|
+
};
|
|
138
|
+
compiled.set(moduleType, compiledModule);
|
|
139
|
+
visiting.delete(moduleType);
|
|
140
|
+
ordered.push(compiledModule);
|
|
141
|
+
return compiledModule;
|
|
142
|
+
}
|
|
143
|
+
function resolveImportedModules(compiledModule, compiledByType) {
|
|
144
|
+
return (compiledModule.definition.imports ?? []).map(imported => {
|
|
145
|
+
const importedModule = compiledByType.get(imported);
|
|
146
|
+
if (!importedModule) {
|
|
147
|
+
throw new ModuleGraphError(`Imported module ${imported.name} was not compiled.`, {
|
|
148
|
+
module: imported.name,
|
|
149
|
+
phase: 'module graph validation',
|
|
150
|
+
hint: `Ensure ${imported.name} is decorated with @Module() and included in the imports array of a compiled module.`
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return importedModule;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function createImportedExportedTokenSet(importedModules) {
|
|
157
|
+
return new Set(importedModules.flatMap(imported => Array.from(imported.exportedTokens)));
|
|
158
|
+
}
|
|
159
|
+
function createAccessibleTokenSet(runtimeProviderTokens, moduleProviderTokens, importedExportedTokens, globalExportedTokens) {
|
|
160
|
+
return new Set([...runtimeProviderTokens, ...moduleProviderTokens, ...importedExportedTokens, ...globalExportedTokens]);
|
|
161
|
+
}
|
|
162
|
+
function validateProviderVisibility(compiledModule, scope, accessibleTokens) {
|
|
163
|
+
for (const provider of compiledModule.definition.providers ?? []) {
|
|
164
|
+
validateProviderInjectionMetadata(provider, scope);
|
|
165
|
+
for (const rawToken of providerDependencies(provider)) {
|
|
166
|
+
const token = resolveInjectionToken(rawToken);
|
|
167
|
+
if (!accessibleTokens.has(token)) {
|
|
168
|
+
throw new ModuleVisibilityError(`Provider ${String(providerToken(provider))} in module ${compiledModule.type.name} cannot access token ${String(token)} because it is not local, not exported by an imported module, and not visible through a global module.`, {
|
|
169
|
+
module: compiledModule.type.name,
|
|
170
|
+
token,
|
|
171
|
+
phase: 'provider visibility validation',
|
|
172
|
+
hint: `Add ${String(token)} to the exports array of the module that owns it, then import that module into ${compiledModule.type.name}. Alternatively, mark the owning module with @Global() to make its exports universally visible.`
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function validateControllerVisibility(compiledModule, scope, accessibleTokens) {
|
|
179
|
+
for (const controller of compiledModule.definition.controllers ?? []) {
|
|
180
|
+
validateControllerInjectionMetadata(controller, scope);
|
|
181
|
+
for (const rawToken of controllerDependencies(controller)) {
|
|
182
|
+
const token = resolveInjectionToken(rawToken);
|
|
183
|
+
if (!accessibleTokens.has(token)) {
|
|
184
|
+
throw new ModuleVisibilityError(`Controller ${controller.name} in module ${compiledModule.type.name} cannot access token ${String(token)} because it is not local, not exported by an imported module, and not visible through a global module.`, {
|
|
185
|
+
module: compiledModule.type.name,
|
|
186
|
+
token,
|
|
187
|
+
phase: 'controller visibility validation',
|
|
188
|
+
hint: `Add ${String(token)} to the exports array of the module that owns it, then import that module into ${compiledModule.type.name}. Alternatively, mark the owning module with @Global().`
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function createExportedTokenSet(compiledModule, importedExportedTokens) {
|
|
195
|
+
const exportedTokens = new Set();
|
|
196
|
+
for (const token of compiledModule.definition.exports ?? []) {
|
|
197
|
+
if (!compiledModule.providerTokens.has(token) && !importedExportedTokens.has(token)) {
|
|
198
|
+
throw new ModuleVisibilityError(`Module ${compiledModule.type.name} cannot export token ${String(token)} because it is neither local nor re-exported from an imported module.`, {
|
|
199
|
+
module: compiledModule.type.name,
|
|
200
|
+
token,
|
|
201
|
+
phase: 'export validation',
|
|
202
|
+
hint: `Either add a provider for ${String(token)} to ${compiledModule.type.name}'s providers array, or import a module that exports ${String(token)} so it can be re-exported.`
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
exportedTokens.add(token);
|
|
206
|
+
}
|
|
207
|
+
return exportedTokens;
|
|
208
|
+
}
|
|
209
|
+
function validateCompiledModules(modules, runtimeProviders, runtimeProviderTokens) {
|
|
210
|
+
const compiledByType = new Map(modules.map(compiledModule => [compiledModule.type, compiledModule]));
|
|
211
|
+
const globalExportedTokens = new Set();
|
|
212
|
+
for (const provider of runtimeProviders) {
|
|
213
|
+
validateProviderInjectionMetadata(provider, 'bootstrap runtime');
|
|
214
|
+
}
|
|
215
|
+
for (const compiledModule of modules) {
|
|
216
|
+
if (!compiledModule.definition.global) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
for (const token of compiledModule.definition.exports ?? []) {
|
|
220
|
+
globalExportedTokens.add(token);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
for (const compiledModule of modules) {
|
|
224
|
+
const scope = `module ${compiledModule.type.name}`;
|
|
225
|
+
const importedModules = resolveImportedModules(compiledModule, compiledByType);
|
|
226
|
+
const importedExportedTokens = createImportedExportedTokenSet(importedModules);
|
|
227
|
+
const accessibleTokens = createAccessibleTokenSet(runtimeProviderTokens, compiledModule.providerTokens, importedExportedTokens, globalExportedTokens);
|
|
228
|
+
validateProviderVisibility(compiledModule, scope, accessibleTokens);
|
|
229
|
+
validateControllerVisibility(compiledModule, scope, accessibleTokens);
|
|
230
|
+
compiledModule.exportedTokens = createExportedTokenSet(compiledModule, importedExportedTokens);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Compiles and validates the reachable module graph for a bootstrap entry module.
|
|
236
|
+
*
|
|
237
|
+
* @param rootModule Root application module used as the graph entrypoint.
|
|
238
|
+
* @param options Bootstrap options that contribute runtime providers and validation tokens.
|
|
239
|
+
* @returns Compiled modules in dependency order after visibility and injection validation succeed.
|
|
240
|
+
*/
|
|
241
|
+
export function compileModuleGraph(rootModule, options = {}) {
|
|
242
|
+
const ordered = [];
|
|
243
|
+
const runtimeProviders = options.providers ?? [];
|
|
244
|
+
const runtimeProviderTokens = mergeRuntimeTokenSets(runtimeProviders, options.validationTokens ?? []);
|
|
245
|
+
compileModule(rootModule, runtimeProviderTokens, new Map(), new Set(), ordered);
|
|
246
|
+
validateCompiledModules(ordered, runtimeProviders, runtimeProviderTokens);
|
|
247
|
+
return ordered;
|
|
248
|
+
}
|