@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 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,
@@ -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: number;
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: {
@@ -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 : (bun.env ?? undefined),
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
- try {
37
- server = bun.serve({
38
- port: options.port,
39
- ...(options.hostname === undefined ? {} : { hostname: options.hostname }),
40
- fetch: createBunFetchDriver(http),
41
- });
42
- }
43
- catch (error) {
44
- await host.application.close().catch(() => undefined);
45
- throw error;
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
- return {
49
- application: host.application,
50
- async close(signal) {
51
- if (closed)
52
- return;
53
- closed = true;
54
- const errors = [];
55
- try {
56
- await server.stop(true);
57
- }
58
- catch (error) {
59
- errors.push(error);
60
- }
61
- try {
62
- await host.application.close(signal);
63
- }
64
- catch (error) {
65
- errors.push(error);
66
- }
67
- if (errors.length > 0)
68
- throw new AggregateError(errors, 'Bun runtime shutdown failed');
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) => {
@@ -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: number;
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: {
@@ -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
- try {
49
- server = deno.serve({
50
- port: options.port,
51
- ...(options.hostname === undefined
52
- ? {}
53
- : { hostname: options.hostname }),
54
- }, createDenoFetchDriver(http));
55
- }
56
- catch (error) {
57
- await host.application.close().catch(() => undefined);
58
- throw error;
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
- return {
62
- application: host.application,
63
- async close(signal) {
64
- if (closed)
65
- return;
66
- closed = true;
67
- const errors = [];
68
- try {
69
- await server.shutdown();
70
- }
71
- catch (error) {
72
- errors.push(error);
73
- }
74
- try {
75
- await host.application.close(signal);
76
- }
77
- catch (error) {
78
- errors.push(error);
79
- }
80
- if (errors.length > 0)
81
- throw new AggregateError(errors, 'Deno runtime shutdown failed');
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";
@@ -0,0 +1,2 @@
1
+ // package manifestとのずれを避けるため、scripts/generate-version.mjsがbuild前に更新する。
2
+ export const LOUTRE_VERSION = '0.2.0';
@@ -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}が異なるtoken declarationで重複しています`,
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)}が${existing.module}と${moduleName}で重複しています`,
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}が異なるtoken declarationで重複しています`,
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}にはvalidation済みの${required}が必要ですが、validate.${required}より前に配置されています`,
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: `${item.name}が必要とするContext Key ${contextKeyName(required)}は利用できません`,
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}は既存のContext Key ${contextKeyName(provided)}を暗黙に上書きできません`,
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: `${item.name}のshort circuit variant ${shortCircuit.variant}がresponseに宣言されていません`,
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: `${item.name}のshort circuit variant ${shortCircuit.variant}はHTTP ${expectedStatus}である必要があります`,
1008
+ message: `Short-circuit variant ${shortCircuit.variant} from ${item.name} must use HTTP ${expectedStatus}`,
1009
1009
  path,
1010
1010
  });
1011
1011
  }
@@ -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('server-stream responseにはAsyncIterableが必要です');
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('未宣言のHTTP response headerが返されました');
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はbodyを返す必要があります');
461
+ throw new Error('HTTP error mapping must return a body');
462
462
  }
463
463
  return {
464
464
  kind: 'http-result',
@@ -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認証のrealmには空文字列または制御文字を使用できません');
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('credentialsを有効にする場合、CORS originにワイルドカードは使用できません');
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}に不正なHTTP tokenが含まれています`);
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には0以上の安全な整数を指定してください');
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がありません: ${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(`未宣言variantです: ${result.variant}`);
109
+ throw new Error(`Undeclared variant: ${result.variant}`);
110
110
  if (response.stream === 'server') {
111
111
  if (!isAsyncIterable(result.value)) {
112
- throw new Error('server-stream resultにはAsyncIterableが必要です');
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に必要なCapabilityがありません: ${check.missing.join(', ')}`);
14
+ super(`Runtime is missing required capabilities: ${check.missing.join(', ')}`);
15
15
  this.check = check;
16
16
  this.name = 'MissingCapabilitiesError';
17
17
  }
@@ -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: 循環依存を検出しました: ${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(`${provider.select.key}=${String(selected)}に対応するconditional Providerがありません`);
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
+ }
@@ -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';
@@ -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';
@@ -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がresultを生成しませんでした');
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}のnext()は1回だけ実行できます`);
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}はnext()後にshortCircuitできません`);
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}はnext()を1回実行する必要があります`);
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(`${layerName}が必要とするContext Key ${contextKeyName(required)}は利用できません`);
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(`${layer.name}のnext()へ渡せるContext objectは1つだけです`);
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
- ? `${layer.name}はContextをprovideすると宣言していません`
122
- : `${layer.name}が未宣言のContext property ${property}をprovideしました`);
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(`${layer.name}はprovidesで宣言したContextをnext(object)で渡す必要があります`);
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(`${layer.name}が未宣言のContext property ${property}をprovideしました`);
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(`${layer.name}がContext property ${key.name}を重複して宣言しました`);
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(`${layer.name}が宣言したContext Key ${contextKeyName(key)}をprovideしませんでした`);
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(`${layer.name}は既存のContext property ${key.name}を上書きできません`);
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;
@@ -0,0 +1,5 @@
1
+ export function serverUrl(hostname, port) {
2
+ const host = hostname ?? 'localhost';
3
+ const formattedHost = host.includes(':') ? `[${host}]` : host;
4
+ return `http://${formattedHost}:${port}`;
5
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loutrejs/loutre",
3
- "version": "0.1.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"