1
0
mirror of synced 2026-03-28 00:11:22 +00:00

feat(CLI): create CLI, begin work on v2

This commit is contained in:
Jason Dreyzehner
2018-02-22 02:20:38 -05:00
parent a211a54c1f
commit 76336c88db
25 changed files with 11069 additions and 5189 deletions

View File

@@ -1,12 +0,0 @@
// Must first be built by browserify.
// https://github.com/rollup/rollup-plugin-commonjs/issues/105#issuecomment-281917166
import hash from 'hash.js'
/**
* Simulate the Node.js crypto.createHash function using hash.js' implementation.
* @internal
* @hidden (TypeDoc currently doesn't understand @internal)
*/
export function createHash (algorithm: 'sha256') {
return hash.sha256()
}

View File

@@ -1,3 +1,3 @@
export * from './lib/async'
export * from './lib/hash'
export * from './lib/number'
export * from './lib/async';
export * from './lib/hash';
export * from './lib/number';

View File

@@ -1,6 +1,6 @@
import { test } from 'ava'
import { asyncABC } from 'typescript-starter'
import { test } from 'ava';
import { asyncABC } from './async';
test('getABC', async t => {
t.deepEqual(await asyncABC(), ['a','b', 'c'])
})
t.deepEqual(await asyncABC(), ['a', 'b', 'c']);
});

View File

@@ -17,16 +17,16 @@
*
* @returns a Promise which should contain `['a','b','c']`
*/
export async function asyncABC () {
function somethingSlow (index: 0 | 1 | 2) {
let storage = 'abc'.charAt(index)
export async function asyncABC() {
function somethingSlow(index: 0 | 1 | 2) {
const storage = 'abc'.charAt(index);
return new Promise<string>(resolve => {
// here we pretend to wait on the network
setTimeout(() => resolve(storage), 0)
})
setTimeout(() => resolve(storage), 0);
});
}
let a = await somethingSlow(0)
let b = await somethingSlow(1)
let c = await somethingSlow(2)
return [a, b, c]
const a = await somethingSlow(0);
const b = await somethingSlow(1);
const c = await somethingSlow(2);
return [a, b, c];
}

View File

@@ -1,6 +1,17 @@
import { test } from 'ava'
import { sha256 } from 'typescript-starter'
import { Macro, test } from 'ava';
import { sha256, sha256Native } from './hash';
test('sha256', t => {
t.deepEqual(sha256('test'), '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08')
})
const hash: Macro = (t, input: string, expected: string) => {
t.is(sha256(input), expected);
t.is(sha256Native(input), expected);
};
hash.title = (providedTitle: string, input: string, expected: string) =>
`${providedTitle}: ${input} => ${expected}`;
test(
'sha256',
hash,
'test',
'9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'
);

View File

@@ -1,7 +1,8 @@
import { createHash } from 'crypto'
import { createHash } from 'crypto';
import shaJs from 'sha.js';
/**
* Calculate the sha256 digest of a string. On Node.js, this will use the native module, in the browser, it will fall back to a pure javascript implementation.
* Calculate the sha256 digest of a string.
*
* ### Example (es imports)
* ```js
@@ -12,6 +13,19 @@ import { createHash } from 'crypto'
*
* @returns sha256 message digest
*/
export function sha256 (message: string) {
return createHash('sha256').update(message).digest('hex')
export function sha256(message: string) {
return shaJs('sha256')
.update(message)
.digest('hex');
}
/**
* A faster implementation of [[sha256]] which requires the native Node.js module. Browser consumers should use [[sha256]], instead.
*
* @returns sha256 message digest
*/
export function sha256Native(message: string) {
return createHash('sha256')
.update(message)
.digest('hex');
}

View File

@@ -1,10 +1,10 @@
import { test } from 'ava'
import { double, power } from 'typescript-starter'
import { test } from 'ava';
import { double, power } from './number';
test('double', t => {
t.deepEqual(double(2), 4)
})
t.deepEqual(double(2), 4);
});
test('power', t => {
t.deepEqual(power(2,4), 16)
})
t.deepEqual(power(2, 4), 16);
});

View File

