diff --git a/playground/vite.config.ts b/playground/vite.config.ts index 9eaeb2d84a5ea6..7569a98db2dca3 100644 --- a/playground/vite.config.ts +++ b/playground/vite.config.ts @@ -27,6 +27,9 @@ const config: UserConfig = { vueTransformAssetUrls: { img: ['src', 'data-src'] }, + indexHtmlTransforms: [ + ({ code }) => code.replace(/Vite App/, 'Vite Playground') + ], emitManifest: true } diff --git a/src/node/build/buildPluginHtml.ts b/src/node/build/buildPluginHtml.ts index 56ce5d00bf79ba..35a7b2f4034125 100644 --- a/src/node/build/buildPluginHtml.ts +++ b/src/node/build/buildPluginHtml.ts @@ -1,8 +1,16 @@ import { Plugin, RollupOutput, OutputChunk } from 'rollup' import path from 'path' import fs from 'fs-extra' -import { isExternalUrl, cleanUrl, isDataUrl } from '../utils/pathUtils' +import MagicString from 'magic-string' +import { + isExternalUrl, + cleanUrl, + isDataUrl, + transformIndexHtml +} from '../utils' import { resolveAsset, registerAssets } from './buildPluginAsset' +import { InternalResolver } from '../resolver' +import { UserConfig } from '../config' import { parse as Parse, transform as Transform, @@ -11,8 +19,6 @@ import { TextNode, AttributeNode } from '@vue/compiler-dom' -import MagicString from 'magic-string' -import { InternalResolver } from '../resolver' export const createBuildHtmlPlugin = async ( root: string, @@ -21,7 +27,8 @@ export const createBuildHtmlPlugin = async ( assetsDir: string, inlineLimit: number, resolver: InternalResolver, - shouldPreload: ((chunk: OutputChunk) => boolean) | null + shouldPreload: ((chunk: OutputChunk) => boolean) | null, + config: UserConfig ) => { if (!indexPath || !fs.existsSync(indexPath)) { return { @@ -31,10 +38,16 @@ export const createBuildHtmlPlugin = async ( } const rawHtml = await fs.readFile(indexPath, 'utf-8') + const preprocessedHtml = await transformIndexHtml( + rawHtml, + config.indexHtmlTransforms, + 'pre', + true + ) const assets = new Map() let { html: processedHtml, js } = await compileHtml( root, - rawHtml, + preprocessedHtml, publicBasePath, assetsDir, inlineLimit, @@ -91,7 +104,7 @@ export const createBuildHtmlPlugin = async ( } } - const renderIndex = (bundleOutput: RollupOutput['output']) => { + const renderIndex = async (bundleOutput: RollupOutput['output']) => { for (const chunk of bundleOutput) { if (chunk.type === 'chunk') { if (chunk.isEntry) { @@ -112,7 +125,12 @@ export const createBuildHtmlPlugin = async ( } } } - return processedHtml + return await transformIndexHtml( + processedHtml, + config.indexHtmlTransforms, + 'post', + true + ) } return { diff --git a/src/node/build/index.ts b/src/node/build/index.ts index 63dbf769e50665..afee3f1b451c80 100644 --- a/src/node/build/index.ts +++ b/src/node/build/index.ts @@ -296,7 +296,8 @@ export async function build(options: BuildConfig): Promise { assetsDir, assetsInlineLimit, resolver, - shouldPreload + shouldPreload, + options ) const basePlugins = await createBaseRollupPlugins(root, resolver, options) @@ -443,7 +444,7 @@ export async function build(options: BuildConfig): Promise { spinner && spinner.stop() - const indexHtml = emitIndex ? renderIndex(output) : '' + const indexHtml = emitIndex ? await renderIndex(output) : '' if (write) { const printFilesInfo = async ( diff --git a/src/node/config.ts b/src/node/config.ts index da45fddb5fb446..8ca65b75edcae6 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -22,7 +22,11 @@ import { } from './build/buildPluginEsbuild' import { Context, ServerPlugin } from './server' import { Resolver, supportedExts } from './resolver' -import { Transform, CustomBlockTransform } from './transform' +import { + Transform, + CustomBlockTransform, + IndexHtmlTransform +} from './transform' import { DepOptimizationOptions } from './optimizer' import { ServerOptions } from 'https' import { lookupFile } from './utils' @@ -114,6 +118,10 @@ export interface SharedConfig { * Custom file transforms. */ transforms?: Transform[] + /** + * Custom index.html transforms. + */ + indexHtmlTransforms?: IndexHtmlTransform[] /** * Define global variable replacements. * Entries will be defined on `window` during dev and replaced during build. @@ -431,6 +439,7 @@ export interface Plugin UserConfig, | 'alias' | 'transforms' + | 'indexHtmlTransforms' | 'define' | 'resolvers' | 'configureServer' @@ -607,6 +616,10 @@ function resolvePlugin(config: UserConfig, plugin: Plugin): UserConfig { ...config.define }, transforms: [...(config.transforms || []), ...(plugin.transforms || [])], + indexHtmlTransforms: [ + ...(config.indexHtmlTransforms || []), + ...(plugin.indexHtmlTransforms || []) + ], resolvers: [...(config.resolvers || []), ...(plugin.resolvers || [])], configureServer: ([] as ServerPlugin[]).concat( config.configureServer || [], diff --git a/src/node/server/serverPluginHtml.ts b/src/node/server/serverPluginHtml.ts index a84a8a17ad9b22..8f12b3fb5a3449 100644 --- a/src/node/server/serverPluginHtml.ts +++ b/src/node/server/serverPluginHtml.ts @@ -2,7 +2,12 @@ import { rewriteImports, ServerPlugin } from './index' import { debugHmr, ensureMapEntry, importerMap } from './serverPluginHmr' import { clientPublicPath } from './serverPluginClient' import { init as initLexer } from 'es-module-lexer' -import { cleanUrl, readBody, injectScriptToHtml } from '../utils' +import { + cleanUrl, + readBody, + injectScriptToHtml, + transformIndexHtml +} from '../utils' import LRUCache from 'lru-cache' import path from 'path' import chalk from 'chalk' @@ -24,6 +29,12 @@ export const htmlRewritePlugin: ServerPlugin = ({ async function rewriteHtml(importer: string, html: string) { await initLexer + html = await transformIndexHtml( + html, + config.indexHtmlTransforms, + 'pre', + false + ) html = html.replace(scriptRE, (matched, openTag, script) => { if (script) { return `${openTag}${rewriteImports( @@ -45,7 +56,13 @@ export const htmlRewritePlugin: ServerPlugin = ({ return matched } }) - return injectScriptToHtml(html, devInjectionCode) + const processedHtml = injectScriptToHtml(html, devInjectionCode) + return await transformIndexHtml( + processedHtml, + config.indexHtmlTransforms, + 'post', + false + ) } app.use(async (ctx, next) => { diff --git a/src/node/transform.ts b/src/node/transform.ts index bb9782a3b814ce..8fd50bfd3fa005 100644 --- a/src/node/transform.ts +++ b/src/node/transform.ts @@ -49,6 +49,26 @@ export interface Transform { transform: TransformFn } +export interface IndexHtmlTransformContext { + code: string + isBuild: boolean +} + +export type IndexHtmlTransformFn = ( + ctx: IndexHtmlTransformContext +) => string | Promise + +export type IndexHtmlTransform = + | IndexHtmlTransformFn + | { + /** + * Timing for applying the transform. + * @default: 'post' + */ + apply?: 'pre' | 'post' + transform: IndexHtmlTransformFn + } + export type CustomBlockTransform = TransformFn export function createServerTransformPlugin( diff --git a/src/node/utils/transformUtils.ts b/src/node/utils/transformUtils.ts index 761c50360a2fb4..34280a019c92d4 100644 --- a/src/node/utils/transformUtils.ts +++ b/src/node/utils/transformUtils.ts @@ -1,3 +1,5 @@ +import { IndexHtmlTransform } from '../transform' + export async function asyncReplace( input: string, re: RegExp, @@ -27,3 +29,25 @@ export function injectScriptToHtml(html: string, script: string) { // if no tag or doctype is present, just prepend return script + html } + +export async function transformIndexHtml( + html: string, + transforms: IndexHtmlTransform[] = [], + apply: 'pre' | 'post', + isBuild = false +) { + const trans = transforms + .map((t) => { + return typeof t === 'function' && apply === 'post' + ? t + : t.apply === apply + ? t.transform + : undefined + }) + .filter(Boolean) + let code = html + for (const transform of trans) { + code = await transform!({ isBuild, code }) + } + return code +} diff --git a/test/test.js b/test/test.js index 23c9525c02819a..5aee0bcb6cd97a 100644 --- a/test/test.js +++ b/test/test.js @@ -832,7 +832,7 @@ describe('vite', () => { declareTests(false) test('hmr (index.html full-reload)', async () => { - expect(await getText('title')).toMatch('Vite App') + expect(await getText('title')).toMatch('Vite Playground') // hmr const reload = page.waitForNavigation({ waitUntil: 'domcontentloaded' @@ -841,12 +841,12 @@ describe('vite', () => { content.replace('Vite App', 'Vite App Test') ) await reload - await expectByPolling(() => getText('title'), 'Vite App Test') + await expectByPolling(() => getText('title'), 'Vite Playground Test') }) test('hmr (html full-reload)', async () => { await page.goto('http://localhost:3000/test.html') - expect(await getText('title')).toMatch('Vite App') + expect(await getText('title')).toMatch('Vite Playground') // hmr const reload = page.waitForNavigation({ waitUntil: 'domcontentloaded' @@ -855,7 +855,7 @@ describe('vite', () => { content.replace('Vite App', 'Vite App Test') ) await reload - await expectByPolling(() => getText('title'), 'Vite App Test') + await expectByPolling(() => getText('title'), 'Vite Playground Test') }) // Assert that all edited files are reflected on page reload