@trapi/metadata
Version:
Generate REST-API metadata scheme from TypeScript Decorators.
1 lines • 246 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","names":["arrayLiteral","readDecoratorName","modelTypeDeclaration","stringifyFlatted","parseFlatted","joinFilePath"],"sources":["../src/core/error/base.ts","../src/core/error/config.ts","../src/core/error/config-codes.ts","../src/core/error/generator.ts","../src/core/error/generator-codes.ts","../src/core/error/parameter-codes.ts","../src/core/error/parameter.ts","../src/core/error/resolver.ts","../src/core/error/validator.ts","../src/core/error/validator-codes.ts","../src/adapters/typescript/js-doc/constants.ts","../src/core/utils/array.ts","../src/core/utils/object.ts","../src/core/utils/path-normalize.ts","../src/adapters/typescript/js-doc/utils.ts","../src/adapters/typescript/js-doc/module.ts","../src/adapters/typescript/initializer.ts","../src/adapters/decorator/typescript/utils.ts","../src/adapters/decorator/typescript/module.ts","../src/adapters/decorator/orchestrator/module.ts","../src/adapters/typescript/validator/module.ts","../src/adapters/typescript/resolver/extension/module.ts","../src/adapters/typescript/resolver/sub/array.ts","../src/adapters/typescript/resolver/sub/base.ts","../src/adapters/typescript/resolver/utils.ts","../src/adapters/typescript/resolver/sub/indexed-access.ts","../src/adapters/typescript/resolver/sub/intersection.ts","../src/adapters/typescript/resolver/sub/literal.ts","../src/adapters/typescript/resolver/sub/mapped.ts","../src/adapters/typescript/resolver/sub/object-literal.ts","../src/adapters/typescript/resolver/sub/primitive.ts","../src/adapters/typescript/resolver/sub/reference.ts","../src/adapters/typescript/resolver/sub/tuple.ts","../src/adapters/typescript/resolver/sub/type-operator.ts","../src/adapters/typescript/resolver/sub/union.ts","../src/adapters/typescript/resolver/module.ts","../src/adapters/typescript/resolver/cache.ts","../src/adapters/typescript/node-utils/module.ts","../src/adapters/filesystem/source-files.ts","../src/adapters/filesystem/tsconfig/module.ts","../src/adapters/cache/constants.ts","../src/adapters/cache/utils.ts","../src/adapters/cache/client.ts","../src/app/generator/parameter/module.ts","../src/app/generator/method/module.ts","../src/app/generator/controller/module.ts","../src/app/generator/metadata/module.ts","../src/app/generate.ts"],"sourcesContent":["/*\n * Copyright (c) 2023.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport { BaseError } from '@ebec/core';\n\nexport class MetadataError extends BaseError {\n\n}\n","/*\n * Copyright (c) 2025.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport { MetadataError } from './base';\n\nexport class ConfigError extends MetadataError {\n\n}\n","/*\n * Copyright (c) 2025.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nexport const ConfigErrorCode = {\n TSCONFIG_MALFORMED: 'CONFIG_TSCONFIG_MALFORMED',\n PRESET_NOT_FOUND: 'CONFIG_PRESET_NOT_FOUND',\n PRESET_MISSING: 'CONFIG_PRESET_MISSING',\n} as const;\nexport type ConfigErrorCode = typeof ConfigErrorCode[keyof typeof ConfigErrorCode];\n","/*\n * Copyright (c) 2025.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport { isBaseError } from '@ebec/core';\nimport { MetadataError } from './base';\n\nexport class GeneratorError extends MetadataError {\n\n}\n\nexport function isGeneratorError(input: unknown): input is GeneratorError & { code: string } {\n if (!isBaseError(input)) {\n return false;\n }\n\n return typeof input.code === 'string';\n}\n","/*\n * Copyright (c) 2025.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nexport const GeneratorErrorCode = {\n CONTROLLER_NO_SOURCE_FILE: 'GENERATOR_CONTROLLER_NO_SOURCE_FILE',\n CONTROLLER_NO_NAME: 'GENERATOR_CONTROLLER_NO_NAME',\n PARAMETER_GENERATION_FAILED: 'GENERATOR_PARAMETER_GENERATION_FAILED',\n BODY_PARAMETER_DUPLICATE: 'GENERATOR_BODY_PARAMETER_DUPLICATE',\n BODY_FORM_CONFLICT: 'GENERATOR_BODY_FORM_CONFLICT',\n STRICT_UNMATCHED_DECORATORS: 'GENERATOR_STRICT_UNMATCHED_DECORATORS',\n} as const;\nexport type GeneratorErrorCode = typeof GeneratorErrorCode[keyof typeof GeneratorErrorCode];\n","/*\n * Copyright (c) 2023.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nexport const ParameterErrorCode = {\n TYPE_UNSUPPORTED: 'PARAMETER_TYPE_UNSUPPORTED',\n METHOD_UNSUPPORTED: 'PARAMETER_METHOD_UNSUPPORTED',\n PATH_MISMATCH: 'PARAMETER_PATH_MISMATCH',\n SCOPE_REQUIRED: 'PARAMETER_SCOPE_REQUIRED',\n INVALID_EXAMPLE: 'PARAMETER_INVALID_EXAMPLE',\n} as const;\nexport type ParameterErrorCode = typeof ParameterErrorCode[keyof typeof ParameterErrorCode];\n","/*\n * Copyright (c) 2022-2023.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\nimport { isClassDeclaration, isMethodDeclaration } from 'typescript';\nimport type { Node } from 'typescript';\nimport type { BaseType } from '@trapi/core';\nimport { ParameterErrorCode } from './parameter-codes';\nimport { MetadataError } from './base';\n\ntype UnsupportedTypeContext = {\n decoratorName: string,\n propertyName: string,\n type: BaseType,\n node?: Node\n};\n\ntype UnsupportedMethodContext = {\n decoratorName: string,\n propertyName: string,\n method: string,\n node?: Node\n};\n\ntype PathMatchInvalidContext = {\n decoratorName: string,\n propertyName: string,\n path: string,\n node?: Node\n};\n\ntype ScopeRequiredContext = {\n decoratorName: string,\n node?: Node\n};\n\nexport class ParameterError extends MetadataError {\n static typeUnsupported(context: UnsupportedTypeContext) {\n const location = context.node ? ParameterError.getCurrentLocation(context.node) : undefined;\n return new ParameterError({\n message: `@${context.decoratorName}('${context.propertyName}') does not support '${context.type.typeName}' type${location ? ` at ${location}` : ''}.`,\n code: ParameterErrorCode.TYPE_UNSUPPORTED,\n });\n }\n\n static methodUnsupported(context: UnsupportedMethodContext) {\n const location = context.node ? ParameterError.getCurrentLocation(context.node) : undefined;\n return new ParameterError({\n message: `@${context.decoratorName}('${context.propertyName}') does not support method '${context.method}'${location ? ` at ${location}` : ''}.`,\n code: ParameterErrorCode.METHOD_UNSUPPORTED,\n });\n }\n\n static invalidPathMatch(context: PathMatchInvalidContext) {\n const location = context.node ? ParameterError.getCurrentLocation(context.node) : undefined;\n return new ParameterError({\n message: `@${context.decoratorName}('${context.propertyName}') does not exist in path '${context.path}'${location ? ` at ${location}` : ''}.`,\n code: ParameterErrorCode.PATH_MISMATCH,\n });\n }\n\n static scopeRequired(context: ScopeRequiredContext) {\n const location = context.node ? ParameterError.getCurrentLocation(context.node) : undefined;\n return new ParameterError({\n message: `@${context.decoratorName}() requires a scope argument${location ? ` at ${location}` : ''}.`,\n code: ParameterErrorCode.SCOPE_REQUIRED,\n });\n }\n\n static invalidExampleSchema() {\n return new ParameterError({\n message: 'The @example JSDoc tag contains invalid JSON.',\n code: ParameterErrorCode.INVALID_EXAMPLE,\n });\n }\n\n public static getCurrentLocation(node: Node) {\n const parts : string[] = [];\n\n if (isMethodDeclaration(node.parent)) {\n parts.push(node.parent.name.getText());\n\n if (isClassDeclaration(node.parent.parent) && node.parent.parent.name) {\n parts.unshift(node.parent.parent.name.text);\n }\n }\n\n return parts.join('.');\n }\n}\n","/*\n * Copyright (c) 2021.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport { normalize } from 'node:path';\nimport { isBaseError } from '@ebec/core';\nimport type { Node, TypeNode } from 'typescript';\nimport { MetadataError } from './base';\n\nexport class ResolverError extends MetadataError {\n public readonly file?: string;\n\n public readonly line?: number;\n\n constructor(\n message: string,\n node?: Node | TypeNode,\n options?: boolean | { onlyCurrent?: boolean, cause?: unknown },\n ) {\n const opts = typeof options === 'boolean' ? { onlyCurrent: options } : options;\n const onlyCurrent = opts?.onlyCurrent ?? false;\n\n const parts: string[] = [message];\n let file: string | undefined;\n let line: number | undefined;\n\n if (node) {\n const location = prettyLocationOfNode(node);\n if (location) {\n parts.push(location.text);\n file = location.file;\n line = location.line;\n }\n\n parts.push(prettyTroubleCause(node, onlyCurrent));\n }\n\n super({\n message: parts.join('\\n'),\n cause: opts?.cause,\n });\n\n this.file = file;\n this.line = line;\n }\n}\n\nexport function isResolverError(input: unknown): input is ResolverError {\n if (!isBaseError(input)) {\n return false;\n }\n\n return 'file' in input && 'line' in input;\n}\n\nexport function prettyLocationOfNode(node: Node | TypeNode): {\n text: string,\n file: string,\n line?: number,\n} | undefined {\n try {\n const sourceFile = node.getSourceFile();\n if (!sourceFile) return undefined;\n\n const token = node.getFirstToken() || node.parent?.getFirstToken();\n const start = token ? sourceFile.getLineAndCharacterOfPosition(token.getStart()).line + 1 : undefined;\n const end = token ? sourceFile.getLineAndCharacterOfPosition(token.getEnd()).line + 1 : undefined;\n\n const normalizedFile = normalize(sourceFile.fileName);\n const startSuffix = start ? `:${start}` : '';\n const endSuffix = end ? `:${end}` : '';\n\n return {\n text: `At: ${normalizedFile}${startSuffix}${endSuffix}.`,\n file: sourceFile.fileName,\n line: start,\n };\n } catch {\n return undefined;\n }\n}\n\nexport function prettyTroubleCause(node: Node | TypeNode, onlyCurrent = false) {\n try {\n let name: string;\n if (onlyCurrent || !node.parent) {\n name = node.pos !== -1 ? node.getText() : (node as any).name.text;\n } else {\n name = node.parent.pos !== -1 ? node.parent.getText() : (node as any).parent.name.text;\n }\n\n return `This was caused by '${name}'`;\n } catch {\n return 'This was caused by an unknown node';\n }\n}\n","/*\n * Copyright (c) 2025.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport { MetadataError } from './base';\n\nexport class ValidatorError extends MetadataError {\n\n}\n","/*\n * Copyright (c) 2023.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nexport const ValidatorErrorCode = {\n EXPECTED_NUMBER: 'VALIDATOR_EXPECTED_NUMBER',\n EXPECTED_DATE: 'VALIDATOR_EXPECTED_DATE',\n EXPECTED_STRING: 'VALIDATOR_EXPECTED_STRING',\n} as const;\nexport type ValidatorErrorCode = typeof ValidatorErrorCode[keyof typeof ValidatorErrorCode];\n","/*\n * Copyright (c) 2023.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nexport enum JSDocTagName {\n ABSTRACT = 'abstract',\n ACCESS = 'access',\n ALIAS = 'alias',\n ASYNC = 'async',\n /**\n * Alias for @extends\n */\n AUGMENTS = 'augments',\n AUTHOR = 'author',\n\n // todo: ... add missing\n\n DEFAULT = 'default',\n DEPRECATED = 'deprecated',\n DESCRIPTION = 'description',\n EXAMPLE = 'example',\n\n FORMAT = 'format',\n\n IGNORE = 'ignore',\n\n SUMMARY = 'summary',\n}\n","/*\n * Copyright (c) 2023.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nexport function isStringArray(input: unknown) : input is string[] {\n if (!Array.isArray(input)) {\n return false;\n }\n\n for (const element of input) {\n if (typeof element !== 'string') {\n return false;\n }\n }\n\n return true;\n}\n","/*\n * Copyright (c) 2021-2022.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nexport function hasOwnProperty<Y extends PropertyKey>(obj: unknown, prop: Y): obj is Record<Y, unknown> {\n return Object.prototype.hasOwnProperty.call(obj, prop);\n}\n","/*\n * Copyright (c) 2022.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nexport function normalizePath(str: string) : string {\n // remove slashes\n str = str.replace(/^[/\\\\\\s]+|[/\\\\\\s]+$/g, '');\n\n str = str.replace(/([^:]\\/)\\/+/g, '$1');\n\n return str;\n}\n","/*\n * Copyright (c) 2023.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport { isObject } from 'locter';\nimport type { JSDocComment, NodeArray } from 'typescript';\n\nexport function transformJSDocComment(\n input?: string | NodeArray<JSDocComment>,\n) : string | undefined {\n if (typeof input === 'string') {\n return input;\n }\n\n if (!input || input.length === 0) {\n return undefined;\n }\n\n const comment = input[0];\n if (typeof comment === 'string') {\n return comment;\n }\n\n if (\n isObject(comment) &&\n typeof comment.text === 'string'\n ) {\n return comment.text;\n }\n\n return undefined;\n}\n","/*\n * Copyright (c) 2021-2023.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport type {\n Identifier, \n JSDoc, \n JSDocTag, \n Node,\n} from 'typescript';\nimport { SyntaxKind, isJSDocParameterTag } from 'typescript';\nimport { MetadataError } from '../../../core/error';\nimport { hasOwnProperty } from '../../../core/utils';\nimport type { JSDocTagName } from './constants';\nimport { transformJSDocComment } from './utils';\n\n// -----------------------------------------\n// Description\n// -----------------------------------------\nexport function getJSDocDescription(node: Node, index?: number) : string | undefined {\n const jsDoc = getJSDoc(node, index);\n if (!jsDoc) {\n return undefined;\n }\n\n return transformJSDocComment(jsDoc.comment);\n}\n\n// -----------------------------------------\n// Tag\n// -----------------------------------------\n\nexport function getJSDoc(node: Node, index?: number) : undefined | JSDoc {\n if (!hasOwnProperty(node, 'jsDoc')) {\n return undefined;\n }\n\n const jsDoc : JSDoc[] | undefined = (node as any).jsDoc as JSDoc[];\n\n if (!jsDoc || !Array.isArray(jsDoc) || !jsDoc.length) {\n return undefined;\n }\n\n index = index ?? 0;\n return jsDoc.length > index && index >= 0 ? jsDoc[index] : undefined; // jsDoc[0] else case\n}\n\nexport function getJSDocTags(\n node: Node,\n isMatching?:\n | `${JSDocTagName}` |\n `${JSDocTagName}`[] |\n (string & {}) |\n (string & {})[] |\n ((tag: JSDocTag) => boolean),\n) : JSDocTag[] {\n const jsDoc = getJSDoc(node);\n if (typeof jsDoc === 'undefined') {\n return [];\n }\n\n const jsDocTags : JSDocTag[] = jsDoc.tags as unknown as JSDocTag[];\n\n if (typeof jsDocTags === 'undefined') {\n return [];\n }\n\n if (typeof isMatching === 'undefined') {\n return jsDocTags;\n }\n\n if (typeof isMatching === 'function') {\n return jsDocTags.filter(isMatching);\n }\n\n const tagNames : string[] = Array.isArray(isMatching) ? isMatching : [isMatching];\n\n return jsDocTags.filter((tag) => tagNames.includes(tag.tagName.text));\n}\n\nexport function hasJSDocTag(node: Node, tagName: ((tag: JSDocTag) => boolean) | `${JSDocTagName}` | (string & {})) : boolean {\n const tags : JSDocTag[] = getJSDocTags(node, tagName);\n\n return !(!tags || !tags.length);\n}\n\n// -----------------------------------------\n// Tag Comment(s)\n// -----------------------------------------\n\nexport function getJSDocTagComment(node: Node, tagName: ((tag: JSDocTag) => boolean) | `${JSDocTagName}`) : undefined | string {\n const tags : JSDocTag[] = getJSDocTags(node, tagName);\n const first = tags[0];\n if (!first || typeof first.comment !== 'string') {\n return undefined;\n }\n return first.comment;\n}\n\n// -----------------------------------------\n// Tag Names\n// -----------------------------------------\n\nexport function getJSDocTagNames(node: Node, requireTagName = false) : string[] {\n let tags: JSDocTag[];\n\n /* istanbul ignore next */\n if (node.kind === SyntaxKind.Parameter) {\n const parameterName = ((node as any).name as Identifier).text;\n tags = getJSDocTags(node.parent as any, (tag) => {\n if (isJSDocParameterTag(tag)) {\n return false;\n } if (tag.comment === undefined) {\n throw new MetadataError(`Orphan tag: @${String(tag.tagName.text || tag.tagName.escapedText)} must be followed by a parameter name.`);\n }\n return typeof tag.comment === 'string' ? tag.comment.startsWith(parameterName) : false;\n });\n } else {\n tags = getJSDocTags(node as any, (tag) => (requireTagName ? tag.comment !== undefined : true));\n }\n\n return tags.map((tag) => tag.tagName.text);\n}\n","/*\n * Copyright (c) 2021-2023.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport { SyntaxKind, isImportSpecifier } from 'typescript';\nimport type {\n ArrayLiteralExpression,\n Declaration,\n Expression,\n HasInitializer,\n Identifier,\n ImportSpecifier,\n NewExpression,\n Node,\n NumericLiteral,\n ObjectLiteralExpression,\n PrefixUnaryExpression,\n StringLiteral,\n Symbol as TsSymbol,\n TypeChecker,\n} from 'typescript';\nimport { MetadataError } from '../../core/error';\nimport type { Type } from '@trapi/core';\nimport { hasOwnProperty } from '../../core/utils/object';\n\nexport function getInitializerValue(\n initializer?: Expression,\n typeChecker?: TypeChecker,\n type?: Type,\n) : unknown {\n if (!initializer) {\n return undefined;\n }\n\n switch (initializer.kind) {\n case SyntaxKind.ArrayLiteralExpression: {\n const arrayLiteral = initializer as ArrayLiteralExpression;\n return arrayLiteral.elements.map((element) => getInitializerValue(element, typeChecker));\n }\n case SyntaxKind.StringLiteral:\n case SyntaxKind.NoSubstitutionTemplateLiteral:\n return (initializer as StringLiteral).text;\n case SyntaxKind.TrueKeyword:\n return true;\n case SyntaxKind.FalseKeyword:\n return false;\n case SyntaxKind.PrefixUnaryExpression: {\n const prefixUnary = initializer as PrefixUnaryExpression;\n switch (prefixUnary.operator) {\n case SyntaxKind.PlusToken:\n return Number((prefixUnary.operand as NumericLiteral).text);\n case SyntaxKind.MinusToken:\n return Number(`-${(prefixUnary.operand as NumericLiteral).text}`);\n default:\n throw new MetadataError(`Unsupported prefix operator token: ${prefixUnary.operator}`);\n }\n }\n case SyntaxKind.NumberKeyword:\n case SyntaxKind.FirstLiteralToken:\n return Number((initializer as NumericLiteral).text);\n case SyntaxKind.NewExpression: {\n const newExpression = initializer as NewExpression;\n const ident = newExpression.expression as Identifier;\n\n if (ident.text === 'Date') {\n let date = new Date();\n if (newExpression.arguments) {\n const newArguments = newExpression.arguments.filter((args) => args.kind !== undefined);\n const argsValue = newArguments.map((args) => getInitializerValue(args, typeChecker));\n if (argsValue.length > 0) {\n date = new Date(argsValue as any);\n }\n }\n const dateString = date.toISOString();\n if (type && type.typeName === 'date') {\n return dateString.split('T')[0];\n }\n\n return dateString;\n }\n\n return undefined;\n }\n case SyntaxKind.NullKeyword: {\n return null;\n }\n case SyntaxKind.ObjectLiteralExpression: {\n const objectLiteral = initializer as ObjectLiteralExpression;\n const nestedObject: any = {};\n objectLiteral.properties.forEach((p: any) => {\n nestedObject[p.name.text] = getInitializerValue(p.initializer, typeChecker);\n });\n return nestedObject;\n }\n case SyntaxKind.ImportSpecifier: {\n if (typeof typeChecker === 'undefined') {\n return undefined;\n }\n\n const importSpecifier = (initializer as any) as ImportSpecifier;\n const importSymbol = typeChecker.getSymbolAtLocation(importSpecifier.name);\n if (!importSymbol) {\n return undefined;\n }\n\n const aliasedSymbol = typeChecker.getAliasedSymbol(importSymbol);\n const declarations = aliasedSymbol.getDeclarations();\n const declaration = declarations && declarations.length > 0 ? declarations[0] : undefined;\n return getInitializerValue(extractInitializer(declaration), typeChecker);\n }\n default: {\n if (typeof initializer === 'undefined') {\n return undefined;\n }\n if (\n typeof initializer.parent === 'undefined' ||\n typeof typeChecker === 'undefined'\n ) {\n if (hasOwnProperty(initializer, 'text')) {\n return initializer.text;\n }\n\n return undefined;\n }\n\n const symbol = typeChecker.getSymbolAtLocation(initializer);\n if (!symbol) {\n return undefined;\n }\n return getInitializerValue(\n extractInitializer(symbol.valueDeclaration) || extractInitializer(extractImportSpecifier(symbol)),\n typeChecker,\n );\n }\n }\n}\n\nexport const hasInitializer = (\n node: Node,\n): node is HasInitializer => Object.prototype.hasOwnProperty.call(node, 'initializer');\nconst extractInitializer = (\n valueDeclaration?: Declaration,\n) => (valueDeclaration && hasInitializer(valueDeclaration) && (valueDeclaration.initializer as Expression)) || undefined;\nconst extractImportSpecifier = (\n symbol?: TsSymbol,\n) => {\n const declaration = symbol?.declarations?.[0];\n return declaration && isImportSpecifier(declaration) ? declaration : undefined;\n};\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport {\n SyntaxKind,\n canHaveDecorators,\n getDecorators,\n isArrayLiteralExpression,\n isCallExpression,\n isIdentifier,\n isNoSubstitutionTemplateLiteral,\n isNumericLiteral,\n isObjectLiteralExpression,\n isPrefixUnaryExpression,\n isPropertyAccessExpression,\n isStringLiteral,\n} from 'typescript';\nimport type {\n Expression,\n Node,\n TypeChecker,\n} from 'typescript';\nimport { getInitializerValue } from '../../typescript/initializer';\nimport type { DecoratorArgument } from '@trapi/core';\n\nexport type RawDecorator = {\n name: string;\n arguments: DecoratorArgument[];\n};\n\n/**\n * Enumerate decorators on a TS node and classify their argument values without\n * going through the registry. Used by read-side consumers (type resolver,\n * extension extraction) that only need decorator names + argument values.\n */\nexport function readNodeDecorators(node: Node, typeChecker?: TypeChecker): RawDecorator[] {\n if (!canHaveDecorators(node)) {\n return [];\n }\n const decorators = getDecorators(node);\n if (!decorators || decorators.length === 0) {\n return [];\n }\n\n const output: RawDecorator[] = [];\n for (const decorator of decorators) {\n const { expression } = decorator;\n let name: string | undefined;\n let argumentExpressions: readonly Expression[] = [];\n\n if (isCallExpression(expression)) {\n argumentExpressions = expression.arguments;\n name = readDecoratorName(expression.expression);\n } else {\n name = readDecoratorName(expression);\n }\n\n if (!name) {\n continue;\n }\n\n output.push({\n name,\n arguments: argumentExpressions.map((a) => buildDecoratorArgument(a, typeChecker)),\n });\n }\n return output;\n}\n\nexport function findDecoratorByName(\n node: Node,\n name: string,\n typeChecker?: TypeChecker,\n): RawDecorator | undefined {\n return readNodeDecorators(node, typeChecker).find((d) => d.name === name);\n}\n\nexport function findDecoratorsByName(\n node: Node,\n name: string,\n typeChecker?: TypeChecker,\n): RawDecorator[] {\n return readNodeDecorators(node, typeChecker).filter((d) => d.name === name);\n}\n\nexport function hasDecoratorNamed(node: Node, name: string, typeChecker?: TypeChecker): boolean {\n return readNodeDecorators(node, typeChecker).some((d) => d.name === name);\n}\n\nfunction readDecoratorName(expression: Node): string | undefined {\n if (isIdentifier(expression)) {\n return expression.text;\n }\n if (isPropertyAccessExpression(expression)) {\n return expression.name.text;\n }\n return undefined;\n}\n\nexport function buildDecoratorArgument(\n expr: Expression,\n typeChecker?: TypeChecker,\n): DecoratorArgument {\n if (\n isStringLiteral(expr) ||\n isNumericLiteral(expr) ||\n isNoSubstitutionTemplateLiteral(expr)\n ) {\n return { raw: getInitializerValue(expr, typeChecker), kind: 'literal' };\n }\n\n if (expr.kind === SyntaxKind.TrueKeyword) {\n return { raw: true, kind: 'literal' };\n }\n\n if (expr.kind === SyntaxKind.FalseKeyword) {\n return { raw: false, kind: 'literal' };\n }\n\n if (expr.kind === SyntaxKind.NullKeyword) {\n return { raw: null, kind: 'literal' };\n }\n\n if (\n isPrefixUnaryExpression(expr) &&\n (expr.operator === SyntaxKind.PlusToken || expr.operator === SyntaxKind.MinusToken) &&\n isNumericLiteral(expr.operand)\n ) {\n return { raw: getInitializerValue(expr, typeChecker), kind: 'literal' };\n }\n\n if (isObjectLiteralExpression(expr)) {\n return { raw: getInitializerValue(expr, typeChecker), kind: 'object' };\n }\n\n if (isArrayLiteralExpression(expr)) {\n return { raw: getInitializerValue(expr, typeChecker), kind: 'array' };\n }\n\n if (isIdentifier(expr) || isPropertyAccessExpression(expr)) {\n const value = getInitializerValue(expr, typeChecker);\n if (typeof value !== 'undefined') {\n return { raw: value, kind: 'identifier' };\n }\n return { raw: undefined, kind: 'unresolvable' };\n }\n\n return { raw: undefined, kind: 'unresolvable' };\n}\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport {\n canHaveDecorators,\n getDecorators,\n isCallExpression,\n isIdentifier,\n isJSDocParameterTag,\n isJSDocPropertyTag,\n isJSDocReturnTag,\n isJSDocThisTag,\n isJSDocTypeTag,\n isPropertyAccessExpression,\n isQualifiedName,\n} from 'typescript';\nimport type {\n Decorator,\n EntityName,\n Expression,\n JSDocTag,\n Node,\n TypeNode,\n} from 'typescript';\nimport { getJSDocTags } from '../../typescript/js-doc';\nimport { transformJSDocComment } from '../../typescript/js-doc/utils';\nimport type {\n DecoratorArgument,\n DecoratorSource,\n DecoratorTypeArgument,\n JsDocSource,\n} from '@trapi/core';\nimport type { DecoratorSourceBuilderOptions, JsDocSourceBuilderOptions } from './types';\nimport { buildDecoratorArgument } from './utils';\n\n// -----------------------------------------------------------------------------\n// Decorator sources\n// -----------------------------------------------------------------------------\n\nexport function buildDecoratorSources(\n node: Node,\n options: DecoratorSourceBuilderOptions,\n): DecoratorSource[] {\n if (!canHaveDecorators(node)) {\n return [];\n }\n\n const decorators = getDecorators(node);\n if (!decorators || decorators.length === 0) {\n return [];\n }\n\n const output: DecoratorSource[] = [];\n for (const decorator of decorators) {\n const source = buildDecoratorSource(decorator, options);\n if (source) {\n output.push(source);\n }\n }\n return output;\n}\n\nfunction buildDecoratorSource(\n decorator: Decorator,\n options: DecoratorSourceBuilderOptions,\n): DecoratorSource | undefined {\n const { expression } = decorator;\n\n let name: string | undefined;\n let argumentExpressions: readonly Expression[] = [];\n let typeArgumentNodes: readonly TypeNode[] = [];\n\n if (isCallExpression(expression)) {\n argumentExpressions = expression.arguments;\n typeArgumentNodes = expression.typeArguments ?? [];\n name = readDecoratorName(expression.expression);\n } else {\n name = readDecoratorName(expression);\n }\n\n if (!name) {\n return undefined;\n }\n\n const decoratorArguments: DecoratorArgument[] = argumentExpressions.map(\n (arg) => buildDecoratorArgument(arg, options.typeChecker),\n );\n\n const decoratorTypeArguments: DecoratorTypeArgument[] = typeArgumentNodes.map(\n (typeNode) => ({ resolve: () => options.resolveTypeNode(typeNode) }),\n );\n\n const sourceFile = decorator.getSourceFile();\n const location = sourceFile ? {\n file: sourceFile.fileName,\n line: sourceFile.getLineAndCharacterOfPosition(decorator.getStart()).line + 1,\n } : undefined;\n\n return {\n name,\n arguments: decoratorArguments,\n typeArguments: decoratorTypeArguments,\n target: options.target,\n host: options.host,\n location,\n };\n}\n\nfunction readDecoratorName(expression: Node): string | undefined {\n if (isIdentifier(expression)) {\n return expression.text;\n }\n if (isPropertyAccessExpression(expression)) {\n return expression.name.text;\n }\n return undefined;\n}\n\n// -----------------------------------------------------------------------------\n// JSDoc sources\n// -----------------------------------------------------------------------------\n\nexport function buildJsDocSources(\n node: Node,\n options: JsDocSourceBuilderOptions,\n): JsDocSource[] {\n const tags = getJSDocTags(node);\n if (tags.length === 0) {\n return [];\n }\n\n const output: JsDocSource[] = [];\n for (const tag of tags) {\n output.push(buildJsDocSource(tag, options));\n }\n return output;\n}\n\nfunction buildJsDocSource(\n tag: JSDocTag,\n options: JsDocSourceBuilderOptions,\n): JsDocSource {\n const tagName = tag.tagName.text;\n const text = transformJSDocComment(tag.comment);\n\n let parameterName: string | undefined;\n let typeNode: TypeNode | undefined;\n\n if (isJSDocParameterTag(tag) || isJSDocPropertyTag(tag)) {\n if (tag.name) {\n parameterName = readEntityName(tag.name);\n }\n typeNode = tag.typeExpression?.type;\n } else if (\n isJSDocReturnTag(tag) ||\n isJSDocTypeTag(tag) ||\n isJSDocThisTag(tag)\n ) {\n typeNode = tag.typeExpression?.type;\n }\n\n const source: JsDocSource = {\n tag: tagName,\n target: options.target,\n host: options.host,\n };\n\n if (typeof text !== 'undefined') {\n source.text = text;\n }\n\n if (typeof parameterName !== 'undefined') {\n source.parameterName = parameterName;\n }\n\n if (typeNode) {\n const capturedTypeNode = typeNode;\n source.typeExpression = { resolve: () => options.resolveTypeNode(capturedTypeNode) };\n }\n\n return source;\n}\n\nfunction readEntityName(name: EntityName): string | undefined {\n if (isIdentifier(name)) {\n return name.text;\n }\n if (isQualifiedName(name)) {\n const left = readEntityName(name.left);\n return left ? `${left}.${name.right.text}` : name.right.text;\n }\n return undefined;\n}\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport type { Node } from 'typescript';\nimport {\n matches,\n matchesJsDoc,\n} from '@trapi/core';\nimport type {\n DecoratorSource,\n HandlerContext,\n JsDocHandlerContext,\n JsDocMatch,\n JsDocSource,\n Match,\n} from '@trapi/core';\nimport { buildDecoratorSources, buildJsDocSources } from '../typescript';\nimport type { ApplyHandlersOptions } from './types';\n\ntype DecoratorHandlerLike<D> = {\n match: Match;\n apply: (ctx: HandlerContext, draft: D) => void;\n};\n\ntype JsDocHandlerLike<D> = {\n match: JsDocMatch;\n apply: (ctx: JsDocHandlerContext, draft: D) => void;\n};\n\nexport function buildHandlerContext(\n source: DecoratorSource,\n options: ApplyHandlersOptions,\n): HandlerContext {\n return {\n host: source.host,\n argument: (i) => source.arguments[i],\n arguments: () => source.arguments,\n typeArgument: (i) => source.typeArguments[i],\n typeArguments: () => source.typeArguments,\n parameterType: options.parameterType ?? (() => undefined),\n };\n}\n\nexport function buildJsDocHandlerContext(\n source: JsDocSource,\n options: ApplyHandlersOptions,\n): JsDocHandlerContext {\n return {\n host: source.host,\n source,\n parameterType: options.parameterType ?? (() => undefined),\n };\n}\n\nexport function applyDecoratorHandlers<D>(\n node: Node,\n handlers: DecoratorHandlerLike<D>[],\n draft: D,\n options: ApplyHandlersOptions,\n): void {\n if (handlers.length === 0 && !options.onUnmatchedDecorator) {\n return;\n }\n const sources = buildDecoratorSources(node, options);\n if (sources.length === 0) {\n return;\n }\n for (const source of sources) {\n let matched = 0;\n for (const handler of handlers) {\n if (matches(handler.match, source)) {\n handler.apply(buildHandlerContext(source, options), draft);\n matched += 1;\n }\n }\n if (matched === 0 && options.onUnmatchedDecorator) {\n // Prefer the decorator's own AST line if `buildDecoratorSources`\n // populated it; fall back to the host node's line otherwise.\n let file: string;\n let line: number;\n if (source.location) {\n file = source.location.file;\n line = source.location.line;\n } else {\n const sourceFile = node.getSourceFile();\n file = sourceFile.fileName;\n line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;\n }\n options.onUnmatchedDecorator(\n {\n name: source.name,\n target: source.target,\n host: source.host,\n file,\n line,\n },\n source,\n );\n }\n }\n}\n\nexport function applyJsDocHandlers<D>(\n node: Node,\n handlers: JsDocHandlerLike<D>[],\n draft: D,\n options: ApplyHandlersOptions,\n): void {\n if (handlers.length === 0) {\n return;\n }\n const sources = buildJsDocSources(node, options);\n if (sources.length === 0) {\n return;\n }\n for (const source of sources) {\n for (const handler of handlers) {\n if (matchesJsDoc(handler.match, source)) {\n handler.apply(buildJsDocHandlerContext(source, options), draft);\n }\n }\n }\n}\n","/*\n * Copyright (c) 2021-2023.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport type {\n ParameterDeclaration, \n PropertyDeclaration, \n PropertySignature, \n TypeAliasDeclaration,\n} from 'typescript';\nimport type { Validator } from '@trapi/core';\nimport { ValidatorName } from '@trapi/core';\nimport { ValidatorError } from '../../../core/error/validator';\nimport { getJSDocTags, transformJSDocComment } from '../js-doc';\nimport { ValidatorErrorCode } from '../../../core/error/validator-codes';\n\nexport function getDeclarationValidators(\n declaration: PropertyDeclaration | TypeAliasDeclaration | PropertySignature | ParameterDeclaration,\n name?: string,\n): Record<string, Validator> {\n if (!declaration.parent) {\n return {};\n }\n\n const getCommentValue = (comment?: string) => comment && comment.split(' ')[0];\n\n const parameterTags = getSupportedParameterTags();\n const tags = getJSDocTags(declaration.parent, (tag) => {\n const { comment } = tag;\n if (!comment) {\n return false;\n }\n\n const text = transformJSDocComment(comment);\n const commentValue = getCommentValue(text);\n\n return parameterTags.some((value) => {\n if (value !== tag.tagName.text) {\n return false;\n }\n\n return !(name && name !== commentValue);\n });\n });\n\n function getErrorMsg(comment?: string, isValue = true) : string | undefined {\n if (!comment) {\n return undefined;\n }\n if (isValue) {\n const indexOf = comment.indexOf(' ');\n if (indexOf > 0) {\n return comment.substring(indexOf + 1);\n }\n return undefined;\n }\n\n return comment;\n }\n\n const validators : Record<string, Validator> = {};\n\n for (const tag of tags) {\n if (!tag.comment) {\n continue;\n }\n\n const name = tag.tagName.text;\n\n const rawComment = transformJSDocComment(tag.comment);\n if (!rawComment) {\n continue;\n }\n const comment = rawComment.substring(rawComment.indexOf(' ') + 1).trim();\n\n const value = getCommentValue(comment);\n\n switch (name) {\n case ValidatorName.UNIQUE_ITEMS:\n validators[name] = {\n message: getErrorMsg(comment, false),\n value: undefined,\n };\n break;\n case ValidatorName.MINIMUM:\n case ValidatorName.MAXIMUM:\n case ValidatorName.MIN_ITEMS:\n case ValidatorName.MAX_ITEMS:\n case ValidatorName.MIN_LENGTH:\n case ValidatorName.MAX_LENGTH:\n {\n const parsed = Number(value);\n if (!Number.isFinite(parsed)) {\n throw new ValidatorError({\n message: `@${name} validator expects a numeric value, got '${value}'.`,\n code: ValidatorErrorCode.EXPECTED_NUMBER,\n });\n }\n validators[name] = {\n message: getErrorMsg(comment),\n value: parsed,\n };\n }\n break;\n case ValidatorName.MIN_DATE:\n case ValidatorName.MAX_DATE:\n if (typeof value !== 'string') {\n throw new ValidatorError({\n message: `@${name} validator expects a date string, got '${typeof value}'.`,\n code: ValidatorErrorCode.EXPECTED_DATE,\n });\n }\n\n validators[name] = {\n message: getErrorMsg(comment),\n value,\n };\n break;\n case ValidatorName.PATTERN:\n if (typeof value !== 'string') {\n throw new ValidatorError({\n message: `@${name} validator expects a string pattern, got '${value}'.`,\n code: ValidatorErrorCode.EXPECTED_STRING,\n });\n }\n\n validators[name] = {\n message: getErrorMsg(comment),\n value: removeSurroundingQuotes(value),\n };\n break;\n default:\n if (name.toLowerCase().startsWith('is')) {\n const errorMsg = getErrorMsg(comment, false);\n if (errorMsg) {\n validators[name] = {\n message: errorMsg,\n value: undefined,\n };\n }\n }\n break;\n }\n }\n\n return validators;\n}\n\nfunction getSupportedParameterTags() {\n return [\n 'isString',\n 'isBoolean',\n 'isInt',\n 'isLong',\n 'isFloat',\n 'isDouble',\n 'isDate',\n 'isDateTime',\n\n 'minItems',\n 'maxItems',\n 'uniqueItems',\n 'minLength',\n 'maxLength',\n 'pattern',\n 'minimum',\n 'maximum',\n 'minDate',\n 'maxDate',\n ];\n}\n\nfunction removeSurroundingQuotes(str: string) {\n if (str.startsWith('`') && str.endsWith('`')) {\n return str.substring(1, str.length - 1);\n }\n if (str.startsWith('```') && str.endsWith('```')) {\n return str.substring(3, str.length - 3);\n }\n return str;\n}\n","/*\n * Copyright (c) 2023-2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport type { Node } from 'typescript';\nimport {\n isExtensionMarker,\n namesForMarker,\n} from '@trapi/core';\nimport type { Extension, Registry } from '@trapi/core';\nimport { findDecoratorsByName } from '../../../decorator';\n\nexport function getNodeExtensions(node: Node, registry: Registry) : Extension[] {\n const names = namesForMarker(registry, isExtensionMarker);\n if (names.size === 0) {\n return [];\n }\n\n const output : Extension[] = [];\n for (const name of names) {\n const decorators = findDecoratorsByName(node, name);\n for (const decorator of decorators) {\n const keyArg = decorator.arguments[0];\n const valueArg = decorator.arguments[1];\n if (!keyArg || keyArg.kind !== 'literal' || typeof keyArg.raw !== 'string') {\n continue;\n }\n if (!valueArg || valueArg.kind === 'unresolvable' || typeof valueArg.raw === 'undefined') {\n continue;\n }\n output.push({ key: keyArg.raw, value: valueArg.raw as never });\n }\n }\n return output;\n}\n","/*\n * Copyright (c) 2025.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport { isArrayTypeNode } from 'typescript';\nimport type { TypeNode } from 'typescript';\nimport { TypeName } from '@trapi/core';\nimport type { ArrayType, Type } from '@trapi/core';\nimport type { SubResolverContext } from '../types';\n\nexport function resolveArrayType(\n typeNode: TypeNode,\n ctx: SubResolverContext,\n): Type | undefined {\n if (!isArrayTypeNode(typeNode)) {\n return undefined;\n }\n\n return {\n typeName: TypeName.ARRAY,\n elementType: ctx.resolveType(\n typeNode.elementType,\n ctx.parentNode,\n ctx.context,\n ),\n } as ArrayType;\n}\n","/*\n * Copyright (c) 2025.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport type { Node } from 'typescript';\nimport {\n SyntaxKind,\n canHaveModifiers,\n getModifiers,\n} from 'typescript';\n\n\nexport class ResolverBase {\n protected hasPublicModifier(node: Node) {\n if (!canHaveModifiers(node)) {\n return true;\n }\n\n const modifiers = getModifiers(node);\n if (!modifiers) {\n return true;\n }\n\n return modifiers.every(\n (modifier) => modifier.kind !== SyntaxKind.ProtectedKeyword && modifier.kind !== SyntaxKind.PrivateKeyword,\n );\n }\n\n protected hasStaticModifier(node: Node) {\n if (!canHaveModifiers(node)) {\n return false;\n }\n\n const modifiers = getModifiers(node);\n\n return modifiers && modifiers.some((modifier) => modifier.kind === SyntaxKind.StaticKeyword);\n }\n\n protected isAccessibleParameter(node: Node) {\n // No modifiers\n if (!canHaveModifiers(node)) {\n return false;\n }\n\n const modifiers = getModifiers(node);\n if (!modifiers) {\n return false;\n }\n\n // public || public readonly\n if (modifiers.some((modifier) => modifier.kind === SyntaxKind.PublicKeyword)) {\n return true;\n }\n\n // readonly, not private readonly, not public readonly\n const isReadonly = modifiers.some((modifier) => modifier.kind === SyntaxKind.ReadonlyKeyword);\n const isProtectedOrPrivate = modifiers.some(\n (modifier) => modifier.kind === SyntaxKind.ProtectedKeyword ||\n modifier.kind === SyntaxKind.PrivateKeyword,\n );\n\n return isReadonly && !isProtectedOrPrivate;\n }\n}\n","/*\n * Copyright (c) 2025.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\nimport type {\n Node,\n TypeChecker,\n TypeNode,\n} from 'typescript';\nimport { SyntaxKind, displayPartsToString } from 'typescript';\nimport { hasOwnProperty } from '../../../core/utils';\nimport { ResolverError } from '../../../core/error/resolver';\n\nexport function getNodeDescription(\n node: Node,\n typeChecker: TypeChecker,\n) {\n if (!hasOwnProperty(node, 'name')) {\n return undefined;\n }\n\n const symbol = typeChecker.getSymbolAtLocation(node.name as Node);\n if (!symbol) {\n return undefined;\n }\n\n /**\n * TODO: Workaround for what seems like a bug in the compiler\n * Warrants more investigation and possibly a PR against typescript\n */\n if (node.kind === SyntaxKind.Parameter) {\n // TypeScript won't parse jsdoc if the flag is 4, i.e. 'Property'\n symbol.flags = 0;\n }\n\n const comments = symbol.getDocumentationComment(typeChecker);\n if (comments.length) {\n return displayPartsToString(comments);\n }\n\n return undefined;\n}\n\nexport function toTypeNodeOrFail(\n typeChecker: TypeChecker,\n ...args: Param