From 1a0eaf5741f0ad2ce3f49f6371e70c36e6faf2b5 Mon Sep 17 00:00:00 2001 From: Taisei Mima Date: Mon, 29 Apr 2024 11:49:04 +0900 Subject: [PATCH] feat: chunk importmap --- docs/config/shared-options.md | 7 ++ docs/guide/features.md | 4 + packages/plugin-legacy/src/index.ts | 85 ++++++++++++- .../vite/src/node/__tests__/build.spec.ts | 36 ++++++ packages/vite/src/node/build.ts | 11 ++ .../vite/src/node/plugins/chunkImportMap.ts | 117 ++++++++++++++++++ packages/vite/src/node/plugins/css.ts | 20 ++- packages/vite/src/node/plugins/html.ts | 28 +++-- .../src/node/plugins/importAnalysisBuild.ts | 48 ++++++- .../__tests__/chunk-importmap.spec.ts | 42 +++++++ playground/chunk-importmap/dynamic.css | 3 + playground/chunk-importmap/dynamic.js | 3 + playground/chunk-importmap/index.html | 20 +++ playground/chunk-importmap/index.js | 20 +++ playground/chunk-importmap/package.json | 12 ++ playground/chunk-importmap/static.css | 3 + playground/chunk-importmap/static.js | 3 + playground/chunk-importmap/vite.config.js | 8 ++ playground/chunk-importmap/worker.js | 5 + .../__tests__/js-sourcemap.spec.ts | 2 +- .../legacy-chunk-importmap.spec.ts | 8 ++ .../legacy/vite.config-chunk-importmap.js | 19 +++ pnpm-lock.yaml | 2 + 23 files changed, 485 insertions(+), 21 deletions(-) create mode 100644 packages/vite/src/node/plugins/chunkImportMap.ts create mode 100644 playground/chunk-importmap/__tests__/chunk-importmap.spec.ts create mode 100644 playground/chunk-importmap/dynamic.css create mode 100644 playground/chunk-importmap/dynamic.js create mode 100644 playground/chunk-importmap/index.html create mode 100644 playground/chunk-importmap/index.js create mode 100644 playground/chunk-importmap/package.json create mode 100644 playground/chunk-importmap/static.css create mode 100644 playground/chunk-importmap/static.js create mode 100644 playground/chunk-importmap/vite.config.js create mode 100644 playground/chunk-importmap/worker.js create mode 100644 playground/legacy/__tests__/chunk-importmap/legacy-chunk-importmap.spec.ts create mode 100644 playground/legacy/vite.config-chunk-importmap.js diff --git a/docs/config/shared-options.md b/docs/config/shared-options.md index 39eef1a5e5f2e9..5c991c12512bcf 100644 --- a/docs/config/shared-options.md +++ b/docs/config/shared-options.md @@ -170,6 +170,13 @@ Enabling this setting causes vite to determine file identity by the original fil A nonce value placeholder that will be used when generating script / style tags. Setting this value will also generate a meta tag with nonce value. +## html.chunkImportMap + +- **Type:** `boolean` +- **Default:** `false` + +Whether to inject importmap for generated chunks. This importmap is used to optimize caching efficiency. + ## css.modules - **Type:** diff --git a/docs/guide/features.md b/docs/guide/features.md index 75940b8d17310c..56b171077e2f51 100644 --- a/docs/guide/features.md +++ b/docs/guide/features.md @@ -715,6 +715,10 @@ By default, during build, Vite inlines small assets as data URIs. Allowing `data Do not allow `data:` for [`script-src`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src). It will allow injection of arbitrary scripts. ::: +## Chunk importmap + +Creating an importmap for chunks helps prevent the cascading cache invalidation issue. This importmap features a list of stable file id linked to filenames with content-based hashes. When one chunk references another, it utilizes the file id instead of the filename hashed by content. As a result, only the updated chunk needs cache invalidation in the browser, leaving intermediary chunks unchanged. This strategy enhances the cache hit rate following deployments. + ## Build Optimizations > Features listed below are automatically applied as part of the build process and there is no need for explicit configuration unless you want to disable them. diff --git a/packages/plugin-legacy/src/index.ts b/packages/plugin-legacy/src/index.ts index ad821f857ab593..c652c28a262426 100644 --- a/packages/plugin-legacy/src/index.ts +++ b/packages/plugin-legacy/src/index.ts @@ -14,6 +14,7 @@ import type { import type { NormalizedOutputOptions, OutputBundle, + OutputChunk, OutputOptions, PreRenderedChunk, RenderedChunk, @@ -122,6 +123,51 @@ const _require = createRequire(import.meta.url) const nonLeadingHashInFileNameRE = /[^/]+\[hash(?::\d+)?\]/ const prefixedHashInFileNameRE = /\W?\[hash(:\d+)?\]/ +const hashPlaceholderLeft = '!~{' +const hashPlaceholderRight = '}~' +const hashPlaceholderOverhead = + hashPlaceholderLeft.length + hashPlaceholderRight.length +const maxHashSize = 22 +// from https://github.com/rollup/rollup/blob/91352494fc722bcd5e8e922cd1497b34aec57a67/src/utils/hashPlaceholders.ts#L41-L46 +const hashPlaceholderRE = new RegExp( + // eslint-disable-next-line regexp/strict, regexp/prefer-w + `${hashPlaceholderLeft}[0-9a-zA-Z_$]{1,${ + maxHashSize - hashPlaceholderOverhead + }}${hashPlaceholderRight}`, + 'g', +) + +const hashPlaceholderToFacadeModuleIdHashMap: Map = new Map() + +function augmentFacadeModuleIdHash(name: string): string { + return name.replace( + hashPlaceholderRE, + (match) => hashPlaceholderToFacadeModuleIdHashMap.get(match) ?? match, + ) +} + +function createChunkImportMap( + bundle: OutputBundle, + base: string, +): Record { + return Object.fromEntries( + Object.values(bundle) + .filter((chunk): chunk is OutputChunk => chunk.type === 'chunk') + .map((output) => { + return [ + base + augmentFacadeModuleIdHash(output.preliminaryFileName), + base + output.fileName, + ] + }), + ) +} + +export function getHash(text: Buffer | string, length = 8): string { + const h = createHash('sha256').update(text).digest('hex').substring(0, length) + if (length <= 64) return h + return h.padEnd(length, '_') +} + function viteLegacyPlugin(options: Options = {}): Plugin[] { let config: ResolvedConfig let targets: Options['targets'] @@ -153,6 +199,7 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] { const isDebug = debugFlags.includes('vite:*') || debugFlags.includes('vite:legacy') + const chunkImportMap = new Map() const facadeToLegacyChunkMap = new Map() const facadeToLegacyPolyfillMap = new Map() const facadeToModernPolyfillMap = new Map() @@ -450,6 +497,16 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] { } } + if (config.build.chunkImportMap) { + const hashPlaceholder = chunk.fileName.match(hashPlaceholderRE)?.[0] + if (hashPlaceholder) { + hashPlaceholderToFacadeModuleIdHashMap.set( + hashPlaceholder, + getHash(chunk.facadeModuleId ?? chunk.fileName), + ) + } + } + if (!genLegacy) { return null } @@ -505,13 +562,26 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] { return null }, - transformIndexHtml(html, { chunk }) { + transformIndexHtml(html, { chunk, filename, bundle }) { if (config.build.ssr) return if (!chunk) return if (chunk.fileName.includes('-legacy')) { // The legacy bundle is built first, and its index.html isn't actually emitted if // modern bundle will be generated. Here we simply record its corresponding legacy chunk. facadeToLegacyChunkMap.set(chunk.facadeModuleId, chunk.fileName) + + if (config.build.chunkImportMap) { + const relativeUrlPath = path.posix.relative( + config.root, + normalizePath(filename), + ) + const assetsBase = getBaseInHTML(relativeUrlPath, config) + chunkImportMap.set( + chunk.facadeModuleId, + createChunkImportMap(bundle!, assetsBase), + ) + } + if (genModern) { return } @@ -634,6 +704,19 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] { }) } + if (config.build.chunkImportMap) { + const imports = chunkImportMap.get(chunk.facadeModuleId) + + if (imports) { + tags.push({ + tag: 'script', + attrs: { type: 'systemjs-importmap' }, + children: JSON.stringify({ imports }), + injectTo: 'head-prepend', + }) + } + } + return { html, tags, diff --git a/packages/vite/src/node/__tests__/build.spec.ts b/packages/vite/src/node/__tests__/build.spec.ts index 2dad85578812cc..638ba5e697bd62 100644 --- a/packages/vite/src/node/__tests__/build.spec.ts +++ b/packages/vite/src/node/__tests__/build.spec.ts @@ -55,6 +55,42 @@ describe('build', () => { assertOutputHashContentChange(result[0], result[1]) }) + test('file hash should not change when changes for dynamic entries while chunk map option enabled', async () => { + const buildProject = async (text: string) => { + return (await build({ + root: resolve(__dirname, 'packages/build-project'), + logLevel: 'silent', + build: { + chunkImportMap: true, + write: false, + }, + plugins: [ + { + name: 'test', + resolveId(id) { + if (id === 'entry.js' || id === 'subentry.js') { + return '\0' + id + } + }, + load(id) { + if (id === '\0entry.js') { + return `window.addEventListener('click', () => { import('subentry.js') });` + } + if (id === '\0subentry.js') { + return `console.log(${text})` + } + }, + }, + ], + })) as RollupOutput + } + + const result = await Promise.all([buildProject('foo'), buildProject('bar')]) + + expect(result[0].output[0].fileName).toBe(result[1].output[0].fileName) + expect(result[0].output[1].fileName).not.toBe(result[1].output[1].fileName) + }) + test('file hash should change when pure css chunk changes', async () => { const buildProject = async (cssColor: string) => { return (await build({ diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 4bc57ce58f76aa..e9866c2d681fec 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -46,6 +46,7 @@ import { import { manifestPlugin } from './plugins/manifest' import type { Logger } from './logger' import { dataURIPlugin } from './plugins/dataUri' +import { chunkImportMapPlugin } from './plugins/chunkImportMap' import { buildImportAnalysisPlugin } from './plugins/importAnalysisBuild' import { ssrManifestPlugin } from './ssr/ssrManifestPlugin' import { loadFallbackPlugin } from './plugins/loadFallback' @@ -247,6 +248,12 @@ export interface BuildOptions { * @default null */ watch?: WatcherOptions | null + /** + * Whether to inject importmap for generated chunks. + * This importmap is used to optimize caching efficiency. + * @default false + */ + chunkImportMap?: boolean } export interface LibraryOptions { @@ -353,6 +360,7 @@ export function resolveBuildOptions( reportCompressedSize: true, chunkSizeWarningLimit: 500, watch: null, + chunkImportMap: false, } const userBuildOptions = raw @@ -443,6 +451,9 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ Boolean, ) as Plugin[]), ...(config.isWorker ? [webWorkerPostPlugin()] : []), + ...(!config.isWorker && options.chunkImportMap + ? [chunkImportMapPlugin()] + : []), ], post: [ buildImportAnalysisPlugin(config), diff --git a/packages/vite/src/node/plugins/chunkImportMap.ts b/packages/vite/src/node/plugins/chunkImportMap.ts new file mode 100644 index 00000000000000..95731d9e66da42 --- /dev/null +++ b/packages/vite/src/node/plugins/chunkImportMap.ts @@ -0,0 +1,117 @@ +import path from 'node:path' +import type { OutputBundle, OutputChunk } from 'rollup' +import MagicString from 'magic-string' +import { getHash, normalizePath } from '../utils' +import type { Plugin } from '../plugin' +import type { ResolvedConfig } from '../config' +import type { IndexHtmlTransformHook } from './html' + +const hashPlaceholderLeft = '!~{' +const hashPlaceholderRight = '}~' +const hashPlaceholderOverhead = + hashPlaceholderLeft.length + hashPlaceholderRight.length +export const maxHashSize = 22 +// from https://github.com/rollup/rollup/blob/91352494fc722bcd5e8e922cd1497b34aec57a67/src/utils/hashPlaceholders.ts#L41-L46 +const hashPlaceholderRE = new RegExp( + // eslint-disable-next-line regexp/strict, regexp/prefer-w + `${hashPlaceholderLeft}[0-9a-zA-Z_$]{1,${ + maxHashSize - hashPlaceholderOverhead + }}${hashPlaceholderRight}`, + 'g', +) + +const hashPlaceholderToFacadeModuleIdHashMap = new Map() + +function augmentFacadeModuleIdHash(name: string): string { + return name.replace( + hashPlaceholderRE, + (match) => hashPlaceholderToFacadeModuleIdHashMap.get(match) ?? match, + ) +} + +export function createChunkImportMap( + bundle: OutputBundle, + base: string = '', +): Record { + return Object.fromEntries( + Object.values(bundle) + .filter((chunk): chunk is OutputChunk => chunk.type === 'chunk') + .map((output) => { + return [ + base + augmentFacadeModuleIdHash(output.preliminaryFileName), + base + output.fileName, + ] + }), + ) +} + +export function chunkImportMapPlugin(): Plugin { + return { + name: 'vite:chunk-importmap', + + // If the hash part is simply removed, there is a risk of key collisions within the importmap. + // For example, both `foo/index-[hash].js` and `index-[hash].js` would become `assets/index-.js`. + // Therefore, a hash is generated from the facadeModuleId to avoid this issue. + renderChunk(code, _chunk, _options, meta) { + Object.values(meta.chunks).forEach((chunk) => { + const hashPlaceholder = chunk.fileName.match(hashPlaceholderRE)?.[0] + if (!hashPlaceholder) return + if (hashPlaceholderToFacadeModuleIdHashMap.get(hashPlaceholder)) return + + hashPlaceholderToFacadeModuleIdHashMap.set( + hashPlaceholder, + getHash(chunk.facadeModuleId ?? chunk.fileName), + ) + }) + + const codeProcessed = augmentFacadeModuleIdHash(code) + return { + code: codeProcessed, + map: new MagicString(codeProcessed).generateMap({ + hires: 'boundary', + }), + } + }, + } +} + +export function postChunkImportMapHook( + config: ResolvedConfig, +): IndexHtmlTransformHook { + return (html, ctx) => { + if (!config.build.chunkImportMap) return + + const { filename, bundle } = ctx + + const relativeUrlPath = path.posix.relative( + config.root, + normalizePath(filename), + ) + const assetsBase = getBaseInHTML(relativeUrlPath, config) + + return { + html, + tags: [ + { + tag: 'script', + attrs: { type: 'importmap' }, + children: JSON.stringify({ + imports: createChunkImportMap(bundle!, assetsBase), + }), + injectTo: 'head-prepend', + }, + ], + } + } +} + +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 +} diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 0e97c247cf01f8..92ae851961f4e5 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -81,6 +81,7 @@ import { } from './asset' import type { ESBuildOptions } from './esbuild' import { getChunkOriginalFileName } from './manifest' +import { createChunkImportMap } from './chunkImportMap' // const debug = createDebugger('vite:css') @@ -837,6 +838,13 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { return } + const chunkImportMap = config.build.chunkImportMap + ? createChunkImportMap(bundle) + : {} + const valueKeyChunkImportMap = Object.fromEntries( + Object.entries(chunkImportMap).map(([k, v]) => [v, k]), + ) + // remove empty css chunks and their imports if (pureCssChunks.size) { // map each pure css chunk (rendered chunk) to it's corresponding bundle @@ -848,9 +856,10 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { .map((chunk) => [chunk.preliminaryFileName, chunk.fileName]), ) - const pureCssChunkNames = [...pureCssChunks].map( - (pureCssChunk) => prelimaryNameToChunkMap[pureCssChunk.fileName], - ) + const pureCssChunkNames = [...pureCssChunks].flatMap((pureCssChunk) => { + const chunkName = prelimaryNameToChunkMap[pureCssChunk.fileName] + return [chunkName, valueKeyChunkImportMap[chunkName]].filter(Boolean) + }) const replaceEmptyChunk = getEmptyChunkReplacer( pureCssChunkNames, @@ -888,7 +897,10 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { const removedPureCssFiles = removedPureCssFilesCache.get(config)! pureCssChunkNames.forEach((fileName) => { - removedPureCssFiles.set(fileName, bundle[fileName] as RenderedChunk) + const chunk = bundle[fileName] as RenderedChunk + if (!chunk) return + removedPureCssFiles.set(fileName, chunk) + removedPureCssFiles.set(valueKeyChunkImportMap[fileName], chunk) delete bundle[fileName] delete bundle[`${fileName}.map`] }) diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index 7d971375040b5c..8da02b85f88417 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -37,6 +37,7 @@ import { urlToBuiltUrl, } from './asset' import { isCSSRequest } from './css' +import { postChunkImportMapHook } from './chunkImportMap' import { modulePreloadPolyfillId } from './modulePreloadPolyfill' interface ScriptAssetsUrl { @@ -57,7 +58,7 @@ const htmlLangRE = /\.(?:html|htm)$/ const spaceRe = /[\t\n\f\r ]/ const importMapRE = - /[ \t]*]*type\s*=\s*(?:"importmap"|'importmap'|importmap)[^>]*>.*?<\/script>/is + /[ \t]*]*type\s*=\s*(?:"importmap"|'importmap'|importmap)[^>]*>(.*?)<\/script>/gis const moduleScriptRE = /[ \t]*]*type\s*=\s*(?:"module"|'module'|module)[^>]*>/i const modulePreloadLinkRE = @@ -313,8 +314,10 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { preHooks.unshift(injectCspNonceMetaTagHook(config)) preHooks.unshift(preImportMapHook(config)) preHooks.push(htmlEnvHook(config)) - postHooks.push(injectNonceAttributeTagHook(config)) + postHooks.push(postChunkImportMapHook(config)) postHooks.push(postImportMapHook()) + postHooks.push(injectNonceAttributeTagHook(config)) + const processedHtml = new Map() const isExcludedUrl = (url: string) => @@ -1075,22 +1078,27 @@ export function preImportMapHook( /** * Move importmap before the first module script and modulepreload link + * Merge all importmaps into one */ export function postImportMapHook(): IndexHtmlTransformHook { return (html) => { if (!importMapAppendRE.test(html)) return - let importMap: string | undefined - html = html.replace(importMapRE, (match) => { - importMap = match + let imports = {} + + html = html.replaceAll(importMapRE, (_, p1) => { + imports = { ...imports, ...JSON.parse(p1).imports } return '' }) - if (importMap) { - html = html.replace( - importMapAppendRE, - (match) => `${importMap}\n${match}`, - ) + if (Object.keys(imports).length > 0) { + html = html.replace(importMapAppendRE, (match) => { + return `${serializeTag({ + tag: 'script', + attrs: { type: 'importmap' }, + children: JSON.stringify({ imports }), + })}\n${match}` + }) } return html diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index 2c3fdcee0e7e23..934140f5b2e570 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -20,6 +20,7 @@ import { toOutputFilePathInJS } from '../build' import { genSourceMapUrl } from '../server/sourcemap' import { removedPureCssFilesCache } from './css' import { createParseErrorInfo } from './importAnalysis' +import { createChunkImportMap } from './chunkImportMap' type FileDep = { url: string @@ -71,6 +72,7 @@ function detectScriptRel() { declare const scriptRel: string declare const seen: Record +declare const chunkImportMapFilePairs: [string, string][] function preload( baseModule: () => Promise<{}>, deps?: string[], @@ -94,6 +96,9 @@ function preload( dep = assetsURL(dep, importerUrl) if (dep in seen) return seen[dep] = true + chunkImportMapFilePairs.forEach(([k, v]) => { + dep = dep.replace(k, v) + }) const isCss = dep.endsWith('.css') const cssSelector = isCss ? '[rel="stylesheet"]' : '' const isBaseRelative = !!importerUrl @@ -196,7 +201,23 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { : // If the base isn't relative, then the deps are relative to the projects `outDir` and the base // is appended inside __vitePreload too. `function(dep) { return ${JSON.stringify(config.base)}+dep }` - const preloadCode = `const scriptRel = ${scriptRel};const assetsURL = ${assetsURL};const seen = {};export const ${preloadMethod} = ${preload.toString()}` + const chunkImportMapFilePairs = () => { + const importMapString = document.head.querySelector( + 'script[type="importmap"]', + )?.textContent + const importMap: Record = importMapString + ? JSON.parse(importMapString).imports + : {} + return Object.entries(importMap) + .map(([k, v]) => { + const key = k.match(/[^/]+\.js$/) + const value = v.match(/[^/]+\.js$/) + if (!key || !value) return null + return [key[0], value[0]] + }) + .filter(Boolean) as [string, string][] + } + const preloadCode = `const scriptRel = ${scriptRel};const assetsURL = ${assetsURL};const seen = {};const chunkImportMapFilePairs = (${chunkImportMapFilePairs.toString()})();export const ${preloadMethod} = ${preload.toString()}` return { name: 'vite:build-import-analysis', @@ -314,6 +335,18 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { return } + const chunkImportMap = config.build.chunkImportMap + ? createChunkImportMap(bundle) + : {} + const valueKeyChunkImportMapFilePairs = Object.entries(chunkImportMap) + .map(([k, v]) => { + const key = k.match(/[^/]+\.js$/) + const value = v.match(/[^/]+\.js$/) + if (!key || !value) return null + return [value[0], key[0]] + }) + .filter(Boolean) as [string, string][] + for (const file in bundle) { const chunk = bundle[file] // can't use chunk.dynamicImports.length here since some modules e.g. @@ -387,7 +420,8 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { if (filename === ownerFilename) return if (analyzed.has(filename)) return analyzed.add(filename) - const chunk = bundle[filename] + const chunk = + bundle[filename] ?? bundle[chunkImportMap[filename]] if (chunk) { deps.add(chunk.fileName) if (chunk.type === 'chunk') { @@ -509,9 +543,13 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { if (fileDeps.length > 0) { const fileDepsCode = `[${fileDeps - .map((fileDep) => - fileDep.runtime ? fileDep.url : JSON.stringify(fileDep.url), - ) + .map((fileDep) => { + let url = fileDep.url + valueKeyChunkImportMapFilePairs.forEach(([v, k]) => { + url = url.replace(v, k) + }) + return fileDep.runtime ? url : JSON.stringify(url) + }) .join(',')}]` const mapDepsCode = `const __vite__fileDeps=${fileDepsCode},__vite__mapDeps=i=>i.map(i=>__vite__fileDeps[i]);\n` diff --git a/playground/chunk-importmap/__tests__/chunk-importmap.spec.ts b/playground/chunk-importmap/__tests__/chunk-importmap.spec.ts new file mode 100644 index 00000000000000..64b2466e1a0816 --- /dev/null +++ b/playground/chunk-importmap/__tests__/chunk-importmap.spec.ts @@ -0,0 +1,42 @@ +import { expect, test } from 'vitest' +import { browserLogs, expectWithRetry, getColor, page } from '~utils' + +test('should have no 404s', () => { + browserLogs.forEach((msg) => { + expect(msg).not.toMatch('404') + }) +}) + +test('index js', async () => { + await expectWithRetry(() => page.textContent('.js')).toBe('js: ok') +}) + +test('importmap', async () => { + await expectWithRetry(() => page.textContent('.importmap')).toContain( + '"/foo": "/bar"', + ) +}) + +test('static js', async () => { + await expectWithRetry(() => page.textContent('.static-js')).toBe( + 'static-js: ok', + ) +}) + +test('dynamic js', async () => { + await expectWithRetry(() => page.textContent('.dynamic-js')).toBe( + 'dynamic-js: ok', + ) +}) + +test('static css', async () => { + expect(await getColor('.static')).toBe('red') +}) + +test('dynamic css', async () => { + expect(await getColor('.dynamic')).toBe('red') +}) + +test('worker', async () => { + await expectWithRetry(() => page.textContent('.worker')).toBe('worker: pong') +}) diff --git a/playground/chunk-importmap/dynamic.css b/playground/chunk-importmap/dynamic.css new file mode 100644 index 00000000000000..ca5140e1c23d94 --- /dev/null +++ b/playground/chunk-importmap/dynamic.css @@ -0,0 +1,3 @@ +.dynamic { + color: red; +} diff --git a/playground/chunk-importmap/dynamic.js b/playground/chunk-importmap/dynamic.js new file mode 100644 index 00000000000000..3d3e3a413e5677 --- /dev/null +++ b/playground/chunk-importmap/dynamic.js @@ -0,0 +1,3 @@ +import './dynamic.css' + +document.querySelector('.dynamic-js').textContent = 'dynamic-js: ok' diff --git a/playground/chunk-importmap/index.html b/playground/chunk-importmap/index.html new file mode 100644 index 00000000000000..271338824679bb --- /dev/null +++ b/playground/chunk-importmap/index.html @@ -0,0 +1,20 @@ + + + + + + + +

