Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
thecrypticace committed Apr 17, 2024
1 parent 5036ff1 commit 4059f5e
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 89 deletions.
1 change: 1 addition & 0 deletions packages/@tailwindcss-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"access": "public"
},
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"@parcel/watcher": "^2.4.1",
"@tailwindcss/oxide": "workspace:^",
"lightningcss": "^1.24.0",
Expand Down
68 changes: 44 additions & 24 deletions packages/@tailwindcss-cli/src/commands/build/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import remapping from '@ampproject/remapping'
import watcher from '@parcel/watcher'
import { IO, Parsing, scanDir, scanFiles, type ChangedContent } from '@tailwindcss/oxide'
import { Features, transform } from 'lightningcss'
Expand Down Expand Up @@ -117,21 +118,15 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
@import '${resolve('tailwindcss/index.css')}';
`

let magic = new MagicString(source)

let inputMap = sourcemapType
? (JSON.parse(
magic
.generateMap({ source: 'input.css', hires: 'boundary', includeContent: true })
.toString(),
) as RawSourceMap)
: null
let magic = new MagicString(source, {
filename: 'source.css',
})

// Resolve the input
let [input, cssImportPaths, intermediateMap] = await handleImports(
let [input, cssImportPaths, importedMap] = await handleImports(
source,
args['--input'] ?? base,
inputMap,
sourcemapType ? true : false,
)

let previous = {
Expand All @@ -142,19 +137,43 @@ export async function handle(args: Result<ReturnType<typeof options>>) {

async function write(
css: string,
map: RawSourceMap | null,
tailwindMap: RawSourceMap | null,
args: Result<ReturnType<typeof options>>,
) {
let output = css
let outputMap = map

let combinedMap = tailwindMap
? remapping(tailwindMap as any, (file) => {
if (file === 'input.css') {
return importedMap
}

if (file === 'imported.css') {
return Object.assign(
magic.generateDecodedMap({
source: 'source.css',
hires: 'boundary',
includeContent: true,
}),
{
version: 3 as const,
},
) as any
}

return null
})
: null

let outputMap = combinedMap

// Optimize the output
if (args['--minify'] || args['--optimize']) {
if (css !== previous.css) {
let { css: optimizedCss, map: optimizedMap } = optimizeCss(css, {
file: args['--input'] ?? 'input.css',
minify: args['--minify'] ?? false,
map,
map: combinedMap,
})
previous.css = css
previous.optimizedCss = optimizedCss
Expand Down Expand Up @@ -183,7 +202,7 @@ export async function handle(args: Result<ReturnType<typeof options>>) {

// Compile the input
let { build, buildSourceMap } = compile(input, {
map: intermediateMap ?? undefined,
map: sourcemapType ? true : false,
})

let outputCss = build(candidates)
Expand Down Expand Up @@ -252,10 +271,11 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
@import '${resolve('tailwindcss/index.css')}';
`,
args['--input'] ?? base,
null,
sourcemapType ? true : false,
)

build = compile(input).build
let compiler = compile(input, { map: sourcemapType ? true : false })
;[build, buildSourceMap] = [compiler.build, compiler.buildSourceMap]
compiledCss = build(candidates)
compiledMap = sourcemapType ? buildSourceMap() : null
}
Expand Down Expand Up @@ -294,33 +314,33 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
}
}

type ImportResult = [css: string, paths: string[], map: RawSourceMap | null]

