import path from 'node:path'
import { URL } from 'node:url'
import type {
  OutputAsset,
  OutputBundle,
  OutputChunk,
  RollupError,
  SourceMapInput,
} from 'rolldown'
import MagicString from 'magic-string'
import colors from 'picocolors'
import type {
  DefaultTreeAdapterMap,
  ErrorCodes,
  ParserError,
  Token,
} from 'parse5'
import { stripLiteral } from 'strip-literal'
import escapeHtml from 'escape-html'
import type { MinimalPluginContextWithoutEnvironment, Plugin } from '../plugin'
import type { ViteDevServer } from '../server'
import {
  decodeURIIfPossible,
  encodeURIPath,
  generateCodeFrame,
  getHash,
  isCSSRequest,
  isDataUrl,
  isExternalUrl,
  normalizePath,
  partialEncodeURIPath,
  processSrcSet,
  removeLeadingSlash,
  unique,
} from '../utils'
import type { ResolvedConfig, ResolvedEnvironmentOptions } from '../config'
import { checkPublicFile } from '../publicDir'
import { BUNDLED_DEV_CLIENT_FILENAME } from '../constants'
import { toOutputFilePathInHtml } from '../build'
import { resolveEnvPrefix } from '../env'
import { cleanUrl } from '../../shared/utils'
import { perEnvironmentState } from '../environment'
import { getNodeAssetAttributes } from '../assetSource'
import type { Logger } from '../logger'
import {
  assetUrlRE,
  getPublicAssetFilename,
  publicAssetUrlRE,
  urlToBuiltUrl,
} from './asset'
import { cssBundleNameCache } from './css'
import { modulePreloadPolyfillId } from './modulePreloadPolyfill'

interface ScriptAssetsUrl {
  start: number
  end: number
  url: string
}

const htmlProxyRE =
  /[?&]html-proxy=?(?:&inline-css)?(?:&style-attr)?&index=(\d+)\.(?:js|css)$/
const isHtmlProxyRE = /[?&]html-proxy\b/

const inlineCSSRE = /__VITE_INLINE_CSS__([a-z\d]{8}_\d+)__/g
// Do not allow preceding '.', but do allow preceding '...' for spread operations
const inlineImportRE =
  /(?<!(?<!\.\.)\.)\bimport\s*\(("(?:[^"]|(?<=\\)")*"|'(?:[^']|(?<=\\)')*')\)/dg
const htmlLangRE = /\.(?:html|htm)$/
const spaceRe = /[\t\n\f\r ]/

const importMapRE =
  /[ \t]*<script[^>]*type\s*=\s*(?:"importmap"|'importmap'|importmap)[^>]*>.*?<\/script>/is
const moduleScriptRE =
  /[ \t]*<script[^>]*type\s*=\s*(?:"module"|'module'|module)[^>]*>/i
const modulePreloadLinkRE =
  /[ \t]*<link[^>]*rel\s*=\s*(?:"modulepreload"|'modulepreload'|modulepreload)[\s\S]*?>/i
const importMapAppendRE = new RegExp(
  [moduleScriptRE, modulePreloadLinkRE].map((r) => r.source).join('|'),
  'i',
)

export const isHTMLProxy = (id: string): boolean => isHtmlProxyRE.test(id)

export const isHTMLRequest = (request: string): boolean =>
  htmlLangRE.test(request)

// HTML Proxy Caches are stored by config -> filePath -> index
export const htmlProxyMap: WeakMap<
  ResolvedConfig,
  Map<
    string,
    {
      code: string
      map?: SourceMapInput
    }[]
  >
> = new WeakMap()

// HTML Proxy Transform result are stored by config
// `${hash(importer)}_${query.index}` -> transformed css code
// PS: key like `hash(/vite/playground/assets/index.html)_1`)
export const htmlProxyResult: Map<string, string> = new Map()

export function htmlInlineProxyPlugin(config: ResolvedConfig): Plugin {
  // Should do this when `constructor` rather than when `buildStart`,
  // `buildStart` will be triggered multiple times then the cached result will be emptied.
  // https://github.com/vitejs/vite/issues/6372
  htmlProxyMap.set(config, new Map())
  return {
    name: 'vite:html-inline-proxy',

    resolveId: {
      filter: { id: isHtmlProxyRE },
      handler(id) {
        return id
      },
    },

    load: {
      filter: { id: isHtmlProxyRE },
      handler(id) {
        const proxyMatch = htmlProxyRE.exec(id)
        if (proxyMatch) {
          const index = Number(proxyMatch[1])
          const file = cleanUrl(id)
          const url = file.replace(normalizePath(config.root), '')
          const result = htmlProxyMap.get(config)!.get(url)?.[index]
          if (result) {
            // set moduleSideEffects to keep the module even if `treeshake.moduleSideEffects=false` is set
            return { ...result, moduleSideEffects: true }
          } else {
            throw new Error(`No matching HTML proxy module found from ${id}`)
          }
        }
      },
    },
  }
}

export function addToHTMLProxyCache(
  config: ResolvedConfig,
  filePath: string,
  index: number,
  result: { code: string; map?: SourceMapInput },
): void {
  if (!htmlProxyMap.get(config)) {
    htmlProxyMap.set(config, new Map())
  }
  if (!htmlProxyMap.get(config)!.get(filePath)) {
    htmlProxyMap.get(config)!.set(filePath, [])
  }
  htmlProxyMap.get(config)!.get(filePath)![index] = result
}

export function addToHTMLProxyTransformResult(
  hash: string,
  code: string,
): void {
  htmlProxyResult.set(hash, code)
}

// Some `<link rel>` elements should not be inlined in build. Excluding:
// - `shortcut`                     : only valid for IE <9, use `icon`
// - `mask-icon`                    : deprecated since Safari 12 (for pinned tabs)
// - `apple-touch-icon-precomposed` : only valid for iOS <7 (for avoiding gloss effect)
const noInlineLinkRels = new Set([
  'icon',
  'apple-touch-icon',
  'apple-touch-startup-image',
  'manifest',
])

export const isAsyncScriptMap: WeakMap<
  ResolvedConfig,
  Map<string, boolean>
> = new WeakMap()

export function nodeIsElement(
  node: DefaultTreeAdapterMap['node'],
): node is DefaultTreeAdapterMap['element'] {
  return node.nodeName[0] !== '#'
}

function traverseNodes(
  node: DefaultTreeAdapterMap['node'],
  visitor: (node: DefaultTreeAdapterMap['node']) => void,
) {
  if (node.nodeName === 'template') {
    node = (node as DefaultTreeAdapterMap['template']).content
  }
  visitor(node)
  if (
    nodeIsElement(node) ||
    node.nodeName === '#document' ||
    node.nodeName === '#document-fragment'
  ) {
    node.childNodes.forEach((childNode) => traverseNodes(childNode, visitor))
  }
}

type ParseWarnings = Partial<Record<ErrorCodes, string>>

export async function traverseHtml(
  html: string,
  filePath: string,
  warn: Logger['warn'],
  visitor: (node: DefaultTreeAdapterMap['node']) => void,
): Promise<void> {
  // lazy load compiler
  const { parse, ErrorCodes } = await import('parse5')
  const warnings: ParseWarnings = {}
  const ast = parse(html, {
    scriptingEnabled: false, // parse inside <noscript>
    sourceCodeLocationInfo: true,
    onParseError: (e: ParserError) => {
      handleParseError(e, ErrorCodes, html, filePath, warnings)
    },
  })
  traverseNodes(ast, visitor)

  for (const message of Object.values(warnings)) {
    warn(colors.yellow(`\n${message}`))
  }
}

export function getScriptInfo(node: DefaultTreeAdapterMap['element']): {
  src: Token.Attribute | undefined
  srcSourceCodeLocation: Token.Location | undefined
  isModule: boolean
  isAsync: boolean
  isIgnored: boolean
} {
  let src: Token.Attribute | undefined
  let srcSourceCodeLocation: Token.Location | undefined
  let isModule = false
  let isAsync = false
  let isIgnored = false
  for (const p of node.attrs) {
    if (p.prefix !== undefined) continue
    if (p.name === 'src') {
      if (!src) {
        src = p
        srcSourceCodeLocation = node.sourceCodeLocation?.attrs!.src
      }
    } else if (p.name === 'type' && p.value === 'module') {
      isModule = true
    } else if (p.name === 'async') {
      isAsync = true
    } else if (p.name === 'vite-ignore') {
      isIgnored = true
    }
  }
  return { src, srcSourceCodeLocation, isModule, isAsync, isIgnored }
}

const attrValueStartRE = /=\s*(.)/

export function overwriteAttrValue(
  s: MagicString,
  sourceCodeLocation: Token.Location,
  newValue: string,
): MagicString {
  const srcString = s.slice(
    sourceCodeLocation.startOffset,
    sourceCodeLocation.endOffset,
  )
  const valueStart = attrValueStartRE.exec(srcString)
  if (!valueStart) {
    // overwrite attr value can only be called for a well-defined value
    throw new Error(
      `[vite:html] internal error, failed to overwrite attribute value`,
    )
  }
  const wrapOffset = valueStart[1] === '"' || valueStart[1] === "'" ? 1 : 0
  const valueOffset = valueStart.index! + valueStart[0].length - 1
  s.update(
    sourceCodeLocation.startOffset + valueOffset + wrapOffset,
    sourceCodeLocation.endOffset - wrapOffset,
    newValue,
  )
  return s
}

export function removeViteIgnoreAttr(
  s: MagicString,
  sourceCodeLocation: Token.Location,
): MagicString {
  const loc = (sourceCodeLocation as Token.LocationWithAttributes).attrs?.[
    'vite-ignore'
  ]
  if (loc) {
    s.remove(loc.startOffset, loc.endOffset)
  }
  return s
}

/**
 * Format parse5 @type {ParserError} to @type {RollupError}
 */
function formatParseError(parserError: ParserError, id: string, html: string) {
  const formattedError = {
    code: parserError.code,
    message: `parse5 error code ${parserError.code}`,
    frame: generateCodeFrame(
      html,
      parserError.startOffset,
      parserError.endOffset,
    ),
    loc: {
      file: id,
      line: parserError.startLine,
      column: parserError.startCol,
    },
  } satisfies RollupError
  return formattedError
}

