@fuzdev/fuz_ui 0.175.0 → 0.176.1

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 (58) hide show
  1. package/README.md +1 -1
  2. package/dist/Alert.svelte +2 -0
  3. package/dist/Alert.svelte.d.ts.map +1 -1
  4. package/dist/ApiModule.svelte +5 -6
  5. package/dist/ApiModule.svelte.d.ts.map +1 -1
  6. package/dist/DeclarationDetail.svelte +2 -1
  7. package/dist/DeclarationDetail.svelte.d.ts.map +1 -1
  8. package/dist/Details.svelte +2 -0
  9. package/dist/Details.svelte.d.ts.map +1 -1
  10. package/dist/Themed.svelte +2 -0
  11. package/dist/Themed.svelte.d.ts.map +1 -1
  12. package/dist/analysis_context.d.ts +195 -0
  13. package/dist/analysis_context.d.ts.map +1 -0
  14. package/dist/analysis_context.js +134 -0
  15. package/dist/library_analysis.d.ts +112 -0
  16. package/dist/library_analysis.d.ts.map +1 -0
  17. package/dist/library_analysis.js +106 -0
  18. package/dist/library_gen.d.ts +88 -5
  19. package/dist/library_gen.d.ts.map +1 -1
  20. package/dist/library_gen.js +163 -69
  21. package/dist/library_gen_helpers.d.ts +81 -72
  22. package/dist/library_gen_helpers.d.ts.map +1 -1
  23. package/dist/library_gen_helpers.js +115 -156
  24. package/dist/library_gen_output.d.ts +34 -0
  25. package/dist/library_gen_output.d.ts.map +1 -0
  26. package/dist/library_gen_output.js +40 -0
  27. package/dist/mdz.d.ts +3 -0
  28. package/dist/mdz.d.ts.map +1 -1
  29. package/dist/mdz.js +12 -3
  30. package/dist/module_helpers.d.ts +246 -24
  31. package/dist/module_helpers.d.ts.map +1 -1
  32. package/dist/module_helpers.js +250 -42
  33. package/dist/svelte_helpers.d.ts +65 -10
  34. package/dist/svelte_helpers.d.ts.map +1 -1
  35. package/dist/svelte_helpers.js +171 -49
  36. package/dist/ts_helpers.d.ts +132 -61
  37. package/dist/ts_helpers.d.ts.map +1 -1
  38. package/dist/ts_helpers.js +423 -282
  39. package/dist/tsdoc_helpers.d.ts +11 -0
  40. package/dist/tsdoc_helpers.d.ts.map +1 -1
  41. package/dist/tsdoc_helpers.js +22 -47
  42. package/dist/tsdoc_mdz.d.ts +36 -0
  43. package/dist/tsdoc_mdz.d.ts.map +1 -0
  44. package/dist/tsdoc_mdz.js +56 -0
  45. package/dist/vite_plugin_library_well_known.js +5 -5
  46. package/package.json +11 -6
  47. package/src/lib/analysis_context.ts +250 -0
  48. package/src/lib/library_analysis.ts +168 -0
  49. package/src/lib/library_gen.ts +247 -84
  50. package/src/lib/library_gen_helpers.ts +148 -215
  51. package/src/lib/library_gen_output.ts +63 -0
  52. package/src/lib/mdz.ts +13 -4
  53. package/src/lib/module_helpers.ts +392 -47
  54. package/src/lib/svelte_helpers.ts +291 -55
  55. package/src/lib/ts_helpers.ts +538 -338
  56. package/src/lib/tsdoc_helpers.ts +24 -49
  57. package/src/lib/tsdoc_mdz.ts +62 -0
  58. package/src/lib/vite_plugin_library_well_known.ts +5 -5
@@ -2,49 +2,288 @@
2
2
  * TypeScript compiler API helpers for extracting metadata from source code.
3
3
  *
4
4
  * All functions are prefixed with `ts_` for clarity.
5
+ *
6
+ * @module
5
7
  */
6
8
  import ts from 'typescript';