function handleImports(
input: string,
file: string,
inputMap: RawSourceMap | null,
):
| [css: string, paths: string[], map: RawSourceMap | null]
| Promise<[css: string, paths: string[], map: RawSourceMap | null]> {
map: boolean,
): ImportResult | Promise<ImportResult> {
// TODO: Should we implement this ourselves instead of relying on PostCSS?
//
// Relevant specification:
// - CSS Import Resolve: https://csstools.github.io/css-import-resolve/

if (!input.includes('@import')) {
return [input, [file], inputMap ?? null]
return [input, [file], null]
}

return postcss()
.use(atImport())
.process(input, {
from: file,
map: inputMap
map: map
? {
// We do not want an inline source map because we'll have to parse it back out
inline: false,

// Pass in the map we generated earlier using MagicString
prev: inputMap,
prev: false,

// We want source data to be included in the resulting map
sourcesContent: true,
Expand Down
14 changes: 14 additions & 0 deletions packages/@tailwindcss-vite/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,20 @@ export default function tailwindcss(): Plugin[] {

let { css, map } = generateCssWithMap(src, inputMap)

await import('fs/promises').then(async ({ writeFile }) => {
function attachInlineMap(source: string, map: any) {
return (
source +
`\n/*# sourceMappingURL=data:application/json;base64,` +
Buffer.from(JSON.stringify(map)).toString('base64') +
' */'
)
}

await writeFile(`input.css`, attachInlineMap(src, inputMap))
await writeFile(`generated.css`, attachInlineMap(css, map))
})

css = await transformWithPlugins(this, id, css)

return {
Expand Down
18 changes: 7 additions & 11 deletions packages/tailwindcss/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SourceMapConsumer, type RawSourceMap } from 'source-map-js'
import { type RawSourceMap } from 'source-map-js'

import { version } from '../package.json'
import { WalkAction, comment, decl, rule, toCss, walk, type AstNode, type Rule } from './ast'
Expand All @@ -10,12 +10,12 @@ import { Theme } from './theme'

export function compile(
css: string,
{ map: rawMap }: { map?: RawSourceMap } = {},
{ map: shouldGenerateMap }: { map?: boolean } = {},
): {
build(candidates: string[]): string
buildSourceMap(): RawSourceMap
} {
let ast = CSS.parse(css, { trackSource: !!rawMap })
let ast = CSS.parse(css, { trackSource: shouldGenerateMap })

if (process.env.NODE_ENV !== 'test') {
ast.unshift(comment(`! tailwindcss v${version} | MIT License | https://tailwindcss.com `))
Expand Down Expand Up @@ -191,13 +191,11 @@ export function compile(
})
}

let originalMap: SourceMapConsumer | undefined

// Track all valid candidates, these are the incoming `rawCandidate` that
// resulted in a generated AST Node. All the other `rawCandidates` are invalid
// and should be ignored.
let allValidCandidates = new Set<string>()
let compiledCss = toCss(ast, { trackDestination: !!rawMap })
let compiledCss = toCss(ast, { trackDestination: shouldGenerateMap })
let previousAstNodeCount = 0

return {
Expand Down Expand Up @@ -238,19 +236,17 @@ export function compile(
previousAstNodeCount = newNodes.length

tailwindUtilitiesNode.nodes = newNodes
compiledCss = toCss(ast, { trackDestination: !!rawMap })
compiledCss = toCss(ast, { trackDestination: shouldGenerateMap })
}

return compiledCss
},
buildSourceMap() {
if (!rawMap) {
if (!shouldGenerateMap) {
throw new Error('buildSourceMap called without passing a source map to compile')
}

originalMap ??= new SourceMapConsumer(rawMap)

return toSourceMap(originalMap, ast)!
return toSourceMap(css, ast)!
},
}
}
Expand Down
65 changes: 25 additions & 40 deletions packages/tailwindcss/src/source-map.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,40 @@
import { SourceMapConsumer, SourceMapGenerator, type RawSourceMap } from 'source-map-js'
import { SourceMapGenerator, type RawSourceMap } from 'source-map-js'
import { walk, type AstNode } from './ast'

export function toSourceMap(original: SourceMapConsumer, ast: AstNode[]): RawSourceMap {
// Record all existing sources
let existingSources = new Set<string>()

original.eachMapping((mapping) => {
if (mapping.source === null) return
existingSources.add(mapping.source)
})

// Add the source content from the existing sources
/**
* Build a source map from the given AST.
*
*
* Our AST is built from a flat CSS string so we only ever have a single source
*
* Rather than take an input source map we require the use of other tools that
* can combine source maps. This simmplifies the implementation and allows us to
* focus on generating mappings for the AST without having to worry about
* referencing a previous source map.
*
* Additionally, other tools might expect a source map to be "local" to the
* passed in source rather than be combined as they handle that themselves.
**/
export function toSourceMap(source: string, ast: AstNode[]): RawSourceMap {
let map = new SourceMapGenerator()
for (let source of existingSources) {
let sourceContent = original.sourceContentFor(source, true)
if (sourceContent == null) continue
map.setSourceContent(source, sourceContent)
}
map.setSourceContent('input.css', source)

walk(ast, (node) => {
let { source, destination } = node

if (!source || !destination) return

let start = original.originalPositionFor({
line: source.start.line,
column: source.start.column,
bias: SourceMapConsumer.GREATEST_LOWER_BOUND,
map.addMapping({
source: 'input.css',
original: { line: source.start.line, column: source.start.column },
generated: { line: destination.start.line, column: destination.start.column },
})

let end = original.originalPositionFor({
line: source.end.line,
column: source.end.column,
bias: SourceMapConsumer.LEAST_UPPER_BOUND,
map.addMapping({
source: 'input.css',
original: { line: source.end.line, column: source.end.column },
generated: { line: destination.end.line, column: destination.end.column },
})

if (start.line !== null && start.column !== null) {
map.addMapping({
source: start.source,
original: start,
generated: { line: destination.start.line, column: destination.start.column },
})
}

if (end.line !== null && end.column !== null) {
map.addMapping({
source: end.source,
original: end,
generated: { line: destination.end.line, column: destination.end.column },
})
}
})

return JSON.parse(map.toString()) as RawSourceMap
Expand Down
41 changes: 27 additions & 14 deletions packages/tailwindcss/src/source-maps.test.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,38 @@
import MagicString from 'magic-string'
import remapping from '@ampproject/remapping'
import MagicString, { Bundle } from 'magic-string'
import { SourceMapConsumer, type RawSourceMap } from 'source-map-js'
import { expect, test } from 'vitest'
import { compile } from '.'

function run(rawCss: string, candidates: string[] = []) {
let str = new MagicString(rawCss)
let originalMap: RawSourceMap = JSON.parse(
str
.generateMap({
hires: 'boundary',
source: 'input.css',
includeContent: true,
})
.toString(),
let source = new MagicString(rawCss)

let bundle = new Bundle()

bundle.addSource({
filename: 'source.css',
content: source,
})

let originalMap = Object.assign(
bundle.generateDecodedMap({
hires: 'boundary',
file: 'source.css.map',
includeContent: true,
}),
{
version: 3 as const,
},
)

let compiler = compile(str.toString(), { map: originalMap })
let compiler = compile(source.toString(), { map: true })

let css = compiler.build(candidates)
let map = compiler.buildSourceMap()
let sources = map.sources

let combined = remapping([map as any, originalMap], () => null)

let sources = combined.sources
let annotations = annotatedMappings(map)

return { css, map, sources, annotations }
Expand All @@ -33,7 +46,7 @@ test('utilities have source maps pointing to the utilities node', async () => {

// All CSS generated by Tailwind CSS should be annotated with source maps
// And always be able to point to the original source file
expect(sources).toEqual(['input.css'])
expect(sources).toEqual(['source.css'])
expect(sources.length).toBe(1)

expect(annotations).toEqual([
Expand All @@ -53,7 +66,7 @@ test('@apply generates source maps', async () => {

// All CSS generated by Tailwind CSS should be annotated with source maps
// And always be able to point to the original source file
expect(sources).toEqual(['input.css'])
expect(sources).toEqual(['source.css'])
expect(sources.length).toBe(1)

expect(annotations).toEqual([
Expand Down

0 comments on commit 4059f5e

Please sign in to comment.