vite icon indicating copy to clipboard operation
vite copied to clipboard

`vite.config.ts` can't import untranspiled ts files from other packages in the same monorepo

Open zheeeng opened this issue 2 years ago • 47 comments

Describe the bug

If we import something from symlink and the importee is ts file. We counter a such error:

failed to load config from /Users/zheeeng/Workspace/foo/bar/baz/vite.config.ts
error when starting dev server:
TypeError: defaultLoader is not a function

There are two workarounds: compile the ts file to the common js file, or specify the importee path to its real file path rather than symlink.

How could we use it without these two approaches?

Reproduction

https://github.com/zheeeng/test-symlink-vite-config

System Info

Core(TM) i7-9750H CPU @ 2.60GHz
    Memory: 1.60 GB / 16.00 GB
    Shell: 5.8 - /bin/zsh
  Binaries:
    Node: 14.17.0 - ~/.nvm/versions/node/v14.17.0/bin/node
    Yarn: 1.22.11 - ~/.nvm/versions/node/v14.17.0/bin/yarn
    npm: 6.14.13 - ~/.nvm/versions/node/v14.17.0/bin/npm
  Browsers:
    Chrome: 95.0.4638.54
    Safari: 14.1.2

Used Package Manager

pnpm

Logs

failed to load config from /Users/zheeeng/Workspace/foo/bar/baz/vite.config.ts
error when starting dev server:
TypeError: defaultLoader is not a function
    at Object.require.extensions.<computed> [as .ts] (/Users/zheeeng/Workspace/foo/node_modules/.pnpm/[email protected][email protected]/node_modules/vite/dist/node/chunks/dep-55830a1a.js:68633:13)
    at Module.load (internal/modules/cjs/loader.js:933:32)
    at Function.Module._load (internal/modules/cjs/loader.js:774:14)
    at Module.require (internal/modules/cjs/loader.js:957:19)
    at require (internal/modules/cjs/helpers.js:88:18)
    at Object.<anonymous> (/Users/zheeeng/Workspace/foo/web/studio/vite.config.ts:37:32)
    at Module._compile (internal/modules/cjs/loader.js:1068:30)
    at Object.require.extensions.<computed> [as .ts] (/Users/zheeeng/Workspace/foo/node_modules/.pnpm/[email protected][email protected]/node_modules/vite/dist/node/chunks/dep-55830a1a.js:68630:20)
    at Module.load (internal/modules/cjs/loader.js:933:32)
    at Function.Module._load (internal/modules/cjs/loader.js:774:14)

Validations

zheeeng avatar Oct 21 '21 07:10 zheeeng

Hi, we've discussed this issue at last Friday's team meeting.

Considering that:

  1. I can't think of an efficient way to support this feature.
    • The packages are excluded by esbuild because they're external.
    • Most of the external packages used by vite.config.* don't (and shouldn't) require transpilation
    • It's already a configuration file, we don't want to introduce another option to configure the loading logic of the configuration file.
  2. The use case is quite rare. The logic in the configuration file is usually not very complex. Even if it is extracted to a separate package, I think it's acceptable to be written in plain JS or processed by an additional run of tsc.

So this feature would be a low priority for the team. But feel free to open a PR if you can find a better and efficient way to handle this use case.

sodatea avatar Oct 25 '21 03:10 sodatea

Thx for your discussion, I would try to transpile the ts config file.

zheeeng avatar Oct 25 '21 04:10 zheeeng

@sodatea Can we add an environment variable for configuring this?

zheeeng avatar Oct 25 '21 05:10 zheeeng

Environment variables are also a kind of configuration, so I don't think we should do that.

sodatea avatar Oct 25 '21 05:10 sodatea

I just realized that we could use a loader like esbuild-register for config loading.

I'm not sure if we can use esbuild-register directly. For now, I can think of a few edge cases:

  1. Each TS module's tsconfig.json isn't correctly respected. We need to reuse https://github.com/dominikg/tsconfck for that.
  2. Plain JS modules that aren't in the project should not be transpiled by esbuild due to performance concerns.

