Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(css): make getEmptyChunkReplacer for unit test #14528

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`getEmptyChunkReplacer > replaces import call 1`] = `
"import \\"some-module\\";
/* empty css */import \\"other-module\\";"
`;

exports[`getEmptyChunkReplacer > replaces require call 1`] = `
"require(\\"some-module\\");
/* empty css */require(\\"other-module\\");"
`;

exports[`getEmptyChunkReplacer > replaces require call in minified code without new lines 1`] = `"require(\\"some-module\\");/* empty css */require(\\"other-module\\");"`;

exports[`removePureCssChunks > import of removed chunk is dropped 1`] = `
"import \\"some-module\\";
/* empty css */import \\"other-module\\";
"
`;

exports[`removePureCssChunks > imported assets of css chunk are transfered 1`] = `
"import \\"some-module\\";
/* empty css */import \\"other-module\\";
"
`;

exports[`removePureCssChunks > require of removed chunk is dropped 1`] = `
"require(\\"some-module\\");
/* empty css */require(\\"other-module\\");
"
`;
142 changes: 142 additions & 0 deletions packages/vite/src/node/__tests__/plugins/css.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import fs from 'node:fs'
import path from 'node:path'
import { describe, expect, test, vi } from 'vitest'
import type { OutputBundle, OutputChunk } from 'rollup'
import { resolveConfig } from '../../config'
import type { InlineConfig } from '../../config'
import {
convertTargets,
cssPlugin,
cssUrlRE,
getEmptyChunkReplacer,
hoistAtRules,
removePureCssChunks,
} from '../../plugins/css'

describe('search css url function', () => {
Expand Down Expand Up @@ -258,3 +261,142 @@ describe('convertTargets', () => {
})
})
})

describe('removePureCssChunks', () => {
test('import of removed chunk is dropped', () => {
const bundle: OutputBundle = {
'main.js': {
code: 'import "some-module";\nimport "pure_css_chunk.js";\nimport "other-module";\n',
imports: ['pure_css_chunk.js', 'some-module', 'other-module'],
type: 'chunk',
viteMetadata: {
importedAssets: new Set<string>(),
importedCss: new Set<string>(),
},
} as any as OutputChunk,
'pure_css_chunk.js': {
type: 'chunk',
code: '',
imports: [],
viteMetadata: {
importedAssets: new Set<string>(),
importedCss: new Set<string>(),
},
} as any as OutputChunk,
}

removePureCssChunks(bundle, ['pure_css_chunk.js'], 'es')

const chunk = bundle['main.js'] as OutputChunk
expect(chunk.code).toMatchSnapshot()
// import is removed
expect(chunk.imports).toEqual(['some-module', 'other-module'])
})

test('require of removed chunk is dropped', () => {
const bundle: OutputBundle = {
'main.js': {
code: 'require("some-module");\nrequire("pure_css_chunk.js");\nrequire("other-module");\n',
imports: ['pure_css_chunk.js', 'some-module', 'other-module'],
type: 'chunk',
viteMetadata: {
importedAssets: new Set<string>(),
importedCss: new Set<string>(),
},
} as any as OutputChunk,
'pure_css_chunk.js': {
type: 'chunk',
code: '',
imports: [],
viteMetadata: {
importedAssets: new Set<string>(),
importedCss: new Set<string>(),
},
} as any as OutputChunk,
}

removePureCssChunks(bundle, ['pure_css_chunk.js'], 'cjs')

const chunk = bundle['main.js'] as OutputChunk
expect(chunk.code).toMatchSnapshot()
// import is removed
expect(chunk.imports).toEqual(['some-module', 'other-module'])
})

test('imported assets of css chunk are transfered', () => {
const bundle: OutputBundle = {
'main.js': {
code: 'import "some-module";\nimport "pure_css_chunk.js";\nimport "other-module";\n',
imports: ['pure_css_chunk.js', 'some-module', 'other-module'],
type: 'chunk',
viteMetadata: {
importedAssets: new Set<string>(),
importedCss: new Set<string>(),
},
} as any as OutputChunk,
'pure_css_chunk.js': {
type: 'chunk',
code: '',
imports: [],
viteMetadata: {
importedAssets: new Set<string>(['some-asset.svg']),
importedCss: new Set<string>(['some-style.css']),
},
} as any as OutputChunk,
}

removePureCssChunks(bundle, ['pure_css_chunk.js'], 'es')

const chunk = bundle['main.js'] as OutputChunk
expect(chunk.code).toMatchSnapshot()
// import is removed
expect(chunk.imports).toEqual(['some-module', 'other-module'])
// metadata is transfered
expect(chunk.viteMetadata?.importedAssets.has('some-asset.svg')).toBe(true)
expect(chunk.viteMetadata?.importedCss.has('some-style.css')).toBe(true)
})
})

