Home Reference Source

lib/contexts/NodeContext.js

const fs = require('fs')
let glob = require('glob')
const path = require('path')
const rollup = require('rollup')
const rollupUglify = require('rollup-plugin-uglify')
const tmp = require('tmp')
const untildify = require('untildify')
const util = require('util')
glob = util.promisify(glob)

const JavascriptContext = require('./JavascriptContext')

/**
 * A Node.js context for executing Javascript code
 */
class NodeContext extends JavascriptContext {
  /**
   * Compile a Javascript source code file
   *
   * @param  {String} file File path
   * @return {Object}      Function object
   */
  async compileFile (file) {
    let content = fs.readFileSync(file, 'utf8')
    try {
      return this.compile(content)
    } catch (error) {
      throw new Error(`Error compiling file "${file}": ` + error.message)
    }
  }

  /**
   * Compile a Stencila library so that it can be loaded either into
   * a `NodeContext` or another `JavascriptContext` (e.g one embedded
   * into a Stencila web or desktop interface).
   *
   * Creates a Javascript bundle which exports both function definitions and
   * a function specification objects.
   *
   * @param {String} src Path to library folder
   * @param {String} dest Destination
   * @param {Boolean} minify Should the bundle be minified? (defaults to true)
   */
  async compileLibrary (library = {}) {
    let src = library.src || '.'
    let dest = library.dest
    let minify = (library.minify === false) ? library.minify : true

    src = path.resolve(untildify(src))
    if (!dest) {
      let file = path.basename(src)
      if (minify) file += '.min'
      file += '.js'
      dest = path.join(src, file)
    }

    try {
      fs.statSync(src)
    } catch (error) {
      throw new Error(`No such folder "${src}"`)
    }

    const name = library.name || path.basename(src)

    let json
    try {
      json = fs.readFileSync(path.join(src, 'package.json'))
    } catch (error) {
      json = '{}'
    }
    const pkg = JSON.parse(json)

    const pattern = path.join(src, 'funcs', '*.js')
    const files = await glob(pattern, {ignore: '**/_*'})
    if (files.length === 0) throw new Error(`No functions found matching pattern "${pattern}"`)

    let funcs = {}
    let index = `
      export const type = 'library'
      export const name = '${name}'
      export let funcs = {}
    `
    for (let file of files) {
      let cell
      try {
        cell = await this.compileFile(file, false)
      } catch (error) {
        throw new Error(`Error compiling file "${file}": ${error.message}`)
      }
      let func = cell.outputs[0].value.data
      const name = func.name
      funcs[name] = func

      const json = JSON.stringify(func, null, '  ')
      index += `import ${name}_ from '${file}'\n`
      index += `funcs['${name}'] = ${json}\n`
      index += `funcs['${name}'].body = ${name}_\n\n`
    }

    const indexPath = tmp.tmpNameSync()
    fs.writeFileSync(indexPath, index)

    const rollupConfig = pkg.rollup || {}

    const plugins = []
    if (minify) plugins.push(rollupUglify())

    const bundle = await rollup.rollup({
      input: indexPath,
      plugins: plugins,
      external: rollupConfig.external
    })

    await bundle.write({
      format: 'umd',
      name: rollupConfig.name || 'local',
      file: dest,
      globals: rollupConfig.globals
    })

    return {
      type: 'library',
      name: name,
      funcs: funcs,
      bundle: dest
    }
  }

  async executeLibrary (library_) {
    const library = await this.compileLibrary(library_)
    library.module = require(library.bundle)
    this._libraries[library.name] = library
    return {
      type: 'library',
      name: library.name,
      funcs: library.funcs
    }
  }
}

NodeContext.spec = {
  name: 'NodeContext',
  client: 'ContextHttpClient'
}

module.exports = NodeContext