Skip to content

Commit

Permalink
feat(vite): handle multiple/custom public dirs (#26163)
Browse files Browse the repository at this point in the history
  • Loading branch information
danielroe committed Mar 14, 2024
1 parent 2561751 commit b102d04
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 23 deletions.
28 changes: 8 additions & 20 deletions packages/nuxt/src/core/plugins/layer-aliasing.ts
@@ -1,8 +1,7 @@
import { existsSync, readdirSync } from 'node:fs'
import { createUnplugin } from 'unplugin'
import type { NuxtConfigLayer } from 'nuxt/schema'
import { resolveAlias } from '@nuxt/kit'
import { join, normalize, relative } from 'pathe'
import { normalize } from 'pathe'
import MagicString from 'magic-string'

interface LayerAliasingOptions {
Expand All @@ -17,21 +16,16 @@ const ALIAS_RE = /(?<=['"])[~@]{1,2}(?=\/)/g
const ALIAS_RE_SINGLE = /(?<=['"])[~@]{1,2}(?=\/)/

export const LayerAliasingPlugin = createUnplugin((options: LayerAliasingOptions) => {
const aliases: Record<string, { aliases: Record<string, string>, prefix: string, publicDir: false | string }> = {}
const aliases: Record<string, Record<string, string>> = {}
for (const layer of options.layers) {
const srcDir = layer.config.srcDir || layer.cwd
const rootDir = layer.config.rootDir || layer.cwd
const publicDir = join(srcDir, layer.config?.dir?.public || 'public')

aliases[srcDir] = {
aliases: {
'~': layer.config?.alias?.['~'] || srcDir,
'@': layer.config?.alias?.['@'] || srcDir,
'~~': layer.config?.alias?.['~~'] || rootDir,
'@@': layer.config?.alias?.['@@'] || rootDir
},
prefix: relative(options.root, publicDir),
publicDir: !options.dev && existsSync(publicDir) && publicDir
'~': layer.config?.alias?.['~'] || srcDir,
'@': layer.config?.alias?.['@'] || srcDir,
'~~': layer.config?.alias?.['~~'] || rootDir,
'@@': layer.config?.alias?.['@@'] || rootDir
}
}
const layers = Object.keys(aliases).sort((a, b) => b.length - a.length)
Expand All @@ -48,13 +42,7 @@ export const LayerAliasingPlugin = createUnplugin((options: LayerAliasingOptions
const layer = layers.find(l => importer.startsWith(l))
if (!layer) { return }

const publicDir = aliases[layer].publicDir
if (id.startsWith('/') && publicDir && readdirSync(publicDir).some(file => file === id.slice(1) || id.startsWith('/' + file + '/'))) {
const resolvedId = '/' + join(aliases[layer].prefix, id.slice(1))
return await this.resolve(resolvedId, importer, { skipSelf: true })
}

const resolvedId = resolveAlias(id, aliases[layer].aliases)
const resolvedId = resolveAlias(id, aliases[layer])
if (resolvedId !== id) {
return await this.resolve(resolvedId, importer, { skipSelf: true })
}
Expand All @@ -76,7 +64,7 @@ export const LayerAliasingPlugin = createUnplugin((options: LayerAliasingOptions
if (!layer || !ALIAS_RE_SINGLE.test(code)) { return }

const s = new MagicString(code)
s.replace(ALIAS_RE, r => aliases[layer].aliases[r as '~'] || r)
s.replace(ALIAS_RE, r => aliases[layer][r as '~'] || r)

if (s.hasChanged()) {
return {
Expand Down
5 changes: 2 additions & 3 deletions packages/schema/src/config/vite.ts
@@ -1,5 +1,4 @@
import { consola } from 'consola'
import { resolve } from 'pathe'
import { isTest } from 'std-env'
import { withoutLeadingSlash } from 'ufo'
import { defineUntypedSchema } from 'untyped'
Expand Down Expand Up @@ -36,11 +35,11 @@ export default defineUntypedSchema({
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
},
publicDir: {
$resolve: async (val, get) => {
$resolve: (val) => {
if (val) {
consola.warn('Directly configuring the `vite.publicDir` option is not supported. Instead, set `dir.public`. You can read more in `https://nuxt.com/docs/api/nuxt-config#public`.')
}
return val ?? await Promise.all([get('srcDir') as Promise<string>, get('dir') as Promise<Record<string, string>>]).then(([srcDir, dir]) => resolve(srcDir, dir.public))
return false
}
},
vue: {
Expand Down
65 changes: 65 additions & 0 deletions packages/vite/src/plugins/public-dirs.ts
@@ -0,0 +1,65 @@
import { existsSync } from 'node:fs'
import { useNitro } from '@nuxt/kit'
import { createUnplugin } from 'unplugin'
import { withLeadingSlash, withTrailingSlash } from 'ufo'
import { dirname, relative } from 'pathe'

const PREFIX = 'virtual:public?'

export const VitePublicDirsPlugin = createUnplugin(() => {
const nitro = useNitro()

function resolveFromPublicAssets (id: string) {
for (const dir of nitro.options.publicAssets) {
if (!id.startsWith(withTrailingSlash(dir.baseURL || '/'))) { continue }
const path = id.replace(withTrailingSlash(dir.baseURL || '/'), withTrailingSlash(dir.dir))
if (existsSync(path)) {
return id
}
}
}

return {
name: 'nuxt:vite-public-dir-resolution',
vite: {
load: {
enforce: 'pre',
handler (id, options) {
if (id.startsWith(PREFIX)) {
const helper = !options?.ssr || nitro.options.imports !== false ? '' : 'globalThis.'
return `export default ${helper}__publicAssetsURL(${JSON.stringify(decodeURIComponent(id.slice(PREFIX.length)))})`
}
}
},
resolveId: {
enforce: 'post',
handler (id) {
if (id === '/__skip_vite' || !id.startsWith('/') || id.startsWith('/@fs')) { return }

if (resolveFromPublicAssets(id)) {
return PREFIX + encodeURIComponent(id)
}
}
},
generateBundle (outputOptions, bundle) {
for (const file in bundle) {
const chunk = bundle[file]
if (!file.endsWith('.css') || chunk.type !== 'asset') { continue }

let css = chunk.source.toString()
let wasReplaced = false
for (const [full, url] of css.matchAll(/url\((\/[^)]+)\)/g)) {
if (resolveFromPublicAssets(url)) {
const relativeURL = relative(withLeadingSlash(dirname(file)), url)
css = css.replace(full, `url(${relativeURL})`)
wasReplaced = true
}
}
if (wasReplaced) {
chunk.source = css
}
}
}
}
}
})
3 changes: 3 additions & 0 deletions packages/vite/src/vite.ts
Expand Up @@ -17,6 +17,7 @@ import { resolveCSSOptions } from './css'
import { composableKeysPlugin } from './plugins/composable-keys'
import { logLevelMap } from './utils/logger'
import { ssrStylesPlugin } from './plugins/ssr-styles'
import { VitePublicDirsPlugin } from './plugins/public-dirs'

export interface ViteBuildContext {
nuxt: Nuxt
Expand Down Expand Up @@ -98,6 +99,8 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
}
},
plugins: [
// add resolver for files in public assets directories
VitePublicDirsPlugin.vite(),
composableKeysPlugin.vite({
sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client,
rootDir: nuxt.options.rootDir,
Expand Down
8 changes: 8 additions & 0 deletions test/basic.test.ts
Expand Up @@ -1815,6 +1815,14 @@ describe.runIf(isDev() && (!isWindows || !isCI))('detecting invalid root nodes',
})
})

describe('public directories', () => {
it('should directly return public directory paths', async () => {
const html = await $fetch('/assets-custom')
expect(html).toContain('"/public.svg"')
expect(html).toContain('"/custom/file.svg"')
})
})

// TODO: dynamic paths in dev
describe.skipIf(isDev())('dynamic paths', () => {
it('should work with no overrides', async () => {
Expand Down
18 changes: 18 additions & 0 deletions test/fixtures/basic/custom-public/file.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions test/fixtures/basic/nuxt.config.ts
Expand Up @@ -46,6 +46,12 @@ export default defineNuxtConfig({
'./extends/node_modules/foo'
],
nitro: {
publicAssets: [
{
dir: '../custom-public',
baseURL: '/custom'
}
],
esbuild: {
options: {
// in order to test bigint serialization
Expand Down
6 changes: 6 additions & 0 deletions test/fixtures/basic/pages/assets-custom.vue
@@ -0,0 +1,6 @@
<template>
<div>
<img src="/public.svg">
<img src="/custom/file.svg">
</div>
</template>

0 comments on commit b102d04

Please sign in to comment.