@znight/http 0.1.2 → 0.1.4

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.
Files changed (46) hide show
  1. package/README.md +154 -35
  2. package/dist/compiler.d.mts +77 -2
  3. package/dist/compiler.mjs +259 -101
  4. package/dist/compiler.mjs.map +1 -1
  5. package/dist/express.mjs +3 -2
  6. package/dist/express.mjs.map +1 -1
  7. package/dist/fastify.mjs +200 -23
  8. package/dist/fastify.mjs.map +1 -1
  9. package/dist/hono.mjs +174 -43
  10. package/dist/hono.mjs.map +1 -1
  11. package/dist/index.d.mts +36 -4
  12. package/dist/index.mjs +1005 -257
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/{node-Clm1Ia7T.mjs → node-DnYv_AMI.mjs} +4 -3
  15. package/dist/{node-Clm1Ia7T.mjs.map → node-DnYv_AMI.mjs.map} +1 -1
  16. package/dist/{node-io-CVnqpVNS.mjs → node-io-BkdRSQeg.mjs} +99 -60
  17. package/dist/{node-io-CVnqpVNS.mjs.map → node-io-BkdRSQeg.mjs.map} +1 -1
  18. package/dist/node.mjs +1 -1
  19. package/dist/utils-DIWdzVht.mjs +46 -0
  20. package/dist/utils-DIWdzVht.mjs.map +1 -0
  21. package/package.json +4 -4
  22. package/src/adapters/express.ts +4 -1
  23. package/src/adapters/fastify.ts +265 -25
  24. package/src/adapters/hono.ts +225 -58
  25. package/src/adapters/node-io.ts +128 -58
  26. package/src/adapters/node.ts +4 -1
  27. package/src/body.ts +31 -2
  28. package/src/coerce.ts +31 -0
  29. package/src/compiler/analyze.ts +3 -3
  30. package/src/compiler/generate.ts +127 -22
  31. package/src/compiler/index.ts +13 -0
  32. package/src/compiler/route-decorators.ts +45 -2
  33. package/src/compiler/route-groups.ts +216 -0
  34. package/src/compiler/validate.ts +15 -126
  35. package/src/context.ts +148 -78
  36. package/src/decorators.ts +19 -3
  37. package/src/index.ts +2 -1
  38. package/src/pipeline.ts +936 -177
  39. package/src/plugin-routes.ts +19 -0
  40. package/src/plugin.ts +7 -3
  41. package/src/response.ts +6 -8
  42. package/src/serialize.ts +48 -21
  43. package/src/utils.ts +25 -2
  44. package/src/validate.ts +28 -12
  45. package/dist/utils-DCbu3a4w.mjs +0 -27
  46. package/dist/utils-DCbu3a4w.mjs.map +0 -1
package/dist/compiler.mjs CHANGED
@@ -514,6 +514,38 @@ function routeMeta(ctx, cls, decorators, target) {
514
514
  }
515
515
  return entries;
516
516
  }
517
+ /**
518
+ * The route metadata entries of a class (`method` omitted) or of one of its methods: the route
519
+ * decorators (`createRouteDecorator` consts) applied to that target, in source order, read exactly
520
+ * as the http extension reads controllers and routes (keys, statically evaluated values, groups,
521
+ * `required`). Problems are reported as ZNIGHT005 through `ctx`, once per call: read each target
522
+ * once. Decorators that are not route decorators are ignored.
523
+ *
524
+ * Works with the analysis context of any extension. The entries of a target are its own: combine a
525
+ * class and a method with {@link effectiveRouteMeta}.
526
+ *
527
+ * ```ts
528
+ * analyzeClass(cls, ctx) {
529
+ * const classMeta = readRouteMeta(ctx, cls)
530
+ * for (const method of cls.methods) {
531
+ * const meta = effectiveRouteMeta(classMeta, readRouteMeta(ctx, cls, method))
532
+ * }
533
+ * }
534
+ * ```
535
+ */
536
+ function readRouteMeta(ctx, cls, method) {
537
+ return method === void 0 ? routeMeta(ctx, cls, cls.decorators, cls.displayName) : routeMeta(ctx, cls, method.decorators, `${cls.displayName}.${method.name}`);
538
+ }
539
+ /**
540
+ * The effective metadata of a method from the entries of its class and its own (RFC 0001 §5.1):
541
+ * the class entries, except those of a group the method decides itself, then the method entries.
542
+ * The same rule as `ctx.route.meta` at runtime.
543
+ */
544
+ function effectiveRouteMeta(classMeta, methodMeta) {
545
+ if (methodMeta.length === 0) return [...classMeta];
546
+ const decided = new Set(methodMeta.map((entry) => entry.group));
547
+ return [...classMeta.filter((entry) => !decided.has(entry.group)), ...methodMeta];
548
+ }
517
549
  /** `true` when the decorator is a route metadata decorator. */
