@ashstack/lint 0.1.0 → 0.2.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 (102) hide show
  1. package/README.md +1 -1
  2. package/dist/core/index.js +1 -222
  3. package/dist/core/rules/core/components-tsx-only.js +1 -37
  4. package/dist/core/rules/core/hoist-intl.js +1 -32
  5. package/dist/core/rules/core/index.js +1 -18
  6. package/dist/core/rules/core/no-comments.js +1 -194
  7. package/dist/core/rules/core/no-naming-convention.js +1 -143
  8. package/dist/core/rules/core/use-design-system.js +1 -147
  9. package/dist/core/rules/zod/index.js +1 -12
  10. package/dist/core/rules/zod/prefer-enum.js +1 -44
  11. package/dist/index.js +1 -3
  12. package/dist/lib/ast.js +1 -162
  13. package/dist/lib/detect.js +1 -42
  14. package/dist/lib/merge.js +1 -18
  15. package/dist/lib/module.js +1 -49
  16. package/dist/lib/types.js +1 -1
  17. package/dist/react/index.js +1 -126
  18. package/dist/react/rules/i18n/index.js +1 -16
  19. package/dist/react/rules/i18n/no-bare-attrs.js +1 -38
  20. package/dist/react/rules/i18n/no-bare-text.js +1 -26
  21. package/dist/react/rules/i18n/no-bare-toast.js +1 -38
  22. package/dist/react/rules/query/index.js +1 -20
  23. package/dist/react/rules/query/next-page-param-undefined.js +1 -26
  24. package/dist/react/rules/query/no-deprecated-filters.js +1 -63
  25. package/dist/react/rules/query/no-fetch-in-query-fn.js +1 -22
  26. package/dist/react/rules/query/no-inline-keys.js +1 -107
  27. package/dist/react/rules/query/require-destructured-hooks.js +1 -41
  28. package/dist/react/rules/query/shared.js +1 -10
  29. package/dist/react/rules/zustand/index.js +1 -12
  30. package/dist/react/rules/zustand/require-selector.js +1 -62
  31. package/dist/react-native/index.js +1 -113
  32. package/dist/react-native/rules/keyboard/index.js +1 -18
  33. package/dist/react-native/rules/legend-list/index.js +1 -51
  34. package/dist/react-native/rules/legend-list/no-flex-in-content-container.js +1 -77
  35. package/dist/react-native/rules/legend-list/no-index-key-extractor.js +1 -26
  36. package/dist/react-native/rules/legend-list/no-inline-data.js +1 -25
  37. package/dist/react-native/rules/legend-list/no-inline-extra-data.js +1 -21
  38. package/dist/react-native/rules/legend-list/no-inline-render-item-props.js +1 -35
  39. package/dist/react-native/rules/legend-list/no-mixed-children.js +1 -24
  40. package/dist/react-native/rules/legend-list/no-remount-key.js +1 -18
  41. package/dist/react-native/rules/legend-list/no-scrollview-map.js +1 -27
  42. package/dist/react-native/rules/legend-list/no-unsupported-props.js +1 -29
  43. package/dist/react-native/rules/legend-list/required-props.js +1 -23
  44. package/dist/react-native/rules/legend-list/shared.js +1 -25
  45. package/dist/react-native/rules/legend-list/typed-items-need-item-type.js +1 -49
  46. package/dist/react-native/rules/legend-state/index.js +1 -35
  47. package/dist/react-native/rules/legend-state/naming.js +1 -26
  48. package/dist/react-native/rules/legend-state/no-assignment.js +1 -31
  49. package/dist/react-native/rules/legend-state/no-nested-observable.js +1 -23
  50. package/dist/react-native/rules/legend-state/no-object-selector.js +1 -47
  51. package/dist/react-native/rules/legend-state/no-observable-in-component.js +1 -46
  52. package/dist/react-native/rules/legend-state/no-peek-in-selector.js +1 -67
  53. package/dist/react-native/rules/legend-state/no-react-mirror.js +1 -33
  54. package/dist/react-native/rules/legend-state/no-untracked-get-in-jsx.js +1 -52
  55. package/dist/react-native/rules/legend-state/shared.js +1 -23
  56. package/dist/react-native/rules/react-native/hoist-stateless-function.js +1 -59
  57. package/dist/react-native/rules/react-native/index.js +1 -50
  58. package/dist/react-native/rules/react-native/no-conditional-style-array.js +1 -23
  59. package/dist/react-native/rules/react-native/no-dynamic-import.js +1 -18
  60. package/dist/react-native/rules/react-native/no-keyboard-will-events.js +1 -18
  61. package/dist/react-native/rules/react-native/no-leaked-render.js +1 -43
  62. package/dist/react-native/rules/react-native/no-manual-memo.js +1 -87
  63. package/dist/react-native/rules/react-native/no-redundant-view-nesting.js +1 -36
  64. package/dist/react-native/rules/react-native/no-rn-image-network-source.js +1 -58
  65. package/dist/react-native/rules/react-native/no-rn-namespace-import.js +1 -38
  66. package/dist/react-native/rules/react-native/no-scroll-position-state.js +1 -26
  67. package/dist/react-native/rules/react-native/no-unlabeled-icon-pressable.js +1 -62
  68. package/dist/react-native/rules/reanimated/animated-reaction-safety.js +1 -110
  69. package/dist/react-native/rules/reanimated/animated-style-needs-animated-component.js +1 -72
  70. package/dist/react-native/rules/reanimated/animated-updater-purity.js +1 -38
  71. package/dist/react-native/rules/reanimated/gpu-properties-only.js +1 -80
  72. package/dist/react-native/rules/reanimated/hoist-layout-animation-builder.js +1 -43
  73. package/dist/react-native/rules/reanimated/index.js +1 -51
  74. package/dist/react-native/rules/reanimated/interpolate-needs-clamp.js +1 -41
  75. package/dist/react-native/rules/reanimated/no-react-state-from-continuous-worklet.js +1 -86
  76. package/dist/react-native/rules/reanimated/no-shared-value-dot-value.js +1 -112
  77. package/dist/react-native/rules/reanimated/prefer-lazy-shared-value-initializer.js +1 -24
  78. package/dist/react-native/rules/reanimated/schedule-on-rn-scope.js +1 -21
  79. package/dist/react-native/rules/reanimated/shared-value-usage.js +1 -105
  80. package/dist/react-native/rules/reanimated/shared.js +1 -13
  81. package/dist/react-native/rules/skia/canvas-opaque.js +1 -49
  82. package/dist/react-native/rules/skia/index.js +1 -21
  83. package/dist/react-native/rules/turbo-image/index.js +1 -14
  84. package/dist/react-native/rules/turbo-image/require-cache-policy.js +1 -21
  85. package/dist/react-native/rules/turbo-image/require-resize.js +1 -22
  86. package/dist/react-native/rules/unistyles/animated-theme.js +1 -37
  87. package/dist/react-native/rules/unistyles/content-container.js +1 -73
  88. package/dist/react-native/rules/unistyles/in-sheet.js +1 -116
  89. package/dist/react-native/rules/unistyles/index.js +1 -58
  90. package/dist/react-native/rules/unistyles/insets.js +1 -92
  91. package/dist/react-native/rules/unistyles/no-hardcoded-color.js +1 -30
  92. package/dist/react-native/rules/unistyles/no-hardcoded-spacing.js +1 -78
  93. package/dist/react-native/rules/unistyles/no-margin.js +1 -19
  94. package/dist/react-native/rules/unistyles/no-style-spread.js +1 -41
  95. package/dist/react-native/rules/unistyles/no-unused-styles.js +1 -99
  96. package/dist/react-native/rules/unistyles/rtl-style-call.js +1 -42
  97. package/dist/react-native/rules/unistyles/shared.js +1 -70
  98. package/dist/react-native/rules/unistyles/theme-screen-component.js +1 -22
  99. package/dist/react-native/rules/unistyles/theme-style-attr.js +1 -18
  100. package/dist/react-native/stylesheet.js +1 -44
  101. package/package.json +3 -4
  102. package/RULES.md +0 -2881
