Skip to content

Commit 2941a7b

Browse files
Track source locations through @plugin and @config (#18329)
Fixes tailwindlabs/tailwindcss-forms#182 Inside JS plugins and configs we weren't tracking source location info so using things like `addBase(…)` could result in warnings inside Vite's url rewriting PostCSS plugin because not all declarations had a source location. The goal here is for calls to `addBase` to generate CSS that points back to the `@plugin` or `@config` that resulted in it being called.
1 parent 9169d73 commit 2941a7b

File tree

5 files changed

+137
-37
lines changed

5 files changed

+137
-37
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Don't consider the global important state in `@apply` ([#18404](https://github.com/tailwindlabs/tailwindcss/pull/18404))
1313
- Fix trailing `)` from interfering with extraction in Clojure keywords ([#18345](https://github.com/tailwindlabs/tailwindcss/pull/18345))
1414
- Detect classes inside Elixir charlist, word list, and string sigils ([#18432](https://github.com/tailwindlabs/tailwindcss/pull/18432))
15+
- Track source locations through `@plugin` and `@config` ([#18345](https://github.com/tailwindlabs/tailwindcss/pull/18345))
1516

1617
## [4.1.11] - 2025-06-26
1718

packages/tailwindcss/src/compat/apply-compat-hooks.ts

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Features } from '..'
22
import { styleRule, toCss, walk, WalkAction, type AstNode } from '../ast'
33
import type { DesignSystem } from '../design-system'
4+
import type { SourceLocation } from '../source-maps/source'
45
import { segment } from '../utils/segment'
56
import { applyConfigToTheme } from './apply-config-to-theme'
67
import { applyKeyframesToTheme } from './apply-keyframes-to-theme'
@@ -38,9 +39,16 @@ export async function applyCompatibilityHooks({
3839
sources: { base: string; pattern: string; negated: boolean }[]
3940
}) {
4041
let features = Features.None
41-
let pluginPaths: [{ id: string; base: string; reference: boolean }, CssPluginOptions | null][] =
42-
[]
43-
let configPaths: { id: string; base: string; reference: boolean }[] = []
42+
let pluginPaths: [
43+
{ id: string; base: string; reference: boolean; src: SourceLocation | undefined },
44+
CssPluginOptions | null,
45+
][] = []
46+
let configPaths: {
47+
id: string
48+
base: string
49+
reference: boolean
50+
src: SourceLocation | undefined
51+
}[] = []
4452

4553
walk(ast, (node, { parent, replaceWith, context }) => {
4654
if (node.kind !== 'at-rule') return
@@ -100,7 +108,12 @@ export async function applyCompatibilityHooks({
100108
}
101109

102110
pluginPaths.push([
103-
{ id: pluginPath, base: context.base as string, reference: !!context.reference },
111+
{
112+
id: pluginPath,
113+
base: context.base as string,
114+
reference: !!context.reference,
115+
src: node.src,
116+
},
104117
Object.keys(options).length > 0 ? options : null,
105118
])
106119

@@ -123,6 +136,7 @@ export async function applyCompatibilityHooks({
123136
id: node.params.slice(1, -1),
124137
base: context.base as string,
125138
reference: !!context.reference,
139+
src: node.src,
126140
})
127141
replaceWith([])
128142
features |= Features.JsPluginCompat
@@ -162,25 +176,27 @@ export async function applyCompatibilityHooks({
162176

163177
let [configs, pluginDetails] = await Promise.all([
164178
Promise.all(
165-
configPaths.map(async ({ id, base, reference }) => {
179+
configPaths.map(async ({ id, base, reference, src }) => {
166180
let loaded = await loadModule(id, base, 'config')
167181
return {
168182
path: id,
169183
base: loaded.base,
170184
config: loaded.module as UserConfig,
171185
reference,
186+
src,
172187
}
173188
}),
174189
),
175190
Promise.all(
176-
pluginPaths.map(async ([{ id, base, reference }, pluginOptions]) => {
191+
pluginPaths.map(async ([{ id, base, reference, src }, pluginOptions]) => {
177192
let loaded = await loadModule(id, base, 'plugin')
178193
return {
179194
path: id,
180195
base: loaded.base,
181196
plugin: loaded.module as Plugin,
182197
options: pluginOptions,
183198
reference,
199+
src,
184200
}
185201
}),
186202
),
@@ -215,13 +231,15 @@ function upgradeToFullPluginSupport({
215231
base: string
216232
config: UserConfig
217233
reference: boolean
234+
src: SourceLocation | undefined
218235
}[]
219236
pluginDetails: {
220237
path: string
221238
base: string
222239
plugin: Plugin
223240
options: CssPluginOptions | null
224241
reference: boolean
242+
src: SourceLocation | undefined
225243
}[]
226244
}) {
227245
let features = Features.None
@@ -231,6 +249,7 @@ function upgradeToFullPluginSupport({
231249
config: { plugins: [detail.plugin] },
232250
base: detail.base,
233251
reference: detail.reference,
252+
src: detail.src,
234253
}
235254
}
236255

@@ -239,6 +258,7 @@ function upgradeToFullPluginSupport({
239258
config: { plugins: [detail.plugin(detail.options)] },
240259
base: detail.base,
241260
reference: detail.reference,
261+
src: detail.src,
242262
}
243263
}
244264

@@ -248,15 +268,32 @@ function upgradeToFullPluginSupport({
248268
let userConfig = [...pluginConfigs, ...configs]
249269

250270
let { resolvedConfig } = resolveConfig(designSystem, [
251-
{ config: createCompatConfig(designSystem.theme), base, reference: true },
271+
{ config: createCompatConfig(designSystem.theme), base, reference: true, src: undefined },
252272
...userConfig,
253-
{ config: { plugins: [darkModePlugin] }, base, reference: true },
273+
{ config: { plugins: [darkModePlugin] }, base, reference: true, src: undefined },
254274
])
255275
let { resolvedConfig: resolvedUserConfig, replacedThemeKeys } = resolveConfig(
256276
designSystem,
257277
userConfig,
258278
)
259279

280+
let pluginApiConfig = {
281+
designSystem,
282+
ast,
283+
resolvedConfig,
284+
featuresRef: {
285+
set current(value: number) {
286+
features |= value
287+
},
288+
},
289+
}
290+
291+
let sharedPluginApi = buildPluginApi({
292+
...pluginApiConfig,
293+
referenceMode: false,
294+
src: undefined,
295+
})
296+
260297
// Replace `resolveThemeValue` with a version that is backwards compatible
261298
// with dot-notation but also aware of any JS theme configurations registered
262299
// by plugins or JS config files. This is significantly slower than just
@@ -270,7 +307,7 @@ function upgradeToFullPluginSupport({
270307
return defaultResolveThemeValue(path, forceInline)
271308
}
272309

273-
let resolvedValue = pluginApi.theme(path, undefined)
310+
let resolvedValue = sharedPluginApi.theme(path, undefined)
274311

275312
if (Array.isArray(resolvedValue) && resolvedValue.length === 2) {
276313
// When a tuple is returned, return the first element
@@ -285,27 +322,17 @@ function upgradeToFullPluginSupport({
285322
}
286323
}
287324

288-
let pluginApiConfig = {
289-
designSystem,
290-
ast,
291-
resolvedConfig,
292-
featuresRef: {
293-
set current(value: number) {
294-
features |= value
295-
},
296-
},
297-
}
298-
299-
let pluginApi = buildPluginApi({ ...pluginApiConfig, referenceMode: false })
300-
let referenceModePluginApi = undefined
325+
for (let { handler, reference, src } of resolvedConfig.plugins) {
326+
// Each plugin gets its own instance of the plugin API because nodes added
327+
// to the AST may need to point to the `@config` or `@plugin` that they
328+
// originated from
329+
let api = buildPluginApi({
330+
...pluginApiConfig,
331+
referenceMode: reference ?? false,
332+
src,
333+
})
301334

302-
for (let { handler, reference } of resolvedConfig.plugins) {
303-
if (reference) {
304-
referenceModePluginApi ||= buildPluginApi({ ...pluginApiConfig, referenceMode: true })
305-
handler(referenceModePluginApi)
306-
} else {
307-
handler(pluginApi)
308-
}
335+
handler(api)
309336
}
310337

311338
// Merge the user-configured theme keys into the design system. The compat

packages/tailwindcss/src/compat/config/resolve-config.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { DesignSystem } from '../../design-system'
2+
import type { SourceLocation } from '../../source-maps/source'
23
import colors from '../colors'
34
import type { PluginWithConfig } from '../plugin-api'
45
import { createThemeFn } from '../plugin-functions'
@@ -16,6 +17,7 @@ export interface ConfigFile {
1617
base: string
1718
config: UserConfig
1819
reference: boolean
20+
src: SourceLocation | undefined
1921
}
2022

2123
interface ResolutionContext {
@@ -131,7 +133,7 @@ export type PluginUtils = {
131133

132134
function extractConfigs(
133135
ctx: ResolutionContext,
134-
{ config, base, path, reference }: ConfigFile,
136+
{ config, base, path, reference, src }: ConfigFile,
135137
): void {
136138
let plugins: PluginWithConfig[] = []
137139

@@ -140,17 +142,17 @@ function extractConfigs(
140142
if ('__isOptionsFunction' in plugin) {
141143
// Happens with `plugin.withOptions()` when no options were passed:
142144
// e.g. `require("my-plugin")` instead of `require("my-plugin")(options)`
143-
plugins.push({ ...plugin(), reference })
145+
plugins.push({ ...plugin(), reference, src })
144146
} else if ('handler' in plugin) {
145147
// Happens with `plugin(…)`:
146148
// e.g. `require("my-plugin")`
147149
//
148150
// or with `plugin.withOptions()` when the user passed options:
149151
// e.g. `require("my-plugin")(options)`
150-
plugins.push({ ...plugin, reference })
152+
plugins.push({ ...plugin, reference, src })
151153
} else {
152154
// Just a plain function without using the plugin(…) API
153-
plugins.push({ handler: plugin, reference })
155+
plugins.push({ handler: plugin, reference, src })
154156
}
155157
}
156158

@@ -162,15 +164,21 @@ function extractConfigs(
162164
}
163165

164166
for (let preset of config.presets ?? []) {
165-
extractConfigs(ctx, { path, base, config: preset, reference })
167+
extractConfigs(ctx, { path, base, config: preset, reference, src })
166168
}
167169

168170
// Apply configs from plugins
169171
for (let plugin of plugins) {
170172
ctx.plugins.push(plugin)
171173

172174
if (plugin.config) {
173-
extractConfigs(ctx, { path, base, config: plugin.config, reference: !!plugin.reference })
175+
extractConfigs(ctx, {
176+
path,
177+
base,
178+
config: plugin.config,
179+
reference: !!plugin.reference,
180+
src: plugin.src ?? src,
181+
})
174182
}
175183
}
176184

packages/tailwindcss/src/compat/plugin-api.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { Candidate, CandidateModifier, NamedUtilityValue } from '../candida
55
import { substituteFunctions } from '../css-functions'
66
import * as CSS from '../css-parser'
77
import type { DesignSystem } from '../design-system'
8+
import type { SourceLocation } from '../source-maps/source'
89
import { withAlpha } from '../utilities'
910
import { DefaultMap } from '../utils/default-map'
1011
import { escape } from '../utils/escape'
@@ -24,6 +25,7 @@ export type PluginWithConfig = {
2425

2526
/** @internal */
2627
reference?: boolean
28+
src?: SourceLocation | undefined
2729
}
2830
export type PluginWithOptions<T> = {
2931
(options?: T): PluginWithConfig
@@ -93,19 +95,25 @@ export function buildPluginApi({
9395
resolvedConfig,
9496
featuresRef,
9597
referenceMode,
98+
src,
9699
}: {
97100
designSystem: DesignSystem
98101
ast: AstNode[]
99102
resolvedConfig: ResolvedConfig
100103
featuresRef: { current: Features }
101104
referenceMode: boolean
105+
src: SourceLocation | undefined
102106
}): PluginAPI {
103107
let api: PluginAPI = {
104108
addBase(css) {
105109
if (referenceMode) return
106110
let baseNodes = objectToAst(css)
107111
featuresRef.current |= substituteFunctions(baseNodes, designSystem)
108-
ast.push(atRule('@layer', 'base', baseNodes))
112+
let rule = atRule('@layer', 'base', baseNodes)
113+
walk([rule], (node) => {
114+
node.src = src
115+
})
116+
ast.push(rule)
109117
},
110118

111119
addVariant(name, variant) {
@@ -255,7 +263,11 @@ export function buildPluginApi({
255263
for (let [name, css] of entries) {
256264
if (name.startsWith('@keyframes ')) {
257265
if (!referenceMode) {
258-
ast.push(rule(name, objectToAst(css)))
266+
let keyframes = rule(name, objectToAst(css))
267+
walk([keyframes], (node) => {
268+
node.src = src
269+
})
270+
ast.push(keyframes)
259271
}
260272
continue
261273
}

packages/tailwindcss/src/source-maps/source-map.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,3 +419,55 @@ test('license comments with new lines preserve source locations', async ({ expec
419419
'input.css: 2:11 <- 2:11',
420420
])
421421
})
422+
423+
test('Source locations for `addBase` point to the `@plugin` that generated them', async ({
424+
expect,
425+
}) => {
426+
let { sources, annotations } = await run({
427+
input: dedent`
428+
@plugin "./plugin.js";
429+
@config "./config.js";
430+
`,
431+
options: {
432+
async loadModule(id, base) {
433+
if (id === './plugin.js') {
434+
return {
435+
module: createPlugin(({ addBase }) => {
436+
addBase({ body: { color: 'red' } })
437+
}),
438+
base,
439+
path: '',
440+
}
441+
}
442+
443+
if (id === './config.js') {
444+
return {
445+
module: {
446+
plugins: [
447+
createPlugin(({ addBase }) => {
448+
addBase({ body: { color: 'green' } })
449+
}),
450+
],
451+
},
452+
base,
453+
path: '',
454+
}
455+
}
456+
457+
throw new Error(`unknown module ${id}`)
458+
},
459+
},
460+
})
461+
462+
expect(sources).toEqual(['input.css'])
463+
464+
expect(annotations).toEqual([
465+
//
466+
'input.css: 1:0-12 <- 1:0-21',
467+
'input.css: 2:2-7 <- 1:0-21',
468+
'input.css: 3:4-14 <- 1:0-21',
469+
'input.css: 6:0-12 <- 2:0-21',
470+
'input.css: 7:2-7 <- 2:0-21',
471+
'input.css: 8:4-16 <- 2:0-21',
472+
])
473+
})

0 commit comments

Comments
 (0)