diff --git a/docs/config/build-options.md b/docs/config/build-options.md index 2b82413fc0e026..5dc83e146b80c9 100644 --- a/docs/config/build-options.md +++ b/docs/config/build-options.md @@ -279,3 +279,10 @@ There are cases that file system watching does not work with WSL2. See [`server.watch`](./server-options.md#server-watch) for more details. ::: + +## build.chunkImportMap + +- **Type:** `boolean` +- **Default:** `false` + +Whether to inject importmap for generated chunks. This importmap is used to optimize caching efficiency. diff --git a/docs/guide/features.md b/docs/guide/features.md index dfd8c57f715ded..95a875365f5d6d 100644 --- a/docs/guide/features.md +++ b/docs/guide/features.md @@ -715,7 +715,7 @@ Do not allow `data:` for [`script-src`](https://developer.mozilla.org/en-US/docs ## 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. +> Features listed below (except for the chunk importmap feature) are automatically applied as part of the build process and there is no need for explicit configuration unless you want to disable them. ### CSS Code Splitting @@ -749,3 +749,7 @@ Entry ---> (A + C) ``` It is possible for `C` to have further imports, which will result in even more roundtrips in the un-optimized scenario. Vite's optimization will trace all the direct imports to completely eliminate the roundtrips regardless of import depth. + +### Chunk importmap + +Creating an import map for chunks helps prevent the issue of cascading cache invalidation. This import map features a list of stable file IDs linked to filenames with content-based hashes. When one chunk references another, it utilizes the file ID instead of the content-hashed filename. 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. diff --git a/packages/plugin-legacy/src/index.ts b/packages/plugin-legacy/src/index.ts index d356793b231c72..24735b3c5420f7 100644 --- a/packages/plugin-legacy/src/index.ts +++ b/packages/plugin-legacy/src/index.ts @@ -136,6 +136,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 = 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 = hash('sha256', text, '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'] @@ -167,6 +212,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() @@ -519,6 +565,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 } @@ -574,13 +630,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 } @@ -703,6 +772,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 62944fee6060aa..0daab67545bab0 100644 --- a/packages/vite/src/node/__tests__/build.spec.ts +++ b/packages/vite/src/node/__tests__/build.spec.ts @@ -60,6 +60,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 ccf6dab5ee5551..2cdb8c9c0cf994 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -53,6 +53,7 @@ import { resolveEnvironmentPlugins } from './plugin' 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 { buildLoadFallbackPlugin } from './plugins/loadFallback' @@ -265,6 +266,12 @@ export interface BuildEnvironmentOptions { * @default null */ watch?: WatcherOptions | null + /** + * Whether to inject importmap for generated chunks. + * This importmap is used to optimize caching efficiency. + * @default false + */ + chunkImportMap?: boolean /** * create the Build Environment instance */ @@ -385,6 +392,7 @@ export function resolveBuildEnvironmentOptions( reportCompressedSize: true, chunkSizeWarningLimit: 500, watch: null, + chunkImportMap: false, createEnvironment: (name, config) => new BuildEnvironment(name, config), } @@ -490,6 +498,9 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ await asyncFlatten(arraify(config.build.rollupOptions.plugins)) ).filter(Boolean) as Plugin[]), ...(config.isWorker ? [webWorkerPostPlugin()] : []), + ...(!config.isWorker && config.build.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..6299f7b55c04aa --- /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 s = new MagicString(code) + // note that MagicString::replace is used instead of String::replace + s.replace( + hashPlaceholderRE, + (match) => hashPlaceholderToFacadeModuleIdHashMap.get(match) ?? match, + ) + return { code: s.toString(), map: s.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 1b430588b6f6d3..d013589e255682 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -87,6 +87,7 @@ import { } from './asset' import type { ESBuildOptions } from './esbuild' import { getChunkOriginalFileName } from './manifest' +import { createChunkImportMap } from './chunkImportMap' const decoder = new TextDecoder() // const debug = createDebugger('vite:css') @@ -897,6 +898,13 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { }) } + 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 @@ -913,7 +921,12 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { // but they are still in `pureCssChunks`. // So we need to filter the names and only use those who are defined const pureCssChunkNames = [...pureCssChunks] - .map((pureCssChunk) => prelimaryNameToChunkMap[pureCssChunk.fileName]) + .map((pureCssChunk) => { + const chunkName = prelimaryNameToChunkMap[pureCssChunk.fileName] + return config.build.chunkImportMap + ? valueKeyChunkImportMap[chunkName] + : chunkName + }) .filter(Boolean) const replaceEmptyChunk = getEmptyChunkReplacer( @@ -952,7 +965,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 6616c0f35ead41..56fe3825007ac6 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -40,6 +40,7 @@ import { urlToBuiltUrl, } from './asset' import { isCSSRequest } from './css' +import { postChunkImportMapHook } from './chunkImportMap' import { modulePreloadPolyfillId } from './modulePreloadPolyfill' interface ScriptAssetsUrl { @@ -60,7 +61,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 = @@ -319,8 +320,9 @@ 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 = usePerEnvironmentState(() => new Map()) const isExcludedUrl = (url: string) => @@ -1083,22 +1085,27 @@ export function preImportMapHook( /** * Move importmap before the first module script and modulepreload link + * Merge all importmaps */ 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 d883e0c4cd222e..56660332246235 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -21,6 +21,7 @@ import { genSourceMapUrl } from '../server/sourcemap' import type { Environment } from '../environment' import { removedPureCssFilesCache } from './css' import { createParseErrorInfo } from './importAnalysis' +import { createChunkImportMap } from './chunkImportMap' type FileDep = { url: string @@ -99,6 +100,7 @@ function preload( deps.map((dep) => { // @ts-expect-error assetsURL is declared before preload.toString() dep = assetsURL(dep, importerUrl) + dep = import.meta.resolve(dep) if (dep in seen) return seen[dep] = true const isCss = dep.endsWith('.css') @@ -118,7 +120,9 @@ function preload( } } } else if ( - document.querySelector(`link[href="${dep}"]${cssSelector}`) + document.querySelector( + `link[href="${new URL(dep).pathname}"]${cssSelector}`, + ) ) { return } @@ -211,6 +215,7 @@ 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()}` return { code: preloadCode, moduleSideEffects: false } } @@ -464,6 +469,18 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { const buildSourcemap = this.environment.config.build.sourcemap const { modulePreload } = this.environment.config.build + const chunkImportMap = config.build.chunkImportMap + ? createChunkImportMap(bundle) + : {} + const valueKeyChunkImportMapFilePairs = Object.entries(chunkImportMap) + .map(([k, v]) => { + const key = /[^/]+\.js$/.exec(k) + const value = /[^/]+\.js$/.exec(v) + 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. @@ -537,7 +554,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') { @@ -610,6 +628,15 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { ] } + if (config.build.chunkImportMap) { + depsArray = depsArray.map((dep) => { + valueKeyChunkImportMapFilePairs.forEach(([v, k]) => { + dep = dep.replace(v, k) + }) + return dep + }) + } + let renderedDeps: number[] if (renderBuiltUrl) { renderedDeps = depsArray.map((dep) => { 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 d475fec710420b..3b97340efab83a 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": ";+8BAAA,OAAO,2BAAuB,0BAE9B,QAAQ,IAAI,uBAAuB", + "mappings": ";0/BAAA,OAAO,2BAAuB,0BAE9B,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 `