prejss

1.4.0 • Public • Published

PreJSS 🎨

Travis branch npm version npm downloads npm license

Fast, component-friendly, fully customizable, universal СSS-to-JSS adapter. Use the best bits of PostCSS, syntax and plugins (1, 2, 3) to get result as JSS objects from Tagged Template literals (a recent addition to JavaScript/ES6).

Architecture

PostCSS is a tool for transforming styles with JS plugins. These plugins can lint your CSS, support variables and mixins, transpile future CSS syntax, inline images, and more.

PreJSS allows you to get JSS objects "on-the-fly" from plain CSS, PostCSS, SCSS, CSS Modules, Stylus and LESS styles. Just put your CSS and get it as JSS.

Beside of that, PreJSS is the shortest way to get high-optimized Critical CSS for Isomorphic Applications while it still fits good for Single Page Applications.

Are you new to JSS? It will save your time, improve your productivity and reduce cognitive loading by allowing you to use CSS and JSS notation together. It means sometimes you can write CSS, sometimes - JSS. That all according to your choice.

  • See PreJSS Example Application with using React.js, isomorphic architecture, Server-Side Rendering (SSR), Hot Module Replacement (HMR), JSS and PreJSS 🎨 with run-time and pre-compilation

Supports:

Diagram

Content

Motivation

CSS is good enough solution when you develop web-sites and simple UIs.

But when you develop Web Applications and complex UIs, CSS is something like legacy.

Since 2015 we use React Native where styles are defined by JavaScript objects and we found it extremely useful.

But how to migrate from CSS/SCSS to JSS "smoothly and on-time"?

At first we developed jss-from-css for process SCSS-to-JSS migration in cheapest and fastest way.

Lately we have found that it could be just very nice to define JSS styles in the format which we already used to and even extend it to use some JavaScript injections and so on. We introduced Adapters which provides mechanism to use this package also with any CSS-in-JS library.

So out-of-the-box PreJSS allows you to use PostCSS features and plugins which enable you to use:

  • plain CSS
  • SCSS
  • SASS
  • LESS
  • Stylus
  • SugarSS

It could help your to migrate "smoothly" from any format above to JSS. That's how we solved this issue.

You can use any of PostCSS plugins like Autoprefixer, postcss-next and so on.

Finally, think about it like:

Getting Started

To get started using PreJSS in your applications, it would be great to know three things:

  • PreJSS Styles Declaration is top-level definition which is processing by PreJSS. In other words, CSS Code with Expressions as Tagged Template String which is converting to string and using as input for Parser in Adapters.

  • Parser is core thing in PreJSS. Usually it's a package with pure function which is using by Adapters. PreJSS Parser can be sync (by default) and async. PreJSS uses prejss-postcss-parser by default (as built-in).

  • Adapters is core thing and the point for customization your PreJSS. PreJSS Adapter is pure function which handle Tagged Template String, prepare specified CSS, parse it to JSS and finalize JSS when we have to adopt it to other CSS-in-JS platforms (Aphrodite, React Native, etc). There is two kinds of Adapters - sync for basic usage and async – for improving performance of Server-Side Rendering.

The best way to get started right now is to take a look at how these three parts come together to example below.

Installation

npm install prejss --save

Example

import color from 'color'
import preJSS from 'prejss'
 
const styles = preJSS`
  $bg-default: #ccc;
  
  button {
    color: ${props => props.isPrimary ? 'palevioletred' : 'green'};
    display: block;
    margin: 0.5em 0;
    font-family: Helvetica, Arial, sans-serif;
 
    &:hover {
      text-decoration: underline;
      animation: ${rotate360} 2s linear infinite;
    }
  }
  
  ctaButton {
    @include button;
    
    &:hover {
      background: ${color('blue').darken(0.3).hex()}
    }
  }
  
  @media (min-width: 1024px) {
    button {
      width: 200px;
    }
  }
 
  @keyframes rotate360 {
    from {
      transform: rotate(0deg);
    }
 
    to {
      transform: rotate(360deg);
    }
  }
 
  @global {
    body {
      color: $bg-default;
    }
    button {
      color: #888888;
    }
  }
`

Result

The example above transform styles to the following object:

// ...
const styles = {
  button: {
    color: props => props.isPrimary ? 'palevioletred' : 'green',
    display: 'block',
    margin: '0.5em 0',
    fontFamily: 'Helvetica, Arial, sans-serif',
 
    '&:hover' {
      textDecoration: 'underline',
      animation: 'rotate360 2s linear infinite'
    }
  },
  
  ctaButton: {
    color: () => 'palevioletred',
    display: 'block',
    margin: '0.5em 0',
    fontFamily: 'Helvetica, Arial, sans-serif',
 
    '&:hover' {
      textDecoration: 'underline',
      animation: 'rotate360 2s linear infinite',
      background: color('blue').darken(0.3).hex()
    }
  },
  
  '@media (min-width: 1024px)': {
    button: {
      width: 200,
    }
  },
 
  '@keyframes rotate360': {
    from: {
      transform: 'rotate(0deg)'
    },
    to: {
      transform: 'rotate(360deg)'
    }
  },
  
  '@global': {
    body: {
      color: '#ccc'
    },
    button: {
      color: '#888888'
    }
  }
}

Render with Vanilla JS

import jss from 'jss'
import preset from 'jss-preset-default'
import styles from './styles'
 
// One time setup with default plugins and settings.
jss.setup(preset())
 
const { classes } = jss.createStyleSheet(styles).attach()
 
document.body.innerHTML = `
  <div>
    <button class="${classes.button}">Button</button>
    <button class="${classes.ctaButton}">CTA Button</button>
  </div>
`

Render with React.js

import jss from 'jss'
import preset from 'jss-preset-default'
import injectSheet from 'react-jss'
import styles from './styles'
 
// One time setup with default plugins and settings.
jss.setup(preset())
 
const Buttons = ({ button, ctaButton }) => (
  <div>
    <button className={button}>Button</button>
    <button className={ctaButton}>CTA Button</button>
  </div>
)
 
export default injectSheet(styles)(Buttons)

Server-Side Rendering

As you well know, React.js and JSS are both support Server-Side Rendering (SSR).

You can use it with prejss without any limitations:

import express from 'express'
import jss from 'jss'
import preset from 'jss-preset-default'
import React from 'react'
import { renderToString } from 'react-dom/server'
import { SheetsRegistryProvider, SheetsRegistry } from 'react-jss'
// this module is defined in the previous example
import Buttons from './buttons' 
 
// One time setup with default plugins and settings.
jss.setup(preset())
const app = express()
 
app.use('/', () => {
  const sheets = new SheetsRegistry()
  const content = renderToString(
    <SheetsRegistryProvider registry={sheets}>
      <Buttons />
    </SheetsRegistryProvider>
  )
  const criticalCSS = sheets.toString()
  res.send(`
    <html>
    <head>
      <style id="critical-css" type="text/css">
      ${criticalCSS}
      </style>
    </head>
    <body>
      ${content}
    </body>
    </html>
  `)
})
 
app.listen(process.env['PORT'] || 3000)

Performance Matters

PostCSS parser is using by default in PreJSS. Since PostCSS is adopted for high performance - it uses async approach. How to get it as sync PreJSS Constraint? At the moment PreJSS uses deasync for parsing CSS styles on the server. It has some unpleasant costs - deasync blocks Event Loop so everything could be blocked until CSS parsing operation is processing.

Everything will be OK if you use basic approach which has been described in Example. In this way deasync affects only total launch time for server application. Generally it's not critical when we compare it with DX (Developer Experience), useful usage.

But if you wrap PreJSS Constraints to functions - it will cause the problem. Let's have a look:

import preJSS from 'prejss'
 
app.use('/', () => {
 
  // At this point Event Loop will be blocked by deasync
  // It means other requests will be "frozen" until CSS is processing
 
  const customStyles = preJSS`
    button {
      color: green;
      display: block;
      margin: 0.5em 0;
      font-family: Helvetica, Arial, sans-serif;
    }
  `
 
  res.send(getCustomizedPage(customStyles))
})

perom

Async Adapters as Solution

If you have wrapped PreJSS Constraints please use async Adapter and async-await:

import preJSS, { preJSSAsync } from 'prejss'
 
app.use('/', async () => {
 
  // At this point Event Loop will not be blocked 
  // Other requests will be processing parallely while CSS is processing
 
  const customStyles = await preJSSAsync`
    button {
      color: green;
      display: block;
      margin: 0.5em 0;
      font-family: Helvetica, Arial, sans-serif;
    }
  `
 
  res.send(getCustomizedPage(customStyles))
})

It will totally solve deasync effect.

