diff --git a/graph/javascript/engine/config-resolution/http-router-apps.dl b/graph/javascript/engine/config-resolution/http-router-apps.dl index 41dafd035..d4bc21c2d 100644 --- a/graph/javascript/engine/config-resolution/http-router-apps.dl +++ b/graph/javascript/engine/config-resolution/http-router-apps.dl @@ -45,6 +45,11 @@ jr_url_path(raw, pkg, rest) :- jr_url_rest(raw, pkg, rest), substr(rest, 0, 1) = jr_url_path(raw, pkg, r2) :- jr_url_rest(raw, pkg, rest), substr(rest, 0, 1) = "@", j = min y : { y = range(0, strlen(rest)), substr(rest, y, 1) = "/" }, r2 = substr(rest, j, strlen(rest) - j). jr_spec(raw, pkg) :- jr_url_path(raw, pkg, "/mod.ts"). +// a published alias and its subpaths (`@hono/hono`, `@hono/hono/factory`): the package's own name. The alias must end +// at a `/` or at the end, so another package that only starts with it (`@hono/hono-x`) is itself +jr_spec(raw, pkg) :- jr_spec(raw, x), router_spec_alias(x, pkg). +jr_spec(raw, cat(pkg, substr(x, strlen(a), strlen(x) - strlen(a)))) :- jr_spec(raw, x), router_spec_alias(a, pkg), + strlen(x) > strlen(a) + 1, substr(x, 0, strlen(a) + 1) = cat(a, "/"). .decl jr_module(c0:symbol) jr_module(spec) :- router_app_class(spec, _). diff --git a/graph/javascript/engine/config-resolution/knobs.dl b/graph/javascript/engine/config-resolution/knobs.dl index 8c9f20a34..ad8477249 100644 --- a/graph/javascript/engine/config-resolution/knobs.dl +++ b/graph/javascript/engine/config-resolution/knobs.dl @@ -30,6 +30,10 @@ router_spec_url("https://deno.land/x/hono", "hono"). router_spec_url("https://esm.sh/hono", "hono"). // the JSR name of the package router_app_class("@hono/hono", "Hono"). +// router_spec_alias(Published, Package): a package published under another name, whose subpaths are the package's +// (`@hono/hono/factory`, as an import map's key or after `jsr:`, is `hono/factory`) +.decl router_spec_alias(c0:symbol, c1:symbol) +router_spec_alias("@hono/hono", "hono"). // router_verb(Method, Verb): a route registration and the verb it serves .decl router_verb(c0:symbol, c1:symbol) router_verb("get", "GET"). router_verb("post", "POST"). diff --git a/graph/test/typescript/hono/cases/deno/jsr-name-subpath/src/deno.json b/graph/test/typescript/hono/cases/deno/jsr-name-subpath/src/deno.json new file mode 100644 index 000000000..2ccaa35e1 --- /dev/null +++ b/graph/test/typescript/hono/cases/deno/jsr-name-subpath/src/deno.json @@ -0,0 +1,6 @@ +{ + "imports": { + "@hono/hono": "jsr:@hono/hono@^4", + "@hono/hono-kit/": "./kit/" + } +} diff --git a/graph/test/typescript/hono/cases/deno/jsr-name-subpath/src/kit/factory.ts b/graph/test/typescript/hono/cases/deno/jsr-name-subpath/src/kit/factory.ts new file mode 100644 index 000000000..8bc02dff6 --- /dev/null +++ b/graph/test/typescript/hono/cases/deno/jsr-name-subpath/src/kit/factory.ts @@ -0,0 +1,5 @@ +// not Hono's factory: the function it returns runs, never the one it is given +type Step = (c: unknown, next: () => Promise) => Promise +export const createMiddleware = (_inner: Step): Step => async (_c, next) => { + await next() +} diff --git a/graph/test/typescript/hono/cases/deno/jsr-name-subpath/src/main.ts b/graph/test/typescript/hono/cases/deno/jsr-name-subpath/src/main.ts new file mode 100644 index 000000000..ff643530b --- /dev/null +++ b/graph/test/typescript/hono/cases/deno/jsr-name-subpath/src/main.ts @@ -0,0 +1,19 @@ +// census: an import map keyed by the JSR name (@hono/hono) and its subpaths imported under that name +// (@hono/hono/factory): createMiddleware's argument is the middleware. Look-alike: a local module whose +// specifier only starts with the JSR name (@hono/hono-kit/factory): its createMiddleware is a project function. +import { Hono } from '@hono/hono' +import { createMiddleware } from '@hono/hono/factory' +import { createMiddleware as kitMiddleware } from '@hono/hono-kit/factory' + +const auth = createMiddleware(async (c, next) => { + await next() +}) +const counted = kitMiddleware(async (c, next) => { + await next() +}) +const app = new Hono() +app.use('/admin/*', auth) +app.get('/admin/users', (c) => c.json([])) +app.get('/open', counted, (c) => c.text('open')) + +Deno.serve(app.fetch) diff --git a/graph/test/typescript/hono/cases/openapi/route-builder/src/builder.ts b/graph/test/typescript/hono/cases/openapi/route-builder/src/builder.ts new file mode 100644 index 000000000..645e030e2 --- /dev/null +++ b/graph/test/typescript/hono/cases/openapi/route-builder/src/builder.ts @@ -0,0 +1,27 @@ +import { createRoute, type RouteConfig, type RouteHandler } from '@hono/zod-openapi' +import type { MiddlewareHandler } from 'hono' + +export function requireKey(): MiddlewareHandler { + return async (c, next) => { + if (!c.req.header('x-key')) return c.text('no key', 401) + await next() + } +} + +// A route builder: the definition it is called with is the route, and `.handler(fn)` hands back both, for the app +// to register in one call: `app.openapi(ep.route, ep.handler)`. +export const defineEndpoint = (config: R) => { + const route = createRoute(config) + const handler = (fn: RouteHandler) => ({ handler: fn, route: route }) + return { handler } +} + +// A builder made per API version: the definition is spread into the one it builds, which adds the key check +export const versioned = (_version: string) => { + const define = (config: R) => { + const route = createRoute({ ...config, middleware: [requireKey()] as const }) + const handler = (fn: RouteHandler) => ({ handler: fn, route }) + return { handler } + } + return { define } +} diff --git a/graph/test/typescript/hono/cases/openapi/route-builder/src/index.ts b/graph/test/typescript/hono/cases/openapi/route-builder/src/index.ts new file mode 100644 index 000000000..3f85ec7c6 --- /dev/null +++ b/graph/test/typescript/hono/cases/openapi/route-builder/src/index.ts @@ -0,0 +1,14 @@ +// census: create_route, openapi_register, new_openapihono (route builder: a project helper returns { route, handler } +// for each definition and the app registers both: app.openapi(ep.route, ep.handler); a plain builder and a version-scoped +// builder whose definition is spread into createRoute with the builder's own middleware). +// Look-alikes: an endpoint the builder made that nothing registers, and `.openapi(..)` on an object that is no app. +import { OpenAPIHono } from '@hono/zod-openapi' +import { draft, getItem, listItems } from './routes' + +const docs = { openapi: (_route: unknown, _handler: unknown) => undefined } +const app = new OpenAPIHono() +app.openapi(getItem.route, getItem.handler) +app.openapi(listItems.route, listItems.handler) +docs.openapi(draft.route, draft.handler) + +export default app diff --git a/graph/test/typescript/hono/cases/openapi/route-builder/src/routes.ts b/graph/test/typescript/hono/cases/openapi/route-builder/src/routes.ts new file mode 100644 index 000000000..ff6f1dfdf --- /dev/null +++ b/graph/test/typescript/hono/cases/openapi/route-builder/src/routes.ts @@ -0,0 +1,24 @@ +import { z } from '@hono/zod-openapi' +import { defineEndpoint, versioned } from './builder' + +const v1 = versioned('v1') + +export const getItem = defineEndpoint({ + method: 'get', + path: '/items/{id}', + request: { params: z.object({ id: z.string() }) }, + responses: { 200: { description: 'one item' } }, +}).handler(async (c) => c.json({ id: c.req.valid('param').id }, 200)) + +export const listItems = v1.define({ + method: 'get', + path: '/v1/items', + responses: { 200: { description: 'every item' } }, +}).handler(async (c) => c.json([], 200)) + +// built but never registered: no route +export const draft = defineEndpoint({ + method: 'post', + path: '/drafts', + responses: { 201: { description: 'made' } }, +}).handler(async (c) => c.json({}, 201)) diff --git a/graph/test/typescript/hono/expected/deno/jsr-name-subpath.entries b/graph/test/typescript/hono/expected/deno/jsr-name-subpath.entries new file mode 100644 index 000000000..05d5840c6 --- /dev/null +++ b/graph/test/typescript/hono/expected/deno/jsr-name-subpath.entries @@ -0,0 +1,6 @@ +# hand-reviewed 2026-11-04 against the fixture and the oracle: @hono/hono and @hono/hono/factory named by the import map key; the factory's argument is the middleware of /admin/users; the @hono/hono-kit look-alike is a project function, so what it returns is the middleware of /open +# census: an import map keyed by the JSR name (@hono/hono) and its subpaths imported under that name +entry kit/factory.ts:3:57 http +entry main.ts:16:25 http +entry main.ts:17:27 http +entry main.ts:8:31 http diff --git a/graph/test/typescript/hono/expected/deno/jsr-name-subpath.tsv b/graph/test/typescript/hono/expected/deno/jsr-name-subpath.tsv new file mode 100644 index 000000000..e8826197c --- /dev/null +++ b/graph/test/typescript/hono/expected/deno/jsr-name-subpath.tsv @@ -0,0 +1,8 @@ +# hand-reviewed 2026-11-04 against the fixture and the oracle: @hono/hono and @hono/hono/factory named by the import map key; the factory's argument is the middleware of /admin/users; the @hono/hono-kit look-alike is a project function, so what it returns is the middleware of /open +# census: an import map keyed by the JSR name (@hono/hono) and its subpaths imported under that name +route GET /admin/users main.ts:16:25 main.ts:14:13 1 +route GET /open main.ts:17:27 main.ts:14:13 3 +mw main.ts:8:31 main.ts:16:25 GET /admin/users 0 main.ts:14:13 +mw kit/factory.ts:3:57 main.ts:17:27 GET /open 0 main.ts:14:13 +regby main.ts:1:1 main.ts:16:25 GET /admin/users main.ts:14:13 +regby main.ts:1:1 main.ts:17:27 GET /open main.ts:14:13 diff --git a/graph/test/typescript/hono/expected/openapi/route-builder.entries b/graph/test/typescript/hono/expected/openapi/route-builder.entries new file mode 100644 index 000000000..257fe65b7 --- /dev/null +++ b/graph/test/typescript/hono/expected/openapi/route-builder.entries @@ -0,0 +1,5 @@ +# hand-reviewed 2026-11-04 against the fixture and the oracle's runtime half (the static half cannot follow a closure's local through the builder and reports both registrations unknown: an oracle gap): GET /items/:id with its param schema, GET /v1/items with the version builder's key check at position 0; the never-registered /drafts and the docs.openapi look-alike are no route +# census: create_route, openapi_register, new_openapihono (route builder: a project helper returns { route, handler } +entry builder.ts:5:10 http +entry routes.ts:11:12 http +entry routes.ts:17:12 http diff --git a/graph/test/typescript/hono/expected/openapi/route-builder.tsv b/graph/test/typescript/hono/expected/openapi/route-builder.tsv new file mode 100644 index 000000000..e5488ce89 --- /dev/null +++ b/graph/test/typescript/hono/expected/openapi/route-builder.tsv @@ -0,0 +1,8 @@ +# hand-reviewed 2026-11-04 against the fixture and the oracle's runtime half (the static half cannot follow a closure's local through the builder and reports both registrations unknown: an oracle gap): GET /items/:id with its param schema, GET /v1/items with the version builder's key check at position 0; the never-registered /drafts and the docs.openapi look-alike are no route +# census: create_route, openapi_register, new_openapihono (route builder: a project helper returns { route, handler } +route GET /items/:id routes.ts:11:12 index.ts:9:13 0 +route GET /v1/items routes.ts:17:12 index.ts:9:13 1 +mw builder.ts:5:10 routes.ts:17:12 GET /v1/items 0 index.ts:9:13 +schema routes.ts:11:12 param expr@routes.ts:9:22 +regby index.ts:1:1 routes.ts:11:12 GET /items/:id index.ts:9:13 +regby index.ts:1:1 routes.ts:17:12 GET /v1/items index.ts:9:13 diff --git a/graph/typescript/engine/config-resolution/http-router-apps.dl b/graph/typescript/engine/config-resolution/http-router-apps.dl index 2e29dcd35..e9a73afd3 100644 --- a/graph/typescript/engine/config-resolution/http-router-apps.dl +++ b/graph/typescript/engine/config-resolution/http-router-apps.dl @@ -58,6 +58,11 @@ hr_url_path(raw, pkg, rest) :- hr_url_rest(raw, pkg, rest), substr(rest, 0, 1) = hr_url_path(raw, pkg, r2) :- hr_url_rest(raw, pkg, rest), substr(rest, 0, 1) = "@", j = min y : { y = range(0, strlen(rest)), substr(rest, y, 1) = "/" }, r2 = substr(rest, j, strlen(rest) - j). hr_spec(raw, pkg) :- hr_url_path(raw, pkg, "/mod.ts"). +// a published alias and its subpaths (`@hono/hono`, `@hono/hono/factory`): the package's own name. The alias must end +// at a `/` or at the end, so another package that only starts with it (`@hono/hono-x`) is itself +hr_spec(raw, pkg) :- hr_spec(raw, x), router_spec_alias(x, pkg). +hr_spec(raw, cat(pkg, substr(x, strlen(a), strlen(x) - strlen(a)))) :- hr_spec(raw, x), router_spec_alias(a, pkg), + strlen(x) > strlen(a) + 1, substr(x, 0, strlen(a) + 1) = cat(a, "/"). .decl hr_import_spec(c0:symbol, c1:symbol) hr_import_spec(ih, s) :- rd_import_path(ih, raw), hr_spec(raw, s). @@ -501,6 +506,48 @@ hr_def_param(y, p) :- hr_def_param_need(y), expr_kind("client", k, _, y), expr_k hr_def_val(e, o) :- hr_def_val(i, o), var_initializer("client", _, i, v), module_export_any("client", m, n, "VARIABLE", v), import_binds_namespace(ih, "client", m), expr_referenced("client", "IMPORT_BINDING", ih, r), expr_child("client", e, "RECEIVER", _, r), property_access_name(e, n). +// …a route BUILDER: a project function W returns an object whose method H (a closure of W) hands back `{ route, +// handler }` (`const ep = defineEndpoint({ method, path, .. }).handler(async (c) => ..)`, then +// `app.openapi(ep.route, ep.handler)`). Each such method call is its own route: the definition is what W's call was given +// (createRoute(config) of W's parameter, or of an object W spreads it into, which adds W's own keys), the handler the +// method call's argument (http-router-routes.dl hr_bld_arg). Read only from a registration's `.route` / `.handler` +// argument, never for every object a call returns. +.decl hr_bld_need(c0:symbol) +hr_bld_need(q) :- hr_reg(ce, _, cn), router_openapi(cn), expr_child("client", ce, "ARGUMENT", pos, a), (pos = "0" ; pos = "1"), + property_access_name(a, _), expr_child("client", a, "RECEIVER", _, q). +hr_bld_need(i) :- hr_bld_need(x), hr_denotes_var(x, v), var_initializer("client", _, i, v), i != "". +hr_bld_need(c) :- hr_bld_need(x), expr_kind("client", k, _, x), (expr_kind_is_transparent(k) ; k = "AWAIT_EXPRESSION"), + expr_child("client", x, _, _, c). +// (method call, an expression its callee returns, or one inside it through parentheses and casts) +.decl hr_bld_ret(c0:symbol, c1:symbol) +hr_bld_ret(x, e) :- hr_bld_need(x), call_site("client", "METHOD_CALL", _, _, _, x, _), hr_call_of(x, h), hr_ret_expr(h, e). +hr_bld_ret(x, c) :- hr_bld_ret(x, e), expr_kind("client", k, _, e), expr_kind_is_transparent(k), expr_child("client", e, _, _, c). +// (expression, the method call it is, the method, the object literal it returns) +.decl hr_bld_call(c0:symbol, c1:symbol, c2:symbol, c3:symbol) +hr_bld_call(x, x, h, o) :- hr_bld_ret(x, o), expr_kind("client", "OBJECT_LITERAL", _, o), hr_call_of(x, h), hr_ret_expr(h, _). +hr_bld_call(x, c, h, o) :- hr_bld_need(x), hr_denotes_var(x, v), var_initializer("client", _, i, v), i != "", hr_bld_call(i, c, h, o). +hr_bld_call(x, c, h, o) :- hr_bld_need(x), expr_kind("client", k, _, x), (expr_kind_is_transparent(k) ; k = "AWAIT_EXPRESSION"), + expr_child("client", x, _, _, y), hr_bld_call(y, c, h, o). +// (registration argument `ep.route`, the call of W the method was called on, the createRoute call W made, its argument) +.decl hr_bld_route(c0:symbol, c1:symbol, c2:symbol, c3:symbol) +hr_bld_route(e, c1, d, a) :- hr_reg(ce, _, cn), router_openapi(cn), expr_child("client", ce, "ARGUMENT", "0", e), + property_access_name(e, n), expr_child("client", e, "RECEIVER", _, q), hr_bld_call(q, c, _, o), rd_obj_prop(o, n, v), + hr_denotes_var(v, rv), var_initializer("client", _, d, rv), d != "", + call_site("client", "FUNCTION_CALL", _, _, _, d, _), expr_child("client", d, "METHOD_NAME", _, f), + expr_referenced("client", "IMPORT_BINDING", ih, f), import_binding("client", _, _, orig, _, ih), hr_import_spec(ih, spec), + router_route_def(spec, orig), expr_child("client", d, "ARGUMENT", "0", a), + call_site("client", "METHOD_CALL", _, _, c1, c, _), c1 != "". +// the object createRoute is given in W (in place or a const), with W's own keys +hr_def_val(e, o) :- hr_bld_route(e, _, d, _), hr_def_val(d, o). +hr_def_param_need(a) :- hr_bld_route(_, _, _, a). +// …and what W's call was given: W's parameter passed on, or spread into that object +.decl hr_bld_cfg(c0:symbol, c1:symbol) +hr_bld_cfg(e, y) :- hr_bld_route(e, c1, _, a), hr_def_param(a, p), hr_call_of(c1, w), param_decl("client", _, pos, _, w, p), + expr_child("client", c1, "ARGUMENT", pos, y). +hr_bld_cfg(e, y) :- hr_bld_route(e, c1, _, a), hr_def_obj(a, o), expr_child("client", o, "SPREAD_OPERAND", _, sx), hv_spread_src(sx, z), + hr_def_param(z, p), hr_call_of(c1, w), param_decl("client", _, pos, _, w, p), expr_child("client", c1, "ARGUMENT", pos, y). +hr_def_obj_need(y) :- hr_bld_cfg(_, y). +hr_def_val(e, o2) :- hr_bld_cfg(e, y), hr_def_obj(y, o2). .decl hr_openapi_def(c0:symbol, c1:symbol) hr_openapi_def(ce, o) :- hr_reg(ce, _, cn), router_openapi(cn), expr_child("client", ce, "ARGUMENT", "0", x), hr_def_val(x, o). rd_demand(v) :- hr_openapi_def(_, o), rd_obj_prop(o, k, v), (router_def_key(k, "method") ; router_def_key(k, "path")). diff --git a/graph/typescript/engine/config-resolution/http-router-routes.dl b/graph/typescript/engine/config-resolution/http-router-routes.dl index e638361e2..a767382f6 100644 --- a/graph/typescript/engine/config-resolution/http-router-routes.dl +++ b/graph/typescript/engine/config-resolution/http-router-routes.dl @@ -187,6 +187,13 @@ hr_fprop(a, v) :- hr_farg(a), property_access_name(a, n), expr_child("client", a hr_fprop(a, v) :- hr_farg(a), expr_referenced("client", "VARIABLE", d, a), var_binding("client", _, "PROPERTY", src, i, d), i != "", hr_fobj(i, o), rd_obj_prop(o, src, v). hr_farg(v) :- hr_fprop(_, v). +// a route builder's `.handler` (http-router-apps.dl hr_bld_call): the property is the method's own parameter, so its value +// is that method call's argument (`({ handler: fn, route })` returned by `.handler(fn)`) +.decl hr_bld_arg(c0:symbol, c1:symbol) +hr_bld_arg(a, y) :- hr_farg(a), property_access_name(a, n), expr_child("client", a, "RECEIVER", _, q), hr_bld_call(q, c, h, o), + rd_obj_prop(o, n, v), expr_referenced("client", "PARAMETER", p, v), param_decl("client", _, pos, _, h, p), + expr_child("client", c, "ARGUMENT", pos, y). +hr_farg(y) :- hr_bld_arg(_, y). .decl hr_wrapper_call(c0:symbol) hr_wrapper_call(a) :- call_site("client", "METHOD_CALL", cn, _, r, a, _), router_factory_mw(cn), hr_factory_val(r, _). hr_wrapper_call(a) :- hr_detached(a, n, _), router_factory_mw(n). @@ -212,6 +219,7 @@ hr_fn(a, m) :- hr_farg(a), hr_call_of(a, f), hr_ret_expr(f, e), hr_fn(e, m). hr_fn(a, m) :- hr_farg(a), hr_wrapper_call(a), expr_child("client", a, "ARGUMENT", "0", x), hr_fn(x, m). hr_fn(a, m) :- hr_farg(a), hr_assign_call(a), expr_child("client", a, "ARGUMENT", "0", x), hr_fn(x, m). hr_fn(a, m) :- hr_fprop(a, v), hr_fn(v, m). +hr_fn(a, m) :- hr_bld_arg(a, y), hr_fn(y, m). hr_fn(a, m) :- hr_farg(a), expr_kind("client", k, _, a), expr_kind_is_transparent(k), expr_child("client", a, _, _, c), hr_fn(c, m). diff --git a/graph/typescript/engine/config-resolution/knobs.dl b/graph/typescript/engine/config-resolution/knobs.dl index 87c876ff6..46bf4395a 100644 --- a/graph/typescript/engine/config-resolution/knobs.dl +++ b/graph/typescript/engine/config-resolution/knobs.dl @@ -288,6 +288,10 @@ router_spec_url("https://deno.land/x/hono", "hono"). router_spec_url("https://esm.sh/hono", "hono"). // the JSR name of the package router_app_class("@hono/hono", "Hono"). +// router_spec_alias(Published, Package): a package published under another name, whose subpaths are the package's +// (`@hono/hono/factory`, as an import map's key or after `jsr:`, is `hono/factory`) +.decl router_spec_alias(c0:symbol, c1:symbol) +router_spec_alias("@hono/hono", "hono"). // router_verb(Method, Verb): a route registration and the verb it serves .decl router_verb(c0:symbol, c1:symbol) router_verb("get", "GET"). router_verb("post", "POST").