Fix source map generation during when watching files on the CLI (#19373)

Fixes #19362

We were overwriting the source map with the "decoded" map returned by
the compiler but didn't wrap it in the helper intended to help inline vs
file maps. This resulted in two issues:
1. `undefined` being appended to the CSS file when using `--map`
2. `undefined` being passed to `writeFile(…)` when using `--map <file>`

This PR fixes both.
This commit is contained in:
Jordan Pittman 2025-11-25 12:22:57 -05:00 committed by GitHub
parent 28670a9b91
commit 0e8f075ca2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 479 additions and 3 deletions

View File

@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Skip over arbitrary property utilities with a top-level `!` in the value ([#19243](https://github.com/tailwindlabs/tailwindcss/pull/19243))
- Support environment API in `@tailwindcss/vite` ([#18970](https://github.com/tailwindlabs/tailwindcss/pull/18970))
- Preserve case of theme keys from JS configs and plugins ([#19337](https://github.com/tailwindlabs/tailwindcss/pull/19337))
- Write source maps correctly on the CLI when using `--watch` ([#19373](https://github.com/tailwindlabs/tailwindcss/pull/19373))
- Upgrade: Handle `future` and `experimental` config keys ([#19344](https://github.com/tailwindlabs/tailwindcss/pull/19344))
### Added

View File

@ -3,7 +3,7 @@ import os from 'node:os'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe } from 'vitest'
import { candidate, css, html, js, json, test, ts, yaml } from '../utils'
import { candidate, css, html, js, json, retryAssertion, test, ts, yaml } from '../utils'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
@ -819,6 +819,481 @@ describe.each([
})
},
)
test(
'watch mode + 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 ({ spawn, expect, fs, parseSourceMap }) => {
let process = await spawn(
`${command} --input src/index.css --output dist/out.css --map --watch`,
)
await process.onStderr((m) => m.includes('Done in'))
await fs.expectFileToContain('dist/out.css', [candidate`flex`])
let originalCss = await fs.read('dist/out.css')
let currentCss = originalCss
// Make sure we can find a source map
let map = parseSourceMap(currentCss)
expect(map.at(1, 0)).toMatchObject({
source: null,
original: '(none)',
generated: '/*! tailwi...',
})
expect(map.at(2, 0)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: '.flex {...',
})
expect(map.at(3, 2)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: 'display: f...',
})
expect(map.at(4, 0)).toMatchObject({
source: null,
original: '(none)',
generated: '}...',
})
// Write to project source files
await fs.write('src/index.html', html`
<div class="flex underline"></div>
`)
// Wait for the CSS to be rebuilt
await retryAssertion(async () => {
currentCss = await fs.read('dist/out.css')
expect(currentCss).not.toEqual(originalCss)
originalCss = currentCss
})
// Make sure the source map was updated
map = parseSourceMap(currentCss)
expect(map.at(1, 0)).toMatchObject({
source: null,
original: '(none)',
generated: '/*! tailwi...',
})
expect(map.at(2, 0)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: '.flex {...',
})
expect(map.at(3, 2)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: 'display: f...',
})
expect(map.at(4, 0)).toMatchObject({
source: null,
original: '(none)',
generated: '}\n.underli...',
})
expect(map.at(5, 0)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: '.underline...',
})
expect(map.at(6, 2)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: 'text-decor...',
})
expect(map.at(7, 0)).toMatchObject({
source: null,
original: '(none)',
generated: '}...',
})
// Write to the main CSS file
await fs.write(
'src/index.css',
css`
@import 'tailwindcss/utilities';
@source inline("w-auto");
`,
)
// Wait for the CSS to be rebuilt
await retryAssertion(async () => {
currentCss = await fs.read('dist/out.css')
expect(currentCss).not.toEqual(originalCss)
originalCss = currentCss
})
// Make sure the source map was updated
map = parseSourceMap(currentCss)
expect(map.at(1, 0)).toMatchObject({
source: null,
original: '(none)',
generated: '/*! tailwi...',
})
expect(map.at(2, 0)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: '.flex {...',
})
expect(map.at(3, 2)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: 'display: f...',
})
expect(map.at(4, 0)).toMatchObject({
source: null,
original: '(none)',
generated: '}\n.w-auto...',
})
expect(map.at(5, 0)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: '.w-auto {...',
})
expect(map.at(6, 2)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: 'width: aut...',
})
expect(map.at(7, 0)).toMatchObject({
source: null,
original: '(none)',
generated: '}\n.underli...',
})
expect(map.at(8, 0)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: '.underline...',
})
expect(map.at(9, 2)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: 'text-decor...',
})
expect(map.at(10, 0)).toMatchObject({
source: null,
original: '(none)',
generated: '}...',
})
},
)
test(
'watch mode + 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 ({ spawn, expect, fs, parseSourceMap }) => {
let process = await spawn(
`${command} --input src/index.css --output dist/out.css --map dist/out.css.map --watch`,
)
await process.onStderr((m) => m.includes('Done in'))
await fs.expectFileToContain('dist/out.css', [candidate`flex`])
// Make sure we can find a source map
let originalCss = await fs.read('dist/out.css')
let originalMap = await fs.read('dist/out.css.map')
let currentCss = originalCss
let currentMap = originalMap
// Make sure we can find a source map
let map = parseSourceMap({ map: currentMap, content: currentCss })
expect(map.at(1, 0)).toMatchObject({
source: null,
original: '(none)',
generated: '/*! tailwi...',
})
expect(map.at(2, 0)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: '.flex {...',
})
expect(map.at(3, 2)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: 'display: f...',
})
expect(map.at(4, 0)).toMatchObject({
source: null,
original: '(none)',
generated: '}...',
})
// Write to project source files
await fs.write('src/index.html', html`
<div class="flex underline"></div>
`)
// Wait for the CSS to be rebuilt
await retryAssertion(async () => {
currentCss = await fs.read('dist/out.css')
currentMap = await fs.read('dist/out.css.map')
expect(currentCss).not.toEqual(originalCss)
expect(currentMap).not.toEqual(originalMap)
originalCss = currentCss
originalMap = currentMap
})
// Make sure the source map was updated
map = parseSourceMap({ map: currentMap, content: currentCss })
expect(map.at(1, 0)).toMatchObject({
source: null,
original: '(none)',
generated: '/*! tailwi...',
})
expect(map.at(2, 0)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: '.flex {...',
})
expect(map.at(3, 2)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: 'display: f...',
})
expect(map.at(4, 0)).toMatchObject({
source: null,
original: '(none)',
generated: '}\n.underli...',
})
expect(map.at(5, 0)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: '.underline...',
})
expect(map.at(6, 2)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: 'text-decor...',
})
expect(map.at(7, 0)).toMatchObject({
source: null,
original: '(none)',
generated: '}...',
})
// Write to the main CSS file
await fs.write(
'src/index.css',
css`
@import 'tailwindcss/utilities';
@source inline("w-auto");
`,
)
// Wait for the CSS to be rebuilt
await retryAssertion(async () => {
currentCss = await fs.read('dist/out.css')
currentMap = await fs.read('dist/out.css.map')
expect(currentCss).not.toEqual(originalCss)
expect(currentMap).not.toEqual(originalMap)
originalCss = currentCss
originalMap = currentMap
})
// Make sure the source map was updated
map = parseSourceMap({ map: currentMap, content: currentCss })
expect(map.at(1, 0)).toMatchObject({
source: null,
original: '(none)',
generated: '/*! tailwi...',
})
expect(map.at(2, 0)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: '.flex {...',
})
expect(map.at(3, 2)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: 'display: f...',
})
expect(map.at(4, 0)).toMatchObject({
source: null,
original: '(none)',
generated: '}\n.w-auto...',
})
expect(map.at(5, 0)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: '.w-auto {...',
})
expect(map.at(6, 2)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: 'width: aut...',
})
expect(map.at(7, 0)).toMatchObject({
source: null,
original: '(none)',
generated: '}\n.underli...',
})
expect(map.at(8, 0)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: '.underline...',
})
expect(map.at(9, 2)).toMatchObject({
source:
kind === 'CLI'
? expect.stringContaining('utilities.css')
: expect.stringMatching(/\/utilities-\w+\.css$/),
original: '@tailwind...',
generated: 'text-decor...',
})
expect(map.at(10, 0)).toMatchObject({
source: null,
original: '(none)',
generated: '}...',
})
},
)
})
test(

View File

@ -321,7 +321,7 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
if (args['--map']) {
DEBUG && I.start('Build Source Map')
compiledMap = compiler.buildSourceMap() as any
compiledMap = toSourceMap(compiler.buildSourceMap())
DEBUG && I.end('Build Source Map')
}
}
@ -346,7 +346,7 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
if (args['--map']) {
DEBUG && I.start('Build Source Map')
compiledMap = compiler.buildSourceMap() as any
compiledMap = toSourceMap(compiler.buildSourceMap())
DEBUG && I.end('Build Source Map')
}
}