Skip to content

Commit cfd3baf

Browse files
westonrutergemini-code-assist[bot]Copilot
committed
Switch from Esprima to Espree for JavaScript linting in CodeMirror.
Replaces the `esprima`-based validation in the code editor with `espree` to provide support for modern JavaScript (ES6+). Key Changes: 1. **New Linter Integration:** * Introduces `src/js/_enqueues/vendor/codemirror/javascript-lint.js` which uses `espree` (v9.6.1) for parsing and error reporting. * This replaces the previous dependency on the `jshint` and `esprima` scripts. The `espree` module is now loaded via a dynamic import on demand by the new javascript lint integration. * This custom linter is bundled into the CodeMirror build via `tools/vendors/codemirror-entry.js`. 2. **Script Modules:** * Registers `espree` as a script module in `src/wp-includes/script-modules.php`. * Adds a workaround in the `wp-codemirror` registration to ensure `espree` is included in the importmap. 3. **Editor Settings:** * Updates `wp_get_code_editor_settings()` in `src/wp-includes/general-template.php` to use ES11 defaults. * Synchronizes JSHint settings from .jshintrc, even though these are not supported by Espree. 4. **Deprecations:** * Marks `esprima` and `jshint` script handles as deprecated in `src/wp-includes/script-loader.php`. 5. **Build Tools:** * Updates Webpack configuration (`tools/webpack/codemirror.config.js`) to bundle `espree` as a module. * Updates `codemirror-entry.js` to use the new local `javascript-lint.js`. Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 438132f commit cfd3baf

File tree

11 files changed

+233
-65
lines changed

11 files changed

+233
-65
lines changed

package-lock.json

