diff --git a/docs/guide/features.md b/docs/guide/features.md index dfd8c57f7..ea5855ab9 100644 --- a/docs/guide/features.md +++ b/docs/guide/features.md @@ -160,6 +160,35 @@ For example, to make the default import of `*.svg` a React component: ::: +## HTML + +HTML files stand [front-and-center](/guide/#index-html-and-project-root) of a Vite project, serving as the entry points for your application, making it simple to build single-page and [multi-page applications](/guide/build.html#multi-page-app). + +Any HTML files in your project root can be directly accessed by its respective directory path: + +- `/index.html` -> `http://localhost:5173/` +- `/about.html` -> `http://localhost:5173/about.html` +- `/blog/index.html` -> `http://localhost:5173/blog/index.html` + +HTML elements such as ` + + +``` + +To opt-out of HTML processing on certain elements, you can add the `vite-ignore` attribute on the element, which can be useful when referencing external assets or CDN. + ## Vue Vite provides first-class Vue support: diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index cf6d3c3d9..cdf002b7a 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -211,11 +211,13 @@ export function getScriptInfo(node: DefaultTreeAdapterMap['element']): { sourceCodeLocation: Token.Location | undefined isModule: boolean isAsync: boolean + isIgnored: boolean } { let src: Token.Attribute | undefined let sourceCodeLocation: 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') { @@ -227,9 +229,11 @@ export function getScriptInfo(node: DefaultTreeAdapterMap['element']): { isModule = true } else if (p.name === 'async') { isAsync = true + } else if (p.name === 'vite-ignore') { + isIgnored = true } } - return { src, sourceCodeLocation, isModule, isAsync } + return { src, sourceCodeLocation, isModule, isAsync, isIgnored } } const attrValueStartRE = /=\s*(.)/ @@ -260,6 +264,19 @@ export function overwriteAttrValue( 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} */ @@ -437,68 +454,72 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { // script tags if (node.nodeName === 'script') { - const { src, sourceCodeLocation, isModule, isAsync } = + const { src, sourceCodeLocation, isModule, isAsync, isIgnored } = getScriptInfo(node) - const url = src && src.value - const isPublicFile = !!(url && checkPublicFile(url, config)) - if (isPublicFile) { - // referencing public dir url, prefix with base - overwriteAttrValue( - s, - sourceCodeLocation!, - partialEncodeURIPath(toOutputPublicFilePath(url)), - ) - } - - if (isModule) { - inlineModuleIndex++ - if (url && !isExcludedUrl(url) && !isPublicFile) { - setModuleSideEffectPromises.push( - this.resolve(url, id) - .then((resolved) => { - if (!resolved) { - return Promise.reject() - } - return this.load(resolved) - }) - .then((mod) => { - // set this to keep the module even if `treeshake.moduleSideEffects=false` is set - mod.moduleSideEffects = true - }), + 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, + sourceCodeLocation!, + partialEncodeURIPath(toOutputPublicFilePath(url)), ) - // + 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( + ` - 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( - ` - +

index.html (fallback)

+ +
External path:
+ + diff --git a/playground/html/vite.config.js b/playground/html/vite.config.js index cc3f6ba3a..d71da5209 100644 --- a/playground/html/vite.config.js +++ b/playground/html/vite.config.js @@ -231,5 +231,30 @@ ${ }, }, }, + serveExternalPathPlugin(), ], }) + +/** @returns {import('vite').Plugin} */ +function serveExternalPathPlugin() { + const handler = (req, res, next) => { + if (req.url === '/external-path.js') { + res.setHeader('Content-Type', 'application/javascript') + res.end('document.querySelector(".external-path").textContent = "works"') + } else if (req.url === '/external-path.css') { + res.setHeader('Content-Type', 'text/css') + res.end('.external-path{color:red}') + } else { + next() + } + } + return { + name: 'serve-external-path', + configureServer(server) { + server.middlewares.use(handler) + }, + configurePreviewServer(server) { + server.middlewares.use(handler) + }, + } +}