@fuzdev/fuz_ui 0.175.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 (58) hide show
  1. package/README.md +1 -1
  2. package/dist/Alert.svelte +2 -0
  3. package/dist/Alert.svelte.d.ts.map +1 -1
  4. package/dist/ApiModule.svelte +5 -6
  5. package/dist/ApiModule.svelte.d.ts.map +1 -1
  6. package/dist/DeclarationDetail.svelte +2 -1
  7. package/dist/DeclarationDetail.svelte.d.ts.map +1 -1
  8. package/dist/Details.svelte +2 -0
  9. package/dist/Details.svelte.d.ts.map +1 -1
  10. package/dist/Themed.svelte +2 -0
  11. package/dist/Themed.svelte.d.ts.map +1 -1
  12. package/dist/analysis_context.d.ts +195 -0
  13. package/dist/analysis_context.d.ts.map +1 -0
  14. package/dist/analysis_context.js +134 -0
  15. package/dist/library_analysis.d.ts +112 -0
  16. package/dist/library_analysis.d.ts.map +1 -0
  17. package/dist/library_analysis.js +106 -0
  18. package/dist/library_gen.d.ts +73 -5
  19. package/dist/library_gen.d.ts.map +1 -1
  20. package/dist/library_gen.js +130 -68
  21. package/dist/library_gen_helpers.d.ts +81 -72
  22. package/dist/library_gen_helpers.d.ts.map +1 -1
  23. package/dist/library_gen_helpers.js +115 -156
  24. package/dist/library_gen_output.d.ts +34 -0
  25. package/dist/library_gen_output.d.ts.map +1 -0
  26. package/dist/library_gen_output.js +40 -0
  27. package/dist/mdz.d.ts +3 -0
  28. package/dist/mdz.d.ts.map +1 -1
  29. package/dist/mdz.js +12 -3
  30. package/dist/module_helpers.d.ts +246 -24
  31. package/dist/module_helpers.d.ts.map +1 -1
  32. package/dist/module_helpers.js +250 -42
  33. package/dist/svelte_helpers.d.ts +65 -10
  34. package/dist/svelte_helpers.d.ts.map +1 -1
  35. package/dist/svelte_helpers.js +171 -49
  36. package/dist/ts_helpers.d.ts +132 -61
  37. package/dist/ts_helpers.d.ts.map +1 -1
  38. package/dist/ts_helpers.js +423 -282
  39. package/dist/tsdoc_helpers.d.ts +11 -0
  40. package/dist/tsdoc_helpers.d.ts.map +1 -1
  41. package/dist/tsdoc_helpers.js +22 -47
  42. package/dist/tsdoc_mdz.d.ts +36 -0
  43. package/dist/tsdoc_mdz.d.ts.map +1 -0
  44. package/dist/tsdoc_mdz.js +56 -0
  45. package/dist/vite_plugin_library_well_known.js +5 -5
  46. package/package.json +11 -6
  47. package/src/lib/analysis_context.ts +250 -0
  48. package/src/lib/library_analysis.ts +168 -0
  49. package/src/lib/library_gen.ts +199 -83
  50. package/src/lib/library_gen_helpers.ts +148 -215
  51. package/src/lib/library_gen_output.ts +63 -0
  52. package/src/lib/mdz.ts +13 -4
  53. package/src/lib/module_helpers.ts +392 -47
  54. package/src/lib/svelte_helpers.ts +291 -55
  55. package/src/lib/ts_helpers.ts +538 -338
  56. package/src/lib/tsdoc_helpers.ts +24 -49
  57. package/src/lib/tsdoc_mdz.ts +62 -0
  58. package/src/lib/vite_plugin_library_well_known.ts +5 -5
@@ -35,6 +35,8 @@
35
35
  * - TS API strips URL protocols from `@see` tag text; we use `getText()` to preserve original format including `{@link}` syntax
36
36
  *
37
37
  * All functions are prefixed with `tsdoc_` for clarity.
38
+ *
39
+ * @module
38
40
  */
39
41
  import ts from 'typescript';
40
42
  import type { DeclarationJson } from '@fuzdev/fuz_util/source_json.js';
