@twin.org/tools-core 0.0.3-next.3 → 0.0.3-next.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +2 -2
  2. package/dist/es/index.js +21 -11
  3. package/dist/es/index.js.map +1 -1
  4. package/dist/es/models/ITypeScriptToSchemaContext.js +2 -0
  5. package/dist/es/models/ITypeScriptToSchemaContext.js.map +1 -0
  6. package/dist/es/models/ITypeScriptToSchemaDiagnostics.js +4 -0
  7. package/dist/es/models/ITypeScriptToSchemaDiagnostics.js.map +1 -0
  8. package/dist/es/models/ITypeScriptToSchemaOptions.js +4 -0
  9. package/dist/es/models/ITypeScriptToSchemaOptions.js.map +1 -0
  10. package/dist/es/models/embeddedSchemaMode.js +17 -0
  11. package/dist/es/models/embeddedSchemaMode.js.map +1 -0
  12. package/dist/es/utils/constants.js +43 -0
  13. package/dist/es/utils/constants.js.map +1 -0
  14. package/dist/es/utils/diagnosticReporter.js +32 -0
  15. package/dist/es/utils/diagnosticReporter.js.map +1 -0
  16. package/dist/es/utils/disallowedTypeGuard.js +151 -0
  17. package/dist/es/utils/disallowedTypeGuard.js.map +1 -0
  18. package/dist/es/utils/enum.js +152 -0
  19. package/dist/es/utils/enum.js.map +1 -0
  20. package/dist/es/utils/fileUtils.js +132 -0
  21. package/dist/es/utils/fileUtils.js.map +1 -0
  22. package/dist/es/utils/importTypeQuerySchemaResolver.js +363 -0
  23. package/dist/es/utils/importTypeQuerySchemaResolver.js.map +1 -0
  24. package/dist/es/utils/indexSignaturePatternResolver.js +94 -0
  25. package/dist/es/utils/indexSignaturePatternResolver.js.map +1 -0
  26. package/dist/es/utils/intersectionSchemaMerger.js +85 -0
  27. package/dist/es/utils/intersectionSchemaMerger.js.map +1 -0
  28. package/dist/es/utils/jsDoc.js +138 -0
  29. package/dist/es/utils/jsDoc.js.map +1 -0
  30. package/dist/es/utils/jsonSchemaBuilder.js +3415 -0
  31. package/dist/es/utils/jsonSchemaBuilder.js.map +1 -0
  32. package/dist/es/utils/mappedTypeSchemaResolver.js +265 -0
  33. package/dist/es/utils/mappedTypeSchemaResolver.js.map +1 -0
  34. package/dist/es/utils/objectTransformer.js +161 -0
  35. package/dist/es/utils/objectTransformer.js.map +1 -0
  36. package/dist/es/utils/regEx.js +128 -0
  37. package/dist/es/utils/regEx.js.map +1 -0
  38. package/dist/es/utils/resolver.js +177 -0
  39. package/dist/es/utils/resolver.js.map +1 -0
  40. package/dist/es/utils/templateLiteralPatternBuilder.js +94 -0
  41. package/dist/es/utils/templateLiteralPatternBuilder.js.map +1 -0
  42. package/dist/es/utils/typeScriptToSchema.js +319 -0
  43. package/dist/es/utils/typeScriptToSchema.js.map +1 -0
  44. package/dist/es/utils/utilityTypeSchemaMapper.js +475 -0
  45. package/dist/es/utils/utilityTypeSchemaMapper.js.map +1 -0
  46. package/dist/types/index.d.ts +21 -11
  47. package/dist/types/models/ITypeScriptToSchemaContext.d.ts +71 -0
  48. package/dist/types/models/ITypeScriptToSchemaDiagnostics.d.ts +31 -0
  49. package/dist/types/models/ITypeScriptToSchemaOptions.d.ts +22 -0
  50. package/dist/types/models/embeddedSchemaMode.d.ts +17 -0
  51. package/dist/types/utils/constants.d.ts +13 -0
  52. package/dist/types/utils/diagnosticReporter.d.ts +17 -0
  53. package/dist/types/utils/disallowedTypeGuard.d.ts +16 -0
  54. package/dist/types/utils/enum.d.ts +42 -0
  55. package/dist/types/utils/fileUtils.d.ts +66 -0
  56. package/dist/types/utils/importTypeQuerySchemaResolver.d.ts +87 -0
  57. package/dist/types/utils/indexSignaturePatternResolver.d.ts +21 -0
  58. package/dist/types/utils/intersectionSchemaMerger.d.ts +16 -0
  59. package/dist/types/utils/jsDoc.d.ts +53 -0
  60. package/dist/types/utils/jsonSchemaBuilder.d.ts +671 -0
  61. package/dist/types/utils/mappedTypeSchemaResolver.d.ts +81 -0
  62. package/dist/types/utils/objectTransformer.d.ts +33 -0
  63. package/dist/types/utils/regEx.d.ts +24 -0
  64. package/dist/types/utils/resolver.d.ts +22 -0
  65. package/dist/types/utils/templateLiteralPatternBuilder.d.ts +12 -0
  66. package/dist/types/utils/typeScriptToSchema.d.ts +92 -0
  67. package/dist/types/utils/utilityTypeSchemaMapper.d.ts +141 -0
  68. package/docs/changelog.md +351 -34
  69. package/docs/examples.md +87 -1
  70. package/docs/reference/classes/Constants.md +29 -0
  71. package/docs/reference/classes/DiagnosticReporter.md +49 -0
  72. package/docs/reference/classes/DisallowedTypeGuard.md +35 -0
  73. package/docs/reference/classes/Enum.md +93 -0
  74. package/docs/reference/classes/FileUtils.md +237 -0
  75. package/docs/reference/classes/ImportTypeQuerySchemaResolver.md +109 -0
  76. package/docs/reference/classes/IndexSignaturePatternResolver.md +69 -0
  77. package/docs/reference/classes/IntersectionSchemaMerger.md +48 -0
  78. package/docs/reference/classes/JsDoc.md +169 -0
  79. package/docs/reference/classes/JsonSchemaBuilder.md +2578 -0
  80. package/docs/reference/classes/MappedTypeSchemaResolver.md +275 -0
  81. package/docs/reference/classes/ObjectTransformer.md +119 -0
  82. package/docs/reference/classes/RegEx.md +99 -0
  83. package/docs/reference/classes/Resolver.md +52 -0
  84. package/docs/reference/classes/TemplateLiteralPatternBuilder.md +35 -0
  85. package/docs/reference/classes/TypeScriptToSchema.md +91 -0
  86. package/docs/reference/classes/UtilityTypeSchemaMapper.md +343 -0
  87. package/docs/reference/index.md +25 -11
  88. package/docs/reference/interfaces/ITypeScriptToSchemaContext.md +125 -0
  89. package/docs/reference/interfaces/ITypeScriptToSchemaDiagnostics.md +55 -0
  90. package/docs/reference/interfaces/ITypeScriptToSchemaOptions.md +44 -0
  91. package/docs/reference/type-aliases/EmbeddedSchemaMode.md +5 -0
  92. package/docs/reference/variables/EmbeddedSchemaMode.md +19 -0
  93. package/locales/en.json +32 -1
  94. package/package.json +6 -5
  95. package/dist/es/models/IJsonSchema.js +0 -2
  96. package/dist/es/models/IJsonSchema.js.map +0 -1
  97. package/dist/es/models/IOpenApi.js +0 -2
  98. package/dist/es/models/IOpenApi.js.map +0 -1
  99. package/dist/es/models/IOpenApiExample.js +0 -4
  100. package/dist/es/models/IOpenApiExample.js.map +0 -1
  101. package/dist/es/models/IOpenApiHeader.js +0 -4
  102. package/dist/es/models/IOpenApiHeader.js.map +0 -1
  103. package/dist/es/models/IOpenApiPathMethod.js +0 -2
  104. package/dist/es/models/IOpenApiPathMethod.js.map +0 -1
  105. package/dist/es/models/IOpenApiResponse.js +0 -2
  106. package/dist/es/models/IOpenApiResponse.js.map +0 -1
  107. package/dist/es/models/IOpenApiSecurityScheme.js +0 -4
  108. package/dist/es/models/IOpenApiSecurityScheme.js.map +0 -1
  109. package/dist/es/models/IPackageJson.js +0 -4
  110. package/dist/es/models/IPackageJson.js.map +0 -1
  111. package/dist/es/models/jsonTypeName.js +0 -2
  112. package/dist/es/models/jsonTypeName.js.map +0 -1
  113. package/dist/es/utils/jsonSchemaHelper.js +0 -258
  114. package/dist/es/utils/jsonSchemaHelper.js.map +0 -1
  115. package/dist/es/utils/openApiHelper.js +0 -12
  116. package/dist/es/utils/openApiHelper.js.map +0 -1
  117. package/dist/types/models/IJsonSchema.d.ts +0 -5
  118. package/dist/types/models/IOpenApi.d.ts +0 -54
  119. package/dist/types/models/IOpenApiExample.d.ts +0 -13
  120. package/dist/types/models/IOpenApiHeader.d.ts +0 -19
  121. package/dist/types/models/IOpenApiPathMethod.d.ts +0 -65
  122. package/dist/types/models/IOpenApiResponse.d.ts +0 -32
  123. package/dist/types/models/IOpenApiSecurityScheme.d.ts +0 -25
  124. package/dist/types/models/IPackageJson.d.ts +0 -15
  125. package/dist/types/models/jsonTypeName.d.ts +0 -5
  126. package/dist/types/utils/jsonSchemaHelper.d.ts +0 -78
  127. package/dist/types/utils/openApiHelper.d.ts +0 -9
  128. package/docs/reference/classes/JsonSchemaHelper.md +0 -233
  129. package/docs/reference/classes/OpenApiHelper.md +0 -21
  130. package/docs/reference/interfaces/IOpenApi.md +0 -103
  131. package/docs/reference/interfaces/IOpenApiExample.md +0 -19
  132. package/docs/reference/interfaces/IOpenApiHeader.md +0 -31
  133. package/docs/reference/interfaces/IOpenApiPathMethod.md +0 -119
  134. package/docs/reference/interfaces/IOpenApiResponse.md +0 -35
  135. package/docs/reference/interfaces/IOpenApiSecurityScheme.md +0 -43
  136. package/docs/reference/interfaces/IPackageJson.md +0 -23
  137. package/docs/reference/type-aliases/IJsonSchema.md +0 -5
  138. package/docs/reference/type-aliases/JsonTypeName.md +0 -5
