Skip to content

Commit 81885a9

Browse files
committed
Code Editor: Switch from Esprima to Espree for JavaScript linting in CodeMirror.
Esprima is no longer maintained, and it does not support the latest JavaScript features in ES11, as Espree does. - **New Linter Integration:** Introduces `src/js/_enqueues/vendor/codemirror/javascript-lint.js` using `espree` for parsing and error reporting, replacing the dependency on `jshint` and `esprima` scripts. - **Script Modules:** Registers `espree` as a script module and leverages the `module_dependencies` argument in `wp_register_script()` to ensure `espree` is available as a dynamic import. - **Editor Settings:** Updates `wp_get_code_editor_settings()` to use ES11 (ECMAScript 2020) defaults and synchronizes JSHint settings from `.jshintrc` for compatibility. - **Editable Extensions:** Adds `.mjs` to the list of editable file extensions for plugins and themes. - **Deprecations:** Marks `esprima` and `jshint` script handles as deprecated. - **Build Tools:** Updates Webpack configuration to bundle `espree` as a module and use the new local `javascript-lint.js`. Developed in #10806 Follow-up to [61587], [61544], [61539], [42547]. Props westonruter, jonsurrell. See #64562, #61500, #48456, #42850. Fixes #64558. git-svn-id: https://develop.svn.wordpress.org/trunk@61611 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 163bc04 commit 81885a9

11 files changed

Lines changed: 320 additions & 140 deletions

File tree

package-lock.json

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

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"@lodder/grunt-postcss": "^3.1.1",
3131
"@playwright/test": "1.56.1",
3232
"@pmmmwh/react-refresh-webpack-plugin": "0.6.1",
33+
"@types/codemirror": "5.60.17",
3334
"@wordpress/e2e-test-utils-playwright": "1.33.2",
3435
"@wordpress/prettier-config": "4.33.1",
3536
"@wordpress/scripts": "30.26.2",
@@ -79,6 +80,7 @@
7980
"core-js-url-browser": "3.6.4",
8081
"csslint": "1.0.5",
8182
"element-closest": "3.0.2",
83+
"espree": "9.6.1",
8284
"esprima": "4.0.1",
8385
"formdata-polyfill": "4.0.10",
8486
"hoverintent": "2.2.1",
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
* CodeMirror JavaScript linter.
3+
*
4+
* @since 7.0.0
5+
*/
6+
7+
import CodeMirror from 'codemirror';
8+
9+
/**
10+
* CodeMirror Lint Error.
11+
*
12+
* @see https://codemirror.net/5/doc/manual.html#addon_lint
13+
*
14+
* @typedef {Object} CodeMirrorLintError
15+
* @property {string} message - Error message.
16+
* @property {'error'} severity - Severity.
17+
* @property {CodeMirror.Position} from - From position.
18+
* @property {CodeMirror.Position} to - To position.
19+
*/
20+
21+
/**
22+
* JSHint options supported by Espree.
23+
*
24+
* @see https://jshint.com/docs/options/
25+
* @see https://www.npmjs.com/package/espree#options
26+
*
27+
* @typedef {Object} SupportedJSHintOptions
28+
* @property {number} [esversion] - "This option is used to specify the ECMAScript version to which the code must adhere."
29+
* @property {boolean} [es5] - "This option enables syntax first defined in the ECMAScript 5.1 specification. This includes allowing reserved keywords as object properties."
30+
* @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."
31+
* @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."
32+
* @property {'implied'} [strict] - "This option requires the code to run in ECMAScript 5's strict mode."
33+
*/
34+
35+
/**
36+
* Validates JavaScript.
37+
*
38+
* @since 7.0.0
39+
*
40+
* @param {string} text - Source.
41+
* @param {SupportedJSHintOptions} options - Linting options.
42+
* @returns {Promise<CodeMirrorLintError[]>}
43+
*/
44+
async function validator( text, options ) {
45+
const errors = /** @type {CodeMirrorLintError[]} */ [];
46+
try {
47+
const espree = await import( /* webpackIgnore: true */ 'espree' );
48+
espree.parse( text, {
49+
...getEspreeOptions( options ),
50+
loc: true,
51+
} );
52+
} catch ( error ) {
53+
if (
54+
// This is an `EnhancedSyntaxError` in Espree: <https://github.com/brettz9/espree/blob/3c1120280b24f4a5e4c3125305b072fa0dfca22b/packages/espree/lib/espree.js#L48-L54>.
55+
error instanceof SyntaxError &&
56+
typeof error.lineNumber === 'number' &&
57+
typeof error.column === 'number'
58+
) {
59+
const line = error.lineNumber - 1;
60+
errors.push( {
61+
message: error.message,
62+
severity: 'error',
63+
from: CodeMirror.Pos( line, error.column - 1 ),
64+
to: CodeMirror.Pos( line, error.column ),
65+
} );
66+
} else {
67+
console.warn( '[CodeMirror] Unable to lint JavaScript:', error ); // jshint ignore:line
68+
}
69+
}
70+
71+
return errors;
72+
}
73+
74+
CodeMirror.registerHelper( 'lint', 'javascript', validator );
75+
76+
/**
77+
* Gets the options for Espree from the supported JSHint options.
78+
*
79+
* @since 7.0.0
80+
*
81+
* @param {SupportedJSHintOptions} options - Linting options for JSHint.
82+
* @return {{
83+
* ecmaVersion?: number|'latest',
84+
* ecmaFeatures?: {
85+
* impliedStrict?: true
86+
* }
87+
* }}
88+
*/
89+
function getEspreeOptions( options ) {
90+
const ecmaFeatures = {};
91+
if ( options.strict === 'implied' ) {
92+
ecmaFeatures.impliedStrict = true;
93+
}
94+
95+
return {
96+
ecmaVersion: getEcmaVersion( options ),
97+
sourceType: options.module ? 'module' : 'script',
98+
ecmaFeatures,
99+
};
100+
}
101+
102+
/**
103+
* Gets the ECMAScript version.
104+
*
105+
* @since 7.0.0
106+
*
107+
* @param {SupportedJSHintOptions} options - Options.
108+
* @return {number|'latest'} ECMAScript version.
109+
*/
110+
function getEcmaVersion( options ) {
111+
if ( typeof options.esversion === 'number' ) {
112+
return options.esversion;
113+
}
114+
if ( options.es5 ) {
115+
return 5;
116+
}
117+
if ( options.es3 ) {
118+
return 3;
119+
}
120+
return 'latest';
121+
}

