Skip to content

Commit

Permalink
feat: add assetsBase config to support serving assets from CDN
Browse files Browse the repository at this point in the history
refactor: rename `publicPath` option to `assetsBase`
  • Loading branch information
nilennoct committed Dec 4, 2023
1 parent 6be8037 commit d2e8a3b
Show file tree
Hide file tree
Showing 11 changed files with 160 additions and 24 deletions.
19 changes: 19 additions & 0 deletions docs/reference/site-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,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
Expand Down
4 changes: 3 additions & 1 deletion src/client/app/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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(
Expand Down
21 changes: 18 additions & 3 deletions src/node/build/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ import { packageDirectorySync } from 'pkg-dir'
import { rimraf } from 'rimraf'
import { pathToFileURL } from 'url'
import type { BuildOptions, Rollup } from 'vite'
import { resolveConfig, type SiteConfig } from '../config'
import { normalizeBaseUrl, resolveConfig, type SiteConfig } from '../config'
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'
Expand All @@ -25,8 +30,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) {
Expand All @@ -39,6 +53,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,
Expand Down Expand Up @@ -72,7 +87,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
Expand Down
19 changes: 19 additions & 0 deletions src/node/build/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { APP_PATH } from '../alias'
import type { SiteConfig } from '../config'
import { createVitePressPlugin } from '../plugin'
import { sanitizeFileName, slash } from '../shared'
import { normalizeAssetUrl } from '../utils/assetsBase'
import { task } from '../utils/task'
import { buildMPAClient } from './buildMPAClient'

Expand Down Expand Up @@ -66,6 +67,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,
Expand Down
20 changes: 16 additions & 4 deletions src/node/build/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
type PageData,
type SSGContext
} from '../shared'
import { normalizeAssetUrl } from '../utils/assetsBase'
import { version } from '../../../package.json'

export async function renderPage(
Expand Down Expand Up @@ -67,7 +68,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 =
Expand Down Expand Up @@ -99,7 +103,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)
}
])

Expand Down Expand Up @@ -143,7 +149,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>`
}
}
}
Expand All @@ -168,7 +177,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)}
Expand Down
16 changes: 14 additions & 2 deletions src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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, '')
Expand Down Expand Up @@ -236,13 +237,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
Expand Down Expand Up @@ -293,3 +301,7 @@ function resolveSiteDataHead(userConfig?: UserConfig): HeadConfig[] {

return head
}

export function normalizeBaseUrl(baseUrl: string) {
return baseUrl.replace(/^([^/])/, '/$1').replace(/([^/])$/, '$1/')
}
51 changes: 39 additions & 12 deletions src/node/serve/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import path from 'path'
import polka, { type IOptions } from 'polka'
import sirv, { type RequestHandler } 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) {
Expand Down Expand Up @@ -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
}
4 changes: 2 additions & 2 deletions src/node/server.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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
}

Expand Down
1 change: 1 addition & 0 deletions src/node/siteConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface UserConfig<ThemeConfig = any>
extends?: RawConfigExports<ThemeConfig>

base?: string
assetsBase?: string
srcDir?: string
srcExclude?: string[]
outDir?: string
Expand Down
28 changes: 28 additions & 0 deletions src/node/utils/assetsBase.ts
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions types/shared.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export interface Header {

export interface SiteData<ThemeConfig = any> {
base: string
assetsBase: string
cleanUrls?: boolean
lang: string
dir: string
Expand Down

0 comments on commit d2e8a3b

Please sign in to comment.