@nooh-ts/compiler 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -86,18 +86,29 @@ const isPathInside = (file, root) => {
86
86
  const generateAppModule = (plan, model) => {
87
87
  const moduleId = `${plan.outputRoot}/app.ts`;
88
88
  const typesModuleId = `${plan.outputRoot}/types.ts`;
89
+ const errorModuleId = `${plan.outputRoot}/error.ts`;
89
90
  const root = model.groups.find((group) => group.id === "root");
90
91
  if (!root) throw new Error("Nooh compilation requires a root route group.");
91
92
  return {
92
93
  code: [
93
94
  ...[
94
95
  `import { Hono } from "hono";`,
96
+ `import config from ${JSON.stringify(relativeModuleSpecifier(moduleId, model.config.source))};`,
95
97
  `import type { App } from ${JSON.stringify(relativeModuleSpecifier(moduleId, typesModuleId))};`,
98
+ `import { defaultErrorHandler } from ${JSON.stringify(relativeModuleSpecifier(moduleId, errorModuleId))};`,
96
99
  `import root from ${JSON.stringify(relativeModuleSpecifier(moduleId, groupModuleId(plan, root.id)))};`
97
100
  ],
98
101
  "",
99
102
  "const app = new Hono<App>();",
100
103
  "",
104
+ "if (config.onError !== undefined) {",
105
+ " app.onError(",
106
+ " config.onError as Parameters<typeof app.onError>[0],",
107
+ " );",
108
+ "} else {",
109
+ " app.onError(defaultErrorHandler);",
110
+ "}",
111
+ "",
101
112
  "app.route(\"/\", root);",
102
113
  "",
103
114
  "export type AppType = typeof app;",
@@ -110,6 +121,175 @@ const generateAppModule = (plan, model) => {
110
121
  };
111
122
  };
112
123
  //#endregion
124
+ //#region src/generate/dependency.ts
125
+ const generateDependencyModule = (plan) => {
126
+ const moduleId = `${plan.outputRoot}/router/di.ts`;
127
+ return {
128
+ code: [
129
+ "import type {",
130
+ " DependencyResolutionContext,",
131
+ "} from \"@nooh-ts/nooh\";",
132
+ "",
133
+ "const requestContexts =",
134
+ " new WeakMap<object, DependencyResolutionContext>();",
135
+ "",
136
+ "export const createDependencyResolutionContext = (): DependencyResolutionContext => ({",
137
+ " cache: new Map<object, unknown>(),",
138
+ " resolving: new Set<object>(),",
139
+ " stack: [],",
140
+ "});",
141
+ "",
142
+ "export const getDependencyResolutionContext = (",
143
+ " request: object,",
144
+ "): DependencyResolutionContext => {",
145
+ " const existing = requestContexts.get(request);",
146
+ "",
147
+ " if (existing) {",
148
+ " return existing;",
149
+ " }",
150
+ "",
151
+ " const created = createDependencyResolutionContext();",
152
+ "",
153
+ " requestContexts.set(request, created);",
154
+ "",
155
+ " return created;",
156
+ "};",
157
+ ""
158
+ ].join("\n"),
159
+ id: moduleId,
160
+ kind: "di"
161
+ };
162
+ };
163
+ //#endregion
164
+ //#region src/generate/error.ts
165
+ const generateErrorModule = (plan) => {
166
+ const moduleId = `${plan.outputRoot}/error.ts`;
167
+ const typesModuleId = `${plan.outputRoot}/types.ts`;
168
+ return {
169
+ code: [
170
+ `import { HTTPException } from "hono/http-exception";`,
171
+ `import type { ErrorHandler } from "hono";`,
172
+ `import type { App } from ${JSON.stringify(relativeModuleSpecifier(moduleId, typesModuleId))};`,
173
+ `import type { NoohStandardSchema } from "@nooh-ts/nooh";`,
174
+ "",
175
+ "const isRecord = (",
176
+ " value: unknown,",
177
+ "): value is Record<PropertyKey, unknown> =>",
178
+ " typeof value === \"object\" && value !== null;",
179
+ "",
180
+ "export class ResponseValidationError extends Error {",
181
+ " readonly route: string;",
182
+ " readonly issues: readonly unknown[];",
183
+ "",
184
+ " constructor(",
185
+ " route: string,",
186
+ " issues: readonly unknown[],",
187
+ " ) {",
188
+ " super(`Response validation failed for ${route}.`);",
189
+ " this.name = \"ResponseValidationError\";",
190
+ " this.route = route;",
191
+ " this.issues = issues;",
192
+ " }",
193
+ "}",
194
+ "",
195
+ "const extractResponseValue = async (",
196
+ " response: Response,",
197
+ " route: string,",
198
+ "): Promise<unknown> => {",
199
+ " if (",
200
+ " response.status === 204 ||",
201
+ " response.status === 205 ||",
202
+ " response.status === 304 ||",
203
+ " response.body === null",
204
+ " ) {",
205
+ " return undefined;",
206
+ " }",
207
+ "",
208
+ " const contentType =",
209
+ " response.headers.get(\"content-type\")?.split(\";\", 1)[0]?.trim().toLowerCase() ??",
210
+ " \"\";",
211
+ "",
212
+ " let clone: Response;",
213
+ "",
214
+ " try {",
215
+ " clone = response.clone();",
216
+ " } catch {",
217
+ " throw new ResponseValidationError(route, [",
218
+ " {",
219
+ " message: \"The response body could not be cloned for validation.\",",
220
+ " },",
221
+ " ]);",
222
+ " }",
223
+ "",
224
+ " if (",
225
+ " contentType === \"application/json\" ||",
226
+ " contentType.endsWith(\"+json\")",
227
+ " ) {",
228
+ " try {",
229
+ " return await clone.json();",
230
+ " } catch {",
231
+ " throw new ResponseValidationError(route, [",
232
+ " {",
233
+ " message: \"The response body is not valid JSON.\",",
234
+ " },",
235
+ " ]);",
236
+ " }",
237
+ " }",
238
+ "",
239
+ " if (contentType.startsWith(\"text/\")) {",
240
+ " return clone.text();",
241
+ " }",
242
+ "",
243
+ " throw new ResponseValidationError(route, [",
244
+ " {",
245
+ " message:",
246
+ " `Response validation only supports JSON and text responses. ` +",
247
+ " `Received \"${contentType || \"unknown\"}\".`,",
248
+ " },",
249
+ " ]);",
250
+ "};",
251
+ "",
252
+ "export const validateResponse = async (",
253
+ " response: Response,",
254
+ " schema: NoohStandardSchema,",
255
+ " route: string,",
256
+ "): Promise<Response> => {",
257
+ " const value = await extractResponseValue(response, route);",
258
+ "",
259
+ " const validate = schema[\"~standard\"]",
260
+ " .validate as (value: unknown) =>",
261
+ " | unknown",
262
+ " | Promise<unknown>;",
263
+ "",
264
+ " const result = await validate(value);",
265
+ "",
266
+ " if (",
267
+ " isRecord(result) &&",
268
+ " \"issues\" in result &&",
269
+ " Array.isArray(result.issues)",
270
+ " ) {",
271
+ " throw new ResponseValidationError(route, result.issues);",
272
+ " }",
273
+ "",
274
+ " return response;",
275
+ "};",
276
+ "",
277
+ "export const defaultErrorHandler: ErrorHandler<App> = (error, c) => {",
278
+ " if (error instanceof HTTPException) {",
279
+ " return error.getResponse();",
280
+ " }",
281
+ "",
282
+ " console.error(error);",
283
+ "",
284
+ " return c.json({ error: \"Internal Server Error\" }, 500);",
285
+ "};",
286
+ ""
287
+ ].join("\n"),
288
+ id: moduleId,
289
+ kind: "error"
290
+ };
291
+ };
292
+ //#endregion
113
293
  //#region src/generate/group.ts
114
294
  const getGroupRoutes = (model, group) => {
115
295
  const ids = new Set(group.routes);
@@ -162,10 +342,32 @@ const generateGroupModule = (plan, model, group) => {
162
342
  "",
163
343
  "use(\"*\", ...(groupConfig.middleware ?? []));"
164
344
  ] : [];
165
- const registrations = routes.map((route, index) => {
166
- return ` ${`register${capitalize(route.method)}`}(${JSON.stringify(ensureLeadingSlash(route.localPath))}, ...endpoint${index});`;
345
+ const routeRegistrations = routes.map((route, index) => {
346
+ const register = `register${capitalize(route.method)}`;
347
+ const path = JSON.stringify(ensureLeadingSlash(route.localPath));
348
+ const endpointApp = `endpointApp${index}`;
349
+ const endpointRegister = `registerEndpoint${index}`;
350
+ return [
351
+ ` if (endpoint${index}.onError === undefined) {`,
352
+ ` ${register}(${path}, ...endpoint${index});`,
353
+ " } else {",
354
+ ` const ${endpointApp} = new Hono<App>();`,
355
+ ` ${endpointApp}.onError(`,
356
+ ` endpoint${index}.onError as Parameters<typeof ${endpointApp}.onError>[0],`,
357
+ " );",
358
+ ` const ${endpointRegister} = ${endpointApp}.${route.method} as unknown as RouteRegister;`,
359
+ ` ${endpointRegister}(${path}, ...endpoint${index});`,
360
+ ` route.route(${path}, ${endpointApp});`,
361
+ " }"
362
+ ].join("\n");
167
363
  });
168
364
  const childRegistrations = children.map((child, index) => ` route.route(${JSON.stringify(getChildPath(group, child))}, child${index});`);
365
+ const groupErrorHandler = group.configSource ? [
366
+ "",
367
+ "if (groupConfig.onError !== undefined) {",
368
+ " route.onError(groupConfig.onError);",
369
+ "}"
370
+ ] : [];
169
371
  return {
170
372
  code: [
171
373
  ...imports,
@@ -176,7 +378,8 @@ const generateGroupModule = (plan, model, group) => {
176
378
  "",
177
379
  ...registerDeclarations,
178
380
  ...useDeclaration.length > 0 ? ["", ...useDeclaration] : [],
179
- ...registrations.length > 0 ? ["", ...registrations] : [],
381
+ ...groupErrorHandler,
382
+ ...routeRegistrations.length > 0 ? ["", ...routeRegistrations] : [],
180
383
  ...childRegistrations.length > 0 ? ["", ...childRegistrations] : [],
181
384
  "",
182
385
  "export default route;",
@@ -204,14 +407,6 @@ const generateMiddlewareModule = (plan) => {
204
407
  };
205
408
  //#endregion
206
409
  //#region src/generate/router.ts
207
- const VALIDATION_TARGETS = [
208
- "json",
209
- "form",
210
- "query",
211
- "param",
212
- "header",
213
- "cookie"
214
- ];
215
410
  const METHOD_FUNCTION_NAMES = {
216
411
  all: "all",
217
412
  delete: "del",
@@ -223,96 +418,316 @@ const METHOD_FUNCTION_NAMES = {
223
418
  put: "put"
224
419
  };
225
420
  const getRoutesForRouter = (model, routerPath) => model.routes.filter((route) => route.routerPath === routerPath).sort((a, b) => a.method.localeCompare(b.method));
226
- const renderMethod = (route) => {
227
- const { method } = route;
228
- const functionName = METHOD_FUNCTION_NAMES[method];
229
- return [
230
- `type Path = ${JSON.stringify(ensureLeadingSlash(route.localPath))};`,
421
+ const getRouteDependencyModel = (model, routeId) => model.routeDependencies.find((route) => route.routeId === routeId);
422
+ const getDependencyNames = (model, route) => {
423
+ const dependencyModel = getRouteDependencyModel(model, route.id);
424
+ if (!dependencyModel) return [];
425
+ return dependencyModel.roots.map((dependencyId) => {
426
+ const node = model.dependencies.nodes.get(dependencyId);
427
+ if (!node) throw new Error([
428
+ "Nooh internal error:",
429
+ `dependency "${dependencyId}"`,
430
+ `for route "${route.id}" is missing`,
431
+ "from the dependency graph."
432
+ ].join(" "));
433
+ return node.declaration.name;
434
+ });
435
+ };
436
+ const renderMethod = (model, route, mode) => {
437
+ const functionName = METHOD_FUNCTION_NAMES[route.method];
438
+ const prefix = functionName.charAt(0).toUpperCase() + functionName.slice(1);
439
+ const path = JSON.stringify(ensureLeadingSlash(route.localPath));
440
+ const routeLabel = JSON.stringify(`${route.method.toUpperCase()} ${ensureLeadingSlash(route.localPath)}`);
441
+ const dependencyNames = getDependencyNames(model, route);
442
+ const common = [
443
+ `type ${prefix}Path = ${path};`,
231
444
  "",
232
- "type RouteMiddleware = MiddlewareHandler<App, Path>;",
445
+ `type ${prefix}RouteMiddleware = MiddlewareHandler<App, ${prefix}Path>;`,
233
446
  "",
234
- "type ValidationTarget = Parameters<typeof sValidator>[0];",
235
- "type StandardSchema = Parameters<typeof sValidator>[1];",
447
+ `type ${prefix}RouteHandler = Handler<App, ${prefix}Path, any, any>;`,
236
448
  "",
237
- "type ValidationOptions = Partial<",
238
- " Record<ValidationTarget, StandardSchema>",
449
+ `type ${prefix}ValidationSchemaMap = Record<`,
450
+ " NoohRequestValidationTarget,",
451
+ " NoohStandardSchema",
239
452
  ">;",
240
453
  "",
241
- "type HandlerInput<T> = T extends Handler<",
242
- " any,",
243
- " any,",
244
- " infer I,",
245
- " any",
246
- "> ? I : never;",
454
+ `type ${prefix}ValidationOptions = Partial<${prefix}ValidationSchemaMap> & {`,
455
+ " readonly response?: NoohStandardSchema;",
456
+ "};",
247
457
  "",
248
- "type ValidationHandler<",
249
- " Target extends ValidationTarget,",
250
- " Schema extends StandardSchema,",
251
- "> = ReturnType<",
252
- " typeof sValidator<Schema, Target, App, Path>",
253
- ">;",
458
+ `type ${prefix}ValidationEntry<`,
459
+ " Target extends NoohRequestValidationTarget,",
460
+ " Schema extends NoohStandardSchema",
461
+ "> = undefined extends NoohStandardSchemaInput<Schema>",
462
+ " ? {",
463
+ " readonly in: {",
464
+ " readonly [Key in Target]?: NoohStandardSchemaInput<Schema>;",
465
+ " };",
466
+ " readonly out: {",
467
+ " readonly [Key in Target]: NoohStandardSchemaOutput<Schema>;",
468
+ " };",
469
+ " }",
470
+ " : {",
471
+ " readonly in: {",
472
+ " readonly [Key in Target]: NoohStandardSchemaInput<Schema>;",
473
+ " };",
474
+ " readonly out: {",
475
+ " readonly [Key in Target]: NoohStandardSchemaOutput<Schema>;",
476
+ " };",
477
+ " };",
254
478
  "",
255
- "type ValidationInput<V extends ValidationOptions> =",
256
- ...VALIDATION_TARGETS.map((target) => ` & (${JSON.stringify(target)} extends keyof V ? HandlerInput<ValidationHandler<${JSON.stringify(target)}, NonNullable<V[${JSON.stringify(target)}]>>> : {})`),
257
- ";",
479
+ `type ${prefix}ValidationInput<V extends ${prefix}ValidationOptions> =`,
480
+ " keyof V extends never",
481
+ " ? {}",
482
+ " : UnionToIntersection<{",
483
+ " [Target in keyof V & NoohRequestValidationTarget]:",
484
+ " V[Target] extends NoohStandardSchema",
485
+ " ? ",
486
+ ` ${prefix}ValidationEntry<Target, V[Target]>`,
487
+ " : never",
488
+ " }[keyof V &",
489
+ " NoohRequestValidationTarget]>;",
258
490
  "",
259
- "type RouteHandler = Handler<App, Path, any, any>;",
491
+ `type ${prefix}ResponseSchema<V extends ${prefix}ValidationOptions> =`,
492
+ " V extends {",
493
+ " readonly response: infer Schema extends NoohStandardSchema;",
494
+ " }",
495
+ " ? Schema",
496
+ " : never;",
497
+ "",
498
+ `type ${prefix}NoohHandlerReturn<V extends ${prefix}ValidationOptions> =`,
499
+ " V extends {",
500
+ " readonly response: infer Schema extends NoohStandardSchema;",
501
+ " }",
502
+ " ? Response &",
503
+ " TypedResponse<",
504
+ " NoohStandardSchemaInput<Schema>,",
505
+ " any,",
506
+ " any",
507
+ " >",
508
+ " | Promise<",
509
+ " Response &",
510
+ " TypedResponse<",
511
+ " NoohStandardSchemaInput<Schema>,",
512
+ " any,",
513
+ " any",
514
+ " >",
515
+ " >",
516
+ ` : ReturnType<${prefix}RouteHandler>;`,
260
517
  "",
261
- "type EndpointOptions<",
262
- " M extends readonly RouteMiddleware[],",
263
- " V extends ValidationOptions,",
264
- " H extends Handler<App, Path, ValidationInput<V>>,",
518
+ `type ${prefix}RouteContext<V extends ${prefix}ValidationOptions> =`,
519
+ " Context<",
520
+ " App,",
521
+ ` ${prefix}Path,`,
522
+ ` ${prefix}ValidationInput<V>`,
523
+ " >;",
524
+ "",
525
+ `type ${prefix}RouteNext = Parameters<${prefix}RouteHandler>[1];`,
526
+ "",
527
+ `type ${prefix}RouteErrorHandler = NoohErrorHandler<App, ${prefix}Path>;`,
528
+ "",
529
+ `type ${prefix}HandlerInput<`,
530
+ " D extends readonly RouteDependency[],",
531
+ ` V extends ${prefix}ValidationOptions,`,
532
+ " E extends ErrorDefinitions",
533
+ "> = {",
534
+ ` readonly c: ${prefix}RouteContext<V>;`,
535
+ ` readonly next: ${prefix}RouteNext;`,
536
+ "} & DependencyContext<D> & (",
537
+ " keyof E extends never",
538
+ " ? {}",
539
+ " : { readonly errors: ErrorContext<E> }",
540
+ ");",
541
+ "",
542
+ `type ${prefix}NoohHandler<`,
543
+ " D extends readonly RouteDependency[],",
544
+ ` V extends ${prefix}ValidationOptions,`,
545
+ " E extends ErrorDefinitions",
546
+ "> = (",
547
+ ` input: ${prefix}HandlerInput<D, V, E>`,
548
+ `) => ${prefix}NoohHandlerReturn<V>;`,
549
+ "",
550
+ `type ${prefix}RouteHandlers = readonly ${prefix}RouteHandler[] & {`,
551
+ ` readonly onError?: ${prefix}RouteErrorHandler;`,
552
+ "};",
553
+ "",
554
+ `type ${prefix}EndpointOptions<`,
555
+ " D extends readonly RouteDependency[] = readonly RouteDependency[],",
556
+ ` V extends ${prefix}ValidationOptions = ${prefix}ValidationOptions,`,
557
+ ` M extends readonly ${prefix}RouteMiddleware[] = readonly ${prefix}RouteMiddleware[],`,
558
+ " E extends ErrorDefinitions = {}",
265
559
  "> = {",
266
560
  " readonly middleware?: M;",
267
561
  " readonly validation?: V;",
268
- " readonly handler: H;",
562
+ " readonly deps?: D & ValidateDependencies<D, ReservedDependencyName>;",
563
+ " readonly errors?: E;",
564
+ ` readonly onError?: ${prefix}RouteErrorHandler;`,
565
+ ` readonly handler: ${prefix}NoohHandler<D, V, E>;`,
269
566
  "};",
270
567
  "",
271
- `export function ${functionName}<H extends Handler<App, Path>>(`,
272
- " handler: H,",
273
- "): readonly RouteHandler[];",
568
+ `export function ${functionName}(`,
569
+ ` handler: ${prefix}RouteHandler`,
570
+ `): ${prefix}RouteHandlers;`,
274
571
  "",
275
572
  `export function ${functionName}<`,
276
- " M extends readonly RouteMiddleware[],",
277
- " V extends ValidationOptions,",
278
- " H extends Handler<App, Path, ValidationInput<V>>,",
573
+ " const D extends readonly RouteDependency[] = [],",
574
+ ` const V extends ${prefix}ValidationOptions = {},`,
575
+ ` const M extends readonly ${prefix}RouteMiddleware[] = [],`,
576
+ " const E extends ErrorDefinitions = {}",
279
577
  ">(",
280
- " options: EndpointOptions<M, V, H>,",
281
- "): readonly RouteHandler[];",
578
+ ` options: ${prefix}EndpointOptions<D, V, M, E>`,
579
+ `): ${prefix}RouteHandlers;`,
282
580
  "",
283
- `export function ${functionName}<`,
284
- " M extends readonly RouteMiddleware[],",
285
- " V extends ValidationOptions,",
286
- " H extends Handler<App, Path, ValidationInput<V>>,",
287
- ">(",
581
+ `export function ${functionName}(`,
288
582
  " input:",
289
- " | H",
290
- " | EndpointOptions<M, V, H>,",
291
- "): readonly RouteHandler[] {",
583
+ ` | ${prefix}RouteHandler`,
584
+ ` | ${prefix}EndpointOptions`,
585
+ `): ${prefix}RouteHandlers {`
586
+ ];
587
+ if (mode === "introspection") return [
588
+ ...common,
589
+ "",
590
+ " if (typeof input === \"function\") {",
591
+ " return defineRouteMetadata(",
592
+ " [input],",
593
+ " [],",
594
+ ` ) as unknown as ${prefix}RouteHandlers;`,
595
+ " }",
596
+ "",
597
+ " return defineRouteMetadata(",
598
+ ` [input.handler as unknown as ${prefix}RouteHandler],`,
599
+ " input.deps ?? [],",
600
+ ` ) as unknown as ${prefix}RouteHandlers;`,
601
+ "}",
602
+ ""
603
+ ].join("\n");
604
+ const dependencyResolution = dependencyNames.flatMap((_, index) => [` const resolvedDependency${index} = dependencies[${index}]!.resolve(dependencyContext);`]);
605
+ const dependencyProperties = dependencyNames.map((name, index) => ` ${JSON.stringify(name)}: resolvedDependency${index},`);
606
+ return [
607
+ ...common,
608
+ "",
292
609
  " if (typeof input === \"function\") {",
293
- " return [input as RouteHandler];",
610
+ ` return defineRouteHandlers<${prefix}Path>([input]);`,
294
611
  " }",
295
612
  "",
296
- " return [",
297
- " ...(input.middleware ?? []) as readonly RouteHandler[],",
298
- ...VALIDATION_TARGETS.map((target) => ` ...(input.validation?.${target} !== undefined ? [sValidator(${JSON.stringify(target)}, input.validation.${target}) as RouteHandler] : []),`),
299
- " input.handler as RouteHandler,",
300
- " ];",
613
+ " const dependencies = input.deps ?? [];",
614
+ "",
615
+ " const errors =",
616
+ " input.errors === undefined",
617
+ " ? undefined",
618
+ " : createErrorContext(input.errors);",
619
+ "",
620
+ ` const handler: ${prefix}RouteHandler = async (c, next) => {`,
621
+ ...dependencyNames.length > 0 ? [
622
+ " const dependencyContext =",
623
+ " getDependencyResolutionContext(c);",
624
+ "",
625
+ ...dependencyResolution,
626
+ ""
627
+ ] : [],
628
+ " const response = await input.handler({",
629
+ " c: c as unknown as",
630
+ ` ${prefix}RouteContext<${prefix}ValidationOptions>,`,
631
+ " next,",
632
+ ...dependencyNames.length > 0 ? dependencyProperties : [],
633
+ " ...(errors !== undefined ? { errors } : {})",
634
+ " });",
635
+ "",
636
+ " if (input.validation?.response !== undefined) {",
637
+ " return validateResponse(",
638
+ " response as Response,",
639
+ " input.validation.response,",
640
+ ` ${routeLabel}`,
641
+ " );",
642
+ " }",
643
+ "",
644
+ " return response;",
645
+ " };",
646
+ "",
647
+ " const handlers = [",
648
+ ` ...((input.middleware ?? []) as readonly ${prefix}RouteHandler[]),`,
649
+ ...[
650
+ "json",
651
+ "form",
652
+ "query",
653
+ "param",
654
+ "header",
655
+ "cookie"
656
+ ].map((target) => ` ...(input.validation?.${target} !== undefined ? [validatorEngine(${JSON.stringify(target)}, input.validation.${target}) as ${prefix}RouteHandler] : []),`),
657
+ " handler,",
658
+ " ] as const;",
659
+ "",
660
+ ` return defineRouteHandlers<${prefix}Path>(handlers, input.onError);`,
301
661
  "}",
302
662
  ""
303
663
  ].join("\n");
304
664
  };
305
- const generateRouterModule = (plan, model, routerPath) => {
665
+ const COMMON_TYPES = [
666
+ "type RouteDependency = AnyDependencyReference;",
667
+ "",
668
+ "type ErrorDefinitions = Record<string, ErrorConstructor>;",
669
+ "",
670
+ "type UnionToIntersection<Union> =",
671
+ " (Union extends unknown",
672
+ " ? (value: Union) => void",
673
+ " : never) extends",
674
+ " (value: infer Intersection) => void",
675
+ " ? Intersection",
676
+ " : never;",
677
+ "",
678
+ "type ReservedDependencyName =",
679
+ " | \"c\"",
680
+ " | \"next\"",
681
+ " | \"error\"",
682
+ " | \"errors\"",
683
+ " | \"onError\"",
684
+ " | \"response\"",
685
+ " | NoohValidationTarget;"
686
+ ];
687
+ const generateRouterModule = (plan, model, routerPath, mode = "runtime") => {
306
688
  const routes = getRoutesForRouter(model, routerPath);
307
689
  const moduleId = `${plan.outputRoot}/${routerPath}.ts`;
308
690
  const typesModuleId = `${plan.outputRoot}/types.ts`;
691
+ const dependencyModuleId = `${plan.outputRoot}/router/di.ts`;
692
+ const errorModuleId = `${plan.outputRoot}/error.ts`;
693
+ const configModuleId = model.config.source;
694
+ const usesDependencies = mode === "runtime" && routes.some((route) => {
695
+ return !!getRouteDependencyModel(model, route.id)?.roots.length;
696
+ });
697
+ const imports = [
698
+ "import type {",
699
+ " Context,",
700
+ " Handler,",
701
+ " MiddlewareHandler,",
702
+ " TypedResponse",
703
+ "} from \"hono\";",
704
+ "import type {",
705
+ " AnyDependencyReference,",
706
+ " DependencyContext,",
707
+ " ErrorContext,",
708
+ " ErrorConstructor,",
709
+ " NoohErrorHandler,",
710
+ " NoohRequestValidationTarget,",
711
+ " NoohStandardSchema,",
712
+ " NoohStandardSchemaInput,",
713
+ " NoohStandardSchemaOutput,",
714
+ " NoohValidationTarget,",
715
+ " ValidateDependencies",
716
+ "} from \"@nooh-ts/nooh\";",
717
+ `import type { App } from ${JSON.stringify(relativeModuleSpecifier(moduleId, typesModuleId))};`
718
+ ];
719
+ if (mode === "runtime") imports.unshift(`import { sValidator } from "@hono/standard-validator";`, `import config from ${JSON.stringify(relativeModuleSpecifier(moduleId, configModuleId))};`, `import { validateResponse } from ${JSON.stringify(relativeModuleSpecifier(moduleId, errorModuleId))};`);
720
+ if (usesDependencies) imports.push("import {", " getDependencyResolutionContext", "} from " + JSON.stringify(relativeModuleSpecifier(moduleId, dependencyModuleId)) + ";");
721
+ const prelude = ["", ...COMMON_TYPES];
722
+ if (mode === "runtime") prelude.push("", "const validatorEngine = (", " config.validator?.engine ?? sValidator", ") as unknown as (", " target: NoohRequestValidationTarget,", " schema: NoohStandardSchema", ") => unknown;");
723
+ if (mode === "runtime") prelude.push("", "const createErrorContext = <", " const E extends ErrorDefinitions", ">(", " definitions: E | undefined", "): ErrorContext<E> => {", " const errors: Record<string, unknown> = {};", "", " if (definitions === undefined) {", " return errors as ErrorContext<E>;", " }", "", " for (const [name, Constructor] of Object.entries(definitions)) {", " errors[name] = (...args: unknown[]) =>", " Reflect.construct(Constructor, args);", " }", "", " return errors as ErrorContext<E>;", "};", "", "const defineRouteHandlers = <", " const P extends string,", " const T extends readonly Handler<App, P, any, any>[]", ">(", " handlers: T,", " onError?: NoohErrorHandler<App, P>", "): T & { readonly onError?: NoohErrorHandler<App, P> } => {", " if (onError !== undefined) {", " Object.defineProperty(handlers, \"onError\", {", " configurable: false,", " enumerable: false,", " value: onError,", " writable: false", " });", " }", "", " return handlers as T & {", " readonly onError?: NoohErrorHandler<App, P>", " };", "};");
724
+ if (mode === "introspection") prelude.push("", "const NOOH_ROUTE_METADATA = Symbol.for(\"nooh.route\");", "", "type NoohRouteMetadata = {", " readonly kind: \"route\";", " readonly dependencies: readonly RouteDependency[]", "};", "", "const defineRouteMetadata = <", " T extends readonly unknown[]", ">(", " handlers: T,", " dependencies: readonly RouteDependency[]", "): T => {", " const metadata: NoohRouteMetadata = {", " kind: \"route\",", " dependencies", "};", "", " Object.defineProperty(", " handlers,", " NOOH_ROUTE_METADATA,", " {", " value: metadata,", " enumerable: false", " }", " );", "", " return handlers;", "};");
309
725
  return {
310
726
  code: [
311
- `import { sValidator } from "@hono/standard-validator";`,
312
- `import type { Handler, MiddlewareHandler } from "hono";`,
313
- `import type { App } from ${JSON.stringify(relativeModuleSpecifier(moduleId, typesModuleId))};`,
727
+ ...imports,
728
+ ...prelude,
314
729
  "",
315
- ...routes.map(renderMethod)
730
+ ...routes.map((route) => renderMethod(model, route, mode))
316
731
  ].join("\n"),
317
732
  id: moduleId,
318
733
  kind: "router"
@@ -341,19 +756,405 @@ const generateTypesModule = (plan, config) => {
341
756
  };
342
757
  //#endregion
343
758
  //#region src/generate/index.ts
759
+ const getRouterPaths = (model) => [...new Set(model.routes.map((route) => route.routerPath))].sort();
344
760
  const generate = (plan, model) => {
345
761
  const modules = [];
346
762
  modules.push(generateTypesModule(plan, model.config));
763
+ modules.push(generateDependencyModule(plan));
764
+ modules.push(generateErrorModule(plan));
347
765
  modules.push(generateMiddlewareModule(plan));
348
- const routerPaths = [...new Set(model.routes.map((route) => route.routerPath))].sort();
349
- for (const routerPath of routerPaths) modules.push(generateRouterModule(plan, model, routerPath));
766
+ for (const routerPath of getRouterPaths(model)) modules.push(generateRouterModule(plan, model, routerPath));
350
767
  for (const group of model.groups) modules.push(generateGroupModule(plan, model, group));
351
768
  modules.push(generateAppModule(plan, model));
352
769
  modules.sort((a, b) => a.id.localeCompare(b.id));
353
770
  return { modules };
354
771
  };
772
+ const generateRouteIntrospection = (plan, model) => {
773
+ const modules = [];
774
+ for (const routerPath of getRouterPaths(model)) modules.push(generateRouterModule(plan, model, routerPath, "introspection"));
775
+ modules.sort((a, b) => a.id.localeCompare(b.id));
776
+ return { modules };
777
+ };
778
+ //#endregion
779
+ //#region src/pipeline/dependencies.ts
780
+ const isRecord$2 = (value) => typeof value === "object" && value !== null;
781
+ const isDependencyReference = (value) => isRecord$2(value) && value.__nooh_dependency === true && typeof value.name === "string" && Array.isArray(value.dependencies) && (value.scope === "value" || value.scope === "singleton" || value.scope === "request" || value.scope === "transient");
782
+ const isDependencyContainer = (value) => {
783
+ if (!isRecord$2(value) || Array.isArray(value)) return false;
784
+ if (!Object.isFrozen(value)) return false;
785
+ const entries = Object.entries(value);
786
+ return entries.length > 0 && entries.every(([, entry]) => isDependencyReference(entry));
787
+ };
788
+ const emptyGraph = () => ({
789
+ nodes: /* @__PURE__ */ new Map(),
790
+ order: [],
791
+ references: /* @__PURE__ */ new Map()
792
+ });
793
+ const canDependOn = (parent, child) => {
794
+ if (parent === "singleton") return child === "singleton" || child === "value";
795
+ if (parent === "request") return child === "singleton" || child === "request" || child === "transient" || child === "value";
796
+ if (parent === "transient") return true;
797
+ return child === "value";
798
+ };
799
+ const createScopeDiagnostic = (declaration, dependency) => {
800
+ if (canDependOn(declaration.scope, dependency.scope)) return null;
801
+ return {
802
+ code: "NOOH020",
803
+ file: declaration.source,
804
+ message: [`Dependency "${declaration.name}" with scope "${declaration.scope}"`, `cannot depend on "${dependency.name}" with scope "${dependency.scope}".`].join(" "),
805
+ severity: "error"
806
+ };
807
+ };
808
+ const createMissingDependencyDiagnostic = (declaration, dependencyId) => ({
809
+ code: "NOOH021",
810
+ file: declaration.source,
811
+ message: [`Dependency "${declaration.name}" references`, `unknown dependency "${dependencyId}".`].join(" "),
812
+ severity: "error"
813
+ });
814
+ const createDuplicateDependencyDiagnostic = (declaration) => ({
815
+ code: "NOOH022",
816
+ file: declaration.source,
817
+ message: `Duplicate dependency provider "${declaration.id}".`,
818
+ severity: "error"
819
+ });
820
+ const createCycleDiagnostic = (cycle) => {
821
+ const [first] = cycle;
822
+ return {
823
+ code: "NOOH023",
824
+ ...first?.declaration.source !== void 0 && { file: first.declaration.source },
825
+ message: `Circular dependency detected: ${cycle.map((node) => node.declaration.name).join(" -> ")}`,
826
+ severity: "error"
827
+ };
828
+ };
829
+ const topologicalOrder = (nodes, diagnostics) => {
830
+ const state = /* @__PURE__ */ new Map();
831
+ const stack = [];
832
+ const order = [];
833
+ const visit = (id) => {
834
+ const current = state.get(id);
835
+ if (current === "visited") return;
836
+ if (current === "visiting") {
837
+ const cycleStart = stack.indexOf(id);
838
+ const cycle = (cycleStart === -1 ? [...stack, id] : [...stack.slice(cycleStart), id]).map((cycleId) => nodes.get(cycleId)).filter((n) => n !== void 0);
839
+ diagnostics.push(createCycleDiagnostic(cycle));
840
+ return;
841
+ }
842
+ const node = nodes.get(id);
843
+ if (!node) return;
844
+ state.set(id, "visiting");
845
+ stack.push(id);
846
+ for (const dependencyId of node.declaration.dependencies) visit(dependencyId);
847
+ stack.pop();
848
+ state.set(id, "visited");
849
+ order.push(id);
850
+ };
851
+ const ids = [...nodes.keys()].sort();
852
+ for (const id of ids) visit(id);
853
+ return order;
854
+ };
855
+ const createDependencyGraph = (declarations, references = /* @__PURE__ */ new Map()) => {
856
+ if (declarations.length === 0) return {
857
+ diagnostics: [],
858
+ graph: {
859
+ ...emptyGraph(),
860
+ references
861
+ }
862
+ };
863
+ const diagnostics = [];
864
+ const nodes = /* @__PURE__ */ new Map();
865
+ for (const declaration of declarations) {
866
+ if (nodes.has(declaration.id)) {
867
+ diagnostics.push(createDuplicateDependencyDiagnostic(declaration));
868
+ continue;
869
+ }
870
+ nodes.set(declaration.id, { declaration });
871
+ }
872
+ for (const declaration of declarations) {
873
+ if (!nodes.get(declaration.id)) continue;
874
+ for (const dependencyId of declaration.dependencies) {
875
+ const dependencyNode = nodes.get(dependencyId);
876
+ if (!dependencyNode) {
877
+ diagnostics.push(createMissingDependencyDiagnostic(declaration, dependencyId));
878
+ continue;
879
+ }
880
+ const scopeDiagnostic = createScopeDiagnostic(declaration, dependencyNode.declaration);
881
+ if (scopeDiagnostic) diagnostics.push(scopeDiagnostic);
882
+ }
883
+ }
884
+ const order = topologicalOrder(nodes, diagnostics);
885
+ const unique = /* @__PURE__ */ new Map();
886
+ for (const diagnostic of diagnostics) {
887
+ const key = [
888
+ diagnostic.code,
889
+ diagnostic.file ?? "",
890
+ diagnostic.message
891
+ ].join("\0");
892
+ unique.set(key, diagnostic);
893
+ }
894
+ return {
895
+ diagnostics: [...unique.values()],
896
+ graph: {
897
+ nodes,
898
+ order,
899
+ references
900
+ }
901
+ };
902
+ };
903
+ const loadModule = async (loader, path) => {
904
+ if (loader.loadModule) return loader.loadModule(path);
905
+ return { default: await loader.loadDefault(path) };
906
+ };
907
+ const collectReferences = (source, module) => {
908
+ const diagnostics = [];
909
+ const references = [];
910
+ const seenObjects = /* @__PURE__ */ new Set();
911
+ const addReference = (id, value) => {
912
+ if (seenObjects.has(value)) {
913
+ diagnostics.push({
914
+ code: "NOOH027",
915
+ file: source,
916
+ message: [
917
+ `Dependency reference "${value.name}"`,
918
+ "is exported more than once.",
919
+ `Duplicate provider: "${id}".`
920
+ ].join(" "),
921
+ severity: "error"
922
+ });
923
+ return;
924
+ }
925
+ seenObjects.add(value);
926
+ references.push({
927
+ id,
928
+ name: value.name,
929
+ reference: value,
930
+ scope: value.scope,
931
+ source
932
+ });
933
+ };
934
+ for (const [exportName, exported] of Object.entries(module)) {
935
+ if (exportName === "__esModule") continue;
936
+ if (isDependencyReference(exported)) {
937
+ addReference(`${source}#${exportName}`, exported);
938
+ continue;
939
+ }
940
+ if (isDependencyContainer(exported)) for (const [name, reference] of Object.entries(exported)) {
941
+ if (!isDependencyReference(reference)) continue;
942
+ addReference(`${source}#${exportName}.${name}`, reference);
943
+ }
944
+ }
945
+ return {
946
+ diagnostics,
947
+ references
948
+ };
949
+ };
950
+ const loadDependencyGraph = async (sources, loader) => {
951
+ if (sources.length === 0) return {
952
+ diagnostics: [],
953
+ graph: emptyGraph()
954
+ };
955
+ const diagnostics = [];
956
+ const loaded = [];
957
+ for (const source of sources) {
958
+ let module;
959
+ try {
960
+ module = await loadModule(loader, source.path);
961
+ } catch (error) {
962
+ diagnostics.push({
963
+ code: "NOOH024",
964
+ file: source.path,
965
+ message: error instanceof Error ? ["Failed to load dependency module:", error.message].join(" ") : "Failed to load dependency module.",
966
+ severity: "error"
967
+ });
968
+ continue;
969
+ }
970
+ const result = collectReferences(source.path, module);
971
+ diagnostics.push(...result.diagnostics);
972
+ loaded.push(...result.references);
973
+ }
974
+ const references = /* @__PURE__ */ new Map();
975
+ for (const entry of loaded) references.set(entry.reference, entry.id);
976
+ const declarations = [];
977
+ for (const entry of loaded) {
978
+ const dependencies = [];
979
+ for (const dependency of entry.reference.dependencies) {
980
+ if (!isDependencyReference(dependency)) {
981
+ diagnostics.push({
982
+ code: "NOOH026",
983
+ file: entry.source,
984
+ message: [`Dependency "${entry.name}" contains`, "an invalid dependency reference."].join(" "),
985
+ severity: "error"
986
+ });
987
+ continue;
988
+ }
989
+ const dependencyId = references.get(dependency);
990
+ if (!dependencyId) {
991
+ diagnostics.push({
992
+ code: "NOOH021",
993
+ file: entry.source,
994
+ message: [`Dependency "${entry.name}" references`, `an unknown dependency "${dependency.name}".`].join(" "),
995
+ severity: "error"
996
+ });
997
+ continue;
998
+ }
999
+ dependencies.push(dependencyId);
1000
+ }
1001
+ declarations.push({
1002
+ dependencies,
1003
+ id: entry.id,
1004
+ name: entry.name,
1005
+ scope: entry.scope,
1006
+ source: entry.source
1007
+ });
1008
+ }
1009
+ const result = createDependencyGraph(declarations, references);
1010
+ return {
1011
+ diagnostics: [...diagnostics, ...result.diagnostics],
1012
+ graph: result.graph
1013
+ };
1014
+ };
1015
+ const dependencyClosure = (graph, roots) => {
1016
+ const visited = /* @__PURE__ */ new Set();
1017
+ const result = [];
1018
+ const visit = (id) => {
1019
+ if (visited.has(id)) return;
1020
+ visited.add(id);
1021
+ const node = graph.nodes.get(id);
1022
+ if (!node) return;
1023
+ for (const dependencyId of node.declaration.dependencies) visit(dependencyId);
1024
+ result.push(id);
1025
+ };
1026
+ for (const root of roots) visit(root);
1027
+ return result;
1028
+ };
1029
+ //#endregion
1030
+ //#region src/introspect.ts
1031
+ const NOOH_ROUTE_METADATA = Symbol.for("nooh.route");
1032
+ const isRecord$1 = (value) => typeof value === "object" && value !== null;
1033
+ const isRouteMetadata = (value) => {
1034
+ if (!isRecord$1(value)) return false;
1035
+ return value.kind === "route" && Array.isArray(value.dependencies);
1036
+ };
1037
+ const readRouteMetadata = (value) => {
1038
+ if (!isRecord$1(value)) return null;
1039
+ const metadata = value[NOOH_ROUTE_METADATA];
1040
+ return isRouteMetadata(metadata) ? metadata : null;
1041
+ };
1042
+ const introspectRoute = async (route, loader) => {
1043
+ let exported;
1044
+ try {
1045
+ exported = await loader.loadDefault(route.source);
1046
+ } catch (error) {
1047
+ return { diagnostics: [{
1048
+ code: "NOOH031",
1049
+ file: route.source,
1050
+ message: error instanceof Error ? `Failed to introspect route: ${error.message}` : "Failed to introspect route.",
1051
+ severity: "error"
1052
+ }] };
1053
+ }
1054
+ const metadata = readRouteMetadata(exported);
1055
+ if (!metadata) return { diagnostics: [{
1056
+ code: "NOOH032",
1057
+ file: route.source,
1058
+ message: [
1059
+ "The route did not expose Nooh route metadata.",
1060
+ "",
1061
+ "Make sure the route is evaluated against",
1062
+ "the generated Nooh introspection router."
1063
+ ].join("\n"),
1064
+ severity: "error"
1065
+ }] };
1066
+ return {
1067
+ diagnostics: [],
1068
+ metadata
1069
+ };
1070
+ };
1071
+ const unknownDependencyDiagnostic = (route, dependency) => ({
1072
+ code: "NOOH034",
1073
+ file: route.source,
1074
+ message: isRecord$1(dependency) && typeof dependency.name === "string" ? [`Route "${route.id}" references unknown dependency`, `"${dependency.name}".`].join(" ") : `Route "${route.id}" contains an invalid dependency reference.`,
1075
+ severity: "error"
1076
+ });
1077
+ const duplicateRouteDependencyNameDiagnostic = (route, name) => ({
1078
+ code: "NOOH035",
1079
+ file: route.source,
1080
+ message: [`Route "${route.id}" injects multiple dependencies`, `with the same name "${name}".`].join(" "),
1081
+ severity: "error"
1082
+ });
1083
+ const introspectCompilation = async (input) => {
1084
+ if (!input.compilation.plan) return {
1085
+ diagnostics: [{
1086
+ code: "NOOH033",
1087
+ message: "Cannot introspect a compilation without a compilation plan.",
1088
+ severity: "error"
1089
+ }],
1090
+ introspection: null
1091
+ };
1092
+ const graph = input.compilation.model.dependencies;
1093
+ const diagnostics = [];
1094
+ const routeDependencies = /* @__PURE__ */ new Map();
1095
+ for (const route of input.compilation.model.routes) {
1096
+ const result = await introspectRoute(route, input.loader);
1097
+ diagnostics.push(...result.diagnostics);
1098
+ if (!result.metadata) continue;
1099
+ const roots = [];
1100
+ const names = /* @__PURE__ */ new Set();
1101
+ for (const dependency of result.metadata.dependencies) {
1102
+ if (typeof dependency !== "object" || dependency === null) {
1103
+ diagnostics.push(unknownDependencyDiagnostic(route, dependency));
1104
+ continue;
1105
+ }
1106
+ const id = graph.references.get(dependency);
1107
+ if (!id) {
1108
+ diagnostics.push(unknownDependencyDiagnostic(route, dependency));
1109
+ continue;
1110
+ }
1111
+ const node = graph.nodes.get(id);
1112
+ if (!node) {
1113
+ diagnostics.push({
1114
+ code: "NOOH034",
1115
+ file: route.source,
1116
+ message: [`Route "${route.id}" references dependency`, `"${id}" that is missing from the dependency graph.`].join(" "),
1117
+ severity: "error"
1118
+ });
1119
+ continue;
1120
+ }
1121
+ const { name } = node.declaration;
1122
+ if (names.has(name)) {
1123
+ diagnostics.push(duplicateRouteDependencyNameDiagnostic(route, name));
1124
+ continue;
1125
+ }
1126
+ names.add(name);
1127
+ roots.push(id);
1128
+ }
1129
+ const closure = dependencyClosure(graph, roots);
1130
+ routeDependencies.set(route.id, {
1131
+ closure,
1132
+ roots
1133
+ });
1134
+ }
1135
+ if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) return {
1136
+ diagnostics,
1137
+ introspection: null
1138
+ };
1139
+ return {
1140
+ diagnostics,
1141
+ introspection: {
1142
+ dependencies: graph,
1143
+ routeDependencies: [...routeDependencies.entries()].map(([routeId, value]) => ({
1144
+ closure: value.closure,
1145
+ roots: value.roots,
1146
+ routeId
1147
+ })).sort((a, b) => a.routeId.localeCompare(b.routeId))
1148
+ }
1149
+ };
1150
+ };
355
1151
  //#endregion
356
1152
  //#region src/pipeline/analyze.ts
1153
+ const emptyDependencyGraph$1 = () => ({
1154
+ nodes: /* @__PURE__ */ new Map(),
1155
+ order: [],
1156
+ references: /* @__PURE__ */ new Map()
1157
+ });
357
1158
  const segmentToHono = (segment) => {
358
1159
  switch (segment.kind) {
359
1160
  case "static": return segment.value;
@@ -453,7 +1254,7 @@ const buildGroups = (parsed, routeModels, diagnostics) => {
453
1254
  };
454
1255
  });
455
1256
  };
456
- const analyze = (parsed, config) => {
1257
+ const analyze = (parsed, config, dependencies = emptyDependencyGraph$1()) => {
457
1258
  const diagnostics = [...parsed.diagnostics];
458
1259
  const routeModels = [];
459
1260
  const seen = /* @__PURE__ */ new Map();
@@ -493,16 +1294,20 @@ const analyze = (parsed, config) => {
493
1294
  diagnostics,
494
1295
  model: {
495
1296
  config,
1297
+ dependencies,
496
1298
  groups: buildGroups(parsed, routeModels, diagnostics),
1299
+ routeDependencies: [],
497
1300
  routes: routeModels
498
1301
  }
499
1302
  };
500
1303
  };
501
1304
  //#endregion
502
1305
  //#region src/pipeline/config.ts
1306
+ const DEFAULT_DEPENDENCIES_ROOT = "src/deps";
503
1307
  const DEFAULT_ROUTES_ROOT = "src/routes";
504
1308
  const isRecord = (value) => typeof value === "object" && value !== null;
505
1309
  const isString = (value) => typeof value === "string";
1310
+ const isFunction = (value) => typeof value === "function";
506
1311
  const loadConfig = async (input) => {
507
1312
  let defaultExport;
508
1313
  try {
@@ -528,14 +1333,41 @@ const loadConfig = async (input) => {
528
1333
  message: "The Nooh config \"routes\" option must be a string.",
529
1334
  severity: "error"
530
1335
  }] };
1336
+ const dependenciesValue = defaultExport.dependencies;
1337
+ if (dependenciesValue !== void 0 && !isString(dependenciesValue)) return { diagnostics: [{
1338
+ code: "NOOH013",
1339
+ file: input.config,
1340
+ message: "The Nooh config \"dependencies\" option must be a string.",
1341
+ severity: "error"
1342
+ }] };
1343
+ const validatorValue = defaultExport.validator;
1344
+ if (validatorValue !== void 0 && !isRecord(validatorValue)) return { diagnostics: [{
1345
+ code: "NOOH014",
1346
+ file: input.config,
1347
+ message: "The Nooh config \"validator\" option must be an object.",
1348
+ severity: "error"
1349
+ }] };
1350
+ if (isRecord(validatorValue) && validatorValue.engine !== void 0 && !isFunction(validatorValue.engine)) return { diagnostics: [{
1351
+ code: "NOOH015",
1352
+ file: input.config,
1353
+ message: "The Nooh config \"validator.engine\" option must be a function.",
1354
+ severity: "error"
1355
+ }] };
531
1356
  const root = normalizePath(input.root ?? "");
532
1357
  const source = toProjectPath(input.config, root);
1358
+ const routes = routesValue ?? DEFAULT_ROUTES_ROOT;
1359
+ const dependencies = dependenciesValue ?? DEFAULT_DEPENDENCIES_ROOT;
1360
+ const routesRoot = toProjectPath(routes, root);
533
1361
  return {
534
1362
  config: {
1363
+ dependenciesRoot: toProjectPath(dependencies, root),
535
1364
  root,
536
- routesRoot: toProjectPath(routesValue ?? DEFAULT_ROUTES_ROOT, root),
1365
+ routesRoot,
537
1366
  source,
538
- value: { routes: routesValue }
1367
+ value: {
1368
+ dependencies: dependenciesValue,
1369
+ routes: routesValue
1370
+ }
539
1371
  },
540
1372
  diagnostics: []
541
1373
  };
@@ -549,6 +1381,14 @@ const isGroupFile = (filePath) => {
549
1381
  const filename = basename(filePath);
550
1382
  return filename === "$.ts" || filename === "$.tsx";
551
1383
  };
1384
+ const discoverDependency = (config, file) => {
1385
+ const source = toProjectPath(file.path, config.root);
1386
+ if (!isPathInside(source, config.dependenciesRoot)) return null;
1387
+ return {
1388
+ content: file.content,
1389
+ path: source
1390
+ };
1391
+ };
552
1392
  const discoverGroup = (config, file) => {
553
1393
  const source = toProjectPath(file.path, config.root);
554
1394
  if (!isPathInside(source, config.routesRoot)) return null;
@@ -574,8 +1414,14 @@ const discoverEndpoint = (config, file) => {
574
1414
  const discover = (snapshot, config) => {
575
1415
  const endpoints = [];
576
1416
  const groups = [];
1417
+ const dependencies = [];
577
1418
  for (const file of snapshot.files) {
578
1419
  if (!isTypeScriptFile(file)) continue;
1420
+ const dependency = discoverDependency(config, file);
1421
+ if (dependency) {
1422
+ dependencies.push(dependency);
1423
+ continue;
1424
+ }
579
1425
  const group = discoverGroup(config, file);
580
1426
  if (group) {
581
1427
  groups.push(group);
@@ -584,10 +1430,12 @@ const discover = (snapshot, config) => {
584
1430
  const endpoint = discoverEndpoint(config, file);
585
1431
  if (endpoint) endpoints.push(endpoint);
586
1432
  }
1433
+ dependencies.sort((a, b) => a.path.localeCompare(b.path));
587
1434
  endpoints.sort((a, b) => a.source.localeCompare(b.source));
588
1435
  groups.sort((a, b) => a.source.localeCompare(b.source));
589
1436
  return {
590
1437
  config,
1438
+ dependencies,
591
1439
  endpoints,
592
1440
  groups
593
1441
  };
@@ -716,13 +1564,24 @@ const DEFAULT_OUTPUT_ROOT = ".nooh";
716
1564
  const modulePath = (outputRoot, value) => normalizePath(`${outputRoot}/${value}.ts`);
717
1565
  const plan = (model, outputRoot = DEFAULT_OUTPUT_ROOT) => {
718
1566
  const normalizedOutputRoot = normalizePath(outputRoot);
719
- const modules = [{
720
- id: modulePath(normalizedOutputRoot, "types"),
721
- kind: "types"
722
- }, {
723
- id: modulePath(normalizedOutputRoot, "router/middleware"),
724
- kind: "middleware"
725
- }];
1567
+ const modules = [
1568
+ {
1569
+ id: modulePath(normalizedOutputRoot, "types"),
1570
+ kind: "types"
1571
+ },
1572
+ {
1573
+ id: modulePath(normalizedOutputRoot, "router/di"),
1574
+ kind: "di"
1575
+ },
1576
+ {
1577
+ id: modulePath(normalizedOutputRoot, "router/middleware"),
1578
+ kind: "middleware"
1579
+ },
1580
+ {
1581
+ id: modulePath(normalizedOutputRoot, "error"),
1582
+ kind: "error"
1583
+ }
1584
+ ];
726
1585
  const routerPaths = [...new Set(model.routes.map((route) => route.routerPath))].sort();
727
1586
  for (const routerPath of routerPaths) {
728
1587
  const route = model.routes.find((candidate) => candidate.routerPath === routerPath);
@@ -747,12 +1606,18 @@ const plan = (model, outputRoot = DEFAULT_OUTPUT_ROOT) => {
747
1606
  });
748
1607
  modules.sort((a, b) => a.id.localeCompare(b.id));
749
1608
  return {
1609
+ dependencies: model.dependencies,
750
1610
  modules,
751
1611
  outputRoot: normalizedOutputRoot
752
1612
  };
753
1613
  };
754
1614
  //#endregion
755
1615
  //#region src/compiler.ts
1616
+ const emptyDependencyGraph = {
1617
+ nodes: /* @__PURE__ */ new Map(),
1618
+ order: [],
1619
+ references: /* @__PURE__ */ new Map()
1620
+ };
756
1621
  const createCompiler = () => ({
757
1622
  analyze,
758
1623
  compile: async (input) => {
@@ -761,12 +1626,15 @@ const createCompiler = () => ({
761
1626
  diagnostics: configResult.diagnostics,
762
1627
  model: {
763
1628
  config: {
1629
+ dependenciesRoot: "",
764
1630
  root: input.root ? input.root.replaceAll("\\", "/") : "",
765
1631
  routesRoot: "",
766
1632
  source: input.config,
767
1633
  value: {}
768
1634
  },
1635
+ dependencies: emptyDependencyGraph,
769
1636
  groups: [],
1637
+ routeDependencies: [],
770
1638
  routes: []
771
1639
  },
772
1640
  output: null,
@@ -774,8 +1642,13 @@ const createCompiler = () => ({
774
1642
  };
775
1643
  const discovered = discover(input.sources, configResult.config);
776
1644
  const parsed = parse(discovered);
777
- const analyzed = analyze(parsed, configResult.config);
778
- const diagnostics = [...configResult.diagnostics, ...analyzed.diagnostics];
1645
+ const dependencyResult = await loadDependencyGraph(discovered.dependencies, input.loader);
1646
+ const analyzed = analyze(parsed, configResult.config, dependencyResult.graph);
1647
+ const diagnostics = [
1648
+ ...configResult.diagnostics,
1649
+ ...dependencyResult.diagnostics,
1650
+ ...analyzed.diagnostics
1651
+ ];
779
1652
  if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) return {
780
1653
  diagnostics,
781
1654
  model: analyzed.model,
@@ -783,7 +1656,7 @@ const createCompiler = () => ({
783
1656
  plan: null
784
1657
  };
785
1658
  const compilationPlan = plan(analyzed.model, input.options?.outputRoot);
786
- const output = generate(compilationPlan, analyzed.model);
1659
+ const output = generateRouteIntrospection(compilationPlan, analyzed.model);
787
1660
  return {
788
1661
  diagnostics,
789
1662
  model: analyzed.model,
@@ -793,10 +1666,12 @@ const createCompiler = () => ({
793
1666
  },
794
1667
  discover,
795
1668
  generate,
1669
+ generateRouteIntrospection,
796
1670
  loadConfig,
797
1671
  parse,
798
1672
  plan
799
1673
  });
1674
+ const introspect = (input) => introspectCompilation(input);
800
1675
  //#endregion
801
1676
  //#region src/compile.ts
802
1677
  const compile = (input) => createCompiler().compile(input);
@@ -830,6 +1705,26 @@ const diff = (previous, next) => {
830
1705
  };
831
1706
  };
832
1707
  //#endregion
1708
+ //#region src/finalize.ts
1709
+ const finalizeCompilation = (compilation, introspection) => {
1710
+ if (!(compilation.plan && compilation.output)) return compilation;
1711
+ const model = {
1712
+ ...compilation.model,
1713
+ dependencies: introspection.dependencies,
1714
+ routeDependencies: introspection.routeDependencies
1715
+ };
1716
+ const plan = {
1717
+ ...compilation.plan,
1718
+ dependencies: introspection.dependencies,
1719
+ routeDependencies: introspection.routeDependencies
1720
+ };
1721
+ return {
1722
+ ...compilation,
1723
+ model,
1724
+ plan
1725
+ };
1726
+ };
1727
+ //#endregion
833
1728
  //#region src/recompile.ts
834
1729
  const recompile = (input) => {
835
1730
  const compiler = createCompiler();
@@ -843,6 +1738,6 @@ const recompile = (input) => {
843
1738
  });
844
1739
  };
845
1740
  //#endregion
846
- export { analyze, compile, createCompiler, diff, discover, generate, loadConfig, parse, plan, recompile };
1741
+ export { NOOH_ROUTE_METADATA, analyze, compile, createCompiler, createDependencyGraph, dependencyClosure, diff, discover, finalizeCompilation, generate, generateRouteIntrospection, introspect, introspectCompilation, introspectRoute, loadConfig, loadDependencyGraph, parse, plan, readRouteMetadata, recompile };
847
1742
 
848
1743
  //# sourceMappingURL=index.mjs.map