Skip to content
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

Add: progress message & additional log #10

Merged
merged 4 commits into from
Jul 5, 2022
Merged
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"docs": "typedoc src/index.ts"
},
"dependencies": {
"ora": "^6.1.2",
"ts-morph": "^15.1.0",
"yargs": "^17.5.1"
},
Expand Down
69 changes: 55 additions & 14 deletions src/EjectEnum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
SyntaxKind,
VariableDeclarationKind,
} from "ts-morph";
import { initProgressLogger, ProgressLogger } from "./ProgressLogger";

/**
* Target of the conversion. You can specify one of:
Expand Down Expand Up @@ -80,6 +81,18 @@ function addSourceFilesInTarget(project: Project, target: EjectEnumTarget) {
}
}

/**
* Additional options for the conversion.
*/
export type EjectEnumOptions = {
/**
* If `true`, all outputs are suppressed.
*
* @defaultValue `false`
*/
silent?: boolean;
};

/**
* Ejects enums from all files specified by `target`.
*
Expand All @@ -102,33 +115,56 @@ function addSourceFilesInTarget(project: Project, target: EjectEnumTarget) {
* ```
*
* @param target Target specification of the conversion.
* @param options Additional options for the conversion.
*/
export function ejectEnum(target: EjectEnumTarget) {
export function ejectEnum(
target: EjectEnumTarget,
{ silent = false }: EjectEnumOptions = {}
) {
const project = new Project();
addSourceFilesInTarget(project, target);

const progLogger = initProgressLogger(silent);
progLogger.start(project.getSourceFiles().length);

for (const srcFile of project.getSourceFiles()) {
ejectEnumFromSourceFile(srcFile);
ejectEnumFromSourceFile(srcFile, progLogger);
}

progLogger.finish();

project.saveSync();
}

// Ejects enums from single source file. It is exported for the purpose of testing.
export function ejectEnumFromSourceFile(srcFile: SourceFile) {
export function ejectEnumFromSourceFile(
srcFile: SourceFile,
progLogger?: ProgressLogger
) {
const ctx: EjectionContext = {
rootSrcFile: srcFile,
probe: new EjectionProbe(),
progLogger,
};

// convert top-level statements
ejectEnumFromStatementedNode(srcFile, ctx);
// convert nested statements
srcFile.forEachDescendant(statementedNodesVisitor(ctx));

// format file only if at least one ejection happened.
if (ctx.probe.ejected) {
const n = ctx.probe.numEjected;
ctx.progLogger?.log(
`${path.relative(
process.cwd(),
ctx.rootSrcFile.getFilePath()
)}: ejected ${n} enum${n >= 2 ? "s" : ""}.`
);

// format file only if at least one ejection happened.
srcFile.formatText();
}
ctx.progLogger?.notifyFinishFile();
}

function statementedNodesVisitor(ctx: EjectionContext): (node: Node) => void {
Expand All @@ -151,20 +187,20 @@ function ejectEnumFromStatementedNode(
node: StatementedNode,
ctx: EjectionContext
) {
node.getEnums().forEach((enumDecl) => {
for (const enumDecl of node.getEnums()) {
if (!isEjectableEnum(enumDecl)) {
console.error(
ctx.progLogger?.log(
`${path.relative(
process.cwd(),
ctx.rootSrcFile.getFilePath()
)} > ${enumDecl.getName()}: it has a member whose value can't be known at compile-time. skipped.`
);
return;
continue;
}

convertEnumDeclaration(node, enumDecl, enumDecl.getChildIndex());
ctx.probe.setEjected();
});
ctx.probe.notifyEjected();
}
}

function isEjectableEnum(enumDecl: EnumDeclaration): boolean {
Expand Down Expand Up @@ -284,23 +320,28 @@ function getLeadingCommentsAssociatedWithDecl(

// Object to detect an ejection of enum.
class EjectionProbe {
#ejected: boolean;
#numEjected: number;

constructor() {
this.#ejected = false;
this.#numEjected = 0;
}

get ejected(): boolean {
return this.#ejected;
return this.#numEjected > 0;
}

get numEjected(): number {
return this.#numEjected;
}

setEjected() {
this.#ejected = true;
notifyEjected() {
this.#numEjected++;
}
}

// Context of the conversion of single source file.
type EjectionContext = {
rootSrcFile: SourceFile;
probe: EjectionProbe;
progLogger: ProgressLogger | undefined;
};
68 changes: 68 additions & 0 deletions src/ProgressLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import ora, { Ora } from "ora";

export interface ProgressLogger {
start(numFiles: number): void;
finish(): void;
notifyFinishFile(): void;
log(l: string): void;
}

export const initProgressLogger = (silent: boolean): ProgressLogger =>
silent ? noopProgressLogger : new DefaultProgressLogger();

const progressText = (numFinished: number, numFiles: number): string =>
`Ejecting... ${numFinished}/${numFiles} (${(
(numFinished / numFiles) *
100
).toFixed(0)}%)`;

class DefaultProgressLogger implements ProgressLogger {
#spinner: Ora;

#numFiles = 0;
#numFinished = 0;

constructor() {
this.#spinner = ora();
}

public start(numFiles: number) {
this.#numFiles = numFiles;
this.#spinner.text = progressText(0, this.#numFiles);

this.#spinner.render();
}

public finish() {
this.#spinner.succeed("Ejection finished");
}

public notifyFinishFile() {
this.#numFinished++;
this.#spinner.text = progressText(this.#numFinished, this.#numFiles);
this.#spinner.render();
}

public log(l: string) {
this.#spinner.clear();

console.log(l);

this.#spinner.render();
}
}

const noopProgressLogger: ProgressLogger = {
start() {
/* no-op */
},
finish() {
/* no-op */
},
notifyFinishFile() {
/* no-op */
},
log() {
/* no-op */
},
};
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { ejectEnum, EjectEnumTarget } from "./EjectEnum";
export type { EjectEnumOptions } from "./EjectEnum";
85 changes: 48 additions & 37 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,65 @@
import type { Argv as YargsArgv } from "yargs";
import { hideBin } from "yargs/helpers";
import yargs from "yargs/yargs";
import type { EjectEnumOptions } from "./EjectEnum";
import { ejectEnum, EjectEnumTarget } from "./EjectEnum";

const argvParser = yargs(hideBin(process.argv))
.option("project", {
alias: "p",
type: "string",
array: true,
description: "Paths to TS config files",
default: [] as string[],
})
.option("include", {
alias: "i",
type: "string",
array: true,
description: "Paths to include in the conversion target",
default: [] as string[],
})
.option("exclude", {
alias: "e",
type: "string",
array: true,
description:
"Paths to exclude from the conversion target.\nYou CAN'T exclude paths included by TS configs of --project by this option!",
default: [] as string[],
})
.option("silent", {
type: "boolean",
description: "Suppress outputs",
default: false,
})
.usage(
"usage: $0 [--project path/to/tsconfig.json] [--include path/to/include [--exclude path/to/exclude]]"
)
.help();

// entrypoint of the CLI.
export function main() {
const argvParser = yargs(hideBin(process.argv))
.option("project", {
alias: "p",
type: "string",
array: true,
description: "Paths to TS config files",
default: [] as string[],
})
.option("include", {
alias: "i",
type: "string",
array: true,
description: "Paths to include in the conversion target",
default: [] as string[],
})
.option("exclude", {
alias: "e",
type: "string",
array: true,
description:
"Paths to exclude from the conversion target.\nYou CAN'T exclude paths included by TS configs of --project by this option!",
default: [] as string[],
})
.usage(
"usage: $0 [--project path/to/tsconfig.json] [--include path/to/include [--exclude path/to/exclude]]"
)
.help();

const argv = argvParser.parseSync();

let target: EjectEnumTarget;
try {
target = optionsFromArgv(argv);
target = targetFromArgv(argv);
} catch (e) {
console.error(`${e}`);
argvParser.showHelp();
process.exit(1);
}

ejectEnum(target);
ejectEnum(target, optionsFromArgv(argv));
}

type ParsedArgv = {
project: string[];
include: string[];
exclude: string[];
};
type ParsedArgv = typeof argvParser extends YargsArgv<infer T> ? T : never;
type KeysAboutTarget = "project" | "include" | "exclude";

// gets EjectEnumOptions from parsed command arguments.
// throws if pre-conditions about the arguments are not satisfied.
export function optionsFromArgv(argv: ParsedArgv): EjectEnumTarget {
export function targetFromArgv(
argv: Pick<ParsedArgv, KeysAboutTarget>
): EjectEnumTarget {
if (argv.project.length === 0 && argv.include.length === 0) {
throw Error("specify at least one of --project or --include");
}
Expand All @@ -66,3 +71,9 @@ export function optionsFromArgv(argv: ParsedArgv): EjectEnumTarget {
exclude: argv.exclude,
});
}

export function optionsFromArgv(
argv: Omit<ParsedArgv, KeysAboutTarget>
): EjectEnumOptions {
return { silent: argv.silent };
}
12 changes: 6 additions & 6 deletions test/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { describe, expect, test } from "vitest";
import { EjectEnumTarget } from "../src/EjectEnum";
import { optionsFromArgv } from "../src/main";
import { targetFromArgv } from "../src/main";

describe("optionsFromArgv", () => {
describe("targetFromArgv", () => {
test("--project -> EjectTarget.tsConfig", () => {
const argv = {
project: ["tsconfig.json", "tsconfig2.json"],
include: [],
exclude: [],
};
expect(optionsFromArgv(argv)).toEqual(
expect(targetFromArgv(argv)).toEqual(
EjectEnumTarget.tsConfig(argv.project)
);
});
Expand All @@ -20,7 +20,7 @@ describe("optionsFromArgv", () => {
include: ["src/**/*", "test/**/*"],
exclude: ["src/hoge/*.ts", "src/fuga.ts"],
};
expect(optionsFromArgv(argv)).toEqual(
expect(targetFromArgv(argv)).toEqual(
EjectEnumTarget.paths({
include: argv.include,
exclude: argv.exclude,
Expand All @@ -34,7 +34,7 @@ describe("optionsFromArgv", () => {
include: ["src/**/*", "test/**/*"],
exclude: ["src/hoge/*.ts", "src/fuga.ts"],
};
expect(optionsFromArgv(argv)).toEqual(
expect(targetFromArgv(argv)).toEqual(
EjectEnumTarget.tsConfig(argv.project)
);
});
Expand All @@ -46,7 +46,7 @@ describe("optionsFromArgv", () => {
exclude: ["hoge"],
};
expect(() => {
optionsFromArgv(argv);
targetFromArgv(argv);
}).toThrow();
});
});
Loading