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 938596e8ca4d..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,19 +95,25 @@ 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) { 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 = srcRef.current + }) + ast.push(rule) }, addVariant(name, variant) { diff --git a/packages/tailwindcss/src/source-maps/source-map.test.ts b/packages/tailwindcss/src/source-maps/source-map.test.ts index 0f5ec5483827..e97956735aad 100644 --- a/packages/tailwindcss/src/source-maps/source-map.test.ts +++ b/packages/tailwindcss/src/source-maps/source-map.test.ts @@ -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', + ]) +})