@athoscommerce/snap-preact 1.12.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/cjs/components/src/components/Atoms/Price/Price.js +2 -2
  2. package/dist/cjs/components/src/components/Molecules/CustomComponentWrapper/CustomComponentWrapper.js +1 -1
  3. package/dist/cjs/components/src/components/Molecules/Slideshow/Slideshow.js +59 -30
  4. package/dist/cjs/components/src/components/Organisms/AutocompleteLayout/AutocompleteLayout.js +13 -8
  5. package/dist/cjs/components/src/components/Organisms/QuickviewLayout/QuickviewLayout.js +13 -8
  6. package/dist/cjs/components/src/hooks/useLayoutOptions.js +19 -14
  7. package/dist/cjs/components/src/utilities/mergeProps.js +647 -184
  8. package/dist/cjs/src/Snap.js +1 -1
  9. package/dist/cjs/src/Templates/SnapTemplates.js +17 -53
  10. package/dist/cjs/src/Templates/Stores/LibraryStore.js +2 -0
  11. package/dist/cjs/src/Templates/Stores/TemplateStore.js +37 -8
  12. package/dist/cjs/src/Templates/Stores/ThemeStore.js +125 -32
  13. package/dist/cjs/src/Templates/Stores/library/currencies/currencies.js +52 -29
  14. package/dist/cjs/src/Templates/Stores/library/plugins/shopify/pluginCurrency.js +5 -0
  15. package/dist/esm/components/src/components/Atoms/Price/Price.d.ts +2 -0
  16. package/dist/esm/components/src/components/Atoms/Price/Price.d.ts.map +1 -1
  17. package/dist/esm/components/src/components/Atoms/Price/Price.js +2 -2
  18. package/dist/esm/components/src/components/Molecules/CustomComponentWrapper/CustomComponentWrapper.d.ts.map +1 -1
  19. package/dist/esm/components/src/components/Molecules/CustomComponentWrapper/CustomComponentWrapper.js +1 -1
  20. package/dist/esm/components/src/components/Molecules/Select/Select.d.ts +2 -2
  21. package/dist/esm/components/src/components/Molecules/Select/Select.d.ts.map +1 -1
  22. package/dist/esm/components/src/components/Molecules/Slideshow/Slideshow.d.ts +1 -0
  23. package/dist/esm/components/src/components/Molecules/Slideshow/Slideshow.d.ts.map +1 -1
  24. package/dist/esm/components/src/components/Molecules/Slideshow/Slideshow.js +59 -30
  25. package/dist/esm/components/src/components/Organisms/AutocompleteLayout/AutocompleteLayout.d.ts.map +1 -1
  26. package/dist/esm/components/src/components/Organisms/AutocompleteLayout/AutocompleteLayout.js +13 -8
  27. package/dist/esm/components/src/components/Organisms/QuickviewLayout/QuickviewLayout.d.ts.map +1 -1
  28. package/dist/esm/components/src/components/Organisms/QuickviewLayout/QuickviewLayout.js +13 -8
  29. package/dist/esm/components/src/hooks/useLayoutOptions.d.ts.map +1 -1
  30. package/dist/esm/components/src/hooks/useLayoutOptions.js +19 -14
  31. package/dist/esm/components/src/providers/langComponents.d.ts +5 -0
  32. package/dist/esm/components/src/providers/langComponents.d.ts.map +1 -1
  33. package/dist/esm/components/src/providers/themeComponents.d.ts +214 -193
  34. package/dist/esm/components/src/providers/themeComponents.d.ts.map +1 -1
  35. package/dist/esm/components/src/utilities/mergeProps.d.ts +11 -0
  36. package/dist/esm/components/src/utilities/mergeProps.d.ts.map +1 -1
  37. package/dist/esm/components/src/utilities/mergeProps.js +647 -184
  38. package/dist/esm/src/Snap.js +1 -1
  39. package/dist/esm/src/Templates/SnapTemplates.d.ts +32 -19
  40. package/dist/esm/src/Templates/SnapTemplates.d.ts.map +1 -1
  41. package/dist/esm/src/Templates/SnapTemplates.js +16 -51
  42. package/dist/esm/src/Templates/Stores/LibraryStore.d.ts +7 -0
  43. package/dist/esm/src/Templates/Stores/LibraryStore.d.ts.map +1 -1
  44. package/dist/esm/src/Templates/Stores/LibraryStore.js +2 -0
  45. package/dist/esm/src/Templates/Stores/TemplateStore.d.ts +11 -7
  46. package/dist/esm/src/Templates/Stores/TemplateStore.d.ts.map +1 -1
  47. package/dist/esm/src/Templates/Stores/TemplateStore.js +31 -8
  48. package/dist/esm/src/Templates/Stores/ThemeStore.d.ts +5 -2
  49. package/dist/esm/src/Templates/Stores/ThemeStore.d.ts.map +1 -1
  50. package/dist/esm/src/Templates/Stores/ThemeStore.js +125 -32
  51. package/dist/esm/src/Templates/Stores/library/currencies/currencies.d.ts +27 -0
  52. package/dist/esm/src/Templates/Stores/library/currencies/currencies.d.ts.map +1 -1
  53. package/dist/esm/src/Templates/Stores/library/currencies/currencies.js +52 -29
  54. package/dist/esm/src/Templates/Stores/library/plugins/shopify/pluginCurrency.d.ts +2 -0
  55. package/dist/esm/src/Templates/Stores/library/plugins/shopify/pluginCurrency.d.ts.map +1 -0
  56. package/dist/esm/src/Templates/Stores/library/plugins/shopify/pluginCurrency.js +1 -0
  57. package/eslint/src/validate-config.cjs +520 -0
  58. package/package.json +13 -13
