Skip to content

Commit

Permalink
Component declared styles (#1533)
Browse files Browse the repository at this point in the history
* chore: add example type test

* feat: introduce stricter types part 1

* chore: update docs

* feat: wire up xcss prop

* feat: adds xcss inline object support

* feat: add support for simple collection of pass styles

* chore: move

* feat: expose cx util

* chore: add failing test

* chore: remove strict types and replace with direct cx usage

* chore: fix types

* chore: code review

* feat: block at rules from xcss prop

* chore: add css prop test

* fix: update jsx namespace

* chore: add tests and jsdoc

* chore: code review

* chore: update changeset message

* chore: update type from code review

* chore: fix spelling

* chore: resolve code review comments

* chore: remove unused

* chore: fix types

* chore: ensure selectors isnt available

* feat: add scope option

---------

Co-authored-by: Alex Hinds <[email protected]>
  • Loading branch information
itsdouges and Alex Hinds authored Oct 26, 2023
1 parent 4a5449c commit 4caa678
Show file tree
Hide file tree
Showing 15 changed files with 808 additions and 94 deletions.
54 changes: 54 additions & 0 deletions .changeset/brown-students-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
'@compiled/babel-plugin': patch
'@compiled/react': patch
---

The xcss prop is now available.
Declare styles your component takes with all other styles marked as violations
by the TypeScript compiler. There are two primary use cases for xcss prop:

- safe style overrides
- inverting style declarations

Interverting style declarations is interesting for platform teams as
it means products only pay for styles they use as they're now the ones who declare
the styles!

The `XCSSProp` type has generics which must be defined — of which should be what you
explicitly want to maintain as API. Use `XCSSAllProperties` and `XCSSAllPseudos` types
to enable all properties and pseudos.

```tsx
import { type XCSSProp } from '@compiled/react';

interface MyComponentProps {
// Color is accepted, all other properties / pseudos are considered violations.
xcss?: XCSSProp<'color', never>;

// Only backgrond color and hover pseudo is accepted.
xcss?: XCSSProp<'backgroundColor', '&:hover'>;

// All properties are accepted, all pseudos are considered violations.
xcss?: XCSSProp<XCSSAllProperties, never>;

// All properties are accepted, only the hover pseudo is accepted.
xcss?: XCSSProp<XCSSAllProperties, '&:hover'>;
}

function MyComponent({ xcss }: MyComponentProps) {
return <div css={{ color: 'var(--ds-text-danger)' }} className={xcss} />;
}
```

The xcss prop works with static inline objects and the [cssMap](https://compiledcssinjs.com/docs/api-cssmap) API.

```tsx
// Declared as an inline object
<Component xcss={{ color: 'var(--ds-text)' }} />;

// Declared with the cssMap API
const styles = cssMap({ text: { color: 'var(--ds-text)' } });
<Component xcss={styles.text} />;
```

To concatenate and conditonally apply styles use the `cssMap` and `cx` functions.
13 changes: 13 additions & 0 deletions packages/babel-plugin/src/babel-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
isCompiledCSSMapCallExpression,
} from './utils/is-compiled';
import { normalizePropsUsage } from './utils/normalize-props-usage';
import { visitXcssPropPath } from './xcss-prop';

// eslint-disable-next-line @typescript-eslint/no-var-requires
const packageJson = require('../package.json');
Expand Down Expand Up @@ -65,6 +66,8 @@ export default declare<State>((api) => {
paths: [state.opts.root ?? this.cwd],
}));
}

