diff --git a/package-lock.json b/package-lock.json index 059b0ffd..be6d5d17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1624,13 +1624,13 @@ } }, "node_modules/@swc/core": { - "version": "1.5.7", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.5.7.tgz", - "integrity": "sha512-U4qJRBefIJNJDRCCiVtkfa/hpiZ7w0R6kASea+/KLp+vkus3zcLSB8Ub8SvKgTIxjWpwsKcZlPf5nrv4ls46SQ==", + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.10.6.tgz", + "integrity": "sha512-zgXXsI6SAVwr6XsXyMnqlyLoa1lT+r09bAWI1xT3679ejWqI1Vnl14eJG0GjWYXCEMKHCNytfMq3OOQ62C39QQ==", "hasInstallScript": true, "dependencies": { - "@swc/counter": "^0.1.2", - "@swc/types": "0.1.7" + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.17" }, "engines": { "node": ">=10" @@ -1640,19 +1640,19 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.5.7", - "@swc/core-darwin-x64": "1.5.7", - "@swc/core-linux-arm-gnueabihf": "1.5.7", - "@swc/core-linux-arm64-gnu": "1.5.7", - "@swc/core-linux-arm64-musl": "1.5.7", - "@swc/core-linux-x64-gnu": "1.5.7", - "@swc/core-linux-x64-musl": "1.5.7", - "@swc/core-win32-arm64-msvc": "1.5.7", - "@swc/core-win32-ia32-msvc": "1.5.7", - "@swc/core-win32-x64-msvc": "1.5.7" + "@swc/core-darwin-arm64": "1.10.6", + "@swc/core-darwin-x64": "1.10.6", + "@swc/core-linux-arm-gnueabihf": "1.10.6", + "@swc/core-linux-arm64-gnu": "1.10.6", + "@swc/core-linux-arm64-musl": "1.10.6", + "@swc/core-linux-x64-gnu": "1.10.6", + "@swc/core-linux-x64-musl": "1.10.6", + "@swc/core-win32-arm64-msvc": "1.10.6", + "@swc/core-win32-ia32-msvc": "1.10.6", + "@swc/core-win32-x64-msvc": "1.10.6" }, "peerDependencies": { - "@swc/helpers": "^0.5.0" + "@swc/helpers": "*" }, "peerDependenciesMeta": { "@swc/helpers": { @@ -1661,9 +1661,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.5.7", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.5.7.tgz", - "integrity": "sha512-bZLVHPTpH3h6yhwVl395k0Mtx8v6CGhq5r4KQdAoPbADU974Mauz1b6ViHAJ74O0IVE5vyy7tD3OpkQxL/vMDQ==", + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.10.6.tgz", + "integrity": "sha512-USbMvT8Rw5PvIfF6HyTm+yW84J9c45emzmHBDIWY76vZHkFsS5MepNi+JLQyBzBBgE7ScwBRBNhRx6VNhkSoww==", "cpu": [ "arm64" ], @@ -1676,9 +1676,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.5.7", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.5.7.tgz", - "integrity": "sha512-RpUyu2GsviwTc2qVajPL0l8nf2vKj5wzO3WkLSHAHEJbiUZk83NJrZd1RVbEknIMO7+Uyjh54hEh8R26jSByaw==", + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.10.6.tgz", + "integrity": "sha512-7t2IozcZN4r1p27ei+Kb8IjN4aLoBDn107fPi+aPLcVp2uFgJEUzhCDuZXBNW2057Mx1OHcjzrkaleRpECz3Xg==", "cpu": [ "x64" ], @@ -1691,9 +1691,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.5.7", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.5.7.tgz", - "integrity": "sha512-cTZWTnCXLABOuvWiv6nQQM0hP6ZWEkzdgDvztgHI/+u/MvtzJBN5lBQ2lue/9sSFYLMqzqff5EHKlFtrJCA9dQ==", + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.10.6.tgz", + "integrity": "sha512-CPgWT+D0bDp/qhXsLkIJ54LmKU1/zvyGaf/yz8A4iR+YoF6R5CSXENXhNJY8cIrb6+uNWJZzHJ+gefB5V51bpA==", "cpu": [ "arm" ], @@ -1706,9 +1706,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.5.7", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.5.7.tgz", - "integrity": "sha512-hoeTJFBiE/IJP30Be7djWF8Q5KVgkbDtjySmvYLg9P94bHg9TJPSQoC72tXx/oXOgXvElDe/GMybru0UxhKx4g==", + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.10.6.tgz", + "integrity": "sha512-5qZ6hVnqO/ShETXdGSzvdGUVx372qydlj1YWSYiaxQzTAepEBc8TC1NVUgYtOHOKVRkky1d7p6GQ9lymsd4bHw==", "cpu": [ "arm64" ], @@ -1721,9 +1721,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.5.7", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.5.7.tgz", - "integrity": "sha512-+NDhK+IFTiVK1/o7EXdCeF2hEzCiaRSrb9zD7X2Z7inwWlxAntcSuzZW7Y6BRqGQH89KA91qYgwbnjgTQ22PiQ==", + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.10.6.tgz", + "integrity": "sha512-hB2xZFmXCKf2iJF5y2z01PSuLqEoUP3jIX/XlIHN+/AIP7PkSKsValE63LnjlnWPnSEI0IxUyRE3T3FzWE/fQQ==", "cpu": [ "arm64" ], @@ -1736,9 +1736,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.5.7", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.5.7.tgz", - "integrity": "sha512-25GXpJmeFxKB+7pbY7YQLhWWjkYlR+kHz5I3j9WRl3Lp4v4UD67OGXwPe+DIcHqcouA1fhLhsgHJWtsaNOMBNg==", + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.10.6.tgz", + "integrity": "sha512-PRGPp0I22+oJ8RMGg8M4hXYxEffH3ayu0WoSDPOjfol1F51Wj1tfTWN4wVa2RibzJjkBwMOT0KGLGb/hSEDDXQ==", "cpu": [ "x64" ], @@ -1751,9 +1751,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.5.7", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.5.7.tgz", - "integrity": "sha512-0VN9Y5EAPBESmSPPsCJzplZHV26akC0sIgd3Hc/7S/1GkSMoeuVL+V9vt+F/cCuzr4VidzSkqftdP3qEIsXSpg==", + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.10.6.tgz", + "integrity": "sha512-SoNBxlA86lnoV9vIz/TCyakLkdRhFSHx6tFMKNH8wAhz1kKYbZfDmpYoIzeQqdTh0tpx8e/Zu1zdK4smovsZqQ==", "cpu": [ "x64" ], @@ -1766,9 +1766,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.5.7", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.5.7.tgz", - "integrity": "sha512-RtoNnstBwy5VloNCvmvYNApkTmuCe4sNcoYWpmY7C1+bPR+6SOo8im1G6/FpNem8AR5fcZCmXHWQ+EUmRWJyuA==", + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.10.6.tgz", + "integrity": "sha512-6L5Y2E+FVvM+BtoA+mJFjf/SjpFr73w2kHBxINxwH8/PkjAjkePDr5m0ibQhPXV61bTwX49+1otzTY85EsUW9Q==", "cpu": [ "arm64" ], @@ -1781,9 +1781,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.5.7", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.5.7.tgz", - "integrity": "sha512-Xm0TfvcmmspvQg1s4+USL3x8D+YPAfX2JHygvxAnCJ0EHun8cm2zvfNBcsTlnwYb0ybFWXXY129aq1wgFC9TpQ==", + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.10.6.tgz", + "integrity": "sha512-kxK3tW8DJwEkAkwy0vhwoBAShRebH1QTe0mvH9tlBQ21rToVZQn+GCV/I44dind80hYPw0Tw2JKFVfoEJyBszg==", "cpu": [ "ia32" ], @@ -1796,9 +1796,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.5.7", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.5.7.tgz", - "integrity": "sha512-tp43WfJLCsKLQKBmjmY/0vv1slVywR5Q4qKjF5OIY8QijaEW7/8VwPyUyVoJZEnDgv9jKtUTG5PzqtIYPZGnyg==", + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.10.6.tgz", + "integrity": "sha512-4pJka/+t8XcHee12G/R5VWcilkp5poT2EJhrybpuREkpQ7iC/4WOlOVrohbWQ4AhDQmojYQI/iS+gdF2JFLzTQ==", "cpu": [ "x64" ], @@ -1816,9 +1816,9 @@ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" }, "node_modules/@swc/types": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.7.tgz", - "integrity": "sha512-scHWahbHF0eyj3JsxG9CFJgFdFNaVQCNAimBlT6PzS3n/HptxqREjsm4OH6AN3lYcffZYSPxXW8ua2BEHp0lJQ==", + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz", + "integrity": "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==", "dependencies": { "@swc/counter": "^0.1.3" } @@ -14453,7 +14453,7 @@ "version": "0.0.0-semantically-released", "license": "Apache-2.0", "dependencies": { - "@swc/core": "1.5.7", + "@swc/core": "1.10.6", "cmd-ts": "0.13.0", "comment-parser": "1.4.1", "fp-ts": "2.16.9", @@ -14473,8 +14473,8 @@ "typescript": "4.7.4" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.5.7", - "@swc/core-linux-x64-gnu": "1.5.7" + "@swc/core-darwin-arm64": "1.10.6", + "@swc/core-linux-x64-gnu": "1.10.6" } }, "packages/openapi-generator/node_modules/typescript": { diff --git a/packages/openapi-generator/package.json b/packages/openapi-generator/package.json index c0ef1378..142b19fa 100644 --- a/packages/openapi-generator/package.json +++ b/packages/openapi-generator/package.json @@ -22,7 +22,7 @@ "test:target": "c8 --all --src src node --require @swc-node/register" }, "dependencies": { - "@swc/core": "1.5.7", + "@swc/core": "1.10.6", "cmd-ts": "0.13.0", "comment-parser": "1.4.1", "fp-ts": "2.16.9", @@ -39,8 +39,8 @@ "typescript": "4.7.4" }, "optionalDependencies": { - "@swc/core-linux-x64-gnu": "1.5.7", - "@swc/core-darwin-arm64": "1.5.7" + "@swc/core-linux-x64-gnu": "1.10.6", + "@swc/core-darwin-arm64": "1.10.6" }, "publishConfig": { "access": "public" diff --git a/packages/openapi-generator/src/comments.ts b/packages/openapi-generator/src/comments.ts index 54723da8..48c04059 100644 --- a/packages/openapi-generator/src/comments.ts +++ b/packages/openapi-generator/src/comments.ts @@ -1,13 +1,46 @@ import { parse as parseComment, Block } from 'comment-parser'; import { Schema } from './ir'; +/** + * Convert a UTF-8 byte offset to a JavaScript string character offset. + * SWC (written in Rust) uses byte offsets, but JavaScript strings use + * UTF-16 code unit offsets. This function handles the conversion by + * iterating through the string and accumulating byte lengths. + * + * @param str The source string + * @param byteOffset The byte offset to convert + * @returns The corresponding character offset + */ +function byteOffsetToCharOffset(str: string, byteOffset: number): number { + let charCount = 0; + let byteCount = 0; + + for (const char of str) { + const charBytes = Buffer.byteLength(char, 'utf8'); + if (byteCount + charBytes > byteOffset) break; + byteCount += charBytes; + charCount++; + } + + return charCount; +} + export function leadingComment( src: string, srcSpanStart: number, start: number, end: number, ): Block[] { - let commentString = src.slice(start - srcSpanStart, end - srcSpanStart).trim(); + // SWC uses byte offsets, but JavaScript strings use character offsets. + // When there are multibyte UTF-8 characters (e.g., À, 日, 😀), we need to + // convert byte offsets to character offsets for correct string slicing. + const startByteOffset = start - srcSpanStart; + const endByteOffset = end - srcSpanStart; + + const startCharOffset = byteOffsetToCharOffset(src, startByteOffset); + const endCharOffset = byteOffsetToCharOffset(src, endByteOffset); + + let commentString = src.slice(startCharOffset, endCharOffset).trim(); if (commentString.includes(' * ') && !/\/\*\*([\s\S]*?)\*\//.test(commentString)) { // The comment block seems to be JSDoc but was sliced incorrectly diff --git a/packages/openapi-generator/test/openapi/comments.test.ts b/packages/openapi-generator/test/openapi/comments.test.ts index fc8930e6..a61b2863 100644 --- a/packages/openapi-generator/test/openapi/comments.test.ts +++ b/packages/openapi-generator/test/openapi/comments.test.ts @@ -1488,4 +1488,506 @@ testCase("route with overriden comments in union", ROUTE_WITH_OVERRIDEN_COMMENTS } } } +}); + +// ============================================================================ +// Multibyte Character Tests +// These tests verify that SWC byte offsets are correctly converted to +// JavaScript character offsets when the source contains multibyte UTF-8 chars. +// ============================================================================ + +// Test case 1: Extended Latin characters (2-byte UTF-8) +const ROUTE_WITH_LATIN_EXTENDED_CHARS = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +export const Body = t.type({ + /** + * Name with accented characters (À-ÿ, Ā-ſ) + * @pattern ^[A-Za-zÀ-ÿĀ-ſ\\s'-]+$ + */ + firstName: t.string, + /** + * Surname field (supports ñ, ü, ø, etc.) + * @pattern ^[A-Za-zÀ-ÿĀ-ſ\\s'-]+$ + */ + lastName: t.string, +}); + +/** + * Route testing Latin extended characters + * + * @operationId api.v1.latinChars + * @tag Test Routes + */ +export const route = h.httpRoute({ + path: '/latin-chars', + method: 'POST', + request: h.httpRequest({ + body: Body, + }), + response: { + 200: { + result: t.string + } + }, +}); +`; + +testCase('route with latin extended characters', ROUTE_WITH_LATIN_EXTENDED_CHARS, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0', + }, + paths: { + '/latin-chars': { + post: { + summary: 'Route testing Latin extended characters', + operationId: 'api.v1.latinChars', + tags: ['Test Routes'], + parameters: [], + requestBody: { + content: { + 'application/json': { + schema: { + properties: { + firstName: { + type: 'string', + description: "Name with accented characters (À-ÿ, Ā-ſ)", + pattern: "^[A-Za-zÀ-ÿĀ-ſ\\s'-]+$", + }, + lastName: { + type: 'string', + description: "Surname field (supports ñ, ü, ø, etc.)", + pattern: "^[A-Za-zÀ-ÿĀ-ſ\\s'-]+$", + }, + }, + required: ['firstName', 'lastName'], + type: 'object', + }, + }, + }, + }, + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + result: { + type: 'string', + }, + }, + required: ['result'], + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Body: { + title: 'Body', + type: 'object', + properties: { + firstName: { + type: 'string', + description: "Name with accented characters (À-ÿ, Ā-ſ)", + pattern: "^[A-Za-zÀ-ÿĀ-ſ\\s'-]+$", + }, + lastName: { + type: 'string', + description: "Surname field (supports ñ, ü, ø, etc.)", + pattern: "^[A-Za-zÀ-ÿĀ-ſ\\s'-]+$", + }, + }, + required: ['firstName', 'lastName'], + }, + }, + }, +}); + +// Test case 2: CJK characters (3-byte UTF-8) +const ROUTE_WITH_CJK_CHARS = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +export const Body = t.type({ + /** + * 日本語の名前フィールド (Japanese name field) + * @example 山田太郎 + */ + japaneseName: t.string, + /** + * 中文名字字段 (Chinese name field) + * @example 张三 + */ + chineseName: t.string, + /** + * 한국어 이름 필드 (Korean name field) + * @example 김철수 + */ + koreanName: t.string, +}); + +/** + * Route testing CJK characters (日本語, 中文, 한국어) + * + * @operationId api.v1.cjkChars + * @tag Test Routes + */ +export const route = h.httpRoute({ + path: '/cjk-chars', + method: 'POST', + request: h.httpRequest({ + body: Body, + }), + response: { + 200: { + result: t.string + } + }, +}); +`; + +testCase('route with CJK characters', ROUTE_WITH_CJK_CHARS, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0', + }, + paths: { + '/cjk-chars': { + post: { + summary: 'Route testing CJK characters (日本語, 中文, 한국어)', + operationId: 'api.v1.cjkChars', + tags: ['Test Routes'], + parameters: [], + requestBody: { + content: { + 'application/json': { + schema: { + properties: { + japaneseName: { + type: 'string', + description: '日本語の名前フィールド (Japanese name field)', + example: '山田太郎', + }, + chineseName: { + type: 'string', + description: '中文名字字段 (Chinese name field)', + example: '张三', + }, + koreanName: { + type: 'string', + description: '한국어 이름 필드 (Korean name field)', + example: '김철수', + }, + }, + required: ['japaneseName', 'chineseName', 'koreanName'], + type: 'object', + }, + }, + }, + }, + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + result: { + type: 'string', + }, + }, + required: ['result'], + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Body: { + title: 'Body', + type: 'object', + properties: { + japaneseName: { + type: 'string', + description: '日本語の名前フィールド (Japanese name field)', + example: '山田太郎', + }, + chineseName: { + type: 'string', + description: '中文名字字段 (Chinese name field)', + example: '张三', + }, + koreanName: { + type: 'string', + description: '한국어 이름 필드 (Korean name field)', + example: '김철수', + }, + }, + required: ['japaneseName', 'chineseName', 'koreanName'], + }, + }, + }, +}); + +// Test case 3: Mixed multibyte characters at multiple positions +const ROUTE_WITH_MIXED_MULTIBYTE = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * Café menu item (note: café has 2-byte é) + */ +const CaféItem = t.type({ + /** Item name (日本語 OK) */ + name: t.string, + /** Price in € (euros) */ + price: t.number, +}); + +export const Body = t.type({ + /** + * Order at Müller's café + * @example Crème brûlée + */ + item: CaféItem, + /** + * Customer name (supports: José, François, 田中) + */ + customerName: t.string, +}); + +/** + * Route with mixed multibyte: é, ü, è, û, 日本語 + * + * @operationId api.v1.mixedMultibyte + * @tag Test Routes + */ +export const route = h.httpRoute({ + path: '/mixed-multibyte', + method: 'POST', + request: h.httpRequest({ + body: Body, + }), + response: { + 200: { + result: t.string + } + }, +}); +`; + +testCase('route with mixed multibyte characters', ROUTE_WITH_MIXED_MULTIBYTE, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0', + }, + paths: { + '/mixed-multibyte': { + post: { + summary: 'Route with mixed multibyte: é, ü, è, û, 日本語', + operationId: 'api.v1.mixedMultibyte', + tags: ['Test Routes'], + parameters: [], + requestBody: { + content: { + 'application/json': { + schema: { + properties: { + item: { + allOf: [ + { + $ref: '#/components/schemas/CaféItem', + }, + ], + description: "Order at Müller's café", + example: 'Crème brûlée', + }, + customerName: { + type: 'string', + description: 'Customer name (supports: José, François, 田中)', + }, + }, + required: ['item', 'customerName'], + type: 'object', + }, + }, + }, + }, + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + result: { + type: 'string', + }, + }, + required: ['result'], + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + CaféItem: { + title: 'CaféItem', + description: 'Café menu item (note: café has 2-byte é)', + type: 'object', + properties: { + name: { + type: 'string', + description: 'Item name (日本語 OK)', + }, + price: { + type: 'number', + description: 'Price in € (euros)', + }, + }, + required: ['name', 'price'], + }, + Body: { + title: 'Body', + type: 'object', + properties: { + item: { + allOf: [ + { + $ref: '#/components/schemas/CaféItem', + }, + ], + description: "Order at Müller's café", + example: 'Crème brûlée', + }, + customerName: { + type: 'string', + description: 'Customer name (supports: José, François, 田中)', + }, + }, + required: ['item', 'customerName'], + }, + }, + }, +}); + +// Test case 4: Multibyte characters at the very start of the file +const ROUTE_WITH_MULTIBYTE_AT_START = `/** + * 日本語コメント at the very start + */ +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +export const Body = t.type({ + /** Normal field after multibyte start */ + value: t.string, +}); + +/** + * Route where file starts with multibyte chars + * + * @operationId api.v1.multibyteStart + * @tag Test Routes + */ +export const route = h.httpRoute({ + path: '/multibyte-start', + method: 'POST', + request: h.httpRequest({ + body: Body, + }), + response: { + 200: { + result: t.string + } + }, +}); +`; + +testCase('route with multibyte at file start', ROUTE_WITH_MULTIBYTE_AT_START, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0', + }, + paths: { + '/multibyte-start': { + post: { + summary: 'Route where file starts with multibyte chars', + operationId: 'api.v1.multibyteStart', + tags: ['Test Routes'], + parameters: [], + requestBody: { + content: { + 'application/json': { + schema: { + properties: { + value: { + type: 'string', + description: 'Normal field after multibyte start', + }, + }, + required: ['value'], + type: 'object', + }, + }, + }, + }, + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + result: { + type: 'string', + }, + }, + required: ['result'], + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Body: { + title: 'Body', + type: 'object', + properties: { + value: { + type: 'string', + description: 'Normal field after multibyte start', + }, + }, + required: ['value'], + }, + }, + }, }); \ No newline at end of file