From a722b50170210cae1789c8d6ed2d8aa5af5b925d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 27 Jan 2026 12:33:06 -0800 Subject: [PATCH 01/13] 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> --- package-lock.json | 5 +- package.json | 1 + .../vendor/codemirror/javascript-lint.js | 128 ++++++++++++++++++ src/wp-includes/general-template.php | 56 ++++---- src/wp-includes/script-loader.php | 4 +- src/wp-includes/script-modules.php | 19 +++ tests/phpunit/tests/dependencies/scripts.php | 18 +-- .../tests/widgets/wpWidgetCustomHtml.php | 1 - tools/vendors/codemirror-entry.js | 3 +- tools/vendors/espree-entry.js | 1 + tools/webpack/codemirror.config.js | 61 ++++++--- 11 files changed, 232 insertions(+), 65 deletions(-) create mode 100644 src/js/_enqueues/vendor/codemirror/javascript-lint.js create mode 100644 tools/vendors/espree-entry.js 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..86a50db96a0ef --- /dev/null +++ b/src/js/_enqueues/vendor/codemirror/javascript-lint.js @@ -0,0 +1,128 @@ +/* globals define, CodeMirror */ +/* jshint devel: true */ + +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/5/LICENSE + +( 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'; + + /** + * CodeMirror Lint Error. + * + * @see https://codemirror.net/5/doc/manual.html#addon_lint + * + * @typedef {Object} CodeMirrorLintError + * @property {string} message - Error message. + * @property {'error'} severity - Severity. + * @property {{line: number, ch: number}} from - From position. + * @property {{line: number, ch: number}} to - To position. + */ + + /** + * 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." + */ + + /** + * Validates JavaScript. + * + * @param {string} text - Source. + * @param {SupportedJSHintOptions} options - Linting options. + * @returns {Promise} + */ + async function validator( text, options ) { + const errors = /** @type {CodeMirrorLintError[]} */ []; + try { + const espree = await import( 'espree' ); + espree.parse( text, { + ...getEspreeOptions( options ), + loc: true, + } ); + } catch ( error ) { + if ( + // This is an `EnhancedSyntaxError` in Espree: . + error instanceof SyntaxError && + typeof error.lineNumber === 'number' && + typeof error.column === 'number' + ) { + const line = error.lineNumber - 1; + errors.push( { + message: error.message, + severity: 'error', + from: { line, ch: error.column - 1 }, + to: { line, ch: error.column }, + } ); + } else { + console.warn( '[CodeMirror] Unable to lint JavaScript:', error ); + } + } + + return errors; + } + + CodeMirror.registerHelper( 'lint', 'javascript', validator ); + + /** + * 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..e5147a6c5cbfb 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, + 'esversion' => 11, + + // The following are copied from . + // Nevertheless, they 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/tests/phpunit/tests/widgets/wpWidgetCustomHtml.php b/tests/phpunit/tests/widgets/wpWidgetCustomHtml.php index 1a61d944719b6..c9377ba54e655 100644 --- a/tests/phpunit/tests/widgets/wpWidgetCustomHtml.php +++ b/tests/phpunit/tests/widgets/wpWidgetCustomHtml.php @@ -251,7 +251,6 @@ public function test_enqueue_admin_scripts_when_logged_in_and_syntax_highlightin $this->assertTrue( wp_script_is( 'code-editor', 'enqueued' ) ); $this->assertTrue( wp_script_is( 'wp-codemirror', 'enqueued' ) ); $this->assertTrue( wp_script_is( 'csslint', 'enqueued' ) ); - $this->assertTrue( wp_script_is( 'jshint', 'enqueued' ) ); $this->assertTrue( wp_script_is( 'htmlhint', 'enqueued' ) ); } 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 aac048dccc1ef..59b8e611b5713 100644 --- a/tools/webpack/codemirror.config.js +++ b/tools/webpack/codemirror.config.js @@ -6,32 +6,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: [ @@ -42,4 +46,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 ]; }; From bd2376c538f5c7571d8cdb0106414e050f779192 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 28 Jan 2026 10:39:46 -0800 Subject: [PATCH 02/13] Simplify javascript-lint module definition and ensure proper types --- package-lock.json | 21 ++++++++++++++++ package.json | 1 + .../vendor/codemirror/javascript-lint.js | 24 +++++-------------- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index d53ce82a83f7c..cae57f5937eca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "@lodder/grunt-postcss": "^3.1.1", "@playwright/test": "1.56.1", "@pmmmwh/react-refresh-webpack-plugin": "0.6.1", + "@types/codemirror": "5.60.17", "@wordpress/e2e-test-utils-playwright": "1.33.2", "@wordpress/prettier-config": "4.33.1", "@wordpress/scripts": "30.26.2", @@ -5132,6 +5133,16 @@ "@types/node": "*" } }, + "node_modules/@types/codemirror": { + "version": "5.60.17", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.17.tgz", + "integrity": "sha512-AZq2FIsUHVMlp7VSe2hTfl5w4pcUkoFkM3zVsRKsn1ca8CXRDYvnin04+HP2REkwsxemuHqvDofdlhUWNpbwfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/tern": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -5421,6 +5432,16 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "node_modules/@types/tern": { + "version": "0.23.9", + "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", + "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, "node_modules/@types/tough-cookie": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.3.tgz", diff --git a/package.json b/package.json index 695c7cd851454..4bf5b24887cef 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@lodder/grunt-postcss": "^3.1.1", "@playwright/test": "1.56.1", "@pmmmwh/react-refresh-webpack-plugin": "0.6.1", + "@types/codemirror": "5.60.17", "@wordpress/e2e-test-utils-playwright": "1.33.2", "@wordpress/prettier-config": "4.33.1", "@wordpress/scripts": "30.26.2", diff --git a/src/js/_enqueues/vendor/codemirror/javascript-lint.js b/src/js/_enqueues/vendor/codemirror/javascript-lint.js index 86a50db96a0ef..c229dbf01926e 100644 --- a/src/js/_enqueues/vendor/codemirror/javascript-lint.js +++ b/src/js/_enqueues/vendor/codemirror/javascript-lint.js @@ -1,21 +1,9 @@ -/* globals define, CodeMirror */ /* jshint devel: true */ // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: https://codemirror.net/5/LICENSE -( 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 ) { +( ( CodeMirror ) => { 'use strict'; /** @@ -26,8 +14,8 @@ * @typedef {Object} CodeMirrorLintError * @property {string} message - Error message. * @property {'error'} severity - Severity. - * @property {{line: number, ch: number}} from - From position. - * @property {{line: number, ch: number}} to - To position. + * @property {CodeMirror.Position} from - From position. + * @property {CodeMirror.Position} to - To position. */ /** @@ -70,8 +58,8 @@ errors.push( { message: error.message, severity: 'error', - from: { line, ch: error.column - 1 }, - to: { line, ch: error.column }, + from: CodeMirror.Pos( line, error.column - 1 ), + to: CodeMirror.Pos( line, error.column ), } ); } else { console.warn( '[CodeMirror] Unable to lint JavaScript:', error ); @@ -125,4 +113,4 @@ } return 'latest'; } -} ); +} )( require( 'codemirror' ) ); From daa5e2eda13789dbf4be5e297c25ce101a842ae6 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 30 Jan 2026 19:53:57 -0800 Subject: [PATCH 03/13] Squash sirreal:scripts/allow-script-module-dependency (https://github.com/WordPress/wordpress-develop/pull/8024) at 9acd7f0a3a7a1d4c96be7d2f08dc6afed2bfb961 Co-authored-by: Jon Surrell Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/wp-includes/class-wp-script-modules.php | 66 +++++- src/wp-includes/class-wp-scripts.php | 39 ++++ src/wp-includes/functions.wp-scripts.php | 122 +++++++---- tests/phpunit/tests/dependencies/scripts.php | 119 +++++++++++ .../tests/script-modules/wpScriptModules.php | 192 ++++++++++++++++-- 5 files changed, 475 insertions(+), 63 deletions(-) diff --git a/src/wp-includes/class-wp-script-modules.php b/src/wp-includes/class-wp-script-modules.php index abbbd6383fec6..56bc3c5d34444 100644 --- a/src/wp-includes/class-wp-script-modules.php +++ b/src/wp-includes/class-wp-script-modules.php @@ -533,13 +533,77 @@ public function print_import_map() { * Returns the import map array. * * @since 6.5.0 + * @since 7.0.0 Script module dependencies ('module_dependencies') of classic scripts are now included. + * + * @global WP_Scripts $wp_scripts * * @return array> Array with an `imports` key mapping to an array of script module * identifiers and their respective URLs, including the version query. */ private function get_import_map(): array { + global $wp_scripts; + $imports = array(); - foreach ( array_keys( $this->get_dependencies( $this->queue ) ) as $id ) { + + // Identify script modules that are dependencies of classic scripts. + $classic_script_module_dependencies = array(); + if ( $wp_scripts instanceof WP_Scripts ) { + $handles = array_merge( + $wp_scripts->queue, + $wp_scripts->to_do, + $wp_scripts->done + ); + + $processed = array(); + while ( ! empty( $handles ) ) { + $handle = array_pop( $handles ); + if ( isset( $processed[ $handle ] ) || ! isset( $wp_scripts->registered[ $handle ] ) ) { + continue; + } + $processed[ $handle ] = true; + + $module_dependencies = $wp_scripts->get_data( $handle, 'module_dependencies' ); + if ( is_array( $module_dependencies ) ) { + $missing_module_dependencies = array(); + foreach ( $module_dependencies as $id ) { + if ( ! isset( $this->registered[ $id ] ) ) { + $missing_module_dependencies[] = $id; + } else { + $classic_script_module_dependencies[] = $id; + } + } + + if ( count( $missing_module_dependencies ) > 0 ) { + _doing_it_wrong( + 'WP_Scripts::add_data', + sprintf( + /* translators: 1: Script handle, 2: 'module_dependencies', 3: List of missing dependency IDs. */ + __( 'The script with the handle "%1$s" was enqueued with script module dependencies ("%2$s") that are not registered: %3$s.' ), + $handle, + 'module_dependencies', + implode( wp_get_list_item_separator(), $missing_module_dependencies ) + ), + '7.0.0' + ); + } + } + + foreach ( $wp_scripts->registered[ $handle ]->deps as $dep ) { + if ( ! isset( $processed[ $dep ] ) ) { + $handles[] = $dep; + } + } + } + } + + // Note: the script modules in $this->queue are not included in the importmap because they get printed as scripts. + $ids = array_unique( + array_merge( + $classic_script_module_dependencies, + array_keys( $this->get_dependencies( array_merge( $this->queue, $classic_script_module_dependencies ) ) ) + ) + ); + foreach ( $ids as $id ) { $src = $this->get_src( $id ); if ( '' !== $src ) { $imports[ $id ] = $src; diff --git a/src/wp-includes/class-wp-scripts.php b/src/wp-includes/class-wp-scripts.php index 0ed7087a93f4c..fcc4fe0e5a04c 100644 --- a/src/wp-includes/class-wp-scripts.php +++ b/src/wp-includes/class-wp-scripts.php @@ -920,6 +920,45 @@ public function add_data( $handle, $key, $value ) { ); return false; } + } elseif ( 'module_dependencies' === $key ) { + if ( ! is_array( $value ) ) { + _doing_it_wrong( + __METHOD__, + sprintf( + /* translators: 1: 'module_dependencies', 2: Script handle. */ + __( 'The value for "%1$s" must be an array for the "%2$s" script.' ), + 'module_dependencies', + $handle + ), + '7.0.0' + ); + return false; + } + + $sanitized_value = array(); + $has_invalid_ids = false; + foreach ( $value as $id ) { + if ( ! is_string( $id ) ) { + $has_invalid_ids = true; + } else { + $sanitized_value[] = $id; + } + } + + if ( $has_invalid_ids ) { + _doing_it_wrong( + __METHOD__, + sprintf( + /* translators: 1: Script handle, 2: 'module_dependencies' */ + __( 'The script handle "%1$s" has one or more of its script module dependencies ("%2$s") which are not strings.' ), + $handle, + 'module_dependencies' + ), + '7.0.0' + ); + } + + $value = $sanitized_value; } return parent::add_data( $handle, $key, $value ); } diff --git a/src/wp-includes/functions.wp-scripts.php b/src/wp-includes/functions.wp-scripts.php index f1a9b2afd6b7c..23ed882517ba7 100644 --- a/src/wp-includes/functions.wp-scripts.php +++ b/src/wp-includes/functions.wp-scripts.php @@ -68,6 +68,47 @@ function _wp_scripts_maybe_doing_it_wrong( $function_name, $handle = '' ) { ); } +/** + * Adds the data for the recognized args and warns for unrecognized args. + * + * @ignore + * @since 7.0.0 + * + * @param string $function_name Function name. + * @param WP_Scripts $wp_scripts WP_Scripts instance. + * @param string $handle Script handle. + * @param array $args Array of extra args for the script. + */ +function _wp_scripts_add_args_data( string $function_name, WP_Scripts $wp_scripts, string $handle, array $args ) { + $allowed_keys = array( 'strategy', 'in_footer', 'fetchpriority', 'module_dependencies' ); + $unknown_keys = array_diff( array_keys( $args ), $allowed_keys ); + if ( ! empty( $unknown_keys ) ) { + _doing_it_wrong( + $function_name, + sprintf( + /* translators: 1: $args, 2: List of unrecognized keys. */ + __( 'Unrecognized keys in the %1$s array: %2$s.' ), + '$args', + implode( wp_get_list_item_separator(), $unknown_keys ) + ), + '7.0.0' + ); + } + + if ( ! empty( $args['in_footer'] ) ) { + $wp_scripts->add_data( $handle, 'group', 1 ); + } + if ( ! empty( $args['strategy'] ) ) { + $wp_scripts->add_data( $handle, 'strategy', $args['strategy'] ); + } + if ( ! empty( $args['fetchpriority'] ) ) { + $wp_scripts->add_data( $handle, 'fetchpriority', $args['fetchpriority'] ); + } + if ( ! empty( $args['module_dependencies'] ) ) { + $wp_scripts->add_data( $handle, 'module_dependencies', $args['module_dependencies'] ); + } +} + /** * Prints scripts in document head that are in the $handles queue. * @@ -159,22 +200,24 @@ function wp_add_inline_script( $handle, $data, $position = 'after' ) { * @since 4.3.0 A return value was added. * @since 6.3.0 The $in_footer parameter of type boolean was overloaded to be an $args parameter of type array. * @since 6.9.0 The $fetchpriority parameter of type string was added to the $args parameter of type array. - * - * @param string $handle Name of the script. Should be unique. - * @param string|false $src Full URL of the script, or path of the script relative to the WordPress root directory. - * If source is set to false, script is an alias of other scripts it depends on. - * @param string[] $deps Optional. An array of registered script handles this script depends on. Default empty array. - * @param string|bool|null $ver Optional. String specifying script version number, if it has one, which is added to the URL - * as a query string for cache busting purposes. If version is set to false, a version - * number is automatically added equal to current installed WordPress version. - * If set to null, no version is added. - * @param array|bool $args { - * Optional. An array of additional script loading strategies. Default empty array. + * @since 7.0.0 The $module_dependencies parameter of type string[] was added to the $args parameter of type array. + * + * @param string $handle Name of the script. Should be unique. + * @param string|false $src Full URL of the script, or path of the script relative to the WordPress root directory. + * If source is set to false, script is an alias of other scripts it depends on. + * @param string[] $deps Optional. An array of registered script handles this script depends on. Default empty array. + * @param string|bool|null $ver Optional. String specifying script version number, if it has one, which is added to the URL + * as a query string for cache busting purposes. If version is set to false, a version + * number is automatically added equal to current installed WordPress version. + * If set to null, no version is added. + * @param array|bool $args { + * Optional. An array of extra args for the script. Default empty array. * Otherwise, it may be a boolean in which case it determines whether the script is printed in the footer. Default false. * - * @type string $strategy Optional. If provided, may be either 'defer' or 'async'. - * @type bool $in_footer Optional. Whether to print the script in the footer. Default 'false'. - * @type string $fetchpriority Optional. The fetch priority for the script. Default 'auto'. + * @type string $strategy Optional. If provided, may be either 'defer' or 'async'. + * @type bool $in_footer Optional. Whether to print the script in the footer. Default 'false'. + * @type string $fetchpriority Optional. The fetch priority for the script. Default 'auto'. + * @type string[] $module_dependencies Optional. IDs for module dependencies loaded via dynamic import. Default empty array. * } * @return bool Whether the script has been registered. True on success, false on failure. */ @@ -189,15 +232,8 @@ function wp_register_script( $handle, $src, $deps = array(), $ver = false, $args $wp_scripts = wp_scripts(); $registered = $wp_scripts->add( $handle, $src, $deps, $ver ); - if ( ! empty( $args['in_footer'] ) ) { - $wp_scripts->add_data( $handle, 'group', 1 ); - } - if ( ! empty( $args['strategy'] ) ) { - $wp_scripts->add_data( $handle, 'strategy', $args['strategy'] ); - } - if ( ! empty( $args['fetchpriority'] ) ) { - $wp_scripts->add_data( $handle, 'fetchpriority', $args['fetchpriority'] ); - } + _wp_scripts_add_args_data( __FUNCTION__, $wp_scripts, $handle, $args ); + return $registered; } @@ -345,22 +381,24 @@ function wp_deregister_script( $handle ) { * @since 2.1.0 * @since 6.3.0 The $in_footer parameter of type boolean was overloaded to be an $args parameter of type array. * @since 6.9.0 The $fetchpriority parameter of type string was added to the $args parameter of type array. - * - * @param string $handle Name of the script. Should be unique. - * @param string $src Full URL of the script, or path of the script relative to the WordPress root directory. - * Default empty. - * @param string[] $deps Optional. An array of registered script handles this script depends on. Default empty array. - * @param string|bool|null $ver Optional. String specifying script version number, if it has one, which is added to the URL - * as a query string for cache busting purposes. If version is set to false, a version - * number is automatically added equal to current installed WordPress version. - * If set to null, no version is added. - * @param array|bool $args { - * Optional. An array of additional script loading strategies. Default empty array. + * @since 7.0.0 The $module_dependencies parameter of type string[] was added to the $args parameter of type array. + * + * @param string $handle Name of the script. Should be unique. + * @param string $src Full URL of the script, or path of the script relative to the WordPress root directory. + * Default empty. + * @param string[] $deps Optional. An array of registered script handles this script depends on. Default empty array. + * @param string|bool|null $ver Optional. String specifying script version number, if it has one, which is added to the URL + * as a query string for cache busting purposes. If version is set to false, a version + * number is automatically added equal to current installed WordPress version. + * If set to null, no version is added. + * @param array|bool $args { + * Optional. An array of extra args for the script. Default empty array. * Otherwise, it may be a boolean in which case it determines whether the script is printed in the footer. Default false. * - * @type string $strategy Optional. If provided, may be either 'defer' or 'async'. - * @type bool $in_footer Optional. Whether to print the script in the footer. Default 'false'. - * @type string $fetchpriority Optional. The fetch priority for the script. Default 'auto'. + * @type string $strategy Optional. If provided, may be either 'defer' or 'async'. + * @type bool $in_footer Optional. Whether to print the script in the footer. Default 'false'. + * @type string $fetchpriority Optional. The fetch priority for the script. Default 'auto'. + * @type string[] $module_dependencies Optional. IDs for module dependencies loaded via dynamic import. Default empty array. * } */ function wp_enqueue_script( $handle, $src = '', $deps = array(), $ver = false, $args = array() ) { @@ -379,14 +417,8 @@ function wp_enqueue_script( $handle, $src = '', $deps = array(), $ver = false, $ if ( $src ) { $wp_scripts->add( $_handle[0], $src, $deps, $ver ); } - if ( ! empty( $args['in_footer'] ) ) { - $wp_scripts->add_data( $_handle[0], 'group', 1 ); - } - if ( ! empty( $args['strategy'] ) ) { - $wp_scripts->add_data( $_handle[0], 'strategy', $args['strategy'] ); - } - if ( ! empty( $args['fetchpriority'] ) ) { - $wp_scripts->add_data( $_handle[0], 'fetchpriority', $args['fetchpriority'] ); + if ( ! empty( $args ) ) { + _wp_scripts_add_args_data( __FUNCTION__, $wp_scripts, $_handle[0], $args ); } } diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index bfcbc097aaa9e..c546e19f2e8db 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -105,6 +105,73 @@ public function get_script_html() { $this->assertEqualHTML( $expected, $processor->get_script_html(), '', $message ); } + /** + * Tests that scripts trigger _doing_it_wrong for unrecognized keys in the $args array. + * + * @ticket 63486 + * + * @covers ::wp_register_script + * @covers ::wp_enqueue_script + * @covers ::_wp_scripts_add_args_data + * + * @dataProvider data_unrecognized_keys_in_args + * + * @param string $function_name Function name to call. + * @param array $args Arguments to pass to the function. + * @param string $expected_msg Expected error message substring. + */ + public function test_unrecognized_keys_in_args( string $function_name, array $args, string $expected_msg ) { + $this->setExpectedIncorrectUsage( $function_name ); + + call_user_func_array( $function_name, $args ); + + $this->assertStringContainsString( + $expected_msg, + $this->caught_doing_it_wrong[ $function_name ] + ); + } + + /** + * Data provider for test_unrecognized_keys_in_args. + * + * @return array + */ + public function data_unrecognized_keys_in_args(): array { + return array( + 'register_script' => array( + 'function_name' => 'wp_register_script', + 'args' => array( + 'unrecognized-key-register', + '/script.js', + array(), + null, + array( + 'unrecognized_key' => 'value', + 'another_bad_key' => 'value', + ), + ), + 'expected_msg' => 'Unrecognized keys in the $args array: unrecognized_key, another_bad_key', + ), + 'enqueue_script' => array( + 'function_name' => 'wp_enqueue_script', + 'args' => array( + 'unrecognized-key-enqueue', + '/script.js', + array(), + null, + array( + 'strategy' => 'defer', + 'in_footer' => true, + 'fetchpriority' => 'high', + 'module_dependencies' => array( 'foo' ), + 'invalid_key' => 'bar', + ), + ), + 'expected_msg' => 'Unrecognized keys in the $args array: invalid_key', + ), + ); + } + /** * Test versioning * @@ -1277,6 +1344,58 @@ public function test_invalid_fetchpriority_on_alias() { $this->assertArrayNotHasKey( 'fetchpriority', wp_scripts()->registered['alias']->extra ); } + /** + * Tests validation of module_dependencies in WP_Scripts::add_data(). + * + * @ticket 61500 + * + * @covers WP_Scripts::add_data + * + * @dataProvider data_add_data_module_dependencies_validation + * + * @param mixed $data Data to add. + * @param string $message Expected error message. + * @param bool $expected Expected return value. + * @param array|null $stored Expected stored value. + */ + public function test_add_data_module_dependencies_validation( $data, string $message, bool $expected, ?array $stored ) { + wp_register_script( 'test-script', '/test.js' ); + + $expected_incorrect_usage = 'WP_Scripts::add_data'; + $this->setExpectedIncorrectUsage( $expected_incorrect_usage ); + + $this->assertSame( $expected, wp_scripts()->add_data( 'test-script', 'module_dependencies', $data ) ); + $this->assertStringContainsString( $message, $this->caught_doing_it_wrong[ $expected_incorrect_usage ] ); + + if ( null === $stored ) { + $this->assertFalse( wp_scripts()->get_data( 'test-script', 'module_dependencies' ) ); + } else { + $this->assertSame( $stored, wp_scripts()->get_data( 'test-script', 'module_dependencies' ) ); + } + } + + /** + * Data provider. + * + * @return array + */ + public function data_add_data_module_dependencies_validation(): array { + return array( + 'non-array' => array( + 'data' => 'not-an-array', + 'message' => 'The value for "module_dependencies" must be an array', + 'expected' => false, + 'stored' => null, + ), + 'bad-items' => array( + 'data' => array( 'valid', 123, true, array() ), + 'message' => 'has one or more of its script module dependencies ("module_dependencies") which are not strings', + 'expected' => true, + 'stored' => array( 'valid' ), + ), + ); + } + /** * Data provider. * diff --git a/tests/phpunit/tests/script-modules/wpScriptModules.php b/tests/phpunit/tests/script-modules/wpScriptModules.php index 1bd8b5c1663d3..f34c32b7d96d9 100644 --- a/tests/phpunit/tests/script-modules/wpScriptModules.php +++ b/tests/phpunit/tests/script-modules/wpScriptModules.php @@ -11,43 +11,39 @@ */ class Tests_Script_Modules_WpScriptModules extends WP_UnitTestCase { - /** - * @var WP_Script_Modules - */ - protected $original_script_modules; + protected WP_Script_Modules $original_script_modules; - /** - * @var string - */ - protected $original_wp_version; + protected string $original_wp_version; - /** - * Instance of WP_Script_Modules. - * - * @var WP_Script_Modules - */ - protected $script_modules; + protected ?WP_Scripts $old_wp_scripts; + + protected WP_Script_Modules $script_modules; /** * Set up. */ public function set_up() { - global $wp_script_modules, $wp_version; + global $wp_script_modules, $wp_scripts, $wp_version; parent::set_up(); $this->original_script_modules = $wp_script_modules; $this->original_wp_version = $wp_version; + $this->old_wp_scripts = $GLOBALS['wp_scripts'] ?? null; $wp_script_modules = null; $this->script_modules = wp_script_modules(); + + $wp_scripts = new WP_Scripts(); + $wp_scripts->default_version = get_bloginfo( 'version' ); } /** * Tear down. */ public function tear_down() { - global $wp_script_modules, $wp_version; - parent::tear_down(); + global $wp_script_modules, $wp_scripts, $wp_version; $wp_script_modules = $this->original_script_modules; $wp_version = $this->original_wp_version; + $wp_scripts = $this->old_wp_scripts; + parent::tear_down(); } /** @@ -1985,6 +1981,168 @@ public static function data_invalid_script_module_data(): array { ); } + /** + * Tests that script modules identified as dependencies of classic scripts are included in the import map. + * + * @ticket 61500 + * + * @covers WP_Script_Modules::get_import_map + */ + public function test_included_module_appears_in_importmap() { + $this->script_modules->register( 'dependency', '/dep.js' ); + $this->script_modules->register( 'example', '/example.js', array( 'dependency' ) ); + + // Nothing printed now. + $this->assertSame( array(), $this->get_enqueued_script_modules(), 'Initial enqueued script modules was wrong.' ); + $this->assertSame( array(), $this->get_preloaded_script_modules(), 'Initial module preloads was wrong.' ); + $this->assertSame( array(), $this->get_import_map(), 'Initial import map was wrong.' ); + + // Enqueuing a script with a module dependency should add it to the import map. + wp_enqueue_script( + 'classic', + '/classic.js', + array( 'classic-dependency' ), + false, + array( + 'module_dependencies' => array( 'example' ), + ) + ); + + $this->assertSame( array(), $this->get_enqueued_script_modules(), 'Final enqueued script modules was wrong.' ); + $this->assertSame( array(), $this->get_preloaded_script_modules(), 'Final module preloads was wrong.' ); + $this->assertEqualSets( + array( 'example', 'dependency' ), + array_keys( $this->get_import_map() ), + 'Import map keys were wrong.' + ); + } + + /** + * Tests that dynamic dependencies of enqueued script modules are included in the import map. + * + * @ticket 61500 + * + * @covers WP_Script_Modules::get_import_map + */ + public function test_import_map_includes_dynamic_dependencies_of_enqueued_modules() { + $this->script_modules->register( 'dependency-of-enqueued', '/dependency-of-enqueued.js' ); + $this->script_modules->enqueue( + 'enqueued', + '/enqueued.js', + array( + array( + 'id' => 'dependency-of-enqueued', + 'import' => 'dynamic', + ), + ) + ); + + $enqueued = $this->get_enqueued_script_modules(); + $this->assertCount( 1, $enqueued, 'Enqueue count was wrong.' ); + $this->assertArrayHasKey( 'enqueued', $enqueued, 'Missing "enqueued" script module enqueue.' ); + $this->assertCount( 0, $this->get_preloaded_script_modules(), 'Module preload count was wrong.' ); + $this->assertEqualSets( + array( 'dependency-of-enqueued' ), + array_keys( $this->get_import_map() ), + 'Import map keys were wrong.' + ); + } + + /** + * Tests that script module dependencies of enqueued classic scripts (including transitive ones) are included in the import map. + * + * @ticket 61500 + * + * @covers WP_Script_Modules::get_import_map + */ + public function test_import_map_includes_dependencies_of_classic_scripts_recursive() { + $this->script_modules->register( 'classic-transitive-dependency', '/classic-transitive-dependency.js' ); + $this->script_modules->register( 'dependency-of-not-enqueued', '/dependency-of-not-enqueued.js' ); + $this->script_modules->register( 'not-enqueued', '/not-enqueued.js', array( 'dependency-of-not-enqueued' ) ); + + // Enqueuing a script with a module dependency should add it to the import map. + wp_register_script( + 'classic-transitive-dep', + '/classic-transitive-dep.js', + array(), + false, + array( + 'module_dependencies' => array( 'classic-transitive-dependency' ), + ) + ); + wp_enqueue_script( + 'classic', + '/classic.js', + array( 'classic-transitive-dep' ), + false, + array( + 'module_dependencies' => array( 'not-enqueued' ), + ) + ); + + $enqueued = $this->get_enqueued_script_modules(); + $this->assertCount( 0, $enqueued, 'Enqueue count was wrong.' ); + $this->assertCount( 0, $this->get_preloaded_script_modules(), 'Module preload count was wrong.' ); + $this->assertEqualSets( + array( + 'classic-transitive-dependency', + 'not-enqueued', + 'dependency-of-not-enqueued', + ), + array_keys( $this->get_import_map() ), + 'Import map keys were wrong.' + ); + } + + /** + * Tests that WP_Scripts emits a _doing_it_wrong() notice for missing script module dependencies. + * + * @ticket 61500 + * @ticket 64229 + * @covers WP_Script_Modules::get_import_map + */ + public function test_wp_scripts_doing_it_wrong_for_missing_script_module_dependencies() { + $expected_incorrect_usage = 'WP_Scripts::add_data'; + $this->setExpectedIncorrectUsage( $expected_incorrect_usage ); + + wp_enqueue_script( + 'registered-dep', + '/registered-dep.js', + array(), + null, + array( + 'module_dependencies' => array( 'does-not-exist' ), + ) + ); + + $import_map = $this->get_import_map(); + $this->assertSame( array(), $import_map, 'Expected importmap to be empty.' ); + $markup = get_echo( 'wp_print_scripts' ); + + /* + * In the future, we may want to have missing script module dependencies for classic scripts to cause the + * classic script to not be printed. This would align the behavior with script modules that have missing + * script module dependencies, and classic scripts that have missing classic script dependencies. Nevertheless, + * since script module dependencies rely on dynamic imports, the dependency may not be as strong. This means + * the classic script may still work or have a fallback in case the script module fails to dynamically import. + * This same change could be made for script modules as well, where if a script module has a missing dynamic + * script module dependency, this might similarly not be sufficient reason to omit printing the dependent script module. + */ + $this->assertStringContainsString( 'registered-dep.js', $markup, 'Expected script to be present, even though it has a missing script module dependency.' ); + + $this->assertArrayHasKey( + $expected_incorrect_usage, + $this->caught_doing_it_wrong, + "Expected $expected_incorrect_usage to trigger a _doing_it_wrong() notice for missing dependency." + ); + + $this->assertStringContainsString( + 'The script with the handle "registered-dep" was enqueued with script module dependencies ("module_dependencies") that are not registered: does-not-exist', + $this->caught_doing_it_wrong[ $expected_incorrect_usage ], + 'Expected _doing_it_wrong() notice to indicate missing script module dependencies for enqueued script.' + ); + } + /** * Tests various ways of printing and dependency ordering of script modules. * From fe846aca1acd3e09aa9b1498c79d698ba38b8684 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 30 Jan 2026 20:23:48 -0800 Subject: [PATCH 04/13] CodeMirror: Use native dynamic import for Espree to allow Import Map resolution. By adding /* webpackIgnore: true */ to the dynamic import in javascript-lint.js, we prevent Webpack from bundling Espree into a separate chunk. This allows the browser to resolve Espree at runtime using the Import Map. Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/js/_enqueues/vendor/codemirror/javascript-lint.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/_enqueues/vendor/codemirror/javascript-lint.js b/src/js/_enqueues/vendor/codemirror/javascript-lint.js index c229dbf01926e..079a75b0d1ff9 100644 --- a/src/js/_enqueues/vendor/codemirror/javascript-lint.js +++ b/src/js/_enqueues/vendor/codemirror/javascript-lint.js @@ -42,7 +42,7 @@ async function validator( text, options ) { const errors = /** @type {CodeMirrorLintError[]} */ []; try { - const espree = await import( 'espree' ); + const espree = await import( /* webpackIgnore: true */ 'espree' ); espree.parse( text, { ...getEspreeOptions( options ), loc: true, From 34f5e4c6d7204f03e7447dd46ed4973e004ba35a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 30 Jan 2026 20:24:28 -0800 Subject: [PATCH 05/13] Leverage the module_dependencies arg to add espree to importmap --- src/wp-includes/general-template.php | 1 - src/wp-includes/script-loader.php | 1 + src/wp-includes/script-modules.php | 12 ------------ 3 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php index e5147a6c5cbfb..384021fea0046 100644 --- a/src/wp-includes/general-template.php +++ b/src/wp-includes/general-template.php @@ -4045,7 +4045,6 @@ 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']; diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index c51840d958c3a..4e9de5a0a7ed9 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -1196,6 +1196,7 @@ function wp_default_scripts( $scripts ) { ); $scripts->add( 'wp-codemirror', '/wp-includes/js/codemirror/codemirror.min.js', array(), '5.65.20' ); + did_action( 'init' ) && $scripts->add_data( 'wp-codemirror', 'module_dependencies', array( 'espree' ) ); $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' ); // Deprecated. Use 'espree' script module. $scripts->add( 'jshint', '/wp-includes/js/codemirror/fakejshint.js', array( 'esprima' ), '2.9.5' ); // Deprecated. diff --git a/src/wp-includes/script-modules.php b/src/wp-includes/script-modules.php index 9acf9f8035144..7f17aedfcd82c 100644 --- a/src/wp-includes/script-modules.php +++ b/src/wp-includes/script-modules.php @@ -201,18 +201,6 @@ function wp_default_script_modules() { 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', - ), - ) - ); } /** From 57cd3b0369fab1b7376d3ad6b3a2842165c3ad8f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 30 Jan 2026 20:27:17 -0800 Subject: [PATCH 06/13] Fix minification of espree after debugging --- tools/webpack/codemirror.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/webpack/codemirror.config.js b/tools/webpack/codemirror.config.js index 59b8e611b5713..ad0420c410659 100644 --- a/tools/webpack/codemirror.config.js +++ b/tools/webpack/codemirror.config.js @@ -9,7 +9,7 @@ module.exports = ( env = { buildTarget: 'src/' } ) => { const outputPath = path.resolve( __dirname, '../../', buildTarget, 'wp-includes/js/codemirror' ); const optimization = { - minimize: !true, + minimize: true, minimizer: [ new TerserPlugin( { terserOptions: { From 99a6994b4c131811a59c137c60d23aa741145cca Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 30 Jan 2026 20:40:56 -0800 Subject: [PATCH 07/13] CodeMirror: Unwrap javascript-lint.js IIFE and modernize with const. The IIFE in javascript-lint.js is removed as it is redundant when bundled by Webpack. Both javascript-lint.js and codemirror-entry.js are updated to use 'const' instead of 'var' for the CodeMirror require, aligning with modern JavaScript practices in the project. Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../vendor/codemirror/javascript-lint.js | 196 +++++++++--------- tools/vendors/codemirror-entry.js | 2 +- 2 files changed, 98 insertions(+), 100 deletions(-) diff --git a/src/js/_enqueues/vendor/codemirror/javascript-lint.js b/src/js/_enqueues/vendor/codemirror/javascript-lint.js index 079a75b0d1ff9..bd9482673402d 100644 --- a/src/js/_enqueues/vendor/codemirror/javascript-lint.js +++ b/src/js/_enqueues/vendor/codemirror/javascript-lint.js @@ -3,114 +3,112 @@ // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: https://codemirror.net/5/LICENSE -( ( CodeMirror ) => { - 'use strict'; +const CodeMirror = require( 'codemirror' ); - /** - * CodeMirror Lint Error. - * - * @see https://codemirror.net/5/doc/manual.html#addon_lint - * - * @typedef {Object} CodeMirrorLintError - * @property {string} message - Error message. - * @property {'error'} severity - Severity. - * @property {CodeMirror.Position} from - From position. - * @property {CodeMirror.Position} to - To position. - */ +/** + * CodeMirror Lint Error. + * + * @see https://codemirror.net/5/doc/manual.html#addon_lint + * + * @typedef {Object} CodeMirrorLintError + * @property {string} message - Error message. + * @property {'error'} severity - Severity. + * @property {CodeMirror.Position} from - From position. + * @property {CodeMirror.Position} to - To position. + */ - /** - * 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." - */ +/** + * 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." + */ - /** - * Validates JavaScript. - * - * @param {string} text - Source. - * @param {SupportedJSHintOptions} options - Linting options. - * @returns {Promise} - */ - async function validator( text, options ) { - const errors = /** @type {CodeMirrorLintError[]} */ []; - try { - const espree = await import( /* webpackIgnore: true */ 'espree' ); - espree.parse( text, { - ...getEspreeOptions( options ), - loc: true, +/** + * Validates JavaScript. + * + * @param {string} text - Source. + * @param {SupportedJSHintOptions} options - Linting options. + * @returns {Promise} + */ +async function validator( text, options ) { + const errors = /** @type {CodeMirrorLintError[]} */ []; + try { + const espree = await import( /* webpackIgnore: true */ 'espree' ); + espree.parse( text, { + ...getEspreeOptions( options ), + loc: true, + } ); + } catch ( error ) { + if ( + // This is an `EnhancedSyntaxError` in Espree: . + error instanceof SyntaxError && + typeof error.lineNumber === 'number' && + typeof error.column === 'number' + ) { + const line = error.lineNumber - 1; + errors.push( { + message: error.message, + severity: 'error', + from: CodeMirror.Pos( line, error.column - 1 ), + to: CodeMirror.Pos( line, error.column ), } ); - } catch ( error ) { - if ( - // This is an `EnhancedSyntaxError` in Espree: . - error instanceof SyntaxError && - typeof error.lineNumber === 'number' && - typeof error.column === 'number' - ) { - const line = error.lineNumber - 1; - errors.push( { - message: error.message, - severity: 'error', - from: CodeMirror.Pos( line, error.column - 1 ), - to: CodeMirror.Pos( line, error.column ), - } ); - } else { - console.warn( '[CodeMirror] Unable to lint JavaScript:', error ); - } + } else { + console.warn( '[CodeMirror] Unable to lint JavaScript:', error ); } - - return errors; } - CodeMirror.registerHelper( 'lint', 'javascript', validator ); + return errors; +} - /** - * 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; - } +CodeMirror.registerHelper( 'lint', 'javascript', validator ); - return { - ecmaVersion: getEcmaVersion( options ), - sourceType: options.module ? 'module' : 'script', - ecmaFeatures, - }; +/** + * 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; } - /** - * 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'; + 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; } -} )( require( 'codemirror' ) ); + return 'latest'; +} diff --git a/tools/vendors/codemirror-entry.js b/tools/vendors/codemirror-entry.js index 3b558cfb62a14..5c14017653141 100644 --- a/tools/vendors/codemirror-entry.js +++ b/tools/vendors/codemirror-entry.js @@ -1,5 +1,5 @@ // Import CodeMirror core to be exposed as window.wp.CodeMirror. -var CodeMirror = require( 'codemirror/lib/codemirror' ); +const CodeMirror = require( 'codemirror/lib/codemirror' ); // Keymaps require( 'codemirror/keymap/emacs' ); From 0f8878d332591bc149e96b4796e9c46f0cc93562 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 30 Jan 2026 20:51:24 -0800 Subject: [PATCH 08/13] CodeMirror: Add since 7.0.0 JSDoc tags to javascript-lint.js. Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../_enqueues/vendor/codemirror/javascript-lint.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/js/_enqueues/vendor/codemirror/javascript-lint.js b/src/js/_enqueues/vendor/codemirror/javascript-lint.js index bd9482673402d..4ba23c79b234f 100644 --- a/src/js/_enqueues/vendor/codemirror/javascript-lint.js +++ b/src/js/_enqueues/vendor/codemirror/javascript-lint.js @@ -1,5 +1,11 @@ /* jshint devel: true */ +/** + * CodeMirror JavaScript linter. + * + * @since 7.0.0 + */ + // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: https://codemirror.net/5/LICENSE @@ -34,6 +40,8 @@ const CodeMirror = require( 'codemirror' ); /** * Validates JavaScript. * + * @since 7.0.0 + * * @param {string} text - Source. * @param {SupportedJSHintOptions} options - Linting options. * @returns {Promise} @@ -73,6 +81,8 @@ CodeMirror.registerHelper( 'lint', 'javascript', validator ); /** * Gets the options for Espree from the supported JSHint options. * + * @since 7.0.0 + * * @param {SupportedJSHintOptions} options - Linting options for JSHint. * @return {{ * ecmaVersion?: number|'latest', @@ -97,6 +107,8 @@ function getEspreeOptions( options ) { /** * Gets the ECMAScript version. * + * @since 7.0.0 + * * @param {SupportedJSHintOptions} options - Options. * @return {number|'latest'} ECMAScript version. */ From 89c8db8311caa5bf32dec0495f6b1d284ac22882 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 30 Jan 2026 20:54:36 -0800 Subject: [PATCH 09/13] Change method for suppressing JSHint warning for console.warn() in javascript-lint.js. Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/js/_enqueues/vendor/codemirror/javascript-lint.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/js/_enqueues/vendor/codemirror/javascript-lint.js b/src/js/_enqueues/vendor/codemirror/javascript-lint.js index 4ba23c79b234f..615914e075d5d 100644 --- a/src/js/_enqueues/vendor/codemirror/javascript-lint.js +++ b/src/js/_enqueues/vendor/codemirror/javascript-lint.js @@ -1,14 +1,9 @@ -/* jshint devel: true */ - /** * CodeMirror JavaScript linter. * * @since 7.0.0 */ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/5/LICENSE - const CodeMirror = require( 'codemirror' ); /** @@ -69,7 +64,7 @@ async function validator( text, options ) { to: CodeMirror.Pos( line, error.column ), } ); } else { - console.warn( '[CodeMirror] Unable to lint JavaScript:', error ); + console.warn( '[CodeMirror] Unable to lint JavaScript:', error ); // jshint ignore:line } } From 0edaefa909c6e4e8b9ee80c09fc4524fa94d0792 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Feb 2026 17:45:40 -0800 Subject: [PATCH 10/13] Squash sirreal:scripts/allow-script-module-dependency (https://github.com/WordPress/wordpress-develop/pull/8024) at 9acd7f0...3a67800 Co-authored-by: Jon Surrell Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/wp-includes/class-wp-script-modules.php | 112 ++++++++++-------- src/wp-includes/class-wp-scripts.php | 13 +- src/wp-includes/functions.wp-scripts.php | 76 ++++++------ src/wp-includes/script-modules.php | 100 ++++++++-------- tests/phpunit/tests/dependencies/scripts.php | 12 +- .../tests/script-modules/wpScriptModules.php | 18 ++- 6 files changed, 177 insertions(+), 154 deletions(-) diff --git a/src/wp-includes/class-wp-script-modules.php b/src/wp-includes/class-wp-script-modules.php index 56bc3c5d34444..627ccbd0f424e 100644 --- a/src/wp-includes/class-wp-script-modules.php +++ b/src/wp-includes/class-wp-script-modules.php @@ -88,31 +88,31 @@ class WP_Script_Modules { * @since 6.5.0 * @since 6.9.0 Added the $args parameter. * - * @param string $id The identifier of the script module. Should be unique. It will be used in the - * final import map. - * @param string $src Optional. Full URL of the script module, or path of the script module relative - * to the WordPress root directory. If it is provided and the script module has - * not been registered yet, it will be registered. - * @param array $deps { - * Optional. List of dependencies. - * - * @type string|array ...$0 { - * An array of script module identifiers of the dependencies of this script - * module. The dependencies can be strings or arrays. If they are arrays, - * they need an `id` key with the script module identifier, and can contain - * an `import` key with either `static` or `dynamic`. By default, - * dependencies that don't contain an `import` key are considered static. - * - * @type string $id The script module identifier. - * @type string $import Optional. Import type. May be either `static` or - * `dynamic`. Defaults to `static`. - * } - * } - * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false. - * It is added to the URL as a query string for cache busting purposes. If $version - * is set to false, the version number is the currently installed WordPress version. - * If $version is set to null, no version is added. - * @param array $args { + * @param string $id The identifier of the script module. Should be unique. It will be used in the + * final import map. + * @param string $src Optional. Full URL of the script module, or path of the script module relative + * to the WordPress root directory. If it is provided and the script module has + * not been registered yet, it will be registered. + * @param array> $deps { + * Optional. List of dependencies. + * + * @type string|array ...$0 { + * An array of script module identifiers of the dependencies of this script + * module. The dependencies can be strings or arrays. If they are arrays, + * they need an `id` key with the script module identifier, and can contain + * an `import` key with either `static` or `dynamic`. By default, + * dependencies that don't contain an `import` key are considered static. + * + * @type string $id The script module identifier. + * @type string $import Optional. Import type. May be either `static` or + * `dynamic`. Defaults to `static`. + * } + * } + * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false. + * It is added to the URL as a query string for cache busting purposes. If $version + * is set to false, the version number is the currently installed WordPress version. + * If $version is set to null, no version is added. + * @param array $args { * Optional. An array of additional args. Default empty array. * * @type bool $in_footer Whether to print the script module in the footer. Only relevant to block themes. Default 'false'. Optional. @@ -260,31 +260,31 @@ public function set_in_footer( string $id, bool $in_footer ): bool { * @since 6.5.0 * @since 6.9.0 Added the $args parameter. * - * @param string $id The identifier of the script module. Should be unique. It will be used in the - * final import map. - * @param string $src Optional. Full URL of the script module, or path of the script module relative - * to the WordPress root directory. If it is provided and the script module has - * not been registered yet, it will be registered. - * @param array $deps { - * Optional. List of dependencies. - * - * @type string|array ...$0 { - * An array of script module identifiers of the dependencies of this script - * module. The dependencies can be strings or arrays. If they are arrays, - * they need an `id` key with the script module identifier, and can contain - * an `import` key with either `static` or `dynamic`. By default, - * dependencies that don't contain an `import` key are considered static. - * - * @type string $id The script module identifier. - * @type string $import Optional. Import type. May be either `static` or - * `dynamic`. Defaults to `static`. - * } - * } - * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false. - * It is added to the URL as a query string for cache busting purposes. If $version - * is set to false, the version number is the currently installed WordPress version. - * If $version is set to null, no version is added. - * @param array $args { + * @param string $id The identifier of the script module. Should be unique. It will be used in the + * final import map. + * @param string $src Optional. Full URL of the script module, or path of the script module relative + * to the WordPress root directory. If it is provided and the script module has + * not been registered yet, it will be registered. + * @param array> $deps { + * Optional. List of dependencies. + * + * @type string|array ...$0 { + * An array of script module identifiers of the dependencies of this script + * module. The dependencies can be strings or arrays. If they are arrays, + * they need an `id` key with the script module identifier, and can contain + * an `import` key with either `static` or `dynamic`. By default, + * dependencies that don't contain an `import` key are considered static. + * + * @type string $id The script module identifier. + * @type string $import Optional. Import type. May be either `static` or + * `dynamic`. Defaults to `static`. + * } + * } + * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false. + * It is added to the URL as a query string for cache busting purposes. If $version + * is set to false, the version number is the currently installed WordPress version. + * If $version is set to null, no version is added. + * @param array $args { * Optional. An array of additional args. Default empty array. * * @type bool $in_footer Whether to print the script module in the footer. Only relevant to block themes. Default 'false'. Optional. @@ -565,7 +565,17 @@ private function get_import_map(): array { $module_dependencies = $wp_scripts->get_data( $handle, 'module_dependencies' ); if ( is_array( $module_dependencies ) ) { $missing_module_dependencies = array(); - foreach ( $module_dependencies as $id ) { + foreach ( $module_dependencies as $module ) { + if ( is_string( $module ) ) { + $id = $module; + } elseif ( is_array( $module ) && isset( $module['id'] ) && is_string( $module['id'] ) ) { + $id = $module['id']; + } else { + // Invalid module dependency was supplied by direct manipulation of the extra data. + // Normally, this error scenario would be caught when WP_Scripts::add_data() is called. + continue; + } + if ( ! isset( $this->registered[ $id ] ) ) { $missing_module_dependencies[] = $id; } else { diff --git a/src/wp-includes/class-wp-scripts.php b/src/wp-includes/class-wp-scripts.php index fcc4fe0e5a04c..bb17c08bb52cb 100644 --- a/src/wp-includes/class-wp-scripts.php +++ b/src/wp-includes/class-wp-scripts.php @@ -937,11 +937,14 @@ public function add_data( $handle, $key, $value ) { $sanitized_value = array(); $has_invalid_ids = false; - foreach ( $value as $id ) { - if ( ! is_string( $id ) ) { - $has_invalid_ids = true; + foreach ( $value as $module ) { + if ( + is_string( $module ) || + ( is_array( $module ) && isset( $module['id'] ) && is_string( $module['id'] ) ) + ) { + $sanitized_value[] = $module; } else { - $sanitized_value[] = $id; + $has_invalid_ids = true; } } @@ -950,7 +953,7 @@ public function add_data( $handle, $key, $value ) { __METHOD__, sprintf( /* translators: 1: Script handle, 2: 'module_dependencies' */ - __( 'The script handle "%1$s" has one or more of its script module dependencies ("%2$s") which are not strings.' ), + __( 'The script handle "%1$s" has one or more of its script module dependencies ("%2$s") which are invalid.' ), $handle, 'module_dependencies' ), diff --git a/src/wp-includes/functions.wp-scripts.php b/src/wp-includes/functions.wp-scripts.php index 23ed882517ba7..59e4e54a1a1ad 100644 --- a/src/wp-includes/functions.wp-scripts.php +++ b/src/wp-includes/functions.wp-scripts.php @@ -74,22 +74,24 @@ function _wp_scripts_maybe_doing_it_wrong( $function_name, $handle = '' ) { * @ignore * @since 7.0.0 * - * @param string $function_name Function name. - * @param WP_Scripts $wp_scripts WP_Scripts instance. - * @param string $handle Script handle. - * @param array $args Array of extra args for the script. + * @param WP_Scripts $wp_scripts WP_Scripts instance. + * @param string $handle Script handle. + * @param array $args Array of extra args for the script. */ -function _wp_scripts_add_args_data( string $function_name, WP_Scripts $wp_scripts, string $handle, array $args ) { +function _wp_scripts_add_args_data( WP_Scripts $wp_scripts, string $handle, array $args ) { $allowed_keys = array( 'strategy', 'in_footer', 'fetchpriority', 'module_dependencies' ); $unknown_keys = array_diff( array_keys( $args ), $allowed_keys ); if ( ! empty( $unknown_keys ) ) { + $trace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 2 ); + $function_name = ( $trace[1]['class'] ?? '' ) . ( $trace[1]['type'] ?? '' ) . $trace[1]['function']; _doing_it_wrong( $function_name, sprintf( - /* translators: 1: $args, 2: List of unrecognized keys. */ - __( 'Unrecognized keys in the %1$s array: %2$s.' ), + /* translators: 1: $args, 2: List of unrecognized keys, 3: List of supported keys. */ + __( 'Unrecognized key(s) in the %1$s param: %2$s. Supported keys: %3$s' ), '$args', - implode( wp_get_list_item_separator(), $unknown_keys ) + implode( wp_get_list_item_separator(), $unknown_keys ), + implode( wp_get_list_item_separator(), $allowed_keys ) ), '7.0.0' ); @@ -202,22 +204,23 @@ function wp_add_inline_script( $handle, $data, $position = 'after' ) { * @since 6.9.0 The $fetchpriority parameter of type string was added to the $args parameter of type array. * @since 7.0.0 The $module_dependencies parameter of type string[] was added to the $args parameter of type array. * - * @param string $handle Name of the script. Should be unique. - * @param string|false $src Full URL of the script, or path of the script relative to the WordPress root directory. - * If source is set to false, script is an alias of other scripts it depends on. - * @param string[] $deps Optional. An array of registered script handles this script depends on. Default empty array. - * @param string|bool|null $ver Optional. String specifying script version number, if it has one, which is added to the URL - * as a query string for cache busting purposes. If version is set to false, a version - * number is automatically added equal to current installed WordPress version. - * If set to null, no version is added. - * @param array|bool $args { + * @param string $handle Name of the script. Should be unique. + * @param string|false $src Full URL of the script, or path of the script relative to the WordPress root directory. + * If source is set to false, script is an alias of other scripts it depends on. + * @param string[] $deps Optional. An array of registered script handles this script depends on. Default empty array. + * @param string|bool|null $ver Optional. String specifying script version number, if it has one, which is added to the URL + * as a query string for cache busting purposes. If version is set to false, a version + * number is automatically added equal to current installed WordPress version. + * If set to null, no version is added. + * @param array>>|bool $args { * Optional. An array of extra args for the script. Default empty array. * Otherwise, it may be a boolean in which case it determines whether the script is printed in the footer. Default false. * - * @type string $strategy Optional. If provided, may be either 'defer' or 'async'. - * @type bool $in_footer Optional. Whether to print the script in the footer. Default 'false'. - * @type string $fetchpriority Optional. The fetch priority for the script. Default 'auto'. - * @type string[] $module_dependencies Optional. IDs for module dependencies loaded via dynamic import. Default empty array. + * @type string $strategy Optional. If provided, may be either 'defer' or 'async'. + * @type bool $in_footer Optional. Whether to print the script in the footer. Default 'false'. + * @type string $fetchpriority Optional. The fetch priority for the script. Default 'auto'. + * @type array> $module_dependencies Optional. IDs for module dependencies loaded via dynamic import. Default empty array. + * For the full data format, see the `$deps` param of {@see wp_register_script_module()}. * } * @return bool Whether the script has been registered. True on success, false on failure. */ @@ -232,7 +235,7 @@ function wp_register_script( $handle, $src, $deps = array(), $ver = false, $args $wp_scripts = wp_scripts(); $registered = $wp_scripts->add( $handle, $src, $deps, $ver ); - _wp_scripts_add_args_data( __FUNCTION__, $wp_scripts, $handle, $args ); + _wp_scripts_add_args_data( $wp_scripts, $handle, $args ); return $registered; } @@ -383,22 +386,23 @@ function wp_deregister_script( $handle ) { * @since 6.9.0 The $fetchpriority parameter of type string was added to the $args parameter of type array. * @since 7.0.0 The $module_dependencies parameter of type string[] was added to the $args parameter of type array. * - * @param string $handle Name of the script. Should be unique. - * @param string $src Full URL of the script, or path of the script relative to the WordPress root directory. - * Default empty. - * @param string[] $deps Optional. An array of registered script handles this script depends on. Default empty array. - * @param string|bool|null $ver Optional. String specifying script version number, if it has one, which is added to the URL - * as a query string for cache busting purposes. If version is set to false, a version - * number is automatically added equal to current installed WordPress version. - * If set to null, no version is added. - * @param array|bool $args { + * @param string $handle Name of the script. Should be unique. + * @param string $src Full URL of the script, or path of the script relative to the WordPress root directory. + * Default empty. + * @param string[] $deps Optional. An array of registered script handles this script depends on. Default empty array. + * @param string|bool|null $ver Optional. String specifying script version number, if it has one, which is added to the URL + * as a query string for cache busting purposes. If version is set to false, a version + * number is automatically added equal to current installed WordPress version. + * If set to null, no version is added. + * @param array>>|bool $args { * Optional. An array of extra args for the script. Default empty array. * Otherwise, it may be a boolean in which case it determines whether the script is printed in the footer. Default false. * - * @type string $strategy Optional. If provided, may be either 'defer' or 'async'. - * @type bool $in_footer Optional. Whether to print the script in the footer. Default 'false'. - * @type string $fetchpriority Optional. The fetch priority for the script. Default 'auto'. - * @type string[] $module_dependencies Optional. IDs for module dependencies loaded via dynamic import. Default empty array. + * @type string $strategy Optional. If provided, may be either 'defer' or 'async'. + * @type bool $in_footer Optional. Whether to print the script in the footer. Default 'false'. + * @type string $fetchpriority Optional. The fetch priority for the script. Default 'auto'. + * @type array> $module_dependencies Optional. IDs for module dependencies loaded via dynamic import. Default empty array. + * For the full data format, see the `$deps` param of {@see wp_register_script_module()}. * } */ function wp_enqueue_script( $handle, $src = '', $deps = array(), $ver = false, $args = array() ) { @@ -418,7 +422,7 @@ function wp_enqueue_script( $handle, $src = '', $deps = array(), $ver = false, $ $wp_scripts->add( $_handle[0], $src, $deps, $ver ); } if ( ! empty( $args ) ) { - _wp_scripts_add_args_data( __FUNCTION__, $wp_scripts, $_handle[0], $args ); + _wp_scripts_add_args_data( $wp_scripts, $_handle[0], $args ); } } diff --git a/src/wp-includes/script-modules.php b/src/wp-includes/script-modules.php index 7f17aedfcd82c..0a39efea1dc27 100644 --- a/src/wp-includes/script-modules.php +++ b/src/wp-includes/script-modules.php @@ -37,31 +37,31 @@ function wp_script_modules(): WP_Script_Modules { * @since 6.5.0 * @since 6.9.0 Added the $args parameter. * - * @param string $id The identifier of the script module. Should be unique. It will be used in the - * final import map. - * @param string $src Optional. Full URL of the script module, or path of the script module relative - * to the WordPress root directory. If it is provided and the script module has - * not been registered yet, it will be registered. - * @param array $deps { - * Optional. List of dependencies. - * - * @type string|array ...$0 { - * An array of script module identifiers of the dependencies of this script - * module. The dependencies can be strings or arrays. If they are arrays, - * they need an `id` key with the script module identifier, and can contain - * an `import` key with either `static` or `dynamic`. By default, - * dependencies that don't contain an `import` key are considered static. - * - * @type string $id The script module identifier. - * @type string $import Optional. Import type. May be either `static` or - * `dynamic`. Defaults to `static`. - * } - * } - * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false. - * It is added to the URL as a query string for cache busting purposes. If $version - * is set to false, the version number is the currently installed WordPress version. - * If $version is set to null, no version is added. - * @param array $args { + * @param string $id The identifier of the script module. Should be unique. It will be used in the + * final import map. + * @param string $src Optional. Full URL of the script module, or path of the script module relative + * to the WordPress root directory. If it is provided and the script module has + * not been registered yet, it will be registered. + * @param array> $deps { + * Optional. List of dependencies. + * + * @type string|array ...$0 { + * An array of script module identifiers of the dependencies of this script + * module. The dependencies can be strings or arrays. If they are arrays, + * they need an `id` key with the script module identifier, and can contain + * an `import` key with either `static` or `dynamic`. By default, + * dependencies that don't contain an `import` key are considered static. + * + * @type string $id The script module identifier. + * @type string $import Optional. Import type. May be either `static` or + * `dynamic`. Defaults to `static`. + * } + * } + * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false. + * It is added to the URL as a query string for cache busting purposes. If $version + * is set to false, the version number is the currently installed WordPress version. + * If $version is set to null, no version is added. + * @param array $args { * Optional. An array of additional args. Default empty array. * * @type bool $in_footer Whether to print the script module in the footer. Only relevant to block themes. Default 'false'. Optional. @@ -81,31 +81,31 @@ function wp_register_script_module( string $id, string $src, array $deps = array * @since 6.5.0 * @since 6.9.0 Added the $args parameter. * - * @param string $id The identifier of the script module. Should be unique. It will be used in the - * final import map. - * @param string $src Optional. Full URL of the script module, or path of the script module relative - * to the WordPress root directory. If it is provided and the script module has - * not been registered yet, it will be registered. - * @param array $deps { - * Optional. List of dependencies. - * - * @type string|array ...$0 { - * An array of script module identifiers of the dependencies of this script - * module. The dependencies can be strings or arrays. If they are arrays, - * they need an `id` key with the script module identifier, and can contain - * an `import` key with either `static` or `dynamic`. By default, - * dependencies that don't contain an `import` key are considered static. - * - * @type string $id The script module identifier. - * @type string $import Optional. Import type. May be either `static` or - * `dynamic`. Defaults to `static`. - * } - * } - * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false. - * It is added to the URL as a query string for cache busting purposes. If $version - * is set to false, the version number is the currently installed WordPress version. - * If $version is set to null, no version is added. - * @param array $args { + * @param string $id The identifier of the script module. Should be unique. It will be used in the + * final import map. + * @param string $src Optional. Full URL of the script module, or path of the script module relative + * to the WordPress root directory. If it is provided and the script module has + * not been registered yet, it will be registered. + * @param array> $deps { + * Optional. List of dependencies. + * + * @type string|array ...$0 { + * An array of script module identifiers of the dependencies of this script + * module. The dependencies can be strings or arrays. If they are arrays, + * they need an `id` key with the script module identifier, and can contain + * an `import` key with either `static` or `dynamic`. By default, + * dependencies that don't contain an `import` key are considered static. + * + * @type string $id The script module identifier. + * @type string $import Optional. Import type. May be either `static` or + * `dynamic`. Defaults to `static`. + * } + * } + * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false. + * It is added to the URL as a query string for cache busting purposes. If $version + * is set to false, the version number is the currently installed WordPress version. + * If $version is set to null, no version is added. + * @param array $args { * Optional. An array of additional args. Default empty array. * * @type bool $in_footer Whether to print the script module in the footer. Only relevant to block themes. Default 'false'. Optional. diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index c546e19f2e8db..b1d2f3b2db471 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -150,7 +150,7 @@ public function data_unrecognized_keys_in_args(): array { 'another_bad_key' => 'value', ), ), - 'expected_msg' => 'Unrecognized keys in the $args array: unrecognized_key, another_bad_key', + 'expected_msg' => 'Unrecognized key(s) in the $args param: unrecognized_key, another_bad_key. Supported keys: strategy, in_footer, fetchpriority, module_dependencies', ), 'enqueue_script' => array( 'function_name' => 'wp_enqueue_script', @@ -167,7 +167,7 @@ public function data_unrecognized_keys_in_args(): array { 'invalid_key' => 'bar', ), ), - 'expected_msg' => 'Unrecognized keys in the $args array: invalid_key', + 'expected_msg' => 'Unrecognized key(s) in the $args param: invalid_key. Supported keys: strategy, in_footer, fetchpriority, module_dependencies', ), ); } @@ -1377,7 +1377,7 @@ public function test_add_data_module_dependencies_validation( $data, string $mes /** * Data provider. * - * @return array + * @return array>|null}> */ public function data_add_data_module_dependencies_validation(): array { return array( @@ -1388,10 +1388,10 @@ public function data_add_data_module_dependencies_validation(): array { 'stored' => null, ), 'bad-items' => array( - 'data' => array( 'valid', 123, true, array() ), - 'message' => 'has one or more of its script module dependencies ("module_dependencies") which are not strings', + 'data' => array( 'valid', 123, true, array(), array( 'id' => 'valid2' ) ), + 'message' => 'has one or more of its script module dependencies ("module_dependencies") which are invalid', 'expected' => true, - 'stored' => array( 'valid' ), + 'stored' => array( 'valid', array( 'id' => 'valid2' ) ), ), ); } diff --git a/tests/phpunit/tests/script-modules/wpScriptModules.php b/tests/phpunit/tests/script-modules/wpScriptModules.php index f34c32b7d96d9..3b7de73c71b63 100644 --- a/tests/phpunit/tests/script-modules/wpScriptModules.php +++ b/tests/phpunit/tests/script-modules/wpScriptModules.php @@ -15,7 +15,7 @@ class Tests_Script_Modules_WpScriptModules extends WP_UnitTestCase { protected string $original_wp_version; - protected ?WP_Scripts $old_wp_scripts; + protected ?WP_Scripts $original_wp_scripts; protected WP_Script_Modules $script_modules; @@ -27,7 +27,7 @@ public function set_up() { parent::set_up(); $this->original_script_modules = $wp_script_modules; $this->original_wp_version = $wp_version; - $this->old_wp_scripts = $GLOBALS['wp_scripts'] ?? null; + $this->original_wp_scripts = $wp_scripts ?? null; $wp_script_modules = null; $this->script_modules = wp_script_modules(); @@ -39,11 +39,11 @@ public function set_up() { * Tear down. */ public function tear_down() { + parent::tear_down(); global $wp_script_modules, $wp_scripts, $wp_version; $wp_script_modules = $this->original_script_modules; $wp_version = $this->original_wp_version; - $wp_scripts = $this->old_wp_scripts; - parent::tear_down(); + $wp_scripts = $this->original_wp_scripts; } /** @@ -1991,6 +1991,7 @@ public static function data_invalid_script_module_data(): array { public function test_included_module_appears_in_importmap() { $this->script_modules->register( 'dependency', '/dep.js' ); $this->script_modules->register( 'example', '/example.js', array( 'dependency' ) ); + $this->script_modules->register( 'example2', '/example2.js' ); // Nothing printed now. $this->assertSame( array(), $this->get_enqueued_script_modules(), 'Initial enqueued script modules was wrong.' ); @@ -2004,14 +2005,19 @@ public function test_included_module_appears_in_importmap() { array( 'classic-dependency' ), false, array( - 'module_dependencies' => array( 'example' ), + 'module_dependencies' => array( + 'example', + array( + 'id' => 'example2', + ), + ), ) ); $this->assertSame( array(), $this->get_enqueued_script_modules(), 'Final enqueued script modules was wrong.' ); $this->assertSame( array(), $this->get_preloaded_script_modules(), 'Final module preloads was wrong.' ); $this->assertEqualSets( - array( 'example', 'dependency' ), + array( 'example', 'example2', 'dependency' ), array_keys( $this->get_import_map() ), 'Import map keys were wrong.' ); From 9e3038d822e2a3ea115f3b28e2ad52dab2c2d4f2 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Feb 2026 17:56:32 -0800 Subject: [PATCH 11/13] Convert vendor source files to ESM imports. Update src/js/_enqueues/vendor/codemirror/javascript-lint.js and tools/vendors/codemirror-entry.js to use ESM import statements instead of CommonJS require(). This aligns with modern JavaScript standards and ensures compatibility with the project's build process. Co-authored-by: Jon Surrell Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../vendor/codemirror/javascript-lint.js | 2 +- tools/vendors/codemirror-entry.js | 152 +++++++++--------- 2 files changed, 77 insertions(+), 77 deletions(-) diff --git a/src/js/_enqueues/vendor/codemirror/javascript-lint.js b/src/js/_enqueues/vendor/codemirror/javascript-lint.js index 615914e075d5d..3f98ad523346c 100644 --- a/src/js/_enqueues/vendor/codemirror/javascript-lint.js +++ b/src/js/_enqueues/vendor/codemirror/javascript-lint.js @@ -4,7 +4,7 @@ * @since 7.0.0 */ -const CodeMirror = require( 'codemirror' ); +import CodeMirror from 'codemirror'; /** * CodeMirror Lint Error. diff --git a/tools/vendors/codemirror-entry.js b/tools/vendors/codemirror-entry.js index 5c14017653141..a8856f55d11da 100644 --- a/tools/vendors/codemirror-entry.js +++ b/tools/vendors/codemirror-entry.js @@ -1,91 +1,91 @@ // Import CodeMirror core to be exposed as window.wp.CodeMirror. -const CodeMirror = require( 'codemirror/lib/codemirror' ); +import CodeMirror from 'codemirror/lib/codemirror'; // Keymaps -require( 'codemirror/keymap/emacs' ); -require( 'codemirror/keymap/sublime' ); -require( 'codemirror/keymap/vim' ); +import 'codemirror/keymap/emacs'; +import 'codemirror/keymap/sublime'; +import 'codemirror/keymap/vim'; // Addons (Hinting) -require( 'codemirror/addon/hint/show-hint' ); -require( 'codemirror/addon/hint/anyword-hint' ); -require( 'codemirror/addon/hint/css-hint' ); -require( 'codemirror/addon/hint/html-hint' ); -require( 'codemirror/addon/hint/javascript-hint' ); -require( 'codemirror/addon/hint/sql-hint' ); -require( 'codemirror/addon/hint/xml-hint' ); +import 'codemirror/addon/hint/show-hint'; +import 'codemirror/addon/hint/anyword-hint'; +import 'codemirror/addon/hint/css-hint'; +import 'codemirror/addon/hint/html-hint'; +import 'codemirror/addon/hint/javascript-hint'; +import 'codemirror/addon/hint/sql-hint'; +import 'codemirror/addon/hint/xml-hint'; // Addons (Linting) -require( 'codemirror/addon/lint/lint' ); -require( 'codemirror/addon/lint/css-lint' ); -require( 'codemirror/addon/lint/html-lint' ); +import 'codemirror/addon/lint/lint'; +import 'codemirror/addon/lint/css-lint'; +import 'codemirror/addon/lint/html-lint'; -require( '../../src/js/_enqueues/vendor/codemirror/javascript-lint' ); -require( 'codemirror/addon/lint/json-lint' ); +import '../../src/js/_enqueues/vendor/codemirror/javascript-lint'; +import 'codemirror/addon/lint/json-lint'; // Addons (Other) -require( 'codemirror/addon/comment/comment' ); -require( 'codemirror/addon/comment/continuecomment' ); -require( 'codemirror/addon/fold/xml-fold' ); -require( 'codemirror/addon/mode/overlay' ); -require( 'codemirror/addon/edit/closebrackets' ); -require( 'codemirror/addon/edit/closetag' ); -require( 'codemirror/addon/edit/continuelist' ); -require( 'codemirror/addon/edit/matchbrackets' ); -require( 'codemirror/addon/edit/matchtags' ); -require( 'codemirror/addon/edit/trailingspace' ); -require( 'codemirror/addon/dialog/dialog' ); -require( 'codemirror/addon/display/autorefresh' ); -require( 'codemirror/addon/display/fullscreen' ); -require( 'codemirror/addon/display/panel' ); -require( 'codemirror/addon/display/placeholder' ); -require( 'codemirror/addon/display/rulers' ); -require( 'codemirror/addon/fold/brace-fold' ); -require( 'codemirror/addon/fold/comment-fold' ); -require( 'codemirror/addon/fold/foldcode' ); -require( 'codemirror/addon/fold/foldgutter' ); -require( 'codemirror/addon/fold/indent-fold' ); -require( 'codemirror/addon/fold/markdown-fold' ); -require( 'codemirror/addon/merge/merge' ); -require( 'codemirror/addon/mode/loadmode' ); -require( 'codemirror/addon/mode/multiplex' ); -require( 'codemirror/addon/mode/simple' ); -require( 'codemirror/addon/runmode/runmode' ); -require( 'codemirror/addon/runmode/colorize' ); -require( 'codemirror/addon/runmode/runmode-standalone' ); -require( 'codemirror/addon/scroll/annotatescrollbar' ); -require( 'codemirror/addon/scroll/scrollpastend' ); -require( 'codemirror/addon/scroll/simplescrollbars' ); -require( 'codemirror/addon/search/search' ); -require( 'codemirror/addon/search/jump-to-line' ); -require( 'codemirror/addon/search/match-highlighter' ); -require( 'codemirror/addon/search/matchesonscrollbar' ); -require( 'codemirror/addon/search/searchcursor' ); -require( 'codemirror/addon/tern/tern' ); -require( 'codemirror/addon/tern/worker' ); -require( 'codemirror/addon/wrap/hardwrap' ); -require( 'codemirror/addon/selection/active-line' ); -require( 'codemirror/addon/selection/mark-selection' ); -require( 'codemirror/addon/selection/selection-pointer' ); +import 'codemirror/addon/comment/comment'; +import 'codemirror/addon/comment/continuecomment'; +import 'codemirror/addon/fold/xml-fold'; +import 'codemirror/addon/mode/overlay'; +import 'codemirror/addon/edit/closebrackets'; +import 'codemirror/addon/edit/closetag'; +import 'codemirror/addon/edit/continuelist'; +import 'codemirror/addon/edit/matchbrackets'; +import 'codemirror/addon/edit/matchtags'; +import 'codemirror/addon/edit/trailingspace'; +import 'codemirror/addon/dialog/dialog'; +import 'codemirror/addon/display/autorefresh'; +import 'codemirror/addon/display/fullscreen'; +import 'codemirror/addon/display/panel'; +import 'codemirror/addon/display/placeholder'; +import 'codemirror/addon/display/rulers'; +import 'codemirror/addon/fold/brace-fold'; +import 'codemirror/addon/fold/comment-fold'; +import 'codemirror/addon/fold/foldcode'; +import 'codemirror/addon/fold/foldgutter'; +import 'codemirror/addon/fold/indent-fold'; +import 'codemirror/addon/fold/markdown-fold'; +import 'codemirror/addon/merge/merge'; +import 'codemirror/addon/mode/loadmode'; +import 'codemirror/addon/mode/multiplex'; +import 'codemirror/addon/mode/simple'; +import 'codemirror/addon/runmode/runmode'; +import 'codemirror/addon/runmode/colorize'; +import 'codemirror/addon/runmode/runmode-standalone'; +import 'codemirror/addon/scroll/annotatescrollbar'; +import 'codemirror/addon/scroll/scrollpastend'; +import 'codemirror/addon/scroll/simplescrollbars'; +import 'codemirror/addon/search/search'; +import 'codemirror/addon/search/jump-to-line'; +import 'codemirror/addon/search/match-highlighter'; +import 'codemirror/addon/search/matchesonscrollbar'; +import 'codemirror/addon/search/searchcursor'; +import 'codemirror/addon/tern/tern'; +import 'codemirror/addon/tern/worker'; +import 'codemirror/addon/wrap/hardwrap'; +import 'codemirror/addon/selection/active-line'; +import 'codemirror/addon/selection/mark-selection'; +import 'codemirror/addon/selection/selection-pointer'; // Modes -require( 'codemirror/mode/meta' ); -require( 'codemirror/mode/clike/clike' ); -require( 'codemirror/mode/css/css' ); -require( 'codemirror/mode/diff/diff' ); -require( 'codemirror/mode/htmlmixed/htmlmixed' ); -require( 'codemirror/mode/http/http' ); -require( 'codemirror/mode/javascript/javascript' ); -require( 'codemirror/mode/jsx/jsx' ); -require( 'codemirror/mode/markdown/markdown' ); -require( 'codemirror/mode/gfm/gfm' ); -require( 'codemirror/mode/nginx/nginx' ); -require( 'codemirror/mode/php/php' ); -require( 'codemirror/mode/sass/sass' ); -require( 'codemirror/mode/shell/shell' ); -require( 'codemirror/mode/sql/sql' ); -require( 'codemirror/mode/xml/xml' ); -require( 'codemirror/mode/yaml/yaml' ); +import 'codemirror/mode/meta'; +import 'codemirror/mode/clike/clike'; +import 'codemirror/mode/css/css'; +import 'codemirror/mode/diff/diff'; +import 'codemirror/mode/htmlmixed/htmlmixed'; +import 'codemirror/mode/http/http'; +import 'codemirror/mode/javascript/javascript'; +import 'codemirror/mode/jsx/jsx'; +import 'codemirror/mode/markdown/markdown'; +import 'codemirror/mode/gfm/gfm'; +import 'codemirror/mode/nginx/nginx'; +import 'codemirror/mode/php/php'; +import 'codemirror/mode/sass/sass'; +import 'codemirror/mode/shell/shell'; +import 'codemirror/mode/sql/sql'; +import 'codemirror/mode/xml/xml'; +import 'codemirror/mode/yaml/yaml'; /** * Please note that the codemirror-standalone "runmode" addon is setting `window.CodeMirror` From 87cd17532c771f35a30c77f568349688b5f0dc97 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Feb 2026 22:16:28 -0800 Subject: [PATCH 12/13] Clarify comment for JSHint rules Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/wp-includes/general-template.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php index 384021fea0046..725f12b7c1b2b 100644 --- a/src/wp-includes/general-template.php +++ b/src/wp-includes/general-template.php @@ -4153,8 +4153,11 @@ function wp_get_code_editor_settings( $args ) { 'jshint' => array( 'esversion' => 11, - // The following are copied from . - // Nevertheless, they are not supported by Espree, which is used instead of JSHint for licensing reasons. + // The following JSHint *linting rule* options are copied from + // . + // Parsing-related options such as `esversion` (and, in other contexts, `es5`, `es3`, `module`, `strict`) + // are honored by the Espree-based integration, but these linting-rule options are not interpreted by Espree + // and are kept only for compatibility/documentation with the original JSHint configuration. 'boss' => true, 'curly' => true, 'eqeqeq' => true, From 2661c91db567ebed0ba3d5d5fdcc0ab89355df61 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 6 Feb 2026 21:37:20 -0800 Subject: [PATCH 13/13] Simplify Webpack configuration for Espree. By using 'espree' directly as the entry point, the proxy entry file 'tools/vendors/espree-entry.js' is no longer needed and can be removed. Co-authored-by: Jon Surrell Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tools/vendors/espree-entry.js | 1 - tools/webpack/codemirror.config.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 tools/vendors/espree-entry.js diff --git a/tools/vendors/espree-entry.js b/tools/vendors/espree-entry.js deleted file mode 100644 index 5fb7373ebed6f..0000000000000 --- a/tools/vendors/espree-entry.js +++ /dev/null @@ -1 +0,0 @@ -export * from 'espree'; diff --git a/tools/webpack/codemirror.config.js b/tools/webpack/codemirror.config.js index ad0420c410659..b6e99dd289daf 100644 --- a/tools/webpack/codemirror.config.js +++ b/tools/webpack/codemirror.config.js @@ -51,7 +51,7 @@ module.exports = ( env = { buildTarget: 'src/' } ) => { target: 'browserslist', mode: 'production', entry: { - 'espree.min': './tools/vendors/espree-entry.js', + 'espree.min': 'espree', }, output: { path: outputPath,