js: error

+

+
+  

static

+

dynamic

+ +

static-js: error

+

dynamic-js: error

+ +

worker: ping

+ diff --git a/playground/chunk-importmap/index.js b/playground/chunk-importmap/index.js new file mode 100644 index 00000000000000..2adf2e2e44e049 --- /dev/null +++ b/playground/chunk-importmap/index.js @@ -0,0 +1,20 @@ +import './static.js' +import('./dynamic.js') + +import myWorker from './worker.js?worker' + +document.querySelector('.js').textContent = 'js: ok' + +document.querySelector('.importmap').textContent = JSON.stringify( + JSON.parse( + document.head.querySelector('script[type="importmap"]')?.textContent, + ), + null, + 2, +) + +const worker = new myWorker() +worker.postMessage('ping') +worker.addEventListener('message', (e) => { + document.querySelector('.worker').textContent = e.data.message +}) diff --git a/playground/chunk-importmap/package.json b/playground/chunk-importmap/package.json new file mode 100644 index 00000000000000..b9fe2c5eb95cbf --- /dev/null +++ b/playground/chunk-importmap/package.json @@ -0,0 +1,12 @@ +{ + "name": "@vitejs/test-chunk-importmap", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "debug": "node --inspect-brk ../../packages/vite/bin/vite", + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + } +} diff --git a/playground/chunk-importmap/static.css b/playground/chunk-importmap/static.css new file mode 100644 index 00000000000000..b985ef305f00db --- /dev/null +++ b/playground/chunk-importmap/static.css @@ -0,0 +1,3 @@ +.static { + color: red; +} diff --git a/playground/chunk-importmap/static.js b/playground/chunk-importmap/static.js new file mode 100644 index 00000000000000..563281d6ed7cb9 --- /dev/null +++ b/playground/chunk-importmap/static.js @@ -0,0 +1,3 @@ +import './static.css' + +document.querySelector('.static-js').textContent = 'static-js: ok' diff --git a/playground/chunk-importmap/vite.config.js b/playground/chunk-importmap/vite.config.js new file mode 100644 index 00000000000000..a67c7e102fc3d8 --- /dev/null +++ b/playground/chunk-importmap/vite.config.js @@ -0,0 +1,8 @@ +import { defineConfig } from 'vite' + +export default defineConfig({ + build: { + chunkImportMap: true, + sourcemap: true, + }, +}) diff --git a/playground/chunk-importmap/worker.js b/playground/chunk-importmap/worker.js new file mode 100644 index 00000000000000..21ec980e4f3187 --- /dev/null +++ b/playground/chunk-importmap/worker.js @@ -0,0 +1,5 @@ +self.onmessage = (e) => { + if (e.data === 'ping') { + self.postMessage({ message: 'worker: pong' }) + } +} diff --git a/playground/js-sourcemap/__tests__/js-sourcemap.spec.ts b/playground/js-sourcemap/__tests__/js-sourcemap.spec.ts index 15d82acd776283..bbf994019e273e 100644 --- a/playground/js-sourcemap/__tests__/js-sourcemap.spec.ts +++ b/playground/js-sourcemap/__tests__/js-sourcemap.spec.ts @@ -140,7 +140,7 @@ describe.runIf(isBuild)('build tests', () => { expect(formatSourcemapForSnapshot(JSON.parse(map))).toMatchInlineSnapshot(` { "ignoreList": [], - "mappings": ";w+BAAA,OAAO,2BAAuB,EAAC,wBAE/B,QAAQ,IAAI,uBAAuB", + "mappings": ";kxCAAA,OAAO,2BAAuB,EAAC,wBAE/B,QAAQ,IAAI,uBAAuB", "sources": [ "../../after-preload-dynamic.js", ], diff --git a/playground/legacy/__tests__/chunk-importmap/legacy-chunk-importmap.spec.ts b/playground/legacy/__tests__/chunk-importmap/legacy-chunk-importmap.spec.ts new file mode 100644 index 00000000000000..1228b392a1f3fa --- /dev/null +++ b/playground/legacy/__tests__/chunk-importmap/legacy-chunk-importmap.spec.ts @@ -0,0 +1,8 @@ +import { expect, test } from 'vitest' +import { browserLogs } from '~utils' + +test('should have no 404s', () => { + browserLogs.forEach((msg) => { + expect(msg).not.toMatch('404') + }) +}) diff --git a/playground/legacy/vite.config-chunk-importmap.js b/playground/legacy/vite.config-chunk-importmap.js new file mode 100644 index 00000000000000..ba16dfc56a5e8c --- /dev/null +++ b/playground/legacy/vite.config-chunk-importmap.js @@ -0,0 +1,19 @@ +import fs from 'node:fs' +import path from 'node:path' +import legacy from '@vitejs/plugin-legacy' +import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [legacy()], + build: { chunkImportMap: true }, + // for tests, remove `