@@ -95,4 +97,13 @@ export declare const tsdoc_parse: (node: ts.Node, source_file: ts.SourceFile) =>
95
97
  * @mutates declaration - adds doc_comment, deprecated_message, examples, see_also, throws, since fields
96
98
  */
97
99
  export declare const tsdoc_apply_to_declaration: (declaration: DeclarationJson, tsdoc: TsdocParsedComment | undefined) => void;
100
+ /**
101
+ * Clean raw JSDoc comment text by removing comment markers and leading asterisks.
102
+ *
103
+ * Transforms `/** ... *\/` style comments into clean text.
104
+ *
105
+ * @param comment_text The raw comment text including `/**` and `*\/` markers
106
+ * @returns Cleaned comment text, or undefined if empty after cleaning
107
+ */
108
+ export declare const tsdoc_clean_comment: (comment_text: string) => string | undefined;
98
109
  //# sourceMappingURL=tsdoc_helpers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tsdoc_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/tsdoc_helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,EAAE,MAAM,YAAY,CAAC;AAC5B,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,iCAAiC,CAAC;AAErE;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAClC,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mCAAmC;IACnC,MAAM,CAAC,EAAE,KAAK,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAC,CAAC,CAAC;IACrD,oCAAoC;IACpC,QAAQ,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACzB,6CAA6C;IAC7C,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,qCAAqC;IACrC,QAAQ,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACzB,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,OAAO,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACxB,iEAAiE;IACjE,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB;AAiDD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,WAAW,GACvB,MAAM,EAAE,CAAC,IAAI,EACb,aAAa,EAAE,CAAC,UAAU,KACxB,kBAAkB,GAAG,SAyFvB,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,0BAA0B,GACtC,aAAa,eAAe,EAC5B,OAAO,kBAAkB,GAAG,SAAS,KACnC,IAmBF,CAAC"}
1
+ {"version":3,"file":"tsdoc_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/tsdoc_helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,EAAE,MAAM,YAAY,CAAC;AAC5B,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,iCAAiC,CAAC;AAErE;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAClC,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mCAAmC;IACnC,MAAM,CAAC,EAAE,KAAK,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAC,CAAC,CAAC;IACrD,oCAAoC;IACpC,QAAQ,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACzB,6CAA6C;IAC7C,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,qCAAqC;IACrC,QAAQ,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACzB,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,OAAO,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACxB,iEAAiE;IACjE,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,WAAW,GACvB,MAAM,EAAE,CAAC,IAAI,EACb,aAAa,EAAE,CAAC,UAAU,KACxB,kBAAkB,GAAG,SAyFvB,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,0BAA0B,GACtC,aAAa,eAAe,EAC5B,OAAO,kBAAkB,GAAG,SAAS,KACnC,IAmBF,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,GAAI,cAAc,MAAM,KAAG,MAAM,GAAG,SAUnE,CAAC"}
@@ -35,53 +35,10 @@
35
35
  * - TS API strips URL protocols from `@see` tag text; we use `getText()` to preserve original format including `{@link}` syntax
36
36
  *
37
37
  * All functions are prefixed with `tsdoc_` for clarity.
38
- */
39
- import ts from 'typescript';
40
- /**
41
- * Convert TSDoc link syntax to mdz-compatible format.
42
38
  *
43
- * Conversions:
44
- * - `{@link url|text}` → `[text](url)` (markdown link)
45
- * - `{@link https://...}` → `https://...` (bare URL)
46
- * - `{@link identifier}` → `` `identifier` `` (backticks)
47
- * - `@see` variants follow same rules
48
- *
49
- * @param content The @see tag content to convert
39
+ * @module
50
40
  */
