diff --git a/packages/@tailwindcss-node/package.json b/packages/@tailwindcss-node/package.json index 5fc6727ee..a7c867013 100644 --- a/packages/@tailwindcss-node/package.json +++ b/packages/@tailwindcss-node/package.json @@ -37,9 +37,11 @@ } }, "dependencies": { + "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "catalog:", + "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "workspace:*" } diff --git a/packages/@tailwindcss-node/src/optimize.ts b/packages/@tailwindcss-node/src/optimize.ts index 168b3f832..9ebffae98 100644 --- a/packages/@tailwindcss-node/src/optimize.ts +++ b/packages/@tailwindcss-node/src/optimize.ts @@ -1,4 +1,6 @@ +import remapping from '@ampproject/remapping' import { Features, transform } from 'lightningcss' +import MagicString from 'magic-string' export interface OptimizeOptions { /** @@ -10,22 +12,31 @@ export interface OptimizeOptions { * Enabled minified output */ minify?: boolean + + /** + * The output source map before optimization + * + * If omitted an resulting source map will not be available + */ + map?: string } export interface TransformResult { code: string + map: string | undefined } export function optimize( input: string, - { file = 'input.css', minify = false }: OptimizeOptions = {}, + { file = 'input.css', minify = false, map }: OptimizeOptions = {}, ): TransformResult { - function optimize(code: Buffer | Uint8Array) { + function optimize(code: Buffer | Uint8Array, map: string | undefined) { return transform({ filename: file, code, minify, - sourceMap: false, + sourceMap: typeof map !== 'undefined', + inputSourceMap: map, drafts: { customMedia: true, }, @@ -46,13 +57,34 @@ export function optimize( // Running Lightning CSS twice to ensure that adjacent rules are merged after // nesting is applied. This creates a more optimized output. - let result = optimize(Buffer.from(input)) - result = optimize(result.code) + let result = optimize(Buffer.from(input), map) + map = result.map?.toString() + + result = optimize(result.code, map) + map = result.map?.toString() let code = result.code.toString() - code = code.replaceAll('@media not (', '@media not all and (') + + // Work around an issue where the media query range syntax transpilation + // generates code that is invalid with `@media` queries level 3. + let magic = new MagicString(code) + magic.replaceAll('@media not (', '@media not all and (') + + // We have to use a source-map-preserving method of replacing the content + // which requires the use of Magic String + remapping(…) to make sure + // the resulting map is correct + if (map !== undefined && magic.hasChanged()) { + let magicMap = magic.generateMap({ source: 'original', hires: 'boundary' }).toString() + + let remapped = remapping([magicMap, map], () => null) + + map = remapped.toString() + } + + code = magic.toString() return { code, + map, } }