-
Notifications
You must be signed in to change notification settings - Fork 125
Expand file tree
/
Copy pathInlined_React_Runtime_Check.php
More file actions
259 lines (232 loc) · 8.93 KB
/
Copy pathInlined_React_Runtime_Check.php
File metadata and controls
259 lines (232 loc) · 8.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
<?php
/**
* Class Inlined_React_Runtime_Check.
*
* @package plugin-check
*/
namespace WordPress\Plugin_Check\Checker\Checks\Performance;
use Exception;
use WordPress\Plugin_Check\Checker\Check_Categories;
use WordPress\Plugin_Check\Checker\Check_Result;
use WordPress\Plugin_Check\Checker\Checks\Abstract_File_Check;
use WordPress\Plugin_Check\Traits\Amend_Check_Result;
use WordPress\Plugin_Check\Traits\Stable_Check;
/**
* Check to detect a bundled, outdated React runtime that breaks under React 19.
*
* WordPress is moving from React 18 to React 19. Testing has shown that the vast
* majority of plugin breakages come from a single cause: the plugin inlines the
* `react/jsx-runtime` library into its build output instead of externalizing it
* (i.e. relying on the copy shipped with WordPress). The element object shape
* changed between React 18 and 19, so elements produced by an inlined pre-19
* runtime are rejected by the React 19 bundled with WordPress.
*
* Detection is based on the high confidence signal `Symbol.for( 'react.element' )`,
* which only appears when a pre-19 JSX runtime is inlined. React 19 uses a
* different marker (`react.transitional.element`), so this does not match builds
* that already externalize the runtime. As an additional signal, usages of React
* APIs that were removed in React 19 are reported as well.
*
* @since 2.0.0
*/
class Inlined_React_Runtime_Check extends Abstract_File_Check {
use Amend_Check_Result;
use Stable_Check;
/**
* Gets the categories for the check.
*
* Every check must have at least one category.
*
* @since 2.0.0
*
* @return array The categories for the check.
*/
public function get_categories() {
return array( Check_Categories::CATEGORY_PERFORMANCE );
}
/**
* Amends the given result by running the check on the given list of files.
*
* @since 2.0.0
*
* @param Check_Result $result The check result to amend, including the plugin context to check.
* @param array $files List of absolute file paths.
*
* @throws Exception Thrown when the check fails with a critical error (unrelated to any errors detected as part of
* the check).
*/
protected function check_files( Check_Result $result, array $files ) {
$js_files = self::filter_files_by_extension( $files, 'js' );
foreach ( $js_files as $file ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
$contents = file_get_contents( $file );
if ( false === $contents ) {
continue;
}
$this->look_for_inlined_jsx_runtime( $result, $file, $contents );
$this->look_for_removed_react_apis( $result, $file, $contents );
}
}
/**
* Reports an inlined pre-19 JSX runtime, unless the runtime is externalized.
*
* @since 2.0.0
*
* @param Check_Result $result The check result to amend.
* @param string $file Absolute path to the JavaScript file.
* @param string $contents Contents of the JavaScript file.
*/
private function look_for_inlined_jsx_runtime( Check_Result $result, $file, $contents ) {
// `Symbol.for( 'react.element' )` is only emitted by an inlined pre-19 JSX runtime.
$position = $this->find_first_match( '/Symbol\.for\(\s*[\'"]react\.element[\'"]\s*\)/', $contents );
if ( false === $position ) {
return;
}
// Do not warn when the runtime is externalized to the copy shipped with WordPress.
if ( $this->is_jsx_runtime_externalized( $contents ) ) {
return;
}
$this->add_result_warning_for_file(
$result,
__( 'This file appears to inline the React JSX runtime instead of externalizing it. Bundled pre-React 19 runtimes break when WordPress upgrades to React 19. Use the dependency extraction webpack plugin so that "react-jsx-runtime" is loaded from WordPress instead.', 'plugin-check' ),
'inlined_jsx_runtime',
$file,
$position['line'],
$position['column'],
'https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dependency-extraction-webpack-plugin/',
6
);
}
/**
* Reports usage of React APIs that were removed in React 19.
*
* @since 2.0.0
*
* @param Check_Result $result The check result to amend.
* @param string $file Absolute path to the JavaScript file.
* @param string $contents Contents of the JavaScript file.
*/
private function look_for_removed_react_apis( Check_Result $result, $file, $contents ) {
// Blank out comments and string literals first, so a mention in a code
// comment, changelog entry, or translation string is not reported as usage.
$scannable = $this->blank_comments_and_strings( $contents );
// These identifiers are React-specific and were removed in React 19.
$matched = '';
$position = $this->find_first_match( '/\b(?:unmountComponentAtNode|findDOMNode|ReactCurrentOwner)\b/', $scannable, $matched );
if ( false === $position ) {
return;
}
$this->add_result_warning_for_file(
$result,
sprintf(
/* translators: %s: the removed React API name */
__( 'This file references "%s", a React API that was removed in React 19 and will stop working once WordPress upgrades React. Update the bundled code to a React 19 compatible version.', 'plugin-check' ),
$matched
),
'react_removed_api',
$file,
$position['line'],
$position['column'],
'https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dependency-extraction-webpack-plugin/',
5
);
}
/**
* Determines whether the file references the externalized JSX runtime.
*
* A build that externalizes the runtime references the global
* `window.ReactJSXRuntime` shipped with WordPress. A `react-jsx-runtime`
* dependency declared in the sibling `*.asset.php` file is deliberately not
* treated as proof here: the `Symbol.for( 'react.element' )` marker means the
* file already inlines a pre-19 runtime, and a declared dependency does not
* rule out a stale or mixed build that still bundles its own copy. Trusting
* the asset file in that case would hide the exact breakage this check exists
* to surface.
*
* @since 2.0.0
*
* @param string $contents Contents of the JavaScript file.
* @return bool True if the runtime is externalized, false otherwise.
*/
private function is_jsx_runtime_externalized( $contents ) {
return str_contains( $contents, 'window.ReactJSXRuntime' );
}
/**
* Blanks out comments and string literals in JavaScript contents.
*
* Characters inside line comments, block comments, and single-, double-, or
* backtick-quoted strings are replaced with spaces. The length of the string
* and every newline are preserved, so match offsets still map to the correct
* line and column in the original contents.
*
* @since 2.0.0
*
* @param string $contents Contents of the JavaScript file.
* @return string The contents with comments and string literals blanked out.
*/
private function blank_comments_and_strings( $contents ) {
$pattern = '~/\*.*?\*/|//[^\r\n]*|"(?:\\\\.|[^"\\\\])*"|\'(?:\\\\.|[^\'\\\\])*\'|`(?:\\\\.|[^`\\\\])*`~s';
$blanked = preg_replace_callback(
$pattern,
static function ( $matches ) {
return preg_replace( '/[^\r\n]/', ' ', $matches[0] );
},
$contents
);
// On a PCRE failure (e.g. backtracking limit) fall back to the raw contents.
return is_string( $blanked ) ? $blanked : $contents;
}
/**
* Finds the first occurrence of a pattern and returns its line and column.
*
* @since 2.0.0
*
* @param string $pattern The regular expression pattern to search for.
* @param string $contents The contents to search.
* @param string|null $matched Optional. Populated with the matched text, passed by reference.
* @return array|false Array with `line` and `column` keys, or false if no match was found.
*/
private function find_first_match( $pattern, $contents, &$matched = null ) {
if ( ! preg_match( $pattern, $contents, $matches, PREG_OFFSET_CAPTURE ) ) {
return false;
}
$matched = $matches[0][0];
$offset = $matches[0][1];
if ( 0 === $offset ) {
return array(
'line' => 1,
'column' => 1,
);
}
$before = substr( $contents, 0, $offset );
$exploded = explode( PHP_EOL, $before );
return array(
'line' => count( $exploded ),
'column' => strlen( (string) end( $exploded ) ) + 1,
);
}
/**
* Gets the description for the check.
*
* Every check must have a short description explaining what the check does.
*
* @since 2.0.0
*
* @return string Description.
*/
public function get_description(): string {
return __( 'Detects a bundled, outdated React runtime that is incompatible with React 19.', 'plugin-check' );
}
/**
* Gets the documentation URL for the check.
*
* Every check must have a URL with further information about the check.
*
* @since 2.0.0
*
* @return string The documentation URL.
*/
public function get_documentation_url(): string {
return __( 'https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dependency-extraction-webpack-plugin/', 'plugin-check' );
}
}