@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,314 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { CONTROL_PLANE_HEADER } from "../controlPlane/wire.js";
|
|
4
|
+
import { routeSpecOf } from "../http/routeSpec.js";
|
|
5
|
+
import { requestContractOf } from "../http/validation.js";
|
|
6
|
+
import { ErrorBody } from "../error/wire.js";
|
|
7
|
+
import { pathParams } from "../http/routeContract.js";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
//#region src/openapi/document.ts
|
|
10
|
+
/**
|
|
11
|
+
* **The OpenAPI contract, read off a composed app.**
|
|
12
|
+
*
|
|
13
|
+
* Nothing here is written by hand. A route's request schemas come from its `validate` middleware, its
|
|
14
|
+
* summary, verification and responses from its `describeRoute`, and both are found by walking
|
|
15
|
+
* `app.routes` — the same table Hono dispatches from. So the document describes exactly what the Worker
|
|
16
|
+
* serves, the adopter's own routes included, and a schema edit reaches it with no second step.
|
|
17
|
+
*
|
|
18
|
+
* Zod is already the source of truth for every request and response, and every field carries a
|
|
19
|
+
* `.describe()` (CLAUDE.md §Zod). `z.toJSONSchema` keeps those descriptions, which is what lets a client
|
|
20
|
+
* generated in Swift or Kotlin carry the same documentation as the TypeScript one.
|
|
21
|
+
*
|
|
22
|
+
* OpenAPI 3.1, whose schema dialect is JSON Schema 2020-12 — the dialect `z.toJSONSchema` writes — so
|
|
23
|
+
* every schema crosses unchanged.
|
|
24
|
+
*/
|
|
25
|
+
/** The OpenAPI version this module writes. */
|
|
26
|
+
const OPENAPI_VERSION = "3.1.0";
|
|
27
|
+
/** How each verification strategy is presented on the wire. `signed-webhook` and `public` have no scheme. */
|
|
28
|
+
const SECURITY_SCHEMES = {
|
|
29
|
+
bearer: {
|
|
30
|
+
type: "http",
|
|
31
|
+
scheme: "bearer",
|
|
32
|
+
description: "A short-lived access token from `@pithy-sh/auth`, sent as `Authorization: Bearer <token>`."
|
|
33
|
+
},
|
|
34
|
+
session: {
|
|
35
|
+
type: "apiKey",
|
|
36
|
+
in: "cookie",
|
|
37
|
+
name: "better-auth.session_token",
|
|
38
|
+
description: "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."
|
|
39
|
+
},
|
|
40
|
+
controlPlane: {
|
|
41
|
+
type: "apiKey",
|
|
42
|
+
in: "header",
|
|
43
|
+
name: CONTROL_PLANE_HEADER,
|
|
44
|
+
description: "A customer-issued, scoped control-plane credential. Default-denied."
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
/** The `security` requirement for each strategy that has one. */
|
|
48
|
+
const REQUIREMENT = {
|
|
49
|
+
bearer: { bearer: [] },
|
|
50
|
+
session: { session: [] },
|
|
51
|
+
"control-plane": { controlPlane: [] },
|
|
52
|
+
public: {}
|
|
53
|
+
};
|
|
54
|
+
const ERROR_RESPONSE = "Error";
|
|
55
|
+
/** Hono's `/a/:id{[0-9]+}/*` as OpenAPI's `/a/{id}/{path}`. */
|
|
56
|
+
function openApiPath(path) {
|
|
57
|
+
return path.replace(/:([A-Za-z0-9_]+)(\{[^}]*\})?\??/g, "{$1}").replace(/\/\*$/, "/{path}").replace(/\*/g, "{path}");
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* `get` + `/leaderboard/{board}/top` → `getLeaderboardByBoardTop`. A parameter reads as `By<Name>`, so
|
|
61
|
+
* `/items/{id}` and `/items/id` do not meet. Stable, but not unique on its own — `/a-b` and `/a/b` still
|
|
62
|
+
* share one — so {@link openApiDocument} numbers a repeat rather than writing an invalid document.
|
|
63
|
+
*/
|
|
64
|
+
function operationId(method, path) {
|
|
65
|
+
const words = path.split("/").flatMap((segment) => {
|
|
66
|
+
const param = /^\{(.+)\}$/.exec(segment);
|
|
67
|
+
return param ? ["by", ...(param[1] ?? "").split(/[^A-Za-z0-9]+/)] : segment.split(/[^A-Za-z0-9]+/);
|
|
68
|
+
}).filter((word) => word.length > 0).map((word) => `${word[0]?.toUpperCase()}${word.slice(1)}`);
|
|
69
|
+
return `${method.toLowerCase()}${words.join("")}`;
|
|
70
|
+
}
|
|
71
|
+
/** A Zod schema as JSON Schema, without the `$schema` key OpenAPI does not want inside a document. */
|
|
72
|
+
function jsonSchema(schema, io) {
|
|
73
|
+
const { $schema: _dialect, ...rest } = z.toJSONSchema(schema, {
|
|
74
|
+
io,
|
|
75
|
+
unrepresentable: "any"
|
|
76
|
+
});
|
|
77
|
+
return rest;
|
|
78
|
+
}
|
|
79
|
+
function operationsOf(routes) {
|
|
80
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
81
|
+
for (const route of routes) {
|
|
82
|
+
const key = `${route.method} ${route.path}`;
|
|
83
|
+
const entry = byKey.get(key) ?? {
|
|
84
|
+
method: route.method,
|
|
85
|
+
path: route.path,
|
|
86
|
+
handlers: []
|
|
87
|
+
};
|
|
88
|
+
entry.handlers.push(route.handler);
|
|
89
|
+
byKey.set(key, entry);
|
|
90
|
+
}
|
|
91
|
+
return [...byKey.values()];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* An `ALL` entry with no spec is `app.use` — middleware, not a route — because Hono records both as
|
|
95
|
+
* `ALL`. An `app.all` route that forgot its `describeRoute` is therefore invisible to the coverage
|
|
96
|
+
* check, which is the one blind spot here; the kit has one such route and it is described.
|
|
97
|
+
*/
|
|
98
|
+
function isMiddlewareOnly(operation) {
|
|
99
|
+
return operation.method === "ALL" && !operation.handlers.some((handler) => routeSpecOf(handler));
|
|
100
|
+
}
|
|
101
|
+
/** Every route in these groups that registered no `describeRoute`. Empty is the contract being met. */
|
|
102
|
+
function undescribedRoutes(groups) {
|
|
103
|
+
return groups.flatMap((group) => operationsOf(group.routes).filter((operation) => !isMiddlewareOnly(operation)).filter((operation) => !operation.handlers.some((handler) => routeSpecOf(handler))).map(({ method, path }) => ({
|
|
104
|
+
method,
|
|
105
|
+
path
|
|
106
|
+
})));
|
|
107
|
+
}
|
|
108
|
+
/** The parameters one validator contributes: one per property of its object schema. */
|
|
109
|
+
function parametersOf(contract) {
|
|
110
|
+
const location = {
|
|
111
|
+
param: "path",
|
|
112
|
+
query: "query",
|
|
113
|
+
header: "header",
|
|
114
|
+
cookie: "cookie"
|
|
115
|
+
}[contract.target];
|
|
116
|
+
if (location === void 0) return [];
|
|
117
|
+
const schema = jsonSchema(contract.schema, "input");
|
|
118
|
+
const properties = schema.properties ?? {};
|
|
119
|
+
const required = new Set(schema.required ?? []);
|
|
120
|
+
return Object.entries(properties).map(([name, property]) => {
|
|
121
|
+
const { description, ...rest } = property;
|
|
122
|
+
return {
|
|
123
|
+
name,
|
|
124
|
+
in: location,
|
|
125
|
+
required: location === "path" || required.has(name),
|
|
126
|
+
...typeof description === "string" ? { description } : {},
|
|
127
|
+
schema: rest
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** The request body one validator contributes, if it reads the body. */
|
|
132
|
+
function requestBodyOf(contract) {
|
|
133
|
+
const media = {
|
|
134
|
+
json: "application/json",
|
|
135
|
+
form: "multipart/form-data"
|
|
136
|
+
}[contract.target];
|
|
137
|
+
if (media === void 0) return void 0;
|
|
138
|
+
const schema = jsonSchema(contract.schema, "input");
|
|
139
|
+
return {
|
|
140
|
+
required: !contract.schema.safeParse(void 0).success,
|
|
141
|
+
...typeof schema.description === "string" ? { description: schema.description } : {},
|
|
142
|
+
content: { [media]: { schema } }
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function isZodSchema(value) {
|
|
146
|
+
return value instanceof z.ZodType;
|
|
147
|
+
}
|
|
148
|
+
/** One `responses` entry. A bare schema is a JSON body; a {@link RouteResponse} says what it is. */
|
|
149
|
+
function responseOf(declared) {
|
|
150
|
+
if (isZodSchema(declared)) {
|
|
151
|
+
const schema = jsonSchema(declared, "input");
|
|
152
|
+
return {
|
|
153
|
+
description: typeof schema.description === "string" ? schema.description : "Success.",
|
|
154
|
+
content: { "application/json": { schema } }
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const media = declared.contentType ?? (declared.schema ? "application/json" : void 0);
|
|
158
|
+
return {
|
|
159
|
+
description: declared.description,
|
|
160
|
+
...media === void 0 ? {} : { content: { [media]: { schema: declared.schema ? jsonSchema(declared.schema, "input") : {} } } }
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function strategiesOf(spec) {
|
|
164
|
+
return typeof spec.verification === "string" ? [spec.verification] : spec.verification;
|
|
165
|
+
}
|
|
166
|
+
/** One OpenAPI operation for a described route. */
|
|
167
|
+
function operationOf(tag, method, operation, spec) {
|
|
168
|
+
const contracts = operation.handlers.flatMap((handler) => {
|
|
169
|
+
const contract = requestContractOf(handler);
|
|
170
|
+
return contract ? [contract] : [];
|
|
171
|
+
});
|
|
172
|
+
const parameters = contracts.flatMap(parametersOf);
|
|
173
|
+
for (const name of pathParams(operation.path)) if (!parameters.some((parameter) => parameter.in === "path" && parameter.name === name)) parameters.push({
|
|
174
|
+
name,
|
|
175
|
+
in: "path",
|
|
176
|
+
required: true,
|
|
177
|
+
schema: { type: "string" }
|
|
178
|
+
});
|
|
179
|
+
if (/\*/.test(operation.path)) parameters.push({
|
|
180
|
+
name: "path",
|
|
181
|
+
in: "path",
|
|
182
|
+
required: true,
|
|
183
|
+
description: "The rest of the path, passed through to the handler.",
|
|
184
|
+
schema: { type: "string" }
|
|
185
|
+
});
|
|
186
|
+
const body = contracts.map(requestBodyOf).find((candidate) => candidate !== void 0);
|
|
187
|
+
const strategies = strategiesOf(spec);
|
|
188
|
+
const security = strategies.flatMap((strategy) => {
|
|
189
|
+
const requirement = REQUIREMENT[strategy];
|
|
190
|
+
return requirement === void 0 ? [] : [requirement];
|
|
191
|
+
});
|
|
192
|
+
const responses = {};
|
|
193
|
+
for (const [status, declared] of Object.entries(spec.responses)) responses[status] = responseOf(declared);
|
|
194
|
+
responses.default = { $ref: `#/components/responses/${ERROR_RESPONSE}` };
|
|
195
|
+
return {
|
|
196
|
+
tags: [tag],
|
|
197
|
+
operationId: operationId(method, openApiPath(operation.path)),
|
|
198
|
+
summary: spec.summary,
|
|
199
|
+
...spec.description === void 0 ? {} : { description: spec.description },
|
|
200
|
+
...parameters.length === 0 ? {} : { parameters },
|
|
201
|
+
...body === void 0 ? {} : { requestBody: body },
|
|
202
|
+
responses,
|
|
203
|
+
...strategies.length === 0 ? {} : {
|
|
204
|
+
security,
|
|
205
|
+
"x-pithy-verification": strategies
|
|
206
|
+
},
|
|
207
|
+
...spec.scopes === void 0 || spec.scopes.length === 0 ? {} : { "x-pithy-scopes": spec.scopes }
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/** The methods an operation is documented under: its own, or for `app.all` the ones its spec names. */
|
|
211
|
+
function methodsOf(operation, spec) {
|
|
212
|
+
if (operation.method !== "ALL") return [operation.method.toLowerCase()];
|
|
213
|
+
return spec.methods ?? ["get", "post"];
|
|
214
|
+
}
|
|
215
|
+
/** The spec an undescribed route is written with under `undescribed: "include"`. It claims nothing. */
|
|
216
|
+
const UNDESCRIBED = {
|
|
217
|
+
summary: "Not described. Add describeRoute to this route.",
|
|
218
|
+
verification: [],
|
|
219
|
+
responses: {}
|
|
220
|
+
};
|
|
221
|
+
/**
|
|
222
|
+
* The OpenAPI document for these route groups. Undescribed routes are left out rather than guessed at,
|
|
223
|
+
* unless {@link DocumentOptions.undescribed} says otherwise; {@link undescribedRoutes} is the check that
|
|
224
|
+
* keeps that set empty.
|
|
225
|
+
*/
|
|
226
|
+
function openApiDocument(info, groups, options = {}) {
|
|
227
|
+
const paths = {};
|
|
228
|
+
const tags = [];
|
|
229
|
+
const ids = /* @__PURE__ */ new Map();
|
|
230
|
+
/** An operation id no earlier operation took: the derived one, or it with the next free number. */
|
|
231
|
+
const unique = (id) => {
|
|
232
|
+
const seen = ids.get(id) ?? 0;
|
|
233
|
+
ids.set(id, seen + 1);
|
|
234
|
+
return seen === 0 ? id : `${id}${seen + 1}`;
|
|
235
|
+
};
|
|
236
|
+
for (const group of groups) {
|
|
237
|
+
let documented = false;
|
|
238
|
+
for (const operation of operationsOf(group.routes)) {
|
|
239
|
+
if (isMiddlewareOnly(operation)) continue;
|
|
240
|
+
const declared = operation.handlers.map(routeSpecOf).find((candidate) => candidate !== void 0);
|
|
241
|
+
if (declared === void 0 && options.undescribed !== "include") continue;
|
|
242
|
+
const spec = declared ?? UNDESCRIBED;
|
|
243
|
+
documented = true;
|
|
244
|
+
const path = openApiPath(operation.path);
|
|
245
|
+
const item = paths[path] ?? {};
|
|
246
|
+
for (const method of methodsOf(operation, spec)) {
|
|
247
|
+
const derived = operationOf(group.tag, method, operation, spec);
|
|
248
|
+
const written = {
|
|
249
|
+
...derived,
|
|
250
|
+
operationId: unique(String(derived.operationId))
|
|
251
|
+
};
|
|
252
|
+
item[method] = declared === void 0 ? {
|
|
253
|
+
...written,
|
|
254
|
+
"x-pithy-undescribed": true
|
|
255
|
+
} : written;
|
|
256
|
+
}
|
|
257
|
+
paths[path] = item;
|
|
258
|
+
}
|
|
259
|
+
if (documented) tags.push({
|
|
260
|
+
name: group.tag,
|
|
261
|
+
...group.description ? { description: group.description } : {}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
openapi: OPENAPI_VERSION,
|
|
266
|
+
info,
|
|
267
|
+
tags,
|
|
268
|
+
paths,
|
|
269
|
+
components: {
|
|
270
|
+
schemas: { [ERROR_RESPONSE]: jsonSchema(ErrorBody, "input") },
|
|
271
|
+
responses: { [ERROR_RESPONSE]: {
|
|
272
|
+
description: "Any failure. The status is the error's own; the body is always this shape.",
|
|
273
|
+
content: { "application/json": { schema: { $ref: `#/components/schemas/${ERROR_RESPONSE}` } } }
|
|
274
|
+
} },
|
|
275
|
+
securitySchemes: SECURITY_SCHEMES
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
const escapePointer = (segment) => segment.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
280
|
+
/**
|
|
281
|
+
* Every object property and every parameter in the document without a description. The kit holds every
|
|
282
|
+
* Zod field to `.describe()`, and this is how that rule is checked on the wire shape a client actually
|
|
283
|
+
* generates from, rather than on the source it came from.
|
|
284
|
+
*/
|
|
285
|
+
function undescribedFields(document) {
|
|
286
|
+
return undescribedFieldsIn(document.paths, "/paths");
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* The same sweep over any part of a contract document, from a JSON pointer: every object property and every
|
|
290
|
+
* OpenAPI parameter below `node` without a description. The AsyncAPI document runs it over its messages.
|
|
291
|
+
*/
|
|
292
|
+
function undescribedFieldsIn(node, at) {
|
|
293
|
+
const found = [];
|
|
294
|
+
const visit = (node, pointer) => {
|
|
295
|
+
if (Array.isArray(node)) {
|
|
296
|
+
node.forEach((child, index) => {
|
|
297
|
+
visit(child, `${pointer}/${index}`);
|
|
298
|
+
});
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (node === null || typeof node !== "object") return;
|
|
302
|
+
const record = node;
|
|
303
|
+
if (typeof record.in === "string" && typeof record.name === "string" && typeof record.description !== "string") found.push({ pointer });
|
|
304
|
+
const properties = record.properties;
|
|
305
|
+
if (properties !== null && typeof properties === "object" && !Array.isArray(properties)) {
|
|
306
|
+
for (const [name, property] of Object.entries(properties)) if (!(property !== null && typeof property === "object" && typeof property.description === "string")) found.push({ pointer: `${pointer}/properties/${escapePointer(name)}` });
|
|
307
|
+
}
|
|
308
|
+
for (const [key, child] of Object.entries(record)) visit(child, `${pointer}/${escapePointer(key)}`);
|
|
309
|
+
};
|
|
310
|
+
visit(node, at);
|
|
311
|
+
return found;
|
|
312
|
+
}
|
|
313
|
+
//#endregion
|
|
314
|
+
export { OPENAPI_VERSION, jsonSchema, openApiDocument, openApiPath, operationId, undescribedFields, undescribedFieldsIn, undescribedRoutes };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import type { RouteGroup } from "./document";
|
|
4
|
+
/** Note the route groups of an assembled app. Called by `createBackend` and nothing else. */
|
|
5
|
+
export declare function recordRouteGroups(app: object, groups: readonly RouteGroup[]): void;
|
|
6
|
+
/** The route groups `createBackend` recorded for this app, or `undefined` for an app it did not assemble. */
|
|
7
|
+
export declare function routeGroupsOf(app: object): readonly RouteGroup[] | undefined;
|
|
8
|
+
//# sourceMappingURL=routeGroups.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routeGroups.d.ts","sourceRoot":"","sources":["../../src/openapi/routeGroups.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAa7C,6FAA6F;AAC7F,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,IAAI,CAElF;AAED,6GAA6G;AAC7G,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,UAAU,EAAE,GAAG,SAAS,CAE5E"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
//#region src/openapi/routeGroups.ts
|
|
4
|
+
/**
|
|
5
|
+
* Which capability registered which routes, recorded by `createBackend` as it mounts them.
|
|
6
|
+
*
|
|
7
|
+
* Every capability registers onto the one shared app with absolute paths, so the finished route table
|
|
8
|
+
* says nothing about who owns a route. `createBackend` is the one place that knows — it calls each
|
|
9
|
+
* `routes` hook in turn — so it notes the slice of `app.routes` each hook added, and the contract tags
|
|
10
|
+
* each operation with the capability that owns it. Held beside the app rather than on it, so the app a
|
|
11
|
+
* Worker exports is the plain Hono instance it always was.
|
|
12
|
+
*/
|
|
13
|
+
const recorded = /* @__PURE__ */ new WeakMap();
|
|
14
|
+
/** Note the route groups of an assembled app. Called by `createBackend` and nothing else. */
|
|
15
|
+
function recordRouteGroups(app, groups) {
|
|
16
|
+
recorded.set(app, groups);
|
|
17
|
+
}
|
|
18
|
+
/** The route groups `createBackend` recorded for this app, or `undefined` for an app it did not assemble. */
|
|
19
|
+
function routeGroupsOf(app) {
|
|
20
|
+
return recorded.get(app);
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
export { recordRouteGroups, routeGroupsOf };
|
package/dist/seed/writeD1.d.ts
CHANGED
|
@@ -12,8 +12,14 @@ export interface SeedD1Options {
|
|
|
12
12
|
export interface SeedD1Result {
|
|
13
13
|
/** The table seeded. */
|
|
14
14
|
table: string;
|
|
15
|
-
/** The number of rows
|
|
15
|
+
/** The number of rows the fixture holds, every one validated. */
|
|
16
16
|
rows: number;
|
|
17
|
+
/**
|
|
18
|
+
* The number of rows this call inserted. Under `INSERT OR IGNORE` a row whose key is already there is
|
|
19
|
+
* not, so a re-run writes none, and says so here rather than repeating `rows` (#764). Always `0` under
|
|
20
|
+
* `dryRun`.
|
|
21
|
+
*/
|
|
22
|
+
written: number;
|
|
17
23
|
}
|
|
18
24
|
/**
|
|
19
25
|
* Seed one D1 group, idempotently and non-destructively.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"writeD1.d.ts","sourceRoot":"","sources":["../../src/seed/writeD1.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAE1C,uCAAuC;AACvC,MAAM,WAAW,aAAa;IAC5B,+CAA+C;IAC/C,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,2CAA2C;AAC3C,MAAM,WAAW,YAAY;IAC3B,wBAAwB;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,
|
|
1
|
+
{"version":3,"file":"writeD1.d.ts","sourceRoot":"","sources":["../../src/seed/writeD1.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAE1C,uCAAuC;AACvC,MAAM,WAAW,aAAa;IAC5B,+CAA+C;IAC/C,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,2CAA2C;AAC3C,MAAM,WAAW,YAAY;IAC3B,wBAAwB;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC;CACjB;AAqBD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,WAAW,CAE/B,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,EACf,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,CAAC,CAAC,OAAO,EACjB,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,YAAY,CAAC,CA+BvB"}
|
package/dist/seed/writeD1.js
CHANGED
|
@@ -63,12 +63,18 @@ async function seedD1Group(db, group, schema, options = {}) {
|
|
|
63
63
|
});
|
|
64
64
|
if (options.dryRun || encoded.length === 0) return {
|
|
65
65
|
table: group.table,
|
|
66
|
-
rows: encoded.length
|
|
66
|
+
rows: encoded.length,
|
|
67
|
+
written: 0
|
|
67
68
|
};
|
|
68
|
-
|
|
69
|
+
let written = 0n;
|
|
70
|
+
for (const chunk of chunkRowsByBoundParameters(encoded, columnsPerRow(encoded))) {
|
|
71
|
+
const results = await db.insertInto(group.table).orIgnore().values(chunk).execute();
|
|
72
|
+
for (const result of results) written += result.numInsertedOrUpdatedRows ?? 0n;
|
|
73
|
+
}
|
|
69
74
|
return {
|
|
70
75
|
table: group.table,
|
|
71
|
-
rows: encoded.length
|
|
76
|
+
rows: encoded.length,
|
|
77
|
+
written: Number(written)
|
|
72
78
|
};
|
|
73
79
|
}
|
|
74
80
|
//#endregion
|
package/dist/seed/writeKv.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface SeedKvResult {
|
|
|
15
15
|
store: string;
|
|
16
16
|
/** The number of entries validated (and, unless `dryRun`, considered for writing). */
|
|
17
17
|
entries: number;
|
|
18
|
+
/** The number of entries this call put. An existing key is left alone and not counted (#764). Always `0` under `dryRun`. */
|
|
19
|
+
written: number;
|
|
18
20
|
}
|
|
19
21
|
/**
|
|
20
22
|
* Seed one KV group, idempotently and non-destructively.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"writeKv.d.ts","sourceRoot":"","sources":["../../src/seed/writeKv.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAE1C,uCAAuC;AACvC,MAAM,WAAW,aAAa;IAC5B,oCAAoC;IACpC,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,2CAA2C;AAC3C,MAAM,WAAW,YAAY;IAC3B,wBAAwB;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,sFAAsF;IACtF,OAAO,EAAE,MAAM,CAAC;CACjB;AA4CD;;;;;;;;;;GAUG;AACH,wBAAsB,WAAW,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC,SAAS,CAAC,CAAC,SAAS,EAAE,CAAC,SAAS,CAAC,CAAC,OAAO,EAC/F,KAAK,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EACvB,KAAK,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC3B,IAAI,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC1B,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,YAAY,CAAC,
|
|
1
|
+
{"version":3,"file":"writeKv.d.ts","sourceRoot":"","sources":["../../src/seed/writeKv.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAE1C,uCAAuC;AACvC,MAAM,WAAW,aAAa;IAC5B,oCAAoC;IACpC,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,2CAA2C;AAC3C,MAAM,WAAW,YAAY;IAC3B,wBAAwB;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,sFAAsF;IACtF,OAAO,EAAE,MAAM,CAAC;IAChB,4HAA4H;IAC5H,OAAO,EAAE,MAAM,CAAC;CACjB;AA4CD;;;;;;;;;;GAUG;AACH,wBAAsB,WAAW,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC,SAAS,CAAC,CAAC,SAAS,EAAE,CAAC,SAAS,CAAC,CAAC,OAAO,EAC/F,KAAK,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EACvB,KAAK,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC3B,IAAI,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC1B,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,YAAY,CAAC,CAmBvB"}
|
package/dist/seed/writeKv.js
CHANGED
|
@@ -50,15 +50,19 @@ async function seedKvGroup(store, group, spec, options = {}) {
|
|
|
50
50
|
});
|
|
51
51
|
if (options.dryRun) return {
|
|
52
52
|
store: group.store,
|
|
53
|
-
entries: group.entries.length
|
|
53
|
+
entries: group.entries.length,
|
|
54
|
+
written: 0
|
|
54
55
|
};
|
|
56
|
+
let written = 0;
|
|
55
57
|
for (const entry of group.entries) {
|
|
56
58
|
if (await keyExists(store, entry.key)) continue;
|
|
57
59
|
await store.put(entry.key, entry.value, entry.metadata !== void 0 ? { metadata: entry.metadata } : {});
|
|
60
|
+
written += 1;
|
|
58
61
|
}
|
|
59
62
|
return {
|
|
60
63
|
store: group.store,
|
|
61
|
-
entries: group.entries.length
|
|
64
|
+
entries: group.entries.length,
|
|
65
|
+
written
|
|
62
66
|
};
|
|
63
67
|
}
|
|
64
68
|
//#endregion
|
package/dist/text/comments.js
CHANGED
|
@@ -82,9 +82,18 @@ function blankCommentsAndStrings(source) {
|
|
|
82
82
|
}
|
|
83
83
|
/** The one walk behind both: comments always blanked, a quoted run kept or blanked inside its delimiters. */
|
|
84
84
|
function blank(source, strings) {
|
|
85
|
+
return walkCode(source, 0, strings, false).out;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Walk code from `start`. Inside an interpolation (`nested`), the `}` that closes it ends the walk, and is
|
|
89
|
+
* included. A template is walked by {@link walkTemplate}, which walks each of its interpolations as code in turn
|
|
90
|
+
* — so a comment inside `${…}` is blanked like any other, and a template inside one closes nothing (#711).
|
|
91
|
+
*/
|
|
92
|
+
function walkCode(source, start, strings, nested) {
|
|
85
93
|
let out = "";
|
|
86
94
|
let previous = "";
|
|
87
|
-
let
|
|
95
|
+
let depth = 0;
|
|
96
|
+
let index = start;
|
|
88
97
|
while (index < source.length) {
|
|
89
98
|
const char = source[index];
|
|
90
99
|
const pair = source.slice(index, index + 2);
|
|
@@ -94,8 +103,15 @@ function blank(source, strings) {
|
|
|
94
103
|
index = stop;
|
|
95
104
|
continue;
|
|
96
105
|
}
|
|
97
|
-
if (char === "
|
|
98
|
-
const
|
|
106
|
+
if (char === "`") {
|
|
107
|
+
const walked = walkTemplate(source, index, strings);
|
|
108
|
+
out += walked.out;
|
|
109
|
+
index = walked.end;
|
|
110
|
+
previous = char;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (char === "\"" || char === "'") {
|
|
114
|
+
const stop = endOfQuoted(source, index, char, true);
|
|
99
115
|
const run = source.slice(index, stop);
|
|
100
116
|
out += strings ? blankInside(run, char) : run;
|
|
101
117
|
index = stop;
|
|
@@ -109,11 +125,57 @@ function blank(source, strings) {
|
|
|
109
125
|
previous = "/";
|
|
110
126
|
continue;
|
|
111
127
|
}
|
|
128
|
+
if (char === "{") depth += 1;
|
|
129
|
+
if (char === "}") {
|
|
130
|
+
if (nested && depth === 0) return {
|
|
131
|
+
out: `${out}}`,
|
|
132
|
+
end: index + 1
|
|
133
|
+
};
|
|
134
|
+
depth -= 1;
|
|
135
|
+
}
|
|
112
136
|
out += char;
|
|
113
137
|
if (!/\s/.test(char)) previous = char;
|
|
114
138
|
index += 1;
|
|
115
139
|
}
|
|
116
|
-
return
|
|
140
|
+
return {
|
|
141
|
+
out,
|
|
142
|
+
end: index
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Walk one template literal from its opening backtick. Its text is kept, or blanked whole — `${}` included — when
|
|
147
|
+
* `strings` asks; each interpolation is walked as code, so its comments are blanked and its own templates close
|
|
148
|
+
* nothing. Returns the text and the index past the closing backtick.
|
|
149
|
+
*/
|
|
150
|
+
function walkTemplate(source, start, strings) {
|
|
151
|
+
let out = "`";
|
|
152
|
+
let index = start + 1;
|
|
153
|
+
while (index < source.length) {
|
|
154
|
+
const char = source[index];
|
|
155
|
+
if (char === "\\") {
|
|
156
|
+
const escaped = source.slice(index, index + 2);
|
|
157
|
+
out += strings ? escaped.replace(/[^\n]/g, " ") : escaped;
|
|
158
|
+
index += 2;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (char === "`") return {
|
|
162
|
+
out: `${out}\``,
|
|
163
|
+
end: index + 1
|
|
164
|
+
};
|
|
165
|
+
if (char === "$" && source[index + 1] === "{") {
|
|
166
|
+
const walked = walkCode(source, index + 2, strings, true);
|
|
167
|
+
const text = `\${${walked.out}`;
|
|
168
|
+
out += strings ? text.replace(/[^\n]/g, " ") : text;
|
|
169
|
+
index = walked.end;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
out += strings && char !== "\n" ? " " : char;
|
|
173
|
+
index += 1;
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
out,
|
|
177
|
+
end: index
|
|
178
|
+
};
|
|
117
179
|
}
|
|
118
180
|
/** A quoted run with everything between its delimiters blanked, newlines kept. An unterminated run has no closer. */
|
|
119
181
|
function blankInside(run, quote) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
/**
|
|
4
|
+
* **Every string a source file writes, as it reads at run time near enough to scan (#711)** — each quoted or
|
|
5
|
+
* template literal's text, quotes off, escapes resolved to the character they stand for, and every `${…}` blanked
|
|
6
|
+
* to `<x>`.
|
|
7
|
+
*
|
|
8
|
+
* Gates that read what the code *says* — `packages/cli/src/ci/runnableCommands.test.ts` reads every `pithy …` a
|
|
9
|
+
* line names — used one regular expression for "a string or template literal", and it has a measured false
|
|
10
|
+
* reading: a template whose interpolation holds a template of its own, `` `${path}${detail ? ` — ${detail}` :
|
|
11
|
+
* ""}` ``, closed at the inner backtick, and everything after it was read as a literal that opened there. The
|
|
12
|
+
* scan then reported text the file does not contain, and missed the text it does. So this walks the source and
|
|
13
|
+
* keeps a stack: a `${` opens an expression, whose own literals are read as literals in their turn, and its
|
|
14
|
+
* matching `}` resumes the template it interrupted.
|
|
15
|
+
*
|
|
16
|
+
* Comments must be blanked first (`./comments`), as every caller here already does. A regex literal is stepped
|
|
17
|
+
* over by the same rule `./comments` uses — a `/` where a value may begin opens one — so a quote inside a pattern
|
|
18
|
+
* does not open a string.
|
|
19
|
+
*/
|
|
20
|
+
export declare function literalTexts(source: string, options?: LiteralOptions): string[];
|
|
21
|
+
/** How a source is read. */
|
|
22
|
+
export interface LiteralOptions {
|
|
23
|
+
/**
|
|
24
|
+
* The source is a `.tsx` file: an element's text and its attribute strings are read as literals too, and a
|
|
25
|
+
* `<` where a value may begin opens an element rather than comparing. A JSX text run is read with its
|
|
26
|
+
* whitespace collapsed, as React renders it; a `{…}` child is `<x>`.
|
|
27
|
+
*/
|
|
28
|
+
readonly jsx?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** One literal: its text as {@link literalTexts} reads it, and the source range it spans, delimiters included. */
|
|
31
|
+
export interface LiteralSpan {
|
|
32
|
+
readonly text: string;
|
|
33
|
+
readonly start: number;
|
|
34
|
+
readonly end: number;
|
|
35
|
+
/** A template's interpolations, each the range of the code between `${` and `}`. Absent for any other literal. */
|
|
36
|
+
readonly holes?: readonly (readonly [number, number])[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* {@link literalTexts}, with where each one sits — so a gate can read what is around a literal: a `+` beside it, or
|
|
40
|
+
* the array it is an element of being joined.
|
|
41
|
+
*/
|
|
42
|
+
export declare function literalSpans(source: string, options?: LiteralOptions): LiteralSpan[];
|
|
43
|
+
//# sourceMappingURL=literals.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"literals.d.ts","sourceRoot":"","sources":["../../src/text/literals.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,MAAM,EAAE,CAEnF;AAED,4BAA4B;AAC5B,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,kHAAkH;AAClH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,kHAAkH;IAClH,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC;CACzD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,WAAW,EAAE,CAIxF"}
|