@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.
- package/README.md +1 -1
- package/dist/Alert.svelte +2 -0
- package/dist/Alert.svelte.d.ts.map +1 -1
- package/dist/ApiModule.svelte +5 -6
- package/dist/ApiModule.svelte.d.ts.map +1 -1
- package/dist/DeclarationDetail.svelte +2 -1
- package/dist/DeclarationDetail.svelte.d.ts.map +1 -1
- package/dist/Details.svelte +2 -0
- package/dist/Details.svelte.d.ts.map +1 -1
- package/dist/EcosystemLinks.svelte +3 -3
- package/dist/EcosystemLinks.svelte.d.ts +1 -1
- package/dist/EcosystemLinks.svelte.d.ts.map +1 -1
- package/dist/Themed.svelte +2 -0
- package/dist/Themed.svelte.d.ts.map +1 -1
- package/dist/analysis_context.d.ts +195 -0
- package/dist/analysis_context.d.ts.map +1 -0
- package/dist/analysis_context.js +134 -0
- package/dist/library_analysis.d.ts +112 -0
- package/dist/library_analysis.d.ts.map +1 -0
- package/dist/library_analysis.js +106 -0
- package/dist/library_gen.d.ts +73 -5
- package/dist/library_gen.d.ts.map +1 -1
- package/dist/library_gen.js +130 -68
- package/dist/library_gen_helpers.d.ts +81 -72
- package/dist/library_gen_helpers.d.ts.map +1 -1
- package/dist/library_gen_helpers.js +115 -156
- package/dist/library_gen_output.d.ts +34 -0
- package/dist/library_gen_output.d.ts.map +1 -0
- package/dist/library_gen_output.js +40 -0
- package/dist/mdz.d.ts +3 -0
- package/dist/mdz.d.ts.map +1 -1
- package/dist/mdz.js +12 -3
- package/dist/module_helpers.d.ts +246 -24
- package/dist/module_helpers.d.ts.map +1 -1
- package/dist/module_helpers.js +250 -42
- package/dist/svelte_helpers.d.ts +65 -10
- package/dist/svelte_helpers.d.ts.map +1 -1
- package/dist/svelte_helpers.js +171 -49
- package/dist/ts_helpers.d.ts +132 -61
- package/dist/ts_helpers.d.ts.map +1 -1
- package/dist/ts_helpers.js +423 -282
- package/dist/tsdoc_helpers.d.ts +11 -0
- package/dist/tsdoc_helpers.d.ts.map +1 -1
- package/dist/tsdoc_helpers.js +22 -47
- package/dist/tsdoc_mdz.d.ts +36 -0
- package/dist/tsdoc_mdz.d.ts.map +1 -0
- package/dist/tsdoc_mdz.js +56 -0
- package/dist/vite_plugin_library_well_known.js +5 -5
- package/package.json +12 -7
- package/src/lib/analysis_context.ts +250 -0
- package/src/lib/library_analysis.ts +168 -0
- package/src/lib/library_gen.ts +199 -83
- package/src/lib/library_gen_helpers.ts +148 -215
- package/src/lib/library_gen_output.ts +63 -0
- package/src/lib/mdz.ts +13 -4
- package/src/lib/module_helpers.ts +392 -47
- package/src/lib/svelte_helpers.ts +291 -55
- package/src/lib/ts_helpers.ts +538 -338
- package/src/lib/tsdoc_helpers.ts +24 -49
- package/src/lib/tsdoc_mdz.ts +62 -0
- package/src/lib/vite_plugin_library_well_known.ts +5 -5
package/src/lib/ts_helpers.ts
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
|
263
|
+
return {
|
|
264
|
+
module_comment,
|
|
265
|
+
declarations,
|
|
266
|
+
re_exports,
|
|
267
|
+
star_exports,
|
|
268
|
+
};
|
|
31
269
|
};
|
|
32
270
|
|
|
33
271
|
/**
|
|
34
|
-
*
|
|
272
|
+
* Analyze a TypeScript symbol and extract rich metadata.
|
|
35
273
|
*
|
|
36
|
-
*
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
146
|
-
|
|
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 (
|
|
172
|
-
|
|
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:
|
|
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
|
-
|
|
298
|
-
if (
|
|
299
|
-
const
|
|
300
|
-
|
|
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 =
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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 (
|
|
368
|
-
|
|
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 (
|
|
391
|
-
|
|
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
|
-
*
|
|
804
|
+
* Extract line and column from a TypeScript node.
|
|
805
|
+
* Returns 1-based line and column numbers.
|
|
397
806
|
*/
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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 (
|
|
434
|
-
|
|
822
|
+
if (param.constraint) {
|
|
823
|
+
result.constraint = param.constraint.getText();
|
|
435
824
|
}
|
|
436
825
|
|
|
437
|
-
|
|
438
|
-
|
|
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
|
|
830
|
+
return result;
|
|
459
831
|
};
|
|
460
832
|
|
|
461
833
|
/**
|
|
462
|
-
*
|
|
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
|
-
*
|
|
488
|
-
*
|
|
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
|
|
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
|
|
842
|
+
* Extract modifier keywords from a node's modifiers.
|
|
574
843
|
*
|
|
575
|
-
*
|
|
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
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
-
|
|
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
|
};
|