this.transformCache = new WeakMap();
},
visitor: {
Program: {
Expand All @@ -87,6 +90,15 @@ export default declare<State>((api) => {
}
}
}

if (
!state.opts.requireCompiledInScopeForXCSSProp &&
!state.compiledImports &&
/(x|X)css={/.exec(file.code)
) {
// xcss prop was found turn on Compiled
state.compiledImports = {};
}
},
exit(path, state) {
if (!state.compiledImports) {
Expand Down Expand Up @@ -229,6 +241,7 @@ export default declare<State>((api) => {
return;
}

visitXcssPropPath(path, { context: 'root', state, parentPath: path });
visitCssPropPath(path, { context: 'root', state, parentPath: path });
},
},
Expand Down
17 changes: 16 additions & 1 deletion packages/babel-plugin/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ export interface PluginOptions {
*/
importReact?: boolean;

/**
* By default the `xcss` prop works by just using it. To aid repositories
* migrating to Compiled `xcss` prop, you can use this config to have it
* only be enabled when Compiled has been activated either by jsx pragma
* or other Compiled APIs.
*
* Defaults to `false`.
*/
requireCompiledInScopeForXCSSProp?: boolean;

/**
* Security nonce that will be applied to inline style elements if defined.
*/
Expand Down Expand Up @@ -144,14 +154,19 @@ export interface State extends PluginPass {
includedFiles: string[];

/**
* Holds a record of currently evaluated CSS Map and its sheets in the module.
* Holds a record of evaluated `cssMap()` calls with their compiled style sheets in the current pass.
*/
cssMap: Record<string, string[]>;

/**
* A custom resolver used to statically evaluate import declarations
*/
resolver?: Resolver;

/**
* Holds paths that have been transformed that we can ignore.
*/
transformCache: WeakMap<NodePath<any>, true>;
}

interface CommonMetadata {
Expand Down
193 changes: 193 additions & 0 deletions packages/babel-plugin/src/xcss-prop/__tests__/transformation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { transform } from '../../test-utils';

describe('xcss prop transformation', () => {
it('should transform static inline object', () => {
const result = transform(`
<Component xcss={{ color: 'red' }} />
`);

expect(result).toMatchInlineSnapshot(`
"import * as React from "react";
import { ax, ix, CC, CS } from "@compiled/react/runtime";
const _ = "._syaz5scu{color:red}";
<CC>
<CS>{[_]}</CS>
{<Component xcss={"_syaz5scu"} />}
</CC>;
"
`);
});

it('should throw when not static', () => {
expect(() => {
transform(
`
import { bar } from './foo';
<Component xcss={{ color: bar }} />
`,
{ highlightCode: false }
);
}).toThrowErrorMatchingInlineSnapshot(`
"unknown file: Object given to the xcss prop must be static (4:23).
2 | import { bar } from './foo';
3 |
> 4 | <Component xcss={{ color: bar }} />
| ^^^^^^^^^^^^^^
5 | "
`);
});

it('should transform named xcss prop usage', () => {
const result = transform(`
<Component innerXcss={{ color: 'red' }} />
`);

expect(result).toMatchInlineSnapshot(`
"import * as React from "react";
import { ax, ix, CC, CS } from "@compiled/react/runtime";
const _ = "._syaz5scu{color:red}";
<CC>
<CS>{[_]}</CS>
{<Component innerXcss={"_syaz5scu"} />}
</CC>;
"
`);
});

it('should work with css map', () => {
const result = transform(`
import { cssMap } from '@compiled/react';
const styles = cssMap({
primary: { color: 'red' },
});
<Component xcss={styles.primary} />
`);

expect(result).toMatchInlineSnapshot(`
"import * as React from "react";
import { ax, ix, CC, CS } from "@compiled/react/runtime";
const _ = "._syaz5scu{color:red}";
const styles = {
primary: "_syaz5scu",
};
<CC>
<CS>{[_]}</CS>
{<Component xcss={styles.primary} />}
</CC>;
"
`);
});

it('should allow ternaries', () => {
const result = transform(`
import { cssMap } from '@compiled/react';
const styles = cssMap({
primary: { color: 'red' },
secondary: { color: 'blue' }
});
<Component xcss={isPrimary ? styles.primary : styles.secondary} />
`);

expect(result).toMatchInlineSnapshot(`
"import * as React from "react";
import { ax, ix, CC, CS } from "@compiled/react/runtime";
const _2 = "._syaz13q2{color:blue}";
const _ = "._syaz5scu{color:red}";
const styles = {
primary: "_syaz5scu",
secondary: "_syaz13q2",
};
<CC>
<CS>{[_, _2]}</CS>
{<Component xcss={isPrimary ? styles.primary : styles.secondary} />}
</CC>;
"
`);
});

it('should allow concatenating styles', () => {
const result = transform(`
import { cssMap, j } from '@compiled/react';
const styles = cssMap({
primary: { color: 'red' },
secondary: { color: 'blue' }
});
<Component xcss={j(isPrimary && styles.primary, !isPrimary && styles.secondary)} />
`);

expect(result).toMatchInlineSnapshot(`
"import * as React from "react";
import { ax, ix, CC, CS } from "@compiled/react/runtime";
import { j } from "@compiled/react";
const _2 = "._syaz13q2{color:blue}";
const _ = "._syaz5scu{color:red}";
const styles = {
primary: "_syaz5scu",
secondary: "_syaz13q2",
};
<CC>
<CS>{[_, _2]}</CS>
{
<Component
xcss={j(isPrimary && styles.primary, !isPrimary && styles.secondary)}
/>
}
</CC>;
"
`);
});

it('should ignore xcss prop when compiled must be in scope', () => {
const result = transform(
`
<Component xcss={{ color: 'red' }} />
`,
{ requireCompiledInScopeForXCSSProp: true }
);

expect(result).toMatchInlineSnapshot(`
"<Component
xcss={{
color: "red",
}}
/>;
"
`);
});

it('should transform xcss prop when compiled is in scope', () => {
const result = transform(
`
import { cssMap } from '@compiled/react';
const styles = cssMap({
primary: { color: 'red' },
});
<Component xcss={styles.primary} />
`,
{ requireCompiledInScopeForXCSSProp: true }
);

expect(result).toMatchInlineSnapshot(`
"import * as React from "react";
import { ax, ix, CC, CS } from "@compiled/react/runtime";
const _ = "._syaz5scu{color:red}";
const styles = {
primary: "_syaz5scu",
};
<CC>
<CS>{[_]}</CS>
{<Component xcss={styles.primary} />}
</CC>;
"
`);
});
});
Loading

0 comments on commit 4caa678

Please sign in to comment.