mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-06 08:23:19 +08:00
The declaration-typed boundary already reaches zod 4 schemas and hono's
c.req.valid(): zod 4's z.infer and hono's validator Input generics resolve
to plain object types in the declaration world. Three gaps remained:
- Boolean literal types printed as `false` (the TS7 facade leaves
intrinsicName unset), so `{ ok: true } | { ok: false }` lost its true
arm and `boolean | undefined` became `undefined | false`. The printer
reads the literal's value.
- `r.data` after `const r = S.safeParse(x); if (r.success)` never
crossed: zod 4's result union is not data (its error arm is a class),
and reads on projectable receivers were skipped on the assumption that
something else types the receiver. Such reads now cross when the
receiver is a package call whose type does not project, or a const
bound to one.
- Reads through bindings annotated with a projected type no longer get a
redundant cast, and aliases are named only for casts that survive.
New npm cases zod4-boundary and hono-zod-validator run real zod 4.6.7,
hono 4.13.13 and @hono/zod-validator 0.9.1 (vendored file closures) under
Node and natively; decl-boundary.test.ts checks that c.req.valid() is a
checked cast with a path TypeError.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
382 lines
9.9 KiB
JavaScript
382 lines
9.9 KiB
JavaScript
import { HonoRequest } from "./request.js";
|
|
import { HtmlEscapedCallbackPhase, resolveCallback } from "./utils/html.js";
|
|
//#region src/context.ts
|
|
const TEXT_PLAIN = "text/plain; charset=UTF-8";
|
|
const setDefaultContentType = (contentType, headers) => {
|
|
return {
|
|
"Content-Type": contentType,
|
|
...headers
|
|
};
|
|
};
|
|
const createResponseInstance = (body, init) => new Response(body, init);
|
|
var Context = class {
|
|
#rawRequest;
|
|
#req;
|
|
/**
|
|
* `.env` can get bindings (environment variables, secrets, KV namespaces, D1 database, R2 bucket etc.) in Cloudflare Workers.
|
|
*
|
|
* @see {@link https://hono.dev/docs/api/context#env}
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* // Environment object for Cloudflare Workers
|
|
* app.get('*', async c => {
|
|
* const counter = c.env.COUNTER
|
|
* })
|
|
* ```
|
|
*/
|
|
env = {};
|
|
#var;
|
|
finalized = false;
|
|
/**
|
|
* `.error` can get the error object from the middleware if the Handler throws an error.
|
|
*
|
|
* @see {@link https://hono.dev/docs/api/context#error}
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* app.use('*', async (c, next) => {
|
|
* await next()
|
|
* if (c.error) {
|
|
* // do something...
|
|
* }
|
|
* })
|
|
* ```
|
|
*/
|
|
error;
|
|
#status;
|
|
#executionCtx;
|
|
#res;
|
|
#layout;
|
|
#renderer;
|
|
#notFoundHandler;
|
|
#preparedHeaders;
|
|
#matchResult;
|
|
#path;
|
|
/**
|
|
* Creates an instance of the Context class.
|
|
*
|
|
* @param req - The Request object.
|
|
* @param options - Optional configuration options for the context.
|
|
*/
|
|
constructor(req, options) {
|
|
this.#rawRequest = req;
|
|
if (options) {
|
|
this.#executionCtx = options.executionCtx;
|
|
this.env = options.env;
|
|
this.#notFoundHandler = options.notFoundHandler;
|
|
this.#path = options.path;
|
|
this.#matchResult = options.matchResult;
|
|
}
|
|
}
|
|
/**
|
|
* `.req` is the instance of {@link HonoRequest}.
|
|
*/
|
|
get req() {
|
|
this.#req ??= new HonoRequest(this.#rawRequest, this.#path, this.#matchResult);
|
|
return this.#req;
|
|
}
|
|
/**
|
|
* @see {@link https://hono.dev/docs/api/context#event}
|
|
* The FetchEvent associated with the current request.
|
|
*
|
|
* @throws Will throw an error if the context does not have a FetchEvent.
|
|
*/
|
|
get event() {
|
|
if (this.#executionCtx && "respondWith" in this.#executionCtx) return this.#executionCtx;
|
|
else throw Error("This context has no FetchEvent");
|
|
}
|
|
/**
|
|
* @see {@link https://hono.dev/docs/api/context#executionctx}
|
|
* The ExecutionContext associated with the current request.
|
|
*
|
|
* @throws Will throw an error if the context does not have an ExecutionContext.
|
|
*/
|
|
get executionCtx() {
|
|
if (this.#executionCtx) return this.#executionCtx;
|
|
else throw Error("This context has no ExecutionContext");
|
|
}
|
|
/**
|
|
* @see {@link https://hono.dev/docs/api/context#res}
|
|
* The Response object for the current request.
|
|
*/
|
|
get res() {
|
|
return this.#res ||= createResponseInstance(null, { headers: this.#preparedHeaders ??= new Headers() });
|
|
}
|
|
/**
|
|
* Sets the Response object for the current request.
|
|
*
|
|
* @param _res - The Response object to set.
|
|
*/
|
|
set res(_res) {
|
|
if (this.#res && _res) {
|
|
_res = createResponseInstance(_res.body, _res);
|
|
for (const [k, v] of this.#res.headers.entries()) {
|
|
if (k === "content-type") continue;
|
|
if (k === "set-cookie") {
|
|
const cookies = this.#res.headers.getSetCookie();
|
|
_res.headers.delete("set-cookie");
|
|
for (const cookie of cookies) _res.headers.append("set-cookie", cookie);
|
|
} else _res.headers.set(k, v);
|
|
}
|
|
}
|
|
this.#res = _res;
|
|
this.finalized = true;
|
|
}
|
|
/**
|
|
* `.render()` can create a response within a layout.
|
|
*
|
|
* @see {@link https://hono.dev/docs/api/context#render-setrenderer}
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* app.get('/', (c) => {
|
|
* return c.render('Hello!')
|
|
* })
|
|
* ```
|
|
*/
|
|
render = (...args) => {
|
|
this.#renderer ??= (content) => this.html(content);
|
|
return this.#renderer(...args);
|
|
};
|
|
/**
|
|
* Sets the layout for the response.
|
|
*
|
|
* @param layout - The layout to set.
|
|
* @returns The layout function.
|
|
*/
|
|
setLayout = (layout) => this.#layout = layout;
|
|
/**
|
|
* Gets the current layout for the response.
|
|
*
|
|
* @returns The current layout function.
|
|
*/
|
|
getLayout = () => this.#layout;
|
|
/**
|
|
* `.setRenderer()` can set the layout in the custom middleware.
|
|
*
|
|
* @see {@link https://hono.dev/docs/api/context#render-setrenderer}
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* app.use('*', async (c, next) => {
|
|
* c.setRenderer((content) => {
|
|
* return c.html(
|
|
* <html>
|
|
* <body>
|
|
* <p>{content}</p>
|
|
* </body>
|
|
* </html>
|
|
* )
|
|
* })
|
|
* await next()
|
|
* })
|
|
* ```
|
|
*/
|
|
setRenderer = (renderer) => {
|
|
this.#renderer = renderer;
|
|
};
|
|
/**
|
|
* `.header()` can set headers.
|
|
*
|
|
* @see {@link https://hono.dev/docs/api/context#header}
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* app.get('/welcome', (c) => {
|
|
* // Set headers
|
|
* c.header('X-Message', 'Hello!')
|
|
* c.header('Content-Type', 'text/plain')
|
|
*
|
|
* // Append multiple headers using the append option (e.g. Vary)
|
|
* c.header('Vary', 'Accept-Encoding', { append: true })
|
|
* c.header('Vary', 'User-Agent', { append: true })
|
|
*
|
|
* return c.body('Thank you for coming')
|
|
* })
|
|
* ```
|
|
*/
|
|
header = (name, value, options) => {
|
|
if (this.finalized) this.#res = createResponseInstance(this.#res.body, this.#res);
|
|
const headers = this.#res ? this.#res.headers : this.#preparedHeaders ??= new Headers();
|
|
if (value === void 0) headers.delete(name);
|
|
else if (options?.append) headers.append(name, value);
|
|
else headers.set(name, value);
|
|
};
|
|
status = (status) => {
|
|
this.#status = status;
|
|
};
|
|
/**
|
|
* `.set()` can set the value specified by the key.
|
|
*
|
|
* @see {@link https://hono.dev/docs/api/context#set-get}
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* app.use('*', async (c, next) => {
|
|
* c.set('message', 'Hono is hot!!')
|
|
* await next()
|
|
* })
|
|
* ```
|
|
*/
|
|
set = (key, value) => {
|
|
this.#var ??= /* @__PURE__ */ new Map();
|
|
this.#var.set(key, value);
|
|
};
|
|
/**
|
|
* `.get()` can use the value specified by the key.
|
|
*
|
|
* @see {@link https://hono.dev/docs/api/context#set-get}
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* app.get('/', (c) => {
|
|
* const message = c.get('message')
|
|
* return c.text(`The message is "${message}"`)
|
|
* })
|
|
* ```
|
|
*/
|
|
get = (key) => {
|
|
return this.#var ? this.#var.get(key) : void 0;
|
|
};
|
|
/**
|
|
* `.var` can access the value of a variable.
|
|
*
|
|
* @see {@link https://hono.dev/docs/api/context#var}
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* const result = c.var.client.oneMethod()
|
|
* ```
|
|
*/
|
|
get var() {
|
|
if (!this.#var) return {};
|
|
return Object.fromEntries(this.#var);
|
|
}
|
|
#newResponse(data, arg, headers) {
|
|
let responseHeaders = this.#res ? new Headers(this.#res.headers) : this.#preparedHeaders;
|
|
if (typeof arg === "object" && arg.headers) {
|
|
responseHeaders ??= new Headers();
|
|
for (const [key, value] of new Headers(arg.headers)) if (key === "set-cookie") responseHeaders.append(key, value);
|
|
else responseHeaders.set(key, value);
|
|
}
|
|
if (headers) {
|
|
if (!responseHeaders) {
|
|
let count = 0;
|
|
for (const k in headers) if (++count > 1 || typeof headers[k] !== "string") {
|
|
responseHeaders = new Headers();
|
|
break;
|
|
}
|
|
}
|
|
if (responseHeaders) for (const k in headers) {
|
|
const v = headers[k];
|
|
if (typeof v === "string") responseHeaders.set(k, v);
|
|
else {
|
|
responseHeaders.delete(k);
|
|
for (const v2 of v) responseHeaders.append(k, v2);
|
|
}
|
|
}
|
|
}
|
|
const status = typeof arg === "number" ? arg : arg?.status ?? this.#status;
|
|
return createResponseInstance(data, {
|
|
status,
|
|
headers: responseHeaders ?? headers
|
|
});
|
|
}
|
|
newResponse = (...args) => this.#newResponse(...args);
|
|
/**
|
|
* `.body()` can return the HTTP response.
|
|
* You can set headers with `.header()` and set HTTP status code with `.status`.
|
|
* This can also be set in `.text()`, `.json()` and so on.
|
|
*
|
|
* @see {@link https://hono.dev/docs/api/context#body}
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* app.get('/welcome', (c) => {
|
|
* // Set headers
|
|
* c.header('X-Message', 'Hello!')
|
|
* c.header('Content-Type', 'text/plain')
|
|
* // Set HTTP status code
|
|
* c.status(201)
|
|
*
|
|
* // Return the response body
|
|
* return c.body('Thank you for coming')
|
|
* })
|
|
* ```
|
|
*/
|
|
body = (data, arg, headers) => this.#newResponse(data, arg, headers);
|
|
/**
|
|
* `.text()` can render text as `Content-Type:text/plain`.
|
|
*
|
|
* @see {@link https://hono.dev/docs/api/context#text}
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* app.get('/say', (c) => {
|
|
* return c.text('Hello!')
|
|
* })
|
|
* ```
|
|
*/
|
|
text = (text, arg, headers) => {
|
|
return !this.#preparedHeaders && !this.#status && !arg && !headers && !this.finalized ? new Response(text) : this.#newResponse(text, arg, setDefaultContentType(TEXT_PLAIN, headers));
|
|
};
|
|
/**
|
|
* `.json()` can render JSON as `Content-Type:application/json`.
|
|
*
|
|
* @see {@link https://hono.dev/docs/api/context#json}
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* app.get('/api', (c) => {
|
|
* return c.json({ message: 'Hello!' })
|
|
* })
|
|
* ```
|
|
*/
|
|
json = (object, arg, headers) => {
|
|
return this.#newResponse(JSON.stringify(object), arg, setDefaultContentType("application/json", headers));
|
|
};
|
|
html = (html, arg, headers) => {
|
|
const res = (html) => this.#newResponse(html, arg, setDefaultContentType("text/html; charset=UTF-8", headers));
|
|
return typeof html === "object" ? resolveCallback(html, HtmlEscapedCallbackPhase.Stringify, false, {}).then(res) : res(html);
|
|
};
|
|
/**
|
|
* `.redirect()` can Redirect, default status code is 302.
|
|
*
|
|
* @see {@link https://hono.dev/docs/api/context#redirect}
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* app.get('/redirect', (c) => {
|
|
* return c.redirect('/')
|
|
* })
|
|
* app.get('/redirect-permanently', (c) => {
|
|
* return c.redirect('/', 301)
|
|
* })
|
|
* ```
|
|
*/
|
|
redirect = (location, status) => {
|
|
const locationString = String(location);
|
|
this.header("Location", !/[^\x00-\xFF]/.test(locationString) ? locationString : encodeURI(locationString));
|
|
return this.newResponse(null, status ?? 302);
|
|
};
|
|
/**
|
|
* `.notFound()` can return the Not Found Response.
|
|
*
|
|
* @see {@link https://hono.dev/docs/api/context#notfound}
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* app.get('/notfound', (c) => {
|
|
* return c.notFound()
|
|
* })
|
|
* ```
|
|
*/
|
|
notFound = () => {
|
|
this.#notFoundHandler ??= () => createResponseInstance();
|
|
return this.#notFoundHandler(this);
|
|
};
|
|
};
|
|
//#endregion
|
|
export { Context, TEXT_PLAIN };
|