Lines changed: 1 addition & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"core-js-url-browser": "3.6.4",
8080
"csslint": "1.0.5",
8181
"element-closest": "3.0.2",
82+
"espree": "9.6.1",
8283
"esprima": "4.0.1",
8384
"formdata-polyfill": "4.0.10",
8485
"hoverintent": "2.2.1",
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/* globals define, CodeMirror */
2+
/* jshint devel: true */
3+
/* jshint esversion: 11 */
4+
5+
// CodeMirror, copyright (c) by Marijn Haverbeke and others
6+
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
7+
8+
( function ( mod ) {
9+
if ( typeof exports === 'object' && typeof module === 'object' ) {
10+
// CommonJS
11+
mod( require( 'codemirror' ) );
12+
} else if ( typeof define === 'function' && define.amd ) {
13+
// AMD
14+
define( [ 'codemirror' ], mod );
15+
// Plain browser env
16+
} else {
17+
mod( CodeMirror );
18+
}
19+
} )( function ( CodeMirror ) {
20+
'use strict';
21+
22+
/**
23+
* CodeMirror Lint Error.
24+
*
25+
* @see https://codemirror.net/5/doc/manual.html#addon_lint
26+
*
27+
* @typedef {Object} CodeMirrorLintError
28+
* @property {string} message - Error message.
29+
* @property {'error'} severity - Severity.
30+
* @property {{line: number, ch: number}} from - From position.
31+
* @property {{line: number, ch: number}} to - To position.
32+
*/
33+
34+
/**
35+
* JSHint options supported by Espree.
36+
*
37+
* @see https://jshint.com/docs/options/
38+
* @see https://www.npmjs.com/package/espree#options
39+
*
40+
* @typedef {Object} SupportedJSHintOptions
41+
* @property {number} [esversion] - "This option is used to specify the ECMAScript version to which the code must adhere."
42+
* @property {boolean} [es5] - "This option enables syntax first defined in the ECMAScript 5.1 specification. This includes allowing reserved keywords as object properties."
43+
* @property {boolean} [es3] - "This option tells JSHint that your code needs to adhere to ECMAScript 3 specification. Use this option if you need your program to be executable in older browsers—such as Internet Explorer 6/7/8/9—and other legacy JavaScript environments."
44+
* @property {boolean} [module] - "This option informs JSHint that the input code describes an ECMAScript 6 module. All module code is interpreted as strict mode code."
45+
* @property {'implied'} [strict] - "This option requires the code to run in ECMAScript 5's strict mode."
46+
*/
47+
48+
/**
49+
* Validates JavaScript.
50+
*
51+
* @param {string} text - Source.
52+
* @param {SupportedJSHintOptions} options - Linting options.
53+
* @returns {Promise<CodeMirrorLintError[]>}
54+
*/
55+
async function validator( text, options ) {
56+
const errors = /** @type {CodeMirrorLintError[]} */ [];
57+
try {
58+
const espree = await import( 'espree' );
59+
espree.parse( text, {
60+
...getEspreeOptions( options ),
61+
loc: true,
62+
} );
63+
} catch ( error ) {
64+
if (
65+
// This is an `EnhancedSyntaxError` in Espree: <https://github.com/brettz9/espree/blob/3c1120280b24f4a5e4c3125305b072fa0dfca22b/packages/espree/lib/espree.js#L48-L54>.
66+
error instanceof SyntaxError &&
67+
typeof error.lineNumber === 'number' &&
68+
typeof error.column === 'number'
69+
) {
70+
const line = error.lineNumber - 1;
71+
errors.push( {
72+
message: error.message,
73+
severity: 'error',
74+
from: { line, ch: error.column - 1 },
75+
to: { line, ch: error.column },
76+
} );
77+
} else {
78+
console.warn( '[CodeMirror] Unable to lint JavaScript:', error );
79+
}
80+
}
81+
82+
return errors;
83+
}
84+
85+
CodeMirror.registerHelper( 'lint', 'javascript', validator );
86+
87+
/**
88+
* Gets the options for Espree from the supported JSHint options.
89+
*
90+
* @param {SupportedJSHintOptions} options - Linting options for JSHint.
91+
* @return {{
92+
* ecmaVersion?: number|'latest',
93+
* ecmaFeatures?: {
94+
* impliedStrict?: true
95+
* }
96+
* }}
97+
*/
98+
function getEspreeOptions( options ) {
99+
const ecmaFeatures = {};
100+
if ( options.strict === 'implied' ) {
101+
ecmaFeatures.impliedStrict = true;
102+
}
103+
104+
return {
105+
ecmaVersion: getEcmaVersion( options ),
106+
sourceType: options.module ? 'module' : 'script',
107+
ecmaFeatures,
108+
};
109+
}
110+
111+
/**
112+
* Gets the ECMAScript version.
113+
*
114+
* @param {SupportedJSHintOptions} options - Options.
115+
* @return {number|'latest'} ECMAScript version.
116+
*/
117+
function getEcmaVersion( options ) {
118+
if ( typeof options.esversion === 'number' ) {
119+
return options.esversion;
120+
}
121+
if ( options.es5 ) {
122+
return 5;
123+
}
124+
if ( options.es3 ) {
125+
return 3;
126+
}
127+
return 'latest';
128+
}
129+
} );

