Add source map support to the CLI

This commit is contained in:
Jordan Pittman 2025-04-29 19:15:47 -04:00
parent acf5f3182e
commit 2f25e3ec3a
2 changed files with 268 additions and 5 deletions

View file

@ -23,7 +23,7 @@ describe.each([
'Standalone CLI',
path.resolve(__dirname, `../../packages/@tailwindcss-standalone/dist/${STANDALONE_BINARY}`),
],
])('%s', (_, command) => {
])('%s', (kind, command) => {
test(
'production build',
{
@ -628,6 +628,198 @@ describe.each([
])
},
)
test(
'production build + inline source maps',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "workspace:^",
"@tailwindcss/cli": "workspace:^"
}
}
`,
'ssrc/index.html': html`
<div class="flex"></div>
`,
'src/index.css': css`
@import 'tailwindcss/utilities';
/* */
`,
},
},
async ({ exec, expect, fs, parseSourceMap }) => {
await exec(`${command} --input src/index.css --output dist/out.css --map`)
console.log(await fs.read('dist/out.css'))
await fs.expectFileToContain('dist/out.css', [candidate`flex`])
// Make sure we can find a source map
let map = parseSourceMap(await fs.read('dist/out.css'))
expect(map.at(1, 0)).toMatchObject({
source: null,
original: '(none)',
generated: '/*! tailwi...',
})
expect(map.at(2, 0)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('node_modules/tailwindcss/utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: '.flex {...',
})
expect(map.at(3, 2)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('node_modules/tailwindcss/utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: 'display: f...',
})
expect(map.at(4, 0)).toMatchObject({
source: null,
original: '(none)',
generated: '}...',
})
},
)
test(
'production build + separate source maps',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "workspace:^",
"@tailwindcss/cli": "workspace:^"
}
}
`,
'ssrc/index.html': html`
<div class="flex"></div>
`,
'src/index.css': css`
@import 'tailwindcss/utilities';
/* */
`,
},
},
async ({ exec, expect, fs, parseSourceMap }) => {
await exec(`${command} --input src/index.css --output dist/out.css --map dist/out.css.map`)
await fs.expectFileToContain('dist/out.css', [candidate`flex`])
// Make sure we can find a source map
let map = parseSourceMap({
map: await fs.read('dist/out.css.map'),
content: await fs.read('dist/out.css'),
})
expect(map.at(1, 0)).toMatchObject({
source: null,
original: '(none)',
generated: '/*! tailwi...',
})
expect(map.at(2, 0)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('node_modules/tailwindcss/utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: '.flex {...',
})
expect(map.at(3, 2)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('node_modules/tailwindcss/utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: 'display: f...',
})
expect(map.at(4, 0)).toMatchObject({
source: null,
original: '(none)',
generated: '}...',
})
},
)
// Skipped because Lightning CSS has a bug with source maps containing
// license comments and it breaks stuff.
test.skip(
'production build + minify + source maps',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "workspace:^",
"@tailwindcss/cli": "workspace:^"
}
}
`,
'ssrc/index.html': html`
<div class="flex"></div>
`,
'src/index.css': css`
@import 'tailwindcss/utilities';
/* */
`,
},
},
async ({ exec, expect, fs, parseSourceMap }) => {
await exec(`${command} --input src/index.css --output dist/out.css --minify --map`)
await fs.expectFileToContain('dist/out.css', [candidate`flex`])
console.log(await fs.read('dist/out.css'))
// Make sure we can find a source map
let map = parseSourceMap(await fs.read('dist/out.css'))
expect(map.at(1, 0)).toMatchObject({
source: null,
original: '(none)',
generated: '/*! tailwi...',
})
expect(map.at(2, 0)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('node_modules/tailwindcss/utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: '.flex {...',
})
expect(map.at(3, 2)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('node_modules/tailwindcss/utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: 'display: f...',
})
expect(map.at(4, 0)).toMatchObject({
source: null,
original: '(none)',
generated: '}...',
})
},
)
})
test(

View file

@ -1,5 +1,12 @@
import watcher from '@parcel/watcher'
import { compile, env, Instrumentation, optimize } from '@tailwindcss/node'
import {
compile,
env,
Instrumentation,
optimize,
toSourceMap,
type SourceMap,
} from '@tailwindcss/node'
import { clearRequireCache } from '@tailwindcss/node/require-cache'
import { Scanner, type ChangedContent } from '@tailwindcss/oxide'
import { existsSync, type Stats } from 'node:fs'
@ -52,6 +59,11 @@ export function options() {
description: 'The current working directory',
default: '.',
},
'--map': {
type: 'boolean | string',
description: 'Generate a source map',
default: false,
},
} satisfies Arg
}
@ -105,6 +117,21 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
process.exit(1)
}
// If the user passes `{bin} build --map -` then this likely means they want to output the map inline
// this is the default behavior of `{bin build} --map` to inform the user of that
if (args['--map'] === '-') {
eprintln(header())
eprintln()
eprintln(`Use --map without a value to inline the source map`)
process.exit(1)
}
// Resolve the map as an absolute path. If the output is true then we
// don't need to resolve it because it'll be an inline source map
if (args['--map'] && args['--map'] !== true) {
args['--map'] = path.resolve(base, args['--map'])
}
let start = process.hrtime.bigint()
let input = args['--input']
@ -120,7 +147,12 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
optimizedCss: '',
}
async function write(css: string, args: Result<ReturnType<typeof options>>, I: Instrumentation) {
async function write(
css: string,
map: SourceMap | null,
args: Result<ReturnType<typeof options>>,
I: Instrumentation,
) {
let output = css
// Optimize the output
@ -130,10 +162,14 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
let optimized = optimize(css, {
file: args['--input'] ?? 'input.css',
minify: args['--minify'] ?? false,
map: map?.raw ?? undefined,
})
DEBUG && I.end('Optimize CSS')
previous.css = css
previous.optimizedCss = optimized.code
if (optimized.map) {
map = toSourceMap(optimized.map)
}
output = optimized.code
} else {
output = previous.optimizedCss
@ -141,6 +177,18 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
}
// Write the output
if (map) {
// Inline the source map
if (args['--map'] === true) {
output += `\n`
output += map.inline
} else if (typeof args['--map'] === 'string') {
DEBUG && I.start('Write source map')
await outputFile(args['--map'], map.raw)
DEBUG && I.end('Write source map')
}
}
DEBUG && I.start('Write output')
if (args['--output'] && args['--output'] !== '-') {
await outputFile(args['--output'], output)
@ -160,6 +208,7 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
async function createCompiler(css: string, I: Instrumentation) {
DEBUG && I.start('Setup compiler')
let compiler = await compile(css, {
from: args['--output'] ? (inputFilePath ?? 'stdin.css') : undefined,
base: inputBasePath,
onDependency(path) {
fullRebuildPaths.push(path)
@ -231,6 +280,7 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
// Track the compiled CSS
let compiledCss = ''
let compiledMap: SourceMap | null = null
// Scan the entire `base` directory for full rebuilds.
if (rebuildStrategy === 'full') {
@ -269,6 +319,12 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
DEBUG && I.start('Build CSS')
compiledCss = compiler.build(candidates)
DEBUG && I.end('Build CSS')
if (args['--map']) {
DEBUG && I.start('Build Source Map')
compiledMap = compiler.buildSourceMap() as any
DEBUG && I.end('Build Source Map')
}
}
// Scan changed files only for incremental rebuilds.
@ -288,9 +344,15 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
DEBUG && I.start('Build CSS')
compiledCss = compiler.build(newCandidates)
DEBUG && I.end('Build CSS')
if (args['--map']) {
DEBUG && I.start('Build Source Map')
compiledMap = compiler.buildSourceMap() as any
DEBUG && I.end('Build Source Map')
}
}
await write(compiledCss, args, I)
await write(compiledCss, compiledMap, args, I)
let end = process.hrtime.bigint()
eprintln(`Done in ${formatDuration(end - start)}`)
@ -325,7 +387,16 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
DEBUG && I.start('Build CSS')
let output = await handleError(() => compiler.build(candidates))
DEBUG && I.end('Build CSS')
await write(output, args, I)
let map: SourceMap | null = null
if (args['--map']) {
DEBUG && I.start('Build Source Map')
map = await handleError(() => toSourceMap(compiler.buildSourceMap()))
DEBUG && I.end('Build Source Map')
}
await write(output, map, args, I)
let end = process.hrtime.bigint()
eprintln(header())