react-native-boost 0.6.2 → 1.0.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 (34) hide show
  1. package/README.md +0 -3
  2. package/dist/plugin/esm/index.mjs +709 -199
  3. package/dist/plugin/esm/index.mjs.map +1 -1
  4. package/dist/plugin/index.d.ts +54 -0
  5. package/dist/plugin/index.js +709 -199
  6. package/dist/plugin/index.js.map +1 -1
  7. package/dist/runtime/esm/index.mjs +15 -3
  8. package/dist/runtime/esm/index.mjs.map +1 -1
  9. package/dist/runtime/esm/index.web.mjs.map +1 -1
  10. package/dist/runtime/index.d.ts +51 -4
  11. package/dist/runtime/index.js +16 -4
  12. package/dist/runtime/index.js.map +1 -1
  13. package/dist/runtime/index.web.d.ts +13 -1
  14. package/dist/runtime/index.web.js.map +1 -1
  15. package/package.json +13 -14
  16. package/src/plugin/index.ts +27 -5
  17. package/src/plugin/optimizers/text/index.ts +116 -92
  18. package/src/plugin/optimizers/view/index.ts +53 -31
  19. package/src/plugin/types/index.ts +67 -17
  20. package/src/plugin/utils/common/attributes.ts +165 -0
  21. package/src/plugin/utils/common/index.ts +1 -3
  22. package/src/plugin/utils/common/validation.ts +513 -0
  23. package/src/plugin/utils/constants.ts +9 -0
  24. package/src/plugin/utils/format-test-result.ts +29 -0
  25. package/src/plugin/utils/generate-test-plugin.ts +9 -3
  26. package/src/plugin/utils/helpers.ts +15 -0
  27. package/src/plugin/utils/logger.ts +109 -2
  28. package/src/runtime/components/native-text.tsx +21 -5
  29. package/src/runtime/components/native-view.tsx +21 -5
  30. package/src/runtime/index.ts +20 -0
  31. package/src/runtime/types/index.ts +5 -0
  32. package/src/runtime/utils/constants.ts +6 -2
  33. package/src/plugin/utils/common/ancestors.ts +0 -120
  34. package/src/plugin/utils/common/node-types.ts +0 -22
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../src/runtime/utils/constants.ts","../../src/runtime/components/native-text.tsx","../../src/runtime/components/native-view.tsx","../../src/runtime/index.ts"],"sourcesContent":["// Maps the `userSelect` prop to the native `selectable` prop\nexport const userSelectToSelectableMap = {\n auto: true,\n text: true,\n none: false,\n contain: true,\n all: true,\n};\n\n// Maps the `verticalAlign` prop to the native `textAlignVertical` prop\nexport const verticalAlignToTextAlignVerticalMap = {\n auto: 'auto',\n top: 'top',\n bottom: 'bottom',\n middle: 'center',\n};\n","/* eslint-disable @typescript-eslint/no-require-imports,unicorn/prefer-module */\nimport { Platform } from 'react-native';\n\nexport const NativeText =\n Platform.OS === 'web'\n ? require('react-native').Text\n : require('react-native/Libraries/Text/TextNativeComponent').NativeText;\n","/* eslint-disable @typescript-eslint/no-require-imports,unicorn/prefer-module */\nimport { Platform } from 'react-native';\n\nexport const NativeView =\n Platform.OS === 'web'\n ? require('react-native').View\n : require('react-native/Libraries/Components/View/ViewNativeComponent').default;\n","import { TextProps, TextStyle, StyleSheet } from 'react-native';\nimport { GenericStyleProp } from './types';\nimport { userSelectToSelectableMap, verticalAlignToTextAlignVerticalMap } from './utils/constants';\n\nconst propsCache = new WeakMap();\n\nexport function processTextStyle(style: GenericStyleProp<TextStyle>): Partial<TextProps> {\n if (!style) return {};\n\n // Cache the computed props\n let props = propsCache.get(style);\n if (props) return props;\n\n props = {};\n propsCache.set(style, props);\n\n style = StyleSheet.flatten(style) as TextStyle;\n\n if (!style) return {};\n\n if (typeof style?.fontWeight === 'number') {\n style.fontWeight = style.fontWeight.toString() as TextStyle['fontWeight'];\n }\n\n if (style?.userSelect != null) {\n props.selectable = userSelectToSelectableMap[style.userSelect];\n delete style.userSelect;\n }\n\n if (style?.verticalAlign != null) {\n style.textAlignVertical = verticalAlignToTextAlignVerticalMap[\n style.verticalAlign\n ] as TextStyle['textAlignVertical'];\n delete style.verticalAlign;\n }\n\n props.style = style;\n return props;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function processAccessibilityProps(props: Record<string, any>): Record<string, any> {\n const {\n accessibilityLabel,\n ['aria-label']: ariaLabel,\n accessibilityState,\n ['aria-busy']: ariaBusy,\n ['aria-checked']: ariaChecked,\n ['aria-disabled']: ariaDisabled,\n ['aria-expanded']: ariaExpanded,\n ['aria-selected']: ariaSelected,\n accessible,\n ...restProperties\n } = props;\n\n // Merge label props: prefer the aria-label if defined.\n const normalizedLabel = ariaLabel ?? accessibilityLabel;\n\n // Merge the accessibilityState with any provided ARIA properties.\n let normalizedState = accessibilityState;\n if (ariaBusy != null || ariaChecked != null || ariaDisabled != null || ariaExpanded != null || ariaSelected != null) {\n normalizedState =\n normalizedState == null\n ? {\n busy: ariaBusy,\n checked: ariaChecked,\n disabled: ariaDisabled,\n expanded: ariaExpanded,\n selected: ariaSelected,\n }\n : {\n busy: ariaBusy ?? normalizedState.busy,\n checked: ariaChecked ?? normalizedState.checked,\n disabled: ariaDisabled ?? normalizedState.disabled,\n expanded: ariaExpanded ?? normalizedState.expanded,\n selected: ariaSelected ?? normalizedState.selected,\n };\n }\n\n // For the accessible prop, if not provided, default to `true`\n const normalizedAccessible = accessible == null ? true : accessible;\n\n return {\n ...restProperties,\n accessibilityLabel: normalizedLabel,\n accessibilityState: normalizedState,\n accessible: normalizedAccessible,\n };\n}\n\nexport * from './types';\nexport * from './utils/constants';\nexport * from './components/native-text';\nexport * from './components/native-view';\n"],"names":["Platform","StyleSheet"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../../src/runtime/utils/constants.ts","../../src/runtime/components/native-text.tsx","../../src/runtime/components/native-view.tsx","../../src/runtime/index.ts"],"sourcesContent":["/**\n * Maps CSS-like `userSelect` values to React Native's `selectable` prop.\n */\nexport const userSelectToSelectableMap = {\n auto: true,\n text: true,\n none: false,\n contain: true,\n all: true,\n};\n\n/**\n * Maps CSS-like `verticalAlign` values to React Native's `textAlignVertical`.\n */\nexport const verticalAlignToTextAlignVerticalMap = {\n auto: 'auto',\n top: 'top',\n bottom: 'bottom',\n middle: 'center',\n};\n","/* eslint-disable @typescript-eslint/no-require-imports,unicorn/prefer-module */\n\nimport type { ComponentType } from 'react';\nimport type { TextProps } from 'react-native';\n\nconst reactNative = require('react-native');\nconst isWeb = reactNative.Platform.OS === 'web';\n\nlet nativeText = reactNative.unstable_NativeText;\n\nif (isWeb || nativeText == null) {\n // Fallback to regular Text component if unstable_NativeText is not available or we're on Web\n nativeText = reactNative.Text;\n}\n\n/**\n * Native Text component with graceful fallback.\n *\n * @remarks\n * Uses `unstable_NativeText` on supported native runtimes and falls back to `Text`\n * on web or when the unstable export is unavailable.\n */\nexport const NativeText: ComponentType<TextProps> = nativeText;\n","/* eslint-disable @typescript-eslint/no-require-imports,unicorn/prefer-module */\n\nimport type { ComponentType } from 'react';\nimport type { ViewProps } from 'react-native';\n\nconst reactNative = require('react-native');\nconst isWeb = reactNative.Platform.OS === 'web';\n\nlet nativeView = reactNative.unstable_NativeView;\n\nif (isWeb || nativeView == null) {\n // Fallback to regular View component if unstable_NativeView is not available or we're on Web\n nativeView = reactNative.View;\n}\n\n/**\n * Native View component with graceful fallback.\n *\n * @remarks\n * Uses `unstable_NativeView` on supported native runtimes and falls back to `View`\n * on web or when the unstable export is unavailable.\n */\nexport const NativeView: ComponentType<ViewProps> = nativeView;\n","import { TextProps, TextStyle, StyleSheet } from 'react-native';\nimport { GenericStyleProp } from './types';\nimport { userSelectToSelectableMap, verticalAlignToTextAlignVerticalMap } from './utils/constants';\n\nconst propsCache = new WeakMap();\n\n/**\n * Normalizes `Text` style values for `NativeText`.\n *\n * @param style - Style prop passed to a text-like component.\n * @returns Native-friendly text props. Returns an empty object when `style` is falsy or cannot be normalized.\n * @remarks\n * - Flattens style arrays via `StyleSheet.flatten`\n * - Converts numeric `fontWeight` values to string values\n * - Maps `userSelect` and `verticalAlign` to native-compatible props\n */\nexport function processTextStyle(style: GenericStyleProp<TextStyle>): Partial<TextProps> {\n if (!style) return {};\n\n // Cache the computed props\n let props = propsCache.get(style);\n if (props) return props;\n\n props = {};\n propsCache.set(style, props);\n\n style = StyleSheet.flatten(style) as TextStyle;\n\n if (!style) return {};\n\n if (typeof style?.fontWeight === 'number') {\n style.fontWeight = style.fontWeight.toString() as TextStyle['fontWeight'];\n }\n\n if (style?.userSelect != null) {\n props.selectable = userSelectToSelectableMap[style.userSelect];\n delete style.userSelect;\n }\n\n if (style?.verticalAlign != null) {\n style.textAlignVertical = verticalAlignToTextAlignVerticalMap[\n style.verticalAlign\n ] as TextStyle['textAlignVertical'];\n delete style.verticalAlign;\n }\n\n props.style = style;\n return props;\n}\n\n/**\n * Normalizes accessibility and ARIA props for runtime native components.\n *\n * @param props - Accessibility and ARIA props.\n * @returns Props with normalized accessibility fields.\n * @remarks\n * - Merges `aria-label` with `accessibilityLabel`\n * - Merges ARIA state fields into `accessibilityState`\n * - Defaults `accessible` to `true` when omitted\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function processAccessibilityProps(props: Record<string, any>): Record<string, any> {\n const {\n accessibilityLabel,\n ['aria-label']: ariaLabel,\n accessibilityState,\n ['aria-busy']: ariaBusy,\n ['aria-checked']: ariaChecked,\n ['aria-disabled']: ariaDisabled,\n ['aria-expanded']: ariaExpanded,\n ['aria-selected']: ariaSelected,\n accessible,\n ...restProperties\n } = props;\n\n // Merge label props: prefer the aria-label if defined.\n const normalizedLabel = ariaLabel ?? accessibilityLabel;\n\n // Merge the accessibilityState with any provided ARIA properties.\n let normalizedState = accessibilityState;\n if (ariaBusy != null || ariaChecked != null || ariaDisabled != null || ariaExpanded != null || ariaSelected != null) {\n normalizedState =\n normalizedState == null\n ? {\n busy: ariaBusy,\n checked: ariaChecked,\n disabled: ariaDisabled,\n expanded: ariaExpanded,\n selected: ariaSelected,\n }\n : {\n busy: ariaBusy ?? normalizedState.busy,\n checked: ariaChecked ?? normalizedState.checked,\n disabled: ariaDisabled ?? normalizedState.disabled,\n expanded: ariaExpanded ?? normalizedState.expanded,\n selected: ariaSelected ?? normalizedState.selected,\n };\n }\n\n // For the accessible prop, if not provided, default to `true`\n const normalizedAccessible = accessible == null ? true : accessible;\n\n return {\n ...restProperties,\n accessibilityLabel: normalizedLabel,\n accessibilityState: normalizedState,\n accessible: normalizedAccessible,\n };\n}\n\nexport * from './types';\nexport * from './utils/constants';\nexport * from './components/native-text';\nexport * from './components/native-view';\n"],"names":["reactNative","isWeb","StyleSheet"],"mappings":"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"}
@@ -1,7 +1,15 @@
1
1
  import { TextStyle, TextProps } from 'react-native';
