From 35e80cba9cb56368631e1b6890efffe084f9010c Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 25 Nov 2025 09:37:50 -0500 Subject: [PATCH 1/5] Fix source map generation during when watching files on the CLI --- packages/@tailwindcss-cli/src/commands/build/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@tailwindcss-cli/src/commands/build/index.ts b/packages/@tailwindcss-cli/src/commands/build/index.ts index 794b54ae3be6..f0d441fe2d99 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') } } From d7d7a4c53af8233dcc92b664104234a75a010dec Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 25 Nov 2025 09:59:23 -0500 Subject: [PATCH 2/5] Add tests --- integrations/cli/index.test.ts | 449 +++++++++++++++++++++++++++++++++ 1 file changed, 449 insertions(+) diff --git a/integrations/cli/index.test.ts b/integrations/cli/index.test.ts index 5f3b0fcb42ca..73d857eb0f2f 100644 --- a/integrations/cli/index.test.ts +++ b/integrations/cli/index.test.ts @@ -819,6 +819,455 @@ 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`]) + + // 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('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 + let written = process.onStderr((m) => m.includes('Done in')) + await fs.write('src/index.html', html` +
+ `) + await written + + // Make sure the source map was updated + 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('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 + written = process.onStderr((m) => m.includes('Done in')) + await fs.write( + 'src/index.css', + css` + @import 'tailwindcss/utilities'; + @source inline("w-auto"); + `, + ) + await written + + // Make sure the source map was updated + 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('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 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('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 + let written = process.onStderr((m) => m.includes('Done in')) + await fs.write('src/index.html', html` +
+ `) + await written + + // Make sure the source map was updated + 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('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 + written = process.onStderr((m) => m.includes('Done in')) + await fs.write( + 'src/index.css', + css` + @import 'tailwindcss/utilities'; + @source inline("w-auto"); + `, + ) + await written + + // Make sure the source map was updated + 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('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( From 068a1ec3bf9684b82e75310cf123368aef48514b Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 25 Nov 2025 10:03:23 -0500 Subject: [PATCH 3/5] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fba8eaf94a2..03fdcc5bff13 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 in 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 From deb4fe95e813244c394e925c340b002f33f28cea Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 25 Nov 2025 11:23:55 -0500 Subject: [PATCH 4/5] Tweak assertion setup --- integrations/cli/index.test.ts | 74 +++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 24 deletions(-) diff --git a/integrations/cli/index.test.ts b/integrations/cli/index.test.ts index 73d857eb0f2f..867d4d777d4e 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)) @@ -849,8 +849,11 @@ describe.each([ 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(await fs.read('dist/out.css')) + let map = parseSourceMap(currentCss) expect(map.at(1, 0)).toMatchObject({ source: null, @@ -883,14 +886,19 @@ describe.each([ }) // Write to project source files - let written = process.onStderr((m) => m.includes('Done in')) await fs.write('src/index.html', html`
`) - await written + + // 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(await fs.read('dist/out.css')) + map = parseSourceMap(currentCss) expect(map.at(1, 0)).toMatchObject({ source: null, @@ -947,7 +955,6 @@ describe.each([ }) // Write to the main CSS file - written = process.onStderr((m) => m.includes('Done in')) await fs.write( 'src/index.css', css` @@ -955,10 +962,16 @@ describe.each([ @source inline("w-auto"); `, ) - await written + + // 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(await fs.read('dist/out.css')) + map = parseSourceMap(currentCss) expect(map.at(1, 0)).toMatchObject({ source: null, @@ -1070,10 +1083,13 @@ describe.each([ 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'), - }) + 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, @@ -1106,18 +1122,23 @@ describe.each([ }) // Write to project source files - let written = process.onStderr((m) => m.includes('Done in')) await fs.write('src/index.html', html`
`) - await written - // Make sure the source map was updated - map = parseSourceMap({ - map: await fs.read('dist/out.css.map'), - content: await fs.read('dist/out.css'), + // 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)', @@ -1173,7 +1194,6 @@ describe.each([ }) // Write to the main CSS file - written = process.onStderr((m) => m.includes('Done in')) await fs.write( 'src/index.css', css` @@ -1181,14 +1201,20 @@ describe.each([ @source inline("w-auto"); `, ) - await written - // Make sure the source map was updated - map = parseSourceMap({ - map: await fs.read('dist/out.css.map'), - content: await fs.read('dist/out.css'), + // 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)', From 12c476dc00d66bd2c358040a418bc4e8099664e6 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 25 Nov 2025 11:24:49 -0500 Subject: [PATCH 5/5] Fix typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03fdcc5bff13..61eeebe95b03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +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 in when using `--watch` ([#19373](https://github.com/tailwindlabs/tailwindcss/pull/19373)) +- 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