@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
@@ -10,16 +10,179 @@
10
10
  * Workflow: Transform Svelte to TypeScript via svelte2tsx, parse the transformed
11
11
  * TypeScript with the TS Compiler API, extract component-level JSDoc from original source.
12
12
  *
13
+ * **Svelte 5 only**: The svelte2tsx output format changed significantly between versions.
14
+ * This module requires Svelte 5+ and will throw a clear error if an older version is detected.
15
+ * There is no Svelte 4 compatibility layer.
16
+ *
13
17
  * All functions are prefixed with `svelte_` for clarity.
18
+ *
19
+ * @module
14
20
  */
15
21
 
16
22
  import ts from 'typescript';
17
- import {readFileSync} from 'node:fs';
18
23
  import {svelte2tsx} from 'svelte2tsx';
24
+ import {TraceMap, originalPositionFor} from '@jridgewell/trace-mapping';
25
+ import {VERSION} from 'svelte/compiler';
19
26
  import type {DeclarationJson, ComponentPropInfo} from '@fuzdev/fuz_util/source_json.js';
20
27
 
21
28
  import {tsdoc_parse, tsdoc_apply_to_declaration} from './tsdoc_helpers.js';
22
- import {module_get_component_name} from './module_helpers.js';
29
+ import {ts_extract_module_comment} from './ts_helpers.js';
30
+ import {
31
+ type SourceFileInfo,
32
+ type ModuleSourceOptions,
33
+ module_get_component_name,
34
+ module_extract_dependencies,
35
+ } from './module_helpers.js';
36
+ import type {AnalysisContext} from './analysis_context.js';
37
+ // Import shared types from library_analysis (type-only import avoids circular runtime dependency)
38
+ import type {ModuleAnalysis} from './library_analysis.js';
39
+
40
+ /** Guard to ensure version check runs only once. */
41
+ let svelte_version_checked = false;
42
+
43
+ /**
44
+ * Assert Svelte 5+ is installed (lazy, runs once on first use).
45
+ * Throws a clear error message if an older version is detected.
46
+ */
47
+ const svelte_assert_version = (): void => {
48
+ if (svelte_version_checked) return;
49
+ svelte_version_checked = true;
50
+ const [major] = VERSION.split('.');
51
+ if (parseInt(major!, 10) < 5) {
52
+ throw new Error(
53
+ `Svelte ${VERSION} detected but Svelte 5+ is required for source analysis. ` +
54
+ `The svelte2tsx output format changed significantly between versions.`,
55
+ );
56
+ }
57
+ };
58
+
59
+ /** Result of analyzing a Svelte file. */
60
+ export interface SvelteFileAnalysis {
61
+ /** The component declaration metadata. */
62
+ declaration: DeclarationJson;
63
+ /** Module-level documentation comment, if present. */
64
+ module_comment?: string;
65
+ }
66
+
67
+ /**
68
+ * Analyze a Svelte component file and extract module metadata.
69
+ *
70
+ * Wraps `svelte_analyze_file` and adds dependency information
71
+ * from the source file info if available.
72
+ *
73
+ * This is a high-level function suitable for building documentation or library metadata.
74
+ * For lower-level analysis, use `svelte_analyze_file` directly.
75
+ *
76
+ * Returns raw analysis data matching `ModuleAnalysis` structure.
77
+ * Consumer decides filtering policy (Svelte components are never nodocs).
78
+ *
79
+ * @param source_file The source file info (from Gro filer, file system, or other source)
80
+ * @param module_path The module path (relative to source root)
81
+ * @param checker TypeScript type checker
82
+ * @param options Module source options for path extraction
83
+ * @param ctx Analysis context for collecting diagnostics
84
+ * @returns Module analysis matching ModuleAnalysis structure
85
+ */
86
+ export const svelte_analyze_module = (
87
+ source_file: SourceFileInfo,
88
+ module_path: string,
89
+ checker: ts.TypeChecker,
90
+ options: ModuleSourceOptions,
91
+ ctx: AnalysisContext,
92
+ ): ModuleAnalysis => {
93
+ // Use the existing helper for core analysis
94
+ const {declaration, module_comment} = svelte_analyze_file(source_file, module_path, checker, ctx);
95
+
96
+ // Extract dependencies and dependents if provided
97
+ const {dependencies, dependents} = module_extract_dependencies(source_file, options);
98
+
99
+ return {
100
+ path: module_path,
101
+ module_comment,
102
+ // Wrap declaration in DeclarationAnalysis format (Svelte components are never nodocs)
103
+ declarations: [{declaration, nodocs: false}],
104
+ dependencies,
105
+ dependents,
106
+ star_exports: [],
107
+ re_exports: [],
108
+ };
109
+ };
110
+
111
+ /**
112
+ * Analyze a Svelte component file.
113
+ *
114
+ * This is a high-level function that handles the complete workflow:
115
+ * 1. Transform Svelte source to TypeScript via svelte2tsx
116
+ * 2. Extract component metadata (props, documentation)
117
+ * 3. Extract module-level documentation
118
+ *
119
+ * Suitable for use in documentation generators, build tools, and analysis.
120
+ *
121
+ * @param source_file Source file info with path and content
122
+ * @param module_path Module path relative to source root (e.g., 'Alert.svelte')
123
+ * @param checker TypeScript type checker for type resolution
124
+ * @param ctx Analysis context for collecting diagnostics
125
+ * @returns Component declaration and optional module-level comment
126
+ */
127
+ export const svelte_analyze_file = (
128
+ source_file: SourceFileInfo,
129
+ module_path: string,
130
+ checker: ts.TypeChecker,
131
+ ctx: AnalysisContext,
132
+ ): SvelteFileAnalysis => {
133
+ svelte_assert_version();
134
+ const svelte_source = source_file.content;
135
+
136
+ // Check if component uses TypeScript
137
+ const is_ts_file = svelte_source.includes('lang="ts"');
138
+
139
+ // Transform Svelte to TS
140
+ const ts_result = svelte2tsx(svelte_source, {
141
+ filename: source_file.id,
142
+ isTsFile: is_ts_file,
143
+ emitOnTemplateError: true, // Handle malformed templates gracefully
144
+ });
145
+
146
+ // Create source map for position mapping back to original .svelte file
147
+ let source_map: TraceMap | null = null;
148
+ try {
149
+ // svelte2tsx returns a magic-string SourceMap which is compatible with TraceMap
150
+ // Cast to unknown first since the types don't perfectly align but are compatible
151
+ source_map = new TraceMap(
152
+ ts_result.map as unknown as ConstructorParameters<typeof TraceMap>[0],
153
+ );
154
+ } catch (_error) {
155
+ // If source map parsing fails, diagnostics will use virtual file positions
156
+ }
157
+
158
+ // Get component name from filename
159
+ const component_name = module_get_component_name(module_path);
160
+
161
+ // Create a temporary source file from the original Svelte content for JSDoc extraction
162
+ const temp_source = ts.createSourceFile(
163
+ source_file.id,
164
+ svelte_source,
165
+ ts.ScriptTarget.Latest,
166
+ true,
167
+ );
168
+
169
+ // Analyze the component using the existing lower-level function
170
+ const declaration = svelte_analyze_component(
171
+ ts_result.code,
172
+ temp_source,
173
+ checker,
174
+ component_name,
175
+ module_path,
176
+ source_map,
177
+ ctx,
178
+ );
179
+
180
+ // Extract module-level comment from the script content
181
+ const script_content = svelte_extract_script_content(svelte_source);
182
+ const module_comment = script_content ? svelte_extract_module_comment(script_content) : undefined;
183
+
184
+ return {declaration, module_comment};
185
+ };
23
186
 
