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' })
-})