From be7cebd8ba6e979ebafeaeee7a63022513f7346a Mon Sep 17 00:00:00 2001 From: bluwy Date: Wed, 27 Jul 2022 17:19:43 +0800 Subject: [PATCH 01/12] fix: prepend module scripts after importmap --- packages/vite/src/node/plugins/html.ts | 44 ++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index ac7ac834ff941a..af69d4138f7696 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -868,6 +868,8 @@ function getBaseInHTML(urlRelativePath: string, config: ResolvedConfig) { const headInjectRE = /([ \t]*)<\/head>/i const headPrependInjectRE = /([ \t]*)]*>/i +const importMapPrependInjectRE = + /([ \t]*)]*type\s*=\s*["']?importmap["']?[^>]*>.*?<\/script>/is const htmlInjectRE = /<\/html>/i const htmlPrependInjectRE = /([ \t]*)]*>/i @@ -884,7 +886,49 @@ function injectToHead( ) { if (tags.length === 0) return html + // sort importmap script as first + const importMapIndex = tags.findIndex( + (t) => t.tag === 'script' && t.attrs?.type === 'importmap' + ) + if (importMapIndex !== -1) { + tags = [ + tags[importMapIndex], + ...tags.slice(0, importMapIndex), + ...tags.slice(importMapIndex + 1) + ] + } + if (prepend) { + // special treatment for module script tags if have existing importmap + if (importMapPrependInjectRE.test(html)) { + // split tags between module scripts and others + const moduleScriptTags: HtmlTagDescriptor[] = [] + const otherTags: HtmlTagDescriptor[] = [] + for (const tag of tags) { + if (tag.tag === 'script' && tag.attrs?.type === 'module') { + moduleScriptTags.push(tag) + } else { + otherTags.push(tag) + } + } + // module script tags inject after importmap instead + if (moduleScriptTags.length) { + html = html.replace( + importMapPrependInjectRE, + (match, p1) => + `${match}\n${serializeTags(moduleScriptTags, incrementIndent(p1))}` + ) + } + // rest inject as the first element of head + if (otherTags.length) { + html = html.replace( + headPrependInjectRE, + (match, p1) => + `${match}\n${serializeTags(otherTags, incrementIndent(p1))}` + ) + } + return html + } // inject as the first element of head if (headPrependInjectRE.test(html)) { return html.replace( From 2d0c9762218631f1488029544150b4949c56abfc Mon Sep 17 00:00:00 2001 From: bluwy Date: Wed, 27 Jul 2022 17:20:06 +0800 Subject: [PATCH 02/12] chore: add tests --- .../external/__tests__/external.spec.ts | 8 +++++++- playground/html/__tests__/html.spec.ts | 16 +++++++++++++++- playground/html/vite.config.js | 19 +++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/playground/external/__tests__/external.spec.ts b/playground/external/__tests__/external.spec.ts index ed1d1872181c02..031e8adf3510b5 100644 --- a/playground/external/__tests__/external.spec.ts +++ b/playground/external/__tests__/external.spec.ts @@ -1,4 +1,10 @@ -import { isBuild, page } from '~utils' +import { browserLogs, isBuild, isServe, page } from '~utils' + +test.runIf(isServe)('importmap', () => { + expect(browserLogs).not.toContain( + 'An import map is added after module script load was triggered.' + ) +}) describe.runIf(isBuild)('build', () => { test('should externalize imported packages', async () => { diff --git a/playground/html/__tests__/html.spec.ts b/playground/html/__tests__/html.spec.ts index 4c0376636abd5b..c592a69f0c3424 100644 --- a/playground/html/__tests__/html.spec.ts +++ b/playground/html/__tests__/html.spec.ts @@ -1,5 +1,13 @@ import { beforeAll, describe, expect, test } from 'vitest' -import { editFile, getColor, isBuild, isServe, page, viteTestUrl } from '~utils' +import { + browserLogs, + editFile, + getColor, + isBuild, + isServe, + page, + viteTestUrl +} from '~utils' function testPage(isNested: boolean) { test('pre transform', async () => { @@ -242,3 +250,9 @@ describe.runIf(isServe)('invalid', () => { expect(content).toBeTruthy() }) }) + +test.runIf(isServe)('importmap', () => { + expect(browserLogs).not.toContain( + 'An import map is added after module script load was triggered.' + ) +}) diff --git a/playground/html/vite.config.js b/playground/html/vite.config.js index 31cc1656d2f19e..143051a0326fd6 100644 --- a/playground/html/vite.config.js +++ b/playground/html/vite.config.js @@ -160,6 +160,25 @@ ${ } ] } + }, + { + name: 'head-prepend-importmap', + transformIndexHtml() { + return [ + { + tag: 'script', + attrs: { type: 'importmap' }, + children: ` + { + "imports": { + "vue": "https://unpkg.com/vue@3.2.0/dist/vue.runtime.esm-browser.js" + } + } + `, + injectTo: 'head-prepend' + } + ] + } } ] } From 6370ec4a07376b46ec8f51e1f052160dcc848f20 Mon Sep 17 00:00:00 2001 From: bluwy Date: Wed, 27 Jul 2022 17:24:07 +0800 Subject: [PATCH 03/12] refactor: rename inject re --- packages/vite/src/node/plugins/html.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index af69d4138f7696..e3ddf7c8aec332 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -868,7 +868,7 @@ function getBaseInHTML(urlRelativePath: string, config: ResolvedConfig) { const headInjectRE = /([ \t]*)<\/head>/i const headPrependInjectRE = /([ \t]*)]*>/i -const importMapPrependInjectRE = +const importMapInjectRE = /([ \t]*)]*type\s*=\s*["']?importmap["']?[^>]*>.*?<\/script>/is const htmlInjectRE = /<\/html>/i @@ -900,7 +900,7 @@ function injectToHead( if (prepend) { // special treatment for module script tags if have existing importmap - if (importMapPrependInjectRE.test(html)) { + if (importMapInjectRE.test(html)) { // split tags between module scripts and others const moduleScriptTags: HtmlTagDescriptor[] = [] const otherTags: HtmlTagDescriptor[] = [] @@ -914,7 +914,7 @@ function injectToHead( // module script tags inject after importmap instead if (moduleScriptTags.length) { html = html.replace( - importMapPrependInjectRE, + importMapInjectRE, (match, p1) => `${match}\n${serializeTags(moduleScriptTags, incrementIndent(p1))}` ) From 05a10c60ef9be5a113a66af84940c74a2dc66afc Mon Sep 17 00:00:00 2001 From: bluwy Date: Wed, 27 Jul 2022 22:52:33 +0800 Subject: [PATCH 04/12] refactor: simplify replace --- packages/vite/src/node/plugins/html.ts | 74 ++++++++++--------------- packages/vite/src/node/plugins/index.ts | 9 ++- 2 files changed, 37 insertions(+), 46 deletions(-) diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index e3ddf7c8aec332..83aaa6fb174c55 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -218,6 +218,36 @@ function handleParseError( ) } +/** + * Move importmap to top of with `transformIndexHtml`. + * Must be added after post plugins. + */ +export function htmlImportMapPlugin(): Plugin { + const importMapRE = + /[ \t]*]*type\s*=\s*["']?importmap["']?[^>]*>.*?<\/script>/is + return { + name: 'vite:html-importmap', + transformIndexHtml(html) { + // HTML must have head for importmap + if (!headPrependInjectRE.test(html)) return + + let importMap: string | undefined + html = html.replace(importMapRE, (match) => { + importMap = match + return '' + }) + if (importMap) { + html = html.replace( + headPrependInjectRE, + (match) => `${match}\n${importMap}` + ) + } + + return html + } + } +} + /** * Compiles index.html into an entry js module */ @@ -868,8 +898,6 @@ function getBaseInHTML(urlRelativePath: string, config: ResolvedConfig) { const headInjectRE = /([ \t]*)<\/head>/i const headPrependInjectRE = /([ \t]*)]*>/i -const importMapInjectRE = - /([ \t]*)]*type\s*=\s*["']?importmap["']?[^>]*>.*?<\/script>/is const htmlInjectRE = /<\/html>/i const htmlPrependInjectRE = /([ \t]*)]*>/i @@ -886,49 +914,7 @@ function injectToHead( ) { if (tags.length === 0) return html - // sort importmap script as first - const importMapIndex = tags.findIndex( - (t) => t.tag === 'script' && t.attrs?.type === 'importmap' - ) - if (importMapIndex !== -1) { - tags = [ - tags[importMapIndex], - ...tags.slice(0, importMapIndex), - ...tags.slice(importMapIndex + 1) - ] - } - if (prepend) { - // special treatment for module script tags if have existing importmap - if (importMapInjectRE.test(html)) { - // split tags between module scripts and others - const moduleScriptTags: HtmlTagDescriptor[] = [] - const otherTags: HtmlTagDescriptor[] = [] - for (const tag of tags) { - if (tag.tag === 'script' && tag.attrs?.type === 'module') { - moduleScriptTags.push(tag) - } else { - otherTags.push(tag) - } - } - // module script tags inject after importmap instead - if (moduleScriptTags.length) { - html = html.replace( - importMapInjectRE, - (match, p1) => - `${match}\n${serializeTags(moduleScriptTags, incrementIndent(p1))}` - ) - } - // rest inject as the first element of head - if (otherTags.length) { - html = html.replace( - headPrependInjectRE, - (match, p1) => - `${match}\n${serializeTags(otherTags, incrementIndent(p1))}` - ) - } - return html - } // inject as the first element of head if (headPrependInjectRE.test(html)) { return html.replace( diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index a2fbebcc75b66e..74d57a8f28bb99 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -12,7 +12,11 @@ import { importAnalysisPlugin } from './importAnalysis' import { cssPlugin, cssPostPlugin } from './css' import { assetPlugin } from './asset' import { clientInjectionsPlugin } from './clientInjections' -import { buildHtmlPlugin, htmlInlineProxyPlugin } from './html' +import { + buildHtmlPlugin, + htmlImportMapPlugin, + htmlInlineProxyPlugin +} from './html' import { wasmFallbackPlugin, wasmHelperPlugin } from './wasm' import { modulePreloadPolyfillPlugin } from './modulePreloadPolyfill' import { webWorkerPlugin } from './worker' @@ -96,6 +100,7 @@ export async function resolvePlugins( // internal server-only plugins are always applied after everything else ...(isBuild ? [] - : [clientInjectionsPlugin(config), importAnalysisPlugin(config)]) + : [clientInjectionsPlugin(config), importAnalysisPlugin(config)]), + htmlImportMapPlugin() ].filter(Boolean) as Plugin[] } From 4eb159c2c72d7c63a7d24d729e7bbb36f68c36bf Mon Sep 17 00:00:00 2001 From: bluwy Date: Thu, 28 Jul 2022 14:45:13 +0800 Subject: [PATCH 05/12] feat: add warning --- packages/vite/src/node/plugins/html.ts | 50 ++++++++++++++++++++----- packages/vite/src/node/plugins/index.ts | 10 +++-- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index 83aaa6fb174c55..68572a69e523ae 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -7,6 +7,7 @@ import type { SourceMapInput } from 'rollup' import MagicString from 'magic-string' +import colors from 'picocolors' import type { AttributeNode, CompilerError, @@ -52,6 +53,10 @@ const inlineImportRE = /(?]*type\s*=\s*["']?importmap["']?[^>]*>.*?<\/script>/is +const moduleScriptRE = /[ \t]*]*type\s*=\s*["']?module["']?[^>]*>/is + export const isHTMLProxy = (id: string): boolean => htmlProxyRE.test(id) export const isHTMLRequest = (request: string): boolean => @@ -218,18 +223,46 @@ function handleParseError( ) } +/** + * Check if importmap is after any script module. Warn user if so. + * Must be added before pre plugins. + */ +export function preHtmlImportMapPlugin(config: ResolvedConfig): Plugin { + return { + name: 'vite:pre-html-importmap', + transformIndexHtml: { + enforce: 'pre', + transform(html, ctx) { + const importMapIndex = html.match(importMapRE)?.index + if (!importMapIndex) return + + const moduleScriptIndex = html.match(moduleScriptRE)?.index + if (!moduleScriptIndex) return + + if (moduleScriptIndex < importMapIndex) { + const relativeHtml = path.relative(config.root, ctx.filename) + config.logger.warnOnce( + colors.yellow( + colors.bold( + `(!)