react-native-boost 0.7.0 → 1.1.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 +13 -7
- package/dist/plugin/esm/index.mjs +219 -43
- package/dist/plugin/esm/index.mjs.map +1 -1
- package/dist/plugin/index.d.ts +54 -0
- package/dist/plugin/index.js +219 -43
- 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 +49 -3
- package/dist/runtime/index.js +16 -4
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/index.web.d.ts +11 -0
- package/dist/runtime/index.web.js.map +1 -1
- package/package.json +3 -2
- package/src/plugin/index.ts +26 -4
- package/src/plugin/optimizers/text/index.ts +52 -21
- package/src/plugin/optimizers/view/index.ts +57 -10
- package/src/plugin/types/index.ts +61 -21
- package/src/plugin/utils/common/validation.ts +23 -28
- package/src/plugin/utils/generate-test-plugin.ts +7 -1
- package/src/plugin/utils/helpers.ts +15 -0
- package/src/plugin/utils/logger.ts +123 -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
|
@@ -39,54 +39,52 @@ export const isIgnoredFile = (path: NodePath<t.JSXOpeningElement>, ignores: stri
|
|
|
39
39
|
return false;
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
+
export const isForcedLine = (path: NodePath<t.JSXOpeningElement>): boolean => {
|
|
43
|
+
return hasDecoratorComment(path, '@boost-force');
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const isIgnoredLine = (path: NodePath<t.JSXOpeningElement>): boolean => {
|
|
47
|
+
return hasDecoratorComment(path, '@boost-ignore');
|
|
48
|
+
};
|
|
49
|
+
|
|
42
50
|
/**
|
|
43
|
-
* Checks if the JSX element
|
|
44
|
-
*
|
|
45
|
-
* The function looks up the JSXOpeningElement's own leading comments as well as
|
|
46
|
-
* the parent element's comments before falling back to inspect siblings.
|
|
51
|
+
* Checks if the JSX element has a preceding comment containing the given decorator string.
|
|
47
52
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
53
|
+
* Scans the JSXOpeningElement's own leading comments, the parent element's comments,
|
|
54
|
+
* ObjectProperty containers, and backward siblings.
|
|
50
55
|
*/
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (path.node.leadingComments?.some((comment) => comment.value.includes('@boost-ignore'))) {
|
|
56
|
+
function hasDecoratorComment(path: NodePath<t.JSXOpeningElement>, decorator: string): boolean {
|
|
57
|
+
if (path.node.leadingComments?.some((comment) => comment.value.includes(decorator))) {
|
|
54
58
|
return true;
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
// Check for @boost-ignore in the leading comments on the parent JSX element.
|
|
58
61
|
const jsxElementPath = path.parentPath;
|
|
59
|
-
if (jsxElementPath.node.leadingComments?.some((comment) => comment.value.includes(
|
|
62
|
+
if (jsxElementPath.node.leadingComments?.some((comment) => comment.value.includes(decorator))) {
|
|
60
63
|
return true;
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
//
|
|
64
|
-
// This handles cases where the JSX element is used as a value inside an object literal.
|
|
66
|
+
// Check leading comments on the ObjectProperty (if the JSX element is a value inside an object literal).
|
|
65
67
|
const propertyPath = jsxElementPath.parentPath;
|
|
66
68
|
if (
|
|
67
69
|
propertyPath &&
|
|
68
70
|
propertyPath.isObjectProperty() &&
|
|
69
|
-
propertyPath.node.leadingComments?.some((comment) => comment.value.includes(
|
|
71
|
+
propertyPath.node.leadingComments?.some((comment) => comment.value.includes(decorator))
|
|
70
72
|
) {
|
|
71
73
|
return true;
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
if (!jsxElementPath.parentPath) return false;
|
|
75
77
|
|
|
76
|
-
// Get the container that holds this element (for example, a JSX fragment or JSX element)
|
|
77
78
|
const containerPath = jsxElementPath.parentPath;
|
|
78
79
|
const siblings = ensureArray(containerPath.get('children'));
|
|
79
80
|
const index = siblings.findIndex((sibling) => sibling.node === jsxElementPath.node);
|
|
80
81
|
if (index === -1) return false;
|
|
81
82
|
|
|
82
|
-
// Look backward from the current element for a non-empty node.
|
|
83
83
|
for (let index_ = index - 1; index_ >= 0; index_--) {
|
|
84
84
|
const sibling = siblings[index_];
|
|
85
|
-
// Skip over any whitespace (only in JSXText nodes)
|
|
86
85
|
if (sibling.isJSXText() && sibling.node.value.trim() === '') {
|
|
87
86
|
continue;
|
|
88
87
|
}
|
|
89
|
-
// If the sibling is a JSX expression container, check its empty expression's comments.
|
|
90
88
|
if (sibling.isJSXExpressionContainer()) {
|
|
91
89
|
const expression = sibling.get('expression');
|
|
92
90
|
if (expression && expression.node) {
|
|
@@ -95,22 +93,21 @@ export const isIgnoredLine = (path: NodePath<t.JSXOpeningElement>): boolean => {
|
|
|
95
93
|
...(expression.node.trailingComments || []),
|
|
96
94
|
...(expression.node.innerComments || []),
|
|
97
95
|
].map((comment) => comment.value.trim());
|
|
98
|
-
if (comments.some((comment) => comment.includes(
|
|
96
|
+
if (comments.some((comment) => comment.includes(decorator))) {
|
|
99
97
|
return true;
|
|
100
98
|
}
|
|
101
99
|
}
|
|
102
100
|
}
|
|
103
|
-
// Also check if the node itself carries a leadingComments property.
|
|
104
101
|
if (
|
|
105
102
|
sibling.node.leadingComments &&
|
|
106
|
-
sibling.node.leadingComments.some((comment) => comment.value.includes(
|
|
103
|
+
sibling.node.leadingComments.some((comment) => comment.value.includes(decorator))
|
|
107
104
|
) {
|
|
108
105
|
return true;
|
|
109
106
|
}
|
|
110
|
-
break;
|
|
107
|
+
break;
|
|
111
108
|
}
|
|
112
109
|
return false;
|
|
113
|
-
}
|
|
110
|
+
}
|
|
114
111
|
|
|
115
112
|
/**
|
|
116
113
|
* Checks if the path represents a valid JSX component with the specified name.
|
|
@@ -181,6 +178,7 @@ export const isReactNativeImport = (path: NodePath<t.JSXOpeningElement>, expecte
|
|
|
181
178
|
};
|
|
182
179
|
|
|
183
180
|
type AncestorClassification = 'safe' | 'text' | 'unknown';
|
|
181
|
+
export type ViewAncestorClassification = AncestorClassification;
|
|
184
182
|
type ScopeBinding = NonNullable<ReturnType<NodePath<t.Node>['scope']['getBinding']>>;
|
|
185
183
|
|
|
186
184
|
type AncestorAnalysisContext = {
|
|
@@ -189,11 +187,8 @@ type AncestorAnalysisContext = {
|
|
|
189
187
|
renderExpressionInProgress: WeakSet<t.Node>;
|
|
190
188
|
};
|
|
191
189
|
|
|
192
|
-
export const
|
|
193
|
-
|
|
194
|
-
if (classification === 'text') return true;
|
|
195
|
-
if (classification === 'unknown' && !allowUnknownAncestors) return true;
|
|
196
|
-
return false;
|
|
190
|
+
export const getViewAncestorClassification = (path: NodePath<t.JSXOpeningElement>): ViewAncestorClassification => {
|
|
191
|
+
return classifyViewAncestors(path);
|
|
197
192
|
};
|
|
198
193
|
|
|
199
194
|
function classifyViewAncestors(path: NodePath<t.JSXOpeningElement>): AncestorClassification {
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { declare } from '@babel/helper-plugin-utils';
|
|
2
2
|
import { Optimizer, PluginOptions } from '../types';
|
|
3
|
+
import { createLogger } from './logger';
|
|
3
4
|
|
|
4
5
|
export const generateTestPlugin = (optimizer: Optimizer, options: PluginOptions = {}) => {
|
|
6
|
+
const logger = createLogger({
|
|
7
|
+
verbose: false,
|
|
8
|
+
silent: true,
|
|
9
|
+
});
|
|
10
|
+
|
|
5
11
|
return declare((api) => {
|
|
6
12
|
api.assertVersion(7);
|
|
7
13
|
|
|
@@ -9,7 +15,7 @@ export const generateTestPlugin = (optimizer: Optimizer, options: PluginOptions
|
|
|
9
15
|
name: 'react-native-boost',
|
|
10
16
|
visitor: {
|
|
11
17
|
JSXOpeningElement(path) {
|
|
12
|
-
optimizer(path,
|
|
18
|
+
optimizer(path, logger, options);
|
|
13
19
|
},
|
|
14
20
|
},
|
|
15
21
|
};
|
|
@@ -2,3 +2,18 @@ export const ensureArray = <T>(value: T | T[]): T[] => {
|
|
|
2
2
|
if (Array.isArray(value)) return value;
|
|
3
3
|
return [value];
|
|
4
4
|
};
|
|
5
|
+
|
|
6
|
+
export type BailoutCheck = {
|
|
7
|
+
reason: string;
|
|
8
|
+
shouldBail: () => boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const getFirstBailoutReason = (checks: readonly BailoutCheck[]): string | null => {
|
|
12
|
+
for (const check of checks) {
|
|
13
|
+
if (check.shouldBail()) {
|
|
14
|
+
return check.reason;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return null;
|
|
19
|
+
};
|
|
@@ -1,3 +1,124 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
HubFile,
|
|
3
|
+
OptimizationLogPayload,
|
|
4
|
+
PluginLogger,
|
|
5
|
+
SkippedOptimizationLogPayload,
|
|
6
|
+
WarningLogPayload,
|
|
7
|
+
} from '../types';
|
|
8
|
+
|
|
9
|
+
const LOG_PREFIX = '[react-native-boost]';
|
|
10
|
+
|
|
11
|
+
const ANSI_RESET = '\u001B[0m';
|
|
12
|
+
const ANSI_GREEN = '\u001B[32m';
|
|
13
|
+
const ANSI_YELLOW = '\u001B[33m';
|
|
14
|
+
const ANSI_MAGENTA = '\u001B[35m';
|
|
15
|
+
const ANSI_RED = '\u001B[31m';
|
|
16
|
+
|
|
17
|
+
export const noopLogger: PluginLogger = {
|
|
18
|
+
optimized() {},
|
|
19
|
+
skipped() {},
|
|
20
|
+
forced() {},
|
|
21
|
+
warning() {},
|
|
3
22
|
};
|
|
23
|
+
|
|
24
|
+
export const createLogger = ({ verbose, silent }: { verbose: boolean; silent: boolean }): PluginLogger => {
|
|
25
|
+
if (silent) return noopLogger;
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
optimized(payload) {
|
|
29
|
+
writeLog('optimized', `Optimized ${payload.component} in ${formatPathLocation(payload.path)}`);
|
|
30
|
+
},
|
|
31
|
+
skipped(payload) {
|
|
32
|
+
if (!verbose) return;
|
|
33
|
+
writeLog('skipped', `Skipped ${payload.component} in ${formatPathLocation(payload.path)} (${payload.reason})`);
|
|
34
|
+
},
|
|
35
|
+
forced(payload) {
|
|
36
|
+
writeLog(
|
|
37
|
+
'forced',
|
|
38
|
+
`Force-optimized ${payload.component} in ${formatPathLocation(payload.path)} (skipped bailout: ${payload.reason})`
|
|
39
|
+
);
|
|
40
|
+
},
|
|
41
|
+
warning(payload) {
|
|
42
|
+
const context = formatWarningContext(payload);
|
|
43
|
+
const message = context.length > 0 ? `${context}: ${payload.message}` : payload.message;
|
|
44
|
+
writeLog('warning', message);
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function formatWarningContext(payload: WarningLogPayload): string {
|
|
50
|
+
const location = formatPathLocation(payload.path);
|
|
51
|
+
|
|
52
|
+
if (payload.component && location.length > 0) {
|
|
53
|
+
return `${payload.component} in ${location}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (payload.component) {
|
|
57
|
+
return payload.component;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return location;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type LogLevel = 'optimized' | 'skipped' | 'forced' | 'warning';
|
|
64
|
+
|
|
65
|
+
function writeLog(level: LogLevel, message: string): void {
|
|
66
|
+
const levelTag = formatLevel(level);
|
|
67
|
+
console.log(`${LOG_PREFIX} ${levelTag} ${message}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatLevel(level: LogLevel): string {
|
|
71
|
+
if (level === 'optimized') {
|
|
72
|
+
return colorize('[optimized]', ANSI_GREEN);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (level === 'skipped') {
|
|
76
|
+
return colorize('[skipped]', ANSI_YELLOW);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (level === 'forced') {
|
|
80
|
+
return colorize('[forced]', ANSI_RED);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return colorize('[warning]', ANSI_MAGENTA);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function colorize(value: string, colorCode: string): string {
|
|
87
|
+
if (!shouldUseColor()) return value;
|
|
88
|
+
return `${colorCode}${value}${ANSI_RESET}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function shouldUseColor(): boolean {
|
|
92
|
+
if (process.env.NO_COLOR != null) return false;
|
|
93
|
+
|
|
94
|
+
if (process.env.FORCE_COLOR === '0') return false;
|
|
95
|
+
if (process.env.FORCE_COLOR != null) return true;
|
|
96
|
+
|
|
97
|
+
if (process.env.CLICOLOR === '0') return false;
|
|
98
|
+
if (process.env.CLICOLOR_FORCE != null && process.env.CLICOLOR_FORCE !== '0') return true;
|
|
99
|
+
|
|
100
|
+
if (process.stdout?.isTTY === true || process.stderr?.isTTY === true) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const colorTerm = process.env.COLORTERM;
|
|
105
|
+
if (colorTerm != null && colorTerm !== '') {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const term = process.env.TERM;
|
|
110
|
+
return term != null && term !== '' && term.toLowerCase() !== 'dumb';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function formatPathLocation(
|
|
114
|
+
payloadPath: OptimizationLogPayload['path'] | SkippedOptimizationLogPayload['path'] | undefined
|
|
115
|
+
): string {
|
|
116
|
+
if (!payloadPath) return 'unknown file:unknown line';
|
|
117
|
+
|
|
118
|
+
const hub = payloadPath.hub as unknown;
|
|
119
|
+
const file = typeof hub === 'object' && hub !== null && 'file' in hub ? (hub.file as HubFile) : undefined;
|
|
120
|
+
const filename = file?.opts?.filename ?? 'unknown file';
|
|
121
|
+
const lineNumber = payloadPath.node.loc?.start.line ?? 'unknown line';
|
|
122
|
+
|
|
123
|
+
return `${filename}:${lineNumber}`;
|
|
124
|
+
}
|
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-require-imports,unicorn/prefer-module */
|
|
2
|
-
import { Platform } from 'react-native';
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
import type { ComponentType } from 'react';
|
|
4
|
+
import type { TextProps } from 'react-native';
|
|
5
|
+
|
|
6
|
+
const reactNative = require('react-native');
|
|
7
|
+
const isWeb = reactNative.Platform.OS === 'web';
|
|
8
|
+
|
|
9
|
+
let nativeText = reactNative.unstable_NativeText;
|
|
10
|
+
|
|
11
|
+
if (isWeb || nativeText == null) {
|
|
12
|
+
// Fallback to regular Text component if unstable_NativeText is not available or we're on Web
|
|
13
|
+
nativeText = reactNative.Text;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Native Text component with graceful fallback.
|
|
18
|
+
*
|
|
19
|
+
* @remarks
|
|
20
|
+
* Uses `unstable_NativeText` on supported native runtimes and falls back to `Text`
|
|
21
|
+
* on web or when the unstable export is unavailable.
|
|
22
|
+
*/
|
|
23
|
+
export const NativeText: ComponentType<TextProps> = nativeText;
|
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-require-imports,unicorn/prefer-module */
|
|
2
|
-
import { Platform } from 'react-native';
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
import type { ComponentType } from 'react';
|
|
4
|
+
import type { ViewProps } from 'react-native';
|
|
5
|
+
|
|
6
|
+
const reactNative = require('react-native');
|
|
7
|
+
const isWeb = reactNative.Platform.OS === 'web';
|
|
8
|
+
|
|
9
|
+
let nativeView = reactNative.unstable_NativeView;
|
|
10
|
+
|
|
11
|
+
if (isWeb || nativeView == null) {
|
|
12
|
+
// Fallback to regular View component if unstable_NativeView is not available or we're on Web
|
|
13
|
+
nativeView = reactNative.View;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Native View component with graceful fallback.
|
|
18
|
+
*
|
|
19
|
+
* @remarks
|
|
20
|
+
* Uses `unstable_NativeView` on supported native runtimes and falls back to `View`
|
|
21
|
+
* on web or when the unstable export is unavailable.
|
|
22
|
+
*/
|
|
23
|
+
export const NativeView: ComponentType<ViewProps> = nativeView;
|
package/src/runtime/index.ts
CHANGED
|
@@ -4,6 +4,16 @@ import { userSelectToSelectableMap, verticalAlignToTextAlignVerticalMap } from '
|
|
|
4
4
|
|
|
5
5
|
const propsCache = new WeakMap();
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Normalizes `Text` style values for `NativeText`.
|
|
9
|
+
*
|
|
10
|
+
* @param style - Style prop passed to a text-like component.
|
|
11
|
+
* @returns Native-friendly text props. Returns an empty object when `style` is falsy or cannot be normalized.
|
|
12
|
+
* @remarks
|
|
13
|
+
* - Flattens style arrays via `StyleSheet.flatten`
|
|
14
|
+
* - Converts numeric `fontWeight` values to string values
|
|
15
|
+
* - Maps `userSelect` and `verticalAlign` to native-compatible props
|
|
16
|
+
*/
|
|
7
17
|
export function processTextStyle(style: GenericStyleProp<TextStyle>): Partial<TextProps> {
|
|
8
18
|
if (!style) return {};
|
|
9
19
|
|
|
@@ -38,6 +48,16 @@ export function processTextStyle(style: GenericStyleProp<TextStyle>): Partial<Te
|
|
|
38
48
|
return props;
|
|
39
49
|
}
|
|
40
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Normalizes accessibility and ARIA props for runtime native components.
|
|
53
|
+
*
|
|
54
|
+
* @param props - Accessibility and ARIA props.
|
|
55
|
+
* @returns Props with normalized accessibility fields.
|
|
56
|
+
* @remarks
|
|
57
|
+
* - Merges `aria-label` with `accessibilityLabel`
|
|
58
|
+
* - Merges ARIA state fields into `accessibilityState`
|
|
59
|
+
* - Defaults `accessible` to `true` when omitted
|
|
60
|
+
*/
|
|
41
61
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
62
|
export function processAccessibilityProps(props: Record<string, any>): Record<string, any> {
|
|
43
63
|
const {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Maps CSS-like `userSelect` values to React Native's `selectable` prop.
|
|
3
|
+
*/
|
|
2
4
|
export const userSelectToSelectableMap = {
|
|
3
5
|
auto: true,
|
|
4
6
|
text: true,
|
|
@@ -7,7 +9,9 @@ export const userSelectToSelectableMap = {
|
|
|
7
9
|
all: true,
|
|
8
10
|
};
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Maps CSS-like `verticalAlign` values to React Native's `textAlignVertical`.
|
|
14
|
+
*/
|
|
11
15
|
export const verticalAlignToTextAlignVerticalMap = {
|
|
12
16
|
auto: 'auto',
|
|
13
17
|
top: 'top',
|