Anyway, this is a low-priority but doable feature. We can fix it when we are going to refactor the configuration loading logic in the future.

sodatea avatar Nov 24 '21 07:11 sodatea

Are we still looking at it? because it is very useful when it comes to monorepo and workspaces. Even more so when we can create and use plugins in simple and shared ways within the project.

hiukky avatar Mar 10 '22 03:03 hiukky

Hello @zheeeng. We like your proposal/feedback and would appreciate a contribution via a Pull Request by you or another community member. We thank you in advance for your contribution and are looking forward to reviewing it!

github-actions[bot] avatar Mar 28 '22 05:03 github-actions[bot]

We also encounter this problem. Our use case is that we have a monorepo with 5 micro-frontends (vite apps). We want to have 1 base vite config file and extend from that. It would be really nice to have an option to import and/or extends other config files in .ts format without compiling first.

Anyway, we ended up writing the base config file in plain js with module.exports.

  • vite.apps.base.js
const react = require("@vitejs/plugin-react").default;

module.exports = function ({ root, isDev, plugins }) {
  return {
     ...
      resolve: {
       alias: {
         src: path.resolve(root, "src"),
       },
     },
     plugins: [
         react({
          babel: {
            plugins: [
              [
                "babel-plugin-styled-components",
                {
                  displayName: isDev,
                  fileName: false,
                  ssr: false,
                  minify: !isDev,
                  transpileTemplateLiterals: !isDev,
                  pure: !isDev,
                },
              ],
            ],
          },
        })
       ...more plugins
      ...plugins
     ]
  }
... more options
}

and then we could use like this:

import base from 'config/vite.apps.base';

// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
  const isDev = mode === 'development';

  return base({
    root: __dirname,
    isDev,
    plugins: [
      partytownVite({
        dest: path.resolve(__dirname, 'build', '~partytown'),
      }),
    ],
  });
});

julianklumpers avatar Apr 07 '22 12:04 julianklumpers

use preconstruct on your packages... then you can just sidestep all this headache.

https://preconstruct.tools/

it sets up mjs and cjs stubs that behave differently in development than in production. 👍🏻

airtonix avatar Apr 09 '22 09:04 airtonix

@airtonix does that not need to setup babel and stuff to compile everything? The whole reason we use Vite is for the dx and no need to setup a lot of additional babel stuff.

julianklumpers avatar Apr 09 '22 12:04 julianklumpers

🤷🏻 it's not much tbh. compared to the drama llama of your current situation, i'd happily pick preconstruct.

airtonix avatar Apr 11 '22 02:04 airtonix

@airtonix Preconstruct seems to be a monorepo management tool that scaffolding can do. I think this issue is close to tsconfig's extends or prettier's sharing configurations feature.

IMO, I think we need a feature like webpack-merge of webpack production doc


@julianklumpers I'am using config builder pattern. (Temporary solution)

  • Library: https://github.com/black7375/vite-config-builder
  • Custom Config: https://github.com/black7375/ts-monorepo-template/tree/main/configs/vite-config-custom
  • Use Config: https://github.com/black7375/ts-monorepo-template/blob/main/packages/hello/vite.config.ts

black7375 avatar May 25 '22 02:05 black7375

Wouldn't using something like esbuild-register pollutes the entire nodejs process? e.g. it would affect Vite SSR too which may bring a slight perf penalty, or perhaps inconsistency. Otherwise we'd need to spawn a child process to read the config, but that only works if the config is serializable.

I tried a different solution, by bundling the monorepo package when detected that the resolved path doesn't contain node_modules. It works but if the package defines it's own __filename or _dirname, it would break loading the config. Plus if you're linking Vite locally too, it'll have to re-bundle it entirely (also suffers from __filename issue).

