diff --git a/e2e/docs/.vuepress/config.ts b/e2e/docs/.vuepress/config.ts index 363ef38117..ac9c9c7aca 100644 --- a/e2e/docs/.vuepress/config.ts +++ b/e2e/docs/.vuepress/config.ts @@ -83,4 +83,13 @@ export default defineUserConfig({ }, plugins: [fooPlugin], + + // The alias entries are intentionally ordered by key length to ensure + // that more specific aliases (e.g., '@dir/a.js') take precedence over + // less specific ones (e.g., '@dir'). Do not reorder these entries. + alias: { + '@dir/a.js': path.resolve(__dirname, '../../modules/dir2/a.js'), + '@dir': path.resolve(__dirname, '../../modules/dir1'), + '@dir/b.js': path.resolve(__dirname, '../../modules/dir2/b.js'), + }, }) diff --git a/e2e/docs/hooks/alias/dir.md b/e2e/docs/hooks/alias/dir.md new file mode 100644 index 0000000000..bf68981a75 --- /dev/null +++ b/e2e/docs/hooks/alias/dir.md @@ -0,0 +1,5 @@ +

{{result}}

+ + diff --git a/e2e/docs/hooks/alias/override.md b/e2e/docs/hooks/alias/override.md new file mode 100644 index 0000000000..d39d345b96 --- /dev/null +++ b/e2e/docs/hooks/alias/override.md @@ -0,0 +1,7 @@ +

{{aResult}}

+

{{bResult}}

+ + diff --git a/e2e/modules/dir1/a.js b/e2e/modules/dir1/a.js new file mode 100644 index 0000000000..39449c1d6a --- /dev/null +++ b/e2e/modules/dir1/a.js @@ -0,0 +1 @@ +export const result = 'dir1 > a' diff --git a/e2e/modules/dir1/b.js b/e2e/modules/dir1/b.js new file mode 100644 index 0000000000..3076f25a84 --- /dev/null +++ b/e2e/modules/dir1/b.js @@ -0,0 +1 @@ +export const result = 'dir1 > b' diff --git a/e2e/modules/dir1/c.js b/e2e/modules/dir1/c.js new file mode 100644 index 0000000000..98b8c542a2 --- /dev/null +++ b/e2e/modules/dir1/c.js @@ -0,0 +1 @@ +export const result = 'dir1 > c' diff --git a/e2e/modules/dir2/a.js b/e2e/modules/dir2/a.js new file mode 100644 index 0000000000..85d92dcc32 --- /dev/null +++ b/e2e/modules/dir2/a.js @@ -0,0 +1 @@ +export const result = 'dir2 > a' diff --git a/e2e/modules/dir2/b.js b/e2e/modules/dir2/b.js new file mode 100644 index 0000000000..fa5dae7dba --- /dev/null +++ b/e2e/modules/dir2/b.js @@ -0,0 +1 @@ +export const result = 'dir2 > b' diff --git a/e2e/tests/hooks/alias/dir.spec.ts b/e2e/tests/hooks/alias/dir.spec.ts new file mode 100644 index 0000000000..22cc8a195c --- /dev/null +++ b/e2e/tests/hooks/alias/dir.spec.ts @@ -0,0 +1,6 @@ +import { expect, test } from '@playwright/test' + +test('should apply alias to subpath', async ({ page }) => { + await page.goto('hooks/alias/dir.html') + await expect(page.locator('#result')).toHaveText('dir1 > c') +}) diff --git a/e2e/tests/hooks/alias/override.spec.ts b/e2e/tests/hooks/alias/override.spec.ts new file mode 100644 index 0000000000..1f2494aab2 --- /dev/null +++ b/e2e/tests/hooks/alias/override.spec.ts @@ -0,0 +1,7 @@ +import { expect, test } from '@playwright/test' + +test('longer aliases should override shorter ones', async ({ page }) => { + await page.goto('hooks/alias/override.html') + await expect(page.locator('#a')).toHaveText('dir2 > a') + await expect(page.locator('#b')).toHaveText('dir2 > b') +}) diff --git a/packages/bundler-vite/src/plugins/vuepressConfigPlugin.ts b/packages/bundler-vite/src/plugins/vuepressConfigPlugin.ts index b1187019f0..60c77e5d66 100644 --- a/packages/bundler-vite/src/plugins/vuepressConfigPlugin.ts +++ b/packages/bundler-vite/src/plugins/vuepressConfigPlugin.ts @@ -25,16 +25,17 @@ const resolveAlias = async ({ const aliasResult = await app.pluginApi.hooks.alias.process(app, isServer) aliasResult.forEach((aliasObject) => { - Object.entries(aliasObject).forEach(([key, value]) => { - alias[key] = value as string - }) + Object.assign(alias, aliasObject) }) return [ - ...Object.keys(alias).map((item) => ({ - find: item, - replacement: alias[item], - })), + ...Object.keys(alias) + // sort alias by length in descending order to ensure longer alias is handled first + .sort((a, b) => b.length - a.length) + .map((item) => ({ + find: item, + replacement: alias[item], + })), ...(isServer ? [] : [ diff --git a/packages/bundler-webpack/src/config/handleResolve.ts b/packages/bundler-webpack/src/config/handleResolve.ts index b024210ec3..6c1c7a9e00 100644 --- a/packages/bundler-webpack/src/config/handleResolve.ts +++ b/packages/bundler-webpack/src/config/handleResolve.ts @@ -14,17 +14,27 @@ export const handleResolve = async ({ isServer: boolean }): Promise => { // aliases - config.resolve.alias - .set('@source', app.dir.source()) - .set('@temp', app.dir.temp()) - .set('@internal', app.dir.temp('internal')) + const alias = { + '@source': app.dir.source(), + '@temp': app.dir.temp(), + '@internal': app.dir.temp('internal'), + } - // extensionAlias - config.resolve.extensionAlias.merge({ - '.js': ['.js', '.ts'], - '.mjs': ['.mjs', '.mts'], + // plugin hook: alias + const aliasResult = await app.pluginApi.hooks.alias.process(app, isServer) + + aliasResult.forEach((aliasObject) => { + Object.assign(alias, aliasObject) }) + // set aliases + config.resolve.alias.merge( + Object.fromEntries( + // sort alias by length in descending order to ensure longer alias is handled first + Object.entries(alias).sort(([a], [b]) => b.length - a.length), + ), + ) + // extensions config.resolve.extensions.merge([ '.js', @@ -35,13 +45,9 @@ export const handleResolve = async ({ '.json', ]) - // plugin hook: alias - const aliasResult = await app.pluginApi.hooks.alias.process(app, isServer) - - // set aliases - aliasResult.forEach((aliasObject) => { - Object.entries(aliasObject).forEach(([key, value]) => { - config.resolve.alias.set(key, value as string) - }) + // extensionAlias + config.resolve.extensionAlias.merge({ + '.js': ['.js', '.ts'], + '.mjs': ['.mjs', '.mts'], }) } diff --git a/packages/core/src/pluginApi/createPluginApiRegisterHooks.ts b/packages/core/src/pluginApi/createPluginApiRegisterHooks.ts index 22c94c4a01..333e40174f 100644 --- a/packages/core/src/pluginApi/createPluginApiRegisterHooks.ts +++ b/packages/core/src/pluginApi/createPluginApiRegisterHooks.ts @@ -1,4 +1,4 @@ -import type { PluginApi } from '../types/index.js' +import type { AliasHook, DefineHook, PluginApi } from '../types/index.js' import { normalizeAliasDefineHook } from './normalizeAliasDefineHook.js' import { normalizeClientConfigFileHook } from './normalizeClientConfigFileHook.js' @@ -30,14 +30,14 @@ export const createPluginApiRegisterHooks = if (alias) { hooks.alias.add({ pluginName, - hook: normalizeAliasDefineHook(alias), + hook: normalizeAliasDefineHook(alias), }) } if (define) { hooks.define.add({ pluginName, - hook: normalizeAliasDefineHook(define), + hook: normalizeAliasDefineHook(define), }) } diff --git a/packages/core/src/pluginApi/normalizeAliasDefineHook.ts b/packages/core/src/pluginApi/normalizeAliasDefineHook.ts index 1b12ac361b..da8098fbea 100644 --- a/packages/core/src/pluginApi/normalizeAliasDefineHook.ts +++ b/packages/core/src/pluginApi/normalizeAliasDefineHook.ts @@ -1,5 +1,5 @@ import { isFunction } from '@vuepress/shared' -import type { AliasDefineHook } from '../types/index.js' +import type { AliasHook, DefineHook } from '../types/index.js' /** * Normalize alias and define hook @@ -7,6 +7,6 @@ import type { AliasDefineHook } from '../types/index.js' * @internal */ export const normalizeAliasDefineHook = - (hook: AliasDefineHook['exposed']): AliasDefineHook['normalized'] => + (hook: T['exposed']): T['normalized'] => async (app, isServer) => isFunction(hook) ? hook(app, isServer) : hook diff --git a/packages/core/src/types/pluginApi/hooks.ts b/packages/core/src/types/pluginApi/hooks.ts index cab24a8a13..29a9244edd 100644 --- a/packages/core/src/types/pluginApi/hooks.ts +++ b/packages/core/src/types/pluginApi/hooks.ts @@ -41,8 +41,15 @@ export type ClientConfigFileHook = Hook< (app: App) => Promise > -// alias and define hook -export type AliasDefineHook = Hook< +// alias hook +export type AliasHook = Hook< + | Record + | ((app: App, isServer: boolean) => PromiseOrNot>), + (app: App, isServer: boolean) => Promise> +> + +// define hook +export type DefineHook = Hook< | Record | ((app: App, isServer: boolean) => PromiseOrNot>), (app: App, isServer: boolean) => Promise> @@ -62,8 +69,8 @@ export interface Hooks { extendsPage: ExtendsHook extendsBundlerOptions: ExtendsHook clientConfigFile: ClientConfigFileHook - alias: AliasDefineHook - define: AliasDefineHook + alias: AliasHook + define: DefineHook } /** diff --git a/packages/core/tests/pluginApi/normalizeAliasDefineHook.spec.ts b/packages/core/tests/pluginApi/normalizeAliasDefineHook.spec.ts index 137894ab04..3c54aac7d2 100644 --- a/packages/core/tests/pluginApi/normalizeAliasDefineHook.spec.ts +++ b/packages/core/tests/pluginApi/normalizeAliasDefineHook.spec.ts @@ -1,6 +1,6 @@ import { path } from '@vuepress/utils' import { expect, it, vi } from 'vitest' -import type { AliasDefineHook, Bundler } from '../../src/index.js' +import type { AliasHook, Bundler, DefineHook } from '../../src/index.js' import { createBaseApp, normalizeAliasDefineHook } from '../../src/index.js' const app = createBaseApp({ @@ -9,13 +9,19 @@ const app = createBaseApp({ bundler: {} as Bundler, }) +it('should wrap object with a function', async () => { + const rawHook: AliasHook['exposed'] = { + foo: 'bar', + } + const normalizedHook = normalizeAliasDefineHook(rawHook) + expect(await normalizedHook(app, true)).toEqual({ foo: 'bar' }) +}) + it('should keep function as is', async () => { - const rawHook: AliasDefineHook['exposed'] = vi.fn( - (_app, isServer: boolean) => ({ - foo: 'bar', - isServer, - }), - ) + const rawHook: DefineHook['exposed'] = vi.fn((_app, isServer: boolean) => ({ + foo: 'bar', + isServer, + })) const normalizedHook = normalizeAliasDefineHook(rawHook) expect(await normalizedHook(app, true)).toEqual({ foo: 'bar', @@ -24,11 +30,3 @@ it('should keep function as is', async () => { expect(rawHook).toHaveBeenCalledTimes(1) expect(rawHook).toHaveBeenCalledWith(app, true) }) - -it('should wrap object with a function', async () => { - const rawHook: AliasDefineHook['exposed'] = { - foo: 'bar', - } - const normalizedHook = normalizeAliasDefineHook(rawHook) - expect(await normalizedHook(app, true)).toEqual({ foo: 'bar' }) -})