@@ -12,6 +12,21 @@ const PROVIDED_COMPONENT_KEYS = {
12
12
 
13
13
  const VALIDATE_CONFIG_FUNCTION_NAMES = ['validateTemplatesConfig', 'validateTemplatesConfigUnlocked'];
14
14
  const TEMPLATES_CONSTRUCTOR_NAMES = ['SnapTemplates', 'SnapHybrid'];
15
+ const OPEN_NAMED_COMPONENT_PROPS_TYPE = {
16
+ facet: 'FacetTemplatesLegalProps',
17
+ variantSelection: 'VariantSelectionTemplatesLegalProps',
18
+ recommendation: 'RecommendationTemplatesLegalProps',
19
+ recommendationBundle: 'RecommendationBundleTemplatesLegalProps',
20
+ recommendationBundleEasyAdd: 'RecommendationBundleEasyAddTemplatesLegalProps',
21
+ recommendationBundleList: 'RecommendationBundleListTemplatesLegalProps',
22
+ recommendationBundleVertical: 'RecommendationBundleVerticalTemplatesLegalProps',
23
+ recommendationGrid: 'RecommendationGridTemplatesLegalProps',
24
+ };
25
+
26
+ // allowed on every override regardless of component - see ThemeComponentCascade/ThemeComponentAllowedProps
27
+ const ALWAYS_ALLOWED_OVERRIDE_PROPS = new Set(['$children', 'themeStyleScript']);
28
+
29
+ const openNamedPropsCacheByProgram = new WeakMap();
15
30
 
16
31
  module.exports = {
17
32
  meta: {
@@ -35,6 +50,13 @@ module.exports = {
35
50
  'Tab "{{ id }}" uses siteId "{{ siteId }}", which is already used by another tab in {{ feature }}.tabs. Tabs within the same feature must target unique siteIds.',
36
51
  mismatchedTabSiteId:
37
52
  'Tab "{{ id }}" (param "{{ param }}") uses siteId "{{ siteId }}", but other tabs sharing param "{{ param }}" use siteId "{{ expectedSiteId }}". Tab controllers that share a param must use the same siteId.',
53
+ invalidOpenNamedSelectorProp:
54
+ '"{{ value }}" is not a valid prop for the "{{ selector }}" override ({{ componentType }} resolves to {{ typeName }}). Must be one of: {{ validKeys }}.',
55
+ invalidOpenNamedSelectorPropType:
56
+ '"{{ value }}" on the "{{ selector }}" override ({{ typeName }}) expects type {{ expectedType }}, but got {{ actualType }}.',
57
+ unknownOverrideSelector: '"{{ selector }}" is not a valid theme override selector ("{{ segment }}" does not resolve to any component).',
58
+ mixedSelectorGroup:
59
+ '"{{ selector }}" mixes component types ({{ kinds }}). Comma-separated selectors must all target the same component type, since the override props resolve against that component.',
38
60
  },
39
61
  schema: [],
40
62
  },
@@ -167,6 +189,504 @@ module.exports = {
167
189
 
168
190
  // Validate search/autocomplete tab controller configs
169
191
  validateTabs(init, context);
192
+
193
+ // Comma-separated selector groups must target one component type - pure syntax,
194
+ // so this runs even without typed linting
195
+ validateSelectorGroupKinds(init, context);
196
+
197
+ // ADVISORY typed checks: inline squiggles for bad theme-override selectors and for
198
+ // bad props under open-named dotted selectors (facet.price, ...). Best-effort - only
199
+ // runs under typed linting (see getProgramAndChecker) and fails open. Correctness
200
+ // does not depend on this: the compiler enforces the same rules through
201
+ // validateTemplatesConfig's conditional return type (see SnapTemplates.tsx), which
202
+ // errors at the config's use site; these squiggles exist to pinpoint the exact
203
+ // offending key at its source location.
204
+ validateOverrideSelectors(init, context);
205
+ }
206
+
207
+ /**
208
+ * A comma-separated selector applies ONE override object to several tree paths, so
209
+ * every part must target the same component type (its final segment's component
210
+ * name) - otherwise no single props type could validate the override. Mirrors the
211
+ * compiler-side ThemeSelectorGroupKinds check (themeComponents.ts); this squiggle is
212
+ * the pinpoint version, and needs no type information.
213
+ */
214
+ function validateSelectorGroupKinds(configObjectExpression, context) {
215
+ const overridesContainer = findOverridesObject(configObjectExpression);
216
+ if (!overridesContainer) return;
217
+
218
+ const walkGroups = (selectorMapObjectExpression) => {
219
+ for (const prop of selectorMapObjectExpression.properties) {
220
+ if (prop.type !== 'Property') continue;
221
+ const selector = getPropertyName(prop);
222
+
223
+ if (selector && selector.includes(',')) {
224
+ const kinds = Array.from(
225
+ new Set(
226
+ selector.split(',').map((part) => {
227
+ const segment = part.trim().split(' ').pop();
228
+ return segment.split('.')[0];
229
+ })
230
+ )
231
+ );
232
+ if (kinds.length > 1) {
233
+ context.report({
234
+ node: prop.key,
235
+ messageId: 'mixedSelectorGroup',
236
+ data: { selector, kinds: kinds.join(' vs ') },
237
+ });
238
+ }
239
+ }
240
+
241
+ if (prop.value.type === 'ObjectExpression') {
242
+ const childrenProp = prop.value.properties.find(
243
+ (p) => p.type === 'Property' && getPropertyName(p) === '$children' && p.value.type === 'ObjectExpression'
244
+ );
245
+ if (childrenProp) walkGroups(childrenProp.value);
246
+ }
247
+ }
248
+ };
249
+
250
+ for (const breakpointProp of overridesContainer.properties) {
251
+ if (breakpointProp.type === 'Property' && breakpointProp.value.type === 'ObjectExpression') {
252
+ walkGroups(breakpointProp.value);
253
+ }
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Walk every selector map in `theme.overrides.default/mobile/tablet/desktop`, mirroring
259
+ * the compiler-side `ThemeOverridesErrors` walk (see themeComponents.ts). The compiler
260
+ * reports these problems at the config's USE site (`new SnapTemplates(config)`), because
261
+ * the breakpoint maps are inferred type parameters; the squiggles here are the pinpoint
262
+ * version of the same errors:
263
+ *
264
+ * - Selector KEYS are validated everywhere (top level and every `$children` map).
265
+ *
266
+ * - Under a NAMED selector, prop KEY existence is checked and `$children` is descended
267
+ * into. Prop VALUE types are left to TS, which reports them at the literal already.
268
+ *
269
+ * - Under an open-named dotted selector like `facet.price`, the whole value is
270
+ * `unknown` to TS, so props are fully checked: existence and value types against the
271
+ * REAL props type of the targeted component, resolved live via the type checker,
272
+ * recursing through `$children`.
273
+ *
274
+ * Unknown config keys and breakpoint names are NOT reported here: the rest of the config
275
+ * is a concrete type, so the compiler already reports those on the exact line.
276
+ *
277
+ * Silently does nothing when typed linting isn't configured (no parserServices.program),
278
+ * and fails open on any resolution hiccup - this is an advisory squiggle layer; the
279
+ * compiler's return-type check is the authority.
280
+ */
281
+ function validateOverrideSelectors(configObjectExpression, context) {
282
+ const programAndChecker = getProgramAndChecker(context);
283
+ if (!programAndChecker) return;
284
+
285
+ const overridesContainer = findOverridesObject(configObjectExpression);
286
+ if (!overridesContainer) return;
287
+
288
+ const overridesObjects = collectOverridesObjects(configObjectExpression);
289
+ if (overridesObjects.length === 0) return;
290
+
291
+ const filename = context.filename ?? context.getFilename();
292
+
293
+ for (const overridesObject of overridesObjects) {
294
+ walkSelectorMap(overridesObject, programAndChecker, filename, context);
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Validate each selector key, hand open-named dotted selectors to full prop checking, and
300
+ * recurse through named selectors' $children.
301
+ */
302
+ function walkSelectorMap(selectorMapObjectExpression, programAndChecker, filename, context) {
303
+ for (const prop of selectorMapObjectExpression.properties) {
304
+ if (prop.type !== 'Property' || prop.value.type !== 'ObjectExpression') continue;
305
+
306
+ const selector = getPropertyName(prop);
307
+ if (!selector) continue;
308
+
309
+ // only the selector's final (space-separated) segment determines its component -
310
+ // e.g. 'search facet.price' targets the same 'facet.price' as a bare selector
311
+ const finalSegment = selector.includes(' ') ? selector.split(' ').pop() : selector;
312
+ const dotIndex = finalSegment.indexOf('.');
313
+ const componentType = dotIndex === -1 ? null : finalSegment.slice(0, dotIndex);
314
+
315
+ if (componentType && OPEN_NAMED_COMPONENT_PROPS_TYPE[componentType]) {
316
+ // TS is blind to this whole subtree - full prop checking
317
+ walkUnknownRegionEntry(prop.value, selector, programAndChecker, filename, context);
318
+ continue;
319
+ }
320
+
321
+ if (isKnownSelectorSegment(programAndChecker, filename, finalSegment) === false) {
322
+ context.report({
323
+ node: prop.key,
324
+ messageId: 'unknownOverrideSelector',
325
+ data: { selector, segment: finalSegment },
326
+ });
327
+ continue;
328
+ }
329
+
330
+ // known (or unresolvable - fail open): check this named selector's prop KEYS
331
+ // (TS covers their value types, but not unknown keys - see the doc above) and
332
+ // walk its $children selector map. A customComponent swaps in a component
333
+ // whose subtree isn't known - skip it, same as the compiler-side walk does.
334
+ const hasCustomComponent = prop.value.properties.some(
335
+ (valueProp) => valueProp.type === 'Property' && getPropertyName(valueProp) === 'customComponent'
336
+ );
337
+ if (hasCustomComponent) continue;
338
+
339
+ const resolved = resolveSelectorPropsType(programAndChecker, filename, finalSegment);
340
+ if (resolved) {
341
+ for (const valueProp of prop.value.properties) {
342
+ if (valueProp.type !== 'Property') continue;
343
+ const propName = getPropertyName(valueProp);
344
+ if (!propName || ALWAYS_ALLOWED_OVERRIDE_PROPS.has(propName)) continue;
345
+ if (!resolved.propNames.has(propName)) {
346
+ context.report({
347
+ node: valueProp.key,
348
+ messageId: 'invalidOpenNamedSelectorProp',
349
+ data: {
350
+ value: propName,
351
+ selector,
352
+ componentType: finalSegment,
353
+ typeName: safeTypeToString(programAndChecker, resolved.type),
354
+ // structural keys ($children etc.) aren't component props - keep them out of the suggestion list
355
+ validKeys: Array.from(resolved.propNames)
356
+ .filter((name) => !ALWAYS_ALLOWED_OVERRIDE_PROPS.has(name))
357
+ .join(', '),
358
+ },
359
+ });
360
+ }
361
+ }
362
+ }
363
+
364
+ const childrenProp = prop.value.properties.find(
365
+ (p) => p.type === 'Property' && getPropertyName(p) === '$children' && p.value.type === 'ObjectExpression'
366
+ );
367
+ if (childrenProp) {
368
+ walkSelectorMap(childrenProp.value, programAndChecker, filename, context);
369
+ }
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Whether a selector's final segment resolves to anything in `ThemeComponentsRestricted`:
375
+ * a literal property (named selectors and finite named-dotted forms), or a pattern index
376
+ * signature (template open-named forms like `search.${string}` and the tree-path
377
+ * patterns). Returns true/false, or null when it cannot tell (fail open - never report
378
+ * off a guess).
379
+ */
380
+ function isKnownSelectorSegment(programAndChecker, filename, segment) {
381
+ try {
382
+ const anchor = resolveNamedTypeFromComponentsModule(programAndChecker, 'ThemeComponentsRestricted', filename);
383
+ if (!anchor) return null;
384
+
385
+ const { checker } = programAndChecker;
386
+ if (checker.getPropertyOfType(anchor.type, segment)) return true;
387
+
388
+ const ts = require('typescript');
389
+ const indexInfos = checker.getIndexInfosOfType(anchor.type) || [];
390
+ for (const info of indexInfos) {
391
+ if (keyTypeMatchesName(ts, info.keyType, segment)) return true;
392
+ }
393
+ return false;
394
+ } catch {
395
+ return null;
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Whether a concrete property name matches an index signature's key type. The selector
401
+ * pattern families only ever use `${string}` placeholders, so template literal keys are
402
+ * matched by locating their static text chunks in order (permissive on ambiguity - a
403
+ * false "match" just means no squiggle, never a wrong one).
404
+ */
405
+ function keyTypeMatchesName(ts, keyType, name) {
406
+ if (keyType.flags & ts.TypeFlags.String) return true;
407
+ if (keyType.flags & ts.TypeFlags.StringLiteral) return keyType.value === name;
408
+ if (keyType.flags & ts.TypeFlags.Union) return keyType.types.some((member) => keyTypeMatchesName(ts, member, name));
409
+ if (!(keyType.flags & ts.TypeFlags.TemplateLiteral)) return false;
410
+
411
+ const texts = keyType.texts;
412
+ if (!name.startsWith(texts[0])) return false;
413
+ let pos = texts[0].length;
414
+ for (let i = 1; i < texts.length; i++) {
415
+ const chunk = texts[i];
416
+ if (i === texts.length - 1) {
417
+ if (chunk === '') return name.length >= pos;
418
+ const at = name.lastIndexOf(chunk);
419
+ return at >= pos && at + chunk.length === name.length;
420
+ }
421
+ if (chunk === '') continue;
422
+ const at = name.indexOf(chunk, pos);
423
+ if (at === -1) return false;
424
+ pos = at + chunk.length;
425
+ }
426
+ return true;
427
+ }
428
+
429
+ /**
430
+ * Validate one selector's authored value against its resolved type (props existing +
431
+ * value types), then - unless a customComponent escape hatch is present - recurse into
432
+ * its `$children`, if any, with the same treatment. Used both for the top-level
433
+ * open-named-dotted selector that got us into "unknown" territory, and for every
434
+ * selector found while walking inside it.
435
+ */
436
+ function walkUnknownRegionEntry(valueObjectExpression, selector, programAndChecker, filename, context) {
437
+ // a customComponent swaps in a component whose props aren't known - already
438
+ // unchecked at the type level too (its whole subtree resolves to `unknown`
439
+ // regardless), left unchecked here for the same reason
440
+ const hasCustomComponent = valueObjectExpression.properties.some(
441
+ (valueProp) => valueProp.type === 'Property' && getPropertyName(valueProp) === 'customComponent'
442
+ );
443
+ if (hasCustomComponent) return;
444
+
445
+ const finalSegment = selector.includes(' ') ? selector.split(' ').pop() : selector;
446
+ const resolved = resolveSelectorPropsType(programAndChecker, filename, finalSegment);
447
+ if (resolved) {
448
+ const { propNames: validPropNames, type: resolvedType, sourceFile } = resolved;
449
+
450
+ for (const valueProp of valueObjectExpression.properties) {
451
+ if (valueProp.type !== 'Property') continue;
452
+ const propName = getPropertyName(valueProp);
453
+ if (!propName || ALWAYS_ALLOWED_OVERRIDE_PROPS.has(propName)) continue;
454
+
455
+ if (!validPropNames.has(propName)) {
456
+ context.report({
457
+ node: valueProp.key,
458
+ messageId: 'invalidOpenNamedSelectorProp',
459
+ data: {
460
+ value: propName,
461
+ selector,
462
+ componentType: finalSegment.split('.')[0],
463
+ typeName: safeTypeToString(programAndChecker, resolvedType),
464
+ validKeys: Array.from(validPropNames).join(', '),
465
+ },
466
+ });
467
+ continue;
468
+ }
469
+
470
+ // the key is valid - also check the authored value's type against the
471
+ // real prop type (key existence alone doesn't catch e.g. `color: 5`
472
+ // where `color` is real but expects a string)
473
+ const typeMismatch = checkPropValueType(programAndChecker, resolvedType, sourceFile, propName, valueProp.value);
474
+ if (typeMismatch) {
475
+ context.report({
476
+ node: valueProp.value,
477
+ messageId: 'invalidOpenNamedSelectorPropType',
478
+ data: {
479
+ value: propName,
480
+ selector,
481
+ componentType: finalSegment.split('.')[0],
482
+ typeName: safeTypeToString(programAndChecker, resolvedType),
483
+ expectedType: typeMismatch.expectedType,
484
+ actualType: typeMismatch.actualType,
485
+ },
486
+ });
487
+ }
488
+ }
489
+ }
490
+ // couldn't resolve (e.g. renamed/unrecognized selector) - fail open, never
491
+ // false-positive, but still recurse into $children below since that part is
492
+ // independent of whether we could name this particular selector's own type
493
+
494
+ const childrenProp = valueObjectExpression.properties.find(
495
+ (p) => p.type === 'Property' && getPropertyName(p) === '$children' && p.value.type === 'ObjectExpression'
496
+ );
497
+ if (!childrenProp) return;
498
+
499
+ for (const childProp of childrenProp.value.properties) {
500
+ if (childProp.type !== 'Property' || childProp.value.type !== 'ObjectExpression') continue;
501
+ const childSelector = getPropertyName(childProp);
502
+ if (!childSelector) continue;
503
+
504
+ // selector keys inside this blind region get no checking from TS at all -
505
+ // squiggle unresolvable ones (the compiler-side check errors on them too)
506
+ const childSegment = childSelector.includes(' ') ? childSelector.split(' ').pop() : childSelector;
507
+ const childDotIndex = childSegment.indexOf('.');
508
+ const childComponentType = childDotIndex === -1 ? null : childSegment.slice(0, childDotIndex);
509
+ if (!(childComponentType && OPEN_NAMED_COMPONENT_PROPS_TYPE[childComponentType])) {
510
+ if (isKnownSelectorSegment(programAndChecker, filename, childSegment) === false) {
511
+ context.report({
512
+ node: childProp.key,
513
+ messageId: 'unknownOverrideSelector',
514
+ data: { selector: childSelector, segment: childSegment },
515
+ });
516
+ continue;
517
+ }
518
+ }
519
+
520
+ walkUnknownRegionEntry(childProp.value, childSelector, programAndChecker, filename, context);
521
+ }
522
+ }
523
+
524
+ /**
525
+ * Resolve a selector's final segment to its real props type, live via the type checker:
526
+ * first as a literal property of `ThemeComponentsRestricted` (covers every bare selector
527
+ * and every named-dotted selector with a finite suffix union, e.g. `icon.next` - i.e.
528
+ * everything TypeScript itself would resolve precisely at the top level), falling back
529
+ * to OPEN_NAMED_COMPONENT_PROPS_TYPE only for the open-named-dotted form that isn't a
530
+ * literal property on that type (e.g. `facet.custom`).
531
+ */
532
+ function resolveSelectorPropsType(programAndChecker, filename, finalSegment) {
533
+ const { checker } = programAndChecker;
534
+ const anchor = resolveNamedTypeFromComponentsModule(programAndChecker, 'ThemeComponentsRestricted', filename);
535
+
536
+ if (anchor) {
537
+ try {
538
+ const propSymbol = checker.getPropertyOfType(anchor.type, finalSegment);
539
+ if (propSymbol) {
540
+ // the property is optional (`prop?: X`), so its declared type is `X | undefined` -
541
+ // getPropertiesOfType on that union intersects across members and returns nothing,
542
+ // since `undefined` has no properties, so unwrap it first
543
+ const propType = checker.getNonNullableType(checker.getTypeOfSymbolAtLocation(propSymbol, anchor.sourceFile));
544
+ return {
545
+ propNames: new Set(checker.getPropertiesOfType(propType).map((p) => p.name)),
546
+ type: propType,
547
+ sourceFile: anchor.sourceFile,
548
+ };
549
+ }
550
+ } catch {
551
+ // fall through to the open-named fallback below
552
+ }
553
+ }
554
+
555
+ const componentType = finalSegment.split('.')[0];
556
+ const typeName = OPEN_NAMED_COMPONENT_PROPS_TYPE[componentType];
557
+ if (!typeName) return null;
558
+ return resolveNamedTypeFromComponentsModule(programAndChecker, typeName, filename);
559
+ }
560
+
561
+ /** `checker.typeToString`, defensively - only used to format a message, never gates behavior. */
562
+ function safeTypeToString(programAndChecker, type) {
563
+ try {
564
+ return programAndChecker.checker.typeToString(type);
565
+ } catch {
566
+ return '(unresolved)';
567
+ }
568
+ }
569
+
570
+ /**
571
+ * Collect the ObjectExpression for each of theme.overrides.default/mobile/tablet/desktop
572
+ * that's present as a plain object literal (a spread can't be statically resolved here).
573
+ */
574
+ /**
575
+ * The theme.overrides ObjectExpression itself, or null when absent/not a literal.
576
+ */
577
+ function findOverridesObject(configObjectExpression) {
578
+ const themeProp = findProperty(configObjectExpression, 'theme');
579
+ if (!themeProp || themeProp.value.type !== 'ObjectExpression') return null;
580
+
581
+ const overridesProp = findProperty(themeProp.value, 'overrides');
582
+ if (!overridesProp || overridesProp.value.type !== 'ObjectExpression') return null;
583
+
584
+ return overridesProp.value;
585
+ }
586
+
587
+ function collectOverridesObjects(configObjectExpression) {
588
+ const themeProp = findProperty(configObjectExpression, 'theme');
589
+ if (!themeProp || themeProp.value.type !== 'ObjectExpression') return [];
590
+
591
+ const overridesProp = findProperty(themeProp.value, 'overrides');
592
+ if (!overridesProp || overridesProp.value.type !== 'ObjectExpression') return [];
593
+
594
+ const results = [];
595
+ for (const breakpoint of ['default', 'mobile', 'tablet', 'desktop']) {
596
+ const breakpointProp = findProperty(overridesProp.value, breakpoint);
597
+ if (breakpointProp && breakpointProp.value.type === 'ObjectExpression') {
598
+ results.push(breakpointProp.value);
599
+ }
600
+ }
601
+ return results;
602
+ }
603
+
604
+ /**
605
+ * Get { program, checker, esTreeNodeToTSNodeMap } from typed-linting parser services,
606
+ * or null if this rule is running without type information (e.g. no
607
+ * `parserOptions.project` configured).
608
+ */
609
+ function getProgramAndChecker(context) {
610
+ const services = context.sourceCode?.parserServices ?? context.parserServices;
611
+ if (!services || !services.program) return null;
612
+ return {
613
+ program: services.program,
614
+ checker: services.program.getTypeChecker(),
615
+ esTreeNodeToTSNodeMap: services.esTreeNodeToTSNodeMap,
616
+ };
617
+ }
618
+
619
+ /**
620
+ * Check an authored prop value's type against the real prop's declared type. Returns
621
+ * { expectedType, actualType } (as display strings) if they're incompatible, or null if
622
+ * they're compatible or couldn't be checked (fail open - never false-positive).
623
+ */
624
+ function checkPropValueType(programAndChecker, resolvedType, sourceFile, propName, valueNode) {
625
+ const { checker, esTreeNodeToTSNodeMap } = programAndChecker;
626
+ if (!esTreeNodeToTSNodeMap) return null;
627
+
628
+ try {
629
+ const propSymbol = checker.getPropertyOfType(resolvedType, propName);
630
+ const tsValueNode = esTreeNodeToTSNodeMap.get(valueNode);
631
+ if (!propSymbol || !tsValueNode) return null;
632
+
633
+ const expectedType = checker.getTypeOfSymbolAtLocation(propSymbol, sourceFile);
634
+ const actualType = checker.getTypeAtLocation(tsValueNode);
635
+ if (checker.isTypeAssignableTo(actualType, expectedType)) return null;
636
+
637
+ return {
638
+ expectedType: checker.typeToString(expectedType),
639
+ actualType: checker.typeToString(actualType),
640
+ };
641
+ } catch {
642
+ return null;
643
+ }
644
+ }
645
+
646
+ /**
647
+ * Resolve the real property names of an exported type from
648
+ * '@athoscommerce/snap-preact/components', by name, live from the type checker.
649
+ * Cached per Program so each type is only resolved once per lint run - but only
650
+ * successful resolutions are cached. A failure (thrown error, or a symbol/type
651
+ * genuinely not found) is never memoized: the Program this is keyed on outlives a
652
+ * single lint run (typescript-eslint reuses it across calls with an unchanged
653
+ * tsconfig+file), so caching a one-off failure would silently and permanently
654
+ * disable this check for that type for the rest of the Program's lifetime instead
655
+ * of just this one call.
656
+ */
657
+ function resolveNamedTypeFromComponentsModule(programAndChecker, typeName, containingFileName) {
658
+ const { program, checker } = programAndChecker;
659
+
660
+ let cache = openNamedPropsCacheByProgram.get(program);
661
+ if (!cache) {
662
+ cache = new Map();
663
+ openNamedPropsCacheByProgram.set(program, cache);
664
+ }
665
+ if (cache.has(typeName)) return cache.get(typeName);
666
+
667
+ let result = null;
668
+ try {
669
+ const ts = require('typescript');
670
+ const resolved = ts.resolveModuleName('@athoscommerce/snap-preact/components', containingFileName, program.getCompilerOptions(), ts.sys);
671
+ const resolvedFileName = resolved.resolvedModule && resolved.resolvedModule.resolvedFileName;
672
+ const sourceFile = resolvedFileName && program.getSourceFile(resolvedFileName);
673
+ const moduleSymbol = sourceFile && checker.getSymbolAtLocation(sourceFile);
674
+ const exportSymbol = moduleSymbol && checker.getExportsOfModule(moduleSymbol).find((s) => s.name === typeName);
675
+
676
+ if (exportSymbol) {
677
+ const type = checker.getDeclaredTypeOfSymbol(exportSymbol);
678
+ result = {
679
+ propNames: new Set(checker.getPropertiesOfType(type).map((p) => p.name)),
680
+ type,
681
+ sourceFile,
682
+ };
683
+ }
684
+ } catch {
685
+ result = null;
686
+ }
687
+
688
+ if (result) cache.set(typeName, result);
689
+ return result;
170
690
  }
171
691
 
172
692
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@athoscommerce/snap-preact",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "Snap Preact",
5
5
  "main": "dist/cjs/src/index.js",
6
6
  "module": "dist/esm/src/index.js",
@@ -28,16 +28,16 @@
28
28
  "storybook": "storybook dev -c ./components/.storybook -p 6006"
29
29
  },
30
30
  "dependencies": {
31
- "@athoscommerce/snap-client": "1.12.0",
32
- "@athoscommerce/snap-controller": "1.12.0",
33
- "@athoscommerce/snap-event-manager": "1.12.0",
34
- "@athoscommerce/snap-logger": "1.12.0",
35
- "@athoscommerce/snap-platforms": "1.12.0",
36
- "@athoscommerce/snap-profiler": "1.12.0",
37
- "@athoscommerce/snap-store-mobx": "1.12.0",
38
- "@athoscommerce/snap-toolbox": "1.12.0",
39
- "@athoscommerce/snap-tracker": "1.12.0",
40
- "@athoscommerce/snap-url-manager": "1.12.0",
31
+ "@athoscommerce/snap-client": "1.13.0",
32
+ "@athoscommerce/snap-controller": "1.13.0",
33
+ "@athoscommerce/snap-event-manager": "1.13.0",
34
+ "@athoscommerce/snap-logger": "1.13.0",
35
+ "@athoscommerce/snap-platforms": "1.13.0",
36
+ "@athoscommerce/snap-profiler": "1.13.0",
37
+ "@athoscommerce/snap-store-mobx": "1.13.0",
38
+ "@athoscommerce/snap-toolbox": "1.13.0",
39
+ "@athoscommerce/snap-tracker": "1.13.0",
40
+ "@athoscommerce/snap-url-manager": "1.13.0",
41
41
  "@emotion/react": "11.13.0",
42
42
  "acorn": "8.16.0",
43
43
  "classnames": "^2.3.2",
@@ -45,7 +45,7 @@
45
45
  "deepmerge": "4.3.1",
46
46
  "dequal": "2.0.3",
47
47
  "intersection-observer": "0.12.2",
48
- "is-plain-object": "5.0.0",
48
+ "is-plain-object": "5.1.0",
49
49
  "react-ranger": "2.1.0",
50
50
  "swiper": "11.0.7"
51
51
  },
@@ -106,5 +106,5 @@
106
106
  "./webpack": "./webpack/index.js",
107
107
  "./eslintplugin": "./eslint/src/index.cjs"
108
108
  },
109
- "gitHead": "cc4e3d016b33074fe8242a7b7ea3c625e2c8708b"
109
+ "gitHead": "f5ec33b6fa8255ab40fd7e2d5ea8f73a37dc783b"
110
110
  }