src/wp-includes/general-template.php

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4045,6 +4045,7 @@ function wp_enqueue_code_editor( $args ) {
40454045

40464046
wp_enqueue_script( 'code-editor' );
40474047
wp_enqueue_style( 'code-editor' );
4048+
wp_enqueue_script_module( 'wp-codemirror' ); // Hack to get importmap printed with espree.
40484049

40494050
if ( isset( $settings['codemirror']['mode'] ) ) {
40504051
$mode = $settings['codemirror']['mode'];
@@ -4069,7 +4070,6 @@ function wp_enqueue_code_editor( $args ) {
40694070
case 'text/x-php':
40704071
wp_enqueue_script( 'htmlhint' );
40714072
wp_enqueue_script( 'csslint' );
4072-
wp_enqueue_script( 'jshint' );
40734073
if ( ! current_user_can( 'unfiltered_html' ) ) {
40744074
wp_enqueue_script( 'htmlhint-kses' );
40754075
}
@@ -4081,7 +4081,6 @@ function wp_enqueue_code_editor( $args ) {
40814081
case 'application/ld+json':
40824082
case 'text/typescript':
40834083
case 'application/typescript':
4084-
wp_enqueue_script( 'jshint' );
40854084
wp_enqueue_script( 'jsonlint' );
40864085
break;
40874086
}
@@ -4153,30 +4152,35 @@ function wp_get_code_editor_settings( $args ) {
41534152
'outline-none' => true,
41544153
),
41554154
'jshint' => array(
4156-
// The following are copied from <https://github.com/WordPress/wordpress-develop/blob/4.8.1/.jshintrc>.
4157-
'boss' => true,
4158-
'curly' => true,
4159-
'eqeqeq' => true,
4160-
'eqnull' => true,
4161-
'es3' => true,
4162-
'expr' => true,
4163-
'immed' => true,
4164-
'noarg' => true,
4165-
'nonbsp' => true,
4166-
'onevar' => true,
4167-
'quotmark' => 'single',
4168-
'trailing' => true,
4169-
'undef' => true,
4170-
'unused' => true,
4171-
4172-
'browser' => true,
4173-
4174-
'globals' => array(
4175-
'_' => false,
4176-
'Backbone' => false,
4177-
'jQuery' => false,
4178-
'JSON' => false,
4179-
'wp' => false,
4155+
'esversion' => 11,
4156+
4157+
// The following are copied from <https://github.com/WordPress/wordpress-develop/blob/6.9.0/.jshintrc>.
4158+
// Nevertheless, they are not supported by Espree, which is used instead of JSHint for licensing reasons.
4159+
'boss' => true,
4160+
'curly' => true,
4161+
'eqeqeq' => true,
4162+
'eqnull' => true,
4163+
'expr' => true,
4164+
'immed' => true,
4165+
'noarg' => true,
4166+
'nonbsp' => true,
4167+
'quotmark' => 'single',
4168+
'undef' => true,
4169+
'unused' => true,
4170+
'browser' => true,
4171+
'globals' => array(
4172+
'_' => false,
4173+
'Backbone' => false,
4174+
'jQuery' => false,
4175+
'JSON' => false,
4176+
'wp' => false,
4177+
'export' => false,
4178+
'module' => false,
4179+
'require' => false,
4180+
'WorkerGlobalScope' => false,
4181+
'self' => false,
4182+
'OffscreenCanvas' => false,
4183+
'Promise' => false,
41804184
),
41814185
),
41824186
'htmlhint' => array(

src/wp-includes/script-loader.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,8 +1197,8 @@ function wp_default_scripts( $scripts ) {
11971197

11981198
$scripts->add( 'wp-codemirror', '/wp-includes/js/codemirror/codemirror.min.js', array(), '5.65.20' );
11991199
$scripts->add( 'csslint', '/wp-includes/js/codemirror/csslint.js', array(), '1.0.5' );
1200-
$scripts->add( 'esprima', '/wp-includes/js/codemirror/esprima.js', array(), '4.0.1' );
1201-
$scripts->add( 'jshint', '/wp-includes/js/codemirror/fakejshint.js', array( 'esprima' ), '2.9.5' );
1200+
$scripts->add( 'esprima', '/wp-includes/js/codemirror/esprima.js', array(), '4.0.1' ); // Deprecated. Use 'espree' script module.
1201+
$scripts->add( 'jshint', '/wp-includes/js/codemirror/fakejshint.js', array( 'esprima' ), '2.9.5' ); // Deprecated.
12021202
$scripts->add( 'jsonlint', '/wp-includes/js/codemirror/jsonlint.js', array(), '1.6.3' );
12031203
$scripts->add( 'htmlhint', '/wp-includes/js/codemirror/htmlhint.js', array(), '1.8.0' );
12041204
$scripts->add( 'htmlhint-kses', '/wp-includes/js/codemirror/htmlhint-kses.js', array( 'htmlhint' ) );

src/wp-includes/script-modules.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,25 @@ function wp_default_script_modules() {
194194
$module_deps = $script_module_data['module_dependencies'] ?? array();
195195
wp_register_script_module( $script_module_id, $path, $module_deps, $script_module_data['version'], $args );
196196
}
197+
198+
wp_register_script_module(
199+
'espree',
200+
includes_url( 'js/codemirror/espree.min.js' ),
201+
array(),
202+
'9.6.1'
203+
);
204+
205+
// The following is a workaround for classic scripts not yet being able to depend on modules. See <https://core.trac.wordpress.org/ticket/61500>.
206+
wp_register_script_module(
207+
'wp-codemirror',
208+
'', // An empty string is a hack to cause the dependencies to be printed in the importmap without a dependent script being printed.
209+
array(
210+
array(
211+
'id' => 'espree',
212+
'import' => 'dynamic',
213+
),
214+
)
215+
);
197216
}
198217

199218
/**

tests/phpunit/tests/dependencies/scripts.php

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3039,14 +3039,12 @@ public function test_wp_enqueue_code_editor_when_php_file_will_be_passed() {
30393039
'curly',
30403040
'eqeqeq',
30413041
'eqnull',
3042-
'es3',
3042+
'esversion',
30433043
'expr',
30443044
'immed',
30453045
'noarg',
30463046
'nonbsp',
3047-
'onevar',
30483047
'quotmark',
3049-
'trailing',
30503048
'undef',
30513049
'unused',
30523050
'browser',
@@ -3123,14 +3121,12 @@ public function test_wp_enqueue_code_editor_when_generated_array_by_compact_will
31233121
'curly',
31243122
'eqeqeq',
31253123
'eqnull',
3126-
'es3',
3124+
'esversion',
31273125
'expr',
31283126
'immed',
31293127
'noarg',
31303128
'nonbsp',
3131-
'onevar',
31323129
'quotmark',
3133-
'trailing',
31343130
'undef',
31353131
'unused',
31363132
'browser',
@@ -3221,14 +3217,12 @@ public function test_wp_enqueue_code_editor_when_generated_array_by_array_merge_
32213217
'curly',
32223218
'eqeqeq',
32233219
'eqnull',
3224-
'es3',
3220+
'esversion',
32253221
'expr',
32263222
'immed',
32273223
'noarg',
32283224
'nonbsp',
3229-
'onevar',
32303225
'quotmark',
3231-
'trailing',
32323226
'undef',
32333227
'unused',
32343228
'browser',
@@ -3316,14 +3310,12 @@ public function test_wp_enqueue_code_editor_when_simple_array_will_be_passed() {
33163310
'curly',
33173311
'eqeqeq',
33183312
'eqnull',
3319-
'es3',
3313+
'esversion',
33203314
'expr',
33213315
'immed',
33223316
'noarg',
33233317
'nonbsp',
3324-
'onevar',
33253318
'quotmark',
3326-
'trailing',
33273319
'undef',
33283320
'unused',
33293321
'browser',
@@ -3901,7 +3893,7 @@ static function ( $dependency ) {
39013893
);
39023894

39033895
// Exclude packages that are not registered in WordPress.
3904-
$exclude = array( 'react-is', 'json2php' );
3896+
$exclude = array( 'react-is', 'json2php', 'espree' );
39053897
$package_json_dependencies = array_diff( $package_json_dependencies, $exclude );
39063898

39073899
/*

tests/phpunit/tests/widgets/wpWidgetCustomHtml.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,6 @@ public function test_enqueue_admin_scripts_when_logged_in_and_syntax_highlightin
251251
$this->assertTrue( wp_script_is( 'code-editor', 'enqueued' ) );
252252
$this->assertTrue( wp_script_is( 'wp-codemirror', 'enqueued' ) );
253253
$this->assertTrue( wp_script_is( 'csslint', 'enqueued' ) );
254-
$this->assertTrue( wp_script_is( 'jshint', 'enqueued' ) );
255254
$this->assertTrue( wp_script_is( 'htmlhint', 'enqueued' ) );
256255
}
257256

tools/vendors/codemirror-entry.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ require( 'codemirror/addon/hint/xml-hint' );
1919
require( 'codemirror/addon/lint/lint' );
2020
require( 'codemirror/addon/lint/css-lint' );
2121
require( 'codemirror/addon/lint/html-lint' );
22-
require( 'codemirror/addon/lint/javascript-lint' );
22+
23+
require( '../../src/js/_enqueues/vendor/codemirror/javascript-lint' );
2324
require( 'codemirror/addon/lint/json-lint' );
2425

2526
// Addons (Other)

tools/vendors/espree-entry.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from 'espree';

0 commit comments

Comments
 (0)