@loutrejs/loutre 0.1.0 → 0.2.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 +69 -0
- package/dist/adapters/aws-lambda.js +2 -0
- package/dist/adapters/bun.d.ts +3 -1
- package/dist/adapters/bun.js +90 -33
- package/dist/adapters/cloudflare-workers.js +2 -0
- package/dist/adapters/deno.d.ts +3 -1
- package/dist/adapters/deno.js +99 -33
- package/dist/adapters/electron.js +2 -0
- package/dist/generated/version.d.ts +1 -0
- package/dist/generated/version.js +2 -0
- package/dist/graph/graph.js +9 -9
- package/dist/http/application.js +5 -5
- package/dist/http/basic-auth.js +1 -1
- package/dist/http/cors.js +4 -4
- package/dist/message-port/index.js +4 -4
- package/dist/presentation.d.ts +33 -0
- package/dist/presentation.js +233 -0
- package/dist/runtime/capability.js +1 -1
- package/dist/runtime/di.js +2 -2
- package/dist/runtime/engine.d.ts +3 -0
- package/dist/runtime/engine.js +38 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/pipeline.js +13 -13
- package/dist/runtime/server-url.d.ts +1 -0
- package/dist/runtime/server-url.js +5 -0
- package/package.json +5 -1
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# @loutrejs/loutre
|
|
2
|
+
|
|
3
|
+
LoutreのCore packageです。Application Definition、Contract、DI、Pipeline、Task、Trigger、Environment、Arguments、Runtime bindingなど、Loutre Applicationを構成するpublic APIを提供します。
|
|
4
|
+
|
|
5
|
+
> Loutreは現在v0.xです。Public APIには破壊的変更が入る可能性があります。
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @loutrejs/loutre
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
新しいApplicationを始める場合はinitializerも利用できます。
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm create loutre@latest my-app
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Application model
|
|
20
|
+
|
|
21
|
+
LoutreはHTTP endpointだけを中心にせず、Applicationが実行する処理を同じmodelへ載せます。
|
|
22
|
+
|
|
23
|
+
- **Contract / Implementation** — protocolごとのinterfaceと実装
|
|
24
|
+
- **Module / DI** — dependencyとApplication構成
|
|
25
|
+
- **Pipeline / Layer** — requestやexecution contextの処理
|
|
26
|
+
- **Task / Trigger / Queue** — 明示実行、schedule、queue consumer
|
|
27
|
+
- **Environment / Arguments** — Hostから渡されるtyped input
|
|
28
|
+
- **Application Graph** — Application構造をRuntimeとToolingで共有
|
|
29
|
+
|
|
30
|
+
## Example
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { defineApplication, task } from '@loutrejs/loutre'
|
|
34
|
+
|
|
35
|
+
export const hello = task<void, string>({
|
|
36
|
+
name: 'hello',
|
|
37
|
+
factory: () => () => 'Hello, Loutre!',
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
export default defineApplication({
|
|
41
|
+
modules: [],
|
|
42
|
+
tasks: [hello],
|
|
43
|
+
})
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Entry points
|
|
47
|
+
|
|
48
|
+
主なsubpath exportは次のとおりです。
|
|
49
|
+
|
|
50
|
+
| Entry point | Role |
|
|
51
|
+
| ------------------------------- | ------------------------ |
|
|
52
|
+
| `@loutrejs/loutre/http` | HTTP Contract / Pipeline |
|
|
53
|
+
| `@loutrejs/loutre/host` | Application bootstrap |
|
|
54
|
+
| `@loutrejs/loutre/binding` | Host / Queue binding |
|
|
55
|
+
| `@loutrejs/loutre/runtime` | Runtime capability |
|
|
56
|
+
| `@loutrejs/loutre/graph` | Application Graph |
|
|
57
|
+
| `@loutrejs/loutre/openapi` | OpenAPI generation |
|
|
58
|
+
| `@loutrejs/loutre/presentation` | startup presentation |
|
|
59
|
+
| `@loutrejs/loutre/message-port` | MessagePort protocol |
|
|
60
|
+
|
|
61
|
+
## Documentation
|
|
62
|
+
|
|
63
|
+
- [Getting Started](https://github.com/come25136/loutrejs/blob/main/docs/getting-started.md)
|
|
64
|
+
- [Architecture](https://github.com/come25136/loutrejs/blob/main/docs/architecture.md)
|
|
65
|
+
- [Examples](https://github.com/come25136/loutrejs/tree/main/examples)
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
[MIT](https://github.com/come25136/loutrejs/blob/main/LICENSE)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { binding, } from '../application/index.js';
|
|
2
|
+
import { assertRuntimeEngine } from '../runtime/engine.js';
|
|
2
3
|
export const awsLambdaRuntime = {
|
|
3
4
|
runtime: 'aws-lambda',
|
|
4
5
|
capabilities: new Set([
|
|
@@ -12,6 +13,7 @@ export const awsLambdaRuntime = {
|
|
|
12
13
|
bind,
|
|
13
14
|
};
|
|
14
15
|
function bind(options) {
|
|
16
|
+
assertRuntimeEngine('aws-lambda');
|
|
15
17
|
const invocation = binding.invocation({
|
|
16
18
|
application: options.application,
|
|
17
19
|
environment: 'environment' in options ? options.environment : process.env,
|
package/dist/adapters/bun.d.ts
CHANGED
|
@@ -3,12 +3,14 @@ type IsAny<TValue> = 0 extends 1 & TValue ? true : false;
|
|
|
3
3
|
type HttpApplication<TDefinition extends ApplicationDefinition> = IsAny<TDefinition> extends true ? TDefinition : HasHttp<TDefinition> extends true ? TDefinition : never;
|
|
4
4
|
export type BunServeOptions<TDefinition extends ApplicationDefinition> = {
|
|
5
5
|
readonly application: HttpApplication<TDefinition>;
|
|
6
|
-
readonly port
|
|
6
|
+
readonly port?: number;
|
|
7
7
|
readonly hostname?: string;
|
|
8
|
+
readonly shutdownHooks?: boolean;
|
|
8
9
|
readonly environment?: unknown;
|
|
9
10
|
} & BootstrapArguments<TDefinition>;
|
|
10
11
|
export interface BunServeHandle<TDefinition extends ApplicationDefinition = ApplicationDefinition> {
|
|
11
12
|
readonly application: HostBindingApplication<TDefinition>;
|
|
13
|
+
readonly port: number;
|
|
12
14
|
close(signal?: string): Promise<void>;
|
|
13
15
|
}
|
|
14
16
|
export declare const bunRuntime: {
|
package/dist/adapters/bun.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { binding, } from '../application/index.js';
|
|
2
|
+
import { LOUTRE_VERSION, detectPresentationTerminal, startStartupPresentation, } from '../presentation.js';
|
|
3
|
+
import { assertRuntimeEngine } from '../runtime/engine.js';
|
|
4
|
+
import { serverUrl } from '../runtime/server-url.js';
|
|
2
5
|
export const bunRuntime = {
|
|
3
6
|
runtime: 'bun',
|
|
4
7
|
capabilities: new Set([
|
|
@@ -15,13 +18,20 @@ export const bunRuntime = {
|
|
|
15
18
|
serve,
|
|
16
19
|
};
|
|
17
20
|
async function serve(options) {
|
|
21
|
+
assertRuntimeEngine('bun');
|
|
18
22
|
const bun = bunGlobal();
|
|
19
23
|
if (!bun?.serve) {
|
|
20
24
|
throw new Error('LUTRE_BUN_UNAVAILABLE: Bun.serve() is not available.');
|
|
21
25
|
}
|
|
26
|
+
const startedAt = performance.now();
|
|
27
|
+
const environment = bun.env ?? {};
|
|
28
|
+
const presentation = startStartupPresentation({ version: LOUTRE_VERSION }, {
|
|
29
|
+
terminal: detectPresentationTerminal(process.stdout, environment),
|
|
30
|
+
write: (value) => console.log(value),
|
|
31
|
+
});
|
|
22
32
|
const host = binding.host({
|
|
23
33
|
application: options.application,
|
|
24
|
-
environment: 'environment' in options ? options.environment :
|
|
34
|
+
environment: 'environment' in options ? options.environment : environment,
|
|
25
35
|
...('arguments' in options ? { arguments: options.arguments } : {}),
|
|
26
36
|
});
|
|
27
37
|
const http = 'http' in host ? host.http : undefined;
|
|
@@ -33,41 +43,88 @@ async function serve(options) {
|
|
|
33
43
|
if ('triggers' in host.application)
|
|
34
44
|
await host.application.triggers.start();
|
|
35
45
|
let server;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
const requestedPort = options.port;
|
|
47
|
+
let port = requestedPort ?? 3000;
|
|
48
|
+
while (true) {
|
|
49
|
+
try {
|
|
50
|
+
server = bun.serve({
|
|
51
|
+
port,
|
|
52
|
+
...(options.hostname === undefined
|
|
53
|
+
? {}
|
|
54
|
+
: { hostname: options.hostname }),
|
|
55
|
+
fetch: createBunFetchDriver(http),
|
|
56
|
+
});
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (requestedPort !== undefined || !canRetryOnNextPort(error, port)) {
|
|
61
|
+
await host.application.close().catch(() => undefined);
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
port += 1;
|
|
65
|
+
}
|
|
46
66
|
}
|
|
67
|
+
presentation.ready({
|
|
68
|
+
server: serverUrl(options.hostname, port),
|
|
69
|
+
runtime: `Bun ${bun.version ?? 'unknown'}`,
|
|
70
|
+
environment: environment.NODE_ENV ?? 'development',
|
|
71
|
+
startupDurationMs: performance.now() - startedAt,
|
|
72
|
+
});
|
|
47
73
|
let closed = false;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
const closeRuntime = async (signal) => {
|
|
75
|
+
if (closed)
|
|
76
|
+
return;
|
|
77
|
+
closed = true;
|
|
78
|
+
const errors = [];
|
|
79
|
+
try {
|
|
80
|
+
await server.stop(true);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
errors.push(error);
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
await host.application.close(signal);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
errors.push(error);
|
|
90
|
+
}
|
|
91
|
+
if (errors.length > 0)
|
|
92
|
+
throw new AggregateError(errors, 'Bun runtime shutdown failed');
|
|
93
|
+
};
|
|
94
|
+
const removeShutdownHooks = options.shutdownHooks === false
|
|
95
|
+
? undefined
|
|
96
|
+
: registerBunShutdownHooks(closeRuntime);
|
|
97
|
+
const close = async (signal) => {
|
|
98
|
+
removeShutdownHooks?.();
|
|
99
|
+
await closeRuntime(signal);
|
|
100
|
+
};
|
|
101
|
+
return { application: host.application, port, close };
|
|
102
|
+
}
|
|
103
|
+
function registerBunShutdownHooks(close) {
|
|
104
|
+
const handlers = new Map();
|
|
105
|
+
const remove = () => {
|
|
106
|
+
for (const [signal, handler] of handlers)
|
|
107
|
+
process.off(signal, handler);
|
|
108
|
+
handlers.clear();
|
|
70
109
|
};
|
|
110
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
111
|
+
const handler = () => {
|
|
112
|
+
remove();
|
|
113
|
+
void close(signal);
|
|
114
|
+
};
|
|
115
|
+
handlers.set(signal, handler);
|
|
116
|
+
process.once(signal, handler);
|
|
117
|
+
}
|
|
118
|
+
return remove;
|
|
119
|
+
}
|
|
120
|
+
function canRetryOnNextPort(error, port) {
|
|
121
|
+
return port < 65_535 && isAddressInUseError(error);
|
|
122
|
+
}
|
|
123
|
+
function isAddressInUseError(error) {
|
|
124
|
+
return (typeof error === 'object' &&
|
|
125
|
+
error !== null &&
|
|
126
|
+
'code' in error &&
|
|
127
|
+
error.code === 'EADDRINUSE');
|
|
71
128
|
}
|
|
72
129
|
function createBunFetchDriver(application) {
|
|
73
130
|
let initialization;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { binding, } from '../application/index.js';
|
|
2
|
+
import { assertRuntimeEngine } from '../runtime/engine.js';
|
|
2
3
|
export const cloudflareWorkersRuntime = {
|
|
3
4
|
runtime: 'cloudflare-workers',
|
|
4
5
|
compatibilityDateMinimum: '2026-08-04',
|
|
@@ -15,6 +16,7 @@ export const cloudflareWorkersRuntime = {
|
|
|
15
16
|
bind,
|
|
16
17
|
};
|
|
17
18
|
function bind(options) {
|
|
19
|
+
assertRuntimeEngine('cloudflare-workers');
|
|
18
20
|
let invocation;
|
|
19
21
|
let fetch;
|
|
20
22
|
const resolve = (environment) => {
|
package/dist/adapters/deno.d.ts
CHANGED
|
@@ -6,8 +6,9 @@ export type DenoRuntimeOptions<TDefinition extends ApplicationDefinition> = {
|
|
|
6
6
|
readonly environment?: unknown;
|
|
7
7
|
} & BootstrapArguments<TDefinition>;
|
|
8
8
|
export type DenoServeOptions<TDefinition extends ApplicationDefinition> = DenoRuntimeOptions<TDefinition> & {
|
|
9
|
-
readonly port
|
|
9
|
+
readonly port?: number;
|
|
10
10
|
readonly hostname?: string;
|
|
11
|
+
readonly shutdownHooks?: boolean;
|
|
11
12
|
};
|
|
12
13
|
export interface DenoBinding {
|
|
13
14
|
fetch(request: Request): Promise<Response>;
|
|
@@ -15,6 +16,7 @@ export interface DenoBinding {
|
|
|
15
16
|
}
|
|
16
17
|
export interface DenoServeHandle<TDefinition extends ApplicationDefinition = ApplicationDefinition> {
|
|
17
18
|
readonly application: HostBindingApplication<TDefinition>;
|
|
19
|
+
readonly port: number;
|
|
18
20
|
close(signal?: string): Promise<void>;
|
|
19
21
|
}
|
|
20
22
|
export declare const denoRuntime: {
|
package/dist/adapters/deno.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { binding, } from '../application/index.js';
|
|
2
|
+
import { LOUTRE_VERSION, startStartupPresentation } from '../presentation.js';
|
|
3
|
+
import { assertRuntimeEngine } from '../runtime/engine.js';
|
|
4
|
+
import { serverUrl } from '../runtime/server-url.js';
|
|
2
5
|
export const denoRuntime = {
|
|
3
6
|
runtime: 'deno',
|
|
4
7
|
capabilities: new Set([
|
|
@@ -16,6 +19,7 @@ export const denoRuntime = {
|
|
|
16
19
|
serve,
|
|
17
20
|
};
|
|
18
21
|
function bind(options) {
|
|
22
|
+
assertRuntimeEngine('deno');
|
|
19
23
|
const invocation = binding.invocation(bindingOptions(options, denoEnvironment()));
|
|
20
24
|
const http = 'http' in invocation
|
|
21
25
|
? invocation.http
|
|
@@ -31,10 +35,20 @@ function bind(options) {
|
|
|
31
35
|
};
|
|
32
36
|
}
|
|
33
37
|
async function serve(options) {
|
|
38
|
+
assertRuntimeEngine('deno');
|
|
34
39
|
const deno = denoGlobal();
|
|
35
40
|
if (!deno?.serve) {
|
|
36
41
|
throw new Error('LUTRE_DENO_UNAVAILABLE: Deno.serve() is not available.');
|
|
37
42
|
}
|
|
43
|
+
const startedAt = performance.now();
|
|
44
|
+
const isTTY = deno.stdout?.isTerminal?.() === true;
|
|
45
|
+
const presentation = startStartupPresentation({ version: LOUTRE_VERSION }, {
|
|
46
|
+
terminal: {
|
|
47
|
+
isTTY,
|
|
48
|
+
color: isTTY && deno.env?.get?.('NO_COLOR') === undefined,
|
|
49
|
+
},
|
|
50
|
+
write: (value) => console.log(value),
|
|
51
|
+
});
|
|
38
52
|
const host = binding.host(bindingOptions(options, denoEnvironment()));
|
|
39
53
|
const http = 'http' in host ? host.http : undefined;
|
|
40
54
|
if (!http) {
|
|
@@ -45,42 +59,94 @@ async function serve(options) {
|
|
|
45
59
|
if ('triggers' in host.application)
|
|
46
60
|
await host.application.triggers.start();
|
|
47
61
|
let server;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
const requestedPort = options.port;
|
|
63
|
+
let port = requestedPort ?? 3000;
|
|
64
|
+
while (true) {
|
|
65
|
+
try {
|
|
66
|
+
server = deno.serve({
|
|
67
|
+
port,
|
|
68
|
+
...(options.hostname === undefined
|
|
69
|
+
? {}
|
|
70
|
+
: { hostname: options.hostname }),
|
|
71
|
+
onListen: () => undefined,
|
|
72
|
+
}, createDenoFetchDriver(http));
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (requestedPort !== undefined || !canRetryOnNextPort(error, port)) {
|
|
77
|
+
await host.application.close().catch(() => undefined);
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
port += 1;
|
|
81
|
+
}
|
|
59
82
|
}
|
|
83
|
+
presentation.ready({
|
|
84
|
+
server: serverUrl(options.hostname, port),
|
|
85
|
+
runtime: `Deno ${deno.version?.deno ?? 'unknown'}`,
|
|
86
|
+
environment: deno.env?.get?.('DENO_ENV') ??
|
|
87
|
+
deno.env?.get?.('NODE_ENV') ??
|
|
88
|
+
'development',
|
|
89
|
+
startupDurationMs: performance.now() - startedAt,
|
|
90
|
+
});
|
|
60
91
|
let closed = false;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
92
|
+
const closeRuntime = async (signal) => {
|
|
93
|
+
if (closed)
|
|
94
|
+
return;
|
|
95
|
+
closed = true;
|
|
96
|
+
const errors = [];
|
|
97
|
+
try {
|
|
98
|
+
await server.shutdown();
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
errors.push(error);
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
await host.application.close(signal);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
errors.push(error);
|
|
108
|
+
}
|
|
109
|
+
if (errors.length > 0)
|
|
110
|
+
throw new AggregateError(errors, 'Deno runtime shutdown failed');
|
|
111
|
+
};
|
|
112
|
+
const removeShutdownHooks = options.shutdownHooks === false
|
|
113
|
+
? undefined
|
|
114
|
+
: registerDenoShutdownHooks(deno, closeRuntime);
|
|
115
|
+
const close = async (signal) => {
|
|
116
|
+
removeShutdownHooks?.();
|
|
117
|
+
await closeRuntime(signal);
|
|
118
|
+
};
|
|
119
|
+
return { application: host.application, port, close };
|
|
120
|
+
}
|
|
121
|
+
function registerDenoShutdownHooks(deno, close) {
|
|
122
|
+
if (!deno.addSignalListener || !deno.removeSignalListener) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
const handlers = new Map();
|
|
126
|
+
const remove = () => {
|
|
127
|
+
for (const [signal, handler] of handlers) {
|
|
128
|
+
deno.removeSignalListener?.(signal, handler);
|
|
129
|
+
}
|
|
130
|
+
handlers.clear();
|
|
83
131
|
};
|
|
132
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
133
|
+
const handler = () => {
|
|
134
|
+
remove();
|
|
135
|
+
void close(signal);
|
|
136
|
+
};
|
|
137
|
+
handlers.set(signal, handler);
|
|
138
|
+
deno.addSignalListener(signal, handler);
|
|
139
|
+
}
|
|
140
|
+
return remove;
|
|
141
|
+
}
|
|
142
|
+
function canRetryOnNextPort(error, port) {
|
|
143
|
+
return port < 65_535 && isAddressInUseError(error);
|
|
144
|
+
}
|
|
145
|
+
function isAddressInUseError(error) {
|
|
146
|
+
return (typeof error === 'object' &&
|
|
147
|
+
error !== null &&
|
|
148
|
+
'code' in error &&
|
|
149
|
+
error.code === 'EADDRINUSE');
|
|
84
150
|
}
|
|
85
151
|
function createDenoFetchDriver(application) {
|
|
86
152
|
let initialization;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { binding, } from '../application/index.js';
|
|
2
2
|
import { attachMessagePort, } from '../message-port/index.js';
|
|
3
|
+
import { assertRuntimeEngine } from '../runtime/engine.js';
|
|
3
4
|
export const electronRuntime = {
|
|
4
5
|
runtime: 'electron',
|
|
5
6
|
capabilities: new Set([
|
|
@@ -15,6 +16,7 @@ export const electronRuntime = {
|
|
|
15
16
|
attach,
|
|
16
17
|
};
|
|
17
18
|
function attach(options) {
|
|
19
|
+
assertRuntimeEngine('electron');
|
|
18
20
|
const invocation = binding.invocation({
|
|
19
21
|
application: options.application,
|
|
20
22
|
environment: 'environment' in options
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const LOUTRE_VERSION = "0.2.0";
|
package/dist/graph/graph.js
CHANGED
|
@@ -465,7 +465,7 @@ function buildDependencyGraph(modules, descriptors, targets, tasks, diagnostics)
|
|
|
465
465
|
diagnostic.message.includes(token.id))) {
|
|
466
466
|
diagnostics.push({
|
|
467
467
|
code: 'LUTRE_TOKEN_001',
|
|
468
|
-
message: `Token ID ${token.id}
|
|
468
|
+
message: `Token ID ${token.id} is duplicated across different token declarations`,
|
|
469
469
|
path: `dependency:${token.id}`,
|
|
470
470
|
});
|
|
471
471
|
}
|
|
@@ -803,7 +803,7 @@ function validateDuplicateProviders(modules, diagnostics) {
|
|
|
803
803
|
}
|
|
804
804
|
diagnostics.push({
|
|
805
805
|
code: 'LUTRE_DI_003',
|
|
806
|
-
message: `Provider ${tokenName(provider.provide)}
|
|
806
|
+
message: `Provider ${tokenName(provider.provide)} is duplicated in ${existing.module} and ${moduleName}`,
|
|
807
807
|
path: `${moduleName}.providers.${tokenName(provider.provide)}`,
|
|
808
808
|
});
|
|
809
809
|
continue;
|
|
@@ -827,7 +827,7 @@ function collectCustomTokens(providers, targets, diagnostics) {
|
|
|
827
827
|
if (existing && existing !== candidate) {
|
|
828
828
|
diagnostics.push({
|
|
829
829
|
code: 'LUTRE_TOKEN_001',
|
|
830
|
-
message: `Token ID ${candidate.id}
|
|
830
|
+
message: `Token ID ${candidate.id} is duplicated across different token declarations`,
|
|
831
831
|
path,
|
|
832
832
|
});
|
|
833
833
|
return;
|
|
@@ -856,7 +856,7 @@ function collectContextKeys(targets, diagnostics) {
|
|
|
856
856
|
if (existing && existing !== key) {
|
|
857
857
|
diagnostics.push({
|
|
858
858
|
code: 'LUTRE_CONTEXT_002',
|
|
859
|
-
message: `Context Key ${key.name}
|
|
859
|
+
message: `Context Key ${key.name} is duplicated across different declarations`,
|
|
860
860
|
path,
|
|
861
861
|
});
|
|
862
862
|
continue;
|
|
@@ -964,7 +964,7 @@ function validatePipeline(target, diagnostics) {
|
|
|
964
964
|
if (!validated.has(required)) {
|
|
965
965
|
diagnostics.push({
|
|
966
966
|
code: 'LUTRE_VALIDATION_001',
|
|
967
|
-
message: `${item.name}
|
|
967
|
+
message: `${item.name} requires validated ${required} but appears before validate.${required}`,
|
|
968
968
|
path,
|
|
969
969
|
});
|
|
970
970
|
}
|
|
@@ -973,7 +973,7 @@ function validatePipeline(target, diagnostics) {
|
|
|
973
973
|
if (!available.has(required)) {
|
|
974
974
|
diagnostics.push({
|
|
975
975
|
code: 'LUTRE_PIPELINE_004',
|
|
976
|
-
message:
|
|
976
|
+
message: `Context Key ${contextKeyName(required)} required by ${item.name} is unavailable`,
|
|
977
977
|
path,
|
|
978
978
|
});
|
|
979
979
|
}
|
|
@@ -982,7 +982,7 @@ function validatePipeline(target, diagnostics) {
|
|
|
982
982
|
if (available.has(provided)) {
|
|
983
983
|
diagnostics.push({
|
|
984
984
|
code: 'LUTRE_CONTEXT_003',
|
|
985
|
-
message: `${item.name}
|
|
985
|
+
message: `${item.name} cannot implicitly overwrite existing Context Key ${contextKeyName(provided)}`,
|
|
986
986
|
path,
|
|
987
987
|
});
|
|
988
988
|
}
|
|
@@ -995,7 +995,7 @@ function validatePipeline(target, diagnostics) {
|
|
|
995
995
|
if (!response) {
|
|
996
996
|
diagnostics.push({
|
|
997
997
|
code: 'LUTRE_SHORT_CIRCUIT_001',
|
|
998
|
-
message:
|
|
998
|
+
message: `Short-circuit variant ${shortCircuit.variant} from ${item.name} is not declared in the response`,
|
|
999
999
|
path,
|
|
1000
1000
|
});
|
|
1001
1001
|
continue;
|
|
@@ -1005,7 +1005,7 @@ function validatePipeline(target, diagnostics) {
|
|
|
1005
1005
|
response.status !== expectedStatus) {
|
|
1006
1006
|
diagnostics.push({
|
|
1007
1007
|
code: 'LUTRE_SHORT_CIRCUIT_002',
|
|
1008
|
-
message:
|
|
1008
|
+
message: `Short-circuit variant ${shortCircuit.variant} from ${item.name} must use HTTP ${expectedStatus}`,
|
|
1009
1009
|
path,
|
|
1010
1010
|
});
|
|
1011
1011
|
}
|
package/dist/http/application.js
CHANGED
|
@@ -301,7 +301,7 @@ async function finalizeResponse(definition, result, signal) {
|
|
|
301
301
|
const responseHeaders = await validateResponseHeaders(response.headers, result.headers);
|
|
302
302
|
if (response.stream === 'server') {
|
|
303
303
|
if (!isAsyncIterable(result.body)) {
|
|
304
|
-
throw new Error('
|
|
304
|
+
throw new Error('Server-stream response requires an AsyncIterable');
|
|
305
305
|
}
|
|
306
306
|
const encoder = new TextEncoder();
|
|
307
307
|
const iterator = result.body[Symbol.asyncIterator]();
|
|
@@ -315,7 +315,7 @@ async function finalizeResponse(definition, result, signal) {
|
|
|
315
315
|
finished = true;
|
|
316
316
|
signal.removeEventListener('abort', abort);
|
|
317
317
|
void iterator.return?.(signal.reason).finally(() => {
|
|
318
|
-
controller.error(signal.reason ?? new Error('HTTP request
|
|
318
|
+
controller.error(signal.reason ?? new Error('HTTP request was aborted'));
|
|
319
319
|
});
|
|
320
320
|
};
|
|
321
321
|
if (signal.aborted)
|
|
@@ -368,7 +368,7 @@ async function finalizeResponse(definition, result, signal) {
|
|
|
368
368
|
async function validateResponseHeaders(schema, headers) {
|
|
369
369
|
if (!schema) {
|
|
370
370
|
if (headers !== undefined) {
|
|
371
|
-
throw new Error('
|
|
371
|
+
throw new Error('Undeclared HTTP response header was returned');
|
|
372
372
|
}
|
|
373
373
|
return undefined;
|
|
374
374
|
}
|
|
@@ -376,7 +376,7 @@ async function validateResponseHeaders(schema, headers) {
|
|
|
376
376
|
if (validated === undefined)
|
|
377
377
|
return undefined;
|
|
378
378
|
if (!isHttpHeaders(validated)) {
|
|
379
|
-
throw new Error('HTTP response header schema
|
|
379
|
+
throw new Error('HTTP response header schema produced an invalid value');
|
|
380
380
|
}
|
|
381
381
|
return validated;
|
|
382
382
|
}
|
|
@@ -458,7 +458,7 @@ async function mapDeclaredError(definition, error) {
|
|
|
458
458
|
if (typeof result !== 'object' ||
|
|
459
459
|
result === null ||
|
|
460
460
|
!('body' in result)) {
|
|
461
|
-
throw new Error('HTTP error mapping
|
|
461
|
+
throw new Error('HTTP error mapping must return a body');
|
|
462
462
|
}
|
|
463
463
|
return {
|
|
464
464
|
kind: 'http-result',
|
package/dist/http/basic-auth.js
CHANGED
|
@@ -40,7 +40,7 @@ function unauthorizedResult(unauthorized, challenge) {
|
|
|
40
40
|
}
|
|
41
41
|
function formatBasicChallenge(realm) {
|
|
42
42
|
if (realm.length === 0 || /[\u0000-\u001f\u007f]/.test(realm)) {
|
|
43
|
-
throw new TypeError('Basic
|
|
43
|
+
throw new TypeError('Basic authentication realm cannot be empty or contain control characters');
|
|
44
44
|
}
|
|
45
45
|
const escaped = realm.replace(/[\\"]/g, '\\$&');
|
|
46
46
|
return `Basic realm="${escaped}", charset="UTF-8"`;
|
package/dist/http/cors.js
CHANGED
|
@@ -123,12 +123,12 @@ function normalizeOrigin(origin, credentials) {
|
|
|
123
123
|
}
|
|
124
124
|
for (const value of origins) {
|
|
125
125
|
if (value.length === 0 || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
126
|
-
throw new TypeError('CORS origin
|
|
126
|
+
throw new TypeError('CORS origin cannot be empty or contain control characters');
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
if (origins.includes('*')) {
|
|
130
130
|
if (credentials) {
|
|
131
|
-
throw new TypeError('
|
|
131
|
+
throw new TypeError('CORS origin cannot be a wildcard when credentials are enabled');
|
|
132
132
|
}
|
|
133
133
|
return Object.freeze({ kind: 'wildcard' });
|
|
134
134
|
}
|
|
@@ -141,7 +141,7 @@ function normalizeTokens(values, option, uppercase) {
|
|
|
141
141
|
return Object.freeze(values.map((value) => {
|
|
142
142
|
const token = value.trim();
|
|
143
143
|
if (!httpToken.test(token)) {
|
|
144
|
-
throw new TypeError(`CORS ${option}
|
|
144
|
+
throw new TypeError(`CORS ${option} contains an invalid HTTP token`);
|
|
145
145
|
}
|
|
146
146
|
return uppercase ? token.toUpperCase() : token;
|
|
147
147
|
}));
|
|
@@ -150,7 +150,7 @@ function normalizeMaxAge(maxAge) {
|
|
|
150
150
|
if (maxAge === undefined)
|
|
151
151
|
return undefined;
|
|
152
152
|
if (!Number.isSafeInteger(maxAge) || maxAge < 0) {
|
|
153
|
-
throw new TypeError('CORS maxAge
|
|
153
|
+
throw new TypeError('CORS maxAge must be a non-negative safe integer');
|
|
154
154
|
}
|
|
155
155
|
return maxAge;
|
|
156
156
|
}
|
|
@@ -41,7 +41,7 @@ export function createMessagePortExecution(options) {
|
|
|
41
41
|
return await runtime.execute(async () => {
|
|
42
42
|
const route = routes.find((candidate) => candidate.procedure === procedure);
|
|
43
43
|
if (!route)
|
|
44
|
-
throw new Error(`MessagePort procedure
|
|
44
|
+
throw new Error(`MessagePort procedure not found: ${procedure}`);
|
|
45
45
|
invocationLogger = invocationLogger.child({
|
|
46
46
|
source: `${route.implementation.name}.${route.procedure}`,
|
|
47
47
|
});
|
|
@@ -66,7 +66,7 @@ export function createMessagePortExecution(options) {
|
|
|
66
66
|
const target = runtime.container.implementationRuntime(route.implementation);
|
|
67
67
|
const method = target[route.procedure];
|
|
68
68
|
if (typeof method !== 'function')
|
|
69
|
-
throw new Error('Handler method
|
|
69
|
+
throw new Error('Handler method is missing');
|
|
70
70
|
return Reflect.apply(method, target, [
|
|
71
71
|
terminalContext,
|
|
72
72
|
]);
|
|
@@ -106,10 +106,10 @@ export function createMessagePortExecution(options) {
|
|
|
106
106
|
async function finalize(definition, result) {
|
|
107
107
|
const response = definition.responses[result.variant];
|
|
108
108
|
if (!response)
|
|
109
|
-
throw new Error(
|
|
109
|
+
throw new Error(`Undeclared variant: ${result.variant}`);
|
|
110
110
|
if (response.stream === 'server') {
|
|
111
111
|
if (!isAsyncIterable(result.value)) {
|
|
112
|
-
throw new Error('
|
|
112
|
+
throw new Error('Server-stream result requires an AsyncIterable');
|
|
113
113
|
}
|
|
114
114
|
const source = result.value;
|
|
115
115
|
return {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { LOUTRE_VERSION } from './generated/version.js';
|
|
2
|
+
export interface StartupPresentationInfo {
|
|
3
|
+
readonly version: string;
|
|
4
|
+
}
|
|
5
|
+
export interface StartupStatusInfo {
|
|
6
|
+
readonly server: string;
|
|
7
|
+
readonly runtime: string;
|
|
8
|
+
readonly environment: string;
|
|
9
|
+
readonly startupDurationMs: number;
|
|
10
|
+
}
|
|
11
|
+
export interface StartupPresentationSession {
|
|
12
|
+
ready(info: StartupStatusInfo): void;
|
|
13
|
+
}
|
|
14
|
+
export interface StartupPresentationStartOptions {
|
|
15
|
+
readonly terminal: PresentationRenderOptions;
|
|
16
|
+
readonly write: (value: string) => void;
|
|
17
|
+
}
|
|
18
|
+
export { LOUTRE_VERSION };
|
|
19
|
+
export interface PresentationRenderOptions {
|
|
20
|
+
readonly isTTY: boolean;
|
|
21
|
+
readonly color: boolean;
|
|
22
|
+
readonly columns?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface PresentationTerminalOutput {
|
|
25
|
+
readonly isTTY?: boolean;
|
|
26
|
+
readonly columns?: number;
|
|
27
|
+
readonly getColorDepth: (environment?: Readonly<Record<string, string | undefined>>) => number;
|
|
28
|
+
}
|
|
29
|
+
export declare function detectPresentationTerminal(output: PresentationTerminalOutput, environment: Readonly<Record<string, string | undefined>>): PresentationRenderOptions;
|
|
30
|
+
export declare function startStartupPresentation(info: StartupPresentationInfo, options: StartupPresentationStartOptions): StartupPresentationSession;
|
|
31
|
+
export declare function renderLoutreBrand(options: PresentationRenderOptions): string;
|
|
32
|
+
export declare function renderStartupPrelude(info: StartupPresentationInfo, options: PresentationRenderOptions): string;
|
|
33
|
+
export declare function renderStartupStatus(info: StartupStatusInfo, options: PresentationRenderOptions): string;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { LOUTRE_VERSION } from './generated/version.js';
|
|
2
|
+
export { LOUTRE_VERSION };
|
|
3
|
+
export function detectPresentationTerminal(output, environment) {
|
|
4
|
+
const isTTY = output.isTTY === true;
|
|
5
|
+
const colorDisabled = environment.NO_COLOR !== undefined ||
|
|
6
|
+
environment.NODE_DISABLE_COLORS !== undefined;
|
|
7
|
+
const columns = output.columns;
|
|
8
|
+
return {
|
|
9
|
+
isTTY,
|
|
10
|
+
color: isTTY && !colorDisabled && output.getColorDepth(environment) >= 24,
|
|
11
|
+
...(columns !== undefined && columns > 0 ? { columns } : {}),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function startStartupPresentation(info, options) {
|
|
15
|
+
options.write(renderStartupPrelude(info, options.terminal));
|
|
16
|
+
return {
|
|
17
|
+
ready(status) {
|
|
18
|
+
options.write(renderStartupStatus(status, options.terminal));
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const colors = {
|
|
23
|
+
otterLight: [244, 211, 164],
|
|
24
|
+
otterSand: [224, 179, 119],
|
|
25
|
+
otterCaramel: [198, 139, 75],
|
|
26
|
+
otterBrown: [167, 105, 58],
|
|
27
|
+
otterDeep: [112, 66, 45],
|
|
28
|
+
lavender: [192, 132, 252],
|
|
29
|
+
lavenderSoft: [196, 181, 253],
|
|
30
|
+
label: [103, 232, 249],
|
|
31
|
+
value: [74, 222, 128],
|
|
32
|
+
text: [148, 163, 184],
|
|
33
|
+
frame: [71, 85, 105],
|
|
34
|
+
};
|
|
35
|
+
const logoGlyphs = [
|
|
36
|
+
['██╗ ', '██║ ', '██║ ', '██║ ', '███████╗', '╚══════╝'],
|
|
37
|
+
[
|
|
38
|
+
' ██████╗ ',
|
|
39
|
+
'██╔═══██╗',
|
|
40
|
+
'██║ ██║',
|
|
41
|
+
'██║ ██║',
|
|
42
|
+
'╚██████╔╝',
|
|
43
|
+
' ╚═════╝ ',
|
|
44
|
+
],
|
|
45
|
+
[
|
|
46
|
+
'██╗ ██╗',
|
|
47
|
+
'██║ ██║',
|
|
48
|
+
'██║ ██║',
|
|
49
|
+
'██║ ██║',
|
|
50
|
+
'╚██████╔╝',
|
|
51
|
+
' ╚═════╝ ',
|
|
52
|
+
],
|
|
53
|
+
[
|
|
54
|
+
'████████╗',
|
|
55
|
+
'╚══██╔══╝',
|
|
56
|
+
' ██║ ',
|
|
57
|
+
' ██║ ',
|
|
58
|
+
' ██║ ',
|
|
59
|
+
' ╚═╝ ',
|
|
60
|
+
],
|
|
61
|
+
['██████╗ ', '██╔══██╗', '██████╔╝', '██╔══██╗', '██║ ██║', '╚═╝ ╚═╝'],
|
|
62
|
+
['███████╗', '██╔════╝', '█████╗ ', '██╔══╝ ', '███████╗', '╚══════╝'],
|
|
63
|
+
];
|
|
64
|
+
const logoColors = [
|
|
65
|
+
colors.otterLight,
|
|
66
|
+
colors.otterSand,
|
|
67
|
+
colors.otterCaramel,
|
|
68
|
+
colors.otterBrown,
|
|
69
|
+
colors.otterDeep,
|
|
70
|
+
colors.otterCaramel,
|
|
71
|
+
];
|
|
72
|
+
const minimumContentWidth = 68;
|
|
73
|
+
const metadataIndent = ' ';
|
|
74
|
+
const mascot = 'ʕ•ᴥ•ʔ';
|
|
75
|
+
export function renderLoutreBrand(options) {
|
|
76
|
+
if (!options.isTTY)
|
|
77
|
+
return 'Loutre';
|
|
78
|
+
if (options.columns !== undefined && options.columns < logoWidth()) {
|
|
79
|
+
return `${mascot} Loutre`;
|
|
80
|
+
}
|
|
81
|
+
const logo = renderLogo(options.color);
|
|
82
|
+
const brand = options.color
|
|
83
|
+
? `${paint(colors.lavender, mascot)} ${paint(colors.lavenderSoft, 'Loutre')}`
|
|
84
|
+
: `${mascot} Loutre`;
|
|
85
|
+
return [...logo.styled, '', brand].join('\n');
|
|
86
|
+
}
|
|
87
|
+
export function renderStartupPrelude(info, options) {
|
|
88
|
+
if (!options.isTTY)
|
|
89
|
+
return renderCompactPrelude(info);
|
|
90
|
+
const layout = createStartupLayout(info);
|
|
91
|
+
if (options.columns !== undefined && options.columns < layout.frameWidth) {
|
|
92
|
+
return renderCompactPrelude(info);
|
|
93
|
+
}
|
|
94
|
+
return renderRichPrelude(info, layout.contentWidth, options.color);
|
|
95
|
+
}
|
|
96
|
+
export function renderStartupStatus(info, options) {
|
|
97
|
+
if (!options.isTTY)
|
|
98
|
+
return renderCompactStatus(info);
|
|
99
|
+
const layout = createStartupLayout(info);
|
|
100
|
+
if (options.columns !== undefined && options.columns < layout.frameWidth) {
|
|
101
|
+
return renderCompactStatus(info);
|
|
102
|
+
}
|
|
103
|
+
return renderRichStatus(info, layout.contentWidth, options.color);
|
|
104
|
+
}
|
|
105
|
+
function renderCompactPrelude(info) {
|
|
106
|
+
return `Loutre ${info.version}`;
|
|
107
|
+
}
|
|
108
|
+
function renderCompactStatus(info) {
|
|
109
|
+
return [
|
|
110
|
+
`Server: ${info.server}`,
|
|
111
|
+
`Runtime: ${info.runtime}`,
|
|
112
|
+
`Environment: ${info.environment}`,
|
|
113
|
+
`Ready in ${formatDuration(info.startupDurationMs)}`,
|
|
114
|
+
].join('\n');
|
|
115
|
+
}
|
|
116
|
+
function renderRichPrelude(info, contentWidth, color) {
|
|
117
|
+
const layout = createFrameLayout(contentWidth, color);
|
|
118
|
+
const logo = renderLogo(color);
|
|
119
|
+
const brand = `${mascot} version ${info.version}`;
|
|
120
|
+
const styledBrand = `${paintWhen(color, colors.lavender, mascot)} ${paintWhen(color, colors.lavenderSoft, `version ${info.version}`)}`;
|
|
121
|
+
return [
|
|
122
|
+
layout.topBorder,
|
|
123
|
+
layout.row(''),
|
|
124
|
+
...logo.plain.map((line, index) => layout.centered(line, logo.styled[index])),
|
|
125
|
+
layout.row(''),
|
|
126
|
+
layout.centered(brand, styledBrand),
|
|
127
|
+
layout.row(''),
|
|
128
|
+
].join('\n');
|
|
129
|
+
}
|
|
130
|
+
function renderRichStatus(info, contentWidth, color) {
|
|
131
|
+
const layout = createFrameLayout(contentWidth, color);
|
|
132
|
+
const labels = ['Server', 'Runtime', 'Environment'];
|
|
133
|
+
const labelWidth = Math.max(...labels.map(displayWidth));
|
|
134
|
+
const metadata = [
|
|
135
|
+
['Server', info.server],
|
|
136
|
+
['Runtime', info.runtime],
|
|
137
|
+
['Environment', info.environment],
|
|
138
|
+
];
|
|
139
|
+
return [
|
|
140
|
+
...metadata.map(([label, value]) => {
|
|
141
|
+
const gap = ' '.repeat(labelWidth - displayWidth(label) + 3);
|
|
142
|
+
const plain = `${metadataIndent}${label}${gap}${value}`;
|
|
143
|
+
const styled = `${metadataIndent}${paintWhen(color, colors.label, label)}${gap}${paintWhen(color, colors.value, value)}`;
|
|
144
|
+
return layout.row(plain, styled);
|
|
145
|
+
}),
|
|
146
|
+
layout.row(''),
|
|
147
|
+
layout.bottomBorder,
|
|
148
|
+
'',
|
|
149
|
+
` ${paintWhen(color, colors.value, '✓ Ready')} ${paintWhen(color, colors.text, `in ${formatDuration(info.startupDurationMs)}`)}`,
|
|
150
|
+
].join('\n');
|
|
151
|
+
}
|
|
152
|
+
function createStartupLayout(info) {
|
|
153
|
+
const labelWidth = displayWidth('Environment');
|
|
154
|
+
const metadata = ('server' in info ? [info.server, info.runtime, info.environment] : []).map((value) => displayWidth(metadataIndent) + labelWidth + 3 + displayWidth(value));
|
|
155
|
+
const contentWidth = Math.max(minimumContentWidth, logoWidth(), 'version' in info
|
|
156
|
+
? displayWidth(`${mascot} version ${info.version}`)
|
|
157
|
+
: minimumContentWidth, ...metadata);
|
|
158
|
+
return { contentWidth, frameWidth: contentWidth + 4 };
|
|
159
|
+
}
|
|
160
|
+
function createFrameLayout(contentWidth, color) {
|
|
161
|
+
const frame = (value) => paintWhen(color, colors.frame, value);
|
|
162
|
+
const row = (plain, styled = plain) => `${frame('│')} ${styled}${' '.repeat(contentWidth - displayWidth(plain))} ${frame('│')}`;
|
|
163
|
+
const centered = (plain, styled = plain) => {
|
|
164
|
+
const remaining = contentWidth - displayWidth(plain);
|
|
165
|
+
const left = Math.floor(remaining / 2);
|
|
166
|
+
return row(`${' '.repeat(left)}${plain}`, `${' '.repeat(left)}${styled}`);
|
|
167
|
+
};
|
|
168
|
+
const border = frame('─'.repeat(contentWidth + 2));
|
|
169
|
+
return {
|
|
170
|
+
topBorder: `${frame('╭')}${border}${frame('╮')}`,
|
|
171
|
+
bottomBorder: `${frame('╰')}${border}${frame('╯')}`,
|
|
172
|
+
row,
|
|
173
|
+
centered,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function renderLogo(color) {
|
|
177
|
+
const widths = logoGlyphs.map((glyph) => Math.max(...glyph.map(displayWidth)));
|
|
178
|
+
const plain = [];
|
|
179
|
+
const styled = [];
|
|
180
|
+
for (let row = 0; row < logoGlyphs[0].length; row += 1) {
|
|
181
|
+
const plainParts = [];
|
|
182
|
+
const styledParts = [];
|
|
183
|
+
for (let letter = 0; letter < logoGlyphs.length; letter += 1) {
|
|
184
|
+
const value = logoGlyphs[letter][row];
|
|
185
|
+
const padded = `${value}${' '.repeat(widths[letter] - displayWidth(value))}`;
|
|
186
|
+
const rgb = logoColors[letter];
|
|
187
|
+
plainParts.push(padded);
|
|
188
|
+
styledParts.push(paintWhen(color, rgb, padded));
|
|
189
|
+
}
|
|
190
|
+
plain.push(plainParts.join(' '));
|
|
191
|
+
styled.push(styledParts.join(' '));
|
|
192
|
+
}
|
|
193
|
+
return { plain, styled };
|
|
194
|
+
}
|
|
195
|
+
function logoWidth() {
|
|
196
|
+
const widths = logoGlyphs.map((glyph) => Math.max(...glyph.map(displayWidth)));
|
|
197
|
+
return widths.reduce((sum, width) => sum + width, logoGlyphs.length - 1);
|
|
198
|
+
}
|
|
199
|
+
function paintWhen(color, rgb, value) {
|
|
200
|
+
return color ? paint(rgb, value) : value;
|
|
201
|
+
}
|
|
202
|
+
function paint(rgb, value) {
|
|
203
|
+
return `\u001B[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m${value}\u001B[0m`;
|
|
204
|
+
}
|
|
205
|
+
function formatDuration(durationMs) {
|
|
206
|
+
return `${Math.max(0, Math.round(durationMs))} ms`;
|
|
207
|
+
}
|
|
208
|
+
function displayWidth(value) {
|
|
209
|
+
let width = 0;
|
|
210
|
+
const ansiControlSequence = new RegExp(`${String.fromCodePoint(0x1b)}\\[[0-?]*[ -/]*[@-~]`, 'g');
|
|
211
|
+
for (const character of value.replace(ansiControlSequence, '')) {
|
|
212
|
+
const codePoint = character.codePointAt(0);
|
|
213
|
+
if (/\p{Mark}/u.test(character))
|
|
214
|
+
continue;
|
|
215
|
+
width += isFullWidth(codePoint) ? 2 : 1;
|
|
216
|
+
}
|
|
217
|
+
return width;
|
|
218
|
+
}
|
|
219
|
+
function isFullWidth(codePoint) {
|
|
220
|
+
return (codePoint >= 0x1100 &&
|
|
221
|
+
(codePoint <= 0x115f ||
|
|
222
|
+
codePoint === 0x2329 ||
|
|
223
|
+
codePoint === 0x232a ||
|
|
224
|
+
(codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
|
|
225
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
226
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
227
|
+
(codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
|
|
228
|
+
(codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
|
|
229
|
+
(codePoint >= 0xff00 && codePoint <= 0xff60) ||
|
|
230
|
+
(codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
|
|
231
|
+
(codePoint >= 0x1b000 && codePoint <= 0x1ffff) ||
|
|
232
|
+
(codePoint >= 0x20000 && codePoint <= 0x3fffd)));
|
|
233
|
+
}
|
|
@@ -11,7 +11,7 @@ export function checkCapabilities(required, runtime) {
|
|
|
11
11
|
export class MissingCapabilitiesError extends Error {
|
|
12
12
|
check;
|
|
13
13
|
constructor(check) {
|
|
14
|
-
super(`Runtime
|
|
14
|
+
super(`Runtime is missing required capabilities: ${check.missing.join(', ')}`);
|
|
15
15
|
this.check = check;
|
|
16
16
|
this.name = 'MissingCapabilitiesError';
|
|
17
17
|
}
|
package/dist/runtime/di.js
CHANGED
|
@@ -222,7 +222,7 @@ export class Container {
|
|
|
222
222
|
const cycle = [...lineage.slice(cycleStart), token]
|
|
223
223
|
.map(tokenName)
|
|
224
224
|
.join(' -> ');
|
|
225
|
-
throw new DependencyResolutionError(`LUTRE_DI_CYCLE:
|
|
225
|
+
throw new DependencyResolutionError(`LUTRE_DI_CYCLE: Circular dependency detected: ${cycle}`);
|
|
226
226
|
}
|
|
227
227
|
const provider = this.#providers.get(token);
|
|
228
228
|
if (!provider) {
|
|
@@ -286,7 +286,7 @@ export class Container {
|
|
|
286
286
|
const selected = input[provider.select.key];
|
|
287
287
|
const implementation = provider.mapping[selected];
|
|
288
288
|
if (!implementation) {
|
|
289
|
-
throw new DependencyResolutionError(
|
|
289
|
+
throw new DependencyResolutionError(`No conditional Provider matches ${provider.select.key}=${String(selected)}`);
|
|
290
290
|
}
|
|
291
291
|
return this.#instantiate(implementation, lineage);
|
|
292
292
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export type RuntimeEngine = 'node' | 'bun' | 'deno' | 'cloudflare-workers' | 'aws-lambda' | 'electron' | 'unknown';
|
|
2
|
+
export declare function detectRuntimeEngine(): RuntimeEngine;
|
|
3
|
+
export declare function assertRuntimeEngine(expected: Exclude<RuntimeEngine, 'unknown'>): void;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const runtimeLabels = {
|
|
2
|
+
node: 'Node.js',
|
|
3
|
+
bun: 'Bun',
|
|
4
|
+
deno: 'Deno',
|
|
5
|
+
'cloudflare-workers': 'Cloudflare Workers',
|
|
6
|
+
'aws-lambda': 'AWS Lambda',
|
|
7
|
+
electron: 'Electron',
|
|
8
|
+
unknown: 'an unknown runtime',
|
|
9
|
+
};
|
|
10
|
+
export function detectRuntimeEngine() {
|
|
11
|
+
const globals = globalThis;
|
|
12
|
+
if (globals.Bun !== undefined ||
|
|
13
|
+
globals.process?.versions?.bun !== undefined) {
|
|
14
|
+
return 'bun';
|
|
15
|
+
}
|
|
16
|
+
if (globals.Deno !== undefined)
|
|
17
|
+
return 'deno';
|
|
18
|
+
if (globals.navigator?.userAgent === 'Cloudflare-Workers' ||
|
|
19
|
+
globals.WebSocketPair !== undefined) {
|
|
20
|
+
return 'cloudflare-workers';
|
|
21
|
+
}
|
|
22
|
+
if (globals.process?.versions?.electron !== undefined)
|
|
23
|
+
return 'electron';
|
|
24
|
+
if (globals.process?.env?.AWS_EXECUTION_ENV?.startsWith('AWS_Lambda_nodejs')) {
|
|
25
|
+
return 'aws-lambda';
|
|
26
|
+
}
|
|
27
|
+
if (globals.process?.release?.name === 'node' ||
|
|
28
|
+
globals.process?.versions?.node !== undefined) {
|
|
29
|
+
return 'node';
|
|
30
|
+
}
|
|
31
|
+
return 'unknown';
|
|
32
|
+
}
|
|
33
|
+
export function assertRuntimeEngine(expected) {
|
|
34
|
+
const actual = detectRuntimeEngine();
|
|
35
|
+
if (actual === expected)
|
|
36
|
+
return;
|
|
37
|
+
throw new Error(`LUTRE_RUNTIME_MISMATCH: Expected ${runtimeLabels[expected]}, detected ${runtimeLabels[actual]}.`);
|
|
38
|
+
}
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export * from './application.js';
|
|
|
2
2
|
export * from './capability.js';
|
|
3
3
|
export * from './di.js';
|
|
4
4
|
export * from './error.js';
|
|
5
|
+
export * from './engine.js';
|
|
5
6
|
export * from './logger.js';
|
|
6
7
|
export * from './pipeline.js';
|
|
7
8
|
export * from './node-capabilities.js';
|
|
9
|
+
export * from './server-url.js';
|
package/dist/runtime/index.js
CHANGED
|
@@ -2,6 +2,8 @@ export * from './application.js';
|
|
|
2
2
|
export * from './capability.js';
|
|
3
3
|
export * from './di.js';
|
|
4
4
|
export * from './error.js';
|
|
5
|
+
export * from './engine.js';
|
|
5
6
|
export * from './logger.js';
|
|
6
7
|
export * from './pipeline.js';
|
|
7
8
|
export * from './node-capabilities.js';
|
|
9
|
+
export * from './server-url.js';
|
package/dist/runtime/pipeline.js
CHANGED
|
@@ -9,7 +9,7 @@ export async function executePipeline(pipeline, hooks) {
|
|
|
9
9
|
const context = hooks.context;
|
|
10
10
|
const flow = await executeSegment(pipeline, 0, hooks, context, new Set());
|
|
11
11
|
if (flow.kind === 'continue') {
|
|
12
|
-
throw new Error('Pipeline
|
|
12
|
+
throw new Error('Pipeline did not produce a result');
|
|
13
13
|
}
|
|
14
14
|
return flow.result;
|
|
15
15
|
}
|
|
@@ -48,7 +48,7 @@ async function executeLayer(layer, runtime, continuation, context, availableKeys
|
|
|
48
48
|
const next = (...arguments_) => {
|
|
49
49
|
calls += 1;
|
|
50
50
|
if (calls > 1) {
|
|
51
|
-
contractError = new LayerContractError(`LUTRE_LAYER_NEXT_REENTRY: Layer ${layer.name}
|
|
51
|
+
contractError = new LayerContractError(`LUTRE_LAYER_NEXT_REENTRY: Layer ${layer.name} can call next() only once`);
|
|
52
52
|
return Promise.reject(contractError);
|
|
53
53
|
}
|
|
54
54
|
applyProvidedContext(layer, context, availableKeys, arguments_);
|
|
@@ -90,7 +90,7 @@ async function executeLayer(layer, runtime, continuation, context, availableKeys
|
|
|
90
90
|
throw runtimeError;
|
|
91
91
|
if (isShortCircuit(runtimeResult)) {
|
|
92
92
|
if (calls > 0) {
|
|
93
|
-
throw new LayerContractError(`LUTRE_LAYER_SHORT_CIRCUIT_AFTER_NEXT: Layer ${layer.name}
|
|
93
|
+
throw new LayerContractError(`LUTRE_LAYER_SHORT_CIRCUIT_AFTER_NEXT: Layer ${layer.name} cannot shortCircuit after next()`);
|
|
94
94
|
}
|
|
95
95
|
return {
|
|
96
96
|
kind: 'complete',
|
|
@@ -98,28 +98,28 @@ async function executeLayer(layer, runtime, continuation, context, availableKeys
|
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
100
|
if (calls === 0) {
|
|
101
|
-
throw new LayerContractError(`LUTRE_LAYER_NEXT_SKIPPED: Layer ${layer.name}
|
|
101
|
+
throw new LayerContractError(`LUTRE_LAYER_NEXT_SKIPPED: Layer ${layer.name} must call next() exactly once`);
|
|
102
102
|
}
|
|
103
103
|
return continuationFlow;
|
|
104
104
|
}
|
|
105
105
|
function assertRequiredContext(layerName, requiredKeys, availableKeys) {
|
|
106
106
|
for (const required of requiredKeys) {
|
|
107
107
|
if (!availableKeys.has(required)) {
|
|
108
|
-
throw new LayerContractError(
|
|
108
|
+
throw new LayerContractError(`Context Key ${contextKeyName(required)} required by ${layerName} is unavailable`);
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
function applyProvidedContext(layer, context, availableKeys, arguments_) {
|
|
113
113
|
if (arguments_.length > 1) {
|
|
114
|
-
throw new LayerContractError(
|
|
114
|
+
throw new LayerContractError(`Layer ${layer.name} can pass only one Context object to next()`);
|
|
115
115
|
}
|
|
116
116
|
if (layer.provides.length === 0) {
|
|
117
117
|
if (arguments_.length > 0) {
|
|
118
118
|
const provided = arguments_[0];
|
|
119
119
|
const property = firstProperty(provided);
|
|
120
120
|
throw new LayerContractError(property === undefined
|
|
121
|
-
?
|
|
122
|
-
:
|
|
121
|
+
? `Layer ${layer.name} does not declare any provided Context`
|
|
122
|
+
: `Layer ${layer.name} provided undeclared Context property ${property}`);
|
|
123
123
|
}
|
|
124
124
|
return;
|
|
125
125
|
}
|
|
@@ -127,26 +127,26 @@ function applyProvidedContext(layer, context, availableKeys, arguments_) {
|
|
|
127
127
|
if (typeof provided !== 'object' ||
|
|
128
128
|
provided === null ||
|
|
129
129
|
Array.isArray(provided)) {
|
|
130
|
-
throw new LayerContractError(
|
|
130
|
+
throw new LayerContractError(`Layer ${layer.name} must pass its declared Context to next(object)`);
|
|
131
131
|
}
|
|
132
132
|
const additions = provided;
|
|
133
133
|
const declaredNames = new Set(layer.provides.map((key) => key.name));
|
|
134
134
|
for (const property of Object.keys(additions)) {
|
|
135
135
|
if (!declaredNames.has(property)) {
|
|
136
|
-
throw new LayerContractError(
|
|
136
|
+
throw new LayerContractError(`Layer ${layer.name} provided undeclared Context property ${property}`);
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
const providedNames = new Set();
|
|
140
140
|
for (const key of layer.provides) {
|
|
141
141
|
if (providedNames.has(key.name)) {
|
|
142
|
-
throw new LayerContractError(
|
|
142
|
+
throw new LayerContractError(`Layer ${layer.name} declared duplicate Context property ${key.name}`);
|
|
143
143
|
}
|
|
144
144
|
providedNames.add(key.name);
|
|
145
145
|
if (!Object.hasOwn(additions, key.name)) {
|
|
146
|
-
throw new LayerContractError(
|
|
146
|
+
throw new LayerContractError(`Layer ${layer.name} did not provide declared Context Key ${contextKeyName(key)}`);
|
|
147
147
|
}
|
|
148
148
|
if (Object.hasOwn(context, key.name)) {
|
|
149
|
-
throw new LayerContractError(
|
|
149
|
+
throw new LayerContractError(`Layer ${layer.name} cannot overwrite existing Context property ${key.name}`);
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
Object.assign(context, additions);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function serverUrl(hostname: string | undefined, port: number): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loutrejs/loutre",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
"types": "./dist/application/openapi.d.ts",
|
|
25
25
|
"import": "./dist/application/openapi.js"
|
|
26
26
|
},
|
|
27
|
+
"./presentation": {
|
|
28
|
+
"types": "./dist/presentation.d.ts",
|
|
29
|
+
"import": "./dist/presentation.js"
|
|
30
|
+
},
|
|
27
31
|
"./graph": {
|
|
28
32
|
"types": "./dist/graph/index.d.ts",
|
|
29
33
|
"import": "./dist/graph/index.js"
|