@@ -0,0 +1,177 @@
1
+ // Copyright 2026 IOTA Stiftung.
2
+ // SPDX-License-Identifier: Apache-2.0.
3
+ import * as ts from "typescript";
4
+ import { FileUtils } from "./fileUtils.js";
5
+ /**
6
+ * Resolve TypeScript type declarations from package names.
7
+ */
8
+ export class Resolver {
9
+ /**
10
+ * Cache for resolved package entry files.
11
+ * @internal
12
+ */
13
+ static _resolvedModuleFileCache = {};
14
+ /**
15
+ * Cache for parsed source files.
16
+ * @internal
17
+ */
18
+ static _sourceFileCache = {};
19
+ /**
20
+ * Cache for resolved type declarations.
21
+ * @internal
22
+ */
23
+ static _typeDeclarationCache = {};
24
+ /**
25
+ * Resolve a type declaration AST from a package and type name.
26
+ * @param packageName The package to inspect.
27
+ * @param typeName The type to resolve.
28
+ * @param containingFilePath An optional source file path to use as the starting point for
29
+ * package resolution. When provided, TypeScript module resolution walks up from that file's
30
+ * directory, which allows transitive dependencies installed alongside the source file (e.g.
31
+ * in a sub-directory node_modules) to be found even when they are not reachable from the
32
+ * current working directory. The path is normalised to absolute before use;
33
+ * falls back to process.cwd() when omitted.
34
+ * @returns The resolved declaration AST.
35
+ */
36
+ static resolveTypeDeclarationAst(packageName, typeName, containingFilePath) {
37
+ // path.resolve normalises both absolute and relative paths to an absolute form so that
38
+ // TypeScript module resolution can correctly walk up the directory tree to node_modules.
39
+ const containingFile = containingFilePath
40
+ ? FileUtils.normalizeFilePath(FileUtils.resolvePath(containingFilePath))
41
+ : `${FileUtils.normalizeFilePath(FileUtils.getCurrentWorkingDirectory())}/__typeScriptToSchema__.ts`;
42
+ const resolveDir = FileUtils.getDirectoryPath(containingFile);
43
+ const cacheKey = `${resolveDir}::${packageName}::${typeName}`;
44
+ const moduleCacheKey = `${resolveDir}::${packageName}`;
45
+ const cachedDeclaration = Resolver._typeDeclarationCache[cacheKey];
46
+ if (cachedDeclaration !== undefined) {
47
+ return cachedDeclaration ?? undefined;
48
+ }
49
+ const compilerOptions = Resolver.getModuleResolutionCompilerOptions();
50
+ const resolvedModuleFileName = Resolver.resolvePackageEntryFile(packageName, containingFile, compilerOptions, moduleCacheKey);
51
+ if (!resolvedModuleFileName) {
52
+ Resolver._typeDeclarationCache[cacheKey] = null;
53
+ return undefined;
54
+ }
55
+ const declarationResult = Resolver.findTypeDeclarationInModuleGraph(resolvedModuleFileName, typeName, new Set(), compilerOptions);
56
+ Resolver._typeDeclarationCache[cacheKey] = declarationResult ?? null;
57
+ return declarationResult;
58
+ }
59
+ /**
60
+ * Resolve and cache the package entry file for a module name.
61
+ * @param packageName The package to resolve.
62
+ * @param containingFile The containing file for module resolution.
63
+ * @param compilerOptions Compiler options for module resolution.
64
+ * @param cacheKey The cache key to use for the resolved module file cache.
65
+ * @returns The resolved entry file path.
66
+ * @internal
67
+ */
68
+ static resolvePackageEntryFile(packageName, containingFile, compilerOptions, cacheKey) {
69
+ const cachedResolvedModuleFile = Resolver._resolvedModuleFileCache[cacheKey];
70
+ if (cachedResolvedModuleFile !== undefined) {
71
+ return cachedResolvedModuleFile ?? undefined;
72
+ }
73
+ const resolvedModule = ts.resolveModuleName(packageName, containingFile, compilerOptions, ts.sys).resolvedModule;
74
+ const resolvedModuleFileName = resolvedModule?.resolvedFileName;
75
+ if (!resolvedModuleFileName) {
76
+ Resolver._resolvedModuleFileCache[cacheKey] = null;
77
+ return undefined;
78
+ }
79
+ Resolver._resolvedModuleFileCache[cacheKey] = resolvedModuleFileName;
80
+ return resolvedModuleFileName;
81
+ }
82
+ /**
83
+ * Resolve compiler options for module lookup.
84
+ * @returns The compiler options.
85
+ * @internal
86
+ */
87
+ static getModuleResolutionCompilerOptions() {
88
+ return {
89
+ module: ts.ModuleKind.NodeNext,
90
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
91
+ target: ts.ScriptTarget.ESNext,
92
+ skipLibCheck: true
93
+ };
94
+ }
95
+ /**
96
+ * Find a type declaration by walking a module import/export graph.
97
+ * @param sourceFilePath The source file path to inspect.
98
+ * @param typeName The type name to find.
99
+ * @param visitedFiles The visited file set.
100
+ * @param compilerOptions Compiler options for module resolution.
101
+ * @returns The matched declaration with its source file.
102
+ * @internal
103
+ */
104
+ static findTypeDeclarationInModuleGraph(sourceFilePath, typeName, visitedFiles, compilerOptions) {
105
+ const absoluteSourcePath = FileUtils.normalizeFilePath(sourceFilePath);
106
+ if (visitedFiles.has(absoluteSourcePath)) {
107
+ return undefined;
108
+ }
109
+ visitedFiles.add(absoluteSourcePath);
110
+ const sourceFile = Resolver.getOrCreateSourceFile(sourceFilePath);
111
+ if (!sourceFile) {
112
+ return undefined;
113
+ }
114
+ for (const statement of sourceFile.statements) {
115
+ // interface IFoo { ... } or type Foo = ... (the declaration we are searching for)
116
+ if ((ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)) &&
117
+ statement.name.text === typeName) {
118
+ return {
119
+ sourceFile,
120
+ declaration: statement
121
+ };
122
+ }
123
+ }
124
+ const moduleSpecifiers = sourceFile.statements
125
+ // import { ... } from "..." or export { ... } from "..." (re-export and import chains)
126
+ .filter(statement => ts.isExportDeclaration(statement) || ts.isImportDeclaration(statement))
127
+ .flatMap(statement => {
128
+ const moduleSpecifier = statement.moduleSpecifier;
129
+ // "./module.js" (string literal module specifier)
130
+ return moduleSpecifier && ts.isStringLiteral(moduleSpecifier) ? [moduleSpecifier.text] : [];
131
+ });
132
+ for (const moduleSpecifier of moduleSpecifiers) {
133
+ const resolvedImportPath = Resolver.resolveModuleSpecifierFromFile(moduleSpecifier, sourceFilePath, compilerOptions);
134
+ if (resolvedImportPath) {
135
+ const declarationResult = Resolver.findTypeDeclarationInModuleGraph(resolvedImportPath, typeName, visitedFiles, compilerOptions);
136
+ if (declarationResult) {
137
+ return declarationResult;
138
+ }
139
+ }
140
+ }
141
+ return undefined;
142
+ }
143
+ /**
144
+ * Read and cache a parsed source file.
145
+ * @param sourceFilePath The source file path.
146
+ * @returns The parsed source file.
147
+ * @internal
148
+ */
149
+ static getOrCreateSourceFile(sourceFilePath) {
150
+ const normalizedPath = FileUtils.normalizeFilePath(sourceFilePath);
151
+ const cachedSourceFile = Resolver._sourceFileCache[normalizedPath];
152
+ if (cachedSourceFile !== undefined) {
153
+ return cachedSourceFile ?? undefined;
154
+ }
155
+ const source = FileUtils.readFile(sourceFilePath);
156
+ if (!source) {
157
+ Resolver._sourceFileCache[normalizedPath] = null;
158
+ return undefined;
159
+ }
160
+ const sourceFile = ts.createSourceFile(sourceFilePath, source, ts.ScriptTarget.Latest, true);
161
+ Resolver._sourceFileCache[normalizedPath] = sourceFile;
162
+ return sourceFile;
163
+ }
164
+ /**
165
+ * Resolve a module specifier from a containing file.
166
+ * @param moduleSpecifier The module specifier text.
167
+ * @param containingFilePath The file containing the import/export.
168
+ * @param compilerOptions Compiler options for module resolution.
169
+ * @returns The resolved file path.
170
+ * @internal
171
+ */
172
+ static resolveModuleSpecifierFromFile(moduleSpecifier, containingFilePath, compilerOptions) {
173
+ return ts.resolveModuleName(moduleSpecifier, containingFilePath, compilerOptions, ts.sys)
174
+ .resolvedModule?.resolvedFileName;
175
+ }
176
+ }
177
+ //# sourceMappingURL=resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolver.js","sourceRoot":"","sources":["../../../src/utils/resolver.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport * as ts from \"typescript\";\nimport { FileUtils } from \"./fileUtils.js\";\n\n/**\n * Resolve TypeScript type declarations from package names.\n */\nexport class Resolver {\n\t/**\n\t * Cache for resolved package entry files.\n\t * @internal\n\t */\n\tprivate static readonly _resolvedModuleFileCache: { [id: string]: string | null | undefined } =\n\t\t{};\n\n\t/**\n\t * Cache for parsed source files.\n\t * @internal\n\t */\n\tprivate static readonly _sourceFileCache: { [id: string]: ts.SourceFile | null | undefined } = {};\n\n\t/**\n\t * Cache for resolved type declarations.\n\t * @internal\n\t */\n\tprivate static readonly _typeDeclarationCache: {\n\t\t[id: string]:\n\t\t\t| {\n\t\t\t\t\tsourceFile: ts.SourceFile;\n\t\t\t\t\tdeclaration: ts.InterfaceDeclaration | ts.TypeAliasDeclaration;\n\t\t\t }\n\t\t\t| null\n\t\t\t| undefined;\n\t} = {};\n\n\t/**\n\t * Resolve a type declaration AST from a package and type name.\n\t * @param packageName The package to inspect.\n\t * @param typeName The type to resolve.\n\t * @param containingFilePath An optional source file path to use as the starting point for\n\t * package resolution. When provided, TypeScript module resolution walks up from that file's\n\t * directory, which allows transitive dependencies installed alongside the source file (e.g.\n\t * in a sub-directory node_modules) to be found even when they are not reachable from the\n\t * current working directory. The path is normalised to absolute before use;\n\t * falls back to process.cwd() when omitted.\n\t * @returns The resolved declaration AST.\n\t */\n\tpublic static resolveTypeDeclarationAst(\n\t\tpackageName: string,\n\t\ttypeName: string,\n\t\tcontainingFilePath?: string\n\t):\n\t\t| {\n\t\t\t\tsourceFile: ts.SourceFile;\n\t\t\t\tdeclaration: ts.InterfaceDeclaration | ts.TypeAliasDeclaration;\n\t\t }\n\t\t| undefined {\n\t\t// path.resolve normalises both absolute and relative paths to an absolute form so that\n\t\t// TypeScript module resolution can correctly walk up the directory tree to node_modules.\n\t\tconst containingFile = containingFilePath\n\t\t\t? FileUtils.normalizeFilePath(FileUtils.resolvePath(containingFilePath))\n\t\t\t: `${FileUtils.normalizeFilePath(FileUtils.getCurrentWorkingDirectory())}/__typeScriptToSchema__.ts`;\n\t\tconst resolveDir = FileUtils.getDirectoryPath(containingFile);\n\t\tconst cacheKey = `${resolveDir}::${packageName}::${typeName}`;\n\t\tconst moduleCacheKey = `${resolveDir}::${packageName}`;\n\n\t\tconst cachedDeclaration = Resolver._typeDeclarationCache[cacheKey];\n\t\tif (cachedDeclaration !== undefined) {\n\t\t\treturn cachedDeclaration ?? undefined;\n\t\t}\n\n\t\tconst compilerOptions = Resolver.getModuleResolutionCompilerOptions();\n\t\tconst resolvedModuleFileName = Resolver.resolvePackageEntryFile(\n\t\t\tpackageName,\n\t\t\tcontainingFile,\n\t\t\tcompilerOptions,\n\t\t\tmoduleCacheKey\n\t\t);\n\n\t\tif (!resolvedModuleFileName) {\n\t\t\tResolver._typeDeclarationCache[cacheKey] = null;\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst declarationResult = Resolver.findTypeDeclarationInModuleGraph(\n\t\t\tresolvedModuleFileName,\n\t\t\ttypeName,\n\t\t\tnew Set<string>(),\n\t\t\tcompilerOptions\n\t\t);\n\n\t\tResolver._typeDeclarationCache[cacheKey] = declarationResult ?? null;\n\t\treturn declarationResult;\n\t}\n\n\t/**\n\t * Resolve and cache the package entry file for a module name.\n\t * @param packageName The package to resolve.\n\t * @param containingFile The containing file for module resolution.\n\t * @param compilerOptions Compiler options for module resolution.\n\t * @param cacheKey The cache key to use for the resolved module file cache.\n\t * @returns The resolved entry file path.\n\t * @internal\n\t */\n\tprivate static resolvePackageEntryFile(\n\t\tpackageName: string,\n\t\tcontainingFile: string,\n\t\tcompilerOptions: ts.CompilerOptions,\n\t\tcacheKey: string\n\t): string | undefined {\n\t\tconst cachedResolvedModuleFile = Resolver._resolvedModuleFileCache[cacheKey];\n\t\tif (cachedResolvedModuleFile !== undefined) {\n\t\t\treturn cachedResolvedModuleFile ?? undefined;\n\t\t}\n\n\t\tconst resolvedModule = ts.resolveModuleName(\n\t\t\tpackageName,\n\t\t\tcontainingFile,\n\t\t\tcompilerOptions,\n\t\t\tts.sys\n\t\t).resolvedModule;\n\t\tconst resolvedModuleFileName = resolvedModule?.resolvedFileName;\n\t\tif (!resolvedModuleFileName) {\n\t\t\tResolver._resolvedModuleFileCache[cacheKey] = null;\n\t\t\treturn undefined;\n\t\t}\n\n\t\tResolver._resolvedModuleFileCache[cacheKey] = resolvedModuleFileName;\n\t\treturn resolvedModuleFileName;\n\t}\n\n\t/**\n\t * Resolve compiler options for module lookup.\n\t * @returns The compiler options.\n\t * @internal\n\t */\n\tprivate static getModuleResolutionCompilerOptions(): ts.CompilerOptions {\n\t\treturn {\n\t\t\tmodule: ts.ModuleKind.NodeNext,\n\t\t\tmoduleResolution: ts.ModuleResolutionKind.NodeNext,\n\t\t\ttarget: ts.ScriptTarget.ESNext,\n\t\t\tskipLibCheck: true\n\t\t};\n\t}\n\n\t/**\n\t * Find a type declaration by walking a module import/export graph.\n\t * @param sourceFilePath The source file path to inspect.\n\t * @param typeName The type name to find.\n\t * @param visitedFiles The visited file set.\n\t * @param compilerOptions Compiler options for module resolution.\n\t * @returns The matched declaration with its source file.\n\t * @internal\n\t */\n\tprivate static findTypeDeclarationInModuleGraph(\n\t\tsourceFilePath: string,\n\t\ttypeName: string,\n\t\tvisitedFiles: Set<string>,\n\t\tcompilerOptions: ts.CompilerOptions\n\t):\n\t\t| {\n\t\t\t\tsourceFile: ts.SourceFile;\n\t\t\t\tdeclaration: ts.InterfaceDeclaration | ts.TypeAliasDeclaration;\n\t\t }\n\t\t| undefined {\n\t\tconst absoluteSourcePath = FileUtils.normalizeFilePath(sourceFilePath);\n\t\tif (visitedFiles.has(absoluteSourcePath)) {\n\t\t\treturn undefined;\n\t\t}\n\t\tvisitedFiles.add(absoluteSourcePath);\n\n\t\tconst sourceFile = Resolver.getOrCreateSourceFile(sourceFilePath);\n\t\tif (!sourceFile) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tfor (const statement of sourceFile.statements) {\n\t\t\t// interface IFoo { ... } or type Foo = ... (the declaration we are searching for)\n\t\t\tif (\n\t\t\t\t(ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)) &&\n\t\t\t\tstatement.name.text === typeName\n\t\t\t) {\n\t\t\t\treturn {\n\t\t\t\t\tsourceFile,\n\t\t\t\t\tdeclaration: statement\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tconst moduleSpecifiers = sourceFile.statements\n\t\t\t// import { ... } from \"...\" or export { ... } from \"...\" (re-export and import chains)\n\t\t\t.filter(statement => ts.isExportDeclaration(statement) || ts.isImportDeclaration(statement))\n\t\t\t.flatMap(statement => {\n\t\t\t\tconst moduleSpecifier = statement.moduleSpecifier;\n\t\t\t\t// \"./module.js\" (string literal module specifier)\n\t\t\t\treturn moduleSpecifier && ts.isStringLiteral(moduleSpecifier) ? [moduleSpecifier.text] : [];\n\t\t\t});\n\n\t\tfor (const moduleSpecifier of moduleSpecifiers) {\n\t\t\tconst resolvedImportPath = Resolver.resolveModuleSpecifierFromFile(\n\t\t\t\tmoduleSpecifier,\n\t\t\t\tsourceFilePath,\n\t\t\t\tcompilerOptions\n\t\t\t);\n\t\t\tif (resolvedImportPath) {\n\t\t\t\tconst declarationResult = Resolver.findTypeDeclarationInModuleGraph(\n\t\t\t\t\tresolvedImportPath,\n\t\t\t\t\ttypeName,\n\t\t\t\t\tvisitedFiles,\n\t\t\t\t\tcompilerOptions\n\t\t\t\t);\n\t\t\t\tif (declarationResult) {\n\t\t\t\t\treturn declarationResult;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Read and cache a parsed source file.\n\t * @param sourceFilePath The source file path.\n\t * @returns The parsed source file.\n\t * @internal\n\t */\n\tprivate static getOrCreateSourceFile(sourceFilePath: string): ts.SourceFile | undefined {\n\t\tconst normalizedPath = FileUtils.normalizeFilePath(sourceFilePath);\n\t\tconst cachedSourceFile = Resolver._sourceFileCache[normalizedPath];\n\t\tif (cachedSourceFile !== undefined) {\n\t\t\treturn cachedSourceFile ?? undefined;\n\t\t}\n\n\t\tconst source = FileUtils.readFile(sourceFilePath);\n\t\tif (!source) {\n\t\t\tResolver._sourceFileCache[normalizedPath] = null;\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst sourceFile = ts.createSourceFile(sourceFilePath, source, ts.ScriptTarget.Latest, true);\n\t\tResolver._sourceFileCache[normalizedPath] = sourceFile;\n\t\treturn sourceFile;\n\t}\n\n\t/**\n\t * Resolve a module specifier from a containing file.\n\t * @param moduleSpecifier The module specifier text.\n\t * @param containingFilePath The file containing the import/export.\n\t * @param compilerOptions Compiler options for module resolution.\n\t * @returns The resolved file path.\n\t * @internal\n\t */\n\tprivate static resolveModuleSpecifierFromFile(\n\t\tmoduleSpecifier: string,\n\t\tcontainingFilePath: string,\n\t\tcompilerOptions: ts.CompilerOptions\n\t): string | undefined {\n\t\treturn ts.resolveModuleName(moduleSpecifier, containingFilePath, compilerOptions, ts.sys)\n\t\t\t.resolvedModule?.resolvedFileName;\n\t}\n}\n"]}
@@ -0,0 +1,94 @@
1
+ // Copyright 2026 IOTA Stiftung.
2
+ // SPDX-License-Identifier: Apache-2.0.
3
+ import { Is } from "@twin.org/core";
4
+ import * as ts from "typescript";
5
+ /**
6
+ * Builds regex patterns for TypeScript template literal types.
7
+ */
8
+ export class TemplateLiteralPatternBuilder {
9
+ /**
10
+ * Build a regular-expression pattern for a template literal type.
11
+ * @param typeNode The template literal type node.
12
+ * @returns The regex pattern, or undefined if a safe pattern cannot be derived.
13
+ */
14
+ static buildTemplateLiteralPattern(typeNode) {
15
+ // The head is the fixed prefix text before the first interpolation, e.g. "prefix-" in
16
+ // `prefix-${T}-suffix`
17
+ let templatePattern = `^${TemplateLiteralPatternBuilder.escapeRegexPattern(typeNode.head.text)}`;
18
+ // Each span is one ${T} interpolation plus the literal text that follows it
19
+ for (const templateSpan of typeNode.templateSpans) {
20
+ const spanPattern = TemplateLiteralPatternBuilder.buildTemplateLiteralSpanPattern(templateSpan.type);
21
+ if (!spanPattern) {
22
+ return undefined;
23
+ }
24
+ templatePattern += `${spanPattern}${TemplateLiteralPatternBuilder.escapeRegexPattern(templateSpan.literal.text)}`;
25
+ }
26
+ return `${templatePattern}$`;
27
+ }
28
+ /**
29
+ * Build a regex fragment for a template literal placeholder type.
30
+ * @param typeNode The placeholder type node.
31
+ * @returns The regex fragment, or undefined if unsupported.
32
+ * @internal
33
+ */
34
+ static buildTemplateLiteralSpanPattern(typeNode) {
35
+ // string (string keyword lets any content through)
36
+ if (typeNode.kind === ts.SyntaxKind.StringKeyword) {
37
+ return ".*";
38
+ }
39
+ // MyAlias (plain type reference with no type arguments is treated as open string)
40
+ if (ts.isTypeReferenceNode(typeNode) &&
41
+ ts.isIdentifier(typeNode.typeName) &&
42
+ (typeNode.typeArguments?.length ?? 0) === 0) {
43
+ return ".*";
44
+ }
45
+ // number (number keyword matches optional sign, digits, and optional decimal)
46
+ if (typeNode.kind === ts.SyntaxKind.NumberKeyword) {
47
+ return "-?(?:0|[1-9]\\d*)(?:\\.\\d+)?";
48
+ }
49
+ // boolean (boolean keyword matches the two literal words)
50
+ if (typeNode.kind === ts.SyntaxKind.BooleanKeyword) {
51
+ return "(?:true|false)";
52
+ }
53
+ // "hello" or 42 or true or false (literal type node)
54
+ if (ts.isLiteralTypeNode(typeNode)) {
55
+ // "hello" or 42 (string or numeric literal)
56
+ if (ts.isStringLiteral(typeNode.literal) || ts.isNumericLiteral(typeNode.literal)) {
57
+ return TemplateLiteralPatternBuilder.escapeRegexPattern(typeNode.literal.text);
58
+ }
59
+ // true (true keyword literal)
60
+ if (typeNode.literal.kind === ts.SyntaxKind.TrueKeyword) {
61
+ return "true";
62
+ }
63
+ // false (false keyword literal)
64
+ if (typeNode.literal.kind === ts.SyntaxKind.FalseKeyword) {
65
+ return "false";
66
+ }
67
+ }
68
+ // "a" | "b" | "c" (union of literal types)
69
+ if (ts.isUnionTypeNode(typeNode)) {
70
+ const unionPatterns = typeNode.types
71
+ .map(unionType => TemplateLiteralPatternBuilder.buildTemplateLiteralSpanPattern(unionType))
72
+ .filter((pattern) => Is.stringValue(pattern));
73
+ if (unionPatterns.length === typeNode.types.length) {
74
+ return `(?:${unionPatterns.join("|")})`;
75
+ }
76
+ }
77
+ // `prefix-${T}` (nested template literal type, strip outer anchors)
78
+ if (ts.isTemplateLiteralTypeNode(typeNode)) {
79
+ const nestedPattern = TemplateLiteralPatternBuilder.buildTemplateLiteralPattern(typeNode);
80
+ return nestedPattern ? nestedPattern.replace(/^\^/, "").replace(/\$$/, "") : undefined;
81
+ }
82
+ return undefined;
83
+ }
84
+ /**
85
+ * Escape regex metacharacters in a literal text fragment so it matches verbatim.
86
+ * @param value The text to escape.
87
+ * @returns The escaped regex text.
88
+ * @internal
89
+ */
90
+ static escapeRegexPattern(value) {
91
+ return value.replace(/[$()*+.?[\\\]^{|}-]/g, "\\$&");
92
+ }
93
+ }
94
+ //# sourceMappingURL=templateLiteralPatternBuilder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templateLiteralPatternBuilder.js","sourceRoot":"","sources":["../../../src/utils/templateLiteralPatternBuilder.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { Is } from \"@twin.org/core\";\nimport * as ts from \"typescript\";\n\n/**\n * Builds regex patterns for TypeScript template literal types.\n */\nexport class TemplateLiteralPatternBuilder {\n\t/**\n\t * Build a regular-expression pattern for a template literal type.\n\t * @param typeNode The template literal type node.\n\t * @returns The regex pattern, or undefined if a safe pattern cannot be derived.\n\t */\n\tpublic static buildTemplateLiteralPattern(\n\t\ttypeNode: ts.TemplateLiteralTypeNode\n\t): string | undefined {\n\t\t// The head is the fixed prefix text before the first interpolation, e.g. \"prefix-\" in\n\t\t// `prefix-${T}-suffix`\n\t\tlet templatePattern = `^${TemplateLiteralPatternBuilder.escapeRegexPattern(typeNode.head.text)}`;\n\n\t\t// Each span is one ${T} interpolation plus the literal text that follows it\n\t\tfor (const templateSpan of typeNode.templateSpans) {\n\t\t\tconst spanPattern = TemplateLiteralPatternBuilder.buildTemplateLiteralSpanPattern(\n\t\t\t\ttemplateSpan.type\n\t\t\t);\n\t\t\tif (!spanPattern) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\ttemplatePattern += `${spanPattern}${TemplateLiteralPatternBuilder.escapeRegexPattern(templateSpan.literal.text)}`;\n\t\t}\n\n\t\treturn `${templatePattern}$`;\n\t}\n\n\t/**\n\t * Build a regex fragment for a template literal placeholder type.\n\t * @param typeNode The placeholder type node.\n\t * @returns The regex fragment, or undefined if unsupported.\n\t * @internal\n\t */\n\tprivate static buildTemplateLiteralSpanPattern(typeNode: ts.TypeNode): string | undefined {\n\t\t// string (string keyword lets any content through)\n\t\tif (typeNode.kind === ts.SyntaxKind.StringKeyword) {\n\t\t\treturn \".*\";\n\t\t}\n\n\t\t// MyAlias (plain type reference with no type arguments is treated as open string)\n\t\tif (\n\t\t\tts.isTypeReferenceNode(typeNode) &&\n\t\t\tts.isIdentifier(typeNode.typeName) &&\n\t\t\t(typeNode.typeArguments?.length ?? 0) === 0\n\t\t) {\n\t\t\treturn \".*\";\n\t\t}\n\n\t\t// number (number keyword matches optional sign, digits, and optional decimal)\n\t\tif (typeNode.kind === ts.SyntaxKind.NumberKeyword) {\n\t\t\treturn \"-?(?:0|[1-9]\\\\d*)(?:\\\\.\\\\d+)?\";\n\t\t}\n\n\t\t// boolean (boolean keyword matches the two literal words)\n\t\tif (typeNode.kind === ts.SyntaxKind.BooleanKeyword) {\n\t\t\treturn \"(?:true|false)\";\n\t\t}\n\n\t\t// \"hello\" or 42 or true or false (literal type node)\n\t\tif (ts.isLiteralTypeNode(typeNode)) {\n\t\t\t// \"hello\" or 42 (string or numeric literal)\n\t\t\tif (ts.isStringLiteral(typeNode.literal) || ts.isNumericLiteral(typeNode.literal)) {\n\t\t\t\treturn TemplateLiteralPatternBuilder.escapeRegexPattern(typeNode.literal.text);\n\t\t\t}\n\n\t\t\t// true (true keyword literal)\n\t\t\tif (typeNode.literal.kind === ts.SyntaxKind.TrueKeyword) {\n\t\t\t\treturn \"true\";\n\t\t\t}\n\n\t\t\t// false (false keyword literal)\n\t\t\tif (typeNode.literal.kind === ts.SyntaxKind.FalseKeyword) {\n\t\t\t\treturn \"false\";\n\t\t\t}\n\t\t}\n\n\t\t// \"a\" | \"b\" | \"c\" (union of literal types)\n\t\tif (ts.isUnionTypeNode(typeNode)) {\n\t\t\tconst unionPatterns = typeNode.types\n\t\t\t\t.map(unionType => TemplateLiteralPatternBuilder.buildTemplateLiteralSpanPattern(unionType))\n\t\t\t\t.filter((pattern): pattern is string => Is.stringValue(pattern));\n\n\t\t\tif (unionPatterns.length === typeNode.types.length) {\n\t\t\t\treturn `(?:${unionPatterns.join(\"|\")})`;\n\t\t\t}\n\t\t}\n\n\t\t// `prefix-${T}` (nested template literal type, strip outer anchors)\n\t\tif (ts.isTemplateLiteralTypeNode(typeNode)) {\n\t\t\tconst nestedPattern = TemplateLiteralPatternBuilder.buildTemplateLiteralPattern(typeNode);\n\t\t\treturn nestedPattern ? nestedPattern.replace(/^\\^/, \"\").replace(/\\$$/, \"\") : undefined;\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Escape regex metacharacters in a literal text fragment so it matches verbatim.\n\t * @param value The text to escape.\n\t * @returns The escaped regex text.\n\t * @internal\n\t */\n\tprivate static escapeRegexPattern(value: string): string {\n\t\treturn value.replace(/[$()*+.?[\\\\\\]^{|}-]/g, \"\\\\$&\");\n\t}\n}\n"]}
@@ -0,0 +1,319 @@
1
+ // Copyright 2026 IOTA Stiftung.
2
+ // SPDX-License-Identifier: Apache-2.0.
3
+ import { Is, ObjectHelper, StringHelper } from "@twin.org/core";
4
+ import { FileUtils } from "./fileUtils.js";
5
+ import { JsonSchemaBuilder } from "./jsonSchemaBuilder.js";
6
+ import { Resolver } from "./resolver.js";
7
+ import { EmbeddedSchemaMode } from "../models/embeddedSchemaMode.js";
8
+ /**
9
+ * Class for converting TypeScript types to JSON Schema.
10
+ */
11
+ export class TypeScriptToSchema {
12
+ /**
13
+ * Generates a JSON schema from a TypeScript source file or type name.
14
+ * @param namespace The schema namespace.
15
+ * @param packageName The package name.
16
+ * @param schemas The package schema map.
17
+ * @param sourceFileOrTypeName The source file to process or type name to resolve.
18
+ * @param options Additional generation options.
19
+ * @returns The generated JSON schemas indexed by title.
20
+ */
21
+ async generateSchema(namespace, packageName, schemas, sourceFileOrTypeName, options) {
22
+ const suppressPackageWarnings = (options?.suppressPackageWarnings ?? [])
23
+ .filter(packageNameToSkip => Is.stringValue(packageNameToSkip))
24
+ .map(packageNameToSkip => packageNameToSkip.toLowerCase());
25
+ const filteredOptions = {
26
+ ...options,
27
+ onDiagnostic: diagnostic => {
28
+ if (suppressPackageWarnings.some(packageToSkip => this.isDiagnosticFromPackage(diagnostic.path, diagnostic.fileName, packageToSkip))) {
29
+ return;
30
+ }
31
+ options?.onDiagnostic?.(diagnostic);
32
+ }
33
+ };
34
+ const context = {
35
+ namespace,
36
+ packageName,
37
+ schemas,
38
+ options: filteredOptions
39
+ };
40
+ let generatedTitles = [];
41
+ context.schemas[context.packageName] ??= {};
42
+ const visitedFiles = [];
43
+ const sourceFiles = FileUtils.resolveSourceFiles(sourceFileOrTypeName);
44
+ if (sourceFiles.length > 0) {
45
+ for (const sourceFilePath of sourceFiles) {
46
+ const source = FileUtils.readFile(sourceFilePath);
47
+ if (source) {
48
+ const parsedTitles = JsonSchemaBuilder.parseAllObjectSchemas(context, sourceFilePath, source, visitedFiles);
49
+ for (const parsedTitle of parsedTitles) {
50
+ if (!generatedTitles.includes(parsedTitle)) {
51
+ generatedTitles.push(parsedTitle);
52
+ }
53
+ }
54
+ }
55
+ }
56
+ }
57
+ else if (this.isTypeNameInput(sourceFileOrTypeName)) {
58
+ const declarationResult = Resolver.resolveTypeDeclarationAst(context.packageName, sourceFileOrTypeName);
59
+ if (declarationResult) {
60
+ generatedTitles = JsonSchemaBuilder.parseAllObjectSchemas(context, declarationResult.sourceFile.fileName, declarationResult.sourceFile.getFullText(), visitedFiles);
61
+ }
62
+ }
63
+ else {
64
+ return {};
65
+ }
66
+ const generatedSchemas = {};
67
+ for (const generatedTitle of generatedTitles) {
68
+ const generatedSchema = context.schemas[context.packageName][generatedTitle];
69
+ if (generatedSchema) {
70
+ generatedSchemas[generatedTitle] = generatedSchema;
71
+ }
72
+ }
73
+ if (this.isTypeNameInput(sourceFileOrTypeName)) {
74
+ const requestedTitle = StringHelper.stripPrefix(sourceFileOrTypeName);
75
+ const requestedSchema = context.schemas[context.packageName][requestedTitle];
76
+ if (requestedSchema) {
77
+ generatedSchemas[requestedTitle] = requestedSchema;
78
+ }
79
+ }
80
+ for (const [generatedTitle, generatedSchema] of Object.entries(generatedSchemas)) {
81
+ this.inlineEmbeddedSchemas(context, generatedSchema, generatedTitle);
82
+ }
83
+ return generatedSchemas;
84
+ }
85
+ /**
86
+ * Determine if a diagnostic originates from a specific package.
87
+ * @param path The schema or source path associated with the diagnostic.
88
+ * @param fileName The source filename associated with the diagnostic.
89
+ * @param packageName The package name to check, e.g. jose.
90
+ * @returns True if the diagnostic originated from the package.
91
+ */
92
+ isDiagnosticFromPackage(path, fileName, packageName) {
93
+ if (!Is.stringValue(packageName)) {
94
+ return false;
95
+ }
96
+ const sourcePaths = [fileName, path]
97
+ .filter((value) => Is.stringValue(value))
98
+ .map(value => value.replace(/\\/g, "/").toLowerCase());
99
+ const normalisedPackageName = packageName.toLowerCase();
100
+ const packagePath = `/node_modules/${normalisedPackageName}/`;
101
+ const packagePathNoTrailingSlash = `/node_modules/${normalisedPackageName}`;
102
+ return sourcePaths.some(sourcePath => sourcePath.includes(packagePath) || sourcePath.endsWith(packagePathNoTrailingSlash));
103
+ }
104
+ /**
105
+ * Rewrite refs to @json-schema embedded local types into the current schema's $defs.
106
+ * @param context The generation context.
107
+ * @param schema The root schema to rewrite.
108
+ * @param rootTitle The title of the root schema.
109
+ */
110
+ inlineEmbeddedSchemas(context, schema, rootTitle) {
111
+ this.inlineEmbeddedSchemasInNode(context, schema, schema, rootTitle, new Set([rootTitle]));
112
+ }
113
+ /**
114
+ * Traverse a schema tree and move embedded local refs into the root schema's $defs.
115
+ * @param context The generation context.
116
+ * @param rootSchema The root schema document being rewritten.
117
+ * @param currentNode The current schema node.
118
+ * @param rootTitle The title of the root schema.
119
+ * @param ancestry Titles currently being expanded to avoid recursion cycles.
120
+ * @returns True if inline embedding changed this node or a descendant.
121
+ */
122
+ inlineEmbeddedSchemasInNode(context, rootSchema, currentNode, rootTitle, ancestry) {
123
+ let hasInlineReplacement = false;
124
+ if (Is.stringValue(currentNode.$ref)) {
125
+ const refId = currentNode.$ref;
126
+ const embeddedMode = context.embeddedSchemaModes?.[refId];
127
+ const embeddedSchema = this.findSchemaById(context, refId);
128
+ const defsKey = embeddedSchema
129
+ ? this.getEmbeddedDefinitionKey(embeddedSchema, refId)
130
+ : undefined;
131
+ if (embeddedMode &&
132
+ embeddedSchema &&
133
+ defsKey &&
134
+ defsKey !== rootTitle &&
135
+ !ancestry.has(defsKey)) {
136
+ const embeddedClone = this.createEmbeddedSchemaClone(context, embeddedSchema, embeddedMode);
137
+ this.inlineEmbeddedSchemasInNode(context, rootSchema, embeddedClone, rootTitle, new Set([...ancestry, defsKey]));
138
+ if (embeddedMode === EmbeddedSchemaMode.Defs) {
139
+ rootSchema.$defs ??= {};
140
+ rootSchema.$defs[defsKey] ??= embeddedClone;
141
+ currentNode.$ref = `#/$defs/${defsKey}`;
142
+ }
143
+ else {
144
+ this.replaceSchemaNode(currentNode, embeddedClone);
145
+ hasInlineReplacement = true;
146
+ }
147
+ }
148
+ }
149
+ for (const value of Object.values(currentNode)) {
150
+ if (Is.array(value)) {
151
+ for (const item of value) {
152
+ if (Is.object(item)) {
153
+ hasInlineReplacement =
154
+ this.inlineEmbeddedSchemasInNode(context, rootSchema, item, rootTitle, ancestry) ||
155
+ hasInlineReplacement;
156
+ }
157
+ }
158
+ }
159
+ else if (Is.object(value)) {
160
+ for (const nestedValue of Object.values(value)) {
161
+ if (Is.object(nestedValue)) {
162
+ hasInlineReplacement =
163
+ this.inlineEmbeddedSchemasInNode(context, rootSchema, nestedValue, rootTitle, ancestry) || hasInlineReplacement;
164
+ }
165
+ }
166
+ }
167
+ }
168
+ if (hasInlineReplacement) {
169
+ this.flattenInlineAllOfBranches(currentNode);
170
+ }
171
+ return hasInlineReplacement;
172
+ }
173
+ /**
174
+ * Flatten inline-expanded object allOf branches into the containing schema.
175
+ * @param schema The schema to flatten.
176
+ */
177
+ flattenInlineAllOfBranches(schema) {
178
+ if (!Is.array(schema.allOf) || schema.allOf.length === 0) {
179
+ return;
180
+ }
181
+ const remainingBranches = [];
182
+ const mergedProperties = {};
183
+ const mergedRequired = new Set((Is.array(schema.required) ? schema.required : []).filter((requiredKey) => Is.stringValue(requiredKey)));
184
+ let hasMergedBranch = false;
185
+ for (const branch of schema.allOf) {
186
+ if (this.canFlattenInlineAllOfBranch(branch)) {
187
+ hasMergedBranch = true;
188
+ schema.type ??= "object";
189
+ if (Is.object(branch.properties)) {
190
+ Object.assign(mergedProperties, branch.properties);
191
+ }
192
+ if (Is.array(branch.required)) {
193
+ for (const requiredKey of branch.required) {
194
+ if (Is.stringValue(requiredKey)) {
195
+ mergedRequired.add(requiredKey);
196
+ }
197
+ }
198
+ }
199
+ }
200
+ else {
201
+ remainingBranches.push(branch);
202
+ }
203
+ }
204
+ if (!hasMergedBranch) {
205
+ return;
206
+ }
207
+ if (Object.keys(mergedProperties).length > 0) {
208
+ schema.properties = Object.assign(schema.properties ?? {}, mergedProperties);
209
+ }
210
+ if (mergedRequired.size > 0) {
211
+ schema.required = [...mergedRequired];
212
+ }
213
+ if (remainingBranches.length > 0) {
214
+ schema.allOf = remainingBranches;
215
+ }
216
+ else {
217
+ delete schema.allOf;
218
+ }
219
+ }
220
+ /**
221
+ * Determine whether an allOf branch can be flattened into its parent after inline embedding.
222
+ * @param branch The allOf branch.
223
+ * @returns True if the branch is a plain object schema.
224
+ */
225
+ canFlattenInlineAllOfBranch(branch) {
226
+ return Boolean(!branch.$ref &&
227
+ !branch.allOf &&
228
+ !branch.anyOf &&
229
+ !branch.oneOf &&
230
+ !branch.items &&
231
+ (branch.type === "object" || Is.object(branch.properties) || Is.array(branch.required)));
232
+ }
233
+ /**
234
+ * Create a clone of an embedded schema suitable for defs or inline expansion.
235
+ * @param context The generation context.
236
+ * @param embeddedSchema The source schema.
237
+ * @param embeddedMode The embedding mode.
238
+ * @returns The cloned schema.
239
+ */
240
+ createEmbeddedSchemaClone(context, embeddedSchema, embeddedMode) {
241
+ let embeddedClone = ObjectHelper.clone(embeddedSchema);
242
+ if (embeddedMode === EmbeddedSchemaMode.Inline) {
243
+ embeddedClone = JsonSchemaBuilder.expandAllOfReferences(context, embeddedClone);
244
+ this.removeSchemaComments(embeddedClone);
245
+ }
246
+ delete embeddedClone.$schema;
247
+ delete embeddedClone.$id;
248
+ if (embeddedMode === EmbeddedSchemaMode.Inline) {
249
+ delete embeddedClone.title;
250
+ }
251
+ return embeddedClone;
252
+ }
253
+ /**
254
+ * Replace the contents of a schema node while keeping the same object reference.
255
+ * @param target The schema node to mutate.
256
+ * @param replacement The replacement content.
257
+ */
258
+ replaceSchemaNode(target, replacement) {
259
+ for (const key of Object.keys(target)) {
260
+ delete target[key];
261
+ }
262
+ Object.assign(target, replacement);
263
+ }
264
+ /**
265
+ * Find a generated schema by its canonical id across all loaded packages.
266
+ * @param context The generation context.
267
+ * @param schemaId The schema id to locate.
268
+ * @returns The schema when found.
269
+ */
270
+ findSchemaById(context, schemaId) {
271
+ return Object.values(context.schemas)
272
+ .flatMap(packageSchemas => Object.values(packageSchemas))
273
+ .find(schema => schema.$id === schemaId);
274
+ }
275
+ /**
276
+ * Resolve the local $defs key to use for an embedded schema.
277
+ * @param schema The embedded schema.
278
+ * @param schemaId The canonical schema id.
279
+ * @returns The local definition key.
280
+ */
281
+ getEmbeddedDefinitionKey(schema, schemaId) {
282
+ return schema.title ?? schemaId.split("/").pop() ?? schemaId;
283
+ }
284
+ /**
285
+ * Remove $comment fields from a schema tree.
286
+ * @param schema The schema tree to clean.
287
+ */
288
+ removeSchemaComments(schema) {
289
+ delete schema.$comment;
290
+ for (const value of Object.values(schema)) {
291
+ if (Is.array(value)) {
292
+ for (const item of value) {
293
+ if (Is.object(item)) {
294
+ this.removeSchemaComments(item);
295
+ }
296
+ }
297
+ }
298
+ else if (Is.object(value)) {
299
+ for (const nestedValue of Object.values(value)) {
300
+ if (Is.object(nestedValue)) {
301
+ this.removeSchemaComments(nestedValue);
302
+ }
303
+ }
304
+ }
305
+ }
306
+ }
307
+ /**
308
+ * Determine whether an input value is a valid TypeScript type identifier.
309
+ * An identifier must start with a letter, underscore, or dollar sign and contain only
310
+ * alphanumerics, underscores, or dollar signs thereafter.
311
+ * @param value The value to inspect.
312
+ * @returns True if the value looks like a type name.
313
+ * @internal
314
+ */
315
+ isTypeNameInput(value) {
316
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(value);
317
+ }
318
+ }
319
+ //# sourceMappingURL=typeScriptToSchema.js.map