Repository navigation
Add support for source maps - #17775
Merged
Merged
Conversation
thecrypticace
force-pushed
the
feat/v4-source-maps-v2
branch
3 times, most recently
from
April 30, 2025 15:01
ebf39e4 to
865f2cc
Compare
thecrypticace
force-pushed
the
feat/v4-source-maps-v2
branch
from
May 5, 2025 18:07
865f2cc to
502bda7
Compare
thecrypticace
marked this pull request as ready for review
May 6, 2025 11:34
philipp-spiess
left a comment
Contributor
There was a problem hiding this comment.
This looks really sick. Leaving an early review here. It would be cool if you can include a summary of the implementation in the PR description too including lining out how to test things locally a bit (I assume I can just enable a dev build of Vite with css source maps and then look at the dev tools? I haven't looked into that yet, though)
RobinMalfait
reviewed
May 6, 2025
RobinMalfait
approved these changes
May 8, 2025
RobinMalfait
left a comment
Contributor
There was a problem hiding this comment.
Just a rebase/merge of main and a changelog entry please.
Left a note about the CLI where the header shouldn't be printed anymore (otherwise you will have a double header)
Source map support needs this in loadStylesheet but it makes more sense to be consistent and require it for both.
Now that we can handle CRLF correctly in the parser this is no longer necessary
These are stored as character offsets for the original input (`src`) as well as the printed output (`dst`)
The actual unoptimized bytecode contains one additional instruction but this is definitely a bit faster in benchmarks
thecrypticace
force-pushed
the
feat/v4-source-maps-v2
branch
from
May 8, 2025 20:15
83e2cd2 to
bffe8fe
Compare
RobinMalfait
added a commit
that referenced
this pull request
Oct 14, 2026
…` in optimized builds (#20576) ## Summary Fixes #20570 Vite adds its own `postcss-import` before the PostCSS plugins of the user. Thus, when `@tailwindcss/postcss` runs, the `@import`-ed stylesheets are already inlined. Their relative `url(…)`s are still relative to their own file. At the end, Vite resolves each `url(…)` against the `source.input.file` of its declaration. Without optimization, we keep the `source` of each node (since #17775), so this works. With optimization, we parse the output of Lightning CSS again, and every node gets the input file as its `source`. `optimize` is on by default when `NODE_ENV` is `production`, and `vite build` sets it. Vite then looks for the asset next to the input file, does not find it, and keeps the URL as it is: ``` ./fonts/ExampleSans.woff2 referenced in ./fonts/ExampleSans.woff2 didn't resolve at build time, it will remain unchanged to be resolved at runtime ``` This is the problem that #16636 and #16700 reported. #16965 fixed URL rewriting only for the stylesheets that we inline ourselves. `transformAssetUrls` (#18321) has no effect here. ## Solution When we optimize, we now rewrite the relative `url(…)`s of declarations that come from another file, so that they are relative to the input file. This is the same rewrite that we do for the stylesheets that we inline ourselves. After the rewrite, the URLs match the `source` that the optimized nodes get. - We rewrite the AST that we get from PostCSS, before we compile it. The stylesheets that we load ourselves are not in that AST yet, so we do not rewrite their URLs two times. - Without optimization, nothing changes, because the nodes keep their `source`. - `transformAssetUrls: false` disables the rewrite. To share the code, `rewriteUrls` in `@tailwindcss/node` now uses a new `rewriteUrlsInAst` function. A callback gives it the base folder of each declaration. This also changes optimized builds of other setups that run `postcss-import` before `@tailwindcss/postcss`, like `postcss-cli`. Their URLs are now relative to the input file, like the URLs of the stylesheets that we inline ourselves. ## Test plan - Added unit tests in `packages/@tailwindcss-postcss/src/index.test.ts`. They run `postcss-import` before the plugin with `optimize` on, with `optimize` off, and with `transformAssetUrls: false`. The test with `optimize` on fails on `main`. - Added an integration test in `integrations/postcss/url-rewriting.test.ts`. It runs `vite build` with `@tailwindcss/postcss` in a PostCSS config. Without this change, Vite emits no fonts and the test fails. With this change, it passes. - `pnpm vitest run`: 5598 passed. - `pnpm vitest --root=./integrations run` on `postcss/url-rewriting`, `vite/url-rewriting`, `postcss/index`, `postcss/source`, `postcss/plugins`, `postcss/config`, `postcss/core-as-postcss-plugin` and `postcss/multi-root`: all passed. I did not run `postcss/next.test.ts`. - Built a Vite 8.2.0 project with a font CSS file in a package and one in a local folder. With `optimize: true`, the fonts are now emitted to `dist/assets` and the URLs are rewritten, the same as with `optimize: false`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #13694
Closes #13591
Source Maps Support for Tailwind CSS
This PR adds support for source maps to Tailwind CSS v4 allowing us to track where styles come from whether that be user CSS, imported stylesheets, or generated utilities. This will improve debuggability in browser dev tools and gives us a good foundation for producing better error messages. I'll go over the details on how end users can enable source maps, any limitations in our implementation, changes to the internal
compile(…)API, and some details and reasoning around the implementation we chose.Usage
CLI
Source maps can be enabled in the CLI by using the command line argument
--mapwhich will generate an inline source map comment at the bottom of your CSS. A separate file may be generated by passing a file name to--map:PostCSS
Source maps are supported when using Tailwind as a PostCSS plugin in development mode only. They may or may not be enabled by default depending on your build tool. If they are not you may be able to configure them within your PostCSS config:
Vite
Source maps are supported when using the Tailwind CSS Vite plugin in development mode only by enabling the
css.devSourcemapsetting:Now when a CSS file is requested by the browser it'll have an inline source map comment that the browser can use.
Limitations
write_strfrom breaking source maps parcel-bundler/lightningcss#971 for more details.Testing
Here's how to test the source map functionality in different environments:
Testing the CLI
input.cssornode_modules/tailwindcss/index.cssTesting with Vite
Testing in Vite will require building and installing necessary files under
dist/*.tgz.vite.config.js:npm run dev, open DevTools, and inspect elements to verify source mapping works for both utility classes and custom CSS.Testing with PostCSS CLI
Testing with PostCSS + Next.js
Testing in Next.js will require building and installing necessary files under
dist/*.tgz. However, I've not been able to get CSS source maps to work in Next.js without this hack:This is definitely not supported and also doesn't work with turbopack. This can be used to test them temporarily but I suspect that they just don't work there.
Manual source map analysis
You can analyze source maps using Evan Wallace's Source Map Visualization tool which will help to verify the accuracy and quality of source maps. This is what I used extensively while developing this implementation.
It'll help verify that custom, user CSS maps back to itself in the input, that generated utilities all map back to
@tailwind utilities;, that source locations from imported files are also handled correctly, etc… It also highlights the ranges of stuff so it's easy to see if there are off-by-one errors.It's easiest to use inline source maps with this tool because you can take the CSS file and drop it on the page and it'll analyze it while showing the file content.
If you're using Vite you'll want to access the CSS file with
?directat the end so you don't get a JS module back.Implementation
The source map implementation follows the ECMA-426 specification and includes several key components to aid in that goal:
Source Location Tracking
Each emittable AST node in the compilation pipeline tracks two types of source locations:
src: Original source location - [source file, start offset, end offset]dst: Generated source location - [output file, start offset, end offset]This dual tracking allows us to maintain mappings between the original source and generated output for things like user CSS, generated utilities, uses of
@apply, and tracking theme variables.It is important to note that source locations for nodes never overlap within a file which helps simplify source map generation. As such each type of node tracks a specific piece of itself rather than its entire "block":
SourceLocationrepresents@/*and end*/markersWindows line endings when parsing CSS
Because our AST tracks nodes through offsets we must ensure that any mutations to the file do not change the lenth of the string. We were previously replacing
\r\nwith\n(see filter code points from the spec) — which changes the length of the string and all offsets may end up incorrect. The CSS parser was updated to handle the CRLF token directly by skipping over the\rand letting remaining code handle\nas it did previously. Some additional tweaks were required when "peeking" the input but those changes were fairly small.Tracking of imports
Source maps need paths to the actual imported stylesheets but the resolve step for stylesheets happens inside the call to
loadStylesheetwhich make the file path unavailable to us. Because of this theloadStylesheetAPI was augmented such that it has to return apathproperty that we can then use to identify imported sources. I've also made the same change to theloadModuleAPI for consistency but nothing currently uses this property.The
pathproperty likely makesbaseredundant but elminating that (if we even want to) is a future task.Optimizing the AST
Our optimization pass may intoduce some nodes, for example, fallbacks we create for
@property. These nodes are linked back to@tailwind utilitiesas ultimately that is what is responsible for creating them.Line Offset Tables
A key component to our source map generation is the line offset table, which was inspired by some ESBuild internals. It stores a sorted list of offsets for the start of each line allowing us to translate offsets to line/column
Positions inO(log N)time and fromPositions to offsets inO(1)time. Creation of the table takesO(N)time.This means that we can store code point offsets for source locations and not have to worry about computing or tracking line/column numbers during parsing and serialization. Only when a source map is generated do these offsets need to be computed. This ensures the performance penalty when not using source maps is minimal.
Source Map Generation
The source map returned by
buildSourceMap()is designed to follow the ECMA-426 spec. Because that spec is not completely finalized we consider the result ofbuildSourceMap()to be internal API that may change as the spec chamges.The produces source map is a "decoded" map such that all sources and mappings are in an object graph. A library like
source-map-jsmust be used to convert this to an encoded source map of the right version where mappings are encoded with base 64 VLQs.Any specific integration (Vite, PostCSS, etc…) can then use
toSourceMap()from@tailwindcss/nodeto convert from the internal source map to an spec-compliant encoded source map that can be understood by other tools.Handling minification in Lightning
Since we use Lightning CSS for optimization, and it takes in an input map, we generate an encoded source map that we then pass to lightning. The output source map from lighting itself is then passed back in during the second optimization pass. The final map is then passed from lightning to the CLI (but not Vite or PostCSS — see the limitations section for details).
In some cases we have to "fix up" the output CSS. When this happens we use
magic-stringto do the replacement in a way that is trackable and@amppproject/remappingto map that change back onto the original source map. Once the need for these fix ups disappear these dependencies can go away.Notes:
How source locations flow through the system
@tailwind utilitiesunless generated by@apply.