Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/vite/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,24 @@ if ('document' in globalThis) {
})
}

function getNonceFromElements(
selectors: 'style' | 'link[rel="stylesheet"]' | 'script',
) {
return [
...document.querySelectorAll<
HTMLStyleElement | HTMLLinkElement | HTMLScriptElement
>(selectors),
].find((element) => !!element.nonce)?.nonce
}

const nonceValue =
'document' in globalThis
? // prioritize style/link[rel="stylesheet"] tag's nonce as that will work with style-src
getNonceFromElements('style') ??
getNonceFromElements('link[rel="stylesheet"]') ??
getNonceFromElements('script')
: undefined

// all css imports should be inserted at the same position
// because after build it will be a single css file
let lastInsertedStyle: HTMLStyleElement | undefined
Expand All @@ -402,6 +420,9 @@ export function updateStyle(id: string, content: string): void {
style = document.createElement('style')
style.setAttribute('type', 'text/css')
style.setAttribute('data-vite-dev-id', id)
if (nonceValue) {
style.setAttribute('nonce', nonceValue)
}
style.textContent = content

if (!lastInsertedStyle) {
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/client/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"compilerOptions": {
"types": [],
"target": "ES2019",
"lib": ["ESNext", "DOM"],
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"declaration": false
}
}
132 changes: 106 additions & 26 deletions packages/vite/src/node/plugins/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,15 @@ export const assetAttrsConfig: Record<string, string[]> = {
use: ['xlink:href', 'href'],
}

export const isAsyncScriptMap = new WeakMap<
const scriptStyleMetaMap = new WeakMap<
ResolvedConfig,
Map<string, boolean>
Map<
string,
{
isScriptAllAsync: boolean
nonceValue: { script?: string; style?: string }
}
>
>()

export function nodeIsElement(
Expand Down Expand Up @@ -298,7 +304,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
isDataUrl(url) ||
checkPublicFile(url, config)
// Same reason with `htmlInlineProxyPlugin`
isAsyncScriptMap.set(config, new Map())
scriptStyleMetaMap.set(config, new Map())

return {
name: 'vite:build-html',
Expand Down Expand Up @@ -361,6 +367,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
html = await applyHtmlTransforms(html, preHooks, {
path: publicPath,
filename: id,
nonce: {},
})

let js = ''
Expand All @@ -376,6 +383,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
let everyScriptIsAsync = true
let someScriptsAreAsync = false
let someScriptsAreDefer = false
const nonce: { script?: string; style?: string } = {}

await traverseHtml(html, id, (node) => {
if (!nodeIsElement(node)) {
Expand All @@ -389,6 +397,10 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
const { src, sourceCodeLocation, isModule, isAsync } =
getScriptInfo(node)

if (!nonce.script) {
nonce.script = getNonceValue(node)
}

const url = src && src.value
const isPublicFile = !!(url && checkPublicFile(url, config))
if (isPublicFile) {
Expand Down Expand Up @@ -504,23 +516,33 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
)
}

if (!nonce.style && isLinkRelStyleSheet(node)) {
nonce.style = getNonceValue(node)
}

// <style>...</style>
if (node.nodeName === 'style' && node.childNodes.length) {
const styleNode =
node.childNodes.pop() as DefaultTreeAdapterMap['textNode']
const filePath = id.replace(normalizePath(config.root), '')
inlineModuleIndex++
addToHTMLProxyCache(config, filePath, inlineModuleIndex, {
code: styleNode.value,
})
js += `\nimport "${id}?html-proxy&inline-css&index=${inlineModuleIndex}.css"`
const hash = getHash(cleanUrl(id))
// will transform in `applyHtmlTransforms`
s.update(
styleNode.sourceCodeLocation!.startOffset,
styleNode.sourceCodeLocation!.endOffset,
`__VITE_INLINE_CSS__${hash}_${inlineModuleIndex}__`,
)
if (node.nodeName === 'style') {
if (node.childNodes.length) {
const styleNode =
node.childNodes.pop() as DefaultTreeAdapterMap['textNode']
const filePath = id.replace(normalizePath(config.root), '')
inlineModuleIndex++
addToHTMLProxyCache(config, filePath, inlineModuleIndex, {
code: styleNode.value,
})
js += `\nimport "${id}?html-proxy&inline-css&index=${inlineModuleIndex}.css"`
const hash = getHash(cleanUrl(id))
// will transform in `applyHtmlTransforms`
s.update(
styleNode.sourceCodeLocation!.startOffset,
styleNode.sourceCodeLocation!.endOffset,
`__VITE_INLINE_CSS__${hash}_${inlineModuleIndex}__`,
)
}

if (!nonce.style) {
nonce.style = getNonceValue(node)
}
}

if (shouldRemove) {
Expand All @@ -533,7 +555,10 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
}
})

isAsyncScriptMap.get(config)!.set(id, everyScriptIsAsync)
scriptStyleMetaMap.get(config)!.set(id, {
isScriptAllAsync: everyScriptIsAsync,
nonceValue: nonce,
})

if (someScriptsAreAsync && someScriptsAreDefer) {
config.logger.warn(
Expand Down Expand Up @@ -642,19 +667,22 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
chunk: OutputChunk,
toOutputPath: (filename: string) => string,
isAsync: boolean,
nonce: string | undefined,
): HtmlTagDescriptor => ({
tag: 'script',
attrs: {
...(isAsync ? { async: true } : {}),
type: 'module',
crossorigin: true,
src: toOutputPath(chunk.fileName),
nonce: nonce,
},
})

const toPreloadTag = (
filename: string,
toOutputPath: (filename: string) => string,
nonce: string | undefined,
): HtmlTagDescriptor => ({
tag: 'link',
attrs: {
Expand All @@ -667,6 +695,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
const getCssTagsForChunk = (
chunk: OutputChunk,
toOutputPath: (filename: string) => string,
nonce: string | undefined,
seen: Set<string> = new Set(),
): HtmlTagDescriptor[] => {
const tags: HtmlTagDescriptor[] = []
Expand All @@ -675,7 +704,9 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
chunk.imports.forEach((file) => {
const importee = bundle[file]
if (importee?.type === 'chunk') {
tags.push(...getCssTagsForChunk(importee, toOutputPath, seen))
tags.push(
...getCssTagsForChunk(importee, toOutputPath, nonce, seen),
)
}
})
}
Expand All @@ -688,6 +719,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
attrs: {
rel: 'stylesheet',
href: toOutputPath(file),
nonce,
},
})
}
Expand Down Expand Up @@ -726,7 +758,9 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
const toOutputPublicAssetFilePath = (filename: string) =>
toOutputFilePath(filename, 'public')

const isAsync = isAsyncScriptMap.get(config)!.get(id)!
const { isScriptAllAsync: isAsync, nonceValue } = scriptStyleMetaMap
.get(config)!
.get(id)!

let result = html

Expand Down Expand Up @@ -755,10 +789,22 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
let assetTags: HtmlTagDescriptor[]
if (canInlineEntry) {
assetTags = imports.map((chunk) =>
toScriptTag(chunk, toOutputAssetFilePath, isAsync),
toScriptTag(
chunk,
toOutputAssetFilePath,
isAsync,
nonceValue.script || nonceValue.style,
),
)
} else {
assetTags = [toScriptTag(chunk, toOutputAssetFilePath, isAsync)]
assetTags = [
toScriptTag(
chunk,
toOutputAssetFilePath,
isAsync,
nonceValue.script || nonceValue.style,
),
]
const { modulePreload } = config.build
if (modulePreload !== false) {
const resolveDependencies =
Expand All @@ -773,12 +819,22 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
: importsFileNames
assetTags.push(
...resolvedDeps.map((i) =>
toPreloadTag(i, toOutputAssetFilePath),
toPreloadTag(
i,
toOutputAssetFilePath,
nonceValue.script || nonceValue.style,
),
),
)
}
}
assetTags.push(...getCssTagsForChunk(chunk, toOutputAssetFilePath))
assetTags.push(
...getCssTagsForChunk(
chunk,
toOutputAssetFilePath,
nonceValue.style || nonceValue.script,
),
)

result = injectToHead(result, assetTags)
}
Expand Down Expand Up @@ -822,6 +878,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
filename: id,
bundle,
chunk,
nonce: {},
},
)
// resolve asset url references
Expand Down Expand Up @@ -859,6 +916,24 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
}
}

export function getNonceValue(
node: DefaultTreeAdapterMap['element'],
): string | undefined {
return node.attrs.find(
(attr) => attr.prefix === undefined && attr.name === 'nonce',
)?.value
}

export function isLinkRelStyleSheet(
node: DefaultTreeAdapterMap['element'],
): boolean {
return (
node.nodeName === 'link' &&
node.attrs.find((attr) => attr.prefix === undefined && attr.name === 'rel')
?.value === 'stylesheet'
)
}

// <tag style="... url(...) or image-set(...) ..."></tag>
// extract inline styles as virtual css
export function findNeedTransformStyleAttribute(
Expand Down Expand Up @@ -930,6 +1005,11 @@ export interface IndexHtmlTransformContext {
bundle?: OutputBundle
chunk?: OutputChunk
originalUrl?: string
/** nonce value extracted from script/style tag only available in normal/post hooks during dev */
nonce: {
script?: string
style?: string
}
}

export type IndexHtmlTransformHook = (
Expand Down
Loading