function handleParseError(
  parserError: ParserError,
  errorCodes: typeof ErrorCodes,
  html: string,
  filePath: string,
  warnings: ParseWarnings,
) {
  switch (parserError.code) {
    case errorCodes.missingDoctype:
      // ignore missing DOCTYPE
      return
    case errorCodes.abandonedHeadElementChild:
      // Accept elements without closing tag in <head>
      return
    case errorCodes.duplicateAttribute:
      // Accept duplicate attributes #5966
      // The first attribute is used, browsers silently ignore duplicates
      return
    case errorCodes.nonVoidHtmlElementStartTagWithTrailingSolidus:
      // Allow self closing on non-void elements #10439
      return
    case errorCodes.unexpectedQuestionMarkInsteadOfTagName:
      // Allow <?xml> declaration and <?> empty elements
      // lit generates <?>: https://github.com/lit/lit/issues/2470
      return
  }
  const parseError = formatParseError(parserError, filePath, html)
  warnings[parseError.code] ??=
    `Unable to parse HTML; ${parseError.message}\n` +
    ` at ${parseError.loc.file}:${parseError.loc.line}:${parseError.loc.column}\n` +
    parseError.frame
}

/**
 * Collects CSS files for a chunk by traversing its imports depth-first,
 * using a cache to avoid re-analyzing chunks while still returning the
 * correct files when the same chunk is reached via different entry points.
 */
export function getCssFilesForChunk(
  chunk: OutputChunk,
  bundle: OutputBundle,
  analyzedImportedCssFiles: Map<OutputChunk, string[]>,
  seenChunks: Set<string> = new Set(),
  seenCss: Set<string> = new Set(),
): string[] {
  if (seenChunks.has(chunk.fileName)) {
    return []
  }
  seenChunks.add(chunk.fileName)

  if (analyzedImportedCssFiles.has(chunk)) {
    const files = analyzedImportedCssFiles.get(chunk)!
    const additionals = files.filter((file) => !seenCss.has(file))
    additionals.forEach((file) => seenCss.add(file))
    return additionals
  }

  // Collect all CSS from imports (unfiltered for caching, filtered for return)
  const allFiles: string[] = []
  const filteredFiles: string[] = []
  chunk.imports.forEach((file) => {
    const importee = bundle[file]
    if (importee?.type === 'chunk') {
      const importeeCss = getCssFilesForChunk(
        importee,
        bundle,
        analyzedImportedCssFiles,
        seenChunks,
        seenCss,
      )
      filteredFiles.push(...importeeCss)
      // For cache: use the importee's full cached list
      if (analyzedImportedCssFiles.has(importee)) {
        allFiles.push(...analyzedImportedCssFiles.get(importee)!)
      } else {
        allFiles.push(...importeeCss)
      }
    }
  })

  chunk.viteMetadata!.importedCss.forEach((file) => {
    allFiles.push(file)
    if (!seenCss.has(file)) {
      seenCss.add(file)
      filteredFiles.push(file)
    }
  })

  analyzedImportedCssFiles.set(chunk, unique(allFiles))

  return filteredFiles
}

/**
 * Compiles index.html into an entry js module
 */
