Skip to content

Switch from Esprima to Espree for JavaScript linting in CodeMirror.#10806

Closed
westonruter wants to merge 24 commits intoWordPress:trunkfrom
westonruter:replace-esprima-with-espree
Closed

Switch from Esprima to Espree for JavaScript linting in CodeMirror.#10806
westonruter wants to merge 24 commits intoWordPress:trunkfrom
westonruter:replace-esprima-with-espree

Conversation

@westonruter
Copy link
Member

@westonruter westonruter commented Jan 27, 2026

Previously developed in westonruter#4 as a sub-PR off of #10778

Depends on:

  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.

Trac ticket: https://core.trac.wordpress.org/ticket/64558


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

@github-actions
Copy link

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • The Plugin and Theme Directories cannot be accessed within Playground.
  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

westonruter and others added 2 commits January 28, 2026 10:11
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>
@westonruter westonruter force-pushed the replace-esprima-with-espree branch from cfd3baf to bd2376c Compare January 28, 2026 18:39
westonruter and others added 6 commits January 30, 2026 19:53
… at 9acd7f0

Co-authored-by: Jon Surrell <jonsurrell@git.wordpress.org>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
…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>
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>
westonruter and others added 2 commits January 30, 2026 20:54
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
…vascript-lint.js.

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
westonruter and others added 2 commits February 3, 2026 17:45
… at 9acd7f0...3a67800

Co-authored-by: Jon Surrell <jonsurrell@git.wordpress.org>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
@westonruter westonruter marked this pull request as ready for review February 4, 2026 01:47
@github-actions
Copy link

github-actions bot commented Feb 4, 2026

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props westonruter, jonsurrell.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

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 <jonsurrell@git.wordpress.org>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
@@ -0,0 +1 @@
export * from 'espree';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, Epsree does not have an ES module included in its distribution.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can webpack still use espree as an entry without requiring this file?

