Skip to content

Commit ed7bf3b

Browse files
committed
refactor(css): make removePureCssChunks a function so it can be unit tested
Signed-off-by: Ferdinand Thiessen <[email protected]>
1 parent a30fdd9 commit ed7bf3b

File tree

3 files changed

+272
-38
lines changed

3 files changed

+272
-38
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`removePureCssChunks > import of removed chunk is dropped 1`] = `
4+
"import \\"some-module\\";
5+
/* empty css */import \\"other-module\\";
6+
"
7+
`;
8+
9+
exports[`removePureCssChunks > imported assets of css chunk are transfered 1`] = `
10+
"import \\"some-module\\";
11+
/* empty css */import \\"other-module\\";
12+
"
13+
`;
14+
15+
exports[`removePureCssChunks > require of removed chunk is dropped (minified, no new line) 1`] = `"require(\\"some-module\\");/* empty css */require(\\"other-module\\");"`;
16+
17+
exports[`removePureCssChunks > require of removed chunk is dropped 1`] = `
18+
"require(\\"some-module\\");
19+
/* empty css */require(\\"other-module\\");
20+
"
21+
`;

packages/vite/src/node/__tests__/plugins/css.spec.ts

+192
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import fs from 'node:fs'
22
import path from 'node:path'
33
import { describe, expect, test, vi } from 'vitest'
4+
import type { OutputBundle, OutputChunk } from 'rollup'
45
import { resolveConfig } from '../../config'
56
import type { InlineConfig } from '../../config'
67
import {
78
convertTargets,
89
cssPlugin,
910
cssUrlRE,
1011
hoistAtRules,
12+
removePureCssChunks,
1113
} from '../../plugins/css'
1214