export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
  const [preHooks, normalHooks, postHooks] = resolveHtmlTransforms(
    config.plugins,
  )
  preHooks.unshift(injectCspNonceMetaTagHook(config))
  preHooks.unshift(preImportMapHook(config))
  preHooks.push(htmlEnvHook(config))
  postHooks.push(injectNonceAttributeTagHook(config))
  postHooks.push(postImportMapHook(config))
  const processedHtml = perEnvironmentState(() => new Map<string, string>())

  const isExcludedUrl = (url: string) =>
    url[0] === '#' || isExternalUrl(url) || isDataUrl(url)

  // Same reason with `htmlInlineProxyPlugin`
  isAsyncScriptMap.set(config, new Map())

  return {
    name: 'vite:build-html',

    applyToEnvironment(environment) {
      return environment.config.isBundled
    },

    transform: {
      filter: { id: /\.html$/ },
      async handler(html, id) {
        id = normalizePath(id)
        const relativeUrlPath = normalizePath(path.relative(config.root, id))
        const publicPath = `/${relativeUrlPath}`
        const publicBase = getBaseInHTML(relativeUrlPath, config)

        const publicToRelative = (filename: string) => publicBase + filename
        const toOutputPublicFilePath = (url: string) =>
          toOutputFilePathInHtml(
            url.slice(1),
            'public',
            relativeUrlPath,
            'html',
            config,
            publicToRelative,
          )
        // Determines true start position for the node, either the < character
        // position, or the newline at the end of the previous line's node.
        const nodeStartWithLeadingWhitespace = (
          node: DefaultTreeAdapterMap['node'],
        ) => {
          const startOffset = node.sourceCodeLocation!.startOffset
          if (startOffset === 0) return 0

          // Gets the offset for the start of the line including the
          // newline trailing the previous node
          const lineStartOffset =
            startOffset - node.sourceCodeLocation!.startCol

          // <previous-line-node></previous-line-node>
          // <target-node></target-node>
          //
          // Here we want to target the newline at the end of the previous line
          // as the start position for our target.
          //
          // <previous-node></previous-node>
          // <doubled-up-node></doubled-up-node><target-node></target-node>
          //
          // However, if there is content between our target node start and the
          // previous newline, we cannot strip it out without risking content deletion.
          let isLineEmpty = false
          try {
            const line = s.slice(Math.max(0, lineStartOffset), startOffset)
            isLineEmpty = !line.trim()
          } catch {
            // magic-string may throw if there's some content removed in the sliced string,
            // which we ignore and assume the line is not empty
          }

          return isLineEmpty ? lineStartOffset : startOffset
        }

        // pre-transform
        html = await applyHtmlTransforms(html, preHooks, this, {
          path: publicPath,
          filename: id,
        })

        let js = ''
        const s = new MagicString(html)
        const scriptUrls: ScriptAssetsUrl[] = []
        const styleUrls: ScriptAssetsUrl[] = []
        let inlineModuleIndex = -1

        let everyScriptIsAsync = true
        let someScriptsAreAsync = false
        let someScriptsAreDefer = false

        const assetUrlsPromises: Promise<void>[] = []

        // for each encountered asset url, rewrite original html so that it
        // references the post-build location, ignoring empty attributes and
        // attributes that directly reference named output.
        const namedOutput = Object.keys(
          config.build.rolldownOptions.input || {},
        )
        const processAssetUrl = async (url: string, shouldInline?: boolean) => {
          if (
            url !== '' && // Empty attribute
            !namedOutput.includes(url) && // Direct reference to named output
            !namedOutput.includes(removeLeadingSlash(url)) // Allow for absolute references as named output can't be an absolute path
          ) {
            try {
              return await urlToBuiltUrl(this, url, id, shouldInline)
            } catch (e) {
              if (e.code !== 'ENOENT') {
                throw e
              }
            }
          }
          return url
        }

        const setModuleSideEffectPromises: Promise<void>[] = []
        await traverseHtml(html, id, config.logger.warn, (node) => {
          if (!nodeIsElement(node)) {
            return
          }

          let shouldRemove = false

          // script tags
          if (node.nodeName === 'script') {
            const { src, srcSourceCodeLocation, isModule, isAsync, isIgnored } =
              getScriptInfo(node)

            if (isIgnored) {
              removeViteIgnoreAttr(s, node.sourceCodeLocation!)
            } else {
              const url = src && src.value
              const isPublicFile = !!(url && checkPublicFile(url, config))
              if (isPublicFile) {
                // referencing public dir url, prefix with base
                overwriteAttrValue(
                  s,
                  srcSourceCodeLocation!,
                  partialEncodeURIPath(toOutputPublicFilePath(url)),
                )
              }

              if (isModule) {
                inlineModuleIndex++
                if (url && !isExcludedUrl(url) && !isPublicFile) {
                  setModuleSideEffectPromises.push(
                    this.resolve(url, id).then((resolved) => {
                      if (!resolved) {
                        return Promise.reject(
                          new Error(`Failed to resolve ${url} from ${id}`),
                        )
                      }
                      // set moduleSideEffects to keep the module even if `treeshake.moduleSideEffects=false` is set
                      const moduleInfo = this.getModuleInfo(resolved.id)
                      if (moduleInfo) {
                        moduleInfo.moduleSideEffects = true
                      } else if (!resolved.external) {
                        return this.load({
                          ...resolved,
                          moduleSideEffects: true,
                        }).then(() => {})
                      }
                    }),
                  )
                  // <script type="module" src="..."/>
                  // add it as an import
                  js += `\nimport ${JSON.stringify(url)}`
                  shouldRemove = true
                } else if (node.childNodes.length) {
                  const scriptNode =
                    node.childNodes.pop() as DefaultTreeAdapterMap['textNode']
                  const contents = scriptNode.value
                  // <script type="module">...</script>
                  const filePath = id.replace(normalizePath(config.root), '')
                  addToHTMLProxyCache(config, filePath, inlineModuleIndex, {
                    code: contents,
                  })
                  js += `\nimport "${id}?html-proxy&index=${inlineModuleIndex}.js"`
                  shouldRemove = true
                }

                everyScriptIsAsync &&= isAsync
                someScriptsAreAsync ||= isAsync
                someScriptsAreDefer ||= !isAsync
              } else if (url && !isPublicFile) {
                if (!isExcludedUrl(url)) {
                  config.logger.warn(
                    `<script src="${url}"> in "${publicPath}" can't be bundled without type="module" attribute`,
                  )
                }
              } else if (node.childNodes.length) {
                const scriptNode =
                  node.childNodes.pop() as DefaultTreeAdapterMap['textNode']
                scriptUrls.push(
                  ...extractImportExpressionFromClassicScript(scriptNode),
                )
              }
            }
          }

          // For asset references in index.html, also generate an import
          // statement for each - this will be handled by the asset plugin
          const assetAttributes = getNodeAssetAttributes(
            node,
            config.html?.additionalAssetSources,
          )
          for (const attr of assetAttributes) {
            if (attr.type === 'remove') {
              s.remove(attr.location.startOffset, attr.location.endOffset)
              continue
            } else if (attr.type === 'srcset') {
              assetUrlsPromises.push(
                (async () => {
                  const processedEncodedUrl = await processSrcSet(
                    attr.value,
                    async ({ url }) => {
                      const decodedUrl = decodeURIIfPossible(url)
                      if (
                        decodedUrl !== undefined &&
                        !isExcludedUrl(decodedUrl)
                      ) {
                        const result = await processAssetUrl(url)
                        return result !== decodedUrl
                          ? encodeURIPath(result)
                          : url
                      }
                      return url
                    },
                  )
                  if (processedEncodedUrl !== attr.value) {
                    overwriteAttrValue(s, attr.location, processedEncodedUrl)
                  }
                })(),
              )
            } else if (attr.type === 'src') {
              const url = decodeURIIfPossible(attr.value)
              if (url === undefined) {
                // ignore it
              } else if (checkPublicFile(url, config)) {
                overwriteAttrValue(
                  s,
                  attr.location,
                  partialEncodeURIPath(toOutputPublicFilePath(url)),
                )
              } else if (!isExcludedUrl(url)) {
                if (
                  node.nodeName === 'link' &&
                  isCSSRequest(url) &&
                  // should not be converted if following attributes are present (#6748)
                  !('media' in attr.attributes || 'disabled' in attr.attributes)
                ) {
                  // CSS references, convert to import
                  const importExpression = `\nimport ${JSON.stringify(url)}`
                  styleUrls.push({
                    url,
                    start: nodeStartWithLeadingWhitespace(node),
                    end: node.sourceCodeLocation!.endOffset,
                  })
                  js += importExpression
                } else {
                  // If the node is a link, check if it can be inlined. If not, set `shouldInline`
                  // to `false` to force no inline. If `undefined`, it leaves to the default heuristics.
                  const isNoInlineLink =
                    node.nodeName === 'link' &&
                    attr.attributes.rel &&
                    parseRelAttr(attr.attributes.rel).some((v) =>
                      noInlineLinkRels.has(v),
                    )
                  const shouldInline = isNoInlineLink ? false : undefined
                  assetUrlsPromises.push(
                    (async () => {
                      const processedUrl = await processAssetUrl(
                        url,
                        shouldInline,
                      )
                      if (processedUrl !== url) {
                        overwriteAttrValue(
                          s,
                          attr.location,
                          partialEncodeURIPath(processedUrl),
                        )
                      }
                    })(),
                  )
                }
              }
            }
          }

          const inlineStyle = findNeedTransformStyleAttribute(node)
          if (inlineStyle) {
            inlineModuleIndex++
            // replace `inline style` with __VITE_INLINE_CSS__**_**__
            // and import css in js code
            const code = inlineStyle.attr.value
            const filePath = id.replace(normalizePath(config.root), '')
            addToHTMLProxyCache(config, filePath, inlineModuleIndex, { code })
            // will transform with css plugin and cache result with css-post plugin
            js += `\nimport "${id}?html-proxy&inline-css&style-attr&index=${inlineModuleIndex}.css"`
            const hash = getHash(cleanUrl(id))
            // will transform in `applyHtmlTransforms`
            overwriteAttrValue(
              s,
              inlineStyle.location!,
              `__VITE_INLINE_CSS__${hash}_${inlineModuleIndex}__`,
            )
          }

          // <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 (shouldRemove) {
            // remove the script tag from the html. we are going to inject new
            // ones in the end.
            s.remove(
              nodeStartWithLeadingWhitespace(node),
              node.sourceCodeLocation!.endOffset,
            )
          }
        })

        isAsyncScriptMap.get(config)!.set(id, everyScriptIsAsync)

        if (someScriptsAreAsync && someScriptsAreDefer) {
          config.logger.warn(
            `\nMixed async and defer script modules in ${id}, output script will fallback to defer. Every script, including inline ones, need to be marked as async for your output script to be async.`,
          )
        }

        await Promise.all(assetUrlsPromises)

        // emit <script>import("./aaa")</script> asset
        for (const { start, end, url } of scriptUrls) {
          if (checkPublicFile(url, config)) {
            s.update(
              start,
              end,
              partialEncodeURIPath(toOutputPublicFilePath(url)),
            )
          } else if (!isExcludedUrl(url)) {
            s.update(
              start,
              end,
              partialEncodeURIPath(await urlToBuiltUrl(this, url, id)),
            )
          }
        }

        // ignore <link rel="stylesheet"> if its url can't be resolved
        const resolvedStyleUrls = await Promise.all(
          styleUrls.map(async (styleUrl) => ({
            ...styleUrl,
            resolved: await this.resolve(styleUrl.url, id),
          })),
        )
        for (const { start, end, url, resolved } of resolvedStyleUrls) {
          if (resolved == null) {
            config.logger.warnOnce(
              `\n${url} doesn't exist at build time, it will remain unchanged to be resolved at runtime`,
            )
            const importExpression = `\nimport ${JSON.stringify(url)}`
            js = js.replace(importExpression, '')
          } else {
            s.remove(start, end)
          }
        }

        processedHtml(this).set(id, s.toString())

        // inject module preload polyfill only when configured and needed
        const { modulePreload } = this.environment.config.build
        if (
          modulePreload !== false &&
          modulePreload.polyfill &&
          (someScriptsAreAsync || someScriptsAreDefer)
        ) {
          js = `import "${modulePreloadPolyfillId}";\n${js}`
        }

        await Promise.all(setModuleSideEffectPromises)

        // Force rollup to keep this module from being shared between other entry points.
        // If the resulting chunk is empty, it will be removed in generateBundle.
        return {
          code: js,
          map: { mappings: '' },
          moduleSideEffects: 'no-treeshake',
        }
      },
    },

    async generateBundle(options, bundle) {
      const analyzedImportedCssFiles = new Map<OutputChunk, string[]>()
      const inlineEntryChunk = new Set<string>()
      const getImportedChunks = (
        chunk: OutputChunk,
        seen: Set<string> = new Set(),
      ): (OutputChunk | string)[] => {
        const chunks: (OutputChunk | string)[] = []
        chunk.imports.forEach((file) => {
          const importee = bundle[file]
          if (importee) {
            if (importee.type === 'chunk' && !seen.has(file)) {
              seen.add(file)

              // post-order traversal
              chunks.push(...getImportedChunks(importee, seen))
              chunks.push(importee)
            }
          } else {
            // external imports
            chunks.push(file)
          }
        })
        return chunks
      }

      const toScriptTag = (
        chunkOrUrl: OutputChunk | string,
        toOutputPath: (filename: string) => string,
        isAsync: boolean,
      ): HtmlTagDescriptor => ({
        tag: 'script',
        attrs: {
          ...(isAsync ? { async: true } : {}),
          type: 'module',
          // crossorigin must be set not only for serving assets in a different origin
          // but also to make it possible to preload the script using `<link rel="preload">`.
          // `<script type="module">` used to fetch the script with credential mode `omit`,
          // however `crossorigin` attribute cannot specify that value.
          // https://developer.chrome.com/blog/modulepreload/#ok-so-why-doesnt-link-relpreload-work-for-modules:~:text=For%20%3Cscript%3E,of%20other%20modules.
          // Now `<script type="module">` uses `same origin`: https://github.com/whatwg/html/pull/3656#:~:text=Module%20scripts%20are%20always%20fetched%20with%20credentials%20mode%20%22same%2Dorigin%22%20by%20default%20and%20can%20no%20longer%0Ause%20%22omit%22
          crossorigin: true,
          src:
            typeof chunkOrUrl === 'string'
              ? chunkOrUrl
              : toOutputPath(chunkOrUrl.fileName),
        },
      })

      const toPreloadTag = (
        filename: string,
        toOutputPath: (filename: string) => string,
      ): HtmlTagDescriptor => ({
        tag: 'link',
        attrs: {
          rel: 'modulepreload',
          crossorigin: true,
          href: toOutputPath(filename),
        },
      })

      const toStyleSheetLinkTag = (
        file: string,
        toOutputPath: (filename: string) => string,
      ): HtmlTagDescriptor => ({
        tag: 'link',
        attrs: {
          rel: 'stylesheet',
          crossorigin: true,
          href: toOutputPath(file),
        },
      })

      const getCssTagsForChunk = (
        chunk: OutputChunk,
        toOutputPath: (filename: string) => string,
      ) =>
        getCssFilesForChunk(chunk, bundle, analyzedImportedCssFiles).map(
          (file) => toStyleSheetLinkTag(file, toOutputPath),
        )

      for (const [normalizedId, html] of processedHtml(this)) {
        const relativeUrlPath = normalizePath(
          path.relative(config.root, normalizedId),
        )
        const assetsBase = getBaseInHTML(relativeUrlPath, config)
        const toOutputFilePath = (
          filename: string,
          type: 'asset' | 'public',
        ) => {
          if (isExternalUrl(filename)) {
            return filename
          } else {
            return toOutputFilePathInHtml(
              filename,
              type,
              relativeUrlPath,
              'html',
              config,
              (filename) => assetsBase + filename,
            )
          }
        }

        const toOutputAssetFilePath = (filename: string) =>
          toOutputFilePath(filename, 'asset')

        const toOutputPublicAssetFilePath = (filename: string) =>
          toOutputFilePath(filename, 'public')

        const isAsync = isAsyncScriptMap.get(config)!.get(normalizedId)!

        let result = html

        // find corresponding entry chunk
        const chunk = Object.values(bundle).find(
          (chunk) =>
            chunk.type === 'chunk' &&
            chunk.isEntry &&
            chunk.facadeModuleId &&
            normalizePath(chunk.facadeModuleId) === normalizedId,
        ) as OutputChunk | undefined

        let canInlineEntry = false

        // inject chunk asset links
        if (chunk) {
          // an entry chunk can be inlined if
          //  - it's an ES module (e.g. not generated by the legacy plugin)
          //  - it contains no meaningful code other than import statements
          if (options.format === 'es' && isEntirelyImport(chunk.code)) {
            canInlineEntry = true
          }

          // when not inlined, inject <script> for entry and modulepreload its dependencies
          // when inlined, discard entry chunk and inject <script> for everything in post-order
          const imports = getImportedChunks(chunk)
          let assetTags: HtmlTagDescriptor[]
          if (canInlineEntry) {
            assetTags = imports.map((chunk) =>
              toScriptTag(chunk, toOutputAssetFilePath, isAsync),
            )
          } else {
            const { modulePreload } = this.environment.config.build
            assetTags = [toScriptTag(chunk, toOutputAssetFilePath, isAsync)]
            if (modulePreload !== false) {
              const resolveDependencies =
                typeof modulePreload === 'object' &&
                modulePreload.resolveDependencies
              const importsFileNames = imports
                .filter((chunkOrUrl) => typeof chunkOrUrl !== 'string')
                .map((chunk) => chunk.fileName)
              const resolvedDeps = resolveDependencies
                ? resolveDependencies(chunk.fileName, importsFileNames, {
                    hostId: relativeUrlPath,
                    hostType: 'html',
                  })
                : importsFileNames
              assetTags.push(
                ...resolvedDeps.map((i) =>
                  toPreloadTag(i, toOutputAssetFilePath),
                ),
              )
            }
          }
          assetTags.push(...getCssTagsForChunk(chunk, toOutputAssetFilePath))

          result = injectToHead(result, assetTags)
        }

        // prepend dev client runtime for bundled client build before other chunk scripts
        if (
          config.command === 'serve' &&
          this.environment.config.consumer === 'client' &&
          this.environment.config.isBundled
        ) {
          result = injectToHead(
            result,
            [
              {
                tag: 'script',
                attrs: {
                  type: 'module',
                  src: path.posix.join(
                    config.base,
                    BUNDLED_DEV_CLIENT_FILENAME,
                  ),
                },
              },
            ],
            true,
          )
        }

        // inject css link when cssCodeSplit is false
        if (!this.environment.config.build.cssCodeSplit) {
          const cssBundleName = cssBundleNameCache.get(config)
          const cssChunk =
            cssBundleName &&
            (Object.values(bundle).find(
              (chunk) =>
                chunk.type === 'asset' && chunk.names.includes(cssBundleName),
            ) as OutputAsset | undefined)
          if (cssChunk) {
            result = injectToHead(result, [
              {
                tag: 'link',
                attrs: {
                  rel: 'stylesheet',
                  crossorigin: true,
                  href: toOutputAssetFilePath(cssChunk.fileName),
                },
              },
            ])
          }
        }

        // no use assets plugin because it will emit file
        let match: RegExpExecArray | null
        let s: MagicString | undefined
        inlineCSSRE.lastIndex = 0
        while ((match = inlineCSSRE.exec(result))) {
          s ||= new MagicString(result)
          const { 0: full, 1: scopedName } = match
          const cssTransformedCode = htmlProxyResult.get(scopedName)!
          s.update(match.index, match.index + full.length, cssTransformedCode)
        }
        if (s) {
          result = s.toString()
        }
        result = await applyHtmlTransforms(
          result,
          [...normalHooks, ...postHooks],
          this,
          {
            path: '/' + relativeUrlPath,
            filename: normalizedId,
            bundle,
            chunk,
          },
        )
        // resolve asset url references
        result = result.replace(assetUrlRE, (_, fileHash, postfix = '') => {
          const file = this.getFileName(fileHash)
          if (chunk) {
            chunk.viteMetadata!.importedAssets.add(cleanUrl(file))
          }
          return encodeURIPath(toOutputAssetFilePath(file)) + postfix
        })

        result = result.replace(publicAssetUrlRE, (_, fileHash) => {
          const publicAssetPath = toOutputPublicAssetFilePath(
            getPublicAssetFilename(fileHash, config)!,
          )

          return encodeURIPath(
            URL.canParse(publicAssetPath)
              ? publicAssetPath
              : normalizePath(publicAssetPath),
          )
        })

        if (chunk && canInlineEntry) {
          inlineEntryChunk.add(chunk.fileName)
        }

        const shortEmitName = normalizePath(
          path.relative(config.root, normalizedId),
        )
        this.emitFile({
          type: 'asset',
          originalFileName: normalizedId,
          fileName: shortEmitName,
          source: result,
        })
      }

      for (const fileName of inlineEntryChunk) {
        // all imports from entry have been inlined to html, prevent rollup from outputting it
        delete bundle[fileName]
      }
    },
  }
}

