1
0
mirror of synced 2025-11-08 21:07:23 +00:00

feat: TS v4, tslint -> eslint, add cspell support (#240)

Update all dependencies, migrate from tslint (deprecated) to typescript-eslint, and add support for
cspell.

BREAKING CHANGE: migrated from tslint (deprecated) to eslint.
This commit is contained in:
Jason Dreyzehner
2020-09-01 19:13:25 -04:00
committed by GitHub
parent ab50e80ec8
commit 390041b510
31 changed files with 6526 additions and 4298 deletions

View File

@@ -1,7 +1,6 @@
// tslint:disable:no-console no-if-statement no-expression-statement
import meow from 'meow';
import { Package, UpdateInfo, UpdateNotifier } from 'update-notifier';
import { Package, UpdateNotifier } from 'update-notifier';
import { Runner, TypescriptStarterArgsOptions, validateName } from './utils';
export async function checkArgs(): Promise<TypescriptStarterArgsOptions> {
@@ -23,8 +22,9 @@ export async function checkArgs(): Promise<TypescriptStarterArgsOptions> {
--yarn use yarn (default: npm)
--no-circleci don't include CircleCI
--no-cspell don't include cspell
--no-editorconfig don't include .editorconfig
--no-immutable don't enable tslint-immutable
--no-functional don't enable eslint-plugin-functional
--no-install skip yarn/npm install
--no-vscode don't include VS Code debugging config
@@ -35,103 +35,107 @@ export async function checkArgs(): Promise<TypescriptStarterArgsOptions> {
flags: {
appveyor: {
default: false,
type: 'boolean'
type: 'boolean',
},
circleci: {
default: true,
type: 'boolean'
type: 'boolean',
},
cspell: {
default: true,
type: 'boolean',
},
description: {
alias: 'd',
default: 'a typescript-starter project',
type: 'string'
type: 'string',
},
dom: {
default: false,
type: 'boolean'
type: 'boolean',
},
editorconfig: {
default: true,
type: 'boolean'
type: 'boolean',
},
immutable: {
functional: {
default: true,
type: 'boolean'
type: 'boolean',
},
install: {
default: true,
type: 'boolean'
type: 'boolean',
},
node: {
default: false,
type: 'boolean'
type: 'boolean',
},
strict: {
default: false,
type: 'boolean'
type: 'boolean',
},
travis: {
default: false,
type: 'boolean'
type: 'boolean',
},
vscode: {
default: true,
type: 'boolean'
type: 'boolean',
},
yarn: {
default: false,
type: 'boolean'
}
}
type: 'boolean',
},
},
}
);
// immediately check for updates every time we run typescript-starter
const updateInfo = await new Promise<UpdateInfo>((resolve, reject) => {
const notifier = new UpdateNotifier({
callback: (error, update) => {
error ? reject(error) : resolve(update);
},
pkg: cli.pkg as Package
});
notifier.check();
});
if (updateInfo.type !== 'latest') {
const info = await new UpdateNotifier({
pkg: cli.pkg as Package,
}).fetchInfo();
if (info.type !== 'latest') {
// eslint-disable-next-line functional/no-throw-statement
throw new Error(`
Your version of typescript-starter is outdated.
Consider using 'npx typescript-starter' to always get the latest version.
`);
Your version of typescript-starter is outdated.
Consider using 'npx typescript-starter' to always get the latest version.
`);
}
const version = cli.pkg.version as string;
const input = cli.input[0];
if (!input) {
// no project-name provided, return to collect options in interactive mode
// note: we always return `install`, so --no-install always works
// (important for test performance)
/**
* No project-name provided, return to collect options in interactive mode.
* Note: we always return `install`, so --no-install always works
* (important for test performance).
*/
return {
install: cli.flags.install,
starterVersion: cli.pkg.version
starterVersion: version,
};
}
const validOrMsg = await validateName(input);
const validOrMsg = validateName(input);
if (typeof validOrMsg === 'string') {
// eslint-disable-next-line functional/no-throw-statement
throw new Error(validOrMsg);
}
return {
appveyor: cli.flags.appveyor,
circleci: cli.flags.circleci,
cspell: cli.flags.cspell,
description: cli.flags.description,
domDefinitions: cli.flags.dom,
editorconfig: cli.flags.editorconfig,
immutable: cli.flags.immutable,
functional: cli.flags.functional,
install: cli.flags.install,
nodeDefinitions: cli.flags.node,
projectName: input,
runner: cli.flags.yarn ? Runner.Yarn : Runner.Npm,
starterVersion: cli.pkg.version,
starterVersion: version,
strict: cli.flags.strict,
travis: cli.flags.travis,
vscode: cli.flags.vscode
vscode: cli.flags.vscode,
};
}

View File

@@ -1,5 +1,5 @@
// tslint:disable:no-expression-statement no-console
import chalk from 'chalk';
import { checkArgs } from './args';
import { inquire } from './inquire';
import { addInferredOptions, LiveTasks } from './tasks';
@@ -15,7 +15,7 @@ import { getIntro, hasCLIOptions, TypescriptStarterUserOptions } from './utils';
console.log(getIntro(process.stdout.columns));
return inquire();
})()),
...argInfo
...argInfo,
};
const options = await addInferredOptions(userOptions);
return typescriptStarter(options, LiveTasks);

View File

@@ -1,4 +1,5 @@
import { DistinctQuestion, prompt } from 'inquirer';
import { Runner, TypescriptStarterCLIOptions, validateName } from './utils';
export async function inquire(): Promise<TypescriptStarterCLIOptions> {
@@ -7,22 +8,21 @@ export async function inquire(): Promise<TypescriptStarterCLIOptions> {
message: '📦 Enter the new package name:',
name: 'projectName',
type: 'input',
validate: validateName
validate: validateName,
};
enum ProjectType {
Node = 'node',
Library = 'lib'
Library = 'lib',
}
const projectTypeQuestion: DistinctQuestion = {
// tslint:disable-next-line:readonly-array
choices: [
{ name: 'Node.js application', value: ProjectType.Node },
{ name: 'Javascript library', value: ProjectType.Library }
{ name: 'Javascript library', value: ProjectType.Library },
],
message: '🔨 What are you making?',
name: 'type',
type: 'list'
type: 'list',
};
const packageDescriptionQuestion: DistinctQuestion = {
@@ -30,103 +30,106 @@ export async function inquire(): Promise<TypescriptStarterCLIOptions> {
message: '💬 Enter the package description:',
name: 'description',
type: 'input',
validate: (answer: string) => answer.length > 0
validate: (answer: string) => answer.length > 0,
};
const runnerQuestion: DistinctQuestion = {
// tslint:disable-next-line:readonly-array
choices: [
{ name: 'npm', value: Runner.Npm },
{ name: 'yarn', value: Runner.Yarn }
{ name: 'yarn', value: Runner.Yarn },
],
message: '🚄 Will this project use npm or yarn?',
name: 'runner',
type: 'list'
type: 'list',
};
enum TypeDefinitions {
none = 'none',
node = 'node',
dom = 'dom',
nodeAndDom = 'both'
nodeAndDom = 'both',
}
const typeDefsQuestion: DistinctQuestion = {
// tslint:disable-next-line:readonly-array
choices: [
{
name: `None — the library won't use any globals or modules from Node.js or the DOM`,
value: TypeDefinitions.none
value: TypeDefinitions.none,
},
{
name: `Node.js — parts of the library require access to Node.js globals or built-in modules`,
value: TypeDefinitions.node
value: TypeDefinitions.node,
},
{
name: `DOM — parts of the library require access to the Document Object Model (DOM)`,
value: TypeDefinitions.dom
value: TypeDefinitions.dom,
},
{
name: `Both Node.js and DOM — some parts of the library require Node.js, other parts require DOM access`,
value: TypeDefinitions.nodeAndDom
}
value: TypeDefinitions.nodeAndDom,
},
],
message: '📚 Which global type definitions do you want to include?',
name: 'definitions',
type: 'list',
when: (answers: any) => answers.type === ProjectType.Library
when: (answers) => answers.type === ProjectType.Library,
};
enum Extras {
appveyor = 'appveyor',
circleci = 'circleci',
cspell = 'cspell',
editorconfig = 'editorconfig',
immutable = 'immutable',
functional = 'functional',
strict = 'strict',
travis = 'travis',
vscode = 'vscode'
vscode = 'vscode',
}
const extrasQuestion: DistinctQuestion = {
// tslint:disable-next-line:readonly-array
choices: [
{
name: 'Enable stricter type-checking',
value: Extras.strict
value: Extras.strict,
},
{
checked: true,
name: 'Enable tslint-immutable',
value: Extras.immutable
name: 'Enable eslint-plugin-functional',
value: Extras.functional,
},
{
checked: true,
name: 'Include .editorconfig',
value: Extras.editorconfig
value: Extras.editorconfig,
},
{
checked: true,
name: 'Include cspell',
value: Extras.cspell,
},
{
checked: true,
name: 'Include VS Code debugging config',
value: Extras.vscode
value: Extras.vscode,
},
{
checked: true,
name: 'Include CircleCI config',
value: Extras.circleci
value: Extras.circleci,
},
{
checked: false,
name: 'Include Appveyor (Windows-based CI) config',
value: Extras.appveyor
value: Extras.appveyor,
},
{
checked: false,
name: 'Include Travis CI config',
value: Extras.travis
}
value: Extras.travis,
},
],
message: '🚀 More fun stuff:',
name: 'extras',
type: 'checkbox'
type: 'checkbox',
};
return prompt([
@@ -135,15 +138,15 @@ export async function inquire(): Promise<TypescriptStarterCLIOptions> {
packageDescriptionQuestion,
runnerQuestion,
typeDefsQuestion,
extrasQuestion
]).then(answers => {
extrasQuestion,
]).then((answers) => {
const {
definitions,
description,
extras,
projectName,
runner,
type
type,
} = answers as {
readonly definitions?: TypeDefinitions;
readonly description: string;
@@ -155,6 +158,7 @@ export async function inquire(): Promise<TypescriptStarterCLIOptions> {
return {
appveyor: extras.includes(Extras.appveyor),
circleci: extras.includes(Extras.circleci),
cspell: extras.includes(Extras.cspell),
description,
domDefinitions: definitions
? [TypeDefinitions.dom, TypeDefinitions.nodeAndDom].includes(
@@ -162,7 +166,7 @@ export async function inquire(): Promise<TypescriptStarterCLIOptions> {
)
: false,
editorconfig: extras.includes(Extras.editorconfig),
immutable: extras.includes(Extras.immutable),
functional: extras.includes(Extras.functional),
install: true,
nodeDefinitions: definitions
? [TypeDefinitions.node, TypeDefinitions.nodeAndDom].includes(
@@ -173,7 +177,7 @@ export async function inquire(): Promise<TypescriptStarterCLIOptions> {
runner,
strict: extras.includes(Extras.strict),
travis: extras.includes(Extras.travis),
vscode: extras.includes(Extras.vscode)
vscode: extras.includes(Extras.vscode),
};
});
}

View File

@@ -1,18 +1,19 @@
// tslint:disable:no-console no-if-statement no-expression-statement
import { join } from 'path';
import execa, { Options } from 'execa';
import githubUsername from 'github-username';
import { join } from 'path';
import {
Runner,
TypescriptStarterInferredOptions,
TypescriptStarterOptions,
TypescriptStarterUserOptions
TypescriptStarterUserOptions,
} from './utils';
export enum Placeholders {
email = 'YOUR_EMAIL',
name = 'YOUR_NAME',
username = 'YOUR_GITHUB_USER_NAME'
username = 'YOUR_GITHUB_USER_NAME',
}
// We implement these as function factories to make unit testing easier.
@@ -38,21 +39,23 @@ export const cloneRepo = (
'--depth=1',
`--branch=${repoInfo.branch}`,
repoInfo.repo,
dir
dir,
];
try {
await spawner('git', args, {
cwd: workingDirectory,
stdio: suppressOutput ? 'pipe' : 'inherit'
stdio: suppressOutput ? 'pipe' : 'inherit',
});
} catch (err) {
if (err.exitCodeName === 'ENOENT') {
// eslint-disable-next-line functional/no-throw-statement
throw new Error(`
Git is not installed on your PATH. Please install Git and try again.
For more information, visit: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git
`);
} else {
// eslint-disable-next-line functional/no-throw-statement
throw new Error(`Git clone failed.`);
}
}
@@ -60,19 +63,20 @@ export const cloneRepo = (
const revParseResult = await spawner('git', ['rev-parse', 'HEAD'], {
cwd: projectDir,
encoding: 'utf8',
// tslint:disable-next-line:readonly-array
stdio: ['pipe', 'pipe', 'inherit']
stdio: ['pipe', 'pipe', 'inherit'],
});
const commitHash = revParseResult.stdout;
return { commitHash, gitHistoryDir };
} catch (err) {
// eslint-disable-next-line functional/no-throw-statement
throw new Error(`Git rev-parse failed.`);
}
};
export const getGithubUsername = (fetcher: any) => async (
email: string | undefined
): Promise<string> => {
export const getGithubUsername = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fetcher: any
) => async (email: string | undefined): Promise<string> => {
if (email === Placeholders.email) {
return Placeholders.username;
}
@@ -84,20 +88,19 @@ export const getGithubUsername = (fetcher: any) => async (
export const getUserInfo = (spawner: typeof execa) => async () => {
const opts: Options = {
encoding: 'utf8',
// tslint:disable-next-line:readonly-array
stdio: ['pipe', 'pipe', 'inherit']
stdio: ['pipe', 'pipe', 'inherit'],
};
try {
const nameResult = await spawner('git', ['config', 'user.name'], opts);
const emailResult = await spawner('git', ['config', 'user.email'], opts);
return {
gitEmail: emailResult.stdout,
gitName: nameResult.stdout
gitName: nameResult.stdout,
};
} catch (err) {
return {
gitEmail: Placeholders.email,
gitName: Placeholders.name
gitName: Placeholders.name,
};
}
};
@@ -109,7 +112,7 @@ export const initialCommit = (spawner: typeof execa) => async (
const opts: Options = {
cwd: projectDir,
encoding: 'utf8',
stdio: 'pipe'
stdio: 'pipe',
};
await spawner('git', ['init'], opts);
await spawner('git', ['add', '-A'], opts);
@@ -118,7 +121,7 @@ export const initialCommit = (spawner: typeof execa) => async (
[
'commit',
'-m',
`Initial commit\n\nCreated with bitjson/typescript-starter@${hash}`
`Initial commit\n\nCreated with bitjson/typescript-starter@${hash}`,
],
opts
);
@@ -131,13 +134,14 @@ export const install = (spawner: typeof execa) => async (
const opts: Options = {
cwd: projectDir,
encoding: 'utf8',
stdio: 'inherit'
stdio: 'inherit',
};
try {
runner === Runner.Npm
? spawner('npm', ['install'], opts)
: spawner('yarn', opts);
} catch (err) {
// eslint-disable-next-line functional/no-throw-statement
throw new Error(`Installation failed. You'll need to install manually.`);
}
};
@@ -155,15 +159,15 @@ export const getRepoInfo = (starterVersion: string) => {
branch: process.env.TYPESCRIPT_STARTER_REPO_BRANCH
? process.env.TYPESCRIPT_STARTER_REPO_BRANCH
: 'master',
repo: process.env.TYPESCRIPT_STARTER_REPO_URL
repo: process.env.TYPESCRIPT_STARTER_REPO_URL,
}
: {
branch: `v${starterVersion}`,
repo: 'https://github.com/bitjson/typescript-starter.git'
repo: 'https://github.com/bitjson/typescript-starter.git',
};
};
export interface Tasks {
export type Tasks = {
readonly cloneRepo: (
repoInfo: {
readonly branch: string;
@@ -178,12 +182,12 @@ export interface Tasks {
name: string
) => Promise<void>;
readonly install: (runner: Runner, projectDir: string) => Promise<void>;
}
};
export const LiveTasks: Tasks = {
cloneRepo: cloneRepo(execa),
initialCommit: initialCommit(execa),
install: install(execa)
install: install(execa),
};
export const addInferredOptions = async (
userOptions: TypescriptStarterUserOptions
@@ -195,22 +199,23 @@ export const addInferredOptions = async (
fullName: gitName,
githubUsername: username,
repoInfo: getRepoInfo(userOptions.starterVersion),
workingDirectory: process.cwd()
workingDirectory: process.cwd(),
};
return {
...inferredOptions,
appveyor: userOptions.appveyor,
circleci: userOptions.circleci,
cspell: userOptions.cspell,
description: userOptions.description,
domDefinitions: userOptions.domDefinitions,
editorconfig: userOptions.editorconfig,
immutable: userOptions.immutable,
functional: userOptions.functional,
install: userOptions.install,
nodeDefinitions: userOptions.nodeDefinitions,
projectName: userOptions.projectName,
runner: userOptions.runner,
strict: userOptions.strict,
travis: userOptions.travis,
vscode: userOptions.vscode
vscode: userOptions.vscode,
};
};

View File

@@ -13,14 +13,15 @@
* `diff build/test-one/package.json build/test-two/package.json`
*/
// tslint:disable:no-expression-statement
import { join, relative } from 'path';
import test, { ExecutionContext } from 'ava';
import del from 'del';
import execa from 'execa';
import globby from 'globby';
import md5File from 'md5-file';
import meow from 'meow';
import { join, relative } from 'path';
import { cloneRepo, Placeholders, Tasks } from '../tasks';
import { typescriptStarter } from '../typescript-starter';
import { normalizePath, Runner } from '../utils';
@@ -37,17 +38,17 @@ const branch = execa.sync('git', [
'rev-parse',
'--symbolic-full-name',
'--abbrev-ref',
'HEAD'
'HEAD',
]).stdout;
const repoInfo = {
// if local repo is in a detached HEAD state, providing --branch to `git clone` will fail.
branch: branch === 'HEAD' ? '.' : branch,
repo: process.cwd()
repo: process.cwd(),
};
const buildDir = join(process.cwd(), 'build');
const env = {
TYPESCRIPT_STARTER_REPO_BRANCH: repoInfo.branch,
TYPESCRIPT_STARTER_REPO_URL: repoInfo.repo
TYPESCRIPT_STARTER_REPO_URL: repoInfo.repo,
};
enum TestDirectories {
@@ -56,7 +57,7 @@ enum TestDirectories {
three = 'test-3',
four = 'test-4',
five = 'test-5',
six = 'test-6'
six = 'test-6',
}
// If the tests all pass, the TestDirectories will automatically be cleaned up.
@@ -67,23 +68,23 @@ test.after(async () => {
`./build/${TestDirectories.three}`,
`./build/${TestDirectories.four}`,
`./build/${TestDirectories.five}`,
`./build/${TestDirectories.six}`
`./build/${TestDirectories.six}`,
]);
});
test('returns version', async t => {
test('returns version', async (t) => {
const expected = meow('').pkg.version;
t.truthy(typeof expected === 'string');
const { stdout } = await execa(`./bin/typescript-starter`, ['--version']);
t.is(stdout, expected);
});
test('returns help/usage', async t => {
test('returns help/usage', async (t) => {
const { stdout } = await execa(`./bin/typescript-starter`, ['--help']);
t.regex(stdout, /Usage/);
});
test('errors if project name collides with an existing path', async t => {
test('errors if project name collides with an existing path', async (t) => {
const existingDir = 'build';
const error = await t.throwsAsync<execa.ExecaError>(
execa(`./bin/typescript-starter`, [existingDir])
@@ -91,7 +92,7 @@ test('errors if project name collides with an existing path', async t => {
t.regex(error.stderr, /"build" path already exists/);
});
test('errors if project name is not in kebab-case', async t => {
test('errors if project name is not in kebab-case', async (t) => {
const error = await t.throwsAsync<execa.ExecaError>(
execa(`./bin/typescript-starter`, ['name with spaces'])
);
@@ -106,24 +107,17 @@ async function hashAllTheThings(
const rawFilePaths: ReadonlyArray<string> = await globby(
[projectDir, `!${projectDir}/.git`],
{
dot: true
dot: true,
}
);
const filePaths = sandboxed
? rawFilePaths
: rawFilePaths.filter(
path =>
(path) =>
// When not sandboxed, these files will change based on git config
!['LICENSE', 'package.json'].includes(relative(projectDir, path))
);
const hashAll = filePaths.map<Promise<string>>(
path =>
new Promise<string>((resolve, reject) => {
md5File(path, (err: Error, result: string) => {
err ? reject(err) : resolve(result);
});
})
);
const hashAll = filePaths.map<Promise<string>>((path) => md5File(path));
const hashes = await Promise.all(hashAll);
return hashes.reduce<{ readonly [filename: string]: string }>(
(acc, hash, i) => {
@@ -132,7 +126,7 @@ async function hashAllTheThings(
);
return {
...acc,
[trimmedNormalizedFilePath]: hash
[trimmedNormalizedFilePath]: hash,
};
},
{}
@@ -140,19 +134,19 @@ async function hashAllTheThings(
}
/**
* Since we're using Greenkeeper to keep dependencies fresh, including
* Since we're using Dependabot to keep dependencies fresh, including
* `package.json` in these file fingerprint tests guarantees that every
* Greenkeeper PR will trigger a false-positive test failure.
* Dependabot PR will trigger a false-positive test failure.
*
* Here we trade complete assurance that `package.json` is correct for much less
* noisy build results.
*/
const ignorePackageJson = (map: { readonly [file: string]: string }) =>
Object.entries(map)
.filter(entry => !entry[0].includes('package.json'))
.filter((entry) => !entry[0].includes('package.json'))
.reduce((ret, [path, hash]) => ({ ...ret, [path]: hash }), {});
test(`${TestDirectories.one}: parses CLI arguments, handles default options`, async t => {
test(`${TestDirectories.one}: parses CLI arguments, handles default options`, async (t) => {
const description = 'example description 1';
const { stdout } = await execa(
`../bin/typescript-starter`,
@@ -160,40 +154,40 @@ test(`${TestDirectories.one}: parses CLI arguments, handles default options`, as
`${TestDirectories.one}`,
// (user entered `-d='example description 1'`)
`-d=${description}`,
'--no-install'
'--no-install',
],
{
cwd: buildDir,
env
env,
}
);
t.regex(stdout, new RegExp(`Created ${TestDirectories.one} 🎉`));
const map = await hashAllTheThings(TestDirectories.one);
t.deepEqual(map, {
'test-1/.circleci/config.yml': 'd29447823209aa9a230f08af7e7bfcc7',
'test-1/.circleci/config.yml': '44d2fd424c93d381d41030f789efabba',
'test-1/.cspell.json': '5b2e0ad337bc05ef2ce5635270d983fd',
'test-1/.editorconfig': '44a3e6c69d9267b0f756986fd970a8f4',
'test-1/.eslintrc.json': '4e74756d24eaccb7f28d4999e4bd6f0d',
'test-1/.github/CONTRIBUTING.md': '5f0dfa7fdf9bf828e3a3aa8fcaeece08',
'test-1/.github/ISSUE_TEMPLATE.md': '82d1b99b29f32d851627b317195e73d2',
'test-1/.github/ISSUE_TEMPLATE.md': 'e70a0b70402765682b1a961af855040e',
'test-1/.github/PULL_REQUEST_TEMPLATE.md':
'710eb5973a8cda83fc568cb1bbe7c026',
'70f4b97f3914e2f399bcc5868e072c29',
'test-1/.gitignore': '892227b7f662b74410e9bf6fb2ae887f',
'test-1/.npmignore': '49c9375c9a1b4a1b74076f62379b0297',
'test-1/.prettierignore': '1da1ce4fdb868f0939608fafd38f9683',
'test-1/.vscode/debug-ts.js': '23eb6ab10faaa25a95f5bd3325d0455c',
'test-1/.vscode/launch.json': '669e4d1dda91c781177c6adae7aa7e00',
'test-1/.vscode/settings.json': '10c634c5fef6ecd298b6e41bf159f2cc',
'test-1/.vscode/extensions.json': '2d26a716ba181656faac4cd2d38ec139',
'test-1/.vscode/launch.json': '140e17d591e03b8850c456ade3aefb1f',
'test-1/.vscode/settings.json': '1671948882faee285f18d7491f0fc913',
'test-1/README.md': '7a9f4efa9213266c3800f3cc82a53ba7',
'test-1/src/index.ts': '5025093b4dc30524d349fd1cc465ed30',
'test-1/src/lib/number.spec.ts': '6a9a00630b10e7d57a79678c74a0e4df',
'test-1/src/lib/number.ts': '43756f90e6ac0b1c4ee6c81d8ab969c7',
'test-1/src/types/example.d.ts': '4221812f6f0434eec77ccb1fba1e3759',
'test-1/src/lib/number.spec.ts': '0592001d71aa3b3e6bf72f4cd95dc1b9',
'test-1/src/lib/number.ts': 'dcbcc98fea337d07e81728c1a6526a1e',
'test-1/src/types/example.d.ts': '76642861732b16754b0110fb1de49823',
'test-1/tsconfig.json': '0e04adfce2f26c6473f079f6dabd108a',
'test-1/tsconfig.module.json': '2fda4c8760c6cfa3462b40df0645850d',
'test-1/tslint.json': '7ac167ffbcb724a6c270e8dc4e747067'
});
});
test(`${TestDirectories.two}: parses CLI arguments, handles all options`, async t => {
test(`${TestDirectories.two}: parses CLI arguments, handles all options`, async (t) => {
const description = 'example description 2';
const { stdout } = await execa(
`../bin/typescript-starter`,
@@ -205,40 +199,40 @@ test(`${TestDirectories.two}: parses CLI arguments, handles all options`, async
'--yarn',
'--node',
'--dom',
'--no-install'
'--no-install',
],
{
cwd: buildDir,
env
env,
}
);
t.regex(stdout, new RegExp(`Created ${TestDirectories.two} 🎉`));
const map = await hashAllTheThings(TestDirectories.two);
t.deepEqual(map, {
'test-2/.circleci/config.yml': 'd29447823209aa9a230f08af7e7bfcc7',
'test-2/.circleci/config.yml': '44d2fd424c93d381d41030f789efabba',
'test-2/.cspell.json': '5b2e0ad337bc05ef2ce5635270d983fd',
'test-2/.editorconfig': '44a3e6c69d9267b0f756986fd970a8f4',
'test-2/.eslintrc.json': '4e74756d24eaccb7f28d4999e4bd6f0d',
'test-2/.github/CONTRIBUTING.md': '5f0dfa7fdf9bf828e3a3aa8fcaeece08',
'test-2/.github/ISSUE_TEMPLATE.md': '82d1b99b29f32d851627b317195e73d2',
'test-2/.github/ISSUE_TEMPLATE.md': 'e70a0b70402765682b1a961af855040e',
'test-2/.github/PULL_REQUEST_TEMPLATE.md':
'710eb5973a8cda83fc568cb1bbe7c026',
'70f4b97f3914e2f399bcc5868e072c29',
'test-2/.gitignore': 'af817565c661f1b15514584c8ea9e469',
'test-2/.npmignore': '49c9375c9a1b4a1b74076f62379b0297',
'test-2/.prettierignore': '1da1ce4fdb868f0939608fafd38f9683',
'test-2/.vscode/debug-ts.js': '23eb6ab10faaa25a95f5bd3325d0455c',
'test-2/.vscode/launch.json': '669e4d1dda91c781177c6adae7aa7e00',
'test-2/.vscode/settings.json': '10c634c5fef6ecd298b6e41bf159f2cc',
'test-2/.vscode/extensions.json': '2d26a716ba181656faac4cd2d38ec139',
'test-2/.vscode/launch.json': '140e17d591e03b8850c456ade3aefb1f',
'test-2/.vscode/settings.json': '1671948882faee285f18d7491f0fc913',
'test-2/README.md': 'ddaf27da4cc4ca5225785f0ac8f4da58',
'test-2/src/index.ts': 'fbc67c2cbf3a7d37e4e02583bf06eec9',
'test-2/src/lib/async.spec.ts': '1f51a721fffe53832fb289429baba971',
'test-2/src/lib/async.ts': '9012c267bb25fa98ad2561929de3d4e2',
'test-2/src/lib/hash.spec.ts': '11589e1960ddd75e7597c9de6854cd08',
'test-2/src/lib/hash.ts': 'a4c552897f25da5963f410e375264bd1',
'test-2/src/lib/number.spec.ts': '6a9a00630b10e7d57a79678c74a0e4df',
'test-2/src/lib/number.ts': '43756f90e6ac0b1c4ee6c81d8ab969c7',
'test-2/src/types/example.d.ts': '4221812f6f0434eec77ccb1fba1e3759',
'test-2/src/lib/async.spec.ts': '65b10546885ebad41c098318b896f23c',
'test-2/src/lib/async.ts': '926732fef1285cb0abb5c6e287ed24df',
'test-2/src/lib/hash.spec.ts': '06f6bfc1b03f893a16448bb6d6806ea2',
'test-2/src/lib/hash.ts': 'cf3659937ff3162bc525c8bfac18b7cf',
'test-2/src/lib/number.spec.ts': '0592001d71aa3b3e6bf72f4cd95dc1b9',
'test-2/src/lib/number.ts': 'dcbcc98fea337d07e81728c1a6526a1e',
'test-2/src/types/example.d.ts': '76642861732b16754b0110fb1de49823',
'test-2/tsconfig.json': '8a55379f60e4e6d4fad1f0b2318b74c4',
'test-2/tsconfig.module.json': '2fda4c8760c6cfa3462b40df0645850d',
'test-2/tslint.json': '7ac167ffbcb724a6c270e8dc4e747067'
});
});
@@ -246,7 +240,7 @@ const down = '\x1B\x5B\x42';
const up = '\x1B\x5B\x41';
const enter = '\x0D';
const ms = (milliseconds: number) =>
new Promise<void>(resolve => setTimeout(resolve, milliseconds));
new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
async function testInteractive(
projectName: string,
@@ -255,18 +249,18 @@ async function testInteractive(
const typeDefs = entry[3] !== '';
const proc = execa(`../bin/typescript-starter`, ['--no-install'], {
cwd: buildDir,
env
env,
});
// TODO: missing in Node.js type definition's ChildProcess.stdin?
// https://nodejs.org/api/process.html#process_process_stdin
// proc.stdin.setEncoding('utf8');
const type = (input: string) => proc.stdin.write(input);
const type = (input: string) => proc.stdin?.write(input);
// wait for first chunk to be emitted
await new Promise(resolve => {
proc.stdout.once('data', resolve);
await new Promise((resolve) => {
proc.stdout?.once('data', resolve);
});
await ms(200);
type(`${projectName}${enter}`);
@@ -277,7 +271,6 @@ async function testInteractive(
await ms(200);
type(`${entry[2][0]}${enter}`);
await ms(200);
// tslint:disable-next-line:no-if-statement
if (typeDefs) {
type(`${entry[3][0]}${enter}`);
await ms(200);
@@ -287,84 +280,87 @@ async function testInteractive(
return proc;
}
test(`${TestDirectories.three}: interactive mode: javascript library`, async t => {
test(`${TestDirectories.three}: interactive mode: javascript library`, async (t) => {
const proc = await testInteractive(`${TestDirectories.three}`, [
[`${down}${up}${down}`, `Javascript library`],
`integration test 3 description`,
[`${down}${up}${down}${enter}`, `yarn`],
[`${down}${down}${down}${enter}`, `Both Node.js and DOM`],
[' ', 'stricter type-checking[\\s\\S]*tslint-immutable[\\s\\S]*VS Code']
[
' ',
'stricter type-checking[\\s\\S]*eslint-plugin-functional[\\s\\S]*VS Code',
],
]);
await proc;
const map = await hashAllTheThings(TestDirectories.three);
t.deepEqual(map, {
'test-3/.circleci/config.yml': 'd29447823209aa9a230f08af7e7bfcc7',
'test-3/.circleci/config.yml': '44d2fd424c93d381d41030f789efabba',
'test-3/.cspell.json': '5b2e0ad337bc05ef2ce5635270d983fd',
'test-3/.editorconfig': '44a3e6c69d9267b0f756986fd970a8f4',
'test-3/.eslintrc.json': '4e74756d24eaccb7f28d4999e4bd6f0d',
'test-3/.github/CONTRIBUTING.md': '5f0dfa7fdf9bf828e3a3aa8fcaeece08',
'test-3/.github/ISSUE_TEMPLATE.md': '82d1b99b29f32d851627b317195e73d2',
'test-3/.github/ISSUE_TEMPLATE.md': 'e70a0b70402765682b1a961af855040e',
'test-3/.github/PULL_REQUEST_TEMPLATE.md':
'710eb5973a8cda83fc568cb1bbe7c026',
'70f4b97f3914e2f399bcc5868e072c29',
'test-3/.gitignore': 'af817565c661f1b15514584c8ea9e469',
'test-3/.npmignore': '49c9375c9a1b4a1b74076f62379b0297',
'test-3/.prettierignore': '1da1ce4fdb868f0939608fafd38f9683',
'test-3/.vscode/debug-ts.js': '23eb6ab10faaa25a95f5bd3325d0455c',
'test-3/.vscode/launch.json': '669e4d1dda91c781177c6adae7aa7e00',
'test-3/.vscode/settings.json': '10c634c5fef6ecd298b6e41bf159f2cc',
'test-3/.vscode/extensions.json': '2d26a716ba181656faac4cd2d38ec139',
'test-3/.vscode/launch.json': '140e17d591e03b8850c456ade3aefb1f',
'test-3/.vscode/settings.json': '1671948882faee285f18d7491f0fc913',
'test-3/README.md': 'c52631ebf78f6b030af9a109b769b647',
'test-3/src/index.ts': 'fbc67c2cbf3a7d37e4e02583bf06eec9',
'test-3/src/lib/async.spec.ts': '1f51a721fffe53832fb289429baba971',
'test-3/src/lib/async.ts': '9012c267bb25fa98ad2561929de3d4e2',
'test-3/src/lib/hash.spec.ts': '11589e1960ddd75e7597c9de6854cd08',
'test-3/src/lib/hash.ts': 'a4c552897f25da5963f410e375264bd1',
'test-3/src/lib/number.spec.ts': '6a9a00630b10e7d57a79678c74a0e4df',
'test-3/src/lib/number.ts': '43756f90e6ac0b1c4ee6c81d8ab969c7',
'test-3/src/types/example.d.ts': '4221812f6f0434eec77ccb1fba1e3759',
'test-3/src/lib/async.spec.ts': '65b10546885ebad41c098318b896f23c',
'test-3/src/lib/async.ts': '926732fef1285cb0abb5c6e287ed24df',
'test-3/src/lib/hash.spec.ts': '06f6bfc1b03f893a16448bb6d6806ea2',
'test-3/src/lib/hash.ts': 'cf3659937ff3162bc525c8bfac18b7cf',
'test-3/src/lib/number.spec.ts': '0592001d71aa3b3e6bf72f4cd95dc1b9',
'test-3/src/lib/number.ts': 'dcbcc98fea337d07e81728c1a6526a1e',
'test-3/src/types/example.d.ts': '76642861732b16754b0110fb1de49823',
'test-3/tsconfig.json': '43817952d399db9e44977b3703edd7cf',
'test-3/tsconfig.module.json': '2fda4c8760c6cfa3462b40df0645850d',
'test-3/tslint.json': '7ac167ffbcb724a6c270e8dc4e747067'
});
});
test(`${TestDirectories.four}: interactive mode: node.js application`, async t => {
test(`${TestDirectories.four}: interactive mode: node.js application`, async (t) => {
const proc = await testInteractive(`${TestDirectories.four}`, [
[`${down}${up}`, `Node.js application`],
`integration test 4 description`,
[`${down}${up}${enter}`, `npm`],
'',
[`${down} `, 'VS Code']
[`${down} `, 'VS Code'],
]);
await proc;
const map = await hashAllTheThings(TestDirectories.four);
t.deepEqual(map, {
'test-4/.circleci/config.yml': 'd29447823209aa9a230f08af7e7bfcc7',
'test-4/.circleci/config.yml': '44d2fd424c93d381d41030f789efabba',
'test-4/.cspell.json': '5b2e0ad337bc05ef2ce5635270d983fd',
'test-4/.editorconfig': '44a3e6c69d9267b0f756986fd970a8f4',
'test-4/.eslintrc.json': '941448b089cd055bbe476a84c8f96cfe',
'test-4/.github/CONTRIBUTING.md': '5f0dfa7fdf9bf828e3a3aa8fcaeece08',
'test-4/.github/ISSUE_TEMPLATE.md': '82d1b99b29f32d851627b317195e73d2',
'test-4/.github/ISSUE_TEMPLATE.md': 'e70a0b70402765682b1a961af855040e',
'test-4/.github/PULL_REQUEST_TEMPLATE.md':
'710eb5973a8cda83fc568cb1bbe7c026',
'70f4b97f3914e2f399bcc5868e072c29',
'test-4/.gitignore': '892227b7f662b74410e9bf6fb2ae887f',
'test-4/.npmignore': '49c9375c9a1b4a1b74076f62379b0297',
'test-4/.prettierignore': '1da1ce4fdb868f0939608fafd38f9683',
'test-4/.vscode/debug-ts.js': '23eb6ab10faaa25a95f5bd3325d0455c',
'test-4/.vscode/launch.json': '669e4d1dda91c781177c6adae7aa7e00',
'test-4/.vscode/settings.json': '10c634c5fef6ecd298b6e41bf159f2cc',
'test-4/.vscode/extensions.json': '2d26a716ba181656faac4cd2d38ec139',
'test-4/.vscode/launch.json': '140e17d591e03b8850c456ade3aefb1f',
'test-4/.vscode/settings.json': '1671948882faee285f18d7491f0fc913',
'test-4/README.md': 'a3e0699b39498df4843c9dde95f1e000',
'test-4/src/index.ts': 'fbc67c2cbf3a7d37e4e02583bf06eec9',
'test-4/src/lib/async.spec.ts': '1f51a721fffe53832fb289429baba971',
'test-4/src/lib/async.ts': '9012c267bb25fa98ad2561929de3d4e2',
'test-4/src/lib/hash.spec.ts': '11589e1960ddd75e7597c9de6854cd08',
'test-4/src/lib/hash.ts': 'a4c552897f25da5963f410e375264bd1',
'test-4/src/lib/number.spec.ts': '6a9a00630b10e7d57a79678c74a0e4df',
'test-4/src/lib/number.ts': '43756f90e6ac0b1c4ee6c81d8ab969c7',
'test-4/src/types/example.d.ts': '4221812f6f0434eec77ccb1fba1e3759',
'test-4/src/lib/async.spec.ts': '65b10546885ebad41c098318b896f23c',
'test-4/src/lib/async.ts': '926732fef1285cb0abb5c6e287ed24df',
'test-4/src/lib/hash.spec.ts': '06f6bfc1b03f893a16448bb6d6806ea2',
'test-4/src/lib/hash.ts': 'cf3659937ff3162bc525c8bfac18b7cf',
'test-4/src/lib/number.spec.ts': '0592001d71aa3b3e6bf72f4cd95dc1b9',
'test-4/src/lib/number.ts': 'dcbcc98fea337d07e81728c1a6526a1e',
'test-4/src/types/example.d.ts': '76642861732b16754b0110fb1de49823',
'test-4/tsconfig.json': 'e41d08f0aca16cb05430b61e4b6286db',
'test-4/tsconfig.module.json': '2fda4c8760c6cfa3462b40df0645850d',
'test-4/tslint.json': '99f6f8fa763bfc2a32377739b3e5dd5c'
});
});
const sandboxTasks = (
t: ExecutionContext<{}>,
t: ExecutionContext<unknown>,
commit: boolean,
install: boolean
): Tasks => {
@@ -375,7 +371,7 @@ const sandboxTasks = (
},
install: async () => {
install ? t.pass() : t.fail();
}
},
};
};
@@ -383,108 +379,109 @@ const sandboxOptions = {
description: 'this is an example description',
githubUsername: 'SOME_GITHUB_USERNAME',
repoInfo,
workingDirectory: buildDir
workingDirectory: buildDir,
};
const silenceConsole = (console: any) => {
// tslint:disable-next-line:no-object-mutation no-console
const silenceConsole = (console: Console) => {
// eslint-disable-next-line functional/immutable-data
console.log = () => {
// mock console.log to silence it
return;
};
};
test(`${TestDirectories.five}: Sandboxed: npm install, initial commit`, async t => {
test(`${TestDirectories.five}: Sandboxed: npm install, initial commit`, async (t) => {
t.plan(3);
const options = {
...sandboxOptions,
appveyor: false,
circleci: false,
cspell: false,
domDefinitions: false,
editorconfig: false,
email: 'email@example.com',
// cspell: disable-next-line
fullName: 'Satoshi Nakamoto',
immutable: true,
functional: true,
install: true,
nodeDefinitions: false,
projectName: TestDirectories.five,
runner: Runner.Npm,
strict: true,
travis: false,
vscode: false
vscode: false,
};
silenceConsole(console);
await typescriptStarter(options, sandboxTasks(t, true, true));
const map = await hashAllTheThings(TestDirectories.five, true);
t.deepEqual(ignorePackageJson(map), {
'test-5/.eslintrc.json': '4e74756d24eaccb7f28d4999e4bd6f0d',
'test-5/.github/CONTRIBUTING.md': '5f0dfa7fdf9bf828e3a3aa8fcaeece08',
'test-5/.github/ISSUE_TEMPLATE.md': '82d1b99b29f32d851627b317195e73d2',
'test-5/.github/ISSUE_TEMPLATE.md': 'e70a0b70402765682b1a961af855040e',
'test-5/.github/PULL_REQUEST_TEMPLATE.md':
'710eb5973a8cda83fc568cb1bbe7c026',
'70f4b97f3914e2f399bcc5868e072c29',
'test-5/.gitignore': '892227b7f662b74410e9bf6fb2ae887f',
'test-5/.npmignore': '49c9375c9a1b4a1b74076f62379b0297',
'test-5/.prettierignore': '1da1ce4fdb868f0939608fafd38f9683',
'test-5/LICENSE': '317693126d229a3cdd19725a624a56fc',
'test-5/README.md': '8fc7ecb21d7d47289e4b2469eea4db39',
'test-5/src/index.ts': '5025093b4dc30524d349fd1cc465ed30',
'test-5/src/lib/number.spec.ts': '6a9a00630b10e7d57a79678c74a0e4df',
'test-5/src/lib/number.ts': '43756f90e6ac0b1c4ee6c81d8ab969c7',
'test-5/src/types/example.d.ts': '4221812f6f0434eec77ccb1fba1e3759',
'test-5/src/lib/number.spec.ts': '0592001d71aa3b3e6bf72f4cd95dc1b9',
'test-5/src/lib/number.ts': 'dcbcc98fea337d07e81728c1a6526a1e',
'test-5/src/types/example.d.ts': '76642861732b16754b0110fb1de49823',
'test-5/tsconfig.json': 'f36dc6407fc898f41a23cb620b2f4884',
'test-5/tsconfig.module.json': '2fda4c8760c6cfa3462b40df0645850d',
'test-5/tslint.json': '7ac167ffbcb724a6c270e8dc4e747067'
});
});
test(`${TestDirectories.six}: Sandboxed: yarn, no initial commit`, async t => {
test(`${TestDirectories.six}: Sandboxed: yarn, no initial commit`, async (t) => {
t.plan(2);
const options = {
...sandboxOptions,
appveyor: true,
circleci: true,
cspell: false,
domDefinitions: true,
editorconfig: true,
email: Placeholders.email,
fullName: Placeholders.name,
immutable: true,
functional: true,
install: true,
nodeDefinitions: true,
projectName: TestDirectories.six,
runner: Runner.Yarn,
strict: false,
travis: true,
vscode: true
vscode: true,
};
silenceConsole(console);
await typescriptStarter(options, sandboxTasks(t, false, true));
const map = await hashAllTheThings(TestDirectories.six, true);
t.deepEqual(ignorePackageJson(map), {
'test-6/.circleci/config.yml': 'd29447823209aa9a230f08af7e7bfcc7',
'test-6/.circleci/config.yml': '44d2fd424c93d381d41030f789efabba',
'test-6/.editorconfig': '44a3e6c69d9267b0f756986fd970a8f4',
'test-6/.eslintrc.json': '4e74756d24eaccb7f28d4999e4bd6f0d',
'test-6/.github/CONTRIBUTING.md': '5f0dfa7fdf9bf828e3a3aa8fcaeece08',
'test-6/.github/ISSUE_TEMPLATE.md': '82d1b99b29f32d851627b317195e73d2',
'test-6/.github/ISSUE_TEMPLATE.md': 'e70a0b70402765682b1a961af855040e',
'test-6/.github/PULL_REQUEST_TEMPLATE.md':
'710eb5973a8cda83fc568cb1bbe7c026',
'70f4b97f3914e2f399bcc5868e072c29',
'test-6/.gitignore': 'af817565c661f1b15514584c8ea9e469',
'test-6/.npmignore': '49c9375c9a1b4a1b74076f62379b0297',
'test-6/.prettierignore': '1da1ce4fdb868f0939608fafd38f9683',
'test-6/.travis.yml': 'b56cf7194d8ff58d6cf51c34b0c645e0',
'test-6/.vscode/debug-ts.js': '23eb6ab10faaa25a95f5bd3325d0455c',
'test-6/.vscode/launch.json': '669e4d1dda91c781177c6adae7aa7e00',
'test-6/.vscode/settings.json': '10c634c5fef6ecd298b6e41bf159f2cc',
'test-6/.vscode/extensions.json': '2d26a716ba181656faac4cd2d38ec139',
'test-6/.vscode/launch.json': '140e17d591e03b8850c456ade3aefb1f',
'test-6/.vscode/settings.json': 'f70eb64341e74d24d901055a26dc8242',
'test-6/LICENSE': '03ffa741a4f7e356b69353efa4937d2b',
'test-6/README.md': 'd809bcbf240f44b51b575a3d49936232',
'test-6/appveyor.yml': '27c787d8e288f89c71357b1ac32b42e8',
'test-6/appveyor.yml': '214e043a9baa2a9d2579a0af0a5621a3',
'test-6/src/index.ts': 'fbc67c2cbf3a7d37e4e02583bf06eec9',
'test-6/src/lib/async.spec.ts': '1f51a721fffe53832fb289429baba971',
'test-6/src/lib/async.ts': '9012c267bb25fa98ad2561929de3d4e2',
'test-6/src/lib/hash.spec.ts': '11589e1960ddd75e7597c9de6854cd08',
'test-6/src/lib/hash.ts': 'a4c552897f25da5963f410e375264bd1',
'test-6/src/lib/number.spec.ts': '6a9a00630b10e7d57a79678c74a0e4df',
'test-6/src/lib/number.ts': '43756f90e6ac0b1c4ee6c81d8ab969c7',
'test-6/src/types/example.d.ts': '4221812f6f0434eec77ccb1fba1e3759',
'test-6/src/lib/async.spec.ts': '65b10546885ebad41c098318b896f23c',
'test-6/src/lib/async.ts': '926732fef1285cb0abb5c6e287ed24df',
'test-6/src/lib/hash.spec.ts': '06f6bfc1b03f893a16448bb6d6806ea2',
'test-6/src/lib/hash.ts': 'cf3659937ff3162bc525c8bfac18b7cf',
'test-6/src/lib/number.spec.ts': '0592001d71aa3b3e6bf72f4cd95dc1b9',
'test-6/src/lib/number.ts': 'dcbcc98fea337d07e81728c1a6526a1e',
'test-6/src/types/example.d.ts': '76642861732b16754b0110fb1de49823',
'test-6/tsconfig.json': '8a55379f60e4e6d4fad1f0b2318b74c4',
'test-6/tsconfig.module.json': '2fda4c8760c6cfa3462b40df0645850d',
'test-6/tslint.json': '7ac167ffbcb724a6c270e8dc4e747067'
});
});

View File

@@ -1,8 +1,8 @@
// tslint:disable:no-expression-statement
import test from 'ava';
import execa from 'execa';
import meow from 'meow';
import nock from 'nock';
import { checkArgs } from '../args';
import {
cloneRepo,
@@ -11,11 +11,11 @@ import {
getUserInfo,
initialCommit,
install,
Placeholders
Placeholders,
} from '../tasks';
import { getIntro, Runner, validateName } from '../utils';
test('errors if outdated', async t => {
test('errors if outdated', async (t) => {
nock.disableNetConnect();
nock('https://registry.npmjs.org:443')
.get('/typescript-starter')
@@ -24,9 +24,9 @@ test('errors if outdated', async t => {
name: 'typescript-starter',
versions: {
'9000.0.1': {
version: '9000.0.1'
}
}
version: '9000.0.1',
},
},
});
const error = await t.throwsAsync(checkArgs);
t.regex(error.message, /is outdated/);
@@ -41,20 +41,20 @@ const pretendLatestVersionIs = (version: string) => {
name: 'typescript-starter',
versions: {
[version]: {
version
}
}
version,
},
},
});
};
test("doesn't error if not outdated", async t => {
const currentVersion = meow('').pkg.version;
test("doesn't error if not outdated", async (t) => {
const currentVersion = meow('').pkg.version as string;
t.truthy(typeof currentVersion === 'string');
pretendLatestVersionIs(currentVersion);
await t.notThrows(checkArgs);
});
test('errors if update-notifier fails', async t => {
test('errors if update-notifier fails', async (t) => {
nock.disableNetConnect();
nock('https://registry.npmjs.org:443')
.get('/typescript-starter')
@@ -63,35 +63,38 @@ test('errors if update-notifier fails', async t => {
t.regex(error.message, /could not be found/);
});
test('checkArgs returns the right options', async t => {
test('checkArgs returns the right options', async (t) => {
pretendLatestVersionIs('1.0.0');
// tslint:disable-next-line:no-object-mutation
// eslint-disable-next-line functional/immutable-data
process.argv = [
'path/to/node',
'path/to/typescript-starter',
'example-project',
'--appveyor',
`-description "example description"`,
'--description',
'"example description"',
'--dom',
'--node',
'--strict',
'--travis',
'--yarn',
'--no-circleci',
'--no-cspell',
'--no-editorconfig',
'--no-immutable',
'--no-functional',
'--no-install',
'--no-vscode'
'--no-vscode',
];
const opts = await checkArgs();
const currentVersion = meow('').pkg.version;
const currentVersion = meow('').pkg.version as string;
t.deepEqual(opts, {
appveyor: true,
circleci: false,
description: '',
cspell: false,
description: 'example description',
domDefinitions: true,
editorconfig: false,
immutable: false,
functional: false,
install: false,
nodeDefinitions: true,
projectName: 'example-project',
@@ -99,75 +102,78 @@ test('checkArgs returns the right options', async t => {
starterVersion: currentVersion,
strict: true,
travis: true,
vscode: false
vscode: false,
});
});
test('checkArgs always returns a TypescriptStarterRequiredConfig, even in interactive mode', async t => {
test('checkArgs always returns a TypescriptStarterRequiredConfig, even in interactive mode', async (t) => {
pretendLatestVersionIs('1.0.0');
// tslint:disable-next-line:no-object-mutation
// eslint-disable-next-line functional/immutable-data
process.argv = ['path/to/node', 'path/to/typescript-starter'];
const opts = await checkArgs();
t.true(typeof opts.install === 'boolean');
t.true(typeof opts.starterVersion === 'string');
});
test('only accepts valid package names', async t => {
test('only accepts valid package names', async (t) => {
t.true(validateName('package-name'));
t.true(validateName('package-name-2'));
t.true(validateName('@example/package-name-2'));
});
test('ascii art shows if stdout has 85+ columns', async t => {
test('ascii art shows if stdout has 85+ columns', async (t) => {
const jumbo = getIntro(100);
const snippet = `| __| | | | '_ \\ / _ \\/ __|/ __| '__| | '_ \\|`;
t.regex(jumbo, new RegExp(snippet));
});
test('small ascii art shows if stdout has 74-84 columns', async t => {
test('small ascii art shows if stdout has 74-84 columns', async (t) => {
const jumbo = getIntro(80);
const snippet = `| _| || | '_ \\/ -_|_-</ _| '_| | '_ \\ _|`;
t.regex(jumbo, new RegExp(snippet));
});
const mockErr = (code: number = 1, name: string = 'ERR') =>
const mockErr = (code = 1, name = 'ERR') =>
((() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const err: any = new Error();
// tslint:disable-next-line:no-object-mutation
// eslint-disable-next-line functional/immutable-data
err.exitCode = code;
// tslint:disable-next-line:no-object-mutation
// eslint-disable-next-line functional/immutable-data
err.exitCodeName = name;
// eslint-disable-next-line functional/no-throw-statement
throw err;
}) as any) as typeof execa;
}) as unknown) as typeof execa;
test('cloneRepo: errors when Git is not installed on PATH', async t => {
test('cloneRepo: errors when Git is not installed on PATH', async (t) => {
const error = await t.throwsAsync(
cloneRepo(mockErr(1, 'ENOENT'))({ repo: 'r', branch: '.' }, 'd', 'p')
);
t.regex(error.message, /Git is not installed on your PATH/);
});
test('cloneRepo: throws when clone fails', async t => {
test('cloneRepo: throws when clone fails', async (t) => {
const error = await t.throwsAsync(
cloneRepo(mockErr(128))({ repo: 'r', branch: 'b' }, 'd', 'p')
);
t.regex(error.message, /Git clone failed./);
});
test('cloneRepo: throws when rev-parse fails', async t => {
// tslint:disable-next-line:prefer-const no-let
test('cloneRepo: throws when rev-parse fails', async (t) => {
// eslint-disable-next-line functional/no-let
let calls = 0;
const mock = ((async () => {
calls++;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return calls === 1 ? {} : (mockErr(128) as any)();
}) as any) as typeof execa;
}) as unknown) as typeof execa;
const error = await t.throwsAsync(
cloneRepo(mock)({ repo: 'r', branch: 'b' }, 'd', 'p')
);
t.regex(error.message, /Git rev-parse failed./);
});
test('getGithubUsername: returns found users', async t => {
test('getGithubUsername: returns found users', async (t) => {
const mockFetcher = async (email: string) => email.split('@')[0];
const username: string = await getGithubUsername(mockFetcher)(
'bitjson@github.com'
@@ -175,7 +181,7 @@ test('getGithubUsername: returns found users', async t => {
t.is(username, 'bitjson');
});
test("getGithubUsername: returns placeholder if user doesn't have Git user.email set", async t => {
test("getGithubUsername: returns placeholder if user doesn't have Git user.email set", async (t) => {
const mockFetcher = async () => t.fail();
const username: string = await getGithubUsername(mockFetcher)(
Placeholders.email
@@ -183,8 +189,9 @@ test("getGithubUsername: returns placeholder if user doesn't have Git user.email
t.is(username, Placeholders.username);
});
test('getGithubUsername: returns placeholder if not found', async t => {
test('getGithubUsername: returns placeholder if not found', async (t) => {
const mockFetcher = async () => {
// eslint-disable-next-line functional/no-throw-statement
throw new Error();
};
const username: string = await getGithubUsername(mockFetcher)(
@@ -193,72 +200,72 @@ test('getGithubUsername: returns placeholder if not found', async t => {
t.is(username, Placeholders.username);
});
test('getUserInfo: suppresses errors and returns empty strings', async t => {
test('getUserInfo: suppresses errors and returns empty strings', async (t) => {
const result = await getUserInfo(mockErr(1))();
t.deepEqual(result, {
gitEmail: Placeholders.email,
gitName: Placeholders.name
gitName: Placeholders.name,
});
});
test('getUserInfo: returns results properly', async t => {
test('getUserInfo: returns results properly', async (t) => {
const mock = ((async () => {
return {
stdout: 'result'
stdout: 'result',
};
}) as any) as typeof execa;
}) as unknown) as typeof execa;
const result = await getUserInfo(mock)();
t.deepEqual(result, {
gitEmail: 'result',
gitName: 'result'
gitName: 'result',
});
});
test('initialCommit: throws generated errors', async t => {
test('initialCommit: throws generated errors', async (t) => {
const error = await t.throwsAsync<execa.ExecaError>(
initialCommit(mockErr(1))('deadbeef', 'fail')
);
t.is(error.exitCode, 1);
});
test('initialCommit: spawns 3 times', async t => {
test('initialCommit: spawns 3 times', async (t) => {
t.plan(4);
const mock = ((async () => {
t.pass();
}) as any) as typeof execa;
}) as unknown) as typeof execa;
await t.notThrowsAsync(initialCommit(mock)('commit', 'dir'));
});
test('install: uses the correct runner', async t => {
test('install: uses the correct runner', async (t) => {
const mock = ((async (runner: Runner) => {
runner === Runner.Yarn ? t.pass() : t.fail();
}) as any) as typeof execa;
}) as unknown) as typeof execa;
await install(mock)(Runner.Yarn, 'pass');
});
test('install: throws pretty error on failure', async t => {
test('install: throws pretty error on failure', async (t) => {
const error = await t.throwsAsync(install(mockErr())(Runner.Npm, 'fail'));
t.is(error.message, "Installation failed. You'll need to install manually.");
});
test("getRepoInfo: returns defaults when TYPESCRIPT_STARTER_REPO_URL/BRANCH aren't set", async t => {
test("getRepoInfo: returns defaults when TYPESCRIPT_STARTER_REPO_URL/BRANCH aren't set", async (t) => {
const thisRelease = '9000.0.1';
t.deepEqual(getRepoInfo(thisRelease), {
branch: `v${thisRelease}`,
repo: 'https://github.com/bitjson/typescript-starter.git'
repo: 'https://github.com/bitjson/typescript-starter.git',
});
const url = 'https://another/repo';
// tslint:disable-next-line:no-object-mutation
// eslint-disable-next-line functional/immutable-data
process.env.TYPESCRIPT_STARTER_REPO_URL = url;
t.deepEqual(getRepoInfo(thisRelease), {
branch: `master`,
repo: url
repo: url,
});
const branch = 'test';
// tslint:disable-next-line:no-object-mutation
// eslint-disable-next-line functional/immutable-data
process.env.TYPESCRIPT_STARTER_REPO_BRANCH = branch;
t.deepEqual(getRepoInfo(thisRelease), {
branch,
repo: url
repo: url,
});
});

View File

@@ -1,24 +1,36 @@
// tslint:disable:no-console no-if-statement no-expression-statement
import { readFileSync, renameSync, writeFileSync } from 'fs';
import { join } from 'path';
import chalk from 'chalk';
import del from 'del';
import { readFileSync, renameSync, writeFileSync } from 'fs';
import ora from 'ora';
import { join } from 'path';
import replace from 'replace-in-file';
import { replaceInFile } from 'replace-in-file';
import { Placeholders, Tasks } from './tasks';
import { normalizePath, Runner, TypescriptStarterOptions } from './utils';
const readPackageJson = (path: string) =>
JSON.parse(readFileSync(path, 'utf8'));
const writePackageJson = (path: string, pkg: unknown) => {
// 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';
return writeFileSync(path, stringified);
};
export async function typescriptStarter(
{
appveyor,
circleci,
cspell,
description,
domDefinitions,
editorconfig,
email,
fullName,
githubUsername,
immutable,
functional,
install,
nodeDefinitions,
projectName,
@@ -27,7 +39,7 @@ export async function typescriptStarter(
strict,
travis,
vscode,
workingDirectory
workingDirectory,
}: TypescriptStarterOptions,
tasks: Tasks
): Promise<void> {
@@ -47,29 +59,35 @@ export async function typescriptStarter(
const pkgPath = join(projectPath, 'package.json');
const keptDevDeps: ReadonlyArray<string> = [
'@bitjson/npm-scripts-info',
'@bitjson/typedoc',
'@ava/typescript',
'@istanbuljs/nyc-config-typescript',
'@typescript-eslint/eslint-plugin',
'@typescript-eslint/parser',
'ava',
'codecov',
'cspell',
'cz-conventional-changelog',
'eslint',
'eslint-config-prettier',
'eslint-plugin-eslint-comments',
...(functional ? ['eslint-plugin-functional'] : []),
'eslint-plugin-import',
'gh-pages',
'npm-run-all',
'npm-scripts-info',
'nyc',
'open-cli',
'prettier',
'standard-version',
'trash-cli',
'tslint',
'tslint-config-prettier',
'tslint-immutable',
'ts-node',
'typedoc',
'typescript'
'typescript',
];
// dependencies to retain for Node.js applications
const nodeKeptDeps: ReadonlyArray<string> = ['sha.js'];
/**
* dependencies to retain for Node.js applications
*/
const nodeKeptDeps: ReadonlyArray<string> = ['@bitauth/libauth'];
const filterAllBut = (
keep: ReadonlyArray<string>,
@@ -90,7 +108,6 @@ export async function typescriptStarter(
: {},
description,
devDependencies: filterAllBut(keptDevDeps, pkg.devDependencies),
// tslint:disable-next-line:readonly-array
keywords: [],
name: projectName,
repository: `https://github.com/${githubUsername}/${projectName}`,
@@ -98,40 +115,43 @@ export async function typescriptStarter(
runner === Runner.Yarn
? {
...pkg.scripts,
preinstall: `node -e \"if(process.env.npm_execpath.indexOf('yarn') === -1) throw new Error('${projectName} must be installed with Yarn: https://yarnpkg.com/')\"`
'reset-hard': `git clean -dfx && git reset --hard && yarn`,
}
: { ...pkg.scripts },
version: '1.0.0'
version: '1.0.0',
};
// tslint:disable:no-delete no-object-mutation
// eslint-disable-next-line functional/immutable-data
delete newPkg.bin;
// eslint-disable-next-line functional/immutable-data
delete newPkg.NOTE;
// tslint:enable:no-delete no-object-mutation
// eslint-disable-next-line functional/immutable-data
delete newPkg.NOTE_2;
writePackageJson(pkgPath, newPkg);
spinnerPackage.succeed();
const spinnerGitignore = ora('Updating .gitignore').start();
if (runner === Runner.Yarn) {
await replace({
await replaceInFile({
files: join(projectPath, '.gitignore'),
from: 'yarn.lock',
to: 'package-lock.json'
to: 'package-lock.json',
});
}
spinnerGitignore.succeed();
const spinnerLicense = ora('Updating LICENSE').start();
await replace({
await replaceInFile({
files: join(projectPath, 'LICENSE'),
// cspell: disable-next-line
from: 'Jason Dreyzehner',
to: fullName
to: fullName,
});
await replace({
await replaceInFile({
files: join(projectPath, 'LICENSE'),
from: '2017',
to: new Date().getUTCFullYear().toString()
to: new Date().getUTCFullYear().toString(),
});
spinnerLicense.succeed();
@@ -143,7 +163,6 @@ export async function typescriptStarter(
normalizePath(join(projectPath, 'package-lock.json')),
normalizePath(join(projectPath, 'bin')),
normalizePath(join(projectPath, 'src', 'cli')),
normalizePath(join(projectPath, 'src', 'types', 'cli.d.ts'))
]);
if (!appveyor) {
del([normalizePath(join(projectPath, 'appveyor.yml'))]);
@@ -151,6 +170,21 @@ export async function typescriptStarter(
if (!circleci) {
del([normalizePath(join(projectPath, '.circleci'))]);
}
if (!cspell) {
del([normalizePath(join(projectPath, '.cspell.json'))]);
if (vscode) {
await replaceInFile({
files: join(projectPath, '.vscode', 'settings.json'),
from: ` "cSpell.userWords": [], // only use words from .cspell.json\n`,
to: '',
});
await replaceInFile({
files: join(projectPath, '.vscode', 'settings.json'),
from: ` "cSpell.enabled": true,\n`,
to: '',
});
}
}
if (!travis) {
del([normalizePath(join(projectPath, '.travis.yml'))]);
}
@@ -163,16 +197,16 @@ export async function typescriptStarter(
spinnerDelete.succeed();
const spinnerTsconfigModule = ora('Removing traces of the CLI').start();
await replace({
await replaceInFile({
files: join(projectPath, 'tsconfig.module.json'),
from: /,\s+\/\/ typescript-starter:[\s\S]*"src\/cli\/\*\*\/\*\.ts"/,
to: ''
to: '',
});
if (vscode) {
await replace({
await replaceInFile({
files: join(projectPath, '.vscode', 'launch.json'),
from: /,[\s]*\/\/ --- cut here ---[\s\S]*]/,
to: ']'
to: ']',
});
}
spinnerTsconfigModule.succeed();
@@ -182,67 +216,74 @@ export async function typescriptStarter(
join(projectPath, 'README-starter.md'),
join(projectPath, 'README.md')
);
await replace({
await replaceInFile({
files: join(projectPath, 'README.md'),
from: '[package-name]',
to: projectName
to: projectName,
});
await replace({
await replaceInFile({
files: join(projectPath, 'README.md'),
from: '[description]',
to: description
to: description,
});
spinnerReadme.succeed();
if (!strict) {
const spinnerStrict = ora(`tsconfig: disable strict`).start();
await replace({
await replaceInFile({
files: join(projectPath, 'tsconfig.json'),
from: '"strict": true',
to: '// "strict": true'
to: '// "strict": true',
});
spinnerStrict.succeed();
}
if (!domDefinitions) {
const spinnerDom = ora(`tsconfig: don't include "dom" lib`).start();
await replace({
await replaceInFile({
files: join(projectPath, 'tsconfig.json'),
from: '"lib": ["es2017", "dom"]',
to: '"lib": ["es2017"]'
to: '"lib": ["es2017"]',
});
spinnerDom.succeed();
}
if (!nodeDefinitions) {
const spinnerNode = ora(`tsconfig: don't include "node" types`).start();
await replace({
await replaceInFile({
files: join(projectPath, 'tsconfig.json'),
from: '"types": ["node"]',
to: '"types": []'
to: '"types": []',
});
await replace({
await replaceInFile({
files: join(projectPath, 'src', 'index.ts'),
from: /^export[\S\s]*hash';\s*/,
to: ''
to: '',
});
await del([
normalizePath(join(projectPath, 'src', 'lib', 'hash.ts')),
normalizePath(join(projectPath, 'src', 'lib', 'hash.spec.ts')),
normalizePath(join(projectPath, 'src', 'lib', 'async.ts')),
normalizePath(join(projectPath, 'src', 'lib', 'async.spec.ts'))
normalizePath(join(projectPath, 'src', 'lib', 'async.spec.ts')),
]);
spinnerNode.succeed();
}
if (!immutable) {
const spinnerTslint = ora(`tslint: disable tslint-immutable`).start();
await replace({
files: join(projectPath, 'tslint.json'),
from: /,[\s]*\/\* tslint-immutable rules \*\/[\s\S]*\/\* end tslint-immutable rules \*\//,
to: ''
if (!functional) {
const spinnerEslint = ora(
`eslint: disable eslint-plugin-functional`
).start();
await replaceInFile({
files: join(projectPath, '.eslintrc.json'),
from: '"plugins": ["import", "eslint-comments", "functional"]',
to: '"plugins": ["import", "eslint-comments"]',
});
spinnerTslint.succeed();
await replaceInFile({
files: join(projectPath, '.eslintrc.json'),
from: '"plugin:functional/lite",\n',
to: '',
});
spinnerEslint.succeed();
}
if (install) {
@@ -261,13 +302,3 @@ export async function typescriptStarter(
console.log(`\n${chalk.blue.bold(`Created ${projectName} 🎉`)}\n`);
}
const readPackageJson = (path: string) =>
JSON.parse(readFileSync(path, 'utf8'));
const 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';
return writeFileSync(path, stringified);
};

View File

@@ -1,20 +1,22 @@
import chalk from 'chalk';
import { existsSync } from 'fs';
import gradient from 'gradient-string';
import { normalize } from 'path';
import chalk from 'chalk';
import gradient from 'gradient-string';
import validateNpmPackageName from 'validate-npm-package-name';
export enum Runner {
Npm = 'npm',
Yarn = 'yarn'
Yarn = 'yarn',
}
export interface TypescriptStarterCLIOptions {
export type TypescriptStarterCLIOptions = {
readonly appveyor: boolean;
readonly circleci: boolean;
readonly cspell: boolean;
readonly description: string;
readonly domDefinitions: boolean;
readonly editorconfig: boolean;
readonly immutable: boolean;
readonly functional: boolean;
readonly install: boolean;
readonly nodeDefinitions: boolean;
readonly projectName: string;
@@ -22,12 +24,12 @@ export interface TypescriptStarterCLIOptions {
readonly strict: boolean;
readonly travis: boolean;
readonly vscode: boolean;
}
};
export interface TypescriptStarterRequiredConfig {
export type TypescriptStarterRequiredConfig = {
readonly starterVersion: string;
readonly install: boolean;
}
};
export type TypescriptStarterUserOptions = TypescriptStarterCLIOptions &
TypescriptStarterRequiredConfig;
@@ -36,7 +38,7 @@ export type TypescriptStarterArgsOptions =
| TypescriptStarterUserOptions
| TypescriptStarterRequiredConfig;
export interface TypescriptStarterInferredOptions {
export type TypescriptStarterInferredOptions = {
readonly githubUsername: string;
readonly fullName: string;
readonly email: string;
@@ -45,13 +47,12 @@ export interface TypescriptStarterInferredOptions {
readonly branch: string;
};
readonly workingDirectory: string;
}
};
export interface TypescriptStarterOptions
extends TypescriptStarterCLIOptions,
TypescriptStarterInferredOptions {
// readonly starterVersion?: string;
}
export type TypescriptStarterOptions = TypescriptStarterCLIOptions &
TypescriptStarterInferredOptions & {
// readonly starterVersion?: string;
};
export function hasCLIOptions(
opts: TypescriptStarterArgsOptions
@@ -96,5 +97,5 @@ _ _ _ _ _
* On Windows, normalize returns "\\" as the path separator.
* This method normalizes with POSIX.
*/
export const normalizePath = (path: string) =>
export const normalizePath = (path: string): string =>
normalize(path).replace(/\\/g, '/');