describe('getEmptyChunkReplacer', () => {
test('replaces import call', () => {
const code =
'import "some-module";\nimport "pure_css_chunk.js";\nimport "other-module";'

const replacer = getEmptyChunkReplacer(['pure_css_chunk.js'], 'es')
expect(replacer(code)).toMatchSnapshot()
})

test('replaces require call', () => {
const code =
'require("some-module");\nrequire("pure_css_chunk.js");\nrequire("other-module");'

const replacer = getEmptyChunkReplacer(['pure_css_chunk.js'], 'cjs')
expect(replacer(code)).toMatchSnapshot()
})

test('replaces require call in minified code without new lines', () => {
const code =
'require("some-module");require("pure_css_chunk.js");require("other-module");'

const replacer = getEmptyChunkReplacer(['pure_css_chunk.js'], 'cjs')
expect(replacer(code)).toMatchSnapshot()
})

/* Currently broken as the code still contains the css chunk
test('replaces require call in minified code that uses comma operator', () => {
const code = 'require("some-module"),require("pure_css_chunk.js"),require("other-module");'

const replacer = getEmptyChunkReplacer(['pure_css_chunk.js'], 'cjs')
const newCode = replacer(code)
expect(newCode).toMatchSnapshot()
// So there should be no pure css chunk anymore
expect(newCode.match(/pure_css_chunk\.js/)).toBeNull()
}) */

/* Currently broken as the code is not valid
test('replaces require call in minified code that uses comma operator followed by assignment', () => {
const code = 'require("some-module"),require("pure_css_chunk.js");const v=require("other-module");'
const replacer = getEmptyChunkReplacer(['pure_css_chunk.js'], 'cjs')
expect(replacer(code)).toMatchSnapshot()
}) */
})
115 changes: 77 additions & 38 deletions packages/vite/src/node/plugins/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import glob from 'fast-glob'
import postcssrc from 'postcss-load-config'
import type {
ExistingRawSourceMap,
ModuleFormat,
OutputBundle,
OutputChunk,
RenderedChunk,
RollupError,
Expand Down Expand Up @@ -735,44 +737,8 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
(pureCssChunk) => prelimaryNameToChunkMap[pureCssChunk.fileName],
)

const emptyChunkFiles = pureCssChunkNames
.map((file) => path.basename(file))
.join('|')
.replace(/\./g, '\\.')
const emptyChunkRE = new RegExp(
opts.format === 'es'
? `\\bimport\\s*["'][^"']*(?:${emptyChunkFiles})["'];\n?`
: `\\brequire\\(\\s*["'][^"']*(?:${emptyChunkFiles})["']\\);\n?`,
'g',
)
for (const file in bundle) {
const chunk = bundle[file]
if (chunk.type === 'chunk') {
// remove pure css chunk from other chunk's imports,
// and also register the emitted CSS files under the importer
// chunks instead.
chunk.imports = chunk.imports.filter((file) => {
if (pureCssChunkNames.includes(file)) {
const { importedCss, importedAssets } = (
bundle[file] as OutputChunk
).viteMetadata!
importedCss.forEach((file) =>
chunk.viteMetadata!.importedCss.add(file),
)
importedAssets.forEach((file) =>
chunk.viteMetadata!.importedAssets.add(file),
)
return false
}
return true
})
chunk.code = chunk.code.replace(
emptyChunkRE,
// remove css import while preserving source map location
(m) => `/* empty css ${''.padEnd(m.length - 15)}*/`,
)
}
}
removePureCssChunks(bundle, pureCssChunkNames, opts.format)

const removedPureCssFiles = removedPureCssFilesCache.get(config)!
pureCssChunkNames.forEach((fileName) => {
removedPureCssFiles.set(fileName, bundle[fileName] as RenderedChunk)
Expand Down Expand Up @@ -818,6 +784,79 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
}
}

/**
* Create a replacer function that takes code and replaces given pure CSS chunk imports
* @param pureCssChunkNames The chunks that only contain pure CSS and should be replaced
* @param outputFormat The module output format to decide whether to replace `import` or `require`
*/
export function getEmptyChunkReplacer(
pureCssChunkNames: string[],
outputFormat: ModuleFormat,
): (code: string) => string {
const emptyChunkFiles = pureCssChunkNames
.map((file) => path.basename(file))
.join('|')
.replace(/\./g, '\\.')

// require and import calls might be chained by minifier using the comma operator
// in this case we have to keep one comma
// if a next require is chained or add a semicolon to terminate the chain.
const emptyChunkRE = new RegExp(
outputFormat === 'es'
? `\\bimport\\s*["'][^"']*(?:${emptyChunkFiles})["'];\n?`
: `\\brequire\\(\\s*["'][^"']*(?:${emptyChunkFiles})["']\\);\n?`,
'g',
)

return (code: string) =>
code.replace(
emptyChunkRE,
// remove css import while preserving source map location
(m) => `/* empty css ${''.padEnd(m.length - 15)}*/`,
)
}

/**
* Remove pure CSS chunks from the output bundle
* @param bundle The output bundle
* @param pureCssChunkNames Array of pure CSS chunk names
* @param outputFormat The current output format, to decide whether `require` or `import` is used
*/
export function removePureCssChunks(
bundle: OutputBundle,
pureCssChunkNames: string[],
outputFormat: ModuleFormat,
): void {
const replaceEmptyChunk = getEmptyChunkReplacer(
pureCssChunkNames,
outputFormat,
)

for (const file in bundle) {
const chunk = bundle[file]
if (chunk.type === 'chunk') {
// remove pure css chunk from other chunk's imports,
// and also register the emitted CSS files under the importer
// chunks instead.
chunk.imports = chunk.imports.filter((file) => {
if (pureCssChunkNames.includes(file)) {
const { importedCss, importedAssets } = (bundle[file] as OutputChunk)
.viteMetadata!
importedCss.forEach((file) =>
chunk.viteMetadata!.importedCss.add(file),
)
importedAssets.forEach((file) =>
chunk.viteMetadata!.importedAssets.add(file),
)
return false
}
return true
})
chunk.code = replaceEmptyChunk(chunk.code)
}
}
}

interface CSSAtImportResolvers {
css: ResolveFn
sass: ResolveFn
Expand Down