export function parseRelAttr(attr: string): string[] {
  return attr.split(spaceRe).map((v) => v.toLowerCase())
}

// <tag style="... url(...) or image-set(...) ..."></tag>
// extract inline styles as virtual css
export function findNeedTransformStyleAttribute(
  node: DefaultTreeAdapterMap['element'],
): { attr: Token.Attribute; location?: Token.Location } | undefined {
  const attr = node.attrs.find(
    (prop) =>
      prop.prefix === undefined &&
      prop.name === 'style' &&
      // only url(...) or image-set(...) in css need to emit file
      (prop.value.includes('url(') || prop.value.includes('image-set(')),
  )
  if (!attr) return undefined
  const location = node.sourceCodeLocation?.attrs?.style
  return { attr, location }
}

export function extractImportExpressionFromClassicScript(
  scriptTextNode: DefaultTreeAdapterMap['textNode'],
): ScriptAssetsUrl[] {
  const startOffset = scriptTextNode.sourceCodeLocation!.startOffset
  const cleanCode = stripLiteral(scriptTextNode.value)

  const scriptUrls: ScriptAssetsUrl[] = []
  let match: RegExpExecArray | null
  inlineImportRE.lastIndex = 0
  while ((match = inlineImportRE.exec(cleanCode))) {
    const [, [urlStart, urlEnd]] = match.indices as Array<[number, number]>
    const start = urlStart + 1
    const end = urlEnd - 1
    scriptUrls.push({
      start: start + startOffset,
      end: end + startOffset,
      url: scriptTextNode.value.slice(start, end),
    })
  }
  return scriptUrls
}

export interface HtmlTagDescriptor {
  tag: string
  /**
   * attribute values will be escaped automatically if needed
   */
  attrs?: Record<string, string | boolean | undefined>
  children?: string | HtmlTagDescriptor[]
  /**
   * default: 'head-prepend'
   */
  injectTo?: 'head' | 'body' | 'head-prepend' | 'body-prepend'
}

export type IndexHtmlTransformResult =
  | string
  | HtmlTagDescriptor[]
  | {
      html: string
      tags: HtmlTagDescriptor[]
    }

export interface IndexHtmlTransformContext {
  /**
   * public path when served
   */
  path: string
  /**
   * filename on disk
   */
  filename: string
  server?: ViteDevServer
  bundle?: OutputBundle
  chunk?: OutputChunk
  originalUrl?: string
}

export type IndexHtmlTransformHook = (
  this: MinimalPluginContextWithoutEnvironment,
  html: string,
  ctx: IndexHtmlTransformContext,
) => IndexHtmlTransformResult | void | Promise<IndexHtmlTransformResult | void>

export type IndexHtmlTransform =
  | IndexHtmlTransformHook
  | {
      order?: 'pre' | 'post' | null
      handler: IndexHtmlTransformHook
    }

export function preImportMapHook(
  config: ResolvedConfig,
): IndexHtmlTransformHook {
  return (html, ctx) => {
    const importMapIndex = html.search(importMapRE)
    if (importMapIndex < 0) return

    const importMapAppendIndex = html.search(importMapAppendRE)
    if (importMapAppendIndex < 0) return

    if (importMapAppendIndex < importMapIndex) {
      const relativeHtml = normalizePath(
        path.relative(config.root, ctx.filename),
      )
      config.logger.warnOnce(
        colors.yellow(
          colors.bold(
            `(!) <script type="importmap"> should come before <script type="module"> and <link rel="modulepreload"> in /${relativeHtml}`,
          ),
        ),
      )
    }
  }
}

/**
 * Move importmap before the first module script and modulepreload link
 */
export function postImportMapHook(
  config: ResolvedConfig,
): IndexHtmlTransformHook {
  const decoder = new TextDecoder()
  return function (html, { bundle }) {
    const chunkImportMapEnabled =
      config.command === 'build' &&
      config.environments.client.build.chunkImportMap

    if (importMapAppendRE.test(html)) {
      let importMap: string | undefined
      html = html.replace(importMapRE, (match) => {
        importMap = match
        return ''
      })

      if (importMap) {
        html = html.replace(
          importMapAppendRE,
          (match) => `${importMap}\n${match}`,
        )
        if (chunkImportMapEnabled) {
          // https://caniuse.com/mdn-html_elements_script_type_importmap_multiple_import_maps
          this.warn(
            `The \`build.chunkImportMap\` option is enabled but an import map also exists in the input HTML file.` +
              ` This leads to multiple import maps generated in the output HTML, which is not supported by older browsers and Firefox.`,
          )
        }
      }
    }

    if (chunkImportMapEnabled) {
      const nonce = config.html?.cspNonce
      const importMap = bundle![
        getImportMapFilename(config.environments.client)
      ] as OutputAsset
      const importMapHtml = serializeTag({
        tag: 'script',
        attrs: { type: 'importmap', ...(nonce ? { nonce } : {}) },
        children:
          typeof importMap.source === 'string'
            ? importMap.source
            : decoder.decode(importMap.source),
      })
      if (importMapAppendRE.test(html)) {
        // NOTE: insert before the existing import map so that our import map takes precedence
        html = html.replace(
          importMapAppendRE,
          (match) => `${importMapHtml}\n${match}`,
        )
      } else {
        html = `${importMapHtml}\n${html}`
      }
    }

    return html
  }
}

export function injectCspNonceMetaTagHook(
  config: ResolvedConfig,
): IndexHtmlTransformHook {
  return () => {
    if (!config.html?.cspNonce) return

    return [
      {
        tag: 'meta',
        injectTo: 'head',
        // use nonce attribute so that it's hidden
        // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce#accessing_nonces_and_nonce_hiding
        attrs: { property: 'csp-nonce', nonce: config.html.cspNonce },
      },
    ]
  }
}

/**
 * Support `%ENV_NAME%` syntax in html files
 */
export function htmlEnvHook(config: ResolvedConfig): IndexHtmlTransformHook {
  const pattern = /%(\S+?)%/g
  const envPrefix = resolveEnvPrefix({ envPrefix: config.envPrefix })
  const env: Record<string, any> = { ...config.env }

  // account for user env defines
  for (const key in config.define) {
    if (key.startsWith(`import.meta.env.`)) {
      const val = config.define[key]
      if (typeof val === 'string') {
        try {
          const parsed = JSON.parse(val)
          env[key.slice(16)] = typeof parsed === 'string' ? parsed : val
        } catch {
          env[key.slice(16)] = val
        }
      } else {
        env[key.slice(16)] = JSON.stringify(val)
      }
    }
  }
  return (html, ctx) => {
    return html.replace(pattern, (text, key) => {
      if (key in env) {
        return env[key]
      } else {
        if (envPrefix.some((prefix) => key.startsWith(prefix))) {
          const relativeHtml = normalizePath(
            path.relative(config.root, ctx.filename),
          )
          config.logger.warn(
            colors.yellow(
              colors.bold(
                `(!) ${text} is not defined in env variables found in /${relativeHtml}. ` +
                  `Is the variable mistyped?`,
              ),
            ),
          )
        }

        return text
      }
    })
  }
}

export function injectNonceAttributeTagHook(
  config: ResolvedConfig,
): IndexHtmlTransformHook {
  const processRelType = new Set(['stylesheet', 'modulepreload', 'preload'])

  return async (html, { filename }) => {
    const nonce = config.html?.cspNonce
    if (!nonce) return

    const s = new MagicString(html)

    await traverseHtml(html, filename, config.logger.warn, (node) => {
      if (!nodeIsElement(node)) {
        return
      }

      const { nodeName, attrs, sourceCodeLocation } = node

      if (
        nodeName === 'script' ||
        nodeName === 'style' ||
        (nodeName === 'link' &&
          attrs.some(
            (attr) =>
              attr.name === 'rel' &&
              parseRelAttr(attr.value).some((a) => processRelType.has(a)),
          ))
      ) {
        // If we already have a nonce attribute, we don't need to add another one
        if (attrs.some(({ name }) => name === 'nonce')) {
          return
        }

        const startTagEndOffset = sourceCodeLocation!.startTag!.endOffset

        // if the closing of the start tag includes a `/`, the offset should be 2 so the nonce
        // is appended prior to the `/`
        const appendOffset = html[startTagEndOffset - 2] === '/' ? 2 : 1

        s.appendRight(startTagEndOffset - appendOffset, ` nonce="${nonce}"`)
      }
    })

    return s.toString()
  }
}

