├── 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 | [![experimental](http://badges.github.io/stability-badges/dist/experimental.svg)](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 | } --------------------------------------------------------------------------------