Skip to content

Conversation

@jsnajdr
Copy link
Member

@jsnajdr jsnajdr commented Dec 8, 2025

This PR implements the same thing as #73422, but as part of esbuild onResolve callback, without a separate post-processing step with Babel.

Local imports like import from './proxy' are resolved either to './proxy.js' or './proxy/index.js', depending on which file/directory exists. That way the imports become compatible with Node.js ESM import rules, and also with browsers' native ESM import rules. They both require full file paths, without any shortcuts.

I originally wanted to eliminate local imports completely in #73516, but that proved too ambitious and risky.

This esbuild approach is very simple and flexible. If we want to, we can easily modify it to make all imports use the .mjs or .cjs extension. That will be useful for creating fully compliant dual CJS/ESM packages.

Before we merge this, I'd like to fine-tune the algorithm a bit. Make sure that special paths like '.' and '..' are handled correctly, that imports with an existing non-JS extension (like block.json) are not handled with this code, etc. But I didn't want to delay the PR further. Let's test and review.

@jsnajdr jsnajdr self-assigned this Dec 8, 2025
@jsnajdr jsnajdr added [Type] Build Tooling Issues or PRs related to build tooling [Package] wp-build /packages/wp-build labels Dec 8, 2025
@github-actions
Copy link

github-actions bot commented Dec 8, 2025

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.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: jsnajdr <[email protected]>
Co-authored-by: aduth <[email protected]>
Co-authored-by: youknowriad <[email protected]>
Co-authored-by: anomiex <[email protected]>

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

@youknowriad
Copy link
Contributor

I'm actually a little bit surprised that we have to write it manually ourselves, I've kind expected esbuild (or some plugin) to do it for us already as it feels like a very common thing for folks building packages these days.

@jsnajdr
Copy link
Member Author

jsnajdr commented Dec 9, 2025

You are not alone who is surprised how tough it is to build fully compliant dual CJS/ESM TypeScript packages and how unprepared the common tools are. There are specialized tools like duel or tsup which are rather complex and whose inner workings are non-trivial to reproduce in a build script like ours.

Earlier this year I worked on a dual package in the Jetpack repo. Initially I tried to do my own build script, but eventually gave up and just used duel.

@aduth
Copy link
Member

aduth commented Dec 9, 2025