@@ -19,8 +19,8 @@
* @returns Comment describing the return type.
* @anotherNote Some other value.
*/
export function double (value: number) {
return value * 2
export function double(value: number) {
return value * 2;
}
/**
@@ -40,7 +40,7 @@ export function double (value: number) {
* // => 8
* ```
*/
export function power (base: number, exponent: number) {
export function power(base: number, exponent: number) {
// This is a proposed es7 operator, which should be transpiled by Typescript
return base ** exponent
return base ** exponent;
}

402
src/typescript-starter.ts Normal file
View File

@@ -0,0 +1,402 @@
#!/usr/bin/env node
// tslint:disable:no-console
import chalk from 'chalk';
import spawn from 'cross-spawn';
import { existsSync, readFileSync, renameSync, writeFileSync } from 'fs';
import githubUsername from 'github-username';
import gradient from 'gradient-string';
import { prompt } from 'inquirer';
import ora from 'ora';
import { join } from 'path';
import replace from 'replace-in-file';
import sortedObject from 'sorted-object';
import trash from 'trash';
enum ProjectType {
Node,
Library
}
enum Runner {
Npm,
Yarn
}
enum TypeDefinitions {
none,
Node,
DOM,
NodeAndDOM
}
const ascii = `
_ _ _ _ _
| |_ _ _ _ __ ___ ___ ___ _ __(_)_ __ | |_ ___| |_ __ _ _ __| |_ ___ _ __
| __| | | | '_ \\ / _ \\/ __|/ __| '__| | '_ \\| __|____/ __| __/ _\` | '__| __/ _ \\ '__|
| |_| |_| | |_) | __/\\__ \\ (__| | | | |_) | ||_____\\__ \\ || (_| | | | || __/ |
\\__|\\__, | .__/ \\___||___/\\___|_| |_| .__/ \\__| |___/\\__\\__,_|_| \\__\\___|_|
|___/|_| |_|
`;
const repo =
process.env.TYPESCRIPT_STARTER_REPO_URL ||
'https://github.com/bitjson/typescript-starter.git';
(async () => {
if (process.argv.some(a => a === '-v' || a === '--version')) {
console.log(
JSON.parse(readFileSync(`${__dirname}/../../package.json`, 'utf8'))
.version
);
process.exit(0);
}
if (process.stdout.columns && process.stdout.columns >= 85) {
console.log(chalk.bold(gradient.mind(ascii)));
} else {
console.log(`\n${chalk.cyan.bold.underline('typescript-starter')}\n`);
}
const { definitions, description, name, runner } = await collectOptions();
const commitHash = await cloneRepo(name);
const nodeDefinitions =
definitions === TypeDefinitions.Node ||
definitions === TypeDefinitions.NodeAndDOM
? true
: false;
const domDefinitions =
definitions === TypeDefinitions.DOM ||
definitions === TypeDefinitions.NodeAndDOM
? true
: false;
console.log(`${chalk.dim(`Cloned at commit:${commitHash}`)}\n`);
const { gitName, gitEmail } = await getUserInfo();
const username = await githubUsername(gitEmail).catch(err => {
// if username isn't found, just return a placeholder
return 'YOUR_USER_NAME';
});
const spinner1 = ora('Updating package.json').start();
const projectPath = join(process.cwd(), name);
const pkgPath = join(projectPath, 'package.json');
const pkg = readPackageJson(pkgPath);
pkg.name = name;
pkg.version = '1.0.0';
pkg.description = description;
delete pkg.bin;
pkg.repository = `https://github.com/${username}/${name}`;
pkg.keywords = [];
// dependencies to retain for Node.js applications
const nodeKeptDeps = ['sha.js'];
pkg.dependencies = nodeDefinitions
? nodeKeptDeps.reduce((all, dep) => {
all[dep] = pkg.dependencies[dep];
return all;
}, {})
: {};
if (runner === Runner.Yarn) {
pkg.scripts.preinstall = `node -e \"if(process.env.npm_execpath.indexOf('yarn') === -1) throw new Error('${name} must be installed with Yarn: https://yarnpkg.com/')\"`;
}
writePackageJson(pkgPath, pkg);
spinner1.succeed();
const spinner2 = ora('Updating .gitignore').start();
if (runner === Runner.Yarn) {
await replace({
files: join(projectPath, '.gitignore'),
from: 'yarn.lock',
to: 'package-lock.json'
});
}
spinner2.succeed();
const spinner3 = ora('Updating .npmignore').start();
await replace({
files: join(projectPath, '.npmignore'),
from: 'examples\n',
to: ''
});
spinner3.succeed();
const spinner4 = ora('Updating LICENSE').start();
await replace({
files: join(projectPath, 'LICENSE'),
from: 'Jason Dreyzehner',
to: gitName
});
spinner4.succeed();
const spinner5 = ora('Deleting unnecessary files').start();
await trash([
join(projectPath, 'examples'),
join(projectPath, 'CHANGELOG.md'),
join(projectPath, 'README.md'),
join(projectPath, 'package-lock.json'),
join(projectPath, 'src', 'typescript-starter.ts')
]);
spinner5.succeed();
const spinner6 = ora('Updating README.md').start();
renameSync(
join(projectPath, 'README-starter.md'),
join(projectPath, 'README.md')
);
await replace({
files: join(projectPath, 'README.md'),
from: 'package-name',
to: name
});
spinner6.succeed();
if (!domDefinitions) {
const spinner6A = ora(`tsconfig: don't include "dom" lib`).start();
await replace({
files: join(projectPath, 'tsconfig.json'),
from: '"lib": ["es2017", "dom"]',
to: '"lib": ["es2017"]'
});
spinner6A.succeed();
}
if (!nodeDefinitions) {
const spinner6B = ora(`tsconfig: don't include "node" types`).start();
await replace({
files: join(projectPath, 'tsconfig.json'),
from: '"types": ["node"]',
to: '"types": []'
});
await replace({
files: join(projectPath, 'src', 'index.ts'),
from: `export * from './lib/hash';\n`,
to: ''
});
await trash([
join(projectPath, 'src', 'lib', 'hash.ts'),
join(projectPath, 'src', 'lib', 'hash.spec.ts')
join(projectPath, 'src', 'lib', 'async.ts'),
join(projectPath, 'src', 'lib', 'async.spec.ts')
]);
spinner6B.succeed();
}
console.log(`\n${chalk.green.bold('Installing dependencies...')}\n`);
await install(runner, projectPath);
console.log();
const spinner7 = ora(`Initializing git repository`).start();
await initialCommit(commitHash, projectPath);
spinner7.succeed();
console.log(`\n${chalk.blue.bold(`Created ${name} 🎉`)}\n`);
// TODO:
// readme: add how to work on this file
// `npm link`, `npm run watch`, and in a test directory `TYPESCRIPT_STARTER_REPO_URL='/local/path/to/typescript-starter' typescript-starter`
})();
async function collectOptions() {
const packageName = {
filter: (answer: string) => answer.trim(),
message: 'Enter the new package name:',
name: 'name',
type: 'input',
validate: (answer: string) =>
!/^\s*[a-zA-Z]+(-[a-zA-Z]+)*\s*$/.test(answer)
? 'Name should be in-kebab-case'
: existsSync(answer)
? `The ${answer} path already exists in this directory.`
: true
};
const node = 'Node.js application';
const lib = 'Javascript library';
const projectType = {
choices: [node, lib],
filter: val => (val === node ? ProjectType.Node : ProjectType.Library),
message: 'What are you making?',
name: 'type',
type: 'list'
};
const packageDescription = {
filter: answer => answer.trim(),
message: 'Enter the package description:',
name: 'description',
type: 'input',
validate: (answer: string) => answer.length > 0
};
const runnerChoices = ['npm', 'yarn'];
const runner = {
choices: runnerChoices,
filter: val => runnerChoices.indexOf(val),
message: 'Will this project use npm or yarn?',
name: 'runner',
type: 'list'
};
const typeDefChoices = [
`None — the library won't use any globals or modules from Node.js or the DOM`,
`Node.js — parts of the library require access to Node.js globals or built-in modules`,
`DOM — parts of the library require access to the Document Object Model (DOM)`,
`Both Node.js and DOM — some parts of the library require Node.js, other parts require DOM access`
];
const typeDefs = {
choices: typeDefChoices,
filter: val => typeDefChoices.indexOf(val),
message: 'Which global type definitions do you want to include?',
name: 'definitions',
type: 'list',
when: answers => answers.type === ProjectType.Library
};
return (prompt([
packageName,
projectType,
packageDescription,
runner,
typeDefs
]) as Promise<{
name: string;
type: ProjectType;
description: string;
runner: Runner;
definitions?: TypeDefinitions;
}>).then(answers => {
return {
definitions:
answers.definitions === undefined
? TypeDefinitions.Node
: answers.definitions,
description: answers.description,
name: answers.name,
runner: answers.runner,
type: answers.type
};
});
}
async function cloneRepo(dir: string) {
console.log();
const cwd = process.cwd();
const projectDir = join(cwd, dir);
const gitHistoryDir = join(projectDir, '.git');
const clone = spawn.sync('git', ['clone', '--depth=1', repo, dir], {
cwd,
stdio: 'inherit'
});
if (clone.error && clone.error.code === 'ENOENT') {
console.error(
chalk.red(
`\nGit is not installed on your PATH. Please install Git and try again.`
)
);
console.log(
chalk.dim(
`\nFor more information, visit: ${chalk.bold.underline(
'https://git-scm.com/book/en/v2/Getting-Started-Installing-Git'
)}\n`
)
);
process.exit(1);
} else if (clone.status !== 0) {
abort(chalk.red(`Git clone failed. Correct the issue and try again.`));
}
console.log();
const revParse = spawn.sync('git', ['rev-parse', 'HEAD'], {
cwd: projectDir,
encoding: 'utf8',
stdio: ['pipe', 'pipe', process.stderr]
});
if (revParse.status !== 0) {
abort(chalk.red(`Git rev-parse failed.`));
}
const commitHash = revParse.stdout.trim();
await trash([gitHistoryDir]);
return commitHash;
}
async function getUserInfo() {
const gitNameProc = spawn.sync('git', ['config', 'user.name'], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', process.stderr]
});
if (gitNameProc.status !== 0) {
abort(chalk.red(`Couldn't get name from Git config.`));
}
const gitName = gitNameProc.stdout.trim();
const gitEmailProc = spawn.sync('git', ['config', 'user.email'], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', process.stderr]
});
if (gitEmailProc.status !== 0) {
abort(chalk.red(`Couldn't get email from Git config.`));
}
const gitEmail = gitEmailProc.stdout.trim();
return {
gitEmail,
gitName
};
}
function readPackageJson(path: string) {
return JSON.parse(readFileSync(path, 'utf8'));
}
function writePackageJson(path: string, pkg: any) {
// write using the same format as npm:
// https://github.com/npm/npm/blob/latest/lib/install/update-package-json.js#L48
const stringified = JSON.stringify(pkg, null, 2) + '\n';
writeFileSync(path, stringified);
}
async function install(runner: Runner, projectDir: string) {
const opts = {
cwd: projectDir,
encoding: 'utf8',
stdio: ['inherit', 'inherit', process.stderr]
};
const runnerProc =
runner === Runner.Npm
? spawn.sync('npm', ['install'], opts)
: spawn.sync('yarn', opts);
if (runnerProc.status !== 0) {
abort(chalk.red(`Installation failed. You'll need to install manually.`));
}
}
async function initialCommit(hash: string, projectDir: string) {
const opts = {
cwd: projectDir,
encoding: 'utf8',
stdio: ['ignore', 'ignore', process.stderr]
};
const init = spawn.sync('git', ['init'], opts);
if (init.status !== 0) {
abort(chalk.red(`Git repo initialization failed.`));
}
const add = spawn.sync('git', ['add', '-A'], opts);
if (add.status !== 0) {
abort(chalk.red(`Could not stage initial commit.`));
}
const commit = spawn.sync(
'git',
[
'commit',
'-m',
`Initial commit\n\nCreated with typescript-starter@${hash}`
],
opts
);
if (commit.status !== 0) {
abort(chalk.red(`Initial commit failed.`));
}
}
function abort(msg: string) {
console.error(`\n${msg}\n`);
process.exit(1);
}