feat: improve base url options (#2655)

This commit is contained in:
Daniel Roe 2022-01-18 16:59:14 +00:00 committed by GitHub
parent 05e75426ce
commit d07d572263
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 298 additions and 98 deletions

View File

@ -15,6 +15,13 @@ export function setupNitroBridge () {
throw new Error('[nitro] Please use `nuxt generate` for static target')
}
// Handle legacy property name `assetsPath`
nuxt.options.app.buildAssetsDir = nuxt.options.app.buildAssetsDir || nuxt.options.app.assetsPath
nuxt.options.app.assetsPath = nuxt.options.app.buildAssetsDir
// Nitro expects app config on `config.app` rather than `config._app`
nuxt.options.publicRuntimeConfig.app = nuxt.options.publicRuntimeConfig.app || {}
Object.assign(nuxt.options.publicRuntimeConfig.app, nuxt.options.publicRuntimeConfig._app)
// Disable loading-screen
// @ts-ignore
nuxt.options.build.loadingScreen = false

View File

@ -3,6 +3,7 @@ import * as vite from 'vite'
import { createVuePlugin } from 'vite-plugin-vue2'
import PluginLegacy from '@vitejs/plugin-legacy'
import consola from 'consola'
import { joinURL } from 'ufo'
import { devStyleSSRPlugin } from '../../../vite/src/plugins/dev-ssr-css'
import { jsxPlugin } from './plugins/jsx'
import { ViteBuildContext, ViteOptions } from './types'
@ -28,7 +29,6 @@ export async function buildClient (ctx: ViteBuildContext) {
},
build: {
outDir: resolve(ctx.nuxt.options.buildDir, 'dist/client'),
assetsDir: '.',
rollupOptions: {
input: resolve(ctx.nuxt.options.buildDir, 'client.js')
},
@ -39,7 +39,10 @@ export async function buildClient (ctx: ViteBuildContext) {
jsxPlugin(),
createVuePlugin(ctx.config.vue),
PluginLegacy(),
devStyleSSRPlugin(ctx.nuxt.options.rootDir)
devStyleSSRPlugin({
rootDir: ctx.nuxt.options.rootDir,
buildAssetsURL: joinURL(ctx.nuxt.options.app.baseURL, ctx.nuxt.options.app.buildAssetsDir)
})
],
server: {
middlewareMode: true

View File

@ -46,7 +46,6 @@ export async function prepareManifests (ctx: ViteBuildContext) {
export async function generateBuildManifest (ctx: ViteBuildContext) {
const rDist = (...args: string[]): string => resolve(ctx.nuxt.options.buildDir, 'dist', ...args)
const publicPath = ctx.nuxt.options.app.assetsPath // Default: /nuxt/
const viteClientManifest = await fse.readJSON(rDist('client/manifest.json'))
const clientEntries = Object.entries(viteClientManifest)
@ -59,12 +58,13 @@ export async function generateBuildManifest (ctx: ViteBuildContext) {
const polyfillName = initialEntries.find(id => id.startsWith('polyfills-legacy.'))
// @vitejs/plugin-legacy uses SystemJS which need to call `System.import` to load modules
const clientImports = initialJs.filter(id => id !== polyfillName).map(id => publicPath + id)
const clientImports = initialJs.filter(id => id !== polyfillName)
const clientEntryCode = `var imports = ${JSON.stringify(clientImports)}\nimports.reduce((p, id) => p.then(() => System.import(id)), Promise.resolve())`
const clientEntryName = 'entry-legacy.' + hash(clientEntryCode) + '.js'
const clientManifest = {
publicPath,
// This publicPath will be ignored by Nitro and computed dynamically
publicPath: ctx.nuxt.options.app.buildAssetsDir,
all: uniq([
polyfillName,
clientEntryName,
@ -74,12 +74,12 @@ export async function generateBuildManifest (ctx: ViteBuildContext) {
polyfillName,
clientEntryName,
...initialAssets
],
].filter(Boolean),
async: [
// We move initial entries to the client entry
...initialJs,
...asyncEntries
],
].filter(Boolean),
modules: {},
assetsMapping: {}
}

View File

@ -51,7 +51,6 @@ export async function buildServer (ctx: ViteBuildContext) {
},
build: {
outDir: resolve(ctx.nuxt.options.buildDir, 'dist/server'),
assetsDir: ctx.nuxt.options.app.assetsPath.replace(/^\/|\/$/, ''),
ssr: true,
ssrManifest: true,
rollupOptions: {

View File

@ -1,6 +1,7 @@
import { resolve } from 'pathe'
import * as vite from 'vite'
import consola from 'consola'
import { withoutLeadingSlash } from 'ufo'
import { distDir } from '../dirs'
import { warmupViteServer } from '../../../vite/src/utils/warmup'
import { buildClient } from './client'
@ -73,6 +74,7 @@ async function bundle (nuxt: Nuxt, builder: any) {
publicDir: resolve(nuxt.options.srcDir, nuxt.options.dir.static),
clearScreen: false,
build: {
assetsDir: withoutLeadingSlash(nuxt.options.app.buildAssetsDir),
emptyOutDir: false
},
plugins: [

View File

@ -4,7 +4,7 @@ import * as rollup from 'rollup'
import fse from 'fs-extra'
import { printFSTree } from './utils/tree'
import { getRollupConfig } from './rollup/config'
import { hl, prettyPath, serializeTemplate, writeFile, isDirectory, readDirRecursively, replaceAll } from './utils'
import { hl, prettyPath, serializeTemplate, writeFile, isDirectory, replaceAll } from './utils'
import { NitroContext } from './context'
import { scanMiddleware } from './server/middleware'
@ -30,20 +30,17 @@ async function cleanupDir (dir: string) {
export async function generate (nitroContext: NitroContext) {
consola.start('Generating public...')
await nitroContext._internal.hooks.callHook('nitro:generate', nitroContext)
const publicDir = nitroContext._nuxt.publicDir
let publicFiles: string[] = []
if (await isDirectory(publicDir)) {
publicFiles = readDirRecursively(publicDir).map(r => r.replace(publicDir, ''))
await fse.copy(publicDir, nitroContext.output.publicDir)
}
const clientDist = resolve(nitroContext._nuxt.buildDir, 'dist/client')
if (await isDirectory(clientDist)) {
await fse.copy(clientDist, join(nitroContext.output.publicDir, nitroContext._nuxt.publicPath), {
// TODO: Workaround vite's issue that duplicates public files
// https://github.com/nuxt/framework/issues/1192
filter: src => !publicFiles.includes(src.replace(clientDist, ''))
})
const buildAssetsDir = join(nitroContext.output.publicDir, nitroContext._nuxt.buildAssetsDir)
await fse.copy(clientDist, buildAssetsDir)
}
consola.success('Generated public ' + prettyPath(nitroContext.output.publicDir))

View File

@ -19,6 +19,7 @@ export interface NitroHooks {
'nitro:document': (htmlTemplate: { src: string, contents: string, dst: string }) => void
'nitro:rollup:before': (context: NitroContext) => void | Promise<void>
'nitro:compiled': (context: NitroContext) => void
'nitro:generate': (context: NitroContext) => void | Promise<void>
'close': () => void
}
@ -71,8 +72,8 @@ export interface NitroContext {
generateDir: string
publicDir: string
serverDir: string
routerBase: string
publicPath: string
baseURL: string
buildAssetsDir: string
isStatic: boolean
fullStatic: boolean
staticAssets: any
@ -139,8 +140,8 @@ export function getNitroContext (nuxtOptions: NuxtOptions, input: NitroInput): N
generateDir: nuxtOptions.generate.dir,
publicDir: resolve(nuxtOptions.srcDir, nuxtOptions.dir.public || nuxtOptions.dir.static),
serverDir: resolve(nuxtOptions.srcDir, (nuxtOptions.dir as any).server || 'server'),
routerBase: nuxtOptions.router.base,
publicPath: nuxtOptions.app.assetsPath,
baseURL: nuxtOptions.app.baseURL,
buildAssetsDir: nuxtOptions.app.buildAssetsDir,
isStatic: nuxtOptions.target === 'static' && !nuxtOptions.dev,
fullStatic: nuxtOptions.target === 'static' && !nuxtOptions._legacyGenerate,
staticAssets: nuxtOptions.generate.staticAssets,

View File

@ -1,17 +1,19 @@
import { existsSync, promises as fsp } from 'fs'
import { resolve } from 'pathe'
import consola from 'consola'
import { joinURL } from 'ufo'
import { extendPreset, prettyPath } from '../utils'
import { NitroPreset, NitroContext, NitroInput } from '../context'
import { worker } from './worker'
export const browser: NitroPreset = extendPreset(worker, (input: NitroInput) => {
const routerBase = input._nuxt.routerBase
// TODO: Join base at runtime
const baseURL = input._nuxt.baseURL
const script = `<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('${routerBase}sw.js');
navigator.serviceWorker.register('${joinURL(baseURL, 'sw.js')}');
});
}
</script>`
@ -21,11 +23,11 @@ if ('serviceWorker' in navigator) {
<html>
<head>
<meta charset="utf-8">
<link rel="prefetch" href="${routerBase}sw.js">
<link rel="prefetch" href="${routerBase}_server/index.mjs">
<link rel="prefetch" href="${joinURL(baseURL, 'sw.js')}">
<link rel="prefetch" href="${joinURL(baseURL, '_server/index.mjs')}">
<script>
async function register () {
const registration = await navigator.serviceWorker.register('${routerBase}sw.js')
const registration = await navigator.serviceWorker.register('${joinURL(baseURL, 'sw.js')}')
await navigator.serviceWorker.ready
registration.active.addEventListener('statechange', (event) => {
if (event.target.state === 'activated') {
@ -62,7 +64,7 @@ if ('serviceWorker' in navigator) {
tmpl.contents = tmpl.contents.replace('</body>', script + '</body>')
},
async 'nitro:compiled' ({ output }: NitroContext) {
await fsp.writeFile(resolve(output.publicDir, 'sw.js'), `self.importScripts('${input._nuxt.routerBase}_server/index.mjs');`, 'utf8')
await fsp.writeFile(resolve(output.publicDir, 'sw.js'), `self.importScripts('${joinURL(baseURL, '_server/index.mjs')}');`, 'utf8')
// Temp fix
if (!existsSync(resolve(output.publicDir, 'index.html'))) {

View File

@ -159,8 +159,6 @@ export const getRollupConfig = (nitroContext: NitroContext) => {
'process.server': 'true',
'process.client': 'false',
'process.env.NUXT_NO_SSR': JSON.stringify(!nitroContext._nuxt.ssr),
'process.env.ROUTER_BASE': JSON.stringify(nitroContext._nuxt.routerBase),
'process.env.PUBLIC_PATH': JSON.stringify(nitroContext._nuxt.publicPath),
'process.env.NUXT_STATIC_BASE': JSON.stringify(nitroContext._nuxt.staticAssets.base),
'process.env.NUXT_STATIC_VERSION': JSON.stringify(nitroContext._nuxt.staticAssets.version),
'process.env.NUXT_FULL_STATIC': nitroContext._nuxt.fullStatic as unknown as string,
@ -227,6 +225,7 @@ export const getRollupConfig = (nitroContext: NitroContext) => {
entries: {
'#nitro': nitroContext._internal.runtimeDir,
'#nitro-renderer': resolve(nitroContext._internal.runtimeDir, 'app', renderer),
'#paths': resolve(nitroContext._internal.runtimeDir, 'app/paths'),
'#config': resolve(nitroContext._internal.runtimeDir, 'app/config'),
'#nitro-vue-renderer': vue2ServerRenderer,
// Only file and data URLs are supported by the default ESM loader on Windows (#427)

View File

@ -11,6 +11,12 @@ for (const type of ['private', 'public']) {
}
}
// Load dynamic app configuration
const appConfig = _runtimeConfig.public.app
appConfig.baseURL = process.env.NUXT_APP_BASE_URL || appConfig.baseURL
appConfig.cdnURL = process.env.NUXT_APP_CDN_URL || appConfig.cdnURL
appConfig.buildAssetsDir = process.env.NUXT_APP_BUILD_ASSETS_DIR || appConfig.buildAssetsDir
// Named exports
export const privateConfig = deepFreeze(defu(_runtimeConfig.private, _runtimeConfig.public))
export const publicConfig = deepFreeze(_runtimeConfig.public)

View File

@ -0,0 +1,19 @@
import { joinURL } from 'ufo'
import config from '#config'
export function baseURL () {
return config.app.baseURL
}
export function buildAssetsDir () {
return config.app.buildAssetsDir
}
export function buildAssetsURL (...path: string[]) {
return joinURL(publicAssetsURL(), config.app.buildAssetsDir, ...path)
}
export function publicAssetsURL (...path: string[]) {
const publicBase = config.app.cdnURL || config.app.baseURL
return path.length ? joinURL(publicBase, ...path) : publicBase
}

View File

@ -2,6 +2,7 @@ import type { ServerResponse } from 'http'
import { createRenderer } from 'vue-bundle-renderer'
import devalue from '@nuxt/devalue'
import { privateConfig, publicConfig } from './config'
import { buildAssetsURL } from './paths'
// @ts-ignore
import htmlTemplate from '#build/views/document.template.mjs'
@ -12,8 +13,6 @@ const PAYLOAD_JS = '/payload.js'
const getClientManifest = cachedImport(() => import('#build/dist/server/client.manifest.mjs'))
const getSSRApp = !process.env.NUXT_NO_SSR && cachedImport(() => import('#build/dist/server/server.mjs'))
const publicPath = (publicConfig.app && publicConfig.app.assetsPath) || process.env.PUBLIC_PATH || '/_nuxt'
const getSSRRenderer = cachedResult(async () => {
// Load client manifest
const clientManifest = await getClientManifest()
@ -23,7 +22,7 @@ const getSSRRenderer = cachedResult(async () => {
if (!createSSRApp) { throw new Error('Server bundle is not available') }
// Create renderer
const { renderToString } = await import('#nitro-renderer')
return createRenderer((createSSRApp), { clientManifest, renderToString, publicPath }).renderToString
return createRenderer((createSSRApp), { clientManifest, renderToString, publicPath: buildAssetsURL() }).renderToString
})
const getSPARenderer = cachedResult(async () => {
@ -50,13 +49,13 @@ const getSPARenderer = cachedResult(async () => {
entryFiles
.flatMap(({ css }) => css)
.filter(css => css != null)
.map(file => `<link rel="stylesheet" href="${publicPath}${file}">`)
.map(file => `<link rel="stylesheet" href="${buildAssetsURL(file)}">`)
.join(''),
renderScripts: () =>
entryFiles
.map(({ file }) => {
const isMJS = !file.endsWith('.js')
return `<script ${isMJS ? 'type="module"' : ''} src="${publicPath}${file}"></script>`
return `<script ${isMJS ? 'type="module"' : ''} src="${buildAssetsURL(file)}"></script>`
})
.join('')
}

View File

@ -1,9 +1,9 @@
import '#polyfill'
import { getAssetFromKV } from '@cloudflare/kv-asset-handler'
import { getAssetFromKV, mapRequestToAsset } from '@cloudflare/kv-asset-handler'
import { withoutBase } from 'ufo'
import { localCall } from '../server'
import { requestHasBody, useRequestBody } from '../server/utils'
const PUBLIC_PATH = process.env.PUBLIC_PATH // Default: /_nuxt/
import { buildAssetsURL, baseURL } from '#paths'
addEventListener('fetch', (event: any) => {
event.respondWith(handleEvent(event))
@ -11,7 +11,7 @@ addEventListener('fetch', (event: any) => {
async function handleEvent (event) {
try {
return await getAssetFromKV(event, { cacheControl: assetsCacheControl })
return await getAssetFromKV(event, { cacheControl: assetsCacheControl, mapRequestToAsset: baseURLModifier })
} catch (_err) {
// Ignore
}
@ -42,7 +42,7 @@ async function handleEvent (event) {
}
function assetsCacheControl (request) {
if (request.url.includes(PUBLIC_PATH) /* TODO: Check with routerBase */) {
if (request.url.startsWith(buildAssetsURL())) {
return {
browserTTL: 31536000,
edgeTTL: 31536000
@ -50,3 +50,8 @@ function assetsCacheControl (request) {
}
return {}
}
const baseURLModifier = (request: Request) => {
const url = withoutBase(request.url, baseURL())
return mapRequestToAsset(new Request(url, request))
}

View File

@ -3,6 +3,7 @@ import { Server as HttpServer } from 'http'
import { Server as HttpsServer } from 'https'
import destr from 'destr'
import { handle } from '../server'
import { baseURL } from '#paths'
const cert = process.env.NITRO_SSL_CERT
const key = process.env.NITRO_SSL_KEY
@ -19,7 +20,7 @@ server.listen(port, hostname, (err) => {
process.exit(1)
}
const protocol = cert && key ? 'https' : 'http'
console.log(`Listening on ${protocol}://${hostname}:${port}`)
console.log(`Listening on ${protocol}://${hostname}:${port}${baseURL()}`)
})
export default {}

View File

@ -1,8 +1,8 @@
import '../app/config'
import { createApp, useBase } from 'h3'
import { createFetch, Headers } from 'ohmyfetch'
import destr from 'destr'
import { createCall, createFetch as createLocalFetch } from 'unenv/runtime/fetch/index'
import { baseURL } from '../app/paths'
import { timingMiddleware } from './timing'
import { handleError } from './error'
// @ts-ignore
@ -18,7 +18,7 @@ app.use(serverMiddleware)
app.use(() => import('../app/render').then(e => e.renderMiddleware), { lazy: true })
export const stack = app.stack
export const handle = useBase(process.env.ROUTER_BASE, app)
export const handle = useBase(baseURL(), app)
export const localCall = createCall(handle)
export const localFetch = createLocalFetch(localCall, globalThis.fetch)

View File

@ -2,9 +2,10 @@ import { createError } from 'h3'
import { withoutTrailingSlash, withLeadingSlash, parseURL } from 'ufo'
// @ts-ignore
import { getAsset, readAsset } from '#static'
import { buildAssetsDir } from '#paths'
const METHODS = ['HEAD', 'GET']
const PUBLIC_PATH = process.env.PUBLIC_PATH // Default: /_nuxt/
const TWO_DAYS = 2 * 60 * 60 * 24
const STATIC_ASSETS_BASE = process.env.NUXT_STATIC_BASE + '/' + process.env.NUXT_STATIC_VERSION
@ -26,8 +27,10 @@ export default async function serveStatic (req, res) {
}
}
const isBuildAsset = id.startsWith(buildAssetsDir())
if (!asset) {
if (id.startsWith(PUBLIC_PATH) && !id.startsWith(STATIC_ASSETS_BASE)) {
if (isBuildAsset && !id.startsWith(STATIC_ASSETS_BASE)) {
throw createError({
statusMessage: 'Cannot find static asset ' + id,
statusCode: 404
@ -62,7 +65,7 @@ export default async function serveStatic (req, res) {
res.setHeader('Last-Modified', asset.mtime)
}
if (id.startsWith(PUBLIC_PATH)) {
if (isBuildAsset) {
res.setHeader('Cache-Control', `max-age=${TWO_DAYS}, immutable`)
}

View File

@ -12,6 +12,7 @@ import servePlaceholder from 'serve-placeholder'
import serveStatic from 'serve-static'
import { resolve } from 'pathe'
import connect from 'connect'
import { joinURL } from 'ufo'
import type { NitroContext } from '../context'
import { handleVfs } from './vfs'
@ -76,8 +77,9 @@ export function createDevServer (nitroContext: NitroContext) {
const app = createApp()
// _nuxt and static
app.use(nitroContext._nuxt.publicPath, serveStatic(resolve(nitroContext._nuxt.buildDir, 'dist/client')))
app.use(nitroContext._nuxt.routerBase, serveStatic(resolve(nitroContext._nuxt.publicDir)))
const buildAssetsURL = joinURL(nitroContext._nuxt.baseURL, nitroContext._nuxt.buildAssetsDir)
app.use(buildAssetsURL, serveStatic(resolve(nitroContext._nuxt.buildDir, 'dist/client')))
app.use(nitroContext._nuxt.baseURL, serveStatic(resolve(nitroContext._nuxt.publicDir)))
// debugging endpoint to view vfs
app.use('/_vfs', useBase('/_vfs', handleVfs(nitroContext)))
@ -89,7 +91,7 @@ export function createDevServer (nitroContext: NitroContext) {
app.use(devMiddleware.middleware)
// serve placeholder 404 assets instead of hitting SSR
app.use(nitroContext._nuxt.publicPath, servePlaceholder())
app.use(buildAssetsURL, servePlaceholder())
// SSR Proxy
const proxy = httpProxy.createProxy()

View File

@ -23,10 +23,10 @@ export interface ServerMiddleware {
promisify?: boolean // Default is true
}
function filesToMiddleware (files: string[], baseDir: string, basePath: string, overrides?: Partial<ServerMiddleware>): ServerMiddleware[] {
function filesToMiddleware (files: string[], baseDir: string, baseURL: string, overrides?: Partial<ServerMiddleware>): ServerMiddleware[] {
return files.map((file) => {
const route = joinURL(
basePath,
baseURL,
file
.slice(0, file.length - extname(file).length)
.replace(/\/index$/, '')

View File

@ -1,5 +1,5 @@
import { createRequire } from 'module'
import { relative, dirname, join, resolve } from 'pathe'
import { relative, dirname, resolve } from 'pathe'
import fse from 'fs-extra'
import jiti from 'jiti'
import defu from 'defu'
@ -152,11 +152,3 @@ export function readPackageJson (
throw error
}
}
export function readDirRecursively (dir: string) {
return fse.readdirSync(dir).reduce((files, file) => {
const name = join(dir, file)
const isDirectory = fse.statSync(name).isDirectory()
return isDirectory ? [...files, ...readDirRecursively(name)] : [...files, name]
}, [])
}

View File

@ -21,3 +21,11 @@ declare module '#config' {
const runtimeConfig: PrivateRuntimeConfig & PublicRuntimeConfig
export default runtimeConfig
}
declare module '#paths' {
export const baseURL: () => string
export const buildAssetsDir: () => string
export const buildAssetsURL: (...path: string[]) => string
export const publicAssetsURL: (...path: string[]) => string
}

View File

@ -20,6 +20,7 @@ export function initNitro (nuxt: Nuxt) {
nuxt.hooks.addHooks(nitroContext.nuxtHooks)
nuxt.hook('close', () => nitroContext._internal.hooks.callHook('close'))
nitroContext._internal.hooks.hook('nitro:document', template => nuxt.callHook('nitro:document', template))
nitroContext._internal.hooks.hook('nitro:generate', ctx => nuxt.callHook('nitro:generate', ctx))
// @ts-ignore
nuxt.hooks.addHooks(nitroDevContext.nuxtHooks)

View File

@ -8,7 +8,7 @@ import {
import NuxtNestedPage from './nested-page.vue'
import NuxtPage from './page.vue'
import NuxtLayout from './layout'
import { defineNuxtPlugin } from '#app'
import { defineNuxtPlugin, useRuntimeConfig } from '#app'
// @ts-ignore
import routes from '#build/routes'
@ -29,9 +29,10 @@ export default defineNuxtPlugin((nuxtApp) => {
// TODO: remove before release - present for backwards compatibility & intentionally undocumented
nuxtApp.vueApp.component('NuxtChild', NuxtNestedPage)
const { baseURL } = useRuntimeConfig().app
const routerHistory = process.client
? createWebHistory()
: createMemoryHistory()
? createWebHistory(baseURL)
: createMemoryHistory(baseURL)
const router = createRouter({
history: routerHistory,

View File

@ -1,7 +1,6 @@
import { resolve, join } from 'pathe'
import { existsSync, readdirSync } from 'fs'
import defu from 'defu'
import { isRelative, joinURL, hasProtocol } from 'ufo'
export default {
/** Vue.js config */
@ -29,16 +28,40 @@ export default {
/**
* Nuxt App configuration.
* @version 2
* @version 3
*/
app: {
$resolve: (val, get) => {
const useCDN = hasProtocol(get('build.publicPath'), true) && !get('dev')
const isRelativePublicPath = isRelative(get('build.publicPath'))
return defu(val, {
basePath: get('router.base'),
assetsPath: isRelativePublicPath ? get('build.publicPath') : useCDN ? '/' : joinURL(get('router.base'), get('build.publicPath')),
cdnURL: useCDN ? get('build.publicPath') : null
})
/**
* The base path of your Nuxt application.
*
* This can be set at runtime by setting the BASE_PATH environment variable.
* @example
* ```bash
* BASE_PATH=/prefix/ node .output/server/index.mjs
* ```
*/
baseURL: '/',
/** The folder name for the built site assets, relative to `baseURL` (or `cdnURL` if set). This is set at build time and should not be customized at runtime. */
buildAssetsDir: '/_nuxt/',
/**
* The folder name for the built site assets, relative to `baseURL` (or `cdnURL` if set).
* @deprecated - use `buildAssetsDir` instead
* @version 2
*/
assetsPath: {
$resolve: (val, get) => val ?? get('buildAssetsDir')
},
/**
* An absolute URL to serve the public folder from (production-only).
*
* This can be set to a different value at runtime by setting the CDN_URL environment variable.
* @example
* ```bash
* CDN_URL=https://mycdn.org/ node .output/server/index.mjs
* ```
*/
cdnURL: {
$resolve: (val, get) => get('dev') ? null : val || null
}
},
@ -302,7 +325,7 @@ export default {
* @see [vue@3 documentation](https://v3.vuejs.org/guide/transitions-enterleave.html)
* @version 2
*/
pageTransition: {
pageTransition: {
$resolve: (val, get) => {
val = typeof val === 'string' ? { name: val } : val
return defu(val, {

View File

@ -1,5 +1,5 @@
import { isCI, isTest } from 'std-env'
import { hasProtocol } from 'ufo'
import { normalizeURL, withTrailingSlash } from 'ufo'
export default {
/**
@ -153,13 +153,9 @@ export default {
* }
* ```
* @version 2
* @version 3
*/
publicPath: {
$resolve: (val, get) => {
if (hasProtocol(val, true) && get('dev')) { val = null }
return (val || '/_nuxt/').replace(/([^/])$/, '$1/')
}
$resolve: (val, get) => val ? withTrailingSlash(normalizeURL(val)) : get('app.buildAssetsDir')
},
/**

View File

@ -160,7 +160,7 @@ export default {
* The full path to the directory underneath `/_nuxt/` where static assets
* (payload, state and manifest files) will live.
*/
base: { $resolve: (val, get) => val || joinURL(get('app.assetsPath'), get('generate.dir')) },
base: { $resolve: (val, get) => val || joinURL(get('app.buildAssetsDir'), get('generate.dir')) },
/** The full path to the versioned directory where static assets for the current buidl are located. */
versionBase: { $resolve: (val, get) => val || joinURL(get('generate.base'), get('generate.version')) },
/** A unique string to uniquely identify payload versions (defaults to the current timestamp). */

View File

@ -16,10 +16,9 @@ export default {
* This can be useful if you need to serve Nuxt as a different context root, from
* within a bigger web site.
* @version 2
* @version 3
*/
base: {
$resolve: (val = '/') => withTrailingSlash(normalizeURL(val))
$resolve: (val, get) => val ? withTrailingSlash(normalizeURL(val)) : get('app.baseURL')
},
/** @private */

View File

@ -80,6 +80,7 @@ export interface NuxtHooks {
// @nuxt/nitro
'nitro:document': (template: { src: string, contents: string }) => HookResult
'nitro:context': (context: any) => HookResult
'nitro:generate': (context: any) => HookResult
// @nuxt/cli
'generate:cache:ignore': (ignore: string[]) => HookResult

View File

@ -5,12 +5,14 @@ import vuePlugin from '@vitejs/plugin-vue'
import viteJsxPlugin from '@vitejs/plugin-vue-jsx'
import type { Connect } from 'vite'
import { joinURL, withoutLeadingSlash } from 'ufo'
import { cacheDirPlugin } from './plugins/cache-dir'
import { analyzePlugin } from './plugins/analyze'
import { wpfs } from './utils/wpfs'
import type { ViteBuildContext, ViteOptions } from './vite'
import { writeManifest } from './manifest'
import { devStyleSSRPlugin } from './plugins/dev-ssr-css'
import { DynamicBasePlugin } from './plugins/dynamic-base'
export async function buildClient (ctx: ViteBuildContext) {
const clientConfig: vite.InlineConfig = vite.mergeConfig(ctx.config, {
@ -25,6 +27,7 @@ export async function buildClient (ctx: ViteBuildContext) {
}
},
build: {
assetsDir: ctx.nuxt.options.dev ? withoutLeadingSlash(ctx.nuxt.options.app.buildAssetsDir) : '.',
rollupOptions: {
output: {
chunkFileNames: ctx.nuxt.options.dev ? undefined : '[name]-[hash].mjs',
@ -38,7 +41,11 @@ export async function buildClient (ctx: ViteBuildContext) {
cacheDirPlugin(ctx.nuxt.options.rootDir, 'client'),
vuePlugin(ctx.config.vue),
viteJsxPlugin(),
devStyleSSRPlugin(ctx.nuxt.options.rootDir)
DynamicBasePlugin.vite({ env: 'client', devAppConfig: ctx.nuxt.options.app }),
devStyleSSRPlugin({
rootDir: ctx.nuxt.options.rootDir,
buildAssetsURL: joinURL(ctx.nuxt.options.app.baseURL, ctx.nuxt.options.app.buildAssetsDir)
})
],
server: {
middlewareMode: true

View File

@ -1,5 +1,6 @@
import fse from 'fs-extra'
import { resolve } from 'pathe'
import { joinURL } from 'ufo'
import type { ViteBuildContext } from './vite'
export async function writeManifest (ctx: ViteBuildContext, extraEntries: string[] = []) {
@ -15,7 +16,7 @@ export async function writeManifest (ctx: ViteBuildContext, extraEntries: string
// Legacy dev manifest
const devClientManifest = {
publicPath: ctx.nuxt.options.build.publicPath,
publicPath: joinURL(ctx.nuxt.options.app.baseURL, ctx.nuxt.options.app.buildAssetsDir),
all: entries,
initial: entries,
async: [],

View File

@ -1,7 +1,13 @@
import { joinURL } from 'ufo'
import { Plugin } from 'vite'
import { isCSS } from '../utils'
export function devStyleSSRPlugin (rootDir: string): Plugin {
export interface DevStyleSSRPluginOptions {
rootDir: string
buildAssetsURL: string
}
export function devStyleSSRPlugin (options: DevStyleSSRPluginOptions): Plugin {
return {
name: 'nuxt:dev-style-ssr',
apply: 'serve',
@ -12,13 +18,13 @@ export function devStyleSSRPlugin (rootDir: string): Plugin {
}
let moduleId = id
if (moduleId.startsWith(rootDir)) {
moduleId = moduleId.slice(rootDir.length)
if (moduleId.startsWith(options.rootDir)) {
moduleId = moduleId.slice(options.rootDir.length)
}
// When dev `<style>` is injected, remove the `<link>` styles from manifest
// TODO: Use `app.assetsPath` or unique hash
return code + `\ndocument.querySelectorAll(\`link[href="/_nuxt${moduleId}"]\`).forEach(i=>i.remove())`
const selector = joinURL(options.buildAssetsURL, moduleId)
return code + `\ndocument.querySelectorAll(\`link[href="${selector}"]\`).forEach(i=>i.remove())`
}
}
}

View File

@ -0,0 +1,57 @@
import { createUnplugin } from 'unplugin'
interface DynamicBasePluginOptions {
env: 'dev' | 'server' | 'client'
devAppConfig?: Record<string, any>
globalPublicPath?: string
}
export const DynamicBasePlugin = createUnplugin(function (options: DynamicBasePluginOptions) {
return {
name: 'nuxt:dynamic-base-path',
resolveId (id) {
if (id.startsWith('/__NUXT_BASE__')) {
return id.replace('/__NUXT_BASE__', '')
}
},
enforce: 'post',
transform (code, id) {
if (options.globalPublicPath && id.includes('entry.ts')) {
code = 'import { joinURL } from "ufo";' +
`${options.globalPublicPath} = joinURL(NUXT_BASE, NUXT_CONFIG.app.buildAssetsDir);` + code
}
if (code.includes('NUXT_BASE') && !code.includes('const NUXT_BASE =')) {
code = 'const NUXT_BASE = NUXT_CONFIG.app.cdnURL || NUXT_CONFIG.app.baseURL;' + code
if (options.env === 'dev') {
code = `const NUXT_CONFIG = { app: ${JSON.stringify(options.devAppConfig)} };` + code
} else if (options.env === 'server') {
code = 'import NUXT_CONFIG from "#config";' + code
} else {
code = 'const NUXT_CONFIG = __NUXT__.config;' + code
}
}
if (id === 'vite/preload-helper') {
// Define vite base path as buildAssetsUrl (i.e. including _nuxt/)
code = code.replace(
/const base = ['"]\/__NUXT_BASE__\/['"]/,
'import { joinURL } from "ufo";' +
'const base = joinURL(NUXT_BASE, NUXT_CONFIG.app.buildAssetsDir);')
}
// Sanitize imports
code = code.replace(/from *['"]\/__NUXT_BASE__(\/[^'"]*)['"]/g, 'from "$1"')
// Dynamically compute string URLs featuring baseURL
for (const delimiter of ['`', '"', "'"]) {
const delimiterRE = new RegExp(`${delimiter}([^${delimiter}]*)\\/__NUXT_BASE__\\/([^${delimiter}]*)${delimiter}`, 'g')
/* eslint-disable-next-line no-template-curly-in-string */
code = code.replace(delimiterRE, '`$1${NUXT_BASE}$2`')
}
return code
}
}
})

View File

@ -1,4 +1,4 @@
import { resolve, normalize } from 'pathe'
import { join, resolve, normalize } from 'pathe'
import * as vite from 'vite'
import vuePlugin from '@vitejs/plugin-vue'
import viteJsxPlugin from '@vitejs/plugin-vue-jsx'
@ -6,12 +6,14 @@ import fse from 'fs-extra'
import pDebounce from 'p-debounce'
import consola from 'consola'
import { resolveModule } from '@nuxt/kit'
import { withoutTrailingSlash } from 'ufo'
import { ViteBuildContext, ViteOptions } from './vite'
import { wpfs } from './utils/wpfs'
import { cacheDirPlugin } from './plugins/cache-dir'
import { DynamicBasePlugin } from './plugins/dynamic-base'
import { bundleRequest } from './dev-bundler'
import { writeManifest } from './manifest'
import { isCSS } from './utils'
import { isCSS, isDirectory, readDirRecursively } from './utils'
export async function buildServer (ctx: ViteBuildContext) {
const _resolve = id => resolveModule(id, { paths: ctx.nuxt.options.modulesDir })
@ -38,7 +40,7 @@ export async function buildServer (ctx: ViteBuildContext) {
}
},
ssr: {
external: [],
external: ['#config'],
noExternal: [
...ctx.nuxt.options.build.transpile,
// TODO: Use externality for production (rollup) build
@ -75,12 +77,41 @@ export async function buildServer (ctx: ViteBuildContext) {
plugins: [
cacheDirPlugin(ctx.nuxt.options.rootDir, 'server'),
vuePlugin(ctx.config.vue),
DynamicBasePlugin.vite({ env: ctx.nuxt.options.dev ? 'dev' : 'server', devAppConfig: ctx.nuxt.options.app }),
viteJsxPlugin()
]
} as ViteOptions)
await ctx.nuxt.callHook('vite:extendConfig', serverConfig, { isClient: false, isServer: true })
ctx.nuxt.hook('nitro:generate', async () => {
const clientDist = resolve(ctx.nuxt.options.buildDir, 'dist/client')
// Remove public files that have been duplicated into buildAssetsDir
// TODO: Add option to configure this behaviour in vite
const publicDir = join(ctx.nuxt.options.srcDir, ctx.nuxt.options.dir.public)
let publicFiles: string[] = []
if (await isDirectory(publicDir)) {
publicFiles = readDirRecursively(publicDir).map(r => r.replace(publicDir, ''))
for (const file of publicFiles) {
try {
fse.rmSync(join(clientDist, file))
} catch {}
}
}
// Copy doubly-nested /_nuxt/_nuxt files into buildAssetsDir
// TODO: Workaround vite issue
if (await isDirectory(clientDist)) {
const nestedAssetsPath = withoutTrailingSlash(join(clientDist, ctx.nuxt.options.app.buildAssetsDir))
if (await isDirectory(nestedAssetsPath)) {
await fse.copy(nestedAssetsPath, clientDist, { recursive: true })
await fse.remove(nestedAssetsPath)
}
}
})
const onBuild = () => ctx.nuxt.callHook('build:resources', wpfs)
// Production build

View File

@ -1,4 +1,6 @@
import { createHash } from 'crypto'
import { promises as fsp, readdirSync, statSync } from 'fs'
import { join } from 'pathe'
export function uniq<T> (arr: T[]): T[] {
return Array.from(new Set(arr))
@ -32,3 +34,19 @@ export function hash (input: string, length = 8) {
.digest('hex')
.slice(0, length)
}
export function readDirRecursively (dir: string) {
return readdirSync(dir).reduce((files, file) => {
const name = join(dir, file)
const isDirectory = statSync(name).isDirectory()
return isDirectory ? [...files, ...readDirRecursively(name)] : [...files, name]
}, [])
}
export async function isDirectory (path: string) {
try {
return (await fsp.stat(path)).isDirectory()
} catch (_err) {
return false
}
}

View File

@ -5,6 +5,7 @@ import type { Nuxt } from '@nuxt/schema'
import type { InlineConfig, SSROptions } from 'vite'
import type { Options } from '@vitejs/plugin-vue'
import { sanitizeFilePath } from 'mlly'
import { joinURL, withoutLeadingSlash } from 'ufo'
import { buildClient } from './client'
import { buildServer } from './server'
import virtual from './plugins/virtual'
@ -47,7 +48,9 @@ export async function bundle (nuxt: Nuxt) {
'abort-controller': 'unenv/runtime/mock/empty'
}
},
base: nuxt.options.build.publicPath,
base: nuxt.options.dev
? joinURL(nuxt.options.app.baseURL, nuxt.options.app.buildAssetsDir)
: '/__NUXT_BASE__/',
publicDir: resolve(nuxt.options.srcDir, nuxt.options.dir.public),
// TODO: move to kit schema when it exists
vue: {
@ -71,6 +74,7 @@ export async function bundle (nuxt: Nuxt) {
},
clearScreen: false,
build: {
assetsDir: withoutLeadingSlash(nuxt.options.app.buildAssetsDir),
emptyOutDir: false,
rollupOptions: {
input: resolve(nuxt.options.appDir, 'entry'),

View File

@ -4,6 +4,7 @@ import webpack from 'webpack'
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'
import type { ClientOptions } from 'webpack-hot-middleware'
import { joinURL } from 'ufo'
import { applyPresets, WebpackConfigContext } from '../utils/config'
import { nuxt } from '../presets/nuxt'
@ -53,7 +54,7 @@ function clientHMR (ctx: WebpackConfigContext) {
const hotMiddlewareClientOptions = {
reload: true,
timeout: 30000,
path: `${options.router.base}/__webpack_hmr/${ctx.name}`.replace(/\/\//g, '/'),
path: joinURL(options.app.baseURL, '__webpack_hmr', ctx.name),
...clientOptions,
ansiColors: JSON.stringify(clientOptions.ansiColors || {}),
overlayStyles: JSON.stringify(clientOptions.overlayStyles || {}),

View File

@ -45,9 +45,13 @@ function serverStandalone (ctx: WebpackConfigContext) {
'#',
...ctx.options.build.transpile
]
const external = ['#config']
if (!Array.isArray(ctx.config.externals)) { return }
ctx.config.externals.push(({ request }, cb) => {
if (external.includes(request)) {
return cb(null, true)
}
if (
request[0] === '.' ||
isAbsolute(request) ||

View File

@ -5,7 +5,7 @@ import consola from 'consola'
import webpack from 'webpack'
import FriendlyErrorsWebpackPlugin from '@nuxt/friendly-errors-webpack-plugin'
import { escapeRegExp } from 'lodash-es'
import { hasProtocol, joinURL } from 'ufo'
import { joinURL } from 'ufo'
import WarningIgnorePlugin from '../plugins/warning-ignore'
import { WebpackConfigContext, applyPresets, fileName } from '../utils/config'
@ -194,9 +194,7 @@ function getOutput (ctx: WebpackConfigContext): webpack.Configuration['output']
path: resolve(options.buildDir, 'dist', ctx.isServer ? 'server' : 'client'),
filename: fileName(ctx, 'app'),
chunkFilename: fileName(ctx, 'chunk'),
publicPath: hasProtocol(options.build.publicPath, true)
? options.build.publicPath
: joinURL(options.router.base, options.build.publicPath)
publicPath: joinURL(options.app.baseURL, options.app.buildAssetsDir)
}
}

View File

@ -13,6 +13,8 @@ import type { Context as WebpackDevMiddlewareContext, Options as WebpackDevMiddl
import type { MiddlewareOptions as WebpackHotMiddlewareOptions } from 'webpack-hot-middleware'
import type { Nuxt } from '@nuxt/schema'
import { joinURL } from 'ufo'
import { DynamicBasePlugin } from '../../vite/src/plugins/dynamic-base'
import { createMFS } from './utils/mfs'
import { client, server } from './configs'
import { createWebpackConfigContext, applyPresets, getWebpackConfig } from './utils/config'
@ -113,6 +115,11 @@ class WebpackBundler {
this.compilers = webpackConfigs.map((config) => {
// Support virtual modules (input)
config.plugins.push(this.virtualModules)
config.plugins.push(DynamicBasePlugin.webpack({
env: this.nuxt.options.dev ? 'dev' : config.name as 'client',
devAppConfig: this.nuxt.options.app,
globalPublicPath: '__webpack_public_path__'
}))
// Create compiler
const compiler = webpack(config)
@ -210,7 +217,7 @@ class WebpackBundler {
// @ts-ignore
compiler,
{
publicPath: buildOptions.publicPath,
publicPath: joinURL(this.nuxt.options.app.baseURL, this.nuxt.options.app.buildAssetsDir),
outputFileSystem: this.mfs,
stats: 'none',
...buildOptions.devMiddleware
@ -229,7 +236,7 @@ class WebpackBundler {
{
log: false,
heartbeat: 10000,
path: `/__webpack_hmr/${name}`,
path: joinURL(this.nuxt.options.app.baseURL, '__webpack_hmr', name),
...hotMiddlewareOptions
} as WebpackHotMiddlewareOptions
)