1
0
mirror of synced 2025-11-08 04:48:04 +00:00

feat(CLI): add support for TYPESCRIPT_STARTER_REPO_BRANCH process.env, default to tag of current rel

This commit is contained in:
Jason Dreyzehner
2018-03-15 21:12:59 -04:00
parent e68eddbe12
commit 24ef06e1fe
9 changed files with 166 additions and 63 deletions

View File

@@ -145,6 +145,8 @@ This project is tooled for [conventional changelog](https://github.com/conventio
npm run changelog
```
You may find a tool like [**`wip`**](https://github.com/bitjson/wip) helpful for managing work in progress before you're ready to create a meaningful commit.
## One-step publish preparation script
Bringing together many of the steps above, this repo includes a one-step release preparation command.
@@ -330,12 +332,14 @@ cd typescript-starter-testing
TYPESCRIPT_STARTER_REPO_URL='/local/path/to/typescript-starter' typescript-starter
```
You can also `TYPESCRIPT_STARTER_REPO_URL` to any valid Git URL, such as your fork of this repo:
You can also set `TYPESCRIPT_STARTER_REPO_URL` to any valid Git URL, such as your fork of this repo:
```
TYPESCRIPT_STARTER_REPO_URL='https://github.com/YOUR_USERNAME/typescript-starter.git' typescript-starter
```
If `TYPESCRIPT_STARTER_REPO_BRANCH` is not provided, it will default to `master`.
If you're using [VS Code](https://code.visualstudio.com/), the `Debug CLI` launch configuration also allows you to immediately build and step through execution of the CLI.
# In the wild

View File

@@ -2,11 +2,9 @@
import meow from 'meow';
import { Package, UpdateInfo, UpdateNotifier } from 'update-notifier';
import { Runner, TypescriptStarterUserOptions, validateName } from './utils';
import { Runner, TypescriptStarterArgsOptions, validateName } from './utils';
export async function checkArgs(): Promise<
Partial<TypescriptStarterUserOptions>
> {
export async function checkArgs(): Promise<TypescriptStarterArgsOptions> {
const cli = meow(
`
Usage
@@ -90,7 +88,8 @@ export async function checkArgs(): Promise<
// note: we always return `install`, so --no-install always works
// (important for test performance)
return {
install: cli.flags.install
install: cli.flags.install,
starterVersion: cli.pkg.version
};
}
const validOrMsg = await validateName(input);
@@ -106,6 +105,7 @@ export async function checkArgs(): Promise<
nodeDefinitions: cli.flags.node,
projectName: input,
runner: cli.flags.yarn ? Runner.Yarn : Runner.Npm,
starterVersion: cli.pkg.version,
strict: cli.flags.strict,
vscode: cli.flags.vscode
};

View File

@@ -2,23 +2,23 @@
import chalk from 'chalk';
import { checkArgs } from './args';
import { inquire } from './inquire';
import { getInferredOptions, LiveTasks } from './tasks';
import { addInferredOptions, LiveTasks } from './tasks';
import { typescriptStarter } from './typescript-starter';
import { getIntro, TypescriptStarterUserOptions } from './utils';
import { getIntro, hasCLIOptions, TypescriptStarterUserOptions } from './utils';
(async () => {
const cliOptions = await checkArgs();
const userOptions = cliOptions.projectName
? (cliOptions as TypescriptStarterUserOptions)
const argInfo = await checkArgs();
const userOptions: TypescriptStarterUserOptions = hasCLIOptions(argInfo)
? argInfo
: {
...(await (async () => {
console.log(getIntro(process.stdout.columns));
return inquire();
})()),
...cliOptions // merge in cliOptions.install
...argInfo
};
const inferredOptions = await getInferredOptions();
return typescriptStarter({ ...inferredOptions, ...userOptions }, LiveTasks);
const options = await addInferredOptions(userOptions);
return typescriptStarter(options, LiveTasks);
})().catch((err: Error) => {
console.error(`
${chalk.red(err.message)}

View File

@@ -1,7 +1,7 @@
import { prompt, Question } from 'inquirer';
import { Runner, TypescriptStarterUserOptions, validateName } from './utils';
import { Runner, TypescriptStarterCLIOptions, validateName } from './utils';
export async function inquire(): Promise<TypescriptStarterUserOptions> {
export async function inquire(): Promise<TypescriptStarterCLIOptions> {
const packageNameQuestion: Question = {
filter: (answer: string) => answer.trim(),
message: '📦 Enter the new package name:',

View File

@@ -2,7 +2,12 @@
import execa, { ExecaStatic, Options, StdIOOption } from 'execa';
import githubUsername from 'github-username';
import { join } from 'path';
import { Runner, TypescriptStarterInferredOptions } from './utils';
import {
Runner,
TypescriptStarterInferredOptions,
TypescriptStarterOptions,
TypescriptStarterUserOptions
} from './utils';
// TODO: await https://github.com/DefinitelyTyped/DefinitelyTyped/pull/24209
const inherit = 'inherit' as StdIOOption;
@@ -18,14 +23,25 @@ export enum Placeholders {
export const cloneRepo = (
spawner: ExecaStatic,
suppressOutput = false
) => async (repoURL: string, workingDirectory: string, dir: string) => {
) => async (
repoInfo: {
readonly branch: string;
readonly repo: string;
},
workingDirectory: string,
dir: string
) => {
const projectDir = join(workingDirectory, dir);
const gitHistoryDir = join(projectDir, '.git');
try {
await spawner('git', ['clone', '--depth=1', repoURL, dir], {
cwd: workingDirectory,
stdio: suppressOutput ? 'pipe' : 'inherit'
});
await spawner(
'git',
['clone', '--depth=1', `--branch=${repoInfo.branch}`, repoInfo.repo, dir],
{
cwd: workingDirectory,
stdio: suppressOutput ? 'pipe' : 'inherit'
}
);
} catch (err) {
if (err.code === 'ENOENT') {
throw new Error(`
@@ -97,7 +113,7 @@ export const initialCommit = (spawner: ExecaStatic) => async (
[
'commit',
'-m',
`Initial commit\n\nCreated with typescript-starter@${hash}`
`Initial commit\n\nCreated with bitjson/typescript-starter@${hash}`
],
opts
);
@@ -121,16 +137,33 @@ export const install = (spawner: ExecaStatic) => async (
}
};
export const getRepoUrl = () => {
return (
process.env.TYPESCRIPT_STARTER_REPO_URL ||
'https://github.com/bitjson/typescript-starter.git'
);
/**
* Returns the URL and branch to clone. We clone the branch (tag) at the current
* release rather than `master`. This ensures we get the exact files expected by
* this version of the CLI. (If we cloned master, changes merged to master, but
* not yet released, may cause unexpected results.)
* @param starterVersion the current version of this CLI
*/
export const getRepoInfo = (starterVersion: string) => {
return process.env.TYPESCRIPT_STARTER_REPO_URL
? {
branch: process.env.TYPESCRIPT_STARTER_REPO_BRANCH
? process.env.TYPESCRIPT_STARTER_REPO_BRANCH
: 'master',
repo: process.env.TYPESCRIPT_STARTER_REPO_URL
}
: {
branch: `v${starterVersion}`,
repo: 'https://github.com/bitjson/typescript-starter.git'
};
};
export interface Tasks {
readonly cloneRepo: (
repoURL: string,
repoInfo: {
readonly branch: string;
readonly repo: string;
},
workingDirectory: string,
dir: string
) => Promise<{ readonly commitHash: string; readonly gitHistoryDir: string }>;
@@ -147,16 +180,28 @@ export const LiveTasks: Tasks = {
initialCommit: initialCommit(execa),
install: install(execa)
};
export const getInferredOptions = async (): Promise<
TypescriptStarterInferredOptions
> => {
export const addInferredOptions = async (
userOptions: TypescriptStarterUserOptions
): Promise<TypescriptStarterOptions> => {
const { gitName, gitEmail } = await getUserInfo(execa)();
const username = await getGithubUsername(githubUsername)(gitEmail);
return {
const inferredOptions: TypescriptStarterInferredOptions = {
email: gitEmail,
fullName: gitName,
githubUsername: username,
repoURL: getRepoUrl(),
repoInfo: getRepoInfo(userOptions.starterVersion),
workingDirectory: process.cwd()
};
return {
...inferredOptions,
description: userOptions.description,
domDefinitions: userOptions.domDefinitions,
immutable: userOptions.immutable,
install: userOptions.install,
nodeDefinitions: userOptions.nodeDefinitions,
projectName: userOptions.projectName,
runner: userOptions.runner,
strict: userOptions.strict,
vscode: userOptions.vscode
};
};

View File

@@ -33,8 +33,18 @@ import { Runner } from '../utils';
* directory for easier clean up.
*/
const repoURL = process.cwd();
const branch = execa.sync('git', [
'rev-parse',
'--symbolic-full-name',
'--abbrev-ref',
'HEAD'
]).stdout;
const repoInfo = { repo: process.cwd(), branch };
const buildDir = join(process.cwd(), 'build');
const env = {
TYPESCRIPT_STARTER_REPO_BRANCH: repoInfo.branch,
TYPESCRIPT_STARTER_REPO_URL: repoInfo.repo
};
enum TestDirectories {
one = 'test-1',
@@ -134,9 +144,7 @@ test(`${
],
{
cwd: buildDir,
env: {
TYPESCRIPT_STARTER_REPO_URL: repoURL
}
env
}
);
t.regex(stdout, new RegExp(`Created ${TestDirectories.one} 🎉`));
@@ -172,9 +180,7 @@ test(`${
],
{
cwd: buildDir,
env: {
TYPESCRIPT_STARTER_REPO_URL: repoURL
}
env
}
);
t.regex(stdout, new RegExp(`Created ${TestDirectories.two} 🎉`));
@@ -210,9 +216,7 @@ async function testInteractive(
const typeDefs = entry[3] !== '';
const proc = execa(`../bin/typescript-starter`, ['--no-install'], {
cwd: buildDir,
env: {
TYPESCRIPT_STARTER_REPO_URL: repoURL
}
env
});
// TODO: missing in Node.js type definition's ChildProcess.stdin?
@@ -351,7 +355,7 @@ const sandboxTasks = (
const sandboxOptions = {
description: 'this is an example description',
githubUsername: 'SOME_GITHUB_USERNAME',
repoURL,
repoInfo,
workingDirectory: buildDir
};

View File

@@ -7,7 +7,7 @@ import { checkArgs } from '../args';
import {
cloneRepo,
getGithubUsername,
getRepoUrl,
getRepoInfo,
getUserInfo,
initialCommit,
install,
@@ -32,7 +32,7 @@ test('errors if outdated', async t => {
t.regex(error.message, /is outdated/);
});
const passUpdateNotifier = (version: string) => {
const pretendLatestVersionIs = (version: string) => {
nock.disableNetConnect();
nock('https://registry.npmjs.org:443')
.get('/typescript-starter')
@@ -50,7 +50,7 @@ const passUpdateNotifier = (version: string) => {
test("doesn't error if not outdated", async t => {
const currentVersion = meow('').pkg.version;
t.truthy(typeof currentVersion === 'string');
passUpdateNotifier(currentVersion);
pretendLatestVersionIs(currentVersion);
await t.notThrows(checkArgs);
});
@@ -64,7 +64,7 @@ test('errors if update-notifier fails', async t => {
});
test('checkArgs returns the right options', async t => {
passUpdateNotifier('1.0.0');
pretendLatestVersionIs('1.0.0');
// tslint:disable-next-line:no-object-mutation
process.argv = [
'path/to/node',
@@ -80,6 +80,7 @@ test('checkArgs returns the right options', async t => {
'--no-vscode'
];
const opts = await checkArgs();
const currentVersion = meow('').pkg.version;
t.deepEqual(opts, {
description: '',
domDefinitions: true,
@@ -88,17 +89,19 @@ test('checkArgs returns the right options', async t => {
nodeDefinitions: true,
projectName: 'example-project',
runner: Runner.Yarn,
starterVersion: currentVersion,
strict: true,
vscode: false
});
});
test('checkArgs always returns { install } (so --no-install works in interactive mode)', async t => {
passUpdateNotifier('1.0.0');
test('checkArgs always returns a TypescriptStarterRequiredConfig, even in interactive mode', async t => {
pretendLatestVersionIs('1.0.0');
// tslint:disable-next-line:no-object-mutation
process.argv = ['path/to/node', 'path/to/typescript-starter'];
const opts = await checkArgs();
t.deepEqual(opts, { install: true });
t.true(typeof opts.install === 'boolean');
t.true(typeof opts.starterVersion === 'string');
});
test('only accepts valid package names', async t => {
@@ -128,12 +131,16 @@ const mockErr = (code?: string | number) =>
}) as any) as ExecaStatic;
test('cloneRepo: errors when Git is not installed on PATH', async t => {
const error = await t.throws(cloneRepo(mockErr('ENOENT'))('r', 'd', 'p'));
const error = await t.throws(
cloneRepo(mockErr('ENOENT'))({ repo: 'r', branch: 'b' }, 'd', 'p')
);
t.regex(error.message, /Git is not installed on your PATH/);
});
test('cloneRepo: throws when clone fails', async t => {
const error = await t.throws(cloneRepo(mockErr(128))('r', 'd', 'p'));
const error = await t.throws(
cloneRepo(mockErr(128))({ repo: 'r', branch: 'b' }, 'd', 'p')
);
t.regex(error.message, /Git clone failed./);
});
@@ -144,7 +151,9 @@ test('cloneRepo: throws when rev-parse fails', async t => {
calls++;
return calls === 1 ? {} : (mockErr(128) as any)();
}) as any) as ExecaStatic;
const error = await t.throws(cloneRepo(mock)('r', 'd', 'p'));
const error = await t.throws(
cloneRepo(mock)({ repo: 'r', branch: 'b' }, 'd', 'p')
);
t.regex(error.message, /Git rev-parse failed./);
});
@@ -220,6 +229,24 @@ test('install: throws pretty error on failure', async t => {
t.is(error.message, "Installation failed. You'll need to install manually.");
});
test("getRepoUrl: returns GitHub repo when TYPESCRIPT_STARTER_REPO_URL isn't set", async t => {
t.is(getRepoUrl(), 'https://github.com/bitjson/typescript-starter.git');
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'
});
const url = 'https://another/repo';
// tslint:disable-next-line:no-object-mutation
process.env.TYPESCRIPT_STARTER_REPO_URL = url;
t.deepEqual(getRepoInfo(thisRelease), {
branch: `master`,
repo: url
});
const branch = 'test';
// tslint:disable-next-line:no-object-mutation
process.env.TYPESCRIPT_STARTER_REPO_BRANCH = branch;
t.deepEqual(getRepoInfo(thisRelease), {
branch,
repo: url
});
});

View File

@@ -19,7 +19,7 @@ export async function typescriptStarter(
install,
nodeDefinitions,
projectName,
repoURL,
repoInfo,
runner,
strict,
vscode,
@@ -29,7 +29,7 @@ export async function typescriptStarter(
): Promise<void> {
console.log();
const { commitHash, gitHistoryDir } = await tasks.cloneRepo(
repoURL,
repoInfo,
workingDirectory,
projectName
);

View File

@@ -7,7 +7,7 @@ export enum Runner {
Yarn = 'yarn'
}
export interface TypescriptStarterUserOptions {
export interface TypescriptStarterCLIOptions {
readonly description: string;
readonly domDefinitions: boolean;
readonly immutable: boolean;
@@ -19,17 +19,40 @@ export interface TypescriptStarterUserOptions {
readonly vscode: boolean;
}
export interface TypescriptStarterRequiredConfig {
readonly starterVersion: string;
readonly install: boolean;
}
export type TypescriptStarterUserOptions = TypescriptStarterCLIOptions &
TypescriptStarterRequiredConfig;
export type TypescriptStarterArgsOptions =
| TypescriptStarterUserOptions
| TypescriptStarterRequiredConfig;
export interface TypescriptStarterInferredOptions {
readonly githubUsername: string;
readonly fullName: string;
readonly email: string;
readonly repoURL: string;
readonly repoInfo: {
readonly repo: string;
readonly branch: string;
};
readonly workingDirectory: string;
}
export interface TypescriptStarterOptions
extends TypescriptStarterUserOptions,
TypescriptStarterInferredOptions {}
extends TypescriptStarterCLIOptions,
TypescriptStarterInferredOptions {
// readonly starterVersion?: string;
}
export function hasCLIOptions(
opts: TypescriptStarterArgsOptions
): opts is TypescriptStarterUserOptions {
return (opts as TypescriptStarterUserOptions).projectName !== undefined;
}
export function validateName(input: string): true | string {
return !validateNpmPackageName(input).validForNewPackages