Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: inline proxies for virtual html in transformIndexHtml #7993

Merged
merged 10 commits into from May 4, 2022
31 changes: 27 additions & 4 deletions packages/playground/ssr-html/server.js
Expand Up @@ -14,6 +14,14 @@ const DYNAMIC_SCRIPTS = `
<script type="module" src="/src/app.js"></script>
`

const DYNAMIC_STYLES = `
<style>
h1 {
background-color: blue;
}
</style>
`

async function createServer(
root = process.cwd(),
isProd = process.env.NODE_ENV === 'production'
Expand Down Expand Up @@ -42,15 +50,30 @@ async function createServer(
// use vite's connect instance as middleware
app.use(vite.middlewares)

app.use('*', async (req, res) => {
app.use('*', async (req, res, next) => {
try {
let [url] = req.originalUrl.split('?')
if (url.endsWith('/')) url += 'index.html'

if (url.startsWith('/favicon.ico')) {
return res.status(404).end('404')
}
if (url.startsWith('/@id/__x00__')) {
return next()
}

const htmlLoc = resolve(`.${url}`)
let html = fs.readFileSync(htmlLoc, 'utf8')
html = html.replace('</body>', `${DYNAMIC_SCRIPTS}</body>`)
html = await vite.transformIndexHtml(url, html)
let template = fs.readFileSync(htmlLoc, 'utf-8')

template = template.replace(
'</body>',
`${DYNAMIC_SCRIPTS}${DYNAMIC_STYLES}</body>`
)

// Force calling transformIndexHtml with url === '/', to simulate
// usage by ecosystem that was recommended in the SSR documentation
// as `const url = req.originalUrl`
const html = await vite.transformIndexHtml('/', template)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm forcing '/' as the url here as @brillout described he was doing in vite-plugin-ssr.


res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
} catch (e) {
Expand Down
57 changes: 42 additions & 15 deletions packages/vite/src/node/server/middlewares/indexHtml.ts
@@ -1,6 +1,7 @@
import fs from 'fs'
import path from 'path'
import MagicString from 'magic-string'
import type { SourceMapInput } from 'rollup'
import type { AttributeNode, ElementNode, TextNode } from '@vue/compiler-dom'
import { NodeTypes } from '@vue/compiler-dom'
import type { Connect } from 'types/connect'
Expand All @@ -15,7 +16,12 @@ import {
} from '../../plugins/html'
import type { ResolvedConfig, ViteDevServer } from '../..'
import { send } from '../send'
import { CLIENT_PUBLIC_PATH, FS_PREFIX } from '../../constants'
import {
CLIENT_PUBLIC_PATH,
FS_PREFIX,
VALID_ID_PREFIX,
NULL_BYTE_PLACEHOLDER
} from '../../constants'
import {
cleanUrl,
fsPathFromId,
Expand Down Expand Up @@ -108,32 +114,53 @@ const devHtmlHook: IndexHtmlTransformHook = async (
const { config, moduleGraph, watcher } = server!
const base = config.base || '/'

let proxyModulePath: string
let proxyModuleUrl: string

const trailingSlash = htmlPath.endsWith('/')
if (!trailingSlash && fs.existsSync(filename)) {
proxyModulePath = htmlPath
proxyModuleUrl = base + htmlPath.slice(1)
} else {
// There are users of vite.transformIndexHtml calling it with url '/'
// for SSR integrations #7993, filename is root for this case
// A user may also use a valid name for a virtual html file
// Mark the path as virtual in both cases so sourcemaps aren't processed
// and ids are properly handled
const validPath = `${htmlPath}${trailingSlash ? 'index.html' : ''}`
proxyModulePath = `\0${validPath}`
proxyModuleUrl = `${VALID_ID_PREFIX}${NULL_BYTE_PLACEHOLDER}${validPath}`
}

const s = new MagicString(html)
let inlineModuleIndex = -1
const filePath = cleanUrl(htmlPath)
const proxyCacheUrl = cleanUrl(proxyModulePath).replace(
normalizePath(config.root),
''
)
const styleUrl: AssetNode[] = []

const addInlineModule = (node: ElementNode, ext: 'js') => {
inlineModuleIndex++

const url = filePath.replace(normalizePath(config.root), '')

const contentNode = node.children[0] as TextNode

const code = contentNode.content
const map = new MagicString(html)
.snip(contentNode.loc.start.offset, contentNode.loc.end.offset)
.generateMap({ hires: true })
map.sources = [filename]
map.file = filename

let map: SourceMapInput | undefined
if (!proxyModulePath.startsWith('\0')) {
map = new MagicString(html)
.snip(contentNode.loc.start.offset, contentNode.loc.end.offset)
.generateMap({ hires: true })
map.sources = [filename]
map.file = filename
poyoho marked this conversation as resolved.
Show resolved Hide resolved
}

// add HTML Proxy to Map
addToHTMLProxyCache(config, url, inlineModuleIndex, { code, map })
addToHTMLProxyCache(config, proxyCacheUrl, inlineModuleIndex, { code, map })

// inline js module. convert to src="proxy"
const modulePath = `${
config.base + htmlPath.slice(1)
}?html-proxy&index=${inlineModuleIndex}.${ext}`
const modulePath = `${proxyModuleUrl}?html-proxy&index=${inlineModuleIndex}.${ext}`

// invalidate the module so the newly cached contents will be served
const module = server?.moduleGraph.getModuleById(modulePath)
Expand Down Expand Up @@ -190,13 +217,13 @@ const devHtmlHook: IndexHtmlTransformHook = async (

await Promise.all(
styleUrl.map(async ({ start, end, code }, index) => {
const url = filename + `?html-proxy&${index}.css`
const url = `${proxyModulePath}?html-proxy&index=${index}.css`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The url was missing index= here, I think it worked because this is only needed as a temporary name but still good to follow the same convention as with JS I think.


// ensure module in graph after successful load
const mod = await moduleGraph.ensureEntryFromUrl(url, false)
ensureWatchedFile(watcher, mod.file, config.root)

const result = await server!.pluginContainer.transform(code, url)
const result = await server!.pluginContainer.transform(code, mod.id!)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The transform hook expects the resolved id as second parameter

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, mod.id is /path-to/index.html?html-proxy&index=0.css 😀

s.overwrite(start, end, result?.code || '')
})
)
Expand Down