export function resolveHtmlTransforms(
  plugins: readonly Plugin[],
): [
  IndexHtmlTransformHook[],
  IndexHtmlTransformHook[],
  IndexHtmlTransformHook[],
] {
  const preHooks: IndexHtmlTransformHook[] = []
  const normalHooks: IndexHtmlTransformHook[] = []
  const postHooks: IndexHtmlTransformHook[] = []

  for (const plugin of plugins) {
    const hook = plugin.transformIndexHtml
    if (!hook) continue

    if (typeof hook === 'function') {
      normalHooks.push(hook)
    } else {
      const handler = hook.handler
      if (hook.order === 'pre') {
        preHooks.push(handler)
      } else if (hook.order === 'post') {
        postHooks.push(handler)
      } else {
        normalHooks.push(handler)
      }
    }
  }

  return [preHooks, normalHooks, postHooks]
}

// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head#see_also
const elementsAllowedInHead = new Set([
  'title',
  'base',
  'link',
  'style',
  'meta',
  'script',
  'noscript',
  'template',
])

function headTagInsertCheck(
  tags: HtmlTagDescriptor[],
  ctx: IndexHtmlTransformContext,
) {
  if (!tags.length) return
  const { logger } = ctx.server?.config || {}
  const disallowedTags = tags.filter(
    (tagDescriptor) => !elementsAllowedInHead.has(tagDescriptor.tag),
  )

  if (disallowedTags.length) {
    const dedupedTags = unique(
      disallowedTags.map((tagDescriptor) => `<${tagDescriptor.tag}>`),
    )
    logger?.warn(
      colors.yellow(
        colors.bold(
          `[${dedupedTags.join(',')}] can not be used inside the <head> Element, please check the 'injectTo' value`,
        ),
      ),
    )
  }
}

export async function applyHtmlTransforms(
  html: string,
  hooks: IndexHtmlTransformHook[],
  pluginContext: MinimalPluginContextWithoutEnvironment,
  ctx: IndexHtmlTransformContext,
): Promise<string> {
  for (const hook of hooks) {
    const res = await hook.call(pluginContext, html, ctx)
    if (!res) {
      continue
    }
    if (typeof res === 'string') {
      html = res
    } else {
      let tags: HtmlTagDescriptor[]
      if (Array.isArray(res)) {
        tags = res
      } else {
        html = res.html || html
        tags = res.tags
      }

      let headTags: HtmlTagDescriptor[] | undefined
      let headPrependTags: HtmlTagDescriptor[] | undefined
      let bodyTags: HtmlTagDescriptor[] | undefined
      let bodyPrependTags: HtmlTagDescriptor[] | undefined

      for (const tag of tags) {
        switch (tag.injectTo) {
          case 'body':
            ;(bodyTags ??= []).push(tag)
            break
          case 'body-prepend':
            ;(bodyPrependTags ??= []).push(tag)
            break
          case 'head':
            ;(headTags ??= []).push(tag)
            break
          default:
            ;(headPrependTags ??= []).push(tag)
        }
      }
      headTagInsertCheck([...(headTags || []), ...(headPrependTags || [])], ctx)
      if (headPrependTags) html = injectToHead(html, headPrependTags, true)
      if (headTags) html = injectToHead(html, headTags)
      if (bodyPrependTags) html = injectToBody(html, bodyPrependTags, true)
      if (bodyTags) html = injectToBody(html, bodyTags)
    }
  }

  return html
}

const importOrCommentRE =
  /\s+|\/\*[\s\S]*?\*\/|\/\/[^\n]*(?:\n|$)|import\s*(?:"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*')\s*;*/y

function isEntirelyImport(code: string) {
  // only consider "side-effect" imports, which match <script type=module> semantics exactly
  // the regexes will remove too little in some exotic cases, but false-negatives are alright.
  // Consume one token at a time to avoid backtracking over the whole chunk.
  importOrCommentRE.lastIndex = 0
  while (importOrCommentRE.lastIndex < code.length) {
    if (!importOrCommentRE.test(code)) {
      return false
    }
  }
  return true
}

function getBaseInHTML(urlRelativePath: string, config: ResolvedConfig) {
  // Prefer explicit URL if defined for linking to assets and public files from HTML,
  // even when base relative is specified
  return config.base === './' || config.base === ''
    ? path.posix.join(
        path.posix.relative(urlRelativePath, '').slice(0, -2),
        './',
      )
    : config.base
}

const headInjectRE = /([ \t]*)<\/head>/i
const headPrependInjectRE = /([ \t]*)<head[^>]*>/i

const htmlInjectRE = /<\/html>/i
const htmlPrependInjectRE = /([ \t]*)<html[^>]*>/i

const bodyInjectRE = /([ \t]*)<\/body>/i
const bodyPrependInjectRE = /([ \t]*)<body[^>]*>/i

const doctypePrependInjectRE = /<!doctype html>/i

function injectToHead(
  html: string,
  tags: HtmlTagDescriptor[],
  prepend = false,
) {
  if (tags.length === 0) return html

  if (prepend) {
    // inject as the first element of head
    if (headPrependInjectRE.test(html)) {
      return html.replace(
        headPrependInjectRE,
        (match, p1) => `${match}\n${serializeTags(tags, incrementIndent(p1))}`,
      )
    }
  } else {
    // inject before head close
    if (headInjectRE.test(html)) {
      // respect indentation of head tag
      return html.replace(
        headInjectRE,
        (match, p1) => `${serializeTags(tags, incrementIndent(p1))}${match}`,
      )
    }
    // try to inject before the body tag
    if (bodyPrependInjectRE.test(html)) {
      return html.replace(
        bodyPrependInjectRE,
        (match, p1) => `${serializeTags(tags, p1)}\n${match}`,
      )
    }
  }
  // if no head tag is present, we prepend the tag for both prepend and append
  return prependInjectFallback(html, tags)
}

function injectToBody(
  html: string,
  tags: HtmlTagDescriptor[],
  prepend = false,
) {
  if (tags.length === 0) return html

  if (prepend) {
    // inject after body open
    if (bodyPrependInjectRE.test(html)) {
      return html.replace(
        bodyPrependInjectRE,
        (match, p1) => `${match}\n${serializeTags(tags, incrementIndent(p1))}`,
      )
    }
    // if no there is no body tag, inject after head or fallback to prepend in html
    if (headInjectRE.test(html)) {
      return html.replace(
        headInjectRE,
        (match, p1) => `${match}\n${serializeTags(tags, p1)}`,
      )
    }
    return prependInjectFallback(html, tags)
  } else {
    // inject before body close
    if (bodyInjectRE.test(html)) {
      return html.replace(
        bodyInjectRE,
        (match, p1) => `${serializeTags(tags, incrementIndent(p1))}${match}`,
      )
    }
    // if no body tag is present, append to the html tag, or at the end of the file
    if (htmlInjectRE.test(html)) {
      return html.replace(htmlInjectRE, `${serializeTags(tags)}\n$&`)
    }
    return html + `\n` + serializeTags(tags)
  }
}

function prependInjectFallback(html: string, tags: HtmlTagDescriptor[]) {
  // prepend to the html tag, append after doctype, or the document start
  if (htmlPrependInjectRE.test(html)) {
    return html.replace(htmlPrependInjectRE, `$&\n${serializeTags(tags)}`)
  }
  if (doctypePrependInjectRE.test(html)) {
    return html.replace(doctypePrependInjectRE, `$&\n${serializeTags(tags)}`)
  }
  return serializeTags(tags) + html
}

const unaryTags = new Set(['link', 'meta', 'base'])

function serializeTag(
  { tag, attrs, children }: HtmlTagDescriptor,
  indent: string = '',
): string {
  if (unaryTags.has(tag)) {
    return `<${tag}${serializeAttrs(attrs)}>`
  } else {
    return `<${tag}${serializeAttrs(attrs)}>${serializeTags(
      children,
      incrementIndent(indent),
    )}</${tag}>`
  }
}

function serializeTags(
  tags: HtmlTagDescriptor['children'],
  indent: string = '',
): string {
  if (typeof tags === 'string') {
    return tags
  } else if (tags && tags.length) {
    return tags.map((tag) => `${indent}${serializeTag(tag, indent)}\n`).join('')
  }
  return ''
}

function serializeAttrs(attrs: HtmlTagDescriptor['attrs']): string {
  let res = ''
  for (const key in attrs) {
    if (typeof attrs[key] === 'boolean') {
      res += attrs[key] ? ` ${key}` : ``
    } else {
      res += ` ${key}="${escapeHtml(attrs[key])}"`
    }
  }
  return res
}

function incrementIndent(indent: string = '') {
  return `${indent}${indent[0] === '\t' ? '\t' : '  '}`
}

export function getImportMapFilename(
  options: ResolvedEnvironmentOptions,
): string {
  const chunkImportMap =
    options.build.rolldownOptions.experimental?.chunkImportMap
  if (typeof chunkImportMap === 'object' && chunkImportMap.fileName) {
    return chunkImportMap.fileName
  }
  return 'importmap.json'
}

function getImportMapBaseUrl(options: ResolvedEnvironmentOptions): string {
  const chunkImportMap =
    options.build.rolldownOptions.experimental?.chunkImportMap
  if (typeof chunkImportMap === 'object' && chunkImportMap.baseUrl) {
    return chunkImportMap.baseUrl
  }
  return '/'
}

/**
 * Read and parse the chunk import map asset from the bundle.
 * Returns `undefined` when the import map is not present in the bundle.
 */
export function getImportMap(
  bundle: OutputBundle,
  options: ResolvedEnvironmentOptions & ResolvedConfig,
):
  | {
      asset: OutputAsset
      content: { imports: Record<string, string> }
      /** import map entries with the base stripped (placeholder name -> real name) */
      mapping: Record<string, string>
    }
  | undefined {
  const asset = bundle[getImportMapFilename(options)] as OutputAsset | undefined
  if (!asset) return undefined

  const content: { imports: Record<string, string> } = JSON.parse(
    typeof asset.source === 'string'
      ? asset.source
      : new TextDecoder().decode(asset.source),
  )
  const baseUrl = getImportMapBaseUrl(options)
  const mapping = Object.fromEntries(
    Object.entries(content.imports).map(([k, v]) => [
      k.slice(baseUrl.length),
      v.slice(baseUrl.length),
    ]),
  )
  return { asset, content, mapping }
}


import { expect, test } from 'vitest'
import { getBg, isBundled, page, viteTestUrl } from '~utils'

test('url dependency', async () => {
  const css = await page.$('.url-dep')
  expect(await getBg(css)).toMatch(
    isBundled ? /ok-[-\w]+\.png/ : `${viteTestUrl}/ok.png`,
  )
})


#include <iostream>
#include <nlohmann/json.hpp>

using json = nlohmann::json;
using namespace nlohmann::literals;

int main()
{
    try
    {
        // executing a failing JSON Patch operation
        json value = R"({
            "best_biscuit": {
                "name": "Oreo"
            }
        })"_json;
        json patch = R"([{
            "op": "test",
            "path": "/best_biscuit/name",
            "value": "Choco Leibniz"
        }])"_json;
        value.patch(patch);
    }
    catch (const json::other_error& e)
    {
        // output exception information
        std::cout << "message: " << e.what() << '\n'
                  << "exception id: " << e.id << std::endl;
    }
}


