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

feat(css): support preprocessor with lightningcss #19071

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
241 changes: 194 additions & 47 deletions packages/vite/src/node/plugins/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ export const cssConfigDefaults = Object.freeze({
} satisfies CSSOptions)

export type ResolvedCSSOptions = Omit<CSSOptions, 'lightningcss'> &
Required<Pick<CSSOptions, 'transformer'>> & {
Required<Pick<CSSOptions, 'transformer' | 'devSourcemap'>> & {
lightningcss?: LightningCSSOptions
}

Expand Down Expand Up @@ -1294,7 +1294,11 @@ async function compileCSSPreprocessors(
lang: PreprocessLang,
code: string,
workerController: PreprocessorWorkerController,
): Promise<{ code: string; map?: ExistingRawSourceMap; deps?: Set<string> }> {
): Promise<{
code: string
map?: ExistingRawSourceMap | { mappings: '' }
deps?: Set<string>
}> {
const { config } = environment
const { preprocessorOptions, devSourcemap } = config.css
const atImportResolvers = getAtImportResolvers(
Expand Down Expand Up @@ -1368,15 +1372,11 @@ async function compileCSS(
deps?: Set<string>
}> {
const { config } = environment
if (config.css.transformer === 'lightningcss') {
return compileLightningCSS(id, code, environment, urlResolver)
}

const lang = CSS_LANGS_RE.exec(id)?.[1] as CssLang | undefined
const deps = new Set<string>()

// pre-processors: sass etc.
let preprocessorMap: ExistingRawSourceMap | undefined
let preprocessorMap: ExistingRawSourceMap | { mappings: '' } | undefined
if (isPreProcessor(lang)) {
const preprocessorResult = await compileCSSPreprocessors(
environment,
Expand All @@ -1388,8 +1388,71 @@ async function compileCSS(
code = preprocessorResult.code
preprocessorMap = preprocessorResult.map
preprocessorResult.deps?.forEach((dep) => deps.add(dep))
} else if (lang === 'sss' && config.css.transformer === 'lightningcss') {
const sssResult = await transformSugarSS(environment, id, code)
code = sssResult.code
preprocessorMap = sssResult.map
}

const transformResult = await (config.css.transformer === 'lightningcss'
? compileLightningCSS(
environment,
id,
code,
deps,
workerController,
urlResolver,
)
: compilePostCSS(
environment,
id,
code,
deps,
lang,
workerController,
urlResolver,
))

if (!transformResult) {
return {
code,
map: config.css.devSourcemap ? preprocessorMap : { mappings: '' },
deps,
}
}

return {
...transformResult,
map: config.css.devSourcemap
? combineSourcemapsIfExists(
cleanUrl(id),
typeof transformResult.map === 'string'
? JSON.parse(transformResult.map)
: transformResult.map,
preprocessorMap,
)
: { mappings: '' },
deps,
}
}

async function compilePostCSS(
environment: PartialEnvironment,
id: string,
code: string,
deps: Set<string>,
lang: CssLang | undefined,
workerController: PreprocessorWorkerController,
urlResolver?: CssUrlResolver,
): Promise<
| {
code: string
map?: Exclude<SourceMapInput, string>
modules?: Record<string, string>
}
| undefined
> {
const { config } = environment
const { modules: modulesOptions, devSourcemap } = config.css
const isModule = modulesOptions !== false && cssModuleRE.test(id)
// although at serve time it can work without processing, we do need to
Expand All @@ -1408,7 +1471,7 @@ async function compileCSS(
!needInlineImport &&
!hasUrl
) {
return { code, map: preprocessorMap ?? null, deps }
return
}

// postcss
Expand Down Expand Up @@ -1533,25 +1596,61 @@ async function compileCSS(
lang === 'sss' ? loadSss(config.root) : postcssOptions.parser

if (!postcssPlugins.length && !postcssParser) {
return {
code,
map: preprocessorMap,
deps,
}
return
}

const result = await runPostCSS(
id,
code,
postcssPlugins,
{ ...postcssOptions, parser: postcssParser },
deps,
environment.logger,
devSourcemap,
)
return { ...result, modules }
}

async function transformSugarSS(
environment: PartialEnvironment,
id: string,
code: string,
) {
const { config } = environment
const { devSourcemap } = config.css

const result = await runPostCSS(
id,
code,
[],
{ parser: loadSss(config.root) },
undefined,
environment.logger,
devSourcemap,
)
return result
}

async function runPostCSS(
id: string,
code: string,
plugins: PostCSS.AcceptedPlugin[],
options: PostCSS.ProcessOptions,
deps: Set<string> | undefined,
logger: Logger,
enableSourcemap: boolean,
) {
let postcssResult: PostCSS.Result
try {
const source = removeDirectQuery(id)
const postcss = await importPostcss()

// postcss is an unbundled dep and should be lazy imported
postcssResult = await postcss.default(postcssPlugins).process(code, {
...postcssOptions,
parser: postcssParser,
postcssResult = await postcss.default(plugins).process(code, {
...options,
to: source,
from: source,
...(devSourcemap
...(enableSourcemap
? {
map: {
inline: false,
Expand All @@ -1569,7 +1668,7 @@ async function compileCSS(
// record CSS dependencies from @imports
for (const message of postcssResult.messages) {
if (message.type === 'dependency') {
deps.add(normalizePath(message.file as string))
deps?.add(normalizePath(message.file as string))
} else if (message.type === 'dir-dependency') {
// https://github.com/postcss/postcss/blob/main/docs/guidelines/plugin.md#3-dependencies
const { dir, glob: globPattern = '**' } = message
Expand All @@ -1580,7 +1679,7 @@ async function compileCSS(
ignore: ['**/node_modules/**'],
})
for (let i = 0; i < files.length; i++) {
deps.add(files[i])
deps?.add(files[i])
}
} else if (message.type === 'warning') {
const warning = message as PostCSS.Warning
Expand All @@ -1598,7 +1697,7 @@ async function compileCSS(
}
: undefined,
)}`
environment.logger.warn(colors.yellow(msg))
logger.warn(colors.yellow(msg))
}
}
} catch (e) {
Expand All @@ -1612,17 +1711,14 @@ async function compileCSS(
throw e
}

if (!devSourcemap) {
if (!enableSourcemap) {
return {
code: postcssResult.css,
map: { mappings: '' },
modules,
deps,
map: { mappings: '' as const },
}
}

const rawPostcssMap = postcssResult.map.toJSON()

const postcssMap = await formatPostcssSourceMap(
// version property of rawPostcssMap is declared as string
// but actually it is a number
Expand All @@ -1632,9 +1728,7 @@ async function compileCSS(

return {
code: postcssResult.css,
map: combineSourcemapsIfExists(cleanUrl(id), postcssMap, preprocessorMap),
modules,
deps,
map: postcssMap,
}
}

Expand Down Expand Up @@ -1729,17 +1823,21 @@ export async function formatPostcssSourceMap(

function combineSourcemapsIfExists(
filename: string,
map1: ExistingRawSourceMap | undefined,
map2: ExistingRawSourceMap | undefined,
): ExistingRawSourceMap | undefined {
return map1 && map2
? (combineSourcemaps(filename, [
// type of version property of ExistingRawSourceMap is number
// but it is always 3
map1 as RawSourceMap,
map2 as RawSourceMap,
]) as ExistingRawSourceMap)
: map1
map1: ExistingRawSourceMap | { mappings: '' } | undefined,
map2: ExistingRawSourceMap | { mappings: '' } | undefined,
): ExistingRawSourceMap | { mappings: '' } | undefined {
if (!map1 || !map2) {
return map1
}
if (map1.mappings === '' || map2.mappings === '') {
return { mappings: '' }
}
return combineSourcemaps(filename, [
// type of version property of ExistingRawSourceMap is number
// but it is always 3
map1 as RawSourceMap,
map2 as RawSourceMap,
]) as ExistingRawSourceMap
}

const viteHashUpdateMarker = '/*$vite$:1*/'
Expand Down Expand Up @@ -3296,13 +3394,18 @@ function isPreProcessor(lang: any): lang is PreprocessLang {

const importLightningCSS = createCachedImport(() => import('lightningcss'))
async function compileLightningCSS(
environment: PartialEnvironment,
id: string,
src: string,
environment: PartialEnvironment,
deps: Set<string>,
workerController: PreprocessorWorkerController,
urlResolver?: CssUrlResolver,
): ReturnType<typeof compileCSS> {
): Promise<{
code: string
map?: string | undefined
modules?: Record<string, string>
}> {
const { config } = environment
const deps = new Set<string>()
// replace null byte as lightningcss treats that as a string terminator
// https://github.com/parcel-bundler/lightningcss/issues/874
const filename = removeDirectQuery(id).replace('\0', NULL_BYTE_PLACEHOLDER)
Expand All @@ -3325,11 +3428,32 @@ async function compileLightningCSS(
// projectRoot is needed to get stable hash when using CSS modules
projectRoot: config.root,
resolver: {
read(filePath) {
async read(filePath) {
if (filePath === filename) {
return src
}
return fs.readFileSync(filePath, 'utf-8')

const code = fs.readFileSync(filePath, 'utf-8')
const lang = CSS_LANGS_RE.exec(filePath)?.[1] as
| CssLang
| undefined
if (isPreProcessor(lang)) {
const result = await compileCSSPreprocessors(
environment,
id,
lang,
code,
workerController,
)
result.deps?.forEach((dep) => deps.add(dep))
// TODO: support source map
return result.code
} else if (lang === 'sss') {
const sssResult = await transformSugarSS(environment, id, code)
// TODO: support source map
return sssResult.code
}
return code
},
async resolve(id, from) {
const publicFile = checkPublicFile(
Expand All @@ -3340,10 +3464,34 @@ async function compileLightningCSS(
return publicFile
}

const resolved = await getAtImportResolvers(
// NOTE: with `transformer: 'postcss'`, CSS modules `composes` tried to resolve with
// all resolvers, but in `transformer: 'lightningcss'`, only the one for the
// current file type is used.
const atImportResolvers = getAtImportResolvers(
environment.getTopLevelConfig(),
).css(environment, id, from)
)
const lang = CSS_LANGS_RE.exec(from)?.[1] as CssLang | undefined
let resolver: ResolveIdFn
switch (lang) {
case 'css':
case 'sss':
case 'styl':
case 'stylus':
case undefined:
resolver = atImportResolvers.css
break
case 'sass':
case 'scss':
resolver = atImportResolvers.sass
break
case 'less':
resolver = atImportResolvers.less
break
default:
throw new Error(`Unknown lang: ${lang satisfies never}`)
}

const resolved = await resolver(environment, id, from)
if (resolved) {
deps.add(resolved)
return resolved
Expand Down Expand Up @@ -3458,7 +3606,6 @@ async function compileLightningCSS(
return {
code: css,
map: 'map' in res ? res.map?.toString() : undefined,
deps,
modules,
}
}
Expand Down
Loading
Loading