Overall I'm also not sure if this is worth the complexity.

bluwy avatar Jun 26 '22 05:06 bluwy

Same issue here.

I created 6 package projects within a Lerna workspace. Additionally, I created a "config" package, which is symlinked to each. Importing the tsconfig as extends option works fine, also all other tools complain.

When it comes to vite, it states the above error message when importing and using the configuration function from that other config package.

I would really like to have and use this, it's very useful to have the same configs for each project ( all of them bundle the exact same way), shared as one config in one Central place.

Looks like I really need to convert the shared config to a JS file, loosing TS Support :(

Hobart2967 avatar Jul 02 '22 22:07 Hobart2967

If you want to use a monorepo/workspace with typescript, you should set it up correctly using project references with compilerOptions composite: true, since you can't import an untranspiled .ts file in a transpiled module. Every module (like a plugin) you import into vite.config.ts is already transpiled to js. Even without vite, the projects need to be referenced. While developing you should use ts-node or tsx, so you don't need to rebuild the files all the time.

There are many different ways to set up a working typescript workspace. I created an example vite-typescript-monorepo.

jrson83 avatar Jul 03 '22 04:07 jrson83

If you want to use a monorepo/workspace with typescript, you should set it up correctly using project references with compilerOptions composite: true, since you can't import an untranspiled .ts file in a transpiled module. Every module (like a plugin) you import into vite.config.ts is already transpiled to js. Even without vite, the projects need to be referenced. While developing you should use ts-node or tsx, so you don't need to rebuild the files all the time.

There are many different ways to set up a working typescript workspace. I created an example vite-typescript-monorepo.

May be true, but the expectation from a user's point of view is a little different. If typescript support for configs is coming out of the box, it should be supported fully. In my point of view there's only two ways of solving this:

  1. Remove TypeScript support to avoid "misusage"
  2. Make it work ;)

Hobart2967 avatar Jul 04 '22 07:07 Hobart2967

If you want to use a monorepo/workspace with typescript, you should set it up correctly using project references with compilerOptions composite: true, since you can't import an untranspiled .ts file in a transpiled module. Every module (like a plugin) you import into vite.config.ts is already transpiled to js. Even without vite, the projects need to be referenced. While developing you should use ts-node or tsx, so you don't need to rebuild the files all the time.

There are many different ways to set up a working typescript workspace. I created an example vite-typescript-monorepo.

This is precisely the point of preconstruct.

Feels like everyone is trying to reinvent nodejs to avoid using preconstruct?

because there are several problems here you can keep using your project references with preconstruct.

  1. how do you speed up vscode intellisense resolving and compiling? tsconfig references
  2. how do you resolve packages? either: a. a root tsconfig with all the monorepo packages listed in paths, or b. just use normal nodejs way where each package has it's own package.json and then you use preconstruct to provide easy no-compile access to ts and non ts tooling.

If you go for 2.a, then you cant dogfood your own typescript tooling packages with any tooling you use that doesn't know how to typescript.

because of that, i choose to work with preconstruct

airtonix avatar Jul 13 '22 00:07 airtonix

Now that vite 3 is using esm, I'm getting an issue with importing *.ts files in monorepo packages (TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts"). The "right" answer is to compile the *.ts files. I would love to avoid the extra build step, as it takes development maintenance, time, complexity, & makes the DX considerably worse. Bun will handle this but it's is 6+ months away. At this point, what would be the best way to import *.ts files without having to compile those files? I get that it's a low priority & the "right" answer is to compile the *.ts files, but it sure would be great if it were not necessary.

TSX has been working great for running from the cli. Importing *.ts from monorepo packages worked in vite v2. Now how do we get the automatic transpilation of *.ts back in vite3 + nodejs?

There seems to be a confluence of underlying approaches which cause this problem. The need to automatically transpile ts to js. The need to use a monorepo with separate npm packages. The need to have a quick build...etc

