aleclarson / vite-tsconfig-paths

Support for TypeScript's path mapping in Vite
MIT License
1.24k stars 43 forks source link

Recursively Resolving Paths Outside Tsconfig Includes Array #58

Closed au-z closed 2 years ago

au-z commented 2 years ago

Thanks for creating such a useful package @aleclarson! During development, I've noticed what I think is a small bug affecting module resolution.

I've prepared a minimal reproduction here: https://github.com/auzmartist/vite-tsconfig-paths-recursive-resolution

Some analysis of the vite debug output...

Problem

Using vite-tsconfig-paths, modules placed outside the includes array are normally resolved. However, this only extends to first order dependencies - dependencies referenced directly from project source.

However, if these first order dependencies, depend on other pathed modules which are similarly outside the tsconfig include array, errors are produced during module resolution.

Example Annotated Output

DEBUG=vite-tsconfig-paths vite

Here we crawl for a tsconfig.json. Notably, two paths @moduleA and @moduleB are configured which point to TS modules in ./modules.

> DEBUG=vite-tsconfig-paths vite

  vite-tsconfig-paths crawling "/mnt/f/dev/forked/vite-tsconfig-paths-example" +0ms
  vite-tsconfig-paths options: {
  projects: [ '/mnt/f/dev/forked/vite-tsconfig-paths-example/tsconfig.json' ],
  extensions: [ '.ts', '.tsx', '.js', '.jsx', '.mjs' ]
} +9ms
  vite-tsconfig-paths config loaded: {
  configPath: '/mnt/f/dev/forked/vite-tsconfig-paths-example/tsconfig.json',
  include: [ 'src/**/*.ts' ],
  exclude: undefined,
  allowJs: undefined,
  baseUrl: undefined,
  paths: {
    '@moduleA': [ './modules/moduleA' ],
    '@moduleB': [ './modules/moduleB' ]
  },
  outDir: undefined
} +4ms

Here, we see that the tsconfig only includes TS modules within ./src.

  vite-tsconfig-paths compiled globs: {
  included: [ /^\.\/src\/((?:[^/]*(?:\/|$))*)([^/]*)\.ts$/ ],
  excluded: [
    /^\.\/node_modules\/((?:[^/]*(?:\/|$))*)$/,
    /^\.\/bower_components\/((?:[^/]*(?:\/|$))*)$/,
    /^\.\/jspm_packages\/((?:[^/]*(?:\/|$))*)$/
  ]
} +0ms

Here we start up the dev server and begin resolving modules.

  vite v2.9.9 dev server running at:

  > Local: http://localhost:3000/
  > Network: use `--host` to expose

  ready in 575ms.

Here we resolve @moduleA.

  vite-tsconfig-paths resolved: {
  id: '@moduleA',
  importer: '/mnt/f/dev/forked/vite-tsconfig-paths-example/src/index.ts',
  resolvedId: '/mnt/f/dev/forked/vite-tsconfig-paths-example/modules/moduleA.ts',
  configPath: '/mnt/f/dev/forked/vite-tsconfig-paths-example/tsconfig.json'
} +125ms

Here we resolve @moduleB.

  vite-tsconfig-paths resolved: {
  id: '@moduleB',
  importer: '/mnt/f/dev/forked/vite-tsconfig-paths-example/src/index.ts',
  resolvedId: '/mnt/f/dev/forked/vite-tsconfig-paths-example/modules/moduleB.ts',
  configPath: '/mnt/f/dev/forked/vite-tsconfig-paths-example/tsconfig.json'
} +8ms

Here we see that we cannot resolve @moduleA depended on by @moduleB:

The following dependencies are imported but could not be resolved:

  @moduleA (imported by /mnt/f/dev/forked/vite-tsconfig-paths-example/modules/moduleB.ts)

Are they installed?
Failed to resolve import "@moduleA" from "modules/moduleB.ts". Does the file exist?
5:14:16 PM [vite] Internal server error: Failed to resolve import "@moduleA" from "modules/moduleB.ts". Does the file exist?
  Plugin: vite:import-analysis
  File: /mnt/f/dev/forked/vite-tsconfig-paths-example/modules/moduleB.ts
  1  |  import { A } from "@moduleA";
     |                     ^
  2  |  export function B() {
  3  |    return `MODULE B: ${A}`;
      at formatError (/mnt/f/dev/forked/vite-tsconfig-paths-example/node_modules/vite/dist/node/chunks/dep-59dc6e00.js:38663:46)
      at TransformContext.error (/mnt/f/dev/forked/vite-tsconfig-paths-example/node_modules/vite/dist/node/chunks/dep-59dc6e00.js:38659:19)
      at normalizeUrl (/mnt/f/dev/forked/vite-tsconfig-paths-example/node_modules/vite/dist/node/chunks/dep-59dc6e00.js:56830:26)
      at processTicksAndRejections (node:internal/process/task_queues:96:5)
      at async TransformContext.transform (/mnt/f/dev/forked/vite-tsconfig-paths-example/node_modules/vite/dist/node/chunks/dep-59dc6e00.js:56979:57)
      at async Object.transform (/mnt/f/dev/forked/vite-tsconfig-paths-example/node_modules/vite/dist/node/chunks/dep-59dc6e00.js:38900:30)
      at async doTransform (/mnt/f/dev/forked/vite-tsconfig-paths-example/node_modules/vite/dist/node/chunks/dep-59dc6e00.js:55857:29)

I am not sure if this is the intended behavior of the plugin but it certainly seems unexpected that the tsconfig paths would only apply to dependencies imported from modules within the include array. Perhaps this could be expanded to apply to all modules by default and the current "strict" implementation offered as a configuration option? What do you think?

aleclarson commented 2 years ago

This is expected behavior. There are 3 solutions that don't require a new plugin option:

  1. Move ./modules to ./src/modules
  2. Add modules/**/* to the include array
  3. Add modules/tsconfig.json and have it extend ./tsconfig.json

Do any of these work for you?

aleclarson commented 2 years ago

Interesting to note that TypeScript successfully resolves @moduleA from moduleB.ts until I rename tsconfig.json to src/tsconfig.json (and make necessary changes so the paths are correct again).

In other words, the paths resolution is only applied to transient dependencies when the tsconfig.json exists in a parent directory of the transient dependency.

aleclarson commented 2 years ago

Anyway, my initial conclusion is that there exists good enough workarounds that this isn't a priority. If someone wants to submit a PR, I'll try to review it at some point, but no guarantees.

au-z commented 2 years ago

Thanks for having a look quickly @aleclarson and chiming in. 👍

Because of some organizational constraints beyond this toy example, 1 and 3 are not possible. Luckily, in my case, 2 (appending to the include array) would technically work though it's not quite ideal as I'll have to maintain both the tsconfig paths and a similar include array.

Good to get an official answer on this and to have the example around for posterity. I appreciate it!

peabnuts123 commented 2 months ago

For anyone coming from Google, if you need to workaround this, you can duplicate your path aliases into Vite's config without using any plugins:

// vite.config.js
const config: UserConfigExport = {
    // ...
    resolve: {
      alias: {
        '@moduleA': path.resolve(__dirname, "modules/moduleA"),
        '@moduleB': path.resolve(__dirname, "modules/moduleB"),
      }
    },
    plugins: [
      // ...
    ],
    // ...
  };

See more details here.

Obviously it's suboptimal to duplicate your path definitions, but if they don't change very often, it's good enough IMO.