51
- const tsdoc_convert_link_to_mdz = (content) => {
52
- // Check for {@link ...} or {@see ...} syntax
53
- const link_match = /^\{@(?:link|see)\s+([^}]+)\}$/.exec(content.trim());
54
- if (link_match) {
55
- const inner = link_match[1].trim();
56
- // Check for pipe separator (custom display text)
57
- const pipe_index = inner.indexOf('|');
58
- if (pipe_index !== -1) {
59
- const reference = inner.slice(0, pipe_index).trim();
60
- const display_text = inner.slice(pipe_index + 1).trim();
61
- // Convert to markdown link: [text](url)
62
- return `[${display_text}](${reference})`;
63
- }
64
- // No pipe - check if it's a URL or declaration
65
- if (inner.startsWith('https://') || inner.startsWith('http://')) {
66
- // Bare URL - return as-is
67
- return inner;
68
- }
69
- else {
70
- // Declaration or module - wrap in backticks
71
- return `\`${inner}\``;
72
- }
73
- }
74
- // No {@link} or {@see} syntax - check if it's a bare URL or declaration
75
- const trimmed = content.trim();
76
- if (trimmed.startsWith('https://') || trimmed.startsWith('http://')) {
77
- // Already a bare URL - return as-is
78
- return trimmed;
79
- }
80
- else {
81
- // Declaration or module - wrap in backticks
82
- return `\`${trimmed}\``;
83
- }
84
- };
41
+ import ts from 'typescript';
85
42
  /**
86
43
  * Parse JSDoc comment from a TypeScript node.
87
44
  *
@@ -160,10 +117,10 @@ export const tsdoc_parse = (node, source_file) => {
160
117
  const see_content = full_tag_text
161
118
  .replace(/^@see\s+/, '') // remove @see prefix
162
119
  .replace(/\n\s*\*\s*/g, ' ') // remove JSDoc line continuations
120
+ .replace(/\s*\*\s*$/, '') // remove trailing asterisk artifacts
163
121
  .trim();
164
122
  if (see_content) {
165
- // Convert TSDoc link syntax to mdz-compatible format
166
- see_also.push(tsdoc_convert_link_to_mdz(see_content));
123
+ see_also.push(see_content);
167
124
  }
168
125
  }
169
126
  else if (tag_name === 'since' && tag_text) {
@@ -219,3 +176,21 @@ export const tsdoc_apply_to_declaration = (declaration, tsdoc) => {
219
176
  declaration.since = tsdoc.since;
220
177
  }
221
178
  };
