Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add @import "…" reference #15228

Merged
merged 10 commits into from
Dec 3, 2024
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