24
187
  /**
25
188
  * Analyze a Svelte component from its svelte2tsx transformation.
@@ -29,6 +192,9 @@ export const svelte_analyze_component = (
29
192
  source_file: ts.SourceFile,
30
193
  checker: ts.TypeChecker,
31
194
  component_name: string,
195
+ file_path: string,
196
+ source_map: TraceMap | null,
197
+ ctx: AnalysisContext,
32
198
  ): DeclarationJson => {
33
199
  const result: DeclarationJson = {
34
200
  name: component_name,
@@ -51,7 +217,14 @@ export const svelte_analyze_component = (
51
217
  tsdoc_apply_to_declaration(result, component_tsdoc);
52
218
 
53
219
  // Extract props from svelte2tsx transformed output
54
- const props = svelte_extract_props(virtual_source, checker);
220
+ const props = svelte_extract_props(
221
+ virtual_source,
222
+ checker,
223
+ component_name,
224
+ file_path,
225
+ source_map,
226
+ ctx,
227
+ );
55
228
  if (props.length > 0) {
56
229
  result.props = props;
57
230
  }
@@ -60,14 +233,47 @@ export const svelte_analyze_component = (
60
233
  const start_pos = source_file.getLineAndCharacterOfPosition(0);
61
234
  result.source_line = start_pos.line + 1;
62
235
  } catch (error) {
63
- // If analysis fails, return basic component info
64
- // eslint-disable-next-line no-console
65
- console.error(`Error analyzing Svelte component ${component_name}:`, error);
236
+ throw new Error(`Failed to analyze Svelte component ${component_name}`, {cause: error});
66
237
  }
67
238
 
68
239
  return result;
69
240
  };
70
241
 
242
+ /**
243
+ * Extract the content of the main `<script>` tag from Svelte source.
244
+ *
245
+ * Matches `<script>` or `<script lang="ts">` but not `<script module>`.
246
+ * Returns undefined if no matching script tag is found.
247
+ */
248
+ export const svelte_extract_script_content = (svelte_source: string): string | undefined => {
249
+ // Match <script> or <script lang="ts"> but not <script module>
250
+ // Captures the content between opening and closing tags
251
+ const script_regex = /<script(?:\s+lang=["']ts["'])?(?:\s*)>([^]*?)<\/script>/i;
252
+ const match = script_regex.exec(svelte_source);
253
+ return match?.[1];
254
+ };
255
+
256
+ /**
257
+ * Extract module-level comment from Svelte script content.
258
+ *
259
+ * Requires `@module` tag to identify module comments. The tag line is stripped
260
+ * from the output.
261
+ *
262
+ * @param script_content - The content of the `<script>` tag.
263
+ * @returns The cleaned module comment text, or undefined if none found.
264
+ */
265
+ export const svelte_extract_module_comment = (script_content: string): string | undefined => {
266
+ // Parse the script content as TypeScript and reuse the shared extraction logic
267
+ const source_file = ts.createSourceFile(
268
+ 'script.ts',
269
+ script_content,
270
+ ts.ScriptTarget.Latest,
271
+ true,
272
+ ts.ScriptKind.TS,
273
+ );
274
+ return ts_extract_module_comment(source_file);
275
+ };
276
+
71
277
  /**
72
278
  * Extract component-level TSDoc comment from svelte2tsx transformed output.
73
279
  *
@@ -108,12 +314,16 @@ const svelte_extract_component_tsdoc = (
108
314
  };
109
315
 
110
316
  /**
111
- * Helper to extract prop info from a property signature member.
317
+ * Extract prop info from a property signature member.
112
318
  */
113
319
  const svelte_extract_prop_from_member = (
114
320
  member: ts.PropertySignature,
115
321
  source_file: ts.SourceFile,
116
322
  checker: ts.TypeChecker,
323
+ component_name: string,
324
+ file_path: string,
325
+ source_map: TraceMap | null,
326
+ ctx: AnalysisContext,
117
327
  ): ComponentPropInfo | undefined => {
118
328
  if (!ts.isIdentifier(member.name)) return undefined;
119
329
 
@@ -129,8 +339,32 @@ const svelte_extract_prop_from_member = (
129
339
  try {
130
340
  const prop_type = checker.getTypeAtLocation(member);
131
341
  type_string = checker.typeToString(prop_type);
132
- } catch {
133
- // Fallback to 'any'
342
+ } catch (err) {
343
+ // Fallback to 'any' but report diagnostic with mapped position
344
+ const {line, character} = source_file.getLineAndCharacterOfPosition(member.getStart());
345
+
346
+ // Map virtual position back to original .svelte file if source map available
347
+ let final_line: number | null = line + 1;
348
+ let final_column: number | null = character + 1;
349
+ if (source_map) {
350
+ const original = originalPositionFor(source_map, {line: line + 1, column: character});
351
+ // When line is found, column is guaranteed to be present (same mapping entry)
352
+ if (original.line !== null) {
353
+ final_line = original.line;
354
+ final_column = original.column + 1;
355
+ }
356
+ }
357
+
358
+ ctx.add({
359
+ kind: 'svelte_prop_failed',
360
+ file: file_path,
361
+ line: final_line,
362
+ column: final_column,
363
+ message: `Failed to resolve type for prop "${prop_name}" in ${component_name}, falling back to 'any': ${err instanceof Error ? err.message : String(err)}`,
364
+ severity: 'warning',
365
+ component_name,
366
+ prop_name,
367
+ });
134
368
  }
135
369
  }
136
370
 
@@ -197,12 +431,24 @@ const svelte_extract_props_from_type = (
197
431
  checker: ts.TypeChecker,
198
432
  bindable_props: Set<string>,
199
433
  props: Array<ComponentPropInfo>,
434
+ component_name: string,
435
+ file_path: string,
436
+ source_map: TraceMap | null,
437
+ ctx: AnalysisContext,
200
438
  ): void => {
201
439
  if (ts.isTypeLiteralNode(type_node)) {
202
440
  // Handle direct type literal: { prop1: type1, prop2: type2 }
203
441
  for (const member of type_node.members) {
204
442
  if (ts.isPropertySignature(member)) {
205
- const prop_info = svelte_extract_prop_from_member(member, virtual_source, checker);
443
+ const prop_info = svelte_extract_prop_from_member(
444
+ member,
445
+ virtual_source,
446
+ checker,
447
+ component_name,
448
+ file_path,
449
+ source_map,
450
+ ctx,
451
+ );
206
452
  if (prop_info) {
207
453
  // Mark as bindable if found in bindings
208
454
  if (bindable_props.has(prop_info.name)) {
@@ -215,7 +461,17 @@ const svelte_extract_props_from_type = (
215
461
  } else if (ts.isIntersectionTypeNode(type_node)) {
216
462
  // Handle intersection type: TypeA & TypeB & { prop: type }
217
463
  for (const type_part of type_node.types) {
218
- svelte_extract_props_from_type(type_part, virtual_source, checker, bindable_props, props);
464
+ svelte_extract_props_from_type(
465
+ type_part,
466
+ virtual_source,
467
+ checker,
468
+ bindable_props,
469
+ props,
470
+ component_name,
471
+ file_path,
472
+ source_map,
473
+ ctx,
474
+ );
219
475
  }
220
476
  }
221
477
  // Skip other type references like SvelteHTMLElements['details'] since we can't easily resolve them
@@ -230,6 +486,10 @@ const svelte_extract_props_from_type = (
230
486
  const svelte_extract_props = (
231
487
  virtual_source: ts.SourceFile,
232
488
  checker: ts.TypeChecker,
489
+ component_name: string,
490
+ file_path: string,
491
+ source_map: TraceMap | null,
492
+ ctx: AnalysisContext,
233
493
  ): Array<ComponentPropInfo> => {
234
494
  const props: Array<ComponentPropInfo> = [];
235
495
  const bindable_props = svelte_extract_bindable_props(virtual_source);
@@ -238,13 +498,31 @@ const svelte_extract_props = (
238
498
  ts.forEachChild(virtual_source, (node) => {
239
499
  // Check for type alias ($$ComponentProps)
240
500
  if (ts.isTypeAliasDeclaration(node) && node.name.text === '$$ComponentProps') {
241
- svelte_extract_props_from_type(node.type, virtual_source, checker, bindable_props, props);
501
+ svelte_extract_props_from_type(
502
+ node.type,
503
+ virtual_source,
504
+ checker,
505
+ bindable_props,
506
+ props,
507
+ component_name,
508
+ file_path,
509
+ source_map,
510
+ ctx,
511
+ );
242
512
  }
243
513
  // Also check for Props interface (fallback/older format)
244
514
  else if (ts.isInterfaceDeclaration(node) && node.name.text === 'Props') {
245
515
  for (const member of node.members) {
246
516
  if (ts.isPropertySignature(member)) {
247
- const prop_info = svelte_extract_prop_from_member(member, virtual_source, checker);
517
+ const prop_info = svelte_extract_prop_from_member(
518
+ member,
519
+ virtual_source,
520
+ checker,
521
+ component_name,
522
+ file_path,
523
+ source_map,
524
+ ctx,
525
+ );
248
526
  if (prop_info) {
249
527
  // Mark as bindable if found in bindings
250
528
  if (bindable_props.has(prop_info.name)) {
@@ -259,45 +537,3 @@ const svelte_extract_props = (
259
537
 
260
538
  return props;
261
539
  };
262
-
263
- /**
264
- * Analyze a Svelte component file from disk.
265
- *
266
- * This is a high-level function that handles the complete workflow:
267
- * 1. Read the Svelte source from disk
268
- * 2. Transform to TypeScript via svelte2tsx
269
- * 3. Extract component metadata (props, documentation)
270
- *
271
- * Suitable for use in documentation generators, build tools, and analysis.
272
- *
273
- * @param file_path Absolute path to the .svelte file
274
- * @param module_path Module path relative to src/lib (e.g., 'Alert.svelte')
275
- * @param checker TypeScript type checker for type resolution
276
- * @returns Complete declaration metadata for the component
277
- */
278
- export const svelte_analyze_file = (
279
- file_path: string,
280
- module_path: string,
281
- checker: ts.TypeChecker,
282
- ): DeclarationJson => {
283
- const svelte_source = readFileSync(file_path, 'utf-8');
284
-
285
- // Check if component uses TypeScript
286
- const is_ts_file = svelte_source.includes('lang="ts"');
287
-
288
- // Transform Svelte to TS
289
- const ts_result = svelte2tsx(svelte_source, {
290
- filename: file_path,
291
- isTsFile: is_ts_file,
292
- emitOnTemplateError: true, // Handle malformed templates gracefully
293
- });
294
-
295
- // Get component name from filename
296
- const component_name = module_get_component_name(module_path);
297
-
298
- // Create a temporary source file from the original Svelte content for JSDoc extraction
299
- const temp_source = ts.createSourceFile(file_path, svelte_source, ts.ScriptTarget.Latest, true);
300
-
301
- // Analyze the component using the existing lower-level function
302
- return svelte_analyze_component(ts_result.code, temp_source, checker, component_name);
303
- };