179
+ /**
180
+ * Clean raw JSDoc comment text by removing comment markers and leading asterisks.
181
+ *
182
+ * Transforms `/** ... *\/` style comments into clean text.
183
+ *
184
+ * @param comment_text The raw comment text including `/**` and `*\/` markers
185
+ * @returns Cleaned comment text, or undefined if empty after cleaning
186
+ */
187
+ export const tsdoc_clean_comment = (comment_text) => {
188
+ const text = comment_text
189
+ .replace(/^\/\*\*/, '')
190
+ .replace(/\*\/$/, '')
191
+ .split('\n')
192
+ .map((line) => line.replace(/^\s*\*\s?/, ''))
193
+ .join('\n')
194
+ .trim();
195
+ return text || undefined;
196
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Bridge between TSDoc format and mdz for rendering.
3
+ *
4
+ * This module converts raw TSDoc syntax (from the analysis library) to mdz format
5
+ * (Fuz's documentation rendering dialect). It lives in fuz_ui, not the extracted
6
+ * analysis library, to keep that library format-agnostic.
7
+ *
8
+ * @module
9
+ */
10
+ /**
11
+ * Convert raw TSDoc `@see` content to mdz format for rendering.
12
+ *
13
+ * Handles TSDoc link syntax:
14
+ * - `{@link url|text}` → `[text](url)` (markdown link)
15
+ * - `{@link https://...}` → `https://...` (bare URL, auto-linked by mdz)
16
+ * - `{@link identifier}` → `` `identifier` `` (code formatting)
17
+ * - Bare URLs → returned as-is
18
+ * - Bare identifiers → wrapped in backticks
19
+ *
20
+ * @param content Raw `@see` tag content in TSDoc format
21
+ * @returns mdz-formatted string ready for `<Mdz>` component
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * tsdoc_see_to_mdz('{@link https://fuz.dev|API Docs}')
26
+ * // → '[API Docs](https://fuz.dev)'
27
+ *
28
+ * tsdoc_see_to_mdz('{@link SomeType}')
29
+ * // → '`SomeType`'
30
+ *
31
+ * tsdoc_see_to_mdz('https://example.com')
32
+ * // → 'https://example.com'
33
+ * ```
34
+ */
35
+ export declare const tsdoc_see_to_mdz: (content: string) => string;
36
+ //# sourceMappingURL=tsdoc_mdz.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tsdoc_mdz.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/tsdoc_mdz.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,gBAAgB,GAAI,SAAS,MAAM,KAAG,MAqBlD,CAAC"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Bridge between TSDoc format and mdz for rendering.
3
+ *
4
+ * This module converts raw TSDoc syntax (from the analysis library) to mdz format
5
+ * (Fuz's documentation rendering dialect). It lives in fuz_ui, not the extracted
6
+ * analysis library, to keep that library format-agnostic.
7
+ *
8
+ * @module
9
+ */
10
+ import { mdz_is_url } from './mdz.js';
11
+ /** Format a reference as mdz: URLs pass through, identifiers get backticks. */
12
+ const format_reference = (ref) => (mdz_is_url(ref) ? ref : `\`${ref}\``);
13
+ /**
14
+ * Convert raw TSDoc `@see` content to mdz format for rendering.
15
+ *
16
+ * Handles TSDoc link syntax:
17
+ * - `{@link url|text}` → `[text](url)` (markdown link)
18
+ * - `{@link https://...}` → `https://...` (bare URL, auto-linked by mdz)
19
+ * - `{@link identifier}` → `` `identifier` `` (code formatting)
20
+ * - Bare URLs → returned as-is
21
+ * - Bare identifiers → wrapped in backticks
22
+ *
23
+ * @param content Raw `@see` tag content in TSDoc format
24
+ * @returns mdz-formatted string ready for `<Mdz>` component
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * tsdoc_see_to_mdz('{@link https://fuz.dev|API Docs}')
29
+ * // → '[API Docs](https://fuz.dev)'
30
+ *
31
+ * tsdoc_see_to_mdz('{@link SomeType}')
32
+ * // → '`SomeType`'
33
+ *
34
+ * tsdoc_see_to_mdz('https://example.com')
35
+ * // → 'https://example.com'
36
+ * ```
37
+ */
38
+ export const tsdoc_see_to_mdz = (content) => {
39
+ const trimmed = content.trim();
40
+ if (!trimmed)
41
+ return '';
42
+ // Check for {@link ...} or {@see ...} syntax
43
+ const link_match = /^\{@(?:link|see)\s+([^}]+)\}$/.exec(trimmed);
44
+ if (link_match) {
45
+ const inner = link_match[1].trim();
46
+ // Check for pipe separator (custom display text)
47
+ const pipe_index = inner.indexOf('|');
48
+ if (pipe_index !== -1) {
49
+ const reference = inner.slice(0, pipe_index).trim();
50
+ const display_text = inner.slice(pipe_index + 1).trim();
51
+ return `[${display_text}](${reference})`;
52
+ }
53
+ return format_reference(inner);
54
+ }
55
+ return format_reference(trimmed);
56
+ };
@@ -51,19 +51,19 @@ export const vite_plugin_library_well_known = (options = {}) => {
51
51
  try {
52
52
  json_content = await readFile(resolved_path, 'utf-8');
53
53
  }
54
- catch (err) {
54
+ catch (error) {
55
55
  throw new Error(`vite_plugin_library_well_known: failed to read library.json from "${library_path}"\n` +
56
56
  `Resolved to: ${resolved_path}\n` +
57
57
  `Make sure you've run \`gro gen\` to generate the library metadata.\n` +
58
- `Error: ${err}`);
58
+ `Error: ${error}`);
59
59
  }
60
60
  let raw;
61
61
  try {
62
62
  raw = JSON.parse(json_content);
63
63
  }
64
- catch (err) {
64
+ catch (error) {
65
65
  throw new Error(`vite_plugin_library_well_known: failed to parse library.json from "${library_path}"\n` +
66
- `Error: ${err}`);
66
+ `Error: ${error}`);
67
67
  }
68
68
  // Basic structure validation (file is generated by library_gen)
69
69
  if (raw == null || typeof raw !== 'object') {
@@ -107,7 +107,7 @@ export const vite_plugin_library_well_known = (options = {}) => {
107
107
  throw new Error('not initialized');
108
108
  await ready_promise;
109
109
  }
110
- catch {
110
+ catch (_error) {
111
111
  res.statusCode = 503;
112
112
  respond_json(res, JSON.stringify({ error: 'Library not ready' }));
113
113
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_ui",
3
- "version": "0.175.0",
3
+ "version": "0.176.0",
4
4
  "description": "Svelte UI library",
5
5
  "motto": "friendly user zystem",
6
6
  "glyph": "🧶",
@@ -37,6 +37,7 @@
37
37
  "@fuzdev/fuz_code": ">=0.37.0",
38
38
  "@fuzdev/fuz_css": ">=0.40.0",
39
39
  "@fuzdev/fuz_util": ">=0.42.0",
40
+ "@jridgewell/trace-mapping": "^0.3",
40
41
  "@ryanatkn/gro": ">=0.183.0",
41
42
  "@sveltejs/kit": "^2.47.3",
42
43
  "esm-env": "^1",
@@ -48,6 +49,9 @@
48
49
  "@fuzdev/fuz_code": {
49
50
  "optional": true
50
51
  },
52
+ "@jridgewell/trace-mapping": {
53
+ "optional": true
54
+ },
51
55
  "@ryanatkn/gro": {
52
56
  "optional": true
53
57
  },
@@ -60,6 +64,7 @@
60
64
  "@fuzdev/fuz_code": "^0.38.0",
61
65
  "@fuzdev/fuz_css": "^0.42.1",
62
66
  "@fuzdev/fuz_util": "^0.45.1",
67
+ "@jridgewell/trace-mapping": "^0.3.31",
63
68
  "@ryanatkn/eslint-config": "^0.9.0",
64
69
  "@ryanatkn/gro": "^0.183.0",
65
70
  "@sveltejs/adapter-static": "^3.0.10",
@@ -71,11 +76,11 @@
71
76
  "eslint-plugin-svelte": "^3.13.1",
72
77
  "esm-env": "^1.2.2",
73
78
  "jsdom": "^27.2.0",
74
- "prettier": "^3.6.2",
75
- "prettier-plugin-svelte": "^3.4.0",
76
- "svelte": "^5.45.6",
77
- "svelte-check": "^4.3.4",
78
- "svelte2tsx": "^0.7.45",
79
+ "prettier": "^3.7.4",
80
+ "prettier-plugin-svelte": "^3.4.1",
81
+ "svelte": "^5.46.1",
82
+ "svelte-check": "^4.3.5",
83
+ "svelte2tsx": "^0.7.46",
79
84
  "tslib": "^2.8.1",
80
85
  "typescript": "^5.9.3",
81
86
  "typescript-eslint": "^8.48.1",
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Diagnostic collection for source analysis.
3
+ *
4
+ * Provides structured error/warning collection during TypeScript and Svelte
5
+ * analysis, replacing silent catch blocks with actionable diagnostics.
6
+ *
7
+ * ## Error Handling Contract
8
+ *
9
+ * Analysis functions follow a two-tier error model:
10
+ *
11
+ * **Accumulated (non-fatal)** - Collected in AnalysisContext, analysis continues:
12
+ * - Type resolution failures (complex generics, circular refs)
13
+ * - Missing or unparseable JSDoc
14
+ * - Individual member/prop extraction failures
15
+ * - The return value is still valid but may have partial data
16
+ *
17
+ * **Thrown (fatal)** - Analysis cannot continue for this file:
18
+ * - File not found or unreadable
19
+ * - Syntax errors preventing parsing
20
+ * - svelte2tsx transformation failures
21
+ * - Svelte version incompatibility
22
+ *
23
+ * ## Usage Pattern
24
+ *
25
+ * ```ts
26
+ * const ctx = new AnalysisContext();
27
+ * const results = files.map(f => {
28
+ * try {
29
+ * return library_analyze_module(f, program, options, ctx);
30
+ * } catch (e) {
31
+ * // Fatal error - log and skip this file
32
+ * console.error(`Failed to analyze ${f.id}: ${e}`);
33
+ * return null;
34
+ * }
35
+ * });
36
+ *
37
+ * // Results are valid even with accumulated errors
38
+ * // Check ctx for diagnostics to display to user
39
+ * if (ctx.has_errors()) {
40
+ * for (const err of ctx.errors()) {
41
+ * console.error(format_diagnostic(err));
42
+ * }
43
+ * }
44
+ * ```
45
+ *
46
+ * @example
47
+ * const ctx = new AnalysisContext();
48
+ * // ... analysis functions add diagnostics via ctx.add(...)
49
+ * if (ctx.has_errors()) {
50
+ * for (const err of ctx.errors()) {
51
+ * console.error(`${err.file}:${err.line}: ${err.message}`);
52
+ * }
53
+ * }
54
+ *
55
+ * @module
56
+ */
57
+
58
+ /**
59
+ * Diagnostic severity levels.
60
+ *
61
+ * - `error`: Analysis failed, declaration may be incomplete or missing data
62
+ * - `warning`: Partial success, something seems off but analysis continued
63
+ */
64
+ export type DiagnosticSeverity = 'error' | 'warning';
65
+
66
+ /**
67
+ * Discriminant for diagnostic types.
68
+ */
69
+ export type DiagnosticKind =
70
+ | 'type_extraction_failed'
71
+ | 'signature_analysis_failed'
72
+ | 'class_member_failed'
73
+ | 'svelte_prop_failed'
74
+ | 'module_skipped';
75
+
76
+ /**
77
+ * Base diagnostic fields shared by all diagnostic types.
78
+ */
79
+ export interface BaseDiagnostic {
80
+ kind: DiagnosticKind;
81
+ /** File path relative to project root (display with './' prefix). */
82
+ file: string;
83
+ /** Line number (1-based), or null if location unavailable. */
84
+ line: number | null;
85
+ /** Column number (1-based), or null if location unavailable. */
86
+ column: number | null;
87
+ /** Human-readable description of the issue. */
88
+ message: string;
89
+ severity: DiagnosticSeverity;
90
+ }
91
+
92
+ /**
93
+ * Type extraction failed (e.g., complex or recursive types).
94
+ */
95
+ export interface TypeExtractionDiagnostic extends BaseDiagnostic {
96
+ kind: 'type_extraction_failed';
97
+ /** Name of the symbol whose type couldn't be extracted. */
98
+ symbol_name: string;
99
+ }
100
+
101
+ /**
102
+ * Function/method signature analysis failed.
103
+ */
104
+ export interface SignatureAnalysisDiagnostic extends BaseDiagnostic {
105
+ kind: 'signature_analysis_failed';
106
+ /** Name of the function or method. */
107
+ function_name: string;
108
+ }
109
+
110
+ /**
111
+ * Class member analysis failed.
112
+ */
113
+ export interface ClassMemberDiagnostic extends BaseDiagnostic {
114
+ kind: 'class_member_failed';
115
+ /** Name of the class. */
116
+ class_name: string;
117
+ /** Name of the member that failed. */
118
+ member_name: string;
119
+ }
120
+
121
+ /**
122
+ * Svelte prop type resolution failed.
123
+ */
124
+ export interface SveltePropDiagnostic extends BaseDiagnostic {
125
+ kind: 'svelte_prop_failed';
126
+ /** Name of the component. */
127
+ component_name: string;
128
+ /** Name of the prop. */
129
+ prop_name: string;
130
+ }
131
+
132
+ /**
133
+ * Module was skipped during analysis.
134
+ * Could be due to missing source file in program or no analyzer available.
135
+ */
136
+ export interface ModuleSkippedDiagnostic extends BaseDiagnostic {
137
+ kind: 'module_skipped';
138
+ /** Reason the module was skipped. */
139
+ reason: 'not_in_program' | 'no_analyzer';
140
+ }
141
+
142
+ /**
143
+ * Union of all diagnostic types.
144
+ */
145
+ export type Diagnostic =
146
+ | TypeExtractionDiagnostic
147
+ | SignatureAnalysisDiagnostic
148
+ | ClassMemberDiagnostic
149
+ | SveltePropDiagnostic
150
+ | ModuleSkippedDiagnostic;
151
+
152
+ /**
153
+ * Context for collecting diagnostics during source analysis.
154
+ *
155
+ * Thread an instance through analysis functions to collect errors and warnings
156
+ * without halting analysis. After analysis completes, check `has_errors()` and
157
+ * report collected diagnostics.
158
+ *
159
+ * @example
160
+ * const ctx = new AnalysisContext();
161
+ * ts_analyze_module_exports(source_file, checker, options, ctx);
162
+ * if (ctx.has_errors()) {
163
+ * console.error('Analysis completed with errors:');
164
+ * for (const d of ctx.errors()) {
165
+ * console.error(format_diagnostic(d));
166
+ * }
167
+ * }
168
+ */
169
+ export class AnalysisContext {
170
+ readonly diagnostics: Array<Diagnostic> = [];
171
+
172
+ /**
173
+ * Add a diagnostic to the collection.
174
+ */
175
+ add(diagnostic: Diagnostic): void {
176
+ this.diagnostics.push(diagnostic);
177
+ }
178
+
179
+ /**
180
+ * Check if any errors were collected.
181
+ */
182
+ has_errors(): boolean {
183
+ return this.diagnostics.some((d) => d.severity === 'error');
184
+ }
185
+
186
+ /**
187
+ * Check if any warnings were collected.
188
+ */
189
+ has_warnings(): boolean {
190
+ return this.diagnostics.some((d) => d.severity === 'warning');
191
+ }
192
+
193
+ /**
194
+ * Get all error diagnostics.
195
+ */
196
+ errors(): Array<Diagnostic> {
197
+ return this.diagnostics.filter((d) => d.severity === 'error');
198
+ }
199
+
200
+ /**
201
+ * Get all warning diagnostics.
202
+ */
203
+ warnings(): Array<Diagnostic> {
204
+ return this.diagnostics.filter((d) => d.severity === 'warning');
205
+ }
206
+
207
+ /**
208
+ * Get diagnostics of a specific kind.
209
+ */
210
+ by_kind<K extends DiagnosticKind>(kind: K): Array<Extract<Diagnostic, {kind: K}>> {
211
+ return this.diagnostics.filter((d) => d.kind === kind) as Array<Extract<Diagnostic, {kind: K}>>;
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Options for formatting diagnostics.
217
+ */
218
+ export interface FormatDiagnosticOptions {
219
+ /** Prefix for file path (default: './'). */
220
+ prefix?: string;
221
+ /** Base path to strip from absolute file paths (e.g., process.cwd()). */
222
+ strip_base?: string;
223
+ }
224
+
225
+ /**
226
+ * Format a diagnostic for display.
227
+ *
228
+ * @param diagnostic The diagnostic to format
229
+ * @param options Formatting options
230
+ * @returns Formatted string like './file.ts:10:5: error: message'
231
+ */
232
+ export const format_diagnostic = (
233
+ diagnostic: Diagnostic,
234
+ options?: FormatDiagnosticOptions,
235
+ ): string => {
236
+ const prefix = options?.prefix ?? './';
237
+ const strip_base = options?.strip_base;
238
+
239
+ let file = diagnostic.file;
240
+ if (strip_base && file.startsWith(strip_base)) {
241
+ file = file.slice(strip_base.length);
242
+ // Remove leading slash if present
243
+ if (file.startsWith('/')) file = file.slice(1);
244
+ }
245
+
246
+ const {line, column, severity, message} = diagnostic;
247
+ const location = line !== null ? (column !== null ? `${line}:${column}` : `${line}`) : '';
248
+ const file_part = location ? `${prefix}${file}:${location}` : `${prefix}${file}`;
249
+ return `${file_part}: ${severity}: ${message}`;
250
+ };