typespec-hono 0.5.0 → 0.7.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/src/app.js +54 -12
- package/package.json +2 -2
package/dist/src/app.js
CHANGED
|
@@ -248,7 +248,7 @@ basePaths = [],
|
|
|
248
248
|
* audience is wider.
|
|
249
249
|
*/
|
|
250
250
|
securityFor) {
|
|
251
|
-
const
|
|
251
|
+
const mounted = emitted.routes.flatMap((route) => {
|
|
252
252
|
let dispatched;
|
|
253
253
|
const names = emitted.schemaNames.get(route.operationId);
|
|
254
254
|
// The library declares a `Responses` const for every operation, so a missing entry is a bug in
|
|
@@ -291,6 +291,7 @@ securityFor) {
|
|
|
291
291
|
}
|
|
292
292
|
return [{ route, names, validators, dispatched }];
|
|
293
293
|
});
|
|
294
|
+
const entries = mounted;
|
|
294
295
|
/**
|
|
295
296
|
* **One registration per verb+path, not per operation.**
|
|
296
297
|
*
|
|
@@ -600,7 +601,7 @@ securityFor) {
|
|
|
600
601
|
"\t\t\t},",
|
|
601
602
|
"\t\t)",
|
|
602
603
|
].join("\n");
|
|
603
|
-
return { target, text };
|
|
604
|
+
return { target, text, headOnly };
|
|
604
605
|
});
|
|
605
606
|
/**
|
|
606
607
|
* Every identifier this file names, imported from the module that declares it.
|
|
@@ -608,8 +609,21 @@ securityFor) {
|
|
|
608
609
|
* **Derived from what the rendered text actually references, not from what was available.** An
|
|
609
610
|
* unused import fails the lint a generated file has to pass like any other, and a missing one is a
|
|
610
611
|
* file that does not compile. Both have happened.
|
|
612
|
+
*
|
|
613
|
+
* **The text is TOKENISED and compared by equality, never matched name by name.** It used to build
|
|
614
|
+
* `new RegExp("\\b" + identifier + "\\b")` per name, and that was wrong twice over for any
|
|
615
|
+
* identifier containing a `$`, which TypeSpec permits in a model or operation name: `$` is an
|
|
616
|
+
* anchor in a regular expression, and escaping it alone does not help because `\b` is a WORD
|
|
617
|
+
* boundary and `$` is not a word character, so `\b\$select\b` cannot match either. Measured on
|
|
618
|
+
* `op list$Items(...): Item$Ref[]`, every identifier reported ABSENT, the whole
|
|
619
|
+
* `./schemas.gen.js` import was dropped, and the emitted file failed with four
|
|
620
|
+
* `TS2304: Cannot find name`. No diagnostic anywhere.
|
|
621
|
+
*
|
|
622
|
+
* Splitting on the language's own identifier rule and testing membership has neither failure: no
|
|
623
|
+
* metacharacter can be misread, and `Foo` cannot match `FooExtra`.
|
|
611
624
|
*/
|
|
612
625
|
const rendered = [...registrations.map((r) => r.text), ...methods, ...aliases].join("\n");
|
|
626
|
+
const mentioned = new Set(rendered.match(/[A-Za-z_$][A-Za-z0-9_$]*/g) ?? []);
|
|
613
627
|
const referenced = [
|
|
614
628
|
...new Set(entries.flatMap((entry) => [
|
|
615
629
|
entry.names.path,
|
|
@@ -620,7 +634,7 @@ securityFor) {
|
|
|
620
634
|
entry.names.responses,
|
|
621
635
|
].filter((name) => name !== undefined))),
|
|
622
636
|
]
|
|
623
|
-
.filter((identifier) =>
|
|
637
|
+
.filter((identifier) => mentioned.has(identifier))
|
|
624
638
|
.toSorted();
|
|
625
639
|
const imports = referenced.length === 0
|
|
626
640
|
? ""
|
|
@@ -670,17 +684,47 @@ securityFor) {
|
|
|
670
684
|
// Imported only where a route actually negotiates: an unused import fails the repo's own lint.
|
|
671
685
|
const negotiates = [...grouped.values()].some((group) => group.length > 1);
|
|
672
686
|
// Same rule: imported only where a HEAD operation stands alone on its path.
|
|
673
|
-
const guardsHead = registrations.some((registration) => registration.
|
|
687
|
+
const guardsHead = registrations.some((registration) => registration.headOnly);
|
|
674
688
|
/**
|
|
675
|
-
*
|
|
689
|
+
* **Which runtime imports to write is read from the DATA, never from the rendered text.**
|
|
676
690
|
*
|
|
677
|
-
*
|
|
678
|
-
*
|
|
679
|
-
*
|
|
691
|
+
* Matching a substring of generated output is how one of these went missing already: it tested for
|
|
692
|
+
* `byContentType([`, the emitted call gained arguments before its bracket, the substring stopped
|
|
693
|
+
* matching, the import stopped being written, and the emitted module threw
|
|
680
694
|
* `ReferenceError: byContentType is not defined` at registration. Nothing in the emitter objected,
|
|
681
695
|
* because the check was a string about a string.
|
|
696
|
+
*
|
|
697
|
+
* `guardsHead` above had the same shape, and the sibling library had it in a worse place, deciding
|
|
698
|
+
* whether a parameter gets a wire decoder by `declared.startsWith("z.number()")`. A literal union
|
|
699
|
+
* (`@query size: 10 | 25 | 50`) begins with neither prefix, so every conformant caller of that
|
|
700
|
+
* parameter got a 400 that no document comparison could see.
|
|
701
|
+
*
|
|
702
|
+
* The rendered text is still read to decide which VALIDATOR identifiers to import, and that one is
|
|
703
|
+
* safe in a way these are not: an identifier absent from the text is genuinely not needed, so the
|
|
704
|
+
* check cannot be wrong in the direction that breaks a build.
|
|
682
705
|
*/
|
|
683
706
|
const dispatchesBody = entries.some((entry) => entry.dispatched !== undefined);
|
|
707
|
+
/**
|
|
708
|
+
* **Imported only where a route actually validates something, like every other value import here.**
|
|
709
|
+
*
|
|
710
|
+
* This one was unconditional while the three around it were not, and the comment above them stated
|
|
711
|
+
* the rule it was breaking. A service whose operations declare no parameters at all -- two bare
|
|
712
|
+
* `GET`s, which is where a health check starts and therefore where a new consumer starts -- emitted
|
|
713
|
+
* an import nothing used, and `tsp compile` reported success:
|
|
714
|
+
* `TS6133: 'zValidator' is declared but its value is never read`.
|
|
715
|
+
*
|
|
716
|
+
* Counted from the same filtered list the middleware is rendered from, so it cannot disagree with
|
|
717
|
+
* what was emitted: a dispatched body's validator is `byContentType`, not a `zValidator`.
|
|
718
|
+
*/
|
|
719
|
+
/**
|
|
720
|
+
* **`z` is only ever reached through `z.infer`**, which appears where an operation has an input
|
|
721
|
+
* type or a response body. A service whose every operation takes nothing and returns `void` names
|
|
722
|
+
* neither, so the import was written and never used:
|
|
723
|
+
* `TS6133: 'z' is declared but its value is never read`. Same shape as the `zValidator` one above,
|
|
724
|
+
* one step further along.
|
|
725
|
+
*/
|
|
726
|
+
const usesZod = entries.some((entry) => inputTypeOf(entry) !== undefined || entry.names.response !== undefined);
|
|
727
|
+
const validates = entries.some((entry) => entry.validators.filter(([target]) => entry.dispatched === undefined || target !== VALIDATOR_TARGET.body).length > 0);
|
|
684
728
|
const runtimeModule = JSON.stringify(emitted.options.runtimeModule);
|
|
685
729
|
/**
|
|
686
730
|
* **One base sub-app, mounted with `app.route()`. Hono's own nesting, not a rewritten path on
|
|
@@ -692,10 +736,8 @@ securityFor) {
|
|
|
692
736
|
const usesBasePath = basePaths.length > 0;
|
|
693
737
|
const needsHonoValue = subApps.size > 0 || usesBasePath;
|
|
694
738
|
return `${GENERATED_BANNER}
|
|
695
|
-
import { zValidator } from "@hono/zod-validator";
|
|
696
|
-
${
|
|
697
|
-
import { z } from "zod";
|
|
698
|
-
import type { AppEnv, Awaitable, Ctx, Result, RouteDeps } from ${runtimeModule};${negotiates ? `\nimport { selectContentType } from ${runtimeModule};` : ""}${guardsHead ? `\nimport { headOnly } from ${runtimeModule};` : ""}${dispatchesBody ? `\nimport { byContentType } from ${runtimeModule};` : ""}
|
|
739
|
+
${validates ? 'import { zValidator } from "@hono/zod-validator";\n' : ""}${needsHonoValue ? 'import { Hono } from "hono";\nimport type { Context, Input } from "hono";' : 'import type { Context, Hono, Input } from "hono";'}
|
|
740
|
+
${usesZod ? 'import { z } from "zod";\n' : ""}import type { AppEnv, Awaitable, Ctx, Result, RouteDeps } from ${runtimeModule};${negotiates ? `\nimport { selectContentType } from ${runtimeModule};` : ""}${guardsHead ? `\nimport { headOnly } from ${runtimeModule};` : ""}${dispatchesBody ? `\nimport { byContentType } from ${runtimeModule};` : ""}
|
|
699
741
|
${imports}
|
|
700
742
|
/**
|
|
701
743
|
* One method per operation, each concretely typed from the schemas it validates against.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typespec-hono",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "TypeSpec emitter: generate a Hono server, and the Zod validators it enforces, from an HTTP service definition, agreeing with the OpenAPI document @typespec/openapi3 publishes from the same source.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare-workers",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"provenance": true
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"typespec-http-zod": "^0.
|
|
47
|
+
"typespec-http-zod": "^0.9.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@hono/zod-openapi": "^1.4.0",
|