macro_rules! crate_root {
    () => {
        /// A facade around all the types we need from the `std`, `core`, and `alloc`
        /// crates. This avoids elaborate import wrangling having to happen in every
        /// module.
        mod lib {
            mod core {
                #[cfg(not(feature = "std"))]
                pub use core::*;
                #[cfg(feature = "std")]
                pub use std::*;
            }

            pub use self::core::{f32, f64};
            pub use self::core::{iter, num, str};

            #[cfg(any(feature = "std", feature = "alloc"))]
            pub use self::core::{cmp, mem};

            pub use self::core::cell::{Cell, RefCell};
            pub use self::core::cmp::Reverse;
            pub use self::core::fmt::{self, Debug, Display, Write as FmtWrite};
            pub use self::core::marker::PhantomData;
            pub use self::core::num::Wrapping;
            pub use self::core::ops::{Bound, Range, RangeFrom, RangeInclusive, RangeTo};
            pub use self::core::result;
            pub use self::core::time::Duration;

            #[cfg(all(feature = "alloc", not(feature = "std")))]
            pub use alloc::borrow::{Cow, ToOwned};
            #[cfg(feature = "std")]
            pub use std::borrow::{Cow, ToOwned};

            #[cfg(all(feature = "alloc", not(feature = "std")))]
            pub use alloc::string::{String, ToString};
            #[cfg(feature = "std")]
            pub use std::string::{String, ToString};

            #[cfg(all(feature = "alloc", not(feature = "std")))]
            pub use alloc::vec::Vec;
            #[cfg(feature = "std")]
            pub use std::vec::Vec;

            #[cfg(all(feature = "alloc", not(feature = "std")))]
            pub use alloc::boxed::Box;
            #[cfg(feature = "std")]
            pub use std::boxed::Box;

            #[cfg(all(feature = "rc", feature = "alloc", not(feature = "std")))]
            pub use alloc::rc::{Rc, Weak as RcWeak};
            #[cfg(all(feature = "rc", feature = "std"))]
            pub use std::rc::{Rc, Weak as RcWeak};

            #[cfg(all(feature = "rc", feature = "alloc", not(feature = "std")))]
            pub use alloc::sync::{Arc, Weak as ArcWeak};
            #[cfg(all(feature = "rc", feature = "std"))]
            pub use std::sync::{Arc, Weak as ArcWeak};

            #[cfg(all(feature = "alloc", not(feature = "std")))]
            pub use alloc::collections::{BTreeMap, BTreeSet, BinaryHeap, LinkedList, VecDeque};
            #[cfg(feature = "std")]
            pub use std::collections::{BTreeMap, BTreeSet, BinaryHeap, LinkedList, VecDeque};

            #[cfg(all(not(no_core_cstr), not(feature = "std")))]
            pub use self::core::ffi::CStr;
            #[cfg(feature = "std")]
            pub use std::ffi::CStr;

            #[cfg(all(not(no_core_cstr), feature = "alloc", not(feature = "std")))]
            pub use alloc::ffi::CString;
            #[cfg(feature = "std")]
            pub use std::ffi::CString;

            #[cfg(all(not(no_core_net), not(feature = "std")))]
            pub use self::core::net;
            #[cfg(feature = "std")]
            pub use std::net;

            #[cfg(feature = "std")]
            pub use std::error;

            #[cfg(feature = "std")]
            pub use std::collections::{HashMap, HashSet};
            #[cfg(feature = "std")]
            pub use std::ffi::{OsStr, OsString};
            #[cfg(feature = "std")]
            pub use std::hash::{BuildHasher, Hash};
            #[cfg(feature = "std")]
            pub use std::io::Write;
            #[cfg(feature = "std")]
            pub use std::path::{Path, PathBuf};
            #[cfg(feature = "std")]
            pub use std::sync::{Mutex, RwLock};
            #[cfg(feature = "std")]
            pub use std::time::{SystemTime, UNIX_EPOCH};

            #[cfg(all(feature = "std", no_target_has_atomic, not(no_std_atomic)))]
            pub use std::sync::atomic::{
                AtomicBool, AtomicI16, AtomicI32, AtomicI8, AtomicIsize, AtomicU16, AtomicU32,
                AtomicU8, AtomicUsize, Ordering,
            };
            #[cfg(all(feature = "std", no_target_has_atomic, not(no_std_atomic64)))]
            pub use std::sync::atomic::{AtomicI64, AtomicU64};

            #[cfg(all(feature = "std", not(no_target_has_atomic)))]
            pub use std::sync::atomic::Ordering;
            #[cfg(all(feature = "std", not(no_target_has_atomic), target_has_atomic = "8"))]
            pub use std::sync::atomic::{AtomicBool, AtomicI8, AtomicU8};
            #[cfg(all(feature = "std", not(no_target_has_atomic), target_has_atomic = "16"))]
            pub use std::sync::atomic::{AtomicI16, AtomicU16};
            #[cfg(all(feature = "std", not(no_target_has_atomic), target_has_atomic = "32"))]
            pub use std::sync::atomic::{AtomicI32, AtomicU32};
            #[cfg(all(feature = "std", not(no_target_has_atomic), target_has_atomic = "64"))]
            pub use std::sync::atomic::{AtomicI64, AtomicU64};
            #[cfg(all(feature = "std", not(no_target_has_atomic), target_has_atomic = "ptr"))]
            pub use std::sync::atomic::{AtomicIsize, AtomicUsize};

            #[cfg(not(no_core_num_saturating))]
            pub use self::core::num::Saturating;
        }

        // None of this crate's error handling needs the `From::from` error conversion
        // performed implicitly by the `?` operator or the standard library's `try!`
        // macro. This simplified macro gives a 5.5% improvement in compile time
        // compared to standard `try!`, and 9% improvement compared to `?`.
        macro_rules! tri {
            ($expr:expr) => {
                match $expr {
                    Ok(val) => val,
                    Err(err) => return Err(err),
                }
            };
        }

        #[cfg_attr(all(docsrs, if_docsrs_then_no_serde_core), path = "core/de/mod.rs")]
        pub mod de;
        #[cfg_attr(all(docsrs, if_docsrs_then_no_serde_core), path = "core/ser/mod.rs")]
        pub mod ser;

        #[cfg_attr(all(docsrs, if_docsrs_then_no_serde_core), path = "core/format.rs")]
        mod format;

        #[doc(inline)]
        pub use crate::de::{Deserialize, Deserializer};
        #[doc(inline)]
        pub use crate::ser::{Serialize, Serializer};

        // Used by generated code. Not public API.
        #[doc(hidden)]
        #[cfg_attr(
            all(docsrs, if_docsrs_then_no_serde_core),
            path = "core/private/mod.rs"
        )]
        mod private;

        // Used by declarative macro generated code. Not public API.
        #[doc(hidden)]
        pub mod __private {
            #[doc(hidden)]
            pub use crate::private::doc;
            #[doc(hidden)]
            pub use core::result::Result;
        }

        include!(concat!(env!("OUT_DIR"), "/private.rs"));

        #[cfg(all(not(feature = "std"), no_core_error))]
        #[cfg_attr(all(docsrs, if_docsrs_then_no_serde_core), path = "core/std_error.rs")]
        mod std_error;
    };
}


# <small>nlohmann::basic_json::</small>is_array

```cpp
constexpr bool is_array() const noexcept;
```

This function returns `#!cpp true` if and only if the JSON value is an array.

## Return value

`#!cpp true` if type is an array, `#!cpp false` otherwise.

## Exception safety

No-throw guarantee: this member function never throws exceptions.

## Complexity

Constant.

## Examples

??? example

    The following code exemplifies `is_array()` for all JSON types.

    ```cpp
    --8<-- "examples/is_array.cpp"
    ```

    Output:

    ```json
    --8<-- "examples/is_array.output"
    ```

## Version history

- Added in version 1.0.0.


# Migration from v7

If you are migrating from `rolldown-vite`, the technical preview release for Rolldown integrated Vite for v6 & v7, only the sections with <Badge text="NRV" type="warning" /> in the title are applicable.

## Default Browser Target Change [<Badge text="NRV" type="warning" />](#migration-from-v7)

The default browser values of `build.target` and `'baseline-widely-available'` are updated to newer browser versions:

- Chrome 107 → 111
- Edge 107 → 111
- Firefox 104 → 114
- Safari 16.0 → 16.4

