faceyspacey/babel-plugin-universal-import

View on GitHub
index.js

Summary

Maintainability
A
2 hrs
Test Coverage
A
97%
'use-strict'

const validMagicStrings = [
  'webpackMode',
  // 'webpackMagicChunkName' gets dealt with current implementation & naming/renaming strategy
  'webpackInclude',
  'webpackExclude',
  'webpackIgnore',
  'webpackPreload',
  'webpackPrefetch'
]

const { addDefault } = require('@babel/helper-module-imports')

const path = require('path')

const visited = Symbol('visited')

const IMPORT_UNIVERSAL_DEFAULT = {
  id: Symbol('universalImportId'),
  source: 'babel-plugin-universal-import/universalImport',
  nameHint: 'universalImport'
}

const IMPORT_PATH_DEFAULT = {
  id: Symbol('pathId'),
  source: 'path',
  nameHint: 'path'
}

function getImportArgPath(p) {
  return p.parentPath.get('arguments')[0]
}

function trimChunkNameBaseDir(baseDir) {
  return baseDir.replace(/^[./]+|(\.js$)/g, '')
}

function getImport(p, { source, nameHint }) {
  return addDefault(p, source, { nameHint })
}

function createTrimmedChunkName(t, importArgNode) {
  if (importArgNode.quasis) {
    let quasis = importArgNode.quasis.slice(0)
    const baseDir = trimChunkNameBaseDir(quasis[0].value.cooked)
    quasis[0] = Object.assign({}, quasis[0], {
      value: { raw: baseDir, cooked: baseDir }
    })

    quasis = quasis.map((quasi, i) => (i > 0 ? prepareQuasi(quasi) : quasi))

    return Object.assign({}, importArgNode, {
      quasis
    })
  }

  const moduleName = trimChunkNameBaseDir(importArgNode.value)
  return t.stringLiteral(moduleName)
}

function prepareQuasi(quasi) {
  return Object.assign({}, quasi, {
    value: { raw: quasi.value.cooked, cooked: quasi.value.cooked }
  })
}

function getMagicWebpackComments(importArgNode) {
  const { leadingComments } = importArgNode
  const results = []
  if (leadingComments && leadingComments.length) {
    leadingComments.forEach(comment => {
      try {
        const validMagicString = validMagicStrings.filter(str =>
          new RegExp(`${str}\\w*:`).test(comment.value)
        )
        // keep this comment if we found a match
        if (validMagicString && validMagicString.length === 1) {
          results.push(comment)
        }
      }
      catch (e) {
        // eat the error, but don't give up
      }
    })
  }
  return results
}

function getMagicCommentChunkName(importArgNode) {
  const { quasis, expressions } = importArgNode
  if (!quasis) return trimChunkNameBaseDir(importArgNode.value)

  const baseDir = quasis[0].value.cooked
  const hasExpressions = expressions.length > 0
  const chunkName = baseDir + (hasExpressions ? '[request]' : '')
  return trimChunkNameBaseDir(chunkName)
}

function getComponentId(t, importArgNode) {
  const { quasis, expressions } = importArgNode
  if (!quasis) return importArgNode.value

  return quasis.reduce((str, quasi, i) => {
    const q = quasi.value.cooked
    const id = expressions[i] && expressions[i].name
    str += id ? `${q}\${${id}}` : q
    return str
  }, '')
}