Notice: If you don't have async-await (e.g. you have Node.js version lower than 7.6) it will work as well as Promises.

Disabled JavaScript in Web Browser

Server-Side Rendering and Critical CSS both allows your users to see page even without JavaScript in Web Browsers. You could implement GET and POST fallbacks for all possible actions such as CRUD operations like Google did it.

Parsers

There is two kind of parsers - sync and async. For both cases it's a pure function:

  • (preparedCSS: string): object
  • (preparedCSS: string): Promise

By default prejss-postcss-parser is using by default. This parser uses postcss-js under hood and supports PostCSS config and plugins.

Feel free to create and distribute your own parser. It should have package name in the following format:

  • prejss-<PARSER_NAME>-parser

Adapters

What does it mean "Adapters" in PreJSS?

It looks like as "class-to-function" adapter with lifecycle hooks. You already know this concept if you learned React.js or Ember.js.

PreJSS Adapters covers prepare, parse and finalize steps.

Default (built-in) adapters implements only parse step so you can customize it as you want.

You can create (and distribute!) your own adapters or customize existed one by overriding any of those steps:

  • prepare(rawStyles: string): string

    This hook is using for creating custom pre-processing CSS. It calls before parse() and looks like Redux middleware. For example, you can strip JavaScript comments or execute embedded JavaScript code. See example below.

  • parse(CSS: string): object

    The main hook which is using for converting CSS to JSS Object. Default Adapter uses PostCSS Processor for this operation.

  • finalize(result: object): object

    This hook is using for post-processing your final object. Here you can convert your JSS Objects to React Native or any other JSS library.

Feel free to play with it:

import preJSS, { createAdapter, defaultAdapter, keyframes } from 'prejss'
 
const fromMixedCSS = createAdapter({
  ...defaultAdapter,
  prepare: rawStyles => defaultAdapter.prepare(
    rawStyles.replace(/^\s*\/\/.*$/gm, '') // remove JS comments
  ),
})
 
const getStyles = ({ color, animationSpeed, className }) => fromMixedCSS`
  ${'button' + (className ? '.' + className : '')}
    color: ${() => color || 'palevioletred'};
    display: block;
    margin: 0.5em 0;
    font-family: Helvetica, Arial, sans-serif;
    
    // Let's rotate the board!
    &:hover {
      text-decoration: underline;
      animation: rotate360 ${animationSpeed || '2s'} linear infinite;
    }
  }
  
  // Special styles for Call-to-Action button
  ctaButton {
    @include button;
    
    &:hover {
      background: ${color('blue').darken(0.3).hex()}
    }
  }
`

Pre-compilation

It's not great idea to parse CSS in run-time on client-side. It's slow and expensive operations. Additonaly it requires to include PostCSS (or any other parsers) to JavaScript bundle.

The good news is that we don't have to do it! 🎉 Really.

There is great babel-plugin-prejss plugin which transforms PreJSS Constraints CSS styles example above to JSS object in the final scripts.

Step-by-Step manual:

  1. Add babel-plugin-prejss to your project:

    npm install babel-plugin-prejss --save-dev
  2. Configure it by updating .babelrc in your project directory:

    plugins: [
      [
        'prejss', {
          'namespace': 'preJSS'
        }
      ]
    ]
    
  3. Build your project! In your JavaScript bundles you will have replaced preJSS constraints by JSS objects directly. Babel do it for you. Not a magic - just a next generation JavaScript today. 😉

Hot Module Replacement with webpack

You can get Hot Module Replacement by using webpack and PostJSS loader to get real-time updates while you work with the project.

Step-by-Step manual:

  1. Add postjss to your project:

    npm install postjss --save
  2. Configure your webpack to use it:

    {
      test: tests.js,
      use: [
        'postjss/webpack/report-loader',
        'babel-loader',
        'postjss/webpack/hot-loader',
      ],
    },
    

Linting

Since we use PostCSS as default adapter - you can use stylelint for linting and postcss-reporter for warnings and errors.

So it works with using PostJSS like a charm:

Demo

Ecosystem

Inspiration

Thanks

We would love to say huge thanks for helping us:

  • Oleg Slobodskoi aka kof
  • Artur Kenzhaev aka lttb
  • Andrey Kholmanyuk aka hlomzik

And you? 😉

Dependents (1)

Package Sidebar

Install

npm i prejss

Weekly Downloads

8

Version

1.4.0

License

MIT

Last publish

Collaborators

  • denisaxept