From 11fa0f7456359b86dc804b76e0fc9e6701554814 Mon Sep 17 00:00:00 2001 From: nilennoct Date: Fri, 2 Jun 2023 12:30:25 +0800 Subject: [PATCH] feat: add assetsBase config to support serving assets from CDN refactor: rename `publicPath` option to `assetsBase` --- docs/en/reference/site-config.md | 19 ++++++++++++ docs/es/reference/site-config.md | 19 ++++++++++++ docs/ko/reference/site-config.md | 19 ++++++++++++ docs/pt/reference/site-config.md | 19 ++++++++++++ docs/ru/reference/site-config.md | 19 ++++++++++++ docs/zh/reference/site-config.md | 19 ++++++++++++ src/client/app/utils.ts | 4 ++- src/node/build/build.ts | 21 +++++++++++-- src/node/build/bundle.ts | 19 ++++++++++++ src/node/build/render.ts | 20 ++++++++++--- src/node/config.ts | 16 ++++++++-- src/node/serve/serve.ts | 51 ++++++++++++++++++++++++-------- src/node/server.ts | 4 +-- src/node/siteConfig.ts | 1 + src/node/utils/assetsBase.ts | 28 ++++++++++++++++++ types/shared.d.ts | 1 + 16 files changed, 255 insertions(+), 24 deletions(-) create mode 100644 src/node/utils/assetsBase.ts diff --git a/docs/en/reference/site-config.md b/docs/en/reference/site-config.md index 8cf5e264..3bc90316 100644 --- a/docs/en/reference/site-config.md +++ b/docs/en/reference/site-config.md @@ -343,6 +343,25 @@ export default { } ``` +### assetsBase + +- Type: `string` +- Default: `/assets/` + +The base URL the site assets will be deployed at. You will need to set this if you plan to deploy your site assets to CDN. It is similar to `publicPath` in other module bundler. + +The `assetsBase` is configured to `${base}assets/` by default. It should always start with a slash or a valid protocol, and always end with a slash. + +::: warning +This option only takes effect in production mode. +::: + +```ts +export default { + assetsBase: 'https://cdn.example.com/assets/bar/' +} +``` + ## Routing ### cleanUrls diff --git a/docs/es/reference/site-config.md b/docs/es/reference/site-config.md index b2f3cf83..98e2a382 100644 --- a/docs/es/reference/site-config.md +++ b/docs/es/reference/site-config.md @@ -343,6 +343,25 @@ export default { } ``` +### assetsBase + +- Tipo: `string` +- Predeterminado: `/assets/` + +La URL base donde se desplegarán los activos del sitio. Necesitarás configurar esto si planeas desplegar los activos de tu sitio en un CDN. Es similar a `publicPath` en otros empaquetadores de módulos. + +El `assetsBase` se configura como `${base}assets/` por defecto. Siempre debe comenzar con una barra inclinada o un protocolo válido y siempre terminar con una barra inclinada. + +::: warning +Esta opción solo tiene efecto en modo producción. +::: + +```ts +export default { + assetsBase: 'https://cdn.example.com/assets/bar/' +} +``` + ## Roteamento {#routing} ### cleanUrls diff --git a/docs/ko/reference/site-config.md b/docs/ko/reference/site-config.md index 25dbcc01..949b8452 100644 --- a/docs/ko/reference/site-config.md +++ b/docs/ko/reference/site-config.md @@ -343,6 +343,25 @@ export default { } ``` +### assetsBase + +- 타입: `string` +- 기본값: `/assets/` + +사이트 에셋이 배포될 기본 URL입니다. 사이트 에셋을 CDN에 배포할 계획이라면 이 값을 설정해야 합니다. 이는 다른 모듈 번들러의 `publicPath`와 유사합니다. + +`assetsBase`는 기본적으로 `${base}assets/`로 구성됩니다. 항상 슬래시로 시작하거나 유효한 프로토콜로 시작해야 하며, 항상 슬래시로 끝나야 합니다. + +::: warning +이 옵션은 프로덕션 모드에서만 적용됩니다. +::: + +```ts +export default { + assetsBase: 'https://cdn.example.com/assets/bar/' +} +``` + ## 라우팅 {#routing} ### cleanUrls diff --git a/docs/pt/reference/site-config.md b/docs/pt/reference/site-config.md index 922f698f..3028103a 100644 --- a/docs/pt/reference/site-config.md +++ b/docs/pt/reference/site-config.md @@ -343,6 +343,25 @@ export default { } ``` +### assetsBase + +- Tipo: `string` +- Padrão: `/assets/` + +A URL base onde os recursos do site serão implantados. Você precisará configurar isto se planeja implantar os recursos do seu site em uma CDN. É semelhante a `publicPath` em outros empacotadores de módulos. + +O `assetsBase` é configurado como `${base}assets/` por padrão. Deve sempre começar com uma barra ou um protocolo válido, e sempre terminar com uma barra. + +::: warning +Esta opção só tem efeito no modo de produção. +::: + +```ts +export default { + assetsBase: 'https://cdn.example.com/assets/bar/' +} +``` + ## Roteamento {#routing} ### cleanUrls diff --git a/docs/ru/reference/site-config.md b/docs/ru/reference/site-config.md index 8ba3a110..27a72060 100644 --- a/docs/ru/reference/site-config.md +++ b/docs/ru/reference/site-config.md @@ -343,6 +343,25 @@ export default { } ``` +### assetsBase + +- Тип: `string` +- По умолчанию: `/assets/` + +Базовый URL, по которому будут размещены ресурсы сайта. Вам нужно будет установить это значение, если вы планируете размещать ресурсы вашего сайта на CDN. Это похоже на `publicPath` в других сборщиках модулей. + +По умолчанию `assetsBase` настроен на `${base}assets/`. Оно всегда должно начинаться с косой черты или действительного протокола и всегда заканчиваться косой чертой. + +::: warning +Этот параметр действует только в режиме производства. +::: + +```ts +export default { + assetsBase: 'https://cdn.example.com/assets/bar/' +} +``` + ## Маршрутизация {#routing} ### cleanUrls {#cleanurls} diff --git a/docs/zh/reference/site-config.md b/docs/zh/reference/site-config.md index 72426c61..47b71444 100644 --- a/docs/zh/reference/site-config.md +++ b/docs/zh/reference/site-config.md @@ -343,6 +343,25 @@ export default { } ``` +### assetsBase + +- 类型: `string` +- 默认值: `/assets/` + +站点资源将被部署的 base URL。如果您计划将站点资源部署到 CDN,则需要设置此项。它类似于其他模块打包器中的 `publicPath`。 + +`assetsBase` 默认配置为 `${base}assets/`。它应该始终以斜杠或有效协议开头,并始终以斜杠结尾。 + +::: warning +此选项仅在生产模式下生效。 +::: + +```ts +export default { + assetsBase: 'https://cdn.example.com/assets/bar/' +} +``` + ## 路由 {#routing} ### cleanUrls diff --git a/src/client/app/utils.ts b/src/client/app/utils.ts index 45496b40..d7893970 100644 --- a/src/client/app/utils.ts +++ b/src/client/app/utils.ts @@ -47,6 +47,8 @@ export function pathToFile(path: string) { // /foo/bar.html -> ./foo_bar.md if (inBrowser) { const base = import.meta.env.BASE_URL + const assetsBase = + import.meta.env.VITE_VP_ASSETS_BASE ?? `${base}${__ASSETS_DIR__}/` pagePath = sanitizeFileName( pagePath.slice(base.length).replace(/\//g, '_') || 'index' @@ -61,7 +63,7 @@ export function pathToFile(path: string) { pageHash = __VP_HASH_MAP__[pagePath.toLowerCase()] } if (!pageHash) return null - pagePath = `${base}${__ASSETS_DIR__}/${pagePath}.${pageHash}.js` + pagePath = `${assetsBase}${pagePath}.${pageHash}.js` } else { // ssr build uses much simpler name mapping pagePath = `./${sanitizeFileName( diff --git a/src/node/build/build.ts b/src/node/build/build.ts index 8f3df57a..293ee58f 100644 --- a/src/node/build/build.ts +++ b/src/node/build/build.ts @@ -8,10 +8,15 @@ import pMap from 'p-map' import { packageDirectorySync } from 'pkg-dir' import { rimraf } from 'rimraf' import type { BuildOptions, Rollup } from 'vite' -import { resolveConfig, type SiteConfig } from '../config' +import { normalizeBaseUrl, resolveConfig, type SiteConfig } from '../config' import { clearCache } from '../markdownToVue' import { slash, type HeadConfig } from '../shared' import { deserializeFunctions, serializeFunctions } from '../utils/fnSerialize' +import { + getDefaultAssetsBase, + isDefaultAssetsBase, + normalizeAssetUrl +} from '../utils/assetsBase' import { task } from '../utils/task' import { bundle } from './bundle' import { generateSitemap } from './generateSitemap' @@ -30,8 +35,17 @@ export async function build( const unlinkVue = linkVue() if (buildOptions.base) { - siteConfig.site.base = buildOptions.base + const shouldUpdateAssetsBase = isDefaultAssetsBase( + siteConfig.site.base, + siteConfig.site.assetsBase + ) + + siteConfig.site.base = normalizeBaseUrl(buildOptions.base) delete buildOptions.base + + if (shouldUpdateAssetsBase) { + siteConfig.site.assetsBase = getDefaultAssetsBase(siteConfig.site.base) + } } if (buildOptions.mpa) { @@ -44,6 +58,7 @@ export async function build( delete buildOptions.outDir } + process.env.VITE_VP_ASSETS_BASE = siteConfig.site.assetsBase try { const { clientResult, serverResult, pageToHashMap } = await bundle( siteConfig, @@ -77,7 +92,7 @@ export async function build( .filter( (chunk) => chunk.type === 'asset' && !chunk.fileName.endsWith('.css') ) - .map((asset) => siteConfig.site.base + asset.fileName) + .map((asset) => normalizeAssetUrl(siteConfig.site, asset.fileName)) // default theme special handling: inject font preload // custom themes will need to use `transformHead` to inject this diff --git a/src/node/build/bundle.ts b/src/node/build/bundle.ts index f0305fd0..7da95179 100644 --- a/src/node/build/bundle.ts +++ b/src/node/build/bundle.ts @@ -12,6 +12,7 @@ import { APP_PATH } from '../alias' import type { SiteConfig } from '../config' import { createVitePressPlugin } from '../plugin' import { escapeRegExp, sanitizeFileName, slash } from '../shared' +import { normalizeAssetUrl } from '../utils/assetsBase' import { task } from '../utils/task' import { buildMPAClient } from './buildMPAClient' @@ -83,6 +84,24 @@ export async function bundle( ssr: { noExternal: ['vitepress', '@docsearch/css'] }, + experimental: { + renderBuiltUrl: (filename, type) => { + let result: string | undefined + + if (type.type === 'asset') { + result = normalizeAssetUrl(config.site, filename) + } + + if (config.vite?.experimental?.renderBuiltUrl) { + return ( + config.vite.experimental.renderBuiltUrl(result ?? filename, type) ?? + result + ) + } + + return result + } + }, build: { ...options, emptyOutDir: true, diff --git a/src/node/build/render.ts b/src/node/build/render.ts index 9e9b9b26..1f7a9594 100644 --- a/src/node/build/render.ts +++ b/src/node/build/render.ts @@ -18,6 +18,7 @@ import { type PageData, type SSGContext } from '../shared' +import { normalizeAssetUrl } from '../utils/assetsBase' export async function renderPage( render: (path: string) => Promise, @@ -72,7 +73,10 @@ export async function renderPage( const title: string = createTitle(siteData, pageData) const description: string = pageData.description || siteData.description const stylesheetLink = cssChunk - ? `` + ? `` : '' let preloadLinks = @@ -104,7 +108,9 @@ export async function renderPage( { rel, // don't add base to external urls - href: (EXTERNAL_URL_RE.test(file) ? '' : siteData.base) + file + href: EXTERNAL_URL_RE.test(file) + ? file + : normalizeAssetUrl(siteData, file) } ]) @@ -148,7 +154,10 @@ export async function renderPage( inlinedScript = `` fs.removeSync(path.resolve(config.outDir, matchingChunk.fileName)) } else { - inlinedScript = `` + inlinedScript = `` } } } @@ -176,7 +185,10 @@ export async function renderPage( ${metadataScript.inHead ? metadataScript.html : ''} ${ appChunk - ? `` + ? `` : '' } ${await renderHead(head)} diff --git a/src/node/config.ts b/src/node/config.ts index 9f34ab43..650726ae 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -19,6 +19,7 @@ import { type SiteData } from './shared' import type { RawConfigExports, SiteConfig, UserConfig } from './siteConfig' +import { getDefaultAssetsBase, normalizeAssetsBase } from './utils/assetsBase' export { resolvePages } from './plugins/dynamicRoutesPlugin' export * from './siteConfig' @@ -72,7 +73,7 @@ export async function resolveConfig( prefix: '[vitepress]', allowClearScreen: userConfig.vite?.clearScreen }) - const site = await resolveSiteData(root, userConfig) + const site = await resolveSiteData(root, userConfig, command, mode) const srcDir = normalizePath(path.resolve(root, userConfig.srcDir || '.')) const assetsDir = userConfig.assetsDir ? slash(userConfig.assetsDir).replace(/^\.?\/|\/$/g, '') @@ -238,13 +239,20 @@ export async function resolveSiteData( ): Promise { userConfig = userConfig || (await resolveUserConfig(root, command, mode))[0] + const base = userConfig.base ? normalizeBaseUrl(userConfig.base) : '/' + const assetsBase = + mode === 'production' && userConfig.assetsBase + ? normalizeAssetsBase(userConfig.assetsBase) + : getDefaultAssetsBase(base) + return { lang: userConfig.lang || 'en-US', dir: userConfig.dir || 'ltr', title: userConfig.title || 'VitePress', titleTemplate: userConfig.titleTemplate, description: userConfig.description || 'A VitePress site', - base: userConfig.base ? userConfig.base.replace(/([^/])$/, '$1/') : '/', + base, + assetsBase, head: resolveSiteDataHead(userConfig), router: { prefetchLinks: userConfig.router?.prefetchLinks ?? true @@ -300,3 +308,7 @@ function resolveSiteDataHead(userConfig?: UserConfig): HeadConfig[] { return head } + +export function normalizeBaseUrl(baseUrl: string) { + return baseUrl.replace(/^([^/])/, '/$1').replace(/([^/])$/, '$1/') +} diff --git a/src/node/serve/serve.ts b/src/node/serve/serve.ts index 2c5295a4..677ab70d 100644 --- a/src/node/serve/serve.ts +++ b/src/node/serve/serve.ts @@ -4,6 +4,8 @@ import path from 'path' import polka, { type IOptions } from 'polka' import sirv from 'sirv' import { resolveConfig } from '../config' +import { isExternal } from '../shared' +import { getDefaultAssetsBase, isDefaultAssetsBase } from '../utils/assetsBase' function trimChar(str: string, char: string) { while (str.charAt(0) === char) { @@ -51,19 +53,44 @@ export async function serve(options: ServeOptions = {}) { } }) - if (base) { - return polka({ onNoMatch }) - .use(base, compress, serve) - .listen(port, () => { - config.logger.info( - `Built site served at http://localhost:${port}/${base}/` + const server = polka({ onNoMatch }) + + const assetsBase = config.site.assetsBase + if (isExternal(assetsBase)) { + config.logger.warn( + `Using external assets base (${assetsBase}) will break assets serving` + ) + } else if (!isDefaultAssetsBase(config.site.base, assetsBase)) { + const defaultAssetsBase = getDefaultAssetsBase( + options.base ?? config.site.base + ) + + // redirect non-default asset requests + server.use((req, res, next) => { + if (req.url.startsWith(assetsBase)) { + res.statusCode = 307 + res.setHeader( + 'Location', + `${defaultAssetsBase}${req.url.slice(assetsBase.length)}` ) - }) + res.end() + } else { + next() + } + }) + } + + if (base) { + server.use(base, compress, serve).listen(port, () => { + config.logger.info( + `Built site served at http://localhost:${port}/${base}/` + ) + }) } else { - return polka({ onNoMatch }) - .use(compress, serve) - .listen(port, () => { - config.logger.info(`Built site served at http://localhost:${port}/`) - }) + server.use(compress, serve).listen(port, () => { + config.logger.info(`Built site served at http://localhost:${port}/`) + }) } + + return server } diff --git a/src/node/server.ts b/src/node/server.ts index 4105edfe..4deba40b 100644 --- a/src/node/server.ts +++ b/src/node/server.ts @@ -1,5 +1,5 @@ import { createServer as createViteServer, type ServerOptions } from 'vite' -import { resolveConfig } from './config' +import { normalizeBaseUrl, resolveConfig } from './config' import { createVitePressPlugin } from './plugin' export async function createServer( @@ -10,7 +10,7 @@ export async function createServer( const config = await resolveConfig(root) if (serverOptions.base) { - config.site.base = serverOptions.base + config.site.base = normalizeBaseUrl(serverOptions.base) delete serverOptions.base } diff --git a/src/node/siteConfig.ts b/src/node/siteConfig.ts index ebd99096..08cf664d 100644 --- a/src/node/siteConfig.ts +++ b/src/node/siteConfig.ts @@ -59,6 +59,7 @@ export interface UserConfig extends?: RawConfigExports base?: string + assetsBase?: string srcDir?: string srcExclude?: string[] outDir?: string diff --git a/src/node/utils/assetsBase.ts b/src/node/utils/assetsBase.ts new file mode 100644 index 00000000..57c778f9 --- /dev/null +++ b/src/node/utils/assetsBase.ts @@ -0,0 +1,28 @@ +import { isExternal, type SiteData } from '../shared' + +export function getDefaultAssetsBase(base: string) { + return `${base}assets/` +} + +export function isDefaultAssetsBase(base: string, assetsBase: string) { + return assetsBase === getDefaultAssetsBase(base) +} + +export function normalizeAssetsBase(assetsBase: string) { + // add leading slash if given `assetsBase` is not external + if (!isExternal(assetsBase)) { + assetsBase = assetsBase.replace(/^([^/])/, '/$1') + } + + // add trailing slash + return assetsBase.replace(/([^/])$/, '$1/') +} + +export function normalizeAssetUrl(siteData: SiteData, filename: string) { + // normalize assets only + if (filename.startsWith('assets/')) { + return `${siteData.assetsBase}${filename.slice(7)}` + } + + return `${siteData.base}${filename}` +} diff --git a/types/shared.d.ts b/types/shared.d.ts index 1ebfa9b9..437ee5f4 100644 --- a/types/shared.d.ts +++ b/types/shared.d.ts @@ -109,6 +109,7 @@ export interface Header { export interface SiteData { base: string + assetsBase: string cleanUrls?: boolean lang: string dir: string