Prep for source maps in build tools

This commit is contained in:
Jordan Pittman 2025-04-29 19:18:59 -04:00
parent a0a9a33952
commit b191165f44
9 changed files with 102 additions and 18 deletions

View file

@ -127,14 +127,14 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
if (args['--minify'] || args['--optimize']) {
if (css !== previous.css) {
DEBUG && I.start('Optimize CSS')
let optimizedCss = optimize(css, {
let optimized = optimize(css, {
file: args['--input'] ?? 'input.css',
minify: args['--minify'] ?? false,
})
DEBUG && I.end('Optimize CSS')
previous.css = css
previous.optimizedCss = optimizedCss
output = optimizedCss
previous.optimizedCss = optimized.code
output = optimized.code
} else {
output = previous.optimizedCss
}

View file

@ -40,6 +40,7 @@
"enhanced-resolve": "^5.18.1",
"jiti": "^2.4.2",
"lightningcss": "catalog:",
"source-map-js": "^1.2.1",
"tailwindcss": "workspace:*"
}
}

View file

@ -5,6 +5,7 @@ export * from './compile'
export * from './instrumentation'
export * from './normalize-path'
export * from './optimize'
export * from './source-maps'
export { env }
// In Bun, ESM modules will also populate `require.cache`, so the module hook is

View file

@ -5,6 +5,7 @@ export * from './compile'
export * from './instrumentation'
export * from './normalize-path'
export * from './optimize'
export * from './source-maps'
export { env }
// In Bun, ESM modules will also populate `require.cache`, so the module hook is

View file

@ -1,13 +1,29 @@
import { Features, transform } from 'lightningcss'
export interface OptimizeOptions {
/**
* The file being transformed
*/
file?: string
/**
* Enabled minified output
*/
minify?: boolean
}
export interface TransformResult {
code: string
}
export function optimize(
input: string,
{ file = 'input.css', minify = false }: { file?: string; minify?: boolean } = {},
): string {
{ file = 'input.css', minify = false }: OptimizeOptions = {},
): TransformResult {
function optimize(code: Buffer | Uint8Array) {
return transform({
filename: file,
code: code as any,
code,
minify,
sourceMap: false,
drafts: {
@ -25,16 +41,18 @@ export function optimize(
chrome: 111 << 16,
},
errorRecovery: true,
}).code
})
}
// Running Lightning CSS twice to ensure that adjacent rules are merged after
// nesting is applied. This creates a more optimized output.
let out = optimize(optimize(Buffer.from(input))).toString()
let result = optimize(Buffer.from(input))
result = optimize(result.code)
// Work around an issue where the media query range syntax transpilation
// generates code that is invalid with `@media` queries level 3.
out = out.replaceAll('@media not (', '@media not all and (')
let code = result.code.toString()
code = code.replaceAll('@media not (', '@media not all and (')
return out
return {
code,
}
}

View file

@ -0,0 +1,59 @@
import { SourceMapGenerator } from 'source-map-js'
import type { DecodedSource, DecodedSourceMap } from '../../tailwindcss/src/source-maps/source-map'
import { DefaultMap } from '../../tailwindcss/src/utils/default-map'
export type { DecodedSource, DecodedSourceMap }
export interface SourceMap {
readonly raw: string
readonly inline: string
}
function serializeSourceMap(map: DecodedSourceMap): string {
let generator = new SourceMapGenerator()
let id = 1
let sourceTable = new DefaultMap<
DecodedSource | null,
{
url: string
content: string
}
>((src) => {
return {
url: src?.url ?? `<unknown ${id}>`,
content: src?.content ?? '<none>',
}
})
for (let mapping of map.mappings) {
let original = sourceTable.get(mapping.originalPosition?.source ?? null)
generator.addMapping({
generated: mapping.generatedPosition,
original: mapping.originalPosition,
source: original.url,
name: mapping.name,
})
generator.setSourceContent(original.url, original.content)
}
return generator.toString()
}
export function toSourceMap(map: DecodedSourceMap | string): SourceMap {
let raw = typeof map === 'string' ? map : serializeSourceMap(map)
return {
raw,
get inline() {
let tmp = ''
tmp += '/*# sourceMappingURL=data:application/json;base64,'
tmp += Buffer.from(raw, 'utf-8').toString('base64')
tmp += ' */\n'
return tmp
},
}
}

View file

@ -282,13 +282,13 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
DEBUG && I.end('AST -> CSS')
DEBUG && I.start('Lightning CSS')
let ast = optimizeCss(css, {
let optimized = optimizeCss(css, {
minify: typeof optimize === 'object' ? optimize.minify : true,
})
DEBUG && I.end('Lightning CSS')
DEBUG && I.start('CSS -> PostCSS AST')
context.optimizedPostCssAst = postcss.parse(ast, result.opts)
context.optimizedPostCssAst = postcss.parse(optimized.code, result.opts)
DEBUG && I.end('CSS -> PostCSS AST')
DEBUG && I.end('Optimization')

View file

@ -106,7 +106,9 @@ export default function tailwindcss(): Plugin[] {
DEBUG && I.end('[@tailwindcss/vite] Generate CSS (build)')
DEBUG && I.start('[@tailwindcss/vite] Optimize CSS')
result.code = optimize(result.code, { minify })
result = optimize(result.code, {
minify,
})
DEBUG && I.end('[@tailwindcss/vite] Optimize CSS')
return result

View file

@ -7,12 +7,14 @@ export async function compileCss(
options: Parameters<typeof compile>[1] = {},
) {
let { build } = await compile(css, options)
return optimize(build(candidates)).trim()
return optimize(build(candidates)).code.trim()
}
export async function run(candidates: string[]) {
let { build } = await compile('@tailwind utilities;')
return optimize(build(candidates)).trim()
return optimize(build(candidates)).code.trim()
}
export const optimizeCss = optimize
export function optimizeCss(input: string) {
return optimize(input).code
}