From 62d0582beaa83785137b3e5aef3d526e86b29ea9 Mon Sep 17 00:00:00 2001 From: "Mr.Hope" Date: Sat, 3 Feb 2024 00:49:24 +0800 Subject: [PATCH] feat(plugin-redirect): add redirect plugin --- plugins/plugin-redirect/package.json | 57 ++++++ plugins/plugin-redirect/src/cli/index.ts | 132 ++++++++++++++ .../src/client/components/LanguageSwitch.ts | 167 ++++++++++++++++++ .../src/client/composables/index.ts | 1 + .../src/client/composables/redirect.ts | 112 ++++++++++++ plugins/plugin-redirect/src/client/config.ts | 14 ++ plugins/plugin-redirect/src/client/define.ts | 14 ++ .../src/client/styles/language-switch.scss | 67 +++++++ .../plugin-redirect/src/client/utils/index.ts | 1 + .../plugin-redirect/src/client/utils/path.ts | 4 + plugins/plugin-redirect/src/node/constant.ts | 10 ++ plugins/plugin-redirect/src/node/generate.ts | 82 +++++++++ plugins/plugin-redirect/src/node/homepage.ts | 88 +++++++++ plugins/plugin-redirect/src/node/index.ts | 3 + plugins/plugin-redirect/src/node/locale.ts | 64 +++++++ plugins/plugin-redirect/src/node/locales.ts | 151 ++++++++++++++++ plugins/plugin-redirect/src/node/options.ts | 44 +++++ plugins/plugin-redirect/src/node/plugin.ts | 61 +++++++ plugins/plugin-redirect/src/node/prepare.ts | 15 ++ .../src/node/typings/frontmatter.ts | 4 + .../plugin-redirect/src/node/typings/index.ts | 1 + .../plugin-redirect/src/node/utils/index.ts | 4 + .../plugin-redirect/src/node/utils/logger.ts | 4 + .../src/node/utils/normalizePath.ts | 2 + .../src/node/utils/outputFile.ts | 117 ++++++++++++ .../src/node/utils/redirect.ts | 85 +++++++++ plugins/plugin-redirect/src/shared/index.ts | 2 + plugins/plugin-redirect/src/shared/locale.ts | 59 +++++++ plugins/plugin-redirect/src/shared/locales.ts | 29 +++ .../src/shims-redirect-config.d.ts | 3 + .../tests/normalizePath.spec.ts | 62 +++++++ plugins/plugin-redirect/tsconfig.build.json | 10 ++ pnpm-lock.yaml | 21 +++ tsconfig.build.json | 1 + 34 files changed, 1491 insertions(+) create mode 100644 plugins/plugin-redirect/package.json create mode 100644 plugins/plugin-redirect/src/cli/index.ts create mode 100644 plugins/plugin-redirect/src/client/components/LanguageSwitch.ts create mode 100644 plugins/plugin-redirect/src/client/composables/index.ts create mode 100644 plugins/plugin-redirect/src/client/composables/redirect.ts create mode 100644 plugins/plugin-redirect/src/client/config.ts create mode 100644 plugins/plugin-redirect/src/client/define.ts create mode 100644 plugins/plugin-redirect/src/client/styles/language-switch.scss create mode 100644 plugins/plugin-redirect/src/client/utils/index.ts create mode 100644 plugins/plugin-redirect/src/client/utils/path.ts create mode 100644 plugins/plugin-redirect/src/node/constant.ts create mode 100644 plugins/plugin-redirect/src/node/generate.ts create mode 100644 plugins/plugin-redirect/src/node/homepage.ts create mode 100644 plugins/plugin-redirect/src/node/index.ts create mode 100644 plugins/plugin-redirect/src/node/locale.ts create mode 100644 plugins/plugin-redirect/src/node/locales.ts create mode 100644 plugins/plugin-redirect/src/node/options.ts create mode 100644 plugins/plugin-redirect/src/node/plugin.ts create mode 100644 plugins/plugin-redirect/src/node/prepare.ts create mode 100644 plugins/plugin-redirect/src/node/typings/frontmatter.ts create mode 100644 plugins/plugin-redirect/src/node/typings/index.ts create mode 100644 plugins/plugin-redirect/src/node/utils/index.ts create mode 100644 plugins/plugin-redirect/src/node/utils/logger.ts create mode 100644 plugins/plugin-redirect/src/node/utils/normalizePath.ts create mode 100644 plugins/plugin-redirect/src/node/utils/outputFile.ts create mode 100644 plugins/plugin-redirect/src/node/utils/redirect.ts create mode 100644 plugins/plugin-redirect/src/shared/index.ts create mode 100644 plugins/plugin-redirect/src/shared/locale.ts create mode 100644 plugins/plugin-redirect/src/shared/locales.ts create mode 100644 plugins/plugin-redirect/src/shims-redirect-config.d.ts create mode 100644 plugins/plugin-redirect/tests/normalizePath.spec.ts create mode 100644 plugins/plugin-redirect/tsconfig.build.json diff --git a/plugins/plugin-redirect/package.json b/plugins/plugin-redirect/package.json new file mode 100644 index 0000000000..9460a886cb --- /dev/null +++ b/plugins/plugin-redirect/package.json @@ -0,0 +1,57 @@ +{ + "name": "@vuepress/plugin-redirect", + "version": "2.0.0-rc.0", + "description": "VuePress plugin - redirect", + "keywords": [ + "vuepress-plugin", + "vuepress", + "plugin", + "redirect" + ], + "homepage": "https://ecosystem.vuejs.press/plugins/redirect.html", + "bugs": { + "url": "https://github.com/vuepress/ecosystem/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/vuepress/ecosystem.git", + "directory": "plugins/plugin-redirect" + }, + "license": "MIT", + "author": { + "name": "Mr.Hope", + "email": "mister-hope@outlook.com", + "url": "https://mister-hope.com" + }, + "type": "module", + "exports": { + ".": "./lib/node/index.js", + "./package.json": "./package.json" + }, + "main": "./lib/node/index.js", + "types": "./lib/node/index.d.ts", + "bin": { + "vp-redirect": "./lib/cli/index.js" + }, + "files": [ + "lib" + ], + "scripts": { + "build": "tsc -b tsconfig.build.json", + "clean": "rimraf --glob ./lib ./*.tsbuildinfo", + "style": "sass src:lib --style=compressed --no-source-map" + }, + "dependencies": { + "@vuepress/helper": "workspace:*", + "@vueuse/core": "^10.7.2", + "cac": "^6.7.14", + "vue": "^3.4.15", + "vue-router": "^4.2.5" + }, + "peerDependencies": { + "vuepress": "2.0.0-rc.2" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/plugins/plugin-redirect/src/cli/index.ts b/plugins/plugin-redirect/src/cli/index.ts new file mode 100644 index 0000000000..dccbef48d4 --- /dev/null +++ b/plugins/plugin-redirect/src/cli/index.ts @@ -0,0 +1,132 @@ +#!/usr/bin/env node +import { createRequire } from 'node:module' +import { removeEndingSlash, removeLeadingSlash } from '@vuepress/helper' +import { cac } from 'cac' +import { + loadUserConfig, + resolveAppConfig, + resolveCliAppConfig, + resolveUserConfigConventionalPath, + transformUserConfigToPlugin, +} from 'vuepress/cli' +import { createBuildApp } from 'vuepress/core' +import { fs, logger, path } from 'vuepress/utils' +import { getRedirectHTML } from '../node/utils/index.js' + +interface RedirectCommandOptions { + hostname: string + output?: string + config?: string + cache: string + temp?: string + cleanCache?: boolean + cleanTemp?: boolean +} + +const require = createRequire(import.meta.url) + +const cli = cac('vp-redirect') +const { version } = require('@vuepress/plugin-redirect/package.json') as { + version: string +} + +cli + .command( + 'generate [source-dir]', + 'Generate redirect site using VuePress project under source folder', + ) + .option( + '--hostname ', + 'Hostname to redirect to (E.g.: https://new.example.com/)', + { default: '/' }, + ) + .option('-c, --config ', 'Set path to config file') + .option( + '-o, --output ', + 'Set the output directory (default: .vuepress/redirect)', + ) + .option('--cache ', 'Set the directory of the cache files') + .option('-t, --temp ', 'Set the directory of the temporary files') + .option('--clean-cache', 'Clean the cache files before generation') + .option('--clean-temp', 'Clean the temporary files before generation') + .action(async (sourceDir: string, commandOptions: RedirectCommandOptions) => { + if (!sourceDir) return cli.outputHelp() + + // ensure NODE_ENV is set + process.env.NODE_ENV ??= 'production' + + // resolve app config from cli options + const cliAppConfig = resolveCliAppConfig(sourceDir, {}) + + // resolve user config file + const userConfigPath = resolveUserConfigConventionalPath( + cliAppConfig.source, + ) + + const { userConfig } = await loadUserConfig(userConfigPath) + + // resolve the final app config to use + const appConfig = resolveAppConfig({ + defaultAppConfig: {}, + cliAppConfig, + userConfig, + }) + + if (appConfig === null) return + + // create vuepress app + const app = createBuildApp(appConfig) + + // use user-config plugin + app.use(transformUserConfigToPlugin(userConfig, cliAppConfig.source)) + + // clean temp and cache + if (commandOptions.cleanTemp === true) { + logger.info('Cleaning temp...') + await fs.remove(app.dir.temp()) + } + if (commandOptions.cleanCache === true) { + logger.info('Cleaning cache...') + await fs.remove(app.dir.cache()) + } + + const outputFolder = commandOptions.output + ? path.join(process.cwd(), commandOptions.output) + : path.join(app.dir.source(), '.vuepress', 'redirect') + + // empty output directory + await fs.emptyDir(outputFolder) + + // initialize vuepress app to get pages + logger.info('Initializing VuePress and preparing data...') + + await app.init() + + // redirect all pages + + // initialize vuepress app to get pages + logger.info('Generating redirect pages...') + + await Promise.all( + app.pages.map((page) => { + const redirectUrl = `${removeEndingSlash(commandOptions.hostname)}${ + app.options.base + }${removeLeadingSlash(page.path)}` + const destLocation = path.join( + outputFolder, + removeLeadingSlash(page.path.replace(/\/$/, '/index.html')), + ) + + return fs + .ensureDir(path.dirname(destLocation)) + .then(() => fs.writeFile(destLocation, getRedirectHTML(redirectUrl))) + }), + ) + }) + +cli.command('').action(() => cli.outputHelp()) + +cli.help() +cli.version(version) + +cli.parse() diff --git a/plugins/plugin-redirect/src/client/components/LanguageSwitch.ts b/plugins/plugin-redirect/src/client/components/LanguageSwitch.ts new file mode 100644 index 0000000000..d0350abb11 --- /dev/null +++ b/plugins/plugin-redirect/src/client/components/LanguageSwitch.ts @@ -0,0 +1,167 @@ +import { + usePreferredLanguages, + useScrollLock, + useSessionStorage, +} from '@vueuse/core' +import type { VNode } from 'vue' +import { + computed, + defineComponent, + h, + onMounted, + onUnmounted, + ref, + TransitionGroup, + watch, +} from 'vue' +import { useRoute, useRouter } from 'vue-router' +import { useRouteLocale } from 'vuepress/client' +import { + redirectLocaleConfig, + redirectLocaleEntries, + redirectLocales, +} from '../define.js' + +import '../styles/language-switch.scss' + +const { switchLocale } = redirectLocaleConfig + +interface LocaleInfo { + lang: string + localePath: string +} + +const REDIRECT_LOCALE_STORAGE = useSessionStorage>( + 'VUEPRESS_REDIRECT_LOCALES', + {}, +) + +export default defineComponent({ + name: 'LanguageSwitch', + + setup() { + const languages = usePreferredLanguages() + const route = useRoute() + const router = useRouter() + const routeLocale = useRouteLocale() + + const showModal = ref(false) + + const info = computed(() => { + if (redirectLocaleEntries.some(([key]) => routeLocale.value === key)) + for (const language of languages.value) + for (const [localePath, langs] of redirectLocaleEntries) + if (langs.includes(language)) { + if (localePath === routeLocale.value) return null + + return { + lang: language, + localePath, + } + } + + return null + }) + + const locale = computed(() => { + if (info.value) { + const { lang, localePath } = info.value + const locales = [ + redirectLocales[routeLocale.value], + redirectLocales[localePath], + ] + + return { + hint: locales.map(({ hint }) => hint.replace('$1', lang)), + switch: locales + .map(({ switch: switchText }) => switchText.replace('$1', lang)) + .join(' / '), + cancel: locales.map(({ cancel }) => cancel).join(' / '), + } + } + + return null + }) + + const targetRoute = computed(() => + info.value + ? route.path.replace(routeLocale.value, info.value.localePath) + : null, + ) + + const updateStatus = (): void => { + REDIRECT_LOCALE_STORAGE.value[routeLocale.value] = true + showModal.value = false + } + + onMounted(() => { + const isLocked = useScrollLock(document.body) + + if (!REDIRECT_LOCALE_STORAGE.value[routeLocale.value]) + if (info.value) + if (switchLocale === 'direct') router.replace(targetRoute.value!) + else if (switchLocale === 'modal') showModal.value = true + else showModal.value = false + else showModal.value = false + + watch( + showModal, + (value) => { + isLocked.value = value + }, + { immediate: true }, + ) + + onUnmounted(() => { + isLocked.value = false + }) + }) + + return (): VNode | null => + showModal.value + ? h(TransitionGroup, { name: 'lang-modal-fade' }, () => + showModal.value + ? h( + 'div', + { key: 'mask', class: 'lang-modal-mask' }, + h( + 'div', + { + key: 'popup', + class: 'lang-modal-wrapper', + }, + [ + h( + 'div', + { class: 'lang-modal-content' }, + locale.value?.hint.map((text) => h('p', text)), + ), + h( + 'button', + { + type: 'button', + class: 'lang-modal-action primary', + onClick: () => { + updateStatus() + router.replace(targetRoute.value!) + }, + }, + locale.value?.switch, + ), + h( + 'button', + { + type: 'button', + class: 'lang-modal-action', + onClick: () => updateStatus(), + }, + locale.value?.cancel, + ), + ], + ), + ) + : null, + ) + : null + }, +}) diff --git a/plugins/plugin-redirect/src/client/composables/index.ts b/plugins/plugin-redirect/src/client/composables/index.ts new file mode 100644 index 0000000000..74fa819393 --- /dev/null +++ b/plugins/plugin-redirect/src/client/composables/index.ts @@ -0,0 +1 @@ +export * from './redirect.js' diff --git a/plugins/plugin-redirect/src/client/composables/redirect.ts b/plugins/plugin-redirect/src/client/composables/redirect.ts new file mode 100644 index 0000000000..f566017f4b --- /dev/null +++ b/plugins/plugin-redirect/src/client/composables/redirect.ts @@ -0,0 +1,112 @@ +import { redirectConfig } from '@temp/redirect/config.js' +import { entries, isLinkHttp } from '@vuepress/helper/client' +import { usePreferredLanguages } from '@vueuse/core' +import { computed, watch } from 'vue' +import { useRoute, useRouter } from 'vue-router' +import { useRouteLocale } from 'vuepress/client' +import { redirectLocaleConfig, redirectLocaleEntries } from '../define.js' +import { normalizePath } from '../utils/index.js' + +const { + autoLocale, + defaultBehavior, + defaultLocale: defaultLocalePath, + localeFallback, +} = redirectLocaleConfig + +/** + * @description devServer only function to handle redirects + */ +export const setupRedirect = (): void => { + const languages = usePreferredLanguages() + const route = useRoute() + const router = useRouter() + const routeLocale = useRouteLocale() + + const isRootLocale = computed(() => routeLocale.value === '/') + + const handleLocaleRedirect = (): void => { + const routes = router.getRoutes() + const defaultLocale = + defaultLocalePath && + routes.some( + ({ path }) => path === route.path.replace('/', defaultLocalePath), + ) + ? defaultLocalePath + : routes.find( + ({ path }) => + route.path.split('/').length >= 3 && + path === route.path.replace(/^\/[^/]+\//, '/'), + )?.path + + let matchedLocalePath: string | null = null + + // get matched locale + // eslint-disable-next-line no-labels + findLanguage: for (const lang of languages.value) + for (const [localePath, langs] of redirectLocaleEntries) + if (langs.includes(lang)) { + if ( + localeFallback && + routes.every(({ path }) => path !== route.path.replace('/', path)) + ) + continue + + matchedLocalePath = localePath + // eslint-disable-next-line no-labels + break findLanguage + } + + // default link + const defaultRoute = defaultLocale + ? route.fullPath.replace('/', defaultLocale) + : null + + // a locale matches + if (matchedLocalePath) { + const hasLocalePage = routes.some( + ({ path }) => route.path.replace('/', matchedLocalePath!) === path, + ) + const localeRoute = route.fullPath.replace('/', matchedLocalePath) + + const routePath = + // the locale page exists + hasLocalePage + ? localeRoute + : // the page does not exist + defaultBehavior === 'homepage' + ? // locale homepage + matchedLocalePath + : defaultBehavior === 'defaultLocale' && defaultRoute + ? // default locale page + defaultRoute + : // as is to get a 404 page of that locale + localeRoute + + router.replace(routePath) + } + // we have a default page + else if (defaultRoute) { + router.replace(defaultRoute) + } else if (route.path !== '/404.html') { + router.replace('/404.html') + } + } + + watch( + () => route.path, + (path) => { + // handle redirects + for (const [from, to] of entries(redirectConfig)) + if (normalizePath(path.toLowerCase()) === from.toLowerCase()) { + if (isLinkHttp(to)) window.open(to) + else router.replace(to) + + return + } + + if (autoLocale && isRootLocale.value) handleLocaleRedirect() + }, + { immediate: true }, + ) +} diff --git a/plugins/plugin-redirect/src/client/config.ts b/plugins/plugin-redirect/src/client/config.ts new file mode 100644 index 0000000000..c7eb4deba9 --- /dev/null +++ b/plugins/plugin-redirect/src/client/config.ts @@ -0,0 +1,14 @@ +import type { ClientConfig } from 'vuepress/client' +import { defineClientConfig } from 'vuepress/client' +import LanguageSwitch from './components/LanguageSwitch.js' +import { setupRedirect } from './composables/redirect.js' +import { enableLocaleSwitch } from './define.js' + +declare const __VUEPRESS_DEV__: boolean + +export default defineClientConfig({ + setup() { + if (__VUEPRESS_DEV__) setupRedirect() + }, + rootComponents: enableLocaleSwitch ? [LanguageSwitch] : [], +}) as ClientConfig diff --git a/plugins/plugin-redirect/src/client/define.ts b/plugins/plugin-redirect/src/client/define.ts new file mode 100644 index 0000000000..f518b4b60c --- /dev/null +++ b/plugins/plugin-redirect/src/client/define.ts @@ -0,0 +1,14 @@ +import { entries } from '@vuepress/helper/client' +import type { + LocaleRedirectConfig, + RedirectLocaleConfig, +} from '../shared/index.js' + +declare const REDIRECT_LOCALE_CONFIG: LocaleRedirectConfig +declare const REDIRECT_LOCALES: RedirectLocaleConfig +declare const REDIRECT_LOCALE_SWITCH: boolean + +export const redirectLocaleConfig = REDIRECT_LOCALE_CONFIG +export const redirectLocaleEntries = entries(redirectLocaleConfig.localeConfig) +export const redirectLocales = REDIRECT_LOCALES +export const enableLocaleSwitch = REDIRECT_LOCALE_SWITCH diff --git a/plugins/plugin-redirect/src/client/styles/language-switch.scss b/plugins/plugin-redirect/src/client/styles/language-switch.scss new file mode 100644 index 0000000000..b2fa08449e --- /dev/null +++ b/plugins/plugin-redirect/src/client/styles/language-switch.scss @@ -0,0 +1,67 @@ +.lang-modal-fade-enter-active, +.lang-modal-fade-leave-active { + transition: opacity 0.5s; +} + +.lang-modal-fade-enter, +.lang-modal-fade-leave-to { + opacity: 0; +} + +.lang-modal-mask { + position: fixed; + inset: 0; + z-index: 1499; + + display: flex; + align-items: center; + justify-content: center; + + backdrop-filter: blur(10px); + + @media print { + display: none; + } +} + +.lang-modal-wrapper { + position: relative; + z-index: 1500; + + overflow: hidden; + + max-width: 80vw; + padding: 1rem 2rem; + border-radius: 8px; + + background: var(--vp-bg); + box-shadow: 0 2px 6px 0 var(--card-shadow); +} + +.lang-modal-action { + display: block; + + width: 100%; + margin: 1rem 0; + padding: 0.5rem 0.75rem; + border: none; + border-radius: 8px; + + background-color: var(--vp-bglt); + color: var(--vp-c); + + cursor: pointer; + + &:hover { + background-color: var(--vp-bgl); + } + + &.primary { + background-color: var(--vp-tc); + color: var(--white); + + &:hover { + background-color: var(--vp-tcl); + } + } +} diff --git a/plugins/plugin-redirect/src/client/utils/index.ts b/plugins/plugin-redirect/src/client/utils/index.ts new file mode 100644 index 0000000000..a534b0b765 --- /dev/null +++ b/plugins/plugin-redirect/src/client/utils/index.ts @@ -0,0 +1 @@ +export * from './path.js' diff --git a/plugins/plugin-redirect/src/client/utils/path.ts b/plugins/plugin-redirect/src/client/utils/path.ts new file mode 100644 index 0000000000..b8b67d1f73 --- /dev/null +++ b/plugins/plugin-redirect/src/client/utils/path.ts @@ -0,0 +1,4 @@ +export const normalizePath = (url: string): string => + url + .replace(/\/(?:README\.md)$/i, '/index.html') + .replace(/(?:\.(?:html|md))?$/, '.html') diff --git a/plugins/plugin-redirect/src/node/constant.ts b/plugins/plugin-redirect/src/node/constant.ts new file mode 100644 index 0000000000..5e3e435564 --- /dev/null +++ b/plugins/plugin-redirect/src/node/constant.ts @@ -0,0 +1,10 @@ +import { ensureEndingSlash } from '@vuepress/helper' +import { getDirname, path } from 'vuepress/utils' + +const __dirname = getDirname(import.meta.url) + +export const PLUGIN_NAME = '@vuepress/plugin-redirect' + +export const CLIENT_FOLDER = ensureEndingSlash( + path.resolve(__dirname, '../client'), +) diff --git a/plugins/plugin-redirect/src/node/generate.ts b/plugins/plugin-redirect/src/node/generate.ts new file mode 100644 index 0000000000..5e7943fbe6 --- /dev/null +++ b/plugins/plugin-redirect/src/node/generate.ts @@ -0,0 +1,82 @@ +import { + entries, + isLinkAbsolute, + isLinkHttp, + removeEndingSlash, + removeLeadingSlash, +} from '@vuepress/helper' +import type { App } from 'vuepress/core' +import { fs, path, withSpinner } from 'vuepress/utils' +import type { LocaleRedirectConfig } from '../shared/index.js' +import { getLocaleRedirectHTML, getRedirectHTML } from './utils/index.js' + +export const generateAutoLocaleRedirects = async ( + { dir, options, pages }: App, + localeOptions: LocaleRedirectConfig, +): Promise => { + const rootPaths = pages + .filter(({ pathLocale }) => pathLocale === '/') + .map(({ path }) => path) + const localeRedirectMap: Record = {} + + pages + .filter(({ pathLocale }) => pathLocale !== '/') + .forEach(({ path, pathLocale }) => { + const rootPath = path + .replace(pathLocale, '/') + .replace(/\/$/, '/index.html') + + if (!rootPaths.includes(rootPath)) + (localeRedirectMap[rootPath] ??= []).push(pathLocale) + }) + + await withSpinner('Generating locale redirect files')(() => + Promise.all( + entries(localeRedirectMap).map(([rootPath, availableLocales]) => { + const filePath = dir.dest(removeLeadingSlash(rootPath)) + + return fs.existsSync(filePath) + ? Promise.resolve() + : fs + .ensureDir(path.dirname(filePath)) + .then(() => + fs.writeFile( + filePath, + getLocaleRedirectHTML( + localeOptions, + availableLocales, + options.base, + ), + ), + ) + }), + ), + ) +} + +export const generateRedirects = async ( + { dir, options }: App, + config: Record, + hostname = '', +): Promise => { + const resolvedHostname = hostname + ? removeEndingSlash(isLinkHttp(hostname) ? hostname : `https://${hostname}`) + : '' + + await withSpinner('Generating redirect files')(() => + Promise.all( + entries(config).map(([from, to]) => { + const filePath = dir.dest(removeLeadingSlash(from)) + const redirectUrl = isLinkAbsolute(to) + ? `${resolvedHostname}${options.base}${removeLeadingSlash(to)}` + : to + + return fs.existsSync(filePath) + ? Promise.resolve() + : fs + .ensureDir(path.dirname(filePath)) + .then(() => fs.writeFile(filePath, getRedirectHTML(redirectUrl))) + }), + ), + ) +} diff --git a/plugins/plugin-redirect/src/node/homepage.ts b/plugins/plugin-redirect/src/node/homepage.ts new file mode 100644 index 0000000000..ba683043a1 --- /dev/null +++ b/plugins/plugin-redirect/src/node/homepage.ts @@ -0,0 +1,88 @@ +import { removeEndingSlash } from '@vuepress/helper' +import type { App } from 'vuepress/core' +import { createPage } from 'vuepress/core' +import type { LocaleRedirectConfig } from '../shared/index.js' + +export const ensureRootHomePage = async ( + app: App, + localeOptions: LocaleRedirectConfig, +): Promise => { + const { + options: { base }, + pages, + } = app + + if ( + // homepage not exists + pages.every(({ path }) => path !== '/') + ) { + const availableLocales = pages + .filter(({ pathLocale, path }) => pathLocale === path) + .map(({ pathLocale }) => pathLocale) + + pages.push( + await createPage(app, { + path: '/', + frontmatter: { title: 'Home' }, + // set markdown content + content: `\ +Redirecting to the correct locale... + + +`, + }), + ) + } +} diff --git a/plugins/plugin-redirect/src/node/index.ts b/plugins/plugin-redirect/src/node/index.ts new file mode 100644 index 0000000000..cf4c75baf7 --- /dev/null +++ b/plugins/plugin-redirect/src/node/index.ts @@ -0,0 +1,3 @@ +export * from './options.js' +export * from './plugin.js' +export * from './typings/index.js' diff --git a/plugins/plugin-redirect/src/node/locale.ts b/plugins/plugin-redirect/src/node/locale.ts new file mode 100644 index 0000000000..3f01cb18e8 --- /dev/null +++ b/plugins/plugin-redirect/src/node/locale.ts @@ -0,0 +1,64 @@ +import { + deepAssign, + entries, + fromEntries, + isArray, + isPlainObject, + keys, +} from '@vuepress/helper' +import type { App } from 'vuepress/core' +import { colors } from 'vuepress/utils' +import type { LocaleRedirectConfig } from '../shared/index.js' +import type { RedirectOptions } from './options.js' +import { logger } from './utils/index.js' + +const AVAILABLE_FALLBACK = ['defaultLocale', 'homepage', '404'] as const + +export const getLocaleSettings = ( + app: App, + options: RedirectOptions, +): LocaleRedirectConfig => { + const { locales } = app.options + + const localeConfig = deepAssign( + fromEntries( + entries(locales) + .filter(([key, { lang }]) => { + if (key === '/') return false + + if (!lang) { + logger.error( + `Missing ${colors.magenta( + 'lang', + )} option for locale "${key}", this locale will be ignored!`, + ) + + return false + } + + return true + }) + .map(([key, { lang }]) => [key, [lang!]]), + ), + isPlainObject(options.localeConfig) + ? entries(options.localeConfig).map(([routePath, lang]) => [ + routePath, + isArray(lang) ? lang : [lang], + ]) + : {}, + ) + const defaultLocale = options.defaultLocale || keys(localeConfig).pop()! + + return { + autoLocale: options.autoLocale ?? false, + switchLocale: options.switchLocale ?? false, + localeConfig, + defaultLocale, + localeFallback: options.localeFallback ?? true, + defaultBehavior: + options.defaultBehavior && + AVAILABLE_FALLBACK.includes(options.defaultBehavior) + ? options.defaultBehavior + : 'defaultLocale', + } +} diff --git a/plugins/plugin-redirect/src/node/locales.ts b/plugins/plugin-redirect/src/node/locales.ts new file mode 100644 index 0000000000..f669a07854 --- /dev/null +++ b/plugins/plugin-redirect/src/node/locales.ts @@ -0,0 +1,151 @@ +import type { RedirectLocaleConfig } from '../shared/index.js' + +/** Multi language config for redirect popup */ +export const redirectLocales: RedirectLocaleConfig = { + '/en/': { + name: 'English', + hint: 'Your primary language is $1, do you want to switch to it?', + switch: 'Switch to $1', + cancel: 'Cancel', + }, + + '/zh/': { + name: '简体中文', + hint: '您的首选语言是 $1,是否切换到该语言?', + switch: '切换到 $1', + cancel: '取消', + }, + + '/zh-tw/': { + name: '繁體中文', + hint: '您的首選語言是 $1,是否切換到該語言?', + switch: '切換到 $1', + cancel: '取消', + }, + + '/de/': { + name: 'Deutsch', + hint: 'Ihre bevorzugte Sprache ist $1, möchten Sie zu dieser wechseln?', + switch: 'Zu $1 wechseln', + cancel: 'Abbrechen', + }, + + '/de-at/': { + name: 'Deutsch (Österreich)', + hint: 'Ihre bevorzugte Sprache ist $1, möchten Sie zu dieser wechseln?', + switch: 'Zu $1 wechseln', + cancel: 'Abbrechen', + }, + + '/vi/': { + name: 'Tiếng Việt', + hint: 'Ngôn ngữ chính của bạn là $1, bạn có muốn chuyển sang nó?', + switch: 'Chuyển sang $1', + cancel: 'Hủy', + }, + + '/uk/': { + name: 'Українська', + hint: 'Вашою основною мовою є $1, чи бажаєте ви переключитися на неї?', + switch: 'Переключитися на $1', + cancel: 'Скасувати', + }, + + '/ru/': { + name: 'Русский', + hint: 'Ваш основной язык - $1, вы хотите переключиться на него?', + switch: 'Переключиться на $1', + cancel: 'Отмена', + }, + + '/br/': { + name: 'Português (Brasil)', + hint: 'A língua principal é $1, deseja mudar para ela?', + switch: 'Mudar para $1', + cancel: 'Cancelar', + }, + + '/pl/': { + name: 'Polski', + hint: 'Twoim głównym językiem jest $1, czy chcesz przełączyć się na niego?', + switch: 'Przełącz na $1', + cancel: 'Anuluj', + }, + + '/sk/': { + name: 'Slovenčina', + hint: 'Vašou hlavnou jazykom je $1, chcete prepnúť naň?', + switch: 'Prepnúť na $1', + cancel: 'Zrušiť', + }, + + '/fr/': { + name: 'Français', + hint: 'Votre langue principale est $1, voulez-vous la changer ?', + switch: 'Changer pour $1', + cancel: 'Annuler', + }, + + '/tr/': { + name: 'Türkçe', + hint: 'Ana diliniz $1, ona geçmek ister misiniz?', + switch: "$1'e geç", + cancel: 'İptal', + }, + + '/fi/': { + name: 'Suomi', + hint: 'Pääkielenäsi on $1, haluatko vaihtaa siihen?', + switch: 'Vaihda $1:een', + cancel: 'Peruuta', + }, + + '/hu/': { + name: 'Magyar', + hint: 'A fő nyelvét $1, szeretné váltani?', + switch: 'Váltás $1', + cancel: 'Mégse', + }, + + '/id/': { + name: 'Bahasa Indonesia', + hint: 'Bahasa utama Anda adalah $1, apakah Anda ingin beralih ke sana?', + switch: 'Beralih ke $1', + cancel: 'Batal', + }, + + '/nl/': { + name: 'Nederlands', + hint: 'Uw primaire taal is $1, wilt u overschakelen?', + switch: 'Overschakelen naar $1', + cancel: 'Annuleren', + }, + + '/ja/': { + name: '日本語', + hint: 'あなたの主要な言語は $1 です。それに切り替えますか?', + switch: '$1 に切り替える', + cancel: 'キャンセル', + }, + + '/ko/': { + name: '한국어', + hint: '당신의 기본 언어는 $1입니다. 그것으로 전환 하시겠습니까?', + switch: '$1로 전환', + cancel: '취소', + }, + + '/es/': { + name: 'Español', + hint: 'Su idioma principal es $1, ¿desea cambiarlo?', + switch: 'Cambiar a $1', + cancel: 'Cancelar', + }, + + '/pt/': { + name: 'Português', + hint: 'Sua língua principal é $1, deseja mudar para ela?', + switch: 'Mudar para $1', + cancel: 'Cancelar', + }, +} diff --git a/plugins/plugin-redirect/src/node/options.ts b/plugins/plugin-redirect/src/node/options.ts new file mode 100644 index 0000000000..232a57b2f2 --- /dev/null +++ b/plugins/plugin-redirect/src/node/options.ts @@ -0,0 +1,44 @@ +import type { App, LocaleConfig } from 'vuepress/core' +import type { + LocaleRedirectConfig, + RedirectLocaleData, +} from '../shared/index.js' + +export interface RedirectOptions + extends Partial> { + /** + * Redirect mapping + * + * @description if the link starts with `/` then, hostname and base will be added in front of it. + * + * 重定向映射 + * + * @description 如果链接以 `/` 开头,则会在前面添加 hostname 和 base + */ + config?: Record | ((app: App) => Record) + + /** + * domain which to be redirected to + * + * 重定向到的网站域名 + */ + hostname?: string + + /** + * Locale language config + * + * 多语言语言配置 + */ + localeConfig?: Record + + /** + * Locales config + * + * @see [default config](https://github.com/vuepress-theme-hope/vuepress-theme-hope/blob/main/packages/redirect/src/node/locales.ts) + * + * 多语言选项 + * + * @see [默认配置](https://github.com/vuepress-theme-hope/vuepress-theme-hope/blob/main/packages/redirect/src/node/locales.ts) + */ + locales?: LocaleConfig +} diff --git a/plugins/plugin-redirect/src/node/plugin.ts b/plugins/plugin-redirect/src/node/plugin.ts new file mode 100644 index 0000000000..aaf5e07b0e --- /dev/null +++ b/plugins/plugin-redirect/src/node/plugin.ts @@ -0,0 +1,61 @@ +import { addViteSsrNoExternal, getLocaleConfig } from '@vuepress/helper' +import type { PluginFunction } from 'vuepress/core' +import { CLIENT_FOLDER, PLUGIN_NAME } from './constant.js' +import { generateAutoLocaleRedirects, generateRedirects } from './generate.js' +import { ensureRootHomePage } from './homepage.js' +import { getLocaleSettings } from './locale.js' +import { redirectLocales } from './locales.js' +import type { RedirectOptions } from './options.js' +import { prepareRedirects } from './prepare.js' +import { getRedirectMap, handleRedirectTo, logger } from './utils/index.js' + +export const redirectPlugin = + (options: RedirectOptions = {}): PluginFunction => + (app) => { + if (app.env.isDebug) logger.info('Options:', options) + + const localeConfig = getLocaleSettings(app, options) + let redirectMap: Record + + return { + name: PLUGIN_NAME, + + define: { + REDIRECT_LOCALE_CONFIG: localeConfig, + REDIRECT_LOCALE_SWITCH: Boolean(localeConfig.switchLocale), + REDIRECT_LOCALES: getLocaleConfig({ + app, + name: 'redirect', + config: options.locales, + default: redirectLocales, + }), + }, + + extendsBundlerOptions: (bundlerOptions: unknown, app): void => { + addViteSsrNoExternal(bundlerOptions, app, [ + '@vuepress/helper', + 'vuepress-shared', + ]) + }, + + onInitialized: async (app): Promise => { + redirectMap = getRedirectMap(app, options) + + if (app.env.isDebug) logger.info('Redirect Map:', redirectMap) + + handleRedirectTo(app, options) + + if (localeConfig.autoLocale) await ensureRootHomePage(app, localeConfig) + }, + + onPrepared: (app): Promise => prepareRedirects(app, redirectMap), + + onGenerated: async (app): Promise => { + await generateRedirects(app, redirectMap) + if (localeConfig.autoLocale) + await generateAutoLocaleRedirects(app, localeConfig) + }, + + clientConfigFile: `${CLIENT_FOLDER}config.js`, + } + } diff --git a/plugins/plugin-redirect/src/node/prepare.ts b/plugins/plugin-redirect/src/node/prepare.ts new file mode 100644 index 0000000000..fb18b3a547 --- /dev/null +++ b/plugins/plugin-redirect/src/node/prepare.ts @@ -0,0 +1,15 @@ +import type { App } from 'vuepress/core' + +export const prepareRedirects = async ( + app: App, + config: Record, +): Promise => { + await app.writeTemp( + 'redirect/config.js', + `\ +export const redirectConfig = ${ + app.env.isDev ? JSON.stringify(config, null, 2) : '{}' + }; +`, + ) +} diff --git a/plugins/plugin-redirect/src/node/typings/frontmatter.ts b/plugins/plugin-redirect/src/node/typings/frontmatter.ts new file mode 100644 index 0000000000..57b0f1dc3a --- /dev/null +++ b/plugins/plugin-redirect/src/node/typings/frontmatter.ts @@ -0,0 +1,4 @@ +export interface RedirectPluginFrontmatterOption { + redirectFrom?: string | string[] + redirectTo?: string +} diff --git a/plugins/plugin-redirect/src/node/typings/index.ts b/plugins/plugin-redirect/src/node/typings/index.ts new file mode 100644 index 0000000000..6247a1956b --- /dev/null +++ b/plugins/plugin-redirect/src/node/typings/index.ts @@ -0,0 +1 @@ +export * from './frontmatter.js' diff --git a/plugins/plugin-redirect/src/node/utils/index.ts b/plugins/plugin-redirect/src/node/utils/index.ts new file mode 100644 index 0000000000..cbb1f3d9cd --- /dev/null +++ b/plugins/plugin-redirect/src/node/utils/index.ts @@ -0,0 +1,4 @@ +export * from './logger.js' +export * from './normalizePath.js' +export * from './outputFile.js' +export * from './redirect.js' diff --git a/plugins/plugin-redirect/src/node/utils/logger.ts b/plugins/plugin-redirect/src/node/utils/logger.ts new file mode 100644 index 0000000000..956dcf1602 --- /dev/null +++ b/plugins/plugin-redirect/src/node/utils/logger.ts @@ -0,0 +1,4 @@ +import { Logger } from '@vuepress/helper' +import { PLUGIN_NAME } from '../constant.js' + +export const logger = new Logger(PLUGIN_NAME) diff --git a/plugins/plugin-redirect/src/node/utils/normalizePath.ts b/plugins/plugin-redirect/src/node/utils/normalizePath.ts new file mode 100644 index 0000000000..cb1d70040a --- /dev/null +++ b/plugins/plugin-redirect/src/node/utils/normalizePath.ts @@ -0,0 +1,2 @@ +export const normalizePath = (url: string): string => + url.replace(/\/$/, '/index.html').replace(/(?:\.(md|html))?$/, '.html') diff --git a/plugins/plugin-redirect/src/node/utils/outputFile.ts b/plugins/plugin-redirect/src/node/utils/outputFile.ts new file mode 100644 index 0000000000..1134bce67a --- /dev/null +++ b/plugins/plugin-redirect/src/node/utils/outputFile.ts @@ -0,0 +1,117 @@ +import { removeEndingSlash } from '@vuepress/helper' +import type { LocaleRedirectConfig } from '../../shared/index.js' + +export const getLocaleRedirectHTML = ( + { + localeConfig, + defaultBehavior, + defaultLocale, + localeFallback, + }: LocaleRedirectConfig, + availableLocales: string[], + base: string, +): string => ` + + + + + Redirecting... + + + +

Redirecting...

+ + +` + +export const getRedirectHTML = (redirectUrl: string): string => ` + + + + + + + Redirecting... + + + +

Redirecting...

+ + +` diff --git a/plugins/plugin-redirect/src/node/utils/redirect.ts b/plugins/plugin-redirect/src/node/utils/redirect.ts new file mode 100644 index 0000000000..4b88a1806a --- /dev/null +++ b/plugins/plugin-redirect/src/node/utils/redirect.ts @@ -0,0 +1,85 @@ +import { + entries, + fromEntries, + isArray, + isFunction, + isLinkAbsolute, + isLinkHttp, + isPlainObject, + removeEndingSlash, + removeLeadingSlash, +} from '@vuepress/helper' +import type { App, Page } from 'vuepress/core' +import type { RedirectOptions } from '../options.js' +import type { RedirectPluginFrontmatterOption } from '../typings/index.js' +import { normalizePath } from './normalizePath.js' + +export const handleRedirectTo = ( + app: App, + { hostname }: RedirectOptions, +): void => { + const { base } = app.options + const resolvedHostname = hostname + ? removeEndingSlash(isLinkHttp(hostname) ? hostname : `https://${hostname}`) + : '' + + app.pages.forEach(({ frontmatter }) => { + const { redirectTo } = frontmatter as RedirectPluginFrontmatterOption + + if (redirectTo) { + const redirectUrl = normalizePath( + isLinkAbsolute(redirectTo) + ? `${resolvedHostname}${base}${removeLeadingSlash(redirectTo)}` + : redirectTo, + ) + + ;(frontmatter.head ??= []).unshift([ + 'script', + {}, + `{\ +const anchor = window.location.hash.substring(1);\ +location.href=\`${redirectUrl}\${anchor? \`#\${anchor}\`: ""}\`;\ +}`, + ]) + } + }) +} + +export const getRedirectMap = ( + app: App, + options: RedirectOptions, +): Record => { + const config = isFunction(options.config) + ? options.config(app) + : isPlainObject(options.config) + ? options.config + : {} + + return { + ...fromEntries( + ( + app.pages as Page< + Record, + RedirectPluginFrontmatterOption + >[] + ) + .map<[string, string][]>(({ frontmatter, path }) => + isArray(frontmatter.redirectFrom) + ? frontmatter.redirectFrom.map((from) => [ + normalizePath(from), + path, + ]) + : frontmatter.redirectFrom + ? [[normalizePath(frontmatter.redirectFrom), path]] + : [], + ) + .flat(), + ), + ...fromEntries( + entries(config).map(([from, to]) => [ + normalizePath(from), + normalizePath(to), + ]), + ), + } +} diff --git a/plugins/plugin-redirect/src/shared/index.ts b/plugins/plugin-redirect/src/shared/index.ts new file mode 100644 index 0000000000..c883cb2d3c --- /dev/null +++ b/plugins/plugin-redirect/src/shared/index.ts @@ -0,0 +1,2 @@ +export * from './locale.js' +export * from './locales.js' diff --git a/plugins/plugin-redirect/src/shared/locale.ts b/plugins/plugin-redirect/src/shared/locale.ts new file mode 100644 index 0000000000..a9bc43f1c9 --- /dev/null +++ b/plugins/plugin-redirect/src/shared/locale.ts @@ -0,0 +1,59 @@ +export interface LocaleRedirectConfig { + /** + * Whether enable locales redirection + * + * 是否启用语言重定向 + * + * @default false + */ + autoLocale: boolean + + /** + * Whether switch locales + * + * 是否启用重定向语言 + * + * @default false + */ + switchLocale: 'direct' | 'modal' | false + + /** + * Locale language config + * + * 多语言语言配置 + */ + localeConfig: Record + + /** + * Whether fallback to other locales user defined + * + * 是否回退到用户定义的其他语言 + * + * @default true + */ + localeFallback: boolean + + /** + * Behavior when a locale version is not available for current link + * + * @description `"homepage"` and `"404"` is only available when a locale is assigned to current language + * + * 当前链接没有可用的语言版本时的行为 + * + * @description 只有当语言分配给当前语言时,`"homepage"` 和 `"404"` 才可用 + * + * @default "defaultLocale" + */ + defaultBehavior: 'defaultLocale' | 'homepage' | '404' + + /** + * Default locale path + * + * @description the first locale will be used if absent + * + * 默认语言路径 + * + * @description 如果缺失,则使用第一个语言 + */ + defaultLocale: string +} diff --git a/plugins/plugin-redirect/src/shared/locales.ts b/plugins/plugin-redirect/src/shared/locales.ts new file mode 100644 index 0000000000..42af3457c0 --- /dev/null +++ b/plugins/plugin-redirect/src/shared/locales.ts @@ -0,0 +1,29 @@ +import type { ExactLocaleConfig } from '@vuepress/helper/shared' + +export interface RedirectLocaleData { + /** + * Language name + */ + name: string + + /** + * Switch hint + * + * 切换提示 + */ + hint: string + + /** + * Switch button text + */ + switch: string + + /** + * Cancel button text + * + * 取消按钮文字 + */ + cancel: string +} + +export type RedirectLocaleConfig = ExactLocaleConfig diff --git a/plugins/plugin-redirect/src/shims-redirect-config.d.ts b/plugins/plugin-redirect/src/shims-redirect-config.d.ts new file mode 100644 index 0000000000..55847249a9 --- /dev/null +++ b/plugins/plugin-redirect/src/shims-redirect-config.d.ts @@ -0,0 +1,3 @@ +declare module '@temp/redirect/config.js' { + export const redirectConfig: Record +} diff --git a/plugins/plugin-redirect/tests/normalizePath.spec.ts b/plugins/plugin-redirect/tests/normalizePath.spec.ts new file mode 100644 index 0000000000..ab2a2ed18d --- /dev/null +++ b/plugins/plugin-redirect/tests/normalizePath.spec.ts @@ -0,0 +1,62 @@ +import { expect, it } from 'vitest' +import { normalizePath } from '../src/node/utils/normalizePath.js' + +it('Should normalize path', () => { + const testCases = [ + ['/', '/index.html'], + ['/index', '/index.html'], + ['/index.md', '/index.html'], + ['/index.html', '/index.html'], + ['/foo/', '/foo/index.html'], + ['/foo/bar/', '/foo/bar/index.html'], + ['/foo/bar/index', '/foo/bar/index.html'], + ['/foo/bar/index.md', '/foo/bar/index.html'], + ['/foo/bar/index.html', '/foo/bar/index.html'], + ['/foo', '/foo.html'], + ['/foo.md', '/foo.html'], + ['/foo.html', '/foo.html'], + ['/foo/bar', '/foo/bar.html'], + ['/foo/bar.md', '/foo/bar.html'], + ['/foo/bar.html', '/foo/bar.html'], + ['/foo/bar/baz', '/foo/bar/baz.html'], + ['/foo/bar/baz.md', '/foo/bar/baz.html'], + ['/foo/bar/baz.html', '/foo/bar/baz.html'], + ['https://example.com/', 'https://example.com/index.html'], + ['https://example.com/index', 'https://example.com/index.html'], + ['https://example.com/index.md', 'https://example.com/index.html'], + ['https://example.com/index.html', 'https://example.com/index.html'], + ['https://example.com/foo/', 'https://example.com/foo/index.html'], + ['https://example.com/foo/bar/', 'https://example.com/foo/bar/index.html'], + [ + 'https://example.com/foo/bar/index', + 'https://example.com/foo/bar/index.html', + ], + [ + 'https://example.com/foo/bar/index.md', + 'https://example.com/foo/bar/index.html', + ], + [ + 'https://example.com/foo/bar/index.html', + 'https://example.com/foo/bar/index.html', + ], + ['https://example.com/foo', 'https://example.com/foo.html'], + ['https://example.com/foo.md', 'https://example.com/foo.html'], + ['https://example.com/foo.html', 'https://example.com/foo.html'], + ['https://example.com/foo/bar', 'https://example.com/foo/bar.html'], + ['https://example.com/foo/bar.md', 'https://example.com/foo/bar.html'], + ['https://example.com/foo/bar.html', 'https://example.com/foo/bar.html'], + ['https://example.com/foo/bar/baz', 'https://example.com/foo/bar/baz.html'], + [ + 'https://example.com/foo/bar/baz.md', + 'https://example.com/foo/bar/baz.html', + ], + [ + 'https://example.com/foo/bar/baz.html', + 'https://example.com/foo/bar/baz.html', + ], + ] + + testCases.forEach(([input, output]) => { + expect(normalizePath(input)).toBe(output) + }) +}) diff --git a/plugins/plugin-redirect/tsconfig.build.json b/plugins/plugin-redirect/tsconfig.build.json new file mode 100644 index 0000000000..e0a82d8177 --- /dev/null +++ b/plugins/plugin-redirect/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "types": ["vuepress/client-types"] + }, + "include": ["./src"], + "references": [{ "path": "../../tools/helper/tsconfig.build.json" }] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8aad32601e..19091ef027 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -420,6 +420,27 @@ importers: specifier: 2.0.0-rc.2 version: 2.0.0-rc.2(@vuepress/bundler-vite@2.0.0-rc.2)(@vuepress/bundler-webpack@2.0.0-rc.2)(typescript@5.3.3)(vue@3.4.15) + plugins/plugin-redirect: + dependencies: + '@vuepress/helper': + specifier: workspace:* + version: link:../../tools/helper + '@vueuse/core': + specifier: ^10.7.2 + version: 10.7.2(vue@3.4.15) + cac: + specifier: ^6.7.14 + version: 6.7.14 + vue: + specifier: ^3.4.15 + version: 3.4.15(typescript@5.3.3) + vue-router: + specifier: ^4.2.5 + version: 4.2.5(vue@3.4.15) + vuepress: + specifier: 2.0.0-rc.2 + version: 2.0.0-rc.2(@vuepress/bundler-vite@2.0.0-rc.2)(@vuepress/bundler-webpack@2.0.0-rc.2)(typescript@5.3.3)(vue@3.4.15) + plugins/plugin-register-components: dependencies: chokidar: diff --git a/tsconfig.build.json b/tsconfig.build.json index b283bf077d..db7577f74c 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -31,6 +31,7 @@ }, { "path": "./plugins/plugin-pwa/tsconfig.build.json" }, { "path": "./plugins/plugin-pwa-popup/tsconfig.build.json" }, + { "path": "./plugins/plugin-redirect/tsconfig.build.json" }, { "path": "./plugins/plugin-reading-time/tsconfig.build.json" }, { "path": "./plugins/plugin-register-components/tsconfig.build.json"