Skip to content

Commit

Permalink
Introduce createStrictAPI (#1566)
Browse files Browse the repository at this point in the history
* feat: add create api exploration

* fix: type violation when using xcess properties

* feat: add css map types to create api

* chore: add to test case

* chore: another test case

* chore: fix types

* chore: expose cs

* chore: fix

* feat: adds spike code

* feat: add xcss func type

* chore: rename api

* fix: pseudo support for xcss prop

* chore: separate test cases

* chore: fix test

* chore: move api behind a module

* feat: add support for custom module origins

* chore: add assertions to xcss prop usage

* chore: add assertions for css()

* chore: add tests for cssMap()

* feat: add support for absolute/pkg paths

* chore: rename to import sources

* chore: rename to strict

* chore: update jsdoc

* chore: add jsdoc

* chore: stub

* chore: rename

* chore: fix tests

* chore: fix build

* chore: changeset

* chore: use root path

* chore: remove example tags

* chore: update error message to give as much context as possible
  • Loading branch information
itsdouges authored Dec 3, 2023
1 parent 765b599 commit 9857009
Show file tree
Hide file tree
Showing 22 changed files with 847 additions and 31 deletions.
49 changes: 49 additions & 0 deletions .changeset/weak-numbers-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
'@compiled/babel-plugin': patch
'@compiled/react': patch
---

Introduce new API `createStrictAPI` which returns a strict subset of Compiled APIs augmented by a type definition.
This API does not change Compileds build time behavior — merely augmenting
the returned API types which enforce:

- all APIs use object types
- property values declared in the type definition must be used (else fallback to defaults)
- a strict subset of pseudo states/selectors
- unknown properties to be a type violation

To set up:

1. Declare the API in a module (either local or in a package):

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

// ./foo.ts
const { css, cssMap, XCSSProp, cx } = createStrictAPI<{
color: 'var(--ds-text)';
'&:hover': { color: 'var(--ds-text-hover)' };
}>();

// Expose APIs you want to support.
export { css, cssMap, XCSSProp, cx };
```

2. Configure Compiled to pick up this module:

```diff
// .compiledcssrc
{
+ "importSources": ["./foo.ts"]
}
```

3. Use the module in your application code:

```tsx
import { css } from './foo';

const styles = css({ color: 'var(--ds-text)' });

<div css={styles} />;
```
6 changes: 5 additions & 1 deletion babel.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
{
"nonce": "\"k0Mp1lEd\"",
"importReact": false,
"optimizeCss": false
"optimizeCss": false,
"importSources": [
"./packages/react/src/create-strict-api/__tests__/__fixtures__/strict-api",
"@fixture/strict-api-test"
]
}
]
]
Expand Down
9 changes: 9 additions & 0 deletions fixtures/strict-api-test/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "@fixture/strict-api-test",
"version": "0.1.0",
"private": true,
"main": "./src/index.ts",
"dependencies": {
"@compiled/react": "*"
}
}
12 changes: 12 additions & 0 deletions fixtures/strict-api-test/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createStrictAPI } from '@compiled/react';

const { css, XCSSProp, cssMap, cx } = createStrictAPI<{
'&:hover': {
color: 'var(--ds-text-hover)';
background: 'var(--ds-surface-hover)' | 'var(--ds-surface-sunken-hover)';
};
color: 'var(--ds-text)';
background: 'var(--ds-surface)' | 'var(--ds-surface-sunken)';
}>();

export { css, XCSSProp, cssMap, cx };
88 changes: 88 additions & 0 deletions packages/babel-plugin/src/__tests__/custom-import-source.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { transform } from '../test-utils';

describe('custom import source', () => {
it('should pick up custom relative import source', () => {
const actual = transform(
`
import { css } from '../bar/stub-api';
const styles = css({ color: 'red' });
<div css={styles} />
`,
{ filename: './foo/index.js', importSources: ['./bar/stub-api'] }
);

expect(actual).toInclude('@compiled/react/runtime');
});

it('should pick up custom absolute import source', () => {
const actual = transform(
`
import { css } from '/bar/stub-api';
const styles = css({ color: 'red' });
<div css={styles} />
`,
{ filename: './foo/index.js', importSources: ['/bar/stub-api'] }
);

expect(actual).toInclude('@compiled/react/runtime');
});

it('should pick up custom package import source', () => {
const actual = transform(
`
import { css } from '@af/compiled';
const styles = css({ color: 'red' });
<div css={styles} />
`,
{ filename: './foo/index.js', importSources: ['@af/compiled'] }
);

expect(actual).toInclude('@compiled/react/runtime');
});

it("should handle custom package sources that aren't found", () => {
expect(() =>
transform(
`
import { css } from '@af/compiled';
const styles = css({ color: 'red' });
<div css={styles} />
`,
{ filename: './foo/index.js', importSources: ['asdasd2323'] }
)
).not.toThrow();
});

it('should throw error explaining resolution steps when using custom import source that hasnt been configured', () => {
expect(() =>
transform(
`
/** @jsxImportSource @compiled/react */
import { css } from '@private/misconfigured';
const styles = css({ color: 'red' });
<div css={styles} />
`,
{ filename: '/foo/index.js', highlightCode: false }
)
).toThrowErrorMatchingInlineSnapshot(`
"/foo/index.js: This CallExpression was unable to have its styles extracted — no Compiled APIs were found in scope, if you're using createStrictAPI make sure to configure importSources (5:23).
3 | import { css } from '@private/misconfigured';
4 |
> 5 | const styles = css({ color: 'red' });
| ^^^^^^^^^^^^^^^^^^^^^
6 |
7 | <div css={styles} />
8 | "
`);
});
});
2 changes: 1 addition & 1 deletion packages/babel-plugin/src/__tests__/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('error handling', () => {
<div css={() => {}} />
`);
}).toThrowErrorMatchingInlineSnapshot(`
"unknown file: ArrowFunctionExpression isn't a supported CSS type - try using an object or string (4:18).
"unknown file: This ArrowFunctionExpression was unable to have its styles extracted — no Compiled APIs were found in scope, if you're using createStrictAPI make sure to configure importSources (4:18).
2 | import '@compiled/react';
3 |
> 4 | <div css={() => {}} />
Expand Down
47 changes: 42 additions & 5 deletions packages/babel-plugin/src/babel-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { basename } from 'path';
import { basename, resolve, join, dirname } from 'path';

import { declare } from '@babel/helper-plugin-utils';
import jsxSyntax from '@babel/plugin-syntax-jsx';
Expand Down Expand Up @@ -30,7 +30,7 @@ import { visitXcssPropPath } from './xcss-prop';
const packageJson = require('../package.json');
const JSX_SOURCE_ANNOTATION_REGEX = /\*?\s*@jsxImportSource\s+([^\s]+)/;
const JSX_ANNOTATION_REGEX = /\*?\s*@jsx\s+([^\s]+)/;
const COMPILED_MODULE = '@compiled/react';
const DEFAULT_IMPORT_SOURCE = '@compiled/react';

let globalCache: Cache | undefined;

Expand All @@ -41,6 +41,8 @@ export default declare<State>((api) => {
name: packageJson.name,
inherits: jsxSyntax,
pre(state) {
const rootPath = state.opts.root ?? this.cwd;

this.sheets = {};
this.cssMap = {};
let cache: Cache;
Expand All @@ -59,12 +61,25 @@ export default declare<State>((api) => {
this.pathsToCleanup = [];
this.pragma = {};
this.usesXcss = false;
this.importSources = [
DEFAULT_IMPORT_SOURCE,
...(this.opts.importSources
? this.opts.importSources.map((origin) => {
if (origin[0] === '.') {
// We've found a relative path, transform it to be fully qualified.
return join(rootPath, origin);
}

return origin;
})
: []),
];

if (typeof this.opts.resolver === 'object') {
this.resolver = this.opts.resolver;
} else if (typeof this.opts.resolver === 'string') {
this.resolver = require(require.resolve(this.opts.resolver, {
paths: [state.opts.root ?? this.cwd],
paths: [rootPath],
}));
}

Expand All @@ -80,7 +95,9 @@ export default declare<State>((api) => {
const jsxSourceMatches = JSX_SOURCE_ANNOTATION_REGEX.exec(comment.value);
const jsxMatches = JSX_ANNOTATION_REGEX.exec(comment.value);

if (jsxSourceMatches && jsxSourceMatches[1] === COMPILED_MODULE) {
// jsxPragmas currently only run on the top-level compiled module,
// hence we don't interrogate this.importSources.
if (jsxSourceMatches && jsxSourceMatches[1] === DEFAULT_IMPORT_SOURCE) {
// jsxImportSource pragma found - turn on CSS prop!
state.compiledImports = {};
state.pragma.jsxImportSource = true;
Expand Down Expand Up @@ -159,7 +176,27 @@ export default declare<State>((api) => {
},
},
ImportDeclaration(path, state) {
if (path.node.source.value !== COMPILED_MODULE) {
const userLandModule = path.node.source.value;

const isCompiledModule = this.importSources.some((compiledModuleOrigin) => {
if (userLandModule === DEFAULT_IMPORT_SOURCE || compiledModuleOrigin === userLandModule) {
return true;
}

if (
state.filename &&
userLandModule[0] === '.' &&
userLandModule.endsWith(basename(compiledModuleOrigin))
) {
// Relative import that might be a match, resolve the relative path and compare.
const fullpath = resolve(dirname(state.filename), userLandModule);
return fullpath === compiledModuleOrigin;
}

return false;
});

if (!isCompiledModule) {
return;
}

Expand Down
8 changes: 5 additions & 3 deletions packages/babel-plugin/src/styled/__tests__/behaviour.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,9 @@ describe('styled component behaviour', () => {

it('creates a separate var name for positive and negative values of the same interpolation', () => {
const actual = transform(`
import { styled } from '@compiled/react';
import { styled } from '@compiled/react';
const random = Math.random;
const LayoutRight = styled.aside\`
margin-right: -\${random() * 5}px;
margin-left: \${random() * 5}px;
Expand Down Expand Up @@ -749,7 +749,9 @@ describe('styled component behaviour', () => {
\${props => props.isShown && (props.isPrimary ? { color: 'blue' } : { color: 'green' })};
\`;
`)
).toThrow("ConditionalExpression isn't a supported CSS type");
).toThrow(
'This ConditionalExpression was unable to have its styles extracted — try to define them statically using Compiled APIs instead'
);
});

