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.
- package/README.md +0 -3
- package/dist/plugin/esm/index.mjs +709 -199
- package/dist/plugin/esm/index.mjs.map +1 -1
- package/dist/plugin/index.d.ts +54 -0
- package/dist/plugin/index.js +709 -199
- package/dist/plugin/index.js.map +1 -1
- package/dist/runtime/esm/index.mjs +15 -3
- package/dist/runtime/esm/index.mjs.map +1 -1
- package/dist/runtime/esm/index.web.mjs.map +1 -1
- package/dist/runtime/index.d.ts +51 -4
- package/dist/runtime/index.js +16 -4
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/index.web.d.ts +13 -1
- package/dist/runtime/index.web.js.map +1 -1
- package/package.json +13 -14
- package/src/plugin/index.ts +27 -5
- package/src/plugin/optimizers/text/index.ts +116 -92
- package/src/plugin/optimizers/view/index.ts +53 -31
- package/src/plugin/types/index.ts +67 -17
- package/src/plugin/utils/common/attributes.ts +165 -0
- package/src/plugin/utils/common/index.ts +1 -3
- package/src/plugin/utils/common/validation.ts +513 -0
- package/src/plugin/utils/constants.ts +9 -0
- package/src/plugin/utils/format-test-result.ts +29 -0
- package/src/plugin/utils/generate-test-plugin.ts +9 -3
- package/src/plugin/utils/helpers.ts +15 -0
- package/src/plugin/utils/logger.ts +109 -2
- package/src/runtime/components/native-text.tsx +21 -5
- package/src/runtime/components/native-view.tsx +21 -5
- package/src/runtime/index.ts +20 -0
- package/src/runtime/types/index.ts +5 -0
- package/src/runtime/utils/constants.ts +6 -2
- package/src/plugin/utils/common/ancestors.ts +0 -120
- 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":["
|
|
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 {
|
|
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":["
|
|
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.
|
|
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": "
|
|
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": "
|
|
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": "^
|
|
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": "^
|
|
100
|
-
"babel-plugin-tester": "^
|
|
99
|
+
"@types/node": "^24",
|
|
100
|
+
"babel-plugin-tester": "^12.0.0",
|
|
101
101
|
"esbuild-node-externals": "^1.18.0",
|
|
102
|
-
"
|
|
103
|
-
"
|
|
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": "
|
|
109
|
-
"vitest": "^
|
|
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": {
|
package/src/plugin/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
16
|
-
const
|
|
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
|
-
'
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
85
|
+
logger.optimized({
|
|
86
|
+
component: 'Text',
|
|
87
|
+
path,
|
|
88
|
+
});
|
|
58
89
|
|
|
59
90
|
// Process props
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
'
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
85
|
+
logger.optimized({
|
|
86
|
+
component: 'View',
|
|
87
|
+
path,
|
|
88
|
+
});
|
|
67
89
|
|
|
68
90
|
const parent = path.parent as t.JSXElement;
|
|
69
91
|
|