feat: add assetsBase config to support serving assets from CDN

refactor: rename `publicPath` option to `assetsBase`
pull/2467/head
nilennoct 2 years ago
parent 564907152e
commit 11fa0f7456

@ -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

@ -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

@ -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

@ -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

@ -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}

@ -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

@ -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(

@ -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

@ -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,

@ -18,6 +18,7 @@ import {
type PageData,
type SSGContext
} from '../shared'
import { normalizeAssetUrl } from '../utils/assetsBase'
export async function renderPage(
render: (path: string) => Promise<SSGContext>,
@ -72,7 +73,10 @@ export async function renderPage(
const title: string = createTitle(siteData, pageData)
const description: string = pageData.description || siteData.description
const stylesheetLink = cssChunk
? `<link rel="preload stylesheet" href="${siteData.base}${cssChunk.fileName}" as="style">`
? `<link rel="preload stylesheet" href="${normalizeAssetUrl(
siteData,
cssChunk.fileName
)}" as="style">`
: ''
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 = `<script type="module">${matchingChunk.code}</script>`
fs.removeSync(path.resolve(config.outDir, matchingChunk.fileName))
} else {
inlinedScript = `<script type="module" src="${siteData.base}${matchingChunk.fileName}"></script>`
inlinedScript = `<script type="module" src="${normalizeAssetUrl(
siteData,
matchingChunk.fileName
)}"></script>`
}
}
}
@ -176,7 +185,10 @@ export async function renderPage(
${metadataScript.inHead ? metadataScript.html : ''}
${
appChunk
? `<script type="module" src="${siteData.base}${appChunk.fileName}"></script>`
? `<script type="module" src="${normalizeAssetUrl(
siteData,
appChunk.fileName
)}"></script>`
: ''
}
${await renderHead(head)}

@ -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<SiteData> {
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/')
}

@ -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
}

@ -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
}

@ -59,6 +59,7 @@ export interface UserConfig<ThemeConfig = any>
extends?: RawConfigExports<ThemeConfig>
base?: string
assetsBase?: string
srcDir?: string
srcExclude?: string[]
outDir?: string

@ -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}`
}

1
types/shared.d.ts vendored

@ -109,6 +109,7 @@ export interface Header {
export interface SiteData<ThemeConfig = any> {
base: string
assetsBase: string
cleanUrls?: boolean
lang: string
dir: string

Loading…
Cancel
Save