Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tsconfig paths are not transformed if not bundling #394

Open
mhart opened this issue Sep 18, 2020 · 19 comments
Open

tsconfig paths are not transformed if not bundling #394

mhart opened this issue Sep 18, 2020 · 19 comments

Comments

@mhart
Copy link

mhart commented Sep 18, 2020

$ 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

@mhart
Copy link
Author

mhart commented Sep 18, 2020

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

@Shyam-Chen
Copy link

Shyam-Chen commented Feb 19, 2021

How about module-alias? I'm going to use it on Koa apps.

@g00fy-
Copy link

g00fy- commented Apr 20, 2021

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.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

@g00fy-
Copy link

g00fy- commented Apr 21, 2021

workaround is to use tsc-alias

@Shyam-Chen
Copy link

@g00fy- How does it work with esbuild?

@jpike88
Copy link

jpike88 commented Jul 31, 2021

@evanw is there an official workaround to this?

@gajus
Copy link

gajus commented Dec 6, 2021

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'
  ),
}

@frankleng
Copy link

wrote https://www.npmjs.com/package/esbuild-ts-paths
plugins mentioned above seemed overly complex/didn't work for me

@johnnybenson
Copy link

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
  ...

@valeneiko
Copy link

valeneiko commented Oct 15, 2022

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

@dougwithseismic
Copy link

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.

@nadhifikbarw
Copy link

nadhifikbarw commented May 6, 2023

Future ref for people looking for solutions that's in similar build scenario. I performed path alias transformations using tsc-alias

Conditions:

  1. 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
  2. Already type checked implementations with isolatedModules tsconfig option, as described in TS caveats to ensure non-breaking results
  3. 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

@RedStar071
Copy link

is it also possible to use this code with tsup?

@nadhifikbarw
Copy link

@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.

ardelato added a commit to iFixit/vigilo that referenced this issue Nov 30, 2023
For Docker builds, we don't want to bundle the config files into the
dist/index.js file. By bundling them, we lose the ability to change
them without rebuilding the image. Rebuilding the image every time
we want to change the config files is not ideal and would prevent us
from using the image for other sites.

Solving this problem is a bit tricky because we want to use the same
build script for both Docker and non-Docker builds, keep the independent
core and scripts bundling, and retain the path aliasing functionality.
To achieve all of this, I used the following:
- tsup's 'external' option to exclude the "@config" modules from the bundle
- The tsc-alias package to resolve the "@config" paths to a relative path
- The process.env.DOCKER variable to determine whether to use tsup's
  'external' option or not (this variable is set in the Dockerfile).

By doing all of this, we can now pass the config files into the
container as volumes and change them without rebuilding the image. At
the same time, we can still use the same build script for local
development and retain the simplicity of standalone builds.

The use of tsc-alias was discovered in this issue:
evanw/esbuild#394 (comment).
Without it, "@config/*" paths would remain in the bundle and would
cause errors as there is no way for "@config" to be resolved as
a local path.
@maca134
Copy link

maca134 commented Apr 2, 2024

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?

@colin969
Copy link

colin969 commented Apr 2, 2024

It could be worse, at least there isn't a bot closing these for 30 days of inactivity like every other github project lol

@SystemParadox
Copy link

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.

@kuba-orlik
Copy link

Esbuild is treating imports like src/foo/bar.js as external, so they aren't even passed to onResolve for me. @johnnybenson @frankleng how did you work around it?

@frankleng
Copy link

Esbuild is treating imports like src/foo/bar.js as external, so they aren't even passed to onResolve for me. @johnnybenson @frankleng how did you work around it?

i ended up using pnpm workspace for our monorepo. so "external" imports are now aliased from node_modules
no longer using my plugin, just esbuild-config/plugins/esbuild.nativeNodeModulesPlugin

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

No branches or pull requests