Skip to content
11 changes: 6 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Parallelize parsing of individual source files ([#15270](https://github.com/tailwindlabs/tailwindcss/pull/15270))
- Support Vite 6 in the Vite plugin ([#15274](https://github.com/tailwindlabs/tailwindcss/issues/15274))
- Add a new `@import "…" reference` syntax for only importing the Tailwind CSS configurations of a stylesheets ([#15228](https://github.com/tailwindlabs/tailwindcss/pull/15228))

### Fixed

- Ensure absolute `url()`s inside imported CSS files are not rebased when using `@tailwindcss/vite` ([#15275](https://github.com/tailwindlabs/tailwindcss/pull/15275))
Expand All @@ -15,11 +21,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ensure other plugins can run after `@tailwindcss/postcss` ([#15273](https://github.com/tailwindlabs/tailwindcss/pull/15273))
- Rebase `url()` inside imported CSS files when using Vite with the `@tailwindcss/postcss` extension ([#15273](https://github.com/tailwindlabs/tailwindcss/pull/15273))

### Added

- Parallelize parsing of individual source files ([#15270](https://github.com/tailwindlabs/tailwindcss/pull/15270))
- Support Vite 6 in the Vite plugin ([#15274](https://github.com/tailwindlabs/tailwindcss/issues/15274))

## [4.0.0-beta.4] - 2024-11-29

### Fixed
Expand Down
5 changes: 1 addition & 4 deletions integrations/vite/svelte.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,7 @@ test(
target: document.body,
})
`,
'src/index.css': css`
@import 'tailwindcss/theme' theme(reference);
@import 'tailwindcss/utilities';
`,
'src/index.css': css`@import 'tailwindcss' reference;`,
'src/App.svelte': html`
<script>
import './index.css'
Expand Down
6 changes: 2 additions & 4 deletions integrations/vite/vue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,13 @@ test(
`,
'src/App.vue': html`
<style>
@import 'tailwindcss/utilities';
@import 'tailwindcss/theme' theme(reference);
@import 'tailwindcss' reference;
.foo {
@apply text-red-500;
}
</style>
<style scoped>
@import 'tailwindcss/utilities';
@import 'tailwindcss/theme' theme(reference);
@import 'tailwindcss' reference;
:deep(.bar) {
color: red;
}
Expand Down
12 changes: 6 additions & 6 deletions packages/tailwindcss/src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export type Comment = {

export type Context = {
kind: 'context'
context: Record<string, string>
context: Record<string, string | boolean>
nodes: AstNode[]
}

Expand Down Expand Up @@ -82,7 +82,7 @@ export function comment(value: string): Comment {
}
}

export function context(context: Record<string, string>, nodes: AstNode[]): Context {
export function context(context: Record<string, string | boolean>, nodes: AstNode[]): Context {
return {
kind: 'context',
context,
Expand Down Expand Up @@ -115,12 +115,12 @@ export function walk(
utils: {
parent: AstNode | null
replaceWith(newNode: AstNode | AstNode[]): void
context: Record<string, string>
context: Record<string, string | boolean>
path: AstNode[]
},
) => void | WalkAction,
parentPath: AstNode[] = [],
context: Record<string, string> = {},
context: Record<string, string | boolean> = {},
) {
for (let i = 0; i < ast.length; i++) {
let node = ast[i]
Expand Down Expand Up @@ -175,12 +175,12 @@ export function walkDepth(
utils: {
parent: AstNode | null
path: AstNode[]
context: Record<string, string>
context: Record<string, string | boolean>
replaceWith(newNode: AstNode[]): void
},
) => void,
parentPath: AstNode[] = [],
context: Record<string, string> = {},
context: Record<string, string | boolean> = {},
) {
for (let i = 0; i < ast.length; i++) {
let node = ast[i]
Expand Down
41 changes: 29 additions & 12 deletions packages/tailwindcss/src/compat/apply-compat-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ export async function applyCompatibilityHooks({
globs: { origin?: string; pattern: string }[]
}) {
let features = Features.None
let pluginPaths: [{ id: string; base: string }, CssPluginOptions | null][] = []
let configPaths: { id: string; base: string }[] = []
let pluginPaths: [{ id: string; base: string; reference: boolean }, CssPluginOptions | null][] =
[]
let configPaths: { id: string; base: string; reference: boolean }[] = []

walk(ast, (node, { parent, replaceWith, context }) => {
if (node.kind !== 'at-rule') return
Expand Down Expand Up @@ -95,7 +96,7 @@ export async function applyCompatibilityHooks({
}

pluginPaths.push([
{ id: pluginPath, base: context.base },
{ id: pluginPath, base: context.base as string, reference: !!context.reference },
Object.keys(options).length > 0 ? options : null,
])

Expand All @@ -114,7 +115,11 @@ export async function applyCompatibilityHooks({
throw new Error('`@config` cannot be nested.')
}

configPaths.push({ id: node.params.slice(1, -1), base: context.base })
configPaths.push({
id: node.params.slice(1, -1),
base: context.base as string,
reference: !!context.reference,
})
replaceWith([])
features |= Features.JsPluginCompat
return
Expand Down Expand Up @@ -153,23 +158,25 @@ export async function applyCompatibilityHooks({

let [configs, pluginDetails] = await Promise.all([
Promise.all(
configPaths.map(async ({ id, base }) => {
configPaths.map(async ({ id, base, reference }) => {
let loaded = await loadModule(id, base, 'config')
return {
path: id,
base: loaded.base,
config: loaded.module as UserConfig,
reference,
}
}),
),
Promise.all(
pluginPaths.map(async ([{ id, base }, pluginOptions]) => {
pluginPaths.map(async ([{ id, base, reference }, pluginOptions]) => {
let loaded = await loadModule(id, base, 'plugin')
return {
path: id,
base: loaded.base,
plugin: loaded.module as Plugin,
options: pluginOptions,
reference,
}
}),
),
Expand Down Expand Up @@ -203,22 +210,32 @@ function upgradeToFullPluginSupport({
path: string
base: string
config: UserConfig
reference: boolean
}[]
pluginDetails: {
path: string
base: string
plugin: Plugin
options: CssPluginOptions | null
reference: boolean
}[]
}) {
let features = Features.None
let pluginConfigs = pluginDetails.map((detail) => {
if (!detail.options) {
return { config: { plugins: [detail.plugin] }, base: detail.base }
return {
config: { plugins: [detail.plugin] },
base: detail.base,
reference: detail.reference,
}
}

if ('__isOptionsFunction' in detail.plugin) {
return { config: { plugins: [detail.plugin(detail.options)] }, base: detail.base }
return {
config: { plugins: [detail.plugin(detail.options)] },
base: detail.base,
reference: detail.reference,
}
}

throw new Error(`The plugin "${detail.path}" does not accept options`)
Expand All @@ -227,9 +244,9 @@ function upgradeToFullPluginSupport({
let userConfig = [...pluginConfigs, ...configs]

let { resolvedConfig } = resolveConfig(designSystem, [
{ config: createCompatConfig(designSystem.theme), base },
{ config: createCompatConfig(designSystem.theme), base, reference: true },
...userConfig,
{ config: { plugins: [darkModePlugin] }, base },
{ config: { plugins: [darkModePlugin] }, base, reference: true },
])
let { resolvedConfig: resolvedUserConfig, replacedThemeKeys } = resolveConfig(
designSystem,
Expand All @@ -242,8 +259,8 @@ function upgradeToFullPluginSupport({
},
})

for (let { handler } of resolvedConfig.plugins) {
handler(pluginApi)
for (let { handler, reference } of resolvedConfig.plugins) {
handler(reference ? { ...pluginApi, addBase: () => {} } : pluginApi)
}

// Merge the user-configured theme keys into the design system. The compat
Expand Down
16 changes: 10 additions & 6 deletions packages/tailwindcss/src/compat/config/resolve-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface ConfigFile {
path?: string
base: string
config: UserConfig
reference: boolean
}

interface ResolutionContext {
Expand Down Expand Up @@ -128,25 +129,28 @@ export type PluginUtils = {
colors: typeof colors
}

function extractConfigs(ctx: ResolutionContext, { config, base, path }: ConfigFile): void {
function extractConfigs(
ctx: ResolutionContext,
{ config, base, path, reference }: ConfigFile,
): void {
let plugins: PluginWithConfig[] = []

// Normalize plugins so they share the same shape
for (let plugin of config.plugins ?? []) {
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())
plugins.push({ ...plugin(), reference })
} 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)
plugins.push({ ...plugin, reference })
} else {
// Just a plain function without using the plugin(…) API
plugins.push({ handler: plugin })
plugins.push({ handler: plugin, reference })
}
}

Expand All @@ -158,15 +162,15 @@ function extractConfigs(ctx: ResolutionContext, { config, base, path }: ConfigFi
}

for (let preset of config.presets ?? []) {
extractConfigs(ctx, { path, base, config: preset })
extractConfigs(ctx, { path, base, config: preset, reference })
}

// Apply configs from plugins
for (let plugin of plugins) {
ctx.plugins.push(plugin)

if (plugin.config) {
extractConfigs(ctx, { path, base, config: plugin.config })
extractConfigs(ctx, { path, base, config: plugin.config, reference: !!plugin.reference })
}
}

Expand Down
46 changes: 46 additions & 0 deletions packages/tailwindcss/src/compat/plugin-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1490,6 +1490,52 @@ describe('theme', async () => {
})
})

describe('addBase', () => {
test('does not create rules when imported via `@import "…" reference`', async () => {
let input = css`
@tailwind utilities;
@plugin "outside";
@import './inside.css' reference;
`

let compiler = await compile(input, {
loadModule: async (id, base) => {
if (id === 'inside') {
return {
base,
module: plugin(function ({ addBase }) {
addBase({ inside: { color: 'red' } })
}),
}
}
return {
base,
module: plugin(function ({ addBase }) {
addBase({ outside: { color: 'red' } })
}),
}
},
async loadStylesheet() {
return {
content: css`
@plugin "inside";
`,
base: '',
}
},
})

expect(compiler.build([])).toMatchInlineSnapshot(`
"@layer base {
outside {
color: red;
}
}
"
`)
})
})

describe('addVariant', () => {
test('addVariant with string selector', async () => {
let { build } = await compile(
Expand Down
8 changes: 7 additions & 1 deletion packages/tailwindcss/src/compat/plugin-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ import * as SelectorParser from './selector-parser'

export type Config = UserConfig
export type PluginFn = (api: PluginAPI) => void
export type PluginWithConfig = { handler: PluginFn; config?: UserConfig }
export type PluginWithConfig = {
handler: PluginFn;
config?: UserConfig;

/** @internal */
reference?: boolean
}
export type PluginWithOptions<T> = {
(options?: T): PluginWithConfig
__isOptionsFunction: true
Expand Down
Loading