Skip to content
/ essay Public

📝 The real README driven development! Generate a JavaScript library out of an essay. Literate programming for the ES2015 era.

Notifications You must be signed in to change notification settings

dtinth/essay

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

69 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

essay

NPM version Build status Code coverage

Generate your JavaScript library out of an essay!

  • Write code in your README.md file in fenced code blocks, with a comment indicating the file’s name.

  • Write code using ES2016 syntax. essay will use Babel to transpile them to ES5.

  • Test your code using Mocha and power-assert.

  • Measures your code coverage. essay generates code coverage report for your README.md file using Istanbul and babel-plugin-__coverage__.

  • Examples of JavaScript libraries/articles written using essay:

    Project Description
    circumstance BDD for your pure state-updating functions (e.g. Redux reducers)
    positioning-strategy A library that implements an algorithm to calculate where to position an element relative to another element
    code-to-essay Turns your code into an essay-formatted Markdown file.
    timetable-calculator An algorithm for computing the layout for a timetable, handling the case where items are overlapped.
    impure A simple wrapper object for non-deterministic code (like IO monads in Haskell)
    • Using essay to write a library/article? Feel free to add your link here: please submit a PR!

synopsis

For example, you could write your library in a fenced code block in your README.md file like this:

// examples/add.js
export default (a, b) => a + b

And also write a test for it:

// examples/add.test.js
import add from './add'

// describe block called automatically for us!
it('should add two numbers', () => {
  assert(add(1, 2) === 3)
})

getting started

The easiest way to get started is to use initialize-essay.

Manual setup steps
  1. Make sure you already have your README.md file in place and already initialized your npm package (e.g. using npm init).

  2. Install essay as your dev dependency.

    npm install --save-dev essay
    
  3. Ignore these folders in .gitignore:

    src
    lib
    lib-cov
    
  4. Add "files" array to your package.json:

    "files": [
      "lib",
      "src"
    ]
  5. Add the "scripts" to your package.json:

    "scripts": {
      "prepublish": "essay build",
      "test": "essay test"
    },
  6. Set your main to the file you want to use, prefixed with lib/:

    "main": "lib/index.js",

building

When you run:

npm run prepublish # -> essay build

These code blocks will be extracted into its own file:

src
└── examples
    ├── add.js
    └── add.test.js
lib
└── examples
    ├── add.js
    └── add.test.js

The src folder contains the code as written in README.md, and the lib folder contains the transpiled code.

testing

When you run:

npm test # -> essay test

All the files ending with .test.js will be run using Mocha framework. power-assert is included by default (but you can use any assertion library you want).

  examples/add.test.js
    ✓ should add two numbers

Additionally, test coverage report for your README.md file will be generated.

development

You need to use npm install --force, because I use essay to write essay, but npm doesn’t want to install a package as a dependency of itself (even though it is an older version).

commands

essay build

// cli/buildCommand.js
import obtainCodeBlocks from '../obtainCodeBlocks'
import dumpSourceCodeBlocks from '../dumpSourceCodeBlocks'
import transpileCodeBlocks from '../transpileCodeBlocks'
import getBabelConfig from '../getBabelConfig'

export const command = 'build'
export const description = 'Builds the README.md file into lib folder.'
export const builder = (yargs) => yargs
export const handler = async (argv) => {
  const babelConfig = getBabelConfig()
  const targetDirectory = 'lib'
  const codeBlocks = await obtainCodeBlocks()
  await dumpSourceCodeBlocks(codeBlocks)
  await transpileCodeBlocks({ targetDirectory, babelConfig })(codeBlocks)
}

essay test

// cli/testCommand.js
import obtainCodeBlocks from '../obtainCodeBlocks'
import dumpSourceCodeBlocks from '../dumpSourceCodeBlocks'
import transpileCodeBlocks from '../transpileCodeBlocks'
import getTestingBabelConfig from '../getTestingBabelConfig'
import runUnitTests from '../runUnitTests'

