diff --git a/guardrails.suppressions.json b/guardrails.suppressions.json index 73f8dd7..1dacdff 100644 --- a/guardrails.suppressions.json +++ b/guardrails.suppressions.json @@ -2,9 +2,9 @@ "rules": { "anti-slop/no-unknown-parameters": 1 }, - "tsExpectError": 1, + "tsExpectError": 0, "tsIgnore": 0, - "safety": 0, + "safety": 1, "config": {}, "agentFiles": true } diff --git a/knip.config.ts b/knip.config.ts index 1293648..af9c4a7 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -1,5 +1,9 @@ import { antelopeKnipConfig } from "@antelopejs/tooling-configs/knip"; export default antelopeKnipConfig({ - entry: ["src/tests/**/*.test.ts", "src/antelope.test.ts"], + entry: [ + "src/tests/**/*.test.ts", + "src/tests/**/*.untyped.spec.js", + "src/antelope.test.ts", + ], }); diff --git a/src/controller.ts b/src/controller.ts index 03d330c..4dfbddf 100644 --- a/src/controller.ts +++ b/src/controller.ts @@ -116,13 +116,10 @@ export type ParameterModifier = ( */ export interface ComputedParameter { /** - * Source of the parameter. It reads the context its decorator declared, - * which the route that registers it supplies, so it holds a - * `ParameterProvider` of any `RequestContext` subtype. + * Source of the parameter. A provider that declared a `RequestContext` + * subtype is stored here after `ControllerMeta.setProvider` widens it. */ - provider?: { - provide(context: RequestContext): ReturnType; - }["provide"]; + provider?: ParameterProvider; modifiers: ParameterModifier[]; } @@ -227,7 +224,9 @@ export class ControllerMeta { param: number | undefined, provider: ParameterProvider, ) { - this.getComputedParameter(key, param).provider = provider; + // SAFETY: Context names the slots that the caller's controller decorator or prefix handler sets on the request before this provider runs, and computeParameter passes that same request object, so the provider receives the Context it declared. + this.getComputedParameter(key, param).provider = + provider as ParameterProvider; } /** diff --git a/src/tests/route-lifecycle.test.ts b/src/tests/route-lifecycle.test.ts index a1e382b..96217ed 100644 --- a/src/tests/route-lifecycle.test.ts +++ b/src/tests/route-lifecycle.test.ts @@ -114,25 +114,6 @@ describe("Route lifecycle", () => { assert.equal(routesAt("/lifecycle/bystander"), 0); }); - it("refuses a handler that names no controller and registers nothing", () => { - const handler = handlerAt("/lifecycle/no-controller"); - // @ts-expect-error off-contract handler without its required controller; RegisterRoute must throw a TypeError at registration - delete handler.controller; - const before = getRegisteredRoutes().length; - - assert.throws( - () => RegisterRoute(handler), - (error) => - error instanceof TypeError && - error.message.includes("GET") && - error.message.includes("/lifecycle/no-controller") && - error.message.includes("controller"), - ); - - assert.equal(getRegisteredRoutes().length, before); - assert.deepEqual(proxyCalls, []); - }); - it("tolerates unregistering the same id twice", () => { const id = RegisterRoute(handlerAt("/lifecycle/twice")); UnregisterRoute(id); diff --git a/src/tests/route-lifecycle.untyped.spec.js b/src/tests/route-lifecycle.untyped.spec.js new file mode 100644 index 0000000..4aa8724 --- /dev/null +++ b/src/tests/route-lifecycle.untyped.spec.js @@ -0,0 +1,44 @@ +import assert from "node:assert"; + +import { getRegisteredRoutes, RegisterRoute, routesProxy } from "../index"; + +const proxyCalls = []; + +describe("Route lifecycle, off-contract handlers", () => { + before(() => { + routesProxy.onRegister((id) => { + proxyCalls.push({ kind: "register", id }); + }, true); + routesProxy.onUnregister((id) => { + proxyCalls.push({ kind: "unregister", id }); + }); + }); + + beforeEach(() => { + proxyCalls.length = 0; + }); + + it("refuses a handler that names no controller and registers nothing", () => { + const handler = { + location: "/lifecycle/no-controller", + method: "GET", + mode: "handler", + parameters: [], + properties: {}, + callback: async () => new Response(), + }; + const before = getRegisteredRoutes().length; + + assert.throws( + () => RegisterRoute(handler), + (error) => + error instanceof TypeError && + error.message.includes("GET") && + error.message.includes("/lifecycle/no-controller") && + error.message.includes("controller"), + ); + + assert.equal(getRegisteredRoutes().length, before); + assert.deepEqual(proxyCalls, []); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index a6cc7be..e24461a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,8 @@ "target": "esnext", "module": "commonjs", "lib": ["ES2022"], + "allowJs": true, + "checkJs": false, "types": ["node", "mocha"], "esModuleInterop": true, "strict": true,