UNPKG

@trapi/metadata

Version:

Generate REST-API metadata scheme from TypeScript Decorators.

1,217 lines 103 kB
import { BaseError, isBaseError } from "@ebec/core"; import { NodeBuilderFlags, NodeFlags, SymbolFlags, SyntaxKind, canHaveDecorators, canHaveModifiers, convertCompilerOptionsFromJson, createProgram, displayPartsToString, factory, forEachChild, getDecorators, getModifiers, isArrayLiteralExpression, isArrayTypeNode, isCallExpression, isClassDeclaration, isConditionalTypeNode, isConstructorDeclaration, isEnumDeclaration, isEnumMember, isExpressionWithTypeArguments, isFunctionTypeNode, isIdentifier, isImportSpecifier, isIndexSignatureDeclaration, isIndexedAccessTypeNode, isInterfaceDeclaration, isIntersectionTypeNode, isJSDocParameterTag, isJSDocPropertyTag, isJSDocReturnTag, isJSDocThisTag, isJSDocTypeTag, isLiteralTypeNode, isMappedTypeNode, isMethodDeclaration, isMethodSignature, isModuleBlock, isModuleDeclaration, isNamedTupleMember, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isParameter, isParenthesizedTypeNode, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyDeclaration, isPropertySignature, isQualifiedName, isStringLiteral, isTupleTypeNode, isTypeAliasDeclaration, isTypeLiteralNode, isTypeOperatorNode, isTypeReferenceNode, isUnionTypeNode } from "typescript"; import path, { normalize } from "node:path"; import { CollectionFormat, NumericKind, ParamKind, ParameterSource, TypeName, UtilityTypeName, ValidatorName, createRegistry, isArrayType, isDeprecatedMarker, isExtensionMarker, isNestedObjectLiteralType, isRefAliasType, isRefEnumType, isRefObjectType, isStringType, isUnionType, isVoidType, loadRegistry, loadRegistryByName, matches, matchesJsDoc, mergeRegistries, namesForMarker, newControllerDraft, newMethodDraft, newParameterDraft, numericMarkerKind, resolvePresetByName, tagsForMarker } from "@trapi/core"; import { buildFilePath, isObject, load, locateMany } from "locter"; import process from "node:process"; import { parse, stringify } from "flatted"; import crypto from "node:crypto"; import fs from "node:fs"; import { tmpdir } from "node:os"; import { minimatch } from "minimatch"; //#region src/core/error/base.ts var MetadataError = class extends BaseError {}; //#endregion //#region src/core/error/config.ts var ConfigError = class extends MetadataError {}; //#endregion //#region src/core/error/config-codes.ts const ConfigErrorCode = { TSCONFIG_MALFORMED: "CONFIG_TSCONFIG_MALFORMED", PRESET_NOT_FOUND: "CONFIG_PRESET_NOT_FOUND", PRESET_MISSING: "CONFIG_PRESET_MISSING" }; //#endregion //#region src/core/error/generator.ts var GeneratorError = class extends MetadataError {}; function isGeneratorError(input) { if (!isBaseError(input)) return false; return typeof input.code === "string"; } //#endregion //#region src/core/error/generator-codes.ts const GeneratorErrorCode = { CONTROLLER_NO_SOURCE_FILE: "GENERATOR_CONTROLLER_NO_SOURCE_FILE", CONTROLLER_NO_NAME: "GENERATOR_CONTROLLER_NO_NAME", PARAMETER_GENERATION_FAILED: "GENERATOR_PARAMETER_GENERATION_FAILED", BODY_PARAMETER_DUPLICATE: "GENERATOR_BODY_PARAMETER_DUPLICATE", BODY_FORM_CONFLICT: "GENERATOR_BODY_FORM_CONFLICT", STRICT_UNMATCHED_DECORATORS: "GENERATOR_STRICT_UNMATCHED_DECORATORS" }; //#endregion //#region src/core/error/parameter-codes.ts const ParameterErrorCode = { TYPE_UNSUPPORTED: "PARAMETER_TYPE_UNSUPPORTED", METHOD_UNSUPPORTED: "PARAMETER_METHOD_UNSUPPORTED", PATH_MISMATCH: "PARAMETER_PATH_MISMATCH", SCOPE_REQUIRED: "PARAMETER_SCOPE_REQUIRED", INVALID_EXAMPLE: "PARAMETER_INVALID_EXAMPLE" }; //#endregion //#region src/core/error/parameter.ts var ParameterError = class ParameterError extends MetadataError { static typeUnsupported(context) { const location = context.node ? ParameterError.getCurrentLocation(context.node) : void 0; return new ParameterError({ message: `@${context.decoratorName}('${context.propertyName}') does not support '${context.type.typeName}' type${location ? ` at ${location}` : ""}.`, code: ParameterErrorCode.TYPE_UNSUPPORTED }); } static methodUnsupported(context) { const location = context.node ? ParameterError.getCurrentLocation(context.node) : void 0; return new ParameterError({ message: `@${context.decoratorName}('${context.propertyName}') does not support method '${context.method}'${location ? ` at ${location}` : ""}.`, code: ParameterErrorCode.METHOD_UNSUPPORTED }); } static invalidPathMatch(context) { const location = context.node ? ParameterError.getCurrentLocation(context.node) : void 0; return new ParameterError({ message: `@${context.decoratorName}('${context.propertyName}') does not exist in path '${context.path}'${location ? ` at ${location}` : ""}.`, code: ParameterErrorCode.PATH_MISMATCH }); } static scopeRequired(context) { const location = context.node ? ParameterError.getCurrentLocation(context.node) : void 0; return new ParameterError({ message: `@${context.decoratorName}() requires a scope argument${location ? ` at ${location}` : ""}.`, code: ParameterErrorCode.SCOPE_REQUIRED }); } static invalidExampleSchema() { return new ParameterError({ message: "The @example JSDoc tag contains invalid JSON.", code: ParameterErrorCode.INVALID_EXAMPLE }); } static getCurrentLocation(node) { const parts = []; if (isMethodDeclaration(node.parent)) { parts.push(node.parent.name.getText()); if (isClassDeclaration(node.parent.parent) && node.parent.parent.name) parts.unshift(node.parent.parent.name.text); } return parts.join("."); } }; //#endregion //#region src/core/error/resolver.ts var ResolverError = class extends MetadataError { file; line; constructor(message, node, options) { const opts = typeof options === "boolean" ? { onlyCurrent: options } : options; const onlyCurrent = opts?.onlyCurrent ?? false; const parts = [message]; let file; let line; if (node) { const location = prettyLocationOfNode(node); if (location) { parts.push(location.text); file = location.file; line = location.line; } parts.push(prettyTroubleCause(node, onlyCurrent)); } super({ message: parts.join("\n"), cause: opts?.cause }); this.file = file; this.line = line; } }; function isResolverError(input) { if (!isBaseError(input)) return false; return "file" in input && "line" in input; } function prettyLocationOfNode(node) { try { const sourceFile = node.getSourceFile(); if (!sourceFile) return void 0; const token = node.getFirstToken() || node.parent?.getFirstToken(); const start = token ? sourceFile.getLineAndCharacterOfPosition(token.getStart()).line + 1 : void 0; const end = token ? sourceFile.getLineAndCharacterOfPosition(token.getEnd()).line + 1 : void 0; return { text: `At: ${normalize(sourceFile.fileName)}${start ? `:${start}` : ""}${end ? `:${end}` : ""}.`, file: sourceFile.fileName, line: start }; } catch { return; } } function prettyTroubleCause(node, onlyCurrent = false) { try { let name; if (onlyCurrent || !node.parent) name = node.pos !== -1 ? node.getText() : node.name.text; else name = node.parent.pos !== -1 ? node.parent.getText() : node.parent.name.text; return `This was caused by '${name}'`; } catch { return "This was caused by an unknown node"; } } //#endregion //#region src/core/error/validator.ts var ValidatorError = class extends MetadataError {}; //#endregion //#region src/core/error/validator-codes.ts const ValidatorErrorCode = { EXPECTED_NUMBER: "VALIDATOR_EXPECTED_NUMBER", EXPECTED_DATE: "VALIDATOR_EXPECTED_DATE", EXPECTED_STRING: "VALIDATOR_EXPECTED_STRING" }; //#endregion //#region src/adapters/typescript/js-doc/constants.ts let JSDocTagName = /* @__PURE__ */ function(JSDocTagName) { JSDocTagName["ABSTRACT"] = "abstract"; JSDocTagName["ACCESS"] = "access"; JSDocTagName["ALIAS"] = "alias"; JSDocTagName["ASYNC"] = "async"; /** * Alias for @extends */ JSDocTagName["AUGMENTS"] = "augments"; JSDocTagName["AUTHOR"] = "author"; JSDocTagName["DEFAULT"] = "default"; JSDocTagName["DEPRECATED"] = "deprecated"; JSDocTagName["DESCRIPTION"] = "description"; JSDocTagName["EXAMPLE"] = "example"; JSDocTagName["FORMAT"] = "format"; JSDocTagName["IGNORE"] = "ignore"; JSDocTagName["SUMMARY"] = "summary"; return JSDocTagName; }({}); //#endregion //#region src/core/utils/array.ts function isStringArray(input) { if (!Array.isArray(input)) return false; for (const element of input) if (typeof element !== "string") return false; return true; } //#endregion //#region src/core/utils/object.ts function hasOwnProperty(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } //#endregion //#region src/core/utils/path-normalize.ts function normalizePath(str) { str = str.replace(/^[/\\\s]+|[/\\\s]+$/g, ""); str = str.replace(/([^:]\/)\/+/g, "$1"); return str; } //#endregion //#region src/adapters/typescript/js-doc/utils.ts function transformJSDocComment(input) { if (typeof input === "string") return input; if (!input || input.length === 0) return; const comment = input[0]; if (typeof comment === "string") return comment; if (isObject(comment) && typeof comment.text === "string") return comment.text; } //#endregion //#region src/adapters/typescript/js-doc/module.ts function getJSDocDescription(node, index) { const jsDoc = getJSDoc(node, index); if (!jsDoc) return; return transformJSDocComment(jsDoc.comment); } function getJSDoc(node, index) { if (!hasOwnProperty(node, "jsDoc")) return; const jsDoc = node.jsDoc; if (!jsDoc || !Array.isArray(jsDoc) || !jsDoc.length) return; index = index ?? 0; return jsDoc.length > index && index >= 0 ? jsDoc[index] : void 0; } function getJSDocTags(node, isMatching) { const jsDoc = getJSDoc(node); if (typeof jsDoc === "undefined") return []; const jsDocTags = jsDoc.tags; if (typeof jsDocTags === "undefined") return []; if (typeof isMatching === "undefined") return jsDocTags; if (typeof isMatching === "function") return jsDocTags.filter(isMatching); const tagNames = Array.isArray(isMatching) ? isMatching : [isMatching]; return jsDocTags.filter((tag) => tagNames.includes(tag.tagName.text)); } function hasJSDocTag(node, tagName) { const tags = getJSDocTags(node, tagName); return !(!tags || !tags.length); } function getJSDocTagComment(node, tagName) { const first = getJSDocTags(node, tagName)[0]; if (!first || typeof first.comment !== "string") return; return first.comment; } function getJSDocTagNames(node, requireTagName = false) { let tags; /* istanbul ignore next */ if (node.kind === SyntaxKind.Parameter) { const parameterName = node.name.text; tags = getJSDocTags(node.parent, (tag) => { if (isJSDocParameterTag(tag)) return false; if (tag.comment === void 0) throw new MetadataError(`Orphan tag: @${String(tag.tagName.text || tag.tagName.escapedText)} must be followed by a parameter name.`); return typeof tag.comment === "string" ? tag.comment.startsWith(parameterName) : false; }); } else tags = getJSDocTags(node, (tag) => requireTagName ? tag.comment !== void 0 : true); return tags.map((tag) => tag.tagName.text); } //#endregion //#region src/adapters/typescript/initializer.ts function getInitializerValue(initializer, typeChecker, type) { if (!initializer) return; switch (initializer.kind) { case SyntaxKind.ArrayLiteralExpression: return initializer.elements.map((element) => getInitializerValue(element, typeChecker)); case SyntaxKind.StringLiteral: case SyntaxKind.NoSubstitutionTemplateLiteral: return initializer.text; case SyntaxKind.TrueKeyword: return true; case SyntaxKind.FalseKeyword: return false; case SyntaxKind.PrefixUnaryExpression: { const prefixUnary = initializer; switch (prefixUnary.operator) { case SyntaxKind.PlusToken: return Number(prefixUnary.operand.text); case SyntaxKind.MinusToken: return Number(`-${prefixUnary.operand.text}`); default: throw new MetadataError(`Unsupported prefix operator token: ${prefixUnary.operator}`); } } case SyntaxKind.NumberKeyword: case SyntaxKind.FirstLiteralToken: return Number(initializer.text); case SyntaxKind.NewExpression: { const newExpression = initializer; if (newExpression.expression.text === "Date") { let date = /* @__PURE__ */ new Date(); if (newExpression.arguments) { const argsValue = newExpression.arguments.filter((args) => args.kind !== void 0).map((args) => getInitializerValue(args, typeChecker)); if (argsValue.length > 0) date = new Date(argsValue); } const dateString = date.toISOString(); if (type && type.typeName === "date") return dateString.split("T")[0]; return dateString; } return; } case SyntaxKind.NullKeyword: return null; case SyntaxKind.ObjectLiteralExpression: { const objectLiteral = initializer; const nestedObject = {}; objectLiteral.properties.forEach((p) => { nestedObject[p.name.text] = getInitializerValue(p.initializer, typeChecker); }); return nestedObject; } case SyntaxKind.ImportSpecifier: { if (typeof typeChecker === "undefined") return; const importSpecifier = initializer; const importSymbol = typeChecker.getSymbolAtLocation(importSpecifier.name); if (!importSymbol) return; const declarations = typeChecker.getAliasedSymbol(importSymbol).getDeclarations(); return getInitializerValue(extractInitializer(declarations && declarations.length > 0 ? declarations[0] : void 0), typeChecker); } default: { if (typeof initializer === "undefined") return; if (typeof initializer.parent === "undefined" || typeof typeChecker === "undefined") { if (hasOwnProperty(initializer, "text")) return initializer.text; return; } const symbol = typeChecker.getSymbolAtLocation(initializer); if (!symbol) return; return getInitializerValue(extractInitializer(symbol.valueDeclaration) || extractInitializer(extractImportSpecifier(symbol)), typeChecker); } } } const hasInitializer = (node) => Object.prototype.hasOwnProperty.call(node, "initializer"); const extractInitializer = (valueDeclaration) => valueDeclaration && hasInitializer(valueDeclaration) && valueDeclaration.initializer || void 0; const extractImportSpecifier = (symbol) => { const declaration = symbol?.declarations?.[0]; return declaration && isImportSpecifier(declaration) ? declaration : void 0; }; //#endregion //#region src/adapters/decorator/typescript/utils.ts /** * Enumerate decorators on a TS node and classify their argument values without * going through the registry. Used by read-side consumers (type resolver, * extension extraction) that only need decorator names + argument values. */ function readNodeDecorators(node, typeChecker) { if (!canHaveDecorators(node)) return []; const decorators = getDecorators(node); if (!decorators || decorators.length === 0) return []; const output = []; for (const decorator of decorators) { const { expression } = decorator; let name; let argumentExpressions = []; if (isCallExpression(expression)) { argumentExpressions = expression.arguments; name = readDecoratorName$1(expression.expression); } else name = readDecoratorName$1(expression); if (!name) continue; output.push({ name, arguments: argumentExpressions.map((a) => buildDecoratorArgument(a, typeChecker)) }); } return output; } function findDecoratorByName(node, name, typeChecker) { return readNodeDecorators(node, typeChecker).find((d) => d.name === name); } function findDecoratorsByName(node, name, typeChecker) { return readNodeDecorators(node, typeChecker).filter((d) => d.name === name); } function hasDecoratorNamed(node, name, typeChecker) { return readNodeDecorators(node, typeChecker).some((d) => d.name === name); } function readDecoratorName$1(expression) { if (isIdentifier(expression)) return expression.text; if (isPropertyAccessExpression(expression)) return expression.name.text; } function buildDecoratorArgument(expr, typeChecker) { if (isStringLiteral(expr) || isNumericLiteral(expr) || isNoSubstitutionTemplateLiteral(expr)) return { raw: getInitializerValue(expr, typeChecker), kind: "literal" }; if (expr.kind === SyntaxKind.TrueKeyword) return { raw: true, kind: "literal" }; if (expr.kind === SyntaxKind.FalseKeyword) return { raw: false, kind: "literal" }; if (expr.kind === SyntaxKind.NullKeyword) return { raw: null, kind: "literal" }; if (isPrefixUnaryExpression(expr) && (expr.operator === SyntaxKind.PlusToken || expr.operator === SyntaxKind.MinusToken) && isNumericLiteral(expr.operand)) return { raw: getInitializerValue(expr, typeChecker), kind: "literal" }; if (isObjectLiteralExpression(expr)) return { raw: getInitializerValue(expr, typeChecker), kind: "object" }; if (isArrayLiteralExpression(expr)) return { raw: getInitializerValue(expr, typeChecker), kind: "array" }; if (isIdentifier(expr) || isPropertyAccessExpression(expr)) { const value = getInitializerValue(expr, typeChecker); if (typeof value !== "undefined") return { raw: value, kind: "identifier" }; return { raw: void 0, kind: "unresolvable" }; } return { raw: void 0, kind: "unresolvable" }; } //#endregion //#region src/adapters/decorator/typescript/module.ts function buildDecoratorSources(node, options) { if (!canHaveDecorators(node)) return []; const decorators = getDecorators(node); if (!decorators || decorators.length === 0) return []; const output = []; for (const decorator of decorators) { const source = buildDecoratorSource(decorator, options); if (source) output.push(source); } return output; } function buildDecoratorSource(decorator, options) { const { expression } = decorator; let name; let argumentExpressions = []; let typeArgumentNodes = []; if (isCallExpression(expression)) { argumentExpressions = expression.arguments; typeArgumentNodes = expression.typeArguments ?? []; name = readDecoratorName(expression.expression); } else name = readDecoratorName(expression); if (!name) return; const decoratorArguments = argumentExpressions.map((arg) => buildDecoratorArgument(arg, options.typeChecker)); const decoratorTypeArguments = typeArgumentNodes.map((typeNode) => ({ resolve: () => options.resolveTypeNode(typeNode) })); const sourceFile = decorator.getSourceFile(); const location = sourceFile ? { file: sourceFile.fileName, line: sourceFile.getLineAndCharacterOfPosition(decorator.getStart()).line + 1 } : void 0; return { name, arguments: decoratorArguments, typeArguments: decoratorTypeArguments, target: options.target, host: options.host, location }; } function readDecoratorName(expression) { if (isIdentifier(expression)) return expression.text; if (isPropertyAccessExpression(expression)) return expression.name.text; } function buildJsDocSources(node, options) { const tags = getJSDocTags(node); if (tags.length === 0) return []; const output = []; for (const tag of tags) output.push(buildJsDocSource(tag, options)); return output; } function buildJsDocSource(tag, options) { const tagName = tag.tagName.text; const text = transformJSDocComment(tag.comment); let parameterName; let typeNode; if (isJSDocParameterTag(tag) || isJSDocPropertyTag(tag)) { if (tag.name) parameterName = readEntityName(tag.name); typeNode = tag.typeExpression?.type; } else if (isJSDocReturnTag(tag) || isJSDocTypeTag(tag) || isJSDocThisTag(tag)) typeNode = tag.typeExpression?.type; const source = { tag: tagName, target: options.target, host: options.host }; if (typeof text !== "undefined") source.text = text; if (typeof parameterName !== "undefined") source.parameterName = parameterName; if (typeNode) { const capturedTypeNode = typeNode; source.typeExpression = { resolve: () => options.resolveTypeNode(capturedTypeNode) }; } return source; } function readEntityName(name) { if (isIdentifier(name)) return name.text; if (isQualifiedName(name)) { const left = readEntityName(name.left); return left ? `${left}.${name.right.text}` : name.right.text; } } //#endregion //#region src/adapters/decorator/orchestrator/module.ts function buildHandlerContext(source, options) { return { host: source.host, argument: (i) => source.arguments[i], arguments: () => source.arguments, typeArgument: (i) => source.typeArguments[i], typeArguments: () => source.typeArguments, parameterType: options.parameterType ?? (() => void 0) }; } function buildJsDocHandlerContext(source, options) { return { host: source.host, source, parameterType: options.parameterType ?? (() => void 0) }; } function applyDecoratorHandlers(node, handlers, draft, options) { if (handlers.length === 0 && !options.onUnmatchedDecorator) return; const sources = buildDecoratorSources(node, options); if (sources.length === 0) return; for (const source of sources) { let matched = 0; for (const handler of handlers) if (matches(handler.match, source)) { handler.apply(buildHandlerContext(source, options), draft); matched += 1; } if (matched === 0 && options.onUnmatchedDecorator) { let file; let line; if (source.location) { file = source.location.file; line = source.location.line; } else { const sourceFile = node.getSourceFile(); file = sourceFile.fileName; line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1; } options.onUnmatchedDecorator({ name: source.name, target: source.target, host: source.host, file, line }, source); } } } function applyJsDocHandlers(node, handlers, draft, options) { if (handlers.length === 0) return; const sources = buildJsDocSources(node, options); if (sources.length === 0) return; for (const source of sources) for (const handler of handlers) if (matchesJsDoc(handler.match, source)) handler.apply(buildJsDocHandlerContext(source, options), draft); } //#endregion //#region src/adapters/typescript/validator/module.ts function getDeclarationValidators(declaration, name) { if (!declaration.parent) return {}; const getCommentValue = (comment) => comment && comment.split(" ")[0]; const parameterTags = getSupportedParameterTags(); const tags = getJSDocTags(declaration.parent, (tag) => { const { comment } = tag; if (!comment) return false; const commentValue = getCommentValue(transformJSDocComment(comment)); return parameterTags.some((value) => { if (value !== tag.tagName.text) return false; return !(name && name !== commentValue); }); }); function getErrorMsg(comment, isValue = true) { if (!comment) return; if (isValue) { const indexOf = comment.indexOf(" "); if (indexOf > 0) return comment.substring(indexOf + 1); return; } return comment; } const validators = {}; for (const tag of tags) { if (!tag.comment) continue; const name = tag.tagName.text; const rawComment = transformJSDocComment(tag.comment); if (!rawComment) continue; const comment = rawComment.substring(rawComment.indexOf(" ") + 1).trim(); const value = getCommentValue(comment); switch (name) { case ValidatorName.UNIQUE_ITEMS: validators[name] = { message: getErrorMsg(comment, false), value: void 0 }; break; case ValidatorName.MINIMUM: case ValidatorName.MAXIMUM: case ValidatorName.MIN_ITEMS: case ValidatorName.MAX_ITEMS: case ValidatorName.MIN_LENGTH: case ValidatorName.MAX_LENGTH: { const parsed = Number(value); if (!Number.isFinite(parsed)) throw new ValidatorError({ message: `@${name} validator expects a numeric value, got '${value}'.`, code: ValidatorErrorCode.EXPECTED_NUMBER }); validators[name] = { message: getErrorMsg(comment), value: parsed }; } break; case ValidatorName.MIN_DATE: case ValidatorName.MAX_DATE: if (typeof value !== "string") throw new ValidatorError({ message: `@${name} validator expects a date string, got '${typeof value}'.`, code: ValidatorErrorCode.EXPECTED_DATE }); validators[name] = { message: getErrorMsg(comment), value }; break; case ValidatorName.PATTERN: if (typeof value !== "string") throw new ValidatorError({ message: `@${name} validator expects a string pattern, got '${value}'.`, code: ValidatorErrorCode.EXPECTED_STRING }); validators[name] = { message: getErrorMsg(comment), value: removeSurroundingQuotes(value) }; break; default: if (name.toLowerCase().startsWith("is")) { const errorMsg = getErrorMsg(comment, false); if (errorMsg) validators[name] = { message: errorMsg, value: void 0 }; } break; } } return validators; } function getSupportedParameterTags() { return [ "isString", "isBoolean", "isInt", "isLong", "isFloat", "isDouble", "isDate", "isDateTime", "minItems", "maxItems", "uniqueItems", "minLength", "maxLength", "pattern", "minimum", "maximum", "minDate", "maxDate" ]; } function removeSurroundingQuotes(str) { if (str.startsWith("`") && str.endsWith("`")) return str.substring(1, str.length - 1); if (str.startsWith("```") && str.endsWith("```")) return str.substring(3, str.length - 3); return str; } //#endregion //#region src/adapters/typescript/resolver/extension/module.ts function getNodeExtensions(node, registry) { const names = namesForMarker(registry, isExtensionMarker); if (names.size === 0) return []; const output = []; for (const name of names) { const decorators = findDecoratorsByName(node, name); for (const decorator of decorators) { const keyArg = decorator.arguments[0]; const valueArg = decorator.arguments[1]; if (!keyArg || keyArg.kind !== "literal" || typeof keyArg.raw !== "string") continue; if (!valueArg || valueArg.kind === "unresolvable" || typeof valueArg.raw === "undefined") continue; output.push({ key: keyArg.raw, value: valueArg.raw }); } } return output; } //#endregion //#region src/adapters/typescript/resolver/sub/array.ts function resolveArrayType(typeNode, ctx) { if (!isArrayTypeNode(typeNode)) return; return { typeName: TypeName.ARRAY, elementType: ctx.resolveType(typeNode.elementType, ctx.parentNode, ctx.context) }; } //#endregion //#region src/adapters/typescript/resolver/sub/base.ts var ResolverBase = class { hasPublicModifier(node) { if (!canHaveModifiers(node)) return true; const modifiers = getModifiers(node); if (!modifiers) return true; return modifiers.every((modifier) => modifier.kind !== SyntaxKind.ProtectedKeyword && modifier.kind !== SyntaxKind.PrivateKeyword); } hasStaticModifier(node) { if (!canHaveModifiers(node)) return false; const modifiers = getModifiers(node); return modifiers && modifiers.some((modifier) => modifier.kind === SyntaxKind.StaticKeyword); } isAccessibleParameter(node) { if (!canHaveModifiers(node)) return false; const modifiers = getModifiers(node); if (!modifiers) return false; if (modifiers.some((modifier) => modifier.kind === SyntaxKind.PublicKeyword)) return true; const isReadonly = modifiers.some((modifier) => modifier.kind === SyntaxKind.ReadonlyKeyword); const isProtectedOrPrivate = modifiers.some((modifier) => modifier.kind === SyntaxKind.ProtectedKeyword || modifier.kind === SyntaxKind.PrivateKeyword); return isReadonly && !isProtectedOrPrivate; } }; //#endregion //#region src/adapters/typescript/resolver/utils.ts function getNodeDescription(node, typeChecker) { if (!hasOwnProperty(node, "name")) return; const symbol = typeChecker.getSymbolAtLocation(node.name); if (!symbol) return; /** * TODO: Workaround for what seems like a bug in the compiler * Warrants more investigation and possibly a PR against typescript */ if (node.kind === SyntaxKind.Parameter) symbol.flags = 0; const comments = symbol.getDocumentationComment(typeChecker); if (comments.length) return displayPartsToString(comments); } function toTypeNodeOrFail(typeChecker, ...args) { const output = typeChecker.typeToTypeNode(...args); if (typeof output === "undefined") throw new ResolverError("Type could not be transformed to TypeNode."); return output; } //#endregion //#region src/adapters/typescript/resolver/sub/indexed-access.ts function resolveIndexedAccessType(typeNode, ctx) { if (!isIndexedAccessTypeNode(typeNode)) return; if (typeNode.indexType.kind === SyntaxKind.NumberKeyword || typeNode.indexType.kind === SyntaxKind.StringKeyword) { const numberIndexType = typeNode.indexType.kind === SyntaxKind.NumberKeyword; const objectType = ctx.typeChecker.getTypeFromTypeNode(typeNode.objectType); const type = numberIndexType ? objectType.getNumberIndexType() : objectType.getStringIndexType(); if (type === void 0) throw new ResolverError(`Could not determine ${numberIndexType ? "number" : "string"} index on ${ctx.typeChecker.typeToString(objectType)}`, typeNode); return ctx.resolveType(toTypeNodeOrFail(ctx.typeChecker, type, void 0, void 0), typeNode, ctx.context, ctx.referencer); } if (isLiteralTypeNode(typeNode.indexType) && (isStringLiteral(typeNode.indexType.literal) || isNumericLiteral(typeNode.indexType.literal))) { const hasType = (node) => node !== void 0 && Object.prototype.hasOwnProperty.call(node, "type"); const symbol = ctx.typeChecker.getPropertyOfType(ctx.typeChecker.getTypeFromTypeNode(typeNode.objectType), typeNode.indexType.literal.text); if (symbol === void 0) throw new ResolverError(`Could not determine the keys on ${ctx.typeChecker.typeToString(ctx.typeChecker.getTypeFromTypeNode(typeNode.objectType))}`, typeNode); if (hasType(symbol.valueDeclaration) && symbol.valueDeclaration.type) return ctx.resolveType(symbol.valueDeclaration.type, typeNode, ctx.context, ctx.referencer); const declaration = ctx.typeChecker.getTypeOfSymbolAtLocation(symbol, typeNode.objectType); try { return ctx.resolveType(toTypeNodeOrFail(ctx.typeChecker, declaration, void 0, void 0), typeNode, ctx.context, ctx.referencer); } catch (err) { throw new ResolverError(`Could not determine the keys on ${ctx.typeChecker.typeToString(declaration)}`, typeNode, { cause: err }); } } } //#endregion //#region src/adapters/typescript/resolver/sub/intersection.ts function resolveIntersectionType(typeNode, ctx) { if (!isIntersectionTypeNode(typeNode)) return; const members = typeNode.types.map((type) => ctx.resolveType(type, ctx.parentNode, ctx.context)); return { typeName: TypeName.INTERSECTION, members }; } //#endregion //#region src/adapters/typescript/resolver/sub/literal.ts function resolveLiteralType(typeNode, ctx) { if (typeNode.kind === SyntaxKind.NullKeyword) return { typeName: TypeName.ENUM, members: [null] }; if (typeNode.kind === SyntaxKind.AnyKeyword || typeNode.kind === SyntaxKind.UnknownKeyword) return { typeName: TypeName.ANY }; if (isLiteralTypeNode(typeNode)) return { typeName: TypeName.ENUM, members: [getLiteralValue(typeNode)] }; if (typeNode.kind === SyntaxKind.TemplateLiteralType) { const type = ctx.typeChecker.getTypeFromTypeNode(ctx.referencer || typeNode); if (type.isUnion() && type.types.every((t) => t.isStringLiteral())) return { typeName: TypeName.ENUM, members: type.types.map((t) => t.value) }; throw new ResolverError(`Could not resolve type: ${ctx.typeChecker.typeToString(ctx.typeChecker.getTypeFromTypeNode(typeNode), typeNode)}`, typeNode); } if (isParenthesizedTypeNode(typeNode)) return ctx.resolveType(typeNode.type, typeNode, ctx.context, ctx.referencer); if (typeNode.kind === SyntaxKind.ObjectKeyword || isFunctionTypeNode(typeNode)) return { typeName: TypeName.OBJECT }; } function getLiteralValue(typeNode) { let value; switch (typeNode.literal.kind) { case SyntaxKind.TrueKeyword: value = true; break; case SyntaxKind.FalseKeyword: value = false; break; case SyntaxKind.StringLiteral: value = typeNode.literal.text; break; case SyntaxKind.NumericLiteral: value = Number.parseFloat(typeNode.literal.text); break; case SyntaxKind.NullKeyword: value = null; break; default: if (Object.prototype.hasOwnProperty.call(typeNode.literal, "text")) value = typeNode.literal.text; else throw new ResolverError(`Couldn't resolve literal node: ${typeNode.literal.getText()}`, typeNode.literal); } return value; } //#endregion //#region src/adapters/typescript/resolver/sub/mapped.ts function resolveMappedType(typeNode, ctx) { if (!isMappedTypeNode(typeNode) || !ctx.referencer) return; const type = ctx.typeChecker.getTypeFromTypeNode(ctx.referencer); const mappedTypeNode = typeNode; const { typeChecker } = ctx; const getDeclaration = (prop) => prop.declarations && prop.declarations[0]; const isIgnored = (prop) => { const declaration = getDeclaration(prop); if (prop.getJsDocTags().findIndex((tag) => tag.name === "ignore") >= 0) return true; return !!declaration && !isPropertyDeclaration(declaration) && !isPropertySignature(declaration) && !isParameter(declaration); }; const properties = type.getProperties().filter((property) => !isIgnored(property)).map((property) => { const declaration = getDeclaration(property); const overrideToken = mappedTypeNode.questionToken?.kind === SyntaxKind.PlusToken ? factory.createToken(SyntaxKind.QuestionToken) : mappedTypeNode.questionToken; if (declaration && isPropertySignature(declaration)) return { ...ctx.propertyFromSignature(declaration, overrideToken), name: property.getName() }; if (declaration && (isPropertyDeclaration(declaration) || isParameter(declaration))) return { ...ctx.propertyFromDeclaration(declaration, overrideToken), name: property.getName() }; let required = (property.flags & SymbolFlags.Optional) === 0; const typeNode2 = toTypeNodeOrFail(typeChecker, typeChecker.getTypeOfSymbolAtLocation(property, typeNode), void 0, NodeBuilderFlags.NoTruncation); if (mappedTypeNode.questionToken && mappedTypeNode.questionToken.kind === SyntaxKind.MinusToken) required = true; else if (mappedTypeNode.questionToken && (mappedTypeNode.questionToken.kind === SyntaxKind.QuestionToken || mappedTypeNode.questionToken.kind === SyntaxKind.PlusToken)) required = false; return { name: property.getName(), required, deprecated: false, type: ctx.resolveType(typeNode2, typeNode, ctx.context, ctx.referencer), validators: {} }; }); return { typeName: TypeName.NESTED_OBJECT_LITERAL, properties }; } //#endregion //#region src/adapters/typescript/resolver/sub/object-literal.ts function resolveObjectLiteralType(typeNode, ctx) { if (!isTypeLiteralNode(typeNode)) return; const properties = typeNode.members.filter((member) => isPropertySignature(member)).reduce((res, propertySignature) => { if (!propertySignature.type) throw new ResolverError("No valid type found for property declaration.", propertySignature); const type = ctx.resolveType(propertySignature.type, propertySignature, ctx.context); return [{ deprecated: hasJSDocTag(propertySignature, "deprecated"), example: ctx.getNodeExample(propertySignature), extensions: ctx.getNodeExtensions(propertySignature), default: getJSDocTagComment(propertySignature, "default"), description: ctx.getNodeDescription(propertySignature), format: getNodeFormat(propertySignature), name: getPropertyName(propertySignature), required: !propertySignature.questionToken, type, validators: getDeclarationValidators(propertySignature) || {} }, ...res]; }, []); const indexMember = typeNode.members.find((member) => isIndexSignatureDeclaration(member)); let additionalType; if (indexMember) { const indexSignatureDeclaration = indexMember; if (!isStringType(ctx.resolveType(indexSignatureDeclaration.parameters[0].type, ctx.parentNode, ctx.context))) throw new ResolverError("Only string indexes are supported.", typeNode); additionalType = ctx.resolveType(indexSignatureDeclaration.type, ctx.parentNode, ctx.context); } return { additionalProperties: indexMember && additionalType, typeName: TypeName.NESTED_OBJECT_LITERAL, properties }; } function getNodeFormat(node) { return getJSDocTagComment(node, "format"); } function getPropertyName(node) { if (isIdentifier(node.name)) return node.name.text; if (isStringLiteral(node.name) || isNumericLiteral(node.name)) return node.name.text; return node.name.getText(); } //#endregion //#region src/adapters/typescript/resolver/sub/primitive.ts const NUMERIC_KIND_TO_TYPE_NAME = { [NumericKind.Int]: TypeName.INTEGER, [NumericKind.Long]: TypeName.LONG, [NumericKind.Float]: TypeName.FLOAT, [NumericKind.Double]: TypeName.DOUBLE }; var PrimitiveResolver = class { registry; constructor(registry) { this.registry = registry; } resolve(node, parentNode) { const resolved = this.resolveSyntaxKind(node.kind); if (resolved) { if (resolved === "string") return { typeName: TypeName.STRING }; if (resolved === "void") return { typeName: TypeName.VOID }; if (resolved === "boolean") return { typeName: TypeName.BOOLEAN }; if (resolved === "undefined") return { typeName: TypeName.UNDEFINED }; if (resolved === "null") return; if (resolved === "never") return { typeName: TypeName.NEVER }; if (resolved === "bigint") return { typeName: TypeName.BIGINT }; if (resolved === "number") { if (!parentNode) return { typeName: TypeName.DOUBLE }; const presentJsDocTags = new Set(getJSDocTagNames(parentNode).map((tag) => tag.toLowerCase())); for (const kind of [ NumericKind.Int, NumericKind.Long, NumericKind.Float, NumericKind.Double ]) { const matchKind = (m) => numericMarkerKind(m) === kind; const decoratorNames = namesForMarker(this.registry, matchKind); for (const name of decoratorNames) if (hasDecoratorNamed(parentNode, name)) return { typeName: NUMERIC_KIND_TO_TYPE_NAME[kind] }; const jsDocTags = tagsForMarker(this.registry, matchKind); for (const tag of jsDocTags) if (presentJsDocTags.has(tag.toLowerCase())) return { typeName: NUMERIC_KIND_TO_TYPE_NAME[kind] }; } return { typeName: TypeName.DOUBLE }; } } } resolveSyntaxKind(syntaxKind) { switch (syntaxKind) { case SyntaxKind.StringKeyword: return "string"; case SyntaxKind.BooleanKeyword: return "boolean"; case SyntaxKind.VoidKeyword: return "void"; case SyntaxKind.UndefinedKeyword: return "undefined"; case SyntaxKind.NullKeyword: return "null"; case SyntaxKind.NumberKeyword: return "number"; case SyntaxKind.BigIntKeyword: return "bigint"; case SyntaxKind.NeverKeyword: return "never"; default: return; } } }; //#endregion //#region src/adapters/typescript/resolver/sub/reference.ts var ReferenceResolver = class extends ResolverBase { typeChecker; constructor(typeChecker) { super(); this.typeChecker = typeChecker; } merge(referenceTypes) { if (referenceTypes.length === 1) return referenceTypes[0]; if (referenceTypes.every((refType) => refType.refName === TypeName.REF_ENUM)) return this.mergeManyRefEnums(referenceTypes); if (referenceTypes.every((refType) => refType.refName === TypeName.REF_OBJECT)) return this.mergeManyRefObjects(referenceTypes); throw new ResolverError(`These resolved type merge rules are not defined: ${JSON.stringify(referenceTypes)}`); } mergeManyRefEnums(many) { let merged = this.mergeRefEnums(many[0], many[1]); for (let i = 2; i < many.length; ++i) merged = this.mergeRefEnums(merged, many[i]); return merged; } mergeRefEnums(first, second) { let description; if (first.description || second.description) if (!first.description) description = second.description; else if (!second.description) description = first.description; else description = `${first.description}\n${second.description}`; return { typeName: TypeName.REF_ENUM, example: first.example || second.example, description, members: [...first.members || [], ...second.members || []], memberNames: [...first.memberNames || [], ...second.memberNames || []], refName: first.refName, deprecated: first.deprecated || second.deprecated }; } mergeManyRefObjects(many) { let merged = this.mergeRefObject(many[0], many[1]); for (let i = 2; i < many.length; ++i) merged = this.mergeRefObject(merged, many[i]); return merged; } mergeRefObject(first, second) { let description; if (first.description || second.description) if (!first.description) description = second.description; else if (!second.description) description = first.description; else description = `${first.description}\n${second.description}`; const properties = [...first.properties, ...second.properties.filter((prop) => first.properties.every((firstProp) => firstProp.name !== prop.name))]; let additionalProperties; if (first.additionalProperties || second.additionalProperties) if (!first.additionalProperties) additionalProperties = second.additionalProperties; else if (!second.additionalProperties) additionalProperties = first.additionalProperties; else additionalProperties = { typeName: TypeName.UNION, members: [first.additionalProperties, second.additionalProperties] }; return { typeName: TypeName.REF_OBJECT, description, properties, additionalProperties, refName: first.refName, deprecated: first.deprecated || second.deprecated, example: first.example || second.example }; } transformEnum(declaration, enumName) { const isNotUndefined = (item) => item !== void 0; const enums = declaration.members.map(this.typeChecker.getConstantValue.bind(this.typeChecker)).filter(isNotUndefined); const enumNames = declaration.members.map((e) => e.name.getText()).filter(isNotUndefined); return { typeName: TypeName.REF_ENUM, description: getNodeDescription(declaration, this.typeChecker), members: enums, memberNames: enumNames, refName: enumName, deprecated: hasJSDocTag(declaration, "deprecated") }; } transformEnumMember(declaration, enumName) { return { typeName: TypeName.REF_ENUM, refName: enumName, members: [this.typeChecker.getConstantValue(declaration)], memberNames: [declaration.name.getText()], deprecated: hasJSDocTag(declaration, "deprecated") }; } }; //#endregion //#region src/adapters/typescript/resolver/sub/tuple.ts function resolveTupleType(typeNode, ctx) { if (!isTupleTypeNode(typeNode)) return; const elements = typeNode.elements.map((element) => { const isNamed = isNamedTupleMember(element); const actualType = isNamed ? element.type : element; return { type: ctx.resolveType(actualType, ctx.parentNode, ctx.context), ...isNamed && { name: element.name.text } }; }); return { typeName: TypeName.TUPLE, elements }; } //#endregion //#region src/adapters/typescript/resolver/sub/type-operator.ts function resolveTypeOperatorType(typeNode, ctx) { if (!isTypeOperatorNode(typeNode)) return; if (typeNode.operator === SyntaxKind.KeyOfKeyword) { const type = ctx.typeChecker.getTypeFromTypeNode(typeNode); try { return ctx.resolveType(toTypeNodeOrFail(ctx.typeChecker, type, void 0, NodeBuilderFlags.NoTruncation), typeNode, ctx.context, ctx.referencer); } catch (err) { throw new ResolverError(`Could not determine the keys on ${ctx.typeChecker.typeToString(ctx.typeChecker.getTypeFromTypeNode(typeNode.type))}`, typeNode, { cause: err }); } } if (typeNode.operator === SyntaxKind.ReadonlyKeyword) return ctx.resolveType(typeNode.type, typeNode, ctx.context, ctx.referencer); } //#endregion //#region src/adapters/typescript/resolver/sub/union.ts function resolveUnionType(typeNode, ctx) { if (!isUnionTypeNode(typeNode)) return; const members = typeNode.types.map((type) => ctx.resolveType(type, ctx.parentNode, ctx.context)); return { typeName: TypeName.UNION, members }; } //#endregion //#region src/adapters/typescript/resolver/module.ts var TypeNodeResolver = class TypeNodeResolver extends ResolverBase { static MAX_DEPTH = 50; typeNode; current; parentNode; context; referencer; depth; primitiveResolver; referenceResolver; constructor(typeNode, current, parentNode, context, referencer, depth) { super(); this.typeNode = typeNode; this.current = current; this.parentNode = parentNode; this.context = context || {}; this.referencer = referencer; this.depth = depth ?? 0; this.primitiveResolver = new PrimitiveResolver(current.registry); this.referenceResolver = new ReferenceResolver(current.typeChecker); } /** * @deprecated Use resolverCache.clear() on the context instead. * Kept for backward compatibility — no-ops since cache is now instance-scoped. */ static clearCache() {} resolve() { if (this.depth > TypeNodeResolver.MAX_DEPTH) throw new ResolverError(`Type resolution exceeded maximum depth of ${TypeNodeResolver.MAX_DEPTH}. This usually indicates deeply nested or circular generics.`, this.typeNode); const ctx = this.createSubResolverContext(); const result = this.primitiveResolver.resolve(this.typeNode, this.parentNode) ?? resolveLiteralType(this.typeNode, ctx) ?? resolveArrayType(this.typeNode, ctx) ?? resolveUnionType(this.typeNode, ctx) ?? resolveIntersectionType(this.typeNode, ctx) ?? resolveObjectLiteralType(this.typeNode, ctx) ?? resolveTupleType(this.typeNode, ctx) ?? resolveMappedType(this.typeNode, ctx) ?? this.resolveConditionalType() ?? resolveTypeOperatorType(this.typeNode, ctx) ?? resolveIndexedAccessType(this.typeNode, ctx) ?? this.resolveTypeReference(); if (!result) this.throwUnknownType(); return result; } createSubResolverContext() { return { typeChecker: this.current.typeChecker, current: this.current, parentNode: this.parentNode, context: this.context, referencer: this.referencer, resolveType: (typeNode, parentNode, context, referencer) => this.resolveNestedType(typeNode, parentNode, context, referencer), propertyFromSignature: (sig, overrideToken) => this.propertyFromSignature(sig, overrideToken), propertyFromDeclaration: (decl, overrideToken) => this.propertyFromDeclaration(decl, overrideToken), getNodeDescription: (node) => this.getNodeDescription(node), getNodeExample: (node) => this.getNodeExample(node), getNodeExtensions: (node) => this.getNodeExtensions(node) }; } resolveNestedType(typeNode, parentNode, context, referencer) { return new TypeNodeResolver(typeNode, this.current, parentNode, context, referencer, this.depth + 1).resolve(); } throwUnknownType() { throw new ResolverError(`Unknown type: ${SyntaxKind[this.typeNode.kind]}`, this.typeNode); } resolveConditionalType() { if (!isConditionalTypeNode(this.typeNode)) return; if (Object.keys(this.context).length > 0 && this.referencer) return this.resolveTypeViaChecker(this.referencer); return this.resolveTypeViaChecker(this.typeNode); } resolveTypeReference() { if (this.typeNode.kind !== SyntaxKind.TypeReference) return; const typeReference = this.typeNode; if (typeReference.typeName.kind === SyntaxKind.Identifier) { if (typeReference.typeName.text === "Record" && typeReference.typeArguments && typeReference.typeArguments[1]) return { additionalProperties: this.resolveNestedType(typeReference.typeArguments[1], this.parentNode, this.context), typeName: TypeName.NESTED_OBJECT_LITERAL, properties: [] }; const specialReference = TypeNodeResolver.resolveSpecialReference(typeReference.typeName); if (typeof specialReference !== "undefined") return specialReference; if (typeReference.typeName.text === "Date") return this.getDateType(this.parentNode); if (typeReference.typeName.text === "Buffer" || typeReference.typeName.text === "Readable") return { typeName: TypeName.BUFFER }; if (typeReference.typeName.text === "Array" && typeReference.typeArguments?.[0]) return { typeName: TypeName.ARRAY, elementType: this.resolveNestedType(typeReference.typeArguments[0], this.parentNode, this.context) }; if (typeReference.typeName.text === "Promise" && typeReference.typeArguments?.length === 1 && typeReference.typeArguments[0]) return this.resolveNestedType(typeReference.typeArguments[0], this.parentNode, this.context); if (typeReference.typeName.text === "String") return { typeName: TypeName.STRING }; const contextual = this.context[typeReference.typeName.text]; if (contextual) return this.resolveNestedType(contextual, this.parentNode, this.context); if (TypeNodeResolver.isCheckerResolvableUtilityType(typeReference.typeName.text)) return this.resolveUtilityTypeViaChecker(typeReference); } try { const referenceType = this.getReferenceType(typeReference); this.current.addReferenceType(referenceType); return referenceType; } catch (err) { try { return this.resolveTypeViaChecker(typeReference); } catch { throw err; } } } static CHECKER_RESOLVABLE_UTILITY_TYPES = new Set([ UtilityTypeName.NON_NULLABLE, UtilityTypeName.OMIT, UtilityTypeName.PARTIAL, UtilityTypeName.READONLY, UtilityTypeName.REQUIRED, UtilityTypeName.PICK, UtilityTypeName.EXTRACT, UtilityTypeName.EXCLUDE, UtilityTypeName.RETURN_TYPE, UtilityTypeName.PARAMETERS, UtilityTypeName.AWAITED, UtilityTypeName.INSTANCE_TYPE, UtilityTypeName.CONSTRUCTOR_PARAMETERS ]); static isCheckerResolvableUtilityType(name) { return TypeNodeResolver.CHECKER_RESOLVABLE_UTILITY_TYPES.has(name); } resolveUtilityTypeViaChecker(typeReference) { if (this.hasUnboundContextArgs(typeReference) && this.referencer) return this.resolveTypeViaChecker(this.referencer); return this.resolveTypeViaChecker(typeReference); } /** * Shared checker delegation: resolves a type node by letting the TS * type checker evaluate it, converting back to a TypeNode, and * recursively resolving the result. */ resolveTypeViaChecker(typeNode) { const type = this.current.typeChecker.getTypeFromTypeNode(typeNode); const resolvedTypeNode = this.current.typeChecker.typeToTypeNode(type, void 0, NodeBuilderFlags.NoTruncation | NodeBuilderFlags.InTypeAlias); if (!resolvedTypeNode) return { typeName: TypeName.TUPLE, elements: [] }; return this.resolveNestedType(resolvedTypeNode, this.parentNode, this.context); } /