Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Source maps #13387

Draft
wants to merge 36 commits into
base: next
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
77f29a9
Pass source maps through compile and build
KrisBraun Mar 26, 2024
b6355e9
Enable dev source maps in Vite playground
KrisBraun Mar 26, 2024
6f411a8
Parse source map
KrisBraun Mar 26, 2024
f5c9c58
Track source and destination locations
KrisBraun Mar 28, 2024
e554087
Separate buildSourceMap function
KrisBraun Mar 28, 2024
f4fd8b8
Call buildSourceMap conditionally
KrisBraun Mar 28, 2024
9551e0a
Start source destination after indent
KrisBraun Mar 28, 2024
38853bd
Map @keyframe
KrisBraun Mar 28, 2024
7400134
Associate theme values with first @theme
KrisBraun Mar 28, 2024
6ac7a4b
Clean up Vite plugin
KrisBraun Mar 28, 2024
7d37cd4
Only track source locations if a source map is specified
KrisBraun Mar 28, 2024
c41d58c
Refactor
thecrypticace Apr 1, 2024
beba495
wip
thecrypticace Apr 1, 2024
04711ce
Make sure AST shape doesn’t change
thecrypticace Apr 1, 2024
c446150
Prepare for source range mappings
thecrypticace Apr 1, 2024
3efdf06
wip
thecrypticace Apr 2, 2024
c818b25
Revert "wip"
thecrypticace Apr 2, 2024
4a45b84
Add source map support to the CLI
thecrypticace Apr 17, 2024
c2abdb0
wip
thecrypticace Apr 17, 2024
da5918d
Fix source maps for `@apply`
thecrypticace Apr 17, 2024
f51e160
wip
thecrypticace Apr 17, 2024
73481ce
wip
thecrypticace Apr 17, 2024
bc854e4
wip
thecrypticace Apr 17, 2024
3a743a6
wip
thecrypticace Apr 17, 2024
b25d383
Update
thecrypticace Apr 17, 2024
9eba1bb
wip
thecrypticace Apr 17, 2024
b2c6fdd
wip
thecrypticace Apr 17, 2024
e2b0be2
wip
thecrypticace Apr 17, 2024
a713391
Track source locations when parsing and serializing
thecrypticace Apr 17, 2024
03545ba
wip
thecrypticace Apr 26, 2024
361dc8c
wip
thecrypticace Apr 26, 2024
8878b86
wip
thecrypticace Apr 26, 2024
641855b
wip
thecrypticace Apr 26, 2024
a9745e2
wip
thecrypticace Apr 26, 2024
5bb07ca
wip
thecrypticace Apr 29, 2024
0e3a39d
wip
thecrypticace Apr 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/@tailwindcss-cli/package.json
Expand Up @@ -29,9 +29,11 @@
"access": "public"
},
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"@parcel/watcher": "^2.4.1",
"@tailwindcss/oxide": "workspace:^",
"lightningcss": "^1.24.0",
"magic-string": "^0.30.8",
"mri": "^1.2.0",
"picocolors": "^1.0.0",
"postcss": "8.4.24",
Expand Down
165 changes: 144 additions & 21 deletions packages/@tailwindcss-cli/src/commands/build/index.ts
@@ -1,11 +1,14 @@
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'
import MagicString from 'magic-string'
import { existsSync } from 'node:fs'
import fs from 'node:fs/promises'
import path from 'node:path'
import postcss from 'postcss'
import atImport from 'postcss-import'
import type { RawSourceMap } from 'source-map-js'
import { compile } from 'tailwindcss'
import type { Arg, Result } from '../../utils/args'
import {
Expand Down Expand Up @@ -38,6 +41,11 @@ export function options() {
description: 'Watch for changes and rebuild as needed',
alias: '-w',
},
'--map': {
type: 'boolean | string',
description: 'Generate source maps',
alias: '-p',
},
'--minify': {
type: 'boolean',
description: 'Optimize and minify the output',
Expand All @@ -55,7 +63,28 @@ export function options() {
} satisfies Arg
}

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

export async function handle(args: Result<ReturnType<typeof options>>) {
type SourceMapType = null | { kind: 'inline' } | { kind: 'file'; path: string }

let sourcemapType: SourceMapType

if (args['--map'] === true) {
sourcemapType = { kind: 'inline' }
} else if (typeof args['--map'] === 'string') {
sourcemapType = { kind: 'file', path: args['--map'] }
} else {
sourcemapType = null
}

let base = path.resolve(args['--cwd'])

// Resolve the output as an absolute path.
Expand All @@ -81,41 +110,88 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
let start = process.hrtime.bigint()
let { candidates } = scanDir({ base })

let source = args['--input']
? args['--input'] === '-'
? await drainStdin()
: await fs.readFile(args['--input'], 'utf-8')
: css`
@import '${resolve('tailwindcss/index.css')}';
`

let magic = new MagicString(source, {
filename: 'source.css',
})

// Resolve the input
let [input, cssImportPaths] = await handleImports(
args['--input']
? args['--input'] === '-'
? await drainStdin()
: await fs.readFile(args['--input'], 'utf-8')
: css`
@import '${resolve('tailwindcss/index.css')}';
`,
let [input, cssImportPaths, importedMap] = await handleImports(
source,
args['--input'] ?? base,
sourcemapType ? true : false,
)

let previous = {
css: '',
optimizedCss: '',
optimizedMap: null,
}

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

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 optimizedCss = optimizeCss(css, {
let { css: optimizedCss, map: optimizedMap } = optimizeCss(css, {
file: args['--input'] ?? 'input.css',
minify: args['--minify'] ?? false,
map: combinedMap,
})
previous.css = css
previous.optimizedCss = optimizedCss
previous.optimizedMap = optimizedMap
output = optimizedCss
outputMap = optimizedMap
} else {
output = previous.optimizedCss
outputMap = previous.optimizedMap
}
}

if (outputMap && sourcemapType?.kind === 'inline') {
output = attachInlineMap(output, outputMap)
} else if (outputMap && sourcemapType?.kind === 'file') {
await outputFile(sourcemapType.path, JSON.stringify(outputMap))
}

// Write the output
if (args['--output']) {
await outputFile(args['--output'], output)
Expand All @@ -125,9 +201,14 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
}

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

await write(build(candidates), args)
let outputCss = build(candidates)
let outputMap = sourcemapType ? buildSourceMap() : null

await write(outputCss, outputMap, args)

let end = process.hrtime.bigint()
eprintln(header())
Expand Down Expand Up @@ -175,6 +256,7 @@ export async function handle(args: Result<ReturnType<typeof options>>) {

// Track the compiled CSS
let compiledCss = ''
let compiledMap = null

// Scan the entire `base` directory for full rebuilds.
if (rebuildStrategy === 'full') {
Expand All @@ -189,20 +271,24 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
@import '${resolve('tailwindcss/index.css')}';
`,
args['--input'] ?? base,
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
}

// Scan changed files only for incremental rebuilds.
else if (rebuildStrategy === 'incremental') {
let newCandidates = scanFiles(changedFiles, IO.Sequential | Parsing.Sequential)

compiledCss = build(newCandidates)
compiledMap = sourcemapType ? buildSourceMap() : null
}

await write(compiledCss, args)
await write(compiledCss, compiledMap, args)

let end = process.hrtime.bigint()
eprintln(`Done in ${formatDuration(end - start)}`)
Expand All @@ -228,22 +314,46 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
}
}

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

function handleImports(
input: string,
file: string,
): [css: string, paths: string[]] | Promise<[css: string, paths: string[]]> {
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]]
return [input, [file], null]
}

return postcss()
.use(atImport())
.process(input, { from: file })
.process(input, {
from: file,
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: false,

// We want source data to be included in the resulting map
sourcesContent: true,

// Don't add a comment with the source map URL at the end of the file
// We'll do this manually if needed
annotation: false,

// Require absolute paths in the source map
absolute: true,
}
: false,
})
.then((result) => [
result.css,

Expand All @@ -252,18 +362,26 @@ function handleImports(
[file].concat(
result.messages.filter((msg) => msg.type === 'dependency').map((msg) => msg.file),
),

// Return the source map if it exists
result.map?.toJSON() ?? null,
])
}

function optimizeCss(
input: string,
{ file = 'input.css', minify = false }: { file?: string; minify?: boolean } = {},
{
file = 'input.css',
minify = false,
map = null,
}: { file?: string; minify?: boolean; map?: any } = {},
) {
return transform({
let result = transform({
filename: file,
code: Buffer.from(input),
minify,
sourceMap: false,
sourceMap: map ? true : false,
inputSourceMap: map ? JSON.stringify(map) : undefined,
drafts: {
customMedia: true,
},
Expand All @@ -276,5 +394,10 @@ function optimizeCss(
safari: (16 << 16) | (4 << 8),
},
errorRecovery: true,
}).code.toString()
})

return {
css: result.code.toString(),
map: result.map ? JSON.parse(result.map.toString()) : null,
}
}
24 changes: 19 additions & 5 deletions packages/@tailwindcss-postcss/src/index.ts
Expand Up @@ -44,6 +44,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
build: null as null | ReturnType<typeof compile>['build'],
css: '',
optimizedCss: '',
optimizedMap: '',
}
})

Expand Down Expand Up @@ -139,9 +140,12 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {

// Replace CSS
if (css !== context.css && optimize) {
context.optimizedCss = optimizeCss(css, {
let optimized = optimizeCss(css, {
minify: typeof optimize === 'object' ? optimize.minify : true,
})

context.optimizedCss = optimized.css
context.optimizedMap = optimized.map
}
context.css = css
root.removeAll()
Expand All @@ -153,13 +157,18 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {

function optimizeCss(
input: string,
{ file = 'input.css', minify = false }: { file?: string; minify?: boolean } = {},
{
file = 'input.css',
minify = false,
map = null,
}: { file?: string; minify?: boolean; map?: any } = {},
) {
return transform({
let result = transform({
filename: file,
code: Buffer.from(input),
minify,
sourceMap: false,
sourceMap: map ? true : false,
inputSourceMap: map ? JSON.stringify(map) : undefined,
drafts: {
customMedia: true,
},
Expand All @@ -172,7 +181,12 @@ function optimizeCss(
safari: (16 << 16) | (4 << 8),
},
errorRecovery: true,
}).code.toString()
})

return {
css: result.code.toString(),
map: result.map ? JSON.parse(result.map.toString()) : null,
}
}

export default Object.assign(tailwindcss, { postcss: true }) as PluginCreator<PluginOptions>
1 change: 1 addition & 0 deletions packages/@tailwindcss-vite/package.json
Expand Up @@ -34,6 +34,7 @@
},
"devDependencies": {
"@types/node": "^20.11.17",
"rollup": "^4.13.0",
"vite": "^5.2.0"
},
"peerDependencies": {
Expand Down