518
550
  function isRouteMetaDecorator(ctx, decorator) {
519
551
  return decorator.owner === void 0 && decorator.identity !== void 0 && resolveRouteDecorator(ctx, decorator.identity).kind !== "other";
@@ -661,7 +693,7 @@ function analyzeHttpClass(cls, ctx) {
661
693
  const basePath = normalizePath(options.path);
662
694
  const guards = classRefs(ctx, cls, cls.decorators, Ids.UseGuard, "guard");
663
695
  const interceptors = classRefs(ctx, cls, cls.decorators, Ids.UseInterceptor, "interceptor");
664
- const meta = routeMeta(ctx, cls, cls.decorators, cls.displayName);
696
+ const meta = readRouteMeta(ctx, cls);
665
697
  const bodyLimit = bodyLimitOf(ctx, cls.decorators);
666
698
  const routes = [];
667
699
  for (const method of cls.methods) routes.push(...analyzeMethod(ctx, cls, method, basePath, bodyLimit));
@@ -796,7 +828,7 @@ function analyzeMethod(ctx, cls, method, basePath, controllerBodyLimit) {
796
828
  let headers = responseHeaders(ctx, decorators);
797
829
  const guards = classRefs(ctx, cls, decorators, Ids.UseGuard, "guard");
798
830
  const interceptors = classRefs(ctx, cls, decorators, Ids.UseInterceptor, "interceptor");
799
- const meta = routeMeta(ctx, cls, decorators, where);
831
+ const meta = readRouteMeta(ctx, cls, method);
800
832
  const bodySchema = bodyValidation !== void 0 && !params.some((param) => param.source === "body") ? bodyValidation.schema : void 0;
801
833
  let rawResponse = params.some((param) => param.source === "response");
802
834
  const bodyLimit = bodyLimitOf(ctx, decorators) ?? controllerBodyLimit;
@@ -1695,6 +1727,7 @@ function groupsNode(ctx, controllers) {
1695
1727
  function createHelpers(ctx) {
1696
1728
  let coerce;
1697
1729
  let validate;
1730
+ let isThenable;
1698
1731
  let uploads;
1699
1732
  let context;
1700
1733
  return {
@@ -1706,6 +1739,10 @@ function createHelpers(ctx) {
1706
1739
  validate ??= ctx.importFrom(HTTP_PACKAGE, "validate");
1707
1740
  return validate;
1708
1741
  },
1742
+ isThenable: () => {
1743
+ isThenable ??= ctx.importFrom(HTTP_PACKAGE, "isThenable");
1744
+ return isThenable;
1745
+ },
1709
1746
  uploads: () => {
1710
1747
  uploads ??= ctx.importFrom(HTTP_PACKAGE, "uploads");
1711
1748
  return uploads;
@@ -1778,22 +1815,86 @@ function tokens(ctx, refs) {
1778
1815
  return ctx.arr(refs.map((ref) => ctx.tokenOf(ref) ?? ref.token));
1779
1816
  }
1780
1817
  /**
1781
- * `(c: Controller, ctx: HttpContext) => c.handler(...args)`: `async` (awaiting every `validate()`)
1782
- * when the route validates something with a schema, synchronous otherwise.
1818
+ * `(c: Controller, ctx: HttpContext) => c.handler(...args)`, synchronous when no argument is
1819
+ * validated with a schema. A route validating arguments gets a block that stays synchronous as long
1820
+ * as every `validate()` returns a value (see {@link validatingBlock}). `@Validate(S)` without
1821
+ * `@Body()` parameter reads the body in `invoke`: that arrow is `async` and `await`s every
1822
+ * `validate()`.
1783
1823
  */
1784
1824
  function invokeNode(ctx, helpers, controller, route) {
1785
- const async = route.bodySchema !== void 0 || route.params.some((param) => param.schema !== void 0);
1786
1825
  const params = [["c", ctx.importType(controller.identity)]];
1787
1826
  if (route.params.length > 0 || route.bodySchema !== void 0) params.push(["ctx", helpers.context()]);
1788
- return ctx.arrow(params, (c, http) => {
1789
- const args = [];
1790
- for (const param of route.params) args[param.index] = argumentNode(ctx, helpers, http, param);
1791
- const call = ctx.call(ctx.member(c, route.handler), Array.from(args, (arg) => arg ?? ctx.lit(void 0)));
1792
- if (route.bodySchema === void 0) return call;
1827
+ const slots = [];
1828
+ for (const param of route.params) slots[param.index] = param;
1829
+ const bodySchema = route.bodySchema;
1830
+ if (bodySchema !== void 0) return ctx.arrow(params, (c, http) => {
1831
+ const call = handlerCall(ctx, c, route, Array.from(slots, (param) => awaitedArgument(ctx, helpers, http, param)));
1793
1832
  const body = ctx.code`await ${ctx.call(ctx.member(http, "parseBody"))}`;
1794
- const check = validation(ctx, helpers, route.bodySchema, body, "body");
1833
+ const check = ctx.code`await ${validation(ctx, helpers, bodySchema, body, "body")}`;
1795
1834
  return ctx.code`{ ${check}; return ${call} }`;
1796
- }, { async });
1835
+ }, { async: true });
1836
+ const last = slots.findLastIndex((param) => param?.schema !== void 0);
1837
+ return ctx.arrow(params, (c, http) => last === -1 ? handlerCall(ctx, c, route, Array.from(slots, (param) => plainArgument(ctx, helpers, http, param))) : validatingBlock(ctx, helpers, route, slots, last, c, http));
1838
+ }
1839
+ /** `c.handler(args)`. */
1840
+ function handlerCall(ctx, c, route, args) {
1841
+ return ctx.call(ctx.member(c, route.handler), args);
1842
+ }
1843
+ /**
1844
+ * The body of the `invoke` of a route validating arguments with schemas: the arguments up to the
1845
+ * last validated one are evaluated in order into `const`s (`a<index>`), the handler is called with
1846
+ * them. `validate()` returns the value when the schema validates synchronously (Zod, Valibot and
1847
+ * ArkType without async refinements), so the request needs no promise; when one returns a
1848
+ * thenable, the rest runs in an `async` arrow that awaits it, then evaluates the remaining arguments
1849
+ * (`await`ing their validations) and calls the handler: the order of evaluation, the error reported
1850
+ * when several arguments are invalid and the value received by the handler are those of an `async`
1851
+ * `invoke` awaiting every `validate()`.
1852
+ *
1853
+ * ```ts
1854
+ * (c: UserController, ctx: HttpContext) => {
1855
+ * const a0 = coerce.number(ctx.params['id'], 'param "id"')
1856
+ * const a1 = validate(UpdateUser, ctx.body, 'body')
1857
+ * if (isThenable(a1)) return (async () => c.update(a0, await a1, ctx))()
1858
+ * return c.update(a0, a1, ctx)
1859
+ * }
1860
+ * ```
1861
+ */
1862
+ function validatingBlock(ctx, helpers, route, slots, last, c, http) {
1863
+ const bound = [];
1864
+ for (let index = 0; index <= last; index++) {
1865
+ const param = slots[index];
1866
+ if (param !== void 0 && param.source !== "context") bound.push(index);
1867
+ }
1868
+ return ctx.block(bound.map((index) => `a${index}`), (...locals) => {
1869
+ const local = new Map(bound.map((index, position) => [index, locals[position]]));
1870
+ const evaluated = (index) => local.get(index) ?? plainArgument(ctx, helpers, http, slots[index]);
1871
+ const statements = [];
1872
+ for (const index of bound) {
1873
+ const param = slots[index];
1874
+ const name = local.get(index);
1875
+ statements.push(ctx.code`const ${name} = ${plainArgument(ctx, helpers, http, param)}`);
1876
+ if (param.schema !== void 0) {
1877
+ const rest = Array.from(slots, (other, position) => {
1878
+ if (position < index) return evaluated(position);
1879
+ return position === index ? ctx.code`await ${name}` : awaitedArgument(ctx, helpers, http, other);
1880
+ });
1881
+ const resume = ctx.arrow([], () => handlerCall(ctx, c, route, rest), { async: true });
1882
+ statements.push(ctx.code`if (${ctx.call(helpers.isThenable(), [name])}) return (${resume})()`);
1883
+ }
1884
+ }
1885
+ const args = Array.from(slots, (param, position) => position <= last ? evaluated(position) : plainArgument(ctx, helpers, http, param));
1886
+ statements.push(ctx.code`return ${handlerCall(ctx, c, route, args)}`);
1887
+ return statements;
1888
+ });
1889
+ }
1890
+ /** An argument evaluated in an `async` arrow: its validation, if any, is `await`ed. */
1891
+ function awaitedArgument(ctx, helpers, http, param) {
1892
+ const node = plainArgument(ctx, helpers, http, param);
1893
+ return param?.schema === void 0 ? node : ctx.code`await ${node}`;
1894
+ }
1895
+ /** An argument as an expression (`validate()` not awaited); `undefined` for a hole. */
1896
+ function plainArgument(ctx, helpers, http, param) {
1897
+ return param === void 0 ? ctx.lit(void 0) : argumentNode(ctx, helpers, http, param);
1797
1898
  }
1798
1899
  /** The expression passed to the handler for one parameter. */
1799
1900
  function argumentNode(ctx, helpers, http, param) {
@@ -1844,13 +1945,13 @@ function argumentNode(ctx, helpers, http, param) {
1844
1945
  }
1845
1946
  }
1846
1947
  }
1847
- /** `await validate(schema, value, location)`. */
1948
+ /** `validate(schema, value, location)` (the value, or a promise when the schema validates asynchronously). */
1848
1949
  function validation(ctx, helpers, schema, value, location) {
1849
- return ctx.code`await ${ctx.call(helpers.validate(), [
1950
+ return ctx.call(helpers.validate(), [
1850
1951
  ctx.ref(schema),
1851
1952
  value,
1852
1953
  location
1853
- ])}`;
1954
+ ]);
1854
1955
  }
1855
1956
  /** `coerce.X(value, label, ...)`, cast with `as never` when the result does not have the declared type. */
1856
1957
  function coercion(ctx, helpers, coercion, value, label) {
@@ -1885,77 +1986,26 @@ function itemCoercer(ctx, helpers, plan) {
1885
1986
  default: return ctx.member(helpers.coerce(), plan.kind);
1886
1987
  }
1887
1988
  }
1888
- // src/compiler/validate.ts
1889
- /** Validates the routes of every controller. */
1890
- function validateHttp(ctx) {
1891
- const controllers = [...ctx.stored].sort((a, b) => compareStrings(a.token, b.token));
1892
- const registered = /* @__PURE__ */ new Map();
1893
- for (const controller of controllers) {
1894
- const baseIssue = checkRoutePath(controller.rawBasePath);
1895
- if (baseIssue !== void 0) {
1896
- invalidRoute(ctx, controller.pathLocation, `Invalid base path ${JSON.stringify(controller.rawBasePath)} of ${controller.displayName}: ${baseIssue.message}`, GRAMMAR_HINTS);
1897
- continue;
1898
- }
1899
- for (const route of controller.routes) {
1900
- const where = `${controller.displayName}.${route.handler}`;
1901
- const ownIssue = checkRoutePath(route.rawPath);
1902
- const issue = ownIssue ?? checkRoutePath(route.fullPath);
1903
- if (issue !== void 0) {
1904
- const path = ownIssue === void 0 ? route.fullPath : route.rawPath;
1905
- invalidRoute(ctx, route.pathLocation, `Invalid route path ${JSON.stringify(path)} of ${where}: ${issue.message}`, GRAMMAR_HINTS);
1906
- continue;
1907
- }
1908
- const parsed = parseRoutePath(route.fullPath);
1909
- checkParams(ctx, route, parsed, where);
1910
- const key = `${route.method} ${parsed.shape}`;
1911
- const previous = registered.get(key);
1912
- if (previous === void 0) {
1913
- registered.set(key, {
1914
- where,
1915
- route
1916
- });
1917
- continue;
1918
- }
1919
- ctx.report({
1920
- code: Codes.InvalidRoute,
1921
- severity: "error",
1922
- message: `Duplicate route ${route.method} ${route.fullPath}: ${where} conflicts with ${previous.where} (${previous.route.method} ${previous.route.fullPath})`,
1923
- location: route.location,
1924
- hints: ["Two routes with the same method and the same path shape (parameter names aside) cannot coexist"],
1925
- related: [{
1926
- message: `${previous.where} is declared here`,
1927
- location: previous.route.location
1928
- }]
1929
- });
1930
- }
1931
- }
1932
- validateDecisions(ctx, controllers);
1933
- }
1989
+ // src/compiler/route-groups.ts
1934
1990
  /**
1935
- * Every route decorator definition, sorted by identity: the applied ones (first usage) and, when
1936
- * the compiler provides `definitionsOf`, the ones applied nowhere (invalid ones are left to the
1937
- * usages, which report them).
1991
+ * Every route decorator definition, sorted by identity: the applied ones (`applied`, first usage
1992
+ * wins) and, when the compiler provides `definitionsOf`, the ones applied nowhere (invalid ones are
1993
+ * left to the usages, which report them).
1938
1994
  */
1939
- function routeDecoratorDefinitions(ctx, controllers) {
1995
+ function routeDecoratorDefinitions(ctx, applied) {
1940
1996
  const definitions = /* @__PURE__ */ new Map();
1941
- const visit = (entries) => {
1942
- for (const entry of entries) {
1943
- const known = definitions.get(entry.identity);
1944
- if (known === void 0) definitions.set(entry.identity, {
1945
- identity: entry.identity,
1946
- name: entry.decorator,
1947
- key: entry.key,
1948
- group: entry.group,
1949
- required: entry.required,
1950
- definition: entry.definition,
1951
- flag: entry.value === true
1952
- });
1953
- else if (entry.value !== true) known.flag = false;
1954
- }
1955
- };
1956
- for (const controller of controllers) {
1957
- visit(controller.meta);
1958
- for (const route of controller.routes) visit(route.meta);
1997
+ for (const entry of applied) {
1998
+ const known = definitions.get(entry.identity);
1999
+ if (known === void 0) definitions.set(entry.identity, {
2000
+ identity: entry.identity,
2001
+ name: entry.decorator,
2002
+ key: entry.key,
2003
+ group: entry.group,
2004
+ required: entry.required,
2005
+ definition: entry.definition,
2006
+ flag: entry.value === true
2007
+ });
2008
+ else if (entry.value !== true) known.flag = false;
1959
2009
  }
1960
2010
  const all = typeof ctx.definitionsOf === "function" ? ctx.definitionsOf(Ids.createRouteDecorator) : [];
1961
2011
  for (const found of all) {
@@ -1982,20 +2032,26 @@ function routeDecoratorDefinitions(ctx, controllers) {
1982
2032
  return [...definitions.values()].sort((a, b) => compareStrings(a.identity, b.identity));
1983
2033
  }
1984
2034
  /**
1985
- * Route decorator groups (RFC 0001 §5.1): every definition of a key uses the same options and the
1986
- * decorators of a group agree on `required` (ZNIGHT005, at the second definition); every route has
1987
- * a decision of every required group, its own or its controller's (ZNIGHT023).
2035
+ * The route decorator groups of the application (RFC 0001 §5.1): every definition of a key must use
2036
+ * the same options and the decorators of a group must agree on `required` (a conflict is returned
2037
+ * at the second definition, which is then ignored); the required groups with their decorators.
2038
+ * `applied` are the entries of the targets the caller analysed (their first usage of a decorator
2039
+ * wins over its `definitionsOf` entry).
1988
2040
  */
1989
- function validateDecisions(ctx, controllers) {
1990
- const definitions = routeDecoratorDefinitions(ctx, controllers);
1991
- if (definitions.length === 0) return;
2041
+ function analyzeRouteGroups(ctx, applied) {
2042
+ const definitions = routeDecoratorDefinitions(ctx, applied);
2043
+ if (definitions.length === 0) return {
2044
+ conflicts: [],
2045
+ required: []
2046
+ };
2047
+ const conflicts = [];
1992
2048
  const byKey = /* @__PURE__ */ new Map();
1993
2049
  const byGroup = /* @__PURE__ */ new Map();
1994
2050
  const members = /* @__PURE__ */ new Map();
1995
2051
  for (const definition of definitions) {
1996
2052
  const sameKey = byKey.get(definition.key);
1997
2053
  if (sameKey !== void 0 && (sameKey.group !== definition.group || sameKey.required !== definition.required)) {
1998
- ctx.report({
2054
+ conflicts.push({
1999
2055
  code: Codes.InvalidDecorator,
2000
2056
  severity: "error",
2001
2057
  message: `The route decorator ${definition.name} defines the key "${definition.key}" with ${describeOptions(definition)}, but ${sameKey.name} defines it with ${describeOptions(sameKey)}`,
@@ -2010,7 +2066,7 @@ function validateDecisions(ctx, controllers) {
2010
2066
  }
2011
2067
  const sameGroup = byGroup.get(definition.group);
2012
2068
  if (sameGroup !== void 0 && sameGroup.required !== definition.required) {
2013
- ctx.report({
2069
+ conflicts.push({
2014
2070
  code: Codes.InvalidDecorator,
2015
2071
  severity: "error",
2016
2072
  message: `The "${definition.group}" group is ${definition.required ? "" : "not "}required for ${definition.name} but ${sameGroup.required ? "" : "not "}required for ${sameGroup.name}`,
@@ -2029,7 +2085,112 @@ function validateDecisions(ctx, controllers) {
2029
2085
  list.push(definition);
2030
2086
  members.set(definition.group, list);
2031
2087
  }
2032
- const required = [...byGroup.values()].filter((definition) => definition.required);
2088
+ const required = [];
2089
+ for (const first of byGroup.values()) {
2090
+ if (!first.required) continue;
2091
+ const decorators = [...members.get(first.group) ?? [first]].sort((a, b) => compareStrings(a.definition.file, b.definition.file) || a.definition.start - b.definition.start).map((member) => Object.freeze({ ...member }));
2092
+ const usages = decorators.map((member) => `@${member.name}(${member.flag ? "" : "..."})`);
2093
+ const suggestion = usages.length === 1 ? usages[0] : `${usages.slice(0, -1).join(", ")} or ${usages.at(-1)}`;
2094
+ required.push(Object.freeze({
2095
+ group: first.group,
2096
+ decorators: Object.freeze(decorators),
2097
+ suggestion
2098
+ }));
2099
+ }
2100
+ return {
2101
+ conflicts,
2102
+ required
2103
+ };
2104
+ }
2105
+ /**
2106
+ * The required route decorator groups of the application (RFC 0001): what every route-like target
2107
+ * of another extension (a gateway, a GraphQL root field) must decide, exactly as HTTP routes must.
2108
+ * A target without a decision of a returned group is **ZNIGHT023**, reported by the caller.
2109
+ *
2110
+ * `applied` are the route metadata entries the caller read on its own targets
2111
+ * ({@link readRouteMeta}): they make decorators known even when the compiler has no
2112
+ * `definitionsOf`, and refine how flags are suggested. Inconsistent definitions are left out and
2113
+ * not reported here: the http extension reports them (ZNIGHT005) once.
2114
+ *
2115
+ * ```ts
2116
+ * validate(ctx) {
2117
+ * const required = requiredRouteGroups(ctx, ctx.stored.flatMap(gateway => gateway.meta))
2118
+ * for (const gateway of ctx.stored)
2119
+ * for (const group of required)
2120
+ * if (!gateway.meta.some(entry => entry.group === group.group))
2121
+ * ctx.report({ code: 'ZNIGHT023', severity: 'error', message: `${gateway.name} has no "${group.group}" decision`,
2122
+ * location: gateway.location, hints: [`Add ${group.suggestion} to ${gateway.name}`] })
2123
+ * }
2124
+ * ```
2125
+ */
2126
+ function requiredRouteGroups(ctx, applied = []) {
2127
+ return analyzeRouteGroups(ctx, applied).required;
2128
+ }
2129
+ function describeOptions(entry) {
2130
+ const group = entry.group === entry.key ? "no group" : `group "${entry.group}"`;
2131
+ return entry.required ? `${group}, required` : group;
2132
+ }
2133
+ // src/compiler/validate.ts
2134
+ /** Validates the routes of every controller. */
2135
+ function validateHttp(ctx) {
2136
+ const controllers = [...ctx.stored].sort((a, b) => compareStrings(a.token, b.token));
2137
+ const registered = /* @__PURE__ */ new Map();
2138
+ for (const controller of controllers) {
2139
+ const baseIssue = checkRoutePath(controller.rawBasePath);
2140
+ if (baseIssue !== void 0) {
2141
+ invalidRoute(ctx, controller.pathLocation, `Invalid base path ${JSON.stringify(controller.rawBasePath)} of ${controller.displayName}: ${baseIssue.message}`, GRAMMAR_HINTS);
2142
+ continue;
2143
+ }
2144
+ for (const route of controller.routes) {
2145
+ const where = `${controller.displayName}.${route.handler}`;
2146
+ const ownIssue = checkRoutePath(route.rawPath);
2147
+ const issue = ownIssue ?? checkRoutePath(route.fullPath);
2148
+ if (issue !== void 0) {
2149
+ const path = ownIssue === void 0 ? route.fullPath : route.rawPath;
2150
+ invalidRoute(ctx, route.pathLocation, `Invalid route path ${JSON.stringify(path)} of ${where}: ${issue.message}`, GRAMMAR_HINTS);
2151
+ continue;
2152
+ }
2153
+ const parsed = parseRoutePath(route.fullPath);
2154
+ checkParams(ctx, route, parsed, where);
2155
+ const key = `${route.method} ${parsed.shape}`;
2156
+ const previous = registered.get(key);
2157
+ if (previous === void 0) {
2158
+ registered.set(key, {
2159
+ where,
2160
+ route
2161
+ });
2162
+ continue;
2163
+ }
2164
+ ctx.report({
2165
+ code: Codes.InvalidRoute,
2166
+ severity: "error",
2167
+ message: `Duplicate route ${route.method} ${route.fullPath}: ${where} conflicts with ${previous.where} (${previous.route.method} ${previous.route.fullPath})`,
2168
+ location: route.location,
2169
+ hints: ["Two routes with the same method and the same path shape (parameter names aside) cannot coexist"],
2170
+ related: [{
2171
+ message: `${previous.where} is declared here`,
2172
+ location: previous.route.location
2173
+ }]
2174
+ });
2175
+ }
2176
+ }
2177
+ validateDecisions(ctx, controllers);
2178
+ }
2179
+ /** Every route metadata entry of the controllers: controller entries, then route entries. */
2180
+ function* appliedEntries(controllers) {
2181
+ for (const controller of controllers) {
2182
+ yield* controller.meta;
2183
+ for (const route of controller.routes) yield* route.meta;
2184
+ }
2185
+ }
2186
+ /**
2187
+ * Route decorator groups (RFC 0001 §5.1): every definition of a key uses the same options and the
2188
+ * decorators of a group agree on `required` (ZNIGHT005, at the second definition); every route has
2189
+ * a decision of every required group, its own or its controller's (ZNIGHT023).
2190
+ */
2191
+ function validateDecisions(ctx, controllers) {
2192
+ const { conflicts, required } = analyzeRouteGroups(ctx, appliedEntries(controllers));
2193
+ for (const conflict of conflicts) ctx.report(conflict);
2033
2194
  if (required.length === 0) return;
2034
2195
  for (const controller of controllers) {
2035
2196
  const controllerGroups = new Set(controller.meta.map((entry) => entry.group));
@@ -2037,18 +2198,15 @@ function validateDecisions(ctx, controllers) {
2037
2198
  const decided = /* @__PURE__ */ new Set([...controllerGroups, ...route.meta.map((entry) => entry.group)]);
2038
2199
  for (const group of required) {
2039
2200
  if (decided.has(group.group)) continue;
2040
- const inOrder = [...members.get(group.group) ?? [group]].sort((a, b) => compareStrings(a.definition.file, b.definition.file) || a.definition.start - b.definition.start);
2041
- const decorators = inOrder.map((definition) => `@${definition.name}(${definition.flag ? "" : "..."})`);
2042
- const listed = decorators.length === 1 ? decorators[0] : `${decorators.slice(0, -1).join(", ")} or ${decorators.at(-1)}`;
2043
2201
  ctx.report({
2044
2202
  code: Codes.MissingRouteDecision,
2045
2203
  severity: "error",
2046
2204
  message: `Route ${route.method} ${route.fullPath} (${controller.displayName}.${route.handler}) has no "${group.group}" decision`,
2047
2205
  location: route.location,
2048
- hints: [`Add ${listed} to the route or to ${controller.displayName}`],
2206
+ hints: [`Add ${group.suggestion} to the route or to ${controller.displayName}`],
2049
2207
  related: [{
2050
2208
  message: `The "${group.group}" group is required`,
2051
- location: (inOrder[0] ?? group).definition
2209
+ location: firstDefinition(group)
2052
2210
  }, {
2053
2211
  message: `${controller.displayName} is declared here`,
2054
2212
  location: controller.location
@@ -2058,9 +2216,9 @@ function validateDecisions(ctx, controllers) {
2058
2216
  }
2059
2217
  }
2060
2218
  }
2061
- function describeOptions(entry) {
2062
- const group = entry.group === entry.key ? "no group" : `group "${entry.group}"`;
2063
- return entry.required ? `${group}, required` : group;
2219
+ /** Where the first decorator of a group (source order) is defined. */
2220
+ function firstDefinition(group) {
2221
+ return group.decorators[0].definition;
2064
2222
  }
2065
2223
  const GRAMMAR_HINTS = ["Paths are made of static segments (letters, digits and - . _ ~ ! $ & ' + , ; = @), :name parameters and a trailing * wildcard"];
2066
2224
  /** `:param` of the path ↔ `@Param('name')` parameters of the handler. */
@@ -2175,6 +2333,6 @@ const http = Object.freeze({
2175
2333
  validate: validateHttp,
2176
2334
  generate: generateHttp
2177
2335
  });
2178
- export { http as default };
2336
+ export { http as default, effectiveRouteMeta, readRouteMeta, requiredRouteGroups };
2179
2337
 
2180
2338
  //# sourceMappingURL=compiler.mjs.map