-
Notifications
You must be signed in to change notification settings - Fork 416
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(atomic): create an atomic package for the css API #867
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
<p align="center"> | ||
<img alt="Linaria" src="https://raw.githubusercontent.com/callstack/linaria/HEAD/website/assets/linaria-logo@2x.png" width="496"> | ||
</p> | ||
|
||
<p align="center"> | ||
Zero-runtime CSS in JS library. | ||
</p> | ||
|
||
--- | ||
|
||
### 📖 Please refer to the [GitHub](https://github.com/callstack/linaria#readme) for full documentation. | ||
|
||
## Features | ||
|
||
- Write CSS in JS, but with **zero runtime**, CSS is extracted to CSS files during build | ||
- Familiar **CSS syntax** with Sass like nesting | ||
- Use **dynamic prop based styles** with the React bindings, uses CSS variables behind the scenes | ||
- Easily find where the style was defined with **CSS sourcemaps** | ||
- **Lint your CSS** in JS with [stylelint](https://github.com/stylelint/stylelint) | ||
- Use **JavaScript for logic**, no CSS preprocessor needed | ||
- Optionally use any **CSS preprocessor** such as Sass or PostCSS | ||
|
||
**[Why use Linaria](../../docs/BENEFITS.md)** | ||
|
||
## Installation | ||
|
||
```sh | ||
npm install @linaria/core @linaria/react @linaria/babel-preset @linaria/shaker | ||
``` | ||
|
||
or | ||
|
||
```sh | ||
yarn add @linaria/core @linaria/react @linaria/babel-preset @linaria/shaker | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
const config = require('../../babel.config'); | ||
|
||
module.exports = config; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
{ | ||
"name": "@linaria/atomic", | ||
"version": "3.0.0-beta.13", | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"description": "Blazing fast zero-runtime CSS in JS library", | ||
"sideEffects": false, | ||
"main": "lib/index.js", | ||
"module": "esm/index.js", | ||
"types": "types/index.d.ts", | ||
"files": [ | ||
"types/", | ||
"lib/", | ||
"esm/" | ||
], | ||
"license": "MIT", | ||
"repository": "git@github.com:callstack/linaria.git", | ||
"bugs": { | ||
"url": "https://github.com/callstack/linaria/issues" | ||
}, | ||
"homepage": "https://github.com/callstack/linaria#readme", | ||
"keywords": [ | ||
"react", | ||
"linaria", | ||
"css", | ||
"css-in-js", | ||
"styled-components" | ||
], | ||
"scripts": { | ||
"build:lib": "cross-env NODE_ENV=legacy babel src --out-dir lib --extensions '.js,.jsx,.ts,.tsx' --source-maps --delete-dir-on-start", | ||
"build:esm": "babel src --out-dir esm --extensions '.js,.jsx,.ts,.tsx' --source-maps --delete-dir-on-start", | ||
"build": "yarn build:lib && yarn build:esm", | ||
"build:declarations": "tsc --emitDeclarationOnly --outDir types", | ||
"prepare": "yarn build && yarn build:declarations", | ||
"typecheck": "tsc --noEmit --composite false", | ||
"watch": "yarn build --watch" | ||
}, | ||
"dependencies": { | ||
"@linaria/utils": "^3.0.0-beta.13", | ||
"postcss": "^8.3.11" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export type CSSProperties = { | ||
[key: string]: string | number | CSSProperties; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import postcss from 'postcss'; | ||
import { slugify } from '@linaria/utils'; | ||
|
||
export default function atomize(cssText: string) { | ||
const atomicRules: { | ||
className: string; | ||
cssText: string; | ||
property: string; | ||
}[] = []; | ||
|
||
const stylesheet = postcss.parse(cssText); | ||
|
||
stylesheet.walkDecls((decl) => { | ||
const parent = decl.parent; | ||
if (parent === stylesheet) { | ||
const line = `${decl.prop}: ${decl.value};`; | ||
const className = `atm_${slugify(line)}`; | ||
atomicRules.push({ | ||
property: decl.prop, | ||
className, | ||
cssText: line, | ||
}); | ||
} | ||
}); | ||
// Things like @media rules | ||
stylesheet.walkAtRules((atRule) => { | ||
atRule.walkDecls((decl) => { | ||
const slug = slugify( | ||
[atRule.name, atRule.params, decl.prop, decl.value].join(';') | ||
); | ||
const className = `atm_${slug}`; | ||
atomicRules.push({ | ||
// For @ rules we want the unique property we do merging on to contain | ||
// the atrule params, eg. `media only screen and (max-width: 600px)` | ||
// But not the value. That way, our hashes will match when the media rule + | ||
// the declaration property match, and we can merge atomic media rules | ||
property: [atRule.name, atRule.params, decl.prop].join(' '), | ||
className, | ||
cssText: `@${atRule.name} ${atRule.params} { .${className} { ${decl.prop}: ${decl.value}; } }`, | ||
}); | ||
}); | ||
}); | ||
|
||
return atomicRules; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import type { CSSProperties } from './CSSProperties'; | ||
|
||
export interface StyleCollectionObject { | ||
[key: string]: string; | ||
} | ||
|
||
type CSS = ( | ||
strings: TemplateStringsArray, | ||
...exprs: Array<string | number | CSSProperties> | ||
) => StyleCollectionObject; | ||
|
||
export const css: CSS = () => { | ||
throw new Error( | ||
'Using the "css" tag in runtime is not supported. Make sure you have set up the Babel plugin correctly.' | ||
); | ||
}; | ||
|
||
export default css; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export { default as css } from './css'; | ||
export { default as atomize } from './atomize'; | ||
export { cx } from '@linaria/utils'; | ||
|
||
export type { CSSProperties } from './CSSProperties'; | ||
export type { StyleCollectionObject } from './css'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"extends": "../../tsconfig.json", | ||
"compilerOptions": { "paths": {}, "rootDir": "src/" }, | ||
"references": [{ "path": "../utils" }] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
import slugify from '../src/utils/slugify'; | ||
import { slugify } from '@linaria/utils'; | ||
|
||
export default slugify; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -64,7 +64,7 @@ export default function getTemplateProcessor( | |
// Only works when it's assigned to a variable | ||
let isReferenced = true; | ||
|
||
const [, slug, displayName, className] = getLinariaComment(path); | ||
const [type, slug, displayName, className] = getLinariaComment(path); | ||
|
||
const parent = path.findParent( | ||
(p) => | ||
|
@@ -290,24 +290,59 @@ export default function getTemplateProcessor( | |
); | ||
|
||
path.addComment('leading', '#__PURE__'); | ||
} else { | ||
} else if (type === 'css') { | ||
path.replaceWith(t.stringLiteral(className!)); | ||
} | ||
|
||
if (!isReferenced && !cssText.includes(':global')) { | ||
return; | ||
} | ||
|
||
debug( | ||
'evaluator:template-processor:extracted-rule', | ||
`\n${selector} {${cssText}\n}` | ||
); | ||
if (type === 'atomic-css') { | ||
const { atomize } = options; | ||
if (!atomize) { | ||
throw new Error( | ||
'The atomic css API was detected, but an atomize function was not passed in the linaria configuration.' | ||
); | ||
} | ||
const atomicRules = atomize(cssText); | ||
atomicRules.forEach((rule) => { | ||
state.rules[`.${rule.className}`] = { | ||
cssText: rule.cssText, | ||
start: path.parent?.loc?.start ?? null, | ||
className: className!, | ||
displayName: displayName!, | ||
atom: true, | ||
}; | ||
|
||
debug( | ||
'evaluator:template-processor:extracted-atomic-rule', | ||
`\n${rule.cssText}` | ||
); | ||
}); | ||
|
||
const atomicClassObject = t.objectExpression( | ||
atomicRules.map((rule) => | ||
t.objectProperty( | ||
t.stringLiteral(rule.property), | ||
t.stringLiteral(rule.className) | ||
) | ||
) | ||
); | ||
|
||
state.rules[selector] = { | ||
cssText, | ||
className: className!, | ||
displayName: displayName!, | ||
start: path.parent?.loc?.start ?? null, | ||
}; | ||
path.replaceWith(atomicClassObject); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another thought I had was that this is different from the Some alternatives: We could instead produce a string that the cx function would need to resolve, like this:
and then the atomic In performance testing on https://jsbench.me/, there's a slight disadvantage to doing the string manipulation, but only slight. I think this is probably not preferable, as using the class names as strings without Interested in your thoughts though There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think, there should be just one |
||
} else { | ||
debug( | ||
'evaluator:template-processor:extracted-rule', | ||
`\n${selector} {${cssText}\n}` | ||
); | ||
|
||
state.rules[selector] = { | ||
cssText, | ||
className: className!, | ||
displayName: displayName!, | ||
start: path.parent?.loc?.start ?? null, | ||
}; | ||
} | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do you think, is it possible to specify the real positions of rules for source-maps?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a good question – I think it's a little tricky because a single atom might have come from multiple different modules.
The way it's done here, it should be that an atom's source map points to the last place it was generated from (it does work correctly on the website example). I think that's probably better than nothing – in the case where you have a unique atom, and you want to know where it's coming from, the source map will work for that. In the case where the atom is shared, the source map will point to at least one place where it is defined.