Skip to content

Commit 6aac3cb

Browse files
RobinMalfaitadamwathanphilipp-spiess
authored
Throw when layer(…) in @import is in the incorrect spot (#15109)
This PR throws an error when we notice that an `layer(…)` in an `@import` or `@media` is incorrect. This hints the user to ensure that `layer(…)` in an `@import` should be the first condition. In case of an `@media`, it should be an `@layer …` instead. --------- Co-authored-by: Adam Wathan <[email protected]> Co-authored-by: Philipp Spiess <[email protected]>
1 parent 961e8da commit 6aac3cb

File tree

3 files changed

+82
-49
lines changed

3 files changed

+82
-49
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
### Changed
2020

2121
- Interpolate gradients using OKLAB instead of OKLCH by default ([#15201](https://github.com/tailwindlabs/tailwindcss/pull/15201))
22+
- Error when `layer(…)` in `@import` is not first in the list of functions/conditions ([#15109](https://github.com/tailwindlabs/tailwindcss/pull/15109))
2223

2324
## [4.0.0-beta.2] - 2024-11-22
2425

packages/tailwindcss/src/at-import.ts

Lines changed: 56 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -14,47 +14,47 @@ export async function substituteAtImports(
1414

1515
walk(ast, (node, { replaceWith }) => {
1616
if (node.kind === 'at-rule' && node.name === '@import') {
17-
try {
18-
let { uri, layer, media, supports } = parseImportParams(ValueParser.parse(node.params))
19-
20-
// Skip importing data or remote URIs
21-
if (uri.startsWith('data:')) return
22-
if (uri.startsWith('http://') || uri.startsWith('https://')) return
23-
24-
let contextNode = context({}, [])
25-
26-
promises.push(
27-
(async () => {
28-
// Since we do not have fully resolved paths in core, we can't reliably detect circular
29-
// imports. Instead, we try to limit the recursion depth to a number that is too large
30-
// to be reached in practice.
31-
if (recurseCount > 100) {
32-
throw new Error(
33-
`Exceeded maximum recursion depth while resolving \`${uri}\` in \`${base}\`)`,
34-
)
35-
}
36-
37-
const loaded = await loadStylesheet(uri, base)
38-
let ast = CSS.parse(loaded.content)
39-
await substituteAtImports(ast, loaded.base, loadStylesheet, recurseCount + 1)
40-
41-
contextNode.nodes = buildImportNodes(
42-
[context({ base: loaded.base }, ast)],
43-
layer,
44-
media,
45-
supports,
17+
let parsed = parseImportParams(ValueParser.parse(node.params))
18+
if (parsed === null) return
19+
20+
let { uri, layer, media, supports } = parsed
21+
22+
// Skip importing data or remote URIs
23+
if (uri.startsWith('data:')) return
24+
if (uri.startsWith('http://') || uri.startsWith('https://')) return
25+
26+
let contextNode = context({}, [])
27+
28+
promises.push(
29+
(async () => {
30+
// Since we do not have fully resolved paths in core, we can't
31+
// reliably detect circular imports. Instead, we try to limit the
32+
// recursion depth to a number that is too large to be reached in
33+
// practice.
34+
if (recurseCount > 100) {
35+
throw new Error(
36+
`Exceeded maximum recursion depth while resolving \`${uri}\` in \`${base}\`)`,
4637
)
47-
})(),
48-
)
49-
50-
replaceWith(contextNode)
51-
// The resolved Stylesheets already have their transitive @imports
52-
// resolved, so we can skip walking them.
53-
return WalkAction.Skip
54-
} catch (e: any) {
55-
// When an error occurs while parsing the `@import` statement, we skip
56-
// the import.
57-
}
38+
}
39+
40+
let loaded = await loadStylesheet(uri, base)
41+
let ast = CSS.parse(loaded.content)
42+
await substituteAtImports(ast, loaded.base, loadStylesheet, recurseCount + 1)
43+
44+
contextNode.nodes = buildImportNodes(
45+
[context({ base: loaded.base }, ast)],
46+
layer,
47+
media,
48+
supports,
49+
)
50+
})(),
51+
)
52+
53+
replaceWith(contextNode)
54+
55+
// The resolved Stylesheets already have their transitive @imports
56+
// resolved, so we can skip walking them.
57+
return WalkAction.Skip
5858
}
5959
})
6060

@@ -72,30 +72,37 @@ export function parseImportParams(params: ValueParser.ValueAstNode[]) {
7272
let supports: string | null = null
7373

7474
for (let i = 0; i < params.length; i++) {
75-
const node = params[i]
75+
let node = params[i]
7676

7777
if (node.kind === 'separator') continue
7878

7979
if (node.kind === 'word' && !uri) {
80-
if (!node.value) throw new Error(`Unable to find uri`)
81-
if (node.value[0] !== '"' && node.value[0] !== "'") throw new Error('Unable to find uri')
80+
if (!node.value) return null
81+
if (node.value[0] !== '"' && node.value[0] !== "'") return null
8282

8383
uri = node.value.slice(1, -1)
8484
continue
8585
}
8686

8787
if (node.kind === 'function' && node.value.toLowerCase() === 'url') {
88-
throw new Error('`url(…)` functions are not supported')
88+
// `@import` with `url(…)` functions are not inlined but skipped and kept
89+
// in the final CSS instead.
90+
// E.g.: `@import url("https://fonts.google.com")`
91+
return null
8992
}
9093

91-
if (!uri) throw new Error('Unable to find uri')
94+
if (!uri) return null
9295

9396
if (
9497
(node.kind === 'word' || node.kind === 'function') &&
9598
node.value.toLowerCase() === 'layer'
9699
) {
97-
if (layer) throw new Error('Multiple layers')
98-
if (supports) throw new Error('`layer(…)` must be defined before `supports(…)` conditions')
100+
if (layer) return null
101+
if (supports) {
102+
throw new Error(
103+
'`layer(…)` in an `@import` should come before any other functions or conditions',
104+
)
105+
}
99106

100107
if ('nodes' in node) {
101108
layer = ValueParser.toCss(node.nodes)
@@ -107,7 +114,7 @@ export function parseImportParams(params: ValueParser.ValueAstNode[]) {
107114
}
108115

109116
if (node.kind === 'function' && node.value.toLowerCase() === 'supports') {
110-
if (supports) throw new Error('Multiple support conditions')
117+
if (supports) return null
111118
supports = ValueParser.toCss(node.nodes)
112119
continue
113120
}
@@ -116,7 +123,7 @@ export function parseImportParams(params: ValueParser.ValueAstNode[]) {
116123
break
117124
}
118125

119-
if (!uri) throw new Error('Unable to find uri')
126+
if (!uri) return null
120127

121128
return { uri, layer, media, supports }
122129
}

packages/tailwindcss/src/index.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3063,3 +3063,28 @@ test('addBase', async () => {
30633063
}"
30643064
`)
30653065
})
3066+
3067+
it("should error when `layer(…)` is used, but it's not the first param", async () => {
3068+
expect(async () => {
3069+
return await compileCss(
3070+
css`
3071+
@import './bar.css' supports(display: grid) layer(utilities);
3072+
`,
3073+
[],
3074+
{
3075+
async loadStylesheet() {
3076+
return {
3077+
base: '/bar.css',
3078+
content: css`
3079+
.foo {
3080+
@apply underline;
3081+
}
3082+
`,
3083+
}
3084+
},
3085+
},
3086+
)
3087+
}).rejects.toThrowErrorMatchingInlineSnapshot(
3088+
`[Error: \`layer(…)\` in an \`@import\` should come before any other functions or conditions]`,
3089+
)
3090+
})

0 commit comments

Comments
 (0)