@fuzdev/fuz_ui 0.174.0 → 0.176.0

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