From 34894a2c41871a08a86fb9a1852563bc698d9c00 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 1 Feb 2021 10:59:42 -0500 Subject: [PATCH] fix(manifest): include assets referenced via CSS in manifest entries fix #1827 --- .../assets/__tests__/assets.spec.ts | 24 +++++- packages/playground/assets/vite.config.js | 3 +- .../__tests__/css-codesplit.spec.ts | 8 +- packages/playground/testUtils.ts | 11 +++ packages/vite/src/node/plugins/asset.ts | 17 +++-- packages/vite/src/node/plugins/css.ts | 73 +++++++++++-------- 6 files changed, 90 insertions(+), 46 deletions(-) diff --git a/packages/playground/assets/__tests__/assets.spec.ts b/packages/playground/assets/__tests__/assets.spec.ts index 045ec0d4a13a52..694d91709701d0 100644 --- a/packages/playground/assets/__tests__/assets.spec.ts +++ b/packages/playground/assets/__tests__/assets.spec.ts @@ -1,5 +1,12 @@ import { createHash } from 'crypto' -import { findAssetFile, getBg, getColor, isBuild } from '../../testUtils' +import { + findAssetFile, + getBg, + getColor, + isBuild, + listAssets, + readManifest +} from '../../testUtils' const assetMatch = isBuild ? /\/foo\/assets\/asset\.\w{8}\.png/ @@ -133,3 +140,18 @@ test('?url import', async () => { : `/foo/foo.js` ) }) + +if (isBuild) { + test('manifest', async () => { + const manifest = readManifest('foo') + const entry = manifest['index.html'] + + for (const file of listAssets('foo')) { + if (file.endsWith('.css')) { + expect(entry.css).toContain(`assets/${file}`) + } else if (!file.endsWith('.js')) { + expect(entry.assets).toContain(`assets/${file}`) + } + } + }) +} diff --git a/packages/playground/assets/vite.config.js b/packages/playground/assets/vite.config.js index e9e03509b42d82..10096153d865d2 100644 --- a/packages/playground/assets/vite.config.js +++ b/packages/playground/assets/vite.config.js @@ -5,6 +5,7 @@ module.exports = { base: '/foo/', publicDir: 'static', build: { - outDir: 'dist/foo' + outDir: 'dist/foo', + manifest: true } } diff --git a/packages/playground/css-codesplit/__tests__/css-codesplit.spec.ts b/packages/playground/css-codesplit/__tests__/css-codesplit.spec.ts index 0c4833af46decc..95fe97a1b953ba 100644 --- a/packages/playground/css-codesplit/__tests__/css-codesplit.spec.ts +++ b/packages/playground/css-codesplit/__tests__/css-codesplit.spec.ts @@ -1,6 +1,4 @@ -import fs from 'fs' -import path from 'path' -import { findAssetFile, getColor, isBuild, testDir } from '../../testUtils' +import { findAssetFile, getColor, isBuild, readManifest } from '../../testUtils' test('should load both stylesheets', async () => { expect(await getColor('h1')).toBe('red') @@ -15,9 +13,7 @@ if (isBuild) { }) test('should generate correct manifest', async () => { - const manifest = JSON.parse( - fs.readFileSync(path.join(testDir, 'dist', 'manifest.json'), 'utf-8') - ) + const manifest = readManifest() expect(manifest['index.html'].css.length).toBe(2) expect(manifest['other.js'].css.length).toBe(1) }) diff --git a/packages/playground/testUtils.ts b/packages/playground/testUtils.ts index cc69d528f828f9..7ae4281b0569ce 100644 --- a/packages/playground/testUtils.ts +++ b/packages/playground/testUtils.ts @@ -75,6 +75,11 @@ export function removeFile(filename: string) { fs.unlinkSync(path.resolve(testDir, filename)) } +export function listAssets(base = '') { + const assetsDir = path.join(testDir, 'dist', base, 'assets') + return fs.readdirSync(assetsDir) +} + export function findAssetFile(match: string | RegExp, base = '') { const assetsDir = path.join(testDir, 'dist', base, 'assets') const files = fs.readdirSync(assetsDir) @@ -84,6 +89,12 @@ export function findAssetFile(match: string | RegExp, base = '') { return file ? fs.readFileSync(path.resolve(assetsDir, file), 'utf-8') : '' } +export function readManifest(base = '') { + return JSON.parse( + fs.readFileSync(path.join(testDir, 'dist', base, 'manifest.json'), 'utf-8') + ) +} + /** * Poll a getter until the value it returns includes the expected value. */ diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index 583785b26c614f..072725c913a89a 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -59,18 +59,13 @@ export function assetPlugin(config: ResolvedConfig): Plugin { }, renderChunk(code, chunk) { - let emitted = chunkToEmittedAssetsMap.get(chunk) let match let s while ((match = assetUrlQuotedRE.exec(code))) { s = s || (s = new MagicString(code)) const [full, fileHandle, postfix = ''] = match const file = this.getFileName(fileHandle) - if (!emitted) { - emitted = new Set() - chunkToEmittedAssetsMap.set(chunk, emitted) - } - emitted.add(file) + registerAssetToChunk(chunk, file) const outputFilepath = config.base + file + postfix s.overwrite( match.index, @@ -104,6 +99,15 @@ export function assetPlugin(config: ResolvedConfig): Plugin { } } +export function registerAssetToChunk(chunk: RenderedChunk, file: string) { + let emitted = chunkToEmittedAssetsMap.get(chunk) + if (!emitted) { + emitted = new Set() + chunkToEmittedAssetsMap.set(chunk, emitted) + } + emitted.add(cleanUrl(file)) +} + export function checkPublicFile( url: string, { publicDir }: ResolvedConfig @@ -178,7 +182,6 @@ async function fileToBuiltUrl( const file = cleanUrl(id) const { search, hash } = parseUrl(id) const postfix = (search || '') + (hash || '') - // TODO preserve fragment hash or queries const content = await fsp.readFile(file) let url diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 7b38854a95cfb3..b244a66a28c87d 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -16,7 +16,6 @@ import postcssrc from 'postcss-load-config' import { NormalizedOutputOptions, OutputChunk, - PluginContext, RenderedChunk, RollupError, SourceMap @@ -31,7 +30,12 @@ import { PluginCreator } from 'postcss' import { ResolveFn, ViteDevServer } from '../' -import { assetUrlRE, fileToDevUrl, urlToBuiltUrl } from './asset' +import { + assetUrlRE, + fileToDevUrl, + registerAssetToChunk, + urlToBuiltUrl +} from './asset' import MagicString from 'magic-string' import type { ImporterReturnType, @@ -255,13 +259,45 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { return null } + // resolve asset URL placeholders to their built file URLs and perform + // minification if necessary + const process = async ( + css: string, + { + inlined, + minify + }: { + inlined: boolean + minify: boolean + } + ) => { + // replace asset url references with resolved url. + const isRelativeBase = config.base === '' || config.base.startsWith('.') + css = css.replace(assetUrlRE, (_, fileId, postfix = '') => { + const filename = this.getFileName(fileId) + postfix + registerAssetToChunk(chunk, filename) + if (!isRelativeBase || inlined) { + // absoulte base or relative base but inlined (injected as style tag into + // index.html) use the base as-is + return config.base + filename + } else { + // relative base + extracted CSS - asset file will be in the same dir + return `./${path.posix.basename(filename)}` + } + }) + if (minify && config.build.minify) { + css = await minifyCSS(css, config) + } + return css + } + if (config.build.cssCodeSplit) { if (isPureCssChunk) { // this is a shared CSS-only chunk that is empty. pureCssChunks.add(chunk.fileName) } if (opts.format === 'es') { - chunkCSS = await processChunkCSS(chunkCSS, config, this, false) + chunkCSS = await process(chunkCSS, { inlined: false, minify: true }) // emit corresponding css file const fileHandle = this.emitFile({ name: chunk.name + '.css', @@ -274,7 +310,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { ) } else if (!config.build.ssr) { // legacy build, inline css - chunkCSS = await processChunkCSS(chunkCSS, config, this, true) + chunkCSS = await process(chunkCSS, { inlined: true, minify: true }) const style = `__vite_style__` const injectCode = `var ${style} = document.createElement('style');` + @@ -292,7 +328,8 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { } } } else { - chunkCSS = await processChunkCSS(chunkCSS, config, this, false, false) + // non-split extracted CSS will be minified togethter + chunkCSS = await process(chunkCSS, { inlined: false, minify: false }) outputToExtractedCSSMap.set( opts, (outputToExtractedCSSMap.get(opts) || '') + chunkCSS @@ -663,32 +700,6 @@ function rewriteCssUrls( }) } -async function processChunkCSS( - css: string, - config: ResolvedConfig, - pluginCtx: PluginContext, - isInlined: boolean, - minify = true -): Promise { - // replace asset url references with resolved url. - const isRelativeBase = config.base === '' || config.base.startsWith('.') - css = css.replace(assetUrlRE, (_, fileId, postfix = '') => { - const filename = pluginCtx.getFileName(fileId) + postfix - if (!isRelativeBase || isInlined) { - // absoulte base or relative base but inlined (injected as style tag into - // index.html) use the base as-is - return config.base + filename - } else { - // relative base + extracted CSS - asset file will be in the same dir - return `./${path.posix.basename(filename)}` - } - }) - if (minify && config.build.minify) { - css = await minifyCSS(css, config) - } - return css -} - let CleanCSS: any async function minifyCSS(css: string, config: ResolvedConfig) {