This commit is contained in:
bluwy 2024-03-18 14:57:10 +08:00
parent 63f21faf2f
commit 67d2d69587
10 changed files with 255 additions and 147 deletions

View File

@ -1,9 +1,13 @@
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
entries: ['src/index.ts', 'src/postcss/index.ts'],
entries: [
'src/index.ts',
'src/lightningcss/index.ts',
'src/postcss/index.ts',
],
clean: true,
declaration: true,
declaration: 'node16',
rollup: {
inlineDependencies: true,
esbuild: {

View File

@ -3,10 +3,12 @@
"version": "1.0.0",
"license": "MIT",
"private": true,
"type": "module",
"main": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"types": "./dist/index.d.mts",
"exports": {
".": "./dist/index.mjs",
"./lightningcss": "./dist/lightningcss/index.mjs",
"./postcss": "./dist/postcss/index.mjs"
},
"scripts": {
@ -18,9 +20,13 @@
"node": "^18.0.0 || >=20.0.0"
},
"peerDependencies": {
"lightningcss": "^1.21.0",
"postcss": "^8.4.35"
},
"peerDependenciesMeta": {
"lightningcss": {
"optional": true
},
"postcss": {
"optional": true
}
@ -44,6 +50,7 @@
"@types/postcss-modules-local-by-default": "^4.0.2",
"@types/postcss-modules-scope": "^3.0.4",
"@types/postcss-modules-values": "^4.0.2",
"lightningcss": "^1.24.1",
"postcss": "^8.4.35"
}
}

View File

@ -0,0 +1,54 @@
import { transform } from 'lightningcss'
import type { CSSModulesConfig } from 'lightningcss'
import type { CSSModuleData, RawSourceMap } from '../types'
export interface CompileOptions {
cssModules?: CSSModulesConfig
sourcemap?: boolean
}
export interface CompileResult {
css: string
map?: RawSourceMap
data: CSSModuleData
}
export async function compileCSSModule(
css: string,
id: string,
options?: CompileOptions,
): Promise<CompileResult> {
const transformed = transform({
filename: id,
code: Buffer.from(css),
cssModules: options?.cssModules ?? true,
sourceMap: options?.sourcemap,
})
/**
* Addresses non-deterministic exports order:
* https://github.com/parcel-bundler/lightningcss/issues/291
*/
const exports = Object.fromEntries(
Object.entries(
// `exports` is defined if cssModules is true
transformed.exports!,
).sort(
// Cheap alphabetical sort (localCompare is expensive)
([a], [b]) => (a < b ? -1 : a > b ? 1 : 0),
),
)
const map = transformed.map
? (JSON.parse(Buffer.from(transformed.map).toString()) as RawSourceMap)
: undefined
return {
css: transformed.code.toString(),
map,
data: {
exports,
references: transformed.references,
},
}
}

View File

@ -0,0 +1,2 @@
export { compileCSSModule } from './compile'
export type { CompileOptions, CompileResult } from './compile'

View File

@ -7,7 +7,8 @@ import postcss from 'postcss'
import type { CSSModuleData, CSSModulesOptions, RawSourceMap } from '../types'
import { postcssExtractIcss } from './postcss-extract-icss'
export interface CompileOptions extends CSSModulesOptions {
export interface CompileOptions {
cssModules?: CSSModulesOptions
sourcemap?: boolean
}
@ -37,14 +38,15 @@ export async function compileCSSModule(
id: string,
options?: CompileOptions,
): Promise<CompileResult> {
const cssModules = options?.cssModules ?? {}
const generateScopedName =
typeof options?.generateScopedName === 'function'
? options.generateScopedName
: genericNames(options?.generateScopedName ?? defaultScopedName, {
hashPrefix: options?.hashPrefix,
typeof cssModules.generateScopedName === 'function'
? cssModules.generateScopedName
: genericNames(cssModules.generateScopedName ?? defaultScopedName, {
hashPrefix: cssModules.hashPrefix,
})
const isGlobal = options?.globalModulePaths?.some((pattern) =>
const isGlobal = cssModules.globalModulePaths?.some((pattern) =>
pattern.test(id),
)
@ -55,7 +57,7 @@ export async function compileCSSModule(
postcssModulesValues,
postcssModulesLocalByDefault({
mode: isGlobal ? 'global' : options?.scopeBehaviour,
mode: isGlobal ? 'global' : cssModules.scopeBehaviour,
}),
// Declares imports from composes
@ -63,7 +65,7 @@ export async function compileCSSModule(
// Resolves & removes composes
postcssModulesScope({
exportGlobals: options?.exportGlobals,
exportGlobals: cssModules.exportGlobals,
generateScopedName: (exportName, resourceFile, rawCss) => {
const scopedName = generateScopedName(exportName, resourceFile, rawCss)
localClasses.push(scopedName)

View File

@ -12,8 +12,6 @@ import type {
RollupError,
SourceMapInput,
} from 'rollup'
import type { CSSModuleData, CssModuleToEsmResult } from '@vitejs/css-modules'
import type { CompileResult as CompileCSSModuleResult } from '@vitejs/css-modules/postcss'
import colors from 'picocolors'
import MagicString from 'magic-string'
import type * as PostCSS from 'postcss'
@ -48,6 +46,7 @@ import {
arraify,
asyncReplace,
combineSourcemaps,
createCachedImport,
createSerialPromiseQueue,
emptyCssComments,
generateCodeFrame,
@ -81,7 +80,7 @@ import {
} from './asset'
import type { ESBuildOptions } from './esbuild'
import { getChunkOriginalFileName } from './manifest'
import type { TransformResult } from 'lightningcss'
import { cssModulesCache } from './cssModules'
// const debug = createDebugger('vite:css')
@ -210,7 +209,7 @@ const enum PureCssLang {
const enum PostCssDialectLang {
sss = 'sugarss',
}
type CssLang =
export type CssLang =
| keyof typeof PureCssLang
| keyof typeof PreprocessLang
| keyof typeof PostCssDialectLang
@ -227,18 +226,6 @@ export const isDirectCSSRequest = (request: string): boolean =>
export const isDirectRequest = (request: string): boolean =>
directRequestRE.test(request)
interface CSSModuleMetadata extends CSSModuleData {
/**
* Metadata after finishing processing a CSS module file
*/
exportsMetadata?: CssModuleToEsmResult['exportsMetadata']
}
const cssModulesCache = new WeakMap<
ResolvedConfig,
Map<string, CSSModuleMetadata>
>()
export const removedPureCssFilesCache = new WeakMap<
ResolvedConfig,
Map<string, RenderedChunk>
@ -260,7 +247,6 @@ const cssUrlAssetRE = /__VITE_CSS_URL__([\da-f]+)__/g
*/
export function cssPlugin(config: ResolvedConfig): Plugin {
const isBuild = config.command === 'build'
let moduleCache: Map<string, CSSModuleMetadata>
const resolveUrl = config.createResolver({
preferRelative: true,
@ -279,10 +265,6 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
name: 'vite:css',
buildStart() {
// Ensure a new cache for every build (i.e. rebuilding in watch mode)
moduleCache = new Map<string, CSSModuleMetadata>()
cssModulesCache.set(config, moduleCache)
removedPureCssFilesCache.set(config, new Map<string, RenderedChunk>())
preprocessorWorkerController = createPreprocessorWorkerController(
@ -367,7 +349,6 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
const {
code: css,
modules,
deps,
map,
} = await compileCSS(
@ -377,9 +358,6 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
preprocessorWorkerController!,
urlReplacer,
)
if (modules) {
moduleCache.set(id, modules)
}
if (deps) {
for (const file of deps) {
@ -482,51 +460,11 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
// #6984, #7552
// `foo.module.css` => modulesCode
// `foo.module.css?inline` => cssContent
const modulesCode =
modules &&
!inlined &&
(await (async () => {
const { cssModuleToEsm } = await importCssModules()
const atImportResolvers = getAtImportResolvers(config)
const result = await cssModuleToEsm(
{
css,
id,
exports: modules.exports,
references: modules.references,
resolve: async (id, importer) => {
for (const key of getCssResolversKeys(atImportResolvers)) {
const resolved = await atImportResolvers[key](id, importer)
if (resolved) {
return path.resolve(resolved)
}
}
return id
},
loadExports: async (resolvedId) => {
await this.load({ id: resolvedId })
const modules = cssModulesCache.get(config)!.get(resolvedId)
if (!modules || !modules.exportsMetadata) {
throw new Error(
`Failed to find exports from ${JSON.stringify(resolvedId)}`,
)
}
return modules.exportsMetadata
},
},
config.css.transformer === 'postcss' && config.css.modules !== false
? config.css.modules
: undefined,
)
css = result.css
// Save metadata to the modules cache so other CSS modules' `loadExports` can use it
modules.exportsMetadata = result.exportsMetadata
return result.code
})())
let modulesCode: string | undefined
if (modules && !inlined) {
css = modules.css
modulesCode = modules.code
}
if (config.command === 'serve') {
const getContentWithSourcemap = async (content: string) => {
@ -1136,12 +1074,6 @@ function createCSSResolvers(config: ResolvedConfig): CSSAtImportResolvers {
}
}
function getCssResolversKeys(
resolvers: CSSAtImportResolvers,
): Array<keyof CSSAtImportResolvers> {
return Object.keys(resolvers) as unknown as Array<keyof CSSAtImportResolvers>
}
async function compileCSSPreprocessors(
id: string,
lang: PreprocessLang,
@ -1213,7 +1145,9 @@ const configToAtImportResolvers = new WeakMap<
ResolvedConfig,
CSSAtImportResolvers
>()
function getAtImportResolvers(config: ResolvedConfig) {
export function getAtImportResolvers(
config: ResolvedConfig,
): CSSAtImportResolvers {
let atImportResolvers = configToAtImportResolvers.get(config)
if (!atImportResolvers) {
atImportResolvers = createCSSResolvers(config)
@ -1232,7 +1166,6 @@ async function compileCSS(
code: string
map?: SourceMapInput
ast?: PostCSS.Result
modules?: CSSModuleMetadata
deps?: Set<string>
}> {
if (config.css?.transformer === 'lightningcss') {
@ -1428,22 +1361,11 @@ async function compileCSS(
throw e
}
let moduleResult: CompileCSSModuleResult | undefined
if (isModule) {
const { compileCSSModule } = await importCssModulesPostcss()
moduleResult = await compileCSSModule(
postcssResult.css,
removeDirectQuery(id),
{ ...modulesOptions, sourcemap: devSourcemap },
)
}
if (!devSourcemap) {
return {
ast: postcssResult,
code: moduleResult?.css ?? postcssResult.css,
code: postcssResult.css,
map: { mappings: '' },
modules: moduleResult?.data,
deps,
}
}
@ -1461,41 +1383,16 @@ async function compileCSS(
map = combineSourcemapsIfExists(cleanUrl(id), postcssMap, preprocessorMap)
if (moduleResult?.map) {
map = combineSourcemapsIfExists(
cleanUrl(id),
map,
moduleResult.map as ExistingRawSourceMap,
)
}
return {
ast: postcssResult,
code: moduleResult?.css ?? postcssResult.css,
code: postcssResult.css,
map,
modules: moduleResult?.data,
deps,
}
}
function createCachedImport<T>(imp: () => Promise<T>): () => T | Promise<T> {
let cached: T | Promise<T>
return () => {
if (!cached) {
cached = imp().then((module) => {
cached = module
return module
})
}
return cached
}
}
const importPostcssImport = createCachedImport(() => import('postcss-import'))
const importPostcss = createCachedImport(() => import('postcss'))
const importCssModules = createCachedImport(() => import('@vitejs/css-modules'))
const importCssModulesPostcss = createCachedImport(
() => import('@vitejs/css-modules/postcss'),
)
const preprocessorWorkerControllerCache = new WeakMap<
ResolvedConfig,
@ -1508,7 +1405,6 @@ let alwaysFakeWorkerWorkerControllerCache:
export interface PreprocessCSSResult {
code: string
map?: SourceMapInput
modules?: CSSModuleMetadata
deps?: Set<string>
}
@ -2813,25 +2709,10 @@ async function compileLightningCSS(
}
}
let modules: CSSModuleMetadata | undefined
if (!isStyleAttr && 'exports' in res && res.exports) {
const resolved = res as TransformResult
// https://github.com/parcel-bundler/lightningcss/issues/291
const exports = Object.fromEntries(
Object.entries(resolved.exports!).sort(
// Cheap alphabetical sort (localCompare is expensive)
([a], [b]) => (a < b ? -1 : a > b ? 1 : 0),
),
)
modules = { exports, references: resolved.references }
}
return {
code: css,
map: 'map' in res ? res.map?.toString() : undefined,
deps,
modules,
}
}

View File

@ -1,9 +1,149 @@
import type{ Plugin } from "../plugin";
import type { ResolvedConfig } from "../config";
import { cssModuleToEsm } from '@vitejs/css-modules'
import type { CSSModuleData, CssModuleToEsmResult } from '@vitejs/css-modules'
import type { SourceMapInput } from 'rollup'
import type { Plugin } from '../plugin'
import type { ResolvedConfig } from '../config'
import { createCachedImport, removeDirectQuery } from '../utils'
import { CSS_LANGS_RE } from '../constants'
import { type CssLang, getAtImportResolvers, isModuleCSSRequest } from './css'
interface CSSModuleMetadata {
/**
* Transformed CSS
*/
css: string
/**
* JS code for CSS module
*/
code: string
data: CSSModuleData
/**
* Metadata after finishing processing a CSS module file
*/
exportsMetadata?: CssModuleToEsmResult['exportsMetadata']
}
export const cssModulesCache = new WeakMap<
ResolvedConfig,
Map<string, CSSModuleMetadata>
>()
// export function getCSSModuleResult(
// id: string,
// config: ResolvedConfig,
// ): Pick<CSSModuleMetadata, 'css' | 'code'> | undefined {
// const cache = cssModulesCache.get(config)?.get(id)
// if (cache) {
// return { css: cache.css, code: cache.code }
// }
// }
export function cssModulesPlugin(config: ResolvedConfig): Plugin {
let moduleCache: Map<string, CSSModuleMetadata>
return {
name: 'vite:css-modules',
buildStart() {
// Ensure a new cache for every build (i.e. rebuilding in watch mode)
moduleCache = new Map<string, CSSModuleMetadata>()
cssModulesCache.set(config, moduleCache)
},
async transform(css, id) {
if (!isModuleCSSRequest(id)) return
const { css: newCss, map, data } = await compileCSSModule(css, id, config)
const lang = id.match(CSS_LANGS_RE)?.[1] as CssLang | undefined
const atImportResolvers = getAtImportResolvers(config)
const resolver =
lang === 'sass' || lang === 'scss'
? atImportResolvers.sass
: lang === 'less'
? atImportResolvers.less
: atImportResolvers.css
const result = await cssModuleToEsm(
{
css: newCss,
id,
exports: data.exports,
references: data.references,
resolve: async (id, importer) => {
return (await resolver(id, importer)) ?? id
},
loadExports: async (resolvedId) => {
await this.load({ id: resolvedId })
const modules = cssModulesCache.get(config)!.get(resolvedId)
if (!modules || !modules.exportsMetadata) {
throw new Error(
`Failed to find exports from ${JSON.stringify(resolvedId)}`,
)
}
return modules.exportsMetadata
},
},
config.css.transformer === 'postcss'
? config.css.modules || undefined
: undefined,
)
moduleCache.set(id, {
css: result.css,
code: result.code,
data,
exportsMetadata: result.exportsMetadata,
})
return {
code: result.css,
map,
}
},
}
}
}
const importCssModulesPostcss = createCachedImport(
() => import('@vitejs/css-modules/postcss'),
)
const importCssModulesLightningcss = createCachedImport(
() => import('@vitejs/css-modules/lightningcss'),
)
async function compileCSSModule(
css: string,
id: string,
config: ResolvedConfig,
): Promise<{
css: string
map?: SourceMapInput
data: CSSModuleData
}> {
id = removeDirectQuery(id)
if (config.css?.transformer === 'lightningcss') {
const { compileCSSModule: compile } = await importCssModulesLightningcss()
const result = await compile(css, id, {
cssModules: config.css.lightningcss?.cssModules,
sourcemap: config.css.devSourcemap,
})
return {
css: result.css,
map: result.map as SourceMapInput,
data: result.data,
}
} else {
const { compileCSSModule: compile } = await importCssModulesPostcss()
const result = await compile(css, id, {
cssModules: config.css.modules || {},
sourcemap: config.css.devSourcemap,
})
return {
css: result.css,
map: result.map as SourceMapInput,
data: result.data,
}
}
}

View File

@ -90,7 +90,7 @@ export async function resolvePlugins(
...normalPlugins,
wasmFallbackPlugin(),
definePlugin(config),
cssModulesPlugin(config),
config.css.modules !== false ? cssModulesPlugin(config) : null,
cssPostPlugin(config),
isBuild && buildHtmlPlugin(config),
workerImportMetaUrlPlugin(config),

View File

@ -1424,3 +1424,18 @@ export function displayTime(time: number): string {
export function partialEncodeURI(uri: string): string {
return uri.replaceAll('%', '%25')
}
export function createCachedImport<T>(
imp: () => Promise<T>,
): () => T | Promise<T> {
let cached: T | Promise<T>
return () => {
if (!cached) {
cached = imp().then((module) => {
cached = module
return module
})
}
return cached
}
}

View File

@ -253,6 +253,9 @@ importers:
'@types/postcss-modules-values':
specifier: ^4.0.2
version: 4.0.2
lightningcss:
specifier: ^1.24.1
version: 1.24.1
postcss:
specifier: ^8.4.35
version: 8.4.36