I think ESBuild considers relations between files primarily around bundling, which isn't how we're using it. Although to a some extent, JavaScript / Node.js is shifting toward import paths including file extensions, where this behavior of automatic extension resolution is completely unsupported in ESM (source). So if there was an expectation that the file extensions are included in the source files, we wouldn't need this code (.ts vs. .js notwithstanding).

);
if ( isDir ) {
return {
path: args.path + '/index.js',
Copy link
Member

Choose a reason for hiding this comment

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

Should we use path.join ? Not sure about the Windows compatibility of this slash.

Copy link
Member

Choose a reason for hiding this comment

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

yes, but also Node normalizes all paths so Windows compatibility isn’t an issue.

On Windows, both the forward slash (/) and backward slash () are accepted as path segment separators; however, the path methods only add backward slashes ().

https://nodejs.org/api/path.html#pathsep

! args.path.endsWith( '.js' )
) {
const baseDir = path.dirname( args.importer );
for ( const ext of [ '.js', '.jsx', '.ts', '.tsx' ] ) {
Copy link
Member

Choose a reason for hiding this comment

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

Should we reuse SOURCE_EXTENSIONS ? Maybe what we have currently as SOURCE_EXTENSIONS should be a separate constant that uses the raw array of extension values.

Copy link
Member

Choose a reason for hiding this comment

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

Actually, I wonder if we should be using some internal property of ESBuild like resolveExtensions.

To that point, is it possible to tap into ESBuild's internal resolution behavior so we don't have to reimplement this?

Copy link
Member Author

Choose a reason for hiding this comment

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

I rewrote the plugin to use esbuild's internal build.resolve function. In the onResolve callback, ask esbuild to resolve the import (preventing recursion with pluginData) and then only modify the returned path. By replacing the source extensions (.tsx and others) with the target extension, which is either .js or .mjs.

That simplifies the plugin a lot. I no longer need to stat physical files, think about caching etc.

Copy link
Member Author

Choose a reason for hiding this comment

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

Should we reuse SOURCE_EXTENSIONS ?

Probably yes, but there are complications: the globs want a bracket expansion syntax without dots, {js,jsx}, and for my code it's more convenient to have an array with dots, [ '.js', '.jsx' ]. Etc.

Let's consider this after everything else in this PR is in perfect order 🙂

) {
const baseDir = path.dirname( args.importer );
for ( const ext of [ '.js', '.jsx', '.ts', '.tsx' ] ) {
const isFile = await pathIsFile(
Copy link
Member

Choose a reason for hiding this comment

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

Is there enough likelihood of reuse that it'd be worth considering to cache this result?

}
}

const isDir = await pathIsDirectory(
Copy link
Member

Choose a reason for hiding this comment

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

I think we could be more efficient to call stat once for the path and reuse the fs.Stats result for checking if it's a file or directory. With this code, we'll call stat twice for directory imports.

@github-actions
Copy link

github-actions bot commented Dec 10, 2025

Size Change: +746 B (+0.03%)

Total Size: 2.58 MB

Filename Size Change
build/modules/abilities/index.min.js 42.3 kB +2 B (0%)
build/modules/boot/index.min.js 104 kB +28 B (+0.03%)
build/modules/core-abilities/index.min.js 892 B +2 B (+0.22%)
build/modules/edit-site-init/index.min.js 2.14 kB +4 B (+0.19%)
build/modules/lazy-editor/index.min.js 18.8 kB +5 B (+0.03%)
build/scripts/a11y/index.min.js 1.06 kB +1 B (+0.09%)
build/scripts/annotations/index.min.js 2.39 kB +3 B (+0.13%)
build/scripts/api-fetch/index.min.js 2.83 kB +2 B (+0.07%)
build/scripts/block-directory/index.min.js 8.04 kB +9 B (+0.11%)
build/scripts/block-editor/index.min.js 316 kB +289 B (+0.09%)
build/scripts/block-library/index.min.js 281 kB +177 B (+0.06%)
build/scripts/blocks/index.min.js 56.6 kB +18 B (+0.03%)
build/scripts/commands/index.min.js 19.9 kB +1 B (+0.01%)
build/scripts/components/index.min.js 273 kB +129 B (+0.05%)
build/scripts/compose/index.min.js 13.9 kB +12 B (+0.09%)
build/scripts/core-commands/index.min.js 4.13 kB +6 B (+0.15%)
build/scripts/core-data/index.min.js 86.7 kB +18 B (+0.02%)
build/scripts/customize-widgets/index.min.js 12.3 kB +16 B (+0.13%)
build/scripts/data-controls/index.min.js 795 B +2 B (+0.25%)
build/scripts/data/index.min.js 9.63 kB +6 B (+0.06%)
build/scripts/date/index.min.js 23.6 kB +3 B (+0.01%)
build/scripts/deprecated/index.min.js 756 B +4 B (+0.53%)
build/scripts/dom/index.min.js 4.91 kB +1 B (+0.02%)
build/scripts/edit-post/index.min.js 16.3 kB +25 B (+0.15%)
build/scripts/edit-site/index.min.js 234 kB -191 B (-0.08%)
build/scripts/edit-widgets/index.min.js 20 kB +34 B (+0.17%)
build/scripts/editor/index.min.js 284 kB -37 B (-0.01%)
build/scripts/element/index.min.js 5.2 kB +2 B (+0.04%)
build/scripts/format-library/index.min.js 10.8 kB +11 B (+0.1%)
build/scripts/i18n/index.min.js 2.46 kB +1 B (+0.04%)
build/scripts/is-shallow-equal/index.min.js 572 B +4 B (+0.7%)
build/scripts/keyboard-shortcuts/index.min.js 1.57 kB +3 B (+0.19%)
build/scripts/keycodes/index.min.js 1.53 kB +1 B (+0.07%)
build/scripts/list-reusable-blocks/index.min.js 2.44 kB +3 B (+0.12%)
build/scripts/media-utils/index.min.js 69.4 kB +89 B (+0.13%)
build/scripts/notices/index.min.js 1.11 kB +2 B (+0.18%)
build/scripts/nux/index.min.js 1.89 kB +4 B (+0.21%)
build/scripts/patterns/index.min.js 7.88 kB +8 B (+0.1%)
build/scripts/plugins/index.min.js 2.15 kB +2 B (+0.09%)
build/scripts/preferences-persistence/index.min.js 2.15 kB +2 B (+0.09%)
build/scripts/preferences/index.min.js 3.31 kB +7 B (+0.21%)
build/scripts/primitives/index.min.js 1.01 kB +2 B (+0.2%)
build/scripts/priority-queue/index.min.js 1.62 kB +2 B (+0.12%)
build/scripts/react-i18n/index.min.js 833 B +1 B (+0.12%)
build/scripts/redux-routine/index.min.js 3.37 kB +2 B (+0.06%)
build/scripts/reusable-blocks/index.min.js 2.93 kB +5 B (+0.17%)
build/scripts/rich-text/index.min.js 12.9 kB +7 B (+0.05%)
build/scripts/router/index.min.js 5.96 kB +1 B (+0.02%)
build/scripts/server-side-render/index.min.js 1.91 kB +3 B (+0.16%)
build/scripts/theme/index.min.js 20.8 kB +3 B (+0.01%)
build/scripts/undo-manager/index.min.js 918 B +1 B (+0.11%)
build/scripts/url/index.min.js 3.98 kB +1 B (+0.03%)
build/scripts/viewport/index.min.js 1.23 kB +3 B (+0.25%)
build/scripts/widgets/index.min.js 7.82 kB +7 B (+0.09%)
ℹ️ View Unchanged
Filename Size
build/modules/a11y/index.min.js 355 B
build/modules/block-editor/utils/fit-text-frontend.min.js 549 B
build/modules/block-library/accordion/view.min.js 779 B
build/modules/block-library/file/view.min.js 346 B
build/modules/block-library/form/view.min.js 528 B
build/modules/block-library/image/view.min.js 1.95 kB
build/modules/block-library/navigation/view.min.js 1.03 kB
build/modules/block-library/query/view.min.js 518 B
build/modules/block-library/search/view.min.js 498 B
build/modules/block-library/tabs/view.min.js 859 B
build/modules/interactivity-router/full-page.min.js 451 B
build/modules/interactivity-router/index.min.js 11.5 kB
build/modules/interactivity/index.min.js 14.9 kB
build/modules/latex-to-mathml/index.min.js 56.5 kB
build/modules/latex-to-mathml/loader.min.js 131 B
build/modules/route/index.min.js 24.6 kB
build/modules/workflow/index.min.js 36.8 kB
build/scripts/autop/index.min.js 2.18 kB
build/scripts/blob/index.min.js 631 B
build/scripts/block-serialization-default-parser/index.min.js 1.16 kB
build/scripts/block-serialization-spec-parser/index.min.js 3.08 kB
build/scripts/dom-ready/index.min.js 476 B
build/scripts/escape-html/index.min.js 586 B
build/scripts/hooks/index.min.js 1.83 kB
build/scripts/html-entities/index.min.js 494 B
build/scripts/private-apis/index.min.js 1.07 kB
build/scripts/react-refresh-entry/index.min.js 9.44 kB
build/scripts/react-refresh-runtime/index.min.js 3.59 kB
build/scripts/shortcode/index.min.js 1.58 kB
build/scripts/style-engine/index.min.js 2.33 kB
build/scripts/token-list/index.min.js 739 B
build/scripts/vendors/react-dom.min.js 43 kB
build/scripts/vendors/react-jsx-runtime.min.js 691 B
build/scripts/vendors/react.min.js 4.27 kB
build/scripts/warning/index.min.js 454 B
build/scripts/wordcount/index.min.js 1.04 kB
build/styles/block-directory/style-rtl.css 1.05 kB
build/styles/block-directory/style.css 1.05 kB
build/styles/block-editor/content-rtl.css 4.8 kB
build/styles/block-editor/content.css 4.79 kB
build/styles/block-editor/default-editor-styles-rtl.css 224 B
build/styles/block-editor/default-editor-styles.css 224 B
build/styles/block-editor/style-rtl.css 16.4 kB
build/styles/block-editor/style.css 16.3 kB
build/styles/block-library/accordion-heading/style-rtl.css 325 B
build/styles/block-library/accordion-heading/style.css 325 B
build/styles/block-library/accordion-item/style-rtl.css 180 B
build/styles/block-library/accordion-item/style.css 180 B
build/styles/block-library/accordion-panel/style-rtl.css 99 B
build/styles/block-library/accordion-panel/style.css 99 B
build/styles/block-library/accordion/style-rtl.css 62 B
build/styles/block-library/accordion/style.css 62 B
build/styles/block-library/archives/editor-rtl.css 61 B
build/styles/block-library/archives/editor.css 61 B
build/styles/block-library/archives/style-rtl.css 90 B
build/styles/block-library/archives/style.css 90 B
build/styles/block-library/audio/editor-rtl.css 149 B
build/styles/block-library/audio/editor.css 151 B
build/styles/block-library/audio/style-rtl.css 132 B
build/styles/block-library/audio/style.css 132 B
build/styles/block-library/audio/theme-rtl.css 134 B
build/styles/block-library/audio/theme.css 134 B
build/styles/block-library/avatar/editor-rtl.css 115 B
build/styles/block-library/avatar/editor.css 115 B
build/styles/block-library/avatar/style-rtl.css 104 B
build/styles/block-library/avatar/style.css 104 B
build/styles/block-library/breadcrumbs/style-rtl.css 203 B
build/styles/block-library/breadcrumbs/style.css 203 B
build/styles/block-library/button/editor-rtl.css 265 B
build/styles/block-library/button/editor.css 265 B
build/styles/block-library/button/style-rtl.css 554 B
build/styles/block-library/button/style.css 554 B
build/styles/block-library/buttons/editor-rtl.css 291 B
build/styles/block-library/buttons/editor.css 291 B
build/styles/block-library/buttons/style-rtl.css 349 B
build/styles/block-library/buttons/style.css 349 B
build/styles/block-library/calendar/style-rtl.css 239 B
build/styles/block-library/calendar/style.css 239 B
build/styles/block-library/categories/editor-rtl.css 132 B
build/styles/block-library/categories/editor.css 131 B
build/styles/block-library/categories/style-rtl.css 152 B
build/styles/block-library/categories/style.css 152 B
build/styles/block-library/classic-rtl.css 321 B
build/styles/block-library/classic.css 321 B
build/styles/block-library/code/editor-rtl.css 53 B
build/styles/block-library/code/editor.css 53 B
build/styles/block-library/code/style-rtl.css 139 B
build/styles/block-library/code/style.css 139 B
build/styles/block-library/code/theme-rtl.css 122 B
build/styles/block-library/code/theme.css 122 B
build/styles/block-library/columns/editor-rtl.css 108 B
build/styles/block-library/columns/editor.css 108 B
build/styles/block-library/columns/style-rtl.css 421 B
build/styles/block-library/columns/style.css 421 B
build/styles/block-library/comment-author-avatar/editor-rtl.css 124 B
build/styles/block-library/comment-author-avatar/editor.css 124 B
build/styles/block-library/comment-author-name/style-rtl.css 72 B
build/styles/block-library/comment-author-name/style.css 72 B
build/styles/block-library/comment-content/style-rtl.css 120 B
build/styles/block-library/comment-content/style.css 120 B
build/styles/block-library/comment-date/style-rtl.css 65 B
build/styles/block-library/comment-date/style.css 65 B
build/styles/block-library/comment-edit-link/style-rtl.css 70 B
build/styles/block-library/comment-edit-link/style.css 70 B
build/styles/block-library/comment-reply-link/style-rtl.css 71 B
build/styles/block-library/comment-reply-link/style.css 71 B
build/styles/block-library/comment-template/style-rtl.css 191 B
build/styles/block-library/comment-template/style.css 191 B
build/styles/block-library/comments-pagination-numbers/editor-rtl.css 122 B
build/styles/block-library/comments-pagination-numbers/editor.css 121 B
build/styles/block-library/comments-pagination/editor-rtl.css 168 B
build/styles/block-library/comments-pagination/editor.css 168 B
build/styles/block-library/comments-pagination/style-rtl.css 201 B
build/styles/block-library/comments-pagination/style.css 201 B
build/styles/block-library/comments-title/editor-rtl.css 75 B
build/styles/block-library/comments-title/editor.css 75 B
build/styles/block-library/comments/editor-rtl.css 842 B
build/styles/block-library/comments/editor.css 842 B
build/styles/block-library/comments/style-rtl.css 637 B
build/styles/block-library/comments/style.css 637 B
build/styles/block-library/common-rtl.css 1.11 kB
build/styles/block-library/common.css 1.11 kB
build/styles/block-library/cover/editor-rtl.css 631 B
build/styles/block-library/cover/editor.css 631 B
build/styles/block-library/cover/style-rtl.css 1.82 kB
build/styles/block-library/cover/style.css 1.81 kB
build/styles/block-library/details/editor-rtl.css 65 B
build/styles/block-library/details/editor.css 65 B
build/styles/block-library/details/style-rtl.css 86 B
build/styles/block-library/details/style.css 86 B
build/styles/block-library/editor-elements-rtl.css 75 B
build/styles/block-library/editor-elements.css 75 B
build/styles/block-library/editor-rtl.css 11.8 kB
build/styles/block-library/editor.css 11.8 kB
build/styles/block-library/elements-rtl.css 54 B
build/styles/block-library/elements.css 54 B
build/styles/block-library/embed/editor-rtl.css 331 B
build/styles/block-library/embed/editor.css 331 B
build/styles/block-library/embed/style-rtl.css 448 B
build/styles/block-library/embed/style.css 448 B
build/styles/block-library/embed/theme-rtl.css 133 B
build/styles/block-library/embed/theme.css 133 B
build/styles/block-library/file/editor-rtl.css 324 B
build/styles/block-library/file/editor.css 324 B
build/styles/block-library/file/style-rtl.css 278 B
build/styles/block-library/file/style.css 278 B
build/styles/block-library/footnotes/style-rtl.css 198 B
build/styles/block-library/footnotes/style.css 197 B
build/styles/block-library/form-input/editor-rtl.css 229 B
build/styles/block-library/form-input/editor.css 229 B
build/styles/block-library/form-input/style-rtl.css 366 B
build/styles/block-library/form-input/style.css 366 B
build/styles/block-library/form-submission-notification/editor-rtl.css 344 B
build/styles/block-library/form-submission-notification/editor.css 341 B
build/styles/block-library/form-submit-button/style-rtl.css 69 B
build/styles/block-library/form-submit-button/style.css 69 B
build/styles/block-library/freeform/editor-rtl.css 2.59 kB
build/styles/block-library/freeform/editor.css 2.59 kB
build/styles/block-library/gallery/editor-rtl.css 615 B
build/styles/block-library/gallery/editor.css 616 B
build/styles/block-library/gallery/style-rtl.css 1.84 kB
build/styles/block-library/gallery/style.css 1.84 kB
build/styles/block-library/gallery/theme-rtl.css 108 B
build/styles/block-library/gallery/theme.css 108 B
build/styles/block-library/group/editor-rtl.css 335 B
build/styles/block-library/group/editor.css 335 B
build/styles/block-library/group/style-rtl.css 103 B
build/styles/block-library/group/style.css 103 B
build/styles/block-library/group/theme-rtl.css 79 B
build/styles/block-library/group/theme.css 79 B
build/styles/block-library/heading/style-rtl.css 205 B
build/styles/block-library/heading/style.css 205 B
build/styles/block-library/html/editor-rtl.css 419 B
build/styles/block-library/html/editor.css 419 B
build/styles/block-library/image/editor-rtl.css 763 B
build/styles/block-library/image/editor.css 763 B
build/styles/block-library/image/style-rtl.css 1.6 kB
build/styles/block-library/image/style.css 1.59 kB
build/styles/block-library/image/theme-rtl.css 137 B
build/styles/block-library/image/theme.css 137 B
build/styles/block-library/latest-comments/style-rtl.css 355 B
build/styles/block-library/latest-comments/style.css 354 B
build/styles/block-library/latest-posts/editor-rtl.css 139 B
build/styles/block-library/latest-posts/editor.css 138 B
build/styles/block-library/latest-posts/style-rtl.css 520 B
build/styles/block-library/latest-posts/style.css 520 B
build/styles/block-library/list/style-rtl.css 107 B
build/styles/block-library/list/style.css 107 B
build/styles/block-library/loginout/style-rtl.css 61 B
build/styles/block-library/loginout/style.css 61 B
build/styles/block-library/math/editor-rtl.css 105 B
build/styles/block-library/math/editor.css 105 B
build/styles/block-library/math/style-rtl.css 61 B
build/styles/block-library/math/style.css 61 B
build/styles/block-library/media-text/editor-rtl.css 321 B
build/styles/block-library/media-text/editor.css 320 B
build/styles/block-library/media-text/style-rtl.css 543 B
build/styles/block-library/media-text/style.css 542 B
build/styles/block-library/more/editor-rtl.css 393 B
build/styles/block-library/more/editor.css 393 B
build/styles/block-library/navigation-link/editor-rtl.css 645 B
build/styles/block-library/navigation-link/editor.css 647 B
build/styles/block-library/navigation-link/style-rtl.css 190 B
build/styles/block-library/navigation-link/style.css 188 B
build/styles/block-library/navigation-overlay-close/style-rtl.css 249 B
build/styles/block-library/navigation-overlay-close/style.css 249 B
build/styles/block-library/navigation-submenu/editor-rtl.css 295 B
build/styles/block-library/navigation-submenu/editor.css 294 B
build/styles/block-library/navigation/editor-rtl.css 2.25 kB
build/styles/block-library/navigation/editor.css 2.26 kB
build/styles/block-library/navigation/style-rtl.css 2.27 kB
build/styles/block-library/navigation/style.css 2.25 kB
build/styles/block-library/nextpage/editor-rtl.css 392 B
build/styles/block-library/nextpage/editor.css 392 B
build/styles/block-library/page-list/editor-rtl.css 356 B
build/styles/block-library/page-list/editor.css 356 B
build/styles/block-library/page-list/style-rtl.css 192 B
build/styles/block-library/page-list/style.css 192 B
build/styles/block-library/paragraph/editor-rtl.css 251 B
build/styles/block-library/paragraph/editor.css 251 B
build/styles/block-library/paragraph/style-rtl.css 341 B
build/styles/block-library/paragraph/style.css 340 B
build/styles/block-library/post-author-biography/style-rtl.css 74 B
build/styles/block-library/post-author-biography/style.css 74 B
build/styles/block-library/post-author-name/style-rtl.css 69 B
build/styles/block-library/post-author-name/style.css 69 B
build/styles/block-library/post-author/style-rtl.css 188 B
build/styles/block-library/post-author/style.css 189 B
build/styles/block-library/post-comments-count/style-rtl.css 72 B
build/styles/block-library/post-comments-count/style.css 72 B
build/styles/block-library/post-comments-form/editor-rtl.css 96 B
build/styles/block-library/post-comments-form/editor.css 96 B
build/styles/block-library/post-comments-form/style-rtl.css 525 B
build/styles/block-library/post-comments-form/style.css 525 B
build/styles/block-library/post-comments-link/style-rtl.css 71 B
build/styles/block-library/post-comments-link/style.css 71 B
build/styles/block-library/post-content/style-rtl.css 61 B
build/styles/block-library/post-content/style.css 61 B
build/styles/block-library/post-date/style-rtl.css 62 B
build/styles/block-library/post-date/style.css 62 B
build/styles/block-library/post-excerpt/editor-rtl.css 71 B
build/styles/block-library/post-excerpt/editor.css 71 B
build/styles/block-library/post-excerpt/style-rtl.css 155 B
build/styles/block-library/post-excerpt/style.css 155 B
build/styles/block-library/post-featured-image/editor-rtl.css 719 B
build/styles/block-library/post-featured-image/editor.css 717 B
build/styles/block-library/post-featured-image/style-rtl.css 347 B
build/styles/block-library/post-featured-image/style.css 347 B
build/styles/block-library/post-navigation-link/style-rtl.css 215 B
build/styles/block-library/post-navigation-link/style.css 214 B
build/styles/block-library/post-template/style-rtl.css 414 B
build/styles/block-library/post-template/style.css 414 B
build/styles/block-library/post-terms/style-rtl.css 96 B
build/styles/block-library/post-terms/style.css 96 B
build/styles/block-library/post-time-to-read/style-rtl.css 70 B
build/styles/block-library/post-time-to-read/style.css 70 B
build/styles/block-library/post-title/style-rtl.css 162 B
build/styles/block-library/post-title/style.css 162 B
build/styles/block-library/preformatted/style-rtl.css 125 B
build/styles/block-library/preformatted/style.css 125 B
build/styles/block-library/pullquote/editor-rtl.css 133 B
build/styles/block-library/pullquote/editor.css 133 B
build/styles/block-library/pullquote/style-rtl.css 365 B
build/styles/block-library/pullquote/style.css 365 B
build/styles/block-library/pullquote/theme-rtl.css 176 B
build/styles/block-library/pullquote/theme.css 176 B
build/styles/block-library/query-pagination-numbers/editor-rtl.css 121 B
build/styles/block-library/query-pagination-numbers/editor.css 118 B
build/styles/block-library/query-pagination/editor-rtl.css 154 B
build/styles/block-library/query-pagination/editor.css 154 B
build/styles/block-library/query-pagination/style-rtl.css 237 B
build/styles/block-library/query-pagination/style.css 237 B
build/styles/block-library/query-title/style-rtl.css 64 B
build/styles/block-library/query-title/style.css 64 B
build/styles/block-library/query-total/style-rtl.css 64 B
build/styles/block-library/query-total/style.css 64 B
build/styles/block-library/query/editor-rtl.css 438 B
build/styles/block-library/query/editor.css 438 B
build/styles/block-library/quote/style-rtl.css 238 B
build/styles/block-library/quote/style.css 238 B
build/styles/block-library/quote/theme-rtl.css 233 B
build/styles/block-library/quote/theme.css 236 B
build/styles/block-library/read-more/style-rtl.css 131 B
build/styles/block-library/read-more/style.css 131 B
build/styles/block-library/reset-rtl.css 472 B
build/styles/block-library/reset.css 472 B
build/styles/block-library/rss/editor-rtl.css 126 B
build/styles/block-library/rss/editor.css 126 B
build/styles/block-library/rss/style-rtl.css 284 B
build/styles/block-library/rss/style.css 283 B
build/styles/block-library/search/editor-rtl.css 199 B
build/styles/block-library/search/editor.css 199 B
build/styles/block-library/search/style-rtl.css 665 B
build/styles/block-library/search/style.css 666 B
build/styles/block-library/search/theme-rtl.css 113 B
build/styles/block-library/search/theme.css 113 B
build/styles/block-library/separator/editor-rtl.css 100 B
build/styles/block-library/separator/editor.css 100 B
build/styles/block-library/separator/style-rtl.css 248 B
build/styles/block-library/separator/style.css 248 B
build/styles/block-library/separator/theme-rtl.css 195 B
build/styles/block-library/separator/theme.css 195 B
build/styles/block-library/shortcode/editor-rtl.css 286 B
build/styles/block-library/shortcode/editor.css 286 B
build/styles/block-library/site-logo/editor-rtl.css 773 B
build/styles/block-library/site-logo/editor.css 770 B
build/styles/block-library/site-logo/style-rtl.css 218 B
build/styles/block-library/site-logo/style.css 218 B
build/styles/block-library/site-tagline/editor-rtl.css 87 B
build/styles/block-library/site-tagline/editor.css 87 B
build/styles/block-library/site-tagline/style-rtl.css 65 B
build/styles/block-library/site-tagline/style.css 65 B
build/styles/block-library/site-title/editor-rtl.css 85 B
build/styles/block-library/site-title/editor.css 85 B
build/styles/block-library/site-title/style-rtl.css 143 B
build/styles/block-library/site-title/style.css 143 B
build/styles/block-library/social-link/editor-rtl.css 314 B
build/styles/block-library/social-link/editor.css 314 B
build/styles/block-library/social-links/editor-rtl.css 339 B
build/styles/block-library/social-links/editor.css 338 B
build/styles/block-library/social-links/style-rtl.css 1.51 kB
build/styles/block-library/social-links/style.css 1.51 kB
build/styles/block-library/spacer/editor-rtl.css 346 B
build/styles/block-library/spacer/editor.css 346 B
build/styles/block-library/spacer/style-rtl.css 48 B
build/styles/block-library/spacer/style.css 48 B
build/styles/block-library/style-rtl.css 16.6 kB
build/styles/block-library/style.css 16.6 kB
build/styles/block-library/tab/style-rtl.css 202 B
build/styles/block-library/tab/style.css 202 B
build/styles/block-library/table-of-contents/style-rtl.css 83 B
build/styles/block-library/table-of-contents/style.css 83 B
build/styles/block-library/table/editor-rtl.css 394 B
build/styles/block-library/table/editor.css 394 B
build/styles/block-library/table/style-rtl.css 641 B
build/styles/block-library/table/style.css 640 B
build/styles/block-library/table/theme-rtl.css 152 B
build/styles/block-library/table/theme.css 152 B
build/styles/block-library/tabs/editor-rtl.css 236 B
build/styles/block-library/tabs/editor.css 236 B
build/styles/block-library/tabs/style-rtl.css 983 B
build/styles/block-library/tabs/style.css 983 B
build/styles/block-library/tag-cloud/editor-rtl.css 92 B
build/styles/block-library/tag-cloud/editor.css 92 B
build/styles/block-library/tag-cloud/style-rtl.css 248 B
build/styles/block-library/tag-cloud/style.css 248 B
build/styles/block-library/template-part/editor-rtl.css 368 B
build/styles/block-library/template-part/editor.css 368 B
build/styles/block-library/template-part/theme-rtl.css 113 B
build/styles/block-library/template-part/theme.css 113 B
build/styles/block-library/term-count/style-rtl.css 63 B
build/styles/block-library/term-count/style.css 63 B
build/styles/block-library/term-description/style-rtl.css 126 B
build/styles/block-library/term-description/style.css 126 B
build/styles/block-library/term-name/style-rtl.css 62 B
build/styles/block-library/term-name/style.css 62 B
build/styles/block-library/term-template/editor-rtl.css 225 B
build/styles/block-library/term-template/editor.css 225 B
build/styles/block-library/term-template/style-rtl.css 114 B
build/styles/block-library/term-template/style.css 114 B
build/styles/block-library/text-columns/editor-rtl.css 95 B
build/styles/block-library/text-columns/editor.css 95 B
build/styles/block-library/text-columns/style-rtl.css 165 B
build/styles/block-library/text-columns/style.css 165 B
build/styles/block-library/theme-rtl.css 715 B
build/styles/block-library/theme.css 719 B
build/styles/block-library/verse/style-rtl.css 123 B
build/styles/block-library/verse/style.css 123 B
build/styles/block-library/video/editor-rtl.css 415 B
build/styles/block-library/video/editor.css 416 B
build/styles/block-library/video/style-rtl.css 202 B
build/styles/block-library/video/style.css 202 B
build/styles/block-library/video/theme-rtl.css 134 B
build/styles/block-library/video/theme.css 134 B
build/styles/commands/style-rtl.css 1.72 kB
build/styles/commands/style.css 1.72 kB
build/styles/components/style-rtl.css 14 kB
build/styles/components/style.css 14 kB
build/styles/customize-widgets/style-rtl.css 1.44 kB
build/styles/customize-widgets/style.css 1.44 kB
build/styles/edit-post/classic-rtl.css 426 B
build/styles/edit-post/classic.css 427 B
build/styles/edit-post/style-rtl.css 3.38 kB
build/styles/edit-post/style.css 3.38 kB
build/styles/edit-site/style-rtl.css 15.9 kB
build/styles/edit-site/style.css 15.9 kB
build/styles/edit-widgets/style-rtl.css 4.62 kB
build/styles/edit-widgets/style.css 4.62 kB
build/styles/editor/style-rtl.css 18.5 kB
build/styles/editor/style.css 18.5 kB
build/styles/format-library/style-rtl.css 326 B
build/styles/format-library/style.css 326 B
build/styles/list-reusable-blocks/style-rtl.css 1.02 kB
build/styles/list-reusable-blocks/style.css 1.02 kB
build/styles/media-utils/style-rtl.css 400 B
build/styles/media-utils/style.css 400 B
build/styles/nux/style-rtl.css 622 B
build/styles/nux/style.css 618 B
build/styles/patterns/style-rtl.css 611 B
build/styles/patterns/style.css 611 B
build/styles/preferences/style-rtl.css 415 B
build/styles/preferences/style.css 415 B
build/styles/reusable-blocks/style-rtl.css 275 B
build/styles/reusable-blocks/style.css 275 B
build/styles/widgets/style-rtl.css 1.17 kB
build/styles/widgets/style.css 1.18 kB

compressed-size-action

@jsnajdr
Copy link
Member Author

jsnajdr commented Dec 10, 2025

The current state of the PR is quite nice, although CI checks are still failing and attw is not happy 🙂

  • import statements are fully resolved, so that importing ./foo is expanded into the full ./foo.js or ./foo/index.mjs or whatever, depending on import and module type
  • ESM files in build-module all have .mjs extensions
  • specifiers in package.json files all point to build-module/**/*.mjs files (the .mjs extension)

This should produce very nice dual ESM/CJS packages, although attw is very unhappy with the types for reasons I don't understand:

┌───────────────────┬────────────────────────────┬──────────────────────────────┐
│                   │ "@wordpress/ui"            │ "@wordpress/ui/package.json" │
├───────────────────┼────────────────────────────┼──────────────────────────────┤
│ node10            │ ❌ No types                │ 🟢 (JSON)                    │
├───────────────────┼────────────────────────────┼──────────────────────────────┤
│ node16 (from CJS) │ ❌ No types                │ 🟢 (JSON)                    │
│                   │ 🐛 Used fallback condition │                              │
├───────────────────┼────────────────────────────┼──────────────────────────────┤
│ node16 (from ESM) │ ❌ No types                │ 🟢 (JSON)                    │
│                   │ 🐛 Used fallback condition │                              │
├───────────────────┼────────────────────────────┼──────────────────────────────┤
│ bundler           │ ❌ No types                │ 🟢 (JSON)                    │
│                   │ 🐛 Used fallback condition │                              │
└───────────────────┴────────────────────────────┴──────────────────────────────┘

I'll have to debug this further.

An alternative approach would be to use .cjs extensions. There would be type: module declaration in the package.json, all the .mjs files would be .js and all files in build would be .cjs. The upside would be that we're not changing extensions of files that are used 99% of the time, i.e., the ESM ones. Less room for unexpected issues.

@jsnajdr
Copy link
Member Author

jsnajdr commented Dec 10, 2025

Reasons why things fail:

  • the eslint rule import/no-unresolved apparently has problems to resolve the build-module/index.mjs files and marks import statements as bad, that's why the eslint CI check fails massively with 5k errors.
  • the attw tool fails because npm pack doesn't include the build and build-module folders. There is no files field in most package.json files, and the default is to exclude .gitignored files. So, attw runs on a broken tarball. I wonder what the CI action that published the packages does differently.

@jsnajdr
Copy link
Member Author

jsnajdr commented Dec 10, 2025

I wonder what the CI action that published the packages does differently.

The CI action uses lerna publish to publish the package, and internally it doesn't use npm pack at all, but has its own function packDirectory to produce the package tarball.

We'll have to add files field to all our packages so that the contents of the published package are specified precisely. It will have a nice side-effect, we'll stop publishing the tsconfig.tsbuildinfo files and similar that shouldn't be in published NPM packages.

@jsnajdr jsnajdr force-pushed the build/resolve-import-paths branch 3 times, most recently from 95aec17 to 474265f Compare December 10, 2025 19:33
@jsnajdr
Copy link
Member Author

jsnajdr commented Dec 10, 2025

Some progress I made today:

  • added files field to all package.json files so that the package content is exactly specified. Among other things, helps a lot with running attw on the packages.
  • switched most packages to "type": "module" and renamed CJS .js files to .cjs. ESM files keep their .js extension.

There are still some wrinkles to iron out with .d.ts files, but the CI checks are green, build succeeds and the PR is mostly ready.

Fixes both #73362 and #73363.

Comment on lines +1050 to +1052
relativePath =
relativePath.slice( 0, -ext.length ) + newExt;
Copy link
Member

Choose a reason for hiding this comment

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

Feels like a job for path.basename with second argument.

Suggested change
relativePath =
relativePath.slice( 0, -ext.length ) + newExt;
relativePath =
path.basename( relativePath, ext ) + newExt;

Comment on lines +1044 to +1055
// Replace extension: if path ends with one of the extensions, replace it
const exts = [ '.js', '.jsx', '.ts', '.tsx' ];
const newExt =
build.initialOptions.format === 'cjs' ? '.cjs' : '.js';
for ( const ext of exts ) {
if ( relativePath.endsWith( ext ) ) {
relativePath =
relativePath.slice( 0, -ext.length ) + newExt;
break;
}
}
Copy link
Member

Choose a reason for hiding this comment

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

We could probably do this with RegExp, maybe a bit more efficiently too. Not as future-compatible to if we wanted to actually consistently define extensions somewhere as an array (could probably construct from array using new RegExp constructor).

Suggested change
// Replace extension: if path ends with one of the extensions, replace it
const exts = [ '.js', '.jsx', '.ts', '.tsx' ];
const newExt =
build.initialOptions.format === 'cjs' ? '.cjs' : '.js';
for ( const ext of exts ) {
if ( relativePath.endsWith( ext ) ) {
relativePath =
relativePath.slice( 0, -ext.length ) + newExt;
break;
}
}
// Replace extension: if path ends with one of the extensions, replace it
const exts = [ '.js', '.jsx', '.ts', '.tsx' ];
const newExt =
build.initialOptions.format === 'cjs' ? '.cjs' : '.js';
relativePath = relativePath.replace( /\.[jt]sx?$/, '' ) + newExt;

@jsnajdr
Copy link
Member Author

jsnajdr commented Dec 11, 2025

Today I had to solve one more big problem: incompatibility in how esbuild treats default imports from CJS modules.

Consider a CJS module that exports this:

module.exports = {
  __esModule: true,
  default: function foo() { /* ... */ }
};

What happens when you import this CJS module with an ESM import statement and want to use the default export?

import foo from './foo';

The classic webpack/Babel behavior is that you get the function from the default field. That's what the __esModule field is for. It tells the bundler that the default export is not the module.exports object, but its default field. It's a CJS/ESM interop trick.

But Node.js doesn't respect these tricks and will return the module.exports object as the foo value.

What's worse is that esbuild uses the Node.js behavior when there is the type: module field in package.json. And some of our imports will stop working.

One example is

import isShallowEqual from '@wordpress/is-shallow-equal';

The is-shallow-equal package is externalized, and the real content of the module is:

module.exports = window.wp.isShallowEqual;

But the window.wp.isShallowEqual value is { __esModule: true, default: isShallowEqual, /* ... named exports */ }. Suddenly the value of the default import is not the function, it's the object.

I fixed this by adding a named isShallowEqual export to the package. Now the default export can be considered legacy and we use only the named ones. Because default exports don't work reliably when CJS/ESM interop is in play.

Another example is the react-autosize-textarea import:

import TextareaAutosize from 'react-autosize-textarea';

Again, the package exports a { __esModule: true, default } object and it doesn't work any more. I fixed this by using a named export from a sub-path:

import { TextareaAutosize } from 'react-autosize-textarea/lib/TextareaAutosize';

I'm afraid we'll see many more issues like this once we start using type: module.

@anomiex @manzoorwanijk Did you run into similar issues when using type: module in Jetpack? Or is it an esbuild-only problem?

@anomiex
Copy link
Contributor

anomiex commented Dec 11, 2025

https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/CJSOnlyExportsDefault.md has a detailed writeup on the problem.

#59087 is about the same thing with respect to @wordpress/api-fetch. Other than that, I don't recall having run into it often.

@jsnajdr jsnajdr force-pushed the build/resolve-import-paths branch from 640c7e9 to ab6c8b2 Compare December 11, 2025 20:58
@github-actions
Copy link

Flaky tests detected in ab6c8b2.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/20147232455
📝 Reported issues:

Comment on lines +29 to +32

// `isShallowEqual` is exported also as a named export because esbuild cannot
// expose the default export from the `window.wp.isShallowEqual` global.
export { isShallowEqual, isShallowEqualObjects, isShallowEqualArrays };
Copy link
Member

Choose a reason for hiding this comment

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

We should do the same for all the packages mentioned in #59087 (comment)

Copy link
Member Author

Choose a reason for hiding this comment

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

We should do the same for all the packages mentioned in #59087 (comment)

All the other packages should be OK, because they:

  • either have a single default export and the wpScriptDefaultExport flag. Then module.exports value is directly the exported function.
  • or multiple named exports and no default. Then module.exports is an object with a few named field, none of them default.

Both types of modules can be easily mapped between CJS and ESM without any problems.

The is-shallow-equal module is a combination of default and named exports. That's the only case that needs fixing.

Maybe I'm wrong, I'll need to look more closely what exactly is the problem with api-fetch as reported in #59087.

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

Labels

[Package] wp-build /packages/wp-build [Type] Build Tooling Issues or PRs related to build tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants