Skip to content

Refactor to TypeScript #67

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 52 additions & 56 deletions lib/base_command.js → lib/base_command.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,45 @@
'use strict';

const inquirer = require('inquirer');
const path = require('path');
const fs = require('mz/fs');
const mkdirp = require('mz-modules/mkdirp');
const BaseCommand = require('common-bin');
const ConsoleLogger = require('zlogger');
const chalk = require('chalk');
const runscript = require('runscript');
const through = require('through2');
const giturl = require('giturl');
const readJSON = require('utility').readJSON;
const Cache = require('./cache');
const PROMPT = Symbol('prompt');

const configDir = path.join(process.env.HOME, '.projj');
import * as inquirer from 'inquirer';
import * as path from 'path';
import * as fs from 'mz/fs';
import * as mkdirp from 'mz-modules/mkdirp';
import BaseCommand from 'common-bin';
import ConsoleLogger from 'zlogger';
import * as chalk from 'chalk';
import * as runscript from 'runscript';
import * as through from 'through2';
import * as giturl from 'giturl';
import { readJSON } from 'utility';
import Cache from './cache';

interface Config {
base: string[];
hooks: { [key: string]: string };
alias: { [key: string]: string };
}

const configDir = path.join(process.env.HOME as string, '.projj');
const configPath = path.join(configDir, 'config.json');
const cachePath = path.join(configDir, 'cache.json');
const consoleLogger = new ConsoleLogger({
time: false,
});