function existingMagicCommentChunkName(importArgNode) {
  const { leadingComments } = importArgNode
  if (
    leadingComments &&
    leadingComments.length &&
    leadingComments[0].value.indexOf('webpackChunkName') !== -1
  ) {
    try {
      return leadingComments[0].value
        .split('webpackChunkName:')[1]
        .replace(/["']/g, '')
        .trim()
    }
    catch (e) {
      return null
    }
  }
  return null
}

function idOption(t, importArgNode) {
  const id = getComponentId(t, importArgNode)
  return t.objectProperty(t.identifier('id'), t.stringLiteral(id))
}

function fileOption(t, p) {
  return t.objectProperty(
    t.identifier('file'),
    t.stringLiteral(
      path.relative(__dirname, p.hub.file.opts.filename || '') || ''
    )
  )
}

function loadOption(t, loadTemplate, p, importArgNode) {
  const argPath = getImportArgPath(p)
  const generatedChunkName = getMagicCommentChunkName(importArgNode)
  const otherValidMagicComments = getMagicWebpackComments(importArgNode)
  const existingChunkName = t.existingChunkName
  const chunkName = existingChunkName || generatedChunkName

  delete argPath.node.leadingComments
  argPath.addComment('leading', ` webpackChunkName: '${chunkName}' `)
  otherValidMagicComments.forEach(validLeadingComment =>
    argPath.addComment('leading', validLeadingComment.value)
  )

  const load = loadTemplate({
    IMPORT: argPath.parent
  }).expression

  return t.objectProperty(t.identifier('load'), load)
}

function pathOption(t, pathTemplate, p, importArgNode) {
  const path = pathTemplate({
    PATH: getImport(p, IMPORT_PATH_DEFAULT),
    MODULE: importArgNode
  }).expression

  return t.objectProperty(t.identifier('path'), path)
}

function resolveOption(t, resolveTemplate, importArgNode) {
  const resolve = resolveTemplate({
    MODULE: importArgNode
  }).expression

  return t.objectProperty(t.identifier('resolve'), resolve)
}

function chunkNameOption(t, chunkNameTemplate, importArgNode) {
  const existingChunkName = t.existingChunkName
  const generatedChunk = createTrimmedChunkName(t, importArgNode)
  const trimmedChunkName = existingChunkName
    ? t.stringLiteral(existingChunkName)
    : generatedChunk

  const chunkName = chunkNameTemplate({
    MODULE: trimmedChunkName
  }).expression

  return t.objectProperty(t.identifier('chunkName'), chunkName)
}

module.exports = function universalImportPlugin({ types: t, template }) {
  const chunkNameTemplate = template('() => MODULE')
  const pathTemplate = template('() => PATH.join(__dirname, MODULE)')
  const resolveTemplate = template('() => require.resolveWeak(MODULE)')
  const loadTemplate = template(
    '() => Promise.all([IMPORT]).then(proms => proms[0])'
  )

  return {
    name: 'universal-import',
    visitor: {
      Import(p) {
        if (p[visited]) return
        p[visited] = true

        const importArgNode = getImportArgPath(p).node
        t.existingChunkName = existingMagicCommentChunkName(importArgNode)
        // no existing chunkname, no problem - we will reuse that for fixing nested chunk names

        const universalImport = getImport(p, IMPORT_UNIVERSAL_DEFAULT)

        // if being used in an await statement, return load() promise
        if (
          p.parentPath.parentPath.isYieldExpression() || // await transformed already
          t.isAwaitExpression(p.parentPath.parentPath.node) // await not transformed already
        ) {
          const func = t.callExpression(universalImport, [
            loadOption(t, loadTemplate, p, importArgNode).value,
            t.booleanLiteral(false)
          ])

          p.parentPath.replaceWith(func)
          return
        }

        const opts = (this.opts.babelServer
          ? [
            idOption(t, importArgNode),
            this.opts.includeFileName ? fileOption(t, p) : undefined,
            pathOption(t, pathTemplate, p, importArgNode),
            resolveOption(t, resolveTemplate, importArgNode),
            chunkNameOption(t, chunkNameTemplate, importArgNode)
          ]
          : [
            idOption(t, importArgNode),
            this.opts.includeFileName ? fileOption(t, p) : undefined,
            loadOption(t, loadTemplate, p, importArgNode), // only when not on a babel-server
            pathOption(t, pathTemplate, p, importArgNode),
            resolveOption(t, resolveTemplate, importArgNode),
            chunkNameOption(t, chunkNameTemplate, importArgNode)
          ]
        ).filter(Boolean)

        const options = t.objectExpression(opts)

        const func = t.callExpression(universalImport, [options])
        delete t.existingChunkName
        p.parentPath.replaceWith(func)
      }
    }
  }
}