├── test
├── fileMock.js
├── styleMock.js
├── .babelrc
├── index.test.js.css
├── __snapshots__
│ └── index.test.js.snap
└── index.test.js
├── babel.js
├── loader.js
├── .babelrc
├── node
├── .babelrc
├── index.js.css
└── index.js
├── example
├── .babelrc
├── index.html
├── index.js.css
└── index.js
├── src
├── load-link.js
├── react.js
├── parseCSS.js
├── loader.js
├── server.js
├── hash.js
├── index.js
├── sheet.js
└── babel.js
├── .gitignore
├── webpack.umd.js
├── motivation.md
├── webpack.node.js
├── webpack.config.js
├── LICENSE
├── package.json
└── README.md
/test/fileMock.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/babel.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./lib/babel')
--------------------------------------------------------------------------------
/loader.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./lib/loader')
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env", "stage-0"]
3 | }
4 |
--------------------------------------------------------------------------------
/test/styleMock.js:
--------------------------------------------------------------------------------
1 | global.styleMocked = 'alphabetaomega'
--------------------------------------------------------------------------------
/node/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env", "stage-0", "react"],
3 | "plugins": [ "../lib/babel.js" ]
4 | }
--------------------------------------------------------------------------------
/node/index.js.css:
--------------------------------------------------------------------------------
1 | /* do not edit this file */
2 | .css-1wfimov { color: red; font-weight:var(--css-1wfimov-0) }
--------------------------------------------------------------------------------
/test/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env", "stage-0", "react"],
3 | "plugins": [ [ "../lib/babel.js", { "sync": true } ] ]
4 | }
--------------------------------------------------------------------------------
/example/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env", "stage-0", "react"],
3 | "plugins": [ ["../lib/babel.js", { sync: false, inline: true }] ]
4 | }
--------------------------------------------------------------------------------
/src/load-link.js:
--------------------------------------------------------------------------------
1 | export default async function load(path){
2 | // append a link tag
3 | // load it
4 | // resolve promise
5 | // thats it!
6 | }
--------------------------------------------------------------------------------
/node/index.js:
--------------------------------------------------------------------------------
1 | import css, { sheet } from '../src'
2 |
3 | let cls = css` color: red; font-weight:${'bold'} `
4 |
5 | import('./index.js.css').then(() => {
6 | console.log(sheet.rules())
7 | })
8 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | glam
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/test/index.test.js.css:
--------------------------------------------------------------------------------
1 | /* do not edit this file */
2 | .css-hwfcu5 { color:red }
3 | .css-1nd0lsp { color:red, font-weight:var(--css-1nd0lsp-0) }
4 | .css-1mdi6ak { color:red; font-weight:var(--css-1mdi6ak-0) }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # osx noise
2 | .DS_Store
3 | profile
4 |
5 | # xcode noise
6 | build/*
7 | *.mode1
8 | *.mode1v3
9 | *.mode2v3
10 | *.perspective
11 | *.perspectivev3
12 | *.pbxuser
13 | *.xcworkspace
14 | xcuserdata
15 |
16 | # svn & cvs
17 | .svn
18 | CVS
19 | node_modules
20 | lib
21 | example/*bundle*
22 | dist
23 |
--------------------------------------------------------------------------------
/src/react.js:
--------------------------------------------------------------------------------
1 |
2 | export function styled(Tag, cls, fns, content){
3 | return class Target extends React.Component{
4 | static displayName = cls
5 | render(){
6 | // return
7 | }
8 | }
9 | }
10 |
11 | export function createElement(tag, props, children){
12 |
13 | }
--------------------------------------------------------------------------------
/example/index.js.css:
--------------------------------------------------------------------------------
1 | /* do not edit this file */
2 | .frag-vwv3h3 { --frag-vwv3h3: { background-color: gray;
3 | border-radius: var(--frag-vwv3h3-0); }; }
4 | .frag-af1lcy { --frag-af1lcy: { @apply --frag-af1lcy-0;
5 | font-size: var(--frag-af1lcy-1);
6 | color: red; }; }
7 | .css-y3kejt { @apply --css-y3kejt-0;
8 | font-weight: bold; }
--------------------------------------------------------------------------------
/webpack.umd.js:
--------------------------------------------------------------------------------
1 | let path = require('path')
2 |
3 | module.exports = {
4 | devtool: 'source-map',
5 | entry: './src/index.js',
6 | output: {
7 | library: 'css',
8 | libraryTarget: 'umd',
9 | path: path.join(__dirname, './dist'),
10 | filename: 'glam.js'
11 | },
12 |
13 | module: {
14 | rules: [
15 | { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" }
16 | ]
17 | }
18 | }
--------------------------------------------------------------------------------
/motivation.md:
--------------------------------------------------------------------------------
1 | (circa May 2017)
2 |
3 | the biggest advantage of css-in-js systems, is that you can compose and create styling on the fly,
4 | not just with access to a 'real' programming language, but also exposure to the runtime environment.
5 |
6 | there are 2 "costs" to doing it this way -
7 |
8 | - shipping css parse/generation logic to the browser (anywhere from 2k - 20k)
9 | - inlining css into your javascript (as strings and/or objects)
10 |
11 | These costs are mitigated in a number of ways
12 |
13 | - prerendering css, optionally extracting it from html
14 | - processing portions of the code during compile-time, avoiding sending infra
--------------------------------------------------------------------------------
/src/parseCSS.js:
--------------------------------------------------------------------------------
1 | import parse from 'styled-components/lib/vendor/postcss-safe-parser/parse'
2 | // import { parse } from 'postcss'
3 | import postcssNested from 'styled-components/lib/vendor/postcss-nested'
4 | import stringify from 'styled-components/lib/vendor/postcss/stringify'
5 | import autoprefix from 'styled-components/lib/utils/autoprefix'
6 |
7 |
8 | export default function parser(css, options = {}){
9 | // todo - handle errors
10 | const root = parse(css)
11 | if(options.nested !== false) postcssNested(root)
12 | autoprefix(root)
13 |
14 |
15 | return root.nodes.map((node, i) => {
16 | let str = ''
17 | stringify(node, x => {
18 | str+= x
19 | })
20 | return str
21 | })
22 |
23 | }
24 |
25 | // todo -
26 | // select from http://cssnext.io/
--------------------------------------------------------------------------------
/src/loader.js:
--------------------------------------------------------------------------------
1 | let postcss = require('postcss')
2 |
3 | function toIndex(str, {line, column}){
4 |
5 | let regex = /\n/gm, index = 0
6 | for(let i = 0 ; i < line - 1; i++ ){
7 | index = regex.exec(str).index
8 | }
9 | return index + column
10 | }
11 |
12 | function extract(css, start, end){
13 | return css.substring(toIndex(css, start) - 1, toIndex(css, end) + 1).trim()
14 | }
15 |
16 | module.exports = function(content) {
17 | let ast = postcss.parse(content)
18 |
19 | let rules = ast.nodes.map(n =>
20 | extract(content, n.source.start, n.source.end)
21 | )
22 | let newSrc = `var sheet = require(${this.query.modulePath || '"glam"'}).sheet;
23 | [${rules.map(rule => JSON.stringify(rule)).join(',\n')}].forEach(function(rule){ sheet.insert(rule) });
24 | `
25 | return newSrc
26 |
27 | }
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/test/__snapshots__/index.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`dudupe static sections 1`] = `
4 | Array [
5 | "font-weight:bold",
6 | "font-weight:normal",
7 | ]
8 | `;
9 |
10 | exports[`extracts css into a css file 1`] = `
11 | "/* do not edit this file */
12 | .css-hwfcu5 { color:red }
13 | .css-1nd0lsp { color:red, font-weight:var(--css-1nd0lsp-0) }
14 | .css-1mdi6ak { color:red; font-weight:var(--css-1mdi6ak-0) }"
15 | `;
16 |
17 | exports[`injects dynamic values into a sheet 1`] = `Array []`;
18 |
19 | exports[`receives a class and array of var values 1`] = `
20 | Array [
21 | "css-1nd0lsp",
22 | Array [
23 | "bold",
24 | ],
25 | ]
26 | `;
27 |
28 | exports[`returns 2 classes for a dynamic string 1`] = `"css-1nd0lsp vars-1wb9cdj"`;
29 |
30 | exports[`returns a class for a string 1`] = `"css-hwfcu5"`;
31 |
--------------------------------------------------------------------------------
/src/server.js:
--------------------------------------------------------------------------------
1 |
2 | export function extract(html) {
3 | // parse out ids from html
4 | // reconstruct css/rules/cache to pass
5 |
6 | let o = { ids: [], css: '', rules: [] }
7 | let regex = /vars\-([a-zA-Z0-9]+)/gm
8 | let match, ids = {}
9 | while((match = regex.exec(html)) !== null) {
10 | if(!ids[match[1] + '']) {
11 | ids[match[1] + ''] = true
12 | }
13 | }
14 |
15 | o.rules = sheet.rules().filter(x => {
16 | let regex = /css\-([a-zA-Z0-9]+)/gm
17 | let match = regex.exec(x.cssText)
18 | if(match && ids[match[1] + '']) {
19 | return true
20 | }
21 | if(!match) {
22 | return true
23 | }
24 | return false
25 | })
26 | o.ids = Object.keys(inserted).filter(id => !!ids[id + ''] || styleSheet.registered[id].type === 'raw')
27 | o.css = o.rules.map(x => x.cssText).join('')
28 |
29 | return o
30 | }
--------------------------------------------------------------------------------
/webpack.node.js:
--------------------------------------------------------------------------------
1 | let path = require('path')
2 | let cssnext = require('postcss-cssnext')
3 |
4 |
5 | module.exports = {
6 | target: 'node',
7 | entry: './node/index.js',
8 | output: {
9 | path: path.join(__dirname, './node'),
10 | filename: 'bundle.js'
11 | },
12 | module: {
13 | rules: [
14 | { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" },
15 | { test: /\.css$/, use: [
16 | // instead of -
17 | // { loader: "style-loader" },
18 | // { loader: "css-loader", options: { importLoaders: 1 } },
19 | // we use our own version -
20 | { loader: path.join(__dirname, './src/loader'), // this would be '@threepointone/glam/loader'
21 | options: { modulePath: '"../src"' } }, // you don't need this
22 | // add postcss as 'usual'
23 | { loader: "postcss-loader", options: {
24 | plugins: () => [ cssnext({ features: { customProperties: false } }) ] }
25 | }
26 | ] }
27 | ]
28 | }
29 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | let path = require('path')
2 | let cssnext = require('postcss-cssnext')
3 | let webpack = require('webpack')
4 |
5 | module.exports = {
6 | devtool: 'source-map',
7 | entry: './example/index.js',
8 | output: {
9 | publicPath: '/example/',
10 | path: path.join(__dirname, './example'),
11 | filename: 'bundle.js'
12 | },
13 | node: {
14 | Buffer: false // workaround style-loader bug
15 | },
16 | module: {
17 | rules: [
18 | { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" },
19 | { test: /\.css$/, use: [
20 |
21 | // instead of -
22 | // { loader: "style-loader" },
23 | // { loader: "css-loader", options: { importLoaders: 1 } },
24 |
25 | // or -
26 | // { loader: "file-loader" },
27 |
28 | // we can use our own version -
29 | { loader: path.join(__dirname, './src/loader'), // this would be '@threepointone/glam/loader'
30 | options: { modulePath: '"../src"' } } // you don't need this
31 |
32 |
33 | ] }
34 | ]
35 | }
36 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Sunil Pai
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/test/index.test.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const path = require('path')
3 |
4 | const css = require('../src').default
5 |
6 | const { sheet, flush } = require('../src')
7 |
8 | test('returns a class for a string', () => {
9 | expect(css`color:red`).toMatchSnapshot()
10 | })
11 |
12 | test('returns 2 classes for a dynamic string', () => {
13 | expect(css`color:red, font-weight:${'bold'}`).toMatchSnapshot()
14 | })
15 |
16 | test('receives a class and array of var values', () => {
17 | let css = function(a, b){
18 | return [a,b]
19 | }
20 | expect(css`color:red, font-weight:${'bold'}`).toMatchSnapshot()
21 | })
22 |
23 | test('requires css file corresponding to module', () => {
24 | expect(global.styleMocked).toBe('alphabetaomega')
25 | }) // ???
26 |
27 | test('injects dynamic values into a sheet', () => {
28 | flush()
29 | let cls = css`color:red; font-weight:${'bold'}`
30 | expect(sheet.rules()).toMatchSnapshot()
31 | })
32 |
33 | test('dudupe static sections', () => {
34 | flush()
35 | let cls1 = `color:red, font-weight:${'bold'}`
36 | let cls2 = `color:red, font-weight:${'normal'}`
37 | expect(cls1.split(' ')[0]).toBe(cls2.split(' ')[0])
38 | expect([cls1,cls2].map(x => x.split(' ')[1])).toMatchSnapshot()
39 | })
40 |
41 | test(`extracts css into a css file`, () => {
42 | let file = path.join(__dirname,'./index.test.js.css')
43 | expect(fs.existsSync(file)).toBe(true)
44 | expect(fs.readFileSync(file, 'utf8')).toMatchSnapshot()
45 | })
--------------------------------------------------------------------------------
/example/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render } from 'react-dom'
3 | import css, { fragment } from '../src'
4 |
5 | // function css(cls, vars, make){
6 | // console.log(cls, vars, make)
7 | // }
8 |
9 | // let blue = 'blue'
10 | // let myColor = 'green'
11 |
12 | // // input
13 | // let example = css`
14 | // color: red;
15 | // &:hover {
16 | // color: ${myColor}
17 | // }
18 | // `
19 | // // generated
20 | // let example = css('css-wbh6ke', [myColor], (x0) =>
21 | // [
22 | // `.css-wbh6ke { color: red; }
23 | // .css-wbh6ke:hover { color: ${x0}}`
24 | // ]
25 | // );
26 |
27 | // let cls = css`
28 | // display:flex;
29 | // font-weight:bold;
30 | // font-size: calc(${20} * 2px);
31 | // &:hover {
32 | // color: red
33 | // }`
34 |
35 | // let cls2 = css`name: green; color: green;`
36 |
37 | // let cls3 = css`color: ${'yellow'};`
38 |
39 | // let cls4 = css`
40 | // @media screen {
41 | // color: gray
42 | // }
43 | // `
44 |
45 | import('./index.js.css')
46 |
47 | let chunk = fragment`
48 | background-color: gray;
49 | border-radius: ${'50px'};
50 | `
51 |
52 | let frag = fragment`
53 | @apply ${chunk};
54 | font-size: ${'50px'};
55 | color: red;
56 | `
57 |
58 | let cls = css`
59 | @apply ${frag};
60 | font-weight: bold;
61 | `
62 |
63 | class App extends React.Component{
64 | render(){
65 | return
66 | what up
67 |
68 | }
69 | }
70 |
71 | render(, window.root)
72 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "glam",
3 | "version": "4.0.3",
4 | "description": "css-in-js. again.",
5 | "main": "lib/index.js",
6 | "author": "Sunil Pai ",
7 | "license": "MIT",
8 | "dependencies": {
9 | "autoprefixer": "^6.7.7",
10 | "babylon": "^6.17.0",
11 | "styled-components": "^1.4.5",
12 | "touch": "^1.0.0"
13 | },
14 | "files": [
15 | "lib",
16 | "babel.js",
17 | "loader.js"
18 | ],
19 | "scripts": {
20 | "dev": "npm run build && npm start",
21 | "start": "webpack-dev-server",
22 | "build": "babel src -d lib",
23 | "umd": "webpack --config webpack.umd.js -p ",
24 | "test": "jest",
25 | "snapshot": "npm test -- -u"
26 | },
27 | "devDependencies": {
28 | "babel-cli": "^6.24.1",
29 | "babel-loader": "^7.0.0",
30 | "babel-preset-env": "^1.4.0",
31 | "babel-preset-react": "^6.24.1",
32 | "babel-preset-stage-0": "^6.24.1",
33 | "css-loader": "^0.28.0",
34 | "extract-text-webpack-plugin": "^2.1.0",
35 | "file-loader": "^0.11.1",
36 | "jest": "^19.0.2",
37 | "postcss": "^5.2.17",
38 | "postcss-cssnext": "^2.10.0",
39 | "postcss-loader": "^1.3.3",
40 | "react": "next",
41 | "react-dom": "next",
42 | "style-loader": "^0.16.1",
43 | "webpack": "^2.4.1",
44 | "webpack-dev-server": "^2.4.4"
45 | },
46 | "jest": {
47 | "moduleNameMapper": {
48 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/test/fileMock.js",
49 | "\\.(css|less)$": "/test/styleMock.js"
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/hash.js:
--------------------------------------------------------------------------------
1 | // murmurhash2 via https://gist.github.com/raycmorgan/588423
2 |
3 |
4 | export default function hashArray(arr) {
5 | let str = arr.join(',')
6 | return murmur2(str, str.length).toString(36)
7 | }
8 |
9 |
10 | export function murmur2(str, seed) {
11 | let m = 0x5bd1e995
12 | let r = 24
13 | let h = seed ^ str.length
14 | let length = str.length
15 | let currentIndex = 0
16 |
17 | while (length >= 4) {
18 | let k = UInt32(str, currentIndex)
19 |
20 | k = Umul32(k, m)
21 | k ^= k >>> r
22 | k = Umul32(k, m)
23 |
24 | h = Umul32(h, m)
25 | h ^= k
26 |
27 | currentIndex += 4
28 | length -= 4
29 | }
30 |
31 | switch (length) {
32 | case 3:
33 | h ^= UInt16(str, currentIndex)
34 | h ^= str.charCodeAt(currentIndex + 2) << 16
35 | h = Umul32(h, m)
36 | break
37 |
38 | case 2:
39 | h ^= UInt16(str, currentIndex)
40 | h = Umul32(h, m)
41 | break
42 |
43 | case 1:
44 | h ^= str.charCodeAt(currentIndex)
45 | h = Umul32(h, m)
46 | break
47 | }
48 |
49 | h ^= h >>> 13
50 | h = Umul32(h, m)
51 | h ^= h >>> 15
52 |
53 | return h >>> 0
54 | }
55 |
56 | function UInt32(str, pos) {
57 | return (str.charCodeAt(pos++)) +
58 | (str.charCodeAt(pos++) << 8) +
59 | (str.charCodeAt(pos++) << 16) +
60 | (str.charCodeAt(pos) << 24)
61 | }
62 |
63 | function UInt16(str, pos) {
64 | return (str.charCodeAt(pos++)) +
65 | (str.charCodeAt(pos++) << 8)
66 | }
67 |
68 | function Umul32(n, m) {
69 | n = n | 0
70 | m = m | 0
71 | let nlo = n & 0xffff
72 | let nhi = n >>> 16
73 | let res = ((nlo * m) + (((nhi * m) & 0xffff) << 16)) | 0
74 | return res
75 | }
76 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from './sheet'
2 | import hashArray from './hash'
3 |
4 | export const sheet = new StyleSheet()
5 | sheet.inject()
6 |
7 | let inserted = {}
8 |
9 | function values(cls, vars){
10 | let hash = hashArray([cls, ...vars])
11 | if(inserted[hash]) {
12 | return `vars-${hash}`
13 | }
14 | let fragvarcls = []
15 | let src = vars.map((val, i) =>
16 | `--${cls}-${i}: ${
17 | /^frag-/.exec(val) ?
18 | (fragvarcls.push(val), 'var(--' + val.split(' ')[0] + ')')
19 | : val
20 | }`).join('; ')
21 | sheet.insert(`.vars-${hash} {${src}}`)
22 | inserted[hash] = true
23 |
24 | if(fragvarcls.length > 0){
25 | return `vars-${hash} ${fragvarcls.join(' ')}`
26 | }
27 | return `vars-${hash}`
28 |
29 | }
30 |
31 | export function flush(){
32 | sheet.flush()
33 | inserted = {}
34 | sheet.inject()
35 | }
36 |
37 |
38 | export default function css(cls, vars, content){
39 | if(content){
40 | let fragvarcls = []
41 | // inline mode
42 | vars = vars.map(v => /^frag-/.exec(v) ? fragments[v] :
43 | v )
44 | let src = content(...vars) // returns an array
45 | let hash = hashArray(src)
46 |
47 | if(!inserted[hash]){
48 | inserted[hash] = true
49 | src.map(r => r.replace(new RegExp(cls, 'gm'), `${cls}-${hash}`)).forEach(r => sheet.insert(r))
50 | }
51 | return `${cls}-${hash}`
52 |
53 | }
54 | return cls + ((vars && vars.length > 0) ? (' ' + values(cls, vars)) : '')
55 | }
56 |
57 | const fragments = {}
58 |
59 | export function fragment(frag, vars, content){
60 | if(content){
61 | let fragvarcls = []
62 | vars = vars.map(v => /^frag-/.exec(v) ? fragments[v] :
63 | v )
64 | let src = content(...vars) // return array?
65 | if(src.length > 1){
66 | throw new Error('what up!')
67 | }
68 |
69 | let hash = hashArray(src)
70 | src = src.join('')
71 | fragments[`${frag}-${hash}`] = src.substring(src.indexOf('{') + 1, src.length -1)
72 | return `${frag}-${hash}`
73 |
74 | }
75 | return frag + ((vars && vars.length > 0) ? (' ' + values(frag, vars)) : '')
76 | }
77 |
78 |
79 |
80 | export function hydrate(ids){
81 | ids.forEach(id => inserted[id] = true)
82 | }
83 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # glam
2 |
3 | [](http://github.com/badges/stability-badges)
4 |
5 | `npm install glam`
6 |
7 | - super fast + small (<2k gz)
8 | - create real css files
9 | - future facing css : nested selectors, autoprefixing, etc
10 | - real dynamic props / `@apply`!
11 | - Make Alex Happy (tm)
12 |
13 | input -
14 | ```jsx
15 | // index.js
16 |
17 | import css from 'glam'
18 |
19 | let myColor = '#ab67ee'
20 | let radius = '20px'
21 |
22 | let myClass = css`
23 | color: red;
24 | &:hover {
25 | font-weight: bold;
26 | color: ${myColor};
27 | border-radius: ${radius};
28 | }
29 | `
30 | // ...
31 |
32 | what up homies
33 |
34 | `
35 |
36 | ```
37 |
38 | output -
39 | ```jsx
40 | // index.js
41 |
42 | import css from 'glam'
43 |
44 | let myColor = '#ab67ee'
45 | let radius = '20px'
46 |
47 | let myClass = css('css-1bh6s', [myColor, rad])
48 | // generates "css-1bh6s vars-h23psd"
49 | ```
50 |
51 | ```css
52 | /* index.js.css */
53 |
54 | .css-1bh6s {
55 | color: red
56 | }
57 | .css-1bh6s:hover {
58 | font-weight: bold;
59 | color: var(--css-1bh6s-0);
60 | border-radius: var(--css-1bh6s-1);
61 | }
62 | ```
63 |
64 | ```css
65 | /* dynamically added */
66 | .vars-h23psd {
67 | --css-1bh6s-0: #ab67ee;
68 | --css-1bh6s-1: 20px;
69 | }
70 |
71 | ```
72 |
73 | fragments
74 | ---
75 |
76 | glam lets you define reusable css `fragment`s that can be
77 | mixed in with `css` and `fragment` definitions
78 |
79 | ```jsx
80 | import css, {fragment} from 'glam'
81 | let huge = 100,
82 | tiny = 6
83 |
84 | let bold = fragment`
85 | display: flex;
86 | font-weight: bold;`
87 |
88 | let big = fragment`
89 | @apply ${bold};
90 | font-size: ${huge}`
91 |
92 | let small = fragment`
93 | font-size: ${tiny}`
94 |
95 |
99 | compose all your classes!
100 |
101 |
102 | ```
103 |
104 | debugging
105 | ---
106 |
107 | pass a `name` to generate readable classnames
108 |
109 | ```jsx
110 | let cls = css`
111 | name: big;
112 | font-size: 40px;
113 | `
114 | // big-f8xd3
115 | ```
116 |
117 | motivation
118 | ---
119 |
120 | this lib was designed to leverage compile-time to provide great DX,
121 | and ship only enough js as required for the interactive portions.
122 |
123 |
124 | nice things
125 | ---
126 |
127 | - extract out as much static css as possible, before delegating the dynamic bits to javascript
128 | - true composition with css vars / `@apply`
129 | - friendly with server side / static rendering
130 | - backward compatible with older browsers (serve the right bundle!)
131 | - free DCE/async loading/etc via webpack ecosystem
132 |
133 | how does it work?
134 | ---
135 |
136 | [like so](https://gist.github.com/threepointone/0ef30b196682a69327c407124f33d69a)
137 |
138 |
139 | caveats
140 | ---
141 |
142 | - interpolated property *values* can't be 'processed', which should be fine?
143 | - composition isn't as easy as other css-in-js systems
144 | - the `@apply` spec is stalled, and might never make it in.
145 |
146 | usage
147 | ---
148 |
149 | - add `glam/babel` to your babel plugins
150 | - add `glam/loader` to webpack's css loaders
151 | - ???
152 | - profit
153 |
154 |
155 | plugin options
156 | ---
157 |
158 | glam can 'polyfill' for browsers that don't support css vars and / or `@apply`.
159 | fancy! you could then generate separate bundles targeting different browsers.
160 |
161 | - `inline` - bool - default `false`
162 |
163 |
164 | loading css
165 | ---
166 |
167 | add `require('./path/to/file.js.css')` statements as required.
168 | some options to bundle and serve this css -
169 |
170 | - use a classic webpack combo: `style-loader`/`css-loader`
171 | - use `file-loader` and load it with link tags and/or `@import`
172 | - use `glam/loader`
173 | - (todo) - use `glam/server` to extract 'precise' css from html
174 |
175 | I hope to make this simpler.
176 |
177 |
178 | bonus - high perf react variant
179 | ---
180 |
181 | for stuff like animations, you could define your own `css` variant,
182 | further bringing down runtime cost
183 |
184 | (react@16.alpha-11 and above)
185 |
186 | ```jsx
187 | function css(cls, vars = []){
188 | return {
189 | className: cls,
190 | style: vars.reduce((o, v, i) =>
191 | (o[`--${cls}-${i}`] = v, o), {})
192 | }
193 | }
194 |
195 | // ...
196 |
197 |
198 | hello!
199 |
200 |
201 | ```
202 |
203 |
204 | todo
205 | ---
206 | - web components, shadow dom et al
207 | - keyframes, fonts, imports, globals
208 | - more features from cssnext
209 | - ssr
210 | - custom postcss pipeline
211 | - source maps?
212 | - typed om?
213 | - SC api?
214 | - unload styles?
215 | - hot loading support
216 | - test inheritance chains
217 |
218 | previous work
219 | ---
220 |
221 | - [css-literal-loader](https://github.com/4Catalyzer/css-literal-loader)
222 |
223 |
224 | thanks
225 | ---
226 |
227 | - to [@gregtatum](https://github.com/gregtatum) for the `glam` package name!
228 |
229 |
--------------------------------------------------------------------------------
/src/sheet.js:
--------------------------------------------------------------------------------
1 | // import assign from 'object-assign'
2 | /*
3 |
4 | high performance StyleSheet for css-in-js systems
5 |
6 | - uses multiple style tags behind the scenes for millions of rules
7 | - uses `insertRule` for appending in production for *much* faster performance
8 | - 'polyfills' on server side
9 |
10 |
11 | // usage
12 |
13 | import StyleSheet from 'glamor/lib/sheet'
14 | let styleSheet = new StyleSheet()
15 |
16 | styleSheet.inject()
17 | - 'injects' the stylesheet into the page (or into memory if on server)
18 |
19 | styleSheet.insert('#box { border: 1px solid red; }')
20 | - appends a css rule into the stylesheet
21 |
22 | styleSheet.flush()
23 | - empties the stylesheet of all its contents
24 |
25 |
26 | */
27 |
28 | function last(arr) {
29 | return arr[arr.length -1]
30 | }
31 |
32 | function sheetForTag(tag) {
33 | if(tag.sheet) {
34 | return tag.sheet
35 | }
36 |
37 | // this weirdness brought to you by firefox
38 | for(let i = 0; i < document.styleSheets.length; i++) {
39 | if(document.styleSheets[i].ownerNode === tag) {
40 | return document.styleSheets[i]
41 | }
42 | }
43 | }
44 |
45 | const isBrowser = typeof window !== 'undefined'
46 | const isDev = (process.env.NODE_ENV === 'development') || (!process.env.NODE_ENV) //(x => (x === 'development') || !x)(process.env.NODE_ENV)
47 | const isTest = process.env.NODE_ENV === 'test'
48 |
49 | const oldIE = (() => {
50 | if(isBrowser) {
51 | let div = document.createElement('div')
52 | div.innerHTML = ''
53 | return div.getElementsByTagName('i').length === 1
54 | }
55 | })()
56 |
57 | function makeStyleTag() {
58 | let tag = document.createElement('style')
59 | tag.type = 'text/css'
60 | tag.setAttribute('data-glam', '')
61 | tag.appendChild(document.createTextNode(''));
62 | (document.head || document.getElementsByTagName('head')[0]).appendChild(tag)
63 | return tag
64 | }
65 |
66 | export function StyleSheet({
67 | speedy = !isDev && !isTest,
68 | maxLength = (isBrowser && oldIE) ? 4000 : 65000
69 | } = {}) {
70 | this.isSpeedy = speedy // the big drawback here is that the css won't be editable in devtools
71 | this.sheet = undefined
72 | this.tags = []
73 | this.maxLength = maxLength
74 | this.ctr = 0
75 | }
76 |
77 | Object.assign(StyleSheet.prototype, {
78 | getSheet() {
79 | return sheetForTag(last(this.tags))
80 | },
81 | inject() {
82 | if(this.injected) {
83 | throw new Error('already injected!')
84 | }
85 | if(isBrowser) {
86 | this.tags[0] = makeStyleTag()
87 | }
88 | else {
89 | // server side 'polyfill'. just enough behavior to be useful.
90 | this.sheet = {
91 | cssRules: [],
92 | insertRule: rule => {
93 | // enough 'spec compliance' to be able to extract the rules later
94 | // in other words, just the cssText field
95 | this.sheet.cssRules.push({ cssText: rule })
96 | }
97 | }
98 | }
99 | this.injected = true
100 | },
101 | speedy(bool) {
102 | if(this.ctr !== 0) {
103 | // cannot change speedy mode after inserting any rule to sheet. Either call speedy(${bool}) earlier in your app, or call flush() before speedy(${bool})
104 | throw new Error(`cannot change speedy now`)
105 | }
106 | this.isSpeedy = !!bool
107 | },
108 | _insert(rule) {
109 | // this weirdness for perf, and chrome's weird bug
110 | // https://stackoverflow.com/questions/20007992/chrome-suddenly-stopped-accepting-insertrule
111 | try {
112 | let sheet = this.getSheet()
113 | sheet.insertRule(rule, rule.indexOf('@import') !== -1 ? 0 : sheet.cssRules.length)
114 | }
115 | catch(e) {
116 | if(isDev) {
117 | // might need beter dx for this
118 | console.warn('illegal rule', rule) //eslint-disable-line no-console
119 | }
120 | }
121 |
122 | },
123 | insert(rule) {
124 |
125 | if(isBrowser) {
126 | // this is the ultrafast version, works across browsers
127 | if(this.isSpeedy && this.getSheet().insertRule) {
128 | this._insert(rule)
129 | }
130 | // more browser weirdness. I don't even know
131 | // else if(this.tags.length > 0 && this.tags::last().styleSheet) {
132 | // this.tags::last().styleSheet.cssText+= rule
133 | // }
134 | else{
135 | if(rule.indexOf('@import') !== -1) {
136 | const tag = last(this.tags)
137 | tag.insertBefore(document.createTextNode(rule), tag.firstChild)
138 | } else {
139 | last(this.tags).appendChild(document.createTextNode(rule))
140 | }
141 | }
142 | }
143 | else{
144 | // server side is pretty simple
145 | this.sheet.insertRule(rule, rule.indexOf('@import') !== -1 ? 0 : this.sheet.cssRules.length)
146 | }
147 |
148 | this.ctr++
149 | if(isBrowser && this.ctr % this.maxLength === 0) {
150 | this.tags.push(makeStyleTag())
151 | }
152 | return this.ctr -1
153 | },
154 | delete(index) {
155 | // we insert a blank rule when 'deleting' so previously returned indexes remain stable
156 | return this.replace(index, '')
157 | },
158 | flush() {
159 | if(isBrowser) {
160 | this.tags.forEach(tag => tag.parentNode.removeChild(tag))
161 | this.tags = []
162 | this.sheet = null
163 | this.ctr = 0
164 | // todo - look for remnants in document.styleSheets
165 | }
166 | else {
167 | // simpler on server
168 | this.sheet.cssRules = []
169 | }
170 | this.injected = false
171 | },
172 | rules() {
173 | if(!isBrowser) {
174 | return this.sheet.cssRules
175 | }
176 | let arr = []
177 | this.tags.forEach(tag => arr.splice(arr.length, 0, ...Array.from(
178 | sheetForTag(tag).cssRules
179 | )))
180 | return arr
181 | }
182 | })
183 |
--------------------------------------------------------------------------------
/src/babel.js:
--------------------------------------------------------------------------------
1 | import parseCSS from './parseCSS'
2 |
3 | import * as babylon from 'babylon'
4 | import touch from 'touch'
5 | import fs from 'fs'
6 |
7 | import hashArray from './hash'
8 |
9 | function getName(str){
10 | let regex = /name\s*:\s*([A-Za-z0-9\-_]+)\s*/gm
11 | let match = regex.exec(str)
12 | if(match){
13 | return match[1]
14 | }
15 | }
16 |
17 | function parser(path) {
18 | let code = path.hub.file.code
19 | let strs = path.node.quasi.quasis.map(x => x.value.cooked)
20 | let hash = hashArray([...strs]) // todo - add current filename?
21 | let name = getName(strs.join('xxx')) || 'css'
22 |
23 | let stubs = path.node.quasi.expressions.map(x => code.substring(x.start, x.end))
24 | let ctr = 0
25 |
26 | let src = strs.reduce((arr, str, i) => {
27 | arr.push(str)
28 | if(i !== stubs.length) {
29 | // todo - test for preceding @apply
30 | let applyMatch = /@apply\s*$/gm.exec(str)
31 | if(applyMatch){
32 | arr.push(`--${name}-${hash}-${i}`)
33 | }
34 | else arr.push(`var(--${name}-${hash}-${i})`)
35 | }
36 | return arr
37 | }, []).join('').trim()
38 | let rules = parseCSS(`.${name}-${hash} { ${src} }`)
39 | let parsed = rules.join('\n')
40 |
41 | return { hash, parsed, stubs, name }
42 | }
43 |
44 | function inline(path){
45 | let code = path.hub.file.code
46 | let strs = path.node.quasi.quasis.map(x => x.value.cooked)
47 | let hash = hashArray([...strs]) // todo - add current filename?
48 | let name = getName(strs.join('xxx')) || 'css'
49 |
50 | let stubs = path.node.quasi.expressions.map(x => code.substring(x.start, x.end))
51 | let ctr = 0
52 |
53 | let src = strs.reduce((arr, str, i) => {
54 | arr.push(str)
55 | if(i !== stubs.length) {
56 | // todo - test for preceding @apply
57 | let applyMatch = /@apply\s*$/gm.exec(str)
58 | if(applyMatch){
59 | arr.push(`--${name}-${hash}-${i}`)
60 | }
61 | else arr.push(`var(--${name}-${hash}-${i})`)
62 | }
63 | return arr
64 | }, []).join('').trim()
65 |
66 | let rules = parseCSS(`.${name}-${hash} { ${src} }`)
67 | rules = rules.map(rule => rule.replace(/@apply\s+--[A-Za-z0-9-_]+-([0-9]+)/gm, (match, p1) => `$\{x${p1}}` ))
68 | rules = rules.map(rule => rule.replace(/var\(--[A-Za-z0-9-_]+-([0-9]+)\)/gm, (match, p1) => `$\{x${p1}}` ))
69 |
70 |
71 | let parsed = `(${stubs.map((x, i) => `x${i}`).join(', ')}) => [${rules.map(x => '`' + x + '`').join(',\n')}]`
72 | return { hash, parsed, stubs, name }
73 | }
74 |
75 | function fragment(path){
76 | let code = path.hub.file.code
77 | let strs = path.node.quasi.quasis.map(x => x.value.cooked)
78 | let hash = hashArray([...strs]) // todo - add current filename?
79 | let name = getName(strs.join('xxx')) || 'frag'
80 |
81 | let stubs = path.node.quasi.expressions.map(x => code.substring(x.start, x.end))
82 | let ctr = 0
83 |
84 | let src = strs.reduce((arr, str, i) => {
85 | arr.push(str)
86 | if(i !== stubs.length) {
87 | // todo - test for preceding @apply
88 | let applyMatch = /@apply\s*$/gm.exec(str)
89 | if(applyMatch){
90 | arr.push(`--${name}-${hash}-${i}`)
91 | }
92 | else {
93 | arr.push(`var(--${name}-${hash}-${i})`)
94 | }
95 |
96 | }
97 | return arr
98 | }, []).join('').trim()
99 | let rules = parseCSS(`.${name}-${hash} { --${name}-${hash}: { ${src} }; }`, {nested: false})
100 | let parsed = rules.join('\n')
101 |
102 | return { hash, parsed, stubs, name }
103 | }
104 |
105 | function fragmentinline(path){
106 | let code = path.hub.file.code
107 | let strs = path.node.quasi.quasis.map(x => x.value.cooked)
108 | let hash = hashArray([...strs]) // todo - add current filename?
109 | let name = getName(strs.join('xxx')) || 'frag'
110 |
111 | let stubs = path.node.quasi.expressions.map(x => code.substring(x.start, x.end))
112 | let ctr = 0
113 |
114 | let src = strs.reduce((arr, str, i) => {
115 | arr.push(str)
116 | if(i !== stubs.length) {
117 | // todo - test for preceding @apply
118 | let applyMatch = /@apply\s*$/gm.exec(str)
119 | if(applyMatch){
120 | arr.push(`--${name}-${hash}-${i}`)
121 | }
122 | else arr.push(`var(--${name}-${hash}-${i})`)
123 | }
124 | return arr
125 | }, []).join('').trim()
126 |
127 | let rules = parseCSS(`.${name}-${hash} { ${src} }`)
128 | rules = rules.map(rule => rule.replace(/@apply\s+--[A-Za-z0-9-_]+-([0-9]+)/gm, (match, p1) => `$\{x${p1}}` ))
129 | rules = rules.map(rule => rule.replace(/var\(--[A-Za-z0-9-_]+-([0-9]+)\)/gm, (match, p1) => `$\{x${p1}}` ))
130 |
131 |
132 | let parsed = `(${stubs.map((x, i) => `x${i}`).join(', ')}) => [${rules.map(x => '`' + x + '`').join(',\n')}]`
133 | return { hash, parsed, stubs, name }
134 | }
135 |
136 | module.exports = function({ types: t }){
137 | return {
138 | visitor: {
139 | Program: {
140 | enter(path, state){
141 | state.injected = false
142 | let inserted = {}
143 | state.toInsert = []
144 | let file = path.hub.file.opts.filename
145 | state.inject = function(){
146 | if(!state.injected){
147 | state.injected = true
148 |
149 | state.toInsert.push('/* do not edit this file */')
150 |
151 | // let src = (state.opts.sync && !state.opts.inline) ?
152 | // `import './${require('path').basename(file) + '.css'}';` :
153 | // `import('./${require('path').basename(file) + '.css'}');`
154 | // if(!state.opts.inline){
155 | // let impNode = babylon.parse(src, {sourceType: 'module', plugins: ['*']}).program.body[0]
156 | // path.node.body.unshift(impNode)
157 | // }
158 |
159 | }
160 | }
161 | state.insert = function(hash, css){
162 | if(!inserted[hash]){
163 | inserted[hash] = true
164 | state.toInsert.push(css)
165 | }
166 | }
167 | },
168 | exit(path, state){
169 | let file = path.hub.file.opts.filename
170 |
171 | let toWrite = state.toInsert.join('\n').trim()
172 | if(!state.opts.inline && state.injected && (fs.existsSync(file + '.css') ? (fs.readFileSync(file + '.css', 'utf8') !== toWrite) : true) ){
173 | if(!fs.existsSync(file + '.css')) {
174 | touch.sync(file + '.css')
175 | }
176 |
177 | fs.writeFileSync(file + '.css', toWrite)
178 | }
179 | }
180 | },
181 | TaggedTemplateExpression(path, state){
182 | let { tag } = path.node
183 | let code = path.hub.file.code
184 |
185 | if(tag.name === 'css') {
186 |
187 | state.inject()
188 |
189 |
190 | let newSrc
191 |
192 | if(state.opts.inline){
193 | let { hash, parsed, stubs, name } = inline(path)
194 | let cls = `'${name}-${hash}'`
195 | let vars = `[${stubs.join(', ')}]`
196 | newSrc = `css(${cls}, ${vars}, ${parsed})`
197 |
198 | }
199 | else {
200 | let { hash, parsed, stubs, name } = parser(path)
201 | state.insert(hash, parsed)
202 | let cls = `'${name}-${hash}'`
203 | let vars = `[${stubs.join(', ')}]`
204 | newSrc = stubs.length > 0 ? `css(${cls}, ${vars})` : `css(${cls})`
205 | }
206 |
207 | path.replaceWith(babylon.parse(newSrc, {plugins: ['*']}).program.body[0].expression)
208 |
209 | }
210 | if(tag.name === 'fragment'){
211 |
212 | state.inject()
213 | let newSrc
214 | // fragment('frag-[hash]', vars, () => [``])
215 | if(state.opts.inline){
216 | let { hash, parsed, stubs, name } = fragmentinline(path)
217 | let cls = `'${name}-${hash}'`
218 | let vars = `[${stubs.join(', ')}]`
219 | newSrc = `fragment(${cls}, ${vars}, ${parsed})`
220 |
221 | }
222 | else {
223 | let { hash, parsed, stubs, name } = fragment(path, {name: 'frag'})
224 | state.insert(hash, parsed)
225 | let cls = `'${name}-${hash}'`
226 | let vars = `[${stubs.join(', ')}]`
227 | newSrc = stubs.length > 0 ? `fragment(${cls}, ${vars})` : `fragment(${cls})`
228 | }
229 |
230 | path.replaceWith(babylon.parse(newSrc, {plugins: ['*']}).program.body[0].expression)
231 |
232 | }
233 | }
234 | }
235 | }
236 | }
--------------------------------------------------------------------------------