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 @@
+
+
+