英文:
Ambiguous aliases in Vite monorepo
问题
问题出现在Vite的monorepo项目中,由于各自的tsconfig文件(在IDE中可见),TypeScript能够正确解析@
别名,但在Vite构建过程中,这些别名在工作区之间无法区分。
该项目使用Yarn 1.x与工作区,TypeScript 4.9,Vite 3.2,Lerna 6.4(此时不应影响问题)。
项目结构对于monorepo项目是常见的:
packages/
foo-bar/
src/
index.ts
package.json
tsconfig.json
vite.config.ts
yarn.lock
foo-baz/
(同上)
foo-shared/
src/
qux.ts
quux.ts
package.json
tsconfig.json
yarn.lock
lerna.json
package.json
tsconfig.json
yarn.lock
当一个包(foo-bar
)从另一个包(foo-shared
)导入模块时:
packages/foo-bar/src/index.ts
:
import qux from `@foo/shared/qux`;
另一个包在构建时解析本地别名导入到错误的包,因为Vite不了解tsconfig别名:
packages/foo-shared/src/qux.ts
:
import quux from `@/quux`; // 解析为packages/foo-bar/src/quux.ts并出现错误
错误消息类似于:
[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
目前是一个虚拟包,不会独立构建,只是别名,并在其他包中使用。
packages/foo-bar/vite.config.ts
:
// ...
export default defineConfig({
resolve: {
alias: {
'@': path.join(__dirname, './src'),
'@foo/shared': path.join(__dirname, '../foo-shared/src'),
},
},
/* 一些无关紧要的选项 */
});
packages/foo-bar/tsconfig.json
和 packages/foo-shared/tsconfig.json
类似:
{
"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"
],
}
我尝试使用vite-tsconfig-paths
插件替换resolve.alias
,但没有成功。它默认情况下对别名没有影响,而且我不能确定它是否适用于这种情况。
如何配置Vite以根据父模块的路径解析以“@”开头的路径?
英文:
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:
<!-- language: lang-none -->
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?
答案1
得分: 1
以下是您要翻译的内容:
From the Vite docs on 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
<br>
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 theresolveId
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.[...]
英文:
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
<br>
> 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:
> ts
> 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 for resolveDynamicImport
. Additionally, this hook can be triggered during the build phase from plugin hooks by calling this.emitFile
to emit an entry point or at any time by calling this.resolve
to manually resolve an id |
> | Next: | load
if the resolved id has not yet been loaded, otherwise buildEnd
|
>
> typescript
> 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. for
>
> js
> import { 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 be undefined
. An exception here are entry points generated via this.emitFile
as here, you can provide an importer
argument.
>
> [...]
>
> Returning null
defers to other resolveId
functions and eventually the default resolution behavior. Returning false
signals that source
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 the external
option is used.
>
> [...]
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论