From 14cba79cf1c21af84ac3f93c1c58d6b75057db77 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 17 Jun 2025 10:01:52 -0400 Subject: [PATCH 1/3] tweak --- packages/tailwindcss/src/compat/plugin-api.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/compat/plugin-api.ts b/packages/tailwindcss/src/compat/plugin-api.ts index 938596e8ca4d..d728c4934539 100644 --- a/packages/tailwindcss/src/compat/plugin-api.ts +++ b/packages/tailwindcss/src/compat/plugin-api.ts @@ -105,7 +105,8 @@ export function buildPluginApi({ if (referenceMode) return let baseNodes = objectToAst(css) featuresRef.current |= substituteFunctions(baseNodes, designSystem) - ast.push(atRule('@layer', 'base', baseNodes)) + let rule = atRule('@layer', 'base', baseNodes) + ast.push(rule) }, addVariant(name, variant) { From f2693d951b546195a3d95a63f5aadc53b6f41d1e Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 17 Jun 2025 10:02:33 -0400 Subject: [PATCH 2/3] Track source locations through `@plugin` and `@config` --- .../src/compat/apply-compat-hooks.ts | 45 +++++++++++--- .../src/compat/config/resolve-config.ts | 20 ++++-- packages/tailwindcss/src/compat/plugin-api.ts | 7 +++ .../src/source-maps/source-map.test.ts | 62 +++++++++++++++++++ 4 files changed, 119 insertions(+), 15 deletions(-) diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index fb619561bc13..dea69a69f0c2 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -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,9 +268,9 @@ 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, @@ -285,6 +305,7 @@ function upgradeToFullPluginSupport({ } } + let currentSrc: SourceLocation | undefined = undefined let pluginApiConfig = { designSystem, ast, @@ -294,12 +315,18 @@ function upgradeToFullPluginSupport({ features |= value }, }, + srcRef: { + get current() { + return currentSrc + }, + }, } let pluginApi = buildPluginApi({ ...pluginApiConfig, referenceMode: false }) let referenceModePluginApi = undefined - for (let { handler, reference } of resolvedConfig.plugins) { + for (let { handler, reference, src } of resolvedConfig.plugins) { + currentSrc = src if (reference) { referenceModePluginApi ||= buildPluginApi({ ...pluginApiConfig, referenceMode: true }) handler(referenceModePluginApi) diff --git a/packages/tailwindcss/src/compat/config/resolve-config.ts b/packages/tailwindcss/src/compat/config/resolve-config.ts index 6801c7b60f98..3fdc1f4548ae 100644 --- a/packages/tailwindcss/src/compat/config/resolve-config.ts +++ b/packages/tailwindcss/src/compat/config/resolve-config.ts @@ -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, + }) } } diff --git a/packages/tailwindcss/src/compat/plugin-api.ts b/packages/tailwindcss/src/compat/plugin-api.ts index d728c4934539..b907ba6b52a8 100644 --- a/packages/tailwindcss/src/compat/plugin-api.ts +++ b/packages/tailwindcss/src/compat/plugin-api.ts @@ -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 = { (options?: T): PluginWithConfig @@ -93,12 +95,14 @@ export function buildPluginApi({ resolvedConfig, featuresRef, referenceMode, + srcRef, }: { designSystem: DesignSystem ast: AstNode[] resolvedConfig: ResolvedConfig featuresRef: { current: Features } referenceMode: boolean + srcRef: { current: SourceLocation | undefined } }): PluginAPI { let api: PluginAPI = { addBase(css) { @@ -106,6 +110,9 @@ export function buildPluginApi({ let baseNodes = objectToAst(css) featuresRef.current |= substituteFunctions(baseNodes, designSystem) let rule = atRule('@layer', 'base', baseNodes) + walk([rule], (node) => { + node.src = srcRef.current + }) ast.push(rule) }, diff --git a/packages/tailwindcss/src/source-maps/source-map.test.ts b/packages/tailwindcss/src/source-maps/source-map.test.ts index 0f5ec5483827..335a75eb65bf 100644 --- a/packages/tailwindcss/src/source-maps/source-map.test.ts +++ b/packages/tailwindcss/src/source-maps/source-map.test.ts @@ -419,3 +419,65 @@ test('license comments with new lines preserve source locations', async ({ expec 'input.css: 2:11 <- 2:11', ]) }) + +test.only('Source locations for `addBase` point to the `@plugin` that generated them', async ({ + expect, +}) => { + let { sources, annotations, css } = 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(css).toMatchInlineSnapshot(` + "@layer base { + body { + color: red; + } + } + @layer base { + body { + color: green; + } + } + " + `) + + expect(sources).toEqual(['input.css']) + + expect(annotations).toEqual([ + // + 'input.css: 1:0 <- 1:0-2:0', + 'input.css: 2:11 <- 2:11', + ]) +}) From d1873640a5ed2730c248ae0ec0528eca63d19821 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 17 Jun 2025 10:07:28 -0400 Subject: [PATCH 3/3] update tests --- .../src/source-maps/source-map.test.ts | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/packages/tailwindcss/src/source-maps/source-map.test.ts b/packages/tailwindcss/src/source-maps/source-map.test.ts index 335a75eb65bf..e97956735aad 100644 --- a/packages/tailwindcss/src/source-maps/source-map.test.ts +++ b/packages/tailwindcss/src/source-maps/source-map.test.ts @@ -420,10 +420,10 @@ test('license comments with new lines preserve source locations', async ({ expec ]) }) -test.only('Source locations for `addBase` point to the `@plugin` that generated them', async ({ +test('Source locations for `addBase` point to the `@plugin` that generated them', async ({ expect, }) => { - let { sources, annotations, css } = await run({ + let { sources, annotations } = await run({ input: dedent` @plugin "./plugin.js"; @config "./config.js"; @@ -459,25 +459,15 @@ test.only('Source locations for `addBase` point to the `@plugin` that generated }, }) - expect(css).toMatchInlineSnapshot(` - "@layer base { - body { - color: red; - } - } - @layer base { - body { - color: green; - } - } - " - `) - expect(sources).toEqual(['input.css']) expect(annotations).toEqual([ // - 'input.css: 1:0 <- 1:0-2:0', - 'input.css: 2:11 <- 2:11', + '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', ]) })