May be true, but the expectation from a user's point of view is a little different. If typescript support for configs is coming out of the box, it should be supported fully. In my point of view there's only two ways of solving this:

  1. Remove TypeScript support to avoid "misusage"
  2. Make it work ;)

Agreed. The problem with partial support for TypeScript is that the support becomes a moving target...such as the ERR_UNKNOWN_FILE_EXTENSION regression between v2 & v3...which makes for some nasty surprises when upgrading.

btakita avatar Jul 24 '22 16:07 btakita

Just remembered of jiti, and it seems to have the mechanism we need to programmatically load the config, and addresses my previous comment's concern. jiti however seems to have a rather large bundle size. We could probably implement something simlar, leveraging our own esbuild API too. So maybe there's a way to solve this and #9202.

bluwy avatar Jul 24 '22 17:07 bluwy

Any updates?

PurpleTape avatar Dec 03 '22 11:12 PurpleTape

What worked as a workaround for me was using a relative import inside the monorepo instead of absolute package paths. So assuming you have a monorepo with app/ and plugin/ in root packages/, in packages/app/vite.config.ts instead of importing

import MyPlugin from '@me/plugin'

just used

import MyPlugin from '../plugin'

It's not ideal since you are importing from outside of your package, so still hoping this issue gets a proper fix in Vite.

kdembler avatar Dec 06 '22 09:12 kdembler

@kdembler your workaround works for me too, I don't like it but until Vite can fix this issue it'll have to do, this is definitely the simplest workaround I've found so far, thanks!

@btakita

Agreed. The problem with partial support for TypeScript is that the support becomes a moving target...such as the ERR_UNKNOWN_FILE_EXTENSION regression between v2 & v3...which makes for some nasty surprises when upgrading.

I couldn't agree more, partial support for TS is a deal breaker in so many open source projects these days, either support it 100% or don't, there is really no in between.

nedkelly avatar Dec 06 '22 22:12 nedkelly

Compiling the shared config to JS and importing it works fine, untill you need to import another package,~

import IconsResolver from 'unplugin-icons/resolver'
export const components = {
  // relative paths to the directory to search for components
  dirs: ['src/**/components'],
  // allow auto load markdown components under `./src/components/`
  extensions: ['vue'],
  // allow auto import and register components used in markdown
  include: [/\.vue$/, /\.vue\?vue/],
  // custom resolvers
  resolvers: [
    // auto import icons
    // https://github.com/antfu/unplugin-icons
    IconsResolver({
      enabledCollections: ['far'],
    }),
  ],
}

TypeError: (0 , resolver_1.default) is not a function

blowsie avatar Feb 28 '23 17:02 blowsie

For myself, I found a workaround for loading a ts file in vite.config.ts . I import the ts file through js (js file like a proxy)

// root/vite.config.ts

import { funcFromTsModule } from './moduleForViteConfig'
export default ({ mode }) => {
    funcFromTsModule('example')
    return defineConfig({
        build: {
            ...
        },
        server: {
            ...
        },
        plugins: [
        ],
        ...
    })
}
// root/moduleForViteConfig/index.js

export * from './src'
// root/moduleForViteConfig/src/index.ts

export const funcFromTsModule = (str: string) => 'bar' + str

100100101 avatar Mar 31 '23 03:03 100100101

Just remembered of jiti, and it seems to have the mechanism we need to programmatically load the config, and addresses my previous comment's concern. jiti however seems to have a rather large bundle size. We could probably implement something simlar, leveraging our own esbuild API too. So maybe there's a way to solve this and #9202.

