Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

fix(html): move importmap before module scripts #9392

Merged
merged 14 commits into from
Aug 21, 2022
52 changes: 52 additions & 0 deletions packages/vite/src/node/plugins/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
SourceMapInput
} from 'rollup'
import MagicString from 'magic-string'
import colors from 'picocolors'
import type {
AttributeNode,
CompilerError,
Expand Down Expand Up @@ -54,6 +55,10 @@ const inlineImportRE =
/(?<!(?<!\.\.)\.)\bimport\s*\(("([^"]|(?<=\\)")*"|'([^']|(?<=\\)')*')\)/g
const htmlLangRE = /\.(html|htm)$/

const importMapRE =
/[ \t]*<script[^>]*type\s*=\s*["']?importmap["']?[^>]*>.*?<\/script>/is
const moduleScriptRE = /[ \t]*<script[^>]*type\s*=\s*["']?module["']?[^>]*>/is

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

export const isHTMLRequest = (request: string): boolean =>
Expand Down Expand Up @@ -225,6 +230,8 @@ function handleParseError(
*/
export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
const [preHooks, postHooks] = resolveHtmlTransforms(config.plugins)
preHooks.unshift(preImportMapHook(config))
postHooks.push(postImportMapHook())
const processedHtml = new Map<string, string>()
const isExcludedUrl = (url: string) =>
url.startsWith('#') ||
Expand Down Expand Up @@ -796,6 +803,51 @@ export type IndexHtmlTransform =
transform: IndexHtmlTransformHook
}

export function preImportMapHook(
config: ResolvedConfig
): IndexHtmlTransformHook {
return (html, ctx) => {
const importMapIndex = html.match(importMapRE)?.index
if (importMapIndex === undefined) return

const moduleScriptIndex = html.match(moduleScriptRE)?.index
if (moduleScriptIndex === undefined) return

if (moduleScriptIndex < 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"> in /${relativeHtml}`
)
)
)
}
}
}

/**
* Move importmap before the first module script
*/
export function postImportMapHook(): IndexHtmlTransformHook {
return (html) => {
if (!moduleScriptRE.test(html)) return

let importMap: string | undefined
html = html.replace(importMapRE, (match) => {
importMap = match
return ''
})
if (importMap) {
html = html.replace(moduleScriptRE, (match) => `${importMap}\n${match}`)
}

return html
}
}

export function resolveHtmlTransforms(
plugins: readonly Plugin[]
): [IndexHtmlTransformHook[], IndexHtmlTransformHook[]] {
Expand Down
24 changes: 18 additions & 6 deletions packages/vite/src/node/server/middlewares/indexHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
applyHtmlTransforms,
assetAttrsConfig,
getScriptInfo,
postImportMapHook,
preImportMapHook,
resolveHtmlTransforms,
traverseHtml
} from '../../plugins/html'
Expand Down Expand Up @@ -43,12 +45,22 @@ export function createDevHtmlTransformFn(
): (url: string, html: string, originalUrl: string) => Promise<string> {
const [preHooks, postHooks] = resolveHtmlTransforms(server.config.plugins)
return (url: string, html: string, originalUrl: string): Promise<string> => {
return applyHtmlTransforms(html, [...preHooks, devHtmlHook, ...postHooks], {
path: url,
filename: getHtmlFilename(url, server),
server,
originalUrl
})
return applyHtmlTransforms(
html,
[
preImportMapHook(server.config),
...preHooks,
devHtmlHook,
...postHooks,
postImportMapHook()
],
{
path: url,
filename: getHtmlFilename(url, server),
server,
originalUrl
}
)
}
}

Expand Down
8 changes: 7 additions & 1 deletion playground/external/__tests__/external.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { describe, expect, test } from 'vitest'
import { isBuild, page } from '~utils'
import { browserLogs, isBuild, page } from '~utils'

test('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 () => {
Expand Down
16 changes: 15 additions & 1 deletion playground/html/__tests__/html.spec.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down Expand Up @@ -242,3 +250,9 @@ describe.runIf(isServe)('invalid', () => {
expect(content).toBeTruthy()
})
})

test('importmap', () => {
expect(browserLogs).not.toContain(
'An import map is added after module script load was triggered.'
)
})
19 changes: 19 additions & 0 deletions playground/html/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
]
}
}
]
}