Reputation: 222989
The problem occurs for Vite monorepo, @
aliases are respected by TypeScript because of separate tsconfig files (can be visible in IDE) but aren't distinguished among the workspaces by Vite on build.
The project uses Yarn 1.x with workspaces, TypeScript 4.9, Vite 3.2, Lerna 6.4 (shouldn't affect the problem at this point)
Project structure is common for a monorepo:
packages/
foo-bar/
src/
index.ts
package.json
tsconfig.json
vite.config.ts
yarn.lock
foo-baz/
(same as above)
foo-shared/
src/
qux.ts
quux.ts
package.json
tsconfig.json
yarn.lock
lerna.json
package.json
tsconfig.json
yarn.lock
When one package (foo-bar
) imports a module from another (foo-shared
):
packages/foo-bar/src/index.ts:
import qux from `@foo/shared/qux';
Another package resolves local aliased imports to wrong package on build, because Vite is unaware of tsconfig aliases:
packages/foo-shared/src/qux.ts:
import quux from `@/quux'; // resolves to packages/foo-bar/src/quux.ts and errors
The error is something like:
[vite:load-fallback] Could not load ...\packages\foo-bar\src/quux (imported by ../foo-shared/src/qux.ts): ENOENT: no such file or directory, open '...\packages\foo-bar\src\stores\quux' error during build:
foo-shared
is currently a dummy package which isn't built standalone, only aliased and used on other packages.
packages/foo-bar/vite.config.ts:
// ...
export default defineConfig({
resolve: {
alias: {
'@': path.join(__dirname, './src'),
'@foo/shared': path.join(__dirname, '../foo-shared/src'),
},
},
/ * some irrelevant options */
});
packages/foo-bar/tsconfig.json and packages/foo-shared/tsconfig.json are similar:
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@foo/shared/*": ["../foo-shared/src/*"]
},
"typeRoots": [
"./node_modules/@types",
"../../node_modules/@types",
]
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.vue"
],
"exclude": [
"node_modules"
],
}
I tried to replace resolve.alias
with vite-tsconfig-paths
plugin without success. It didn't affect the aliases at all out of the box, and I cannot be sure it's usable for this case.
How can Vite be configured to resolve paths that begin with "@
" to different paths depending on the path of parent module?
Upvotes: 7
Views: 4276
Reputation: 1181
Instead of using alias
, try using the vite-tsconfig-paths
plugin.
e.g.,
import react from '@vitejs/plugin-react-swc';
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vite';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [tsconfigPaths(), react()],
});
Upvotes: 0
Reputation: 51943
From the Vite docs on the the resolve.alias
option:
Will be passed to
@rollup/plugin-alias
as its entries option. Can either be an object, or an array of{ find, replacement, customResolver }
pairs.
Unfortunately, at the time of this writing, the readme for rollup's resolve-alias plugin is... sparse:
Type:
Function | Object
Default:null
Instructs the plugin to use an alternative resolving algorithm, rather than the Rollup's resolver. Please refer to the Rollup documentation for more information about the
resolveId
hook. For a detailed example, see: Custom Resolvers.
And the "detailed example" of a customResolver
being referred to is not instructive at all if you actually want to know how to write one instead of using another mostly-pre-built one (one is left wondering what a resolveId
hook is, and how it is relevant. For reference, I'm looking at the docs for v4.0.3. Hopefully they'll be better in the future)
Its type declaration file helps fill in the blanks. You can find it here: https://github.com/rollup/plugins/blob/master/packages/alias/types/index.d.ts, where you'll see something like:
import type { Plugin, PluginHooks } from 'rollup'; type MapToFunction<T> = T extends Function ? T : never; export type ResolverFunction = MapToFunction<PluginHooks['resolveId']>; export interface ResolverObject { buildStart?: PluginHooks['buildStart']; resolveId: ResolverFunction; } export interface Alias { find: string | RegExp; replacement: string; customResolver?: ResolverFunction | ResolverObject | null; } export interface RollupAliasOptions { /** blah blah not relevant for vite.js */ customResolver?: /* blah blah not relevant for vite.js */; /** * Specifies an `Object`, or an `Array` of `Object`, * which defines aliases used to replace values in `import` or `require` statements. * With either format, the order of the entries is important, * in that the first defined rules are applied first. */ entries?: readonly Alias[] | { [find: string]: string }; }
In particular, that last part of the doc comment for RollupAliasOptions#entries
is important. I'll wager you can resolve your issue by reordering your resolve.alias
entries in your vite.config.js:
alias: {
'@foo/shared': path.join(__dirname, '../foo-shared/src'), // moved to be first
'@': path.join(__dirname, './src'),
}
Now, if that doesn't work, or you find yourself in the future wanting to do anything where that doesn't suffice, you can write a custom resolver (see how the Alias
type has a customResolver
field?). This should answer your ending question: "How can Vite be configured to resolve paths that begin with "@
" to different paths depending on the path of parent module?"
For that, you can see the linked docs in the rollup/plugin-alias docs: https://rollupjs.org/plugin-development/#resolveid. Here's a bit of relevant excerpt from the docs (in particular, note the importer
parameter):
Type: ResolveIdHook
Kind: async, first Previous: buildStart
if we are resolving an entry point,moduleParsed
if we are resolving an import, or as fallback forresolveDynamicImport
. Additionally, this hook can be triggered during the build phase from plugin hooks by callingthis.emitFile
to emit an entry point or at any time by callingthis.resolve
to manually resolve an idNext: load
if the resolved id has not yet been loaded, otherwisebuildEnd
type ResolveIdHook = ( source: string, importer: string | undefined, options: { assertions: Record<string, string>; custom?: { [plugin: string]: any }; isEntry: boolean; } ) => ResolveIdResult; type ResolveIdResult = string | null | false | PartialResolvedId; interface PartialResolvedId { id: string; external?: boolean | 'absolute' | 'relative'; assertions?: Record<string, string> | null; meta?: { [plugin: string]: any } | null; moduleSideEffects?: boolean | 'no-treeshake' | null; resolvedBy?: string | null; syntheticNamedExports?: boolean | string | null; }
Defines a custom resolver. A resolver can be useful for e.g. locating third-party dependencies. Here
source
is the importee exactly as it is written in the import statement, i.e. forimport { foo } from '../bar.js';
the source will be
"../bar.js"
.The
importer
is the fully resolved id of the importing module. When resolving entry points, importer will usually beundefined
. An exception here are entry points generated viathis.emitFile
as here, you can provide animporter
argument.[...]
Returning
null
defers to otherresolveId
functions and eventually the default resolution behavior. Returningfalse
signals thatsource
should be treated as an external module and not included in the bundle. If this happens for a relative import, the id will be renormalized the same way as when theexternal
option is used.[...]
Upvotes: 3