Skip to content
Next Next commit
Add @import ... reference
  • Loading branch information
philipp-spiess committed Dec 3, 2024
commit 20bd735397bf718585340e321b6131a37432d83b
44 changes: 44 additions & 0 deletions packages/tailwindcss/src/at-import.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,50 @@ test('can resolve relative @imports', async () => {
`)
})

test('can resolve @imports as reference', async () => {
let loadStylesheet = async (id: string, base: string) => {
expect(base).toBe('/root')
expect(id).toBe('./foo/bar.css')
return {
content: css`
.foo {
color: red;
}
@utility foo {
color: red;
}
@theme {
--breakpoint-md: 768px;
}
@variant hocus (&:hover, &:focus);
`,
base: '/root/foo',
}
}

await expect(
run(
css`
@import './foo/bar.css' reference;

.bar {
@apply md:hocus:foo;
}
`,
{ loadStylesheet, optimize: false },
),
).resolves.toMatchInlineSnapshot(`
".bar {
@media (width >= 768px) {
&:hover, &:focus {
color: red;
}
}
}
"
`)
})

test('can recursively resolve relative @imports', async () => {
let loadStylesheet = async (id: string, base: string) => {
if (base === '/root' && id === './foo/bar.css') {
Expand Down
51 changes: 50 additions & 1 deletion packages/tailwindcss/src/at-import.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Features } from '.'
import { atRule, context, walk, WalkAction, type AstNode } from './ast'
import * as CSS from './css-parser'
import { segment } from './utils/segment'
import * as ValueParser from './value-parser'

type LoadStylesheet = (id: string, basedir: string) => Promise<{ base: string; content: string }>
Expand All @@ -10,6 +11,7 @@ export async function substituteAtImports(
base: string,
loadStylesheet: LoadStylesheet,
recurseCount = 0,
mode: 'normal' | 'reference' = 'normal',
) {
let features = Features.None
let promises: Promise<void>[] = []
Expand All @@ -21,6 +23,19 @@ export async function substituteAtImports(

features |= Features.AtImport

if (parsed.media) {
let flags = segment(parsed.media, ' ')

if (flags.includes('reference')) {
parsed.media = flags.filter((flag) => flag !== 'reference').join(' ')
mode = 'reference'
}

if (parsed.media === '') {
parsed.media = null
}
}

let { uri, layer, media, supports } = parsed

// Skip importing data or remote URIs
Expand All @@ -43,7 +58,12 @@ export async function substituteAtImports(

let loaded = await loadStylesheet(uri, base)
let ast = CSS.parse(loaded.content)
await substituteAtImports(ast, loaded.base, loadStylesheet, recurseCount + 1)

if (mode === 'reference') {
ast = stripStyleRules(ast)
}

await substituteAtImports(ast, loaded.base, loadStylesheet, recurseCount + 1, mode)

contextNode.nodes = buildImportNodes(
[context({ base: loaded.base }, ast)],
Expand Down Expand Up @@ -158,3 +178,32 @@ function buildImportNodes(

return root
}

function stripStyleRules(ast: AstNode[]) {
let newAst = []
for (let node of ast) {
if (node.kind !== 'at-rule') {
continue
}
switch (node.name) {
case '@theme': {
let themeParams = segment(node.params, ' ')
if (!themeParams.includes('reference')) {
node.params += ' reference'
}
newAst.push(node)
continue
}
case '@import':
case '@config':
case '@plugin':
case '@variant':
case '@utility': {
newAst.push(node)
continue
}
}
}

return newAst
}