1315
describe('search css url function', () => {
@@ -258,3 +260,193 @@ describe('convertTargets', () => {
258260
})
259261
})
260262
})
263+
264+
describe('removePureCssChunks', () => {
265+
test('import of removed chunk is dropped', () => {
266+
const bundle: OutputBundle = {
267+
'main.js': {
268+
code: 'import "some-module";\nimport "pure_css_chunk.js";\nimport "other-module";\n',
269+
imports: ['pure_css_chunk.js', 'some-module', 'other-module'],
270+
type: 'chunk',
271+
viteMetadata: {
272+
importedAssets: new Set<string>(),
273+
importedCss: new Set<string>(),
274+
},
275+
} as any as OutputChunk,
276+
'pure_css_chunk.js': {
277+
type: 'chunk',
278+
code: '',
279+
imports: [],
280+
viteMetadata: {
281+
importedAssets: new Set<string>(),
282+
importedCss: new Set<string>(),
283+
},
284+
} as any as OutputChunk,
285+
}
286+
287+
removePureCssChunks(bundle, ['pure_css_chunk.js'], 'es')
288+
289+
const chunk = bundle['main.js'] as OutputChunk
290+
expect(chunk.code).toMatchSnapshot()
291+
// import is removed
292+
expect(chunk.imports).toEqual(['some-module', 'other-module'])
293+
})
294+
295+
test('require of removed chunk is dropped', () => {
296+
const bundle: OutputBundle = {
297+
'main.js': {
298+
code: 'require("some-module");\nrequire("pure_css_chunk.js");\nrequire("other-module");\n',
299+
imports: ['pure_css_chunk.js', 'some-module', 'other-module'],
300+
type: 'chunk',
301+
viteMetadata: {
302+
importedAssets: new Set<string>(),
303+
importedCss: new Set<string>(),
304+
},
305+
} as any as OutputChunk,
306+
'pure_css_chunk.js': {
307+
type: 'chunk',
308+
code: '',
309+
imports: [],
310+
viteMetadata: {
311+
importedAssets: new Set<string>(),
312+
importedCss: new Set<string>(),
313+
},
314+
} as any as OutputChunk,
315+
}
316+
317+
removePureCssChunks(bundle, ['pure_css_chunk.js'], 'cjs')
318+
319+
const chunk = bundle['main.js'] as OutputChunk
320+
expect(chunk.code).toMatchSnapshot()
321+
// import is removed
322+
expect(chunk.imports).toEqual(['some-module', 'other-module'])
323+
})
324+
325+
test('require of removed chunk is dropped (minified, no new line)', () => {
326+
const bundle: OutputBundle = {
327+
'main.js': {
328+
code: 'require("some-module");require("pure_css_chunk.js");require("other-module");',
329+
imports: ['pure_css_chunk.js', 'some-module', 'other-module'],
330+
type: 'chunk',
331+
viteMetadata: {
332+
importedAssets: new Set<string>(),
333+
importedCss: new Set<string>(),
334+
},
335+
} as any as OutputChunk,
336+
'pure_css_chunk.js': {
337+
type: 'chunk',
338+
code: '',
339+
imports: [],
340+
viteMetadata: {
341+
importedAssets: new Set<string>(),
342+
importedCss: new Set<string>(),
343+
},
344+
} as any as OutputChunk,
345+
}
346+
347+
removePureCssChunks(bundle, ['pure_css_chunk.js'], 'cjs')
348+
349+
const chunk = bundle['main.js'] as OutputChunk
350+
expect(chunk.code).toMatchSnapshot()
351+
// import is removed
352+
expect(chunk.imports).toEqual(['some-module', 'other-module'])
353+
})
354+
355+
/* Currently broken as the code still contains the css chunk
356+
test('require of removed chunk is dropped (minified, with comma operator)', () => {
357+
const bundle: OutputBundle = {
358+
'main.js': {
359+
code: 'require("some-module"),require("pure_css_chunk.js"),require("other-module");',
360+
imports: ['pure_css_chunk.js', 'some-module', 'other-module'],
361+
type: 'chunk',
362+
viteMetadata: {
363+
importedAssets: new Set<string>(),
364+
importedCss: new Set<string>(),
365+
}
366+
} as any as OutputChunk,
367+
'pure_css_chunk.js': {
368+
type: 'chunk',
369+
code: '',
370+
imports: [],
371+
viteMetadata: {
372+
importedAssets: new Set<string>(),
373+
importedCss: new Set<string>(),
374+
}
375+
} as any as OutputChunk,
376+
}
377+
378+
removePureCssChunks(bundle, ['pure_css_chunk.js'], 'cjs')
379+
380+
const chunk = bundle['main.js'] as OutputChunk
381+
expect(chunk.code).toMatchSnapshot()
382+
// import is removed
383+
expect(chunk.imports).toEqual(['some-module', 'other-module'])
384+
expect(chunk.code.match(/pure_css_chunk\.js/)).toBeNull()
385+
})
386+
*/
387+
388+
/* Currently broken as the code is not valid
389+
test('require of removed chunk is dropped (minified, with comma and assignment)', () => {
390+
const bundle: OutputBundle = {
391+
'main.js': {
392+
code: 'require("some-module"),require("pure_css_chunk.js");const v=require("other-module");',
393+
imports: ['pure_css_chunk.js', 'some-module', 'other-module'],
394+
type: 'chunk',
395+
viteMetadata: {
396+
importedAssets: new Set<string>(),
397+
importedCss: new Set<string>(),
398+
}
399+
} as any as OutputChunk,
400+
'pure_css_chunk.js': {
401+
type: 'chunk',
402+
code: '',
403+
imports: [],
404+
viteMetadata: {
405+
importedAssets: new Set<string>(),
406+
importedCss: new Set<string>(),
407+
}
408+
} as any as OutputChunk,
409+
}
410+
411+
removePureCssChunks(bundle, ['pure_css_chunk.js'], 'cjs')
412+
413+
const chunk = bundle['main.js'] as OutputChunk
414+
expect(chunk.code).toMatchSnapshot()
415+
// import is removed
416+
expect(chunk.imports).toEqual(['some-module', 'other-module'])
417+
})
418+
*/
419+
420+
test('imported assets of css chunk are transfered', () => {
421+
const bundle: OutputBundle = {
422+
'main.js': {
423+
code: 'import "some-module";\nimport "pure_css_chunk.js";\nimport "other-module";\n',
424+
imports: ['pure_css_chunk.js', 'some-module', 'other-module'],
425+
type: 'chunk',
426+
viteMetadata: {
427+
importedAssets: new Set<string>(),
428+
importedCss: new Set<string>(),
429+
},
430+
} as any as OutputChunk,
431+
'pure_css_chunk.js': {
432+
type: 'chunk',
433+
code: '',
434+
imports: [],
435+
viteMetadata: {
436+
importedAssets: new Set<string>(['some-asset.svg']),
437+
importedCss: new Set<string>(['some-style.css']),
438+
},
439+
} as any as OutputChunk,
440+
}
441+
442+
removePureCssChunks(bundle, ['pure_css_chunk.js'], 'es')
443+
444+
const chunk = bundle['main.js'] as OutputChunk
445+
expect(chunk.code).toMatchSnapshot()
446+
// import is removed
447+
expect(chunk.imports).toEqual(['some-module', 'other-module'])
448+
// metadata is transfered
449+
expect(chunk.viteMetadata?.importedAssets.has('some-asset.svg')).toBe(true)
450+
expect(chunk.viteMetadata?.importedCss.has('some-style.css')).toBe(true)
451+
})
452+
})

packages/vite/src/node/plugins/css.ts

