diff --git a/package-lock.json b/package-lock.json index 87af6af9dd1c2..d53ce82a83f7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "core-js-url-browser": "3.6.4", "csslint": "1.0.5", "element-closest": "3.0.2", + "espree": "9.6.1", "esprima": "4.0.1", "formdata-polyfill": "4.0.10", "hoverintent": "2.2.1", @@ -7445,7 +7446,6 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -7468,7 +7468,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -13285,7 +13284,6 @@ "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -13302,7 +13300,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" diff --git a/package.json b/package.json index 9ff5ddd3dae97..695c7cd851454 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "core-js-url-browser": "3.6.4", "csslint": "1.0.5", "element-closest": "3.0.2", + "espree": "9.6.1", "esprima": "4.0.1", "formdata-polyfill": "4.0.10", "hoverintent": "2.2.1", diff --git a/src/js/_enqueues/vendor/codemirror/javascript-lint.js b/src/js/_enqueues/vendor/codemirror/javascript-lint.js new file mode 100644 index 0000000000000..7464769457d7f --- /dev/null +++ b/src/js/_enqueues/vendor/codemirror/javascript-lint.js @@ -0,0 +1,109 @@ +/* globals define, CodeMirror */ + +/* jshint esversion: 11 */ + +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/5/LICENSE + +// Depends on jshint.js from https://github.com/jshint/jshint + +( function ( mod ) { + if ( typeof exports === 'object' && typeof module === 'object' ) { + // CommonJS + mod( require( 'codemirror' ) ); + } else if ( typeof define === 'function' && define.amd ) { + // AMD + define( [ 'codemirror' ], mod ); + // Plain browser env + } else { + mod( CodeMirror ); + } +} )( function ( CodeMirror ) { + 'use strict'; + + async function validator( text, options ) { + const espree = await import( 'espree' ); + + const errors = []; + try { + espree.parse( text, { + ...getEspreeOptions( options ), + loc: true, + } ); + } catch ( error ) { + // Note: A lineNumber of 0 causes CodeMirror to log out a warning in the console. This is as desired for a generic Espree error. + + const line = error.lineNumber - 1; + const start = error.column - 1; + const end = error.column; + + errors.push( { + message: error.message, + severity: 'error', + from: CodeMirror.Pos( line, start ), + to: CodeMirror.Pos( line, end ), + } ); + } + + return errors; + } + + CodeMirror.registerHelper( 'lint', 'javascript', validator ); + + /** + * JSHint options supported by Espree. + * + * @see https://jshint.com/docs/options/ + * @see https://www.npmjs.com/package/espree#options + * + * @typedef {Object} SupportedJSHintOptions + * @property {number} [esversion] - "This option is used to specify the ECMAScript version to which the code must adhere." + * @property {boolean} [es5] - "This option enables syntax first defined in the ECMAScript 5.1 specification. This includes allowing reserved keywords as object properties." + * @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." + * @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." + * @property {'implied'} [strict] - "This option requires the code to run in ECMAScript 5's strict mode." + */ + + /** + * Gets the options for Espree from the supported JSHint options. + * + * @param {SupportedJSHintOptions} options - Linting options for JSHint. + * @return {{ + * ecmaVersion?: number|'latest', + * ecmaFeatures?: { + * impliedStrict?: true + * } + * }} + */ + function getEspreeOptions( options ) { + const ecmaFeatures = {}; + if ( options.strict === 'implied' ) { + ecmaFeatures.impliedStrict = true; + } + + return { + ecmaVersion: getEcmaVersion( options ), + sourceType: options.module ? 'module' : 'script', + ecmaFeatures, + }; + } + + /** + * Gets the ECMAScript version. + * + * @param {SupportedJSHintOptions} options - Options. + * @return {number|'latest'} ECMAScript version. + */ + function getEcmaVersion( options ) { + if ( typeof options.esversion === 'number' ) { + return options.esversion; + } + if ( options.es5 ) { + return 5; + } + if ( options.es3 ) { + return 3; + } + return 'latest'; + } +} ); diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php index f5dacf28f7327..23a4570bd5343 100644 --- a/src/wp-includes/general-template.php +++ b/src/wp-includes/general-template.php @@ -4045,6 +4045,7 @@ function wp_enqueue_code_editor( $args ) { wp_enqueue_script( 'code-editor' ); wp_enqueue_style( 'code-editor' ); + wp_enqueue_script_module( 'wp-codemirror' ); // Hack to get importmap printed with espree. if ( isset( $settings['codemirror']['mode'] ) ) { $mode = $settings['codemirror']['mode']; @@ -4069,7 +4070,6 @@ function wp_enqueue_code_editor( $args ) { case 'text/x-php': wp_enqueue_script( 'htmlhint' ); wp_enqueue_script( 'csslint' ); - wp_enqueue_script( 'jshint' ); if ( ! current_user_can( 'unfiltered_html' ) ) { wp_enqueue_script( 'htmlhint-kses' ); } @@ -4081,7 +4081,6 @@ function wp_enqueue_code_editor( $args ) { case 'application/ld+json': case 'text/typescript': case 'application/typescript': - wp_enqueue_script( 'jshint' ); wp_enqueue_script( 'jsonlint' ); break; } @@ -4153,30 +4152,35 @@ function wp_get_code_editor_settings( $args ) { 'outline-none' => true, ), 'jshint' => array( - // The following are copied from . - 'boss' => true, - 'curly' => true, - 'eqeqeq' => true, - 'eqnull' => true, - 'es3' => true, - 'expr' => true, - 'immed' => true, - 'noarg' => true, - 'nonbsp' => true, - 'onevar' => true, - 'quotmark' => 'single', - 'trailing' => true, - 'undef' => true, - 'unused' => true, - - 'browser' => true, - - 'globals' => array( - '_' => false, - 'Backbone' => false, - 'jQuery' => false, - 'JSON' => false, - 'wp' => false, + // This version is copied from . + 'esversion' => 10, + + // The remaining options are not supported by Espree, which is used instead of JSHint for licensing reasons. + 'boss' => true, + 'curly' => true, + 'eqeqeq' => true, + 'eqnull' => true, + 'expr' => true, + 'immed' => true, + 'noarg' => true, + 'nonbsp' => true, + 'quotmark' => 'single', + 'undef' => true, + 'unused' => true, + 'browser' => true, + 'globals' => array( + '_' => false, + 'Backbone' => false, + 'jQuery' => false, + 'JSON' => false, + 'wp' => false, + 'export' => false, + 'module' => false, + 'require' => false, + 'WorkerGlobalScope' => false, + 'self' => false, + 'OffscreenCanvas' => false, + 'Promise' => false, ), ), 'htmlhint' => array( diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 714a92eafe035..90f656d0d8510 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -1197,8 +1197,8 @@ function wp_default_scripts( $scripts ) { $scripts->add( 'wp-codemirror', '/wp-includes/js/codemirror/codemirror.min.js', array(), '5.65.20' ); $scripts->add( 'csslint', '/wp-includes/js/codemirror/csslint.js', array(), '1.0.5' ); - $scripts->add( 'esprima', '/wp-includes/js/codemirror/esprima.js', array(), '4.0.1' ); - $scripts->add( 'jshint', '/wp-includes/js/codemirror/fakejshint.js', array( 'esprima' ), '2.9.5' ); + $scripts->add( 'esprima', '/wp-includes/js/codemirror/esprima.js', array(), '4.0.1' ); // Deprecated. Use 'espree' script module. + $scripts->add( 'jshint', '/wp-includes/js/codemirror/fakejshint.js', array( 'esprima' ), '2.9.5' ); // Deprecated. $scripts->add( 'jsonlint', '/wp-includes/js/codemirror/jsonlint.js', array(), '1.6.3' ); $scripts->add( 'htmlhint', '/wp-includes/js/codemirror/htmlhint.js', array(), '1.8.0' ); $scripts->add( 'htmlhint-kses', '/wp-includes/js/codemirror/htmlhint-kses.js', array( 'htmlhint' ) ); diff --git a/src/wp-includes/script-modules.php b/src/wp-includes/script-modules.php index f851d41bf21f2..9acf9f8035144 100644 --- a/src/wp-includes/script-modules.php +++ b/src/wp-includes/script-modules.php @@ -194,6 +194,25 @@ function wp_default_script_modules() { $module_deps = $script_module_data['module_dependencies'] ?? array(); wp_register_script_module( $script_module_id, $path, $module_deps, $script_module_data['version'], $args ); } + + wp_register_script_module( + 'espree', + includes_url( 'js/codemirror/espree.min.js' ), + array(), + '9.6.1' + ); + + // The following is a workaround for classic scripts not yet being able to depend on modules. See . + wp_register_script_module( + 'wp-codemirror', + '', // An empty string is a hack to cause the dependencies to be printed in the importmap without a dependent script being printed. + array( + array( + 'id' => 'espree', + 'import' => 'dynamic', + ), + ) + ); } /** diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index 846cbb125d26c..bfcbc097aaa9e 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -3039,14 +3039,12 @@ public function test_wp_enqueue_code_editor_when_php_file_will_be_passed() { 'curly', 'eqeqeq', 'eqnull', - 'es3', + 'esversion', 'expr', 'immed', 'noarg', 'nonbsp', - 'onevar', 'quotmark', - 'trailing', 'undef', 'unused', 'browser', @@ -3123,14 +3121,12 @@ public function test_wp_enqueue_code_editor_when_generated_array_by_compact_will 'curly', 'eqeqeq', 'eqnull', - 'es3', + 'esversion', 'expr', 'immed', 'noarg', 'nonbsp', - 'onevar', 'quotmark', - 'trailing', 'undef', 'unused', 'browser', @@ -3221,14 +3217,12 @@ public function test_wp_enqueue_code_editor_when_generated_array_by_array_merge_ 'curly', 'eqeqeq', 'eqnull', - 'es3', + 'esversion', 'expr', 'immed', 'noarg', 'nonbsp', - 'onevar', 'quotmark', - 'trailing', 'undef', 'unused', 'browser', @@ -3316,14 +3310,12 @@ public function test_wp_enqueue_code_editor_when_simple_array_will_be_passed() { 'curly', 'eqeqeq', 'eqnull', - 'es3', + 'esversion', 'expr', 'immed', 'noarg', 'nonbsp', - 'onevar', 'quotmark', - 'trailing', 'undef', 'unused', 'browser', @@ -3901,7 +3893,7 @@ static function ( $dependency ) { ); // Exclude packages that are not registered in WordPress. - $exclude = array( 'react-is', 'json2php' ); + $exclude = array( 'react-is', 'json2php', 'espree' ); $package_json_dependencies = array_diff( $package_json_dependencies, $exclude ); /* diff --git a/tools/vendors/codemirror-entry.js b/tools/vendors/codemirror-entry.js index cf3b7523d0edf..3b558cfb62a14 100644 --- a/tools/vendors/codemirror-entry.js +++ b/tools/vendors/codemirror-entry.js @@ -19,7 +19,8 @@ require( 'codemirror/addon/hint/xml-hint' ); require( 'codemirror/addon/lint/lint' ); require( 'codemirror/addon/lint/css-lint' ); require( 'codemirror/addon/lint/html-lint' ); -require( 'codemirror/addon/lint/javascript-lint' ); + +require( '../../src/js/_enqueues/vendor/codemirror/javascript-lint' ); require( 'codemirror/addon/lint/json-lint' ); // Addons (Other) diff --git a/tools/vendors/espree-entry.js b/tools/vendors/espree-entry.js new file mode 100644 index 0000000000000..5fb7373ebed6f --- /dev/null +++ b/tools/vendors/espree-entry.js @@ -0,0 +1 @@ +export * from 'espree'; diff --git a/tools/webpack/codemirror.config.js b/tools/webpack/codemirror.config.js index 2106e9d207425..045c2113c06de 100644 --- a/tools/webpack/codemirror.config.js +++ b/tools/webpack/codemirror.config.js @@ -7,32 +7,36 @@ const codemirrorBanner = require( './codemirror-banner' ); module.exports = ( env = { buildTarget: 'src/' } ) => { const buildTarget = env.buildTarget || 'src/'; + const outputPath = path.resolve( __dirname, '../../', buildTarget, 'wp-includes/js/codemirror' ); - return { + const optimization = { + minimize: !true, + minimizer: [ + new TerserPlugin( { + terserOptions: { + format: { + comments: /^!/, + }, + }, + extractComments: false, + } ), + ], + }; + + const codemirrorConfig = { target: 'browserslist', mode: 'production', - entry: './tools/vendors/codemirror-entry.js', - output: { - path: path.resolve( __dirname, '../../', buildTarget, 'wp-includes/js/codemirror' ), - filename: 'codemirror.min.js', + entry: { + 'codemirror.min': './tools/vendors/codemirror-entry.js', }, - optimization: { - minimize: true, - minimizer: [ - new TerserPlugin( { - terserOptions: { - format: { - comments: /^!/, - }, - }, - extractComments: false, - } ), - ], + output: { + path: outputPath, + filename: '[name].js', }, + optimization, externals: { 'csslint': 'window.CSSLint', 'htmlhint': 'window.HTMLHint', - 'jshint': 'window.JSHINT', 'jsonlint': 'window.jsonlint', }, plugins: [ @@ -43,4 +47,25 @@ module.exports = ( env = { buildTarget: 'src/' } ) => { } ), ], }; + + const espreeConfig = { + target: 'browserslist', + mode: 'production', + entry: { + 'espree.min': './tools/vendors/espree-entry.js', + }, + output: { + path: outputPath, + filename: '[name].js', + library: { + type: 'module', + }, + }, + experiments: { + outputModule: true, + }, + optimization, + }; + + return [ codemirrorConfig, espreeConfig ]; };