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') 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 // Disable loading-screen
// @ts-ignore // @ts-ignore
nuxt.options.build.loadingScreen = false nuxt.options.build.loadingScreen = false

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import * as rollup from 'rollup'
import fse from 'fs-extra' import fse from 'fs-extra'
import { printFSTree } from './utils/tree' import { printFSTree } from './utils/tree'
import { getRollupConfig } from './rollup/config' 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 { NitroContext } from './context'
import { scanMiddleware } from './server/middleware' import { scanMiddleware } from './server/middleware'
@ -30,20 +30,17 @@ async function cleanupDir (dir: string) {
export async function generate (nitroContext: NitroContext) { export async function generate (nitroContext: NitroContext) {
consola.start('Generating public...') consola.start('Generating public...')
await nitroContext._internal.hooks.callHook('nitro:generate', nitroContext)
const publicDir = nitroContext._nuxt.publicDir const publicDir = nitroContext._nuxt.publicDir
let publicFiles: string[] = []
if (await isDirectory(publicDir)) { if (await isDirectory(publicDir)) {
publicFiles = readDirRecursively(publicDir).map(r => r.replace(publicDir, ''))
await fse.copy(publicDir, nitroContext.output.publicDir) await fse.copy(publicDir, nitroContext.output.publicDir)
} }
const clientDist = resolve(nitroContext._nuxt.buildDir, 'dist/client') const clientDist = resolve(nitroContext._nuxt.buildDir, 'dist/client')
if (await isDirectory(clientDist)) { if (await isDirectory(clientDist)) {
await fse.copy(clientDist, join(nitroContext.output.publicDir, nitroContext._nuxt.publicPath), { const buildAssetsDir = join(nitroContext.output.publicDir, nitroContext._nuxt.buildAssetsDir)
// TODO: Workaround vite's issue that duplicates public files await fse.copy(clientDist, buildAssetsDir)
// https://github.com/nuxt/framework/issues/1192
filter: src => !publicFiles.includes(src.replace(clientDist, ''))
})
} }
consola.success('Generated public ' + prettyPath(nitroContext.output.publicDir)) 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:document': (htmlTemplate: { src: string, contents: string, dst: string }) => void
'nitro:rollup:before': (context: NitroContext) => void | Promise<void> 'nitro:rollup:before': (context: NitroContext) => void | Promise<void>
'nitro:compiled': (context: NitroContext) => void 'nitro:compiled': (context: NitroContext) => void
'nitro:generate': (context: NitroContext) => void | Promise<void>
'close': () => void 'close': () => void
} }
@ -71,8 +72,8 @@ export interface NitroContext {
generateDir: string generateDir: string
publicDir: string publicDir: string
serverDir: string serverDir: string
routerBase: string baseURL: string
publicPath: string buildAssetsDir: string
isStatic: boolean isStatic: boolean
fullStatic: boolean fullStatic: boolean
staticAssets: any staticAssets: any
@ -139,8 +140,8 @@ export function getNitroContext (nuxtOptions: NuxtOptions, input: NitroInput): N
generateDir: nuxtOptions.generate.dir, generateDir: nuxtOptions.generate.dir,
publicDir: resolve(nuxtOptions.srcDir, nuxtOptions.dir.public || nuxtOptions.dir.static), publicDir: resolve(nuxtOptions.srcDir, nuxtOptions.dir.public || nuxtOptions.dir.static),
serverDir: resolve(nuxtOptions.srcDir, (nuxtOptions.dir as any).server || 'server'), serverDir: resolve(nuxtOptions.srcDir, (nuxtOptions.dir as any).server || 'server'),
routerBase: nuxtOptions.router.base, baseURL: nuxtOptions.app.baseURL,
publicPath: nuxtOptions.app.assetsPath, buildAssetsDir: nuxtOptions.app.buildAssetsDir,
isStatic: nuxtOptions.target === 'static' && !nuxtOptions.dev, isStatic: nuxtOptions.target === 'static' && !nuxtOptions.dev,
fullStatic: nuxtOptions.target === 'static' && !nuxtOptions._legacyGenerate, fullStatic: nuxtOptions.target === 'static' && !nuxtOptions._legacyGenerate,
staticAssets: nuxtOptions.generate.staticAssets, staticAssets: nuxtOptions.generate.staticAssets,

View File

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

View File

@ -159,8 +159,6 @@ export const getRollupConfig = (nitroContext: NitroContext) => {
'process.server': 'true', 'process.server': 'true',
'process.client': 'false', 'process.client': 'false',
'process.env.NUXT_NO_SSR': JSON.stringify(!nitroContext._nuxt.ssr), '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_BASE': JSON.stringify(nitroContext._nuxt.staticAssets.base),
'process.env.NUXT_STATIC_VERSION': JSON.stringify(nitroContext._nuxt.staticAssets.version), 'process.env.NUXT_STATIC_VERSION': JSON.stringify(nitroContext._nuxt.staticAssets.version),
'process.env.NUXT_FULL_STATIC': nitroContext._nuxt.fullStatic as unknown as string, 'process.env.NUXT_FULL_STATIC': nitroContext._nuxt.fullStatic as unknown as string,
@ -227,6 +225,7 @@ export const getRollupConfig = (nitroContext: NitroContext) => {
entries: { entries: {
'#nitro': nitroContext._internal.runtimeDir, '#nitro': nitroContext._internal.runtimeDir,
'#nitro-renderer': resolve(nitroContext._internal.runtimeDir, 'app', renderer), '#nitro-renderer': resolve(nitroContext._internal.runtimeDir, 'app', renderer),
'#paths': resolve(nitroContext._internal.runtimeDir, 'app/paths'),
'#config': resolve(nitroContext._internal.runtimeDir, 'app/config'), '#config': resolve(nitroContext._internal.runtimeDir, 'app/config'),
'#nitro-vue-renderer': vue2ServerRenderer, '#nitro-vue-renderer': vue2ServerRenderer,
// Only file and data URLs are supported by the default ESM loader on Windows (#427) // 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 // Named exports
export const privateConfig = deepFreeze(defu(_runtimeConfig.private, _runtimeConfig.public)) export const privateConfig = deepFreeze(defu(_runtimeConfig.private, _runtimeConfig.public))
export const publicConfig = deepFreeze(_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 { createRenderer } from 'vue-bundle-renderer'
import devalue from '@nuxt/devalue' import devalue from '@nuxt/devalue'
import { privateConfig, publicConfig } from './config' import { privateConfig, publicConfig } from './config'
import { buildAssetsURL } from './paths'
// @ts-ignore // @ts-ignore
import htmlTemplate from '#build/views/document.template.mjs' 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 getClientManifest = cachedImport(() => import('#build/dist/server/client.manifest.mjs'))
const getSSRApp = !process.env.NUXT_NO_SSR && cachedImport(() => import('#build/dist/server/server.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 () => { const getSSRRenderer = cachedResult(async () => {
// Load client manifest // Load client manifest
const clientManifest = await getClientManifest() const clientManifest = await getClientManifest()
@ -23,7 +22,7 @@ const getSSRRenderer = cachedResult(async () => {
if (!createSSRApp) { throw new Error('Server bundle is not available') } if (!createSSRApp) { throw new Error('Server bundle is not available') }
// Create renderer // Create renderer
const { renderToString } = await import('#nitro-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 () => { const getSPARenderer = cachedResult(async () => {
@ -50,13 +49,13 @@ const getSPARenderer = cachedResult(async () => {
entryFiles entryFiles
.flatMap(({ css }) => css) .flatMap(({ css }) => css)
.filter(css => css != null) .filter(css => css != null)
.map(file => `<link rel="stylesheet" href="${publicPath}${file}">`) .map(file => `<link rel="stylesheet" href="${buildAssetsURL(file)}">`)
.join(''), .join(''),
renderScripts: () => renderScripts: () =>
entryFiles entryFiles
.map(({ file }) => { .map(({ file }) => {
const isMJS = !file.endsWith('.js') const isMJS = !file.endsWith('.js')
return `<script ${isMJS ? 'type="module"' : ''} src="${publicPath}${file}"></script>` return `<script ${isMJS ? 'type="module"' : ''} src="${buildAssetsURL(file)}"></script>`
}) })
.join('') .join('')
} }

View File

@ -1,9 +1,9 @@
import '#polyfill' 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 { localCall } from '../server'
import { requestHasBody, useRequestBody } from '../server/utils' import { requestHasBody, useRequestBody } from '../server/utils'
import { buildAssetsURL, baseURL } from '#paths'
const PUBLIC_PATH = process.env.PUBLIC_PATH // Default: /_nuxt/
addEventListener('fetch', (event: any) => { addEventListener('fetch', (event: any) => {
event.respondWith(handleEvent(event)) event.respondWith(handleEvent(event))
@ -11,7 +11,7 @@ addEventListener('fetch', (event: any) => {
async function handleEvent (event) { async function handleEvent (event) {
try { try {
return await getAssetFromKV(event, { cacheControl: assetsCacheControl }) return await getAssetFromKV(event, { cacheControl: assetsCacheControl, mapRequestToAsset: baseURLModifier })
} catch (_err) { } catch (_err) {
// Ignore // Ignore
} }
@ -42,7 +42,7 @@ async function handleEvent (event) {
} }
function assetsCacheControl (request) { function assetsCacheControl (request) {
if (request.url.includes(PUBLIC_PATH) /* TODO: Check with routerBase */) { if (request.url.startsWith(buildAssetsURL())) {
return { return {
browserTTL: 31536000, browserTTL: 31536000,
edgeTTL: 31536000 edgeTTL: 31536000
@ -50,3 +50,8 @@ function assetsCacheControl (request) {
} }
return {} 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 { Server as HttpsServer } from 'https'
import destr from 'destr' import destr from 'destr'
import { handle } from '../server' import { handle } from '../server'
import { baseURL } from '#paths'
const cert = process.env.NITRO_SSL_CERT const cert = process.env.NITRO_SSL_CERT
const key = process.env.NITRO_SSL_KEY const key = process.env.NITRO_SSL_KEY
@ -19,7 +20,7 @@ server.listen(port, hostname, (err) => {
process.exit(1) process.exit(1)
} }
const protocol = cert && key ? 'https' : 'http' const protocol = cert && key ? 'https' : 'http'
console.log(`Listening on ${protocol}://${hostname}:${port}`) console.log(`Listening on ${protocol}://${hostname}:${port}${baseURL()}`)
}) })
export default {} export default {}

View File

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

View File

@ -2,9 +2,10 @@ import { createError } from 'h3'
import { withoutTrailingSlash, withLeadingSlash, parseURL } from 'ufo' import { withoutTrailingSlash, withLeadingSlash, parseURL } from 'ufo'
// @ts-ignore // @ts-ignore
import { getAsset, readAsset } from '#static' import { getAsset, readAsset } from '#static'
import { buildAssetsDir } from '#paths'
const METHODS = ['HEAD', 'GET'] const METHODS = ['HEAD', 'GET']
const PUBLIC_PATH = process.env.PUBLIC_PATH // Default: /_nuxt/
const TWO_DAYS = 2 * 60 * 60 * 24 const TWO_DAYS = 2 * 60 * 60 * 24
const STATIC_ASSETS_BASE = process.env.NUXT_STATIC_BASE + '/' + process.env.NUXT_STATIC_VERSION 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 (!asset) {
if (id.startsWith(PUBLIC_PATH) && !id.startsWith(STATIC_ASSETS_BASE)) { if (isBuildAsset && !id.startsWith(STATIC_ASSETS_BASE)) {
throw createError({ throw createError({
statusMessage: 'Cannot find static asset ' + id, statusMessage: 'Cannot find static asset ' + id,
statusCode: 404 statusCode: 404
@ -62,7 +65,7 @@ export default async function serveStatic (req, res) {
res.setHeader('Last-Modified', asset.mtime) res.setHeader('Last-Modified', asset.mtime)
} }
if (id.startsWith(PUBLIC_PATH)) { if (isBuildAsset) {
res.setHeader('Cache-Control', `max-age=${TWO_DAYS}, immutable`) 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 serveStatic from 'serve-static'
import { resolve } from 'pathe' import { resolve } from 'pathe'
import connect from 'connect' import connect from 'connect'
import { joinURL } from 'ufo'
import type { NitroContext } from '../context' import type { NitroContext } from '../context'
import { handleVfs } from './vfs' import { handleVfs } from './vfs'
@ -76,8 +77,9 @@ export function createDevServer (nitroContext: NitroContext) {
const app = createApp() const app = createApp()
// _nuxt and static // _nuxt and static
app.use(nitroContext._nuxt.publicPath, serveStatic(resolve(nitroContext._nuxt.buildDir, 'dist/client'))) const buildAssetsURL = joinURL(nitroContext._nuxt.baseURL, nitroContext._nuxt.buildAssetsDir)
app.use(nitroContext._nuxt.routerBase, serveStatic(resolve(nitroContext._nuxt.publicDir))) 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 // debugging endpoint to view vfs
app.use('/_vfs', useBase('/_vfs', handleVfs(nitroContext))) app.use('/_vfs', useBase('/_vfs', handleVfs(nitroContext)))
@ -89,7 +91,7 @@ export function createDevServer (nitroContext: NitroContext) {
app.use(devMiddleware.middleware) app.use(devMiddleware.middleware)
// serve placeholder 404 assets instead of hitting SSR // serve placeholder 404 assets instead of hitting SSR
app.use(nitroContext._nuxt.publicPath, servePlaceholder()) app.use(buildAssetsURL, servePlaceholder())
// SSR Proxy // SSR Proxy
const proxy = httpProxy.createProxy() const proxy = httpProxy.createProxy()

View File

@ -23,10 +23,10 @@ export interface ServerMiddleware {
promisify?: boolean // Default is true 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) => { return files.map((file) => {
const route = joinURL( const route = joinURL(
basePath, baseURL,
file file
.slice(0, file.length - extname(file).length) .slice(0, file.length - extname(file).length)
.replace(/\/index$/, '') .replace(/\/index$/, '')

View File

@ -1,5 +1,5 @@
import { createRequire } from 'module' import { createRequire } from 'module'
import { relative, dirname, join, resolve } from 'pathe' import { relative, dirname, resolve } from 'pathe'
import fse from 'fs-extra' import fse from 'fs-extra'
import jiti from 'jiti' import jiti from 'jiti'
import defu from 'defu' import defu from 'defu'
@ -152,11 +152,3 @@ export function readPackageJson (
throw error 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 const runtimeConfig: PrivateRuntimeConfig & PublicRuntimeConfig
export default runtimeConfig 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.hooks.addHooks(nitroContext.nuxtHooks)
nuxt.hook('close', () => nitroContext._internal.hooks.callHook('close')) 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:document', template => nuxt.callHook('nitro:document', template))
nitroContext._internal.hooks.hook('nitro:generate', ctx => nuxt.callHook('nitro:generate', ctx))
// @ts-ignore // @ts-ignore
nuxt.hooks.addHooks(nitroDevContext.nuxtHooks) nuxt.hooks.addHooks(nitroDevContext.nuxtHooks)

View File

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

View File

@ -1,7 +1,6 @@
import { resolve, join } from 'pathe' import { resolve, join } from 'pathe'
import { existsSync, readdirSync } from 'fs' import { existsSync, readdirSync } from 'fs'
import defu from 'defu' import defu from 'defu'
import { isRelative, joinURL, hasProtocol } from 'ufo'
export default { export default {
/** Vue.js config */ /** Vue.js config */
@ -29,16 +28,40 @@ export default {
/** /**
* Nuxt App configuration. * Nuxt App configuration.
* @version 2 * @version 2
* @version 3
*/ */
app: { app: {
$resolve: (val, get) => { /**
const useCDN = hasProtocol(get('build.publicPath'), true) && !get('dev') * The base path of your Nuxt application.
const isRelativePublicPath = isRelative(get('build.publicPath')) *
return defu(val, { * This can be set at runtime by setting the BASE_PATH environment variable.
basePath: get('router.base'), * @example
assetsPath: isRelativePublicPath ? get('build.publicPath') : useCDN ? '/' : joinURL(get('router.base'), get('build.publicPath')), * ```bash
cdnURL: useCDN ? get('build.publicPath') : null * 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) * @see [vue@3 documentation](https://v3.vuejs.org/guide/transitions-enterleave.html)
* @version 2 * @version 2
*/ */
pageTransition: { pageTransition: {
$resolve: (val, get) => { $resolve: (val, get) => {
val = typeof val === 'string' ? { name: val } : val val = typeof val === 'string' ? { name: val } : val
return defu(val, { return defu(val, {

View File

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

View File

@ -160,7 +160,7 @@ export default {
* The full path to the directory underneath `/_nuxt/` where static assets * The full path to the directory underneath `/_nuxt/` where static assets
* (payload, state and manifest files) will live. * (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. */ /** 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')) }, 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). */ /** 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 * This can be useful if you need to serve Nuxt as a different context root, from
* within a bigger web site. * within a bigger web site.
* @version 2 * @version 2
* @version 3
*/ */
base: { base: {
$resolve: (val = '/') => withTrailingSlash(normalizeURL(val)) $resolve: (val, get) => val ? withTrailingSlash(normalizeURL(val)) : get('app.baseURL')
}, },
/** @private */ /** @private */

View File

@ -80,6 +80,7 @@ export interface NuxtHooks {
// @nuxt/nitro // @nuxt/nitro
'nitro:document': (template: { src: string, contents: string }) => HookResult 'nitro:document': (template: { src: string, contents: string }) => HookResult
'nitro:context': (context: any) => HookResult 'nitro:context': (context: any) => HookResult
'nitro:generate': (context: any) => HookResult
// @nuxt/cli // @nuxt/cli
'generate:cache:ignore': (ignore: string[]) => HookResult '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 viteJsxPlugin from '@vitejs/plugin-vue-jsx'
import type { Connect } from 'vite' import type { Connect } from 'vite'
import { joinURL, withoutLeadingSlash } from 'ufo'
import { cacheDirPlugin } from './plugins/cache-dir' import { cacheDirPlugin } from './plugins/cache-dir'
import { analyzePlugin } from './plugins/analyze' import { analyzePlugin } from './plugins/analyze'
import { wpfs } from './utils/wpfs' import { wpfs } from './utils/wpfs'
import type { ViteBuildContext, ViteOptions } from './vite' import type { ViteBuildContext, ViteOptions } from './vite'
import { writeManifest } from './manifest' import { writeManifest } from './manifest'
import { devStyleSSRPlugin } from './plugins/dev-ssr-css' import { devStyleSSRPlugin } from './plugins/dev-ssr-css'
import { DynamicBasePlugin } from './plugins/dynamic-base'
export async function buildClient (ctx: ViteBuildContext) { export async function buildClient (ctx: ViteBuildContext) {
const clientConfig: vite.InlineConfig = vite.mergeConfig(ctx.config, { const clientConfig: vite.InlineConfig = vite.mergeConfig(ctx.config, {
@ -25,6 +27,7 @@ export async function buildClient (ctx: ViteBuildContext) {
} }
}, },
build: { build: {
assetsDir: ctx.nuxt.options.dev ? withoutLeadingSlash(ctx.nuxt.options.app.buildAssetsDir) : '.',
rollupOptions: { rollupOptions: {
output: { output: {
chunkFileNames: ctx.nuxt.options.dev ? undefined : '[name]-[hash].mjs', 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'), cacheDirPlugin(ctx.nuxt.options.rootDir, 'client'),
vuePlugin(ctx.config.vue), vuePlugin(ctx.config.vue),
viteJsxPlugin(), 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: { server: {
middlewareMode: true middlewareMode: true

View File

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

View File

@ -1,7 +1,13 @@
import { joinURL } from 'ufo'
import { Plugin } from 'vite' import { Plugin } from 'vite'
import { isCSS } from '../utils' import { isCSS } from '../utils'
export function devStyleSSRPlugin (rootDir: string): Plugin { export interface DevStyleSSRPluginOptions {
rootDir: string
buildAssetsURL: string
}
export function devStyleSSRPlugin (options: DevStyleSSRPluginOptions): Plugin {
return { return {
name: 'nuxt:dev-style-ssr', name: 'nuxt:dev-style-ssr',
apply: 'serve', apply: 'serve',
@ -12,13 +18,13 @@ export function devStyleSSRPlugin (rootDir: string): Plugin {
} }
let moduleId = id let moduleId = id
if (moduleId.startsWith(rootDir)) { if (moduleId.startsWith(options.rootDir)) {
moduleId = moduleId.slice(rootDir.length) moduleId = moduleId.slice(options.rootDir.length)
} }
// When dev `<style>` is injected, remove the `<link>` styles from manifest // When dev `<style>` is injected, remove the `<link>` styles from manifest
// TODO: Use `app.assetsPath` or unique hash const selector = joinURL(options.buildAssetsURL, moduleId)
return code + `\ndocument.querySelectorAll(\`link[href="/_nuxt${moduleId}"]\`).forEach(i=>i.remove())` 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 * as vite from 'vite'
import vuePlugin from '@vitejs/plugin-vue' import vuePlugin from '@vitejs/plugin-vue'
import viteJsxPlugin from '@vitejs/plugin-vue-jsx' import viteJsxPlugin from '@vitejs/plugin-vue-jsx'
@ -6,12 +6,14 @@ import fse from 'fs-extra'
import pDebounce from 'p-debounce' import pDebounce from 'p-debounce'
import consola from 'consola' import consola from 'consola'
import { resolveModule } from '@nuxt/kit' import { resolveModule } from '@nuxt/kit'
import { withoutTrailingSlash } from 'ufo'
import { ViteBuildContext, ViteOptions } from './vite' import { ViteBuildContext, ViteOptions } from './vite'
import { wpfs } from './utils/wpfs' import { wpfs } from './utils/wpfs'
import { cacheDirPlugin } from './plugins/cache-dir' import { cacheDirPlugin } from './plugins/cache-dir'
import { DynamicBasePlugin } from './plugins/dynamic-base'
import { bundleRequest } from './dev-bundler' import { bundleRequest } from './dev-bundler'
import { writeManifest } from './manifest' import { writeManifest } from './manifest'
import { isCSS } from './utils' import { isCSS, isDirectory, readDirRecursively } from './utils'
export async function buildServer (ctx: ViteBuildContext) { export async function buildServer (ctx: ViteBuildContext) {
const _resolve = id => resolveModule(id, { paths: ctx.nuxt.options.modulesDir }) const _resolve = id => resolveModule(id, { paths: ctx.nuxt.options.modulesDir })
@ -38,7 +40,7 @@ export async function buildServer (ctx: ViteBuildContext) {
} }
}, },
ssr: { ssr: {
external: [], external: ['#config'],
noExternal: [ noExternal: [
...ctx.nuxt.options.build.transpile, ...ctx.nuxt.options.build.transpile,
// TODO: Use externality for production (rollup) build // TODO: Use externality for production (rollup) build
@ -75,12 +77,41 @@ export async function buildServer (ctx: ViteBuildContext) {
plugins: [ plugins: [
cacheDirPlugin(ctx.nuxt.options.rootDir, 'server'), cacheDirPlugin(ctx.nuxt.options.rootDir, 'server'),
vuePlugin(ctx.config.vue), vuePlugin(ctx.config.vue),
DynamicBasePlugin.vite({ env: ctx.nuxt.options.dev ? 'dev' : 'server', devAppConfig: ctx.nuxt.options.app }),
viteJsxPlugin() viteJsxPlugin()
] ]
} as ViteOptions) } as ViteOptions)
await ctx.nuxt.callHook('vite:extendConfig', serverConfig, { isClient: false, isServer: true }) 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) const onBuild = () => ctx.nuxt.callHook('build:resources', wpfs)
// Production build // Production build

View File

@ -1,4 +1,6 @@
import { createHash } from 'crypto' import { createHash } from 'crypto'
import { promises as fsp, readdirSync, statSync } from 'fs'
import { join } from 'pathe'
export function uniq<T> (arr: T[]): T[] { export function uniq<T> (arr: T[]): T[] {
return Array.from(new Set(arr)) return Array.from(new Set(arr))
@ -32,3 +34,19 @@ export function hash (input: string, length = 8) {
.digest('hex') .digest('hex')
.slice(0, length) .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 { InlineConfig, SSROptions } from 'vite'
import type { Options } from '@vitejs/plugin-vue' import type { Options } from '@vitejs/plugin-vue'
import { sanitizeFilePath } from 'mlly' import { sanitizeFilePath } from 'mlly'
import { joinURL, withoutLeadingSlash } from 'ufo'
import { buildClient } from './client' import { buildClient } from './client'
import { buildServer } from './server' import { buildServer } from './server'
import virtual from './plugins/virtual' import virtual from './plugins/virtual'
@ -47,7 +48,9 @@ export async function bundle (nuxt: Nuxt) {
'abort-controller': 'unenv/runtime/mock/empty' '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), publicDir: resolve(nuxt.options.srcDir, nuxt.options.dir.public),
// TODO: move to kit schema when it exists // TODO: move to kit schema when it exists
vue: { vue: {
@ -71,6 +74,7 @@ export async function bundle (nuxt: Nuxt) {
}, },
clearScreen: false, clearScreen: false,
build: { build: {
assetsDir: withoutLeadingSlash(nuxt.options.app.buildAssetsDir),
emptyOutDir: false, emptyOutDir: false,
rollupOptions: { rollupOptions: {
input: resolve(nuxt.options.appDir, 'entry'), input: resolve(nuxt.options.appDir, 'entry'),

View File

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

View File

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

View File

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

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