在 Vite monorepo 中的模糊别名

huangapple go评论69阅读模式
英文:

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.jsonpackages/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&#39;;

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&#39;; // 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: {
        &#39;@&#39;: path.join(__dirname, &#39;./src&#39;),
        &#39;@foo/shared&#39;: path.join(__dirname, &#39;../foo-shared/src&#39;),
      },
    },
    / * some irrelevant options */
  });

packages/foo-bar/tsconfig.json and packages/foo-shared/tsconfig.json are similar:

{
  &quot;extends&quot;: &quot;@vue/tsconfig/tsconfig.web.json&quot;,
  &quot;compilerOptions&quot;: {
    &quot;baseUrl&quot;: &quot;.&quot;,
    &quot;paths&quot;: {
      &quot;@/*&quot;: [&quot;./src/*&quot;],
      &quot;@foo/shared/*&quot;: [&quot;../foo-shared/src/*&quot;]
    },
    &quot;typeRoots&quot;: [
      &quot;./node_modules/@types&quot;,
      &quot;../../node_modules/@types&quot;,
    ]
  },
  &quot;include&quot;: [
     &quot;src/**/*.ts&quot;,
     &quot;src/**/*.d.ts&quot;,
     &quot;src/**/*.vue&quot;
   ],
  &quot;exclude&quot;: [
    &quot;node_modules&quot;
  ],
}

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

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.

[...]

英文:

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
&gt; import type { Plugin, PluginHooks } from &#39;rollup&#39;;
&gt;
&gt; type MapToFunction&lt;T&gt; = T extends Function ? T : never;
&gt;
&gt; export type ResolverFunction = MapToFunction&lt;PluginHooks[&#39;resolveId&#39;]&gt;;
&gt;
&gt; export interface ResolverObject {
&gt; buildStart?: PluginHooks[&#39;buildStart&#39;];
&gt; resolveId: ResolverFunction;
&gt; }
&gt;
&gt; export interface Alias {
&gt; find: string | RegExp;
&gt; replacement: string;
&gt; customResolver?: ResolverFunction | ResolverObject | null;
&gt; }
&gt;
&gt; export interface RollupAliasOptions {
&gt; /** blah blah not relevant for vite.js */
&gt; customResolver?: /* blah blah not relevant for vite.js */;
&gt;
&gt; /**
&gt; * Specifies an `Object`, or an `Array` of `Object`,
&gt; * which defines aliases used to replace values in `import` or `require` statements.
&gt; * With either format, the order of the entries is important,
&gt; * in that the first defined rules are applied first.
&gt; */
&gt; entries?: readonly Alias[] | { [find: string]: string };
&gt; }
&gt;

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: {
  &#39;@foo/shared&#39;: path.join(__dirname, &#39;../foo-shared/src&#39;), // moved to be first
  &#39;@&#39;: path.join(__dirname, &#39;./src&#39;),
}

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
&gt; type ResolveIdHook = (
&gt; source: string,
&gt; importer: string | undefined,
&gt; options: {
&gt; assertions: Record&lt;string, string&gt;;
&gt; custom?: { [plugin: string]: any };
&gt; isEntry: boolean;
&gt; }
&gt; ) =&gt; ResolveIdResult;
&gt;
&gt; type ResolveIdResult = string | null | false | PartialResolvedId;
&gt;
&gt; interface PartialResolvedId {
&gt; id: string;
&gt; external?: boolean | &#39;absolute&#39; | &#39;relative&#39;;
&gt; assertions?: Record&lt;string, string&gt; | null;
&gt; meta?: { [plugin: string]: any } | null;
&gt; moduleSideEffects?: boolean | &#39;no-treeshake&#39; | null;
&gt; resolvedBy?: string | null;
&gt; syntheticNamedExports?: boolean | string | null;
&gt; }
&gt;

>
> 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
&gt; import { foo } from &#39;../bar.js&#39;;
&gt;

>
> the source will be &quot;../bar.js&quot;.
>
> 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.
>
> [...]

huangapple
  • 本文由 发表于 2023年2月10日 03:49:38
  • 转载请务必保留本文链接:https://go.coder-hub.com/75403735.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定