src/wp-admin/includes/file.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ function wp_get_plugin_file_editable_extensions( $plugin ) {
202202
'inc',
203203
'include',
204204
'js',
205+
'mjs',
205206
'json',
206207
'jsx',
207208
'less',
@@ -261,6 +262,7 @@ function wp_get_theme_file_editable_extensions( $theme ) {
261262
'inc',
262263
'include',
263264
'js',
265+
'mjs',
264266
'json',
265267
'jsx',
266268
'less',

src/wp-includes/general-template.php

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4069,7 +4069,6 @@ function wp_enqueue_code_editor( $args ) {
40694069
case 'text/x-php':
40704070
wp_enqueue_script( 'htmlhint' );
40714071
wp_enqueue_script( 'csslint' );
4072-
wp_enqueue_script( 'jshint' );
40734072
if ( ! current_user_can( 'unfiltered_html' ) ) {
40744073
wp_enqueue_script( 'htmlhint-kses' );
40754074
}
@@ -4081,7 +4080,6 @@ function wp_enqueue_code_editor( $args ) {
40814080
case 'application/ld+json':
40824081
case 'text/typescript':
40834082
case 'application/typescript':
4084-
wp_enqueue_script( 'jshint' );
40854083
wp_enqueue_script( 'jsonlint' );
40864084
break;
40874085
}
@@ -4153,30 +4151,39 @@ function wp_get_code_editor_settings( $args ) {
41534151
'outline-none' => true,
41544152
),
41554153
'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,
4154+
'esversion' => 11,
4155+
'module' => str_ends_with( $args['file'] ?? '', '.mjs' ),
4156+
4157+
// The following JSHint *linting rule* options are copied from
4158+
// <https://github.com/WordPress/wordpress-develop/blob/6.9.0/.jshintrc>.
4159+
// Parsing-related options such as `esversion` (and, in other contexts, `es5`, `es3`, `module`, `strict`)
4160+
// are honored by the Espree-based integration, but these linting-rule options are not interpreted by Espree
4161+
// and are kept only for compatibility/documentation with the original JSHint configuration.
4162+
'boss' => true,
4163+
'curly' => true,
4164+
'eqeqeq' => true,
4165+
'eqnull' => true,
4166+
'expr' => true,
4167+
'immed' => true,
4168+
'noarg' => true,
4169+
'nonbsp' => true,
4170+
'quotmark' => 'single',
4171+
'undef' => true,
4172+
'unused' => true,
4173+
'browser' => true,
4174+
'globals' => array(
4175+
'_' => false,
4176+
'Backbone' => false,
4177+
'jQuery' => false,
4178+
'JSON' => false,
4179+
'wp' => false,
4180+
'export' => false,
4181+
'module' => false,
4182+
'require' => false,
4183+
'WorkerGlobalScope' => false,
4184+
'self' => false,
4185+
'OffscreenCanvas' => false,
4186+
'Promise' => false,
41804187
),
41814188
),
41824189
'htmlhint' => array(
@@ -4233,6 +4240,7 @@ function wp_get_code_editor_settings( $args ) {
42334240
$type = 'message/http';
42344241
break;
42354242
case 'js':
4243+
case 'mjs':
42364244
$type = 'text/javascript';
42374245
break;
42384246
case 'json':

src/wp-includes/script-loader.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1196,9 +1196,10 @@ function wp_default_scripts( $scripts ) {
11961196
);
11971197

11981198
$scripts->add( 'wp-codemirror', '/wp-includes/js/codemirror/codemirror.min.js', array(), '5.65.20' );
1199+
did_action( 'init' ) && $scripts->add_data( 'wp-codemirror', 'module_dependencies', array( 'espree' ) );
11991200
$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' );
1201+
$scripts->add( 'esprima', '/wp-includes/js/codemirror/esprima.js', array(), '4.0.1' ); // Deprecated. Use 'espree' script module.
1202+
$scripts->add( 'jshint', '/wp-includes/js/codemirror/fakejshint.js', array( 'esprima' ), '2.9.5' ); // Deprecated.
12021203
$scripts->add( 'jsonlint', '/wp-includes/js/codemirror/jsonlint.js', array(), '1.6.3' );
12031204
$scripts->add( 'htmlhint', '/wp-includes/js/codemirror/htmlhint.js', array(), '1.8.0' );
12041205
$scripts->add( 'htmlhint-kses', '/wp-includes/js/codemirror/htmlhint-kses.js', array( 'htmlhint' ) );

src/wp-includes/script-modules.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,13 @@ 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+
);
197204
}
198205

199206
/**

0 commit comments

Comments
 (0)