const defaults = {
base: `${process.env.HOME}/projj`,
const defaults: Config = {
base: [`${process.env.HOME}/projj`],
hooks: {},
alias: {
'github://': 'https://github.com/',
},
};

class Command extends BaseCommand {
protected logger: ConsoleLogger;
protected childLogger: ConsoleLogger;
protected cache: Cache;
protected config: Config;
private PROMPT: any;
Comment on lines +27 to +40
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default configuration and class properties are set up correctly. However, consider using TypeScript's Partial<T> or Readonly<T> where appropriate to enforce immutability or partial configuration.

- const defaults: Config = {
+ const defaults: Readonly<Partial<Config>> = {

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
const defaults: Config = {
base: [`${process.env.HOME}/projj`],
hooks: {},
alias: {
'github://': 'https://github.com/',
},
};
class Command extends BaseCommand {
protected logger: ConsoleLogger;
protected childLogger: ConsoleLogger;
protected cache: Cache;
protected config: Config;
private PROMPT: any;
const defaults: Readonly<Partial<Config>> = {
base: [`${process.env.HOME}/projj`],
hooks: {},
alias: {
'github://': 'https://github.com/',
},
};
class Command extends BaseCommand {
protected logger: ConsoleLogger;
protected childLogger: ConsoleLogger;
protected cache: Cache;
protected config: Config;
private PROMPT: any;


constructor(rawArgv) {
constructor(rawArgv: string[]) {
super(rawArgv);
this.logger = new ConsoleLogger({
prefix: chalk.green('✔︎ '),
Expand All @@ -46,19 +54,18 @@ class Command extends BaseCommand {
this.cache = new Cache({ cachePath });
}

async run({ cwd, rawArgv }) {
async run({ cwd, rawArgv }: { cwd: string; rawArgv: string[] }): Promise<void> {
try {
await this.init();
await this._run(cwd, rawArgv);
consoleLogger.info('✨ Done');
} catch (err) {
this.error(err.message);
// this.logger.error(err.stack);
process.exit(1);
}
}

async init() {
async init(): Promise<void> {
await this.loadConfig();

const cache = await this.cache.get();
Expand All @@ -70,22 +77,21 @@ class Command extends BaseCommand {
for (const key of keys) {
if (path.isAbsolute(key)) continue;
const value = cache[key];
await this.cache.remove([ key ]);
await this.cache.remove([key]);
await this.cache.set(path.join(baseDir, key), value);
}

await this.cache.upgrade();
}
}

async loadConfig() {
async loadConfig(): Promise<void> {
await mkdirp(configDir);
const configExists = await fs.exists(configPath);
let config;
let config: Config;
if (configExists) {
config = await readJSON(configPath);
config = await readJSON(configPath) as Config;
config = resolveConfig(config, defaults);
// ignore when base has been defined in ~/.projj/config
if (config.base) {
this.config = config;
return;
Expand All @@ -98,12 +104,12 @@ class Command extends BaseCommand {
message: 'Set base directory:',
default: defaults.base,
};
const { base } = await this.prompt([ question ]);
const { base } = await this.prompt([question]);
this.config = resolveConfig({ base }, defaults);
await fs.writeFile(configPath, JSON.stringify(this.config, null, 2));
}

async runHook(name, cacheKey) {
async runHook(name: string, cacheKey: string): Promise<void> {
if (!this.config.hooks[name]) return;
const hook = this.config.hooks[name];
const env = {
Expand All @@ -124,16 +130,13 @@ class Command extends BaseCommand {
await this.runScript(hook, opt);
}

async prompt(questions) {
if (!this[PROMPT]) {
// create a self contained inquirer module.
this[PROMPT] = inquirer.createPromptModule();
const promptMapping = this[PROMPT].prompts;
async prompt(questions: inquirer.QuestionCollection<any>): Promise<inquirer.Answers> {
if (!this.PROMPT) {
this.PROMPT = inquirer.createPromptModule();
const promptMapping = this.PROMPT.prompts;
for (const key of Object.keys(promptMapping)) {
const Clz = promptMapping[key];
// extend origin prompt instance to emit event
promptMapping[key] = class CustomPrompt extends Clz {
/* istanbul ignore next */
static get name() { return Clz.name; }
run() {
process.send && process.send({ type: 'prompt', name: this.opt.name });
Expand All @@ -143,10 +146,10 @@ class Command extends BaseCommand {
};
}
}
return this[PROMPT](questions);
return this.PROMPT(questions);
}

async runScript(cmd, opt) {
async runScript(cmd: string, opt: any): Promise<void> {
const stdout = through();
stdout.pipe(this.childLogger.stdout, { end: false });
opt = Object.assign({}, {
Expand All @@ -164,19 +167,16 @@ class Command extends BaseCommand {
}
}

error(msg) {
error(msg: string): void {
consoleLogger.error(chalk.red('✘ ' + msg));
}

// https://github.com/popomore/projj.git
// => $BASE/github.com/popomore/projj
url2dir(url) {
url2dir(url: string): string {
url = giturl.parse(url);
return url.replace(/https?:\/\//, '');
}

async addRepo(repo, cacheKey) {
// preadd hook
async addRepo(repo: string, cacheKey: string): Promise<void> {
await this.runHook('preadd', cacheKey);

const targetPath = cacheKey;
Expand All @@ -188,15 +188,13 @@ class Command extends BaseCommand {
await this.runScript(`git clone ${repo} ${targetPath} > /dev/null`, {
env,
});
// add this repository to cache.json
await this.cache.set(cacheKey, { repo });
await this.cache.dump();

// preadd hook
await this.runHook('postadd', cacheKey);
}

async chooseBaseDirectory() {
async chooseBaseDirectory(): Promise<string> {
const baseDirectories = this.config.base;
if (baseDirectories.length === 1) return baseDirectories[0];

Expand All @@ -206,35 +204,33 @@ class Command extends BaseCommand {
message: 'Choose base directory',
choices: baseDirectories,
};
const { base } = await this.prompt([ question ]);
const { base } = await this.prompt([question]);
return base;
}
}

module.exports = Command;

function resolveConfig(config, defaults) {
function resolveConfig(config: Partial<Config>, defaults: Config): Config {
config = Object.assign({}, defaults, config);
if (!Array.isArray(config.base)) {
config.base = [ config.base ];
config.base = [config.base];
}
config.base = config.base.map(baseDir => {
switch (baseDir[0]) {
case '.':
return path.join(path.dirname(configPath), baseDir);
case '~':
return baseDir.replace('~', process.env.HOME);
return baseDir.replace('~', process.env.HOME as string);
case '/':
return baseDir;
default:
return path.join(process.cwd(), baseDir);
}
});

return config;
return config as Config;
}

function colorStream(stream) {
function colorStream(stream: NodeJS.WritableStream): NodeJS.WritableStream {
Comment on lines +212 to +233
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolveConfig function and colorStream utility function are implemented correctly. Consider adding type annotations for function parameters and return types for better type safety.

- function resolveConfig(config: Partial<Config>, defaults: Config): Config {
+ function resolveConfig(config: Partial<Config>, defaults: Config): Config {
+   // Ensure all properties are properly typed

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
function resolveConfig(config: Partial<Config>, defaults: Config): Config {
config = Object.assign({}, defaults, config);
if (!Array.isArray(config.base)) {
config.base = [ config.base ];
config.base = [config.base];
}
config.base = config.base.map(baseDir => {
switch (baseDir[0]) {
case '.':
return path.join(path.dirname(configPath), baseDir);
case '~':
return baseDir.replace('~', process.env.HOME);
return baseDir.replace('~', process.env.HOME as string);
case '/':
return baseDir;
default:
return path.join(process.cwd(), baseDir);
}
});
return config;
return config as Config;
}
function colorStream(stream) {
function colorStream(stream: NodeJS.WritableStream): NodeJS.WritableStream {
function resolveConfig(config: Partial<Config>, defaults: Config): Config {
// Ensure all properties are properly typed
config = Object.assign({}, defaults, config);
if (!Array.isArray(config.base)) {
config.base = [config.base];
}
config.base = config.base.map(baseDir => {
switch (baseDir[0]) {
case '.':
return path.join(path.dirname(configPath), baseDir);
case '~':
return baseDir.replace('~', process.env.HOME as string);
case '/':
return baseDir;
default:
return path.join(process.cwd(), baseDir);
}
});
return config as Config;
}
function colorStream(stream: NodeJS.WritableStream): NodeJS.WritableStream {

const s = through(function(buf, _, done) {
done(null, chalk.gray(buf.toString()));
});
Expand Down
41 changes: 25 additions & 16 deletions lib/cache.js → lib/cache.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
'use strict';
import * as assert from 'assert';
import * as fs from 'mz/fs';
import { readJSON } from 'utility';

const assert = require('assert');
const fs = require('mz/fs');
const readJSON = require('utility').readJSON;
interface CacheOptions {
cachePath: string;
}

module.exports = class Cache {
constructor(options) {
interface CacheContent {
[key: string]: any;
version?: string;
}

export default class Cache {
private cachePath: string;
private cache?: CacheContent;

constructor(options: CacheOptions) {
assert(options && options.cachePath, 'cachePath is required');
this.cachePath = options.cachePath;
}

async get(key) {
async get(key?: string): Promise<any> {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add type annotations for the return value of the get method to enhance type safety.

async get(key?: string): Promise<any | undefined> {

if (!this.cache) {
if (await fs.exists(this.cachePath)) {
this.cache = await readJSON(this.cachePath);
this.cache = await readJSON(this.cachePath) as CacheContent;
await this.setRepo(this.cache);
} else {
this.cache = {};
Expand All @@ -23,30 +33,30 @@ module.exports = class Cache {
return key ? this.cache[key] : this.cache;
}

async getKeys() {
async getKeys(): Promise<string[]> {
const cache = await this.get();
return Object.keys(cache).filter(key => key !== 'version');
}

async set(key, value) {
async set(key: string, value?: any): Promise<void> {
if (!key) return;
if (!this.cache) await this.get();

this.cache[key] = value || {};
}

async remove(keys) {
async remove(keys: string | string[]): Promise<void> {
if (!keys) return;
if (!Array.isArray(keys)) keys = [ keys ];
keys.forEach(key => delete this.cache[key]);
}

async dump() {
async dump(): Promise<void> {
if (!this.cache) return;
await fs.writeFile(this.cachePath, JSON.stringify(this.cache, null, 2));
}

async setRepo(cache) {
private async setRepo(cache: CacheContent): Promise<void> {
const keys = await this.getKeys();
for (const key of keys) {
if (cache[key] && cache[key].repo) continue;
Expand All @@ -57,7 +67,7 @@ module.exports = class Cache {
await this.dump();
}

async upgrade() {
async upgrade(): Promise<void> {
const cache = await this.get();
switch (cache.version) {
// v1 don't upgrade
Expand All @@ -71,5 +81,4 @@ module.exports = class Cache {

await this.dump();
}

};
}
26 changes: 15 additions & 11 deletions lib/command/add.js → lib/command/add.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
'use strict';
import path from 'path';
import fs from 'mz/fs';
import chalk from 'chalk';
import clipboardy from 'clipboardy';
import utils from '../utils';
import BaseCommand from '../base_command';

const path = require('path');
const fs = require('mz/fs');
const chalk = require('chalk');
const clipboardy = require('clipboardy');
const utils = require('../utils');
const BaseCommand = require('../base_command');
interface RepoConfig {
alias: { [key: string]: string };
change_directory?: boolean;
}

class AddCommand extends BaseCommand {
config: RepoConfig;

async _run(_, [ repo ]) {
async _run(_, [repo]: string[]): Promise<void> {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider handling the case where repo is undefined or null before normalizing it.

if (!repo) throw new Error('Repository URL must be provided');

repo = this.normalizeRepo(repo);
const key = this.url2dir(repo);
const base = await this.chooseBaseDirectory();
Expand Down Expand Up @@ -44,7 +48,7 @@ class AddCommand extends BaseCommand {
}
}

normalizeRepo(repo) {
normalizeRepo(repo: string): string {
const alias = this.config.alias;
const keys = Object.keys(alias);
for (const key of keys) {
Expand All @@ -57,10 +61,10 @@ class AddCommand extends BaseCommand {
return repo;
}

get description() {
get description(): string {
return 'Add repository';
}

}

module.exports = AddCommand;
export default AddCommand;
Loading