@fuzdev/fuz_util 0.49.1 → 0.49.3

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.
@@ -67,6 +67,31 @@ 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;
78
+ }
79
+ /**
80
+ * Extracts a conditional expression where both branches are static strings.
81
+ *
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.
85
+ *
86
+ * Returns `null` if the attribute value is not an `ExpressionTag` containing a
87
+ * `ConditionalExpression`, or if either branch is not statically resolvable.
88
+ *
89
+ * @param value The attribute value from `AST.Attribute['value']`.
90
+ * @param source The full source string (needed to slice the test expression source text).
91
+ * @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.
93
+ */
94
+ export declare const try_extract_conditional: (value: AST.Attribute["value"], source: string, bindings: ReadonlyMap<string, string>) => ConditionalStaticStrings | null;
70
95
  /**
71
96
  * Builds a map of statically resolvable `const` bindings from a Svelte AST.
72
97
  *
@@ -123,6 +148,10 @@ export declare const generate_import_lines: (imports: Map<string, PreprocessImpo
123
148
  * `skip` set are excluded from traversal — used to skip `ImportDeclaration`
124
149
  * nodes so the import's own specifier identifier doesn't false-positive.
125
150
  *
151
+ * Skips `Identifier` nodes in non-reference positions defined by
152
+ * `NON_REFERENCE_FIELDS` — for example, `obj.Mdz` (non-computed member property),
153
+ * `{ Mdz: value }` (non-computed object key), and statement labels.
154
+ *
126
155
  * Safe for Svelte template ASTs: `Component.name` is a plain string property
127
156
  * (not an `Identifier` node), so `<Mdz>` tags do not produce false matches.
128
157
  *
@@ -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;;;;;;;;;;;;;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;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,OAAO,EACb,MAAM,MAAM,EACZ,OAAO,GAAG,CAAC,OAAO,CAAC,KACjB,OAYF,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":"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"}
@@ -112,6 +112,41 @@ export const extract_static_string = (value, bindings) => {
112
112
  return null;
113
113
  return evaluate_static_expr(expr, bindings);
114
114
  };
115
+ /**
116
+ * Extracts a conditional expression where both branches are static strings.
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.
121
+ *
122
+ * Returns `null` if the attribute value is not an `ExpressionTag` containing a
123
+ * `ConditionalExpression`, or if either branch is not statically resolvable.
124
+ *
125
+ * @param value The attribute value from `AST.Attribute['value']`.
126
+ * @param source The full source string (needed to slice the test expression source text).
127
+ * @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.
129
+ */
130
+ export const try_extract_conditional = (value, source, bindings) => {
131
+ if (value === true || Array.isArray(value))
132
+ return null;
133
+ const expr = value.expression;
134
+ if (expr.type !== 'ConditionalExpression')
135
+ 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 };
145
+ };
146
+ // TODO cross-import tracing: resolve `import {x} from './constants.js'` by reading
147
+ // and parsing the imported module, extracting `export const` values. Would need path
148
+ // resolution ($lib, tsconfig paths), a Program-node variant of this function, and
149
+ // cache invalidation when the imported file changes. Start with relative .ts/.js only.
115
150
  /**
116
151
  * Builds a map of statically resolvable `const` bindings from a Svelte AST.
117
152
  *
@@ -235,6 +270,26 @@ export const generate_import_lines = (imports, indent = '\t') => {
235
270
  }
236
271
  return lines.join('\n');
237
272
  };
273
+ /**
274
+ * ESTree node fields that contain `Identifier` nodes which are NOT binding references.
275
+ *
276
+ * Keyed by node type. Each entry lists fields to skip during identifier search,
277
+ * optionally conditioned on the node's `computed` property being falsy.
278
+ *
279
+ * Examples of non-reference positions:
280
+ * - `obj.Mdz` — `MemberExpression.property` when `computed: false`
281
+ * - `{ Mdz: value }` — `Property.key` when `computed: false`
282
+ * - `label: for(...)` — `LabeledStatement.label`
283
+ */
284
+ const NON_REFERENCE_FIELDS = new Map([
285
+ ['MemberExpression', [{ field: 'property', when_not_computed: true }]],
286
+ ['Property', [{ field: 'key', when_not_computed: true }]],
287
+ ['PropertyDefinition', [{ field: 'key', when_not_computed: true }]],
288
+ ['MethodDefinition', [{ field: 'key', when_not_computed: true }]],
289
+ ['LabeledStatement', [{ field: 'label' }]],
290
+ ['BreakStatement', [{ field: 'label' }]],
291
+ ['ContinueStatement', [{ field: 'label' }]],
292
+ ]);
238
293
  /**
239
294
  * Checks if an identifier with the given name appears anywhere in an AST subtree.
240
295
  *
@@ -243,6 +298,10 @@ export const generate_import_lines = (imports, indent = '\t') => {
243
298
  * `skip` set are excluded from traversal — used to skip `ImportDeclaration`
244
299
  * nodes so the import's own specifier identifier doesn't false-positive.
245
300
  *
301
+ * Skips `Identifier` nodes in non-reference positions defined by
302
+ * `NON_REFERENCE_FIELDS` — for example, `obj.Mdz` (non-computed member property),
303
+ * `{ Mdz: value }` (non-computed object key), and statement labels.
304
+ *
246
305
  * Safe for Svelte template ASTs: `Component.name` is a plain string property
247
306
  * (not an `Identifier` node), so `<Mdz>` tags do not produce false matches.
248
307
  *
@@ -262,7 +321,11 @@ export const has_identifier_in_tree = (node, name, skip) => {
262
321
  const record = node;
263
322
  if (record.type === 'Identifier' && record.name === name)
264
323
  return true;
324
+ const rules = NON_REFERENCE_FIELDS.get(record.type);
265
325
  for (const key of Object.keys(record)) {
326
+ if (rules?.some((r) => r.field === key && (!r.when_not_computed || !record.computed))) {
327
+ continue;
328
+ }
266
329
  if (has_identifier_in_tree(record[key], name, skip))
267
330
  return true;
268
331
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_util",
3
- "version": "0.49.1",
3
+ "version": "0.49.3",
4
4
  "description": "utility belt for JS",
5
5
  "glyph": "🦕",
6
6
  "logo": "logo.svg",
@@ -136,6 +136,55 @@ export const extract_static_string = (
136
136
  return evaluate_static_expr(expr, bindings);
137
137
  };
138
138
 
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;
147
+ }
148
+
149
+ /**
150
+ * Extracts a conditional expression where both branches are static strings.
151
+ *
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.
155
+ *
156
+ * Returns `null` if the attribute value is not an `ExpressionTag` containing a
157
+ * `ConditionalExpression`, or if either branch is not statically resolvable.
158
+ *
159
+ * @param value The attribute value from `AST.Attribute['value']`.
160
+ * @param source The full source string (needed to slice the test expression source text).
161
+ * @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.
163
+ */
164
+ export const try_extract_conditional = (
165
+ value: AST.Attribute['value'],
166
+ source: string,
167
+ bindings: ReadonlyMap<string, string>,
168
+ ): ConditionalStaticStrings | null => {
169
+ if (value === true || Array.isArray(value)) return null;
170
+ const expr = value.expression;
171
+ if (expr.type !== 'ConditionalExpression') return null;
172
+
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;
177
+
178
+ const test = expr.test as any;
179
+ const test_source = source.slice(test.start, test.end);
180
+ return {test_source, consequent, alternate};
181
+ };
182
+
183
+ // TODO cross-import tracing: resolve `import {x} from './constants.js'` by reading
184
+ // and parsing the imported module, extracting `export const` values. Would need path
185
+ // resolution ($lib, tsconfig paths), a Program-node variant of this function, and
186
+ // cache invalidation when the imported file changes. Start with relative .ts/.js only.
187
+
139
188
  /**
140
189
  * Builds a map of statically resolvable `const` bindings from a Svelte AST.
141
190
  *
@@ -262,6 +311,30 @@ export const generate_import_lines = (
262
311
  return lines.join('\n');
263
312
  };
264
313
 
314
+ /**
315
+ * ESTree node fields that contain `Identifier` nodes which are NOT binding references.
316
+ *
317
+ * Keyed by node type. Each entry lists fields to skip during identifier search,
318
+ * optionally conditioned on the node's `computed` property being falsy.
319
+ *
320
+ * Examples of non-reference positions:
321
+ * - `obj.Mdz` — `MemberExpression.property` when `computed: false`
322
+ * - `{ Mdz: value }` — `Property.key` when `computed: false`
323
+ * - `label: for(...)` — `LabeledStatement.label`
324
+ */
325
+ const NON_REFERENCE_FIELDS: Map<
326
+ string,
327
+ Array<{field: string; when_not_computed?: boolean}>
328
+ > = new Map([
329
+ ['MemberExpression', [{field: 'property', when_not_computed: true}]],
330
+ ['Property', [{field: 'key', when_not_computed: true}]],
331
+ ['PropertyDefinition', [{field: 'key', when_not_computed: true}]],
332
+ ['MethodDefinition', [{field: 'key', when_not_computed: true}]],
333
+ ['LabeledStatement', [{field: 'label'}]],
334
+ ['BreakStatement', [{field: 'label'}]],
335
+ ['ContinueStatement', [{field: 'label'}]],
336
+ ]);
337
+
265
338
  /**
266
339
  * Checks if an identifier with the given name appears anywhere in an AST subtree.
267
340
  *
@@ -270,6 +343,10 @@ export const generate_import_lines = (
270
343
  * `skip` set are excluded from traversal — used to skip `ImportDeclaration`
271
344
  * nodes so the import's own specifier identifier doesn't false-positive.
272
345
  *
346
+ * Skips `Identifier` nodes in non-reference positions defined by
347
+ * `NON_REFERENCE_FIELDS` — for example, `obj.Mdz` (non-computed member property),
348
+ * `{ Mdz: value }` (non-computed object key), and statement labels.
349
+ *
273
350
  * Safe for Svelte template ASTs: `Component.name` is a plain string property
274
351
  * (not an `Identifier` node), so `<Mdz>` tags do not produce false matches.
275
352
  *
@@ -290,7 +367,11 @@ export const has_identifier_in_tree = (
290
367
  }
291
368
  const record = node as Record<string, unknown>;
292
369
  if (record.type === 'Identifier' && record.name === name) return true;
370
+ const rules = NON_REFERENCE_FIELDS.get(record.type as string);
293
371
  for (const key of Object.keys(record)) {
372
+ if (rules?.some((r) => r.field === key && (!r.when_not_computed || !record.computed))) {
373
+ continue;
374
+ }
294
375
  if (has_identifier_in_tree(record[key], name, skip)) return true;
295
376
  }
296
377
  return false;