martinandert/babel-plugin-css-in-js

View on GitHub
src/index.js

Summary

Maintainability
B
5 hrs
Test Coverage
import assert from 'assert';
import extend from 'object-assign';
import foreach from 'foreach';
import { writeFileSync } from 'fs';
import { relative, join, dirname, resolve } from 'path';
import { sync as mkDirPSync } from 'mkdirp';

import transformIntoStyleSheetObject from './transformIntoStyleSheetObject';
import transformStyleSheetObjectIntoSpecification from './transformStyleSheetObjectIntoSpecification';
import generateClassName from './generateClassName';
import buildCSS from './buildCSS';

const KEY = '__cssinjs';

const DEFAULT_OPTIONS = {
  identifier: 'cssInJS',
  vendorPrefixes: false,
  minify: false,
  compressClassNames: false,
  mediaMap: {},
  context: null,
  cacheDir: 'tmp/cache/',
  bundleFile: 'bundle.css',
  transformOptions: { presets: ['es2015'] },
};

export default function plugin(context) {
  context[KEY] = {
    cache: {},
    visiting: {},
  };

  return {
    visitor: visitor(context),
  };
}

function visitor(context) {
  const t = context.types;

  return {
    Program: {
      enter() {
        const filename = relative(process.cwd(), this.file.opts.filename);
        const options = buildOptions(this.opts, filename);

        this.cssInJS = { filename, options, stylesheets: {} };

        context[KEY].visiting[filename] = true;
      },

      exit() {
        const filename = this.cssInJS.filename;

        /* istanbul ignore if */
        if (!context[KEY].visiting[filename]) return;

        const css = buildCSS(this.cssInJS.stylesheets, this.cssInJS.options);

        this.file.metadata.css = css;

        if (css && css.length) {
          context[KEY].cache[this.cssInJS.filename] = css;
        } else {
          delete context[KEY].cache[this.cssInJS.filename];
        }

        if (this.cssInJS.options.bundleFile && Object.keys(context[KEY].cache).length) {
          const bundleFile = join(process.cwd(), this.cssInJS.options.bundleFile);
          const output = [];

          mkDirPSync(dirname(bundleFile));

          foreach(context[KEY].cache, (fileCSS) => {
            output.push(fileCSS);
          });

          writeFileSync(bundleFile, output.join(''), { encoding: 'utf8' });
        }

        context[KEY].visiting[filename] = false;
      },
    },

    CallExpression(path) {
      if (!t.isIdentifier(path.node.callee, { name: this.cssInJS.options.identifier })) {
        return;
      }

      assert(
        t.isVariableDeclarator(path.parentPath.node),
        'return value of cssInJS(...) must be assigned to a variable'
      );

      const sheetId = path.parentPath.node.id.name;
      const expr = path.node.arguments[0];

      assert(expr, 'cssInJS(...) call is missing an argument');

      const obj = transformIntoStyleSheetObject(expr, this.cssInJS.options.context, this.cssInJS.options.transformOptions);
      const sheet = transformStyleSheetObjectIntoSpecification(obj);

      this.cssInJS.stylesheets[sheetId] = sheet;

      const gcnOptions = extend({}, this.cssInJS.options, { prefixes: [this.cssInJS.filename, sheetId] });

      const properties = Object.keys(sheet).reduce((memo, styleId) => {
        if (styleId.charAt(0) === '$' || styleId.indexOf(' ') > 0) {
          return memo;
        }

        return memo.concat(
          t.objectProperty(
            t.identifier(styleId),
            t.stringLiteral(generateClassName(styleId, gcnOptions))
          )
        );
      }, []);

      path.replaceWith(t.objectExpression(properties));
    },
  };
}

const contextFileCache = {};

function buildOptions(options, filename) {
  options = extend({}, DEFAULT_OPTIONS, options, { filename });

  if (typeof options.context === 'string') {
    const file = resolve(options.context);

    if (typeof contextFileCache[file] === 'undefined') {
      contextFileCache[file] = require(file);
    }

    options.context = contextFileCache[file];
  }

  return options;
}