react-email 6.8.1 → 6.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # react-email
2
2
 
3
+ ## 6.9.1
4
+
5
+ ### Patch Changes
6
+
7
+ - ea02028: Serve static assets with URL-encoded filenames correctly in the preview server while preventing decoded paths from escaping the static directory.
8
+ - 60c96a6: Fix `<Tailwind>` dropping `dark:` and other media-query variants with `tailwindcss@4.3.3`+, where the conditional value was inlined as the base style and the `@media` rule never reached the `<head>`
9
+
10
+ ## 6.9.0
11
+
3
12
  ## 6.8.1
4
13
 
5
14
  ## 6.8.0
@@ -6533,7 +6533,7 @@ const getTracingRootDir = async (usersProjectLocation) => {
6533
6533
  //#region package.json
6534
6534
  var package_default = {
6535
6535
  name: "react-email",
6536
- version: "6.8.1",
6536
+ version: "6.9.1",
6537
6537
  description: "A live preview of your emails right in your browser.",
6538
6538
  bin: { "email": "./dist/cli/index.mjs" },
6539
6539
  type: "module",
@@ -7249,15 +7249,24 @@ const getEnvVariablesForPreviewApp = (relativePathToEmailsDirectory, previewServ
7249
7249
  //#endregion
7250
7250
  //#region src/cli/utils/preview/serve-static-file.ts
7251
7251
  const serveStaticFile = async (res, pathname, staticDirRelativePath) => {
7252
- const pathname_ = pathname.replace("/static", "./static");
7253
- const ext = path.parse(pathname_).ext;
7254
- const staticBaseDir = path.resolve(process.cwd(), staticDirRelativePath);
7255
- const fileAbsolutePath = path.resolve(staticBaseDir, pathname_);
7256
- if (!fileAbsolutePath.startsWith(staticBaseDir)) {
7252
+ let decodedPathname;
7253
+ try {
7254
+ decodedPathname = decodeURIComponent(pathname);
7255
+ } catch {
7256
+ res.statusCode = 400;
7257
+ res.end();
7258
+ return;
7259
+ }
7260
+ const staticBaseDir = path.resolve(process.cwd(), staticDirRelativePath, "static");
7261
+ const staticFilePath = decodedPathname.replace(/^\/static\/?/, "");
7262
+ const fileAbsolutePath = path.resolve(staticBaseDir, staticFilePath);
7263
+ const relativeFilePath = path.relative(staticBaseDir, fileAbsolutePath);
7264
+ if (relativeFilePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativeFilePath)) {
7257
7265
  res.statusCode = 403;
7258
7266
  res.end();
7259
7267
  return;
7260
7268
  }
7269
+ const ext = path.extname(fileAbsolutePath);
7261
7270
  try {
7262
7271
  const fileHandle = await promises.open(fileAbsolutePath, "r");
7263
7272
  const fileData = await promises.readFile(fileHandle);
@@ -0,0 +1,4 @@
1
+ //#region src/components/tailwind/utils/css/constants.ts
2
+ const NON_INLINABLE_ATRULES = new Set(["media", "supports"]);
3
+ //#endregion
4
+ exports.NON_INLINABLE_ATRULES = NON_INLINABLE_ATRULES;
@@ -0,0 +1,4 @@
1
+ //#region src/components/tailwind/utils/css/constants.ts
2
+ const NON_INLINABLE_ATRULES = new Set(["media", "supports"]);
3
+ //#endregion
4
+ export { NON_INLINABLE_ATRULES };
@@ -1,4 +1,5 @@
1
1
  require("../../../../_virtual/_rolldown/runtime.cjs");
2
+ const require_constants = require("./constants.cjs");
2
3
  let css_tree = require("css-tree");
3
4
  //#region src/components/tailwind/utils/css/downlevel-for-email-clients.ts
4
5
  /**
@@ -43,7 +44,7 @@ function unnestMediaQueries(styleSheet) {
43
44
  const nestedAtrules = [];
44
45
  const remainingChildren = [];
45
46
  rule.block.children.forEach((child) => {
46
- if (child.type === "Atrule" && (child.name === "media" || child.name === "supports")) nestedAtrules.push(child);
47
+ if (child.type === "Atrule" && require_constants.NON_INLINABLE_ATRULES.has(child.name.toLowerCase())) nestedAtrules.push(child);
47
48
  else remainingChildren.push(child);
48
49
  });
49
50
  if (nestedAtrules.length > 0) transforms.push({
@@ -1,3 +1,4 @@
1
+ import { NON_INLINABLE_ATRULES } from "./constants.mjs";
1
2
  import { List, clone, walk } from "css-tree/dist/csstree.esm";
2
3
  //#region src/components/tailwind/utils/css/downlevel-for-email-clients.ts
3
4
  /**
@@ -42,7 +43,7 @@ function unnestMediaQueries(styleSheet) {
42
43
  const nestedAtrules = [];
43
44
  const remainingChildren = [];
44
45
  rule.block.children.forEach((child) => {
45
- if (child.type === "Atrule" && (child.name === "media" || child.name === "supports")) nestedAtrules.push(child);
46
+ if (child.type === "Atrule" && NON_INLINABLE_ATRULES.has(child.name.toLowerCase())) nestedAtrules.push(child);
46
47
  else remainingChildren.push(child);
47
48
  });
48
49
  if (nestedAtrules.length > 0) transforms.push({
@@ -1,8 +1,38 @@
1
1
  require("../../../../_virtual/_rolldown/runtime.cjs");
2
+ const require_constants = require("./constants.cjs");
2
3
  const require_is_rule_inlinable = require("./is-rule-inlinable.cjs");
3
4
  const require_split_mixed_rule = require("./split-mixed-rule.cjs");
4
5
  let css_tree = require("css-tree");
5
6
  //#region src/components/tailwind/utils/css/extract-rules-per-class.ts
7
+ /**
8
+ * Re-nest the at-rule(s) back inside the rule so the pipeline stays
9
+ * version-agnostic across Tailwind's variant shape change.
10
+ * See https://github.com/resend/react-email/issues/3662
11
+ */
12
+ function nestAtRulesInsideRule(rule, enclosingAtRules) {
13
+ let children = (0, css_tree.clone)(rule).block.children;
14
+ for (let i = enclosingAtRules.length - 1; i >= 0; i--) {
15
+ const atRule = enclosingAtRules[i];
16
+ const wrapped = {
17
+ type: "Atrule",
18
+ name: atRule.name,
19
+ prelude: atRule.prelude ? (0, css_tree.clone)(atRule.prelude) : null,
20
+ block: {
21
+ type: "Block",
22
+ children
23
+ }
24
+ };
25
+ children = new css_tree.List().fromArray([wrapped]);
26
+ }
27
+ return {
28
+ type: "Rule",
29
+ prelude: (0, css_tree.clone)(rule.prelude),
30
+ block: {
31
+ type: "Block",
32
+ children
33
+ }
34
+ };
35
+ }
6
36
  function extractRulesPerClass(root, classes) {
7
37
  const classSet = new Set(classes);
8
38
  const inlinableRules = /* @__PURE__ */ new Map();
@@ -12,28 +42,41 @@ function extractRulesPerClass(root, classes) {
12
42
  if (existing) existing.push(rule);
13
43
  else map.set(className, [rule]);
14
44
  };
15
- (0, css_tree.walk)(root, {
16
- visit: "Rule",
17
- enter(rule) {
18
- const firstSelector = rule.prelude.type === "SelectorList" ? rule.prelude.children.first : null;
19
- if (firstSelector?.type === "Selector" && firstSelector.children.first?.type === "NestingSelector") return;
20
- const selectorClasses = [];
21
- (0, css_tree.walk)(rule.prelude, {
22
- visit: "ClassSelector",
23
- enter(classSelector) {
24
- selectorClasses.push(css_tree.string.decode(classSelector.name));
25
- }
26
- });
27
- if (require_is_rule_inlinable.isRuleInlinable(rule)) {
28
- for (const className of selectorClasses) if (classSet.has(className)) appendRule(inlinableRules, className, rule);
29
- } else {
30
- const { inlinablePart, nonInlinablePart } = require_split_mixed_rule.splitMixedRule(rule);
31
- for (const className of selectorClasses) {
32
- if (!classSet.has(className)) continue;
33
- if (inlinablePart) appendRule(inlinableRules, className, inlinablePart);
34
- if (nonInlinablePart) appendRule(nonInlinableRules, className, nonInlinablePart);
35
- }
45
+ const enclosingAtRules = [];
46
+ const handleRule = (rule) => {
47
+ const firstSelector = rule.prelude.type === "SelectorList" ? rule.prelude.children.first : null;
48
+ if (firstSelector?.type === "Selector" && firstSelector.children.first?.type === "NestingSelector") return;
49
+ const selectorClasses = [];
50
+ (0, css_tree.walk)(rule.prelude, {
51
+ visit: "ClassSelector",
52
+ enter(classSelector) {
53
+ selectorClasses.push(css_tree.string.decode(classSelector.name));
36
54
  }
55
+ });
56
+ if (enclosingAtRules.length > 0) {
57
+ const nonInlinablePart = nestAtRulesInsideRule(rule, enclosingAtRules);
58
+ for (const className of selectorClasses) if (classSet.has(className)) appendRule(nonInlinableRules, className, nonInlinablePart);
59
+ return;
60
+ }
61
+ if (require_is_rule_inlinable.isRuleInlinable(rule)) {
62
+ for (const className of selectorClasses) if (classSet.has(className)) appendRule(inlinableRules, className, rule);
63
+ } else {
64
+ const { inlinablePart, nonInlinablePart } = require_split_mixed_rule.splitMixedRule(rule);
65
+ for (const className of selectorClasses) {
66
+ if (!classSet.has(className)) continue;
67
+ if (inlinablePart) appendRule(inlinableRules, className, inlinablePart);
68
+ if (nonInlinablePart) appendRule(nonInlinableRules, className, nonInlinablePart);
69
+ }
70
+ }
71
+ };
72
+ (0, css_tree.walk)(root, {
73
+ enter(node) {
74
+ if (node.type === "Atrule") {
75
+ if (require_constants.NON_INLINABLE_ATRULES.has(node.name.toLowerCase())) enclosingAtRules.push(node);
76
+ } else if (node.type === "Rule") handleRule(node);
77
+ },
78
+ leave(node) {
79
+ if (node.type === "Atrule" && require_constants.NON_INLINABLE_ATRULES.has(node.name.toLowerCase())) enclosingAtRules.pop();
37
80
  }
38
81
  });
39
82
  return {
@@ -1,7 +1,37 @@
1
+ import { NON_INLINABLE_ATRULES } from "./constants.mjs";
1
2
  import { isRuleInlinable } from "./is-rule-inlinable.mjs";
2
3
  import { splitMixedRule } from "./split-mixed-rule.mjs";
3
- import { string, walk } from "css-tree/dist/csstree.esm";
4
+ import { List, clone, string, walk } from "css-tree/dist/csstree.esm";
4
5
  //#region src/components/tailwind/utils/css/extract-rules-per-class.ts
6
+ /**
7
+ * Re-nest the at-rule(s) back inside the rule so the pipeline stays
8
+ * version-agnostic across Tailwind's variant shape change.
9
+ * See https://github.com/resend/react-email/issues/3662
10
+ */
11
+ function nestAtRulesInsideRule(rule, enclosingAtRules) {
12
+ let children = clone(rule).block.children;
13
+ for (let i = enclosingAtRules.length - 1; i >= 0; i--) {
14
+ const atRule = enclosingAtRules[i];
15
+ const wrapped = {
16
+ type: "Atrule",
17
+ name: atRule.name,
18
+ prelude: atRule.prelude ? clone(atRule.prelude) : null,
19
+ block: {
20
+ type: "Block",
21
+ children
22
+ }
23
+ };
24
+ children = new List().fromArray([wrapped]);
25
+ }
26
+ return {
27
+ type: "Rule",
28
+ prelude: clone(rule.prelude),
29
+ block: {
30
+ type: "Block",
31
+ children
32
+ }
33
+ };
34
+ }
5
35
  function extractRulesPerClass(root, classes) {
6
36
  const classSet = new Set(classes);
7
37
  const inlinableRules = /* @__PURE__ */ new Map();
@@ -11,28 +41,41 @@ function extractRulesPerClass(root, classes) {
11
41
  if (existing) existing.push(rule);
12
42
  else map.set(className, [rule]);
13
43
  };
14
- walk(root, {
15
- visit: "Rule",
16
- enter(rule) {
17
- const firstSelector = rule.prelude.type === "SelectorList" ? rule.prelude.children.first : null;
18
- if (firstSelector?.type === "Selector" && firstSelector.children.first?.type === "NestingSelector") return;
19
- const selectorClasses = [];
20
- walk(rule.prelude, {
21
- visit: "ClassSelector",
22
- enter(classSelector) {
23
- selectorClasses.push(string.decode(classSelector.name));
24
- }
25
- });
26
- if (isRuleInlinable(rule)) {
27
- for (const className of selectorClasses) if (classSet.has(className)) appendRule(inlinableRules, className, rule);
28
- } else {
29
- const { inlinablePart, nonInlinablePart } = splitMixedRule(rule);
30
- for (const className of selectorClasses) {
31
- if (!classSet.has(className)) continue;
32
- if (inlinablePart) appendRule(inlinableRules, className, inlinablePart);
33
- if (nonInlinablePart) appendRule(nonInlinableRules, className, nonInlinablePart);
34
- }
44
+ const enclosingAtRules = [];
45
+ const handleRule = (rule) => {
46
+ const firstSelector = rule.prelude.type === "SelectorList" ? rule.prelude.children.first : null;
47
+ if (firstSelector?.type === "Selector" && firstSelector.children.first?.type === "NestingSelector") return;
48
+ const selectorClasses = [];
49
+ walk(rule.prelude, {
50
+ visit: "ClassSelector",
51
+ enter(classSelector) {
52
+ selectorClasses.push(string.decode(classSelector.name));
35
53
  }
54
+ });
55
+ if (enclosingAtRules.length > 0) {
56
+ const nonInlinablePart = nestAtRulesInsideRule(rule, enclosingAtRules);
57
+ for (const className of selectorClasses) if (classSet.has(className)) appendRule(nonInlinableRules, className, nonInlinablePart);
58
+ return;
59
+ }
60
+ if (isRuleInlinable(rule)) {
61
+ for (const className of selectorClasses) if (classSet.has(className)) appendRule(inlinableRules, className, rule);
62
+ } else {
63
+ const { inlinablePart, nonInlinablePart } = splitMixedRule(rule);
64
+ for (const className of selectorClasses) {
65
+ if (!classSet.has(className)) continue;
66
+ if (inlinablePart) appendRule(inlinableRules, className, inlinablePart);
67
+ if (nonInlinablePart) appendRule(nonInlinableRules, className, nonInlinablePart);
68
+ }
69
+ }
70
+ };
71
+ walk(root, {
72
+ enter(node) {
73
+ if (node.type === "Atrule") {
74
+ if (NON_INLINABLE_ATRULES.has(node.name.toLowerCase())) enclosingAtRules.push(node);
75
+ } else if (node.type === "Rule") handleRule(node);
76
+ },
77
+ leave(node) {
78
+ if (node.type === "Atrule" && NON_INLINABLE_ATRULES.has(node.name.toLowerCase())) enclosingAtRules.pop();
36
79
  }
37
80
  });
38
81
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-email",
3
- "version": "6.8.1",
3
+ "version": "6.9.1",
4
4
  "description": "A live preview of your emails right in your browser.",
5
5
  "bin": {
6
6
  "email": "./dist/cli/index.mjs"
@@ -0,0 +1,5 @@
1
+ // At-rules whose contents are conditional, so their declarations can't be
2
+ // inlined onto an element (e.g. `@media (prefers-color-scheme: dark)`). Shared
3
+ // so the "mark non-inlinable" (extract-rules-per-class) and "hoist out of the
4
+ // rule" (downlevel-for-email-clients) passes can't disagree on the set.
5
+ export const NON_INLINABLE_ATRULES = new Set(['media', 'supports']);
@@ -26,6 +26,7 @@ import {
26
26
  type StyleSheet,
27
27
  walk,
28
28
  } from 'css-tree';
29
+ import { NON_INLINABLE_ATRULES } from './constants.js';
29
30
 
30
31
  /**
31
32
  * css-tree 3.x introduced new AST node types for query-related at-rules that
@@ -114,7 +115,7 @@ function unnestMediaQueries(styleSheet: StyleSheet): void {
114
115
  rule.block.children.forEach((child) => {
115
116
  if (
116
117
  child.type === 'Atrule' &&
117
- (child.name === 'media' || child.name === 'supports')
118
+ NON_INLINABLE_ATRULES.has(child.name.toLowerCase())
118
119
  ) {
119
120
  nestedAtrules.push(child);
120
121
  } else {
@@ -1,4 +1,4 @@
1
- import { generate, type Rule } from 'css-tree';
1
+ import { generate, parse, type Rule, type StyleSheet } from 'css-tree';
2
2
  import { setupTailwind } from '../tailwindcss/setup-tailwind.js';
3
3
  import { extractRulesPerClass } from './extract-rules-per-class.js';
4
4
 
@@ -207,6 +207,98 @@ describe('extractRulesPerClass()', async () => {
207
207
  `);
208
208
  });
209
209
 
210
+ // Wrapped shape emitted by Tailwind >=4.3.3.
211
+ // See https://github.com/resend/react-email/issues/3662
212
+ it('treats @media-wrapped rules (Tailwind >=4.3.3) as non-inlinable', () => {
213
+ const classes = [
214
+ 'bg-white',
215
+ 'text-black',
216
+ 'dark:bg-black',
217
+ 'dark:text-white',
218
+ ];
219
+ const stylesheet = parse(`
220
+ @layer utilities {
221
+ .bg-white { background-color: rgb(255, 255, 255); }
222
+ .text-black { color: rgb(0, 0, 0); }
223
+ @media (prefers-color-scheme: dark) {
224
+ .dark\\:bg-black { background-color: rgb(0, 0, 0); }
225
+ .dark\\:text-white { color: rgb(255, 255, 255); }
226
+ }
227
+ }
228
+ `) as StyleSheet;
229
+
230
+ const { inlinable, nonInlinable } = extractRulesPerClass(
231
+ stylesheet,
232
+ classes,
233
+ );
234
+
235
+ expect(convertToComparable(inlinable)).toMatchInlineSnapshot(`
236
+ {
237
+ "bg-white": [
238
+ ".bg-white{background-color:rgb(255,255,255)}",
239
+ ],
240
+ "text-black": [
241
+ ".text-black{color:rgb(0,0,0)}",
242
+ ],
243
+ }
244
+ `);
245
+ expect(convertToComparable(nonInlinable)).toMatchInlineSnapshot(`
246
+ {
247
+ "dark:bg-black": [
248
+ ".dark\\:bg-black{@media (prefers-color-scheme:dark){background-color:rgb(0,0,0)}}",
249
+ ],
250
+ "dark:text-white": [
251
+ ".dark\\:text-white{@media (prefers-color-scheme:dark){color:rgb(255,255,255)}}",
252
+ ],
253
+ }
254
+ `);
255
+ });
256
+
257
+ // Nested shape emitted by Tailwind <=4.3.2; must classify identically.
258
+ // See https://github.com/resend/react-email/issues/3662
259
+ it('treats @media-nested rules (Tailwind <=4.3.2) as non-inlinable', () => {
260
+ const classes = [
261
+ 'bg-white',
262
+ 'text-black',
263
+ 'dark:bg-black',
264
+ 'dark:text-white',
265
+ ];
266
+ const stylesheet = parse(`
267
+ @layer utilities {
268
+ .bg-white { background-color: rgb(255, 255, 255); }
269
+ .text-black { color: rgb(0, 0, 0); }
270
+ .dark\\:bg-black { @media (prefers-color-scheme: dark) { background-color: rgb(0, 0, 0); } }
271
+ .dark\\:text-white { @media (prefers-color-scheme: dark) { color: rgb(255, 255, 255); } }
272
+ }
273
+ `) as StyleSheet;
274
+
275
+ const { inlinable, nonInlinable } = extractRulesPerClass(
276
+ stylesheet,
277
+ classes,
278
+ );
279
+
280
+ expect(convertToComparable(inlinable)).toMatchInlineSnapshot(`
281
+ {
282
+ "bg-white": [
283
+ ".bg-white{background-color:rgb(255,255,255)}",
284
+ ],
285
+ "text-black": [
286
+ ".text-black{color:rgb(0,0,0)}",
287
+ ],
288
+ }
289
+ `);
290
+ expect(convertToComparable(nonInlinable)).toMatchInlineSnapshot(`
291
+ {
292
+ "dark:bg-black": [
293
+ ".dark\\:bg-black{@media (prefers-color-scheme:dark){background-color:rgb(0,0,0)}}",
294
+ ],
295
+ "dark:text-white": [
296
+ ".dark\\:text-white{@media (prefers-color-scheme:dark){color:rgb(255,255,255)}}",
297
+ ],
298
+ }
299
+ `);
300
+ });
301
+
210
302
  it('does not emit a bare nested rule for group/peer marker classes', async () => {
211
303
  const tailwind = await setupTailwind({});
212
304
  const classes = ['group', 'group-hover:underline'];
@@ -1,7 +1,52 @@
1
- import { type CssNode, type Rule, string, walk } from 'css-tree';
1
+ import {
2
+ type Atrule,
3
+ type CssNode,
4
+ clone,
5
+ List,
6
+ type Rule,
7
+ string,
8
+ walk,
9
+ } from 'css-tree';
10
+ import { NON_INLINABLE_ATRULES } from './constants.js';
2
11
  import { isRuleInlinable } from './is-rule-inlinable.js';
3
12
  import { splitMixedRule } from './split-mixed-rule.js';
4
13
 
14
+ /**
15
+ * Re-nest the at-rule(s) back inside the rule so the pipeline stays
16
+ * version-agnostic across Tailwind's variant shape change.
17
+ * See https://github.com/resend/react-email/issues/3662
18
+ */
19
+ function nestAtRulesInsideRule(rule: Rule, enclosingAtRules: Atrule[]): Rule {
20
+ const ruleClone = clone(rule) as Rule;
21
+ let children = ruleClone.block.children;
22
+
23
+ // Wrap from the innermost enclosing at-rule outward.
24
+ for (let i = enclosingAtRules.length - 1; i >= 0; i--) {
25
+ const atRule = enclosingAtRules[i]!;
26
+ const wrapped: Atrule = {
27
+ type: 'Atrule',
28
+ name: atRule.name,
29
+ prelude: atRule.prelude
30
+ ? (clone(atRule.prelude) as Atrule['prelude'])
31
+ : null,
32
+ block: {
33
+ type: 'Block',
34
+ children,
35
+ },
36
+ };
37
+ children = new List<CssNode>().fromArray([wrapped]);
38
+ }
39
+
40
+ return {
41
+ type: 'Rule',
42
+ prelude: clone(rule.prelude) as Rule['prelude'],
43
+ block: {
44
+ type: 'Block',
45
+ children,
46
+ },
47
+ };
48
+ }
49
+
5
50
  export function extractRulesPerClass(root: CssNode, classes: string[]) {
6
51
  const classSet = new Set(classes);
7
52
 
@@ -23,52 +68,83 @@ export function extractRulesPerClass(root: CssNode, classes: string[]) {
23
68
  }
24
69
  };
25
70
 
26
- walk(root, {
27
- visit: 'Rule',
28
- enter(rule) {
29
- // A nested rule (e.g. group/peer's `&:is(:where(.group):hover *)`) belongs
30
- // to its parent utility; processing it standalone emits a bare, parentless
31
- // `&` rule into the <style> block, so skip it here.
32
- const firstSelector =
33
- rule.prelude.type === 'SelectorList'
34
- ? rule.prelude.children.first
35
- : null;
36
- if (
37
- firstSelector?.type === 'Selector' &&
38
- firstSelector.children.first?.type === 'NestingSelector'
39
- ) {
40
- return;
71
+ // A plain-looking rule inside these wrappers is still conditional and must
72
+ // not be inlined. See https://github.com/resend/react-email/issues/3662
73
+ const enclosingAtRules: Atrule[] = [];
74
+
75
+ const handleRule = (rule: Rule) => {
76
+ // A nested rule (e.g. group/peer's `&:is(:where(.group):hover *)`) belongs
77
+ // to its parent utility; processing it standalone emits a bare, parentless
78
+ // `&` rule into the <style> block, so skip it here.
79
+ const firstSelector =
80
+ rule.prelude.type === 'SelectorList' ? rule.prelude.children.first : null;
81
+ if (
82
+ firstSelector?.type === 'Selector' &&
83
+ firstSelector.children.first?.type === 'NestingSelector'
84
+ ) {
85
+ return;
86
+ }
87
+
88
+ // Only the prelude names the class that owns the rule; classes referenced
89
+ // inside the block (e.g. `.group` in `:where(.group)`) must not key it.
90
+ const selectorClasses: string[] = [];
91
+ walk(rule.prelude, {
92
+ visit: 'ClassSelector',
93
+ enter(classSelector) {
94
+ selectorClasses.push(string.decode(classSelector.name));
95
+ },
96
+ });
97
+
98
+ if (enclosingAtRules.length > 0) {
99
+ const nonInlinablePart = nestAtRulesInsideRule(rule, enclosingAtRules);
100
+ for (const className of selectorClasses) {
101
+ if (classSet.has(className)) {
102
+ appendRule(nonInlinableRules, className, nonInlinablePart);
103
+ }
41
104
  }
105
+ return;
106
+ }
42
107
 
43
- // Only the prelude names the class that owns the rule; classes referenced
44
- // inside the block (e.g. `.group` in `:where(.group)`) must not key it.
45
- const selectorClasses: string[] = [];
46
- walk(rule.prelude, {
47
- visit: 'ClassSelector',
48
- enter(classSelector) {
49
- selectorClasses.push(string.decode(classSelector.name));
50
- },
51
- });
52
- if (isRuleInlinable(rule)) {
53
- for (const className of selectorClasses) {
54
- if (classSet.has(className)) {
55
- appendRule(inlinableRules, className, rule);
56
- }
108
+ if (isRuleInlinable(rule)) {
109
+ for (const className of selectorClasses) {
110
+ if (classSet.has(className)) {
111
+ appendRule(inlinableRules, className, rule);
112
+ }
113
+ }
114
+ } else {
115
+ const { inlinablePart, nonInlinablePart } = splitMixedRule(rule);
116
+ for (const className of selectorClasses) {
117
+ if (!classSet.has(className)) continue;
118
+ if (inlinablePart) {
119
+ appendRule(inlinableRules, className, inlinablePart);
57
120
  }
58
- } else {
59
- const { inlinablePart, nonInlinablePart } = splitMixedRule(rule);
60
- for (const className of selectorClasses) {
61
- if (!classSet.has(className)) continue;
62
- if (inlinablePart) {
63
- appendRule(inlinableRules, className, inlinablePart);
64
- }
65
- if (nonInlinablePart) {
66
- appendRule(nonInlinableRules, className, nonInlinablePart);
67
- }
121
+ if (nonInlinablePart) {
122
+ appendRule(nonInlinableRules, className, nonInlinablePart);
68
123
  }
69
124
  }
125
+ }
126
+ };
127
+
128
+ walk(root, {
129
+ enter(node) {
130
+ if (node.type === 'Atrule') {
131
+ if (NON_INLINABLE_ATRULES.has(node.name.toLowerCase())) {
132
+ enclosingAtRules.push(node);
133
+ }
134
+ } else if (node.type === 'Rule') {
135
+ handleRule(node);
136
+ }
137
+ },
138
+ leave(node) {
139
+ if (
140
+ node.type === 'Atrule' &&
141
+ NON_INLINABLE_ATRULES.has(node.name.toLowerCase())
142
+ ) {
143
+ enclosingAtRules.pop();
144
+ }
70
145
  },
71
146
  });
147
+
72
148
  return {
73
149
  inlinable: inlinableRules,
74
150
  nonInlinable: nonInlinableRules,