Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
thecrypticace committed Apr 26, 2024
1 parent 6d76a9a commit 6e8474a
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 29 deletions.
13 changes: 9 additions & 4 deletions packages/tailwindcss/src/ast.ts
@@ -1,5 +1,8 @@
export type Location = {
/** The line number for this location, one-based */
line: number

/** The column number for this location, one-based */
column: number
}

Expand Down Expand Up @@ -113,7 +116,7 @@ function span(value: string, location: Location) {
let line = location.line
let column = location.column

let start = { line, column }
let start = { line: line + 1, column: column + 1 }

for (let i = 0; i < value.length; ++i) {
if (value.charCodeAt(i) === 0x0a) {
Expand All @@ -126,7 +129,7 @@ function span(value: string, location: Location) {
}
}

let end = { line, column }
let end = { line: line + 1, column: column + 1 }

location.line = line
location.column = column
Expand Down Expand Up @@ -213,11 +216,13 @@ export function toCss(ast: AstNode[], track?: boolean) {

let head = ''
head += node.selector
head += ' {'
head += ' '

css += head

node.destination = track ? [span(head, location)] : []

css += '{'
css += '\n'
location.line += 1
location.column = 0
Expand Down Expand Up @@ -291,7 +296,7 @@ export function toCss(ast: AstNode[], track?: boolean) {
return ''
}

let location = { line: 1, column: 0 }
let location = { line: 0, column: 0 }
let css = ''

css += stringifyAll(ast, location)
Expand Down
37 changes: 26 additions & 11 deletions packages/tailwindcss/src/css-parser.ts
Expand Up @@ -37,15 +37,16 @@ export function parse(input: string, track?: boolean) {
let peekChar

// The current line number
let line = 1
let line = 0

// The index of the first non-whitespace character on the current line
let lineStart = 0

// Source location tracking
let sourceStartLine = 1
// The are 0-indexed instead of 1-indexed to simplify the math
let sourceStartLine = 0
let sourceStartColumn = 0
let sourceEndLine = 1
let sourceEndLine = 0
let sourceEndColumn = 0

function sourceRange() {
Expand All @@ -54,12 +55,12 @@ export function parse(input: string, track?: boolean) {
return [
{
start: {
line: sourceStartLine,
column: sourceStartColumn,
line: sourceStartLine + 1,
column: sourceStartColumn + 1,
},
end: {
line: sourceEndLine,
column: sourceEndColumn,
line: sourceEndLine + 1,
column: sourceEndColumn + 1,
},
},
]
Expand Down Expand Up @@ -114,6 +115,8 @@ export function parse(input: string, track?: boolean) {
else if (currentChar === SLASH && input.charCodeAt(i + 1) === ASTERISK) {
let start = i

// TODO: Track source ranges

for (let j = i + 2; j < input.length; j++) {
peekChar = input.charCodeAt(j)

Expand Down Expand Up @@ -149,6 +152,16 @@ export function parse(input: string, track?: boolean) {
if (commentString.charCodeAt(2) === EXCLAMATION_MARK) {
let node = comment(commentString.slice(2, -2))
licenseComments.push(node)

for (let i = 0; i < commentString.length; ++i) {
if (commentString.charCodeAt(i) === LINE_BREAK) {
sourceEndLine += 1
sourceEndColumn = 0
} else {
sourceEndColumn += 1
}
}

node.source = sourceRange()!
}
}
Expand Down Expand Up @@ -394,15 +407,15 @@ export function parse(input: string, track?: boolean) {
}

// Track the source location for source maps
sourceEndLine = line
sourceEndColumn = i - lineStart
node.source = sourceRange()!

// Reset the state for the next node.
buffer = ''
node = null
sourceStartLine = line
sourceStartColumn = i - lineStart
sourceEndLine = line
sourceEndColumn = i - lineStart
}

// End of a declaration.
Expand All @@ -425,13 +438,13 @@ export function parse(input: string, track?: boolean) {
}

// Track the source location for source maps
sourceEndLine = line
sourceEndColumn = i - lineStart
declaration.source = sourceRange()!

buffer = ''
sourceStartLine = line
sourceStartColumn = i - lineStart
sourceEndLine = line
sourceEndColumn = i - lineStart
}

// Start of a block.
Expand All @@ -455,6 +468,8 @@ export function parse(input: string, track?: boolean) {
parent = node

// Track the source location for source maps
sourceEndLine = line
sourceEndColumn = i - lineStart
node.source = sourceRange()!

// Reset the state for the next node.
Expand Down
6 changes: 3 additions & 3 deletions packages/tailwindcss/src/index.ts
Expand Up @@ -15,7 +15,7 @@ export function compile(
build(candidates: string[]): string
buildSourceMap(): RawSourceMap
} {
let ast = CSS.parse(css, { trackSource: shouldGenerateMap })
let ast = CSS.parse(css, true)

if (process.env.NODE_ENV !== 'test') {
ast.unshift(comment(`! tailwindcss v${version} | MIT License | https://tailwindcss.com `))
Expand Down Expand Up @@ -195,7 +195,7 @@ export function compile(
// 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: shouldGenerateMap })
let compiledCss = toCss(ast, true)
let previousAstNodeCount = 0

return {
Expand Down Expand Up @@ -237,7 +237,7 @@ export function compile(
previousAstNodeCount = newNodes.length

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

return compiledCss
Expand Down
62 changes: 51 additions & 11 deletions packages/tailwindcss/src/source-maps.test.ts
Expand Up @@ -3,6 +3,8 @@ import MagicString, { Bundle } from 'magic-string'
import { SourceMapConsumer, type RawSourceMap } from 'source-map-js'
import { expect, test } from 'vitest'
import { compile } from '.'
import { toCss, type Range } from './ast'
import * as CSS from './css-parser'

function run(rawCss: string, candidates: string[] = []) {
let source = new MagicString(rawCss)
Expand Down Expand Up @@ -38,6 +40,29 @@ function run(rawCss: string, candidates: string[] = []) {
return { css, map, sources, annotations }
}

test('source locations are tracked during parsing and serializing', async () => {
let ast = CSS.parse(`.foo { color: red; }`, true)
toCss(ast, true)

if (ast[0].kind !== 'rule') throw new Error('Expected a rule')

let src = annotatedLocations({
selector: ast[0].source[0],
decl: ast[0].nodes[0].source[0],
})

let dst = annotatedLocations({
selector: ast[0].destination[0],
decl: ast[0].nodes[0].destination[0],
})

expect(src.selector).toEqual('1:1-1:6')
expect(dst.selector).toEqual('1:1-1:6')

expect(src.decl).toEqual('1:8-1:18')
expect(dst.decl).toEqual('2:3-2:14')
})

test('utilities have source maps pointing to the utilities node', async () => {
let { sources, annotations } = run(`@tailwind utilities;`, [
//
Expand All @@ -51,8 +76,8 @@ test('utilities have source maps pointing to the utilities node', async () => {

expect(annotations).toEqual([
//
'1:0-12 <- 1:0',
'2:2-34 <- 1:0',
'1:1-12 <- 1:1-20',
'2:3-35 <- 1:1-20',
])
})

Expand All @@ -69,14 +94,15 @@ test('@apply generates source maps', async () => {
expect(sources).toEqual(['source.css'])
expect(sources.length).toBe(1)

// TODO: Some numbers are off by one (too large?) in the destination
expect(annotations).toEqual([
'1:0-6 <- 1:0',
'2:2-14 <- 2:2',
'3:2-14 <- 3:2',
'4:2-11 <- 3:2',
'5:4-16 <- 3:2',
'7:2-34 <- 4:2',
'8:2-13 <- 5:2',
'1:1-6 <- 1:1-6',
'2:3-15 <- 2:3-14',
'3:3-15 <- 3:3-39',
'4:3-11 <- 3:3-39',
'5:5-17 <- 3:3-39',
'7:3-35 <- 4:3-19',
'8:3-14 <- 5:3-13',
])
})

Expand All @@ -88,7 +114,7 @@ test('license comments preserve source locations', async () => {
expect(sources).toEqual(['source.css'])
expect(sources.length).toBe(1)

expect(annotations).toEqual(['1:0-19 <- 1:0'])
expect(annotations).toEqual(['1:1-20 <- 1:1-20'])
})

test('license comments with new lines preserve source locations', async () => {
Expand All @@ -100,9 +126,23 @@ test('license comments with new lines preserve source locations', async () => {
expect(sources.length).toBe(1)

// Ideally we'd write this as this instead `1:0-2:11 <- 1:0-2:11`
expect(annotations).toEqual(['1:0 <- 2:0', '2:11 <- 2:0'])
expect(annotations).toEqual(['1:1 <- 2:1', '2:12 <- 3:12'])
})

/**
* An string annotation that represents one or more source locations
*/
function annotatedLocations<T extends string>(map: Record<T, Range>): Record<T, string> {
let res: Record<string, string> = {} as any

for (let key in map) {
let range = map[key]
res[key] = `${range.start.line}:${range.start.column}-${range.end.line}:${range.end.column}`
}

return res
}

/**
* An string annotation that represents a source map
*
Expand Down

0 comments on commit 6e8474a

Please sign in to comment.