Repository: atlassian-labs/compiled
Pull request #1829: Fix the case when the styles are defined below the component and the …
Author: login: liamqma
Created: 2025-05-14T01:50:08Z | Merged: 2025-05-19T00:46:30Z

This PR fixes the below case:

```
<Box xcss={styles.main}>{children}</Box>

const styles = cssMap({
	main: {
		paddingTop: "10px",
	},
});

```

### PR checklist

Don't delete me!

I have...

- [x] Updated or added applicable tests
- [x] Updated the documentation in `website/`
- [x] Added a changeset (if making any changes that affect Compiled's behaviour)



Diff:
diff --git a/.changeset/wild-numbers-share.md b/.changeset/wild-numbers-share.md
new file mode 100644
index 000000000..d0a9f836f
--- /dev/null
+++ b/.changeset/wild-numbers-share.md
@@ -0,0 +1,5 @@
+---
+'@compiled/babel-plugin': patch
+---
+
+Resolve an issue where cssMap was being defined after its consumer, which relies on the xcss prop.
diff --git a/packages/babel-plugin/src/css-map/__tests__/index.test.ts b/packages/babel-plugin/src/css-map/__tests__/index.test.ts
index b557ee9d7..83d00c4ed 100644
--- a/packages/babel-plugin/src/css-map/__tests__/index.test.ts
+++ b/packages/babel-plugin/src/css-map/__tests__/index.test.ts
@@ -36,6 +36,37 @@ describe('css map basic functionality', () => {
     ]);
   });
 