+59-38
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import glob from 'fast-glob'
66
import postcssrc from 'postcss-load-config'
77
import type {
88
ExistingRawSourceMap,
9+
ModuleFormat,
10+
OutputBundle,
911
OutputChunk,
1012
RenderedChunk,
1113
RollupError,
@@ -735,44 +737,8 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
735737
(pureCssChunk) => prelimaryNameToChunkMap[pureCssChunk.fileName],
736738
)
737739

738-
const emptyChunkFiles = pureCssChunkNames
739-
.map((file) => path.basename(file))
740-
.join('|')
741-
.replace(/\./g, '\\.')
742-
const emptyChunkRE = new RegExp(
743-
opts.format === 'es'
744-
? `\\bimport\\s*["'][^"']*(?:${emptyChunkFiles})["'];\n?`
745-
: `\\brequire\\(\\s*["'][^"']*(?:${emptyChunkFiles})["']\\);\n?`,
746-
'g',
747-
)
748-
for (const file in bundle) {
749-
const chunk = bundle[file]
750-
if (chunk.type === 'chunk') {
751-
// remove pure css chunk from other chunk's imports,
752-
// and also register the emitted CSS files under the importer
753-
// chunks instead.
754-
chunk.imports = chunk.imports.filter((file) => {
755-
if (pureCssChunkNames.includes(file)) {
756-
const { importedCss, importedAssets } = (
757-
bundle[file] as OutputChunk
758-
).viteMetadata!
759-
importedCss.forEach((file) =>
760-
chunk.viteMetadata!.importedCss.add(file),
761-
)
762-
importedAssets.forEach((file) =>
763-
chunk.viteMetadata!.importedAssets.add(file),
764-
)
765-
return false
766-
}
767-
return true
768-
})
769-
chunk.code = chunk.code.replace(
770-
emptyChunkRE,
771-
// remove css import while preserving source map location
772-
(m) => `/* empty css ${''.padEnd(m.length - 15)}*/`,
773-
)
774-
}
775-
}
740+
removePureCssChunks(bundle, pureCssChunkNames, opts.format)
741+
776742
const removedPureCssFiles = removedPureCssFilesCache.get(config)!
777743
pureCssChunkNames.forEach((fileName) => {
778744
removedPureCssFiles.set(fileName, bundle[fileName] as RenderedChunk)
@@ -818,6 +784,61 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
818784
}
819785
}
820786

787+
/**
788+
* Remove pure CSS chunks from the output bundle
789+
* @param bundle The output bundle
790+
* @param pureCssChunkNames Array of pure CSS chunk names
791+
* @param outputFormat The current output format, to decide whether `require` or `import` is used
792+
*/
793+
export function removePureCssChunks(
794+
bundle: OutputBundle,
795+
pureCssChunkNames: string[],
796+
outputFormat: ModuleFormat,
797+
): void {
798+
const emptyChunkFiles = pureCssChunkNames
799+
.map((file) => path.basename(file))
800+
.join('|')
801+
.replace(/\./g, '\\.')
802+
803+
// require and import calls might be chained by minifier using the comma operator
804+
// in this case we have to keep one comma
805+
// if a next require is chained or add a semicolon to terminate the chain.
806+
const emptyChunkRE = new RegExp(
807+
outputFormat === 'es'
808+
? `\\bimport\\s*["'][^"']*(?:${emptyChunkFiles})["'];\n?`
809+
: `\\brequire\\(\\s*["'][^"']*(?:${emptyChunkFiles})["']\\);\n?`,
810+
'g',
811+
)
812+
813+
for (const file in bundle) {
814+
const chunk = bundle[file]
815+
if (chunk.type === 'chunk') {
816+
// remove pure css chunk from other chunk's imports,
817+
// and also register the emitted CSS files under the importer
818+
// chunks instead.
819+
chunk.imports = chunk.imports.filter((file) => {
820+
if (pureCssChunkNames.includes(file)) {
821+
const { importedCss, importedAssets } = (bundle[file] as OutputChunk)
822+
.viteMetadata!
823+
importedCss.forEach((file) =>
824+
chunk.viteMetadata!.importedCss.add(file),
825+
)
826+
importedAssets.forEach((file) =>
827+
chunk.viteMetadata!.importedAssets.add(file),
828+
)
829+
return false
830+
}
831+
return true
832+
})
833+
chunk.code = chunk.code.replace(
834+
emptyChunkRE,
835+
// remove css import while preserving source map location
836+
(m) => `/* empty css ${''.padEnd(m.length - 15)}*/`,
837+
)
838+
}
839+
}
840+
}
841+
821842
interface CSSAtImportResolvers {
822843
css: ResolveFn
823844
sass: ResolveFn

0 commit comments

Comments
 (0)