From b102d041857a5dc097aa97df9c080fe23a26870d Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 13 Mar 2024 17:18:44 -0700 Subject: [PATCH] feat(vite): handle multiple/custom public dirs (#26163) --- .../nuxt/src/core/plugins/layer-aliasing.ts | 28 +++----- packages/schema/src/config/vite.ts | 5 +- packages/vite/src/plugins/public-dirs.ts | 65 +++++++++++++++++++ packages/vite/src/vite.ts | 3 + test/basic.test.ts | 8 +++ test/fixtures/basic/custom-public/file.svg | 18 +++++ test/fixtures/basic/nuxt.config.ts | 6 ++ test/fixtures/basic/pages/assets-custom.vue | 6 ++ 8 files changed, 116 insertions(+), 23 deletions(-) create mode 100644 packages/vite/src/plugins/public-dirs.ts create mode 100644 test/fixtures/basic/custom-public/file.svg create mode 100644 test/fixtures/basic/pages/assets-custom.vue diff --git a/packages/nuxt/src/core/plugins/layer-aliasing.ts b/packages/nuxt/src/core/plugins/layer-aliasing.ts index f73f67297f09..250bdcc50bcc 100644 --- a/packages/nuxt/src/core/plugins/layer-aliasing.ts +++ b/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 { @@ -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, prefix: string, publicDir: false | string }> = {} + const aliases: Record> = {} 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) @@ -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 }) } @@ -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 { diff --git a/packages/schema/src/config/vite.ts b/packages/schema/src/config/vite.ts index 072adf3dba2c..070c3eb4b814 100644 --- a/packages/schema/src/config/vite.ts +++ b/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' @@ -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, get('dir') as Promise>]).then(([srcDir, dir]) => resolve(srcDir, dir.public)) + return false } }, vue: { diff --git a/packages/vite/src/plugins/public-dirs.ts b/packages/vite/src/plugins/public-dirs.ts new file mode 100644 index 000000000000..ea36b90e4776 --- /dev/null +++ b/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 + } + } + } + } + } +}) diff --git a/packages/vite/src/vite.ts b/packages/vite/src/vite.ts index a72fdb34adae..9c11437d9012 100644 --- a/packages/vite/src/vite.ts +++ b/packages/vite/src/vite.ts @@ -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 @@ -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, diff --git a/test/basic.test.ts b/test/basic.test.ts index de935d2728aa..e7cd7e15acbe 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -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 () => { diff --git a/test/fixtures/basic/custom-public/file.svg b/test/fixtures/basic/custom-public/file.svg new file mode 100644 index 000000000000..c31de56afcf1 --- /dev/null +++ b/test/fixtures/basic/custom-public/file.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/test/fixtures/basic/nuxt.config.ts b/test/fixtures/basic/nuxt.config.ts index d51bef276f27..7269a31f135c 100644 --- a/test/fixtures/basic/nuxt.config.ts +++ b/test/fixtures/basic/nuxt.config.ts @@ -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 diff --git a/test/fixtures/basic/pages/assets-custom.vue b/test/fixtures/basic/pages/assets-custom.vue new file mode 100644 index 000000000000..2eb1ba9b0f17 --- /dev/null +++ b/test/fixtures/basic/pages/assets-custom.vue @@ -0,0 +1,6 @@ +