@pithy-sh/core 0.9.0 → 0.11.0
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/access/access.d.ts +172 -0
- package/dist/access/access.d.ts.map +1 -0
- package/dist/access/access.js +147 -0
- package/dist/access/acting.d.ts +49 -0
- package/dist/access/acting.d.ts.map +1 -0
- package/dist/access/acting.js +21 -0
- package/dist/access/policyCheck.d.ts +33 -0
- package/dist/access/policyCheck.d.ts.map +1 -0
- package/dist/access/policyCheck.js +48 -0
- package/dist/access/usage.d.ts +83 -0
- package/dist/access/usage.d.ts.map +1 -0
- package/dist/access/usage.js +104 -0
- package/dist/access/visibility.d.ts +66 -0
- package/dist/access/visibility.d.ts.map +1 -0
- package/dist/access/visibility.js +86 -0
- package/dist/asyncapi/document.d.ts +46 -0
- package/dist/asyncapi/document.d.ts.map +1 -0
- package/dist/asyncapi/document.js +115 -0
- package/dist/audit/auditEvent.d.ts +5 -0
- package/dist/audit/auditEvent.d.ts.map +1 -1
- package/dist/audit/auditEvent.js +2 -0
- package/dist/capability/bindings.d.ts +47 -2
- package/dist/capability/bindings.d.ts.map +1 -1
- package/dist/capability/bindings.js +117 -7
- package/dist/capability/capability.d.ts +105 -28
- package/dist/capability/capability.d.ts.map +1 -1
- package/dist/capability/composition.d.ts.map +1 -1
- package/dist/capability/composition.js +5 -1
- package/dist/capability/manifest.d.ts +2 -1
- package/dist/capability/manifest.d.ts.map +1 -1
- package/dist/capability/manifest.js +1 -1
- package/dist/capability/settings.d.ts +96 -4
- package/dist/capability/settings.d.ts.map +1 -1
- package/dist/capability/settings.js +85 -4
- package/dist/cli/command.d.ts +364 -0
- package/dist/cli/command.d.ts.map +1 -0
- package/dist/cli/command.js +418 -0
- package/dist/cli/commandCatalog.generated.d.ts +2437 -0
- package/dist/cli/commandCatalog.generated.d.ts.map +1 -0
- package/dist/cli/commandCatalog.generated.js +2564 -0
- package/dist/cli/commandFacts.d.ts +164 -0
- package/dist/cli/commandFacts.d.ts.map +1 -0
- package/dist/cli/commandFacts.js +216 -0
- package/dist/controlPlane/capability.d.ts.map +1 -1
- package/dist/controlPlane/capability.js +12 -2
- package/dist/controlPlane/context.d.ts +5 -0
- package/dist/controlPlane/context.d.ts.map +1 -1
- package/dist/controlPlane/context.js +3 -1
- package/dist/controlPlane/data/connection.d.ts +47 -0
- package/dist/controlPlane/data/connection.d.ts.map +1 -1
- package/dist/controlPlane/data/connection.js +55 -1
- package/dist/controlPlane/error/errors.d.ts.map +1 -1
- package/dist/controlPlane/error/errors.js +4 -3
- package/dist/controlPlane/http/cors.d.ts +4 -4
- package/dist/controlPlane/http/cors.js +4 -4
- package/dist/controlPlane/http/guard.d.ts +11 -0
- package/dist/controlPlane/http/guard.d.ts.map +1 -1
- package/dist/controlPlane/http/guard.js +28 -2
- package/dist/controlPlane/http/handlers.d.ts +60 -0
- package/dist/controlPlane/http/handlers.d.ts.map +1 -1
- package/dist/controlPlane/http/handlers.js +31 -2
- package/dist/controlPlane/http/readiness.d.ts +33 -0
- package/dist/controlPlane/http/readiness.d.ts.map +1 -0
- package/dist/controlPlane/http/readiness.js +39 -0
- package/dist/controlPlane/http/responses.d.ts +147 -0
- package/dist/controlPlane/http/responses.d.ts.map +1 -1
- package/dist/controlPlane/http/responses.js +61 -4
- package/dist/controlPlane/http/routes.d.ts +2 -1
- package/dist/controlPlane/http/routes.d.ts.map +1 -1
- package/dist/controlPlane/http/routes.js +50 -9
- package/dist/controlPlane/http/verify.d.ts.map +1 -1
- package/dist/controlPlane/http/verify.js +2 -1
- package/dist/controlPlane/migrationState/migrationState.d.ts +102 -0
- package/dist/controlPlane/migrationState/migrationState.d.ts.map +1 -0
- package/dist/controlPlane/migrationState/migrationState.js +142 -0
- package/dist/controlPlane/migrations/0003_connection_principal.d.ts +21 -0
- package/dist/controlPlane/migrations/0003_connection_principal.d.ts.map +1 -0
- package/dist/controlPlane/migrations/0003_connection_principal.js +33 -0
- package/dist/controlPlane/scope/scope.d.ts +13 -2
- package/dist/controlPlane/scope/scope.d.ts.map +1 -1
- package/dist/controlPlane/scope/scope.js +14 -3
- package/dist/createBackend.d.ts +7 -0
- package/dist/createBackend.d.ts.map +1 -1
- package/dist/createBackend.js +41 -6
- package/dist/createEntrypoint.d.ts.map +1 -1
- package/dist/createEntrypoint.js +5 -3
- package/dist/entitlement/allowance.d.ts +25 -0
- package/dist/entitlement/allowance.d.ts.map +1 -0
- package/dist/entitlement/allowance.js +80 -0
- package/dist/entitlement/entitlement.d.ts +41 -0
- package/dist/entitlement/entitlement.d.ts.map +1 -1
- package/dist/entitlement/meter.d.ts +57 -0
- package/dist/entitlement/meter.d.ts.map +1 -0
- package/dist/entitlement/meter.js +13 -0
- package/dist/entitlement/require.js +1 -1
- package/dist/error/cause.d.ts +16 -116
- package/dist/error/cause.d.ts.map +1 -1
- package/dist/error/cause.js +19 -153
- package/dist/error/http.d.ts +184 -0
- package/dist/error/http.d.ts.map +1 -1
- package/dist/error/payload.d.ts +368 -0
- package/dist/error/payload.d.ts.map +1 -1
- package/dist/error/payload.js +126 -4
- package/dist/error/pithyError.d.ts +44 -0
- package/dist/error/pithyError.d.ts.map +1 -1
- package/dist/error/pithyError.js +56 -1
- package/dist/error/unresolved.d.ts +265 -0
- package/dist/error/unresolved.d.ts.map +1 -0
- package/dist/error/unresolved.js +599 -0
- package/dist/error/wire.d.ts +20 -0
- package/dist/error/wire.d.ts.map +1 -0
- package/dist/error/wire.js +19 -0
- package/dist/http/authContext.d.ts +7 -0
- package/dist/http/authContext.d.ts.map +1 -1
- package/dist/http/authContext.js +8 -1
- package/dist/http/oidcWebhook.d.ts +3 -3
- package/dist/http/oidcWebhook.js +3 -3
- package/dist/http/routeContract.js +1 -1
- package/dist/http/routeSpec.d.ts +66 -0
- package/dist/http/routeSpec.d.ts.map +1 -0
- package/dist/http/routeSpec.js +40 -0
- package/dist/http/sameOrigin.d.ts +1 -1
- package/dist/http/sameOrigin.js +1 -1
- package/dist/http/scopeGates.d.ts +20 -0
- package/dist/http/scopeGates.d.ts.map +1 -0
- package/dist/http/scopeGates.js +36 -0
- package/dist/http/signedWebhook.d.ts +3 -3
- package/dist/http/signedWebhook.js +3 -3
- package/dist/http/socket.d.ts +52 -0
- package/dist/http/socket.d.ts.map +1 -0
- package/dist/http/socket.js +2 -0
- package/dist/http/subprotocol.d.ts +29 -0
- package/dist/http/subprotocol.d.ts.map +1 -0
- package/dist/http/subprotocol.js +37 -0
- package/dist/http/validation.d.ts +40 -8
- package/dist/http/validation.d.ts.map +1 -1
- package/dist/http/validation.js +25 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/logger/record.d.ts +107 -0
- package/dist/logger/record.d.ts.map +1 -1
- package/dist/migrations/bookkeeping.d.ts +21 -0
- package/dist/migrations/bookkeeping.d.ts.map +1 -1
- package/dist/migrations/bookkeeping.js +32 -3
- package/dist/migrations/capabilitySets.d.ts +23 -0
- package/dist/migrations/capabilitySets.d.ts.map +1 -0
- package/dist/migrations/capabilitySets.js +74 -0
- package/dist/migrations/groups.d.ts +140 -0
- package/dist/migrations/groups.d.ts.map +1 -0
- package/dist/migrations/groups.js +191 -0
- package/dist/migrations/migrateStep.d.ts +13 -0
- package/dist/migrations/migrateStep.d.ts.map +1 -0
- package/dist/migrations/migrateStep.js +20 -0
- package/dist/migrations/owner.d.ts +7 -0
- package/dist/migrations/owner.d.ts.map +1 -1
- package/dist/migrations/owner.js +11 -7
- package/dist/migrations/registry.d.ts +26 -0
- package/dist/migrations/registry.d.ts.map +1 -1
- package/dist/migrations/registry.js +38 -7
- package/dist/migrations/retained.js +2 -2
- package/dist/migrations/runner.d.ts +76 -5
- package/dist/migrations/runner.d.ts.map +1 -1
- package/dist/migrations/runner.js +130 -19
- package/dist/migrations/state.d.ts +60 -0
- package/dist/migrations/state.d.ts.map +1 -0
- package/dist/migrations/state.js +46 -0
- package/dist/naming/domains.d.ts +17 -0
- package/dist/naming/domains.d.ts.map +1 -1
- package/dist/naming/domains.js +62 -4
- package/dist/naming/environment.d.ts.map +1 -1
- package/dist/naming/environment.js +2 -1
- package/dist/naming/feature.d.ts +27 -5
- package/dist/naming/feature.d.ts.map +1 -1
- package/dist/naming/feature.js +36 -6
- package/dist/naming/limits.d.ts +47 -3
- package/dist/naming/limits.d.ts.map +1 -1
- package/dist/naming/limits.js +47 -3
- package/dist/naming/packageName.d.ts +17 -0
- package/dist/naming/packageName.d.ts.map +1 -0
- package/dist/naming/packageName.js +22 -0
- package/dist/naming/provisionScope.d.ts +6 -0
- package/dist/naming/provisionScope.d.ts.map +1 -1
- package/dist/naming/provisionScope.js +10 -4
- package/dist/naming/resourceNames.d.ts +21 -2
- package/dist/naming/resourceNames.d.ts.map +1 -1
- package/dist/naming/resourceNames.js +21 -3
- package/dist/openapi/document.d.ts +106 -0
- package/dist/openapi/document.d.ts.map +1 -0
- package/dist/openapi/document.js +314 -0
- package/dist/openapi/routeGroups.d.ts +8 -0
- package/dist/openapi/routeGroups.d.ts.map +1 -0
- package/dist/openapi/routeGroups.js +23 -0
- package/dist/seed/writeD1.d.ts +7 -1
- package/dist/seed/writeD1.d.ts.map +1 -1
- package/dist/seed/writeD1.js +9 -3
- package/dist/seed/writeKv.d.ts +2 -0
- package/dist/seed/writeKv.d.ts.map +1 -1
- package/dist/seed/writeKv.js +6 -2
- package/dist/text/comments.js +66 -4
- package/dist/text/literals.d.ts +43 -0
- package/dist/text/literals.d.ts.map +1 -0
- package/dist/text/literals.js +293 -0
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.d.ts.map +1 -1
- package/dist/version.generated.js +1 -1
- package/dist/worker/serviceLoopback.d.ts +36 -0
- package/dist/worker/serviceLoopback.d.ts.map +1 -0
- package/dist/worker/serviceLoopback.js +74 -0
- package/dist/workflow/dispatchRoute.d.ts.map +1 -1
- package/dist/workflow/dispatchRoute.js +12 -6
- package/dist/workflow/faults.d.ts +12 -1
- package/dist/workflow/faults.d.ts.map +1 -1
- package/dist/workflow/faults.js +24 -18
- package/dist/workflow/hostEnv.d.ts +1 -0
- package/dist/workflow/hostEnv.d.ts.map +1 -1
- package/dist/workflow/hostEnv.js +4 -2
- package/dist/workflow/loopback.d.ts.map +1 -1
- package/dist/workflow/loopback.js +2 -1
- package/package.json +4 -4
- package/src/access/access.ts +298 -0
- package/src/access/acting.ts +59 -0
- package/src/access/policyCheck.ts +77 -0
- package/src/access/usage.ts +141 -0
- package/src/access/visibility.ts +109 -0
- package/src/asyncapi/document.ts +164 -0
- package/src/audit/auditEvent.ts +4 -0
- package/src/capability/bindings.ts +159 -13
- package/src/capability/capability.ts +106 -29
- package/src/capability/composition.ts +2 -1
- package/src/capability/settings.ts +165 -4
- package/src/cli/command.ts +865 -0
- package/src/cli/commandCatalog.generated.ts +2723 -0
- package/src/cli/commandFacts.ts +220 -0
- package/src/controlPlane/capability.ts +25 -1
- package/src/controlPlane/context.ts +4 -0
- package/src/controlPlane/data/connection.ts +90 -0
- package/src/controlPlane/error/errors.ts +10 -3
- package/src/controlPlane/http/cors.ts +4 -4
- package/src/controlPlane/http/guard.ts +35 -2
- package/src/controlPlane/http/handlers.ts +41 -0
- package/src/controlPlane/http/readiness.ts +50 -0
- package/src/controlPlane/http/responses.ts +140 -2
- package/src/controlPlane/http/routes.ts +82 -10
- package/src/controlPlane/http/verify.ts +2 -0
- package/src/controlPlane/migrationState/migrationState.ts +200 -0
- package/src/controlPlane/migrations/0003_connection_principal.ts +37 -0
- package/src/controlPlane/scope/scope.ts +14 -2
- package/src/createBackend.ts +59 -8
- package/src/createEntrypoint.ts +9 -3
- package/src/entitlement/allowance.ts +85 -0
- package/src/entitlement/entitlement.ts +38 -0
- package/src/entitlement/meter.ts +69 -0
- package/src/entitlement/require.ts +1 -1
- package/src/error/cause.ts +18 -181
- package/src/error/payload.ts +270 -4
- package/src/error/pithyError.ts +76 -0
- package/src/error/unresolved.ts +853 -0
- package/src/error/wire.ts +40 -0
- package/src/http/authContext.ts +13 -1
- package/src/http/oidcWebhook.ts +3 -3
- package/src/http/routeContract.ts +1 -1
- package/src/http/routeSpec.ts +81 -0
- package/src/http/sameOrigin.ts +1 -1
- package/src/http/scopeGates.ts +34 -0
- package/src/http/signedWebhook.ts +3 -3
- package/src/http/socket.ts +55 -0
- package/src/http/subprotocol.ts +47 -0
- package/src/http/validation.ts +50 -8
- package/src/index.ts +1 -0
- package/src/migrations/bookkeeping.ts +39 -2
- package/src/migrations/capabilitySets.ts +87 -0
- package/src/migrations/groups.ts +295 -0
- package/src/migrations/migrateStep.ts +19 -0
- package/src/migrations/owner.ts +10 -6
- package/src/migrations/registry.ts +46 -2
- package/src/migrations/retained.ts +2 -2
- package/src/migrations/runner.ts +214 -22
- package/src/migrations/state.ts +94 -0
- package/src/naming/domains.ts +75 -16
- package/src/naming/environment.ts +2 -1
- package/src/naming/feature.ts +46 -7
- package/src/naming/limits.ts +35 -3
- package/src/naming/packageName.ts +20 -0
- package/src/naming/provisionScope.ts +21 -2
- package/src/naming/resourceNames.ts +52 -5
- package/src/openapi/document.ts +412 -0
- package/src/openapi/routeGroups.ts +25 -0
- package/src/seed/writeD1.ts +13 -4
- package/src/seed/writeKv.ts +6 -2
- package/src/text/comments.ts +56 -4
- package/src/text/literals.ts +294 -0
- package/src/version.generated.ts +1 -1
- package/src/worker/serviceLoopback.ts +105 -0
- package/src/workflow/dispatchRoute.ts +13 -6
- package/src/workflow/faults.ts +56 -27
- package/src/workflow/hostEnv.ts +9 -1
- package/src/workflow/loopback.ts +2 -1
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import type { Hono } from "hono";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { CONTROL_PLANE_HEADER } from "../controlPlane/wire";
|
|
7
|
+
import { ErrorBody } from "../error/wire";
|
|
8
|
+
import { pathParams } from "../http/routeContract";
|
|
9
|
+
import { type DocumentedMethod, type RouteResponse, type RouteSpec, routeSpecOf } from "../http/routeSpec";
|
|
10
|
+
import { type RequestContract, requestContractOf } from "../http/validation";
|
|
11
|
+
import type { VerificationStrategy } from "../http/verification";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* **The OpenAPI contract, read off a composed app.**
|
|
15
|
+
*
|
|
16
|
+
* Nothing here is written by hand. A route's request schemas come from its `validate` middleware, its
|
|
17
|
+
* summary, verification and responses from its `describeRoute`, and both are found by walking
|
|
18
|
+
* `app.routes` — the same table Hono dispatches from. So the document describes exactly what the Worker
|
|
19
|
+
* serves, the adopter's own routes included, and a schema edit reaches it with no second step.
|
|
20
|
+
*
|
|
21
|
+
* Zod is already the source of truth for every request and response, and every field carries a
|
|
22
|
+
* `.describe()` (CLAUDE.md §Zod). `z.toJSONSchema` keeps those descriptions, which is what lets a client
|
|
23
|
+
* generated in Swift or Kotlin carry the same documentation as the TypeScript one.
|
|
24
|
+
*
|
|
25
|
+
* OpenAPI 3.1, whose schema dialect is JSON Schema 2020-12 — the dialect `z.toJSONSchema` writes — so
|
|
26
|
+
* every schema crosses unchanged.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** The OpenAPI version this module writes. */
|
|
30
|
+
export const OPENAPI_VERSION = "3.1.0";
|
|
31
|
+
|
|
32
|
+
/** One entry of a Hono app's route table: a method, a path and one handler. */
|
|
33
|
+
export type RouterRoute = Hono["routes"][number];
|
|
34
|
+
|
|
35
|
+
/** One capability's routes, as registered by its `routes` hook. Becomes one tag. */
|
|
36
|
+
export interface RouteGroup {
|
|
37
|
+
/** The tag, normally the capability's name. */
|
|
38
|
+
readonly tag: string;
|
|
39
|
+
/** What the capability does, for the tag's description. */
|
|
40
|
+
readonly description?: string;
|
|
41
|
+
/** The routes it registered, in registration order. */
|
|
42
|
+
readonly routes: readonly RouterRoute[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The document's `info` block. */
|
|
46
|
+
export interface ContractInfo {
|
|
47
|
+
readonly title: string;
|
|
48
|
+
readonly version: string;
|
|
49
|
+
readonly description?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A JSON Schema object, as `z.toJSONSchema` writes one. */
|
|
53
|
+
export type JsonSchema = Record<string, unknown>;
|
|
54
|
+
|
|
55
|
+
/** The subset of an OpenAPI 3.1 document this module writes. */
|
|
56
|
+
export interface OpenApiDocument {
|
|
57
|
+
openapi: string;
|
|
58
|
+
info: ContractInfo;
|
|
59
|
+
tags: { name: string; description?: string }[];
|
|
60
|
+
paths: Record<string, Record<string, unknown>>;
|
|
61
|
+
components: {
|
|
62
|
+
schemas: Record<string, JsonSchema>;
|
|
63
|
+
responses: Record<string, unknown>;
|
|
64
|
+
securitySchemes: Record<string, unknown>;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A route that registered no `describeRoute`, so the contract can say nothing true about it. */
|
|
69
|
+
export interface UndescribedRoute {
|
|
70
|
+
readonly method: string;
|
|
71
|
+
readonly path: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** How each verification strategy is presented on the wire. `signed-webhook` and `public` have no scheme. */
|
|
75
|
+
const SECURITY_SCHEMES: Record<string, unknown> = {
|
|
76
|
+
bearer: {
|
|
77
|
+
type: "http",
|
|
78
|
+
scheme: "bearer",
|
|
79
|
+
description: "A short-lived access token from `@pithy-sh/auth`, sent as `Authorization: Bearer <token>`.",
|
|
80
|
+
},
|
|
81
|
+
session: {
|
|
82
|
+
type: "apiKey",
|
|
83
|
+
in: "cookie",
|
|
84
|
+
name: "better-auth.session_token",
|
|
85
|
+
description:
|
|
86
|
+
"A browser session cookie from `@pithy-sh/auth`, named `__Secure-better-auth.session_token` over HTTPS. A state-changing request also needs its `Origin` to be one the Worker trusts.",
|
|
87
|
+
},
|
|
88
|
+
controlPlane: {
|
|
89
|
+
type: "apiKey",
|
|
90
|
+
in: "header",
|
|
91
|
+
name: CONTROL_PLANE_HEADER,
|
|
92
|
+
description: "A customer-issued, scoped control-plane credential. Default-denied.",
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** The `security` requirement for each strategy that has one. */
|
|
97
|
+
const REQUIREMENT: Partial<Record<VerificationStrategy, Record<string, string[]>>> = {
|
|
98
|
+
bearer: { bearer: [] },
|
|
99
|
+
session: { session: [] },
|
|
100
|
+
"control-plane": { controlPlane: [] },
|
|
101
|
+
public: {},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const ERROR_RESPONSE = "Error";
|
|
105
|
+
|
|
106
|
+
/** Hono's `/a/:id{[0-9]+}/*` as OpenAPI's `/a/{id}/{path}`. */
|
|
107
|
+
export function openApiPath(path: string): string {
|
|
108
|
+
return path
|
|
109
|
+
.replace(/:([A-Za-z0-9_]+)(\{[^}]*\})?\??/g, "{$1}")
|
|
110
|
+
.replace(/\/\*$/, "/{path}")
|
|
111
|
+
.replace(/\*/g, "{path}");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* `get` + `/leaderboard/{board}/top` → `getLeaderboardByBoardTop`. A parameter reads as `By<Name>`, so
|
|
116
|
+
* `/items/{id}` and `/items/id` do not meet. Stable, but not unique on its own — `/a-b` and `/a/b` still
|
|
117
|
+
* share one — so {@link openApiDocument} numbers a repeat rather than writing an invalid document.
|
|
118
|
+
*/
|
|
119
|
+
export function operationId(method: string, path: string): string {
|
|
120
|
+
const words = path
|
|
121
|
+
.split("/")
|
|
122
|
+
.flatMap((segment) => {
|
|
123
|
+
const param = /^\{(.+)\}$/.exec(segment);
|
|
124
|
+
return param ? ["by", ...(param[1] ?? "").split(/[^A-Za-z0-9]+/)] : segment.split(/[^A-Za-z0-9]+/);
|
|
125
|
+
})
|
|
126
|
+
.filter((word) => word.length > 0)
|
|
127
|
+
.map((word) => `${word[0]?.toUpperCase()}${word.slice(1)}`);
|
|
128
|
+
return `${method.toLowerCase()}${words.join("")}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** A Zod schema as JSON Schema, without the `$schema` key OpenAPI does not want inside a document. */
|
|
132
|
+
export function jsonSchema(schema: z.ZodType, io: "input" | "output"): JsonSchema {
|
|
133
|
+
const { $schema: _dialect, ...rest } = z.toJSONSchema(schema, { io, unrepresentable: "any" }) as JsonSchema;
|
|
134
|
+
return rest;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** One route's handlers, grouped by method and path the way Hono stores them: one entry per handler. */
|
|
138
|
+
interface Operation {
|
|
139
|
+
readonly method: string;
|
|
140
|
+
readonly path: string;
|
|
141
|
+
readonly handlers: unknown[];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function operationsOf(routes: readonly RouterRoute[]): Operation[] {
|
|
145
|
+
const byKey = new Map<string, Operation>();
|
|
146
|
+
for (const route of routes) {
|
|
147
|
+
const key = `${route.method} ${route.path}`;
|
|
148
|
+
const entry = byKey.get(key) ?? { method: route.method, path: route.path, handlers: [] };
|
|
149
|
+
entry.handlers.push(route.handler);
|
|
150
|
+
byKey.set(key, entry);
|
|
151
|
+
}
|
|
152
|
+
return [...byKey.values()];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* An `ALL` entry with no spec is `app.use` — middleware, not a route — because Hono records both as
|
|
157
|
+
* `ALL`. An `app.all` route that forgot its `describeRoute` is therefore invisible to the coverage
|
|
158
|
+
* check, which is the one blind spot here; the kit has one such route and it is described.
|
|
159
|
+
*/
|
|
160
|
+
function isMiddlewareOnly(operation: Operation): boolean {
|
|
161
|
+
return operation.method === "ALL" && !operation.handlers.some((handler) => routeSpecOf(handler));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Every route in these groups that registered no `describeRoute`. Empty is the contract being met. */
|
|
165
|
+
export function undescribedRoutes(groups: readonly RouteGroup[]): UndescribedRoute[] {
|
|
166
|
+
return groups.flatMap((group) =>
|
|
167
|
+
operationsOf(group.routes)
|
|
168
|
+
.filter((operation) => !isMiddlewareOnly(operation))
|
|
169
|
+
.filter((operation) => !operation.handlers.some((handler) => routeSpecOf(handler)))
|
|
170
|
+
.map(({ method, path }) => ({ method, path })),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The parameters one validator contributes: one per property of its object schema. */
|
|
175
|
+
function parametersOf(contract: RequestContract): Record<string, unknown>[] {
|
|
176
|
+
const location = { param: "path", query: "query", header: "header", cookie: "cookie" }[contract.target as string];
|
|
177
|
+
if (location === undefined) return [];
|
|
178
|
+
const schema = jsonSchema(contract.schema, "input");
|
|
179
|
+
const properties = (schema.properties ?? {}) as Record<string, JsonSchema>;
|
|
180
|
+
const required = new Set((schema.required ?? []) as string[]);
|
|
181
|
+
return Object.entries(properties).map(([name, property]) => {
|
|
182
|
+
const { description, ...rest } = property;
|
|
183
|
+
return {
|
|
184
|
+
name,
|
|
185
|
+
in: location,
|
|
186
|
+
required: location === "path" || required.has(name),
|
|
187
|
+
...(typeof description === "string" ? { description } : {}),
|
|
188
|
+
schema: rest,
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The request body one validator contributes, if it reads the body. */
|
|
194
|
+
function requestBodyOf(contract: RequestContract): Record<string, unknown> | undefined {
|
|
195
|
+
const media = { json: "application/json", form: "multipart/form-data" }[contract.target as string];
|
|
196
|
+
if (media === undefined) return undefined;
|
|
197
|
+
const schema = jsonSchema(contract.schema, "input");
|
|
198
|
+
return {
|
|
199
|
+
required: !contract.schema.safeParse(undefined).success,
|
|
200
|
+
...(typeof schema.description === "string" ? { description: schema.description } : {}),
|
|
201
|
+
content: { [media]: { schema } },
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function isZodSchema(value: z.ZodType | RouteResponse): value is z.ZodType {
|
|
206
|
+
return value instanceof z.ZodType;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** One `responses` entry. A bare schema is a JSON body; a {@link RouteResponse} says what it is. */
|
|
210
|
+
function responseOf(declared: z.ZodType | RouteResponse): Record<string, unknown> {
|
|
211
|
+
if (isZodSchema(declared)) {
|
|
212
|
+
const schema = jsonSchema(declared, "input");
|
|
213
|
+
return {
|
|
214
|
+
description: typeof schema.description === "string" ? schema.description : "Success.",
|
|
215
|
+
content: { "application/json": { schema } },
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const media = declared.contentType ?? (declared.schema ? "application/json" : undefined);
|
|
219
|
+
return {
|
|
220
|
+
description: declared.description,
|
|
221
|
+
...(media === undefined
|
|
222
|
+
? {}
|
|
223
|
+
: { content: { [media]: { schema: declared.schema ? jsonSchema(declared.schema, "input") : {} } } }),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function strategiesOf(spec: RouteSpec): readonly VerificationStrategy[] {
|
|
228
|
+
return typeof spec.verification === "string" ? [spec.verification] : spec.verification;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** One OpenAPI operation for a described route. */
|
|
232
|
+
function operationOf(tag: string, method: string, operation: Operation, spec: RouteSpec): Record<string, unknown> {
|
|
233
|
+
const contracts = operation.handlers.flatMap((handler) => {
|
|
234
|
+
const contract = requestContractOf(handler);
|
|
235
|
+
return contract ? [contract] : [];
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const parameters = contracts.flatMap(parametersOf);
|
|
239
|
+
// A `:segment` with no param validator still has to be declared, or the document is invalid. It is
|
|
240
|
+
// declared bare — no description — which is exactly what the field-description sweep then reports.
|
|
241
|
+
for (const name of pathParams(operation.path)) {
|
|
242
|
+
if (!parameters.some((parameter) => parameter.in === "path" && parameter.name === name)) {
|
|
243
|
+
parameters.push({ name, in: "path", required: true, schema: { type: "string" } });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (/\*/.test(operation.path)) {
|
|
247
|
+
parameters.push({
|
|
248
|
+
name: "path",
|
|
249
|
+
in: "path",
|
|
250
|
+
required: true,
|
|
251
|
+
description: "The rest of the path, passed through to the handler.",
|
|
252
|
+
schema: { type: "string" },
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const body = contracts.map(requestBodyOf).find((candidate) => candidate !== undefined);
|
|
257
|
+
const strategies = strategiesOf(spec);
|
|
258
|
+
const security = strategies.flatMap((strategy) => {
|
|
259
|
+
const requirement = REQUIREMENT[strategy];
|
|
260
|
+
return requirement === undefined ? [] : [requirement];
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const responses: Record<string, unknown> = {};
|
|
264
|
+
for (const [status, declared] of Object.entries(spec.responses)) responses[status] = responseOf(declared);
|
|
265
|
+
responses.default = { $ref: `#/components/responses/${ERROR_RESPONSE}` };
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
tags: [tag],
|
|
269
|
+
operationId: operationId(method, openApiPath(operation.path)),
|
|
270
|
+
summary: spec.summary,
|
|
271
|
+
...(spec.description === undefined ? {} : { description: spec.description }),
|
|
272
|
+
...(parameters.length === 0 ? {} : { parameters }),
|
|
273
|
+
...(body === undefined ? {} : { requestBody: body }),
|
|
274
|
+
responses,
|
|
275
|
+
...(strategies.length === 0 ? {} : { security, "x-pithy-verification": strategies }),
|
|
276
|
+
...(spec.scopes === undefined || spec.scopes.length === 0 ? {} : { "x-pithy-scopes": spec.scopes }),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** The methods an operation is documented under: its own, or for `app.all` the ones its spec names. */
|
|
281
|
+
function methodsOf(operation: Operation, spec: RouteSpec): readonly DocumentedMethod[] {
|
|
282
|
+
if (operation.method !== "ALL") return [operation.method.toLowerCase() as DocumentedMethod];
|
|
283
|
+
return spec.methods ?? ["get", "post"];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** How {@link openApiDocument} treats a route with no `describeRoute`. */
|
|
287
|
+
export interface DocumentOptions {
|
|
288
|
+
/**
|
|
289
|
+
* `omit` (the default) leaves it out, which is right for the kit: {@link undescribedRoutes} keeps that
|
|
290
|
+
* set empty, so anything missing is a bug the gate names. `include` writes what the validators alone
|
|
291
|
+
* can say — the path, its parameters and body — marked `x-pithy-undescribed`, which is right for an
|
|
292
|
+
* adopter's own routes: a client generated from the contract still reaches them, and the mark says
|
|
293
|
+
* which ones to describe.
|
|
294
|
+
*/
|
|
295
|
+
readonly undescribed?: "omit" | "include";
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** The spec an undescribed route is written with under `undescribed: "include"`. It claims nothing. */
|
|
299
|
+
const UNDESCRIBED: RouteSpec = {
|
|
300
|
+
summary: "Not described. Add describeRoute to this route.",
|
|
301
|
+
verification: [],
|
|
302
|
+
responses: {},
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The OpenAPI document for these route groups. Undescribed routes are left out rather than guessed at,
|
|
307
|
+
* unless {@link DocumentOptions.undescribed} says otherwise; {@link undescribedRoutes} is the check that
|
|
308
|
+
* keeps that set empty.
|
|
309
|
+
*/
|
|
310
|
+
export function openApiDocument(
|
|
311
|
+
info: ContractInfo,
|
|
312
|
+
groups: readonly RouteGroup[],
|
|
313
|
+
options: DocumentOptions = {},
|
|
314
|
+
): OpenApiDocument {
|
|
315
|
+
const paths: OpenApiDocument["paths"] = {};
|
|
316
|
+
const tags: OpenApiDocument["tags"] = [];
|
|
317
|
+
const ids = new Map<string, number>();
|
|
318
|
+
/** An operation id no earlier operation took: the derived one, or it with the next free number. */
|
|
319
|
+
const unique = (id: string): string => {
|
|
320
|
+
const seen = ids.get(id) ?? 0;
|
|
321
|
+
ids.set(id, seen + 1);
|
|
322
|
+
return seen === 0 ? id : `${id}${seen + 1}`;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
for (const group of groups) {
|
|
326
|
+
let documented = false;
|
|
327
|
+
for (const operation of operationsOf(group.routes)) {
|
|
328
|
+
if (isMiddlewareOnly(operation)) continue;
|
|
329
|
+
const declared = operation.handlers.map(routeSpecOf).find((candidate) => candidate !== undefined);
|
|
330
|
+
if (declared === undefined && options.undescribed !== "include") continue;
|
|
331
|
+
const spec = declared ?? UNDESCRIBED;
|
|
332
|
+
documented = true;
|
|
333
|
+
const path = openApiPath(operation.path);
|
|
334
|
+
const item = paths[path] ?? {};
|
|
335
|
+
for (const method of methodsOf(operation, spec)) {
|
|
336
|
+
const derived = operationOf(group.tag, method, operation, spec);
|
|
337
|
+
const written = { ...derived, operationId: unique(String(derived.operationId)) };
|
|
338
|
+
item[method] = declared === undefined ? { ...written, "x-pithy-undescribed": true } : written;
|
|
339
|
+
}
|
|
340
|
+
paths[path] = item;
|
|
341
|
+
}
|
|
342
|
+
if (documented) tags.push({ name: group.tag, ...(group.description ? { description: group.description } : {}) });
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
openapi: OPENAPI_VERSION,
|
|
347
|
+
info,
|
|
348
|
+
tags,
|
|
349
|
+
paths,
|
|
350
|
+
components: {
|
|
351
|
+
schemas: { [ERROR_RESPONSE]: jsonSchema(ErrorBody, "input") },
|
|
352
|
+
responses: {
|
|
353
|
+
[ERROR_RESPONSE]: {
|
|
354
|
+
description: "Any failure. The status is the error's own; the body is always this shape.",
|
|
355
|
+
content: { "application/json": { schema: { $ref: `#/components/schemas/${ERROR_RESPONSE}` } } },
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
securitySchemes: SECURITY_SCHEMES,
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** One schema property somewhere in the document that carries no `description`. */
|
|
364
|
+
export interface UndescribedField {
|
|
365
|
+
/** A JSON pointer to the property, e.g. `/paths/~1leaderboard/get/responses/200/…/properties/rank`. */
|
|
366
|
+
readonly pointer: string;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const escapePointer = (segment: string) => segment.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Every object property and every parameter in the document without a description. The kit holds every
|
|
373
|
+
* Zod field to `.describe()`, and this is how that rule is checked on the wire shape a client actually
|
|
374
|
+
* generates from, rather than on the source it came from.
|
|
375
|
+
*/
|
|
376
|
+
export function undescribedFields(document: OpenApiDocument): UndescribedField[] {
|
|
377
|
+
return undescribedFieldsIn(document.paths, "/paths");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* The same sweep over any part of a contract document, from a JSON pointer: every object property and every
|
|
382
|
+
* OpenAPI parameter below `node` without a description. The AsyncAPI document runs it over its messages.
|
|
383
|
+
*/
|
|
384
|
+
export function undescribedFieldsIn(node: unknown, at: string): UndescribedField[] {
|
|
385
|
+
const found: UndescribedField[] = [];
|
|
386
|
+
const visit = (node: unknown, pointer: string): void => {
|
|
387
|
+
if (Array.isArray(node)) {
|
|
388
|
+
node.forEach((child, index) => {
|
|
389
|
+
visit(child, `${pointer}/${index}`);
|
|
390
|
+
});
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (node === null || typeof node !== "object") return;
|
|
394
|
+
const record = node as Record<string, unknown>;
|
|
395
|
+
if (typeof record.in === "string" && typeof record.name === "string" && typeof record.description !== "string") {
|
|
396
|
+
found.push({ pointer });
|
|
397
|
+
}
|
|
398
|
+
const properties = record.properties;
|
|
399
|
+
if (properties !== null && typeof properties === "object" && !Array.isArray(properties)) {
|
|
400
|
+
for (const [name, property] of Object.entries(properties as Record<string, unknown>)) {
|
|
401
|
+
const described =
|
|
402
|
+
property !== null &&
|
|
403
|
+
typeof property === "object" &&
|
|
404
|
+
typeof (property as Record<string, unknown>).description === "string";
|
|
405
|
+
if (!described) found.push({ pointer: `${pointer}/properties/${escapePointer(name)}` });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
for (const [key, child] of Object.entries(record)) visit(child, `${pointer}/${escapePointer(key)}`);
|
|
409
|
+
};
|
|
410
|
+
visit(node, at);
|
|
411
|
+
return found;
|
|
412
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import type { RouteGroup } from "./document";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Which capability registered which routes, recorded by `createBackend` as it mounts them.
|
|
8
|
+
*
|
|
9
|
+
* Every capability registers onto the one shared app with absolute paths, so the finished route table
|
|
10
|
+
* says nothing about who owns a route. `createBackend` is the one place that knows — it calls each
|
|
11
|
+
* `routes` hook in turn — so it notes the slice of `app.routes` each hook added, and the contract tags
|
|
12
|
+
* each operation with the capability that owns it. Held beside the app rather than on it, so the app a
|
|
13
|
+
* Worker exports is the plain Hono instance it always was.
|
|
14
|
+
*/
|
|
15
|
+
const recorded = new WeakMap<object, readonly RouteGroup[]>();
|
|
16
|
+
|
|
17
|
+
/** Note the route groups of an assembled app. Called by `createBackend` and nothing else. */
|
|
18
|
+
export function recordRouteGroups(app: object, groups: readonly RouteGroup[]): void {
|
|
19
|
+
recorded.set(app, groups);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The route groups `createBackend` recorded for this app, or `undefined` for an app it did not assemble. */
|
|
23
|
+
export function routeGroupsOf(app: object): readonly RouteGroup[] | undefined {
|
|
24
|
+
return recorded.get(app);
|
|
25
|
+
}
|
package/src/seed/writeD1.ts
CHANGED
|
@@ -17,8 +17,14 @@ export interface SeedD1Options {
|
|
|
17
17
|
export interface SeedD1Result {
|
|
18
18
|
/** The table seeded. */
|
|
19
19
|
table: string;
|
|
20
|
-
/** The number of rows
|
|
20
|
+
/** The number of rows the fixture holds, every one validated. */
|
|
21
21
|
rows: number;
|
|
22
|
+
/**
|
|
23
|
+
* The number of rows this call inserted. Under `INSERT OR IGNORE` a row whose key is already there is
|
|
24
|
+
* not, so a re-run writes none, and says so here rather than repeating `rows` (#764). Always `0` under
|
|
25
|
+
* `dryRun`.
|
|
26
|
+
*/
|
|
27
|
+
written: number;
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
/**
|
|
@@ -89,15 +95,18 @@ export async function seedD1Group(
|
|
|
89
95
|
});
|
|
90
96
|
|
|
91
97
|
if (options.dryRun || encoded.length === 0) {
|
|
92
|
-
return { table: group.table, rows: encoded.length };
|
|
98
|
+
return { table: group.table, rows: encoded.length, written: 0 };
|
|
93
99
|
}
|
|
94
100
|
|
|
101
|
+
let written = 0n;
|
|
95
102
|
for (const chunk of chunkRowsByBoundParameters(encoded, columnsPerRow(encoded))) {
|
|
96
|
-
await db
|
|
103
|
+
const results = await db
|
|
97
104
|
.insertInto(group.table)
|
|
98
105
|
.orIgnore()
|
|
99
106
|
.values(chunk as never)
|
|
100
107
|
.execute();
|
|
108
|
+
// D1's `meta.changes`: the rows the statement inserted, which an ignored conflict is not.
|
|
109
|
+
for (const result of results) written += result.numInsertedOrUpdatedRows ?? 0n;
|
|
101
110
|
}
|
|
102
|
-
return { table: group.table, rows: encoded.length };
|
|
111
|
+
return { table: group.table, rows: encoded.length, written: Number(written) };
|
|
103
112
|
}
|
package/src/seed/writeKv.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface SeedKvResult {
|
|
|
19
19
|
store: string;
|
|
20
20
|
/** The number of entries validated (and, unless `dryRun`, considered for writing). */
|
|
21
21
|
entries: number;
|
|
22
|
+
/** The number of entries this call put. An existing key is left alone and not counted (#764). Always `0` under `dryRun`. */
|
|
23
|
+
written: number;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/**
|
|
@@ -87,13 +89,15 @@ export async function seedKvGroup<V extends z.ZodType, K extends z.ZodObject, M
|
|
|
87
89
|
});
|
|
88
90
|
|
|
89
91
|
if (options.dryRun) {
|
|
90
|
-
return { store: group.store, entries: group.entries.length };
|
|
92
|
+
return { store: group.store, entries: group.entries.length, written: 0 };
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
let written = 0;
|
|
93
96
|
for (const entry of group.entries) {
|
|
94
97
|
// Never overwrite an existing key — the KV counterpart of D1's INSERT OR IGNORE.
|
|
95
98
|
if (await keyExists(store, entry.key)) continue;
|
|
96
99
|
await store.put(entry.key, entry.value, entry.metadata !== undefined ? { metadata: entry.metadata } : {});
|
|
100
|
+
written += 1;
|
|
97
101
|
}
|
|
98
|
-
return { store: group.store, entries: group.entries.length };
|
|
102
|
+
return { store: group.store, entries: group.entries.length, written };
|
|
99
103
|
}
|
package/src/text/comments.ts
CHANGED
|
@@ -84,9 +84,19 @@ export function blankCommentsAndStrings(source: string): string {
|
|
|
84
84
|
|
|
85
85
|
/** The one walk behind both: comments always blanked, a quoted run kept or blanked inside its delimiters. */
|
|
86
86
|
function blank(source: string, strings: boolean): string {
|
|
87
|
+
return walkCode(source, 0, strings, false).out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Walk code from `start`. Inside an interpolation (`nested`), the `}` that closes it ends the walk, and is
|
|
92
|
+
* included. A template is walked by {@link walkTemplate}, which walks each of its interpolations as code in turn
|
|
93
|
+
* — so a comment inside `${…}` is blanked like any other, and a template inside one closes nothing (#711).
|
|
94
|
+
*/
|
|
95
|
+
function walkCode(source: string, start: number, strings: boolean, nested: boolean): { out: string; end: number } {
|
|
87
96
|
let out = "";
|
|
88
97
|
let previous = "";
|
|
89
|
-
let
|
|
98
|
+
let depth = 0;
|
|
99
|
+
let index = start;
|
|
90
100
|
while (index < source.length) {
|
|
91
101
|
const char = source[index] as string;
|
|
92
102
|
const pair = source.slice(index, index + 2);
|
|
@@ -96,8 +106,15 @@ function blank(source: string, strings: boolean): string {
|
|
|
96
106
|
index = stop;
|
|
97
107
|
continue;
|
|
98
108
|
}
|
|
99
|
-
if (char ===
|
|
100
|
-
const
|
|
109
|
+
if (char === "`") {
|
|
110
|
+
const walked = walkTemplate(source, index, strings);
|
|
111
|
+
out += walked.out;
|
|
112
|
+
index = walked.end;
|
|
113
|
+
previous = char;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (char === '"' || char === "'") {
|
|
117
|
+
const stop = endOfQuoted(source, index, char, true);
|
|
101
118
|
const run = source.slice(index, stop);
|
|
102
119
|
out += strings ? blankInside(run, char) : run;
|
|
103
120
|
index = stop;
|
|
@@ -111,11 +128,46 @@ function blank(source: string, strings: boolean): string {
|
|
|
111
128
|
previous = "/";
|
|
112
129
|
continue;
|
|
113
130
|
}
|
|
131
|
+
if (char === "{") depth += 1;
|
|
132
|
+
if (char === "}") {
|
|
133
|
+
if (nested && depth === 0) return { out: `${out}}`, end: index + 1 };
|
|
134
|
+
depth -= 1;
|
|
135
|
+
}
|
|
114
136
|
out += char;
|
|
115
137
|
if (!/\s/.test(char)) previous = char;
|
|
116
138
|
index += 1;
|
|
117
139
|
}
|
|
118
|
-
return out;
|
|
140
|
+
return { out, end: index };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Walk one template literal from its opening backtick. Its text is kept, or blanked whole — `${}` included — when
|
|
145
|
+
* `strings` asks; each interpolation is walked as code, so its comments are blanked and its own templates close
|
|
146
|
+
* nothing. Returns the text and the index past the closing backtick.
|
|
147
|
+
*/
|
|
148
|
+
function walkTemplate(source: string, start: number, strings: boolean): { out: string; end: number } {
|
|
149
|
+
let out = "`";
|
|
150
|
+
let index = start + 1;
|
|
151
|
+
while (index < source.length) {
|
|
152
|
+
const char = source[index] as string;
|
|
153
|
+
if (char === "\\") {
|
|
154
|
+
const escaped = source.slice(index, index + 2);
|
|
155
|
+
out += strings ? escaped.replace(/[^\n]/g, " ") : escaped;
|
|
156
|
+
index += 2;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (char === "`") return { out: `${out}\``, end: index + 1 };
|
|
160
|
+
if (char === "$" && source[index + 1] === "{") {
|
|
161
|
+
const walked = walkCode(source, index + 2, strings, true);
|
|
162
|
+
const text = `\${${walked.out}`;
|
|
163
|
+
out += strings ? text.replace(/[^\n]/g, " ") : text;
|
|
164
|
+
index = walked.end;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
out += strings && char !== "\n" ? " " : char;
|
|
168
|
+
index += 1;
|
|
169
|
+
}
|
|
170
|
+
return { out, end: index };
|
|
119
171
|
}
|
|
120
172
|
|
|
121
173
|
/** A quoted run with everything between its delimiters blanked, newlines kept. An unterminated run has no closer. */
|