@fuzdev/mdz 0.1.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 (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -0
  3. package/dist/Mdz.svelte +39 -0
  4. package/dist/Mdz.svelte.d.ts +17 -0
  5. package/dist/Mdz.svelte.d.ts.map +1 -0
  6. package/dist/MdzNodeView.svelte +140 -0
  7. package/dist/MdzNodeView.svelte.d.ts +9 -0
  8. package/dist/MdzNodeView.svelte.d.ts.map +1 -0
  9. package/dist/MdzPrecompiled.svelte +33 -0
  10. package/dist/MdzPrecompiled.svelte.d.ts +17 -0
  11. package/dist/MdzPrecompiled.svelte.d.ts.map +1 -0
  12. package/dist/MdzRoot.svelte +40 -0
  13. package/dist/MdzRoot.svelte.d.ts +14 -0
  14. package/dist/MdzRoot.svelte.d.ts.map +1 -0
  15. package/dist/MdzStream.svelte +37 -0
  16. package/dist/MdzStream.svelte.d.ts +18 -0
  17. package/dist/MdzStream.svelte.d.ts.map +1 -0
  18. package/dist/MdzStreamNodeView.svelte +150 -0
  19. package/dist/MdzStreamNodeView.svelte.d.ts +9 -0
  20. package/dist/MdzStreamNodeView.svelte.d.ts.map +1 -0
  21. package/dist/context_helpers.d.ts +23 -0
  22. package/dist/context_helpers.d.ts.map +1 -0
  23. package/dist/context_helpers.js +19 -0
  24. package/dist/mdz.d.ts +152 -0
  25. package/dist/mdz.d.ts.map +1 -0
  26. package/dist/mdz.js +61 -0
  27. package/dist/mdz_contexts.d.ts +106 -0
  28. package/dist/mdz_contexts.d.ts.map +1 -0
  29. package/dist/mdz_contexts.js +42 -0
  30. package/dist/mdz_helpers.d.ts +321 -0
  31. package/dist/mdz_helpers.d.ts.map +1 -0
  32. package/dist/mdz_helpers.js +668 -0
  33. package/dist/mdz_lexer.d.ts +123 -0
  34. package/dist/mdz_lexer.d.ts.map +1 -0
  35. package/dist/mdz_lexer.js +1405 -0
  36. package/dist/mdz_opcodes.d.ts +193 -0
  37. package/dist/mdz_opcodes.d.ts.map +1 -0
  38. package/dist/mdz_opcodes.js +14 -0
  39. package/dist/mdz_opcodes_to_nodes.d.ts +26 -0
  40. package/dist/mdz_opcodes_to_nodes.d.ts.map +1 -0
  41. package/dist/mdz_opcodes_to_nodes.js +410 -0
  42. package/dist/mdz_stream_parser.d.ts +109 -0
  43. package/dist/mdz_stream_parser.d.ts.map +1 -0
  44. package/dist/mdz_stream_parser.js +699 -0
  45. package/dist/mdz_stream_parser_block.d.ts +99 -0
  46. package/dist/mdz_stream_parser_block.d.ts.map +1 -0
  47. package/dist/mdz_stream_parser_block.js +437 -0
  48. package/dist/mdz_stream_parser_inline.d.ts +98 -0
  49. package/dist/mdz_stream_parser_inline.d.ts.map +1 -0
  50. package/dist/mdz_stream_parser_inline.js +481 -0
  51. package/dist/mdz_stream_parser_link.d.ts +29 -0
  52. package/dist/mdz_stream_parser_link.d.ts.map +1 -0
  53. package/dist/mdz_stream_parser_link.js +227 -0
  54. package/dist/mdz_stream_parser_list.d.ts +106 -0
  55. package/dist/mdz_stream_parser_list.d.ts.map +1 -0
  56. package/dist/mdz_stream_parser_list.js +438 -0
  57. package/dist/mdz_stream_parser_state.d.ts +476 -0
  58. package/dist/mdz_stream_parser_state.d.ts.map +1 -0
  59. package/dist/mdz_stream_parser_state.js +651 -0
  60. package/dist/mdz_stream_parser_text.d.ts +25 -0
  61. package/dist/mdz_stream_parser_text.d.ts.map +1 -0
  62. package/dist/mdz_stream_parser_text.js +67 -0
  63. package/dist/mdz_stream_parser_url.d.ts +76 -0
  64. package/dist/mdz_stream_parser_url.d.ts.map +1 -0
  65. package/dist/mdz_stream_parser_url.js +328 -0
  66. package/dist/mdz_stream_state.svelte.d.ts +53 -0
  67. package/dist/mdz_stream_state.svelte.d.ts.map +1 -0
  68. package/dist/mdz_stream_state.svelte.js +360 -0
  69. package/dist/mdz_to_svelte.d.ts +71 -0
  70. package/dist/mdz_to_svelte.d.ts.map +1 -0
  71. package/dist/mdz_to_svelte.js +127 -0
  72. package/dist/mdz_token_parser.d.ts +20 -0
  73. package/dist/mdz_token_parser.d.ts.map +1 -0
  74. package/dist/mdz_token_parser.js +449 -0
  75. package/dist/svelte_preprocess_mdz.d.ts +80 -0
  76. package/dist/svelte_preprocess_mdz.d.ts.map +1 -0
  77. package/dist/svelte_preprocess_mdz.js +543 -0
  78. package/dist/tsdoc_mdz.d.ts +45 -0
  79. package/dist/tsdoc_mdz.d.ts.map +1 -0
  80. package/dist/tsdoc_mdz.js +88 -0
  81. package/package.json +131 -0
  82. package/src/lib/context_helpers.ts +52 -0
  83. package/src/lib/mdz.ts +190 -0
  84. package/src/lib/mdz_contexts.ts +99 -0
  85. package/src/lib/mdz_helpers.ts +727 -0
  86. package/src/lib/mdz_lexer.ts +1761 -0
  87. package/src/lib/mdz_opcodes.ts +227 -0
  88. package/src/lib/mdz_opcodes_to_nodes.ts +470 -0
  89. package/src/lib/mdz_stream_parser.ts +766 -0
  90. package/src/lib/mdz_stream_parser_block.ts +490 -0
  91. package/src/lib/mdz_stream_parser_inline.ts +527 -0
  92. package/src/lib/mdz_stream_parser_link.ts +283 -0
  93. package/src/lib/mdz_stream_parser_list.ts +525 -0
  94. package/src/lib/mdz_stream_parser_state.ts +957 -0
  95. package/src/lib/mdz_stream_parser_text.ts +95 -0
  96. package/src/lib/mdz_stream_parser_url.ts +386 -0
  97. package/src/lib/mdz_stream_state.svelte.ts +383 -0
  98. package/src/lib/mdz_to_svelte.ts +213 -0
  99. package/src/lib/mdz_token_parser.ts +544 -0
  100. package/src/lib/svelte_preprocess_mdz.ts +777 -0
  101. package/src/lib/tsdoc_mdz.ts +97 -0
@@ -0,0 +1,777 @@
1
+ /**
2
+ * Svelte preprocessor that compiles static `Mdz` content to Svelte markup at build time.
3
+ *
4
+ * Detects `Mdz` components with static string `content` props, parses the mdz content,
5
+ * renders each `MdzNode` to equivalent Svelte markup via `mdz_to_svelte`, and replaces
6
+ * the `Mdz` with `MdzPrecompiled` containing pre-rendered children.
7
+ *
8
+ * Also handles ternary chains (`content={a ? 'x' : b ? 'y' : 'z'}`) where all leaf
9
+ * values are statically resolvable strings, emitting `{#if a}markup_x{:else if b}markup_y{:else}markup_z{/if}`
10
+ * as children of a single `MdzPrecompiled`.
11
+ *
12
+ * Truly dynamic `content` props are left untouched.
13
+ *
14
+ * @module
15
+ */
16
+
17
+ import type {ImportDeclaration, VariableDeclaration} from 'estree';
18
+ import {parse, type PreprocessorGroup, type AST} from 'svelte/compiler';
19
+ import MagicString from 'magic-string';
20
+ import {walk} from 'zimmerframe';
21
+ import {should_exclude_path} from '@fuzdev/fuz_util/path.js';
22
+ import {
23
+ find_attribute,
24
+ extract_static_string,
25
+ try_extract_conditional_chain,
26
+ build_static_bindings,
27
+ resolve_component_names,
28
+ has_identifier_in_tree,
29
+ find_import_insert_position,
30
+ generate_import_lines,
31
+ remove_variable_declaration,
32
+ remove_import_declaration,
33
+ remove_import_specifier,
34
+ handle_preprocess_error,
35
+ type PreprocessImportInfo,
36
+ type ResolvedComponentImport,
37
+ } from '@fuzdev/fuz_util/svelte_preprocess_helpers.js';
38
+
39
+ import {mdz_parse} from './mdz.js';
40
+ import {mdz_to_svelte} from './mdz_to_svelte.js';
41
+
42
+ /**
43
+ * An estree `ImportDeclaration` augmented with Svelte's position data.
44
+ * Svelte's parser adds `start`/`end` to all AST nodes, but the estree
45
+ * types don't declare them.
46
+ */
47
+ type PositionedImportDeclaration = ImportDeclaration & AST.BaseNode;
48
+
49
+ /**
50
+ * Options for `svelte_preprocess_mdz`.
51
+ */
52
+ export interface SveltePreprocessMdzOptions {
53
+ /** File patterns to exclude. */
54
+ exclude?: Array<string | RegExp>;
55
+
56
+ /**
57
+ * Component import mapping for mdz content.
58
+ * Key: component name as used in mdz (e.g., 'Alert').
59
+ * Value: import path (e.g., '$lib/Alert.svelte').
60
+ *
61
+ * If mdz content references a component not in this map,
62
+ * that `Mdz` usage is skipped (left as runtime).
63
+ */
64
+ components?: Record<string, string>;
65
+
66
+ /**
67
+ * Allowed HTML elements in mdz content (e.g., `{aside: true, details: true}`).
68
+ * Values are boolean placeholders for future per-element configuration,
69
+ * mirroring the runtime `MdzElements` registry.
70
+ * If mdz content references an element not in this record,
71
+ * that `Mdz` usage is skipped (left as runtime).
72
+ */
73
+ elements?: Record<string, boolean>;
74
+
75
+ /**
76
+ * Import sources that resolve to the `Mdz` component.
77
+ * Used to verify that `Mdz` in templates refers to mdz's `Mdz.svelte`.
78
+ *
79
+ * @default ['@fuzdev/mdz/Mdz.svelte']
80
+ */
81
+ mdz_component_imports?: Array<string>;
82
+
83
+ /**
84
+ * Import path for the precompiled wrapper component.
85
+ *
86
+ * @default '@fuzdev/mdz/MdzPrecompiled.svelte'
87
+ */
88
+ compiled_component_import?: string;
89
+
90
+ /**
91
+ * Import path for the inline `code` component (receives `reference`).
92
+ * When omitted, inline code compiles to a plain `<code>` element, matching
93
+ * `Mdz`'s runtime default. Set e.g. to `@fuzdev/fuz_ui/DocsLink.svelte` to
94
+ * auto-link backticked identifiers.
95
+ */
96
+ code_component_import?: string;
97
+
98
+ /**
99
+ * Import path for the codeblock component (receives `lang` and `content`).
100
+ * When omitted, code blocks compile to a plain `<pre><code>` element. Set
101
+ * e.g. to `@fuzdev/fuz_code/Code.svelte` for syntax highlighting.
102
+ */
103
+ codeblock_component_import?: string;
104
+
105
+ /**
106
+ * How to handle errors during mdz parsing or rendering.
107
+ *
108
+ * @default 'throw' in CI, 'log' otherwise
109
+ */
110
+ on_error?: 'log' | 'throw';
111
+ }
112
+
113
+ const PRECOMPILED_NAME = 'MdzPrecompiled';
114
+
115
+ /**
116
+ * Creates a Svelte preprocessor that compiles static `Mdz` content at build time.
117
+ *
118
+ * @param options - configuration for component/element resolution and file filtering
119
+ * @returns a Svelte `PreprocessorGroup` for use in `svelte.config.js`
120
+ */
121
+ export const svelte_preprocess_mdz = (
122
+ options: SveltePreprocessMdzOptions = {},
123
+ ): PreprocessorGroup => {
124
+ const {
125
+ exclude = [],
126
+ components = {},
127
+ elements = {},
128
+ mdz_component_imports = ['@fuzdev/mdz/Mdz.svelte'],
129
+ compiled_component_import = '@fuzdev/mdz/MdzPrecompiled.svelte',
130
+ code_component_import,
131
+ codeblock_component_import,
132
+ on_error = process.env.CI === 'true' ? 'throw' : 'log',
133
+ } = options;
134
+
135
+ return {
136
+ name: 'fuz-mdz',
137
+
138
+ markup: ({content, filename}) => {
139
+ if (should_exclude_path(filename, exclude)) {
140
+ return {code: content};
141
+ }
142
+
143
+ // Quick bail: does file mention any known Mdz import source?
144
+ if (!mdz_component_imports.some((source) => content.includes(source))) {
145
+ return {code: content};
146
+ }
147
+
148
+ const ast = parse(content, {filename, modern: true});
149
+
150
+ // Resolve which local names map to the Mdz component
151
+ const mdz_names = resolve_component_names(ast, mdz_component_imports);
152
+ if (mdz_names.size === 0) {
153
+ return {code: content};
154
+ }
155
+
156
+ // Check for MdzPrecompiled name collision
157
+ if (has_name_collision(ast, compiled_component_import)) {
158
+ return {code: content};
159
+ }
160
+
161
+ const s = new MagicString(content);
162
+ const bindings = build_static_bindings(ast);
163
+
164
+ // Find and transform Mdz usages with static content
165
+ const {transformations, total_usages, transformed_usages} = find_mdz_usages(ast, mdz_names, {
166
+ components,
167
+ elements,
168
+ filename,
169
+ source: content,
170
+ bindings,
171
+ on_error,
172
+ code_component_import,
173
+ codeblock_component_import,
174
+ });
175
+
176
+ if (transformations.length === 0) {
177
+ return {code: content};
178
+ }
179
+
180
+ // Apply transformations
181
+ for (const t of transformations) {
182
+ s.overwrite(t.start, t.end, t.replacement);
183
+ }
184
+
185
+ // Remove dead const bindings that were consumed by transformations
186
+ remove_dead_const_bindings(s, ast, transformations, content);
187
+
188
+ // Determine which Mdz imports can be removed
189
+ const removable_imports = find_removable_mdz_imports(
190
+ ast,
191
+ mdz_names,
192
+ total_usages,
193
+ transformed_usages,
194
+ );
195
+
196
+ // Add required imports and remove unused Mdz imports
197
+ manage_imports(
198
+ s,
199
+ ast,
200
+ transformations,
201
+ removable_imports,
202
+ compiled_component_import,
203
+ content,
204
+ );
205
+
206
+ return {
207
+ code: s.toString(),
208
+ map: s.generateMap({hires: true}),
209
+ };
210
+ },
211
+ };
212
+ };
213
+
214
+ interface MdzTransformation {
215
+ start: number;
216
+ end: number;
217
+ replacement: string;
218
+ required_imports: Map<string, PreprocessImportInfo>;
219
+ /** Const binding names that were resolved to produce this transformation. */
220
+ consumed_bindings: Set<string>;
221
+ /** The original AST component node, used to skip during dead code analysis. */
222
+ component_node: AST.Component;
223
+ }
224
+
225
+ interface FindMdzUsagesResult {
226
+ transformations: Array<MdzTransformation>;
227
+ /** Total template usages per `Mdz` local name. */
228
+ total_usages: Map<string, number>;
229
+ /** Successfully transformed usages per `Mdz` local name. */
230
+ transformed_usages: Map<string, number>;
231
+ }
232
+
233
+ interface FindMdzUsagesContext {
234
+ components: Record<string, string>;
235
+ elements: Record<string, boolean>;
236
+ filename: string | undefined;
237
+ source: string;
238
+ bindings: ReadonlyMap<string, string>;
239
+ on_error: 'log' | 'throw';
240
+ code_component_import: string | undefined;
241
+ codeblock_component_import: string | undefined;
242
+ }
243
+
244
+ /**
245
+ * Checks if `MdzPrecompiled` is already imported from a different source.
246
+ * If so, the preprocessor bails to avoid name collisions.
247
+ */
248
+ const has_name_collision = (ast: AST.Root, compiled_component_import: string): boolean => {
249
+ for (const script of [ast.instance, ast.module]) {
250
+ if (!script) continue;
251
+ for (const node of script.content.body) {
252
+ if (node.type !== 'ImportDeclaration') continue;
253
+ const source_path = node.source.value as string;
254
+ for (const spec of node.specifiers) {
255
+ if (spec.local.name === PRECOMPILED_NAME && source_path !== compiled_component_import) {
256
+ return true;
257
+ }
258
+ }
259
+ }
260
+ }
261
+ return false;
262
+ };
263
+
264
+ /**
265
+ * Collects identifiers from an expression that resolved through bindings.
266
+ * Only collects top-level Identifier nodes that appear in the bindings map.
267
+ *
268
+ * TODO: support transitive dead const removal — currently only directly-consumed
269
+ * bindings (identifiers in the expression AST) are tracked. Transitive dependencies
270
+ * (e.g., `const a = 'x'; const b = a; content={b}` — `a` is not removed) are not
271
+ * traced. After removing a dead const, re-check whether identifiers in its
272
+ * initializer became dead too.
273
+ */
274
+ const collect_consumed_bindings = (
275
+ value: AST.Attribute['value'],
276
+ bindings: ReadonlyMap<string, string>,
277
+ ): Set<string> => {
278
+ const consumed: Set<string> = new Set();
279
+ if (value === true || Array.isArray(value)) return consumed;
280
+ const collect_from_expr = (expr: {type: string; [key: string]: any}): void => {
281
+ if (expr.type === 'Identifier' && bindings.has(expr.name)) {
282
+ consumed.add(expr.name);
283
+ } else if (expr.type === 'BinaryExpression' && expr.operator === '+') {
284
+ collect_from_expr(expr.left);
285
+ collect_from_expr(expr.right);
286
+ } else if (expr.type === 'TemplateLiteral') {
287
+ for (const e of expr.expressions) {
288
+ collect_from_expr(e);
289
+ }
290
+ } else if (expr.type === 'ConditionalExpression') {
291
+ collect_from_expr(expr.consequent);
292
+ collect_from_expr(expr.alternate);
293
+ }
294
+ };
295
+ collect_from_expr(value.expression);
296
+ return consumed;
297
+ };
298
+
299
+ /**
300
+ * Walks the AST to find `Mdz` component usages with static `content` props
301
+ * and generates transformations to replace them with `MdzPrecompiled` children.
302
+ */
303
+ const find_mdz_usages = (
304
+ ast: AST.Root,
305
+ mdz_names: Map<string, ResolvedComponentImport>,
306
+ context: FindMdzUsagesContext,
307
+ ): FindMdzUsagesResult => {
308
+ const transformations: Array<MdzTransformation> = [];
309
+ const total_usages: Map<string, number> = new Map();
310
+ const transformed_usages: Map<string, number> = new Map();
311
+
312
+ // zimmerframe types visitors against {type: string}, requiring explicit annotations
313
+ // on the callback parameters for Svelte-specific AST types like AST.Component
314
+ walk(ast.fragment as any, null, {
315
+ Component(node: AST.Component, ctx: {next: () => void}) {
316
+ // Always recurse into children so nested Mdz components are found
317
+ ctx.next();
318
+
319
+ if (!mdz_names.has(node.name)) return;
320
+
321
+ // Track total usages per name
322
+ total_usages.set(node.name, (total_usages.get(node.name) ?? 0) + 1);
323
+
324
+ // Skip if spread attributes present — can't determine content statically
325
+ if (node.attributes.some((attr) => attr.type === 'SpreadAttribute')) return;
326
+
327
+ const content_attr = find_attribute(node, 'content');
328
+ if (!content_attr) return;
329
+
330
+ // Extract optional static base prop for relative path resolution.
331
+ // If base is present but dynamic, skip precompilation entirely —
332
+ // MdzPrecompiled doesn't resolve relative paths at runtime,
333
+ // so precompiling with unresolved relative links would be wrong.
334
+ const base_attr = find_attribute(node, 'base');
335
+ let base: string | undefined;
336
+ if (base_attr) {
337
+ const base_value = extract_static_string(base_attr.value, context.bindings);
338
+ if (base_value === null) return; // dynamic base — fall back to runtime
339
+ base = base_value;
340
+ }
341
+
342
+ // Collect attributes to exclude from precompiled output
343
+ const exclude_attrs: Set<AST.Attribute> = new Set([content_attr]);
344
+ if (base_attr) exclude_attrs.add(base_attr);
345
+
346
+ // Extract static string value
347
+ const content_value = extract_static_string(content_attr.value, context.bindings);
348
+ if (content_value !== null) {
349
+ // Parse mdz content and render to Svelte markup
350
+ let result;
351
+ try {
352
+ const nodes = mdz_parse(content_value);
353
+ result = mdz_to_svelte(nodes, {
354
+ components: context.components,
355
+ elements: context.elements,
356
+ base,
357
+ code_component_import: context.code_component_import,
358
+ codeblock_component_import: context.codeblock_component_import,
359
+ });
360
+ } catch (error) {
361
+ handle_preprocess_error(error, '[fuz-mdz]', context.filename, context.on_error);
362
+ return;
363
+ }
364
+
365
+ // If content has unconfigured tags, skip this usage (fall back to runtime)
366
+ if (result.has_unconfigured_tags) return;
367
+
368
+ const consumed = collect_consumed_bindings(content_attr.value, context.bindings);
369
+ const replacement = build_replacement(node, exclude_attrs, result.markup, context.source);
370
+ transformed_usages.set(node.name, (transformed_usages.get(node.name) ?? 0) + 1);
371
+ transformations.push({
372
+ start: node.start,
373
+ end: node.end,
374
+ replacement,
375
+ required_imports: result.imports,
376
+ consumed_bindings: consumed,
377
+ component_node: node,
378
+ });
379
+ return;
380
+ }
381
+
382
+ // Try conditional chain (handles both simple and nested ternaries)
383
+ const chain = try_extract_conditional_chain(
384
+ content_attr.value,
385
+ context.source,
386
+ context.bindings,
387
+ );
388
+ if (chain === null) return;
389
+
390
+ // Parse and render each branch
391
+ const branch_results: Array<{markup: string; imports: Map<string, PreprocessImportInfo>}> =
392
+ [];
393
+ try {
394
+ for (const branch of chain) {
395
+ const nodes = mdz_parse(branch.value);
396
+ const result = mdz_to_svelte(nodes, {
397
+ components: context.components,
398
+ elements: context.elements,
399
+ base,
400
+ code_component_import: context.code_component_import,
401
+ codeblock_component_import: context.codeblock_component_import,
402
+ });
403
+ if (result.has_unconfigured_tags) return;
404
+ branch_results.push({markup: result.markup, imports: result.imports});
405
+ }
406
+ } catch (error) {
407
+ handle_preprocess_error(error, '[fuz-mdz]', context.filename, context.on_error);
408
+ return;
409
+ }
410
+
411
+ // Build {#if}/{:else if}/{:else} markup
412
+ let children_markup = '';
413
+ for (let i = 0; i < chain.length; i++) {
414
+ const branch = chain[i]!;
415
+ const result = branch_results[i]!;
416
+ if (i === 0) {
417
+ children_markup += `{#if ${branch.test_source}}${result.markup}`;
418
+ } else if (branch.test_source !== null) {
419
+ children_markup += `{:else if ${branch.test_source}}${result.markup}`;
420
+ } else {
421
+ children_markup += `{:else}${result.markup}`;
422
+ }
423
+ }
424
+ children_markup += '{/if}';
425
+
426
+ const replacement = build_replacement(node, exclude_attrs, children_markup, context.source);
427
+
428
+ // Merge imports from all branches
429
+ const merged_imports: Map<string, PreprocessImportInfo> = new Map();
430
+ for (const result of branch_results) {
431
+ for (const [name, info] of result.imports) {
432
+ merged_imports.set(name, info);
433
+ }
434
+ }
435
+
436
+ const consumed = collect_consumed_bindings(content_attr.value, context.bindings);
437
+ transformed_usages.set(node.name, (transformed_usages.get(node.name) ?? 0) + 1);
438
+ transformations.push({
439
+ start: node.start,
440
+ end: node.end,
441
+ replacement,
442
+ required_imports: merged_imports,
443
+ consumed_bindings: consumed,
444
+ component_node: node,
445
+ });
446
+ },
447
+ });
448
+
449
+ return {transformations, total_usages, transformed_usages};
450
+ };
451
+
452
+ /**
453
+ * Removes dead `const` bindings from the instance script that were consumed
454
+ * by transformations and are no longer referenced anywhere else in the file.
455
+ *
456
+ * Only handles single-declarator `const` statements. Skips `const a = 'x', b = 'y'`
457
+ * and module script variables (which could be exported/imported by other files).
458
+ */
459
+ const remove_dead_const_bindings = (
460
+ s: MagicString,
461
+ ast: AST.Root,
462
+ transformations: Array<MdzTransformation>,
463
+ source: string,
464
+ ): void => {
465
+ // Collect all consumed binding names across transformations
466
+ const all_consumed: Set<string> = new Set();
467
+ for (const t of transformations) {
468
+ for (const name of t.consumed_bindings) {
469
+ all_consumed.add(name);
470
+ }
471
+ }
472
+ if (all_consumed.size === 0) return;
473
+
474
+ // Only remove from instance script (module script variables could be exported)
475
+ const instance = ast.instance;
476
+ if (!instance) return;
477
+
478
+ // Build a skip set of transformed component nodes so their attribute
479
+ // expressions (which still contain the old identifiers) don't false-match
480
+ const skip: Set<unknown> = new Set();
481
+ for (const t of transformations) {
482
+ skip.add(t.component_node);
483
+ }
484
+
485
+ for (const name of all_consumed) {
486
+ // Find the VariableDeclaration containing this binding in instance script
487
+ let declaration_node: (VariableDeclaration & {start: number; end: number}) | null = null;
488
+ for (const node of instance.content.body) {
489
+ if (node.type !== 'VariableDeclaration' || node.kind !== 'const') continue;
490
+ for (const decl of node.declarations) {
491
+ if (decl.id.type === 'Identifier' && decl.id.name === name) {
492
+ declaration_node = node as VariableDeclaration & {start: number; end: number};
493
+ break;
494
+ }
495
+ }
496
+ if (declaration_node) break;
497
+ }
498
+
499
+ if (!declaration_node) continue;
500
+
501
+ // Only handle single-declarator statements
502
+ if (declaration_node.declarations.length !== 1) continue;
503
+
504
+ // Check if the identifier is referenced anywhere else
505
+ const id_skip = new Set([...skip, declaration_node]);
506
+
507
+ // Check instance script (excluding the declaration itself)
508
+ if (has_identifier_in_tree(instance.content, name, id_skip)) continue;
509
+
510
+ // Check module script
511
+ if (ast.module?.content && has_identifier_in_tree(ast.module.content, name)) continue;
512
+
513
+ // Check template — skip transformed component nodes whose attribute expressions
514
+ // still contain the old identifier references in the AST
515
+ if (has_identifier_in_tree(ast.fragment, name, id_skip)) continue;
516
+
517
+ remove_variable_declaration(s, declaration_node, source);
518
+ // Add to skip set so chained dead const checks don't find references to this removed node
519
+ skip.add(declaration_node);
520
+ }
521
+ };
522
+
523
+ /**
524
+ * Builds the replacement string for a transformed `Mdz` component.
525
+ *
526
+ * Reconstructs the opening tag as `<MdzPrecompiled` with all attributes except `content`,
527
+ * using source text slicing to preserve exact formatting and dynamic expressions.
528
+ */
529
+ const build_replacement = (
530
+ node: AST.Component,
531
+ exclude_attrs: ReadonlySet<AST.Attribute>,
532
+ children_markup: string,
533
+ source: string,
534
+ ): string => {
535
+ // Collect source ranges of all attributes except excluded ones (content, base when resolved)
536
+ const other_attr_ranges: Array<{start: number; end: number}> = [];
537
+ for (const attr of node.attributes) {
538
+ if (exclude_attrs.has(attr as AST.Attribute)) continue;
539
+ other_attr_ranges.push({start: attr.start, end: attr.end});
540
+ }
541
+
542
+ // Build opening tag with MdzPrecompiled name
543
+ let opening = `<${PRECOMPILED_NAME}`;
544
+ for (const range of other_attr_ranges) {
545
+ opening += ' ' + source.slice(range.start, range.end);
546
+ }
547
+ opening += '>';
548
+
549
+ return `${opening}${children_markup}</${PRECOMPILED_NAME}>`;
550
+ };
551
+
552
+ /** Result of import removability analysis for a single `Mdz` name. */
553
+ interface ImportRemovalAction {
554
+ /** The import declaration node. */
555
+ node: PositionedImportDeclaration;
556
+ /** 'full' removes the entire declaration; 'partial' removes only the `Mdz` specifier. */
557
+ kind: 'full' | 'partial';
558
+ /** For partial removal: the specifier to remove. */
559
+ specifier_to_remove?: ImportDeclaration['specifiers'][number];
560
+ }
561
+
562
+ /**
563
+ * Determines which `Mdz` import declarations can be safely removed or trimmed.
564
+ *
565
+ * An import is removable when:
566
+ * 1. All template usages of that name were successfully transformed.
567
+ * 2. The identifier is not referenced elsewhere in script or template expressions.
568
+ *
569
+ * For multi-specifier imports, only the `Mdz` specifier is removed (partial removal).
570
+ * For single-specifier imports, the entire declaration is removed.
571
+ */
572
+ const find_removable_mdz_imports = (
573
+ ast: AST.Root,
574
+ mdz_names: Map<string, ResolvedComponentImport>,
575
+ total_usages: Map<string, number>,
576
+ transformed_usages: Map<string, number>,
577
+ ): Map<string, ImportRemovalAction> => {
578
+ const removable: Map<string, ImportRemovalAction> = new Map();
579
+
580
+ for (const [name, {import_node, specifier}] of mdz_names) {
581
+ const total = total_usages.get(name) ?? 0;
582
+ const transformed = transformed_usages.get(name) ?? 0;
583
+
584
+ // Only remove if ALL template usages were transformed
585
+ if (total === 0 || transformed < total) continue;
586
+
587
+ // Check if identifier is referenced elsewhere in the AST
588
+ const skip: Set<unknown> = new Set([import_node]);
589
+
590
+ // Check instance script body (excluding the import itself)
591
+ if (ast.instance?.content && has_identifier_in_tree(ast.instance.content, name, skip)) {
592
+ continue;
593
+ }
594
+
595
+ // Check module script body (excluding the import itself)
596
+ if (ast.module?.content && has_identifier_in_tree(ast.module.content, name, skip)) {
597
+ continue;
598
+ }
599
+
600
+ // Check template for expression references (Component.name is a string, not Identifier)
601
+ if (has_identifier_in_tree(ast.fragment, name)) {
602
+ continue;
603
+ }
604
+
605
+ const positioned_node = import_node as PositionedImportDeclaration;
606
+ if (import_node.specifiers.length === 1) {
607
+ removable.set(name, {node: positioned_node, kind: 'full'});
608
+ } else {
609
+ removable.set(name, {
610
+ node: positioned_node,
611
+ kind: 'partial',
612
+ specifier_to_remove: specifier,
613
+ });
614
+ }
615
+ }
616
+
617
+ return removable;
618
+ };
619
+
620
+ /**
621
+ * Manages import additions and removals.
622
+ *
623
+ * Adds the `MdzPrecompiled` import and other required imports (injected code/codeblock
624
+ * components, resolve).
625
+ * Removes `Mdz` import declarations that are no longer referenced.
626
+ *
627
+ * Handles both full removal (single-specifier imports) and partial removal
628
+ * (multi-specifier imports where only the `Mdz` specifier is removed).
629
+ *
630
+ * To avoid MagicString boundary conflicts when the insertion position falls inside
631
+ * a removal range, one removable `Mdz` import is overwritten with the `MdzPrecompiled`
632
+ * import line instead of using separate remove + appendLeft.
633
+ */
634
+ const manage_imports = (
635
+ s: MagicString,
636
+ ast: AST.Root,
637
+ transformations: Array<MdzTransformation>,
638
+ removable_imports: Map<string, ImportRemovalAction>,
639
+ compiled_component_import: string,
640
+ source: string,
641
+ ): void => {
642
+ // Collect all required imports across transformations
643
+ const required: Map<string, PreprocessImportInfo> = new Map();
644
+ for (const t of transformations) {
645
+ for (const [name, info] of t.required_imports) {
646
+ required.set(name, info);
647
+ }
648
+ }
649
+
650
+ // Always need MdzPrecompiled when transformations occur
651
+ required.set(PRECOMPILED_NAME, {path: compiled_component_import, kind: 'default'});
652
+
653
+ const script = ast.instance;
654
+
655
+ // Separate full removals from partial removals
656
+ const full_removals: Set<PositionedImportDeclaration> = new Set();
657
+ const partial_removals: Array<ImportRemovalAction> = [];
658
+ for (const [, action] of removable_imports) {
659
+ if (action.kind === 'full') {
660
+ full_removals.add(action.node);
661
+ } else {
662
+ partial_removals.push(action);
663
+ }
664
+ }
665
+
666
+ if (!script) {
667
+ // No instance script — removable_imports won't apply (imports are in module script if any)
668
+ // Just add all required imports
669
+ const lines = generate_import_lines(required);
670
+ if (ast.module) {
671
+ s.appendLeft(ast.module.end, `\n\n<script lang="ts">\n${lines}\n</script>`);
672
+ } else {
673
+ s.prepend(`<script lang="ts">\n${lines}\n</script>\n\n`);
674
+ }
675
+ // Remove Mdz imports from module script if removable
676
+ for (const node of full_removals) {
677
+ remove_import_declaration(s, node, source);
678
+ }
679
+ // Apply partial removals
680
+ for (const action of partial_removals) {
681
+ remove_import_specifier(s, action.node, action.specifier_to_remove!, source);
682
+ }
683
+ return;
684
+ }
685
+
686
+ // Check existing imports to avoid duplicates — tracks both name AND source path
687
+ const existing: Map<string, string> = new Map();
688
+ for (const node of script.content.body) {
689
+ if (node.type === 'ImportDeclaration') {
690
+ const source_path = node.source.value as string;
691
+ for (const spec of node.specifiers) {
692
+ existing.set(spec.local.name, source_path);
693
+ }
694
+ }
695
+ }
696
+
697
+ const to_add: Map<string, PreprocessImportInfo> = new Map();
698
+ for (const [name, info] of required) {
699
+ const existing_path = existing.get(name);
700
+ if (existing_path === undefined) {
701
+ to_add.set(name, info);
702
+ }
703
+ }
704
+
705
+ // Strategy: if we're both adding MdzPrecompiled and removing an Mdz import,
706
+ // overwrite one full-removable import with the MdzPrecompiled line to avoid
707
+ // MagicString boundary conflicts. Other imports use normal appendLeft.
708
+ let overwrite_target: PositionedImportDeclaration | null = null;
709
+ if (to_add.has(PRECOMPILED_NAME) && full_removals.size > 0) {
710
+ // Pick the first removable import to overwrite
711
+ overwrite_target = full_removals.values().next().value ?? null;
712
+ }
713
+
714
+ // Generate the MdzPrecompiled import line separately if using overwrite
715
+ if (overwrite_target) {
716
+ const node_start = overwrite_target.start;
717
+ const node_end = overwrite_target.end;
718
+
719
+ // Find the start of the line (consume leading whitespace)
720
+ let line_start = node_start;
721
+ while (line_start > 0 && (source[line_start - 1] === '\t' || source[line_start - 1] === ' ')) {
722
+ line_start--;
723
+ }
724
+
725
+ // Extract indentation from the original import line rather than hardcoding a tab.
726
+ // The import node's start is after any leading whitespace, so the indentation is
727
+ // the characters between the line start and the node start.
728
+ const original_indent = source.slice(line_start, node_start);
729
+ const precompiled_line = `${original_indent}import ${PRECOMPILED_NAME} from '${compiled_component_import}';`;
730
+
731
+ s.overwrite(line_start, node_end, precompiled_line);
732
+ to_add.delete(PRECOMPILED_NAME);
733
+ }
734
+
735
+ // When there are partial removals but no full-removal overwrite target,
736
+ // the appendLeft at find_import_insert_position can conflict with the
737
+ // partial removal's overwrite (they share the same node.end boundary).
738
+ // Bundle the new imports into the first partial removal's overwrite instead.
739
+ const insert_pos = find_import_insert_position(script);
740
+ let partial_carrier: ImportRemovalAction | null = null;
741
+ if (to_add.size > 0 && partial_removals.length > 0) {
742
+ for (const action of partial_removals) {
743
+ if (action.node.end === insert_pos) {
744
+ partial_carrier = action;
745
+ break;
746
+ }
747
+ }
748
+ }
749
+
750
+ // Add remaining imports — either bundled with a partial removal or via appendLeft
751
+ let carrier_lines = '';
752
+ if (partial_carrier && to_add.size > 0) {
753
+ carrier_lines = '\n' + generate_import_lines(to_add);
754
+ to_add.clear();
755
+ }
756
+ if (to_add.size > 0) {
757
+ const lines = generate_import_lines(to_add);
758
+ s.appendLeft(insert_pos, '\n' + lines);
759
+ }
760
+
761
+ // Remove remaining full Mdz imports (skip the overwrite target)
762
+ for (const node of full_removals) {
763
+ if (node === overwrite_target) continue;
764
+ remove_import_declaration(s, node, source);
765
+ }
766
+
767
+ // Apply partial import removals
768
+ for (const action of partial_removals) {
769
+ remove_import_specifier(
770
+ s,
771
+ action.node,
772
+ action.specifier_to_remove!,
773
+ source,
774
+ action === partial_carrier ? carrier_lines : '',
775
+ );
776
+ }
777
+ };