it('should apply conditional CSS when using "key: value" in string form', () => {
Expand Down
10 changes: 10 additions & 0 deletions packages/babel-plugin/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ export interface PluginOptions {
*/
nonce?: string;

/**
* Custom module origins that Compiled should compile when using APIs from.
*/
importSources?: string[];

/**
* Callback fired at the end of the file pass when files have been included in the transformation.
*/
Expand Down Expand Up @@ -115,6 +120,11 @@ export interface State extends PluginPass {
css?: string;
};

/**
* Modules that expose APIs to be compiled by Compiled.
*/
importSources: string[];

/**
* Details of pragmas that are currently enabled in the pass.
*/
Expand Down
9 changes: 8 additions & 1 deletion packages/babel-plugin/src/utils/css-builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -953,8 +953,15 @@ export const buildCss = (node: t.Expression | t.Expression[], meta: Metadata): C
return buildCss(node.arguments[0] as t.ObjectExpression, meta);
}

const areCompiledAPIsEnabled =
meta.state.compiledImports && Object.keys(meta.state.compiledImports).length > 0;

const errorMessage = areCompiledAPIsEnabled
? 'try to define them statically using Compiled APIs instead'
: "no Compiled APIs were found in scope, if you're using createStrictAPI make sure to configure importSources";

throw buildCodeFrameError(
`${node.type} isn't a supported CSS type - try using an object or string`,
`This ${node.type} was unable to have its styles extracted — ${errorMessage}`,
node,
meta.parentPath
);
Expand Down
1 change: 1 addition & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
},
"devDependencies": {
"@compiled/benchmark": "^1.1.0",
"@fixture/strict-api-test": "*",
"@testing-library/react": "^12.1.5",
"@types/jsdom": "^16.2.15",
"@types/react-dom": "^17.0.22",
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/class-names/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export interface ClassNamesProps<TProps> {
}

/**
* ## Class names
* ## Class Names
*
* Use a component where styles are not necessarily used on a JSX element.
* For further details [read the documentation](https://compiledcssinjs.com/docs/api-class-names).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createStrictAPI } from '@compiled/react';

const { css, XCSSProp, cssMap, cx } = createStrictAPI<{
'&:hover': {
color: 'var(--ds-text-hover)';
background: 'var(--ds-surface-hover)' | 'var(--ds-surface-sunken-hover)';
};
color: 'var(--ds-text)';
background: 'var(--ds-surface)' | 'var(--ds-surface-sunken)';
bkgrnd: 'red' | 'green';
}>();

export { css, XCSSProp, cssMap, cx };
Loading

0 comments on commit 9857009

Please sign in to comment.