@nooh-ts/compiler 0.1.0 → 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
@@ -46,6 +46,12 @@ const dirname = (value) => {
46
46
  if (index === 0) return "/";
47
47
  return normalized.slice(0, index);
48
48
  };
49
+ const basename = (value) => {
50
+ const normalized = normalizePath(value);
51
+ const index = normalized.lastIndexOf("/");
52
+ if (index === -1) return normalized;
53
+ return normalized.slice(index + 1);
54
+ };
49
55
  const relativePath = (from, to) => {
50
56
  const fromParts = normalizePath(from).split("/").filter(Boolean);
51
57
  const toParts = normalizePath(to).split("/").filter(Boolean);
@@ -80,18 +86,29 @@ const isPathInside = (file, root) => {
80
86
  const generateAppModule = (plan, model) => {
81
87
  const moduleId = `${plan.outputRoot}/app.ts`;
82
88
  const typesModuleId = `${plan.outputRoot}/types.ts`;
89
+ const errorModuleId = `${plan.outputRoot}/error.ts`;
83
90
  const root = model.groups.find((group) => group.id === "root");
84
91
  if (!root) throw new Error("Nooh compilation requires a root route group.");
85
92
  return {
86
93
  code: [
87
94
  ...[
88
95
  `import { Hono } from "hono";`,
96
+ `import config from ${JSON.stringify(relativeModuleSpecifier(moduleId, model.config.source))};`,
89
97
  `import type { App } from ${JSON.stringify(relativeModuleSpecifier(moduleId, typesModuleId))};`,
98
+ `import { defaultErrorHandler } from ${JSON.stringify(relativeModuleSpecifier(moduleId, errorModuleId))};`,
90
99
  `import root from ${JSON.stringify(relativeModuleSpecifier(moduleId, groupModuleId(plan, root.id)))};`
91
100
  ],
92
101
  "",
93
102
  "const app = new Hono<App>();",
94
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
+ "",
95
112
  "app.route(\"/\", root);",
96
113
  "",
97
114
  "export type AppType = typeof app;",
@@ -104,6 +121,175 @@ const generateAppModule = (plan, model) => {
104
121
  };
105
122
  };
106
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
107
293
  //#region src/generate/group.ts
108
294
  const getGroupRoutes = (model, group) => {
109
295
  const ids = new Set(group.routes);
@@ -156,10 +342,32 @@ const generateGroupModule = (plan, model, group) => {
156
342
  "",
157
343
  "use(\"*\", ...(groupConfig.middleware ?? []));"
158
344
  ] : [];
159
- const registrations = routes.map((route, index) => {
160
- 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");
161
363
  });
162
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
+ ] : [];
163
371
  return {
164
372
  code: [
165
373
  ...imports,
@@ -170,7 +378,8 @@ const generateGroupModule = (plan, model, group) => {
170
378
  "",
171
379
  ...registerDeclarations,
172
380
  ...useDeclaration.length > 0 ? ["", ...useDeclaration] : [],
173
- ...registrations.length > 0 ? ["", ...registrations] : [],
381
+ ...groupErrorHandler,
382
+ ...routeRegistrations.length > 0 ? ["", ...routeRegistrations] : [],
174
383
  ...childRegistrations.length > 0 ? ["", ...childRegistrations] : [],
175
384
  "",
176
385
  "export default route;",
@@ -198,14 +407,6 @@ const generateMiddlewareModule = (plan) => {
198
407
  };
199
408
  //#endregion
200
409
  //#region src/generate/router.ts
201
- const VALIDATION_TARGETS = [
202
- "json",
203
- "form",
204
- "query",
205
- "param",
206
- "header",
207
- "cookie"
208
- ];
209
410
  const METHOD_FUNCTION_NAMES = {
210
411
  all: "all",
211
412
  delete: "del",
@@ -217,96 +418,316 @@ const METHOD_FUNCTION_NAMES = {
217
418
  put: "put"
218
419
  };
219
420
  const getRoutesForRouter = (model, routerPath) => model.routes.filter((route) => route.routerPath === routerPath).sort((a, b) => a.method.localeCompare(b.method));
220
- const renderMethod = (route) => {
221
- const { method } = route;
222
- const functionName = METHOD_FUNCTION_NAMES[method];
223
- return [
224
- `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};`,
225
444
  "",
226
- "type RouteMiddleware = MiddlewareHandler<App, Path>;",
445
+ `type ${prefix}RouteMiddleware = MiddlewareHandler<App, ${prefix}Path>;`,
227
446
  "",
228
- "type ValidationTarget = Parameters<typeof sValidator>[0];",
229
- "type StandardSchema = Parameters<typeof sValidator>[1];",
447
+ `type ${prefix}RouteHandler = Handler<App, ${prefix}Path, any, any>;`,
230
448
  "",
231
- "type ValidationOptions = Partial<",
232
- " Record<ValidationTarget, StandardSchema>",
449
+ `type ${prefix}ValidationSchemaMap = Record<`,
450
+ " NoohRequestValidationTarget,",
451
+ " NoohStandardSchema",
233
452
  ">;",
234
453
  "",
235
- "type HandlerInput<T> = T extends Handler<",
236
- " any,",
237
- " any,",
238
- " infer I,",
239
- " any",
240
- "> ? I : never;",
454
+ `type ${prefix}ValidationOptions = Partial<${prefix}ValidationSchemaMap> & {`,
455
+ " readonly response?: NoohStandardSchema;",
456
+ "};",
241
457
  "",
242
- "type ValidationHandler<",
243
- " Target extends ValidationTarget,",
244
- " Schema extends StandardSchema,",
245
- "> = ReturnType<",
246
- " typeof sValidator<Schema, Target, App, Path>",
247
- ">;",
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
+ " };",
248
478
  "",
249
- "type ValidationInput<V extends ValidationOptions> =",
250
- ...VALIDATION_TARGETS.map((target) => ` & (${JSON.stringify(target)} extends keyof V ? HandlerInput<ValidationHandler<${JSON.stringify(target)}, NonNullable<V[${JSON.stringify(target)}]>>> : {})`),
251
- ";",
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]>;",
252
490
  "",
253
- "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;",
254
497
  "",
255
- "type EndpointOptions<",
256
- " M extends readonly RouteMiddleware[],",
257
- " V extends ValidationOptions,",
258
- " H extends Handler<App, Path, ValidationInput<V>>,",
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>;`,
517
+ "",
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 = {}",
259
559
  "> = {",
260
560
  " readonly middleware?: M;",
261
561
  " readonly validation?: V;",
262
- " 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>;`,
263
566
  "};",
264
567
  "",
265
- `export function ${functionName}<H extends Handler<App, Path>>(`,
266
- " handler: H,",
267
- "): readonly RouteHandler[];",
568
+ `export function ${functionName}(`,
569
+ ` handler: ${prefix}RouteHandler`,
570
+ `): ${prefix}RouteHandlers;`,
268
571
  "",
269
572
  `export function ${functionName}<`,
270
- " M extends readonly RouteMiddleware[],",
271
- " V extends ValidationOptions,",
272
- " 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 = {}",
273
577
  ">(",
274
- " options: EndpointOptions<M, V, H>,",
275
- "): readonly RouteHandler[];",
578
+ ` options: ${prefix}EndpointOptions<D, V, M, E>`,
579
+ `): ${prefix}RouteHandlers;`,
276
580
  "",
277
- `export function ${functionName}<`,
278
- " M extends readonly RouteMiddleware[],",
279
- " V extends ValidationOptions,",
280
- " H extends Handler<App, Path, ValidationInput<V>>,",
281
- ">(",
581
+ `export function ${functionName}(`,
282
582
  " input:",
283
- " | H",
284
- " | EndpointOptions<M, V, H>,",
285
- "): readonly RouteHandler[] {",
583
+ ` | ${prefix}RouteHandler`,
584
+ ` | ${prefix}EndpointOptions`,
585
+ `): ${prefix}RouteHandlers {`
586
+ ];
587
+ if (mode === "introspection") return [
588
+ ...common,
589
+ "",
286
590
  " if (typeof input === \"function\") {",
287
- " return [input as RouteHandler];",
591
+ " return defineRouteMetadata(",
592
+ " [input],",
593
+ " [],",
594
+ ` ) as unknown as ${prefix}RouteHandlers;`,
288
595
  " }",
289
596
  "",
290
- " return [",
291
- " ...(input.middleware ?? []) as readonly RouteHandler[],",
292
- ...VALIDATION_TARGETS.map((target) => ` ...(input.validation?.${target} !== undefined ? [sValidator(${JSON.stringify(target)}, input.validation.${target}) as RouteHandler] : []),`),
293
- " input.handler as RouteHandler,",
294
- " ];",
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
+ "",
609
+ " if (typeof input === \"function\") {",
610
+ ` return defineRouteHandlers<${prefix}Path>([input]);`,
611
+ " }",
612
+ "",
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);`,
295
661
  "}",
296
662
  ""
297
663
  ].join("\n");
298
664
  };
299
- 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") => {
300
688
  const routes = getRoutesForRouter(model, routerPath);
301
689
  const moduleId = `${plan.outputRoot}/${routerPath}.ts`;
302
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;", "};");
303
725
  return {
304
726
  code: [
305
- `import { sValidator } from "@hono/standard-validator";`,
306
- `import type { Handler, MiddlewareHandler } from "hono";`,
307
- `import type { App } from ${JSON.stringify(relativeModuleSpecifier(moduleId, typesModuleId))};`,
727
+ ...imports,
728
+ ...prelude,
308
729
  "",
309
- ...routes.map(renderMethod)
730
+ ...routes.map((route) => renderMethod(model, route, mode))
310
731
  ].join("\n"),
311
732
  id: moduleId,
312
733
  kind: "router"
@@ -335,19 +756,405 @@ const generateTypesModule = (plan, config) => {
335
756
  };
336
757
  //#endregion
337
758
  //#region src/generate/index.ts
759
+ const getRouterPaths = (model) => [...new Set(model.routes.map((route) => route.routerPath))].sort();
338
760
  const generate = (plan, model) => {
339
761
  const modules = [];
340
762
  modules.push(generateTypesModule(plan, model.config));
763
+ modules.push(generateDependencyModule(plan));
764
+ modules.push(generateErrorModule(plan));
341
765
  modules.push(generateMiddlewareModule(plan));
342
- const routerPaths = [...new Set(model.routes.map((route) => route.routerPath))].sort();
343
- for (const routerPath of routerPaths) modules.push(generateRouterModule(plan, model, routerPath));
766
+ for (const routerPath of getRouterPaths(model)) modules.push(generateRouterModule(plan, model, routerPath));
344
767
  for (const group of model.groups) modules.push(generateGroupModule(plan, model, group));
345
768
  modules.push(generateAppModule(plan, model));
346
769
  modules.sort((a, b) => a.id.localeCompare(b.id));
347
770
  return { modules };
348
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
+ };
349
1151
  //#endregion
350
1152
  //#region src/pipeline/analyze.ts
1153
+ const emptyDependencyGraph$1 = () => ({
1154
+ nodes: /* @__PURE__ */ new Map(),
1155
+ order: [],
1156
+ references: /* @__PURE__ */ new Map()
1157
+ });
351
1158
  const segmentToHono = (segment) => {
352
1159
  switch (segment.kind) {
353
1160
  case "static": return segment.value;
@@ -447,7 +1254,7 @@ const buildGroups = (parsed, routeModels, diagnostics) => {
447
1254
  };
448
1255
  });
449
1256
  };
450
- const analyze = (parsed, config) => {
1257
+ const analyze = (parsed, config, dependencies = emptyDependencyGraph$1()) => {
451
1258
  const diagnostics = [...parsed.diagnostics];
452
1259
  const routeModels = [];
453
1260
  const seen = /* @__PURE__ */ new Map();
@@ -487,16 +1294,20 @@ const analyze = (parsed, config) => {
487
1294
  diagnostics,
488
1295
  model: {
489
1296
  config,
1297
+ dependencies,
490
1298
  groups: buildGroups(parsed, routeModels, diagnostics),
1299
+ routeDependencies: [],
491
1300
  routes: routeModels
492
1301
  }
493
1302
  };
494
1303
  };
495
1304
  //#endregion
496
1305
  //#region src/pipeline/config.ts
1306
+ const DEFAULT_DEPENDENCIES_ROOT = "src/deps";
497
1307
  const DEFAULT_ROUTES_ROOT = "src/routes";
498
1308
  const isRecord = (value) => typeof value === "object" && value !== null;
499
1309
  const isString = (value) => typeof value === "string";
1310
+ const isFunction = (value) => typeof value === "function";
500
1311
  const loadConfig = async (input) => {
501
1312
  let defaultExport;
502
1313
  try {
@@ -522,65 +1333,95 @@ const loadConfig = async (input) => {
522
1333
  message: "The Nooh config \"routes\" option must be a string.",
523
1334
  severity: "error"
524
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
+ }] };
525
1356
  const root = normalizePath(input.root ?? "");
526
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);
527
1361
  return {
528
1362
  config: {
1363
+ dependenciesRoot: toProjectPath(dependencies, root),
529
1364
  root,
530
- routesRoot: toProjectPath(routesValue ?? DEFAULT_ROUTES_ROOT, root),
1365
+ routesRoot,
531
1366
  source,
532
- value: { routes: routesValue }
1367
+ value: {
1368
+ dependencies: dependenciesValue,
1369
+ routes: routesValue
1370
+ }
533
1371
  },
534
1372
  diagnostics: []
535
1373
  };
536
1374
  };
537
1375
  //#endregion
538
1376
  //#region src/pipeline/discover.ts
1377
+ const ROUTE_FILE_PATTERN = /\.(get|post|put|patch|delete|options|head|all)\.(?:ts|tsx)$/;
539
1378
  const isTypeScriptFile = (file) => file.path.endsWith(".ts") || file.path.endsWith(".tsx");
1379
+ const isRouteFile = (filePath) => ROUTE_FILE_PATTERN.test(filePath);
540
1380
  const isGroupFile = (filePath) => {
541
- const filename = filePath.split("/").at(-1);
1381
+ const filename = basename(filePath);
542
1382
  return filename === "$.ts" || filename === "$.tsx";
543
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
+ };
544
1392
  const discoverGroup = (config, file) => {
545
1393
  const source = toProjectPath(file.path, config.root);
546
1394
  if (!isPathInside(source, config.routesRoot)) return null;
547
- const parts = relativePath(config.routesRoot, source).split("/").filter(Boolean);
548
1395
  if (!isGroupFile(source)) return null;
549
- const directoryParts = parts.slice(0, -1);
550
- if (directoryParts.includes("endpoints")) return null;
1396
+ const relative = relativePath(config.routesRoot, source);
1397
+ const directory = dirname(relative);
551
1398
  return {
552
- groupPath: normalizePath(directoryParts.join("/")),
1399
+ groupPath: normalizePath(directory),
553
1400
  source
554
1401
  };
555
1402
  };
556
1403
  const discoverEndpoint = (config, file) => {
557
1404
  const source = toProjectPath(file.path, config.root);
558
1405
  if (!isPathInside(source, config.routesRoot)) return null;
559
- const parts = relativePath(config.routesRoot, source).split("/").filter(Boolean);
560
- const endpointIndex = parts.lastIndexOf("endpoints");
561
- if (endpointIndex === -1) return null;
562
- const endpointsRoot = normalizePath([
563
- config.routesRoot,
564
- ...parts.slice(0, endpointIndex),
565
- "endpoints"
566
- ].join("/"));
567
- const groupParts = parts.slice(0, endpointIndex);
568
- const localParts = parts.slice(endpointIndex + 1);
569
- if (localParts.length === 0) return null;
1406
+ if (!isRouteFile(source)) return null;
1407
+ const relative = relativePath(config.routesRoot, source);
570
1408
  return {
571
- endpointsRoot,
572
- groupPath: normalizePath(groupParts.join("/")),
573
- localPath: normalizePath(localParts.join("/")),
1409
+ groupPath: normalizePath(dirname(relative)),
1410
+ localPath: normalizePath(basename(relative)),
574
1411
  source
575
1412
  };
576
1413
  };
577
1414
  const discover = (snapshot, config) => {
578
1415
  const endpoints = [];
579
1416
  const groups = [];
1417
+ const dependencies = [];
580
1418
  for (const file of snapshot.files) {
581
1419
  if (!isTypeScriptFile(file)) continue;
582
- const source = toProjectPath(file.path, config.root);
583
- if (!isPathInside(source, config.routesRoot)) continue;
1420
+ const dependency = discoverDependency(config, file);
1421
+ if (dependency) {
1422
+ dependencies.push(dependency);
1423
+ continue;
1424
+ }
584
1425
  const group = discoverGroup(config, file);
585
1426
  if (group) {
586
1427
  groups.push(group);
@@ -589,10 +1430,12 @@ const discover = (snapshot, config) => {
589
1430
  const endpoint = discoverEndpoint(config, file);
590
1431
  if (endpoint) endpoints.push(endpoint);
591
1432
  }
1433
+ dependencies.sort((a, b) => a.path.localeCompare(b.path));
592
1434
  endpoints.sort((a, b) => a.source.localeCompare(b.source));
593
1435
  groups.sort((a, b) => a.source.localeCompare(b.source));
594
1436
  return {
595
1437
  config,
1438
+ dependencies,
596
1439
  endpoints,
597
1440
  groups
598
1441
  };
@@ -721,13 +1564,24 @@ const DEFAULT_OUTPUT_ROOT = ".nooh";
721
1564
  const modulePath = (outputRoot, value) => normalizePath(`${outputRoot}/${value}.ts`);
722
1565
  const plan = (model, outputRoot = DEFAULT_OUTPUT_ROOT) => {
723
1566
  const normalizedOutputRoot = normalizePath(outputRoot);
724
- const modules = [{
725
- id: modulePath(normalizedOutputRoot, "types"),
726
- kind: "types"
727
- }, {
728
- id: modulePath(normalizedOutputRoot, "router/middleware"),
729
- kind: "middleware"
730
- }];
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
+ ];
731
1585
  const routerPaths = [...new Set(model.routes.map((route) => route.routerPath))].sort();
732
1586
  for (const routerPath of routerPaths) {
733
1587
  const route = model.routes.find((candidate) => candidate.routerPath === routerPath);
@@ -752,12 +1606,18 @@ const plan = (model, outputRoot = DEFAULT_OUTPUT_ROOT) => {
752
1606
  });
753
1607
  modules.sort((a, b) => a.id.localeCompare(b.id));
754
1608
  return {
1609
+ dependencies: model.dependencies,
755
1610
  modules,
756
1611
  outputRoot: normalizedOutputRoot
757
1612
  };
758
1613
  };
759
1614
  //#endregion
760
1615
  //#region src/compiler.ts
1616
+ const emptyDependencyGraph = {
1617
+ nodes: /* @__PURE__ */ new Map(),
1618
+ order: [],
1619
+ references: /* @__PURE__ */ new Map()
1620
+ };
761
1621
  const createCompiler = () => ({
762
1622
  analyze,
763
1623
  compile: async (input) => {
@@ -766,12 +1626,15 @@ const createCompiler = () => ({
766
1626
  diagnostics: configResult.diagnostics,
767
1627
  model: {
768
1628
  config: {
1629
+ dependenciesRoot: "",
769
1630
  root: input.root ? input.root.replaceAll("\\", "/") : "",
770
1631
  routesRoot: "",
771
1632
  source: input.config,
772
1633
  value: {}
773
1634
  },
1635
+ dependencies: emptyDependencyGraph,
774
1636
  groups: [],
1637
+ routeDependencies: [],
775
1638
  routes: []
776
1639
  },
777
1640
  output: null,
@@ -779,8 +1642,13 @@ const createCompiler = () => ({
779
1642
  };
780
1643
  const discovered = discover(input.sources, configResult.config);
781
1644
  const parsed = parse(discovered);
782
- const analyzed = analyze(parsed, configResult.config);
783
- 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
+ ];
784
1652
  if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) return {
785
1653
  diagnostics,
786
1654
  model: analyzed.model,
@@ -788,7 +1656,7 @@ const createCompiler = () => ({
788
1656
  plan: null
789
1657
  };
790
1658
  const compilationPlan = plan(analyzed.model, input.options?.outputRoot);
791
- const output = generate(compilationPlan, analyzed.model);
1659
+ const output = generateRouteIntrospection(compilationPlan, analyzed.model);
792
1660
  return {
793
1661
  diagnostics,
794
1662
  model: analyzed.model,
@@ -798,10 +1666,12 @@ const createCompiler = () => ({
798
1666
  },
799
1667
  discover,
800
1668
  generate,
1669
+ generateRouteIntrospection,
801
1670
  loadConfig,
802
1671
  parse,
803
1672
  plan
804
1673
  });
1674
+ const introspect = (input) => introspectCompilation(input);
805
1675
  //#endregion
806
1676
  //#region src/compile.ts
807
1677
  const compile = (input) => createCompiler().compile(input);
@@ -835,6 +1705,26 @@ const diff = (previous, next) => {
835
1705
  };
836
1706
  };
837
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
838
1728
  //#region src/recompile.ts
839
1729
  const recompile = (input) => {
840
1730
  const compiler = createCompiler();
@@ -848,6 +1738,6 @@ const recompile = (input) => {
848
1738
  });
849
1739
  };
850
1740
  //#endregion
851
- 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 };
852
1742
 
853
1743
  //# sourceMappingURL=index.mjs.map