<NME> transform.js
<BEF> const isMemberExpression = require('estree-is-member-expression')
const cssResolve = require('style-resolve').sync
const transformAst = require('transform-ast')
const staticEval = require('static-eval')
const parse = require('fast-json-parse')
const findup = require('@choojs/findup')
const sleuth = require('sleuth')
const sheetify = require('./')
const path = require('path')

module.exports = transform


module.exports = transform

function isBabelTemplateDefinition (node) {
  return node.type === 'CallExpression' &&
    node.callee.type === 'Identifier' && node.callee.name === '_taggedTemplateLiteral'
}

function isBubleTemplateDefinition (node) {
  return node.type === 'CallExpression' &&
    isMemberExpression(node.callee, 'Object.freeze') &&
    node.arguments[0] && node.arguments[0].type === 'ArrayExpression' &&
    node.arguments[0].elements.every(function (el) { return el.type === 'Literal' })
}

// inline sheetify transform for browserify
// obj -> (str, opts) -> str
function transform (filename, options) {
  if (/\.json$/i.test(filename)) return through()

  const opts = Object.assign({}, options || {
    basedir: process.cwd(),
    transform: [],
    out: ''
  })

  opts.transform = [].concat(opts.transform || []).concat(opts.t || []).map(function (plugin) {
    // resolve subarg syntax
    if (typeof plugin === 'object' && Array.isArray(plugin._)) {
      return [
        plugin._[0],
        plugin
      ]
    }
    return plugin
  })

  const bufs = []
  const transformStream = through(write, end)
  return transformStream

  // aggregate all AST nodes
  // (buf, str, fn) -> null
  function write (buf, enc, next) {
    bufs.push(buf)
    next()
  }

  // parse and push AST nodes
  // null -> null
  function end () {
    const self = this

    // cool, you've made it this far. We know this is gross,
    // but tough times call for tough measure. Please don't
    // judge us too harshly, we'll work on perf ✨soon✨ -yw
    const nodes = []
    const babelTemplateDeclarations = {}
    const bubleTemplateDeclarations = {}
    const src = Buffer.concat(bufs).toString('utf8')
    var mname = null
    var ast

    // Skip transforming any files that do not contain synonyms for sheetify
    if (!SUPPORTED_SYNONYMS.some(name => src.includes(name))) {
      self.push(src)
      self.push(null)
      return
    }

    findTransforms(filename, function (err, transforms) {
      if (err) return self.emit('error', err)

      opts.transform = transforms.concat(opts.transform || []).concat(opts.t || [])

      try {
        ast = transformAst(src, {
          parser: require('acorn-node'),
          inputFilename: path.basename(filename)
        }, identifyModuleName)
        ast.walk(extractNodes)
      } catch (err) {
    }, function (err, styles) {
      if (err) return stream.emit('error', err)

      const relative = getRequirePath(filename)
      const req = path.join(relative, 'insert-css')

      styles = styles.join('\n')

      ast.program.body.unshift({
        type: 'ExpressionStatement',
        expression: {
          type: 'CallExpression',
          callee: {
            type: 'CallExpression',
            callee: {
              type: 'Identifier',
              name: 'require'
            },
            arguments: [
              {
                type: 'Literal',
                value: req,
                rawValue: req,
                raw: JSON.stringify(req)
              }
            ]
          },
          arguments: [
            {
              type: 'Literal',
              value: styles,
              rawValue: styles,
              raw: JSON.stringify(styles)
            }
          ]
        }
      })

      stream.push(escodegen.generate(ast.program))
      stream.push(null)
    })
  }
}

function getRequirePath (filename) {
  return path.relative(path.dirname(filename), path.dirname(__filename))
}

// doesn't currently handle inline requires, i.e.
        })
      }

      if (node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === mname) {
        if (node.arguments[0] && node.arguments[0].type === 'ArrayExpression') {
          // Buble generates code like
          //     sheetify([":host .class { color: hotpink; }"])
          elements = node.arguments[0].elements
        } else if (node.arguments[0] && node.arguments[0].type === 'Identifier') {
          // Babel generates code like
          //    sheetify(_templateObject)
          var arg = node.arguments[0].name
          var templateDeclaration =
            babelTemplateDeclarations[arg] || bubleTemplateDeclarations[arg]
          if (templateDeclaration) {
            var template = templateDeclaration.init.arguments[0]
            elements = template.elements

            /*
            We want to remove transpiled var declaration for minification to work.

            var ...,
                _templateObject2 = _taggedTemplateLiteral(['.foo { color: blue; }'], ['.foo { color: blue; }']),
                ...;
            */
            removeVariableDeclarator(templateDeclaration)
          }
        }

        if (elements) {
          nodes.push({
            css: elements.map(function (part) { return part.value }).join(''),
            filename: filename,
            opts: Object.assign({}, opts),
            node: node
          })
        }
      }
    }

    function removeVariableDeclarator (node) {
      /*
      Remove `a` declaration from compound `var` statement:
      var ...,
          a = B,
          ...
      */
      var parent = node.parent
      var offset = parent.start
      var at = parent.declarations.indexOf(node)
      var total = parent.declarations.length
      var str = parent.getSource()
      var prevNode, nextNode

      var stripped
      if (total === 1) {
        stripped = ' '.repeat(str.length) // remove whole "var ...;" statement
      } else if (at < total - 1) { // not last
        nextNode = parent.declarations[at + 1]
        stripped =
          str.slice(0, node.start - offset) +
          ' '.repeat(nextNode.start - node.start) +
          str.slice(nextNode.start - offset)
      } else { // last one
        prevNode = parent.declarations[at - 1]
        stripped =
          str.slice(0, prevNode.end - offset) +
          ' '.repeat(node.end - prevNode.end) +
          str.slice(node.end - offset)
      }
      parent.update(stripped)
    }

    function extractImportNodes (node) {
      if (node.type !== 'CallExpression') return
      if (!node.callee || node.callee.type !== 'Identifier') return
      if (node.callee.name !== mname) return
      if (!node.arguments[0] || !node.arguments[0].value) return
      var pathOpts = { basedir: path.dirname(filename) }
      try {
        var resolvePath = cssResolve(node.arguments[0].value, pathOpts)
      } catch (err) {
        return self.emit('error', err)
      }

      const iOpts = node.arguments[1]
        ? Object.assign({}, opts, staticEval(node.arguments[1]))
        : opts

      transformStream.emit('file', resolvePath)

      const val = {
        filename: resolvePath,
        opts: iOpts,
        node: node
      }

      nodes.push(val)
    }
  }

  // iterate over nodes, and apply sheetify transformation
  // then replace the AST nodes with new values
  // (obj, fn) -> null
  function iterate (val, done) {
    if (typeof val.css === 'string') return handleCss(val)

    if (/\.js$/.test(val.filename)) {
      delete require.cache[require.resolve(val.filename)]
      val.css = require(val.filename)
      handleCss(val)
    } else {
      fs.readFile(val.filename, 'utf8', function (err, css) {
        if (err) return done(err)
        val.css = css
        handleCss(val)
      })
    }

    function handleCss (val) {
      sheetify(val.css, val.filename, val.opts, function (err, result, prefix) {
        if (err) return done(err)
        const str = [
          "((require('sheetify/insert')(" + JSON.stringify(result.css) + ')',
          ' || true) && ' + JSON.stringify(prefix) + ')'
        ].join('')

        const parentNodeType = val.node.parent.type
        const lolSemicolon = parentNodeType === 'VariableDeclarator' ||
          parentNodeType === 'AssignmentExpression'
          ? ''
          : ';'
        val.node.update(lolSemicolon + str)

        result.files.forEach(function (includedFile) {
          transformStream.emit('file', includedFile)
        })

        done()
      })
    }
  }

  function findTransforms (filename, done) {
    findup(filename, 'package.json', function (err, pathname) {
      // no package.json found - just run local transforms
      if (err) return done(null, [])

      var filename = path.join(pathname, 'package.json')
      fs.readFile(filename, function (err, file) {
        if (err) return done(err)
        var parsed = parse(file)
        if (parsed.err) return done(parsed.err)
        var json = parsed.value
        var d = json.sheetify
        if (!d) return done(null, [])
        var t = d.transform
        if (!t || !Array.isArray(t)) return done(null, [])

        return done(null, t)
      })
    })
  }
}