2
2
 
3
+ /**
4
+ * Recursive style prop shape accepted by runtime style helpers.
5
+ *
6
+ * @template T - Style object type.
7
+ */
3
8
  type GenericStyleProp<T> = null | void | T | false | '' | ReadonlyArray<GenericStyleProp<T>>;
4
9
 
10
+ /**
11
+ * Maps CSS-like `userSelect` values to React Native's `selectable` prop.
12
+ */
5
13
  declare const userSelectToSelectableMap: {
6
14
  auto: boolean;
7
15
  text: boolean;
@@ -9,6 +17,9 @@ declare const userSelectToSelectableMap: {
9
17
  contain: boolean;
10
18
  all: boolean;
11
19
  };
20
+ /**
21
+ * Maps CSS-like `verticalAlign` values to React Native's `textAlignVertical`.
22
+ */
12
23
  declare const verticalAlignToTextAlignVerticalMap: {
13
24
  auto: string;
14
25
  top: string;
@@ -22,4 +33,5 @@ declare function processAccessibilityProps(props: Record<string, any>): Record<s
22
33
  declare const NativeText: any;
23
34
  declare const NativeView: any;
24
35
 
25
- export { type GenericStyleProp, NativeText, NativeView, processAccessibilityProps, processTextStyle, userSelectToSelectableMap, verticalAlignToTextAlignVerticalMap };
36
+ export { NativeText, NativeView, processAccessibilityProps, processTextStyle, userSelectToSelectableMap, verticalAlignToTextAlignVerticalMap };
37
+ export type { GenericStyleProp };
@@ -1 +1 @@
1
- {"version":3,"file":"index.web.js","sources":["../../src/runtime/utils/constants.ts","../../src/runtime/index.web.ts"],"sourcesContent":["// Maps the `userSelect` prop to the native `selectable` prop\nexport const userSelectToSelectableMap = {\n auto: true,\n text: true,\n none: false,\n contain: true,\n all: true,\n};\n\n// Maps the `verticalAlign` prop to the native `textAlignVertical` prop\nexport const verticalAlignToTextAlignVerticalMap = {\n auto: 'auto',\n top: 'top',\n bottom: 'bottom',\n middle: 'center',\n};\n","// This is a dummy file to ensure that nothing breaks when using the runtime in a web environment.\n\nimport { TextProps, TextStyle } from 'react-native';\nimport { GenericStyleProp } from './types';\n\nexport const processTextStyle = (style: GenericStyleProp<TextStyle>) => ({ style }) as Partial<TextProps>;\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function processAccessibilityProps(props: Record<string, any>): Record<string, any> {\n return props;\n}\n\nexport * from './types';\nexport * from './utils/constants';\n\n// On Web, the native components are not available, so we use the standard components that'll be replaced by their DOM\n// equivalents by react-native-web.\n/* eslint-disable @typescript-eslint/no-require-imports,unicorn/prefer-module */\nexport const NativeText = require('react-native').Text;\nexport const NativeView = require('react-native').View;\n/* eslint-enable @typescript-eslint/no-require-imports,unicorn/prefer-module */\n"],"names":[],"mappings":";;AACO,MAAM,yBAA4B,GAAA;AAAA,EACvC,IAAM,EAAA,IAAA;AAAA,EACN,IAAM,EAAA,IAAA;AAAA,EACN,IAAM,EAAA,KAAA;AAAA,EACN,OAAS,EAAA,IAAA;AAAA,EACT,GAAK,EAAA;AACP;AAGO,MAAM,mCAAsC,GAAA;AAAA,EACjD,IAAM,EAAA,MAAA;AAAA,EACN,GAAK,EAAA,KAAA;AAAA,EACL,MAAQ,EAAA,QAAA;AAAA,EACR,MAAQ,EAAA;AACV;;ACVO,MAAM,gBAAmB,GAAA,CAAC,KAAwC,MAAA,EAAE,KAAM,EAAA;AAG1E,SAAS,0BAA0B,KAAiD,EAAA;AACzF,EAAO,OAAA,KAAA;AACT;AAQa,MAAA,UAAA,GAAa,OAAQ,CAAA,cAAc,CAAE,CAAA;AACrC,MAAA,UAAA,GAAa,OAAQ,CAAA,cAAc,CAAE,CAAA;;;;;;;;;"}
1
+ {"version":3,"file":"index.web.js","sources":["../../src/runtime/utils/constants.ts","../../src/runtime/index.web.ts"],"sourcesContent":["/**\n * Maps CSS-like `userSelect` values to React Native's `selectable` prop.\n */\nexport const userSelectToSelectableMap = {\n auto: true,\n text: true,\n none: false,\n contain: true,\n all: true,\n};\n\n/**\n * Maps CSS-like `verticalAlign` values to React Native's `textAlignVertical`.\n */\nexport const verticalAlignToTextAlignVerticalMap = {\n auto: 'auto',\n top: 'top',\n bottom: 'bottom',\n middle: 'center',\n};\n","// This is a dummy file to ensure that nothing breaks when using the runtime in a web environment.\n\nimport { TextProps, TextStyle } from 'react-native';\nimport { GenericStyleProp } from './types';\n\nexport const processTextStyle = (style: GenericStyleProp<TextStyle>) => ({ style }) as Partial<TextProps>;\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function processAccessibilityProps(props: Record<string, any>): Record<string, any> {\n return props;\n}\n\nexport * from './types';\nexport * from './utils/constants';\n\n// On Web, the native components are not available, so we use the standard components that'll be replaced by their DOM\n// equivalents by react-native-web.\n/* eslint-disable @typescript-eslint/no-require-imports,unicorn/prefer-module */\nexport const NativeText = require('react-native').Text;\nexport const NativeView = require('react-native').View;\n/* eslint-enable @typescript-eslint/no-require-imports,unicorn/prefer-module */\n"],"names":[],"mappings":";;AAGO,MAAM,yBAAA,GAA4B;AAAA,EACvC,IAAA,EAAM,IAAA;AAAA,EACN,IAAA,EAAM,IAAA;AAAA,EACN,IAAA,EAAM,KAAA;AAAA,EACN,OAAA,EAAS,IAAA;AAAA,EACT,GAAA,EAAK;AACP;AAKO,MAAM,mCAAA,GAAsC;AAAA,EACjD,IAAA,EAAM,MAAA;AAAA,EACN,GAAA,EAAK,KAAA;AAAA,EACL,MAAA,EAAQ,QAAA;AAAA,EACR,MAAA,EAAQ;AACV;;ACdO,MAAM,gBAAA,GAAmB,CAAC,KAAA,MAAwC,EAAE,KAAA,EAAM;AAG1E,SAAS,0BAA0B,KAAA,EAAiD;AACzF,EAAA,OAAO,KAAA;AACT;AAQO,MAAM,UAAA,GAAa,OAAA,CAAQ,cAAc,CAAA,CAAE;AAC3C,MAAM,UAAA,GAAa,OAAA,CAAQ,cAAc,CAAA,CAAE;;;;;;;;;"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-native-boost",
3
3
  "description": "🚀 Boost your React Native app's performance with a single line of code",
4
- "version": "0.6.2",
4
+ "version": "1.0.0",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/esm/index.mjs",
7
7
  "types": "dist/index.d.ts",
@@ -43,11 +43,11 @@
43
43
  ],
44
44
  "scripts": {
45
45
  "clean": "rm -rf dist",
46
- "build": "yarn clean && rollup -c",
46
+ "build": "pnpm clean && rollup -c",
47
+ "build:watch": "rollup -c -w",
48
+ "dev": "pnpm build:watch",
47
49
  "test": "vitest",
48
50
  "typecheck": "tsc --noEmit",
49
- "lint": "eslint src/**/*.ts",
50
- "format": "prettier --write .",
51
51
  "release": "release-it",
52
52
  "prepack": "cp ../../README.md ./README.md",
53
53
  "postpack": "rm ./README.md"
@@ -76,7 +76,7 @@
76
76
  "url": "https://github.com/kuatsu/react-native-boost/issues"
77
77
  },
78
78
  "homepage": "https://github.com/kuatsu/react-native-boost#readme",
79
- "packageManager": "yarn@3.6.1",
79
+ "packageManager": "pnpm@10.28.2",
80
80
  "publishConfig": {
81
81
  "registry": "https://registry.npmjs.org"
82
82
  },
@@ -90,27 +90,26 @@
90
90
  "@babel/plugin-syntax-jsx": "^7.25.0",
91
91
  "@babel/preset-typescript": "^7.25.0",
92
92
  "@release-it/conventional-changelog": "^10.0.0",
93
- "@rollup/plugin-alias": "^5.1.1",
93
+ "@rollup/plugin-alias": "^6.0.0",
94
94
  "@rollup/plugin-node-resolve": "^16.0.0",
95
95
  "@rollup/plugin-replace": "^6.0.2",
96
96
  "@rollup/plugin-typescript": "^12.1.2",
97
97
  "@types/babel__helper-module-imports": "^7.0.0",
98
98
  "@types/babel__helper-plugin-utils": "^7.0.0",
99
- "@types/node": "^20",
100
- "babel-plugin-tester": "^11.0.4",
99
+ "@types/node": "^24",
100
+ "babel-plugin-tester": "^12.0.0",
101
101
  "esbuild-node-externals": "^1.18.0",
102
- "globals": "^16.0.0",
103
- "react-native": "0.79.3",
104
- "release-it": "^18.1.2",
102
+ "react-native": "0.83.2",
103
+ "release-it": "^19.2.4",
105
104
  "rollup": "^4.34.8",
106
105
  "rollup-plugin-dts": "^6.1.1",
107
106
  "rollup-plugin-esbuild": "^6.2.0",
108
- "typescript": "^5.7.3",
109
- "vitest": "^3.0.6"
107
+ "typescript": "~5.9.3",
108
+ "vitest": "^4.0.18"
110
109
  },
111
110
  "peerDependencies": {
112
111
  "react": "*",
113
- "react-native": "*"
112
+ "react-native": ">=0.83.0"
114
113
  },
115
114
  "release-it": {
116
115
  "git": {
@@ -1,10 +1,17 @@
1
1
  import { declare } from '@babel/helper-plugin-utils';
2
2
  import { textOptimizer } from './optimizers/text';
3
- import { PluginOptions } from './types';
4
- import { log } from './utils/logger';
3
+ import { PluginLogger, PluginOptions } from './types';
4
+ import { createLogger } from './utils/logger';
5
5
  import { viewOptimizer } from './optimizers/view';
6
6
  import { isIgnoredFile } from './utils/common';
7
7
 
8
+ export type { PluginOptimizationOptions, PluginOptions } from './types';
9
+
10
+ type PluginState = {
11
+ opts?: PluginOptions;
12
+ __reactNativeBoostLogger?: PluginLogger;
13
+ };
14
+
8
15
  export default declare((api) => {
9
16
  api.assertVersion(7);
10
17
 
@@ -12,12 +19,27 @@ export default declare((api) => {
12
19
  name: 'react-native-boost',
13
20
  visitor: {
14
21
  JSXOpeningElement(path, state) {
15
- const options = (state.opts ?? {}) as PluginOptions;
16
- const logger = options.verbose ? log : () => {};
22
+ const pluginState = state as PluginState;
23
+ const options = (pluginState.opts ?? {}) as PluginOptions;
24
+ const logger = getOrCreateLogger(pluginState, options);
25
+
17
26
  if (isIgnoredFile(path, options.ignores ?? [])) return;
18
27
  if (options.optimizations?.text !== false) textOptimizer(path, logger);
19
- if (options.optimizations?.view !== false) viewOptimizer(path, logger);
28
+ if (options.optimizations?.view !== false) viewOptimizer(path, logger, options);
20
29
  },
21
30
  },
22
31
  };
23
32
  });
33
+
34
+ function getOrCreateLogger(state: PluginState, options: PluginOptions): PluginLogger {
35
+ if (state.__reactNativeBoostLogger) {
36
+ return state.__reactNativeBoostLogger;
37
+ }
38
+
39
+ state.__reactNativeBoostLogger = createLogger({
40
+ verbose: options.verbose === true,
41
+ silent: options.silent === true,
42
+ });
43
+
44
+ return state.__reactNativeBoostLogger;
45
+ }
@@ -1,7 +1,9 @@
1
1
  import { NodePath, types as t } from '@babel/core';
2
- import { HubFile, Optimizer } from '../../types';
2
+ import { HubFile, Optimizer, PluginLogger } from '../../types';
3
3
  import PluginError from '../../utils/plugin-error';
4
+ import { getFirstBailoutReason } from '../../utils/helpers';
4
5
  import {
6
+ addDefaultProperty,
5
7
  addFileImportHint,
6
8
  buildPropertiesFromAttributes,
7
9
  hasAccessibilityProperty,
@@ -11,12 +13,13 @@ import {
11
13
  isReactNativeImport,
12
14
  replaceWithNativeComponent,
13
15
  isStringNode,
16
+ hasExpoRouterLinkParentWithAsChild,
14
17
  } from '../../utils/common';
15
18
  import { RUNTIME_MODULE_NAME } from '../../utils/constants';
19
+ import { ACCESSIBILITY_PROPERTIES } from '../../utils/constants';
20
+ import { extractStyleAttribute, extractSelectableAndUpdateStyle } from '../../utils/common';
16
21
 
17
22
  export const textBlacklistedProperties = new Set([
18
- 'allowFontScaling',
19
- 'ellipsizeMode',
20
23
  'id',
21
24
  'nativeID',
22
25
  'onLongPress',
@@ -31,19 +34,46 @@ export const textBlacklistedProperties = new Set([
31
34
  'onStartShouldSetResponder',
32
35
  'pressRetentionOffset',
33
36
  'suppressHighlighting',
34
- 'selectable',
35
- 'selectionColor',
37
+ 'selectionColor', // TODO: we can use react-native's internal `processColor` to process this at runtime
36
38
  ]);
37
39
 
38
- export const textOptimizer: Optimizer = (path, log = () => {}) => {
39
- if (isIgnoredLine(path)) return;
40
+ export const textOptimizer: Optimizer = (path, logger) => {
40
41
  if (!isValidJSXComponent(path, 'Text')) return;
41
- if (!isReactNativeImport(path, 'Text')) return;
42
- if (hasBlacklistedProperty(path, textBlacklistedProperties)) return;
43
42
 
44
43
  // Verify that the Text only has string children
45
44
  const parent = path.parent as t.JSXElement;
46
- if (hasInvalidChildren(path, parent)) return;
45
+
46
+ const skipReason = getFirstBailoutReason([
47
+ {
48
+ reason: 'line is marked with @boost-ignore',
49
+ shouldBail: () => isIgnoredLine(path),
50
+ },
51
+ {
52
+ reason: 'Text is not imported from react-native',
53
+ shouldBail: () => !isReactNativeImport(path, 'Text'),
54
+ },
55
+ {
56
+ reason: 'contains blacklisted props',
57
+ shouldBail: () => hasBlacklistedProperty(path, textBlacklistedProperties),
58
+ },
59
+ {
60
+ reason: 'is a direct child of expo-router Link with asChild',
61
+ shouldBail: () => hasExpoRouterLinkParentWithAsChild(path),
62
+ },
63
+ {
64
+ reason: 'contains non-string children',
65
+ shouldBail: () => hasInvalidChildren(path, parent),
66
+ },
67
+ ]);
68
+
69
+ if (skipReason) {
70
+ logger.skipped({
71
+ component: 'Text',
72
+ path,
73
+ reason: skipReason,
74
+ });
75
+ return;
76
+ }
47
77
 
48
78
  const hub = path.hub as unknown;
49
79
  const file = typeof hub === 'object' && hub !== null && 'file' in hub ? (hub.file as HubFile) : undefined;
@@ -52,14 +82,16 @@ export const textOptimizer: Optimizer = (path, log = () => {}) => {
52
82
  throw new PluginError('No file found in Babel hub');
53
83
  }
54
84
 
55
- const filename = file.opts?.filename || 'unknown file';
56
- const lineNumber = path.node.loc?.start.line ?? 'unknown line';
57
- log(`Optimizing Text component in ${filename}:${lineNumber}`);
85
+ logger.optimized({
86
+ component: 'Text',
87
+ path,
88
+ });
58
89
 
59
90
  // Process props
60
- const originalAttributes = [...path.node.attributes];
61
- fixNegativeNumberOfLines({ path, log });
62
- processProps(path, file, originalAttributes);
91
+ fixNegativeNumberOfLines({ path, logger });
92
+ addDefaultProperty(path, 'allowFontScaling', t.booleanLiteral(true));
93
+ addDefaultProperty(path, 'ellipsizeMode', t.stringLiteral('tail'));
94
+ processProps(path, file);
63
95
 
64
96
  // Replace the Text component with NativeText
65
97
  replaceWithNativeComponent(path, parent, file, 'NativeText');
@@ -95,13 +127,7 @@ function hasInvalidChildren(path: NodePath<t.JSXOpeningElement>, parent: t.JSXEl
95
127
  /**
96
128
  * Fixes negative numberOfLines values by setting them to 0.
97
129
  */
98
- function fixNegativeNumberOfLines({
99
- path,
100
- log,
101
- }: {
102
- path: NodePath<t.JSXOpeningElement>;
103
- log: (message: string) => void;
104
- }) {
130
+ function fixNegativeNumberOfLines({ path, logger }: { path: NodePath<t.JSXOpeningElement>; logger: PluginLogger }) {
105
131
  for (const attribute of path.node.attributes) {
106
132
  if (
107
133
  t.isJSXAttribute(attribute) &&
@@ -120,58 +146,40 @@ function fixNegativeNumberOfLines({
120
146
  originalValue = -attribute.value.expression.argument.value;
121
147
  }
122
148
  if (originalValue !== undefined && originalValue < 0) {
123
- log(
124
- `Warning: 'numberOfLines' in <Text> must be a non-negative number, received: ${originalValue}. The value will be set to 0.`
125
- );
149
+ logger.warning({
150
+ component: 'Text',
151
+ path,
152
+ message: `'numberOfLines' must be a non-negative number, received: ${originalValue}. The value will be set to 0.`,
153
+ });
126
154
  attribute.value.expression = t.numericLiteral(0);
127
155
  }
128
156
  }
129
157
  }
130
158
  }
131
159
 
132
- /**
133
- * Extracts the style attribute from JSX attributes.
134
- */
135
- function extractStyleAttribute(attributes: Array<t.JSXAttribute | t.JSXSpreadAttribute>): {
136
- styleAttribute?: t.JSXAttribute;
137
- styleExpr?: t.Expression;
138
- } {
139
- for (const attribute of attributes) {
140
- if (t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name, { name: 'style' })) {
141
- if (
142
- attribute.value &&
143
- t.isJSXExpressionContainer(attribute.value) &&
144
- !t.isJSXEmptyExpression(attribute.value.expression)
145
- ) {
146
- return {
147
- styleAttribute: attribute,
148
- styleExpr: attribute.value.expression,
149
- };
150
- }
151
- return { styleAttribute: attribute };
152
- }
153
- }
154
- return {};
155
- }
156
-
157
160
  /**
158
161
  * Processes style and accessibility attributes, replacing them with optimized versions.
159
162
  */
160
- function processProps(
161
- path: NodePath<t.JSXOpeningElement>,
162
- file: HubFile,
163
- originalAttributes: Array<t.JSXAttribute | t.JSXSpreadAttribute>
164
- ) {
165
- const { styleExpr } = extractStyleAttribute(originalAttributes);
166
- const hasA11y = hasAccessibilityProperty(path, originalAttributes);
167
-
168
- if (styleExpr && hasA11y) {
169
- // When both style and accessibility properties exist, we split them into two separate spread attributes
170
- const accessibilityAttributes = originalAttributes.filter(
171
- (attribute) => !(t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name, { name: 'style' }))
172
- );
173
-
174
- // Set up the accessibility import
163
+ function processProps(path: NodePath<t.JSXOpeningElement>, file: HubFile) {
164
+ // Grab the up-to-date list of attributes
165
+ const currentAttributes = [...path.node.attributes];
166
+
167
+ const { styleExpr, styleAttribute } = extractStyleAttribute(currentAttributes);
168
+ const hasA11y = hasAccessibilityProperty(path, currentAttributes);
169
+
170
+ // ============================================
171
+ // 1. Prepare spread attributes (style / a11y)
172
+ // ============================================
173
+
174
+ const spreadAttributes: t.JSXSpreadAttribute[] = [];
175
+
176
+ // --- Accessibility ---
177
+ if (hasA11y) {
178
+ const accessibilityAttributes = currentAttributes.filter((attribute) => {
179
+ if (!t.isJSXAttribute(attribute)) return false;
180
+ return t.isJSXIdentifier(attribute.name) && ACCESSIBILITY_PROPERTIES.has(attribute.name.name as string);
181
+ });
182
+
175
183
  const normalizeIdentifier = addFileImportHint({
176
184
  file,
177
185
  nameHint: 'processAccessibilityProps',
@@ -179,10 +187,25 @@ function processProps(
179
187
  importName: 'processAccessibilityProps',
180
188
  moduleName: RUNTIME_MODULE_NAME,
181
189
  });
190
+
182
191
  const accessibilityObject = buildPropertiesFromAttributes(accessibilityAttributes);
183
192
  const accessibilityExpr = t.callExpression(t.identifier(normalizeIdentifier.name), [accessibilityObject]);
193
+ spreadAttributes.push(t.jsxSpreadAttribute(accessibilityExpr));
194
+ }
195
+
196
+ // --- Style ---
197
+ let selectableAttribute: t.JSXAttribute | undefined;
198
+ if (styleExpr) {
199
+ // Attempt a compile-time extraction of `userSelect`
200
+ const selectableValue = extractSelectableAndUpdateStyle(styleExpr);
201
+
202
+ if (selectableValue != null) {
203
+ selectableAttribute = t.jsxAttribute(
204
+ t.jsxIdentifier('selectable'),
205
+ t.jsxExpressionContainer(t.booleanLiteral(selectableValue))
206
+ );
207
+ }
184
208
 
185
- // Set up the style import
186
209
  const flattenIdentifier = addFileImportHint({
187
210
  file,
188
211
  nameHint: 'processTextStyle',
@@ -191,31 +214,32 @@ function processProps(
191
214
  moduleName: RUNTIME_MODULE_NAME,
192
215
  });
193
216
  const flattenedStyleExpr = t.callExpression(t.identifier(flattenIdentifier.name), [styleExpr]);
217
+ spreadAttributes.push(t.jsxSpreadAttribute(flattenedStyleExpr));
218
+ }
194
219
 
195
- // Use two separate JSX spread attributes
196
- path.node.attributes = [t.jsxSpreadAttribute(accessibilityExpr), t.jsxSpreadAttribute(flattenedStyleExpr)];
197
- } else if (styleExpr) {
198
- // Only style attribute is present
199
- const flattenIdentifier = addFileImportHint({
200
- file,
201
- nameHint: 'processTextStyle',
202
- path,
203
- importName: 'processTextStyle',
204
- moduleName: RUNTIME_MODULE_NAME,
205
- });
206
- const flattened = t.callExpression(t.identifier(flattenIdentifier.name), [styleExpr]);
207
- path.node.attributes = [t.jsxSpreadAttribute(flattened)];
208
- } else if (hasA11y) {
209
- // Only accessibility properties are present
210
- const normalizeIdentifier = addFileImportHint({
211
- file,
212
- nameHint: 'processAccessibilityProps',
213
- path,
214
- importName: 'processAccessibilityProps',
215
- moduleName: RUNTIME_MODULE_NAME,
216
- });
217
- const propsObject = buildPropertiesFromAttributes(originalAttributes);
218
- const normalized = t.callExpression(t.identifier(normalizeIdentifier.name), [propsObject]);
219
- path.node.attributes = [t.jsxSpreadAttribute(normalized)];
220
+ // ============================================
221
+ // 2. Collect the remaining (non-processed) attributes
222
+ // ============================================
223
+ const remainingAttributes: (t.JSXAttribute | t.JSXSpreadAttribute)[] = [];
224
+
225
+ for (const attribute of currentAttributes) {
226
+ // Skip the style attribute (we have replaced it with a spread)
227
+ if (styleAttribute && attribute === styleAttribute) continue;
228
+
229
+ // Skip accessibility attributes if we processed them
230
+ if (
231
+ hasA11y &&
232
+ t.isJSXAttribute(attribute) &&
233
+ t.isJSXIdentifier(attribute.name) &&
234
+ ACCESSIBILITY_PROPERTIES.has(attribute.name.name as string)
235
+ ) {
236
+ continue;
237
+ }
238
+
239
+ remainingAttributes.push(attribute);
220
240
  }
241
+
242
+ path.node.attributes = [...spreadAttributes, selectableAttribute, ...remainingAttributes].filter(
243
+ (attribute): attribute is t.JSXAttribute | t.JSXSpreadAttribute => attribute !== undefined
244
+ );
221
245
  }
@@ -1,57 +1,78 @@
1
1
  import { types as t } from '@babel/core';
2
2
  import { HubFile, Optimizer } from '../../types';
3
3
  import PluginError from '../../utils/plugin-error';
4
+ import { getFirstBailoutReason } from '../../utils/helpers';
4
5
  import {
5
6
  hasBlacklistedProperty,
6
7
  isIgnoredLine,
7
8
  isValidJSXComponent,
8
9
  isReactNativeImport,
9
10
  replaceWithNativeComponent,
10
- hasComponentAncestor,
11
+ getViewAncestorClassification,
12
+ ViewAncestorClassification,
11
13
  } from '../../utils/common';
12
14
 
13
15
  export const viewBlacklistedProperties = new Set([
16
+ // TODO: process a11y props at runtime
14
17
  'accessible',
15
18
  'accessibilityLabel',
16
19
  'accessibilityState',
17
- 'allowFontScaling',
18
20
  'aria-busy',
19
21
  'aria-checked',
20
22
  'aria-disabled',
21
23
  'aria-expanded',
22
24
  'aria-label',
23
25
  'aria-selected',
24
- 'ellipsizeMode',
25
- 'disabled',
26
26
  'id',
27
27
  'nativeID',
28
- 'numberOfLines',
29
- 'onLongPress',
30
- 'onPress',
31
- 'onPressIn',
32
- 'onPressOut',
33
- 'onResponderGrant',
34
- 'onResponderMove',
35
- 'onResponderRelease',
36
- 'onResponderTerminate',
37
- 'onResponderTerminationRequest',
38
- 'onStartShouldSetResponder',
39
- 'pressRetentionOffset',
40
- 'selectable',
41
- 'selectionColor',
42
- 'suppressHighlighting',
43
- 'style',
28
+ 'style', // TODO: process style at runtime
44
29
  ]);
45
30
 
46
- // Components to skip when checking for indirect Text ancestors
47
- const skipComponents = ['View', 'Fragment', 'ScrollView', 'FlatList'];
48
-
49
- export const viewOptimizer: Optimizer = (path, log = () => {}) => {
50
- if (isIgnoredLine(path)) return;
31
+ export const viewOptimizer: Optimizer = (path, logger, options) => {
51
32
  if (!isValidJSXComponent(path, 'View')) return;
52
- if (!isReactNativeImport(path, 'View')) return;
53
- if (hasBlacklistedProperty(path, viewBlacklistedProperties)) return;
54
- if (hasComponentAncestor(path, 'Text', skipComponents)) return;
33
+
34
+ let ancestorClassification: ViewAncestorClassification | undefined;
35
+ const getAncestorClassification = () => {
36
+ if (!ancestorClassification) {
37
+ ancestorClassification = getViewAncestorClassification(path);
38
+ }
39
+
40
+ return ancestorClassification;
41
+ };
42
+
43
+ const skipReason = getFirstBailoutReason([
44
+ {
45
+ reason: 'line is marked with @boost-ignore',
46
+ shouldBail: () => isIgnoredLine(path),
47
+ },
48
+ {
49
+ reason: 'View is not imported from react-native',
50
+ shouldBail: () => !isReactNativeImport(path, 'View'),
51
+ },
52
+ {
53
+ reason: 'contains blacklisted props',
54
+ shouldBail: () => hasBlacklistedProperty(path, viewBlacklistedProperties),
55
+ },
56
+ {
57
+ reason: 'has Text ancestor',
58
+ shouldBail: () => getAncestorClassification() === 'text',
59
+ },
60
+ {
61
+ reason: 'has unresolved ancestor and dangerous optimization is disabled',
62
+ shouldBail: () =>
63
+ getAncestorClassification() === 'unknown' && options?.dangerouslyOptimizeViewWithUnknownAncestors !== true,
64
+ },
65
+ ]);
66
+
67
+ if (skipReason) {
68
+ logger.skipped({
69
+ component: 'View',
70
+ path,
71
+ reason: skipReason,
72
+ });
73
+
74
+ return;
75
+ }
55
76
 
56
77
  // Extract the file from the Babel hub
57
78
  const hub = path.hub as unknown;
@@ -61,9 +82,10 @@ export const viewOptimizer: Optimizer = (path, log = () => {}) => {
61
82
  throw new PluginError('No file found in Babel hub');
62
83
  }
63
84
 
64
- const filename = file.opts?.filename || 'unknown file';
65
- const lineNumber = path.node.loc?.start.line ?? 'unknown line';
66
- log(`Optimizing View component in ${filename}:${lineNumber}`);
85
+ logger.optimized({
86
+ component: 'View',
87
+ path,
88
+ });
67
89
 
68
90
  const parent = path.parent as t.JSXElement;
69
91