const espreeConfig = {
	// …snip
	entry: {
		'espree.min': 'espree',
	},
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great feedback! Implemented in 2661c91

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces Esprima/JSHint with Espree for JavaScript linting in CodeMirror to address GPL compatibility issues. The change introduces a new custom JavaScript linter that uses the Espree parser (v9.6.1) for syntax validation, registers Espree as a script module, and deprecates the legacy esprima and jshint script handles.

Changes:

  • Introduces a custom JavaScript linter using Espree for syntax-only validation (no linting rules)
  • Registers Espree as a script module with import map integration via module dependencies
  • Updates build tooling to generate separate espree.min.js module and integrate the new linter into codemirror.min.js

Reviewed changes

Copilot reviewed 9 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/js/_enqueues/vendor/codemirror/javascript-lint.js New custom JavaScript linter using Espree for syntax validation
tools/webpack/codemirror.config.js Adds espree build configuration and restructures codemirror build
tools/vendors/espree-entry.js Entry point for bundling espree as a module
tools/vendors/codemirror-entry.js Updates to use new javascript-lint and ES module imports
src/wp-includes/script-modules.php Registers espree as a script module
src/wp-includes/script-loader.php Adds module dependency for wp-codemirror and deprecates jshint/esprima
src/wp-includes/general-template.php Updates JSHint settings to ES11 and adds clarifying comments
tests/phpunit/tests/dependencies/scripts.php Updates tests to reflect jshint key changes and adds espree to exclusions
tests/phpunit/tests/widgets/wpWidgetCustomHtml.php Removes jshint enqueue test expectation
package.json Adds espree 9.6.1 and @types/codemirror dependencies
package-lock.json Lock file updates for new dependencies

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

westonruter and others added 2 commits February 3, 2026 22:16
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
westonruter and others added 3 commits February 6, 2026 21:33
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 <jonsurrell@git.wordpress.org>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
@sirreal
Copy link
Member

sirreal commented Feb 10, 2026

I noticed that the javascript-lint file isn't correctly watched when testing with npm run dev.

This isn't a blocker, but it's always nice when these tasks all work.

Grunt goes into watch mode, and changes to src/js/_enqueues/vendor/codemirror/javascript-lint.js triggers this:

>> File "src/js/_enqueues/vendor/codemirror/javascript-lint.js" changed.

Running "clean:dynamic" (clean) task
>> 0 paths cleaned.

Running "copy:dynamic-js" (copy) task
Copied 1 file

Running "uglify:dynamic" (uglify) task
>> 1 file created 3.59 kB → 759 B

Running "_watch" task

That copy/uglify seems redundant and maybe a codemirror watch task isn't set up?

@sirreal
Copy link
Member

sirreal commented Feb 10, 2026

I was testing this out and wanted to work with some script modules but that errors on import/export syntax. I created westonruter#5 to add module support based on .mjs extension.

Comment on lines +47 to +51
const espree = await import( /* webpackIgnore: true */ 'espree' );
espree.parse( text, {
...getEspreeOptions( options ),
loc: true,
} );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added some logging here (console.log( 'Parsing with: %o', getEspreeOptions( options ) )) to understand the options and I noticed something (note this is with westonruter#5 so adds module config).

The plugin linting seems to be triggered twice, once with what appear to be defaults and again with the expected config. This creates a race, where sometimes on load this is printed:

Parsing with: {ecmaVersion: 'latest', sourceType: 'script', ecmaFeatures: {…}}
Parsing with: {ecmaVersion: 11, sourceType: 'module', ecmaFeatures: {…}}

And the lint is performed as expected. However, sometimes this is the order and the default linting is applied:

Parsing with: {ecmaVersion: 11, sourceType: 'module', ecmaFeatures: {…}}
Parsing with: {ecmaVersion: 'latest', sourceType: 'script', ecmaFeatures: {…}}

This does seem to be happening before this PR, but it seems very consistent. I'm always seeing it run lint with the desired options second.

Copy link
Member Author

@westonruter westonruter Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm seeing that too.

I tried turning off minification and I captured the stack traces for the first and second invocation:

First
codemirror_entry_validator (codemirror.min.js?ver=5.65.20:33014)
startLinting (codemirror.min.js?ver=5.65.20:7501)
(anonymous) (codemirror.min.js?ver=5.65.20:7599)
CodeMirror (codemirror.min.js?ver=5.65.20:19874)
CodeMirror (codemirror.min.js?ver=5.65.20:19818)
fromTextArea (codemirror.min.js?ver=5.65.20:21688)
initialize (code-editor.js?ver=7.0-alpha-61215-src:298)
initCodeEditor (theme-plugin-editor.js?ver=7.0-alpha-61215-src:417)
(anonymous) (theme-plugin-editor.js?ver=7.0-alpha-61215-src:65)
(anonymous) (underscore.js?ver=1.13.7:1091)
setTimeout
(anonymous) (underscore.js?ver=1.13.7:1090)
(anonymous) (underscore.js?ver=1.13.7:76)
executeBound (underscore.js?ver=1.13.7:991)
bound (underscore.js?ver=1.13.7:1011)
init (theme-plugin-editor.js?ver=7.0-alpha-61215-src:64)
(anonymous) (wp-theme-plugin-editor-js-after:2)
mightThrow (jquery.js?ver=3.7.1:3489)
process (jquery.js?ver=3.7.1:3557)
setTimeout
(anonymous) (jquery.js?ver=3.7.1:3602)
fire (jquery.js?ver=3.7.1:3223)
fireWith (jquery.js?ver=3.7.1:3353)
fire (jquery.js?ver=3.7.1:3361)
fire (jquery.js?ver=3.7.1:3223)
fireWith (jquery.js?ver=3.7.1:3353)
ready (jquery.js?ver=3.7.1:3844)
completed (jquery.js?ver=3.7.1:3854)
Second
codemirror_entry_validator (codemirror.min.js?ver=5.65.20:33014)
startLinting (codemirror.min.js?ver=5.65.20:7501)
(anonymous) (codemirror.min.js?ver=5.65.20:7599)
(anonymous) (codemirror.min.js?ver=5.65.20:15878)
setOption (codemirror.min.js?ver=5.65.20:20213)
configureLinting (code-editor.js?ver=7.0-alpha-61215-src:152)
initialize (code-editor.js?ver=7.0-alpha-61215-src:300)
initCodeEditor (theme-plugin-editor.js?ver=7.0-alpha-61215-src:417)
(anonymous) (theme-plugin-editor.js?ver=7.0-alpha-61215-src:65)
(anonymous) (underscore.js?ver=1.13.7:1091)
setTimeout
(anonymous) (underscore.js?ver=1.13.7:1090)
(anonymous) (underscore.js?ver=1.13.7:76)
executeBound (underscore.js?ver=1.13.7:991)
bound (underscore.js?ver=1.13.7:1011)
init (theme-plugin-editor.js?ver=7.0-alpha-61215-src:64)
(anonymous) (wp-theme-plugin-editor-js-after:2)
mightThrow (jquery.js?ver=3.7.1:3489)
process (jquery.js?ver=3.7.1:3557)
setTimeout
(anonymous) (jquery.js?ver=3.7.1:3602)
fire (jquery.js?ver=3.7.1:3223)
fireWith (jquery.js?ver=3.7.1:3353)
fire (jquery.js?ver=3.7.1:3361)
fire (jquery.js?ver=3.7.1:3223)
fireWith (jquery.js?ver=3.7.1:3353)
ready (jquery.js?ver=3.7.1:3844)
completed (jquery.js?ver=3.7.1:3854)

Diff:

@@ -1,10 +1,10 @@
 codemirror_entry_validator (codemirror.min.js?ver=5.65.20:33014)
 startLinting (codemirror.min.js?ver=5.65.20:7501)
 (anonymous) (codemirror.min.js?ver=5.65.20:7599)
-CodeMirror (codemirror.min.js?ver=5.65.20:19874)
-CodeMirror (codemirror.min.js?ver=5.65.20:19818)
-fromTextArea (codemirror.min.js?ver=5.65.20:21688)
-initialize (code-editor.js?ver=7.0-alpha-61215-src:298)
+(anonymous) (codemirror.min.js?ver=5.65.20:15878)
+setOption (codemirror.min.js?ver=5.65.20:20213)
+configureLinting (code-editor.js?ver=7.0-alpha-61215-src:152)
+initialize (code-editor.js?ver=7.0-alpha-61215-src:300)
 initCodeEditor (theme-plugin-editor.js?ver=7.0-alpha-61215-src:417)
 (anonymous) (theme-plugin-editor.js?ver=7.0-alpha-61215-src:65)
 (anonymous) (underscore.js?ver=1.13.7:1091)

The second call is happening when the lint option gets updated here:

editor.setOption( 'lint', getLintOptions() );

The two calls are happening here:

codemirror = wp.CodeMirror.fromTextArea( $textarea[0], instanceSettings.codemirror );
updateErrorNotice = configureLinting( codemirror, instanceSettings );

So it makes sense that it would be called twice, once with the default config and again with the custom config. The initial implementation is clearly not ideal, as this configureLinting() function should be refactored to construct the lint option earlier to be passed in initially in the call to fromTextArea().

I'll include this work in the follow-up PR to improve the typing for code-editor.js.

Copy link
Member

@sirreal sirreal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've left several comments. I think this is an improvement over trunk and works well in my testing. The things I've flagged seem to already be issues on trunk.

westonruter and others added 2 commits February 10, 2026 11:57
@westonruter
Copy link
Member Author

I noticed that the javascript-lint file isn't correctly watched when testing with npm run dev.

@sirreal I actually have trouble with npm run dev generally. I can't get watch to work reliably with any files. For example, I run it and then I modify src/js/_enqueues/lib/comment-reply.js, and yet in my terminal it just remains indefinitely at:

Running "_watch" task
Waiting...

This seems like a larger issue to be investigated.

pento pushed a commit that referenced this pull request Feb 10, 2026
…CodeMirror.

Esprima is no longer maintained, and it does not support the latest JavaScript features in ES11, as Espree does.

- **New Linter Integration:** Introduces `src/js/_enqueues/vendor/codemirror/javascript-lint.js` using `espree` for parsing and error reporting, replacing the dependency on `jshint` and `esprima` scripts.
- **Script Modules:** Registers `espree` as a script module and leverages the `module_dependencies` argument in `wp_register_script()` to ensure `espree` is available as a dynamic import.
- **Editor Settings:** Updates `wp_get_code_editor_settings()` to use ES11 (ECMAScript 2020) defaults and synchronizes JSHint settings from `.jshintrc` for compatibility.
- **Editable Extensions:** Adds `.mjs` to the list of editable file extensions for plugins and themes.
- **Deprecations:** Marks `esprima` and `jshint` script handles as deprecated.
- **Build Tools:** Updates Webpack configuration to bundle `espree` as a module and use the new local `javascript-lint.js`.

Developed in #10806

Follow-up to [61587], [61544], [61539], [42547].

Props westonruter, jonsurrell.
See #64562, #61500, #48456, #42850.
Fixes #64558.


git-svn-id: https://develop.svn.wordpress.org/trunk@61611 602fd350-edb4-49c9-b593-d223f7449a82
@github-actions
Copy link

A commit was made that fixes the Trac ticket referenced in the description of this pull request.

SVN changeset: 61611
GitHub commit: 81885a9

This PR will be closed, but please confirm the accuracy of this and reopen if there is more work to be done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants