neon 0.4.0

A safe abstraction layer for Node.js.
Documentation
import { readFile, writeFile, mkdirs, stat } from '../async/fs';
import { prompt } from 'inquirer';
import gitconfig from '../async/git-config';
import * as path from 'path';
import * as handlebars from 'handlebars';
import * as semver from 'semver';
import * as style from '../style';
import validateLicense = require('validate-npm-package-license');
import validateName = require('validate-npm-package-name');
import * as JSON from 'ts-typed-json';

const ROOT_DIR = path.resolve(__dirname, '..', '..');
const TEMPLATES_DIR = path.resolve(ROOT_DIR, 'templates');

const NEON_CLI_VERSION =
  JSON.asString(JSON.asObject(JSON.loadSync(path.resolve(ROOT_DIR, 'package.json'))).version);

async function compile(filename: string) {
  let source = await readFile(path.resolve(TEMPLATES_DIR, filename), {
    encoding: 'utf8'
  });
  return handlebars.compile(source, { noEscape: true });
}

const GITIGNORE_TEMPLATE = compile('.gitignore.hbs');
const CARGO_TEMPLATE     = compile('Cargo.toml.hbs');
const NPM_TEMPLATE       = compile('package.json.hbs');
const INDEXJS_TEMPLATE   = compile('index.js.hbs');
const LIBRS_TEMPLATE     = compile('lib.rs.hbs');
const README_TEMPLATE    = compile('README.md.hbs');
const BUILDRS_TEMPLATE   = compile('build.rs.hbs');

async function guessAuthor() {
  let author = {
    name: process.env.USER || process.env.USERNAME,
    email: undefined
  };
  try {
    let config = await gitconfig();
    if (config.user.name) {
      author.name = config.user.name;
    }
    if (config.user.email) {
      author.email = config.user.email;
    }
    return author;
  } catch (e) {
    return author;
  }
}

type NeonVersion = { type: "version" | "range" | "relative" | "absolute", value: string };

async function parseNeonVersion(flag: string | null) : Promise<NeonVersion> {
  if (!flag) {
    return { type: "version", value: NEON_CLI_VERSION };
  }

  if (semver.valid(flag)) {
    return { type: "version", value: flag };
  }

  if (semver.validRange(flag)) {
    return { type: "range", value: flag };
  }

  let stats = await stat(flag);

  if (!stats.isDirectory()) {
    throw new Error("Specified path to Neon is not a directory");
  }

  return { type: path.isAbsolute(flag) ? "absolute" : "relative", value: flag };
}

export default async function wizard(pwd: string, name: string, neon: string | null, features: string | null, noDefaultFeatures: boolean) {
  let its = validateName(name);
  if (!its.validForNewPackages) {
    let errors = (its.errors || []).concat(its.warnings || []);
    throw new Error("Sorry, " + errors.join(" and ") + ".");
  }

  // check for a scoped name
  let scoped = name.match(/@([^\/]+)\/(.*)/);
  let [, scope, local] = scoped ? (scoped as [string, string, string]) : [, null, name];

  console.log("This utility will walk you through creating the " + style.project(name) + " Neon project.");
  console.log("It only covers the most common items, and tries to guess sensible defaults.");
  console.log();
  console.log("Press ^C at any time to quit.");

  let root = path.resolve(pwd, local);
  let guess = await guessAuthor();

  let answers = await prompt([
    {
      type: 'input',
      name: 'version',
      message: "version",
      default: "0.1.0",
      validate: function (input) {
        if (semver.valid(input)) {
          return true;
        }
        return "Invalid version: " + input;
      }
    },
    { type: 'input', name: 'description', message: "description"                               },
    { type: 'input', name: 'node',        message: "node entry point", default: "lib/index.js" },
    { type: 'input', name: 'git',         message: "git repository"                            },
    { type: 'input', name: 'author',      message: "author",           default: guess.name     },
    { type: 'input', name: 'email',       message: "email",            default: guess.email    },
    {
      type: 'input',
      name: 'license',
      message: "license",
      default: "MIT",
      validate: function (input) {
        let its = validateLicense(input);
        if (its.validForNewPackages) {
          return true;
        }
        let errors = its.warnings || [];
        return "Sorry, " + errors.join(" and ") + ".";
      }
    }
  ]);

  answers.name = {
    npm: {
      full: name,
      scope: scope,
      local: local
    },
    cargo: {
      external: local,
      internal: local.replace(/-/g, "_")
    }
  };
  answers.description = escapeQuotes(answers.description);
  answers.git = encodeURI(answers.git);
  answers.author = escapeQuotes(answers.author);

  let neonVersion = await parseNeonVersion(neon);

  let simple = (neonVersion.type === 'version' || neonVersion.type === 'range')
    && !features
    && !noDefaultFeatures;

  let libs: {
    // In the common case, we can make the Cargo.toml manifest simple by just using
    // the semver specifier string for the `neon` and `neon-build` dependencies.
    simple: boolean,
    paths?: { neon: string, 'neon-build': string },
    version?: string,
    features?: Array<string>,
    noDefaultFeatures: boolean
  } = { simple, noDefaultFeatures };

  if (neonVersion.type === 'relative') {
    let neon = path.relative(path.join(name, 'native'), neonVersion.value);
    libs.paths = {
      neon: JSON.stringify(neon),
      'neon-build': JSON.stringify(path.join(neon, 'crates', 'neon-build'))
    };
  } else if (neonVersion.type === 'absolute') {
    libs.paths = {
      neon: JSON.stringify(neonVersion.value),
      'neon-build': JSON.stringify(path.resolve(neonVersion.value, 'crates', 'neon-build'))
    };
  } else {
    libs.version = JSON.stringify(neonVersion.value);
  }

  if (features) {
    libs.features = features.split(/\s+/).map(JSON.stringify);
  }

  let cli = JSON.stringify(neonVersion.type === 'version'
    ? "^" + neonVersion.value
    : neonVersion.type === 'relative'
    ? "file:" + path.join(path.relative(name, neonVersion.value), 'cli')
    : neonVersion.type === 'absolute'
    ? "file:" + path.resolve(neonVersion.value, 'cli')
    : neonVersion.value);

  let ctx = {
    project: answers,
    neon: { cli, libs }
  };

  let lib = path.resolve(root, path.dirname(answers.node));
  let native_ = path.resolve(root, 'native');
  let src = path.resolve(native_, 'src');

  await mkdirs(lib);
  await mkdirs(src);

  await writeFile(path.resolve(root,    '.gitignore'),   (await GITIGNORE_TEMPLATE)(ctx), { flag: 'wx' });
  await writeFile(path.resolve(root,    'package.json'), (await NPM_TEMPLATE)(ctx),       { flag: 'wx' });
  await writeFile(path.resolve(native_, 'Cargo.toml'),   (await CARGO_TEMPLATE)(ctx),     { flag: 'wx' });
  await writeFile(path.resolve(root,    'README.md'),    (await README_TEMPLATE)(ctx),    { flag: 'wx' });
  await writeFile(path.resolve(root,    answers.node),   (await INDEXJS_TEMPLATE)(ctx),   { flag: 'wx' });
  await writeFile(path.resolve(src,     'lib.rs'),       (await LIBRS_TEMPLATE)(ctx),     { flag: 'wx' });
  await writeFile(path.resolve(native_, 'build.rs'),     (await BUILDRS_TEMPLATE)(ctx),   { flag: 'wx' });

  let relativeRoot = path.relative(pwd, root);
  let relativeNode = path.relative(pwd, path.resolve(root, answers.node));
  let relativeRust = path.relative(pwd, path.resolve(src, 'lib.rs'));

  console.log();
  console.log("Woo-hoo! Your Neon project has been created in: " + style.path(relativeRoot));
  console.log();
  console.log("The main Node entry point is at: " + style.path(relativeNode));
  console.log("The main Rust entry point is at: " + style.path(relativeRust));
  console.log();
  console.log("To build your project, just run " + style.command("npm install") + " from within the " + style.path(relativeRoot) + " directory.");
  console.log("Then you can test it out with " + style.command("node -e 'require(\"./\")'") + ".");
  console.log();
  console.log("Happy hacking!");
};

function escapeQuotes(str: string): string {
  return str.replace(/"/g, '\\"');
}