diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index 1948c169b83774..707345a55bfb21 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -600,7 +600,9 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { js = `import "${modulePreloadPolyfillId}";\n${js}` } - return js + // Force rollup to keep this module from being shared between other entry points. + // If the resulting chunk is empty, it will be removed in generateBundle. + return { code: js, moduleSideEffects: 'no-treeshake' } } }, diff --git a/packages/vite/src/node/plugins/importMetaGlob.ts b/packages/vite/src/node/plugins/importMetaGlob.ts index 7ff08e544a78c6..7d765f4dfee16d 100644 --- a/packages/vite/src/node/plugins/importMetaGlob.ts +++ b/packages/vite/src/node/plugins/importMetaGlob.ts @@ -51,7 +51,14 @@ export function getAffectedGlobModules( ): ModuleNode[] { const modules: ModuleNode[] = [] for (const [id, allGlobs] of server._importGlobMap!) { - if (allGlobs.some((glob) => isMatch(file, glob))) + // (glob1 || glob2) && !glob3 && !glob4... + if ( + allGlobs.some( + ({ affirmed, negated }) => + (!affirmed.length || affirmed.some((glob) => isMatch(file, glob))) && + (!negated.length || negated.every((glob) => isMatch(file, glob))), + ) + ) modules.push(...(server.moduleGraph.getModulesByFile(id) || [])) } modules.forEach((i) => { @@ -83,7 +90,18 @@ export function importGlobPlugin(config: ResolvedConfig): Plugin { if (result) { if (server) { const allGlobs = result.matches.map((i) => i.globsResolved) - server._importGlobMap.set(id, allGlobs) + server._importGlobMap.set( + id, + allGlobs.map((globs) => { + const affirmed: string[] = [] + const negated: string[] = [] + + for (const glob of globs) { + ;(glob[0] === '!' ? negated : affirmed).push(glob) + } + return { affirmed, negated } + }), + ) } return transformStableResult(result.s, id, config) } diff --git a/packages/vite/src/node/plugins/reporter.ts b/packages/vite/src/node/plugins/reporter.ts index d61a5649d55c40..cbcb0409d1e3f0 100644 --- a/packages/vite/src/node/plugins/reporter.ts +++ b/packages/vite/src/node/plugins/reporter.ts @@ -139,15 +139,11 @@ export function buildReporterPlugin(config: ResolvedConfig): Plugin { this.warn( `\n(!) ${ module.id - } is dynamically imported by ${module.dynamicImporters - .map((m) => m) - .join( - ', ', - )} but also statically imported by ${module.importers - .map((m) => m) - .join( - ', ', - )}, dynamic import will not move module into another chunk.\n`, + } is dynamically imported by ${module.dynamicImporters.join( + ', ', + )} but also statically imported by ${module.importers.join( + ', ', + )}, dynamic import will not move module into another chunk.\n`, ) } } diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index ccec3b77400a65..67b455735b7be8 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -285,7 +285,7 @@ export interface ViteDevServer { /** * @internal */ - _importGlobMap: Map + _importGlobMap: Map /** * Deps that are externalized * @internal diff --git a/playground/css-codesplit/__tests__/css-codesplit.spec.ts b/playground/css-codesplit/__tests__/css-codesplit.spec.ts index 99d1f2d7040506..2f7d5ab5fc5fba 100644 --- a/playground/css-codesplit/__tests__/css-codesplit.spec.ts +++ b/playground/css-codesplit/__tests__/css-codesplit.spec.ts @@ -43,6 +43,14 @@ describe.runIf(isBuild)('build', () => { expect(findAssetFile(/async.*\.js$/)).toBe('') }) + test('should remove empty chunk, HTML without JS', async () => { + const sharedCSSWithJSChunk = findAssetFile('shared-css-with-js.*.js$') + expect(sharedCSSWithJSChunk).toMatch(`/* empty css`) + // there are functions and modules in the src code that should be tree-shaken + expect(sharedCSSWithJSChunk).not.toMatch('function') + expect(sharedCSSWithJSChunk).not.toMatch(/import(?!".\/modulepreload)/) + }) + test('should generate correct manifest', async () => { const manifest = readManifest() expect(manifest['index.html'].css.length).toBe(2) diff --git a/playground/css-codesplit/shared-css-empty-1.js b/playground/css-codesplit/shared-css-empty-1.js new file mode 100644 index 00000000000000..80636d362c52d5 --- /dev/null +++ b/playground/css-codesplit/shared-css-empty-1.js @@ -0,0 +1,4 @@ +function shouldBeTreeshaken_1() { + // This function should be treeshaken, even if { moduleSideEffects: 'no-treeshake' } + // was used in the JS corresponding to the HTML entrypoint. +} diff --git a/playground/css-codesplit/shared-css-empty-2.js b/playground/css-codesplit/shared-css-empty-2.js new file mode 100644 index 00000000000000..7ce6d30628268d --- /dev/null +++ b/playground/css-codesplit/shared-css-empty-2.js @@ -0,0 +1,4 @@ +export default function shouldBeTreeshaken_2() { + // This function should be treeshaken, even if { moduleSideEffects: 'no-treeshake' } + // was used in the JS corresponding to the HTML entrypoint. +} diff --git a/playground/css-codesplit/shared-css-main.js b/playground/css-codesplit/shared-css-main.js new file mode 100644 index 00000000000000..639861b66321f9 --- /dev/null +++ b/playground/css-codesplit/shared-css-main.js @@ -0,0 +1,10 @@ +import shouldTreeshake from './shared-css-empty-2.js' +document.querySelector('#app').innerHTML = ` +
+

