feat: experimental typescript-nextjs template#152
Conversation
54335e8 to
3033f06
Compare
adds https://github.com/mnahkies/openapi-code-generator/ / https://openapi-code-generator.nahkies.co.nz/ to the list. it currently supports generating typescript client sdks based on fetch / axios, and server routing / request+response validation based on koa (choice of zod / joi for runtime validation). an experimental nextjs server implementation is in the works (mnahkies/openapi-code-generator#152), and my longer term plan is to add other languages such as kotlin to the set of templates.
3033f06 to
58f706b
Compare
fd15300 to
258f9ab
Compare
258f9ab to
aedad74
Compare
9603fcd to
c47b5cb
Compare
c47b5cb to
2e6b6f2
Compare
| paths.includes(relative), | ||
| ) | ||
|
|
||
| return alias ? alias[0].replace("*", "") : undefined |
Check failure
Code scanning / CodeQL
Incomplete string escaping or encoding High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 15 hours ago
The correct fix is to replace all occurrences of *, not just the first one.
Best minimal change: update line 22 in packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts to use replaceAll("*", "") (or regex /\*/g). Since this codebase already uses replaceAll (line 191), replaceAll is consistent and keeps behavior clear.
No additional imports, helper methods, or dependency changes are needed.
| @@ -19,7 +19,7 @@ | ||
| paths.some((p) => p === relative || relative.endsWith(p.substring(1))), | ||
| ) | ||
|
|
||
| return alias ? alias[0].replace("*", "") : undefined | ||
| return alias ? alias[0].replaceAll("*", "") : undefined | ||
| } | ||
|
|
||
| export async function generateTypescriptNextJS( |
fa2f9dc to
f37745b
Compare
introduces a second server template, `typescript-express` **Tasks** - [x] Template - [x] Runtime - [x] Integration tests - [x] Existing E2E tests - [x] Additional E2E tests - [x] Documentation - [x] Review **Why Express** `express` is one of the most popular server frameworks for NodeJS, getting approximately 10x as many weekly downloads as `koa` Its also quite similar to `koa` making it a good candidate for the second server template. This makes it somewhat less interesting to implement, compared to other options like `nextjs` (#152) but also means that its a good way to prompt refactors like #316 without requiring a bunch of new functionality. **Runtime** It's clear at this point that there is a lot of duplicated code between all the runtimes. I like keeping the separate runtime packages for several reasons: - Dependency declaration / management is cleaner - NPM download trends can function as a rough proxy for adoption / usage _(although private caching registries cause this to under-report significantly)_ It probably makes sense to introduce a `typescript-runtime-common` package soon. **Approach** The approach ends up looking near identical to the `typescript-koa` template, in terms of the end developer experience. This is particularly reinforced by the E2E tests and how little difference exists between the two implementations.
a1a0a6a to
b075b59
Compare
- upgrade all dependencies - regenerate with latest nextjs generator from mnahkies/openapi-code-generator#152 - adjust for nextjs 15
b075b59 to
1876c5e
Compare
f61f814 to
3684213
Compare
required in order to use the latest version of `ts-morph` over in #152
da3921c to
36635f0
Compare
1bb2116 to
8974ef3
Compare
d87a2d5 to
d70261a
Compare
| try { | ||
| ${params.hasParams ? `const input = ${inputObject}` : ""} | ||
| const responder = ${responder.implementation} | ||
| const responseValidator = ${builder.responseValidator()} |
Check warning
Code scanning / CodeQL
Improper code sanitization Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 15 hours ago
General fix: sanitize JSON-stringified values before inserting them into generated code strings, using a dedicated escaping routine that replaces JavaScript/HTML-dangerous characters with safe Unicode escape sequences.
Best single fix without changing functionality: in each affected schema builder default-method (joi-schema-builder.ts, zod-v3-schema-builder.ts, zod-v4-schema-builder.ts), add a local helper that post-processes the output of JSON.stringify and use it for defaultValue. Keep existing needsWrapping behavior unchanged so semantics remain identical except safer escaping.
Concretely:
- In each file, define:
unsafeCharacterMapmapping<,>,/,\b,\f,\n,\r,\t,\0,\u2028,\u2029.escapeUnsafeJsonForCode(value: string): stringusing.replace(...).
- Replace
const defaultValue = JSON.stringify(model.default)with:const defaultValue = escapeUnsafeJsonForCode(JSON.stringify(model.default)).
No new external dependency is required.
| @@ -24,6 +24,26 @@ | ||
|
|
||
| const joi = "joi" | ||
|
|
||
| const unsafeCharacterMap: Record<string, string> = { | ||
| "<": "\\u003C", | ||
| ">": "\\u003E", | ||
| "/": "\\u002F", | ||
| "\b": "\\b", | ||
| "\f": "\\f", | ||
| "\n": "\\n", | ||
| "\r": "\\r", | ||
| "\t": "\\t", | ||
| "\0": "\\0", | ||
| "\u2028": "\\u2028", | ||
| "\u2029": "\\u2029", | ||
| } | ||
|
|
||
| function escapeUnsafeJsonForCode(value: string): string { | ||
| return value.replace(/[<>/\b\f\n\r\t\0\u2028\u2029]/g, (char) => { | ||
| return unsafeCharacterMap[char] ?? char | ||
| }) | ||
| } | ||
|
|
||
| const staticSchemas = {} | ||
| type StaticSchemas = typeof staticSchemas | ||
|
|
||
| @@ -402,7 +422,7 @@ | ||
| typeof model.default !== "string" && | ||
| !(model.nullable && model.default === null) | ||
|
|
||
| const defaultValue = JSON.stringify(model.default) | ||
| const defaultValue = escapeUnsafeJsonForCode(JSON.stringify(model.default)) | ||
|
|
||
| return [ | ||
| schema, |
| @@ -23,6 +23,26 @@ | ||
|
|
||
| const zod = "z" | ||
|
|
||
| const unsafeCharacterMap: Record<string, string> = { | ||
| "<": "\\u003C", | ||
| ">": "\\u003E", | ||
| "/": "\\u002F", | ||
| "\b": "\\b", | ||
| "\f": "\\f", | ||
| "\n": "\\n", | ||
| "\r": "\\r", | ||
| "\t": "\\t", | ||
| "\0": "\\0", | ||
| "\u2028": "\\u2028", | ||
| "\u2029": "\\u2029", | ||
| } | ||
|
|
||
| function escapeUnsafeJsonForCode(value: string): string { | ||
| return value.replace(/[<>/\b\f\n\r\t\0\u2028\u2029]/g, (char) => { | ||
| return unsafeCharacterMap[char] ?? char | ||
| }) | ||
| } | ||
|
|
||
| export const staticSchemas = { | ||
| PermissiveBoolean: `${zod}.preprocess((value) => { | ||
| if(typeof value === "string" && (value === "true" || value === "false")) { | ||
| @@ -457,7 +477,7 @@ | ||
| model.type === "string" && | ||
| typeof model.default !== "string" && | ||
| !(model.nullable && model.default === null) | ||
| const defaultValue = JSON.stringify(model.default) | ||
| const defaultValue = escapeUnsafeJsonForCode(JSON.stringify(model.default)) | ||
|
|
||
| return [ | ||
| schema, |
| @@ -23,6 +23,26 @@ | ||
|
|
||
| const zod = "z" | ||
|
|
||
| const unsafeCharacterMap: Record<string, string> = { | ||
| "<": "\\u003C", | ||
| ">": "\\u003E", | ||
| "/": "\\u002F", | ||
| "\b": "\\b", | ||
| "\f": "\\f", | ||
| "\n": "\\n", | ||
| "\r": "\\r", | ||
| "\t": "\\t", | ||
| "\0": "\\0", | ||
| "\u2028": "\\u2028", | ||
| "\u2029": "\\u2029", | ||
| } | ||
|
|
||
| function escapeUnsafeJsonForCode(value: string): string { | ||
| return value.replace(/[<>/\b\f\n\r\t\0\u2028\u2029]/g, (char) => { | ||
| return unsafeCharacterMap[char] ?? char | ||
| }) | ||
| } | ||
|
|
||
| export const staticSchemas = { | ||
| PermissiveBoolean: `${zod}.preprocess((value) => { | ||
| if(typeof value === "string" && (value === "true" || value === "false")) { | ||
| @@ -426,7 +446,7 @@ | ||
| model.type === "string" && | ||
| typeof model.default !== "string" && | ||
| !(model.nullable && model.default === null) | ||
| const defaultValue = JSON.stringify(model.default) | ||
| const defaultValue = escapeUnsafeJsonForCode(JSON.stringify(model.default)) | ||
|
|
||
| return [ | ||
| schema, |
d70261a to
d471709
Compare
Creates a new
typescript-nextjstemplate./src/generatedcontaining the types and validation boilerplateApproach
Due to the NextJS file based routing paradigm I couldn't see anyway to avoid manipulating files that will contain manually written code.
Pending Refactoring
Currently there is a lot of duplicated code between this and the
typescript-koatemplate which needs rationalizing, as well as it depending on thetypescript-koa-runtimepackage. Part of the motivation behind this is to have more than one server template to allow it to become more generic like the client templates.Because of the
typescript-koa-runtimehack, to actually use this in a NextJS application you need to add the following to yournext.config.mjs:I also don't love the explosion of files this creates when running the standard set of integration suites, and might limit the scope down to one spec to reduce the noise. Ideally I'd like to separate the integration tests to their own repo and also start testing more permutations of options (eg:
joi,extract-inline-schemas).Additionally to make multiple suites play nicely I've had to fudge the output paths in the tests, technically producing incorrect routes.
Performance
I've been told that
ts-morphcan be relatively slow, so it's probably useful to check the performance between this andtypescript-koaOverall it looks similar, aside from calculation of the dependency graph. I'm not sure if this is the bug in the timing code, or if the larger number of compilation units is somehow causing this. Warrants some investigation in case we're calculating it repeatedly or something.
typescript-nextjs - api.github.com.yaml
typescript-koa - api.github.com.yaml