Skip to content

Commit d3450ca

Browse files
sapphi-redjrmajor
andauthored
feat(css): support preprocessor with lightningcss (#19071)
Co-authored-by: Jeremiasz Major <[email protected]>
1 parent a92c74b commit d3450ca

14 files changed

+1257
-566
lines changed

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

+195-48
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export interface CSSOptions {
114114
/**
115115
* Using lightningcss is an experimental option to handle CSS modules,
116116
* assets and imports via Lightning CSS. It requires to install it as a
117-
* peer dependency. This is incompatible with the use of preprocessors.
117+
* peer dependency.
118118
*
119119
* @default 'postcss'
120120
* @experimental
@@ -206,7 +206,7 @@ export const cssConfigDefaults = Object.freeze({
206206
} satisfies CSSOptions)
207207

208208
export type ResolvedCSSOptions = Omit<CSSOptions, 'lightningcss'> &
209-
Required<Pick<CSSOptions, 'transformer'>> & {
209+
Required<Pick<CSSOptions, 'transformer' | 'devSourcemap'>> & {
210210
lightningcss?: LightningCSSOptions
211211
}
212212

@@ -1267,7 +1267,11 @@ async function compileCSSPreprocessors(
12671267
lang: PreprocessLang,
12681268
code: string,
12691269
workerController: PreprocessorWorkerController,
1270-
): Promise<{ code: string; map?: ExistingRawSourceMap; deps?: Set<string> }> {
1270+
): Promise<{
1271+
code: string
1272+
map?: ExistingRawSourceMap | { mappings: '' }
1273+
deps?: Set<string>
1274+
}> {
12711275
const { config } = environment
12721276
const { preprocessorOptions, devSourcemap } = config.css
12731277
const atImportResolvers = getAtImportResolvers(
@@ -1341,15 +1345,11 @@ async function compileCSS(
13411345
deps?: Set<string>
13421346
}> {
13431347
const { config } = environment
1344-
if (config.css.transformer === 'lightningcss') {
1345-
return compileLightningCSS(id, code, environment, urlResolver)
1346-
}
1347-
13481348
const lang = CSS_LANGS_RE.exec(id)?.[1] as CssLang | undefined
13491349
const deps = new Set<string>()
13501350

13511351
// pre-processors: sass etc.
1352-
let preprocessorMap: ExistingRawSourceMap | undefined
1352+
let preprocessorMap: ExistingRawSourceMap | { mappings: '' } | undefined
13531353
if (isPreProcessor(lang)) {
13541354
const preprocessorResult = await compileCSSPreprocessors(
13551355
environment,
@@ -1361,8 +1361,71 @@ async function compileCSS(
13611361
code = preprocessorResult.code
13621362
preprocessorMap = preprocessorResult.map
13631363
preprocessorResult.deps?.forEach((dep) => deps.add(dep))
1364+
} else if (lang === 'sss' && config.css.transformer === 'lightningcss') {
1365+
const sssResult = await transformSugarSS(environment, id, code)
1366+
code = sssResult.code
1367+
preprocessorMap = sssResult.map
1368+
}
1369+
1370+
const transformResult = await (config.css.transformer === 'lightningcss'
1371+
? compileLightningCSS(
1372+
environment,
1373+
id,
1374+
code,
1375+
deps,
1376+
workerController,
1377+
urlResolver,
1378+
)
1379+
: compilePostCSS(
1380+
environment,
1381+
id,
1382+
code,
1383+
deps,
1384+
lang,
1385+
workerController,
1386+
urlResolver,
1387+
))
1388+
1389+
if (!transformResult) {
1390+
return {
1391+
code,
1392+
map: config.css.devSourcemap ? preprocessorMap : { mappings: '' },
1393+
deps,
1394+
}
13641395
}
13651396

1397+
return {
1398+
...transformResult,
1399+
map: config.css.devSourcemap
1400+
? combineSourcemapsIfExists(
1401+
cleanUrl(id),
1402+
typeof transformResult.map === 'string'
1403+
? JSON.parse(transformResult.map)
1404+
: transformResult.map,
1405+
preprocessorMap,
1406+
)
1407+
: { mappings: '' },
1408+
deps,
1409+
}
1410+
}
1411+
1412+
async function compilePostCSS(
1413+
environment: PartialEnvironment,
1414+
id: string,
1415+
code: string,
1416+
deps: Set<string>,
1417+
lang: CssLang | undefined,
1418+
workerController: PreprocessorWorkerController,
1419+
urlResolver?: CssUrlResolver,
1420+
): Promise<
1421+
| {
1422+
code: string
1423+
map?: Exclude<SourceMapInput, string>
1424+
modules?: Record<string, string>
1425+
}
1426+
| undefined
1427+
> {
1428+
const { config } = environment
13661429
const { modules: modulesOptions, devSourcemap } = config.css
13671430
const isModule = modulesOptions !== false && cssModuleRE.test(id)
13681431
// although at serve time it can work without processing, we do need to
@@ -1381,7 +1444,7 @@ async function compileCSS(
13811444
!needInlineImport &&
13821445
!hasUrl
13831446
) {
1384-
return { code, map: preprocessorMap ?? null, deps }
1447+
return
13851448
}
13861449

13871450
// postcss
@@ -1506,25 +1569,61 @@ async function compileCSS(
15061569
lang === 'sss' ? loadSss(config.root) : postcssOptions.parser
15071570

15081571
if (!postcssPlugins.length && !postcssParser) {
1509-
return {
1510-
code,
1511-
map: preprocessorMap,
1512-
deps,
1513-
}
1572+
return
15141573
}
15151574

1575+
const result = await runPostCSS(
1576+
id,
1577+
code,
1578+
postcssPlugins,
1579+
{ ...postcssOptions, parser: postcssParser },
1580+
deps,
1581+
environment.logger,
1582+
devSourcemap,
1583+
)
1584+
return { ...result, modules }
1585+
}
1586+
1587+
async function transformSugarSS(
1588+
environment: PartialEnvironment,
1589+
id: string,
1590+
code: string,
1591+
) {
1592+
const { config } = environment
1593+
const { devSourcemap } = config.css
1594+
1595+
const result = await runPostCSS(
1596+
id,
1597+
code,
1598+
[],
1599+
{ parser: loadSss(config.root) },
1600+
undefined,
1601+
environment.logger,
1602+
devSourcemap,
1603+
)
1604+
return result
1605+
}
1606+
1607+
async function runPostCSS(
1608+
id: string,
1609+
code: string,
1610+
plugins: PostCSS.AcceptedPlugin[],
1611+
options: PostCSS.ProcessOptions,
1612+
deps: Set<string> | undefined,
1613+
logger: Logger,
1614+
enableSourcemap: boolean,
1615+
) {
15161616
let postcssResult: PostCSS.Result
15171617
try {
15181618
const source = removeDirectQuery(id)
15191619
const postcss = await importPostcss()
15201620

15211621
// postcss is an unbundled dep and should be lazy imported
1522-
postcssResult = await postcss.default(postcssPlugins).process(code, {
1523-
...postcssOptions,
1524-
parser: postcssParser,
1622+
postcssResult = await postcss.default(plugins).process(code, {
1623+
...options,
15251624
to: source,
15261625
from: source,
1527-
...(devSourcemap
1626+
...(enableSourcemap
15281627
? {
15291628
map: {
15301629
inline: false,
@@ -1542,7 +1641,7 @@ async function compileCSS(
15421641
// record CSS dependencies from @imports
15431642
for (const message of postcssResult.messages) {
15441643
if (message.type === 'dependency') {
1545-
deps.add(normalizePath(message.file as string))
1644+
deps?.add(normalizePath(message.file as string))
15461645
} else if (message.type === 'dir-dependency') {
15471646
// https://github.com/postcss/postcss/blob/main/docs/guidelines/plugin.md#3-dependencies
15481647
const { dir, glob: globPattern = '**' } = message
@@ -1553,7 +1652,7 @@ async function compileCSS(
15531652
ignore: ['**/node_modules/**'],
15541653
})
15551654
for (let i = 0; i < files.length; i++) {
1556-
deps.add(files[i])
1655+
deps?.add(files[i])
15571656
}
15581657
} else if (message.type === 'warning') {
15591658
const warning = message as PostCSS.Warning
@@ -1571,7 +1670,7 @@ async function compileCSS(
15711670
}
15721671
: undefined,
15731672
)}`
1574-
environment.logger.warn(colors.yellow(msg))
1673+
logger.warn(colors.yellow(msg))
15751674
}
15761675
}
15771676
} catch (e) {
@@ -1585,17 +1684,14 @@ async function compileCSS(
15851684
throw e
15861685
}
15871686

1588-
if (!devSourcemap) {
1687+
if (!enableSourcemap) {
15891688
return {
15901689
code: postcssResult.css,
1591-
map: { mappings: '' },
1592-
modules,
1593-
deps,
1690+
map: { mappings: '' as const },
15941691
}
15951692
}
15961693

15971694
const rawPostcssMap = postcssResult.map.toJSON()
1598-
15991695
const postcssMap = await formatPostcssSourceMap(
16001696
// version property of rawPostcssMap is declared as string
16011697
// but actually it is a number
@@ -1605,9 +1701,7 @@ async function compileCSS(
16051701

16061702
return {
16071703
code: postcssResult.css,
1608-
map: combineSourcemapsIfExists(cleanUrl(id), postcssMap, preprocessorMap),
1609-
modules,
1610-
deps,
1704+
map: postcssMap,
16111705
}
16121706
}
16131707

@@ -1702,17 +1796,21 @@ export async function formatPostcssSourceMap(
17021796

17031797
function combineSourcemapsIfExists(
17041798
filename: string,
1705-
map1: ExistingRawSourceMap | undefined,
1706-
map2: ExistingRawSourceMap | undefined,
1707-
): ExistingRawSourceMap | undefined {
1708-
return map1 && map2
1709-
? (combineSourcemaps(filename, [
1710-
// type of version property of ExistingRawSourceMap is number
1711-
// but it is always 3
1712-
map1 as RawSourceMap,
1713-
map2 as RawSourceMap,
1714-
]) as ExistingRawSourceMap)
1715-
: map1
1799+
map1: ExistingRawSourceMap | { mappings: '' } | undefined,
1800+
map2: ExistingRawSourceMap | { mappings: '' } | undefined,
1801+
): ExistingRawSourceMap | { mappings: '' } | undefined {
1802+
if (!map1 || !map2) {
1803+
return map1
1804+
}
1805+
if (map1.mappings === '' || map2.mappings === '') {
1806+
return { mappings: '' }
1807+
}
1808+
return combineSourcemaps(filename, [
1809+
// type of version property of ExistingRawSourceMap is number
1810+
// but it is always 3
1811+
map1 as RawSourceMap,
1812+
map2 as RawSourceMap,
1813+
]) as ExistingRawSourceMap
17161814
}
17171815

17181816
const viteHashUpdateMarker = '/*$vite$:1*/'
@@ -3269,13 +3367,18 @@ function isPreProcessor(lang: any): lang is PreprocessLang {
32693367

32703368
const importLightningCSS = createCachedImport(() => import('lightningcss'))
32713369
async function compileLightningCSS(
3370+
environment: PartialEnvironment,
32723371
id: string,
32733372
src: string,
3274-
environment: PartialEnvironment,
3373+
deps: Set<string>,
3374+
workerController: PreprocessorWorkerController,
32753375
urlResolver?: CssUrlResolver,
3276-
): ReturnType<typeof compileCSS> {
3376+
): Promise<{
3377+
code: string
3378+
map?: string | undefined
3379+
modules?: Record<string, string>
3380+
}> {
32773381
const { config } = environment
3278-
const deps = new Set<string>()
32793382
// replace null byte as lightningcss treats that as a string terminator
32803383
// https://github.com/parcel-bundler/lightningcss/issues/874
32813384
const filename = removeDirectQuery(id).replace('\0', NULL_BYTE_PLACEHOLDER)
@@ -3298,11 +3401,32 @@ async function compileLightningCSS(
32983401
// projectRoot is needed to get stable hash when using CSS modules
32993402
projectRoot: config.root,
33003403
resolver: {
3301-
read(filePath) {
3404+
async read(filePath) {
33023405
if (filePath === filename) {
33033406
return src
33043407
}
3305-
return fs.readFileSync(filePath, 'utf-8')
3408+
3409+
const code = fs.readFileSync(filePath, 'utf-8')
3410+
const lang = CSS_LANGS_RE.exec(filePath)?.[1] as
3411+
| CssLang
3412+
| undefined
3413+
if (isPreProcessor(lang)) {
3414+
const result = await compileCSSPreprocessors(
3415+
environment,
3416+
id,
3417+
lang,
3418+
code,
3419+
workerController,
3420+
)
3421+
result.deps?.forEach((dep) => deps.add(dep))
3422+
// TODO: support source map
3423+
return result.code
3424+
} else if (lang === 'sss') {
3425+
const sssResult = await transformSugarSS(environment, id, code)
3426+
// TODO: support source map
3427+
return sssResult.code
3428+
}
3429+
return code
33063430
},
33073431
async resolve(id, from) {
33083432
const publicFile = checkPublicFile(
@@ -3313,10 +3437,34 @@ async function compileLightningCSS(
33133437
return publicFile
33143438
}
33153439

3316-
const resolved = await getAtImportResolvers(
3440+
// NOTE: with `transformer: 'postcss'`, CSS modules `composes` tried to resolve with
3441+
// all resolvers, but in `transformer: 'lightningcss'`, only the one for the
3442+
// current file type is used.
3443+
const atImportResolvers = getAtImportResolvers(
33173444
environment.getTopLevelConfig(),
3318-
).css(environment, id, from)
3445+
)
3446+
const lang = CSS_LANGS_RE.exec(from)?.[1] as CssLang | undefined
3447+
let resolver: ResolveIdFn
3448+
switch (lang) {
3449+
case 'css':
3450+
case 'sss':
3451+
case 'styl':
3452+
case 'stylus':
3453+
case undefined:
3454+
resolver = atImportResolvers.css
3455+
break
3456+
case 'sass':
3457+
case 'scss':
3458+
resolver = atImportResolvers.sass
3459+
break
3460+
case 'less':
3461+
resolver = atImportResolvers.less
3462+
break
3463+
default:
3464+
throw new Error(`Unknown lang: ${lang satisfies never}`)
3465+
}
33193466

3467+
const resolved = await resolver(environment, id, from)
33203468
if (resolved) {
33213469
deps.add(resolved)
33223470
return resolved
@@ -3431,7 +3579,6 @@ async function compileLightningCSS(
34313579
return {
34323580
code: css,
34333581
map: 'map' in res ? res.map?.toString() : undefined,
3434-
deps,
34353582
modules,
34363583
}
34373584
}

0 commit comments

Comments
 (0)