Interesting. Tailwind had similar issues to this very ticket (https://github.com/vitejs/vite/issues/5370), they addressed it with jiti.

blowsie avatar Apr 19 '23 17:04 blowsie

We're facing the same issue at Nx. Related issue: https://github.com/nrwl/nx/issues/17019#issuecomment-1561452476

and repro: https://github.com/mandarini/vite-paths

mandarini avatar May 24 '23 15:05 mandarini

Just wanted to put out another option that I see mentioned once but as something to look for in the future - I've simply swapped to using bun to run Vite, and there isn't any issue anymore..

This is where I found how to use bun with vite: https://github.com/bluwy/bun-vite-ts-test/blob/master/package.json

denno020 avatar Jul 26 '23 12:07 denno020

We also encounter this problem. Our use case is that we have a monorepo with 5 micro-frontends (vite apps). We want to have 1 base vite config file and extend from that. It would be really nice to have an option to import and/or extends other config files in .ts format without compiling first.

Anyway, we ended up writing the base config file in plain js with module.exports.

  • vite.apps.base.js
const react = require("@vitejs/plugin-react").default;

module.exports = function ({ root, isDev, plugins }) {
  return {
     ...
      resolve: {
       alias: {
         src: path.resolve(root, "src"),
       },
     },
     plugins: [
         react({
          babel: {
            plugins: [
              [
                "babel-plugin-styled-components",
                {
                  displayName: isDev,
                  fileName: false,
                  ssr: false,
                  minify: !isDev,
                  transpileTemplateLiterals: !isDev,
                  pure: !isDev,
                },
              ],
            ],
          },
        })
       ...more plugins
      ...plugins
     ]
  }
... more options
}

and then we could use like this:

import base from 'config/vite.apps.base';

// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
  const isDev = mode === 'development';

  return base({
    root: __dirname,
    isDev,
    plugins: [
      partytownVite({
        dest: path.resolve(__dirname, 'build', '~partytown'),
      }),
    ],
  });
});

You can pay attention to this tool, which may solve your problem vite-run

biggerstar avatar Jul 29 '23 00:07 biggerstar

Maybe like this image You can use pnpm patch to handle it temporarily.

npmrun avatar Nov 07 '23 16:11 npmrun

I've recently run into same/similar problem when using a pnpm monorepo with some custom/user exports conditions in package.json. I tried to work around this limitation with a DIY solution of my own. I first digged into vite's code itself that allows TS support for config files to check if I can customize it, and then I decided to graduate the DIY workaround to its own npm package called import-single-ts.

At the moment that's how I set up the vite config in the porjects of my monorepo:

  1. I rename my vite.config.ts to vite.config.original.ts
  2. I create a js file called vite.config.js with contents of:
    import { importSingleTs } from 'import-single-ts';
    
    export default (await importSingleTs('./vite.config.original.ts')).default;
    

And that's it. It is inspired by how vite does this internally + how esbuild's node-resolve plugin works .

Pros of import-single-ts for the vite.config.ts usecase:

Some benefits compared to solutions suggested earlier in this thread:

  • No need to use relative imports in a monorepo, just use the normal import path
  • No need to patch vite's dist files
  • No need to include a global transpiling runtime like @babel/register, esbuild-register or jiti.register() which latch on require.extensions, add overhead and seem to be deprecated in node
  • It works for both ESM and CJS packages in my monorepo
  • It is very simple, just a single file (take a look!) using things that vite already has like esbuild.

Cons of import-single-ts for the vite.config.ts usecase:

You have an additional almost empty proxy viter.config.js file but I can live with this for now 🤷 .

Final thoughts

I wonder if vite's team is open to use enhance-resolve since this worked for me pretty much out of the box. There could already be such discussion, I know enhanced-resolve is created by webpack and vite has its own resolution logic and of course I don't know the differences, I just saw that enhanced-resolve worked for me so I'm mentioning it.

In my specific use-case as I mentioned I use custom exports conditions which I define like this in the vite.config.js:

import { importSingleTs } from 'import-single-ts';

export default (
  await importSingleTs('./vite.config.original.ts', {
    // this is the additional piece when compared to the first code example
    conditions: ['antitoxic-dev'], 
  })
).default;

antitoxic avatar Nov 08 '23 08:11 antitoxic