Skip to content

Commit 78f5b08

Browse files
Add @import "…" reference (tailwindlabs#15228)
Closes tailwindlabs#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>
1 parent 3e5745f commit 78f5b08

File tree

10 files changed

+226
-40
lines changed

10 files changed

+226
-40
lines changed

CHANGELOG.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

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

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

18-
### Added
19-
20-
- Parallelize parsing of individual source files ([#15270](https://github.com/tailwindlabs/tailwindcss/pull/15270))
21-
- Support Vite 6 in the Vite plugin ([#15274](https://github.com/tailwindlabs/tailwindcss/issues/15274))
22-
2324
## [4.0.0-beta.4] - 2024-11-29
2425

2526
### Fixed

integrations/vite/svelte.test.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,7 @@ test(
4848
target: document.body,
4949
})
5050
`,
51-
'src/index.css': css`
52-
@import 'tailwindcss/theme' theme(reference);
53-
@import 'tailwindcss/utilities';
54-
`,
51+
'src/index.css': css`@import 'tailwindcss' reference;`,
5552
'src/App.svelte': html`
5653
<script>
5754
import './index.css'

integrations/vite/vue.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,13 @@ test(
4545
`,
4646
'src/App.vue': html`
4747
<style>
48-
@import 'tailwindcss/utilities';
49-
@import 'tailwindcss/theme' theme(reference);
48+
@import 'tailwindcss' reference;
5049
.foo {
5150
@apply text-red-500;
5251
}
5352
</style>
5453
<style scoped>
55-
@import 'tailwindcss/utilities';
56-
@import 'tailwindcss/theme' theme(reference);
54+
@import 'tailwindcss' reference;
5755
:deep(.bar) {
5856
color: red;
5957
}

packages/tailwindcss/src/ast.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export type Comment = {
2929

3030
export type Context = {
3131
kind: 'context'
32-
context: Record<string, string>
32+
context: Record<string, string | boolean>
3333
nodes: AstNode[]
3434
}
3535

@@ -82,7 +82,7 @@ export function comment(value: string): Comment {
8282
}
8383
}
8484

85-
export function context(context: Record<string, string>, nodes: AstNode[]): Context {
85+
export function context(context: Record<string, string | boolean>, nodes: AstNode[]): Context {
8686
return {
8787
kind: 'context',
8888
context,
@@ -115,12 +115,12 @@ export function walk(
115115
utils: {
116116
parent: AstNode | null
117117
replaceWith(newNode: AstNode | AstNode[]): void
118-
context: Record<string, string>
118+
context: Record<string, string | boolean>
119119
path: AstNode[]
120120
},
121121
) => void | WalkAction,
122122
parentPath: AstNode[] = [],
123-
context: Record<string, string> = {},
123+
context: Record<string, string | boolean> = {},
124124
) {
125125
for (let i = 0; i < ast.length; i++) {
126126
let node = ast[i]
@@ -175,12 +175,12 @@ export function walkDepth(
175175
utils: {
176176
parent: AstNode | null
177177
path: AstNode[]
178-
context: Record<string, string>
178+
context: Record<string, string | boolean>
179179
replaceWith(newNode: AstNode[]): void
180180
},
181181
) => void,
182182
parentPath: AstNode[] = [],
183-
context: Record<string, string> = {},
183+
context: Record<string, string | boolean> = {},
184184
) {
185185
for (let i = 0; i < ast.length; i++) {
186186
let node = ast[i]

packages/tailwindcss/src/compat/apply-compat-hooks.ts

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@ export async function applyCompatibilityHooks({
3434
globs: { origin?: string; pattern: string }[]
3535
}) {
3636
let features = Features.None
37-
let pluginPaths: [{ id: string; base: string }, CssPluginOptions | null][] = []
38-
let configPaths: { id: string; base: string }[] = []
37+
let pluginPaths: [{ id: string; base: string; reference: boolean }, CssPluginOptions | null][] =
38+
[]
39+
let configPaths: { id: string; base: string; reference: boolean }[] = []
3940

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

9798
pluginPaths.push([
98-
{ id: pluginPath, base: context.base },
99+
{ id: pluginPath, base: context.base as string, reference: !!context.reference },
99100
Object.keys(options).length > 0 ? options : null,
100101
])
101102

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

117-
configPaths.push({ id: node.params.slice(1, -1), base: context.base })
118+
configPaths.push({
119+
id: node.params.slice(1, -1),
120+
base: context.base as string,
121+
reference: !!context.reference,
122+
})
118123
replaceWith([])
119124
features |= Features.JsPluginCompat
120125
return
@@ -153,23 +158,25 @@ export async function applyCompatibilityHooks({
153158

154159
let [configs, pluginDetails] = await Promise.all([
155160
Promise.all(
156-
configPaths.map(async ({ id, base }) => {
161+
configPaths.map(async ({ id, base, reference }) => {
157162
let loaded = await loadModule(id, base, 'config')
158163
return {
159164
path: id,
160165
base: loaded.base,
161166
config: loaded.module as UserConfig,
167+
reference,
162168
}
163169
}),
164170
),
165171
Promise.all(
166-
pluginPaths.map(async ([{ id, base }, pluginOptions]) => {
172+
pluginPaths.map(async ([{ id, base, reference }, pluginOptions]) => {
167173
let loaded = await loadModule(id, base, 'plugin')
168174
return {
169175
path: id,
170176
base: loaded.base,
171177
plugin: loaded.module as Plugin,
172178
options: pluginOptions,
179+
reference,
173180
}
174181
}),
175182
),
@@ -203,22 +210,32 @@ function upgradeToFullPluginSupport({
203210
path: string
204211
base: string
205212
config: UserConfig
213+
reference: boolean
206214
}[]
207215
pluginDetails: {
208216
path: string
209217
base: string
210218
plugin: Plugin
211219
options: CssPluginOptions | null
220+
reference: boolean
212221
}[]
213222
}) {
214223
let features = Features.None
215224
let pluginConfigs = pluginDetails.map((detail) => {
216225
if (!detail.options) {
217-
return { config: { plugins: [detail.plugin] }, base: detail.base }
226+
return {
227+
config: { plugins: [detail.plugin] },
228+
base: detail.base,
229+
reference: detail.reference,
230+
}
218231
}
219232

220233
if ('__isOptionsFunction' in detail.plugin) {
221-
return { config: { plugins: [detail.plugin(detail.options)] }, base: detail.base }
234+
return {
235+
config: { plugins: [detail.plugin(detail.options)] },
236+
base: detail.base,
237+
reference: detail.reference,
238+
}
222239
}
223240

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

229246
let { resolvedConfig } = resolveConfig(designSystem, [
230-
{ config: createCompatConfig(designSystem.theme), base },
247+
{ config: createCompatConfig(designSystem.theme), base, reference: true },
231248
...userConfig,
232-
{ config: { plugins: [darkModePlugin] }, base },
249+
{ config: { plugins: [darkModePlugin] }, base, reference: true },
233250
])
234251
let { resolvedConfig: resolvedUserConfig, replacedThemeKeys } = resolveConfig(
235252
designSystem,
@@ -242,8 +259,8 @@ function upgradeToFullPluginSupport({
242259
},
243260
})
244261

245-
for (let { handler } of resolvedConfig.plugins) {
246-
handler(pluginApi)
262+
for (let { handler, reference } of resolvedConfig.plugins) {
263+
handler(reference ? { ...pluginApi, addBase: () => {} } : pluginApi)
247264
}
248265

249266
// Merge the user-configured theme keys into the design system. The compat

packages/tailwindcss/src/compat/config/resolve-config.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface ConfigFile {
1515
path?: string
1616
base: string
1717
config: UserConfig
18+
reference: boolean
1819
}
1920

2021
interface ResolutionContext {
@@ -128,25 +129,28 @@ export type PluginUtils = {
128129
colors: typeof colors
129130
}
130131

131-
function extractConfigs(ctx: ResolutionContext, { config, base, path }: ConfigFile): void {
132+
function extractConfigs(
133+
ctx: ResolutionContext,
134+
{ config, base, path, reference }: ConfigFile,
135+
): void {
132136
let plugins: PluginWithConfig[] = []
133137

134138
// Normalize plugins so they share the same shape
135139
for (let plugin of config.plugins ?? []) {
136140
if ('__isOptionsFunction' in plugin) {
137141
// Happens with `plugin.withOptions()` when no options were passed:
138142
// e.g. `require("my-plugin")` instead of `require("my-plugin")(options)`
139-
plugins.push(plugin())
143+
plugins.push({ ...plugin(), reference })
140144
} else if ('handler' in plugin) {
141145
// Happens with `plugin(…)`:
142146
// e.g. `require("my-plugin")`
143147
//
144148
// or with `plugin.withOptions()` when the user passed options:
145149
// e.g. `require("my-plugin")(options)`
146-
plugins.push(plugin)
150+
plugins.push({ ...plugin, reference })
147151
} else {
148152
// Just a plain function without using the plugin(…) API
149-
plugins.push({ handler: plugin })
153+
plugins.push({ handler: plugin, reference })
150154
}
151155
}
152156

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

160164
for (let preset of config.presets ?? []) {
161-
extractConfigs(ctx, { path, base, config: preset })
165+
extractConfigs(ctx, { path, base, config: preset, reference })
162166
}
163167

164168
// Apply configs from plugins
165169
for (let plugin of plugins) {
166170
ctx.plugins.push(plugin)
167171

168172
if (plugin.config) {
169-
extractConfigs(ctx, { path, base, config: plugin.config })
173+
extractConfigs(ctx, { path, base, config: plugin.config, reference: !!plugin.reference })
170174
}
171175
}
172176

packages/tailwindcss/src/compat/plugin-api.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1490,6 +1490,52 @@ describe('theme', async () => {
14901490
})
14911491
})
14921492

1493+
describe('addBase', () => {
1494+
test('does not create rules when imported via `@import "…" reference`', async () => {
1495+
let input = css`
1496+
@tailwind utilities;
1497+
@plugin "outside";
1498+
@import './inside.css' reference;
1499+
`
1500+
1501+
let compiler = await compile(input, {
1502+
loadModule: async (id, base) => {
1503+
if (id === 'inside') {
1504+
return {
1505+
base,
1506+
module: plugin(function ({ addBase }) {
1507+
addBase({ inside: { color: 'red' } })
1508+
}),
1509+
}
1510+
}
1511+
return {
1512+
base,
1513+
module: plugin(function ({ addBase }) {
1514+
addBase({ outside: { color: 'red' } })
1515+
}),
1516+
}
1517+
},
1518+
async loadStylesheet() {
1519+
return {
1520+
content: css`
1521+
@plugin "inside";
1522+
`,
1523+
base: '',
1524+
}
1525+
},
1526+
})
1527+
1528+
expect(compiler.build([])).toMatchInlineSnapshot(`
1529+
"@layer base {
1530+
outside {
1531+
color: red;
1532+
}
1533+
}
1534+
"
1535+
`)
1536+
})
1537+
})
1538+
14931539
describe('addVariant', () => {
14941540
test('addVariant with string selector', async () => {
14951541
let { build } = await compile(

packages/tailwindcss/src/compat/plugin-api.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ import * as SelectorParser from './selector-parser'
1717

1818
export type Config = UserConfig
1919
export type PluginFn = (api: PluginAPI) => void
20-
export type PluginWithConfig = { handler: PluginFn; config?: UserConfig }
20+
export type PluginWithConfig = {
21+
handler: PluginFn;
22+
config?: UserConfig;
23+
24+
/** @internal */
25+
reference?: boolean
26+
}
2127
export type PluginWithOptions<T> = {
2228
(options?: T): PluginWithConfig
2329
__isOptionsFunction: true

0 commit comments

Comments
 (0)