mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
While working on another PR, I noticed that some files had missing properties and made TypeScript unhappy. Let's make TypeScript happy again...
476 lines
14 KiB
TypeScript
476 lines
14 KiB
TypeScript
import remapping from '@jridgewell/remapping'
|
|
import dedent from 'dedent'
|
|
import MagicString from 'magic-string'
|
|
import * as fs from 'node:fs/promises'
|
|
import * as path from 'node:path'
|
|
import { SourceMapConsumer, SourceMapGenerator, type RawSourceMap } from 'source-map-js'
|
|
import { test } from 'vitest'
|
|
import { compile } from '..'
|
|
import createPlugin from '../plugin'
|
|
import { DefaultMap } from '../utils/default-map'
|
|
import type { DecodedSource, DecodedSourceMap } from './source-map'
|
|
const css = dedent
|
|
|
|
interface RunOptions {
|
|
input: string
|
|
candidates?: string[]
|
|
options?: Parameters<typeof compile>[1]
|
|
}
|
|
|
|
async function run({ input, candidates, options }: RunOptions) {
|
|
let source = new MagicString(input)
|
|
let root = path.resolve(__dirname, '../..')
|
|
|
|
let compiler = await compile(source.toString(), {
|
|
from: 'input.css',
|
|
async loadStylesheet(id, base) {
|
|
let resolvedPath = path.resolve(root, id === 'tailwindcss' ? 'index.css' : id)
|
|
|
|
return {
|
|
path: path.relative(root, resolvedPath),
|
|
base,
|
|
content: await fs.readFile(resolvedPath, 'utf-8'),
|
|
}
|
|
},
|
|
...options,
|
|
})
|
|
|
|
let css = compiler.build(candidates ?? [])
|
|
let decoded = compiler.buildSourceMap()
|
|
let rawMap = toRawSourceMap(decoded)
|
|
let combined = remapping(rawMap, () => null)
|
|
let map = JSON.parse(rawMap.toString()) as RawSourceMap
|
|
|
|
let sources = combined.sources
|
|
let annotations = formattedMappings(map)
|
|
|
|
return { css, map, sources, annotations }
|
|
}
|
|
|
|
function toRawSourceMap(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 ?? undefined,
|
|
})
|
|
|
|
generator.setSourceContent(original.url, original.content)
|
|
}
|
|
|
|
return generator.toString()
|
|
}
|
|
|
|
/**
|
|
* An string annotation that represents a source map
|
|
*
|
|
* It's not meant to be exhaustive just enough to
|
|
* verify that the source map is working and that
|
|
* lines are mapped back to the original source
|
|
*
|
|
* Including when using @apply with multiple classes
|
|
*/
|
|
function formattedMappings(map: RawSourceMap) {
|
|
const smc = new SourceMapConsumer(map)
|
|
const annotations: Record<
|
|
number,
|
|
{
|
|
original: { start: [number, number]; end: [number, number] }
|
|
generated: { start: [number, number]; end: [number, number] }
|
|
source: string
|
|
}
|
|
> = {}
|
|
|
|
smc.eachMapping((mapping) => {
|
|
let annotation = (annotations[mapping.generatedLine] = annotations[mapping.generatedLine] || {
|
|
...mapping,
|
|
|
|
original: {
|
|
start: [mapping.originalLine, mapping.originalColumn],
|
|
end: [mapping.originalLine, mapping.originalColumn],
|
|
},
|
|
|
|
generated: {
|
|
start: [mapping.generatedLine, mapping.generatedColumn],
|
|
end: [mapping.generatedLine, mapping.generatedColumn],
|
|
},
|
|
|
|
source: mapping.source,
|
|
})
|
|
|
|
annotation.generated.end[0] = mapping.generatedLine
|
|
annotation.generated.end[1] = mapping.generatedColumn
|
|
|
|
annotation.original.end[0] = mapping.originalLine!
|
|
annotation.original.end[1] = mapping.originalColumn!
|
|
})
|
|
|
|
return Object.values(annotations).map((annotation) => {
|
|
return `${annotation.source}: ${formatRange(annotation.generated)} <- ${formatRange(annotation.original)}`
|
|
})
|
|
}
|
|
|
|
function formatRange(range: { start: [number, number]; end: [number, number] }) {
|
|
if (range.start[0] === range.end[0]) {
|
|
// This range is on the same line
|
|
// and the columns are the same
|
|
if (range.start[1] === range.end[1]) {
|
|
return `${range.start[0]}:${range.start[1]}`
|
|
}
|
|
|
|
// This range is on the same line
|
|
// but the columns are different
|
|
return `${range.start[0]}:${range.start[1]}-${range.end[1]}`
|
|
}
|
|
|
|
// This range spans multiple lines
|
|
return `${range.start[0]}:${range.start[1]}-${range.end[0]}:${range.end[1]}`
|
|
}
|
|
|
|
test('source maps trace back to @import location', async ({ expect }) => {
|
|
let { sources, annotations } = await run({
|
|
input: css`
|
|
@import 'tailwindcss';
|
|
|
|
.foo {
|
|
@apply underline;
|
|
}
|
|
`,
|
|
})
|
|
|
|
// All CSS should be mapped back to the original source file
|
|
expect(sources).toEqual([
|
|
//
|
|
'index.css',
|
|
'theme.css',
|
|
'preflight.css',
|
|
'input.css',
|
|
])
|
|
expect(sources.length).toBe(4)
|
|
|
|
// The output CSS should include annotations linking back to:
|
|
// 1. The class definition `.foo`
|
|
// 2. The `@apply underline` line inside of it
|
|
expect(annotations).toEqual([
|
|
'index.css: 1:0-41 <- 1:0-41',
|
|
'index.css: 2:0-13 <- 3:0-34',
|
|
'theme.css: 3:2-15 <- 1:0-15',
|
|
'theme.css: 4:4 <- 2:2-4:0',
|
|
'theme.css: 5:22 <- 4:22',
|
|
'theme.css: 6:4 <- 6:2-8:0',
|
|
'theme.css: 7:13 <- 8:13',
|
|
'theme.css: 8:4-43 <- 446:2-54',
|
|
'theme.css: 9:4-48 <- 449:2-59',
|
|
'index.css: 12:0-12 <- 4:0-37',
|
|
'preflight.css: 13:2-59 <- 7:0-11:23',
|
|
'preflight.css: 14:4-26 <- 12:2-24',
|
|
'preflight.css: 15:4-13 <- 13:2-11',
|
|
'preflight.css: 16:4-14 <- 14:2-12',
|
|
'preflight.css: 17:4-19 <- 15:2-17',
|
|
'preflight.css: 19:2-14 <- 28:0-29:6',
|
|
'preflight.css: 20:4-20 <- 30:2-18',
|
|
'preflight.css: 21:4-34 <- 31:2-32',
|
|
'preflight.css: 22:4-15 <- 32:2-13',
|
|
'preflight.css: 23:4-159 <- 33:2-42:3',
|
|
'preflight.css: 24:4-71 <- 43:2-73',
|
|
'preflight.css: 25:4-75 <- 44:2-77',
|
|
'preflight.css: 26:4-44 <- 45:2-42',
|
|
'preflight.css: 28:2-5 <- 54:0-3',
|
|
'preflight.css: 29:4-13 <- 55:2-11',
|
|
'preflight.css: 30:4-18 <- 56:2-16',
|
|
'preflight.css: 31:4-25 <- 57:2-23',
|
|
'preflight.css: 33:2-22 <- 64:0-20',
|
|
'preflight.css: 34:4-45 <- 65:2-43',
|
|
'preflight.css: 35:4-37 <- 66:2-35',
|
|
'preflight.css: 37:2-25 <- 73:0-78:3',
|
|
'preflight.css: 38:4-22 <- 79:2-20',
|
|
'preflight.css: 39:4-24 <- 80:2-22',
|
|
'preflight.css: 41:2-4 <- 87:0-2',
|
|
'preflight.css: 42:4-18 <- 88:2-16',
|
|
'preflight.css: 43:4-36 <- 89:2-34',
|
|
'preflight.css: 44:4-28 <- 90:2-26',
|
|
'preflight.css: 46:2-12 <- 97:0-98:7',
|
|
'preflight.css: 47:4-23 <- 99:2-21',
|
|
'preflight.css: 49:2-23 <- 109:0-112:4',
|
|
'preflight.css: 50:4-148 <- 113:2-123:3',
|
|
'preflight.css: 51:4-76 <- 124:2-78',
|
|
'preflight.css: 52:4-80 <- 125:2-82',
|
|
'preflight.css: 53:4-18 <- 126:2-16',
|
|
'preflight.css: 55:2-8 <- 133:0-6',
|
|
'preflight.css: 56:4-18 <- 134:2-16',
|
|
'preflight.css: 58:2-11 <- 141:0-142:4',
|
|
'preflight.css: 59:4-18 <- 143:2-16',
|
|
'preflight.css: 60:4-18 <- 144:2-16',
|
|
'preflight.css: 61:4-22 <- 145:2-20',
|
|
'preflight.css: 62:4-28 <- 146:2-26',
|
|
'preflight.css: 64:2-6 <- 149:0-4',
|
|
'preflight.css: 65:4-19 <- 150:2-17',
|
|
'preflight.css: 67:2-6 <- 153:0-4',
|
|
'preflight.css: 68:4-15 <- 154:2-13',
|
|
'preflight.css: 70:2-8 <- 163:0-6',
|
|
'preflight.css: 71:4-18 <- 164:2-16',
|
|
'preflight.css: 72:4-25 <- 165:2-23',
|
|
'preflight.css: 73:4-29 <- 166:2-27',
|
|
'preflight.css: 75:2-18 <- 173:0-16',
|
|
'preflight.css: 76:4-17 <- 174:2-15',
|
|
'preflight.css: 78:2-11 <- 181:0-9',
|
|
'preflight.css: 79:4-28 <- 182:2-26',
|
|
'preflight.css: 81:2-10 <- 189:0-8',
|
|
'preflight.css: 82:4-22 <- 190:2-20',
|
|
'preflight.css: 84:2-15 <- 197:0-199:5',
|
|
'preflight.css: 85:4-20 <- 200:2-18',
|
|
'preflight.css: 87:2-56 <- 209:0-216:7',
|
|
'preflight.css: 88:4-18 <- 217:2-16',
|
|
'preflight.css: 89:4-26 <- 218:2-24',
|
|
'preflight.css: 91:2-13 <- 225:0-226:6',
|
|
'preflight.css: 92:4-19 <- 227:2-17',
|
|
'preflight.css: 93:4-16 <- 228:2-14',
|
|
'preflight.css: 95:2-68 <- 238:0-243:23',
|
|
'preflight.css: 96:4-17 <- 244:2-15',
|
|
'preflight.css: 97:4-34 <- 245:2-32',
|
|
'preflight.css: 98:4-36 <- 246:2-34',
|
|
'preflight.css: 99:4-27 <- 247:2-25',
|
|
'preflight.css: 100:4-18 <- 248:2-16',
|
|
'preflight.css: 101:4-20 <- 249:2-18',
|
|
'preflight.css: 102:4-33 <- 250:2-31',
|
|
'preflight.css: 103:4-14 <- 251:2-12',
|
|
'preflight.css: 105:2-49 <- 258:0-47',
|
|
'preflight.css: 106:4-23 <- 259:2-21',
|
|
'preflight.css: 108:2-56 <- 266:0-54',
|
|
'preflight.css: 109:4-30 <- 267:2-28',
|
|
'preflight.css: 111:2-25 <- 274:0-23',
|
|
'preflight.css: 112:4-26 <- 275:2-24',
|
|
'preflight.css: 114:2-16 <- 282:0-14',
|
|
'preflight.css: 115:4-14 <- 283:2-12',
|
|
'preflight.css: 117:2-92 <- 291:0-292:49',
|
|
'preflight.css: 118:4-18 <- 293:2-16',
|
|
'preflight.css: 119:6-25 <- 294:4-61',
|
|
'preflight.css: 120:6-53 <- 294:4-61',
|
|
'preflight.css: 122:10-67 <- 294:4-61',
|
|
'preflight.css: 127:2-11 <- 302:0-9',
|
|
'preflight.css: 128:4-20 <- 303:2-18',
|
|
'preflight.css: 130:2-30 <- 310:0-28',
|
|
'preflight.css: 131:4-28 <- 311:2-26',
|
|
'preflight.css: 133:2-32 <- 319:0-30',
|
|
'preflight.css: 134:4-19 <- 320:2-17',
|
|
'preflight.css: 135:4-23 <- 321:2-21',
|
|
'preflight.css: 137:2-26 <- 328:0-24',
|
|
'preflight.css: 138:4-24 <- 329:2-22',
|
|
'preflight.css: 140:2-41 <- 336:0-39',
|
|
'preflight.css: 141:4-14 <- 337:2-12',
|
|
'preflight.css: 143:2-329 <- 340:0-348:39',
|
|
'preflight.css: 144:4-20 <- 349:2-18',
|
|
'preflight.css: 146:2-38 <- 356:0-36',
|
|
'preflight.css: 147:4-18 <- 357:2-16',
|
|
'preflight.css: 149:2-19 <- 364:0-17',
|
|
'preflight.css: 150:4-20 <- 365:2-18',
|
|
'preflight.css: 152:2-96 <- 372:0-374:23',
|
|
'preflight.css: 153:4-22 <- 375:2-20',
|
|
'preflight.css: 155:2-59 <- 382:0-383:28',
|
|
'preflight.css: 156:4-16 <- 384:2-14',
|
|
'preflight.css: 158:2-47 <- 391:0-45',
|
|
'preflight.css: 159:4-28 <- 392:2-26',
|
|
'index.css: 162:0-16 <- 5:0-42',
|
|
'input.css: 163:0-5 <- 3:0-5',
|
|
'input.css: 164:2-33 <- 4:9-18',
|
|
])
|
|
})
|
|
|
|
test('source maps are generated for utilities', async ({ expect }) => {
|
|
let {
|
|
sources,
|
|
css: output,
|
|
annotations,
|
|
} = await run({
|
|
input: css`
|
|
@import './utilities.css';
|
|
@plugin "./plugin.js";
|
|
@utility custom {
|
|
color: orange;
|
|
}
|
|
`,
|
|
candidates: ['custom', 'custom-js', 'flex'],
|
|
options: {
|
|
loadModule: async (_, base) => ({
|
|
path: '',
|
|
base,
|
|
module: createPlugin(({ addUtilities }) => {
|
|
addUtilities({ '.custom-js': { color: 'blue' } })
|
|
}),
|
|
}),
|
|
},
|
|
})
|
|
|
|
// All CSS should be mapped back to the original source file
|
|
expect(sources).toEqual(['utilities.css', 'input.css'])
|
|
expect(sources.length).toBe(2)
|
|
|
|
// The output CSS should include annotations linking back to:
|
|
expect(annotations).toEqual([
|
|
// @tailwind utilities
|
|
'utilities.css: 1:0-6 <- 1:0-19',
|
|
'utilities.css: 2:2-15 <- 1:0-19',
|
|
'utilities.css: 4:0-8 <- 1:0-19',
|
|
// color: orange
|
|
'input.css: 5:2-15 <- 4:2-15',
|
|
// @tailwind utilities
|
|
'utilities.css: 7:0-11 <- 1:0-19',
|
|
'utilities.css: 8:2-13 <- 1:0-19',
|
|
])
|
|
|
|
expect(output).toMatchInlineSnapshot(`
|
|
".flex {
|
|
display: flex;
|
|
}
|
|
.custom {
|
|
color: orange;
|
|
}
|
|
.custom-js {
|
|
color: blue;
|
|
}
|
|
"
|
|
`)
|
|
})
|
|
|
|
test('utilities have source maps pointing to the utilities node', async ({ expect }) => {
|
|
let { sources, annotations } = await run({
|
|
input: `@tailwind utilities;`,
|
|
candidates: [
|
|
//
|
|
'underline',
|
|
],
|
|
})
|
|
|
|
expect(sources).toEqual(['input.css'])
|
|
|
|
expect(annotations).toEqual([
|
|
//
|
|
'input.css: 1:0-11 <- 1:0-19',
|
|
'input.css: 2:2-33 <- 1:0-19',
|
|
])
|
|
})
|
|
|
|
test('@apply generates source maps', async ({ expect }) => {
|
|
let { sources, annotations } = await run({
|
|
input: css`
|
|
.foo {
|
|
color: blue;
|
|
@apply text-[#000] hover:text-[#f00];
|
|
@apply underline;
|
|
color: red;
|
|
}
|
|
`,
|
|
})
|
|
|
|
expect(sources).toEqual(['input.css'])
|
|
|
|
expect(annotations).toEqual([
|
|
'input.css: 1:0-5 <- 1:0-5',
|
|
'input.css: 2:2-13 <- 2:2-13',
|
|
'input.css: 3:2-13 <- 3:9-20',
|
|
'input.css: 4:2-10 <- 3:21-38',
|
|
'input.css: 5:4-26 <- 3:21-38',
|
|
'input.css: 6:6-17 <- 3:21-38',
|
|
'input.css: 9:2-33 <- 4:9-18',
|
|
'input.css: 10:2-12 <- 5:2-12',
|
|
])
|
|
})
|
|
|
|
test('license comments preserve source locations', async ({ expect }) => {
|
|
let { sources, annotations } = await run({
|
|
input: `/*! some comment */`,
|
|
})
|
|
|
|
expect(sources).toEqual(['input.css'])
|
|
|
|
expect(annotations).toEqual([
|
|
//
|
|
'input.css: 1:0-19 <- 1:0-19',
|
|
])
|
|
})
|
|
|
|
test('license comments with new lines preserve source locations', async ({ expect }) => {
|
|
let { sources, annotations } = await run({
|
|
input: `/*! some \n comment */`,
|
|
})
|
|
|
|
expect(sources).toEqual(['input.css'])
|
|
|
|
expect(annotations).toEqual([
|
|
//
|
|
'input.css: 1:0 <- 1:0-2:0',
|
|
'input.css: 2:11 <- 2:11',
|
|
])
|
|
})
|
|
|
|
test('Source locations for `addBase` point to the `@plugin` that generated them', async ({
|
|
expect,
|
|
}) => {
|
|
let { sources, annotations } = await run({
|
|
input: dedent`
|
|
@plugin "./plugin.js";
|
|
@config "./config.js";
|
|
`,
|
|
options: {
|
|
async loadModule(id, base) {
|
|
if (id === './plugin.js') {
|
|
return {
|
|
module: createPlugin(({ addBase }) => {
|
|
addBase({ body: { color: 'red' } })
|
|
}),
|
|
base,
|
|
path: '',
|
|
}
|
|
}
|
|
|
|
if (id === './config.js') {
|
|
return {
|
|
module: {
|
|
plugins: [
|
|
createPlugin(({ addBase }) => {
|
|
addBase({ body: { color: 'green' } })
|
|
}),
|
|
],
|
|
},
|
|
base,
|
|
path: '',
|
|
}
|
|
}
|
|
|
|
throw new Error(`unknown module ${id}`)
|
|
},
|
|
},
|
|
})
|
|
|
|
expect(sources).toEqual(['input.css'])
|
|
|
|
expect(annotations).toEqual([
|
|
//
|
|
'input.css: 1:0-12 <- 1:0-21',
|
|
'input.css: 2:2-7 <- 1:0-21',
|
|
'input.css: 3:4-14 <- 1:0-21',
|
|
'input.css: 6:0-12 <- 2:0-21',
|
|
'input.css: 7:2-7 <- 2:0-21',
|
|
'input.css: 8:4-16 <- 2:0-21',
|
|
])
|
|
})
|