export const command = 'test'
export const description = 'Runs the test.'
export const builder = (yargs) => yargs
export const handler = async (argv) => {
  const babelConfig = getTestingBabelConfig()
  const targetDirectory = 'lib-cov'
  const codeBlocks = await obtainCodeBlocks()
  await dumpSourceCodeBlocks(codeBlocks)
  await transpileCodeBlocks({ targetDirectory, babelConfig })(codeBlocks)
  await runUnitTests(codeBlocks)
}

essay lint

// cli/lintCommand.js
import obtainCodeBlocks from '../obtainCodeBlocks'
import runLinter from '../runLinter'
import moduleExists from 'module-exists'

export const command = 'lint'
export const description = 'Runs the linter.'
export const builder = (yargs) => yargs
export const handler = async (argv) => {
  if (allowToUseESLint(moduleExists('eslint'))) {
    const codeBlocks = await obtainCodeBlocks()
    await runLinter(codeBlocks, argv)
  }
}
export const allowToUseESLint = (hasESLintModule) => {
  if (hasESLintModule) return true
  console.log('Please install eslint')
  process.exitCode = 1
}

obtaining code blocks

essay extracts the fenced code blocks from your README.md file:

// obtainCodeBlocks.js
import extractCodeBlocks from './extractCodeBlocks'
import fs from 'fs'

export async function obtainCodeBlocks () {
  const readme = fs.readFileSync('README.md', 'utf8')
  const codeBlocks = extractCodeBlocks(readme)
  return codeBlocks
}

export default obtainCodeBlocks

code block extraction

It extracts a fenced code block that looks like this:

```js
// filename.js
export default 42
```

Once extracted, each code block will have its associated file name, contents, and the line number.

