Build: fully resolve import paths in transpiled files#73822
Conversation
|
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 If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
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. |
|
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 |
|
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 ( |
packages/wp-build/src/build.mjs
Outdated
| ); | ||
| if ( isDir ) { | ||
| return { | ||
| path: args.path + '/index.js', |
There was a problem hiding this comment.
Should we use path.join ? Not sure about the Windows compatibility of this slash.
There was a problem hiding this comment.
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 ().
packages/wp-build/src/build.mjs
Outdated
| ! args.path.endsWith( '.js' ) | ||
| ) { | ||
| const baseDir = path.dirname( args.importer ); | ||
| for ( const ext of [ '.js', '.jsx', '.ts', '.tsx' ] ) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 🙂
packages/wp-build/src/build.mjs
Outdated
| ) { | ||
| const baseDir = path.dirname( args.importer ); | ||
| for ( const ext of [ '.js', '.jsx', '.ts', '.tsx' ] ) { | ||
| const isFile = await pathIsFile( |
There was a problem hiding this comment.
Is there enough likelihood of reuse that it'd be worth considering to cache this result?
packages/wp-build/src/build.mjs
Outdated
| } | ||
| } | ||
|
|
||
| const isDir = await pathIsDirectory( |
There was a problem hiding this comment.
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.
3847d0d to
1f3a7de
Compare
I updated the |
That sounds right, because there's the additional work of resolving every import on the filesystem. Before this PR we were copying the import paths verbatim. But now we do some work that only bundler has to do: actually find the dependency on the fs.
This concern could be addressed if we implemented the package bundling from #73516:
The upside is that both steps do work that is useful, and there's no duplication/overlap between them.
For day to day work in VSCode and with We don't need typechecking for building any executable Gutenberg artifacts, that's true. The only place where we need |
|
@manzoorwanijk @anomiex The fixed packages have been published to NPM under the |
|
Testing in Automattic/jetpack#46389 |
|
No difference because the extension is still |
|
@manzoorwanijk There is a misunderstanding: the Jetpack PR and the attw screenshots still use the I'd like to do at least some external testing before we publish the new packages as stable |
Ah sorry, didn't notice that. I thought it was a regular upgrade. I created a test PR manually to see how it work - Automattic/jetpack#46397 |
|
Almost every package breaks with
Types are still broken https://arethetypeswrong.github.io/?p=%40wordpress%2Fblock-editor%4015.9.1-next.738bb1424.0
|
Yes, the CJS version of It's very likely that many other CJS modules are broken the same way. |
|
We're running into this issue: evanw/esbuild#2623 When a file like import supportedMatchers from './supported-matchers';Then the transpiled CJS files' The advice in the PR is to never use default exports. That's not great. I'm trying to figure out what else we can do. |


This PR implements the same thing as #73422, but as part of esbuild
onResolvecallback, 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
.mjsor.cjsextension. 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 (likeblock.json) are not handled with this code, etc. But I didn't want to delay the PR further. Let's test and review.