function cooked (node) { return node.value.cooked }
function expr (ex) { return { _expr: ex.source() } }

<MSG> get real path of filename being transformed

fixes the following error, caused when using sheetify with [the app symlink pattern](https://github.com/substack/browserify-handbook#symlink)

> Error: Cannot find module '../../../sheetify/insert-css' from 'app/components/thing'

<DFF> @@ -7,6 +7,7 @@ const babylon = require('babylon')
 const sleuth = require('sleuth')
 const sheetify = require('./')
 const path = require('path')
+const fs = require('fs')
 
 module.exports = transform
 
@@ -95,49 +96,58 @@ function transform (filename, options) {
     }, function (err, styles) {
       if (err) return stream.emit('error', err)
 
-      const relative = getRequirePath(filename)
-      const req = path.join(relative, 'insert-css')
+      getRequirePath(filename, function (err, requirePath) {
+        if (err) return stream.emit('error', err)
 
-      styles = styles.join('\n')
+        const req = path.join(requirePath, 'insert-css')
 
-      ast.program.body.unshift({
-        type: 'ExpressionStatement',
-        expression: {
-          type: 'CallExpression',
-          callee: {
+        styles = styles.join('\n')
+
+        ast.program.body.unshift({
+          type: 'ExpressionStatement',
+          expression: {
             type: 'CallExpression',
             callee: {
-              type: 'Identifier',
-              name: 'require'
+              type: 'CallExpression',
+              callee: {
+                type: 'Identifier',
+                name: 'require'
+              },
+              arguments: [
+                {
+                  type: 'Literal',
+                  value: req,
+                  rawValue: req,
+                  raw: JSON.stringify(req)
+                }
+              ]
             },
             arguments: [
               {
                 type: 'Literal',
-                value: req,
-                rawValue: req,
-                raw: JSON.stringify(req)
+                value: styles,
+                rawValue: styles,
+                raw: JSON.stringify(styles)
               }
             ]
-          },
-          arguments: [
-            {
-              type: 'Literal',
-              value: styles,
-              rawValue: styles,
-              raw: JSON.stringify(styles)
-            }
-          ]
-        }
-      })
+          }
+        })
 
-      stream.push(escodegen.generate(ast.program))
-      stream.push(null)
+        stream.push(escodegen.generate(ast.program))
+        stream.push(null)
+      })
     })
   }
 }
 
-function getRequirePath (filename) {
-  return path.relative(path.dirname(filename), path.dirname(__filename))
+function getRequirePath (filename, callback) {
+  fs.realpath(filename, function (err, realname) {
+    if (err) { return callback(err) }
+    const relative = path.relative(
+      path.dirname(realname), path.dirname(__filename)
+    )
+    callback(null, relative)
+  })
 }
 
 // doesn't currently handle inline requires, i.e.

{'addition_count': 38, 'commit_subject': 'get real path of filename being transformed', 'deletion_count': 28, 'file_extension': '.js', 'lang': 'js', 'license': 'mit', 'repo_name': 'stackcss/sheetify'}