These browser versions align with [Baseline Widely Available](https://web-platform-dx.github.io/baseline/) feature sets as of 2026-01-01. In other words, they were all released about two and a half years ago.

## Rolldown

Vite 8 uses [Rolldown](https://rolldown.rs/) and [Oxc](https://oxc.rs/) based tools instead of [esbuild](https://esbuild.github.io/) and [Rollup](https://rollupjs.org/).

### Gradual Migration

The `rolldown-vite` package implements Vite 7 with Rolldown, without other Vite 8 changes. This can be used as an intermediate step to migrate to Vite 8. See [the Rolldown Integration guide](https://v7.vite.dev/guide/rolldown) in the Vite 7 docs to switch to `rolldown-vite` from Vite 7.

For users migrating from `rolldown-vite` to Vite 8, you can undo the dependency changes in `package.json` and update to Vite 8:

```json
{
  "devDependencies": {
    "vite": "npm:rolldown-vite@7.2.2" // [!code --]
    "vite": "^8.0.0" // [!code ++]
  }
}
```

### Dependency Optimizer Now Uses Rolldown

Rolldown is now used for dependency optimization instead of esbuild. Vite still supports [`optimizeDeps.esbuildOptions`](/config/dep-optimization-options#optimizedeps-esbuildoptions) for backward compatibility by converting it to [`optimizeDeps.rolldownOptions`](/config/dep-optimization-options#optimizedeps-rolldownoptions) automatically. `optimizeDeps.esbuildOptions` is now deprecated and will be removed in the future and we encourage you to migrate to `optimizeDeps.rolldownOptions`.

The following options are converted automatically:

- [`esbuildOptions.minify`](https://esbuild.github.io/api/#minify) -> [`rolldownOptions.output.minify`](https://rolldown.rs/reference/OutputOptions.minify)
- [`esbuildOptions.treeShaking`](https://esbuild.github.io/api/#tree-shaking) -> [`rolldownOptions.treeshake`](https://rolldown.rs/reference/InputOptions.treeshake)
- [`esbuildOptions.define`](https://esbuild.github.io/api/#define) -> [`rolldownOptions.transform.define`](https://rolldown.rs/reference/InputOptions.transform#define)
- [`esbuildOptions.loader`](https://esbuild.github.io/api/#loader) -> [`rolldownOptions.moduleTypes`](https://rolldown.rs/reference/InputOptions.moduleTypes)
- [`esbuildOptions.preserveSymlinks`](https://esbuild.github.io/api/#preserve-symlinks) -> [`!rolldownOptions.resolve.symlinks`](https://rolldown.rs/reference/InputOptions.resolve#symlinks)
- [`esbuildOptions.resolveExtensions`](https://esbuild.github.io/api/#resolve-extensions) -> [`rolldownOptions.resolve.extensions`](https://rolldown.rs/reference/InputOptions.resolve#extensions)
- [`esbuildOptions.mainFields`](https://esbuild.github.io/api/#main-fields) -> [`rolldownOptions.resolve.mainFields`](https://rolldown.rs/reference/InputOptions.resolve#mainfields)
- [`esbuildOptions.conditions`](https://esbuild.github.io/api/#conditions) -> [`rolldownOptions.resolve.conditionNames`](https://rolldown.rs/reference/InputOptions.resolve#conditionnames)
- [`esbuildOptions.keepNames`](https://esbuild.github.io/api/#keep-names) -> [`rolldownOptions.output.keepNames`](https://rolldown.rs/reference/OutputOptions.keepNames)
- [`esbuildOptions.platform`](https://esbuild.github.io/api/#platform) -> [`rolldownOptions.platform`](https://rolldown.rs/reference/InputOptions.platform)
- [`esbuildOptions.plugins`](https://esbuild.github.io/plugins/) -> [`rolldownOptions.plugins`](https://rolldown.rs/reference/InputOptions.plugins) (partial support)

You can get the options set by the compatibility layer from the `configResolved` hook:

```js
const plugin = {
  name: 'log-config',
  configResolved(config) {
    console.log('options', config.optimizeDeps.rolldownOptions)
  },
},
```

### JavaScript Transforms by Oxc

Oxc is now used for JavaScript transformation instead of esbuild. Vite still supports the [`esbuild`](/config/shared-options#esbuild) option for backward compatibility by converting it to [`oxc`](/config/shared-options#oxc) automatically. `esbuild` is now deprecated and will be removed in the future and we encourage you to migrate to `oxc`.

The following options are converted automatically:

- `esbuild.jsxInject` -> `oxc.jsxInject`
- `esbuild.include` -> `oxc.include`
- `esbuild.exclude` -> `oxc.exclude`
- [`esbuild.jsx`](https://esbuild.github.io/api/#jsx) -> [`oxc.jsx`](https://oxc.rs/docs/guide/usage/transformer/jsx)
  - `esbuild.jsx: 'preserve'` -> `oxc.jsx: 'preserve'`
  - `esbuild.jsx: 'automatic'` -> `oxc.jsx: { runtime: 'automatic' }`
    - [`esbuild.jsxImportSource`](https://esbuild.github.io/api/#jsx-import-source) -> `oxc.jsx.importSource`
  - `esbuild.jsx: 'transform'` -> `oxc.jsx: { runtime: 'classic' }`
    - [`esbuild.jsxFactory`](https://esbuild.github.io/api/#jsx-factory) -> `oxc.jsx.pragma`
    - [`esbuild.jsxFragment`](https://esbuild.github.io/api/#jsx-fragment) -> `oxc.jsx.pragmaFrag`
  - [`esbuild.jsxDev`](https://esbuild.github.io/api/#jsx-dev) -> `oxc.jsx.development`
  - [`esbuild.jsxSideEffects`](https://esbuild.github.io/api/#jsx-side-effects) -> `oxc.jsx.pure`
- [`esbuild.define`](https://esbuild.github.io/api/#define) -> [`oxc.define`](https://oxc.rs/docs/guide/usage/transformer/global-variable-replacement#define)
- [`esbuild.banner`](https://esbuild.github.io/api/#banner) -> custom plugin using transform hook
- [`esbuild.footer`](https://esbuild.github.io/api/#footer) -> custom plugin using transform hook

The [`esbuild.supported`](https://esbuild.github.io/api/#supported) option is not supported by Oxc. If you need this option, please see [oxc-project/oxc#15373](https://github.com/oxc-project/oxc/issues/15373).

You can get the options set by the compatibility layer from the `configResolved` hook:

```js
const plugin = {
  name: 'log-config',
  configResolved(config) {
    console.log('options', config.oxc)
  },
},
```

Currently, the Oxc transformer does not support lowering native decorators as we are waiting for the specification to progress, see ([oxc-project/oxc#9170](https://github.com/oxc-project/oxc/issues/9170)).

:::: details Workaround for lowering native decorators

You can use [Babel](https://babeljs.io/) or [SWC](https://swc.rs/) to lower native decorators for the time being.

**Using Babel:**

::: code-group

```bash [npm]
$ npm install -D @rolldown/plugin-babel @babel/plugin-proposal-decorators
```

```bash [Yarn]
$ yarn add -D @rolldown/plugin-babel @babel/plugin-proposal-decorators
```

```bash [pnpm]
$ pnpm add -D @rolldown/plugin-babel @babel/plugin-proposal-decorators
```

```bash [Bun]
$ bun add -D @rolldown/plugin-babel @babel/plugin-proposal-decorators
```

```bash [Deno]
$ deno add -D npm:@rolldown/plugin-babel npm:@babel/plugin-proposal-decorators
```

:::

```ts [vite.config.ts]
import { defineConfig } from 'vite'
import babel from '@rolldown/plugin-babel'

function decoratorPreset(options: Record<string, unknown>) {
  return {
    preset: () => ({
      plugins: [['@babel/plugin-proposal-decorators', options]],
    }),
    rolldown: {
      // Only run this transform if the file contains a decorator.
      filter: {
        code: '@',
      },
    },
  }
}

export default defineConfig({
  plugins: [babel({ presets: [decoratorPreset({ version: '2023-11' })] })],
})
```

**Using SWC:**

::: code-group

```bash [npm]
$ npm install -D @rollup/plugin-swc @swc/core
```

```bash [Yarn]
$ yarn add -D @rollup/plugin-swc @swc/core
```

```bash [pnpm]
$ pnpm add -D @rollup/plugin-swc @swc/core
```

```bash [Bun]
$ bun add -D @rollup/plugin-swc @swc/core
```

```bash [Deno]
$ deno add -D npm:@rollup/plugin-swc npm:@swc/core
```

:::

```js
import { defineConfig, withFilter } from 'vite'
import swc from '@rollup/plugin-swc'

export default defineConfig({
  // ...
  plugins: [
    withFilter(
      swc({
        swc: {
          jsc: {
            parser: { decorators: true, decoratorsBeforeExport: true },
            transform: { decoratorVersion: '2023-11' },
          },
        },
      }),
      // Only run this transform if the file contains a decorator.
      { transform: { code: '@' } },
    ),
  ],
})
```

::::

#### esbuild Fallbacks

`esbuild` is no longer directly used by Vite and is now an optional dependency. If you are using a plugin that uses the `transformWithEsbuild` function, you need to install `esbuild` as a `devDependency`. The `transformWithEsbuild` function is deprecated and will be removed in the future. We recommend migrating to the new `transformWithOxc` function instead.

### JavaScript Minification by Oxc

The Oxc Minifier is now used for JavaScript minification instead of esbuild. You can use the deprecated [`build.minify: 'esbuild'`](/config/build-options#build-minify) option to switch back to esbuild. This configuration option will be removed in the future and you need install `esbuild` as a `devDependency` as Vite no longer relies on esbuild directly.

If you were using the `esbuild.minify*` options to control minification behavior, you can now use `build.rolldownOptions.output.minify` instead. If you were using the `esbuild.drop` option, you can now use [`build.rolldownOptions.output.minify.compress.drop*` options](https://oxc.rs/docs/guide/usage/minifier/dead-code-elimination).

Property mangling and its related options ([`mangleProps`, `reserveProps`, `mangleQuoted`, `mangleCache`](https://esbuild.github.io/api/#mangle-props)) are not supported by Oxc. If you need these options, please see [oxc-project/oxc#15375](https://github.com/oxc-project/oxc/issues/15375).

esbuild and Oxc Minifier make slightly different assumptions about source code. In case you suspect the minifier is causing breakage in your code, you can compare these assumptions here:

- [esbuild minify assumptions](https://esbuild.github.io/api/#minify-considerations)
- [Oxc Minifier assumptions](https://github.com/oxc-project/oxc/blob/main/crates/oxc_minifier/docs/ASSUMPTIONS.md)

Please report any issues you find related to minification in your JavaScript apps.

### CSS Minification by Lightning CSS

[Lightning CSS](https://lightningcss.dev/) is now used for CSS minification by default. You can use the [`build.cssMinify: 'esbuild'`](/config/build-options#build-cssminify) option to switch back to esbuild. Note that you need to install `esbuild` as a `devDependency`.

Lightning CSS supports better syntax lowering and your CSS bundle size might increase slightly.

### Consistent CommonJS Interop

The `default` import from a CommonJS (CJS) module is now handled in a consistent way.

If it matches one of the following conditions, the `default` import is the `module.exports` value of the importee CJS module. Otherwise, the `default` import is the `module.exports.default` value of the importee CJS module:

- The importer is `.mjs` or `.mts`.
- The closest `package.json` for the importer has a `type` field set to `module`.
- The `module.exports.__esModule` value of the importee CJS module is not set to `true`.

::: details The previous behavior

In development, if it matches one of the following conditions, the `default` import is the `module.exports` value of the importee CJS module. Otherwise, the `default` import is the `module.exports.default` value of the importee CJS module:

- _The importer is included in the dependency optimization_ and `.mjs` or `.mts`.
- _The importer is included in the dependency optimization_ and the closest `package.json` for the importer has a `type` field set to `module`.
- The `module.exports.__esModule` value of the importee CJS module is not set to `true`.

In build, the conditions were:

- The `module.exports.__esModule` value of the importee CJS module is not set to `true`.
- _`default` property of `module.exports` does not exist_.

(assuming [`build.commonjsOptions.defaultIsModuleExports`](https://github.com/rollup/plugins/tree/master/packages/commonjs#defaultismoduleexports) is not changed from the default `'auto'`)

:::

See Rolldown's docs about this problem for more details: [Ambiguous `default` import from CJS modules - Bundling CJS | Rolldown](https://rolldown.rs/in-depth/bundling-cjs#ambiguous-default-import-from-cjs-modules).

This change may break some existing code importing CJS modules. You can use the deprecated `legacy.inconsistentCjsInterop: true` option to temporarily restore the previous behavior. If you find a package that is affected by this change, please report it to the package author or send them a pull request. Make sure to link to the Rolldown documentation above so that the author can understand the context.

### Removed Module Resolution Using Format Sniffing

When both `browser` and `module` fields are present in `package.json`, Vite used to resolve the field based on the content of the file and it used to pick the ESM file for browsers. This was introduced because some packages were using the `module` field to point to ESM files for Node.js and some other packages were using the `browser` field to point to UMD files for browsers. Given that the modern `exports` field solved this problem and is now adopted by many packages, Vite no longer uses this heuristic and always respects the order of the [`resolve.mainFields`](/config/shared-options#resolve-mainfields) option. If you were relying on this behavior, you can use the [`resolve.alias`](/config/shared-options#resolve-alias) option to map the field to the desired file or apply a patch with your package manager (e.g. `patch-package`, `pnpm patch`).

### Require Calls For Externalized Modules

`require` calls for externalized modules are now preserved as `require` calls and not converted to `import` statements. This is to preserve the semantics of `require` calls. If you want to convert them to `import` statements, you can use [Rolldown's built-in `esmExternalRequirePlugin`](https://rolldown.rs/builtin-plugins/esm-external-require), which is re-exported from `vite`.

```js
import { defineConfig, esmExternalRequirePlugin } from 'vite'

export default defineConfig({
  // ...
  plugins: [
    esmExternalRequirePlugin({
      external: ['react', 'vue', /^node:/],
    }),
  ],
})
```

See Rolldown's docs for more details: [`require` external modules - Bundling CJS | Rolldown](https://rolldown.rs/in-depth/bundling-cjs#require-external-modules).

### `import.meta.url` in UMD / IIFE

`import.meta.url` is no longer polyfilled in UMD / IIFE output formats. It will be replaced with `undefined` by default. If you prefer the previous behavior, you can use the [`define`](/config/shared-options#define) option with [`build.rolldownOptions.output.intro`](https://rolldown.rs/reference/OutputOptions.intro) option. See Rolldown's docs for more details: [Well-known `import.meta` properties - Non ESM Output Formats | Rolldown](https://rolldown.rs/in-depth/non-esm-output-formats#well-known-import-meta-properties).

### Removed `build.rollupOptions.watch.chokidar` option

The `build.rollupOptions.watch.chokidar` option was removed. Please migrate to the [`build.rolldownOptions.watch.watcher`](https://rolldown.rs/reference/InputOptions.watch#watcher) option.

### Removed object form `build.rollupOptions.output.manualChunks` and deprecate function form one

The object form `output.manualChunks` option is not supported anymore. The function form `output.manualChunks` is deprecated. Rolldown has the more flexible [`codeSplitting`](https://rolldown.rs/reference/OutputOptions.codeSplitting) option. See Rolldown's docs for more details about `codeSplitting`: [Manual Code Splitting - Rolldown](https://rolldown.rs/in-depth/manual-code-splitting).

### `build()` Throws `BundleError`

_This change only affects JS API users._

`build()` now throws a [`BundleError`](https://rolldown.rs/reference/TypeAlias.BundleError) instead of the raw error thrown in the plugin. `BundleError` is typed as `Error & { errors?: RolldownError[] }` and it wraps the individual errors in an `errors` array. If you need the individual errors, you need to access `.errors`:

```js
try {
  await build()
} catch (e) {
  if (e.errors) {
    for (const error of e.errors) {
      console.log(error.code) // error code
    }
  }
}
```

### Module Type Support and Auto Detection

_This change only affects plugin authors._

Rolldown has experimental support for [Module types](https://rolldown.rs/guide/notable-features#module-types), similar to [esbuild's `loader` option](https://esbuild.github.io/api/#loader). Due to this, Rolldown automatically sets a module type based on the extension of the resolved id. If you are converting content from other module types to JavaScript in `load` or `transform` hooks, you may need to add `moduleType: 'js'` to the returned value:

```js
const plugin = {
  name: 'txt-loader',
  load(id) {
    if (id.endsWith('.txt')) {
      const content = fs.readFile(id, 'utf-8')
      return {
        code: `export default ${JSON.stringify(content)}`,
        moduleType: 'js', // [!code ++]
      }
    }
  },
}
```

### Other Related Deprecations

The following options are deprecated and will be removed in the future:

- `build.rollupOptions`: renamed to `build.rolldownOptions`
- `worker.rollupOptions`: renamed to `worker.rolldownOptions`
- `build.commonjsOptions`: it is now no-op
- `build.dynamicImportVarsOptions.warnOnError`: it is now no-op
- `resolve.alias[].customResolver`: Use a custom plugin with `resolveId` hook and `enforce: 'pre'` instead

## Removed Deprecated Features [<Badge text="NRV" type="warning" />](#migration-from-v7)

- Passing a URL to `import.meta.hot.accept` is no longer supported. Please pass an id instead. ([#21382](https://github.com/vitejs/vite/pull/21382))

## Advanced

These breaking changes are expected to only affect a minority of use cases:

- [Extglobs](https://github.com/micromatch/picomatch/blob/master/README.md#extglobs) are not supported yet ([rolldown-vite#365](https://github.com/vitejs/rolldown-vite/issues/365))
- TypeScript legacy namespace is only supported partially. See [Oxc Transformer's related documentation](https://oxc.rs/docs/guide/usage/transformer/typescript.html#partial-namespace-support) for more details.
- `define` does not share reference for objects: When you pass an object as a value to `define`, each variable will have a separate copy of the object. See [Oxc Transformer's related documentation](https://oxc.rs/docs/guide/usage/transformer/global-variable-replacement#define) for more details.
- `bundle` object changes (`bundle` is an object passed in `generateBundle` / `writeBundle` hooks, returned by `build` function):
  - Assigning to `bundle[foo]` is not supported. This is discouraged by Rollup as well. Please use `this.emitFile()` instead.
  - the reference is not shared across the hooks ([rolldown-vite#410](https://github.com/vitejs/rolldown-vite/issues/410))
  - `structuredClone(bundle)` errors with `DataCloneError: #<Object> could not be cloned`. This is not supported anymore. Please clone it with `structuredClone({ ...bundle })`. ([rolldown-vite#128](https://github.com/vitejs/rolldown-vite/issues/128))
- All parallel hooks in Rollup work as sequential hooks. See [Rolldown's documentation](https://rolldown.rs/apis/plugin-api#sequential-hook-execution) for more details.
- `"use strict";` is not injected sometimes. See [Rolldown's documentation](https://rolldown.rs/in-depth/directives) for more details.
- Transforming to ES5 and below with plugin-legacy is not supported ([rolldown-vite#452](https://github.com/vitejs/rolldown-vite/issues/452))
- Passing the same browser with multiple versions of it to `build.target` option now errors: esbuild selects the latest version of it, which was probably not what you intended.
- Missing support by Rolldown: The following features are not supported by Rolldown and are no longer supported by Vite.
  - `build.rollupOptions.output.format: 'system'` ([rolldown#2387](https://github.com/rolldown/rolldown/issues/2387))
  - `build.rollupOptions.output.format: 'amd'` ([rolldown#2528](https://github.com/rolldown/rolldown/issues/2528))
  - `shouldTransformCachedModule` hook ([rolldown#4389](https://github.com/rolldown/rolldown/issues/4389))
  - `resolveImportMeta` hook ([rolldown#1010](https://github.com/rolldown/rolldown/issues/1010))
  - `renderDynamicImport` hook ([rolldown#4532](https://github.com/rolldown/rolldown/issues/4532))
  - `resolveFileUrl` hook
- `parseAst` / `parseAstAsync` functions are now deprecated in favor of `parseSync` / `parse` functions which have more features.
- comments are removed before the `renderChunk` hook instead of after the `renderChunk` hook
- comments other than the ones listed [here](https://rolldown.rs/reference/OutputOptions.comments) are moved, while Rollup only removes comments if the adjacent code is removed

## Migration from v6

Check the [Migration from v6 Guide](https://v7.vite.dev/guide/migration) in the Vite v7 docs first to see the needed changes to port your app to Vite 7, and then proceed with the changes on this page.


# JSON_HAS_RANGES

```cpp
#define JSON_HAS_RANGES /* value */
```

This macro indicates whether the standard library has any support for ranges. Implies support for concepts.
Possible values are `1` when supported or `0` when unsupported.

## Default definition

The default value is detected based on the preprocessor macro `#!cpp __cpp_lib_ranges`.

When the macro is not defined, the library will define it to its default value.

!!! info "Known compiler/stdlib exclusions"

    Even when the feature-test macro `__cpp_lib_ranges` indicates ranges support is available, the library disables it on
    the following incomplete or broken toolchains:

    - **GCC 11.1.0** — disabled (the shipped `<ranges>` header has a syntax error; [issue #4440](https://github.com/nlohmann/json/issues/4440))
    - **libstdc++ < 11** — disabled (incomplete C++20 ranges support; [issue #4440](https://github.com/nlohmann/json/issues/4440))
    - **Clang < 16 with libstdc++** — disabled (incomplete ranges support; [issue #4440](https://github.com/nlohmann/json/issues/4440))
    - **libc++ < 160000** — disabled (incomplete C++20 ranges support; [issue #4440](https://github.com/nlohmann/json/issues/4440))
    - **nvcc (CUDA) 12.0.x and 12.1.x** — disabled (the `enable_borrowed_range` variable-template syntax triggers a parse error
      under these two toolkit versions; fixed in CUDA 12.2; [issue #3907](https://github.com/nlohmann/json/issues/3907))

    If `JSON_HAS_RANGES` is `0` despite `__cpp_lib_ranges` being defined, one of the exclusions above likely applies to your toolchain.

## Examples

??? example

    The code below forces the library to enable support for ranges:

    ```cpp
    #define JSON_HAS_RANGES 1
    #include <nlohmann/json.hpp>

    ...
    ```

## Version history

- Added in version 3.11.0.


import ReactDomClient from 'react-dom/client'
import React from 'react'
import Root from './root'

async function main() {
  const el = document.getElementById('root')
  React.startTransition(() => {
    ReactDomClient.hydrateRoot(el!, <Root />)
  })
}

main()