// extractCodeBlocks.js
export function extractCodeBlocks (data) {
  const codeBlocks = { }
  const regexp = /(`[`]`js\s+\/\/\s*(\S+).*\n)([\s\S]+?)`[`]`/g
  data.replace(regexp, (all, before, filename, contents, index) => {
    if (codeBlocks[filename]) throw new Error(filename + ' already exists!')

    // XXX: Not the most efficient way to find the line number.
    const line = data.substr(0, index + before.length).split('\n').length

    codeBlocks[filename] = { contents, line }
  })
  return codeBlocks
}

export default extractCodeBlocks

Test:

// extractCodeBlocks.test.js
import extractCodeBlocks from './extractCodeBlocks'

const END = '`' + '`' + '`'
const BEGIN = END + 'js'

const example = [
  'Hello world!',
  '============',
  '',
  BEGIN,
  '// file1.js',
  'console.log("hello,")',
  END,
  '',
  '- It should work in lists too!',
  '',
  '  ' + BEGIN,
  '  // file2.js',
  '  console.log("world!")',
  '  ' + END,
  '',
  'That’s it!'
].join('\n')

const blocks = extractCodeBlocks(example)

it('should extract code blocks into object', () => {
  assert.deepEqual(Object.keys(blocks).sort(), [ 'file1.js', 'file2.js' ])
})

it('should contain the code block’s contents', () => {
  assert(blocks['file1.js'].contents.trim() === 'console.log("hello,")')
  assert(blocks['file2.js'].contents.trim() === 'console.log("world!")')
})

it('should contain line numbers', () => {
  assert(blocks['file1.js'].line === 6)
  assert(blocks['file2.js'].line === 13)
})

dumping code blocks to source files

Extracted code blocks are first dumped into src directory.

// dumpSourceCodeBlocks.js
import forEachCodeBlock from './forEachCodeBlock'
import saveToFile from './saveToFile'
import path from 'path'

export const dumpSourceCodeBlocks = forEachCodeBlock(async ({ contents }, filename) => {
  const targetFilePath = path.join('src', filename)
  await saveToFile(targetFilePath, contents)
})

export default dumpSourceCodeBlocks

transpilation

We transpile each code block using Babel.

// transpileCodeBlocks.js
import forEachCodeBlock from './forEachCodeBlock'
import transpileCodeBlock from './transpileCodeBlock'

export function transpileCodeBlocks (options) {
  return forEachCodeBlock(transpileCodeBlock(options))
}

export default transpileCodeBlocks

transpiling an individual code block

To speed up transpilation, we’ll skip running Babel if the source file has not been modified since the corresponding transpiled file has been generated (similar to make).

// transpileCodeBlock.js
import path from 'path'
import fs from 'fs'
import { transformFileSync } from 'babel-core'

import saveToFile from './saveToFile'

export function transpileCodeBlock ({ babelConfig, targetDirectory } = { }) {
  return async function (codeBlock, filename) {
    const sourceFilePath = path.join('src', filename)
    const targetFilePath = path.join(targetDirectory, filename)
    if (await isAlreadyUpToDate(sourceFilePath, targetFilePath)) return
    const { code } = transformFileSync(sourceFilePath, babelConfig)
    await saveToFile(targetFilePath, code)
  }
}

async function isAlreadyUpToDate (sourceFilePath, targetFilePath) {
  if (!fs.existsSync(targetFilePath)) return false
  const sourceStats = fs.statSync(sourceFilePath)
  const targetStats = fs.statSync(targetFilePath)
  return targetStats.mtime > sourceStats.mtime
}

export default transpileCodeBlock

babel configuration

// getBabelConfig.js
export function getBabelConfig () {
  return {
    presets: [
      require('babel-preset-latest'),
      require('babel-preset-stage-2')
    ],
    plugins: [
      require('babel-plugin-transform-runtime')
    ]
  }
}

export default getBabelConfig

additional Babel options for testing

// getTestingBabelConfig.js
import getBabelConfig from './getBabelConfig'
import babelPluginIstanbul from 'babel-plugin-istanbul'
import babelPresetPowerAssert from 'babel-preset-power-assert'

export function getTestingBabelConfig () {
  const babelConfig = getBabelConfig()
  return {
    ...babelConfig,
    presets: [
      babelPresetPowerAssert,
      ...babelConfig.presets
    ],
    plugins: [
      babelPluginIstanbul,
      ...babelConfig.plugins
    ]
  }
}

export default getTestingBabelConfig

running unit tests

It’s quite hackish right now, but it works.

// runUnitTests.js
import fs from 'fs'
import saveToFile from './saveToFile'
import mapSourceCoverage from './mapSourceCoverage'

export async function runUnitTests (codeBlocks) {
  // Generate an entry file for mocha to use.
  const testEntryFilename = './lib-cov/_test-entry.js'
  const entry = generateEntryFile(codeBlocks)
  await saveToFile(testEntryFilename, entry)

  // Initialize mocha with the entry file.
  const Mocha = require('mocha')
  const mocha = new Mocha({ ui: 'bdd' })
  mocha.addFile(testEntryFilename)

  // Now go!!
  prepareTestEnvironment()
  await runMocha(mocha)
  await saveCoverageData(codeBlocks)
}

function runMocha (mocha) {
  return new Promise((resolve, reject) => {
    mocha.run(function (failures) {
      if (failures) {
        reject(new Error('There are ' + failures + ' test failure(s).'))
      } else {
        resolve()
      }
    })
  })
}

function generateEntryFile (codeBlocks) {
  const entry = [ '"use strict";' ]
  for (const filename of Object.keys(codeBlocks)) {
    if (filename.match(/\.test\.js$/)) {
      entry.push('describe(' + JSON.stringify(filename) + ', function () {')
      entry.push('  require(' + JSON.stringify('./' + filename) + ')')
      entry.push('})')
    }
  }
  return entry.join('\n')
}

function prepareTestEnvironment () {
  global.assert = require('power-assert')
}

async function saveCoverageData (codeBlocks) {
  const coverage = global['__coverage__']
  if (!coverage) return
  const istanbul = require('istanbul')
  const reporter = new istanbul.Reporter()
  const collector = new istanbul.Collector()
  const synchronously = true
  collector.add(mapSourceCoverage(coverage, {
    codeBlocks,
    sourceFilePath: fs.realpathSync('README.md'),
    targetDirectory: fs.realpathSync('src')
  }))
  reporter.add('lcov')
  reporter.add('text')
  reporter.write(collector, synchronously, () => { })
}

export default runUnitTests

running linter

// runLinter.js
import fs from 'fs'
import saveToFile from './saveToFile'
import padRight from 'lodash/padEnd'
import flatten from 'lodash/flatten'
import isEmpty from 'lodash/isEmpty'
import compact from 'lodash/compact'
import { CLIEngine } from 'eslint'
import Table from 'cli-table'
import moduleExists from 'module-exists'

export const runESLint = (contents, fix, eslintExtends) => {
  const cli = new CLIEngine({
    fix,
    globals: ['describe', 'it', 'should'],
    ...eslintExtends
  })
  const report = cli.executeOnText(contents)
  return report.results
}

export const formatLinterErrorsColumnMode = (errors, resetStyle = {}) => {
  if (isEmpty(errors)) return ''
  const table = new Table({ head: ['Where', 'Path', 'Rule', 'Message'], ...resetStyle })
  errors.map((error) => table.push([
    error.line + ':' + error.column,
    error.filename,
    error.ruleId,
    error.message
  ]))
  return table.toString()
}

const formatLinterSolution = (line, filename, output) => ({
  line,
  filename,
  fixedCode: output
})

const formatLinterError = (line, filename, error) => {
  error.line += line - 1
  error.filename = filename
  return error
}

export const isFix = (options) => !!options._ && options._[0] === 'fix'

export const generateCodeBlock = (code, filename) => {
  const END = '`' + '`' + '`'
  const BEGIN = END + 'js'
  return [
    BEGIN,
    '// ' + filename,
    code + END
  ].join('\n')
}

export const fixLinterErrors = async (errors, codeBlocks, targetPath = 'README.md') => {
  let readme = fs.readFileSync(targetPath, 'utf8')
  errors.map(({ filename, fixedCode }) => {
    const code = codeBlocks[filename].contents
    if (fixedCode) {
      readme = readme.split(generateCodeBlock(code, filename)).join(generateCodeBlock(fixedCode, filename))
    }
  })
  await saveToFile(targetPath, readme)
}

const mergeLinterResults = (prev, cur) => ({
  solutions: compact([...prev.solutions, ...cur.solutions]),
  remainingErrors: compact([...prev.remainingErrors, ...cur.remainingErrors])
})

const defaultLinterResults = { solutions: [], remainingErrors: [] }

export const mapLinterErrorsToLine = (results, line, filename) => (
  results.map(({ messages, output }) => ({
    solutions: [formatLinterSolution(line, filename, output)],
    remainingErrors: messages.map((error) => formatLinterError(line, filename, error))
  })).reduce(mergeLinterResults, defaultLinterResults)
)

export const getESLintExtends = (hasStandardPlugin) => (
  hasStandardPlugin
  ? { baseConfig: { extends: ['standard'] } }
  : {}
)

export async function runLinter (codeBlocks, options) {
  let linterResults = defaultLinterResults
  const fix = isFix(options)
  Object.keys(codeBlocks).map(filename => {
    const { contents, line } = codeBlocks[filename]
    const eslintExtends = getESLintExtends(moduleExists('eslint-plugin-standard'))
    const results = runESLint(contents, fix, eslintExtends)
    linterResults = mergeLinterResults(linterResults, mapLinterErrorsToLine(results, line, filename))
  })
  if (fix) await fixLinterErrors(linterResults.solutions, codeBlocks)
  console.error(formatLinterErrorsColumnMode(linterResults.remainingErrors))
}

export default runLinter

And its tests

// runLinter.test.js
import {
  runESLint,
  mapLinterErrorsToLine,
  formatLinterErrorsColumnMode,
  isFix,
  generateCodeBlock,
  getESLintExtends
} from './runLinter'

it('should map linter errors back to line in README.md', () => {
  const linterResults = [
    { messages: [{ message: 'message-1', line: 2 }], output: 'fixed-code' }
  ]
  const { solutions, remainingErrors } = mapLinterErrorsToLine(linterResults, 5, 'example.js')
  assert.deepEqual(remainingErrors, [{
    line: 6,
    filename: 'example.js',
    message: 'message-1'
  }])
  assert.deepEqual(solutions, [{
    line: 5,
    filename: 'example.js',
    fixedCode: 'fixed-code'
  }])
})

it('should format linter errors on a column mode', () => {
  const errors = [{
    line: 5,
    column: 10,
    message: 'message',
    filename: 'example.js',
    ruleId: 'ruleId-1'
  }]
  const resetStyle = { style: { head: [], border: [] } }
  const table = formatLinterErrorsColumnMode(errors, resetStyle)
  assert(table === [
    '┌───────┬────────────┬──────────┬─────────┐',
    '│ Where │ Path       │ Rule     │ Message │',
    '├───────┼────────────┼──────────┼─────────┤',
    '│ 5:10  │ example.js │ ruleId-1 │ message │',
    '└───────┴────────────┴──────────┴─────────┘'
  ].join('\n'))
  const tableNoErrors = formatLinterErrorsColumnMode([], resetStyle)
  assert(tableNoErrors === '')
})

it('should trigger fix mode when \'fix\' option is given', () => {
  assert(isFix({ _: ['fix'] }) === true)
  assert(isFix({}) === false)
})

it('should insert javascript code block', () => {
  assert(generateCodeBlock('const x = 5\n', 'example.js') === [
    '`' + '`' + '`js',
    '// example.js',
    'const x = 5',
    '`' + '`' + '`'
  ].join('\n'))
})

it('should get correct base config', () => {
  assert(getESLintExtends(false), {})
  assert(getESLintExtends(true).baseConfig.extends, 'standard')
})

the coverage magic

This module rewrites the coverage data so that you can view the coverage report for README.md. It’s quite complex and thus deserves its own section.

// mapSourceCoverage.js
import path from 'path'
import { forOwn } from 'lodash'

export function mapSourceCoverage (coverage, {
  codeBlocks,
  sourceFilePath,
  targetDirectory
}) {
  const result = { }
  const builder = createReadmeDataBuilder(sourceFilePath)
  for (const key of Object.keys(coverage)) {
    const entry = coverage[key]
    const relative = path.relative(targetDirectory, entry.path)
    if (codeBlocks[relative]) {
      builder.add(entry, codeBlocks[relative])
    } else {
      result[key] = entry
    }
  }
  if (!builder.isEmpty()) result[sourceFilePath] = builder.getOutput()
  return result
}

function createReadmeDataBuilder (path) {
  let nextId = 1
  let output = {
    path,
    s: { }, b: { }, f: { },
    statementMap: { }, branchMap: { }, fnMap: { }
  }
  let empty = true
  return {
    add (entry, codeBlock) {
      const id = nextId++
      const prefix = (key) => `${id}.${key}`
      const mapLine = (line) => codeBlock.line - 1 + line
      const map = mapLocation(mapLine)
      empty = false
      forOwn(entry.s, (count, key) => {
        output.s[prefix(key)] = count
      })
      forOwn(entry.statementMap, (loc, key) => {
        output.statementMap[prefix(key)] = map(loc)
      })
      forOwn(entry.b, (count, key) => {
        output.b[prefix(key)] = count
      })
      forOwn(entry.branchMap, (branch, key) => {
        output.branchMap[prefix(key)] = {
          ...branch,
          line: mapLine(branch.line),
          locations: branch.locations.map(map)
        }
      })
      forOwn(entry.f, (count, key) => {
        output.f[prefix(key)] = count
      })
      forOwn(entry.fnMap, (fn, key) => {
        output.fnMap[prefix(key)] = {
          ...fn,
          line: mapLine(fn.line),
          loc: map(fn.loc)
        }
      })
    },
    isEmpty: () => empty,
    getOutput: () => output
  }
}

function mapLocation (mapLine) {
  return ({ start = { }, end = { } }) => ({
    start: { line: mapLine(start.line), column: start.column },
    end: { line: mapLine(end.line), column: end.column }
  })
}

export default mapSourceCoverage

And its tests is quite… ugh!

// mapSourceCoverage.test.js
import mapSourceCoverage from './mapSourceCoverage'
import path from 'path'

const loc = (startLine, startColumn) => (endLine, endColumn) => ({
  start: { line: startLine, column: startColumn },
  end: { line: endLine, column: endColumn }
})
const coverage = {
  '/home/user/essay/src/hello.js': {
    path: '/home/user/essay/src/hello.js',
    s: { 1: 1 },
    b: { 1: [ 1, 2, 3 ] },
    f: { 1: 99, 2: 30 },
    statementMap: {
      1: loc(2, 15)(2, 30)
    },
    branchMap: {
      1: { line: 4, type: 'switch', locations: [
        loc(5, 10)(5, 20),
        loc(7, 10)(7, 25),
        loc(9, 10)(9, 30)
      ] }
    },
    fnMap: {
      1: { name: 'x', line: 10, loc: loc(10, 0)(10, 20) },
      2: { name: 'y', line: 20, loc: loc(20, 0)(20, 15) }
    }
  },
  '/home/user/essay/src/world.js': {
    path: '/home/user/essay/src/world.js',
    s: { 1: 1 },
    b: { },
    f: { },
    statementMap: { 1: loc(1, 0)(1, 30) },
    branchMap: { },
    fnMap: { }
  },
  '/home/user/essay/unrelated.js': {
    path: '/home/user/essay/unrelated.js',
    s: { 1: 1 },
    b: { },
    f: { },
    statementMap: { 1: loc(1, 0)(1, 30) },
    branchMap: { },
    fnMap: { }
  }
}
const codeBlocks = {
  'hello.js': { line: 72 },
  'world.js': { line: 99 }
}
const getMappedCoverage = () => mapSourceCoverage(coverage, {
  codeBlocks,
  sourceFilePath: '/home/user/essay/README.md',
  targetDirectory: '/home/user/essay/src'
})

it('should combine mappings from code blocks', () => {
  const mapped = getMappedCoverage()
  assert(Object.keys(mapped).length === 2)
})

const testReadmeCoverage = (f) => () => (
  f(getMappedCoverage()['/home/user/essay/README.md'])
)

it('should have statements', testReadmeCoverage(entry => {
  const keys = Object.keys(entry.s)
  assert(keys.length === 2)
  assert.deepEqual(keys, [ '1.1', '2.1' ])
}))
it('should have statementMap', testReadmeCoverage(({ statementMap }) => {
  assert(statementMap['1.1'].start.line === 73)
  assert(statementMap['1.1'].end.line === 73)
  assert(statementMap['2.1'].start.line === 99)
  assert(statementMap['2.1'].end.line === 99)
}))
it('should have branches', testReadmeCoverage(({ b }) => {
  assert(Array.isArray(b['1.1']))
}))
it('should have branchMap', testReadmeCoverage(({ branchMap }) => {
  assert(branchMap['1.1'].locations[2].start.line === 80)
  assert(branchMap['1.1'].line === 75)
}))
it('should have functions', testReadmeCoverage(({ f }) => {
  assert(f['1.1'] === 99)
  assert(f['1.2'] === 30)
}))
it('should have function map', testReadmeCoverage(({ fnMap }) => {
  assert(fnMap['1.1'].loc.start.line === 81)
  assert(fnMap['1.2'].line === 91)
}))

acceptance test

// acceptance.test.js
import mkdirp from 'mkdirp'
import fs from 'fs'
import * as buildCommand from './cli/buildCommand'
import * as testCommand from './cli/testCommand'
import * as lintCommand from './cli/lintCommand'

const assertProcessExitCode = (expectedCode) => (
  process.on('exit', (code) => {
    assert(code === expectedCode)
    delete process.exitCode
  })
)
it('works', async () => {
  const example = fs.readFileSync('example.md', 'utf8')
  const eslintrc = fs.readFileSync('.eslintrc', 'utf8')
  await runInTemporaryDir(async () => {
    fs.writeFileSync('README.md', example.replace('a + b', 'a+b'))
    fs.writeFileSync('.eslintrc', eslintrc)
    await buildCommand.handler({ })
    assert(fs.existsSync('src/add.js'))
    assert(fs.existsSync('lib/add.js'))
    await testCommand.handler({ })
    assert(fs.existsSync('coverage/lcov.info'))
    assert(fs.readFileSync('README.md', 'utf8') !== example)
    await lintCommand.handler({ _: ['fix'] })
    assert(fs.readFileSync('README.md', 'utf8') === example)
    assertProcessExitCode(1)
    lintCommand.allowToUseESLint(false)
  })
})

async function runInTemporaryDir (f) {
  const cwd = process.cwd()
  const testDirectory = '/tmp/essay-acceptance-test'
  mkdirp.sync(testDirectory)
  try {
    process.chdir(testDirectory)
    await f()
  } finally {
    process.chdir(cwd)
  }
}

miscellaneous

command line handler

// cli/index.js
import * as buildCommand from './buildCommand'
import * as testCommand from './testCommand'
import * as lintCommand from './lintCommand'

export function main () {
  // XXX: Work around yargs’ lack of default command support.
  const commands = [ buildCommand, testCommand, lintCommand ]
  const yargs = commands.reduce(appendCommandToYargs, require('yargs')).help()
  const registry = commands.reduce(registerCommandToRegistry, { })
  const argv = yargs.argv
  const command = argv._.shift() || 'build'
  const commandObject = registry[command]
  if (commandObject) {
    const subcommand = commandObject.builder(yargs.reset())
    Promise.resolve(commandObject.handler(argv)).catch(e => {
      setTimeout(() => { throw e })
    })
  } else {
    yargs.showHelp()
  }
}

function appendCommandToYargs (yargs, command) {
  return yargs.command(command.command, command.description)
}

function registerCommandToRegistry (registry, command) {
  return Object.assign(registry, {
    [command.command]: command
  })
}

saving file

This function wraps around the normal file system API, but provides this benefits:

  • It will first read the file, and if the content is identical, it will not re-write the file.

  • It displays log message on console.

// saveToFile.js
import fs from 'fs'
import path from 'path'
import mkdirp from 'mkdirp'

export async function saveToFile (filePath, contents) {
  mkdirp.sync(path.dirname(filePath))
  const exists = fs.existsSync(filePath)
  if (exists) {
    const existingData = fs.readFileSync(filePath, 'utf8')
    if (existingData === contents) return
  }
  console.log('%s %s…', exists ? 'Updating' : 'Writing', filePath)
  fs.writeFileSync(filePath, contents)
}

export default saveToFile

run a function for each code block

// forEachCodeBlock.js
export function forEachCodeBlock (fn) {
  return async function (codeBlocks) {
    const filenames = Object.keys(codeBlocks)
    for (const filename of filenames) {
      await fn(codeBlocks[filename], filename, codeBlocks)
    }
  }
}

export default forEachCodeBlock

About

📝 The real README driven development! Generate a JavaScript library out of an essay. Literate programming for the ES2015 era.

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published