From 0e8f075ca265845470d50ed9241e9e1095376a42 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 25 Nov 2025 12:22:57 -0500 Subject: [PATCH] Fix source map generation during when watching files on the CLI (#19373) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ` This PR fixes both. --- CHANGELOG.md | 1 + integrations/cli/index.test.ts | 477 +++++++++++++++++- .../src/commands/build/index.ts | 4 +- 3 files changed, 479 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fba8eaf9..61eeebe95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/integrations/cli/index.test.ts b/integrations/cli/index.test.ts index 5f3b0fcb4..867d4d777 100644 --- a/integrations/cli/index.test.ts +++ b/integrations/cli/index.test.ts @@ -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` +
+ `, + '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` +
+ `) + + // 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` +
+ `, + '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` +
+ `) + + // 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( diff --git a/packages/@tailwindcss-cli/src/commands/build/index.ts b/packages/@tailwindcss-cli/src/commands/build/index.ts index 794b54ae3..f0d441fe2 100644 --- a/packages/@tailwindcss-cli/src/commands/build/index.ts +++ b/packages/@tailwindcss-cli/src/commands/build/index.ts @@ -321,7 +321,7 @@ export async function handle(args: Result>) { 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>) { if (args['--map']) { DEBUG && I.start('Build Source Map') - compiledMap = compiler.buildSourceMap() as any + compiledMap = toSourceMap(compiler.buildSourceMap()) DEBUG && I.end('Build Source Map') } }