7
- import { tsdoc_parse, tsdoc_apply_to_declaration } from './tsdoc_helpers.js';
8
- import { module_extract_path, module_matches_source } from './module_helpers.js';
9
- const ts_parse_generic_param = (param) => {
9
+ import { tsdoc_parse, tsdoc_apply_to_declaration, tsdoc_clean_comment } from './tsdoc_helpers.js';
10
+ import { module_extract_dependencies, module_extract_path, module_is_source, } from './module_helpers.js';
11
+ /**
12
+ * Create TypeScript program for analysis.
13
+ *
14
+ * @param options Configuration options for program creation
15
+ * @param log Optional logger for info messages
16
+ * @returns The program and type checker
17
+ * @throws Error if tsconfig.json is not found
18
+ */
19
+ export const ts_create_program = (options, log) => {
20
+ const root = options?.root ?? './';
21
+ const tsconfig_name = options?.tsconfig ?? 'tsconfig.json';
22
+ const config_path = ts.findConfigFile(root, ts.sys.fileExists, tsconfig_name);
23
+ if (!config_path) {
24
+ throw new Error(`No ${tsconfig_name} found in ${root}`);
25
+ }
26
+ log?.info(`using ${config_path}`);
27
+ const config_file = ts.readConfigFile(config_path, ts.sys.readFile);
28
+ const parsed_config = ts.parseJsonConfigFileContent(config_file.config, ts.sys, root);
29
+ // Merge compiler options if provided
30
+ const compiler_options = options?.compiler_options
31
+ ? { ...parsed_config.options, ...options.compiler_options }
32
+ : parsed_config.options;
33
+ const program = ts.createProgram(parsed_config.fileNames, compiler_options);
34
+ return { program, checker: program.getTypeChecker() };
35
+ };
36
+ /**
37
+ * Analyze a TypeScript file and extract module metadata.
38
+ *
39
+ * Wraps `ts_analyze_module_exports` and adds dependency information
40
+ * from the source file info if available.
41
+ *
42
+ * This is a high-level function suitable for building documentation or library metadata.
43
+ * For lower-level analysis, use `ts_analyze_module_exports` directly.
44
+ *
45
+ * @param source_file_info The source file info (from Gro filer, file system, or other source)
46
+ * @param ts_source_file TypeScript source file from the program
47
+ * @param module_path The module path (relative to source root)
48
+ * @param checker TypeScript type checker
49
+ * @param options Module source options for path extraction
50
+ * @param ctx Analysis context for collecting diagnostics
51
+ * @returns Module metadata and re-export information
52
+ */
53
+ export const ts_analyze_module = (source_file_info, ts_source_file, module_path, checker, options, ctx) => {
54
+ // Use the mid-level helper for core analysis
55
+ const { module_comment, declarations, re_exports, star_exports } = ts_analyze_module_exports(ts_source_file, checker, options, ctx);
56
+ // Extract dependencies and dependents if provided
57
+ const { dependencies, dependents } = module_extract_dependencies(source_file_info, options);
58
+ return {
59
+ path: module_path,
60
+ module_comment,
61
+ declarations,
62
+ dependencies,
63
+ dependents,
64
+ star_exports,
65
+ re_exports,
66
+ };
67
+ };
68
+ /**
69
+ * Analyze all exports from a TypeScript source file.
70
+ *
71
+ * Extracts the module-level comment and all exported declarations with
72
+ * complete metadata. Handles re-exports by:
73
+ * - Same-name re-exports: tracked in `re_exports` for `also_exported_from` building
74
+ * - Renamed re-exports: included as new declarations with `alias_of` metadata
75
+ * - Star exports (`export * from`): tracked in `star_exports` for namespace-level info
76
+ *
77
+ * This is a mid-level function (above `ts_extract_*`, below `library_gen`)
78
+ * suitable for building documentation, API explorers, or analysis tools.
79
+ * For standard SvelteKit library layouts, use `module_create_source_options(process.cwd())`.
80
+ *
81
+ * @param source_file The TypeScript source file to analyze
82
+ * @param checker The TypeScript type checker
83
+ * @param options Module source options for path extraction in re-exports
84
+ * @param ctx Analysis context for collecting diagnostics
85
+ * @returns Module comment, declarations, re-exports, and star exports
86
+ */
87
+ export const ts_analyze_module_exports = (source_file, checker, options, ctx) => {
88
+ const declarations = [];
89
+ const re_exports = [];
90
+ const star_exports = [];
91
+ // Extract module-level comment
92
+ const module_comment = ts_extract_module_comment(source_file);
93
+ // Extract star exports (export * from './module')
94
+ for (const statement of source_file.statements) {
95
+ if (ts.isExportDeclaration(statement) &&
96
+ !statement.exportClause && // No exportClause means `export *`
97
+ statement.moduleSpecifier &&
98
+ ts.isStringLiteral(statement.moduleSpecifier)) {
99
+ // Use the type checker to resolve the module - it has already resolved all imports
100
+ // during program creation, so this leverages TypeScript's full module resolution
101
+ const module_symbol = checker.getSymbolAtLocation(statement.moduleSpecifier);
102
+ if (module_symbol) {
103
+ // Get the source file from the module symbol's declarations
104
+ const module_decl = module_symbol.valueDeclaration ?? module_symbol.declarations?.[0];
105
+ if (module_decl) {
106
+ const resolved_source = module_decl.getSourceFile();
107
+ const resolved_path = resolved_source.fileName;
108
+ // Only include star exports from source modules (not node_modules)
109
+ if (module_is_source(resolved_path, options)) {
110
+ star_exports.push(module_extract_path(resolved_path, options));
111
+ }
112
+ }
113
+ }
114
+ // If module couldn't be resolved (external package, etc.), skip it
115
+ }
116
+ }
117
+ // Get all exported symbols
118
+ const symbol = checker.getSymbolAtLocation(source_file);
119
+ if (symbol) {
120
+ const exports = checker.getExportsOfModule(symbol);
121
+ for (const export_symbol of exports) {
122
+ // Check if this is an alias (potential re-export) using the Alias flag
123
+ const is_alias = (export_symbol.flags & ts.SymbolFlags.Alias) !== 0;
124
+ if (is_alias) {
125
+ // This might be a re-export - use getAliasedSymbol to find the original
126
+ const aliased_symbol = checker.getAliasedSymbol(export_symbol);
127
+ const aliased_decl = aliased_symbol.valueDeclaration || aliased_symbol.declarations?.[0];
128
+ if (aliased_decl) {
129
+ const original_source = aliased_decl.getSourceFile();
130
+ // Check if this is a CROSS-FILE re-export (original in different file)
131
+ if (original_source.fileName !== source_file.fileName) {
132
+ // Only track if the original is from a source module (not node_modules)
133
+ if (module_is_source(original_source.fileName, options)) {
134
+ const original_module = module_extract_path(original_source.fileName, options);
135
+ const original_name = aliased_symbol.name;
136
+ const is_renamed = export_symbol.name !== original_name;
137
+ if (is_renamed) {
138
+ // Renamed re-export (export {foo as bar}) - create new declaration with alias_of
139
+ const kind = ts_infer_declaration_kind(aliased_symbol, aliased_decl);
140
+ const decl = {
141
+ name: export_symbol.name,
142
+ kind,
143
+ alias_of: { module: original_module, name: original_name },
144
+ };
145
+ // Renamed re-exports aren't nodocs - they're new declarations pointing to the original
146
+ declarations.push({ declaration: decl, nodocs: false });
147
+ }
148
+ else {
149
+ // Same-name re-export - track for also_exported_from, skip from declarations
150
+ re_exports.push({
151
+ name: export_symbol.name,
152
+ original_module,
153
+ });
154
+ }
155
+ continue;
156
+ }
157
+ // Re-export from external module (node_modules) - skip entirely
158
+ continue;
159
+ }
160
+ // Within-file alias (export { x as y }) - fall through to normal analysis
161
+ }
162
+ }
163
+ // Normal export or within-file alias - declared in this file
164
+ const { declaration, nodocs } = ts_analyze_declaration(export_symbol, source_file, checker, ctx);
165
+ // Include all declarations with nodocs flag - consumer decides filtering policy
166
+ declarations.push({ declaration, nodocs });
167
+ }
168
+ }
169
+ return {
170
+ module_comment,
171
+ declarations,
172
+ re_exports,
173
+ star_exports,
174
+ };
175
+ };
176
+ /**
177
+ * Analyze a TypeScript symbol and extract rich metadata.
178
+ *
179
+ * This is a high-level function that combines TSDoc parsing with TypeScript
180
+ * type analysis to produce complete declaration metadata. Suitable for use
181
+ * in documentation generators, IDE integrations, and other tooling.
182
+ *
183
+ * @param symbol The TypeScript symbol to analyze
184
+ * @param source_file The source file containing the symbol
185
+ * @param checker The TypeScript type checker
186
+ * @param ctx Optional analysis context for collecting diagnostics
187
+ * @returns Complete declaration metadata including docs, types, and parameters, plus nodocs flag
188
+ */
189
+ export const ts_analyze_declaration = (symbol, source_file, checker, ctx) => {
190
+ const name = symbol.name;
191
+ const decl_node = symbol.valueDeclaration || symbol.declarations?.[0];
192
+ // Determine kind (fallback to 'variable' if no declaration node)
193
+ const kind = decl_node ? ts_infer_declaration_kind(symbol, decl_node) : 'variable';
10
194
  const result = {
11
- name: param.name.text,
195
+ name,
196
+ kind,
12
197
  };
13
- if (param.constraint) {
14
- result.constraint = param.constraint.getText();
198
+ if (!decl_node) {
199
+ return { declaration: result, nodocs: false };
15
200
  }
16
- if (param.default) {
17
- result.default_type = param.default.getText();
201
+ // Extract TSDoc
202
+ const tsdoc = tsdoc_parse(decl_node, source_file);
203
+ const nodocs = tsdoc?.nodocs ?? false;
204
+ tsdoc_apply_to_declaration(result, tsdoc);
205
+ // Extract source line
206
+ const start = decl_node.getStart(source_file);
207
+ const start_pos = source_file.getLineAndCharacterOfPosition(start);
208
+ result.source_line = start_pos.line + 1;
209
+ // Extract type-specific info
210
+ if (result.kind === 'function') {
211
+ ts_extract_function_info(decl_node, symbol, checker, result, tsdoc, ctx);
18
212
  }
19
- return result;
213
+ else if (result.kind === 'type') {
214
+ ts_extract_type_info(decl_node, symbol, checker, result, ctx);
215
+ }
216
+ else if (result.kind === 'class') {
217
+ ts_extract_class_info(decl_node, symbol, checker, result, ctx);
218
+ }
219
+ else if (result.kind === 'variable') {
220
+ ts_extract_variable_info(decl_node, symbol, checker, result, ctx);
221
+ }
222
+ return { declaration: result, nodocs };
20
223
  };
21
224
  /**
22
- * Extract modifier keywords from a node's modifiers.
225
+ * Extract module-level comment.
226
+ *
227
+ * Requires `@module` tag to identify module comments. The tag line is stripped
228
+ * from the output. Supports optional module renaming: `@module custom-name`.
23
229
  *
24
- * Returns an array of modifier strings like ['public', 'readonly', 'static']
230
+ * @see https://typedoc.org/documents/Tags._module.html
25
231
  */
26
- const ts_extract_modifiers = (modifiers) => {
27
- const modifier_flags = [];
28
- if (!modifiers)
29
- return modifier_flags;
30
- for (const mod of modifiers) {
31
- if (mod.kind === ts.SyntaxKind.PublicKeyword)
32
- modifier_flags.push('public');
33
- else if (mod.kind === ts.SyntaxKind.PrivateKeyword)
34
- modifier_flags.push('private');
35
- else if (mod.kind === ts.SyntaxKind.ProtectedKeyword)
36
- modifier_flags.push('protected');
37
- else if (mod.kind === ts.SyntaxKind.ReadonlyKeyword)
38
- modifier_flags.push('readonly');
39
- else if (mod.kind === ts.SyntaxKind.StaticKeyword)
40
- modifier_flags.push('static');
41
- else if (mod.kind === ts.SyntaxKind.AbstractKeyword)
42
- modifier_flags.push('abstract');
232
+ export const ts_extract_module_comment = (source_file) => {
233
+ const full_text = source_file.getFullText();
234
+ // Collect all JSDoc comments in the file
235
+ const all_comments = [];
236
+ // Check for comments at the start of the file (before any statements)
237
+ const leading_comments = ts.getLeadingCommentRanges(full_text, 0);
238
+ if (leading_comments?.length) {
239
+ all_comments.push(...leading_comments);
43
240
  }
44
- return modifier_flags;
241
+ // Check for comments before each statement
242
+ for (const statement of source_file.statements) {
243
+ const comments = ts.getLeadingCommentRanges(full_text, statement.getFullStart());
244
+ if (comments?.length) {
245
+ all_comments.push(...comments);
246
+ }
247
+ }
248
+ // Find the first comment with `@module` tag
249
+ for (const comment of all_comments) {
250
+ const comment_text = full_text.substring(comment.pos, comment.end);
251
+ if (!comment_text.trimStart().startsWith('/**'))
252
+ continue;
253
+ // Clean the comment first, then check for tag at start of line
254
+ const cleaned = tsdoc_clean_comment(comment_text);
255
+ if (!cleaned)
256
+ continue;
257
+ // Check for `@module` as a proper tag (at start of line, not mentioned in prose)
258
+ if (/(?:^|\n)@module\b/.test(cleaned)) {
259
+ const stripped = tsdoc_strip_module_tag(cleaned);
260
+ return stripped || undefined;
261
+ }
262
+ }
263
+ return undefined;
264
+ };
265
+ /**
266
+ * Strip `@module` tag line from comment text.
267
+ *
268
+ * Handles formats:
269
+ * - `@module` (standalone)
270
+ * - `@module module-name` (with rename)
271
+ */
272
+ const tsdoc_strip_module_tag = (text) => {
273
+ // Remove lines that START with `@module` (not mentioned in prose)
274
+ const lines = text.split('\n');
275
+ const filtered = lines.filter((line) => !/^\s*@module\b/.test(line));
276
+ return filtered.join('\n').trim();
45
277
  };
46
278
  /**
47
279
  * Infer declaration kind from symbol and node.
280
+ *
281
+ * Maps TypeScript constructs to `DeclarationKind`:
282
+ * - Classes → `'class'`
283
+ * - Functions (declarations, expressions, arrows) → `'function'`
284
+ * - Interfaces, type aliases → `'type'`
285
+ * - Enums (regular and const) → `'type'`
286
+ * - Variables → `'variable'` (unless function-valued → `'function'`)
48
287
  */
49
288
  export const ts_infer_declaration_kind = (symbol, node) => {
50
289
  // Check symbol flags
@@ -56,6 +295,11 @@ export const ts_infer_declaration_kind = (symbol, node) => {
56
295
  return 'type';
57
296
  if (symbol.flags & ts.SymbolFlags.TypeAlias)
58
297
  return 'type';
298
+ // Enums are treated as types (they define a named type with values)
299
+ if (symbol.flags & ts.SymbolFlags.Enum)
300
+ return 'type';
301
+ if (symbol.flags & ts.SymbolFlags.ConstEnum)
302
+ return 'type';
59
303
  // Check node kind
60
304
  if (ts.isFunctionDeclaration(node) || ts.isArrowFunction(node) || ts.isFunctionExpression(node))
61
305
  return 'function';
@@ -63,6 +307,8 @@ export const ts_infer_declaration_kind = (symbol, node) => {
63
307
  return 'class';
64
308
  if (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node))
65
309
  return 'type';
310
+ if (ts.isEnumDeclaration(node))
311
+ return 'type';
66
312
  if (ts.isVariableDeclaration(node)) {
67
313
  // Check if it's a function-valued variable
68
314
  const init = node.initializer;
@@ -73,13 +319,55 @@ export const ts_infer_declaration_kind = (symbol, node) => {
73
319
  }
74
320
  return 'variable';
75
321
  };
322
+ /**
323
+ * Extract parameters from a TypeScript signature with TSDoc descriptions and default values.
324
+ *
325
+ * Shared helper for extracting parameter information from both standalone functions
326
+ * and class methods/constructors.
327
+ *
328
+ * @param sig The TypeScript signature to extract parameters from
329
+ * @param checker TypeScript type checker for type resolution
330
+ * @param tsdoc_params Map of parameter names to TSDoc descriptions (from tsdoc.params)
331
+ * @returns Array of parameter info objects
332
+ */
333
+ export const ts_extract_signature_parameters = (sig, checker, tsdoc_params) => {
334
+ return sig.parameters.map((param) => {
335
+ const param_decl = param.valueDeclaration;
336
+ // Get type - use declaration location if available, otherwise get declared type
337
+ let type_string = 'unknown';
338
+ if (param_decl) {
339
+ const param_type = checker.getTypeOfSymbolAtLocation(param, param_decl);
340
+ type_string = checker.typeToString(param_type);
341
+ }
342
+ else {
343
+ const param_type = checker.getDeclaredTypeOfSymbol(param);
344
+ type_string = checker.typeToString(param_type);
345
+ }
346
+ // Get TSDoc description for this parameter
347
+ const description = tsdoc_params?.get(param.name);
348
+ // Extract default value from AST
349
+ let default_value;
350
+ if (param_decl && ts.isParameter(param_decl) && param_decl.initializer) {
351
+ default_value = param_decl.initializer.getText();
352
+ }
353
+ const optional = !!(param_decl && ts.isParameter(param_decl) && param_decl.questionToken);
354
+ return {
355
+ name: param.name,
356
+ type: type_string,
357
+ ...(optional && { optional }),
358
+ description,
359
+ default_value,
360
+ };
361
+ });
362
+ };
76
363
  /**
77
364
  * Extract function/method information including parameters
78
365
  * with descriptions and default values.
79
366
  *
367
+ * @internal Use `ts_analyze_declaration` for high-level analysis.
80
368
  * @mutates declaration - adds type_signature, return_type, return_description, throws, since, parameters, generic_params
81
369
  */
82
- export const ts_extract_function_info = (node, symbol, checker, declaration, tsdoc) => {
370
+ export const ts_extract_function_info = (node, symbol, checker, declaration, tsdoc, ctx) => {
83
371
  try {
84
372
  const type = checker.getTypeOfSymbolAtLocation(symbol, node);
85
373
  const signatures = type.getCallSignatures();
@@ -100,29 +388,20 @@ export const ts_extract_function_info = (node, symbol, checker, declaration, tsd
100
388
  declaration.since = tsdoc.since;
101
389
  }
102
390
  // Extract parameters with descriptions and default values
103
- declaration.parameters = sig.parameters.map((param) => {
104
- const param_decl = param.valueDeclaration;
105
- const param_type = checker.getTypeOfSymbolAtLocation(param, param_decl);
106
- // Get TSDoc description for this parameter
107
- const description = tsdoc?.params.get(param.name);
108
- // Extract default value from AST
109
- let default_value;
110
- if (param_decl && ts.isParameter(param_decl) && param_decl.initializer) {
111
- default_value = param_decl.initializer.getText();
112
- }
113
- const optional = !!(param_decl && ts.isParameter(param_decl) && param_decl.questionToken);
114
- return {
115
- name: param.name,
116
- type: checker.typeToString(param_type),
117
- ...(optional && { optional }),
118
- description,
119
- default_value,
120
- };
121
- });
391
+ declaration.parameters = ts_extract_signature_parameters(sig, checker, tsdoc?.params);
122
392
  }
123
393
  }
124
- catch (_err) {
125
- // Ignore: Type checker errors are expected when analyzing incomplete or complex signatures
394
+ catch (err) {
395
+ const loc = ts_get_node_location(node);
396
+ ctx.add({
397
+ kind: 'signature_analysis_failed',
398
+ file: loc.file,
399
+ line: loc.line,
400
+ column: loc.column,
401
+ message: `Failed to analyze signature for "${symbol.name}": ${err instanceof Error ? err.message : String(err)}`,
402
+ severity: 'warning',
403
+ function_name: symbol.name,
404
+ });
126
405
  }
127
406
  // Extract generic type parameters
128
407
  if (ts.isFunctionDeclaration(node) || ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
@@ -134,15 +413,25 @@ export const ts_extract_function_info = (node, symbol, checker, declaration, tsd
134
413
  /**
135
414
  * Extract type/interface information with rich property metadata.
136
415
  *
416
+ * @internal Use `ts_analyze_declaration` for high-level analysis.
137
417
  * @mutates declaration - adds type_signature, generic_params, extends, properties
138
418
  */
139
- export const ts_extract_type_info = (node, _symbol, checker, declaration) => {
419
+ export const ts_extract_type_info = (node, _symbol, checker, declaration, ctx) => {
140
420
  try {
141
421
  const type = checker.getTypeAtLocation(node);
142
422
  declaration.type_signature = checker.typeToString(type);
143
423
  }
144
- catch (_err) {
145
- // Ignore: Type checker may fail on complex or recursive types
424
+ catch (err) {
425
+ const loc = ts_get_node_location(node);
426
+ ctx.add({
427
+ kind: 'type_extraction_failed',
428
+ file: loc.file,
429
+ line: loc.line,
430
+ column: loc.column,
431
+ message: `Failed to extract type for "${declaration.name}": ${err instanceof Error ? err.message : String(err)}`,
432
+ severity: 'warning',
433
+ symbol_name: declaration.name,
434
+ });
146
435
  }
147
436
  if (ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node)) {
148
437
  if (node.typeParameters?.length) {
@@ -186,9 +475,10 @@ export const ts_extract_type_info = (node, _symbol, checker, declaration) => {
186
475
  /**
187
476
  * Extract class information with rich member metadata.
188
477
  *
478
+ * @internal Use `ts_analyze_declaration` for high-level analysis.
189
479
  * @mutates declaration - adds extends, implements, generic_params, members
190
480
  */
191
- export const ts_extract_class_info = (node, _symbol, checker, declaration) => {
481
+ export const ts_extract_class_info = (node, _symbol, checker, declaration, ctx) => {
192
482
  if (!ts.isClassDeclaration(node))
193
483
  return;
194
484
  if (node.heritageClauses) {
@@ -219,13 +509,14 @@ export const ts_extract_class_info = (node, _symbol, checker, declaration) => {
219
509
  // Skip private fields (those starting with #)
220
510
  if (member_name.startsWith('#'))
221
511
  continue;
512
+ const member_kind = is_constructor
513
+ ? 'constructor'
514
+ : ts.isMethodDeclaration(member)
515
+ ? 'function'
516
+ : 'variable';
222
517
  const member_declaration = {
223
518
  name: member_name,
224
- kind: is_constructor
225
- ? 'constructor'
226
- : ts.isMethodDeclaration(member)
227
- ? 'function'
228
- : 'variable',
519
+ kind: member_kind,
229
520
  };
230
521
  // Extract visibility and modifiers
231
522
  const modifier_flags = ts_extract_modifiers(ts.getModifiers(member));
@@ -246,10 +537,13 @@ export const ts_extract_class_info = (node, _symbol, checker, declaration) => {
246
537
  let signatures = [];
247
538
  if (is_constructor) {
248
539
  // For constructors, get construct signatures from the class symbol
249
- const class_symbol = checker.getSymbolAtLocation(node.name);
250
- if (class_symbol) {
251
- const class_type = checker.getTypeOfSymbolAtLocation(class_symbol, node);
252
- signatures = class_type.getConstructSignatures();
540
+ // Skip anonymous classes (no name)
541
+ if (node.name) {
542
+ const class_symbol = checker.getSymbolAtLocation(node.name);
543
+ if (class_symbol) {
544
+ const class_type = checker.getTypeOfSymbolAtLocation(class_symbol, node);
545
+ signatures = class_type.getConstructSignatures();
546
+ }
253
547
  }
254
548
  }
255
549
  else {
@@ -275,27 +569,7 @@ export const ts_extract_class_info = (node, _symbol, checker, declaration) => {
275
569
  }
276
570
  }
277
571
  // Extract parameters with descriptions and default values
278
- member_declaration.parameters = sig.parameters.map((param) => {
279
- const param_decl = param.valueDeclaration;
280
- const param_type = checker.getTypeOfSymbolAtLocation(param, param_decl);
281
- // Get TSDoc description for this parameter
282
- const description = member_tsdoc?.params.get(param.name);
283
- // Extract default value from AST
284
- let default_value;
285
- if (param_decl && ts.isParameter(param_decl) && param_decl.initializer) {
286
- default_value = param_decl.initializer.getText();
287
- }
288
- const optional = !!(param_decl &&
289
- ts.isParameter(param_decl) &&
290
- param_decl.questionToken);
291
- return {
292
- name: param.name,
293
- type: checker.typeToString(param_type),
294
- ...(optional && { optional }),
295
- description,
296
- default_value,
297
- };
298
- });
572
+ member_declaration.parameters = ts_extract_signature_parameters(sig, checker, member_tsdoc?.params);
299
573
  // Extract throws and since from TSDoc (for both methods and constructors)
300
574
  if (member_tsdoc?.throws?.length) {
301
575
  member_declaration.throws = member_tsdoc.throws;
@@ -306,8 +580,19 @@ export const ts_extract_class_info = (node, _symbol, checker, declaration) => {
306
580
  }
307
581
  }
308
582
  }
309
- catch (_err) {
310
- // Ignore: Type checker may fail on complex member signatures
583
+ catch (err) {
584
+ const loc = ts_get_node_location(member);
585
+ const class_name = node.name?.text ?? '<anonymous>';
586
+ ctx.add({
587
+ kind: 'class_member_failed',
588
+ file: loc.file,
589
+ line: loc.line,
590
+ column: loc.column,
591
+ message: `Failed to analyze member "${member_name}" in class "${class_name}": ${err instanceof Error ? err.message : String(err)}`,
592
+ severity: 'warning',
593
+ class_name,
594
+ member_name,
595
+ });
311
596
  }
312
597
  declaration.members.push(member_declaration);
313
598
  }
@@ -316,218 +601,74 @@ export const ts_extract_class_info = (node, _symbol, checker, declaration) => {
316
601
  /**
317
602
  * Extract variable information.
318
603
  *
604
+ * @internal Use `ts_analyze_declaration` for high-level analysis.
319
605
  * @mutates declaration - adds type_signature
320
606
  */
321
- export const ts_extract_variable_info = (node, symbol, checker, declaration) => {
607
+ export const ts_extract_variable_info = (node, symbol, checker, declaration, ctx) => {
322
608
  try {
323
609
  const type = checker.getTypeOfSymbolAtLocation(symbol, node);
324
610
  declaration.type_signature = checker.typeToString(type);
325
611
  }
326
- catch (_err) {
327
- // Ignore: Type checker may fail on complex variable types
328
- }
329
- };
330
- /**
331
- * Analyze a TypeScript symbol and extract rich metadata.
332
- *
333
- * This is a high-level function that combines TSDoc parsing with TypeScript
334
- * type analysis to produce complete declaration metadata. Suitable for use
335
- * in documentation generators, IDE integrations, and other tooling.
336
- *
337
- * @param symbol The TypeScript symbol to analyze
338
- * @param source_file The source file containing the symbol
339
- * @param checker The TypeScript type checker
340
- * @returns Complete declaration metadata including docs, types, and parameters, plus nodocs flag
341
- */
342
- export const ts_analyze_declaration = (symbol, source_file, checker) => {
343
- const name = symbol.name;
344
- const decl_node = symbol.valueDeclaration || symbol.declarations?.[0];
345
- // Determine kind (fallback to 'variable' if no declaration node)
346
- const kind = decl_node ? ts_infer_declaration_kind(symbol, decl_node) : 'variable';
347
- const result = {
348
- name,
349
- kind,
350
- };
351
- if (!decl_node) {
352
- return { declaration: result, nodocs: false };
353
- }
354
- // Extract TSDoc
355
- const tsdoc = tsdoc_parse(decl_node, source_file);
356
- const nodocs = tsdoc?.nodocs ?? false;
357
- tsdoc_apply_to_declaration(result, tsdoc);
358
- // Extract source line
359
- const start = decl_node.getStart(source_file);
360
- const start_pos = source_file.getLineAndCharacterOfPosition(start);
361
- result.source_line = start_pos.line + 1;
362
- // Extract type-specific info
363
- if (result.kind === 'function') {
364
- ts_extract_function_info(decl_node, symbol, checker, result, tsdoc);
365
- }
366
- else if (result.kind === 'type') {
367
- ts_extract_type_info(decl_node, symbol, checker, result);
368
- }
369
- else if (result.kind === 'class') {
370
- ts_extract_class_info(decl_node, symbol, checker, result);
371
- }
372
- else if (result.kind === 'variable') {
373
- ts_extract_variable_info(decl_node, symbol, checker, result);
612
+ catch (err) {
613
+ const loc = ts_get_node_location(node);
614
+ ctx.add({
615
+ kind: 'type_extraction_failed',
616
+ file: loc.file,
617
+ line: loc.line,
618
+ column: loc.column,
619
+ message: `Failed to extract type for variable "${symbol.name}": ${err instanceof Error ? err.message : String(err)}`,
620
+ severity: 'warning',
621
+ symbol_name: symbol.name,
622
+ });
374
623
  }
375
- return { declaration: result, nodocs };
376
624
  };
377
625
  /**
378
- * Analyze all exports from a TypeScript source file.
379
- *
380
- * Extracts the module-level comment and all exported declarations with
381
- * complete metadata. Handles re-exports by:
382
- * - Same-name re-exports: tracked in `re_exports` for `also_exported_from` building
383
- * - Renamed re-exports: included as new declarations with `alias_of` metadata
384
- *
385
- * This is a high-level function suitable for building documentation, API explorers, or analysis tools.
386
- *
387
- * @param source_file The TypeScript source file to analyze
388
- * @param checker The TypeScript type checker
389
- * @returns Module comment, array of analyzed declarations, and re-export information
626
+ * Extract line and column from a TypeScript node.
627
+ * Returns 1-based line and column numbers.
390
628
  */
391
- export const ts_analyze_module_exports = (source_file, checker) => {
392
- const declarations = [];
393
- const re_exports = [];
394
- // Extract module-level comment
395
- const module_comment = ts_extract_module_comment(source_file);
396
- // Get all exported symbols
397
- const symbol = checker.getSymbolAtLocation(source_file);
398
- if (symbol) {
399
- const exports = checker.getExportsOfModule(symbol);
400
- for (const export_symbol of exports) {
401
- // Check if this is an alias (potential re-export) using the Alias flag
402
- const is_alias = (export_symbol.flags & ts.SymbolFlags.Alias) !== 0;
403
- if (is_alias) {
404
- // This might be a re-export - use getAliasedSymbol to find the original
405
- const aliased_symbol = checker.getAliasedSymbol(export_symbol);
406
- const aliased_decl = aliased_symbol.valueDeclaration || aliased_symbol.declarations?.[0];
407
- if (aliased_decl) {
408
- const original_source = aliased_decl.getSourceFile();
409
- // Check if this is a CROSS-FILE re-export (original in different file)
410
- if (original_source.fileName !== source_file.fileName) {
411
- // Only track if the original is from a source module (not node_modules)
412
- if (module_matches_source(original_source.fileName)) {
413
- const original_module = module_extract_path(original_source.fileName);
414
- const original_name = aliased_symbol.name;
415
- const is_renamed = export_symbol.name !== original_name;
416
- if (is_renamed) {
417
- // Renamed re-export (export {foo as bar}) - create new declaration with alias_of
418
- const kind = ts_infer_declaration_kind(aliased_symbol, aliased_decl);
419
- const decl = {
420
- name: export_symbol.name,
421
- kind,
422
- alias_of: { module: original_module, name: original_name },
423
- };
424
- declarations.push(decl);
425
- }
426
- else {
427
- // Same-name re-export - track for also_exported_from, skip from declarations
428
- re_exports.push({
429
- name: export_symbol.name,
430
- original_module,
431
- });
432
- }
433
- continue;
434
- }
435
- // Re-export from external module (node_modules) - skip entirely
436
- continue;
437
- }
438
- // Within-file alias (export { x as y }) - fall through to normal analysis
439
- }
440
- }
441
- // Normal export or within-file alias - declared in this file
442
- const { declaration, nodocs } = ts_analyze_declaration(export_symbol, source_file, checker);
443
- // Skip @nodocs declarations - they're excluded from documentation
444
- if (nodocs)
445
- continue;
446
- declarations.push(declaration);
447
- }
448
- }
629
+ const ts_get_node_location = (node) => {
630
+ const source_file = node.getSourceFile();
631
+ const { line, character } = source_file.getLineAndCharacterOfPosition(node.getStart());
449
632
  return {
450
- module_comment,
451
- declarations,
452
- re_exports,
633
+ file: source_file.fileName,
634
+ line: line + 1, // Convert to 1-based
635
+ column: character + 1, // Convert to 1-based
453
636
  };
454
637
  };
455
- /**
456
- * Extract module-level comment.
457
- *
458
- * Only accepts JSDoc/TSDoc comments (`/** ... *\/`) followed by a blank line to distinguish
459
- * them from identifier-level comments. This prevents accidentally treating function/class
460
- * comments as module comments. Module comments can appear after imports.
461
- */
462
- export const ts_extract_module_comment = (source_file) => {
463
- const full_text = source_file.getFullText();
464
- // Check for comments at the start of the file (before any statements)
465
- const leading_comments = ts.getLeadingCommentRanges(full_text, 0);
466
- if (leading_comments?.length) {
467
- for (const comment of leading_comments) {
468
- const comment_text = full_text.substring(comment.pos, comment.end);
469
- if (!comment_text.trimStart().startsWith('/**'))
470
- continue;
471
- // Check if there's a blank line after this comment
472
- const first_statement = source_file.statements[0];
473
- if (first_statement) {
474
- const between = full_text.substring(comment.end, first_statement.getStart());
475
- if (between.includes('\n\n')) {
476
- return extract_and_clean_jsdoc(full_text, comment);
477
- }
478
- }
479
- else {
480
- // No statements, just return the comment
481
- return extract_and_clean_jsdoc(full_text, comment);
482
- }
483
- }
638
+ const ts_parse_generic_param = (param) => {
639
+ const result = {
640
+ name: param.name.text,
641
+ };
642
+ if (param.constraint) {
643
+ result.constraint = param.constraint.getText();
484
644
  }
485
- // Check for comments before each statement (e.g., after imports)
486
- for (const statement of source_file.statements) {
487
- const statement_start = statement.getFullStart();
488
- const statement_pos = statement.getStart();
489
- // Get comments in the trivia before this statement
490
- const comments = ts.getLeadingCommentRanges(full_text, statement_start);
491
- if (!comments?.length)
492
- continue;
493
- for (const comment of comments) {
494
- const comment_text = full_text.substring(comment.pos, comment.end);
495
- if (!comment_text.trimStart().startsWith('/**'))
496
- continue;
497
- // Check if there's a blank line between comment and statement
498
- const between = full_text.substring(comment.end, statement_pos);
499
- if (between.includes('\n\n')) {
500
- return extract_and_clean_jsdoc(full_text, comment);
501
- }
502
- }
645
+ if (param.default) {
646
+ result.default_type = param.default.getText();
503
647
  }
504
- return undefined;
505
- };
506
- /**
507
- * Extract and clean JSDoc comment text.
508
- */
509
- const extract_and_clean_jsdoc = (full_text, comment) => {
510
- let text = full_text.substring(comment.pos, comment.end);
511
- // Clean comment markers
512
- text = text
513
- .replace(/^\/\*\*/, '')
514
- .replace(/\*\/$/, '')
515
- .split('\n')
516
- .map((line) => line.replace(/^\s*\*\s?/, ''))
517
- .join('\n')
518
- .trim();
519
- return text || undefined;
648
+ return result;
520
649
  };
521
650
  /**
522
- * Create TypeScript program for analysis.
651
+ * Extract modifier keywords from a node's modifiers.
652
+ *
653
+ * Returns an array of modifier strings like `['public', 'readonly', 'static']`.
523
654
  */
524
- export const ts_create_program = (log) => {
525
- const config_path = ts.findConfigFile('./', ts.sys.fileExists, 'tsconfig.json');
526
- if (!config_path) {
527
- log.warn('No tsconfig.json found');
528
- return null;
655
+ const ts_extract_modifiers = (modifiers) => {
656
+ const modifier_flags = [];
657
+ if (!modifiers)
658
+ return modifier_flags;
659
+ for (const mod of modifiers) {
660
+ if (mod.kind === ts.SyntaxKind.PublicKeyword)
661
+ modifier_flags.push('public');
662
+ else if (mod.kind === ts.SyntaxKind.PrivateKeyword)
663
+ modifier_flags.push('private');
664
+ else if (mod.kind === ts.SyntaxKind.ProtectedKeyword)
665
+ modifier_flags.push('protected');
666
+ else if (mod.kind === ts.SyntaxKind.ReadonlyKeyword)
667
+ modifier_flags.push('readonly');
668
+ else if (mod.kind === ts.SyntaxKind.StaticKeyword)
669
+ modifier_flags.push('static');
670
+ else if (mod.kind === ts.SyntaxKind.AbstractKeyword)
671
+ modifier_flags.push('abstract');
529
672
  }
530
- const config_file = ts.readConfigFile(config_path, ts.sys.readFile);
531
- const parsed_config = ts.parseJsonConfigFileContent(config_file.config, ts.sys, './');
532
- return ts.createProgram(parsed_config.fileNames, parsed_config.options);
673
+ return modifier_flags;
533
674
  };