@fuzdev/fuz_util 0.49.3 → 0.50.1

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.
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * @module
13
13
  */
14
- import type { Expression, ImportDeclaration, ImportDefaultSpecifier, ImportSpecifier } from 'estree';
14
+ import type { Expression, ImportDeclaration, ImportDefaultSpecifier, ImportSpecifier, VariableDeclaration } from 'estree';
15
15
  import type { AST } from 'svelte/compiler';
16
16
  /** Import metadata for a single import specifier. */
17
17
  export interface PreprocessImportInfo {
@@ -67,31 +67,33 @@ export declare const evaluate_static_expr: (expr: Expression, bindings?: Readonl
67
67
  * @returns The resolved static string, or `null` if the value is dynamic.
68
68
  */
69
69
  export declare const extract_static_string: (value: AST.Attribute["value"], bindings?: ReadonlyMap<string, string>) => string | null;
70
- /** Result of extracting a conditional expression with static string branches. */
71
- export interface ConditionalStaticStrings {
72
- /** The source text of the test/condition expression. */
73
- test_source: string;
74
- /** The static string value of the consequent (truthy) branch. */
75
- consequent: string;
76
- /** The static string value of the alternate (falsy) branch. */
77
- alternate: string;
70
+ /** A single branch in a conditional chain extracted from nested ternary expressions. */
71
+ export interface ConditionalChainBranch {
72
+ /** The source text of the test expression, or `null` for the final else branch. */
73
+ test_source: string | null;
74
+ /** The resolved static string value for this branch. */
75
+ value: string;
78
76
  }
79
77
  /**
80
- * Extracts a conditional expression where both branches are static strings.
78
+ * Extracts a chain of conditional expressions where all leaf values are static strings.
81
79
  *
82
- * Handles `content={test ? 'a' : 'b'}` where both the consequent and alternate
83
- * branches resolve to static strings via `evaluate_static_expr`. The test expression
84
- * is preserved as source text (sliced from the original source) since it may be dynamic.
80
+ * Handles nested ternaries like `a ? 'x' : b ? 'y' : 'z'` by iteratively walking
81
+ * the right-recursive `ConditionalExpression` chain. At each level, evaluates the
82
+ * consequent via `evaluate_static_expr` and continues into the alternate if it is
83
+ * another `ConditionalExpression`. The final alternate is the else branch.
85
84
  *
86
85
  * Returns `null` if the attribute value is not an `ExpressionTag` containing a
87
- * `ConditionalExpression`, or if either branch is not statically resolvable.
86
+ * `ConditionalExpression`, if any leaf fails to resolve to a static string, or
87
+ * if the chain exceeds 10 branches (safety limit).
88
+ *
89
+ * A 2-branch result covers the simple ternary case (`a ? 'x' : 'y'`).
88
90
  *
89
91
  * @param value The attribute value from `AST.Attribute['value']`.
90
- * @param source The full source string (needed to slice the test expression source text).
92
+ * @param source The full source string (needed to slice test expression source text).
91
93
  * @param bindings Map of variable names to their resolved static string values.
92
- * @returns The condition source and both branch values, or `null` if not extractable.
94
+ * @returns Array of conditional chain branches, or `null` if not extractable.
93
95
  */
94
- export declare const try_extract_conditional: (value: AST.Attribute["value"], source: string, bindings: ReadonlyMap<string, string>) => ConditionalStaticStrings | null;
96
+ export declare const try_extract_conditional_chain: (value: AST.Attribute["value"], source: string, bindings: ReadonlyMap<string, string>) => Array<ConditionalChainBranch> | null;
95
97
  /**
96
98
  * Builds a map of statically resolvable `const` bindings from a Svelte AST.
97
99
  *
@@ -111,7 +113,8 @@ export declare const build_static_bindings: (ast: AST.Root) => Map<string, strin
111
113
  * Resolves local names that import from specified source paths.
112
114
  *
113
115
  * Scans `ImportDeclaration` nodes in both the instance and module scripts.
114
- * Handles default, named, and aliased imports. Skips namespace imports.
116
+ * Handles default, named, and aliased imports. Skips namespace imports
117
+ * and `import type` declarations (both whole-declaration and per-specifier).
115
118
  * Returns import node references alongside names to support import removal.
116
119
  *
117
120
  * @param ast The parsed Svelte AST root node.
@@ -178,4 +181,70 @@ export declare const has_identifier_in_tree: (node: unknown, name: string, skip?
178
181
  * manual escaping is required to match the runtime behavior.
179
182
  */
180
183
  export declare const escape_svelte_text: (text: string) => string;
184
+ /**
185
+ * Removes a single-declarator `VariableDeclaration` from source using MagicString.
186
+ *
187
+ * Consumes leading whitespace (tabs/spaces) and trailing newline to avoid leaving
188
+ * blank lines. Only safe for single-declarator statements (`const x = 'val';`);
189
+ * callers must verify `node.declarations.length === 1` before calling.
190
+ *
191
+ * @param s The MagicString instance to modify.
192
+ * @param declaration_node The `VariableDeclaration` AST node with Svelte position data.
193
+ * @param source The original source string.
194
+ */
195
+ export declare const remove_variable_declaration: (s: {
196
+ remove: (start: number, end: number) => unknown;
197
+ }, declaration_node: VariableDeclaration & {
198
+ start: number;
199
+ end: number;
200
+ }, source: string) => void;
201
+ /**
202
+ * Removes an `ImportDeclaration` from source using MagicString.
203
+ *
204
+ * Consumes leading whitespace (tabs/spaces) and trailing newline to avoid leaving
205
+ * blank lines.
206
+ *
207
+ * @param s The MagicString instance to modify.
208
+ * @param import_node The `ImportDeclaration` AST node with Svelte position data.
209
+ * @param source The original source string.
210
+ */
211
+ export declare const remove_import_declaration: (s: {
212
+ remove: (start: number, end: number) => unknown;
213
+ }, import_node: ImportDeclaration & {
214
+ start: number;
215
+ end: number;
216
+ }, source: string) => void;
217
+ /**
218
+ * Removes a specifier from a multi-specifier import declaration by
219
+ * reconstructing the statement without the removed specifier.
220
+ *
221
+ * Overwrites the entire declaration range to avoid character-level comma surgery.
222
+ *
223
+ * Handles:
224
+ * - `import Mdz, {other} from '...'` → `import {other} from '...'`
225
+ * - `import {default as Mdz, other} from '...'` → `import {other} from '...'`
226
+ * - `import {Mdz, other} from '...'` → `import {other} from '...'`
227
+ *
228
+ * @param s The MagicString instance to modify.
229
+ * @param node The positioned `ImportDeclaration` AST node.
230
+ * @param specifier_to_remove The specifier to remove from the import.
231
+ * @param source The original source string.
232
+ * @param additional_lines Extra content appended after the reconstructed import
233
+ * (used to bundle new imports into the overwrite to avoid MagicString boundary conflicts).
234
+ */
235
+ export declare const remove_import_specifier: (s: {
236
+ overwrite: (start: number, end: number, content: string) => unknown;
237
+ }, node: ImportDeclaration & {
238
+ start: number;
239
+ end: number;
240
+ }, specifier_to_remove: ImportDeclaration["specifiers"][number], source: string, additional_lines?: string) => void;
241
+ /**
242
+ * Handles errors during Svelte preprocessing with configurable behavior.
243
+ *
244
+ * @param error The caught error.
245
+ * @param prefix Log prefix (e.g. `'[fuz-mdz]'`, `'[fuz-code]'`).
246
+ * @param filename The file being processed.
247
+ * @param on_error `'throw'` to re-throw as a new Error, `'log'` to console.error.
248
+ */
249
+ export declare const handle_preprocess_error: (error: unknown, prefix: string, filename: string | undefined, on_error: "throw" | "log") => void;
181
250
  //# sourceMappingURL=svelte_preprocess_helpers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"svelte_preprocess_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/svelte_preprocess_helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAC,UAAU,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,eAAe,EAAC,MAAM,QAAQ,CAAC;AACnG,OAAO,KAAK,EAAC,GAAG,EAAC,MAAM,iBAAiB,CAAC;AAEzC,qDAAqD;AACrD,MAAM,WAAW,oBAAoB;IACpC,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;CAC1B;AAED,qDAAqD;AACrD,MAAM,WAAW,uBAAuB;IACvC,gEAAgE;IAChE,WAAW,EAAE,iBAAiB,CAAC;IAC/B,mDAAmD;IACnD,SAAS,EAAE,eAAe,GAAG,sBAAsB,CAAC;CACpD;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,GAAG,CAAC,SAAS,EAAE,MAAM,MAAM,KAAG,GAAG,CAAC,SAAS,GAAG,SAOlF,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,UAAU,EAChB,WAAW,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,KACpC,MAAM,GAAG,IA+BX,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,qBAAqB,GACjC,OAAO,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,EAC7B,WAAW,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,KACpC,MAAM,GAAG,IAkBX,CAAC;AAEF,iFAAiF;AACjF,MAAM,WAAW,wBAAwB;IACxC,wDAAwD;IACxD,WAAW,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,uBAAuB,GACnC,OAAO,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,EAC7B,QAAQ,MAAM,EACd,UAAU,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,KACnC,wBAAwB,GAAG,IAa7B,CAAC;AAOF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,GAAI,KAAK,GAAG,CAAC,IAAI,KAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAiBvE,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,uBAAuB,GACnC,KAAK,GAAG,CAAC,IAAI,EACb,mBAAmB,KAAK,CAAC,MAAM,CAAC,KAC9B,GAAG,CAAC,MAAM,EAAE,uBAAuB,CAcrC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,2BAA2B,GAAI,QAAQ,GAAG,CAAC,MAAM,KAAG,MAYhE,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,GACjC,SAAS,GAAG,CAAC,MAAM,EAAE,oBAAoB,CAAC,EAC1C,SAAQ,MAAa,KACnB,MAyBF,CAAC;AA0BF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,OAAO,EACb,MAAM,MAAM,EACZ,OAAO,GAAG,CAAC,OAAO,CAAC,KACjB,OAgBF,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,KAAG,MAc/C,CAAC"}
1
+ {"version":3,"file":"svelte_preprocess_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/svelte_preprocess_helpers.ts"],"names":[],"mappings":"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"}
@@ -113,35 +113,55 @@ export const extract_static_string = (value, bindings) => {
113
113
  return evaluate_static_expr(expr, bindings);
114
114
  };
115
115
  /**
116
- * Extracts a conditional expression where both branches are static strings.
116
+ * Extracts a chain of conditional expressions where all leaf values are static strings.
117
117
  *
118
- * Handles `content={test ? 'a' : 'b'}` where both the consequent and alternate
119
- * branches resolve to static strings via `evaluate_static_expr`. The test expression
120
- * is preserved as source text (sliced from the original source) since it may be dynamic.
118
+ * Handles nested ternaries like `a ? 'x' : b ? 'y' : 'z'` by iteratively walking
119
+ * the right-recursive `ConditionalExpression` chain. At each level, evaluates the
120
+ * consequent via `evaluate_static_expr` and continues into the alternate if it is
121
+ * another `ConditionalExpression`. The final alternate is the else branch.
121
122
  *
122
123
  * Returns `null` if the attribute value is not an `ExpressionTag` containing a
123
- * `ConditionalExpression`, or if either branch is not statically resolvable.
124
+ * `ConditionalExpression`, if any leaf fails to resolve to a static string, or
125
+ * if the chain exceeds 10 branches (safety limit).
126
+ *
127
+ * A 2-branch result covers the simple ternary case (`a ? 'x' : 'y'`).
124
128
  *
125
129
  * @param value The attribute value from `AST.Attribute['value']`.
126
- * @param source The full source string (needed to slice the test expression source text).
130
+ * @param source The full source string (needed to slice test expression source text).
127
131
  * @param bindings Map of variable names to their resolved static string values.
128
- * @returns The condition source and both branch values, or `null` if not extractable.
132
+ * @returns Array of conditional chain branches, or `null` if not extractable.
129
133
  */
130
- export const try_extract_conditional = (value, source, bindings) => {
134
+ export const try_extract_conditional_chain = (value, source, bindings) => {
131
135
  if (value === true || Array.isArray(value))
132
136
  return null;
133
137
  const expr = value.expression;
134
138
  if (expr.type !== 'ConditionalExpression')
135
139
  return null;
136
- const consequent = evaluate_static_expr(expr.consequent, bindings);
137
- if (consequent === null)
138
- return null;
139
- const alternate = evaluate_static_expr(expr.alternate, bindings);
140
- if (alternate === null)
141
- return null;
142
- const test = expr.test;
143
- const test_source = source.slice(test.start, test.end);
144
- return { test_source, consequent, alternate };
140
+ const MAX_BRANCHES = 10;
141
+ const branches = [];
142
+ let current = expr;
143
+ for (;;) {
144
+ const consequent = evaluate_static_expr(current.consequent, bindings);
145
+ if (consequent === null)
146
+ return null;
147
+ const test = current.test;
148
+ const test_source = source.slice(test.start, test.end);
149
+ branches.push({ test_source, value: consequent });
150
+ if (branches.length >= MAX_BRANCHES)
151
+ return null;
152
+ if (current.alternate.type === 'ConditionalExpression') {
153
+ current = current.alternate;
154
+ }
155
+ else {
156
+ // Final else branch
157
+ const alternate = evaluate_static_expr(current.alternate, bindings);
158
+ if (alternate === null)
159
+ return null;
160
+ branches.push({ test_source: null, value: alternate });
161
+ break;
162
+ }
163
+ }
164
+ return branches;
145
165
  };
146
166
  // TODO cross-import tracing: resolve `import {x} from './constants.js'` by reading
147
167
  // and parsing the imported module, extracting `export const` values. Would need path
@@ -187,7 +207,8 @@ export const build_static_bindings = (ast) => {
187
207
  * Resolves local names that import from specified source paths.
188
208
  *
189
209
  * Scans `ImportDeclaration` nodes in both the instance and module scripts.
190
- * Handles default, named, and aliased imports. Skips namespace imports.
210
+ * Handles default, named, and aliased imports. Skips namespace imports
211
+ * and `import type` declarations (both whole-declaration and per-specifier).
191
212
  * Returns import node references alongside names to support import removal.
192
213
  *
193
214
  * @param ast The parsed Svelte AST root node.
@@ -204,9 +225,16 @@ export const resolve_component_names = (ast, component_imports) => {
204
225
  continue;
205
226
  if (!component_imports.includes(node.source.value))
206
227
  continue;
228
+ // Skip `import type` declarations — estree types don't declare importKind,
229
+ // but Svelte's parser preserves it from the TypeScript syntax.
230
+ if (node.importKind === 'type')
231
+ continue;
207
232
  for (const specifier of node.specifiers) {
208
233
  if (specifier.type === 'ImportNamespaceSpecifier')
209
234
  continue;
235
+ // Skip `import {type Foo}` specifiers
236
+ if (specifier.importKind === 'type')
237
+ continue;
210
238
  names.set(specifier.local.name, { import_node: node, specifier });
211
239
  }
212
240
  }
@@ -361,3 +389,128 @@ export const escape_svelte_text = (text) => text.replace(/[{}<&]/g, (ch) => {
361
389
  return ch;
362
390
  }
363
391
  });
392
+ /**
393
+ * Removes a single-declarator `VariableDeclaration` from source using MagicString.
394
+ *
395
+ * Consumes leading whitespace (tabs/spaces) and trailing newline to avoid leaving
396
+ * blank lines. Only safe for single-declarator statements (`const x = 'val';`);
397
+ * callers must verify `node.declarations.length === 1` before calling.
398
+ *
399
+ * @param s The MagicString instance to modify.
400
+ * @param declaration_node The `VariableDeclaration` AST node with Svelte position data.
401
+ * @param source The original source string.
402
+ */
403
+ export const remove_variable_declaration = (s, declaration_node, source) => {
404
+ remove_positioned_node(s, declaration_node, source);
405
+ };
406
+ /**
407
+ * Removes an `ImportDeclaration` from source using MagicString.
408
+ *
409
+ * Consumes leading whitespace (tabs/spaces) and trailing newline to avoid leaving
410
+ * blank lines.
411
+ *
412
+ * @param s The MagicString instance to modify.
413
+ * @param import_node The `ImportDeclaration` AST node with Svelte position data.
414
+ * @param source The original source string.
415
+ */
416
+ export const remove_import_declaration = (s, import_node, source) => {
417
+ remove_positioned_node(s, import_node, source);
418
+ };
419
+ /**
420
+ * Removes a positioned AST node from source, consuming surrounding whitespace.
421
+ *
422
+ * Consumes leading whitespace (tabs/spaces) and trailing newline to avoid leaving
423
+ * blank lines. Shared implementation for `remove_variable_declaration` and
424
+ * `remove_import_declaration`.
425
+ */
426
+ const remove_positioned_node = (s, node, source) => {
427
+ let start = node.start;
428
+ let end = node.end;
429
+ // Consume trailing newline
430
+ if (source[end] === '\n') {
431
+ end++;
432
+ }
433
+ else if (source[end] === '\r' && source[end + 1] === '\n') {
434
+ end += 2;
435
+ }
436
+ // Consume leading whitespace on the same line
437
+ while (start > 0 && (source[start - 1] === '\t' || source[start - 1] === ' ')) {
438
+ start--;
439
+ }
440
+ s.remove(start, end);
441
+ };
442
+ /**
443
+ * Removes a specifier from a multi-specifier import declaration by
444
+ * reconstructing the statement without the removed specifier.
445
+ *
446
+ * Overwrites the entire declaration range to avoid character-level comma surgery.
447
+ *
448
+ * Handles:
449
+ * - `import Mdz, {other} from '...'` → `import {other} from '...'`
450
+ * - `import {default as Mdz, other} from '...'` → `import {other} from '...'`
451
+ * - `import {Mdz, other} from '...'` → `import {other} from '...'`
452
+ *
453
+ * @param s The MagicString instance to modify.
454
+ * @param node The positioned `ImportDeclaration` AST node.
455
+ * @param specifier_to_remove The specifier to remove from the import.
456
+ * @param source The original source string.
457
+ * @param additional_lines Extra content appended after the reconstructed import
458
+ * (used to bundle new imports into the overwrite to avoid MagicString boundary conflicts).
459
+ */
460
+ export const remove_import_specifier = (s, node, specifier_to_remove, source, additional_lines = '') => {
461
+ const remaining = node.specifiers.filter((spec) => spec !== specifier_to_remove);
462
+ if (remaining.length === 0)
463
+ return;
464
+ const source_path = node.source.value;
465
+ // Reconstruct the import statement
466
+ const default_specs = remaining.filter((sp) => sp.type === 'ImportDefaultSpecifier');
467
+ const named_specs = remaining.filter((sp) => sp.type === 'ImportSpecifier' || sp.type === 'ImportNamespaceSpecifier');
468
+ let import_clause = '';
469
+ if (default_specs.length > 0) {
470
+ import_clause = default_specs[0].local.name;
471
+ if (named_specs.length > 0) {
472
+ import_clause += `, {${format_named_specifiers(named_specs)}}`;
473
+ }
474
+ }
475
+ else if (named_specs.length > 0) {
476
+ import_clause = `{${format_named_specifiers(named_specs)}}`;
477
+ }
478
+ const reconstructed = `import ${import_clause} from '${source_path}';`;
479
+ // Find leading whitespace to preserve indentation
480
+ let line_start = node.start;
481
+ while (line_start > 0 && (source[line_start - 1] === '\t' || source[line_start - 1] === ' ')) {
482
+ line_start--;
483
+ }
484
+ const indent = source.slice(line_start, node.start);
485
+ s.overwrite(line_start, node.end, `${indent}${reconstructed}${additional_lines}`);
486
+ };
487
+ /** Formats named/namespace specifiers as comma-separated string. */
488
+ const format_named_specifiers = (specs) => specs
489
+ .map((spec) => {
490
+ if (spec.type === 'ImportNamespaceSpecifier')
491
+ return `* as ${spec.local.name}`;
492
+ if (spec.type !== 'ImportSpecifier')
493
+ return spec.local.name;
494
+ const imported_name = spec.imported.type === 'Identifier' ? spec.imported.name : spec.imported.value;
495
+ if (imported_name !== spec.local.name) {
496
+ return `${imported_name} as ${spec.local.name}`;
497
+ }
498
+ return spec.local.name;
499
+ })
500
+ .join(', ');
501
+ /**
502
+ * Handles errors during Svelte preprocessing with configurable behavior.
503
+ *
504
+ * @param error The caught error.
505
+ * @param prefix Log prefix (e.g. `'[fuz-mdz]'`, `'[fuz-code]'`).
506
+ * @param filename The file being processed.
507
+ * @param on_error `'throw'` to re-throw as a new Error, `'log'` to console.error.
508
+ */
509
+ export const handle_preprocess_error = (error, prefix, filename, on_error) => {
510
+ const message = `${prefix} Preprocessing failed${filename ? ` in ${filename}` : ''}: ${error instanceof Error ? error.message : String(error)}`;
511
+ if (on_error === 'throw') {
512
+ throw new Error(message, { cause: error });
513
+ }
514
+ // eslint-disable-next-line no-console
515
+ console.error(message);
516
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_util",
3
- "version": "0.49.3",
3
+ "version": "0.50.1",
4
4
  "description": "utility belt for JS",
5
5
  "glyph": "🦕",
6
6
  "logo": "logo.svg",
@@ -12,7 +12,14 @@
12
12
  * @module
13
13
  */
14
14
 
15
- import type {Expression, ImportDeclaration, ImportDefaultSpecifier, ImportSpecifier} from 'estree';
15
+ import type {
16
+ ConditionalExpression,
17
+ Expression,
18
+ ImportDeclaration,
19
+ ImportDefaultSpecifier,
20
+ ImportSpecifier,
21
+ VariableDeclaration,
22
+ } from 'estree';
16
23
  import type {AST} from 'svelte/compiler';
17
24
 
18
25
  /** Import metadata for a single import specifier. */
@@ -136,48 +143,68 @@ export const extract_static_string = (
136
143
  return evaluate_static_expr(expr, bindings);
137
144
  };
138
145
 
139
- /** Result of extracting a conditional expression with static string branches. */
140
- export interface ConditionalStaticStrings {
141
- /** The source text of the test/condition expression. */
142
- test_source: string;
143
- /** The static string value of the consequent (truthy) branch. */
144
- consequent: string;
145
- /** The static string value of the alternate (falsy) branch. */
146
- alternate: string;
146
+ /** A single branch in a conditional chain extracted from nested ternary expressions. */
147
+ export interface ConditionalChainBranch {
148
+ /** The source text of the test expression, or `null` for the final else branch. */
149
+ test_source: string | null;
150
+ /** The resolved static string value for this branch. */
151
+ value: string;
147
152
  }
148
153
 
149
154
  /**
150
- * Extracts a conditional expression where both branches are static strings.
155
+ * Extracts a chain of conditional expressions where all leaf values are static strings.
151
156
  *
152
- * Handles `content={test ? 'a' : 'b'}` where both the consequent and alternate
153
- * branches resolve to static strings via `evaluate_static_expr`. The test expression
154
- * is preserved as source text (sliced from the original source) since it may be dynamic.
157
+ * Handles nested ternaries like `a ? 'x' : b ? 'y' : 'z'` by iteratively walking
158
+ * the right-recursive `ConditionalExpression` chain. At each level, evaluates the
159
+ * consequent via `evaluate_static_expr` and continues into the alternate if it is
160
+ * another `ConditionalExpression`. The final alternate is the else branch.
155
161
  *
156
162
  * Returns `null` if the attribute value is not an `ExpressionTag` containing a
157
- * `ConditionalExpression`, or if either branch is not statically resolvable.
163
+ * `ConditionalExpression`, if any leaf fails to resolve to a static string, or
164
+ * if the chain exceeds 10 branches (safety limit).
165
+ *
166
+ * A 2-branch result covers the simple ternary case (`a ? 'x' : 'y'`).
158
167
  *
159
168
  * @param value The attribute value from `AST.Attribute['value']`.
160
- * @param source The full source string (needed to slice the test expression source text).
169
+ * @param source The full source string (needed to slice test expression source text).
161
170
  * @param bindings Map of variable names to their resolved static string values.
162
- * @returns The condition source and both branch values, or `null` if not extractable.
171
+ * @returns Array of conditional chain branches, or `null` if not extractable.
163
172
  */
164
- export const try_extract_conditional = (
173
+ export const try_extract_conditional_chain = (
165
174
  value: AST.Attribute['value'],
166
175
  source: string,
167
176
  bindings: ReadonlyMap<string, string>,
168
- ): ConditionalStaticStrings | null => {
177
+ ): Array<ConditionalChainBranch> | null => {
169
178
  if (value === true || Array.isArray(value)) return null;
170
179
  const expr = value.expression;
171
180
  if (expr.type !== 'ConditionalExpression') return null;
172
181
 
173
- const consequent = evaluate_static_expr(expr.consequent, bindings);
174
- if (consequent === null) return null;
175
- const alternate = evaluate_static_expr(expr.alternate, bindings);
176
- if (alternate === null) return null;
182
+ const MAX_BRANCHES = 10;
183
+ const branches: Array<ConditionalChainBranch> = [];
184
+ let current: ConditionalExpression = expr;
185
+
186
+ for (;;) {
187
+ const consequent = evaluate_static_expr(current.consequent, bindings);
188
+ if (consequent === null) return null;
189
+
190
+ const test = current.test as any;
191
+ const test_source = source.slice(test.start, test.end);
192
+ branches.push({test_source, value: consequent});
193
+
194
+ if (branches.length >= MAX_BRANCHES) return null;
195
+
196
+ if (current.alternate.type === 'ConditionalExpression') {
197
+ current = current.alternate;
198
+ } else {
199
+ // Final else branch
200
+ const alternate = evaluate_static_expr(current.alternate, bindings);
201
+ if (alternate === null) return null;
202
+ branches.push({test_source: null, value: alternate});
203
+ break;
204
+ }
205
+ }
177
206
 
178
- const test = expr.test as any;
179
- const test_source = source.slice(test.start, test.end);
180
- return {test_source, consequent, alternate};
207
+ return branches;
181
208
  };
182
209
 
183
210
  // TODO cross-import tracing: resolve `import {x} from './constants.js'` by reading
@@ -222,7 +249,8 @@ export const build_static_bindings = (ast: AST.Root): Map<string, string> => {
222
249
  * Resolves local names that import from specified source paths.
223
250
  *
224
251
  * Scans `ImportDeclaration` nodes in both the instance and module scripts.
225
- * Handles default, named, and aliased imports. Skips namespace imports.
252
+ * Handles default, named, and aliased imports. Skips namespace imports
253
+ * and `import type` declarations (both whole-declaration and per-specifier).
226
254
  * Returns import node references alongside names to support import removal.
227
255
  *
228
256
  * @param ast The parsed Svelte AST root node.
@@ -239,8 +267,13 @@ export const resolve_component_names = (
239
267
  for (const node of script.content.body) {
240
268
  if (node.type !== 'ImportDeclaration') continue;
241
269
  if (!component_imports.includes(node.source.value as string)) continue;
270
+ // Skip `import type` declarations — estree types don't declare importKind,
271
+ // but Svelte's parser preserves it from the TypeScript syntax.
272
+ if ((node as any).importKind === 'type') continue;
242
273
  for (const specifier of node.specifiers) {
243
274
  if (specifier.type === 'ImportNamespaceSpecifier') continue;
275
+ // Skip `import {type Foo}` specifiers
276
+ if ((specifier as any).importKind === 'type') continue;
244
277
  names.set(specifier.local.name, {import_node: node, specifier});
245
278
  }
246
279
  }
@@ -408,3 +441,165 @@ export const escape_svelte_text = (text: string): string =>
408
441
  return ch;
409
442
  }
410
443
  });
444
+
445
+ /**
446
+ * Removes a single-declarator `VariableDeclaration` from source using MagicString.
447
+ *
448
+ * Consumes leading whitespace (tabs/spaces) and trailing newline to avoid leaving
449
+ * blank lines. Only safe for single-declarator statements (`const x = 'val';`);
450
+ * callers must verify `node.declarations.length === 1` before calling.
451
+ *
452
+ * @param s The MagicString instance to modify.
453
+ * @param declaration_node The `VariableDeclaration` AST node with Svelte position data.
454
+ * @param source The original source string.
455
+ */
456
+ export const remove_variable_declaration = (
457
+ s: {remove: (start: number, end: number) => unknown},
458
+ declaration_node: VariableDeclaration & {start: number; end: number},
459
+ source: string,
460
+ ): void => {
461
+ remove_positioned_node(s, declaration_node, source);
462
+ };
463
+
464
+ /**
465
+ * Removes an `ImportDeclaration` from source using MagicString.
466
+ *
467
+ * Consumes leading whitespace (tabs/spaces) and trailing newline to avoid leaving
468
+ * blank lines.
469
+ *
470
+ * @param s The MagicString instance to modify.
471
+ * @param import_node The `ImportDeclaration` AST node with Svelte position data.
472
+ * @param source The original source string.
473
+ */
474
+ export const remove_import_declaration = (
475
+ s: {remove: (start: number, end: number) => unknown},
476
+ import_node: ImportDeclaration & {start: number; end: number},
477
+ source: string,
478
+ ): void => {
479
+ remove_positioned_node(s, import_node, source);
480
+ };
481
+
482
+ /**
483
+ * Removes a positioned AST node from source, consuming surrounding whitespace.
484
+ *
485
+ * Consumes leading whitespace (tabs/spaces) and trailing newline to avoid leaving
486
+ * blank lines. Shared implementation for `remove_variable_declaration` and
487
+ * `remove_import_declaration`.
488
+ */
489
+ const remove_positioned_node = (
490
+ s: {remove: (start: number, end: number) => unknown},
491
+ node: {start: number; end: number},
492
+ source: string,
493
+ ): void => {
494
+ let start: number = node.start;
495
+ let end: number = node.end;
496
+
497
+ // Consume trailing newline
498
+ if (source[end] === '\n') {
499
+ end++;
500
+ } else if (source[end] === '\r' && source[end + 1] === '\n') {
501
+ end += 2;
502
+ }
503
+
504
+ // Consume leading whitespace on the same line
505
+ while (start > 0 && (source[start - 1] === '\t' || source[start - 1] === ' ')) {
506
+ start--;
507
+ }
508
+
509
+ s.remove(start, end);
510
+ };
511
+
512
+ /**
513
+ * Removes a specifier from a multi-specifier import declaration by
514
+ * reconstructing the statement without the removed specifier.
515
+ *
516
+ * Overwrites the entire declaration range to avoid character-level comma surgery.
517
+ *
518
+ * Handles:
519
+ * - `import Mdz, {other} from '...'` → `import {other} from '...'`
520
+ * - `import {default as Mdz, other} from '...'` → `import {other} from '...'`
521
+ * - `import {Mdz, other} from '...'` → `import {other} from '...'`
522
+ *
523
+ * @param s The MagicString instance to modify.
524
+ * @param node The positioned `ImportDeclaration` AST node.
525
+ * @param specifier_to_remove The specifier to remove from the import.
526
+ * @param source The original source string.
527
+ * @param additional_lines Extra content appended after the reconstructed import
528
+ * (used to bundle new imports into the overwrite to avoid MagicString boundary conflicts).
529
+ */
530
+ export const remove_import_specifier = (
531
+ s: {overwrite: (start: number, end: number, content: string) => unknown},
532
+ node: ImportDeclaration & {start: number; end: number},
533
+ specifier_to_remove: ImportDeclaration['specifiers'][number],
534
+ source: string,
535
+ additional_lines: string = '',
536
+ ): void => {
537
+ const remaining = node.specifiers.filter((spec) => spec !== specifier_to_remove);
538
+ if (remaining.length === 0) return;
539
+
540
+ const source_path = node.source.value as string;
541
+
542
+ // Reconstruct the import statement
543
+ const default_specs = remaining.filter((sp) => sp.type === 'ImportDefaultSpecifier');
544
+ const named_specs = remaining.filter(
545
+ (sp) => sp.type === 'ImportSpecifier' || sp.type === 'ImportNamespaceSpecifier',
546
+ );
547
+
548
+ let import_clause = '';
549
+ if (default_specs.length > 0) {
550
+ import_clause = default_specs[0]!.local.name;
551
+ if (named_specs.length > 0) {
552
+ import_clause += `, {${format_named_specifiers(named_specs)}}`;
553
+ }
554
+ } else if (named_specs.length > 0) {
555
+ import_clause = `{${format_named_specifiers(named_specs)}}`;
556
+ }
557
+
558
+ const reconstructed = `import ${import_clause} from '${source_path}';`;
559
+
560
+ // Find leading whitespace to preserve indentation
561
+ let line_start = node.start;
562
+ while (line_start > 0 && (source[line_start - 1] === '\t' || source[line_start - 1] === ' ')) {
563
+ line_start--;
564
+ }
565
+ const indent = source.slice(line_start, node.start);
566
+
567
+ s.overwrite(line_start, node.end, `${indent}${reconstructed}${additional_lines}`);
568
+ };
569
+
570
+ /** Formats named/namespace specifiers as comma-separated string. */
571
+ const format_named_specifiers = (specs: Array<ImportDeclaration['specifiers'][number]>): string =>
572
+ specs
573
+ .map((spec) => {
574
+ if (spec.type === 'ImportNamespaceSpecifier') return `* as ${spec.local.name}`;
575
+ if (spec.type !== 'ImportSpecifier') return spec.local.name;
576
+ const imported_name =
577
+ spec.imported.type === 'Identifier' ? spec.imported.name : spec.imported.value;
578
+ if (imported_name !== spec.local.name) {
579
+ return `${imported_name} as ${spec.local.name}`;
580
+ }
581
+ return spec.local.name;
582
+ })
583
+ .join(', ');
584
+
585
+ /**
586
+ * Handles errors during Svelte preprocessing with configurable behavior.
587
+ *
588
+ * @param error The caught error.
589
+ * @param prefix Log prefix (e.g. `'[fuz-mdz]'`, `'[fuz-code]'`).
590
+ * @param filename The file being processed.
591
+ * @param on_error `'throw'` to re-throw as a new Error, `'log'` to console.error.
592
+ */
593
+ export const handle_preprocess_error = (
594
+ error: unknown,
595
+ prefix: string,
596
+ filename: string | undefined,
597
+ on_error: 'throw' | 'log',
598
+ ): void => {
599
+ const message = `${prefix} Preprocessing failed${filename ? ` in ${filename}` : ''}: ${error instanceof Error ? error.message : String(error)}`;
600
+ if (on_error === 'throw') {
601
+ throw new Error(message, {cause: error});
602
+ }
603
+ // eslint-disable-next-line no-console
604
+ console.error(message);
605
+ };