Skip to content

Commit

Permalink
Add @import "…" reference (#15228)
Browse files Browse the repository at this point in the history
Closes #15219

This PR adds a new feature, `@import "…" reference` that can be used to
load Tailwind CSS configuration files without adding any style rules to
the CSS.

The idea is that you can use this in combination with your Tailwind CSS
root file when you need to have access to your full CSS config outside
of the main stylesheet. A common example is for Vue, Svelte, or CSS
modules:

```css
@import "./tailwind.css" reference;

.link {
  @apply underline;
}
```

Importing a file as a reference will convert all `@theme` block to be
`reference`, so no CSS variables will be emitted. Furthermore it will
strip out all custom styles from the stylesheet. Furthermore plugins
registered via `@plugin` or `@config` inside reference-mode files will
not add any content to the CSS file via `addBase()`.

## Test Plan

Added unit test for when we handle the import resolution and when
`postcss-import` does it outside of Tailwind CSS. I also changed the
Svelte and Vue integration tests to use this new syntax to ensure it
works end to end.

---------

Co-authored-by: Jordan Pittman <jordan@cryptica.me>
  • Loading branch information
philipp-spiess and thecrypticace authored Dec 3, 2024
1 parent 3e5745f commit 78f5b08
Show file tree
Hide file tree
Showing 10 changed files with 226 additions and 40 deletions.
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

0 comments on commit 78f5b08

Please sign in to comment.