Track source locations through @plugin and @config (#18329)
Fixes https://github.com/tailwindlabs/tailwindcss-forms/issues/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.
This commit is contained in:
parent
9169d73aad
commit
2941a7b5c2
5 changed files with 137 additions and 37 deletions
|
|
@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Don't consider the global important state in `@apply` ([#18404](https://github.com/tailwindlabs/tailwindcss/pull/18404))
|
||||
- Fix trailing `)` from interfering with extraction in Clojure keywords ([#18345](https://github.com/tailwindlabs/tailwindcss/pull/18345))
|
||||
- Detect classes inside Elixir charlist, word list, and string sigils ([#18432](https://github.com/tailwindlabs/tailwindcss/pull/18432))
|
||||
- Track source locations through `@plugin` and `@config` ([#18345](https://github.com/tailwindlabs/tailwindcss/pull/18345))
|
||||
|
||||
## [4.1.11] - 2025-06-26
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Features } from '..'
|
||||
import { styleRule, toCss, walk, WalkAction, type AstNode } from '../ast'
|
||||
import type { DesignSystem } from '../design-system'
|
||||
import type { SourceLocation } from '../source-maps/source'
|
||||
import { segment } from '../utils/segment'
|
||||
import { applyConfigToTheme } from './apply-config-to-theme'
|
||||
import { applyKeyframesToTheme } from './apply-keyframes-to-theme'
|
||||
|
|
@ -38,9 +39,16 @@ export async function applyCompatibilityHooks({
|
|||
sources: { base: string; pattern: string; negated: boolean }[]
|
||||
}) {
|
||||
let features = Features.None
|
||||
let pluginPaths: [{ id: string; base: string; reference: boolean }, CssPluginOptions | null][] =
|
||||
[]
|
||||
let configPaths: { id: string; base: string; reference: boolean }[] = []
|
||||
let pluginPaths: [
|
||||
{ id: string; base: string; reference: boolean; src: SourceLocation | undefined },
|
||||
CssPluginOptions | null,
|
||||
][] = []
|
||||
let configPaths: {
|
||||
id: string
|
||||
base: string
|
||||
reference: boolean
|
||||
src: SourceLocation | undefined
|
||||
}[] = []
|
||||
|
||||
walk(ast, (node, { parent, replaceWith, context }) => {
|
||||
if (node.kind !== 'at-rule') return
|
||||
|
|
@ -100,7 +108,12 @@ export async function applyCompatibilityHooks({
|
|||
}
|
||||
|
||||
pluginPaths.push([
|
||||
{ id: pluginPath, base: context.base as string, reference: !!context.reference },
|
||||
{
|
||||
id: pluginPath,
|
||||
base: context.base as string,
|
||||
reference: !!context.reference,
|
||||
src: node.src,
|
||||
},
|
||||
Object.keys(options).length > 0 ? options : null,
|
||||
])
|
||||
|
||||
|
|
@ -123,6 +136,7 @@ export async function applyCompatibilityHooks({
|
|||
id: node.params.slice(1, -1),
|
||||
base: context.base as string,
|
||||
reference: !!context.reference,
|
||||
src: node.src,
|
||||
})
|
||||
replaceWith([])
|
||||
features |= Features.JsPluginCompat
|
||||
|
|
@ -162,18 +176,19 @@ export async function applyCompatibilityHooks({
|
|||
|
||||
let [configs, pluginDetails] = await Promise.all([
|
||||
Promise.all(
|
||||
configPaths.map(async ({ id, base, reference }) => {
|
||||
configPaths.map(async ({ id, base, reference, src }) => {
|
||||
let loaded = await loadModule(id, base, 'config')
|
||||
return {
|
||||
path: id,
|
||||
base: loaded.base,
|
||||
config: loaded.module as UserConfig,
|
||||
reference,
|
||||
src,
|
||||
}
|
||||
}),
|
||||
),
|
||||
Promise.all(
|
||||
pluginPaths.map(async ([{ id, base, reference }, pluginOptions]) => {
|
||||
pluginPaths.map(async ([{ id, base, reference, src }, pluginOptions]) => {
|
||||
let loaded = await loadModule(id, base, 'plugin')
|
||||
return {
|
||||
path: id,
|
||||
|
|
@ -181,6 +196,7 @@ export async function applyCompatibilityHooks({
|
|||
plugin: loaded.module as Plugin,
|
||||
options: pluginOptions,
|
||||
reference,
|
||||
src,
|
||||
}
|
||||
}),
|
||||
),
|
||||
|
|
@ -215,6 +231,7 @@ function upgradeToFullPluginSupport({
|
|||
base: string
|
||||
config: UserConfig
|
||||
reference: boolean
|
||||
src: SourceLocation | undefined
|
||||
}[]
|
||||
pluginDetails: {
|
||||
path: string
|
||||
|
|
@ -222,6 +239,7 @@ function upgradeToFullPluginSupport({
|
|||
plugin: Plugin
|
||||
options: CssPluginOptions | null
|
||||
reference: boolean
|
||||
src: SourceLocation | undefined
|
||||
}[]
|
||||
}) {
|
||||
let features = Features.None
|
||||
|
|
@ -231,6 +249,7 @@ function upgradeToFullPluginSupport({
|
|||
config: { plugins: [detail.plugin] },
|
||||
base: detail.base,
|
||||
reference: detail.reference,
|
||||
src: detail.src,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -239,6 +258,7 @@ function upgradeToFullPluginSupport({
|
|||
config: { plugins: [detail.plugin(detail.options)] },
|
||||
base: detail.base,
|
||||
reference: detail.reference,
|
||||
src: detail.src,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -248,15 +268,32 @@ function upgradeToFullPluginSupport({
|
|||
let userConfig = [...pluginConfigs, ...configs]
|
||||
|
||||
let { resolvedConfig } = resolveConfig(designSystem, [
|
||||
{ config: createCompatConfig(designSystem.theme), base, reference: true },
|
||||
{ config: createCompatConfig(designSystem.theme), base, reference: true, src: undefined },
|
||||
...userConfig,
|
||||
{ config: { plugins: [darkModePlugin] }, base, reference: true },
|
||||
{ config: { plugins: [darkModePlugin] }, base, reference: true, src: undefined },
|
||||
])
|
||||
let { resolvedConfig: resolvedUserConfig, replacedThemeKeys } = resolveConfig(
|
||||
designSystem,
|
||||
userConfig,
|
||||
)
|
||||
|
||||
let pluginApiConfig = {
|
||||
designSystem,
|
||||
ast,
|
||||
resolvedConfig,
|
||||
featuresRef: {
|
||||
set current(value: number) {
|
||||
features |= value
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
let sharedPluginApi = buildPluginApi({
|
||||
...pluginApiConfig,
|
||||
referenceMode: false,
|
||||
src: undefined,
|
||||
})
|
||||
|
||||
// Replace `resolveThemeValue` with a version that is backwards compatible
|
||||
// with dot-notation but also aware of any JS theme configurations registered
|
||||
// by plugins or JS config files. This is significantly slower than just
|
||||
|
|
@ -270,7 +307,7 @@ function upgradeToFullPluginSupport({
|
|||
return defaultResolveThemeValue(path, forceInline)
|
||||
}
|
||||
|
||||
let resolvedValue = pluginApi.theme(path, undefined)
|
||||
let resolvedValue = sharedPluginApi.theme(path, undefined)
|
||||
|
||||
if (Array.isArray(resolvedValue) && resolvedValue.length === 2) {
|
||||
// When a tuple is returned, return the first element
|
||||
|
|
@ -285,27 +322,17 @@ function upgradeToFullPluginSupport({
|
|||
}
|
||||
}
|
||||
|
||||
let pluginApiConfig = {
|
||||
designSystem,
|
||||
ast,
|
||||
resolvedConfig,
|
||||
featuresRef: {
|
||||
set current(value: number) {
|
||||
features |= value
|
||||
},
|
||||
},
|
||||
}
|
||||
for (let { handler, reference, src } of resolvedConfig.plugins) {
|
||||
// Each plugin gets its own instance of the plugin API because nodes added
|
||||
// to the AST may need to point to the `@config` or `@plugin` that they
|
||||
// originated from
|
||||
let api = buildPluginApi({
|
||||
...pluginApiConfig,
|
||||
referenceMode: reference ?? false,
|
||||
src,
|
||||
})
|
||||
|
||||
let pluginApi = buildPluginApi({ ...pluginApiConfig, referenceMode: false })
|
||||
let referenceModePluginApi = undefined
|
||||
|
||||
for (let { handler, reference } of resolvedConfig.plugins) {
|
||||
if (reference) {
|
||||
referenceModePluginApi ||= buildPluginApi({ ...pluginApiConfig, referenceMode: true })
|
||||
handler(referenceModePluginApi)
|
||||
} else {
|
||||
handler(pluginApi)
|
||||
}
|
||||
handler(api)
|
||||
}
|
||||
|
||||
// Merge the user-configured theme keys into the design system. The compat
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { DesignSystem } from '../../design-system'
|
||||
import type { SourceLocation } from '../../source-maps/source'
|
||||
import colors from '../colors'
|
||||
import type { PluginWithConfig } from '../plugin-api'
|
||||
import { createThemeFn } from '../plugin-functions'
|
||||
|
|
@ -16,6 +17,7 @@ export interface ConfigFile {
|
|||
base: string
|
||||
config: UserConfig
|
||||
reference: boolean
|
||||
src: SourceLocation | undefined
|
||||
}
|
||||
|
||||
interface ResolutionContext {
|
||||
|
|
@ -131,7 +133,7 @@ export type PluginUtils = {
|
|||
|
||||
function extractConfigs(
|
||||
ctx: ResolutionContext,
|
||||
{ config, base, path, reference }: ConfigFile,
|
||||
{ config, base, path, reference, src }: ConfigFile,
|
||||
): void {
|
||||
let plugins: PluginWithConfig[] = []
|
||||
|
||||
|
|
@ -140,17 +142,17 @@ function extractConfigs(
|
|||
if ('__isOptionsFunction' in plugin) {
|
||||
// Happens with `plugin.withOptions()` when no options were passed:
|
||||
// e.g. `require("my-plugin")` instead of `require("my-plugin")(options)`
|
||||
plugins.push({ ...plugin(), reference })
|
||||
plugins.push({ ...plugin(), reference, src })
|
||||
} else if ('handler' in plugin) {
|
||||
// Happens with `plugin(…)`:
|
||||
// e.g. `require("my-plugin")`
|
||||
//
|
||||
// or with `plugin.withOptions()` when the user passed options:
|
||||
// e.g. `require("my-plugin")(options)`
|
||||
plugins.push({ ...plugin, reference })
|
||||
plugins.push({ ...plugin, reference, src })
|
||||
} else {
|
||||
// Just a plain function without using the plugin(…) API
|
||||
plugins.push({ handler: plugin, reference })
|
||||
plugins.push({ handler: plugin, reference, src })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -162,7 +164,7 @@ function extractConfigs(
|
|||
}
|
||||
|
||||
for (let preset of config.presets ?? []) {
|
||||
extractConfigs(ctx, { path, base, config: preset, reference })
|
||||
extractConfigs(ctx, { path, base, config: preset, reference, src })
|
||||
}
|
||||
|
||||
// Apply configs from plugins
|
||||
|
|
@ -170,7 +172,13 @@ function extractConfigs(
|
|||
ctx.plugins.push(plugin)
|
||||
|
||||
if (plugin.config) {
|
||||
extractConfigs(ctx, { path, base, config: plugin.config, reference: !!plugin.reference })
|
||||
extractConfigs(ctx, {
|
||||
path,
|
||||
base,
|
||||
config: plugin.config,
|
||||
reference: !!plugin.reference,
|
||||
src: plugin.src ?? src,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type { Candidate, CandidateModifier, NamedUtilityValue } from '../candida
|
|||
import { substituteFunctions } from '../css-functions'
|
||||
import * as CSS from '../css-parser'
|
||||
import type { DesignSystem } from '../design-system'
|
||||
import type { SourceLocation } from '../source-maps/source'
|
||||
import { withAlpha } from '../utilities'
|
||||
import { DefaultMap } from '../utils/default-map'
|
||||
import { escape } from '../utils/escape'
|
||||
|
|
@ -24,6 +25,7 @@ export type PluginWithConfig = {
|
|||
|
||||
/** @internal */
|
||||
reference?: boolean
|
||||
src?: SourceLocation | undefined
|
||||
}
|
||||
export type PluginWithOptions<T> = {
|
||||
(options?: T): PluginWithConfig
|
||||
|
|
@ -93,19 +95,25 @@ export function buildPluginApi({
|
|||
resolvedConfig,
|
||||
featuresRef,
|
||||
referenceMode,
|
||||
src,
|
||||
}: {
|
||||
designSystem: DesignSystem
|
||||
ast: AstNode[]
|
||||
resolvedConfig: ResolvedConfig
|
||||
featuresRef: { current: Features }
|
||||
referenceMode: boolean
|
||||
src: SourceLocation | undefined
|
||||
}): PluginAPI {
|
||||
let api: PluginAPI = {
|
||||
addBase(css) {
|
||||
if (referenceMode) return
|
||||
let baseNodes = objectToAst(css)
|
||||
featuresRef.current |= substituteFunctions(baseNodes, designSystem)
|
||||
ast.push(atRule('@layer', 'base', baseNodes))
|
||||
let rule = atRule('@layer', 'base', baseNodes)
|
||||
walk([rule], (node) => {
|
||||
node.src = src
|
||||
})
|
||||
ast.push(rule)
|
||||
},
|
||||
|
||||
addVariant(name, variant) {
|
||||
|
|
@ -255,7 +263,11 @@ export function buildPluginApi({
|
|||
for (let [name, css] of entries) {
|
||||
if (name.startsWith('@keyframes ')) {
|
||||
if (!referenceMode) {
|
||||
ast.push(rule(name, objectToAst(css)))
|
||||
let keyframes = rule(name, objectToAst(css))
|
||||
walk([keyframes], (node) => {
|
||||
node.src = src
|
||||
})
|
||||
ast.push(keyframes)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
|
|
|||
|
|
@ -419,3 +419,55 @@ test('license comments with new lines preserve source locations', async ({ expec
|
|||
'input.css: 2:11 <- 2:11',
|
||||
])
|
||||
})
|
||||
|
||||
test('Source locations for `addBase` point to the `@plugin` that generated them', async ({
|
||||
expect,
|
||||
}) => {
|
||||
let { sources, annotations } = await run({
|
||||
input: dedent`
|
||||
@plugin "./plugin.js";
|
||||
@config "./config.js";
|
||||
`,
|
||||
options: {
|
||||
async loadModule(id, base) {
|
||||
if (id === './plugin.js') {
|
||||
return {
|
||||
module: createPlugin(({ addBase }) => {
|
||||
addBase({ body: { color: 'red' } })
|
||||
}),
|
||||
base,
|
||||
path: '',
|
||||
}
|
||||
}
|
||||
|
||||
if (id === './config.js') {
|
||||
return {
|
||||
module: {
|
||||
plugins: [
|
||||
createPlugin(({ addBase }) => {
|
||||
addBase({ body: { color: 'green' } })
|
||||
}),
|
||||
],
|
||||
},
|
||||
base,
|
||||
path: '',
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`unknown module ${id}`)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(sources).toEqual(['input.css'])
|
||||
|
||||
expect(annotations).toEqual([
|
||||
//
|
||||
'input.css: 1:0-12 <- 1:0-21',
|
||||
'input.css: 2:2-7 <- 1:0-21',
|
||||
'input.css: 3:4-14 <- 1:0-21',
|
||||
'input.css: 6:0-12 <- 2:0-21',
|
||||
'input.css: 7:2-7 <- 2:0-21',
|
||||
'input.css: 8:4-16 <- 2:0-21',
|
||||
])
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue