esbuild
esbuild copied to clipboard
tsconfig `paths` are not transformed if not bundling
$ cat tsconfig.json
{
"compilerOptions": {
# ...
"paths": {
"lib/*": ["lib/*"]
}
},
}
$ echo "import logger from 'lib/logger'" | esbuild --format=cjs
# ...
const logger = __toModule(require("lib/logger"));
^ the import/require is left as-is (as opposed to being transformed to ./lib/logger
), so will fail if run, say in Node.js
Was thinking about whether it's esbuild's responsibility to do the transform, and I feel it is: as a ts-to-js transformer, it should perform any transforms implied in tsconfig.json, including path transforms
That said. It appears that ts-node
has taken a similar stance, and also doesn't do the transform, which means ppl need to use https://github.com/dividab/tsconfig-paths – I guess that's the solution here? Would be interested to see if you think it's a feature that could be supported though
How about module-alias? I'm going to use it on Koa apps.
the behavior is not consistent with the tsc
output which transforms aliases as expected
most common scenario for this requirement is when creating a lib which should not be published as bundle:
import { MyComponent } from '@/components';
$ cat tsconfig.json { "compilerOptions": { # ... "paths": { "lib/*": ["lib/*"] } }, } $ echo "import logger from 'lib/logger'" | esbuild --format=cjs # ... const logger = __toModule(require("lib/logger"));
^ the import/require is left as-is (as opposed to being transformed to
./lib/logger
), so will fail if run, say in Node.jsWas thinking about whether it's esbuild's responsibility to do the transform, and I feel it is: as a ts-to-js transformer, it should perform any transforms implied in tsconfig.json, including path transforms
workaround is to use tsc-alias
@g00fy- How does it work with esbuild
?
@evanw is there an official workaround to this?
Just create a simple loader, e.g.
const path = require('path');
/**
* @see https://github.com/evanw/esbuild/issues/394
*/
module.exports = function (content) {
const relativePath = path.relative(
path.dirname(this.resourcePath),
path.resolve(__dirname, '../src')
);
return content.replaceAll(
`from "@/`,
`from "${relativePath ? relativePath + '/' : './'}`
);
};
and then use it to pre-process files:
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: path.resolve(__dirname, 'src'),
loader: path.resolve(
__dirname,
'./esbuild-loaders/paths-loader.js'
),
}
wrote https://www.npmjs.com/package/esbuild-ts-paths plugins mentioned above seemed overly complex/didn't work for me
Maybe this is shortsighted of me, having a plugin handle this feels pretty heavy.
Below is not the most robust solution, feels like the right amount of effort.
// tsc-helpers.js
const path = require('path');
const tsconfig = require('../../tsconfig.json');
module.exports = {
// Workaround for Typescript import paths not being transformed inside onResolve
// @link https://github.com/evanw/esbuild/issues/394
fixBaseUrl: (importPath) =>
importPath.startsWith('./')
? importPath
// Assumption: this is an aliased import
// Read tsconfig directly to avoid any drift
: path.join(tsconfig.compilerOptions.baseUrl, importPath),
};
// some-plugin.js
module.exports = {
name: 'my-plugin',
setup(build) {
build.onResolve({ filter: ... }, async (args) => {
args.path = tscHelper.fixBaseUrl(args.path);
// 🍕 delicious
...
the plugin is simple enough, and handles some of the edge cases I found + user requested. but yes rewriting path in esbuild is not difficult
This seems to work: https://nodejs.org/docs/latest-v16.x/api/packages.html#subpath-exports
// tsconfig.json
...
"paths": {
"#Src/*": [
"./src/*"
]
},
...
// package.json
...
"imports": {
"#Src/*": "./dist/*"
}
...
// ./src/index.ts
import { someFunction } from "#Src/util.js"; // <-- the source file name is `util.ts`, but import uses `.js`
someFunction(...);
Built using:
esbuild --format=esm --platform=node --target=node18 --outbase=src --outdir=dist --sourcemap ./src/*.ts
wrote https://www.npmjs.com/package/esbuild-ts-paths plugins mentioned above seemed overly complex/didn't work for me
Works perfectly. Thanks. Looking forward to esbuild adopting this hint hint.
Future ref for people looking for solutions that's in similar build scenario. I performed path alias transformations using tsc-alias
Conditions:
- Need to perform build without bundling, specifically for my scenario I just need to strip down TS type annotations and resolve paths aliasing to produce valid js for it to be run by regular
node
runtime - Already type checked implementations with
isolatedModules
tsconfig option, as described in TS caveats to ensure non-breaking results - Simple multi entrypoints setup pointing to all ts files
build.mjs
setup
import { replaceTscAliasPaths } from 'tsc-alias';
import { build } from 'esbuild';
import fg from 'fast-glob';
await build({
entryPoints: await fg(['src/**/*.ts']),
outdir: 'dist',
platform: 'node',
tsconfig: 'tsconfig.json'
});
// Post build paths resolve since ESBuild only natively
// support paths resolution for bundling scenario
await replaceTscAliasPaths({
configFile: 'tsconfig.json',
watch: false,
outDir: 'dist',
declarationDir: 'dist'
});
If your runtime doesn't support top level await yet, you can change it using async/await IIFE
scripts
entry is as simple as
package.json
{
"scripts": {
"build": "node build.mjs"
}
}
Similar setup can be done only by using CLI instead of defining it programmatically, since tsc-alias
ships with CLI, but you have to make sure your platform can correctly handle globstars or however you define your entrypoints list, personally I prefer this approach and just installing fast-glob
to easily guarantee cross-platform behavior. Hope this helps
is it also possible to use this code with tsup?
@RedStars071 generally yes, the core idea should be the same, assuming tsup
doesn't automatically resolve path aliasing then by adjusting code snippet to their exposed build
api it would follow the same logic.
So 4 years later and its still no easier to fix path alias's at transpile time? Needing 17 extra build steps seems stupid, no?
It could be worse, at least there isn't a bot closing these for 30 days of inactivity like every other github project lol
It might be worth pointing out that if you use --bundle --packages=external
then tsconfig paths are still applied as the per the docs:
- external
This setting considers all import paths that "look like" package imports in the original source code to be package imports. Specifically import paths that don't start with a path segment of / or . or .. are considered to be package imports. The only two exceptions to this rule are subpath imports (which start with a # character) and TypeScript path remappings via paths and/or baseUrl in tsconfig.json (which are applied first).
Note that this is only the case with esbuild 0.19+, not with previous versions.