Shared CSS, with JS

+
+` +function shouldBeTreeshaken_0() { + // This function should be treeshaken, even if { moduleSideEffects: 'no-treeshake' } + // was used in the JS corresponding to the HTML entrypoint. +} diff --git a/playground/css-codesplit/shared-css-no-js.html b/playground/css-codesplit/shared-css-no-js.html new file mode 100644 index 00000000000000..27c666af881f15 --- /dev/null +++ b/playground/css-codesplit/shared-css-no-js.html @@ -0,0 +1,4 @@ + + +

Share CSS, no JS

+ diff --git a/playground/css-codesplit/shared-css-theme.css b/playground/css-codesplit/shared-css-theme.css new file mode 100644 index 00000000000000..adc68fa6a4dfa0 --- /dev/null +++ b/playground/css-codesplit/shared-css-theme.css @@ -0,0 +1,3 @@ +h1 { + color: red; +} diff --git a/playground/css-codesplit/shared-css-with-js.html b/playground/css-codesplit/shared-css-with-js.html new file mode 100644 index 00000000000000..aaa856f2c6c5ef --- /dev/null +++ b/playground/css-codesplit/shared-css-with-js.html @@ -0,0 +1,6 @@ + + + + +

Replaced by shared-css-main.js

+ diff --git a/playground/css-codesplit/vite.config.js b/playground/css-codesplit/vite.config.js index 3e0d3ac3540be5..5042b6d9b9cab7 100644 --- a/playground/css-codesplit/vite.config.js +++ b/playground/css-codesplit/vite.config.js @@ -9,6 +9,8 @@ export default defineConfig({ main: resolve(__dirname, './index.html'), other: resolve(__dirname, './other.js'), style2: resolve(__dirname, './style2.js'), + 'shared-css-with-js': resolve(__dirname, 'shared-css-with-js.html'), + 'shared-css-no-js': resolve(__dirname, 'shared-css-no-js.html'), }, output: { manualChunks(id) { diff --git a/playground/glob-import/__tests__/glob-import.spec.ts b/playground/glob-import/__tests__/glob-import.spec.ts index cf1b6c199e73aa..931edfd99138de 100644 --- a/playground/glob-import/__tests__/glob-import.spec.ts +++ b/playground/glob-import/__tests__/glob-import.spec.ts @@ -178,6 +178,18 @@ if (!isBuild) { expect(JSON.parse(actualRemove)).toStrictEqual(allResult) }) }) + + test('no hmr for adding/removing files', async () => { + let request = page.waitForResponse(/dir\/index\.js$/, { timeout: 200 }) + addFile('nohmr.js', '') + let response = await request.catch(() => ({ status: () => -1 })) + expect(response.status()).toBe(-1) + + request = page.waitForResponse(/dir\/index\.js$/, { timeout: 200 }) + removeFile('nohmr.js') + response = await request.catch(() => ({ status: () => -1 })) + expect(response.status()).toBe(-1) + }) } test('tree-shake eager css', async () => { diff --git a/playground/glob-import/dir/index.js b/playground/glob-import/dir/index.js index 63381e412ecc6f..94ca66f1017093 100644 --- a/playground/glob-import/dir/index.js +++ b/playground/glob-import/dir/index.js @@ -1,6 +1,10 @@ const modules = import.meta.glob('./*.(js|ts)', { eager: true }) const globWithAlias = import.meta.glob('@dir/al*.js', { eager: true }) +// test negative glob +import.meta.glob(['@dir/*.js', '!@dir/x.js']) +import.meta.glob(['!@dir/x.js', '@dir/*.js']) + // test for sourcemap console.log('hello')