{}} />
`);
}).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 |
{}} />
diff --git a/packages/babel-plugin/src/babel-plugin.ts b/packages/babel-plugin/src/babel-plugin.ts
index 59009704e..d9f17477c 100644
--- a/packages/babel-plugin/src/babel-plugin.ts
+++ b/packages/babel-plugin/src/babel-plugin.ts
@@ -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';
@@ -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;
@@ -41,6 +41,8 @@ export default declare((api) => {
name: packageJson.name,
inherits: jsxSyntax,
pre(state) {
+ const rootPath = state.opts.root ?? this.cwd;
+
this.sheets = {};
this.cssMap = {};
let cache: Cache;
@@ -59,12 +61,25 @@ export default declare((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],
}));
}
@@ -80,7 +95,9 @@ export default declare((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;
@@ -159,7 +176,27 @@ export default declare((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;
}
diff --git a/packages/babel-plugin/src/styled/__tests__/behaviour.test.ts b/packages/babel-plugin/src/styled/__tests__/behaviour.test.ts
index fb8836c19..a70dfeadc 100644
--- a/packages/babel-plugin/src/styled/__tests__/behaviour.test.ts
+++ b/packages/babel-plugin/src/styled/__tests__/behaviour.test.ts
@@ -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;
@@ -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', () => {
diff --git a/packages/babel-plugin/src/types.ts b/packages/babel-plugin/src/types.ts
index 2f0b2fade..5d9257b2d 100644
--- a/packages/babel-plugin/src/types.ts
+++ b/packages/babel-plugin/src/types.ts
@@ -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.
*/
@@ -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.
*/
diff --git a/packages/babel-plugin/src/utils/css-builders.ts b/packages/babel-plugin/src/utils/css-builders.ts
index 758f1850a..ad0bcc869 100644
--- a/packages/babel-plugin/src/utils/css-builders.ts
+++ b/packages/babel-plugin/src/utils/css-builders.ts
@@ -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
);
diff --git a/packages/react/package.json b/packages/react/package.json
index c72f8a6b5..f2243a5cc 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -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",
diff --git a/packages/react/src/class-names/index.ts b/packages/react/src/class-names/index.ts
index c836ef21f..51782e3db 100644
--- a/packages/react/src/class-names/index.ts
+++ b/packages/react/src/class-names/index.ts
@@ -19,7 +19,7 @@ export interface ClassNamesProps {
}
/**
- * ## 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).
diff --git a/packages/react/src/create-strict-api/__tests__/__fixtures__/strict-api.ts b/packages/react/src/create-strict-api/__tests__/__fixtures__/strict-api.ts
new file mode 100644
index 000000000..d1a5d8fae
--- /dev/null
+++ b/packages/react/src/create-strict-api/__tests__/__fixtures__/strict-api.ts
@@ -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 };
diff --git a/packages/react/src/create-strict-api/__tests__/index.test.tsx b/packages/react/src/create-strict-api/__tests__/index.test.tsx
new file mode 100644
index 000000000..1a755eb8c
--- /dev/null
+++ b/packages/react/src/create-strict-api/__tests__/index.test.tsx
@@ -0,0 +1,312 @@
+/** @jsxImportSource @compiled/react */
+import { render } from '@testing-library/react';
+
+import { css, cssMap, XCSSProp } from './__fixtures__/strict-api';
+
+describe('createStrictAPI()', () => {
+ describe('css()', () => {
+ it('should type error when circumventing the excess property check', () => {
+ const styles = css({
+ color: 'var(--ds-text)',
+ accentColor: 'red',
+ // @ts-expect-error — Type 'string' is not assignable to type 'undefined'.ts(2322)
+ bkgrnd: 'red',
+ '&:hover': {
+ color: 'var(--ds-text-hover)',
+ // @ts-expect-error — Type 'string' is not assignable to type 'undefined'.ts(2322)
+ bkgrnd: 'red',
+ },
+ });
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('div')).toHaveCompiledCss('color', 'var(--ds-text)');
+ });
+
+ it('should constrain declared types for css() func', () => {
+ // @ts-expect-error — Type '"red"' is not assignable to type '"var(--ds-surface)" | "var(--ds-surface-sunken" | undefined'.ts(2322)
+ const styles = css({ background: 'red' });
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('div')).toHaveCompiledCss('background-color', 'red');
+ });
+
+ it('should mark all properties as optional', () => {
+ const styles1 = css({});
+ const styles2 = css({ '&:hover': {} });
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('div')).not.toHaveCompiledCss('color', 'red');
+ });
+
+ it('should constrain pseudos', () => {
+ const styles = css({
+ // @ts-expect-error — Type '"red"' is not assignable to type '"var(--ds-surface)" | "var(--ds-surface-sunken" | undefined'.ts(2322)
+ background: 'red',
+ '&:hover': {
+ // @ts-expect-error — Type '"red"' is not assignable to type '"var(--ds-surface)" | "var(--ds-surface-sunken" | undefined'.ts(2322)
+ background: 'red',
+ },
+ });
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('div')).toHaveCompiledCss('background-color', 'red', { target: ':hover' });
+ });
+
+ it('should allow valid properties inside pseudos that are different to root', () => {
+ const styles = css({
+ background: 'var(--ds-surface)',
+ '&:hover': {
+ accentColor: 'red',
+ background: 'var(--ds-surface-hover)',
+ },
+ });
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('div')).toHaveCompiledCss('background', 'var(--ds-surface-hover)', {
+ target: ':hover',
+ });
+ });
+
+ it('should allow valid properties', () => {
+ const styles = css({
+ background: 'var(--ds-surface)',
+ accentColor: 'red',
+ color: 'var(--ds-text)',
+ all: 'inherit',
+ '&:hover': { color: 'var(--ds-text-hover)' },
+ '&:invalid': { color: 'orange' },
+ });
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('div')).toHaveCompiledCss('all', 'inherit');
+ });
+ });
+
+ describe('cssMap()', () => {
+ it('should allow valid properties', () => {
+ const styles = cssMap({
+ primary: {
+ background: 'var(--ds-surface)',
+ accentColor: 'red',
+ all: 'inherit',
+ '&:hover': { color: 'var(--ds-text-hover)' },
+ '&:invalid': { color: 'orange' },
+ },
+ });
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('div')).toHaveCompiledCss('background', 'var(--ds-surface)');
+ });
+
+ it('should allow valid properties inside pseudos that are different to root', () => {
+ const styles = cssMap({
+ primary: {
+ background: 'var(--ds-surface)',
+ '&:hover': {
+ accentColor: 'red',
+ background: 'var(--ds-surface-hover)',
+ },
+ },
+ });
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('div')).toHaveCompiledCss('background', 'var(--ds-surface-hover)', {
+ target: ':hover',
+ });
+ });
+
+ it('should type error invalid vales', () => {
+ const styles = cssMap({
+ primary: {
+ // @ts-expect-error — Type '{ val: string; }' is not assignable to type 'Readonly> & PseudosDeclarations & EnforceSchema<{ background: "var(--ds-surface)" | "var(--ds-surface-sunken"; }>'.
+ val: 'ok',
+ },
+ });
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('div')).toHaveCompiledCss('val', 'ok');
+ });
+
+ it('should type error invalid values in pseudos', () => {
+ const styles = cssMap({
+ primary: {
+ // @ts-expect-error — Type '"red"' is not assignable to type '"var(--ds-surface)" | "var(--ds-surface-sunken" | undefined'.ts(2322)
+ background: 'red',
+ '&:hover': {
+ // @ts-expect-error — Type 'string' is not assignable to type 'never'.ts(2322)
+ val: 'ok',
+ },
+ },
+ });
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('div')).toHaveCompiledCss('val', 'ok', { target: ':hover' });
+ });
+ });
+
+ describe('XCSSProp', () => {
+ it('should allow valid values', () => {
+ function Button({ xcss }: { xcss: ReturnType> }) {
+ return ;
+ }
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('button')).toHaveCompiledCss('background', 'var(--ds-surface)');
+ });
+
+ it('should type error for invalid known values', () => {
+ function Button({ xcss }: { xcss: ReturnType> }) {
+ return ;
+ }
+
+ const { getByTestId } = render(
+
+ );
+
+ expect(getByTestId('button')).toHaveCompiledCss('background-color', 'red');
+ });
+
+ it('should type error for invalid unknown values', () => {
+ function Button({ xcss }: { xcss: ReturnType> }) {
+ return ;
+ }
+
+ const { getByTestId } = render(
+