-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(plugin-redirect): add redirect plugin
- Loading branch information
1 parent
af2e351
commit 62d0582
Showing
34 changed files
with
1,491 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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": "[email protected]", | ||
"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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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>', | ||
'Hostname to redirect to (E.g.: https://new.example.com/)', | ||
{ default: '/' }, | ||
) | ||
.option('-c, --config <config>', 'Set path to config file') | ||
.option( | ||
'-o, --output <output>', | ||
'Set the output directory (default: .vuepress/redirect)', | ||
) | ||
.option('--cache <cache>', 'Set the directory of the cache files') | ||
.option('-t, --temp <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() |
167 changes: 167 additions & 0 deletions
167
plugins/plugin-redirect/src/client/components/LanguageSwitch.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Record<string, boolean>>( | ||
'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<LocaleInfo | null>(() => { | ||
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 | ||
}, | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './redirect.js' |
Oops, something went wrong.