UNPKG

afrododi

Version:

Framework-agnostic async-compatible CSS-in-JS with support for server-side rendering, browser prefixing, and minimum CSS generation

209 lines (183 loc) 6.75 kB
/* @flow */ import {hashString} from './util'; import { injectAndGetClassName, startBuffering, createContext, flushToString, flushToStyleTag, addRenderedClassNames, getRenderedClassNames, getBufferedStyles, } from './inject'; import { CSSProvider, withCSS, } from './react'; import {defaultSelectorHandlers} from './generate'; /* :: import type { SelectorHandler } from './generate.js'; import type { StyleContext } from './inject.js'; export type SheetDefinition = { [id:string]: any }; export type SheetDefinitions = SheetDefinition | SheetDefinition[]; type RenderFunction = (context: StyleContext) => string; type AsyncRenderFunction = (context: StyleContext) => Promise<string>; type Extension = { selectorHandler: SelectorHandler }; export type MaybeSheetDefinition = SheetDefinition | false | null | void */ const unminifiedHashFn = (str/* : string */, key/* : string */) => `${key}_${hashString(str)}`; // StyleSheet.create is in a hot path so we want to keep as much logic out of it // as possible. So, we figure out which hash function to use once, and only // switch it out via minify() as necessary. // // This is in an exported function to make it easier to test. export const initialHashFn = () => process.env.NODE_ENV === 'production' ? hashString : unminifiedHashFn; let hashFn = initialHashFn(); const StyleSheet = { create(sheetDefinition /* : SheetDefinition */) /* : Object */ { const mappedSheetDefinition = {}; const keys = Object.keys(sheetDefinition); for (let i = 0; i < keys.length; i += 1) { const key = keys[i]; const val = sheetDefinition[key]; const stringVal = JSON.stringify(val); mappedSheetDefinition[key] = { _len: stringVal.length, _name: hashFn(stringVal, key), _definition: val, }; } return mappedSheetDefinition; }, createContext, rehydrate(context /* : StyleContext */, renderedClassNames /* : string[] */ =[]) { addRenderedClassNames(context, renderedClassNames); }, }; /** * Utilities for using afrododi server-side. * * This can be minified out in client-only bundles by replacing `typeof window` * with `"object"`, e.g. via Webpack's DefinePlugin: * * new webpack.DefinePlugin({ * "typeof window": JSON.stringify("object") * }) */ const StyleSheetServer = typeof window !== 'undefined' ? null : { renderStatic(renderFunc /* : RenderFunction */) { const context = startBuffering(); const html = renderFunc(context); const cssContent = flushToString(context); return { html: html, css: { content: cssContent, renderedClassNames: getRenderedClassNames(context), }, }; }, renderStaticAsync(renderFunc /* : AsyncRenderFunction */) { const context = startBuffering(); return renderFunc(context).then((html /* : string */) => { const cssContent = flushToString(context); return { html: html, css: { content: cssContent, renderedClassNames: getRenderedClassNames(context), }, }; }) }, }; /** * Utilities for using afrododi in tests. * * Not meant to be used in production. */ const StyleSheetTestUtils = process.env.NODE_ENV === 'production' ? null : { /** * Gets a new StyleContext instance to use during testing * * @returns {object} StyleContext instance for use during testing */ getContext() /* : StyleContext */ { return startBuffering(); }, /** * Returns a string of buffered styles which have not been flushed * * @returns {string} Buffer of styles which have not yet been flushed. */ getBufferedStyles(context /* : StyleContext */) /* : string[] */ { return getBufferedStyles(context); } }; /** * Generate the afrododi API exports, with given `selectorHandlers` and * `useImportant` state. */ export default function makeExports( useImportant /* : boolean */, selectorHandlers /* : SelectorHandler[] */ = defaultSelectorHandlers, ) { return { StyleSheet: { ...StyleSheet, /** * Returns a version of the exports of afrododi (i.e. an object * with `css` and `StyleSheet` properties) which have some * extensions included. * * @param {Array.<Object>} extensions: An array of extensions to * add to this instance of afrododi. Each object should have a * single property on it, defining which kind of extension to * add. * @param {SelectorHandler} [extensions[].selectorHandler]: A * selector handler extension. See `defaultSelectorHandlers` in * generate.js. * * @returns {Object} An object containing the exports of the new * instance of afrododi. */ extend(extensions /* : Extension[] */) { const extensionSelectorHandlers = extensions // Pull out extensions with a selectorHandler property .map(extension => extension.selectorHandler) // Remove nulls (i.e. extensions without a selectorHandler property). .filter(handler => handler); return makeExports( useImportant, selectorHandlers.concat(extensionSelectorHandlers) ); }, }, StyleSheetServer, StyleSheetTestUtils, minify(shouldMinify /* : boolean */) { hashFn = shouldMinify ? hashString : unminifiedHashFn; }, css(context /* : StyleContext */, ...styleDefinitions /* : MaybeSheetDefinition[] */) { if (!context || !context.hasOwnProperty('injectionBuffer')) { throw new Error('The css() function was called without a StyleContext instance. Consider using the withCSS() higher-order component instead.') } return injectAndGetClassName( context, useImportant, styleDefinitions, selectorHandlers ); }, CSSProvider, withCSS, flushToStyleTag, injectAndGetClassName, defaultSelectorHandlers, }; }