+  it('should transform css map when the styles are defined below the component and the component uses xcss prop', () => {
+    const actual = transform(
+      `
+      import { cssMap } from '@compiled/react';
+
+      <Box xcss={styles.danger} />
+
+      const styles = cssMap(${styles});
+    `,
+      { pretty: true }
+    );
+
+    expect(actual).toMatchInlineSnapshot(`
+      "import * as React from "react";
+      import { ax, ix, CC, CS } from "@compiled/react/runtime";
+      const _4 = "._bfhkbf54{background-color:green}";
+      const _3 = "._syazbf54{color:green}";
+      const _2 = "._bfhk5scu{background-color:red}";
+      const _ = "._syaz5scu{color:red}";
+      <CC>
+        <CS>{[_, _2, _3, _4]}</CS>
+        {<Box xcss={styles.danger} />}
+      </CC>;
+      const styles = {
+        danger: "_syaz5scu _bfhk5scu",
+        success: "_syazbf54 _bfhkbf54",
+      };
+      "
+    `);
+  });
+
   it('should transform css map even when the styles are defined below the component', () => {
     const actual = transform(`
       import { cssMap } from '@compiled/react';
diff --git a/packages/babel-plugin/src/utils/css-builders.ts b/packages/babel-plugin/src/utils/css-builders.ts
index aa8a98f1d..3b247d28b 100644
--- a/packages/babel-plugin/src/utils/css-builders.ts
+++ b/packages/babel-plugin/src/utils/css-builders.ts
@@ -698,7 +698,7 @@ const extractObjectExpression = (node: t.ObjectExpression, meta: Metadata): CSSO
  *
  * @returns {Boolean} Whether the cache was generated
  */
-const generateCacheForCSSMap = (node: t.Identifier, meta: Metadata): void => {
+export const generateCacheForCSSMap = (node: t.Identifier, meta: Metadata): void => {
   if (meta.state.cssMap[node.name] || meta.state.ignoreMemberExpressions[node.name]) {
     return;
   }
diff --git a/packages/babel-plugin/src/xcss-prop/index.ts b/packages/babel-plugin/src/xcss-prop/index.ts
index cba12ce48..02887803c 100644
--- a/packages/babel-plugin/src/xcss-prop/index.ts
+++ b/packages/babel-plugin/src/xcss-prop/index.ts
@@ -4,7 +4,7 @@ import * as t from '@babel/types';
 import type { Metadata } from '../types';
 import { buildCodeFrameError, getPathOfNode } from '../utils/ast';
 import { compiledTemplate } from '../utils/build-compiled-component';
-import { buildCss } from '../utils/css-builders';
+import { buildCss, generateCacheForCSSMap } from '../utils/css-builders';
 import { transformCssItems } from '../utils/transform-css-items';
 
 function getJsxAttributeExpressionContainer(path?: NodePath<t.JSXAttribute>) {
@@ -29,13 +29,15 @@ function staticObjectInvariant(expression: t.ObjectExpression, meta: Metadata) {
   );
 }
 
-function collectPathMemberExpressionIdentifiers(propPath: NodePath<t.JSXAttribute>): string[] {
-  const identifiers: string[] = [];
+function collectPathMemberExpressionIdentifiers(
+  propPath: NodePath<t.JSXAttribute>
+): t.Identifier[] {
+  const identifiers: t.Identifier[] = [];
 
   propPath.traverse({
     MemberExpression(node) {
       if (node.node.object.type === 'Identifier') {
-        identifiers.push(node.node.object.name);
+        identifiers.push(node.node.object);
       }
     },
   });
@@ -112,7 +114,13 @@ export const visitXcssPropPath = (path: NodePath<t.JSXOpeningElement>, meta: Met
     // 1. Dot notation, such as "styles.text"
     // 2. Bracket notation, such as "styles[appearance]"
     const identifiers = collectPathMemberExpressionIdentifiers(propPath);
-    const sheets = collectPassStyles(meta, identifiers);
+    identifiers.forEach((identifier) => {
+      generateCacheForCSSMap(identifier, meta);
+    });
+    const sheets = collectPassStyles(
+      meta,
+      identifiers.map((identifier) => identifier.name)
+    );
 
     if (sheets.length === 0) {
       // No sheets were extracted — bail out from the transform.


Reviews:
user: login: dddlr
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-15T05:00:56Z
user: login: liamqma
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-15T05:17:33Z
user: login: liamqma
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-15T23:33:47Z
user: login: dddlr
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-15T23:48:26Z
user: login: dddlr
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-15T23:58:52Z
user: login: kylorhall-atlassian
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T00:37:21Z
user: login: kylorhall-atlassian
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-16T00:38:27Z

Review comments:
diff_hunk: @@ -112,7 +114,13 @@ export const visitXcssPropPath = (path: NodePath<t.JSXOpeningElement>, meta: Met
     // 1. Dot notation, such as "styles.text"
     // 2. Bracket notation, such as "styles[appearance]"
     const identifiers = collectPathMemberExpressionIdentifiers(propPath);
-    const sheets = collectPassStyles(meta, identifiers);
+    identifiers.forEach((identifier) => {
path: packages/babel-plugin/src/xcss-prop/index.ts
user: login: dddlr
body: curious how big of a build time impact running `generateCacheForCSSMap` for every identifier in `cssMap` usages here would be

or do you anticipate that it will be negligible because of this line in `generateCacheForCSSMap`, which i assume prevents `generateCacheForCSSMap` from running more than it needs to?

```ts
  if (meta.state.cssMap[node.name] || meta.state.ignoreMemberExpressions[node.name]) {
    return;
  }
```
created_at: 2025-05-15T05:00:55Z
author_association: COLLABORATOR
diff_hunk: @@ -112,7 +114,13 @@ export const visitXcssPropPath = (path: NodePath<t.JSXOpeningElement>, meta: Met
     // 1. Dot notation, such as "styles.text"
     // 2. Bracket notation, such as "styles[appearance]"
     const identifiers = collectPathMemberExpressionIdentifiers(propPath);
-    const sheets = collectPassStyles(meta, identifiers);
+    identifiers.forEach((identifier) => {
path: packages/babel-plugin/src/xcss-prop/index.ts
user: login: liamqma
body: yea, it should be negligible since it's cached.
created_at: 2025-05-15T05:17:33Z
author_association: COLLABORATOR
diff_hunk: @@ -112,7 +114,13 @@ export const visitXcssPropPath = (path: NodePath<t.JSXOpeningElement>, meta: Met
     // 1. Dot notation, such as "styles.text"
     // 2. Bracket notation, such as "styles[appearance]"
     const identifiers = collectPathMemberExpressionIdentifiers(propPath);
-    const sheets = collectPassStyles(meta, identifiers);
+    identifiers.forEach((identifier) => {
path: packages/babel-plugin/src/xcss-prop/index.ts
user: login: liamqma
body: generateCacheForCSSMap is executed for each identifier in the memberExpression.

For instance, consider the following example:

```
<Box xcss={[styles.foo, styles.bar, anotherStyles.foo]} />
```

The function will iterate over the array [styles.foo, styles.bar, anotherStyles.foo], making the following calls:

- generateCacheForCSSMap(styles)
- generateCacheForCSSMap(styles) // it will early return because styles is already parsed
- generateCacheForCSSMap(anotherStyles)
created_at: 2025-05-15T23:33:47Z
author_association: COLLABORATOR
diff_hunk: @@ -112,7 +114,13 @@ export const visitXcssPropPath = (path: NodePath<t.JSXOpeningElement>, meta: Met
     // 1. Dot notation, such as "styles.text"
     // 2. Bracket notation, such as "styles[appearance]"
     const identifiers = collectPathMemberExpressionIdentifiers(propPath);
-    const sheets = collectPassStyles(meta, identifiers);
+    identifiers.forEach((identifier) => {
path: packages/babel-plugin/src/xcss-prop/index.ts
user: login: dddlr
body: thank you for the extra context! yeah looks good 👍 
created_at: 2025-05-15T23:58:52Z
author_association: COLLABORATOR
diff_hunk: @@ -112,7 +114,13 @@ export const visitXcssPropPath = (path: NodePath<t.JSXOpeningElement>, meta: Met
     // 1. Dot notation, such as "styles.text"
     // 2. Bracket notation, such as "styles[appearance]"
     const identifiers = collectPathMemberExpressionIdentifiers(propPath);
-    const sheets = collectPassStyles(meta, identifiers);
+    identifiers.forEach((identifier) => {
path: packages/babel-plugin/src/xcss-prop/index.ts
user: login: kylorhall-atlassian
body: Yeah, I think this makes sense.  Plus, isn't the cache on a per-file basis anyways?  Just avoids that `styles.foo`, `styles.bar` from collecting `styles` twice?

I was never sold that we're drastically bloating memory here, but if it's cached across all tens of thousands of files and it's storing a full `identifier`, not `identifier.name`, it could be bloating something…but I think memory is relatively acceptable unless there's a leak.
created_at: 2025-05-16T00:37:21Z
author_association: COLLABORATOR

PR comments:
user: login: atlassian-cla-bot[bot]
created_at: 2025-05-14T01:50:11Z
body: Thank you for your submission! Like many open source projects, we ask that you sign our <a href=https://opensource.atlassian.com/cla>CLA (Contributor License Agreement)</a> before we can accept your contribution.
      <strong>If your email is listed below</strong>, please ensure that you sign the CLA with the <strong>same email address</strong>.</br>
      <strong>The following users still need to sign our CLA:</strong></br>❌AtlassianRubberDuck
<p><sub>Already signed the CLA? To re-check, try refreshing the page.</sub></p>
author_association: NONE
user: login: changeset-bot[bot]
created_at: 2025-05-14T01:50:11Z
body: ###  🦋  Changeset detected

Latest commit: e6506f12a9685d6686d7d02a31d476ed18ee9644

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name                   | Type  |
| ---------------------- | ----- |
| @compiled/babel-plugin | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/atlassian-labs/compiled/new/fix-xcss?filename=.changeset/tiny-onions-cheer.md&value=---%0A%22%40compiled%2Fbabel-plugin%22%3A%20patch%0A---%0A%0AFix%20the%20case%20when%20the%20styles%20are%20defined%20below%20the%20component%20and%20the%20%E2%80%A6%0A)


author_association: NONE
user: login: netlify[bot]
created_at: 2025-05-14T01:50:22Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *compiled-css-in-js* canceled.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | e6506f12a9685d6686d7d02a31d476ed18ee9644 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/compiled-css-in-js/deploys/68240a8c8377ec0008af845d |
author_association: NONE