@@ -1,62 +1 @@
1
- import { problem } from "../../../lib/ast.js";
2
- const MESSAGES = {
3
- zustandBare: "Pass a selector to this store hook, for example `useSettingsStore(state => state.theme)`, so it re-renders only on that slice. Use `.getState()` instead for an imperative read.",
4
- zustandUndefined: "Replace `undefined` with a selector such as `state => state.theme` — it still subscribes to the whole store otherwise. Use `.getState()` instead for an imperative read.",
5
- };
6
- const STORE_MODULE = /^(?:@\/stores\/|(?:\.\.?\/)+stores\/)[^"']*-store$/;
7
- const STORE_HOOK = /^use[A-Za-z0-9_$]*Store$/;
8
- const storeHookLocalName = (specifier) => {
9
- if (specifier.type !== "ImportSpecifier")
10
- return null;
11
- const imported = specifier.imported.type === "Identifier" ? specifier.imported.name : "";
12
- const local = specifier.local.name;
13
- return STORE_HOOK.test(imported) || STORE_HOOK.test(local) ? local : null;
14
- };
15
- export const requireSelector = problem("Require a selector on a store hook, rather than no arguments or `undefined` in its place.", {
16
- createOnce(context) {
17
- const hooks = new Set();
18
- const calls = [];
19
- return {
20
- before() {
21
- hooks.clear();
22
- calls.length = 0;
23
- },
24
- ImportDeclaration(node) {
25
- if (!STORE_MODULE.test(node.source.value))
26
- return;
27
- for (const specifier of node.specifiers) {
28
- const local = storeHookLocalName(specifier);
29
- if (local !== null)
30
- hooks.add(local);
31
- }
32
- },
33
- CallExpression(node) {
34
- const { callee } = node;
35
- if (callee.type !== "Identifier")
36
- return;
37
- const { name } = callee;
38
- if (!hooks.has(name) && !STORE_HOOK.test(name))
39
- return;
40
- const args = node.arguments;
41
- if (args.length === 0) {
42
- calls.push({ node, name, bare: true });
43
- return;
44
- }
45
- const [first] = args;
46
- if (args.length === 1 && first?.type === "Identifier" && first.name === "undefined") {
47
- calls.push({ node, name, bare: false });
48
- }
49
- },
50
- "Program:exit"() {
51
- for (const call of calls) {
52
- if (!hooks.has(call.name))
53
- continue;
54
- context.report({
55
- node: call.node,
56
- message: call.bare ? MESSAGES.zustandBare : MESSAGES.zustandUndefined,
57
- });
58
- }
59
- },
60
- };
61
- },
62
- });
1
+ import{problem as u}from"../../../lib/ast.js";const c={zustandBare:"Pass a selector to this store hook, for example `useSettingsStore(state => state.theme)`, so it re-renders only on that slice. Use `.getState()` instead for an imperative read.",zustandUndefined:"Replace `undefined` with a selector such as `state => state.theme` — it still subscribes to the whole store otherwise. Use `.getState()` instead for an imperative read."},f=/^(?:@\/stores\/|(?:\.\.?\/)+stores\/)[^"']*-store$/,i=/^use[A-Za-z0-9_$]*Store$/,d=(s)=>{if(s.type!=="ImportSpecifier")return null;const o=s.imported.type==="Identifier"?s.imported.name:"",t=s.local.name;return i.test(o)||i.test(t)?t:null};export const requireSelector=u("Require a selector on a store hook, rather than no arguments or `undefined` in its place.",{createOnce(s){const o=new Set,t=[];return{before(){o.clear();t.length=0},ImportDeclaration(e){if(!f.test(e.source.value))return;for(const n of e.specifiers){const r=d(n);if(r!==null)o.add(r)}},CallExpression(e){const{callee:n}=e;if(n.type!=="Identifier")return;const{name:r}=n;if(!o.has(r)&&!i.test(r))return;const a=e.arguments;if(a.length===0){t.push({node:e,name:r,bare:!0});return}const[l]=a;if(a.length===1&&l?.type==="Identifier"&&l.name==="undefined")t.push({node:e,name:r,bare:!1})},"Program:exit"(){for(const e of t){if(!o.has(e.name))continue;s.report({node:e.node,message:e.bare?c.zustandBare:c.zustandUndefined})}}}}});
@@ -1,113 +1 @@
1
- import { TEST_FILES, coreModules } from "../core/index.js";
2
- import { mergeConfigs } from "../lib/merge.js";
3
- import { composeModules } from "../lib/module.js";
4
- import react, { reactModules } from "../react/index.js";
5
- import keyboardModule from "./rules/keyboard/index.js";
6
- import legendListModule from "./rules/legend-list/index.js";
7
- import legendStateModule from "./rules/legend-state/index.js";
8
- import reactNativeModule from "./rules/react-native/index.js";
9
- import reanimatedModule from "./rules/reanimated/index.js";
10
- import skiaModule from "./rules/skia/index.js";
11
- import turboImageModule from "./rules/turbo-image/index.js";
12
- import unistylesModule from "./rules/unistyles/index.js";
13
- export const reactNativeModules = [
14
- reactNativeModule,
15
- unistylesModule,
16
- legendListModule,
17
- legendStateModule,
18
- reanimatedModule,
19
- turboImageModule,
20
- skiaModule,
21
- keyboardModule,
22
- ];
23
- export const banGroups = [
24
- {
25
- packages: ["react-native-gesture-handler"],
26
- restrictedImports: {
27
- paths: [
28
- {
29
- name: "react-native",
30
- importNames: [
31
- "Pressable",
32
- "TouchableOpacity",
33
- "TouchableHighlight",
34
- "TouchableWithoutFeedback",
35
- "TouchableNativeFeedback",
36
- ],
37
- message: "Import `Pressable` from `react-native-gesture-handler` so it cooperates with the gesture system instead of competing with it. Exception: inside a SwiftUI/Compose host view touches never reach `GestureHandlerRootView` — keep React Native's `Pressable` there and suppress this rule with the host named in the reason.",
38
- },
39
- ],
40
- },
41
- },
42
- {
43
- packages: ["expo-router"],
44
- restrictedImports: {
45
- patterns: [
46
- {
47
- group: ["@react-navigation/stack", "@react-navigation/bottom-tabs"],
48
- message: "Use expo-router's `Stack` / `NativeTabs` instead of a raw react-navigation navigator.",
49
- },
50
- ],
51
- },
52
- },
53
- {
54
- packages: ["expo-font"],
55
- restrictedImports: {
56
- paths: [
57
- {
58
- name: "expo-font",
59
- importNames: ["useFonts", "loadAsync"],
60
- message: "Declare the fonts in the `expo-font` config plugin so they load at build time, instead of `useFonts`/`loadAsync` at runtime.",
61
- },
62
- ],
63
- },
64
- },
65
- {
66
- packages: ["react-native-quick-crypto"],
67
- restrictedImports: {
68
- paths: [
69
- {
70
- name: "crypto-js",
71
- message: "Use `react-native-quick-crypto` instead — `crypto-js` is pure JavaScript and runs orders of magnitude slower.",
72
- },
73
- ],
74
- },
75
- },
76
- ];
77
- const FORBID_EMPTY_NOOP_HANDLERS = {
78
- "eslint/no-empty": "error",
79
- "eslint/no-empty-function": "error",
80
- };
81
- const ALLOW_GESTURE_AND_ANIMATION_EFFECTS = {
82
- "react-effect/no-event-handler": "off",
83
- };
84
- /**
85
- * React Native / Expo entry — everything in react plus generic RN rules, with
86
- * per-library modules (unistyles, legendList, legendState, reanimated,
87
- * turboImage, skia, keyboard) auto-detected from your dependencies. Your
88
- * `rules` block always overrides.
89
- */
90
- const reactNative = (options = {}) => {
91
- const composed = composeModules([...coreModules, ...reactModules, ...reactNativeModules], options, banGroups);
92
- return mergeConfigs(react(options), {
93
- jsPlugins: composed.jsPlugins,
94
- globals: { __DEV__: "readonly" },
95
- rules: {
96
- ...FORBID_EMPTY_NOOP_HANDLERS,
97
- ...ALLOW_GESTURE_AND_ANIMATION_EFFECTS,
98
- "no-restricted-imports": ["error", composed.restricted],
99
- ...composed.rules,
100
- },
101
- ignorePatterns: ["**/.expo/**", "**/android/**", "**/ios/**"],
102
- overrides: [
103
- {
104
- files: TEST_FILES,
105
- rules: {
106
- "@ashstack/react-native/no-dynamic-import": "off",
107
- "@ashstack/react-native/hoist-stateless-function": "off",
108
- },
109
- },
110
- ],
111
- });
112
- };
113
- export default reactNative;
1
+ import{TEST_FILES as o,coreModules as r}from"../core/index.js";import{mergeConfigs as a}from"../lib/merge.js";import{composeModules as s}from"../lib/module.js";import i,{reactModules as n}from"../react/index.js";import c from"./rules/keyboard/index.js";import m from"./rules/legend-list/index.js";import p from"./rules/legend-state/index.js";import d from"./rules/react-native/index.js";import u from"./rules/reanimated/index.js";import l from"./rules/skia/index.js";import f from"./rules/turbo-image/index.js";import g from"./rules/unistyles/index.js";export const reactNativeModules=[d,g,m,p,u,f,l,c],banGroups=[{packages:["react-native-gesture-handler"],restrictedImports:{paths:[{name:"react-native",importNames:["Pressable","TouchableOpacity","TouchableHighlight","TouchableWithoutFeedback","TouchableNativeFeedback"],message:"Import `Pressable` from `react-native-gesture-handler` so it cooperates with the gesture system instead of competing with it. Exception: inside a SwiftUI/Compose host view touches never reach `GestureHandlerRootView` — keep React Native's `Pressable` there and suppress this rule with the host named in the reason."}]}},{packages:["expo-router"],restrictedImports:{patterns:[{group:["@react-navigation/stack","@react-navigation/bottom-tabs"],message:"Use expo-router's `Stack` / `NativeTabs` instead of a raw react-navigation navigator."}]}},{packages:["expo-font"],restrictedImports:{paths:[{name:"expo-font",importNames:["useFonts","loadAsync"],message:"Declare the fonts in the `expo-font` config plugin so they load at build time, instead of `useFonts`/`loadAsync` at runtime."}]}},{packages:["react-native-quick-crypto"],restrictedImports:{paths:[{name:"crypto-js",message:"Use `react-native-quick-crypto` instead — `crypto-js` is pure JavaScript and runs orders of magnitude slower."}]}}];const h={"eslint/no-empty":"error","eslint/no-empty-function":"error"},v={"react-effect/no-event-handler":"off"},b=(t={})=>{const e=s([...r,...n,...reactNativeModules],t,banGroups);return a(i(t),{jsPlugins:e.jsPlugins,globals:{__DEV__:"readonly"},rules:{...h,...v,"no-restricted-imports":["error",e.restricted],...e.rules},ignorePatterns:["**/.expo/**","**/android/**","**/ios/**"],overrides:[{files:o,rules:{"@ashstack/react-native/no-dynamic-import":"off","@ashstack/react-native/hoist-stateless-function":"off"}}]})};export default b;
@@ -1,18 +1 @@
1
- import { defineModule } from "../../../lib/module.js";
2
- export default defineModule({
3
- meta: { name: "@ashstack/keyboard" },
4
- url: import.meta.url,
5
- packages: ["react-native-keyboard-controller"],
6
- option: "keyboard",
7
- docsWhen: "auto-enabled when `react-native-keyboard-controller` is a dependency",
8
- rules: {},
9
- restrictedImports: {
10
- paths: [
11
- {
12
- name: "react-native",
13
- importNames: ["KeyboardAvoidingView"],
14
- message: "Import `KeyboardAvoidingView` from `react-native-keyboard-controller` — same props, and it tracks the keyboard per frame instead of waiting for Android's post-animation `keyboardDidShow`. For a scrolling form use its `KeyboardAwareScrollView`; to just shift a view, use `rt.insets.ime`.",
15
- },
16
- ],
17
- },
18
- });
1
+ import{defineModule as e}from"../../../lib/module.js";export default e({meta:{name:"@ashstack/keyboard"},url:import.meta.url,packages:["react-native-keyboard-controller"],option:"keyboard",docsWhen:"auto-enabled when `react-native-keyboard-controller` is a dependency",rules:{},restrictedImports:{paths:[{name:"react-native",importNames:["KeyboardAvoidingView"],message:"Import `KeyboardAvoidingView` from `react-native-keyboard-controller` — same props, and it tracks the keyboard per frame instead of waiting for Android's post-animation `keyboardDidShow`. For a scrolling form use its `KeyboardAwareScrollView`; to just shift a view, use `rt.insets.ime`."}]}});
@@ -1,51 +1 @@
1
- import { defineModule } from "../../../lib/module.js";
2
- import { noFlexInContentContainer } from "./no-flex-in-content-container.js";
3
- import { noIndexKeyExtractor } from "./no-index-key-extractor.js";
4
- import { noInlineData } from "./no-inline-data.js";
5
- import { noInlineExtraData } from "./no-inline-extra-data.js";
6
- import { noInlineRenderItemProps } from "./no-inline-render-item-props.js";
7
- import { noMixedChildren } from "./no-mixed-children.js";
8
- import { noRemountKey } from "./no-remount-key.js";
9
- import { noScrollviewMap } from "./no-scrollview-map.js";
10
- import { noUnsupportedProps } from "./no-unsupported-props.js";
11
- import { requiredProps } from "./required-props.js";
12
- import { typedItemsNeedItemType } from "./typed-items-need-item-type.js";
13
- export default defineModule({
14
- meta: { name: "@ashstack/legend-list" },
15
- rules: {
16
- "required-props": requiredProps,
17
- "no-index-key-extractor": noIndexKeyExtractor,
18
- "no-remount-key": noRemountKey,
19
- "no-inline-data": noInlineData,
20
- "no-inline-extra-data": noInlineExtraData,
21
- "no-inline-render-item-props": noInlineRenderItemProps,
22
- "no-mixed-children": noMixedChildren,
23
- "no-flex-in-content-container": noFlexInContentContainer,
24
- "typed-items-need-item-type": typedItemsNeedItemType,
25
- "no-scrollview-map": noScrollviewMap,
26
- "no-unsupported-props": noUnsupportedProps,
27
- },
28
- url: import.meta.url,
29
- packages: ["@legendapp/list"],
30
- option: "legendList",
31
- docsWhen: "auto-enabled when `@legendapp/list` is a dependency",
32
- restrictedImports: {
33
- paths: [
34
- {
35
- name: "react-native",
36
- importNames: ["FlatList", "SectionList", "VirtualizedList"],
37
- message: "Render this list with `LegendList` from `@legendapp/list/react-native`; a `ScrollView` plus `map` is fine for fewer than 20 static items.",
38
- },
39
- {
40
- name: "@shopify/flash-list",
41
- message: "Import `LegendList` from `@legendapp/list/react-native` — Legend List replaced FlashList in this stack.",
42
- },
43
- ],
44
- patterns: [
45
- {
46
- group: ["@legendapp/list"],
47
- message: "Import from the platform entrypoint `@legendapp/list/react-native`, not the package root.",
48
- },
49
- ],
50
- },
51
- });
1
+ import{defineModule as e}from"../../../lib/module.js";import{noFlexInContentContainer as t}from"./no-flex-in-content-container.js";import{noIndexKeyExtractor as n}from"./no-index-key-extractor.js";import{noInlineData as o}from"./no-inline-data.js";import{noInlineExtraData as r}from"./no-inline-extra-data.js";import{noInlineRenderItemProps as i}from"./no-inline-render-item-props.js";import{noMixedChildren as a}from"./no-mixed-children.js";import{noRemountKey as p}from"./no-remount-key.js";import{noScrollviewMap as s}from"./no-scrollview-map.js";import{noUnsupportedProps as m}from"./no-unsupported-props.js";import{requiredProps as l}from"./required-props.js";import{typedItemsNeedItemType as d}from"./typed-items-need-item-type.js";export default e({meta:{name:"@ashstack/legend-list"},rules:{"required-props":l,"no-index-key-extractor":n,"no-remount-key":p,"no-inline-data":o,"no-inline-extra-data":r,"no-inline-render-item-props":i,"no-mixed-children":a,"no-flex-in-content-container":t,"typed-items-need-item-type":d,"no-scrollview-map":s,"no-unsupported-props":m},url:import.meta.url,packages:["@legendapp/list"],option:"legendList",docsWhen:"auto-enabled when `@legendapp/list` is a dependency",restrictedImports:{paths:[{name:"react-native",importNames:["FlatList","SectionList","VirtualizedList"],message:"Render this list with `LegendList` from `@legendapp/list/react-native`; a `ScrollView` plus `map` is fine for fewer than 20 static items."},{name:"@shopify/flash-list",message:"Import `LegendList` from `@legendapp/list/react-native` — Legend List replaced FlashList in this stack."}],patterns:[{group:["@legendapp/list"],message:"Import from the platform entrypoint `@legendapp/list/react-native`, not the package root."}]}});
@@ -1,77 +1 @@
1
- import { attributeName, gate, problem } from "../../../lib/ast.js";
2
- import { isStyleSheetCreate, propertyName, stylesObjectOf } from "../../stylesheet.js";
3
- import { expressionOf, isListElement } from "./shared.js";
4
- const MESSAGE = "Move `flex` onto `style` and keep `contentContainerStyle` for padding inside the content. On the content container it sizes the content to the viewport, so the list measures as zero height and renders a blank screen with no error.";
5
- const hasFlexKey = (node) => node?.type === "ObjectExpression" &&
6
- node.properties.some(property => property.type === "Property" && !property.computed && propertyName(property) === "flex");
7
- /** `styles.content` and `styles["content"]` both read as that pair; a dynamic key reads as null. */
8
- const styleReference = (node) => {
9
- if (node?.type !== "MemberExpression" || node.object.type !== "Identifier")
10
- return null;
11
- const sheet = node.object.name;
12
- if (!node.computed && node.property.type === "Identifier")
13
- return { sheet, key: node.property.name };
14
- if (node.computed && node.property.type === "Literal" && typeof node.property.value === "string") {
15
- return { sheet, key: node.property.value };
16
- }
17
- return null;
18
- };
19
- /** A style prop holds one style or an array of them; both read as a list. */
20
- const styleParts = (value) => {
21
- if (!value)
22
- return [];
23
- if (value.type !== "ArrayExpression")
24
- return [value];
25
- return value.elements.filter(element => element !== null);
26
- };
27
- export const noFlexInContentContainer = problem("Disallow `flex` in a Legend List's `contentContainerStyle`, where it sizes the scrolled content to the viewport and the list ends up measuring zero height.", {
28
- createOnce(context) {
29
- let flexKeysBySheet = new Map();
30
- let candidates = [];
31
- const holdsFlex = (part) => {
32
- if (hasFlexKey(part))
33
- return true;
34
- const reference = styleReference(part);
35
- return reference !== null && (flexKeysBySheet.get(reference.sheet)?.has(reference.key) ?? false);
36
- };
37
- return {
38
- before() {
39
- flexKeysBySheet = new Map();
40
- candidates = [];
41
- return gate(context, "contentContainerStyle");
42
- },
43
- VariableDeclarator(node) {
44
- const { id, init } = node;
45
- if (id.type !== "Identifier" || init?.type !== "CallExpression" || !isStyleSheetCreate(init))
46
- return;
47
- const object = stylesObjectOf(init.arguments[0]);
48
- if (!object)
49
- return;
50
- const flexKeys = new Set();
51
- for (const property of object.properties) {
52
- if (property.type !== "Property" || property.computed)
53
- continue;
54
- const name = propertyName(property);
55
- if (name !== "" && hasFlexKey(property.value))
56
- flexKeys.add(name);
57
- }
58
- flexKeysBySheet.set(id.name, flexKeys);
59
- },
60
- JSXAttribute(node) {
61
- if (attributeName(node) !== "contentContainerStyle")
62
- return;
63
- if (!isListElement(node.parent.parent))
64
- return;
65
- const parts = styleParts(expressionOf(node));
66
- if (parts.length > 0)
67
- candidates.push({ node, parts });
68
- },
69
- "Program:exit"() {
70
- for (const { node, parts } of candidates) {
71
- if (parts.some(part => holdsFlex(part)))
72
- context.report({ node, message: MESSAGE });
73
- }
74
- },
75
- };
76
- },
77
- });
1
+ import{attributeName as u,gate as m,problem as h}from"../../../lib/ast.js";import{isStyleSheetCreate as x,propertyName as c,stylesObjectOf as b}from"../../stylesheet.js";import{expressionOf as g,isListElement as d}from"./shared.js";const S="Move `flex` onto `style` and keep `contentContainerStyle` for padding inside the content. On the content container it sizes the content to the viewport, so the list measures as zero height and renders a blank screen with no error.",f=(e)=>e?.type==="ObjectExpression"&&e.properties.some((t)=>t.type==="Property"&&!t.computed&&c(t)==="flex"),w=(e)=>{if(e?.type!=="MemberExpression"||e.object.type!=="Identifier")return null;const t=e.object.name;if(!e.computed&&e.property.type==="Identifier")return{sheet:t,key:e.property.name};if(e.computed&&e.property.type==="Literal"&&typeof e.property.value==="string")return{sheet:t,key:e.property.value};return null},C=(e)=>{if(!e)return[];if(e.type!=="ArrayExpression")return[e];return e.elements.filter((t)=>t!==null)};export const noFlexInContentContainer=h("Disallow `flex` in a Legend List's `contentContainerStyle`, where it sizes the scrolled content to the viewport and the list ends up measuring zero height.",{createOnce(e){let t=new Map,i=[];const y=(r)=>{if(f(r))return!0;const n=w(r);return n!==null&&(t.get(n.sheet)?.has(n.key)??!1)};return{before(){t=new Map;i=[];return m(e,"contentContainerStyle")},VariableDeclarator(r){const{id:n,init:o}=r;if(n.type!=="Identifier"||o?.type!=="CallExpression"||!x(o))return;const p=b(o.arguments[0]);if(!p)return;const a=new Set;for(const s of p.properties){if(s.type!=="Property"||s.computed)continue;const l=c(s);if(l!==""&&f(s.value))a.add(l)}t.set(n.name,a)},JSXAttribute(r){if(u(r)!=="contentContainerStyle")return;if(!d(r.parent.parent))return;const n=C(g(r));if(n.length>0)i.push({node:r,parts:n})},"Program:exit"(){for(const{node:r,parts:n}of i)if(n.some((o)=>y(o)))e.report({node:r,message:S})}}}});
@@ -1,26 +1 @@
1
- import { attributeName, gate, problem, subtreeHas } from "../../../lib/ast.js";
2
- import { expressionOf, isListElement } from "./shared.js";
3
- export const noIndexKeyExtractor = problem("Disallow a `keyExtractor` that uses its index parameter. Cached sizes and recycled row state hang off the key, so a prepend points every measurement at the wrong item.", {
4
- createOnce: context => ({
5
- before: () => gate(context, "keyExtractor"),
6
- JSXAttribute(node) {
7
- if (attributeName(node) !== "keyExtractor")
8
- return;
9
- if (!isListElement(node.parent.parent))
10
- return;
11
- const fn = expressionOf(node);
12
- if (fn?.type !== "ArrowFunctionExpression" && fn?.type !== "FunctionExpression")
13
- return;
14
- const indexParam = fn.params[1];
15
- if (indexParam?.type !== "Identifier")
16
- return;
17
- if (!subtreeHas(fn.body, current => current.type === "Identifier" && current.name === indexParam.name)) {
18
- return;
19
- }
20
- context.report({
21
- node: fn,
22
- message: "Return a stable per-item id from this `keyExtractor` instead of the index. Legend List hangs cached sizes and recycled row state off the key, so one prepend re-points every measurement at the wrong item.",
23
- });
24
- },
25
- }),
26
- });
1
+ import{attributeName as s,gate as i,problem as o,subtreeHas as p}from"../../../lib/ast.js";import{expressionOf as m,isListElement as f}from"./shared.js";export const noIndexKeyExtractor=o("Disallow a `keyExtractor` that uses its index parameter. Cached sizes and recycled row state hang off the key, so a prepend points every measurement at the wrong item.",{createOnce:(r)=>({before:()=>i(r,"keyExtractor"),JSXAttribute(t){if(s(t)!=="keyExtractor")return;if(!f(t.parent.parent))return;const e=m(t);if(e?.type!=="ArrowFunctionExpression"&&e?.type!=="FunctionExpression")return;const n=e.params[1];if(n?.type!=="Identifier")return;if(!p(e.body,(a)=>a.type==="Identifier"&&a.name===n.name))return;r.report({node:e,message:"Return a stable per-item id from this `keyExtractor` instead of the index. Legend List hangs cached sizes and recycled row state off the key, so one prepend re-points every measurement at the wrong item."})}})});
@@ -1,25 +1 @@
1
- import { FUNCTION_TYPES, calleeName, crossesFunctionBefore, findInSubtree, gate, problem } from "../../../lib/ast.js";
2
- import { attributeNamed, expressionOf, isListElement, LIST } from "./shared.js";
3
- const MEMO_HOOKS = new Set(["useMemo", "useCallback"]);
4
- const buildsFreshArray = (node) => node.type === "ArrayExpression" || (node.type === "CallExpression" && node.callee.type === "MemberExpression");
5
- export const noInlineData = problem("Disallow building a Legend List's `data` inline. Each render produces a new array reference, and the list re-diffs, re-keys and drops what it had cached.", {
6
- createOnce: context => ({
7
- before: () => gate(context, LIST),
8
- JSXElement(node) {
9
- if (!isListElement(node))
10
- return;
11
- const data = attributeNamed(node, "data");
12
- if (!data)
13
- return;
14
- if (MEMO_HOOKS.has(calleeName(expressionOf(data))))
15
- return;
16
- const built = findInSubtree(data.value, buildsFreshArray);
17
- if (!built || crossesFunctionBefore(built, data, FUNCTION_TYPES))
18
- return;
19
- context.report({
20
- node: data,
21
- message: "Hoist this `data` into a `useMemo` or compute it upstream so its reference is stable. A new array each render makes the list re-diff, re-key and invalidate everything it had cached.",
22
- });
23
- },
24
- }),
25
- });
1
+ import{FUNCTION_TYPES as s,calleeName as i,crossesFunctionBefore as n,findInSubtree as o,gate as l,problem as c}from"../../../lib/ast.js";import{attributeNamed as d,expressionOf as u,isListElement as f,LIST as m}from"./shared.js";const p=new Set(["useMemo","useCallback"]),h=(e)=>e.type==="ArrayExpression"||e.type==="CallExpression"&&e.callee.type==="MemberExpression";export const noInlineData=c("Disallow building a Legend List's `data` inline. Each render produces a new array reference, and the list re-diffs, re-keys and drops what it had cached.",{createOnce:(e)=>({before:()=>l(e,m),JSXElement(t){if(!f(t))return;const r=d(t,"data");if(!r)return;if(p.has(i(u(r))))return;const a=o(r.value,h);if(!a||n(a,r,s))return;e.report({node:r,message:"Hoist this `data` into a `useMemo` or compute it upstream so its reference is stable. A new array each render makes the list re-diff, re-key and invalidate everything it had cached."})}})});
@@ -1,21 +1 @@
1
- import { attributeName, gate, problem } from "../../../lib/ast.js";
2
- import { expressionOf, isListElement, literalKind } from "./shared.js";
3
- export const noInlineExtraData = problem("An inline object or array as `extraData` takes a new identity on every parent render, and every mounted row re-evaluates with it.", {
4
- createOnce: context => ({
5
- before: () => gate(context, "extraData"),
6
- JSXAttribute(node) {
7
- if (attributeName(node) !== "extraData")
8
- return;
9
- if (!isListElement(node.parent.parent))
10
- return;
11
- const value = expressionOf(node);
12
- const kind = literalKind(value);
13
- if (value === null || kind === null)
14
- return;
15
- context.report({
16
- node: value,
17
- message: `Pass the primitive that actually changed as \`extraData\`, or hoist this ${kind} when it is stable. A fresh ${kind} reference each render re-evaluates every mounted row.`,
18
- });
19
- },
20
- }),
21
- });
1
+ import{attributeName as n,gate as i,problem as s}from"../../../lib/ast.js";import{expressionOf as o,isListElement as l,literalKind as u}from"./shared.js";export const noInlineExtraData=s("An inline object or array as `extraData` takes a new identity on every parent render, and every mounted row re-evaluates with it.",{createOnce:(a)=>({before:()=>i(a,"extraData"),JSXAttribute(e){if(n(e)!=="extraData")return;if(!l(e.parent.parent))return;const t=o(e),r=u(t);if(t===null||r===null)return;a.report({node:t,message:`Pass the primitive that actually changed as \`extraData\`, or hoist this ${r} when it is stable. A fresh ${r} reference each render re-evaluates every mounted row.`})}})});
@@ -1,35 +1 @@
1
- import { attributeName, gate, problem } from "../../../lib/ast.js";
2
- import { literalKind } from "./shared.js";
3
- export const noInlineRenderItemProps = problem("Disallow inline object and array literals on props nested inside `renderItem`. A row whose props take a new identity every render can never be skipped.", {
4
- createOnce(context) {
5
- let depth = 0;
6
- return {
7
- before() {
8
- depth = 0;
9
- return gate(context, "renderItem");
10
- },
11
- JSXAttribute(node) {
12
- if (attributeName(node) === "renderItem") {
13
- depth++;
14
- return;
15
- }
16
- if (depth === 0)
17
- return;
18
- const { value } = node;
19
- if (value?.type !== "JSXExpressionContainer")
20
- return;
21
- const kind = literalKind(value.expression);
22
- if (kind === null)
23
- return;
24
- context.report({
25
- node: value,
26
- message: `Pass \`item\` or its primitive fields and build this ${kind} inside the row, or hoist it to module scope when it is static. A fresh ${kind} each render means the row can never be skipped, so typing anywhere re-renders every visible row.`,
27
- });
28
- },
29
- "JSXAttribute:exit"(node) {
30
- if (attributeName(node) === "renderItem")
31
- depth--;
32
- },
33
- };
34
- },
35
- });
1
+ import{attributeName as s,gate as o,problem as a}from"../../../lib/ast.js";import{literalKind as d}from"./shared.js";export const noInlineRenderItemProps=a("Disallow inline object and array literals on props nested inside `renderItem`. A row whose props take a new identity every render can never be skipped.",{createOnce(n){let e=0;return{before(){e=0;return o(n,"renderItem")},JSXAttribute(r){if(s(r)==="renderItem"){e++;return}if(e===0)return;const{value:t}=r;if(t?.type!=="JSXExpressionContainer")return;const i=d(t.expression);if(i===null)return;n.report({node:t,message:`Pass \`item\` or its primitive fields and build this ${i} inside the row, or hoist it to module scope when it is static. A fresh ${i} each render means the row can never be skipped, so typing anywhere re-renders every visible row.`})},"JSXAttribute:exit"(r){if(s(r)==="renderItem")e--}}}});
@@ -1,24 +1 @@
1
- import { gate, problem } from "../../../lib/ast.js";
2
- import { attributeNamed, isListElement, LIST } from "./shared.js";
3
- const rendersRealChild = (node) => node.type === "JSXElement" &&
4
- node.children.some(child => child.type === "JSXElement" ||
5
- child.type === "JSXFragment" ||
6
- (child.type === "JSXText" && child.value.trim() !== "") ||
7
- (child.type === "JSXExpressionContainer" && child.expression.type !== "JSXEmptyExpression"));
8
- export const noMixedChildren = problem("Disallow passing both `data` and real children to a Legend List. The combination is unsupported and one of the two is dropped without a warning.", {
9
- createOnce: context => ({
10
- before: () => gate(context, LIST),
11
- JSXElement(node) {
12
- if (!isListElement(node))
13
- return;
14
- if (!attributeNamed(node, "data"))
15
- return;
16
- if (!rendersRealChild(node))
17
- return;
18
- context.report({
19
- node: node.openingElement,
20
- message: "Keep either `data` with `renderItem` or children mode here, and remove the other. Passing both fails silently, with no guarantee about which one is ignored.",
21
- });
22
- },
23
- }),
24
- });
1
+ import{gate as r,problem as n}from"../../../lib/ast.js";import{attributeNamed as i,isListElement as o,LIST as a}from"./shared.js";const s=(t)=>t.type==="JSXElement"&&t.children.some((e)=>e.type==="JSXElement"||e.type==="JSXFragment"||e.type==="JSXText"&&e.value.trim()!==""||e.type==="JSXExpressionContainer"&&e.expression.type!=="JSXEmptyExpression");export const noMixedChildren=n("Disallow passing both `data` and real children to a Legend List. The combination is unsupported and one of the two is dropped without a warning.",{createOnce:(t)=>({before:()=>r(t,a),JSXElement(e){if(!o(e))return;if(!i(e,"data"))return;if(!s(e))return;t.report({node:e.openingElement,message:"Keep either `data` with `renderItem` or children mode here, and remove the other. Passing both fails silently, with no guarantee about which one is ignored."})}})});
@@ -1,18 +1 @@
1
- import { gate, problem } from "../../../lib/ast.js";
2
- import { attributeNamed, isListElement, LIST } from "./shared.js";
3
- export const noRemountKey = problem("Disallow `key` on a Legend List, which remounts on any key change and loses its measurements and scroll position. Pass `dataKey` instead.", {
4
- createOnce: context => ({
5
- before: () => gate(context, LIST),
6
- JSXElement(node) {
7
- if (!isListElement(node))
8
- return;
9
- const key = attributeNamed(node, "key");
10
- if (!key)
11
- return;
12
- context.report({
13
- node: key,
14
- message: "Pass `dataKey` instead of `key` here: it re-initialises the list for a different dataset from the inside, without the remount that discards every measurement, cached size and scroll position.",
15
- });
16
- },
17
- }),
18
- });
1
+ import{gate as a,problem as i}from"../../../lib/ast.js";import{attributeNamed as r,isListElement as n,LIST as o}from"./shared.js";export const noRemountKey=i("Disallow `key` on a Legend List, which remounts on any key change and loses its measurements and scroll position. Pass `dataKey` instead.",{createOnce:(e)=>({before:()=>a(e,o),JSXElement(t){if(!n(t))return;const s=r(t,"key");if(!s)return;e.report({node:s,message:"Pass `dataKey` instead of `key` here: it re-initialises the list for a different dataset from the inside, without the remount that discards every measurement, cached size and scroll position."})}})});
@@ -1,27 +1 @@
1
- import { componentName, gate, problem } from "../../../lib/ast.js";
2
- export const noScrollviewMap = problem("A `ScrollView` mounts every child up front, so a mapped collection does not belong in its children.", {
3
- createOnce: context => ({
4
- before: () => gate(context, "ScrollView"),
5
- JSXElement(node) {
6
- if (componentName(node.openingElement.name) !== "ScrollView")
7
- return;
8
- for (const child of node.children) {
9
- if (child.type !== "JSXExpressionContainer")
10
- continue;
11
- const call = child.expression;
12
- if (call.type !== "CallExpression")
13
- continue;
14
- const { callee } = call;
15
- if (callee.type !== "MemberExpression")
16
- continue;
17
- if (callee.property.type !== "Identifier" || callee.property.name !== "map")
18
- continue;
19
- context.report({
20
- node: child,
21
- message: "Render this collection with `LegendList` from '@legendapp/list/react-native', passing the array as `data` and the mapped body as `renderItem`. A ScrollView mounts every child up front, so 200 rows cost 200 mounts to show ten.",
22
- });
23
- return;
24
- }
25
- },
26
- }),
27
- });
1
+ import{componentName as i,gate as l,problem as s}from"../../../lib/ast.js";export const noScrollviewMap=s("A `ScrollView` mounts every child up front, so a mapped collection does not belong in its children.",{createOnce:(n)=>({before:()=>l(n,"ScrollView"),JSXElement(t){if(i(t.openingElement.name)!=="ScrollView")return;for(const e of t.children){if(e.type!=="JSXExpressionContainer")continue;const r=e.expression;if(r.type!=="CallExpression")continue;const{callee:o}=r;if(o.type!=="MemberExpression")continue;if(o.property.type!=="Identifier"||o.property.name!=="map")continue;n.report({node:e,message:"Render this collection with `LegendList` from '@legendapp/list/react-native', passing the array as `data` and the mapped body as `renderItem`. A ScrollView mounts every child up front, so 200 rows cost 200 mounts to show ten."});return}}})});
@@ -1,29 +1 @@
1
- import { attributeName, gate, problem } from "../../../lib/ast.js";
2
- import { attributesOf, isListElement, LIST } from "./shared.js";
3
- const UNSUPPORTED_PROPS = new Set([
4
- "masonry",
5
- "optimizeItemArrangement",
6
- "inverted",
7
- "onBlankArea",
8
- "disableAutoLayout",
9
- "CellRendererComponent",
10
- ]);
11
- export const noUnsupportedProps = problem("Disallow FlashList and FlatList props that Legend List v3 does not have. It ignores them rather than rejecting them, so the feature looks broken.", {
12
- createOnce: context => ({
13
- before: () => gate(context, LIST),
14
- JSXElement(node) {
15
- if (!isListElement(node))
16
- return;
17
- for (const attribute of attributesOf(node)) {
18
- if (attribute.type !== "JSXAttribute")
19
- continue;
20
- if (!UNSUPPORTED_PROPS.has(attributeName(attribute)))
21
- continue;
22
- context.report({
23
- node: attribute,
24
- message: "Remove this prop: Legend List v3 has no such prop and ignores it silently rather than rejecting it. Build inverted chat lists from `maintainScrollAtEnd` / `initialScrollAtEnd` / `maintainVisibleContentPosition` instead.",
25
- });
26
- }
27
- },
28
- }),
29
- });
1
+ import{attributeName as i,gate as o,problem as r}from"../../../lib/ast.js";import{attributesOf as a,isListElement as s,LIST as l}from"./shared.js";const m=new Set(["masonry","optimizeItemArrangement","inverted","onBlankArea","disableAutoLayout","CellRendererComponent"]);export const noUnsupportedProps=r("Disallow FlashList and FlatList props that Legend List v3 does not have. It ignores them rather than rejecting them, so the feature looks broken.",{createOnce:(e)=>({before:()=>o(e,l),JSXElement(n){if(!s(n))return;for(const t of a(n)){if(t.type!=="JSXAttribute")continue;if(!m.has(i(t)))continue;e.report({node:t,message:"Remove this prop: Legend List v3 has no such prop and ignores it silently rather than rejecting it. Build inverted chat lists from `maintainScrollAtEnd` / `initialScrollAtEnd` / `maintainVisibleContentPosition` instead."})}}})});
@@ -1,23 +1 @@
1
- import { gate, problem } from "../../../lib/ast.js";
2
- import { attributeNamed, hasSpread, isListElement, LIST } from "./shared.js";
3
- export const requiredProps = problem("Require `keyExtractor` and an explicit `recycleItems` on a Legend List. Without a key extractor the list keys its rows by index.", {
4
- createOnce: context => ({
5
- before: () => gate(context, LIST),
6
- JSXElement(node) {
7
- if (!isListElement(node) || hasSpread(node))
8
- return;
9
- if (!attributeNamed(node, "keyExtractor")) {
10
- context.report({
11
- node: node.openingElement,
12
- message: "Add `keyExtractor={item => item.id}` returning a stable per-item identity. The index fallback pins cached measurements and recycled row state to a position, so a prepend leaves rows showing the previous row's data.",
13
- });
14
- }
15
- if (!attributeNamed(node, "recycleItems")) {
16
- context.report({
17
- node: node.openingElement,
18
- message: "Add `recycleItems={true}` — recycling is where most of the list's native speed comes from. If a row genuinely cannot be recycled, pass `recycleItems={false}` and state which part of the row required it.",
19
- });
20
- }
21
- },
22
- }),
23
- });
1
+ import{gate as i,problem as s}from"../../../lib/ast.js";import{attributeNamed as r,hasSpread as a,isListElement as o,LIST as n}from"./shared.js";export const requiredProps=s("Require `keyExtractor` and an explicit `recycleItems` on a Legend List. Without a key extractor the list keys its rows by index.",{createOnce:(t)=>({before:()=>i(t,n),JSXElement(e){if(!o(e)||a(e))return;if(!r(e,"keyExtractor"))t.report({node:e.openingElement,message:"Add `keyExtractor={item => item.id}` returning a stable per-item identity. The index fallback pins cached measurements and recycled row state to a position, so a prepend leaves rows showing the previous row's data."});if(!r(e,"recycleItems"))t.report({node:e.openingElement,message:"Add `recycleItems={true}` — recycling is where most of the list's native speed comes from. If a row genuinely cannot be recycled, pass `recycleItems={false}` and state which part of the row required it."})}})});
@@ -1,25 +1 @@
1
- import { attributeName, componentName } from "../../../lib/ast.js";
2
- export const LIST = "LegendList";
3
- export const isListElement = (node) => node?.type === "JSXElement" && componentName(node.openingElement.name) === LIST;
4
- export const attributesOf = (node) => node.type === "JSXElement" ? node.openingElement.attributes : [];
5
- export const attributeNamed = (node, wanted) => {
6
- for (const attribute of attributesOf(node)) {
7
- if (attribute.type === "JSXAttribute" && attributeName(attribute) === wanted)
8
- return attribute;
9
- }
10
- return undefined;
11
- };
12
- export const hasSpread = (node) => attributesOf(node).some(attribute => attribute.type === "JSXSpreadAttribute");
13
- export const expressionOf = (attribute) => {
14
- if (attribute?.type !== "JSXAttribute")
15
- return null;
16
- const { value } = attribute;
17
- return value?.type === "JSXExpressionContainer" ? value.expression : null;
18
- };
19
- export const literalKind = (node) => {
20
- if (node?.type === "ObjectExpression")
21
- return "object";
22
- if (node?.type === "ArrayExpression")
23
- return "array";
24
- return null;
25
- };
1
+ import{attributeName as n,componentName as o}from"../../../lib/ast.js";export const LIST="LegendList",isListElement=(t)=>t?.type==="JSXElement"&&o(t.openingElement.name)===LIST,attributesOf=(t)=>t.type==="JSXElement"?t.openingElement.attributes:[],attributeNamed=(t,e)=>{for(const r of attributesOf(t))if(r.type==="JSXAttribute"&&n(r)===e)return r;return},hasSpread=(t)=>attributesOf(t).some((e)=>e.type==="JSXSpreadAttribute"),expressionOf=(t)=>{if(t?.type!=="JSXAttribute")return null;const{value:e}=t;return e?.type==="JSXExpressionContainer"?e.expression:null},literalKind=(t)=>{if(t?.type==="ObjectExpression")return"object";if(t?.type==="ArrayExpression")return"array";return null};